diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index cf6ff9a..6c49f4e 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -18,84 +18,87 @@ on: # (from https://help.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule) - cron: "0 0 * * *" -jobs: - linux: - runs-on: ubuntu-latest - steps: - - run: sudo apt-get install gfortran - - run: gfortran --version - - windows: - runs-on: windows-latest - steps: - - run: choco install mingw - +jobs: test: name: Test on ${{ matrix.os }}, Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10'] steps: + - uses: actions/checkout@v4 -# Not relevant now, but may be needed when f2py issues are solved, for now only numba -# - name: Set up msys for Windows -# if: matrix.os == 'windows-latest' -# uses: msys2/setup-msys2@v2 -# with: -# update: true -# install: >- -# mingw-w64-x86_64-gcc-fortran -# mingw-w64-x86_64-gcc - - - uses: actions/checkout@v2 - - - name: Additional info about the build - shell: bash - run: | - uname -a - df -h - ulimit -a + - name: Additional info about the build + shell: bash + run: | + uname -a + df -h + ulimit -a - # More info on options: https://github.com/conda-incubator/setup-miniconda - - uses: conda-incubator/setup-miniconda@v2 - with: - python-version: ${{ matrix.python-version }} - environment-file: devtools/conda-envs/test_env.yaml - activate-environment: test - auto-update-conda: false - auto-activate-base: false - show-channel-urls: true + # More info on options: https://github.com/conda-incubator/setup-miniconda + - uses: conda-incubator/setup-miniconda@v3 + with: + python-version: ${{ matrix.python-version }} + environment-file: devtools/conda-envs/test_env.yaml + activate-environment: test + auto-update-conda: false + auto-activate-base: false + show-channel-urls: true + condarc: | + channels: + - conda-forge + create-args: >- + python=${{ matrix.python-version }} - - name: Testing Dependencies - shell: bash -l {0} - run: | - python -m pip install -U pytest pytest-cov codecov - - - name: Install gfortran on mac - if: matrix.os == 'macOS-latest' - shell: bash -l {0} - run: | - conda install -c conda-forge gfortran_osx-64 + - name: Testing Dependencies + shell: bash -l {0} + run: | + python -m pip install -U pytest pytest-cov codecov - - name: Install package - # conda setup requires this special shell - shell: bash -l {0} - run: | - python -m pip install -e . --no-deps -vvv - conda list + - name: Install package + # conda setup requires this special shell + shell: bash -l {0} + run: | + python -m pip install -e . --no-deps -vvv + conda list - - name: Run tests - shell: bash -l {0} - run: | - ls -a despasito/equations_of_state/saft/compiled_modules - pytest -vvv --cov=despasito --cov-report=xml --color=yes despasito/tests/ + - name: Run tests + shell: bash -l {0} + run: | + ls -a despasito/equations_of_state/saft/compiled_modules + pytest -vvv --cov=despasito --cov-report=xml --color=yes despasito/tests/ + + - name: CodeCov + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + flags: unittests + name: codecov-${{ matrix.os }}-py${{ matrix.python-version }} + + - name: Flake8 + shell: bash -l {0} + run: | + python -m flake8 despasito --count --ignore=E741,W503 --max-line-length=92 --show-source --statistics - - name: CodeCov - uses: codecov/codecov-action@v1 - with: - file: ./coverage.xml - flags: unittests - name: codecov-${{ matrix.os }}-py${{ matrix.python-version }} + paper: + runs-on: ubuntu-latest + name: Paper Draft + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build draft PDF + uses: openjournals/openjournals-draft-action@master + with: + journal: joss + # This should be the path to the paper within your repo. + paper-path: paper/paper.md + - name: Upload + uses: actions/upload-artifact@v3 + with: + name: paper + # This is the output path where Pandoc will write the compiled + # PDF. Note, this should be the same directory as the input + # paper.md + path: paper/paper.pdf \ No newline at end of file diff --git a/.gitignore b/.gitignore index eb98524..67dc286 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ *.py[cod] *$py.class +.DS_Store # C extensions *.so diff --git a/.readthedocs.yml b/.readthedocs.yml index b9482ed..6f60b85 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,22 +4,19 @@ # Required version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py -# Build documentation with MkDocs -#mkdocs: -# configuration: mkdocs.yml - # Optionally build your docs in additional formats such as PDF and ePub formats: all # Optionally set the version of Python and requirements required to build your docs python: - version: 3.7 install: - requirements: docs/requirements - -#conda: -# environment: devtools/conda-envs/readthedocs_env.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7688f82 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.testing.pytestArgs": [], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/README.md b/README.md index b77c677..7cb55f4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ DESPASITO [![GitHub Actions Build Status](https://github.com/jaclark5/despasito/workflows/CI/badge.svg)](https://github.com/jaclark5/despasito/actions?query=workflow%3ACI) [![codecov](https://codecov.io/gh/jaclark5/DESPASITO/branch/master/graph/badge.svg)](https://codecov.io/gh/jaclark5/DESPASITO/branch/master) [![Documentation Status](https://readthedocs.org/projects/despasito/badge/?version=latest)](https://despasito.readthedocs.io/en/latest/?badge=latest) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/jaclark5/despasito.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/jaclark5/despasito/context:python) DESPASITO: Determining Equilibrium State and Parametrization Application for SAFT, Intended for Thermodynamic Output @@ -44,7 +43,7 @@ Options * Step 1: Install the prerequisites listed above. * Step 2: Download the master branch from our github page as a zip file, or clone it with git via ``git clone https://github.com/jaclark5/despasito`` to your working directory. - * Step 3: Install with ``pip install despasito/.``, or change directories and run ``pip install .``. + * Step 3: Install with ``pip install despasito/.``, or change directories and run ``pip install -e .``. **NOTE** If [pip](https://pip.pypa.io/en/stable/) is unavailable, follow the instructions outlined [here](https://pip.pypa.io/en/stable/installing/) for installation. diff --git a/despasito/__init__.py b/despasito/__init__.py index 52a8798..b4d5e6b 100644 --- a/despasito/__init__.py +++ b/despasito/__init__.py @@ -1,10 +1,12 @@ """ DESPASITO -DESPASITO: Determining Equilibrium State and Parametrization Application for SAFT, Intended for Thermodynamic Output +DESPASITO: Determining Equilibrium State and Parametrization Application for SAFT, +Intended for Thermodynamic Output """ -# Add imports here -from .main import run +import os +import logging +import logging.handlers # Handle versioneer from ._version import get_versions @@ -14,10 +16,6 @@ __git_revision__ = versions["full-revisionid"] del get_versions, versions -import logging -import logging.handlers -import os - logger = logging.getLogger() logger.setLevel(30) @@ -26,16 +24,26 @@ def initiate_logger(console=None, log_file=None, verbose=30): """ Initiate a logging handler if more detail on the calculations is desired. - This function is useful when DESPASITO is used as an imported package. If a handler of the given type is already present, nothing is done, as is the case when the input file schema is used. If either handler is given a value of False, the handler of that type is removed. + This function is useful when DESPASITO is used as an imported package. If a handler + of the given type is already present, nothing is done, as is the case when the + input file schema is used. If either handler is given a value of False, the handler + of that type is removed. Parameters ---------- console : bool, Optional, default=None - Initiates a stream handler to print to a console. If True, this handler is initiated. If it is False, then any StreamHandler is removed. + Initiates a stream handler to print to a console. If True, this handler is + initiated. If it is False, then any StreamHandler is removed. log_file : bool/str, Optional, default=None - If log output should be recorded in a file, set this keyword to either True or to a name for the log file. If True, the file name 'despasito.log' is used. Note that if a file with the same name already exists, it will be deleted. + If log output should be recorded in a file, set this keyword to either True or + to a name for the log file. If True, the file name 'despasito.log' is used. + Note that if a file with the same name already exists, it will be deleted. verbose : int, Optional, default=30 - The verbosity of logging information can be set to any supported representation of the `logging level `_. + The verbosity of logging information can be set to any supported representation + of the + `logging level `_. + + .. _LOGGER: https://docs.python.org/3/library/logging.html#logging-levels """ logger.setLevel(verbose) @@ -50,7 +58,7 @@ def initiate_logger(console=None, log_file=None, verbose=30): handler_console = tmp # Set up logging to console - if console and handler_console == None: + if console and handler_console is None: console_handler = logging.StreamHandler() # sys.stderr console_handler.setFormatter( logging.Formatter("[%(levelname)s](%(name)s): %(message)s") @@ -59,14 +67,14 @@ def initiate_logger(console=None, log_file=None, verbose=30): logger.addHandler(console_handler) elif console: logger.warning("StreamHandler already exists") - elif handler_console == False: + elif handler_console is not None: handler_console.close() logger.removeHandler(handler_console) # Rotating File Handler - if log_file is not None and handler_logfile == None: + if log_file is not None and handler_logfile is None: - if type(log_file) != str: + if not isinstance(log_file, str): log_file = "despasito.log" if os.path.isfile(log_file): @@ -75,13 +83,14 @@ def initiate_logger(console=None, log_file=None, verbose=30): log_file_handler = logging.handlers.RotatingFileHandler(log_file) log_file_handler.setFormatter( logging.Formatter( - "%(asctime)s [%(levelname)s](%(name)s:%(funcName)s:%(lineno)d): %(message)s" + "%(asctime)s [%(levelname)s](%(name)s:%(funcName)s:%(lineno)d): " + + "%(message)s" ) ) log_file_handler.setLevel(verbose) logger.addHandler(log_file_handler) elif log_file: logger.warning("RotatingFileHandler already exists") - elif handler_logfile == False: + elif not handler_logfile: handler_logfile.close() logger.removeHandler(handler_logfile) diff --git a/despasito/__main__.py b/despasito/__main__.py index 4eed1f4..2fff9a5 100644 --- a/despasito/__main__.py +++ b/despasito/__main__.py @@ -10,7 +10,7 @@ parser = get_parser() args = parser.parse_args() -## Extract arguments +# Extract arguments if args.verbose == 0: args.verbose = 20 elif args.verbose < 4: @@ -56,7 +56,7 @@ kwargs["numba"] = args.numba logging.info("Use Cython: {}".format(args.cython)) kwargs["cython"] = args.cython -logging.info("Pure Python (no Fortran): {}".format(args.python)) +logging.info("Pure Python: {}".format(args.python)) kwargs["python"] = args.python kwargs["MultiprocessingObject"] = MultiprocessingJob(ncores=args.ncores) diff --git a/despasito/_version.py b/despasito/_version.py index 61c0219..734274c 100644 --- a/despasito/_version.py +++ b/despasito/_version.py @@ -120,7 +120,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return { - "version": dirname[len(parentdir_prefix) :], + "version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, @@ -190,7 +190,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -207,7 +207,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix) :] + r = ref[len(tag_prefix):] if verbose: print("picking %s" % r) return { @@ -307,7 +307,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): tag_prefix, ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix) :] + pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) diff --git a/despasito/equations_of_state/__init__.py b/despasito/equations_of_state/__init__.py index f24b524..e4a7f9c 100644 --- a/despasito/equations_of_state/__init__.py +++ b/despasito/equations_of_state/__init__.py @@ -8,49 +8,56 @@ from importlib import import_module import logging + class method_stat: - def __init__(self, numba=False, cython=False, python=False): + def __init__(self, numba=True, cython=False, python=False): self.numba = numba self.cython = cython self.python = python if not any([numba, cython, python]): - self.fortran = True - else: - self.fortran = False + raise ValueError("Calculation type has not been specified.") def __str__(self): - string = "Compilation: numba {}, cython {}, python {}, fortran {}".format(self.numba, self.cython, self.python, self.fortran) + string = "Compilation: numba {}, cython {}, python {}".format( + self.numba, self.cython, self.python + ) return string + logger = logging.getLogger(__name__) def initiate_eos( - eos="saft.gamma_mie", numba=False, cython=False, python=False, **kwargs + eos="saft.gamma_mie", numba=True, cython=False, python=False, **kwargs ): """ Interface between the user and our library of equations of state (EOS). - Input the name of a desired EOS and available classes are automatically searched to allow easy implementation of new EOS. + Input the name of a desired EOS and available classes are automatically searched + to allow easy implementation of new EOS. Parameters ---------- eos : str, Optional, default="saft.gamma_mie" - Name of EOS, see :ref:`EOS-types` in the documentation for additional options. Input should be in the form EOSfamily.EOSname (e.g. saft.gamme_mie). + Name of EOS, see :ref:`EOS-types` in the documentation for additional options. + Input should be in the form EOSfamily.EOSname (e.g. saft.gamme_mie). numba : bool, Optional, default=False If True and available for chosen EOS, numba Just-In-Time compilation is used. cython : bool, Optional, default=False If True and available for chosen EOS, cython pre-compiled modules are used. python : bool, Optional, default=False - If True and available for chosen EOS, pure python is used for everything, note that if association sites are present in the SAFT EOS, this is detrimentally slow + If True and available for chosen EOS, pure python is used for everything, note + that if association sites are present in the SAFT EOS, this is detrimentally + slow kwargs - Other keyword argument inputs for the desired EOS. See specific EOS documentation for required inputs. - + Other keyword argument inputs for the desired EOS. See specific EOS + documentation for required inputs. + Returns ------- instance : obj @@ -61,7 +68,8 @@ def initiate_eos( factory_families = [ "saft" - ] # Eos families in this list have a general object with a factory to import relevant modules + ] # Eos families in this list have a general object with a factory to import + # relevant modules logger.info("Using EOS: {}".format(eos)) @@ -87,7 +95,8 @@ def initiate_eos( eos_class = getattr(eos_module, class_name) except AttributeError: raise ImportError( - "Based on your input, '{}', we expect the class, {}, in a module, {}, found in the package, {}, which indicates the EOS family.".format( + "Based on your input, '{}', we expect the class, {}, in a module, {}," + " found in the package, {}, which indicates the EOS family.".format( eos, class_name, eos_type, eos_fam ) ) diff --git a/despasito/equations_of_state/combining_rule_types.py b/despasito/equations_of_state/combining_rule_types.py index 42a904e..e7955b0 100644 --- a/despasito/equations_of_state/combining_rule_types.py +++ b/despasito/equations_of_state/combining_rule_types.py @@ -1,4 +1,5 @@ -""" Combining rules options called from :func:`~despasito.equations_of_state.eos_toolbox.combining_rules` in the EOS class. +""" Combining rules options called from +:func:`~despasito.equations_of_state.eos_toolbox.combining_rules` in the EOS class. """ import numpy as np @@ -12,7 +13,7 @@ def mean(beadA, beadB, parameter): r""" Calculates cross interaction parameter with a mean: c = (a+b)/2 - + Parameters ---------- beadA : dict @@ -21,7 +22,7 @@ def mean(beadA, beadB, parameter): Dictionary of parameters used to describe a bead parameter : str Name of parameter for which a mixed value is needed - + Returns ------- parameter12 : float @@ -35,7 +36,7 @@ def mean(beadA, beadB, parameter): def geometric_mean(beadA, beadB, parameter): r""" Calculates cross interaction parameter with a geometric mean: c = np.sqrt(a*b) - + Parameters ---------- beadA : dict @@ -44,7 +45,7 @@ def geometric_mean(beadA, beadB, parameter): Dictionary of parameters used to describe a bead parameter : str Name of parameter for which a mixed value is needed - + Returns ------- parameter12 : float @@ -57,8 +58,9 @@ def geometric_mean(beadA, beadB, parameter): def volumetric_geometric_mean(beadA, beadB, parameter, weighting_parameters=[]): r""" - Calculates cross interaction parameter with a volumetric geometric mean: c = np.sqrt(a[0]*b[0]) * np.sqrt(a[1]**3 * b[1]**3) / ((a[1] + b[1])/2)**3 - + Calculates cross interaction parameter with a volumetric geometric mean: + c = np.sqrt(a[0]*b[0]) * np.sqrt(a[1]**3 * b[1]**3) / ((a[1] + b[1])/2)**3 + Parameters ---------- beadA : dict @@ -68,7 +70,8 @@ def volumetric_geometric_mean(beadA, beadB, parameter, weighting_parameters=[]): parameter : str Name of parameter for which a mixed value is needed weighting_parameters : list[str], Optional, default=[] - Given parameter name is a0 and b0, while weighting_parameters should be of length 1 to represent the name for a1 and b1. + Given parameter name is a0 and b0, while weighting_parameters should be of + length 1 to represent the name for a1 and b1. Returns ------- @@ -89,8 +92,9 @@ def volumetric_geometric_mean(beadA, beadB, parameter, weighting_parameters=[]): def weighted_mean(beadA, beadB, parameter, weighting_parameters=[]): r""" - Calculates cross interaction parameter with a weighted mean: (a[0]*a[1] + b[0]*b[1]) / (a[1] + b[1]) - + Calculates cross interaction parameter with a weighted mean: + (a[0]*a[1] + b[0]*b[1]) / (a[1] + b[1]) + Parameters ---------- beadA : dict @@ -100,7 +104,8 @@ def weighted_mean(beadA, beadB, parameter, weighting_parameters=[]): parameter : str Name of parameter for which a mixed value is needed weighting_parameters : list[str], Optional, default=[] - Given parameter name is a0 and b0, while weighting_parameters should be of length 1 to represent the name for a1 and b1. + Given parameter name is a0 and b0, while weighting_parameters should be of + length 1 to represent the name for a1 and b1. Returns ------- @@ -119,8 +124,9 @@ def weighted_mean(beadA, beadB, parameter, weighting_parameters=[]): def mie_exponent(beadA, beadB, parameter): r""" - Calculates cross interaction parameter like a mie_exponent: 3 + np.sqrt((a-3)*(b-3)) - + Calculates cross interaction parameter like a mie_exponent: + 3 + np.sqrt((a-3)*(b-3)) + Parameters ---------- beadA : dict @@ -142,8 +148,9 @@ def mie_exponent(beadA, beadB, parameter): def square_well_berthelot(beadA, beadB, parameter, weighting_parameters=[]): r""" - Calculates cross interaction parameter with a Square_well Berthelot geometric mean: c = np.sqrt(a[0]*b[0]) * np.sqrt(a[1]**3 * b[1]**3) / ((a[1] + b[1])/2)**3 - + Calculates cross interaction parameter with a Square_well Berthelot geometric mean: + c = np.sqrt(a[0]*b[0]) * np.sqrt(a[1]**3 * b[1]**3) / ((a[1] + b[1])/2)**3 + Parameters ---------- beadA : dict @@ -153,7 +160,8 @@ def square_well_berthelot(beadA, beadB, parameter, weighting_parameters=[]): parameter : str Name of parameter for which a mixed value is needed weighting_parameters : list[str], Optional, default=[] - Given parameter name is a0 and b0, while weighting_parameters should be of length 1 to represent the name for a1 and b1. + Given parameter name is a0 and b0, while weighting_parameters should be of + length 1 to represent the name for a1 and b1. Returns ------- @@ -173,7 +181,7 @@ def square_well_berthelot(beadA, beadB, parameter, weighting_parameters=[]): param3_12 = weighted_mean(beadA, beadB, param3, weighting_parameters=[param2]) tmp3 = np.sqrt((beadA[param3] ** 3 - 1) * (beadB[param3] ** 3 - 1)) / ( - param3_12 ** 3 - 1 + param3_12**3 - 1 ) return tmp1 * tmp2 * tmp3 @@ -183,8 +191,9 @@ def multipole( beadA, beadB, parameter, temperature=None, mode="curve fit", scaled=False ): r""" - Calculates cross interaction parameter with the multipole combining rules from the plug-in `MAPSCI `_. - + Calculates cross interaction parameter with the multipole combining rules from + the plug-in `MAPSCI `_. + Parameters ---------- beadA : dict @@ -194,11 +203,15 @@ def multipole( parameter : str Name of parameter for which a mixed value is needed weighting_parameters : list[str], Optional, default=[] - Given parameter name is a0 and b0, while weighting_parameters should be of length 1 to represent the name for a1 and b1. + Given parameter name is a0 and b0, while weighting_parameters should be of + length 1 to represent the name for a1 and b1. mode : str, Optional, default='curve fit' - Dictates the mode by which the parameters are fit. By default the Mie parameters are fit to the multipole with the keyword "curve fit". Alternatively, the keyword "analytical" indicates that the energy parameter is explicitly calculated from the definite integral. + Dictates the mode by which the parameters are fit. By default the Mie + parameters are fit to the multipole with the keyword "curve fit". + Alternatively, the keyword "analytical" indicates that the energy parameter is + explicitly calculated from the definite integral. scaled : bool, Optional, default=False - Dictates whether the shape factor is used to scale the Mie potential + Dictates whether the shape factor is used to scale the Mie potential Returns ------- @@ -211,7 +224,8 @@ def multipole( import mapsci as mr except Exception: raise ImportError( - "Multipole combining rules require 'mapsci' package, which is currently unavailable. Install it from: https://github.com/jaclark5/mapsci" + "Multipole combining rules require 'mapsci' package, which is currently" + " unavailable. Install it from: https://github.com/jaclark5/mapsci" ) if scaled in [True, "True", "true", "yes", "Yes"]: @@ -219,7 +233,7 @@ def multipole( else: shape_factor_scale = False - if not isinstance(temperature, str) and temperature != None: + if not isinstance(temperature, str) and temperature is None: tmp = {"beadA": beadA.copy(), "beadB": beadB.copy()} for key, value in tmp.items(): tmp[key]["sigma"] = value["sigma"] * 10 # convert from nm to angstroms diff --git a/despasito/equations_of_state/constants.py b/despasito/equations_of_state/constants.py index 2593c3d..d67bc39 100755 --- a/despasito/equations_of_state/constants.py +++ b/despasito/equations_of_state/constants.py @@ -1,8 +1,7 @@ """ -Contains constants used in the other files. +Contains constants used in the other files. """ - # physical constants kb = 1.38064852e-23 # Boltzmann constant J/K diff --git a/despasito/equations_of_state/cubic/peng_robinson.py b/despasito/equations_of_state/cubic/peng_robinson.py index 5b066f2..70345ca 100644 --- a/despasito/equations_of_state/cubic/peng_robinson.py +++ b/despasito/equations_of_state/cubic/peng_robinson.py @@ -2,11 +2,11 @@ """ EOS object for Peng-Robinson - """ import numpy as np -#import logging + +# import logging from despasito.equations_of_state import constants from despasito.equations_of_state.interface import EosTemplate @@ -17,38 +17,43 @@ class EosType(EosTemplate): r""" - EOS object for the Peng-Robinson EOS. + EOS object for the Peng-Robinson EOS. All input and calculated parameters are defined as hidden attributes. - + Parameters ---------- beads : list[str] List of unique component names bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters: + A dictionary where bead names are the keys to access EOS self interaction + parameters: - Tc: :math:`T_{C}`, Critical temperature [K] - Pc: :math:`P_{C}`, Critical pressure [Pa] - - omega: :math:`\omega`, Acentric factor + - omega: :math:`\omega`, Acentric factor cross_library : dict, Optional, default={} - Optional library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. + Optional library of bead cross interaction parameters. As many or as few of the + desired parameters may be defined for whichever group combinations are desired. - kij: :math:`k_{ij}`, binary interaction parameter - + Attributes ---------- beads : list[str] List of component names bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters. See description under *Parameters* + A dictionary where bead names are the keys to access EOS self interaction + parameters. See description under *Parameters* cross_library : dict Library of bead cross interaction parameters. See description under *Parameters* parameter_types : list[str] - List of parameter names used Peng-Robinson EOS: ["ai", "bi", "kij", "Tc", "Pc", "omega"]. Used in parameter fitting. + List of parameter names used Peng-Robinson EOS: ["ai", "bi", "kij", "Tc", "Pc", + "omega"]. Used in parameter fitting. parameter_bound_extreme : dict - With each parameter names as an entry representing a list with the minimum and maximum feasible parameter value. For the Peng-Robinson EOS: + With each parameter names as an entry representing a list with the minimum and + maximum feasible parameter value. For the Peng-Robinson EOS: - ai: [0., 50.] - bi: [0., 1e-3] @@ -60,8 +65,10 @@ class EosType(EosTemplate): number_of_components : int Number of components in mixture represented by given EOS object. T : float, default=numpy.nan - Temperature value is initially defined as NaN for a placeholder until temperature dependent attributes are initialized by using a method of this class. - + Temperature value is initially defined as NaN for a placeholder until + temperature dependent attributes are initialized by using a method of this + class. + """ def __init__(self, **kwargs): @@ -129,7 +136,7 @@ def __init__(self, **kwargs): def _calc_temp_dependent_parameters(self, T): """ Compute ai and alpha given temperature - + Parameters ---------- T : float @@ -156,10 +163,9 @@ def _calc_temp_dependent_parameters(self, T): self.eos_dict["alpha"][i] = 1.0 def _calc_mixed_parameters(self, xi, T): - """ Compute mixing aij and bij given composition - + Parameters ---------- xi : list[float] @@ -201,7 +207,7 @@ def _calc_mixed_parameters(self, xi, T): def pressure(self, rho, T, xi): """ Compute pressure given system information - + Parameters ---------- rho : numpy.ndarray @@ -210,11 +216,12 @@ def pressure(self, rho, T, xi): Temperature of the system [K] xi : list[float] Mole fraction of each component - + Returns ------- P : numpy.ndarray - Array of pressure values [Pa] associated with each density and so equal in length + Array of pressure values [Pa] associated with each density and so equal + in length """ if T != self.T: @@ -230,7 +237,7 @@ def pressure(self, rho, T, xi): P = constants.R * self.T * rho / ( 1 - self.eos_dict["bij"] * rho - ) - rho ** 2 * self.eos_dict["aij"] / ( + ) - rho**2 * self.eos_dict["aij"] / ( (1 + self.eos_dict["bij"] * rho) + rho * self.eos_dict["bij"] * (1 - self.eos_dict["bij"] * rho) ) @@ -240,7 +247,7 @@ def pressure(self, rho, T, xi): def fugacity_coefficient(self, P, rho, xi, T): r""" Compute fugacity coefficient - + Parameters ---------- P : float @@ -251,7 +258,7 @@ def fugacity_coefficient(self, P, rho, xi, T): Temperature of the system [K] xi : list[float] Mole fraction of each component - + Returns ------- fugacity_coefficient : numpy.ndarray @@ -283,10 +290,10 @@ def fugacity_coefficient(self, P, rho, xi, T): tmp_RT = constants.R * T Z = P / (tmp_RT * rho) - Ai = self.eos_dict["ai"] * self.eos_dict["alpha"] * P / tmp_RT ** 2 + Ai = self.eos_dict["ai"] * self.eos_dict["alpha"] * P / tmp_RT**2 Bi = self.eos_dict["bi"] * P / tmp_RT B = self.eos_dict["bij"] * P / tmp_RT - A = self.eos_dict["aij"] * P / tmp_RT ** 2 + A = self.eos_dict["aij"] * P / tmp_RT**2 sqrt2 = np.sqrt(2.0) tmp1 = ( @@ -308,10 +315,9 @@ def fugacity_coefficient(self, P, rho, xi, T): return phi def density_max(self, xi, T, maxpack=0.9): - """ Estimate the maximum density based on the hard sphere packing fraction. - + Parameters ---------- xi : list[float] @@ -320,7 +326,7 @@ def density_max(self, xi, T, maxpack=0.9): Temperature of the system [K] maxpack : float, Optional, default=0.9 Maximum packing fraction - + Returns ------- max_density : float @@ -341,14 +347,18 @@ def update_parameter(self, param_name, bead_names, param_value): r""" Update a single parameter value during parameter fitting process. - To refresh those parameters that are dependent on to bead_library or cross_library, use method "parameter refresh". - + To refresh those parameters that are dependent on to bead_library or + cross_library, use method "parameter refresh". + Parameters ---------- param_name : str - Parameter to be fit. See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. kij_CO2). + Parameter to be fit. See EOS documentation for supported parameter names. + Cross interaction parameter names should be composed of parameter name + and the other bead type, separated by an underscore (e.g. kij_CO2). bead_names : list - Bead names to be changed. For a self interaction parameter, the length will be 1, for a cross interaction parameter, the length will be two. + Bead names to be changed. For a self interaction parameter, the length will + be 1, for a cross interaction parameter, the length will be two. param_value : float Value of parameter """ @@ -365,10 +375,12 @@ def update_parameter(self, param_name, bead_names, param_value): super().update_parameter(param_name, bead_names, param_value) def parameter_refresh(self): - r""" + r""" To refresh dependent parameters - - Those parameters that are dependent on bead_library and cross_library attributes **must** be updated by running this function after all parameters from update_parameters method have been changed. + + Those parameters that are dependent on bead_library and cross_library + attributes **must** be updated by running this function after all parameters + from update_parameters method have been changed. """ for i, bead in enumerate(self.beads): @@ -418,7 +430,7 @@ def parameter_refresh(self): self.eos_dict[key][j][i] = tmp self.eos_dict[key][i][j] = tmp - if self.T != None: + if self.T is not None: self._calc_temp_dependent_parameters(self.T) def __str__(self): diff --git a/despasito/equations_of_state/eos_toolbox.py b/despasito/equations_of_state/eos_toolbox.py index f2aa770..f7451d0 100644 --- a/despasito/equations_of_state/eos_toolbox.py +++ b/despasito/equations_of_state/eos_toolbox.py @@ -9,7 +9,8 @@ def remove_insignificant_components(xi_old, massi_old): r""" - This function will remove any components with mole fractions less than or equal to machine precision. + This function will remove any components with mole fractions less than or equal + to machine precision. Parameters ---------- @@ -43,8 +44,9 @@ def partial_density_central_difference( xi, rho, T, func, step_size=1e-2, log_method=False ): """ - Take the derivative of a dependent variable calculated with a given function using the central difference method. - + Take the derivative of a dependent variable calculated with a given function + using the central difference method. + Parameters ---------- xi : list[float] @@ -54,12 +56,14 @@ def partial_density_central_difference( T : float Temperature of the system [K] func : function - Function used in job to calculate dependent factor. This function should have a single output. Inputs arguments should be (rho, T, xi) + Function used in job to calculate dependent factor. This function should have + a single output. Inputs arguments should be (rho, T, xi) step_size : float, Optional, default=1E-2 Step size used in central difference method log_method : bool, Optional, default=False - Choose to use a log transform in central difference method. This allows easier calculations for very small numbers. - + Choose to use a log transform in central difference method. This allows easier + calculations for very small numbers. + Returns ------- dydxi : numpy.ndarray @@ -105,7 +109,7 @@ def partial_density_central_difference( def _partial_density_wrapper(rhoi, T, func): """ Compute derivative of Helmholtz energy with respect to density. - + Parameters ---------- rhoi : float @@ -113,8 +117,9 @@ def _partial_density_wrapper(rhoi, T, func): T : float Temperature of the system [K] func : function - Function used in job to calculate dependent factor. This function should have a single output. - + Function used in job to calculate dependent factor. This function should have + a single output. + Returns ------- Ares : float @@ -133,19 +138,21 @@ def _partial_density_wrapper(rhoi, T, func): def calc_massi(molecular_composition, bead_library, beads): r""" This function extracted the mass of each component - + Parameters ---------- molecular_composition : numpy.ndarray - :math:`\nu_{i,k}/k_B`. Array of number of components by number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`. Array of number of components by number of bead types. + Defines the number of each type of group in each component. bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters: - + A dictionary where bead names are the keys to access EOS self interaction + parameters: + - mass: Bead mass [kg/mol] - + beads : list[str] List of unique bead names used among components - + Returns ------- massi : numpy.ndarray @@ -166,21 +173,26 @@ def calc_massi(molecular_composition, bead_library, beads): def extract_property(prop, bead_library, beads, default=None): r""" - Extract single property or key from a dictionary within a dictionary (e.g. bead parameters) and into a single array of the same length and order as a list of bead names. + Extract single property or key from a dictionary within a dictionary (e.g. bead + parameters) and into a single array of the same length and order as a list of bead + names. + + The expected structure is a dictionary of dictionaries, such as a parameter + library. - The expected structure is a dictionary of dictionaries, such as a parameter library. - Parameters ---------- property : str Name of property in bead_library bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters: + A dictionary where bead names are the keys to access EOS self interaction + parameters: beads : list[str] List of unique bead names used among components default : any, Optional, default=None - If property is not present, set to this value. Although if the default is None, an error will result. - + If property is not present, set to this value. Although if the default is None, + an error will result. + Returns ------- prop_array : numpy.ndarray @@ -192,7 +204,7 @@ def extract_property(prop, bead_library, beads, default=None): if prop in bead_library[bead]: prop_array[i] = bead_library[bead][prop] else: - if default == None: + if default is None: raise ValueError( "The property {} for bead, {}, was not provided.".format(prop, bead) ) @@ -206,12 +218,14 @@ def check_bead_parameters(bead_library0, parameter_defaults): r""" Be sure all needed parameters are available for each bead. - If a parameter is absent and a default value is given, this value will be added to the parameter set. If the default is None, then an error is raised. + If a parameter is absent and a default value is given, this value will be added to + the parameter set. If the default is None, then an error is raised. Parameters ---------- bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters + A dictionary where bead names are the keys to access EOS self interaction + parameters parameter_defaults : dict A dictionary of default values for the required parameters. @@ -227,18 +241,16 @@ def check_bead_parameters(bead_library0, parameter_defaults): for bead, bead_dict in bead_library.items(): for parameter, default in parameter_defaults.items(): if parameter not in bead_dict: - if default != None: + if default is not None: bead_library[bead][parameter] = default logger.info( - "Parameter, {}, is missing for parametrized group, {}. Set to default, {}".format( - parameter, bead, default - ) + "Parameter, {}, is missing for parametrized group, {}. Set to " + "default, {}".format(parameter, bead, default) ) else: raise ValueError( - "Parameter, {}, should have been defined for parametrized group, {}.".format( - parameter, bead - ) + "Parameter, {}, should have been defined for parametrized " + "group, {}.".format(parameter, bead) ) return bead_library @@ -246,24 +258,31 @@ def check_bead_parameters(bead_library0, parameter_defaults): def cross_interaction_from_dict(beads, bead_library, combining_dict, cross_library={}): r""" - Computes matrices of cross interaction parameters defined as the keys in the combining dict parameter are extracted from the bead_library and then the cross library. - + Computes matrices of cross interaction parameters defined as the keys in the + combining dict parameter are extracted from the bead_library and then the cross + library. + Parameters ---------- beads : list[str] List of unique bead names used among components bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters. Those to be calculated are defined by the keys of combining_dict + A dictionary where bead names are the keys to access EOS self interaction + parameters. Those to be calculated are defined by the keys of combining_dict combining_dict : dict - This dictionary contains those bead parameters that should be placed in a matrix and the combining rules for the cross parameters + This dictionary contains those bead parameters that should be placed in a + matrix and the combining rules for the cross parameters cross_library : dict, Optional, default={} - Optional library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. If this matrix isn't provided, the SAFT combining rules are used. - + Optional library of bead cross interaction parameters. As many or as few of the + desired parameters may be defined for whichever group combinations are desired. + If this matrix isn't provided, the SAFT combining rules are used. + Returns ------- output : dict - Dictionary of outputs, with the same keys used in combining dict for the respective interaction matrix - + Dictionary of outputs, with the same keys used in combining dict for the + respective interaction matrix + """ nbeads = len(beads) @@ -276,8 +295,8 @@ def cross_interaction_from_dict(beads, bead_library, combining_dict, cross_libra output[key][k, k] = bead_library[beads[k]][key] # Put cross_library in output matrices - for (i, beadname) in enumerate(beads): - for (j, beadname2) in enumerate(beads): + for i, beadname in enumerate(beads): + for j, beadname2 in enumerate(beads): if j > i: for key in combining_dict: if ( @@ -302,11 +321,10 @@ def cross_interaction_from_dict(beads, bead_library, combining_dict, cross_libra key, **combining_dict[key] ) - # if combining_dict[key]["function"]=="multipole": - # logger.debug("Multipole: {} {}, {}".format(beadname,beadname2,tmp)) except Exception: raise ValueError( - "Unable to calculate '{}' with '{}' method, for beads: '{}' '{}'".format( + "Unable to calculate '{}' with '{}' method, for beads:" + " '{}' '{}'".format( key, combining_dict[key]["function"], beadname, @@ -323,28 +341,31 @@ def cross_interaction_from_dict(beads, bead_library, combining_dict, cross_libra def construct_dummy_bead_library(input_dict, keys=None): r""" - Using arrays of values, a dictionary is populated like a bead_library. + Using arrays of values, a dictionary is populated like a bead_library. If keys are included, they are used, otherwise, integers are used. - + Parameters ---------- input_dict : dict - A dictionary where parameter names are the keys to access EOS arrays of self-interaction parameters. + A dictionary where parameter names are the keys to access EOS arrays of + self-interaction parameters. keys : list[str], Optional, default=None - List must be the same length as the lists in `input_dict`. These are used as labels. - + List must be the same length as the lists in `input_dict`. These are used as + labels. + Returns ------- output_dict : dict - Dictionary of outputs, with the same keys used in combining dict for the respective interaction matrix - + Dictionary of outputs, with the same keys used in combining dict for the + respective interaction matrix + """ output = {} flag = False for parameter in input_dict: - if keys == None: + if keys is None: keys = [str(x) for x in range(len(input_dict[parameter]))] flag = True if len(keys) != len(input_dict[parameter]): @@ -366,7 +387,7 @@ def construct_dummy_bead_library(input_dict, keys=None): def combining_rules(beadA, beadB, parameter, function="mean", **kwargs): r""" Calculates cross interaction parameter according to the calculation method defined. - + Parameters ---------- beadA : dict @@ -376,14 +397,17 @@ def combining_rules(beadA, beadB, parameter, function="mean", **kwargs): parameter : str Name of parameter for which a mixed value is needed function : str, Optional, default=mean - Mixing rule function found in `despasito.equations_of_state.combining_rule_types.py` + Mixing rule function found in + `despasito.equations_of_state.combining_rule_types.py` kwargs : Keyword arguments used in other averaging function - + Returns ------- output_dict : dict - Dictionary with keyword of parameter and mixed interaction parameter. If combining rule type outputs more than one updated variable, it will also be included + Dictionary with keyword of parameter and mixed interaction parameter. If + combining rule type outputs more than one updated variable, it will also be + included """ calc_list = [o[0] for o in getmembers(combining_rule_types) if isfunction(o[1])] @@ -392,9 +416,8 @@ def combining_rules(beadA, beadB, parameter, function="mean", **kwargs): func = getattr(combining_rule_types, function) except Exception: raise ImportError( - "The combining rule type, '{}', was not found\nThe following calculation types are supported: {}".format( - function, ", ".join(calc_list) - ) + "The combining rule type, '{}', was not found\nThe following calculation " + "types are supported: {}".format(function, ", ".join(calc_list)) ) if function != "None": diff --git a/despasito/equations_of_state/interface.py b/despasito/equations_of_state/interface.py index a747a81..551e834 100644 --- a/despasito/equations_of_state/interface.py +++ b/despasito/equations_of_state/interface.py @@ -1,8 +1,8 @@ """ - Interface needed to create further equation of state (EOS) objects. + Interface needed to create further equation of state (EOS) objects. - All folders in this directory refer back to this interface. Using this template all future EOS will be easily exchanged. - + All folders in this directory refer back to this interface. Using this template + all future EOS will be easily exchanged. """ # All folders in this directory refer back to this interface @@ -13,9 +13,9 @@ logger = logging.getLogger(__name__) + # __________________ EOS Interface _________________ class EosTemplate(ABC): - """ Interface used in all EOS object options. @@ -26,9 +26,12 @@ class EosTemplate(ABC): beads : list[str] List of unique bead names used among components bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters + A dictionary where bead names are the keys to access EOS self interaction + parameters cross_library : dict, Optional - Optional library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. The remaining are estimated with mixing rules. + Optional library of bead cross interaction parameters. As many or as few of + the desired parameters may be defined for whichever group combinations are + desired. The remaining are estimated with mixing rules. kwargs Additional keywords from EOS object type @@ -37,21 +40,24 @@ class EosTemplate(ABC): beads : list[str] List of unique bead names used among components bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters + A dictionary where bead names are the keys to access EOS self interaction + parameters cross_library : dict - Library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. + Library of bead cross interaction parameters. As many or as few of the desired + parameters may be defined for whichever group combinations are desired. parameter_types : list[str] - This list of parameter names must be defined in an EOS object for parameter fitting + This list of parameter names must be defined in an EOS object for parameter + fitting parameter_bound_extreme : dict - With each parameter names as an entry representing a list with the minimum and maximum feasible parameter value. + With each parameter names as an entry representing a list with the minimum and + maximum feasible parameter value. number_of_components : int Number of components in mixture represented by given EOS object. - + """ def __init__(self, beads, bead_library, **kwargs): - """ Initiation of EOS object with attributes needed by other modules. - """ + """Initiation of EOS object with attributes needed by other modules.""" self.parameter_types = None self.parameter_bound_extreme = None @@ -71,7 +77,7 @@ def __init__(self, beads, bead_library, **kwargs): def pressure(self, rho, T, xi): """ Compute pressure given system information - + Parameters ---------- rho : numpy.ndarray @@ -80,11 +86,12 @@ def pressure(self, rho, T, xi): Temperature of the system [K] xi : list[float] Mole fraction of each component - + Returns ------- P : numpy.ndarray - Array of pressure values [Pa] associated with each density and so equal in length + Array of pressure values [Pa] associated with each density and so equal in + length """ pass @@ -92,7 +99,7 @@ def pressure(self, rho, T, xi): def fugacity_coefficient(self, P, rho, xi, T): r""" Compute fugacity coefficient - + Parameters ---------- P : float @@ -103,7 +110,7 @@ def fugacity_coefficient(self, P, rho, xi, T): Temperature of the system [K] xi : list[float] Mole fraction of each component - + Returns ------- fugacity_coefficient : numpy.ndarray @@ -115,7 +122,7 @@ def fugacity_coefficient(self, P, rho, xi, T): def density_max(self, xi, T, maxpack=0.9): """ Estimate the maximum density based on the hard sphere packing fraction. - + Parameters ---------- xi : list[float] @@ -124,7 +131,7 @@ def density_max(self, xi, T, maxpack=0.9): Temperature of the system [K] maxpack : float, Optional, default=0.9 Maximum packing fraction - + Returns ------- max_density : float @@ -144,34 +151,33 @@ def guess_parameters(self, param_name, bead_names): parameter : str Parameter to be fit. See EOS documentation for supported parameter names. bead_names : list - Bead names to be changed. For a self interaction parameter, the length will be 1, for a cross interaction parameter, the length will be two. + Bead names to be changed. For a self interaction parameter, the length will + be 1, for a cross interaction parameter, the length will be two. Returns ------- - param_initial_guess : numpy.ndarray, - An initial guess for parameter, it will be optimized throughout the process. + param_initial_guess : numpy.ndarray, + An initial guess for parameter, it will be optimized throughout the p + rocess. """ keys = ["beads", "parameter_types", "bead_library", "cross_library"] for key in keys: - if getattr(self, key) == None: + if getattr(self, key) is None: raise ValueError( - "EOS object attribute, {}, cannot be None. Ensure EOS object initiates this attribute".format( - key - ) + "EOS object attribute, {}, cannot be None. Ensure EOS object " + "initiates this attribute".format(key) ) if len(bead_names) > 2: raise ValueError( - "The bead names {} were given, but only a maximum of 2 are permitted.".format( - ", ".join(bead_names) - ) + "The bead names {} were given, but only a maximum of 2 are " + "permitted.".format(", ".join(bead_names)) ) if not set(bead_names).issubset(self.beads): raise ValueError( - "The bead names {} were given, but they are not in the allowed list: {}".format( - ", ".join(bead_names), ", ".join(self.beads) - ) + "The bead names {} were given, but they are not in the allowed" + " list: {}".format(", ".join(bead_names), ", ".join(self.beads)) ) param_value = None @@ -201,7 +207,7 @@ def guess_parameters(self, param_name, bead_names): param_name ] - if param_value == None: + if param_value is None: bounds = self.check_bounds(bead_names[0], param_name, np.empty(2)) param_value = (bounds[1] - bounds[0]) / 2 + bounds[0] @@ -210,29 +216,30 @@ def guess_parameters(self, param_name, bead_names): def check_bounds(self, parameter, param_name, bounds): """ Generate initial guesses for the parameters to be fit. - + Parameters ---------- parameter : str Parameter to be fit. See EOS documentation for supported parameter names. param_name : str - Full parameter string to be fit. See EOS documentation for supported parameter names. + Full parameter string to be fit. See EOS documentation for supported + parameter names. bounds : list Upper and lower bound for given parameter type - + Returns ------- bounds : list - A screened and possibly corrected low and a high value for the parameter, param_name + A screened and possibly corrected low and a high value for the parameter, + param_name """ keys = ["parameter_types", "parameter_bound_extreme"] for key in keys: - if getattr(self, key) == None: + if getattr(self, key) is None: raise ValueError( - "EOS object attribute, {}, cannot be None. Ensure EOS object initiates this attribute".format( - key - ) + "EOS object attribute, {}, cannot be None. Ensure EOS object " + "initiates this attribute".format(key) ) fit_parameter_names_list = param_name.split("_") @@ -244,7 +251,8 @@ def check_bounds(self, parameter, param_name, bounds): if bounds[0] < self.parameter_bound_extreme[parameter][0]: logger.debug( - "Given {} lower boundary, {}, is less than what is recommended by Eos object. Using value of {}.".format( + "Given {} lower boundary, {}, is less than what is recommended by " + "Eos object. Using value of {}.".format( param_name, bounds[0], self.parameter_bound_extreme[parameter][0], @@ -253,7 +261,8 @@ def check_bounds(self, parameter, param_name, bounds): bounds_new[0] = self.parameter_bound_extreme[parameter][0] elif bounds[0] > self.parameter_bound_extreme[parameter][1]: logger.debug( - "Given {} lower boundary, {}, is greater than what is recommended by Eos object. Using value of {}.".format( + "Given {} lower boundary, {}, is greater than what is recommended " + "by Eos object. Using value of {}.".format( param_name, bounds[0], self.parameter_bound_extreme[parameter][0], @@ -265,7 +274,8 @@ def check_bounds(self, parameter, param_name, bounds): if bounds[1] > self.parameter_bound_extreme[parameter][1]: logger.debug( - "Given {} upper boundary, {}, is greater than what is recommended by Eos object. Using value of {}.".format( + "Given {} upper boundary, {}, is greater than what is recommended " + "by Eos object. Using value of {}.".format( param_name, bounds[1], self.parameter_bound_extreme[parameter][1], @@ -274,7 +284,8 @@ def check_bounds(self, parameter, param_name, bounds): bounds_new[1] = self.parameter_bound_extreme[parameter][1] elif bounds[1] < self.parameter_bound_extreme[parameter][0]: logger.debug( - "Given {} upper boundary, {}, is less than what is recommended by Eos object. Using value of {}.".format( + "Given {} upper boundary, {}, is less than what is recommended " + "by Eos object. Using value of {}.".format( param_name, bounds[1], self.parameter_bound_extreme[parameter][1], @@ -286,9 +297,8 @@ def check_bounds(self, parameter, param_name, bounds): else: raise ValueError( - "The parameter name {} is not found in the allowed parameter types: {}".format( - param_name, ", ".join(self.parameter_types) - ) + "The parameter name {} is not found in the allowed parameter types:" + " {}".format(param_name, ", ".join(self.parameter_types)) ) return bounds_new @@ -300,9 +310,12 @@ def update_parameter(self, param_name, bead_names, param_value): Parameters ---------- param_name : str - Parameter to be fit. See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + Parameter to be fit. See EOS documentation for supported parameter names. + Cross interaction parameter names should be composed of parameter name and + the other bead type, separated by an underscore (e.g. epsilon_CO2). bead_names : list - Bead names to be changed. For a self interaction parameter, the length will be 1, for a cross interaction parameter, the length will be two. + Bead names to be changed. For a self interaction parameter, the length will + be 1, for a cross interaction parameter, the length will be two. param_value : float Value of parameter @@ -310,31 +323,27 @@ def update_parameter(self, param_name, bead_names, param_value): keys = ["beads", "parameter_types", "bead_library", "cross_library"] for key in keys: - if getattr(self, key) == None: + if getattr(self, key) is None: raise ValueError( - "EOS object attribute, {}, cannot be None. Ensure EOS object initiates this attribute".format( - key - ) + "EOS object attribute, {}, cannot be None. Ensure EOS object " + "initiates this attribute".format(key) ) if len(bead_names) > 2: raise ValueError( - "The bead names {} were given, but only a maximum of 2 are permitted.".format( - ", ".join(bead_names) - ) + "The bead names {} were given, but only a maximum of 2 are " + "permitted.".format(", ".join(bead_names)) ) if not set(bead_names).issubset(self.beads): raise ValueError( - "The bead names {} were given, but they are not in the allowed list: {}".format( - ", ".join(bead_names), ", ".join(self.beads) - ) + "The bead names {} were given, but they are not in the allowed " + "list: {}".format(", ".join(bead_names), ", ".join(self.beads)) ) if not any([x in param_name for x in self.parameter_types]): raise ValueError( - "The parameter name {} is not found in the allowed parameter types: {}".format( - param_name, ", ".join(self.parameter_types) - ) + "The parameter name {} is not found in the allowed parameter " + "types: {}".format(param_name, ", ".join(self.parameter_types)) ) # Self interaction parameter diff --git a/despasito/equations_of_state/saft/Aassoc.py b/despasito/equations_of_state/saft/Aassoc.py index 6f913cd..d61ea96 100644 --- a/despasito/equations_of_state/saft/Aassoc.py +++ b/despasito/equations_of_state/saft/Aassoc.py @@ -1,124 +1,88 @@ # -- coding: utf8 -- r""" - EOS object for SAFT association sites contributions to the Helmholtz energy - """ +import sys import numpy as np import logging -from despasito.equations_of_state import constants +from .compiled_modules.ext_Aassoc_numba import calc_Xika as calc_Xika_numba +from .compiled_modules.ext_Aassoc_python import calc_Xika as calc_Xika_python logger = logging.getLogger(__name__) -try: - from .compiled_modules import ext_Aassoc_fortran - flag_fortran = True -except ImportError: - flag_fortran = False - logger.warning("Fortran unavailable, using Numba") - -try: - import cython +if "cython" not in sys.modules: + print("Cython package is unavailable, using Numba") flag_cython = True -except ModuleNotFoundError: +else: flag_cython = False - logger.warning("Cython package is unavailable, using Numba") - -if flag_cython: try: from .compiled_modules.ext_Aassoc_cython import calc_Xika as calc_Xika_cython except ImportError: - raise ImportError("Cython package is available but module: despasito.equations_of_state.saft.compiled_modules.ext_Aassoc_cython, has not been compiled.") - -from .compiled_modules.ext_Aassoc_numba import calc_Xika as calc_Xika_numba -from .compiled_modules.ext_Aassoc_python import calc_Xika as calc_Xika_python + raise ImportError( + "Cython package is available but module: " + "despasito.equations_of_state.saft.compiled_modules.ext_Aassoc_cython, has" + " not been compiled." + ) def _calc_Xika_wrap(*args, method_stat, maxiter=500, tol=1e-12, damp=0.1): - r""" This function wrapper allows difference types of compiled functions to be referenced. - """ + r"""This function wrapper allows difference types of compiled functions to be " + "referenced.""" indices, rho, xi, molecular_composition, nk, Fklab, Kklab, gr_assoc = args if len(np.shape(Kklab)) == 4: - if method_stat.fortran and flag_fortran: - Xika_init = 0.5 * np.ones(len(indices)) - Xika = ext_Aassoc_fortran.calc_xika_4( - indices, - constants.molecule_per_nm3 * rho, - Xika_init, - xi, - molecular_composition, - nk, - Fklab, - Kklab, - gr_assoc, - maxiter, - tol, - ) + if method_stat.cython and flag_cython: + Xika, _ = calc_Xika_cython(*args) + elif method_stat.python: + Xika, _ = calc_Xika_python(*args) + logger.warning("Using pure python. Consider using 'numba' flag") + elif method_stat.numba or not flag_cython: + Xika, _ = calc_Xika_numba(*args) else: - if method_stat.cython and flag_cython: - Xika, _ = calc_Xika_cython(*args) - elif method_stat.python: - Xika, _ = calc_Xika_python(*args) - logger.warning("Using pure python. Consider using 'numba' flag") - elif method_stat.numba or not flag_fortran or not flag_cython: - Xika, _ = calc_Xika_numba(*args) - else: - raise ValueError("Appropriate options for calc_Xika have not been defined.") + raise ValueError("Appropriate options for calc_Xika have not been defined.") elif len(np.shape(Kklab)) == 6: - if method_stat.fortran and flag_fortran: - Xika_init = 0.5 * np.ones(len(indices)) - Xika = ext_Aassoc_fortran.calc_xika_6( - indices, - constants.molecule_per_nm3 * rho, - Xika_init, - xi, - molecular_composition, - nk, - Fklab, - Kklab, - gr_assoc, - maxiter, - tol, - ) + if method_stat.cython and flag_cython: + Xika, _ = calc_Xika_cython(*args) + elif method_stat.python: + Xika, _ = calc_Xika_python(*args) + logger.warning("Using pure python. Consider using 'numba' flag") + elif method_stat.numba or not flag_cython: + Xika, _ = calc_Xika_numba(*args) else: - if method_stat.cython and flag_cython: - Xika, _ = calc_Xika_cython(*args) - elif method_stat.python: - Xika, _ = calc_Xika_python(*args) - logger.warning("Using pure python. Consider using 'numba' flag") - elif method_stat.numba or not flag_fortran or not flag_cython: - Xika, _ = calc_Xika_numba(*args) - else: - raise ValueError("Appropriate options for calc_Xika have not been defined.") + raise ValueError("Appropriate options for calc_Xika have not been defined.") return Xika def assoc_site_indices(nk, molecular_composition, xi=None): r""" - Make a list of sets of indices that allow quick identification of the relevant association sights. - - This is needed for solving Xika, the fraction of molecules of component i that are not bonded at a site of type a on group k. - + Make a list of sets of indices that allow quick identification of the relevant + association sites. + + This is needed for solving Xika, the fraction of molecules of component i that are + not bonded at a site of type a on group k. + Parameters ---------- nk : numpy.ndarray - A matrix of (Nbeads x Nsites) Contains for each bead the number of each type of site + A matrix of (Nbeads x Nsites) Contains for each bead the number of each type + of site molecular_composition : numpy.ndarray - :math:`\nu_{i,k}/k_B`. Array of number of components by number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`. Array of number of components by number of bead types. + Defines the number of each type of group in each component. xi : numpy.ndarray, Optional, default=None Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- indices : list[list] - A list of sets of (component, bead, site) to identify the values of the Xika matrix that are being fit + A list of sets of (component, bead, site) to identify the values of the Xika + matrix that are being fit """ indices = [] @@ -142,31 +106,37 @@ def assoc_site_indices(nk, molecular_composition, xi=None): def initiate_assoc_matrices(beads, bead_library, molecular_composition): r""" - + Generate matrices used for association site calculations. - - Compute epsilonHB (interaction energy for association term), Kklab (association interaction bonding volume, and nk (number of sites ) - + + Compute epsilonHB (interaction energy for association term), Kklab (association + interaction bonding volume, and nk (number of sites ) + Parameters ---------- beads : list[str] List of unique bead names used among components bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters: + A dictionary where bead names are the keys to access EOS self interaction + parameters: - - Nk\*: Optional, The number of sites of from list sitenames. Asterisk represents string from sitenames. + - Nk\*: Optional, The number of sites of from list sitenames. Asterisk + represents string from sitenames. molecular_composition : numpy.ndarray - :math:`\nu_{i,k}/k_B`. Array of number of components by number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`. Array of number of components by number of bead types. + Defines the number of each type of group in each component. Returns ---------- sitenames : list This list shows the names of the various association types found nk : numpy.ndarray - A matrix of (Nbeads x Nsites) Contains for each bead the number of each type of site + A matrix of (Nbeads x Nsites) Contains for each bead the number of each type + of site flag_assoc : bool - If True, this flag indicates that association sites play a role in this system. + If True, this flag indicates that association sites play a role in this + system. """ sitenames = [] @@ -179,7 +149,8 @@ def initiate_assoc_matrices(beads, bead_library, molecular_composition): tmp = key.split("-") if len(tmp) < 2: raise ValueError( - "Association site names should be defined with hyphens (e.g. Nk-H)" + "Association site names should be defined with hyphens " + + "(e.g. Nk-H)" ) else: _, site = tmp @@ -223,46 +194,64 @@ def calc_assoc_matrices( sitenames=None, ): r""" - + Generate matrices used for association site calculations. - - Compute epsilonHB (interaction energy for association term), Kklab (association interaction bonding volume, and nk (number of sites ) - Note: Some papers use rc_klab instead of Kklab. In those cases, a function to calculate Kklab is needed (see Papaioannou 2014). + Compute epsilonHB (interaction energy for association term), Kklab (association + interaction bonding volume, and nk (number of sites ) + + Note: Some papers use rc_klab instead of Kklab. In those cases, a function to + calculate Kklab is needed (see Papaioannou 2014). Parameters ---------- beads : list[str] List of unique bead names used among components bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters: - - - epsilonHB-\*-\*: Optional, Interaction energy between each bead and association site. Asterisk represents string from sitenames. - - K-\*-\*: Optional, Bonding volume between each association site. Asterisk represents two strings from sitenames. - - rc-\*-\*: Optional, Cutoff distance for association sites. Asterisk represents two strings from sitenames. - - rd-\*-\*: Optional, Site position. Asterisk represents two strings from sitenames. - - Nk-\*: Optional, The number of sites of from list sitenames. Asterisk represents string from sitenames. + A dictionary where bead names are the keys to access EOS self interaction + parameters: + + - epsilonHB-\*-\*: Optional, Interaction energy between each bead and + association site. Asterisk represents string from sitenames. + - K-\*-\*: Optional, Bonding volume between each association site. Asterisk + represents two strings from sitenames. + - rc-\*-\*: Optional, Cutoff distance for association sites. Asterisk + represents two strings from sitenames. + - rd-\*-\*: Optional, Site position. Asterisk represents two strings from + sitenames. + - Nk-\*: Optional, The number of sites of from list sitenames. Asterisk + represents string from sitenames. molecular_composition : numpy.ndarray - :math:`\nu_{i,k}/k_B`. Array of number of components by number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`. Array of number of components by number of bead types. + Defines the number of each type of group in each component. cross_library : dict, Optional, default={} - A dictionary where bead names are the keys to access a dictionary of a second tier of bead names. This structure contains the EOS cross interaction parameters: - - - epsilonHB-\*-\*: Optional, Interaction energy between each bead and association site. Asterisk represents string from sitenames. - - K-\*-\*: Optional, Bonding volume between each association site. Asterisk represents two strings from sitenames. - - rc-\*-\*: Optional, Cutoff distance for association sites. Asterisk represents two strings from sitenames. - - rd-\*-\*: Optional, Site position. Asterisk represents two strings from sitenames. + A dictionary where bead names are the keys to access a dictionary of a second + tier of bead names. This structure contains the EOS cross interaction + parameters: + + - epsilonHB-\*-\*: Optional, Interaction energy between each bead and + association site. Asterisk represents string from sitenames. + - K-\*-\*: Optional, Bonding volume between each association site. Asterisk + represents two strings from sitenames. + - rc-\*-\*: Optional, Cutoff distance for association sites. Asterisk + represents two strings from sitenames. + - rd-\*-\*: Optional, Site position. Asterisk represents two strings from + sitenames. nk : numpy.ndarray, Optional, default=None - A matrix of (Nbeads x Nsites) Contains for each bead the number of each type of site + A matrix of (Nbeads x Nsites) Contains for each bead the number of each type + of site } sitenames : list, Optional, default=None This list shows the names of the various association types found - + Returns ------- output_dict : dict - This dictionary contains parameters relevant to calculating association site contributions. The following matrices may be inside, each of the size (ngroups, ngroups, nsites, nsites). + This dictionary contains parameters relevant to calculating association site + contributions. The following matrices may be inside, each of the size (ngroups, + ngroups, nsites, nsites). - epsilonHB: Interaction energy between each bead and association site. - Kklab, Optional: Bonding volume between each association site @@ -272,7 +261,7 @@ def calc_assoc_matrices( """ nbeads = len(beads) - if np.any(sitenames == None) or np.any(nk == None): + if np.any(sitenames is None) or np.any(nk is None): sitenames, nk, _ = initiate_assoc_matrices( bead_library, beads, molecular_composition ) @@ -294,40 +283,64 @@ def calc_assoc_matrices( if nk1[a] == 0.0: continue - for b, site2 in enumerate(sitenames): + for b, site2 in zip(list(range(a, len(sitenames))), sitenames[a:]): if nk1[b] != 0: epsilon_tmp = "-".join(["epsilonHB", site1, site2]) + if epsilon_tmp not in bead_library[bead1]: + epsilon_tmp = "-".join(["epsilonHB", site2, site1]) K_tmp = "-".join(["K", site1, site2]) + if K_tmp not in bead_library[bead1]: + K_tmp = "-".join(["K", site2, site1]) rc_tmp = "-".join(["rc", site1, site2]) + if rc_tmp not in bead_library[bead1]: + rc_tmp = "-".join(["rc", site2, site1]) rd_tmp = "-".join(["rd", site1, site2]) + if rd_tmp not in bead_library[bead1]: + rd_tmp = "-".join(["rd", site2, site1]) if epsilon_tmp in bead_library[bead1] and ( K_tmp not in bead_library[bead1] and rc_tmp not in bead_library[bead1] ): raise ValueError( - "An association site energy parameter for {} was given for bead {}, but not the bonding information. Either K-sitename-sitename or rc-sitename-sitename must be given.".format( - "{}-{}".format(site1, site2), bead1 + "An association site energy parameter for {}-{}".format( + site1, site2, + ) + + " was given for bead {}, but not the bonding".format( + bead1 + ) + + " information. Either K-{}-{}/K-{}-{} or".format( + site1, site2, site2, site1, + ) + + " rc-{}-{}/rc-{}-{} must be given.".format( + site1, site2, site2, site1 ) ) elif K_tmp in bead_library[bead1] and rc_tmp in bead_library[bead1]: raise ValueError( - "Both association site bonding volumes and cutoff distances were provided for bead {}. This is redundant.".format( - bead1 - ) + "Both association site bonding volumes and cutoff " + + "distances were provided for bead {}.".format(bead1) + + " This is redundant." ) elif epsilon_tmp not in bead_library[bead1] and ( K_tmp in bead_library[bead1] or rc_tmp in bead_library[bead1] ): raise ValueError( - "An association site bonding information for {} was given for bead {}, but not the energy parameter. epsilonHB must be given.".format( - "{}-{}".format(site1, site2), bead1 + "An association site bonding information for {}".format( + "{}-{}".format(site1, site2) ) + + " was given for bead {}, but not the energy".format(bead1) + + " parameter. epsilonHB must be given." ) if epsilon_tmp in bead_library[bead1]: - epsilonHB[i, i, a, b] = bead_library[bead1][epsilon_tmp] + if a == b: + epsilonHB[i, i, a, b] = -1 * np.abs( + bead_library[bead1][epsilon_tmp] + ) + else: + epsilonHB[i, i, a, b] = bead_library[bead1][epsilon_tmp] epsilonHB[i, i, b, a] = epsilonHB[i, i, a, b] else: continue @@ -351,78 +364,108 @@ def calc_assoc_matrices( for i, nk1 in enumerate(nk): bead1 = beads[i] for a, site1 in enumerate(sitenames): - if nk1[a] == 0.0 or bead1 not in cross_library: + if nk1[a] == 0.0: continue for b, site2 in enumerate(sitenames): epsilon_tmp = "-".join(["epsilonHB", site1, site2]) K_tmp = "-".join(["K", site1, site2]) + rc_tmp = "-".join(["rc", site1, site2]) + rd_tmp = "-".join(["rd", site1, site2]) for j, bead2 in enumerate(beads): - if i == j: + if i == j and a == b: continue - flag_update = False - if bead2 in cross_library[bead1]: + if ( + bead1 in cross_library + and bead2 in cross_library[bead1] + and epsilon_tmp in cross_library[bead1][bead2] + ): # Update matrix if found in cross_library - if epsilon_tmp in cross_library[bead1][bead2]: - if nk[i][a] == 0 or nk[j][b] == 0: - if 0 not in [nk[i][b], nk[j][a]]: - logger.warning( - "Site names were listed in the wrong order for parameter definitions in cross interaction library. Changing {}_{} - {}_{} interaction to {}_{} - {}_{}".format( - beads[i], - sitenames[a], - beads[j], - sitenames[b], - beads[i], - sitenames[b], - beads[j], - sitenames[a], - ) + if nk[i][a] == 0 or nk[j][b] == 0: + if 0 not in [nk[i][b], nk[j][a]]: + logger.warning( + "Site names were listed in the wrong order for " + + "parameter definitions in cross interaction " + + "library. Changing {}_{} - {}_{}".format( + beads[i], + sitenames[a], + beads[j], + sitenames[b], + ) + + " interaction to {}_{} - {}_{}".format( + beads[i], + sitenames[b], + beads[j], + sitenames[a], ) - a, b = [b, a] - elif nk[i][a] == 0: - raise ValueError( - "Cross interaction library parameters suggest a {}_{} - {}_{} interaction, but {} doesn't have site {}.".format( - beads[i], - sitenames[a], - beads[j], - sitenames[b], - beads[i], - sitenames[a], - ) + ) + a, b = [b, a] + elif nk[i][a] == 0: + raise ValueError( + "Cross interaction library parameters suggest a " + + "{}_{} - {}_{} interaction, but {}".format( + beads[i], + sitenames[a], + beads[j], + sitenames[b], + beads[i], ) - elif nk[j][b] == 0: - raise ValueError( - "Cross interaction library parameters suggest a {}_{} - {}_{} interaction, but {} doesn't have site {}.".format( - beads[i], - sitenames[a], - beads[j], - sitenames[b], - beads[j], - sitenames[b], - ) + + " doesn't have site {}.".format( + sitenames[a], ) + ) + elif nk[j][b] == 0: + raise ValueError( + "Cross interaction library parameters suggest a " + + "{}_{} - {}_{} interaction, but {}".format( + beads[i], + sitenames[a], + beads[j], + sitenames[b], + beads[j], + ) + + " doesn't have site {}.".format( + sitenames[b], + ) + ) - flag_update = True - epsilonHB[i, j, a, b] = cross_library[bead1][bead2][ - epsilon_tmp - ] - epsilonHB[j, i, b, a] = epsilonHB[i, j, a, b] + epsilonHB[i, j, a, b] = cross_library[bead1][bead2][epsilon_tmp] + epsilonHB[j, i, b, a] = epsilonHB[i, j, a, b] - if flag_Kklab: - Kklab[i, j, a, b] = cross_library[bead1][bead2][K_tmp] - Kklab[j, i, b, a] = Kklab[i, j, a, b] + if flag_Kklab and K_tmp in cross_library[bead1][bead2]: + Kklab[i, j, a, b] = cross_library[bead1][bead2][K_tmp] + Kklab[j, i, b, a] = Kklab[i, j, a, b] - if ( - not flag_update - and nk[j][b] != 0 - and epsilonHB[j, i, b, a] == 0.0 - ): - epsilonHB[i, j, a, b] = np.sqrt( - epsilonHB[i, i, a, a] * epsilonHB[j, j, b, b] + if flag_rc_klab and rc_tmp in cross_library[bead1][bead2]: + rc_klab[i, j, a, b] = cross_library[bead1][bead2][rc_tmp] + rc_klab[j, i, b, a] = rc_klab[i, j, a, b] + + if flag_rd_klab and rd_tmp in cross_library[bead1][bead2]: + rd_klab[i, j, a, b] = cross_library[bead1][bead2][rd_tmp] + rd_klab[j, i, b, a] = rd_klab[i, j, a, b] + + elif nk[j][b] != 0: + sitea = epsilon_tmp = "-".join( + ["epsilonHB", sitenames[a], sitenames[a]] ) - epsilonHB[j, i, b, a] = epsilonHB[i, j, a, b] - if flag_Kklab: + siteb = epsilon_tmp = "-".join( + ["epsilonHB", sitenames[b], sitenames[b]] + ) + if ( + epsilonHB[j, i, b, a] == 0.0 + and sitea in bead_library[beads[i]] + and siteb in bead_library[beads[j]] + ): + epsilonHB[i, j, a, b] = np.sqrt( + epsilonHB[i, i, a, a] * epsilonHB[j, j, b, b] + ) + epsilonHB[i, j, a, b] *= -1 * np.sign( + bead_library[beads[i]][sitea] + * bead_library[beads[j]][siteb] + ) + epsilonHB[j, i, b, a] = epsilonHB[i, j, a, b] + if flag_Kklab and Kklab[i, j, a, b] == 0.0: Kklab[i, j, a, b] = ( ( (Kklab[i, i, a, a]) ** (1.0 / 3.0) @@ -432,19 +475,32 @@ def calc_assoc_matrices( ) ** 3 Kklab[j, i, b, a] = Kklab[i, j, a, b] + if flag_rc_klab and rc_klab[i, j, a, b] == 0.0: + rc_klab[i, j, a, b] = ( + rc_klab[i, i, a, a] + rc_klab[j, j, b, b] + ) / 2 + rc_klab[j, i, b, a] = rc_klab[i, j, a, b] + + if flag_rd_klab and rd_klab[i, j, a, b] == 0.0: + rd_klab[i, j, a, b] = ( + rd_klab[i, i, a, a] + rd_klab[j, j, b, b] + ) / 2 + rd_klab[j, i, b, a] = rd_klab[i, j, a, b] + output = {"epsilonHB": epsilonHB} if flag_Kklab: output["Kklab"] = Kklab - elif flag_rc_klab: + if flag_rc_klab: output["rc_klab"] = rc_klab - elif flag_rd_klab: + if flag_rd_klab: output["rd_klab"] = rd_klab if flag_Kklab and flag_rc_klab: raise ValueError( - "Both association site bonding volumes and cutoff distances were provided. This is redundant." + "Both association site bonding volumes and cutoff distances were provided." + " This is redundant." ) - elif flag_rd_klab and not flag_rc_klab: + if flag_rd_klab and not flag_rc_klab: raise ValueError( "Association site position were provided, but not cutoff distances." ) @@ -454,20 +510,23 @@ def calc_assoc_matrices( def calc_bonding_volume(rc_klab, dij_bar, rd_klab=None, reduction_ratio=0.25): """ - Calculate the association site bonding volume matrix + Calculate the association site bonding volume matrix Dimensions of (ncomp, ncomp, nbeads, nbeads, nsite, nsite) Parameters ---------- rc_klab : numpy.ndarray - This matrix of cutoff distances for association sites for each site type in each group type + This matrix of cutoff distances for association sites for each site type in + each group type dij_bar : numpy.ndarray Component averaged hard sphere diameter rd_klab : numpy.ndarray, Optional, default=None Position of association site in each group (nbead, nbead, nsite, nsite) reduction_ratio : float, Optional, default=0.25 - Reduced distance of the sites from the center of the sphere of interaction. This value is used when site position, rd_klab is None + Reduced distance of the sites from the center of the sphere of interaction. + This value is used when site position, rd_klab is not defined for that + site-site interaction. Returns ------- @@ -486,23 +545,23 @@ def calc_bonding_volume(rc_klab, dij_bar, rd_klab=None, reduction_ratio=0.25): for a in range(nsite): for b in range(nsite): if rc_klab[k, l, a, b] != 0: - if rd_klab == None: + if np.all(rd_klab is None) or rd_klab[k, l, a, b] == 0: rd = reduction_ratio * dij_bar[i, j] else: rd = rd_klab[k, l, a, b] - tmp0 = np.pi * dij_bar[i, j] ** 2 / (18 * rd ** 2) + tmp0 = np.pi * dij_bar[i, j] ** 2 / (18 * rd**2) tmp11 = np.log( (rc_klab[k, l, a, b] + 2 * rd) / dij_bar[i, j] ) tmp12 = ( 6 * rc_klab[k, l, a, b] ** 3 + 18 * rc_klab[k, l, a, b] ** 2 * rd - - 24 * rd ** 3 + - 24 * rd**3 ) tmp21 = rc_klab[k, l, a, b] + 2 * rd - dij_bar[i, j] tmp22 = ( - 22 * rd ** 2 + 22 * rd**2 - 5 * rd * rc_klab[k, l, a, b] - 7 * rd * dij_bar[i, j] - 8 * rc_klab[k, l, a, b] ** 2 diff --git a/despasito/equations_of_state/saft/Aideal.py b/despasito/equations_of_state/saft/Aideal.py index c8ac61a..d8383c1 100644 --- a/despasito/equations_of_state/saft/Aideal.py +++ b/despasito/equations_of_state/saft/Aideal.py @@ -1,9 +1,8 @@ # -- coding: utf8 -- r""" - -EOS object for SAFT ideal gas contributions to the Helmholtz energy with :func:`~despasito.equations_of_state.saft.Aideal.Aideal_contribution` - +EOS object for SAFT ideal gas contributions to the Helmholtz energy with +:func:`~despasito.equations_of_state.saft.Aideal.Aideal_contribution` """ import numpy as np @@ -16,13 +15,12 @@ def Aideal_contribution(rho, T, xi, massi, method="Abroglie"): - r""" Return a vector of ideal contribution of the Helmholtz energy. - + :math:`\frac{A^{ideal}}{N k_{B} T}` - Supported methods include: + Supported methods include: :func:`~despasito.equations_of_state.saft.Aideal.Abroglie`, Parameters @@ -36,8 +34,10 @@ def Aideal_contribution(rho, T, xi, massi, method="Abroglie"): massi : numpy.ndarray Vector of component masses that correspond to the mole fractions in xi [kg/mol] method : str, Optional, default=Abroglie - The function name of the method to calculate the ideal contribution of the Helmholtz energy. To add a new one, add a function to: despasito.equations_of_state.saft.Aideal.py - + The function name of the method to calculate the ideal contribution of the + Helmholtz energy. To add a new one, add a function to: + despasito.equations_of_state.saft.Aideal.py + Returns ------- Aideal : numpy.ndarray @@ -57,10 +57,10 @@ def Aideal_contribution(rho, T, xi, massi, method="Abroglie"): def Abroglie(rho, T, xi, massi): - r""" - Return a vector of ideal contribution of Helmholtz energy derived from Broglie wavelength - + Return a vector of ideal contribution of Helmholtz energy derived from Broglie + wavelength + Parameters ---------- rho : numpy.ndarray @@ -71,7 +71,7 @@ def Abroglie(rho, T, xi, massi): Mole fraction of each component, sum(xi) should equal 1.0 massi : numpy.ndarray Vector of component masses that correspond to the mole fractions in xi [kg/mol] - + Returns ------- Aideal : numpy.ndarray @@ -82,7 +82,8 @@ def Abroglie(rho, T, xi, massi): xi_tmp, massi_tmp = tb.remove_insignificant_components(xi, massi) - # rhoi: (number of components,number of densities) number density of each component for each density + # rhoi: (number of components,number of densities) number density of each component + # for each density rhoi = np.outer(rho2, xi_tmp) Lambda = np.sqrt( @@ -90,13 +91,12 @@ def Abroglie(rho, T, xi, massi): * (constants.h / constants.kb * constants.m2nm) / (2.0 * np.pi * massi_tmp * T) ) - log_broglie3_rho = np.log(Lambda ** 3 * rhoi) + log_broglie3_rho = np.log(Lambda**3 * rhoi) if np.isnan(np.sum(np.sum(xi_tmp * log_broglie3_rho, axis=1))): raise ValueError( - "Aideal has values of zero when taking the log. All mole fraction values should be nonzero. Mole fraction: {}".format( - xi_tmp - ) + "Aideal has values of zero when taking the log. All mole fraction values " + "should be nonzero. Mole fraction: {}".format(xi_tmp) ) else: Aideal = np.sum(xi_tmp * log_broglie3_rho, axis=1) - 1.0 diff --git a/despasito/equations_of_state/saft/__init__.py b/despasito/equations_of_state/saft/__init__.py index 8b13789..e69de29 100644 --- a/despasito/equations_of_state/saft/__init__.py +++ b/despasito/equations_of_state/saft/__init__.py @@ -1 +0,0 @@ - diff --git a/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_cython.pyx b/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_cython.pyx index b2795d1..1d1305f 100644 --- a/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_cython.pyx +++ b/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_cython.pyx @@ -66,7 +66,7 @@ cdef calc_Xika_4(int[:,:] indices, double[:] rho, double[:] xi, double[:,:] nui, cdef int nrho = rho.shape[0] cdef int l_ind = indices.shape[0] - cdef int r, knd, ind, i, k, a, jnd, j, l, b, z + cdef int r, knd, ind, iind, i, k, a, jjnd, jnd, j, l, b, z cdef double delta, obj, Xika_max cdef double[:] Xika_elements_new = np.empty(l_ind, dtype=np.float_) @@ -76,17 +76,29 @@ cdef calc_Xika_4(int[:,:] indices, double[:] rho, double[:] xi, double[:,:] nui, for r in range(nrho): for knd in range(maxiter): Xika_elements_new[:] = 1.0 - for ind in range(l_ind): - i = indices[ind][0] - k = indices[ind][1] - a = indices[ind][2] - for jnd in range(l_ind): - j = indices[jnd][0] - l = indices[jnd][1] - b = indices[jnd][2] + ind = 0 + for iind in range(l_ind): + i = indices[iind][0] + k = indices[iind][1] + a = indices[iind][2] + jnd = 0 + for jjnd in range(l_ind): + j = indices[jjnd][0] + l = indices[jjnd][1] + b = indices[jjnd][2] delta = Fklab[k, l, a, b] * Kklab[k, l, a, b] * gr_assoc[r,i, j] - Xika_elements_new[ind] = Xika_elements_new[ind] + const_molecule_per_nm3 * rho[r] * xi[j] * nui[j,l] * nk[l,b] * Xika_elements[jnd] * delta + Xika_elements_new[ind] = Xika_elements_new[ind] + ( + const_molecule_per_nm3 + * rho[r] + * xi[j] + * nui[j,l] + * nk[l,b] * + Xika_elements[jnd] + * delta + ) #printf("[%.14e %.14e %.14e %.14e %.14e %.14e]\n",rho[r], xi[j], nui[j,l], nk[l,b], Xika_elements[jnd], delta) + jnd += 1 + ind += 1 obj = 0 Xika_max = 0 @@ -120,7 +132,7 @@ cdef calc_Xika_6(int[:,:] indices, double[:] rho, double[:] xi, double[:,:] nui, cdef int nrho = rho.shape[0] cdef int l_ind = indices.shape[0] - cdef int r, knd, ind, i, k, a, jnd, j, l, b, z + cdef int r, knd, ind, iind, i, k, a, jnd, jjnd, j, l, b, z cdef double delta, obj, Xika_max cdef double[:] Xika_elements_new = np.empty(l_ind, dtype=np.float_) @@ -130,21 +142,33 @@ cdef calc_Xika_6(int[:,:] indices, double[:] rho, double[:] xi, double[:,:] nui, for r in range(nrho): for knd in range(maxiter): Xika_elements_new[:] = 1.0 - for ind in range(l_ind): - i = indices[ind][0] - k = indices[ind][1] - a = indices[ind][2] - for jnd in range(l_ind): - j = indices[jnd][0] - l = indices[jnd][1] - b = indices[jnd][2] + ind = 0 + for iind in range(l_ind): + i = indices[iind][0] + k = indices[iind][1] + a = indices[iind][2] + jnd = 0 + for jjnd in range(l_ind): + j = indices[jjnd][0] + l = indices[jjnd][1] + b = indices[jjnd][2] delta = Fklab[k, l, a, b] * Kklab[i, j, k, l, a, b] * gr_assoc[r,i, j] - Xika_elements_new[ind] = Xika_elements_new[ind] + const_molecule_per_nm3 * rho[r] * xi[j] * nui[j,l] * nk[l,b] * Xika_elements[jnd] * delta + Xika_elements_new[ind] = Xika_elements_new[ind] + ( + const_molecule_per_nm3 + * rho[r] + * xi[j] + * nui[j,l] + * nk[l,b] + * Xika_elements[jnd] + * delta + ) + jnd += 1 + ind += 1 obj = 0 Xika_max = 0 for z in range(l_ind): - Xika_elements_new[z] = 1.0/Xika_elements_new[z] + Xika_elements_new[z] = 1.0 / Xika_elements_new[z] obj += abs(Xika_elements_new[z]-Xika_elements[z]) if Xika_elements_new[z] > Xika_max: Xika_max = Xika_elements_new[z] @@ -156,7 +180,8 @@ cdef calc_Xika_6(int[:,:] indices, double[:] rho, double[:] xi, double[:,:] nui, for z in range(l_ind): Xika_elements[z] = Xika_elements[z] + damp*(Xika_elements_new[z] - Xika_elements[z]) else: - Xika_elements = Xika_elements_new + for z in range(l_ind): + Xika_elements[z] = Xika_elements_new[z] err_array[r] = obj Xika_final[r,:] = Xika_elements diff --git a/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_fortran.f90 b/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_fortran.f90 deleted file mode 100755 index bc710d9..0000000 --- a/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_fortran.f90 +++ /dev/null @@ -1,149 +0,0 @@ -subroutine calc_Xika_inner_4(Xika0,indices,xi,rho,nui,nk,Fklab,Kklab,Iij,obj_func,Xika,ncomp,nbeads,nsitemax,nind) - implicit none - integer,intent(in) :: ncomp, nbeads, nsitemax, nind - real(8), intent(in), dimension(0:nind-1) :: Xika0 - real(8), intent(in), dimension(0:nind-1, 0:2) :: indices - real(8), intent(in), dimension(0:ncomp-1) :: xi - real(8), intent(in), dimension(0:ncomp-1,0:nbeads-1) :: nui - real(8), intent(in), dimension(0:nbeads-1,0:nsitemax-1) :: nk - real(8), intent(in), dimension(0:nbeads-1,0:nbeads-1,0:nsitemax-1,0:nsitemax-1) :: Fklab - real(8), intent(in), dimension(0:nbeads-1,0:nbeads-1,0:nsitemax-1,0:nsitemax-1) :: Kklab - real(8), intent(in), dimension(0:ncomp-1,0:ncomp-1) :: Iij - real(8), intent(in) :: rho - real(8), intent(out), dimension(0:nind-1) :: obj_func - real(8), intent(out), dimension(0:nind-1) :: Xika - integer :: ii,jj,i,k,a,j,l,b - - !initialize Xika to 1.0 - Xika=1.0 - - do ii=0, nind-1 - i = indices(ii,0) - k = indices(ii,1) - a = indices(ii,2) - do jj=0, nind-1 - j = indices(jj,0) - l = indices(jj,1) - b = indices(jj,2) - Xika(ii)=Xika(ii) + (rho * xi(j) * nui(j,l) * nk(l,b) * Xika0(jj) * Fklab(k,l,a,b) * Kklab(k,l,a,b) * Iij(i,j)) - enddo - enddo - Xika = (1.0/Xika) - - obj_func = Xika - Xika0 - -end subroutine - - -subroutine calc_Xika_4(indices,rho,Xika_init,xi,nui,nk,Fklab,Kklab,Iij,maxiter,tol,Xika_final,nrho,ncomp,nbeads,nsitemax,nind) - implicit none - integer, intent(in) :: nrho, ncomp, nbeads, nsitemax, nind - real(8), intent(in), dimension(0:nind-1, 0:2) :: indices - real(8), intent(in), dimension(0:nrho-1) :: rho - real(8), intent(in), dimension(0:nind-1) :: Xika_init - real(8), intent(in), dimension(0:ncomp-1) :: xi - real(8), intent(in), dimension(0:ncomp-1,0:nbeads-1) :: nui - real(8), intent(in), dimension(0:nbeads-1,0:nsitemax-1) :: nk - real(8), intent(in), dimension(0:nbeads-1,0:nbeads-1,0:nsitemax-1,0:nsitemax-1) :: Fklab - real(8), intent(in), dimension(0:nbeads-1,0:nbeads-1,0:nsitemax-1,0:nsitemax-1) :: Kklab - real(8), intent(in), dimension(0:nrho-1,0:ncomp-1,0:ncomp-1) :: Iij - integer, intent(in) :: maxiter - real(8), intent(in) :: tol - real(8), intent(out), dimension(0:nrho-1,0:nind-1) :: Xika_final - real(8), dimension(0:nind-1) :: Xika,obj_func,Xika0 - integer :: i,iter,j - - - Xika=Xika_init - - do i=0, nrho-1 - !write(*,*) Xika0 - do iter=0, maxiter-1 - Xika0=Xika - call calc_Xika_inner_4(Xika0,indices,xi,rho(i),nui,nk,Fklab,Kklab,Iij(i,:,:),obj_func,Xika,ncomp,nbeads,nsitemax,nind) - !write(*,*) iter, Xika - if(maxval(dabs(obj_func)).lt.tol) exit - enddo - - do j=0, nind-1 - Xika_final(i,j)=Xika(j) - enddo - !write(*,*) i,iter,Xika,obj_func - end do - !write(*,*) i,rho(i),Xika,Xika_final(i,:,:,:) -end subroutine - -subroutine calc_Xika_inner_6(Xika0,indices,xi,rho,nui,nk,Fklab,Kklab,Iij,obj_func,Xika,ncomp,nbeads,nsitemax,nind) - implicit none - integer,intent(in) :: ncomp, nbeads, nsitemax, nind - real(8), intent(in), dimension(0:nind-1) :: Xika0 - real(8), intent(in), dimension(0:nind-1, 0:2) :: indices - real(8), intent(in), dimension(0:ncomp-1) :: xi - real(8), intent(in), dimension(0:ncomp-1,0:nbeads-1) :: nui - real(8), intent(in), dimension(0:nbeads-1,0:nsitemax-1) :: nk - real(8), intent(in), dimension(0:nbeads-1,0:nbeads-1,0:nsitemax-1,0:nsitemax-1) :: Fklab - real(8), intent(in), dimension(0:ncomp-1,0:ncomp-1,0:nbeads-1,0:nbeads-1,0:nsitemax-1,0:nsitemax-1) :: Kklab - real(8), intent(in), dimension(0:ncomp-1,0:ncomp-1) :: Iij - real(8), intent(in) :: rho - real(8), intent(out), dimension(0:nind-1) :: obj_func - real(8), intent(out), dimension(0:nind-1) :: Xika - integer :: ii,jj,i,k,a,j,l,b - - !initialize Xika to 1.0 - Xika=1.0 - - do ii=0, nind-1 - i = indices(ii,0) - k = indices(ii,1) - a = indices(ii,2) - do jj=0, nind-1 - j = indices(jj,0) - l = indices(jj,1) - b = indices(jj,2) - Xika(ii)=Xika(ii) + (rho * xi(j) * nui(j,l) * nk(l,b) * Xika0(jj) * Fklab(k,l,a,b) * Kklab(i,j,k,l,a,b) * Iij(i,j)) - enddo - enddo - Xika = (1.0/Xika) - - obj_func = Xika - Xika0 - -end subroutine - - -subroutine calc_Xika_6(indices,rho,Xika_init,xi,nui,nk,Fklab,Kklab,Iij,maxiter,tol,Xika_final,nrho,ncomp,nbeads,nsitemax,nind) - implicit none - integer, intent(in) :: nrho, ncomp, nbeads, nsitemax, nind - real(8), intent(in), dimension(0:nind-1, 0:2) :: indices - real(8), intent(in), dimension(0:nrho-1) :: rho - real(8), intent(in), dimension(0:nind-1) :: Xika_init - real(8), intent(in), dimension(0:ncomp-1) :: xi - real(8), intent(in), dimension(0:ncomp-1,0:nbeads-1) :: nui - real(8), intent(in), dimension(0:nbeads-1,0:nsitemax-1) :: nk - real(8), intent(in), dimension(0:nbeads-1,0:nbeads-1,0:nsitemax-1,0:nsitemax-1) :: Fklab - real(8), intent(in), dimension(0:ncomp-1,0:ncomp-1,0:nbeads-1,0:nbeads-1,0:nsitemax-1,0:nsitemax-1) :: Kklab - real(8), intent(in), dimension(0:nrho-1,0:ncomp-1,0:ncomp-1) :: Iij - integer, intent(in) :: maxiter - real(8), intent(in) :: tol - real(8), intent(out), dimension(0:nrho-1,0:nind-1) :: Xika_final - real(8), dimension(0:nind-1) :: Xika,obj_func,Xika0 - integer :: i,iter,j - - - Xika=Xika_init - - do i=0, nrho-1 - !write(*,*) Xika0 - do iter=0, maxiter-1 - Xika0=Xika - call calc_Xika_inner_6(Xika0,indices,xi,rho(i),nui,nk,Fklab,Kklab,Iij(i,:,:),obj_func,Xika,ncomp,nbeads,nsitemax,nind) - if(maxval(dabs(obj_func)).lt.tol) exit - enddo - - do j=0, nind-1 - Xika_final(i,j)=Xika(j) - enddo - !write(*,*) i,iter,Xika,obj_func - end do - !write(*,*) i,rho(i),Xika,Xika_final(i,:,:,:) -end subroutine - diff --git a/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_numba.py b/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_numba.py index d2923af..c4d47b8 100644 --- a/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_numba.py +++ b/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_numba.py @@ -5,41 +5,50 @@ def calc_Xika(indices, rho, xi, molecular_composition, nk, Fklab, Kklab, gr_assoc): - r""" - A wrapper to calculate the fraction of molecules of component i that are not bonded at a site of type a on group k. Switched between functions for different Kklab + r""" + A wrapper to calculate the fraction of molecules of component i that are not + bonded at a site of type a on group k. Switched between functions for different + Kklab Parameters ---------- indices : list[list] - A list of sets of (component, bead, site) to identify the values of the Xika matrix that are being fit + A list of sets of (component, bead, site) to identify the values of the Xika + matrix that are being fit rho : numpy.ndarray Number density of system [mol/m^3] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 molecular_composition : numpy.array - :math:`\nu_{i,k}/k_B`, Array of number of components by number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`, Array of number of components by number of bead types. + Defines the number of each type of group in each component. nk : numpy.ndarray For each bead the number of each type of site Fklab : numpy.ndarray - The association strength between a site of type a on a group of type k of component i and a site of type b on a group of type l of component j., known as the Mayer f-function. + The association strength between a site of type a on a group of type k of + component i and a site of type b on a group of type l of component j., known + as the Mayer f-function. Kklab : numpy.ndarray Bonding volume between each association site gr_assoc : numpy.ndarray - Reference fluid pair correlation function used in calculating association sites, (len(rho) x Ncomp x Ncomp) + Reference fluid pair correlation function used in calculating association + sites, (len(rho) x Ncomp x Ncomp) Returns ------- Xika : numpy.ndarray - The fraction of molecules of component i that are not bonded at a site of type a on group k. Matrix (len(rho) x Ncomp x Nbeads x len(sitenames)) + The fraction of molecules of component i that are not bonded at a site of + type a on group k. Matrix (len(rho) x Ncomp x Nbeads x len(sitenames)) err_array : numpy.ndarray - Of the same length of rho, is a list in the error of the total error Xika for each point. + Of the same length of rho, is a list in the error of the total error Xika for + each point. """ l_K = len(np.shape(Kklab)) # Ensure all inputs are numpy arrays tmp_array = [rho, xi, molecular_composition, nk, Fklab, Kklab, gr_assoc] - for i,tmp in enumerate(tmp_array): + for i, tmp in enumerate(tmp_array): if np.shape(tmp): tmp_array[i] = np.array(tmp) else: @@ -58,7 +67,7 @@ def calc_Xika(indices, rho, xi, molecular_composition, nk, Fklab, Kklab, gr_asso return Xika_final, err_array -#@numba.njit( +# @numba.njit( # numba.types.Tuple((numba.f8[:, :], numba.f8[:]))( # numba.i8[:, :], # numba.f8[:], @@ -69,39 +78,47 @@ def calc_Xika(indices, rho, xi, molecular_composition, nk, Fklab, Kklab, gr_asso # numba.f8[:, :, :, :], # numba.f8[:, :, :], # ) -#) +# ) @numba.njit() def calc_Xika_4( indices, rho, xi, molecular_composition, nk, Fklab, Kklab, gr_assoc ): # , maxiter=500, tol=1e-12, damp=.1 - r""" - Calculate the fraction of molecules of component i that are not bonded at a site of type a on group k. + r""" + Calculate the fraction of molecules of component i that are not bonded at a + site of type a on group k. Parameters ---------- indices : list[list] - A list of sets of (component, bead, site) to identify the values of the Xika matrix that are being fit + A list of sets of (component, bead, site) to identify the values of the Xika + matrix that are being fit rho : numpy.ndarray Number density of system [mol/m^3] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 molecular_composition : numpy.array - :math:`\nu_{i,k}/k_B`, Array of number of components by number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`, Array of number of components by number of bead types. + Defines the number of each type of group in each component. nk : numpy.ndarray For each bead the number of each type of site Fklab : numpy.ndarray - The association strength between a site of type a on a group of type k of component i and a site of type b on a group of type l of component j., known as the Mayer f-function. + The association strength between a site of type a on a group of type k of + component i and a site of type b on a group of type l of component j., known + as the Mayer f-function. Kklab : numpy.ndarray Bonding volume between each association site gr_assoc : numpy.ndarray - Reference fluid pair correlation function used in calculating association sites, (len(rho) x Ncomp x Ncomp) + Reference fluid pair correlation function used in calculating association + sites, (len(rho) x Ncomp x Ncomp) Returns ------- Xika : numpy.ndarray - The fraction of molecules of component i that are not bonded at a site of type a on group k. Matrix (len(rho) x Ncomp x Nbeads x len(sitenames)) + The fraction of molecules of component i that are not bonded at a site of + type a on group k. Matrix (len(rho) x Ncomp x Nbeads x len(sitenames)) err_array : numpy.ndarray - Of the same length of rho, is a list in the error of the total error Xika for each point. + Of the same length of rho, is a list in the error of the total error Xika + for each point. """ maxiter = 500 @@ -139,6 +156,7 @@ def calc_Xika_4( ) jnd += 1 ind += 1 + Xika_elements_new = 1.0 / Xika_elements_new obj = np.sum(np.abs(Xika_elements_new - Xika_elements)) @@ -153,16 +171,12 @@ def calc_Xika_4( Xika_elements = Xika_elements_new err_array[r] = obj - Xika_final[r, :] = Xika_elements - # for jjnd in range(l_ind): - # i,k,a = indices[jjnd] - # Xika_final[r,i,k,a] = Xika_elements[jjnd] return Xika_final, err_array -#@numba.njit( +# @numba.njit( # numba.types.Tuple((numba.f8[:, :], numba.f8[:]))( # numba.i8[:, :], # numba.f8[:], @@ -173,39 +187,47 @@ def calc_Xika_4( # numba.f8[:, :, :, :, :, :], # numba.f8[:, :, :], # ) -#) +# ) @numba.njit() def calc_Xika_6( indices, rho, xi, molecular_composition, nk, Fklab, Kklab, gr_assoc ): # , maxiter=500, tol=1e-12, damp=.1 - r""" - Calculate the fraction of molecules of component i that are not bonded at a site of type a on group k. + r""" + Calculate the fraction of molecules of component i that are not bonded at a site + of type a on group k. Parameters ---------- indices : list[list] - A list of sets of (component, bead, site) to identify the values of the Xika matrix that are being fit + A list of sets of (component, bead, site) to identify the values of the Xika + matrix that are being fit rho : numpy.ndarray Number density of system [mol/m^3] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 molecular_composition : numpy.array - :math:`\nu_{i,k}/k_B`, Array of number of components by number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`, Array of number of components by number of bead types. + Defines the number of each type of group in each component. nk : numpy.ndarray For each bead the number of each type of site Fklab : numpy.ndarray - The association strength between a site of type a on a group of type k of component i and a site of type b on a group of type l of component j., known as the Mayer f-function. + The association strength between a site of type a on a group of type k of + component i and a site of type b on a group of type l of component j., known + as the Mayer f-function. Kklab : numpy.ndarray Bonding volume between each association site gr_assoc : numpy.ndarray - Reference fluid pair correlation function used in calculating association sites, (len(rho) x Ncomp x Ncomp) + Reference fluid pair correlation function used in calculating association + sites, (len(rho) x Ncomp x Ncomp) Returns ------- Xika : numpy.ndarray - The fraction of molecules of component i that are not bonded at a site of type a on group k. Matrix (len(rho) x Ncomp x Nbeads x len(sitenames)) + The fraction of molecules of component i that are not bonded at a site of type + a on group k. Matrix (len(rho) x Ncomp x Nbeads x len(sitenames)) err_array : numpy.ndarray - Of the same length of rho, is a list in the error of the total error Xika for each point. + Of the same length of rho, is a list in the error of the total error Xika for + each point. """ maxiter = 500 @@ -245,6 +267,7 @@ def calc_Xika_6( ) jnd += 1 ind += 1 + Xika_elements_new = 1.0 / Xika_elements_new obj = np.sum(np.abs(Xika_elements_new - Xika_elements)) @@ -259,10 +282,6 @@ def calc_Xika_6( Xika_elements = Xika_elements_new err_array[r] = obj - Xika_final[r, :] = Xika_elements - # for jjnd in range(l_ind): - # i,k,a = indices[jjnd] - # Xika_final[r,i,k,a] = Xika_elements[jjnd] return Xika_final, err_array diff --git a/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_python.py b/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_python.py index 8cc3718..e808c05 100644 --- a/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_python.py +++ b/despasito/equations_of_state/saft/compiled_modules/ext_Aassoc_python.py @@ -21,25 +21,31 @@ def calc_Xika( tol=1e-12, damp=0.1, ): - r""" - Calculate the fraction of molecules of component i that are not bonded at a site of type a on group k. + r""" + Calculate the fraction of molecules of component i that are not bonded at a site + of type a on group k. Parameters ---------- indices : list[list] - A list of sets of (component, bead, site) to identify the values of the Xika matrix that are being fit + A list of sets of (component, bead, site) to identify the values of the Xika + matrix that are being fit rho : numpy.ndarray Number density of system [mol/m^3] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 molecular_composition : numpy.array - :math:`\nu_{i,k}/k_B`, Array of number of components by number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`, Array of number of components by number of bead types. + Defines the number of each type of group in each component. nk : numpy.ndarray For each bead the number of each type of site Fklab : numpy.ndarray - The association strength between a site of type a on a group of type k of component i and a site of type b on a group of type l of component j., known as the Mayer f-function. + The association strength between a site of type a on a group of type k of + component i and a site of type b on a group of type l of component j., known + as the Mayer f-function. gr_assoc : numpy.ndarray - Reference fluid pair correlation function used in calculating association sites, (len(rho) x Ncomp x Ncomp) + Reference fluid pair correlation function used in calculating association + sites, (len(rho) x Ncomp x Ncomp) maxiter : int, Optional, default=500 Maximum number of iteration for minimization tol : float, Optional, default=1e-12 @@ -50,9 +56,11 @@ def calc_Xika( Returns ------- Xika : numpy.ndarray - The fraction of molecules of component i that are not bonded at a site of type a on group k. Matrix (len(rho) x Ncomp x Nbeads x len(sitenames)) + The fraction of molecules of component i that are not bonded at a site of type + a on group k. Matrix (len(rho) x Ncomp x Nbeads x len(sitenames)) err_array : numpy.ndarray - Of the same length of rho, is a list in the error of the total error Xika for each point. + Of the same length of rho, is a list in the error of the total error Xika for + each point. """ # ncomp, nbeads = np.shape(molecular_composition) @@ -84,7 +92,7 @@ def calc_Xika( * Kklab[i, j, k, l, a, b] * gr_assoc[r, i, j] ) - + Xika_elements_new[ind] += ( constants.molecule_per_nm3 * rho[r] diff --git a/despasito/equations_of_state/saft/compiled_modules/ext_gamma_mie_numba.py b/despasito/equations_of_state/saft/compiled_modules/ext_gamma_mie_numba.py index 3516f50..51ffda2 100644 --- a/despasito/equations_of_state/saft/compiled_modules/ext_gamma_mie_numba.py +++ b/despasito/equations_of_state/saft/compiled_modules/ext_gamma_mie_numba.py @@ -21,14 +21,14 @@ def prefactor_1d(l_r, l_a): r""" Calculations C, the Mie potential prefactor, defined in eq. 2 - + Parameters ---------- l_a : float Mie potential attractive exponent l_r : float Mie potential attractive exponent - + Returns ------- C : float @@ -51,21 +51,24 @@ def prefactor_1d(l_r, l_a): ) ) def calc_a1s(rho, Cmol2seg, l_kl, zetax, epsilonkl, dkl): - r""" + r""" Return a1s,kl(rho*Cmol2seg,l_kl) in K as defined in eq. 25. - - Used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy. + + Used in the calculation of :math:`A_1` the first order term of the perturbation + expansion corresponding to the mean-attractive energy. Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 l_kl : numpy.ndarray Matrix of mie potential exponents for k,l groups zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter for groups + (k,l) epsilonkl : numpy.ndarray Matrix of well depths for groups (k,l) dkl : numpy.ndarray @@ -74,7 +77,9 @@ def calc_a1s(rho, Cmol2seg, l_kl, zetax, epsilonkl, dkl): Returns ------- a1s : numpy.ndarray - Matrix used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy, size is the Nrho x Ngroups x Ngroups + Matrix used in the calculation of :math:`A_1` the first order term of the + perturbation expansion corresponding to the mean-attractive energy, size is the + Nrho x Ngroups x Ngroups """ nbeads = len(dkl) @@ -107,7 +112,7 @@ def calc_a1s(rho, Cmol2seg, l_kl, zetax, epsilonkl, dkl): * 2.0 * np.pi * Cmol2seg - * ((epsilonkl * (dkl ** 3 * constants.molecule_per_nm3 ** 2)) / (l_kl - 3.0)) + * ((epsilonkl * (dkl**3 * constants.molecule_per_nm3**2)) / (l_kl - 3.0)) ) output = np.transpose(np.transpose(a1s) * rho) @@ -129,9 +134,10 @@ def calc_a1s(rho, Cmol2seg, l_kl, zetax, epsilonkl, dkl): def calc_Bkl(rho, l_kl, Cmol2seg, dkl, epsilonkl, x0kl, zetax): r""" Return Bkl(rho*Cmol2seg,l_kl) in K as defined in eq. 20. - - Used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy. - + + Used in the calculation of :math:`A_1` the first order term of the perturbation + expansion corresponding to the mean-attractive energy. + Parameters ---------- rho : numpy.ndarray @@ -139,21 +145,26 @@ def calc_Bkl(rho, l_kl, Cmol2seg, dkl, epsilonkl, x0kl, zetax): l_kl : numpy.ndarray :math:`\lambda_{k,l}` Matrix of Mie potential exponents for k,l groups Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 dkl : numpy.ndarray Matrix of hard sphere diameters for groups (k,l) epsilonkl : numpy.ndarray Matrix of well depths for groups (k,l) x0kl : numpy.ndarray - Matrix of sigmakl/dkl, ratio of Mie radius for groups scaled by hard sphere interaction (k,l) + Matrix of sigmakl/dkl, ratio of Mie radius for groups scaled by hard sphere + interaction (k,l) zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- Bkl : numpy.ndarray - Matrix used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy, size is rho x l_kl.shape - + Matrix used in the calculation of :math:`A_1` the first order term of the + perturbation expansion corresponding to the mean-attractive energy, size is rho + x l_kl.shape + """ rhos = Cmol2seg * rho @@ -168,7 +179,7 @@ def calc_Bkl(rho, l_kl, Cmol2seg, dkl, epsilonkl, x0kl, zetax): ) / ((l_kl - 3.0) * (l_kl - 4.0)) tmp11 = rhos * (2.0 * np.pi) - tmp12 = (dkl ** 3 * constants.molecule_per_nm3 ** 2) * epsilonkl + tmp12 = (dkl**3 * constants.molecule_per_nm3**2) * epsilonkl tmp2 = (1.0 - (zetax / 2.0)) / ((1.0 - zetax) ** 3) tmp3 = (9.0 * zetax * (1.0 + zetax)) / (2.0 * ((1 - zetax) ** 3)) @@ -196,32 +207,39 @@ def calc_Bkl(rho, l_kl, Cmol2seg, dkl, epsilonkl, x0kl, zetax): def calc_a1ii(rho, Cmol2seg, dkl, l_akl, l_rkl, x0kl, epsilonkl, zetax): r""" Calculate effective first-order perturbation term :math:`\bar{a}_{1,ii}`. - - Used for the contribution of the monomeric interactions to the free energy per segment. - + + Used for the contribution of the monomeric interactions to the free energy per + segment. + Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 dkl : numpy.ndarray Matrix of hard sphere diameters for groups (k,l) l_rkl : numpy.ndarray - :math:`\lambda_{k,l}` Matrix of Mie potential repulsive exponents for k,l groups + :math:`\lambda_{k,l}` Matrix of Mie potential repulsive exponents for + k,l groups l_akl : numpy.ndarray - :math:`\lambda_{k,l}` Matrix of Mie potential attractive exponents for k,l groups + :math:`\lambda_{k,l}` Matrix of Mie potential attractive exponents for + k,l groups x0kl : numpy.ndarray - Matrix of sigmakl/dkl, ratio of Mie radius for groups scaled by hard sphere interaction (k,l) + Matrix of sigmakl/dkl, ratio of Mie radius for groups scaled by hard sphere + interaction (k,l) epsilonkl : numpy.ndarray Matrix of well depths for groups (k,l) zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- a1ii : numpy.ndarray - Matrix used in the calculation of the radial distribution function of a hypothetical one-fluid Mie system. + Matrix used in the calculation of the radial distribution function of a + hypothetical one-fluid Mie system. """ Cii = (l_rkl / (l_rkl - l_akl)) * (l_rkl / l_akl) ** (l_akl / (l_rkl - l_akl)) @@ -231,8 +249,8 @@ def calc_a1ii(rho, Cmol2seg, dkl, l_akl, l_rkl, x0kl, epsilonkl, zetax): a1s_a = calc_a1s(rho, Cmol2seg, l_akl, zetax, epsilonkl, dkl) return Cii * ( - ((x0kl ** l_akl) / constants.molecule_per_nm3 * (a1s_a + Bii_a)) - - ((x0kl ** l_rkl) / constants.molecule_per_nm3 * (a1s_r + Bii_r)) + ((x0kl**l_akl) / constants.molecule_per_nm3 * (a1s_a + Bii_a)) + - ((x0kl**l_rkl) / constants.molecule_per_nm3 * (a1s_r + Bii_r)) ) @@ -244,26 +262,31 @@ def calc_a1ii(rho, Cmol2seg, dkl, l_akl, l_rkl, x0kl, epsilonkl, zetax): def calc_a1s_eff(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): r""" Return a1s,(rho*Cmol2seg,l_ij) in K - + Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 l_ii_avg : numpy.ndarray Average bead (i.e. group or segment) exponent in component (i.e. molecule) i. zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. dii_avg : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. - + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. + Returns ------- a1s_eff : numpy.ndarray - Term used in the used in the calculation of the effective averaged molecular radial distribution function of a hypothetical one-fluid Mie system. + Term used in the used in the calculation of the effective averaged molecular + radial distribution function of a hypothetical one-fluid Mie system. """ ncomp = len(dii_avg) @@ -297,7 +320,7 @@ def calc_a1s_eff(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): * np.pi * Cmol2seg * ( - (epsilonii_avg * (dii_avg ** 3 * constants.molecule_per_nm3 ** 2)) + (epsilonii_avg * (dii_avg**3 * constants.molecule_per_nm3**2)) / (l_ii_avg - 3.0) ) ) @@ -321,7 +344,7 @@ def calc_a1s_eff(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): def calc_Bkl_eff(rho, l_ii_avg, Cmol2seg, dii_avg, epsilonii_avg, x0ii, zetax): r""" Return Bii_avg(rho*Cmol2seg,l_ii_avg) in K as defined in eq. 20. - + Parameters ---------- rho : numpy.ndarray @@ -329,21 +352,25 @@ def calc_Bkl_eff(rho, l_ii_avg, Cmol2seg, dii_avg, epsilonii_avg, x0ii, zetax): l_ii_avg : numpy.ndarray Average bead (i.e. group or segment) exponent in component (i.e. molecule) i. Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 dii_avg : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. x0ii : numpy.ndarray Matrix of sigmaii_avg/dii_eff zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- Bii_avg : numpy.ndarray Bii_avg(rho*Cmol2seg,l_ii_avg) in K as defined in eq. 20. - + """ rhos = Cmol2seg * rho @@ -359,7 +386,7 @@ def calc_Bkl_eff(rho, l_ii_avg, Cmol2seg, dii_avg, epsilonii_avg, x0ii, zetax): ) / ((l_ii_avg - 3.0) * (l_ii_avg - 4.0)) tmp11 = rhos * (2.0 * np.pi) - tmp12 = (dii_avg ** 3 * constants.molecule_per_nm3 ** 2) * epsilonii_avg + tmp12 = (dii_avg**3 * constants.molecule_per_nm3**2) * epsilonii_avg tmp2 = (1.0 - (zetax / 2.0)) / ((1.0 - zetax) ** 3) tmp3 = (9.0 * zetax * (1.0 + zetax)) / (2.0 * ((1 - zetax) ** 3)) @@ -377,30 +404,36 @@ def calc_Bkl_eff(rho, l_ii_avg, Cmol2seg, dii_avg, epsilonii_avg, x0ii, zetax): ) ) def calc_da1sii_drhos(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): - r""" + r""" Return a1s,ii_avg(rho*Cmol2seg,l_ii_avg) in K as defined in eq. 25. - - Used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy. + + Used in the calculation of :math:`A_1` the first order term of the perturbation + expansion corresponding to the mean-attractive energy. Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 l_ii_avg : numpy.ndarray Average bead (i.e. group or segment) exponent in component (i.e. molecule) i. zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. dii_avg : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. Returns ------- calc_da1sii_drhos : numpy.ndarray - Matrix used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy + Matrix used in the calculation of :math:`A_1` the first order term of the + perturbation expansion corresponding to the mean-attractive energy """ ncomp = len(dii_avg) zetax_pow = np.zeros((len(rho), 4), dtype=rho.dtype) @@ -437,7 +470,7 @@ def calc_da1sii_drhos(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): -2.0 * np.pi * ( - (epsilonii_avg * (dii_avg ** 3 * constants.molecule_per_nm3 ** 2)) + (epsilonii_avg * (dii_avg**3 * constants.molecule_per_nm3**2)) / (l_ii_avg - 3.0) ) ) @@ -453,27 +486,34 @@ def calc_da1sii_drhos(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): def calc_dBkl_drhos(l_ii_avg, dii_avg, epsilonii_avg, x0ii, zetax): r""" Return derivative of Bkl(rho*Cmol2seg,l_ii_avg) with respect to :math:`\rho_S`. - - Used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy. - + + Used in the calculation of :math:`A_1` the first order term of the perturbation + expansion corresponding to the mean-attractive energy. + Parameters ---------- l_aii_avg : numpy.ndarray - Average bead (i.e. group or segment) attractive exponent in component (i.e. molecule) i. + Average bead (i.e. group or segment) attractive exponent in component (i.e. + molecule) i. dii_avg : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. x0ii : numpy.ndarray Matrix of sigmaii_avg/dii_eff zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- dBkl_drhos : numpy.ndarray - Matrix used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy, size is rho x l_ii_avg.shape - + Matrix used in the calculation of :math:`A_1` the first order term of the + perturbation expansion corresponding to the mean-attractive energy, size is + rho x l_ii_avg.shape + """ ncomp = len(dii_avg) nrho = len(zetax) @@ -487,7 +527,7 @@ def calc_dBkl_drhos(l_ii_avg, dii_avg, epsilonii_avg, x0ii, zetax): + ((x0ii ** (3.0 - l_ii_avg)) * (l_ii_avg - 4.0)) ) / ((l_ii_avg - 3.0) * (l_ii_avg - 4.0)) - tmp = 2.0 * np.pi * dii_avg ** 3 * epsilonii_avg + tmp = 2.0 * np.pi * dii_avg**3 * epsilonii_avg tmp1 = np.zeros((nrho, ncomp)) tmp2 = np.zeros((nrho, ncomp)) for k in np.arange(ncomp): @@ -497,13 +537,10 @@ def calc_dBkl_drhos(l_ii_avg, dii_avg, epsilonii_avg, x0ii, zetax): tmp2[:, k] = ( (5.0 - 2.0 * zetax) * zetax / (2 * (1.0 - zetax) ** 4) * Iii_avg[k] ) - ( - ( - (9.0 * zetax * (zetax ** 2 + 4.0 * zetax + 1)) - / (2.0 * ((1 - zetax) ** 4)) - ) + ((9.0 * zetax * (zetax**2 + 4.0 * zetax + 1)) / (2.0 * ((1 - zetax) ** 4))) * Jii_avg[k] ) - dBkl_drhos = tmp * (tmp1 + tmp2) * constants.molecule_per_nm3 ** 2 + dBkl_drhos = tmp * (tmp1 + tmp2) * constants.molecule_per_nm3**2 return dBkl_drhos @@ -523,29 +560,35 @@ def calc_dBkl_drhos(l_ii_avg, dii_avg, epsilonii_avg, x0ii, zetax): def calc_da1iidrhos( rho, Cmol2seg, dii_eff, l_aii_avg, l_rii_avg, x0ii, epsilonii_avg, zetax ): - r""" - Compute derivative of the term, :math:`\bar{a}_{1,ii}` with respect to :math:`\rho_s` - + Compute derivative of the term, :math:`\bar{a}_{1,ii}` with respect to + :math:`\rho_s` + Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 dii_eff : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. l_aii_avg : numpy.ndarray - Average bead (i.e. group or segment) attractive exponent in component (i.e. molecule) i. + Average bead (i.e. group or segment) attractive exponent in component (i.e. + molecule) i. l_rii_avg : numpy.ndarray - Average bead (i.e. group or segment) attractive exponent in component (i.e. molecule) i. + Average bead (i.e. group or segment) attractive exponent in component (i.e. + molecule) i. x0ii : numpy.ndarray Matrix of sigmaii_avg/dii_eff epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- da1iidrhos : numpy.ndarray @@ -565,8 +608,8 @@ def calc_da1iidrhos( dB_drhos_r = calc_dBkl_drhos(l_rii_avg, dii_eff, epsilonii_avg, x0ii, zetax) da1iidrhos = Cii * ( - ((x0ii ** l_aii_avg) * (das1_drhos_a + dB_drhos_a)) - - ((x0ii ** l_rii_avg) * (das1_drhos_r + dB_drhos_r)) + ((x0ii**l_aii_avg) * (das1_drhos_a + dB_drhos_a)) + - ((x0ii**l_rii_avg) * (das1_drhos_r + dB_drhos_r)) ) return da1iidrhos @@ -587,43 +630,50 @@ def calc_da1iidrhos( def calc_da2ii_1pchi_drhos( rho, Cmol2seg, epsilonii_avg, dii_eff, x0ii, l_rii_avg, l_aii_avg, zetax ): - r""" - Compute derivative of the term, :math:`\frac{\bar{a}_{2,ii}}{1+\bar{\chi}_{ii}}` with respect to :math:`\rho_s`. - + Compute derivative of the term, :math:`\frac{\bar{a}_{2,ii}}{1+\bar{\chi}_{ii}}` + with respect to :math:`\rho_s`. + Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. dii_eff : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. x0ii : numpy.ndarray Matrix of sigmaii_avg/dii_eff l_rii_avg : numpy.ndarray - Average bead (i.e. group or segment) attractive exponent in component (i.e. molecule) i. + Average bead (i.e. group or segment) attractive exponent in component (i.e. + molecule) i. l_aii_avg : numpy.ndarray - Average bead (i.e. group or segment) attractive exponent in component (i.e. molecule) i. + Average bead (i.e. group or segment) attractive exponent in component (i.e. + molecule) i. zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- da2ii_1pchi_drhos : numpy.ndarray - Term used in the calculation of the second-order term from the macroscopic compressibility - + Term used in the calculation of the second-order term from the macroscopic + compressibility + """ # Calculate terms and derivatives used in derivative chain rule KHS = ((1.0 - zetax) ** 4) / ( - 1.0 + (4.0 * zetax) + (4.0 * (zetax ** 2)) - (4.0 * (zetax ** 3)) + (zetax ** 4) + 1.0 + (4.0 * zetax) + (4.0 * (zetax**2)) - (4.0 * (zetax**3)) + (zetax**4) ) dKHS_drhos = ( - (4.0 * (zetax ** 2 - 5.0 * zetax - 2.0) * (1.0 - zetax) ** 3) - / (zetax ** 4 - 4.0 * zetax ** 3 + 4.0 * zetax ** 2 + 4.0 * zetax + 1.0) ** 2 + (4.0 * (zetax**2 - 5.0 * zetax - 2.0) * (1.0 - zetax) ** 3) + / (zetax**4 - 4.0 * zetax**3 + 4.0 * zetax**2 + 4.0 * zetax + 1.0) ** 2 * (zetax / (rho * Cmol2seg)) ) @@ -678,7 +728,7 @@ def calc_da2ii_1pchi_drhos( + x0ii ** (2.0 * l_rii_avg) * (a1sii_2l_rii_avg + Bii_2l_rii_avg) ) - dA_B = np.transpose(np.transpose(0.5 * epsilonii_avg * Cii ** 2 * B) * dKHS_drhos) + dA_B = np.transpose(np.transpose(0.5 * epsilonii_avg * Cii**2 * B) * dKHS_drhos) dB = ( x0ii ** (2.0 * l_aii_avg) * (da1sii_2l_aii_avg + dBii_2l_aii_avg) @@ -688,7 +738,7 @@ def calc_da2ii_1pchi_drhos( + x0ii ** (2.0 * l_rii_avg) * (da1sii_2l_rii_avg + dBii_2l_rii_avg) ) - A_dB = np.transpose(np.transpose(0.5 * epsilonii_avg * Cii ** 2 * dB) * KHS) + A_dB = np.transpose(np.transpose(0.5 * epsilonii_avg * Cii**2 * dB) * KHS) da2ii_1pchi_drhos = A_dB + dA_B diff --git a/despasito/equations_of_state/saft/compiled_modules/ext_gamma_mie_python.py b/despasito/equations_of_state/saft/compiled_modules/ext_gamma_mie_python.py index 86eee9c..68425e9 100644 --- a/despasito/equations_of_state/saft/compiled_modules/ext_gamma_mie_python.py +++ b/despasito/equations_of_state/saft/compiled_modules/ext_gamma_mie_python.py @@ -1,8 +1,8 @@ r""" Routines for calculating the Helmholtz energy for the SAFT-gamma equation of state. - - Equations referenced in this code are from V. Papaioannou et al. J. Chem. Phys. 140 054107 2014 - + + Equations referenced in this code are from V. Papaioannou et al. J. Chem. Phys. + 140 054107 2014 """ import numpy as np @@ -25,14 +25,14 @@ def prefactor(l_r, l_a): r""" Calculations C, the Mie potential prefactor, defined in eq. 2 - + Parameters ---------- l_a : float Mie potential attractive exponent l_r : float Mie potential attractive exponent - + Returns ------- C : float @@ -45,21 +45,24 @@ def prefactor(l_r, l_a): def calc_a1s(rho, Cmol2seg, l_kl, zetax, epsilonkl, dkl): - r""" + r""" Return a1s,kl(rho*Cmol2seg,l_kl) in K as defined in eq. 25. - - Used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy. + + Used in the calculation of :math:`A_1` the first order term of the perturbation + expansion corresponding to the mean-attractive energy. Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 l_kl : numpy.ndarray Matrix of Mie potential exponents for k,l groups zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) epsilonkl : numpy.ndarray Matrix of well depths for groups (k,l) dkl : numpy.ndarray @@ -68,7 +71,9 @@ def calc_a1s(rho, Cmol2seg, l_kl, zetax, epsilonkl, dkl): Returns ------- a1s : numpy.ndarray - Matrix used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy, size is the Nrho x Ngroups x Ngroups + Matrix used in the calculation of :math:`A_1` the first order term of the + perturbation expansion corresponding to the mean-attractive energy, size is the + Nrho x Ngroups x Ngroups """ @@ -101,7 +106,7 @@ def calc_a1s(rho, Cmol2seg, l_kl, zetax, epsilonkl, dkl): * 2.0 * np.pi * Cmol2seg - * ((epsilonkl * (dkl ** 3 * constants.molecule_per_nm3 ** 2)) / (l_kl - 3.0)) + * ((epsilonkl * (dkl**3 * constants.molecule_per_nm3**2)) / (l_kl - 3.0)) ) output = np.transpose(np.transpose(a1s) * rho) @@ -112,9 +117,10 @@ def calc_a1s(rho, Cmol2seg, l_kl, zetax, epsilonkl, dkl): def calc_Bkl(rho, l_kl, Cmol2seg, dkl, epsilonkl, x0kl, zetax): r""" Return Bkl(rho*Cmol2seg,l_kl) in K as defined in eq. 20. - - Used in the calculation of the first order term of the perturbation expansion corresponding to the mean-attractive energy. - + + Used in the calculation of the first order term of the perturbation expansion + corresponding to the mean-attractive energy. + Parameters ---------- rho : numpy.ndarray @@ -122,21 +128,26 @@ def calc_Bkl(rho, l_kl, Cmol2seg, dkl, epsilonkl, x0kl, zetax): l_kl : numpy.ndarray :math:`\lambda_{k,l}` Matrix of Mie potential exponents for k,l groups Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 dkl : numpy.ndarray Matrix of hard sphere diameters for groups (k,l) epsilonkl : numpy.ndarray Matrix of well depths for groups (k,l) x0kl : numpy.ndarray - Matrix of sigmakl/dkl, ratio of Mie radius for groups scaled by hard sphere interaction (k,l) + Matrix of sigmakl/dkl, ratio of Mie radius for groups scaled by hard sphere + interaction (k,l) zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- Bkl : numpy.ndarray - Matrix used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy, size is rho x l_kl.shape - + Matrix used in the calculation of :math:`A_1` the first order term of the + perturbation expansion corresponding to the mean-attractive energy, size is rho + x l_kl.shape + """ rhos = Cmol2seg * rho @@ -151,7 +162,7 @@ def calc_Bkl(rho, l_kl, Cmol2seg, dkl, epsilonkl, x0kl, zetax): ) / ((l_kl - 3.0) * (l_kl - 4.0)) tmp11 = rhos * (2.0 * np.pi) - tmp12 = (dkl ** 3 * constants.molecule_per_nm3 ** 2) * epsilonkl + tmp12 = (dkl**3 * constants.molecule_per_nm3**2) * epsilonkl tmp2 = (1.0 - (zetax / 2.0)) / ((1.0 - zetax) ** 3) tmp3 = (9.0 * zetax * (1.0 + zetax)) / (2.0 * ((1 - zetax) ** 3)) @@ -167,32 +178,39 @@ def calc_Bkl(rho, l_kl, Cmol2seg, dkl, epsilonkl, x0kl, zetax): def calc_a1ii(rho, Cmol2seg, dkl, l_akl, l_rkl, x0kl, epsilonkl, zetax): r""" Calculate effective first-order perturbation term :math:`\bar{a}_{1,ii}`. - - Used for the contribution of the monomeric interactions to the free energy per segment. - + + Used for the contribution of the monomeric interactions to the free energy per + segment. + Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 dkl : numpy.ndarray Matrix of hard sphere diameters for groups (k,l) l_rkl : numpy.ndarray - :math:`\lambda_{k,l}` Matrix of Mie potential repulsive exponents for k,l groups + :math:`\lambda_{k,l}` Matrix of Mie potential repulsive exponents for + k,l groups l_akl : numpy.ndarray - :math:`\lambda_{k,l}` Matrix of Mie potential attractive exponents for k,l groups + :math:`\lambda_{k,l}` Matrix of Mie potential attractive exponents for + k,l groups x0kl : numpy.ndarray - Matrix of sigmakl/dkl, ratio of Mie radius for groups scaled by hard sphere interaction (k,l) + Matrix of sigmakl/dkl, ratio of Mie radius for groups scaled by hard sphere + interaction (k,l) epsilonkl : numpy.ndarray Matrix of well depths for groups (k,l) zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- a1ii : numpy.ndarray - Matrix used in the calculation of the radial distribution function of a hypothetical one-fluid Mie system. + Matrix used in the calculation of the radial distribution function of a + hypothetical one-fluid Mie system. """ Cii = (l_rkl / (l_rkl - l_akl)) * (l_rkl / l_akl) ** (l_akl / (l_rkl - l_akl)) @@ -202,8 +220,8 @@ def calc_a1ii(rho, Cmol2seg, dkl, l_akl, l_rkl, x0kl, epsilonkl, zetax): a1s_a = calc_a1s(rho, Cmol2seg, l_akl, zetax, epsilonkl, dkl) output = Cii * ( - ((x0kl ** l_akl) / constants.molecule_per_nm3 * (a1s_a + Bii_a)) - - ((x0kl ** l_rkl) / constants.molecule_per_nm3 * (a1s_r + Bii_r)) + ((x0kl**l_akl) / constants.molecule_per_nm3 * (a1s_a + Bii_a)) + - ((x0kl**l_rkl) / constants.molecule_per_nm3 * (a1s_r + Bii_r)) ) return output @@ -212,26 +230,31 @@ def calc_a1ii(rho, Cmol2seg, dkl, l_akl, l_rkl, x0kl, epsilonkl, zetax): def calc_a1s_eff(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): r""" Return a1s,(rho*Cmol2seg,l_ij) in K - + Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to s + egment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 l_ii_avg : numpy.ndarray Average bead (i.e. group or segment) exponent in component (i.e. molecule) i. zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component + (i.e. molecule) i. dii_avg : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. - + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. + Returns ------- a1s_eff : numpy.ndarray - Term used in the used in the calculation of the effective averaged molecular radial distribution function of a hypothetical one-fluid Mie system. + Term used in the used in the calculation of the effective averaged molecular + radial distribution function of a hypothetical one-fluid Mie system. """ ncomp = len(dii_avg) @@ -265,7 +288,7 @@ def calc_a1s_eff(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): * np.pi * Cmol2seg * ( - (epsilonii_avg * (dii_avg ** 3 * constants.molecule_per_nm3 ** 2)) + (epsilonii_avg * (dii_avg**3 * constants.molecule_per_nm3**2)) / (l_ii_avg - 3.0) ) ) @@ -278,7 +301,7 @@ def calc_a1s_eff(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): def calc_Bkl_eff(rho, l_ii_avg, Cmol2seg, dii_avg, epsilonii_avg, x0ii, zetax): r""" Return Bii_avg(rho*Cmol2seg,l_ii_avg) in K as defined in eq. 20. - + Parameters ---------- rho : numpy.ndarray @@ -286,16 +309,20 @@ def calc_Bkl_eff(rho, l_ii_avg, Cmol2seg, dii_avg, epsilonii_avg, x0ii, zetax): l_ii_avg : numpy.ndarray Average bead (i.e. group or segment) exponent in component (i.e. molecule) i. Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 dii_avg : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. x0ii : numpy.ndarray Matrix of sigmaii_avg/dii_eff zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- Bii_avg : numpy.ndarray @@ -316,7 +343,7 @@ def calc_Bkl_eff(rho, l_ii_avg, Cmol2seg, dii_avg, epsilonii_avg, x0ii, zetax): ) / ((l_ii_avg - 3.0) * (l_ii_avg - 4.0)) tmp11 = rhos * (2.0 * np.pi) - tmp12 = (dii_avg ** 3 * constants.molecule_per_nm3 ** 2) * epsilonii_avg + tmp12 = (dii_avg**3 * constants.molecule_per_nm3**2) * epsilonii_avg tmp2 = (1.0 - (zetax / 2.0)) / ((1.0 - zetax) ** 3) tmp3 = (9.0 * zetax * (1.0 + zetax)) / (2.0 * ((1 - zetax) ** 3)) @@ -329,30 +356,37 @@ def calc_Bkl_eff(rho, l_ii_avg, Cmol2seg, dii_avg, epsilonii_avg, x0ii, zetax): def calc_da1sii_drhos(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): - r""" - Return the derivative of a1s,ii_avg(rho*Cmol2seg,l_ii_avg) with represent to number density in Kelvin - - Used in the calculation of the first order term of the perturbation expansion corresponding to the mean-attractive energy. + r""" + Return the derivative of a1s,ii_avg(rho*Cmol2seg,l_ii_avg) with represent to + number density in Kelvin + + Used in the calculation of the first order term of the perturbation expansion + corresponding to the mean-attractive energy. Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 l_ii_avg : numpy.ndarray Average bead (i.e. group or segment) exponent in component (i.e. molecule) i. zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. dii_avg : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. Returns ------- calc_da1sii_drhos : numpy.ndarray - Matrix used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy + Matrix used in the calculation of :math:`A_1` the first order term of the + perturbation expansion corresponding to the mean-attractive energy """ ncomp = len(dii_avg) zetax_pow = np.zeros((len(rho), 4), dtype=rho.dtype) @@ -389,7 +423,7 @@ def calc_da1sii_drhos(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): -2.0 * np.pi * ( - (epsilonii_avg * (dii_avg ** 3 * constants.molecule_per_nm3 ** 2)) + (epsilonii_avg * (dii_avg**3 * constants.molecule_per_nm3**2)) / (l_ii_avg - 3.0) ) ) @@ -402,27 +436,34 @@ def calc_da1sii_drhos(rho, Cmol2seg, l_ii_avg, zetax, epsilonii_avg, dii_avg): def calc_dBkl_drhos(l_ii_avg, dii_avg, epsilonii_avg, x0ii, zetax): r""" Return derivative of Bkl(rho*Cmol2seg,l_ii_avg) with respect to :math:`\rho_S`. - - Used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy. - + + Used in the calculation of :math:`A_1` the first order term of the perturbation + expansion corresponding to the mean-attractive energy. + Parameters ---------- l_aii_avg : numpy.ndarray - Average bead (i.e. group or segment) attractive exponent in component (i.e. molecule) i. + Average bead (i.e. group or segment) attractive exponent in component (i.e. + molecule) i. dii_avg : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. x0ii : numpy.ndarray Matrix of sigmaii_avg/dii_eff zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- dBkl_drhos : numpy.ndarray - Matrix used in the calculation of :math:`A_1` the first order term of the perturbation expansion corresponding to the mean-attractive energy, size is rho x l_ii_avg.shape - + Matrix used in the calculation of :math:`A_1` the first order term of the + perturbation expansion corresponding to the mean-attractive energy, size is + rho x l_ii_avg.shape + """ ncomp = len(dii_avg) nrho = len(zetax) @@ -436,7 +477,7 @@ def calc_dBkl_drhos(l_ii_avg, dii_avg, epsilonii_avg, x0ii, zetax): + ((x0ii ** (3.0 - l_ii_avg)) * (l_ii_avg - 4.0)) ) / ((l_ii_avg - 3.0) * (l_ii_avg - 4.0)) - tmp = 2.0 * np.pi * dii_avg ** 3 * epsilonii_avg + tmp = 2.0 * np.pi * dii_avg**3 * epsilonii_avg tmp1 = np.zeros((nrho, ncomp)) tmp2 = np.zeros((nrho, ncomp)) for k in np.arange(ncomp): @@ -446,13 +487,10 @@ def calc_dBkl_drhos(l_ii_avg, dii_avg, epsilonii_avg, x0ii, zetax): tmp2[:, k] = ( (5.0 - 2.0 * zetax) * zetax / (2 * (1.0 - zetax) ** 4) * Iii_avg[k] ) - ( - ( - (9.0 * zetax * (zetax ** 2 + 4.0 * zetax + 1)) - / (2.0 * ((1 - zetax) ** 4)) - ) + ((9.0 * zetax * (zetax**2 + 4.0 * zetax + 1)) / (2.0 * ((1 - zetax) ** 4))) * Jii_avg[k] ) - dBkl_drhos = tmp * (tmp1 + tmp2) * constants.molecule_per_nm3 ** 2 + dBkl_drhos = tmp * (tmp1 + tmp2) * constants.molecule_per_nm3**2 return dBkl_drhos @@ -460,29 +498,35 @@ def calc_dBkl_drhos(l_ii_avg, dii_avg, epsilonii_avg, x0ii, zetax): def calc_da1iidrhos( rho, Cmol2seg, dii_eff, l_aii_avg, l_rii_avg, x0ii, epsilonii_avg, zetax ): - r""" - Compute derivative of the term, :math:`\bar{a}_{1,ii}` with respect to :math:`\rho_s` - + Compute derivative of the term, + :math:`\bar{a}_{1,ii}` with respect to :math:`\rho_s` + Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 dii_eff : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. l_aii_avg : numpy.ndarray - Average bead (i.e. group or segment) attractive exponent in component (i.e. molecule) i. + Average bead (i.e. group or segment) attractive exponent in component (i.e. + molecule) i. l_rii_avg : numpy.ndarray - Average bead (i.e. group or segment) attractive exponent in component (i.e. molecule) i. + Average bead (i.e. group or segment) attractive exponent in component (i.e. + molecule) i. x0ii : numpy.ndarray Matrix of sigmaii_avg/dii_eff epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- da1iidrhos : numpy.ndarray @@ -502,8 +546,8 @@ def calc_da1iidrhos( dB_drhos_r = calc_dBkl_drhos(l_rii_avg, dii_eff, epsilonii_avg, x0ii, zetax) da1iidrhos = Cii * ( - ((x0ii ** l_aii_avg) * (das1_drhos_a + dB_drhos_a)) - - ((x0ii ** l_rii_avg) * (das1_drhos_r + dB_drhos_r)) + ((x0ii**l_aii_avg) * (das1_drhos_a + dB_drhos_a)) + - ((x0ii**l_rii_avg) * (das1_drhos_r + dB_drhos_r)) ) return da1iidrhos @@ -512,43 +556,50 @@ def calc_da1iidrhos( def calc_da2ii_1pchi_drhos( rho, Cmol2seg, epsilonii_avg, dii_eff, x0ii, l_rii_avg, l_aii_avg, zetax ): - r""" - Compute derivative of the term, :math:`\frac{\bar{a}_{2,ii}}{1+\bar{\chi}_{ii}}` with respect to :math:`\rho_s`. - + Compute derivative of the term, :math:`\frac{\bar{a}_{2,ii}}{1+\bar{\chi}_{ii}}` + with respect to :math:`\rho_s`. + Parameters ---------- rho : numpy.ndarray Number density of system [mol/m^3] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 epsilonii_avg : numpy.ndarray - Average bead (i.e. group or segment) potential well depth in component (i.e. molecule) i. + Average bead (i.e. group or segment) potential well depth in component (i.e. + molecule) i. dii_eff : numpy.ndarray - Effective hard sphere diameter of the beads (i.e. groups or segments) in component (i.e. molecule) i. + Effective hard sphere diameter of the beads (i.e. groups or segments) in + component (i.e. molecule) i. x0ii : numpy.ndarray Matrix of sigmaii_avg/dii_eff l_rii_avg : numpy.ndarray - Average bead (i.e. group or segment) attractive exponent in component (i.e. molecule) i. + Average bead (i.e. group or segment) attractive exponent in component (i.e. + molecule) i. l_aii_avg : numpy.ndarray - Average bead (i.e. group or segment) attractive exponent in component (i.e. molecule) i. + Average bead (i.e. group or segment) attractive exponent in component (i.e. + molecule) i. zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- da2ii_1pchi_drhos : numpy.ndarray - Term used in the calculation of the second-order term from the macroscopic compressibility - + Term used in the calculation of the second-order term from the macroscopic + compressibility + """ # Calculate terms and derivatives used in derivative chain rule KHS = ((1.0 - zetax) ** 4) / ( - 1.0 + (4.0 * zetax) + (4.0 * (zetax ** 2)) - (4.0 * (zetax ** 3)) + (zetax ** 4) + 1.0 + (4.0 * zetax) + (4.0 * (zetax**2)) - (4.0 * (zetax**3)) + (zetax**4) ) dKHS_drhos = ( - (4.0 * (zetax ** 2 - 5.0 * zetax - 2.0) * (1.0 - zetax) ** 3) - / (zetax ** 4 - 4.0 * zetax ** 3 + 4.0 * zetax ** 2 + 4.0 * zetax + 1.0) ** 2 + (4.0 * (zetax**2 - 5.0 * zetax - 2.0) * (1.0 - zetax) ** 3) + / (zetax**4 - 4.0 * zetax**3 + 4.0 * zetax**2 + 4.0 * zetax + 1.0) ** 2 * (zetax / (rho * Cmol2seg)) ) @@ -603,7 +654,7 @@ def calc_da2ii_1pchi_drhos( + x0ii ** (2.0 * l_rii_avg) * (a1sii_2l_rii_avg + Bii_2l_rii_avg) ) - dA_B = np.transpose(np.transpose(0.5 * epsilonii_avg * Cii ** 2 * B) * dKHS_drhos) + dA_B = np.transpose(np.transpose(0.5 * epsilonii_avg * Cii**2 * B) * dKHS_drhos) dB = ( x0ii ** (2.0 * l_aii_avg) * (da1sii_2l_aii_avg + dBii_2l_aii_avg) @@ -613,7 +664,7 @@ def calc_da2ii_1pchi_drhos( + x0ii ** (2.0 * l_rii_avg) * (da1sii_2l_rii_avg + dBii_2l_rii_avg) ) - A_dB = np.transpose(np.transpose(0.5 * epsilonii_avg * Cii ** 2 * dB) * KHS) + A_dB = np.transpose(np.transpose(0.5 * epsilonii_avg * Cii**2 * dB) * KHS) da2ii_1pchi_drhos = A_dB + dA_B @@ -622,9 +673,9 @@ def calc_da2ii_1pchi_drhos( def calc_Iij(rho, T, xi, epsilonii_avg, sigmaii_avg, sigmakl, xskl): r""" - + Reference fluid pair correlation function used in calculating association sites - + Parameters ---------- rho : numpy.ndarray @@ -640,13 +691,15 @@ def calc_Iij(rho, T, xi, epsilonii_avg, sigmaii_avg, sigmakl, xskl): sigmakl : numpy.ndarray Matrix of Mie diameter for groups (k,l) xskl : numpy.ndarray - Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that of bead l - + Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that + of bead l + Returns ------- Iij : numpy.ndarray - A temperature-density polynomial correlation of the association integral for a Lennard-Jones monomer. This matrix is (len(rho) x Ncomp x Ncomp) - + A temperature-density polynomial correlation of the association integral for a + Lennard-Jones monomer. This matrix is (len(rho) x Ncomp x Ncomp) + """ ncomp = len(xi) @@ -662,7 +715,7 @@ def calc_Iij(rho, T, xi, epsilonii_avg, sigmaii_avg, sigmakl, xskl): ) epsilonij[j, i] = epsilonij[i, j] - sigmax3 = np.sum(xskl * (sigmakl ** 3 * constants.molecule_per_nm3)) + sigmax3 = np.sum(xskl * (sigmakl**3 * constants.molecule_per_nm3)) cij = np.array( [ @@ -815,7 +868,8 @@ def calc_Iij(rho, T, xi, epsilonii_avg, sigmaii_avg, sigmakl, xskl): Iij = np.zeros((np.size(rho), ncomp, ncomp)) for p in range(11): for q in range(11 - p): - # Iij += np.einsum("i,jk->ijk", cij[p, q] * ((sigmax3 * rho)**p), ((T / epsilonij)**q)) + # Iij += np.einsum("i,jk->ijk", cij[p, q] * ((sigmax3 * rho)**p), + # ((T / epsilonij)**q)) if p == 0: Iij += np.einsum( "i,jk->ijk", cij[p, q] * np.ones(len(rho)), ((T / epsilonij) ** q) @@ -825,61 +879,61 @@ def calc_Iij(rho, T, xi, epsilonii_avg, sigmaii_avg, sigmakl, xskl): "i,jk->ijk", cij[p, q] * ((sigmax3 * rho)), ((T / epsilonij) ** q) ) elif p == 2: - rho2 = rho ** 2 + rho2 = rho**2 Iij += np.einsum( "i,jk->ijk", - cij[p, q] * ((sigmax3 ** p * rho2)), + cij[p, q] * ((sigmax3**p * rho2)), ((T / epsilonij) ** q), ) elif p == 3: rho3 = rho2 * rho Iij += np.einsum( "i,jk->ijk", - cij[p, q] * ((sigmax3 ** p * rho3)), + cij[p, q] * ((sigmax3**p * rho3)), ((T / epsilonij) ** q), ) elif p == 4: - rho4 = rho2 ** 2 + rho4 = rho2**2 Iij += np.einsum( "i,jk->ijk", - cij[p, q] * ((sigmax3 ** p * rho4)), + cij[p, q] * ((sigmax3**p * rho4)), ((T / epsilonij) ** q), ) elif p == 5: rho5 = rho2 * rho3 Iij += np.einsum( "i,jk->ijk", - cij[p, q] * ((sigmax3 ** p * rho5)), + cij[p, q] * ((sigmax3**p * rho5)), ((T / epsilonij) ** q), ) elif p == 6: Iij += np.einsum( "i,jk->ijk", - cij[p, q] * ((sigmax3 ** p * rho * rho5)), + cij[p, q] * ((sigmax3**p * rho * rho5)), ((T / epsilonij) ** q), ) elif p == 7: Iij += np.einsum( "i,jk->ijk", - cij[p, q] * ((sigmax3 ** p * rho2 * rho5)), + cij[p, q] * ((sigmax3**p * rho2 * rho5)), ((T / epsilonij) ** q), ) elif p == 8: Iij += np.einsum( "i,jk->ijk", - cij[p, q] * ((sigmax3 ** p * rho3 * rho5)), + cij[p, q] * ((sigmax3**p * rho3 * rho5)), ((T / epsilonij) ** q), ) elif p == 9: Iij += np.einsum( "i,jk->ijk", - cij[p, q] * ((sigmax3 ** p * rho4 * rho5)), + cij[p, q] * ((sigmax3**p * rho4 * rho5)), ((T / epsilonij) ** q), ) elif p == 10: Iij += np.einsum( "i,jk->ijk", - cij[p, q] * ((sigmax3 ** p * rho5 * rho5)), + cij[p, q] * ((sigmax3**p * rho5 * rho5)), ((T / epsilonij) ** q), ) diff --git a/despasito/equations_of_state/saft/gamma_mie.py b/despasito/equations_of_state/saft/gamma_mie.py index 418a584..7e6b3d9 100644 --- a/despasito/equations_of_state/saft/gamma_mie.py +++ b/despasito/equations_of_state/saft/gamma_mie.py @@ -1,12 +1,13 @@ # -- coding: utf8 -- r""" - EOS object for SAFT-:math:`\gamma`-Mie - - Equations referenced in this code are from V. Papaioannou et al J. Chem. Phys. 140 054107 2014 - + + Equations referenced in this code are from V. Papaioannou et al J. Chem. Phys. + 140 054107 2014 + """ +import sys import numpy as np import logging @@ -15,125 +16,163 @@ import despasito.equations_of_state.saft.saft_toolbox as stb from despasito.equations_of_state.saft import Aassoc from .compiled_modules.ext_gamma_mie_python import prefactor, calc_Iij +from despasito.equations_of_state.saft.compiled_modules import ( + ext_gamma_mie_numba as ext_numba +) +from despasito.equations_of_state.saft.compiled_modules import ( + ext_gamma_mie_python as ext_python +) logger = logging.getLogger(__name__) - -try: - import cython +if "cython" not in sys.modules: + print("Cython package is unavailable, using Numba") flag_cython = True -except ModuleNotFoundError: +else: flag_cython = False - logger.warning("Cython package is unavailable, using Numba") - -def _import_supporting_functions(method_stat=None): - """ Import appropriate functions for compilation mode - """ - - if method_stat == None or method_stat.fortran or method_stat.python: - import despasito.equations_of_state.saft.compiled_modules.ext_gamma_mie_python as cm - - elif method_stat.cython and flag_cython: - try: - import despasito.equations_of_state.saft.compiled_modules.ext_gamma_mie_cython as cm - except Exception: - raise ImportError("Cython package is available but module: despasito.equations_of_state.saft.compiled_modules.ext_gamma_mie_cython, has not been compiled.") - - elif method_stat.numba or not flag_cython: - import despasito.equations_of_state.saft.compiled_modules.ext_gamma_mie_numba as cm - - else: - raise ValueError("Unknown instructions for importing supportive functions of SAFT") + try: + from despasito.equations_of_state.saft.compiled_modules import ( + ext_gamma_mie_cython as ext_cython + ) + except ImportError: + raise ImportError( + "Cython package is available but module: " + "despasito.equations_of_state.saft.compiled_modules.ext_Aassoc_cython, has" + " not been compiled." + ) - return cm class SaftType: - r""" Object of SAFT-𝛾-Mie - + Parameters ---------- beads : list[str] List of unique bead names used among components molecular_composition : numpy.ndarray - :math:`\nu_{i,k}/k_B`. Array containing the number of components by the number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`. Array containing the number of components by the number + of bead types. Defines the number of each type of group in each component. bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters: - - - epsilon: :math:`\epsilon_{k,k}/k_B`, Energy well depth scaled by Boltzmann constant + A dictionary where bead names are the keys to access EOS self interaction + parameters: + + - epsilon: :math:`\epsilon_{k,k}/k_B`, Energy well depth scaled by Boltzmann + constant - sigma: :math:`\sigma_{k,k}`, Size parameter [nm] - mass: Bead mass [kg/mol] - - lambdar: :math:`\lambda^{r}_{k,k}`, Exponent of repulsive term between groups of type k - - lambdaa: :math:`\lambda^{a}_{k,k}`, Exponent of attractive term between groups of type k - - Sk: Optional, default=1, Shape factor, reflects the proportion with which a given segment contributes to the total free energy + - lambdar: :math:`\lambda^{r}_{k,k}`, Exponent of repulsive term between groups + of type k + - lambdaa: :math:`\lambda^{a}_{k,k}`, Exponent of attractive term between groups + of type k + - Sk: Optional, default=1, Shape factor, reflects the proportion with which a + given segment contributes to the total free energy - Vks: Optional, default=1, Number of segments in this molecular group cross_library : dict, Optional, default={} - Optional library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. + Optional library of bead cross interaction parameters. As many or as few of the + desired parameters may be defined for whichever group combinations are desired. - - epsilon: :math:`\epsilon_{k,k}/k_B`, Energy well depth scaled by Boltzmann constant + - epsilon: :math:`\epsilon_{k,k}/k_B`, Energy well depth scaled by Boltzmann + constant - sigma: :math:`\sigma_{k,k}`, Size parameter [nm] - mass: Bead mass [kg/mol] - - lambdar: :math:`\lambda^{r}_{k,k}`, Exponent of repulsive term between groups of type k - - lambdaa: :math:`\lambda^{a}_{k,k}`, Exponent of attractive term between groups of type k + - lambdar: :math:`\lambda^{r}_{k,k}`, Exponent of repulsive term between groups + of type k + - lambdaa: :math:`\lambda^{a}_{k,k}`, Exponent of attractive term between groups + of type k num_rings : list - Number of rings in each molecule. This will impact the chain contribution to the Helmholtz energy. - + Number of rings in each molecule. This will impact the chain contribution to + the Helmholtz energy. + Attributes ---------- beads : list[str] List of unique bead names used among components bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters. See entry in **Parameters** section. + A dictionary where bead names are the keys to access EOS self interaction + parameters. See entry in **Parameters** section. cross_library : dict, Optional, default={} - Optional library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. Any interaction parameters that aren't provided are computed with the appropriate ``combining_rules``. See entry in **Parameters** section. + Optional library of bead cross interaction parameters. As many or as few of + the desired parameters may be defined for whichever group combinations are + desired. Any interaction parameters that aren't provided are computed with the + appropriate ``combining_rules``. See entry in **Parameters** section. Aideal_method : str - "Abroglie" the default functional form of the ideal gas contribution of the Helmholtz energy + "Abroglie" the default functional form of the ideal gas contribution of the + Helmholtz energy residual_helmholtz_contributions : list[str] - List of methods from the specified ``saft_source`` representing contributions to the Helmholtz energy that are functions of density, temperature, and composition. For this variant, [``Amonomer``, ``Achain``] + List of methods from the specified ``saft_source`` representing contributions + to the Helmholtz energy that are functions of density, temperature, and + composition. For this variant, [``Amonomer``, ``Achain``] parameter_types : list[str] - This list of parameter names, "epsilon", "lambdar", "lambdaa", "sigma", and/or "Sk" as well as parameters for the main saft class. + This list of parameter names, "epsilon", "lambdar", "lambdaa", "sigma", + and/or "Sk" as well as parameters for the main saft class. parameter_bound_extreme : dict - With each parameter name as an entry representing a list with the minimum and maximum feasible parameter value. + With each parameter name as an entry representing a list with the minimum and + maximum feasible parameter value. - epsilon: [100.,1000.] - lambdar: [6.0,100.] - lambdaa: [3.0,100.] - sigma: [0.1,10.0] - Sk: [0.1,1.0] - + combining_rules : dict - Contains functional form and additional information for calculating cross interaction parameters that are not found in `cross_library`. Function must be one of those contained in :mod:`~despasito.equations_of_state.combining_rule_types`. The default values are: + Contains functional form and additional information for calculating cross + interaction parameters that are not found in `cross_library`. Function must be + one of those contained in + :mod:`~despasito.equations_of_state.combining_rule_types`. The default values + are: - sigma: {"function": "mean"} - lambdar: {"function": "mie_exponent"} - lambdar: {"function": "mie_exponent"} - - epsilon: {"function": "volumetric_geometric_mean", "weighting_parameters": ["sigma"]} + - epsilon: {"function": "volumetric_geometric_mean", "weighting_parameters": + ["sigma"]} eos_dict : dict - Dictionary of parameters and specific settings - - - molecular_composition (numpy.ndarray) - :math:`\nu_{i,k}/k_B`. Array containing the number of components by the number of bead types. Defines the number of each type of group in each component. - - num_rings (list) - Number of rings in each molecule. This will impact the chain contribution to the Helmholtz energy. - - Sk (numpy.ndarray) - Shape factor, reflects the proportion which a given segment contributes to the total free energy. Length of ``beads`` array. - - Vks (numpy.ndarray) - Number of segments in this molecular group. Length of ``beads`` array. - - Ckl (numpy.ndarray) - Matrix of Mie potential prefactors between beads (l,k) - - epsilonkl (numpy.ndarray) - Matrix of Mie potential well depths for groups (k,l) + Dictionary of parameters and specific settings + + - molecular_composition (numpy.ndarray) - :math:`\nu_{i,k}/k_B`. Array + containing the number of components by the number of bead types. Defines the + number of each type of group in each component. + - num_rings (list) - Number of rings in each molecule. This will impact the + chain contribution to the Helmholtz energy. + - Sk (numpy.ndarray) - Shape factor, reflects the proportion which a given + segment contributes to the total free energy. Length of ``beads`` array. + - Vks (numpy.ndarray) - Number of segments in this molecular group. Length of + ``beads`` array. + - Ckl (numpy.ndarray) - Matrix of Mie potential prefactors between beads (l,k) + - epsilonkl (numpy.ndarray) - Matrix of Mie potential well depths for + groups (k,l) - sigmakl (numpy.ndarray) - Matrix of bead diameters (k,l) - - lambdarkl (numpy.ndarray) - Matrix of repulsive Mie exponent for groups (k,l) - - lambdaakl (numpy.ndarray) - Matrix of attractive Mie exponent for groups (k,l) - - dkl (numpy.ndarray) - Matrix of hard sphere equivalent for each bead and interaction between them (l,k) - - x0kl (numpy.ndarray) - Matrix of sigmakl/dkl, sigmakl is the Mie radius for groups (k,l) - - Cmol2seg (float) - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. - - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that of bead l - - alphakl (np.array) - (Ngroup,Ngroup) "A dimensionless form of the integrated vdW energy of the Mie potential" eq. 33 - - epsilonii_avg (numpy.ndarray) - Matrix of molecule averaged well depths (i.j) - - sigmaii_avg (numpy.ndarray) - Matrix of molecule averaged Mie diameter (i.j) - - lambdaaii_avg (numpy.ndarray) - Matrix of molecule averaged Mie potential attractive exponents (i.j) - - lambdarii_avg (numpy.ndarray) - Matrix of molecule averaged Mie potential attractive exponents (i.j) - - dii_eff (numpy.ndarray) - Matrix of mole averaged hard sphere equivalent for each bead and interaction between them (i.j) - - x0ii (numpy.ndarray) - Matrix of sigmaii_avg/dii_eff, sigmaii_avg is the average molecular Mie radius and dii_eff the average molecular hard sphere diameter + - lambdarkl (numpy.ndarray) - Matrix of repulsive Mie exponent for + groups (k,l) + - lambdaakl (numpy.ndarray) - Matrix of attractive Mie exponent for + groups (k,l) + - dkl (numpy.ndarray) - Matrix of hard sphere equivalent for each bead and + interaction between them (l,k) + - x0kl (numpy.ndarray) - Matrix of sigmakl/dkl, sigmakl is the Mie radius for + groups (k,l) + - Cmol2seg (float) - Conversion factor from from molecular number density, + :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. + - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment + or group) k multiplied by that of bead l + - alphakl (np.array) - (Ngroup,Ngroup) "A dimensionless form of the integrated + vdW energy of the Mie potential" eq. 33 + - epsilonii_avg (numpy.ndarray) - Matrix of molecule averaged well + depths (i.j) + - sigmaii_avg (numpy.ndarray) - Matrix of molecule averaged Mie + diameter (i.j) + - lambdaaii_avg (numpy.ndarray) - Matrix of molecule averaged Mie potential + attractive exponents (i.j) + - lambdarii_avg (numpy.ndarray) - Matrix of molecule averaged Mie potential + attractive exponents (i.j) + - dii_eff (numpy.ndarray) - Matrix of mole averaged hard sphere equivalent for + each bead and interaction between them (i.j) + - x0ii (numpy.ndarray) - Matrix of sigmaii_avg/dii_eff, sigmaii_avg is the + average molecular Mie radius and dii_eff the average molecular hard sphere + diameter ncomp : int Number of components in the system @@ -142,8 +181,10 @@ class SaftType: xi : numpy.ndarray Mole fraction of each molecule in mixture. Default initialization is np.nan T : float - Temperature value is initially defined as NaN for a placeholder until temperature dependent attributes are initialized by using a method of this class. - + Temperature value is initially defined as NaN for a placeholder until + temperature dependent attributes are initialized by using a method of this + class. + """ def __init__(self, **kwargs): @@ -154,8 +195,6 @@ def __init__(self, **kwargs): else: self.method_stat = None - self._cm = _import_supporting_functions(self.method_stat) - self.Aideal_method = "Abroglie" self.parameter_types = ["epsilon", "sigma", "lambdar", "lambdaa", "Sk"] self._parameter_defaults = { @@ -194,7 +233,7 @@ def __init__(self, **kwargs): if key not in kwargs: raise ValueError( "The one of the following inputs is missing: {}".format( - ", ".join(tmp) + ", ".join(needed_attributes) ) ) elif key == "molecular_composition": @@ -265,18 +304,22 @@ def __init__(self, **kwargs): def calc_component_averaged_properties(self): r""" - Calculate component averaged properties specific to SAFT-𝛾-Mie for the chain term. + Calculate component averaged properties specific to SAFT-𝛾-Mie for the chain + term. Attributes ---------- output : dict - Dictionary of outputs, the following possibilities are calculated if all relevant beads have those properties. - + Dictionary of outputs, the following possibilities are calculated if all + relevant beads have those properties. + - epsilonii_avg (numpy.ndarray) - Matrix of molecule averaged well depths - sigmaii_avg (numpy.ndarray) - Matrix of molecule averaged Mie diameter - - lambdaaii_avg (numpy.ndarray) - Matrix of molecule averaged Mie potential attractive exponents - - lambdarii_avg (numpy.ndarray) - Matrix of molecule averaged Mie potential attractive exponents - + - lambdaaii_avg (numpy.ndarray) - Matrix of molecule averaged Mie potential + attractive exponents + - lambdarii_avg (numpy.ndarray) - Matrix of molecule averaged Mie potential + attractive exponents + """ zki = np.zeros((self.ncomp, self.nbeads), float) @@ -324,7 +367,7 @@ def calc_component_averaged_properties(self): def Ahard_sphere(self, rho, T, xi): r""" Outputs monomer contribution to the Helmholtz energy, :math:`A^{HS}/Nk_{B}T`. - + Parameters ---------- rho : numpy.ndarray @@ -333,7 +376,7 @@ def Ahard_sphere(self, rho, T, xi): Temperature of the system [K] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- Ahard_sphere : numpy.ndarray @@ -375,8 +418,9 @@ def Ahard_sphere(self, rho, T, xi): def Afirst_order(self, rho, T, xi, zetax=None): r""" - Outputs :math:`A^{1st order}/Nk_{B}T`. This is the first order term in the high-temperature perturbation expansion - + Outputs :math:`A^{1st order}/Nk_{B}T`. This is the first order term in the + high-temperature perturbation expansion + Parameters ---------- rho : numpy.ndarray @@ -386,8 +430,9 @@ def Afirst_order(self, rho, T, xi, zetax=None): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter + for groups (k,l) + Returns ------- Afirst_order : numpy.ndarray @@ -407,7 +452,7 @@ def Afirst_order(self, rho, T, xi, zetax=None): ) # compute components of eq. 19 - a1kl = self._cm.calc_a1ii( + args = ( rho, self.eos_dict["Cmol2seg"], self.eos_dict["dkl"], @@ -417,6 +462,12 @@ def Afirst_order(self, rho, T, xi, zetax=None): self.eos_dict["epsilonkl"], zetax, ) + if self.method_stat.cython and flag_cython: + a1kl = ext_cython.calc_a1ii(*args) + elif self.method_stat.python: + a1kl = ext_python.calc_a1ii(*args) + else: + a1kl = ext_numba.calc_a1ii(*args) # eq. 18 a1 = np.einsum("ijk,jk->i", a1kl, self.eos_dict["xskl"]) @@ -426,8 +477,9 @@ def Afirst_order(self, rho, T, xi, zetax=None): def Asecond_order(self, rho, T, xi, zetaxstar=None, zetax=None, KHS=None): r""" - Outputs :math:`A^{2nd order}/Nk_{B}T`. This is the second order term in the high-temperature perturbation expansion - + Outputs :math:`A^{2nd order}/Nk_{B}T`. This is the second order term in the + high-temperature perturbation expansion + Parameters ---------- rho : numpy.ndarray @@ -439,10 +491,12 @@ def Asecond_order(self, rho, T, xi, zetaxstar=None, zetax=None, KHS=None): zetaxstar : numpy.ndarray, Optional, default=None Matrix of hypothetical packing fraction based on sigma for groups (k,l) zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) KHS : numpy.ndarray, Optional, default=None - (length of densities) isothermal compressibility of system with packing fraction zetax - + (length of densities) isothermal compressibility of system with packing + fraction zetax + Returns ------- Asecond_order : numpy.ndarray @@ -472,18 +526,18 @@ def Asecond_order(self, rho, T, xi, zetaxstar=None, zetax=None, KHS=None): if KHS is None: KHS = stb.calc_KHS(zetax) - ## compute a2kl, eq. 30 ##### + # compute a2kl, eq. 30 ##### # compute f1, f2, and f3 for eq. 32 fmlist123 = self.calc_fm(self.eos_dict["alphakl"], np.array([1, 2, 3])) chikl = ( np.einsum("i,jk", zetaxstar, fmlist123[0]) - + np.einsum("i,jk", zetaxstar ** 5, fmlist123[1]) - + np.einsum("i,jk", zetaxstar ** 8, fmlist123[2]) + + np.einsum("i,jk", zetaxstar**5, fmlist123[1]) + + np.einsum("i,jk", zetaxstar**8, fmlist123[2]) ) - a1s_2la = self._cm.calc_a1s( + args = ( rho, self.eos_dict["Cmol2seg"], 2.0 * self.eos_dict["lambdaakl"], @@ -491,7 +545,13 @@ def Asecond_order(self, rho, T, xi, zetaxstar=None, zetax=None, KHS=None): self.eos_dict["epsilonkl"], self.eos_dict["dkl"], ) - a1s_2lr = self._cm.calc_a1s( + if self.method_stat.cython and flag_cython: + a1s_2la = ext_cython.calc_a1s(*args) + elif self.method_stat.python: + a1s_2la = ext_python.calc_a1s(*args) + else: + a1s_2la = ext_numba.calc_a1s(*args) + args = ( rho, self.eos_dict["Cmol2seg"], 2.0 * self.eos_dict["lambdarkl"], @@ -499,7 +559,13 @@ def Asecond_order(self, rho, T, xi, zetaxstar=None, zetax=None, KHS=None): self.eos_dict["epsilonkl"], self.eos_dict["dkl"], ) - a1s_lalr = self._cm.calc_a1s( + if self.method_stat.cython and flag_cython: + a1s_2lr = ext_cython.calc_a1s(*args) + elif self.method_stat.python: + a1s_2lr = ext_python.calc_a1s(*args) + else: + a1s_2lr = ext_numba.calc_a1s(*args) + args = ( rho, self.eos_dict["Cmol2seg"], self.eos_dict["lambdaakl"] + self.eos_dict["lambdarkl"], @@ -507,7 +573,13 @@ def Asecond_order(self, rho, T, xi, zetaxstar=None, zetax=None, KHS=None): self.eos_dict["epsilonkl"], self.eos_dict["dkl"], ) - B_2la = self._cm.calc_Bkl( + if self.method_stat.cython and flag_cython: + a1s_lalr = ext_cython.calc_a1s(*args) + elif self.method_stat.python: + a1s_lalr = ext_python.calc_a1s(*args) + else: + a1s_lalr = ext_numba.calc_a1s(*args) + args = ( rho, 2.0 * self.eos_dict["lambdaakl"], self.eos_dict["Cmol2seg"], @@ -516,7 +588,13 @@ def Asecond_order(self, rho, T, xi, zetaxstar=None, zetax=None, KHS=None): self.eos_dict["x0kl"], zetax, ) - B_2lr = self._cm.calc_Bkl( + if self.method_stat.cython and flag_cython: + B_2la = ext_cython.calc_Bkl(*args) + elif self.method_stat.python: + B_2la = ext_python.calc_Bkl(*args) + else: + B_2la = ext_numba.calc_Bkl(*args) + args = ( rho, 2.0 * self.eos_dict["lambdarkl"], self.eos_dict["Cmol2seg"], @@ -525,7 +603,13 @@ def Asecond_order(self, rho, T, xi, zetaxstar=None, zetax=None, KHS=None): self.eos_dict["x0kl"], zetax, ) - B_lalr = self._cm.calc_Bkl( + if self.method_stat.cython and flag_cython: + B_2lr = ext_cython.calc_Bkl(*args) + elif self.method_stat.python: + B_2lr = ext_python.calc_Bkl(*args) + else: + B_2lr = ext_numba.calc_Bkl(*args) + args = ( rho, self.eos_dict["lambdaakl"] + self.eos_dict["lambdarkl"], self.eos_dict["Cmol2seg"], @@ -534,6 +618,12 @@ def Asecond_order(self, rho, T, xi, zetaxstar=None, zetax=None, KHS=None): self.eos_dict["x0kl"], zetax, ) + if self.method_stat.cython and flag_cython: + B_lalr = ext_cython.calc_Bkl(*args) + elif self.method_stat.python: + B_lalr = ext_python.calc_Bkl(*args) + else: + B_lalr = ext_numba.calc_Bkl(*args) a2kl = ( (self.eos_dict["x0kl"] ** (2.0 * self.eos_dict["lambdaakl"])) @@ -562,14 +652,15 @@ def Asecond_order(self, rho, T, xi, zetaxstar=None, zetax=None, KHS=None): # eq. 29 a2 = np.einsum("ijk,jk->i", a2kl, self.eos_dict["xskl"]) - A2 = (self.eos_dict["Cmol2seg"] / (T ** 2)) * a2 + A2 = (self.eos_dict["Cmol2seg"] / (T**2)) * a2 return A2 def Athird_order(self, rho, T, xi, zetaxstar=None): r""" - Outputs :math:`A^{3rd order}/Nk_{B}T`. This is the third order term in the high-temperature perturbation expansion - + Outputs :math:`A^{3rd order}/Nk_{B}T`. This is the third order term in the + high-temperature perturbation expansion + Parameters ---------- rho : numpy.ndarray @@ -580,7 +671,7 @@ def Athird_order(self, rho, T, xi, zetaxstar=None): Mole fraction of each component, sum(xi) should equal 1.0 zetaxstar : numpy.ndarray, Optional, default=None Matrix of hypothetical packing fraction based on sigma for groups (k,l) - + Returns ------- Athird_order : numpy.ndarray @@ -606,21 +697,24 @@ def Athird_order(self, rho, T, xi, zetaxstar=None): "i,jk", zetaxstar, -(self.eos_dict["epsilonkl"] ** 3) * fmlist456[0] ) * np.exp( np.einsum("i,jk", zetaxstar, fmlist456[1]) - + np.einsum("i,jk", zetaxstar ** 2, fmlist456[2]) - ) # a3kl=-(epsilonkl**3)*fmlist456[0]*zetaxstar*np.exp((fmlist456[1]*zetaxstar)+(fmlist456[2]*(zetaxstar**2))) + + np.einsum("i,jk", zetaxstar**2, fmlist456[2]) + ) # eq. 37 a3 = np.einsum("ijk,jk->i", a3kl, self.eos_dict["xskl"]) - A3 = (self.eos_dict["Cmol2seg"] / (T ** 3)) * a3 + A3 = (self.eos_dict["Cmol2seg"] / (T**3)) * a3 return A3 def Amonomer(self, rho, T, xi): r""" - Outputs the monomer contribution of the Helmholtz energy, :math:`A^{mono.}/Nk_{B}T`. - - This term is composed of: :math:`A^{HS}/Nk_{B}T + A^{1st order}/Nk_{B}T + A^{2nd order}/Nk_{B}T` + :math:`A^{3rd order}/Nk_{B}T` - + Outputs the monomer contribution of the Helmholtz energy, + :math:`A^{mono.}/Nk_{B}T`. + + This term is composed of: + :math:`A^{HS}/Nk_{B}T + A^{1st order}/Nk_{B}T + A^{2nd order}/Nk_{B}T` + + :math:`A^{3rd order}/Nk_{B}T` + Parameters ---------- rho : numpy.ndarray @@ -629,7 +723,7 @@ def Amonomer(self, rho, T, xi): Temperature of the system [K] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- Amonomer : numpy.ndarray @@ -638,9 +732,9 @@ def Amonomer(self, rho, T, xi): if np.all(rho >= self.density_max(xi, T, maxpack=1.0)): raise ValueError( - "Density values should not all be greater than {}, or calc_Amono will fail in log calculation.".format( - self.density_max(xi, T) - ) + "Density values should not all be greater than " + + "{},".format(self.density_max(xi, T)) + + " or calc_Amono will fail in log calculation." ) rho = self._check_density(rho) @@ -667,10 +761,11 @@ def Amonomer(self, rho, T, xi): def gdHS(self, rho, T, xi, zetax=None): r""" - The zeroth order expansion term in calculating the radial distribution function of a Mie fluid. + The zeroth order expansion term in calculating the radial distribution + function of a Mie fluid. This is also known as the hard sphere radial distribution function. - + Parameters ---------- rho : numpy.ndarray @@ -680,8 +775,9 @@ def gdHS(self, rho, T, xi, zetax=None): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- gdHS : numpy.ndarray @@ -704,13 +800,13 @@ def gdHS(self, rho, T, xi, zetax=None): gdHS = np.zeros((np.size(rho), np.size(xi))) km[:, 0] = -np.log(1.0 - zetax) + ( - 42.0 * zetax - 39.0 * zetax ** 2 + 9.0 * zetax ** 3 - 2.0 * zetax ** 4 + 42.0 * zetax - 39.0 * zetax**2 + 9.0 * zetax**3 - 2.0 * zetax**4 ) / (6.0 * (1.0 - zetax) ** 3) - km[:, 1] = (zetax ** 4 + 6.0 * zetax ** 2 - 12.0 * zetax) / ( + km[:, 1] = (zetax**4 + 6.0 * zetax**2 - 12.0 * zetax) / ( 2.0 * (1.0 - zetax) ** 3 ) - km[:, 2] = -3.0 * zetax ** 2 / (8.0 * (1.0 - zetax) ** 2) - km[:, 3] = (-zetax ** 4 + 3.0 * zetax ** 2 + 3.0 * zetax) / ( + km[:, 2] = -3.0 * zetax**2 / (8.0 * (1.0 - zetax) ** 2) + km[:, 3] = (-(zetax**4) + 3.0 * zetax**2 + 3.0 * zetax) / ( 6.0 * (1.0 - zetax) ** 3 ) @@ -726,8 +822,9 @@ def gdHS(self, rho, T, xi, zetax=None): def g1(self, rho, T, xi, zetax=None): r""" - Calculate the first order expansion term in calculating the radial distribution function of a Mie fluid - + Calculate the first order expansion term in calculating the radial + distribution function of a Mie fluid + Parameters ---------- rho : numpy.ndarray @@ -737,12 +834,14 @@ def g1(self, rho, T, xi, zetax=None): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter + for groups (k,l) + Returns ------- g1 : numpy.ndarray - First order expansion term in calculating the radial distribution function of a Mie fluid + First order expansion term in calculating the radial distribution + function of a Mie fluid """ rho = self._check_density(rho) @@ -757,7 +856,7 @@ def g1(self, rho, T, xi, zetax=None): self.eos_dict["dkl"], ) - da1iidrhos = self._cm.calc_da1iidrhos( + args = ( rho, self.eos_dict["Cmol2seg"], self.eos_dict["dii_eff"], @@ -767,8 +866,14 @@ def g1(self, rho, T, xi, zetax=None): self.eos_dict["epsilonii_avg"], zetax, ) + if self.method_stat.cython and flag_cython: + da1iidrhos = ext_cython.calc_da1iidrhos(*args) + elif self.method_stat.python: + da1iidrhos = ext_python.calc_da1iidrhos(*args) + else: + da1iidrhos = ext_numba.calc_da1iidrhos(*args) - a1sii_lambdaaii_avg = self._cm.calc_a1s_eff( + args = ( rho, self.eos_dict["Cmol2seg"], self.eos_dict["lambdaaii_avg"], @@ -776,7 +881,14 @@ def g1(self, rho, T, xi, zetax=None): self.eos_dict["epsilonii_avg"], self.eos_dict["dii_eff"], ) - a1sii_lambdarii_avg = self._cm.calc_a1s_eff( + if self.method_stat.cython and flag_cython: + a1sii_lambdaaii_avg = ext_cython.calc_a1s_eff(*args) + elif self.method_stat.python: + a1sii_lambdaaii_avg = ext_python.calc_a1s_eff(*args) + else: + a1sii_lambdaaii_avg = ext_numba.calc_a1s_eff(*args) + + args = ( rho, self.eos_dict["Cmol2seg"], self.eos_dict["lambdarii_avg"], @@ -784,8 +896,14 @@ def g1(self, rho, T, xi, zetax=None): self.eos_dict["epsilonii_avg"], self.eos_dict["dii_eff"], ) + if self.method_stat.cython and flag_cython: + a1sii_lambdarii_avg = ext_cython.calc_a1s_eff(*args) + elif self.method_stat.python: + a1sii_lambdarii_avg = ext_python.calc_a1s_eff(*args) + else: + a1sii_lambdarii_avg = ext_numba.calc_a1s_eff(*args) - Bii_lambdaaii_avg = self._cm.calc_Bkl_eff( + args = ( rho, self.eos_dict["lambdaaii_avg"], self.eos_dict["Cmol2seg"], @@ -794,7 +912,14 @@ def g1(self, rho, T, xi, zetax=None): self.eos_dict["x0ii"], zetax, ) - Bii_lambdarii_avg = self._cm.calc_Bkl_eff( + if self.method_stat.cython and flag_cython: + Bii_lambdaaii_avg = ext_cython.calc_Bkl_eff(*args) + elif self.method_stat.python: + Bii_lambdaaii_avg = ext_python.calc_Bkl_eff(*args) + else: + Bii_lambdaaii_avg = ext_numba.calc_Bkl_eff(*args) + + args = ( rho, self.eos_dict["lambdarii_avg"], self.eos_dict["Cmol2seg"], @@ -803,6 +928,12 @@ def g1(self, rho, T, xi, zetax=None): self.eos_dict["x0ii"], zetax, ) + if self.method_stat.cython and flag_cython: + Bii_lambdarii_avg = ext_cython.calc_Bkl_eff(*args) + elif self.method_stat.python: + Bii_lambdarii_avg = ext_python.calc_Bkl_eff(*args) + else: + Bii_lambdarii_avg = ext_numba.calc_Bkl_eff(*args) Cii = prefactor(self.eos_dict["lambdarii_avg"], self.eos_dict["lambdaaii_avg"]) @@ -811,7 +942,7 @@ def g1(self, rho, T, xi, zetax=None): * np.pi * self.eos_dict["epsilonii_avg"] * self.eos_dict["dii_eff"] ** 3 - * constants.molecule_per_nm3 ** 2 + * constants.molecule_per_nm3**2 ) tmp11 = 3.0 * da1iidrhos tmp21 = ( @@ -840,8 +971,9 @@ def g1(self, rho, T, xi, zetax=None): def g2(self, rho, T, xi, zetax=None): r""" - Calculate the second order expansion term in calculating the radial distribution function of a Mie fluid - + Calculate the second order expansion term in calculating the radial + distribution function of a Mie fluid + Parameters ---------- rho : numpy.ndarray @@ -851,12 +983,14 @@ def g2(self, rho, T, xi, zetax=None): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter + for groups (k,l) + Returns ------- g2 : numpy.ndarray - Second order expansion term in calculating the radial distribution function of a Mie fluid + Second order expansion term in calculating the radial distribution + function of a Mie fluid """ rho = self._check_density(rho) @@ -894,10 +1028,10 @@ def g2(self, rho, T, xi, zetax=None): * (-np.tanh(phi7[1] * (phi7[2] - alphaii[i])) + 1.0) * zetaxstar * theta[i] - * np.exp(phi7[3] * zetaxstar + phi7[4] * (zetaxstar ** 2)) + * np.exp(phi7[3] * zetaxstar + phi7[4] * (zetaxstar**2)) ) - da2iidrhos = self._cm.calc_da2ii_1pchi_drhos( + args = ( rho, self.eos_dict["Cmol2seg"], self.eos_dict["epsilonii_avg"], @@ -907,8 +1041,14 @@ def g2(self, rho, T, xi, zetax=None): self.eos_dict["lambdaaii_avg"], zetax, ) + if self.method_stat.cython and flag_cython: + da2iidrhos = ext_cython.calc_da2ii_1pchi_drhos(*args) + elif self.method_stat.python: + da2iidrhos = ext_python.calc_da2ii_1pchi_drhos(*args) + else: + da2iidrhos = ext_numba.calc_da2ii_1pchi_drhos(*args) - a1sii_2lambdaaii_avg = self._cm.calc_a1s_eff( + args = ( rho, self.eos_dict["Cmol2seg"], 2.0 * self.eos_dict["lambdaaii_avg"], @@ -916,7 +1056,14 @@ def g2(self, rho, T, xi, zetax=None): self.eos_dict["epsilonii_avg"], self.eos_dict["dii_eff"], ) - a1sii_2lambdarii_avg = self._cm.calc_a1s_eff( + if self.method_stat.cython and flag_cython: + a1sii_2lambdaaii_avg = ext_cython.calc_a1s_eff(*args) + elif self.method_stat.python: + a1sii_2lambdaaii_avg = ext_python.calc_a1s_eff(*args) + else: + a1sii_2lambdaaii_avg = ext_numba.calc_a1s_eff(*args) + + args = ( rho, self.eos_dict["Cmol2seg"], 2.0 * self.eos_dict["lambdarii_avg"], @@ -924,7 +1071,14 @@ def g2(self, rho, T, xi, zetax=None): self.eos_dict["epsilonii_avg"], self.eos_dict["dii_eff"], ) - a1sii_lambdarii_avglambdaaii_avg = self._cm.calc_a1s_eff( + if self.method_stat.cython and flag_cython: + a1sii_2lambdarii_avg = ext_cython.calc_a1s_eff(*args) + elif self.method_stat.python: + a1sii_2lambdarii_avg = ext_python.calc_a1s_eff(*args) + else: + a1sii_2lambdarii_avg = ext_numba.calc_a1s_eff(*args) + + args = ( rho, self.eos_dict["Cmol2seg"], self.eos_dict["lambdaaii_avg"] + self.eos_dict["lambdarii_avg"], @@ -932,8 +1086,14 @@ def g2(self, rho, T, xi, zetax=None): self.eos_dict["epsilonii_avg"], self.eos_dict["dii_eff"], ) + if self.method_stat.cython and flag_cython: + a1sii_lambdarii_avglambdaaii_avg = ext_cython.calc_a1s_eff(*args) + elif self.method_stat.python: + a1sii_lambdarii_avglambdaaii_avg = ext_python.calc_a1s_eff(*args) + else: + a1sii_lambdarii_avglambdaaii_avg = ext_numba.calc_a1s_eff(*args) - Bii_2lambdaaii_avg = self._cm.calc_Bkl_eff( + args = ( rho, 2.0 * self.eos_dict["lambdaaii_avg"], self.eos_dict["Cmol2seg"], @@ -942,7 +1102,14 @@ def g2(self, rho, T, xi, zetax=None): self.eos_dict["x0ii"], zetax, ) - Bii_2lambdarii_avg = self._cm.calc_Bkl_eff( + if self.method_stat.cython and flag_cython: + Bii_2lambdaaii_avg = ext_cython.calc_Bkl_eff(*args) + elif self.method_stat.python: + Bii_2lambdaaii_avg = ext_python.calc_Bkl_eff(*args) + else: + Bii_2lambdaaii_avg = ext_numba.calc_Bkl_eff(*args) + + args = ( rho, 2.0 * self.eos_dict["lambdarii_avg"], self.eos_dict["Cmol2seg"], @@ -951,7 +1118,14 @@ def g2(self, rho, T, xi, zetax=None): self.eos_dict["x0ii"], zetax, ) - Bii_lambdaaii_avglambdarii_avg = self._cm.calc_Bkl_eff( + if self.method_stat.cython and flag_cython: + Bii_2lambdarii_avg = ext_cython.calc_Bkl_eff(*args) + elif self.method_stat.python: + Bii_2lambdarii_avg = ext_python.calc_Bkl_eff(*args) + else: + Bii_2lambdarii_avg = ext_numba.calc_Bkl_eff(*args) + + args = ( rho, self.eos_dict["lambdaaii_avg"] + self.eos_dict["lambdarii_avg"], self.eos_dict["Cmol2seg"], @@ -960,11 +1134,17 @@ def g2(self, rho, T, xi, zetax=None): self.eos_dict["x0ii"], zetax, ) + if self.method_stat.cython and flag_cython: + Bii_lambdaaii_avglambdarii_avg = ext_cython.calc_Bkl_eff(*args) + elif self.method_stat.python: + Bii_lambdaaii_avglambdarii_avg = ext_python.calc_Bkl_eff(*args) + else: + Bii_lambdaaii_avglambdarii_avg = ext_numba.calc_Bkl_eff(*args) eKC2 = np.einsum( "i,j->ij", KHS / rho / self.eos_dict["Cmol2seg"], - self.eos_dict["epsilonii_avg"] * (Cii ** 2), + self.eos_dict["epsilonii_avg"] * (Cii**2), ) g2MCA = ( @@ -974,7 +1154,7 @@ def g2(self, rho, T, xi, zetax=None): * np.pi * (self.eos_dict["epsilonii_avg"] ** 2) * self.eos_dict["dii_eff"] ** 3 - * constants.molecule_per_nm3 ** 2 + * constants.molecule_per_nm3**2 ) ) * ( (3.0 * da2iidrhos) @@ -1004,7 +1184,7 @@ def g2(self, rho, T, xi, zetax=None): def Achain(self, rho, T, xi): r""" Outputs the chain term for the Helmholtz energy, :math:`A^{chain}/Nk_{B}T`. - + Parameters ---------- rho : numpy.ndarray @@ -1013,7 +1193,7 @@ def Achain(self, rho, T, xi): Temperature of the system [K] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- Achain : numpy.ndarray @@ -1053,10 +1233,9 @@ def Achain(self, rho, T, xi): return Achain def density_max(self, xi, T, maxpack=0.65): - """ Estimate the maximum density based on the hard sphere packing fraction. - + Parameters ---------- xi : list[float] @@ -1065,7 +1244,7 @@ def density_max(self, xi, T, maxpack=0.65): Temperature of the system [K] maxpack : float, Optional, default=0.65 Maximum packing fraction - + Returns ------- max_density : float @@ -1094,19 +1273,24 @@ def density_max(self, xi, T, maxpack=0.65): @staticmethod def calc_fm(alphakl, mlist): r""" - Calculate list of coefficients used to compute the correction term for :math:`A_{2nd order}/Nk_{B}T` which is related to the fluctuations of attractive energy. where a list of m values are specified in mlist eq. 39 - + Calculate list of coefficients used to compute the correction term for + :math:`A_{2nd order}/Nk_{B}T` which is related to the fluctuations of + attractive energy. where a list of m values are specified in mlist eq. 39 + Parameters ---------- alphakl : numpy.ndarray - (Ngroup,Ngroup) "A dimensionless form of the integrated vdW energy of the Mie potential" eq. 33 + (Ngroup,Ngroup) "A dimensionless form of the integrated vdW energy of + the Mie potential" eq. 33 mlist : numpy.ndarray - (number of m values) an array of integers used in the calculation of :math:`A^{mono}` - + (number of m values) an array of integers used in the calculation of + :math:`A^{mono}` + Returns ------- fmlist : numpy.ndarray - List of coefficients used to compute the correction term for :math:`A_{2}` which is related to the fluctuations of attractive energy. + List of coefficients used to compute the correction term for + :math:`A_{2}` which is related to the fluctuations of attractive energy. """ if np.size(np.shape(alphakl)) == 2: @@ -1189,7 +1373,7 @@ def calc_fm(alphakl, mlist): for i, m in enumerate(mlist): for n in range(4): - fmlist[i] += phimn[m, n] * (alphakl ** n) + fmlist[i] += phimn[m, n] * (alphakl**n) dum = np.ones_like(fmlist[i]) for n in range(4, 7): dum += phimn[m, n] * (alphakl ** (n - 3.0)) @@ -1199,9 +1383,10 @@ def calc_fm(alphakl, mlist): def calc_gr_assoc(self, rho, T, xi, Ktype="ijklab"): r""" - - Reference fluid pair correlation function used in calculating association sites - + + Reference fluid pair correlation function used in calculating association + sites + Parameters ---------- rho : numpy.ndarray @@ -1213,13 +1398,16 @@ def calc_gr_assoc(self, rho, T, xi, Ktype="ijklab"): Ktype : str, Optional, default='ijklab' Indicates which radial distribution function to return - - 'ijklab': The bonding volume was calculated from self.calc_Kijklab, return gHS_dij) - - 'klab': The bonding volume was provided to saft.py so use temperature-density polynomial correlation - + - 'ijklab': The bonding volume was calculated from self.calc_Kijklab, + return gHS_dij) + - 'klab': The bonding volume was provided to saft.py so use + temperature-density polynomial correlation + Returns ------- Iij : numpy.ndarray - A temperature-density polynomial correlation of the association integral for a Lennard-Jones monomer. This matrix is (len(rho) x Ncomp x Ncomp) + A temperature-density polynomial correlation of the association integral + for a Lennard-Jones monomer. This matrix is (len(rho) x Ncomp x Ncomp) """ rho = self._check_density(rho) @@ -1247,11 +1435,11 @@ def calc_gr_assoc(self, rho, T, xi, Ktype="ijklab"): def calc_gdHS_assoc(self, rho, T, xi): r""" - + Radial distribution function at contact. Papaioannou J. Chem. Phys. 140, 054107 (2014) - + Parameters ---------- rho : numpy.ndarray @@ -1260,7 +1448,7 @@ def calc_gdHS_assoc(self, rho, T, xi): Temperature of the system [K] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- gr : numpy.ndarray @@ -1290,36 +1478,38 @@ def calc_gdHS_assoc(self, rho, T, xi): tmp0 = 1 / (1 - eta[:, 1]) tmp1 = eta[:, 0] / (1 - eta[:, 1]) ** 2 tmp2 = eta[:, 0] ** 2 / (1 - eta[:, 1]) ** 3 - for i in range(ncomp): - for j in range(ncomp): + for i in range(self.ncomp): + for j in range(self.ncomp): tmp = ( self.eos_dict["dii_eff"][i] * self.eos_dict["dii_eff"][j] / (self.eos_dict["dii_eff"][i] + self.eos_dict["dii_eff"][j]) ) - gr[:, i, j] = tmp0 + 3 * tmp * tmp1 + 2 * tmp ** 2 * tmp2 + gr[:, i, j] = tmp0 + 3 * tmp * tmp1 + 2 * tmp**2 * tmp2 return gr def calc_Kijklab(self, T, rc_klab, rd_klab=None, reduction_ratio=0.25): r""" - - Calculation of association site bonding volume, dependent on molecule in addition to group + + Calculation of association site bonding volume, dependent on molecule in + addition to group Papaioannou J. Chem. Phys. 140, 054107 (2014) - + Parameters ---------- T : float Temperature of the system [K] - NoteHere rc_klab : numpy.ndarray - This matrix of cutoff distances for association sites for each site type in each group type + This matrix of cutoff distances for association sites for each site type + in each group type rd_klab : numpy.ndarray, Optional, default=None Position of association site in each group (nbead, nbead, nsite, nsite) reduction_ratio : float, Optional, default=0.25 - Reduced distance of the sites from the center of the sphere of interaction. This value is used when site position, rd_klab is None - + Reduced distance of the sites from the center of the sphere of + interaction. This value is used when site position, rd_klab is None + Returns ------- Kijklab : numpy.ndarray @@ -1342,25 +1532,34 @@ def calc_Kijklab(self, T, rc_klab, rd_klab=None, reduction_ratio=0.25): return Kijklab def parameter_refresh(self, bead_library, cross_library): - r""" + r""" To refresh dependent parameters - - Those parameters that are dependent on bead_library and cross_library attributes **must** be updated by running this function after all parameters from update_parameters method have been changed. + + Those parameters that are dependent on bead_library and cross_library + attributes **must** be updated by running this function after all parameters + from update_parameters method have been changed. Attributes ---------- alphakl : np.array - (Ngroup,Ngroup) "A dimensionless form of the integrated vdW energy of the Mie potential" eq. 33 + (Ngroup,Ngroup) "A dimensionless form of the integrated vdW energy of the + Mie potential" eq. 33 eos_dict : dict The following entries are updated: - - Ckl (numpy.ndarray) - Matrix of Mie potential prefactors between beads (l,k) - - epsilonkl (numpy.ndarray) - Matrix of Mie potential well depths for groups (k,l) + - Ckl (numpy.ndarray) - Matrix of Mie potential prefactors between + beads (l,k) + - epsilonkl (numpy.ndarray) - Matrix of Mie potential well depths for + groups (k,l) - sigmakl (numpy.ndarray) - Matrix of bead diameters (k,l) - - lambdarkl (numpy.ndarray) - Matrix of repulsive Mie exponent for groups (k,l) - - lambdaakl (numpy.ndarray) - Matrix of attractive Mie exponent for groups (k,l) - - Cmol2seg (float) - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. - - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that of bead l + - lambdarkl (numpy.ndarray) - Matrix of repulsive Mie exponent for + groups (k,l) + - lambdaakl (numpy.ndarray) - Matrix of attractive Mie exponent for + groups (k,l) + - Cmol2seg (float) - Conversion factor from from molecular number density, + :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. + - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or + group) k multiplied by that of bead l """ @@ -1383,7 +1582,7 @@ def parameter_refresh(self, bead_library, cross_library): self.eos_dict["lambdarkl"] = output["lambdar"] # Update Non bonded matrices - if not np.isnan(self.T) and self.T != None: + if not np.isnan(self.T) and self.T is not None: self._check_temperature_dependent_parameters(self.T) else: self._check_temperature_dependent_parameters(298) @@ -1392,13 +1591,13 @@ def parameter_refresh(self, bead_library, cross_library): self.calc_component_averaged_properties() if not np.any(np.isnan(self.xi)): - self.eos_dict["Cmol2seg"], self.eos_dict[ - "xskl" - ] = stb.calc_composition_dependent_variables( - self.xi, - self.eos_dict["molecular_composition"], - self.bead_library, - self.beads, + self.eos_dict["Cmol2seg"], self.eos_dict["xskl"] = ( + stb.calc_composition_dependent_variables( + self.xi, + self.eos_dict["molecular_composition"], + self.bead_library, + self.beads, + ) ) self.eos_dict["Ckl"] = prefactor( @@ -1411,8 +1610,9 @@ def parameter_refresh(self, bead_library, cross_library): def _check_density(self, rho): r""" - This function checks that the density array is in the correct format for further calculations. - + This function checks that the density array is in the correct format for + further calculations. + Parameters ---------- rho : numpy.ndarray @@ -1427,7 +1627,7 @@ def _check_density(self, rho): if np.isscalar(rho): rho = np.array([rho]) - elif type(rho) != np.ndarray: + elif not isinstance(rho, np.ndarray): rho = np.array(rho) if len(np.shape(rho)) == 2: rho = rho[0] @@ -1443,13 +1643,14 @@ def _check_density(self, rho): def _check_temperature_dependent_parameters(self, T): r""" - This function checks that the temperature dependent parameters are computed for the correct value. If not, they are recomputed. - + This function checks that the temperature dependent parameters are computed + for the correct value. If not, they are recomputed. + Parameters ---------- T : float Temperature of the system [K] - + Attributes --------- T : float @@ -1457,16 +1658,19 @@ def _check_temperature_dependent_parameters(self, T): eos_dict : dict The following entries are updated: - - dkl (numpy.ndarray) - Matrix of hard sphere equivalent for each bead and interaction between them (l,k) - - x0kl (numpy.ndarray) - Matrix of sigmakl/dkl, sigmakl is the Mie radius for groups (k,l) - - etc. Other matrices will also be updated if temperature dependent multipole mixing rules are used. + - dkl (numpy.ndarray) - Matrix of hard sphere equivalent for each bead and + interaction between them (l,k) + - x0kl (numpy.ndarray) - Matrix of sigmakl/dkl, sigmakl is the Mie radius + for groups (k,l) + - etc. Other matrices will also be updated if temperature dependent + multipole mixing rules are used. """ if self.T != T: self.T = T # Check for temperature dependent mixing rule - if self._mixing_temp_dependence == None: + if self._mixing_temp_dependence is None: self._mixing_temp_dependence = False for key, value in self.combining_rules.items(): if "temperature" in value: @@ -1502,22 +1706,27 @@ def _check_temperature_dependent_parameters(self, T): ) self.calc_component_averaged_properties() - self.eos_dict["dkl"], self.eos_dict[ - "x0kl" - ] = stb.calc_hard_sphere_matricies( - T, self.eos_dict["sigmakl"], self.bead_library, self.beads, prefactor + self.eos_dict["dkl"], self.eos_dict["x0kl"] = ( + stb.calc_hard_sphere_matricies( + T, + self.eos_dict["sigmakl"], + self.bead_library, + self.beads, + prefactor, + ) ) self._update_chain_temperature_dependent_variables(T) def _check_composition_dependent_parameters(self, xi): r""" - This function checks that the composition dependent parameters are computed for the correct value. If not, they are recomputed. - + This function checks that the composition dependent parameters are computed + for the correct value. If not, they are recomputed. + Parameters ---------- xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Attributes --------- xi : numpy.ndarray @@ -1525,31 +1734,34 @@ def _check_composition_dependent_parameters(self, xi): eos_dict : dict The following entries are updated: - - Cmol2seg (float) - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. - - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that of bead l + - Cmol2seg (float) - Conversion factor from from molecular number density, + :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. + - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or + group) k multiplied by that of bead l """ xi = np.array(xi) if not np.all(self.xi == xi): - self.eos_dict["Cmol2seg"], self.eos_dict[ - "xskl" - ] = stb.calc_composition_dependent_variables( - xi, - self.eos_dict["molecular_composition"], - self.bead_library, - self.beads, + self.eos_dict["Cmol2seg"], self.eos_dict["xskl"] = ( + stb.calc_composition_dependent_variables( + xi, + self.eos_dict["molecular_composition"], + self.bead_library, + self.beads, + ) ) self.xi = xi def _update_chain_temperature_dependent_variables(self, T): r""" - This function checks that the temperature dependent parameters for the chain contribution are computed for the correct value. If not, they are recomputed. - + This function checks that the temperature dependent parameters for the chain + contribution are computed for the correct value. If not, they are recomputed. + Parameters ---------- T : float Temperature of the system [K] - + Attributes --------- T : float @@ -1557,8 +1769,11 @@ def _update_chain_temperature_dependent_variables(self, T): eos_dict : dict The following entries are updated: - - dii_eff (numpy.ndarray) - Matrix of mole averaged hard sphere equivalent for each bead and interaction between them (i.j) - - x0ii (numpy.ndarray) - Matrix of sigmaii_avg/dii_eff, sigmaii_avg is the average molecular Mie radius and dii_eff the average molecular hard sphere diameter + - dii_eff (numpy.ndarray) - Matrix of mole averaged hard sphere equivalent + for each bead and interaction between them (i.j) + - x0ii (numpy.ndarray) - Matrix of sigmaii_avg/dii_eff, sigmaii_avg is the + average molecular Mie radius and dii_eff the average molecular hard sphere + diameter """ diff --git a/despasito/equations_of_state/saft/gamma_sw.py b/despasito/equations_of_state/saft/gamma_sw.py index ebcee15..8de5159 100644 --- a/despasito/equations_of_state/saft/gamma_sw.py +++ b/despasito/equations_of_state/saft/gamma_sw.py @@ -1,15 +1,14 @@ # -- coding: utf8 -- r""" EOS object for SAFT-:math:`\gamma`-SW - - Equations referenced in this code are from Lymperiadis, A. et. al, J. Chem. Phys. 127, 234903 (2007) - + + Equations referenced in this code are from Lymperiadis, A. et. al, J. Chem. Phys. + 127, 234903 (2007) """ import numpy as np import logging - import despasito.equations_of_state.eos_toolbox as tb from despasito.equations_of_state import constants import despasito.equations_of_state.saft.saft_toolbox as stb @@ -27,51 +26,68 @@ class SaftType: - r""" Object of SAFT-𝛾-SW (for square well potential) - + Parameters ---------- beads : list[str] List of unique bead names used among components molecular_composition : numpy.ndarray - :math:`\nu_{i,k}/k_B`. Array containing the number of components by the number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`. Array containing the number of components by the number + of bead types. Defines the number of each type of group in each component. bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters: - - - epsilon: :math:`\epsilon_{k,k}/k_B`, Energy well depth scaled by Boltzmann constant + A dictionary where bead names are the keys to access EOS self interaction + parameters: + + - epsilon: :math:`\epsilon_{k,k}/k_B`, Energy well depth scaled by Boltzmann + constant - sigma: :math:`\sigma_{k,k}`, Size parameter, contact distance [nm] - - lambda: :math:`\lambda_{k,k}`, Range of the attractive interaction of well depth, epsilon - - Sk: Optional, default=1, Shape factor, reflects the proportion which a given segment contributes to the total free energy + - lambda: :math:`\lambda_{k,k}`, Range of the attractive interaction of well + depth, epsilon + - Sk: Optional, default=1, Shape factor, reflects the proportion which a given + segment contributes to the total free energy - Vks: Optional, default=1, Number of segments in this molecular group cross_library : dict, Optional, default={} - Optional library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. If this matrix isn't provided, the SAFT ``combining_rules`` are used. - - - epsilon: :math:`\epsilon_{k,l}/k_B`, Energy parameter, well depth, scaled by Boltzmann Constant + Optional library of bead cross interaction parameters. As many or as few of + the desired parameters may be defined for whichever group combinations are + desired. If this matrix isn't provided, the SAFT ``combining_rules`` are used. + + - epsilon: :math:`\epsilon_{k,l}/k_B`, Energy parameter, well depth, scaled + by Boltzmann Constant - sigma: :math:`\sigma_{k,k}`, Size parameter, contact distance [nm] - - lambda: :math:`\lambda_{k,l}`, Range of the attractive interaction of well depth, epsilon + - lambda: :math:`\lambda_{k,l}`, Range of the attractive interaction of well + depth, epsilon num_rings : list - Number of rings in each molecule. This will impact the chain contribution to the Helmholtz energy. - + Number of rings in each molecule. This will impact the chain contribution to + the Helmholtz energy. + Attributes ---------- beads : list[str] List of unique bead names used among components bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters. See entry in **Parameters** section. + A dictionary where bead names are the keys to access EOS self interaction + parameters. See entry in **Parameters** section. cross_library : dict - Library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. See entry in **Parameters** section. + Library of bead cross interaction parameters. As many or as few of the desired + parameters may be defined for whichever group combinations are desired. See + entry in **Parameters** section. Aideal_method : str - "Abroglie" the default functional form of the ideal gas contribution of the Helmholtz energy + "Abroglie" the default functional form of the ideal gas contribution of the + Helmholtz energy residual_helmholtz_contributions : list[str] - List of methods from the specified saft_source representing contributions to the Helmholtz energy that are functions of density, temperature, and composition. For this variant, [``Amonomer``, ``Achain``] + List of methods from the specified saft_source representing contributions to + the Helmholtz energy that are functions of density, temperature, and + composition. For this variant, [``Amonomer``, ``Achain``] parameter_types : list[str] - This list of parameter names, "epsilon", "lambda", "sigma", and/or "Sk" as well as parameters for the specific SAFT variant. + This list of parameter names, "epsilon", "lambda", "sigma", and/or "Sk" as well + as parameters for the specific SAFT variant. parameter_bound_extreme : dict - With each parameter name as an entry representing a list with the minimum and maximum feasible parameter value. + With each parameter name as an entry representing a list with the minimum and + maximum feasible parameter value. - epsilon: [10.,1000.] - lambda: [1.0,10.] @@ -79,29 +95,43 @@ class SaftType: - Sk: [0.1,1.0] combining_rules : dict - Contains functional form and additional information for calculating cross interaction parameters that are not found in ``cross_library``. Function must be one of those contained in :mod:`~despasito.equations_of_state.combining_rule_types`. The default values are: + Contains functional form and additional information for calculating cross + interaction parameters that are not found in ``cross_library``. Function must + be one of those contained in + :mod:`~despasito.equations_of_state.combining_rule_types`. The default values + are: - sigma: {"function": "mean"} - lambda: {"function": "weighted_mean","weighting_parameters": ["sigma"]} - - epsilon: {"function": "square_well_berthelot","weighting_parameters": ["sigma", "lambda"]} + - epsilon: {"function": "square_well_berthelot","weighting_parameters": + ["sigma", "lambda"]} eos_dict : dict - Dictionary of parameters and specific settings - - - molecular_composition (numpy.ndarray) - :math:`\nu_{i,k}/k_B`. Array containing the number of components by the number of bead types. Defines the number of each type of group in each component. - - num_rings (list) - Number of rings in each molecule. This will impact the chain contribution to the Helmholtz energy. - - Sk (numpy.ndarray) - Shape factor, reflects the proportion which a given segment contributes to the total free energy. Length of ``beads`` array. - - Vks (numpy.ndarray) - Number of segments in this molecular group. Length of ``beads`` array. + Dictionary of parameters and specific settings + + - molecular_composition (numpy.ndarray) - :math:`\nu_{i,k}/k_B`. Array + containing the number of components by the number of bead types. Defines the + number of each type of group in each component. + - num_rings (list) - Number of rings in each molecule. This will impact the + chain contribution to the Helmholtz energy. + - Sk (numpy.ndarray) - Shape factor, reflects the proportion which a given + segment contributes to the total free energy. Length of ``beads`` array. + - Vks (numpy.ndarray) - Number of segments in this molecular group. Length of + ``beads`` array. - epsilon_kl (numpy.ndarray) - Matrix of well depths for groups (k,l) - sigma_kl (numpy.ndarray) - Matrix of bead diameters (k,l) - lambda_kl (numpy.ndarray) - Matrix of range of potential well depth (k,l) - - Cmol2seg (float) - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. + - Cmol2seg (float) - Conversion factor from from molecular number density, + :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. - epsilon_ij (numpy.ndarray) - Matrix of average molecular well depths (k,l) - sigma_ij (numpy.ndarray) - Matrix of average molecular diameter (k,l) - - lambda_ij (numpy.ndarray) - Matrix of average molecular range of potential well depth (k,l) - - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that of bead l - - alphakl (np.array) - van der Waals attractive parameter for square-well segments, equal to :math:`\alpha_{k,l}/k_B`. - + - lambda_ij (numpy.ndarray) - Matrix of average molecular range of potential + well depth (k,l) + - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or + group) k multiplied by that of bead l + - alphakl (np.array) - van der Waals attractive parameter for square-well + segments, equal to :math:`\alpha_{k,l}/k_B`. + ncomp : int Number of components in the system nbeads : int @@ -142,7 +172,8 @@ def __init__(self, **kwargs): "function": "square_well_berthelot", "weighting_parameters": ["sigma", "lambda"], }, - } # Note in this EOS object, the combining rules for the group parameters are also used for their corresponding molecular averaged parameters. + } # Note in this EOS object, the combining rules for the group parameters + # are also used for their corresponding molecular averaged parameters. if not hasattr(self, "eos_dict"): self.eos_dict = {} @@ -152,7 +183,7 @@ def __init__(self, **kwargs): if key not in kwargs: raise ValueError( "The one of the following inputs is missing: {}".format( - ", ".join(tmp) + ", ".join(needed_attributes) ) ) elif key == "molecular_composition": @@ -219,17 +250,20 @@ def __init__(self, **kwargs): def calc_component_averaged_properties(self): r""" - Calculate component averaged properties specific to SAFT-𝛾-SW - + Calculate component averaged properties specific to SAFT-𝛾-SW + Attributes ---------- eos_dict : dict - Dictionary of outputs, the following possibilities are calculated if all relevant beads have those properties. - - - epsilon_ij (numpy.ndarray) - Matrix of average molecular well depths (k,l) + Dictionary of outputs, the following possibilities are calculated if all + relevant beads have those properties. + + - epsilon_ij (numpy.ndarray) - Matrix of average molecular well + depths (k,l) - sigma_ij (numpy.ndarray) - Matrix of average molecular diameter (k,l) - - lambda_ij (numpy.ndarray) - Matrix of average molecular range of potential well depth (k,l) - + - lambda_ij (numpy.ndarray) - Matrix of average molecular range of + potential well depth (k,l) + """ ncomp, nbeads = np.shape(self.eos_dict["molecular_composition"]) @@ -278,19 +312,21 @@ def calc_component_averaged_properties(self): def reduced_density(self, rho, xi): r""" - Reduced density matrix where the segment number density is reduced by powers of the size parameter, sigma. - + Reduced density matrix where the segment number density is reduced by powers + of the size parameter, sigma. + Parameters ---------- rho : numpy.ndarray Number density of system [:math:`mol/m^3`] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- zeta : numpy.ndarray - Reduced density matrix of length 4 of varying degrees of dependence on sigma. Units: [molecules/nm^3, molecules/nm^2, molecules/nm, molecules] + Reduced density matrix of length 4 of varying degrees of dependence on + sigma. Units: [molecules/nm^3, molecules/nm^2, molecules/nm, molecules] """ self._check_density(rho) @@ -313,7 +349,7 @@ def reduced_density(self, rho, xi): def effective_packing_fraction(self, rho, xi, zetax=None, mode="normal"): r""" Effective packing fraction for SAFT-gamma with a square-wave potential - + Parameters ---------- rho : numpy.ndarray @@ -321,10 +357,12 @@ def effective_packing_fraction(self, rho, xi, zetax=None, mode="normal"): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter + for groups (k,l) mode : str, Optional, default="normal" - This indicates whether group or effective component parameters are used. Options include: "normal" and "effective" - + This indicates whether group or effective component parameters are used. + Options include: "normal" and "effective" + Returns ------- zeta_eff : numpy.ndarray @@ -365,8 +403,9 @@ def effective_packing_fraction(self, rho, xi, zetax=None, mode="normal"): def _dzetaeff_dzetax(self, rho, xi, zetax=None, mode="normal"): r""" - Derivative of effective packing fraction with respect to the reduced density. Eq. 33 - + Derivative of effective packing fraction with respect to the reduced density. + Eq. 33 + Parameters ---------- rho : numpy.ndarray @@ -374,14 +413,17 @@ def _dzetaeff_dzetax(self, rho, xi, zetax=None, mode="normal"): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) mode : str, Optional, default="normal" - This indicates whether group or effective component parameters are used. Options include: "normal" and "effective" - + This indicates whether group or effective component parameters are used. + Options include: "normal" and "effective" + Returns ------- dzetakl : numpy.ndarray - Derivative of effective packing fraction (len(rho), Nbeads, Nbeads) with respect to the reduced density + Derivative of effective packing fraction (len(rho), Nbeads, Nbeads) with + respect to the reduced density """ self._check_density(rho) @@ -396,9 +438,7 @@ def _dzetaeff_dzetax(self, rho, xi, zetax=None, mode="normal"): if zetax is None: zetax = self.reduced_density(rho, xi)[:, 3] - zetax_pow = np.transpose( - np.array([np.ones(len(rho)), 2 * zetax, 3 * zetax ** 2]) - ) + zetax_pow = np.transpose(np.array([np.ones(len(rho)), 2 * zetax, 3 * zetax**2])) # check if you have more than 1 bead types dzetakl = np.zeros((np.size(rho), lx, lx)) @@ -418,8 +458,9 @@ def _dzetaeff_dzetax(self, rho, xi, zetax=None, mode="normal"): def Ahard_sphere(self, rho, T, xi): r""" - Outputs hard sphere approximation of Helmholtz free energy, :math:`A^{HS}/Nk_{b}T`. - + Outputs hard sphere approximation of Helmholtz free energy, + :math:`A^{HS}/Nk_{b}T`. + Parameters ---------- rho : numpy.ndarray @@ -428,7 +469,7 @@ def Ahard_sphere(self, rho, T, xi): Temperature of the system [K] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- Ahard_sphere : numpy.ndarray @@ -452,8 +493,9 @@ def Ahard_sphere(self, rho, T, xi): def Afirst_order(self, rho, T, xi, zetax=None): r""" - Outputs :math:`A^{1st order}/Nk_{b}T`. This is the first order term in the high-temperature perturbation expansion - + Outputs :math:`A^{1st order}/Nk_{b}T`. This is the first order term in the + high-temperature perturbation expansion + Parameters ---------- rho : numpy.ndarray @@ -463,8 +505,9 @@ def Afirst_order(self, rho, T, xi, zetax=None): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter + for groups (k,l) + Returns ------- Afirst_order : numpy.ndarray @@ -489,8 +532,9 @@ def Afirst_order(self, rho, T, xi, zetax=None): def Asecond_order(self, rho, T, xi, zetax=None, KHS=None): r""" - Outputs :math:`A^{2nd order}/Nk_{b}T`. This is the second order term in the high-temperature perturbation expansion - + Outputs :math:`A^{2nd order}/Nk_{b}T`. This is the second order term in the + high-temperature perturbation expansion + Parameters ---------- rho : numpy.ndarray @@ -500,10 +544,12 @@ def Asecond_order(self, rho, T, xi, zetax=None, KHS=None): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter + for groups (k,l) KHS : numpy.ndarray, Optional, default=None - (length of densities) isothermal compressibility of system with packing fraction zetax - + (length of densities) isothermal compressibility of system with packing + fraction zetax + Returns ------- Asecond_order : numpy.ndarray @@ -538,15 +584,17 @@ def Asecond_order(self, rho, T, xi, zetax=None, KHS=None): / (1 - zeta_eff) ** 4 ) - # Lymperiadis 2007 has a disconnect where Eq. 24 != Eq. 30, as Eq. 24 is missing a minus sign. (Same in Lymperiadis 2008 for Eq. 32 and Eq. 38) - A2 = -(self.eos_dict["Cmol2seg"] / (T ** 2)) * np.sum(a2, axis=(1, 2)) + # Lymperiadis 2007 has a disconnect where Eq. 24 != Eq. 30, as Eq. 24 is + # missing a minus sign. (Same in Lymperiadis 2008 for Eq. 32 and Eq. 38) + A2 = -(self.eos_dict["Cmol2seg"] / (T**2)) * np.sum(a2, axis=(1, 2)) return A2 def Amonomer(self, rho, T, xi): r""" - Outputs the monomer contribution of the Helmholtz energy :math:`A^{mono.}/Nk_{b}T`. - + Outputs the monomer contribution of the Helmholtz energy + :math:`A^{mono.}/Nk_{b}T`. + Parameters ---------- rho : numpy.ndarray @@ -555,7 +603,7 @@ def Amonomer(self, rho, T, xi): Temperature of the system [K] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- Amonomer : numpy.ndarray @@ -564,9 +612,8 @@ def Amonomer(self, rho, T, xi): if np.all(rho > self.density_max(xi, T)): raise ValueError( - "Density values should not all be greater than {}, or calc_Amono will fail in log calculation.".format( - self.density_max(xi, T) - ) + "Density values should not all be greater than {}, or calc_Amono will" + "fail in log calculation.".format(self.density_max(xi, T)) ) rho = self._check_density(rho) @@ -584,8 +631,9 @@ def Amonomer(self, rho, T, xi): def calc_g0HS(self, rho, xi, zetax=None, mode="normal"): r""" - The contact value of the pair correlation function of a hypothetical pure fluid of diameter sigmax evaluated at an effective packing fraction, zeta_eff. - + The contact value of the pair correlation function of a hypothetical pure fluid + of diameter sigmax evaluated at an effective packing fraction, zeta_eff. + Parameters ---------- rho : numpy.ndarray @@ -593,14 +641,18 @@ def calc_g0HS(self, rho, xi, zetax=None, mode="normal"): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) mode : str, Optional, default="normal" - This indicates whether group or effective component parameters are used. Options include: "normal" and "effective", where normal used bead interaction matrices, and effective uses component averaged parameters. - + This indicates whether group or effective component parameters are used. + Options include: "normal" and "effective", where normal used bead + interaction matrices, and effective uses component averaged parameters. + Returns ------- g0HS : numpy.ndarray - The contact value of the pair correlation function of a hypothetical pure fluid + The contact value of the pair correlation function of a hypothetical pure + fluid """ rho = self._check_density(rho) @@ -619,19 +671,22 @@ def calc_gHS(self, rho, xi): r""" Hypothetical pair correlation function of a hypothetical pure fluid. - This fluid is of diameter sigmax evaluated at contact and effective packing fraction zeta_eff. - + This fluid is of diameter sigmax evaluated at contact and effective packing + fraction zeta_eff. + Parameters ---------- rho : numpy.ndarray Number density of system [:math:`mol/m^3`] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- gHS : numpy.ndarray - Hypothetical pair correlation function of a hypothetical pure fluid of diameter sigmax evaluated at contact and effective packing fraction zeta_eff. + Hypothetical pair correlation function of a hypothetical pure fluid of + diameter sigmax evaluated at contact and effective packing fraction + zeta_eff. """ rho = self._check_density(rho) @@ -654,14 +709,15 @@ def calc_gHS(self, rho, xi): + self.eos_dict["sigma_ij"][j, j] ) ) - gHS[:, i, j] = tmp1 + 3 * tmp * tmp2 + 2 * tmp ** 2 * tmp3 + gHS[:, i, j] = tmp1 + 3 * tmp * tmp2 + 2 * tmp**2 * tmp3 return gHS def calc_gSW(self, rho, T, xi, zetax=None): r""" - Calculate the square-well pair correlation function at the effective contact distance and the actual packing fraction of the mixture. - + Calculate the square-well pair correlation function at the effective contact + distance and the actual packing fraction of the mixture. + Parameters ---------- rho : numpy.ndarray @@ -671,12 +727,14 @@ def calc_gSW(self, rho, T, xi, zetax=None): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 zetax : numpy.ndarray, Optional, default=None - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- gSW : numpy.ndarray - Square-well pair correlation function at the effective contact distance and the actual packing fraction of the mixture. + Square-well pair correlation function at the effective contact distance and + the actual packing fraction of the mixture. """ rho = self._check_density(rho) @@ -696,7 +754,7 @@ def calc_gSW(self, rho, T, xi, zetax=None): dckl_coef = np.array( [[-1.50349, 0.249434], [1.40049, -0.827739], [-15.0427, 5.30827]] ) - zetax_pow = np.transpose(np.array([zetax, zetax ** 2, zetax ** 3])) + zetax_pow = np.transpose(np.array([zetax, zetax**2, zetax**3])) dzetaijdlambda = np.zeros((np.size(rho), ncomp, ncomp)) for i in range(ncomp): for j in range(ncomp): @@ -723,7 +781,7 @@ def calc_gSW(self, rho, T, xi, zetax=None): def Achain(self, rho, T, xi): r""" Outputs chain contribution to the Helmholtz energy :math:`A^{chain}/Nk_{b}T`. - + Parameters ---------- rho : numpy.ndarray @@ -732,7 +790,7 @@ def Achain(self, rho, T, xi): Temperature of the system [K] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- Achain : numpy.ndarray @@ -760,10 +818,9 @@ def Achain(self, rho, T, xi): return Achain def density_max(self, xi, T, maxpack=0.65): - """ Estimate the maximum density based on the hard sphere packing fraction. - + Parameters ---------- xi : list[float] @@ -772,7 +829,7 @@ def density_max(self, xi, T, maxpack=0.65): Temperature of the system [K] maxpack : float, Optional, default=0.65 Maximum packing fraction - + Returns ------- max_density : float @@ -799,7 +856,7 @@ def density_max(self, xi, T, maxpack=0.65): def calc_gr_assoc(self, rho, T, xi, Ktype="ijklab"): r""" Reference fluid pair correlation function used in calculating association sites - + Parameters ---------- rho : numpy.ndarray @@ -809,12 +866,15 @@ def calc_gr_assoc(self, rho, T, xi, Ktype="ijklab"): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 Ktype : str, Optional, default='ijklab' - Indicates which radial distribution function to return. The only option is 'ijklab': The bonding volume was calculated from self.calc_Kijklab, return gHS_dij) - + Indicates which radial distribution function to return. The only option is + 'ijklab': The bonding volume was calculated from self.calc_Kijklab, return + gHS_dij) + Returns ------- gr : numpy.ndarray - A temperature-density polynomial correlation of the association integral for a Lennard-Jones monomer. This matrix is (len(rho) x Ncomp x Ncomp) + A temperature-density polynomial correlation of the association integral + for a Lennard-Jones monomer. This matrix is (len(rho) x Ncomp x Ncomp) """ rho = self._check_density(rho) @@ -824,21 +884,25 @@ def calc_gr_assoc(self, rho, T, xi, Ktype="ijklab"): def calc_Kijklab(self, T, rc_klab, rd_klab=None, reduction_ratio=0.25): r""" - Calculation of association site bonding volume, dependent on molecule in addition to group + Calculation of association site bonding volume, dependent on molecule in + addition to group Lymperiadis Fluid Phase Equilibria 274 (2008) 85–104 - + Parameters ---------- T : float - Temperature of the system [K], Note used in this version of saft, but included to allow saft.py to be general + Temperature of the system [K], Note used in this version of saft, but + included to allow saft.py to be general rc_klab : numpy.ndarray - This matrix of cutoff distances for association sites for each site type in each group type + This matrix of cutoff distances for association sites for each site type + in each group type rd_klab : numpy.ndarray, Optional, default=None Position of association site in each group (nbead, nbead, nsite, nsite) reduction_ratio : float, Optional, default=0.25 - Reduced distance of the sites from the center of the sphere of interaction. This value is used when site position, rd_klab is None - + Reduced distance of the sites from the center of the sphere of interaction. + This value is used when site position, rd_klab is None + Returns ------- Kijklab : numpy.ndarray @@ -859,23 +923,29 @@ def calc_Kijklab(self, T, rc_klab, rd_klab=None, reduction_ratio=0.25): return Kijklab def parameter_refresh(self, bead_library, cross_library): - r""" + r""" To refresh dependent parameters - - Those parameters that are dependent on bead_library and cross_library attributes **must** be updated by running this function after all parameters from update_parameters method have been changed. + + Those parameters that are dependent on bead_library and cross_library + attributes **must** be updated by running this function after all parameters + from update_parameters method have been changed. Attributes ---------- alpha : np.array - van der Waals attractive parameter for square-well segments, equal to :math:`\alpha_{k,l}/k_B`. + van der Waals attractive parameter for square-well segments, equal to + :math:`\alpha_{k,l}/k_B`. eos_dict : dict The following entries are updated: - epsilon_kl (numpy.ndarray) - Matrix of well depths for groups (k,l) - sigma_kl (numpy.ndarray) - Matrix of bead diameters (k,l) - - lambda_kl (numpy.ndarray) - Matrix of range of potential well depth (k,l) - - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that of bead l - - Cmol2seg (float) - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. + - lambda_kl (numpy.ndarray) - Matrix of range of potential well + depth (k,l) + - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or + group) k multiplied by that of bead l + - Cmol2seg (float) - Conversion factor from from molecular number density, + :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. """ @@ -899,13 +969,13 @@ def parameter_refresh(self, bead_library, cross_library): self.calc_component_averaged_properties() if not np.any(np.isnan(self.xi)): - self.eos_dict["Cmol2seg"], self.eos_dict[ - "xskl" - ] = stb.calc_composition_dependent_variables( - self.xi, - self.eos_dict["molecular_composition"], - self.bead_library, - self.beads, + self.eos_dict["Cmol2seg"], self.eos_dict["xskl"] = ( + stb.calc_composition_dependent_variables( + self.xi, + self.eos_dict["molecular_composition"], + self.bead_library, + self.beads, + ) ) self.alphakl = ( 2.0 @@ -918,8 +988,9 @@ def parameter_refresh(self, bead_library, cross_library): def _check_density(self, rho): r""" - This function checks that the density array is in the correct format for further calculations. - + This function checks that the density array is in the correct format for + further calculations. + Parameters ---------- rho : numpy.ndarray @@ -933,7 +1004,7 @@ def _check_density(self, rho): if np.isscalar(rho): rho = np.array([rho]) - elif type(rho) != np.ndarray: + elif not isinstance(rho, np.ndarray): rho = np.array(rho) if len(np.shape(rho)) == 2: rho = rho[0] @@ -950,12 +1021,12 @@ def _check_density(self, rho): def _check_composition_dependent_parameters(self, xi): r""" This function updates composition dependent variables - + Parameters ---------- xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Attributes ---------- xi : numpy.ndarray @@ -963,19 +1034,21 @@ def _check_composition_dependent_parameters(self, xi): eos_dict : dict The following entries are updated: - - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that of bead l - - Cmol2seg (float) - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. + - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or + group) k multiplied by that of bead l + - Cmol2seg (float) - Conversion factor from from molecular number density, + :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. """ xi = np.array(xi) if not np.all(self.xi == xi): - self.eos_dict["Cmol2seg"], self.eos_dict[ - "xskl" - ] = stb.calc_composition_dependent_variables( - xi, - self.eos_dict["molecular_composition"], - self.bead_library, - self.beads, + self.eos_dict["Cmol2seg"], self.eos_dict["xskl"] = ( + stb.calc_composition_dependent_variables( + xi, + self.eos_dict["molecular_composition"], + self.bead_library, + self.beads, + ) ) self.xi = xi diff --git a/despasito/equations_of_state/saft/saft.py b/despasito/equations_of_state/saft/saft.py index a747d6a..8eab8ab 100644 --- a/despasito/equations_of_state/saft/saft.py +++ b/despasito/equations_of_state/saft/saft.py @@ -1,12 +1,12 @@ # -- coding: utf8 -- r""" - Parent SAFT EOS class - An additional class with variant specifics imported from ``saft_type.py`` completes the EOS. - + An additional class with variant specifics imported from ``saft_type.py`` + completes the EOS. """ + import numpy as np import logging @@ -24,9 +24,9 @@ def saft_type(name): r""" Initialize EOS object for SAFT variant. - + All input and calculated parameters are defined as hidden attributes. - + Parameters ---------- name : str @@ -38,7 +38,8 @@ def saft_type(name): Returns ------- saft_source : obj - SAFT variant class to be initiated in :class:`~despasito.equations_of_state.saft.saft.EosType` + SAFT variant class to be initiated in + :class:`~despasito.equations_of_state.saft.saft.EosType` """ if name == "gamma_mie": @@ -47,57 +48,80 @@ def saft_type(name): from despasito.equations_of_state.saft.gamma_sw import SaftType as saft_source else: raise ValueError( - "SAFT type, {}, is not supported. Be sure the class is added to the factory function 'saft_type'".format( - name - ) + "SAFT type, {}, is not supported. Be sure the class".format(name) + + " is added to the factory function 'saft_type'" ) return saft_source class EosType(EosTemplate): - r""" Initialize EOS object for SAFT variant. - + All input and calculated parameters are defined as hidden attributes. - + Parameters ---------- beads : list[str] List of unique bead names used among components bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters: + A dictionary where bead names are the keys to access EOS self interaction + parameters: - mass: Bead mass [kg/mol] - - epsilonHB-\*-\*: Optional, Interaction energy between each bead and association site. Asterisk represents string from sitenames. - - K-\*-\*: Optional, Bonding volume between each association site. Asterisk represents two strings from sitenames. - - rc-\*-\*: Optional, Cutoff distance for association sites. Asterisk represents two strings from sitenames. - - rd-\*-\*: Optional, Site position. Asterisk represents two strings from sitenames. - - Nk-\*: Optional, The number of sites of from list sitenames. Asterisk represents string from sitenames. + - epsilonHB-\*-\*: Optional, Interaction energy between each bead and + association site. Asterisk represents string from sitenames. + - K-\*-\*: Optional, Bonding volume between each association site. Asterisk + represents two strings from sitenames. + - rc-\*-\*: Optional, Cutoff distance for association sites. Asterisk + represents two strings from sitenames. + - rd-\*-\*: Optional, Site position. Asterisk represents two strings from + sitenames. + - Nk-\*: Optional, The number of sites of from list sitenames. Asterisk + represents string from sitenames. - etc. depending on SAFT variant cross_library : dict, Optional, default={} - Optional library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. - - - epsilonHB-\*-\*: Optional, Interaction energy between each bead and association site. Asterisk represents string from sitenames. - - K-\*-\*: Optional, Bonding volume between each association site. Asterisk represents two strings from sitenames. - - rc-\*-\*: Optional, Cutoff distance for association sites. Asterisk represents two strings from sitenames. - - rd-\*-\*: Optional, Site position. Asterisk represents two strings from sitenames. + Optional library of bead cross interaction parameters. As many or as few of + the desired parameters may be defined for whichever group combinations are + desired. + Note that the astericks below represent first the sitename of the primary bead + and second the sitename of the secondary bead in the .json or dictionary + structure. + + - epsilonHB-\*-\*: Optional, Interaction energy between each bead and + association site. Asterisk represents string from sitenames. + - K-\*-\*: Optional, Bonding volume between each association site. Asterisk + represents two strings from sitenames. + - rc-\*-\*: Optional, Cutoff distance for association sites. Asterisk + represents two strings from sitenames. + - rd-\*-\*: Optional, Site position. Asterisk represents two strings from + sitenames. - etc. depending on SAFT variant combining_rules : dict, Optional, default=None - Provided to overwrite functional form of combining rules defined for parameters in specific SAFT variant. See appropriate class. + Provided to overwrite functional form of combining rules defined for + parameters in specific SAFT variant. See appropriate class. saft_name : str, Optional, default="gamma_mie" - Define the SAFT variant, options listed in :func:`~despasito.equations_of_state.saft.saft.saft_type`. + Define the SAFT variant, options listed in + :func:`~despasito.equations_of_state.saft.saft.saft_type`. molecular_composition : numpy.ndarray - :math:`\nu_{i,k}/k_B`. Array of number of components by number of bead types. Defines the number of each type of group in each component, as it corresponds to the ``beads`` array. + :math:`\nu_{i,k}/k_B`. Array of number of components by number of bead types. + Defines the number of each type of group in each component, as it corresponds + to the ``beads`` array. Aideal_method : str, Optional - Functional form of ideal gas contribution for Helmholtz energy. Default is defined in SAFT variant defined with ``saft_name``. See :func:`~despasito.equations_of_state.saft.Aideal.Aideal_contribution` for more options. + Functional form of ideal gas contribution for Helmholtz energy. Default is + defined in SAFT variant defined with ``saft_name``. See + :func:`~despasito.equations_of_state.saft.Aideal.Aideal_contribution` for more + options. reduction_ratio : float, Optional - Reduced distance of the sites from the center of the sphere of interaction. This value is used when site position, ``eos_dict['rd_klab'] == None``. See :func:`~despasito.equations_of_state.saft.Aassoc.calc_bonding_volume` for more details. + Reduced distance of the sites from the center of the sphere of interaction. + This value is used when site position, ``eos_dict['rd_klab'] == None``. See + :func:`~despasito.equations_of_state.saft.Aassoc.calc_bonding_volume` for more + details. method_stat : obj - EOS object containing the the method status of the available options. + EOS object containing the the method status of the available options. kwargs Other keywords that are specific to the chosen SAFT variant @@ -106,15 +130,20 @@ class EosType(EosTemplate): beads : list[str] List of unique bead names used among components bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters. See entry in **Parameters** section. + A dictionary where bead names are the keys to access EOS self interaction + parameters. See entry in **Parameters** section. cross_library : dict - Library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. See entry in **Parameters** section. + Library of bead cross interaction parameters. As many or as few of the desired + parameters may be defined for whichever group combinations are desired. See + entry in **Parameters** section. number_of_components : int Number of components in mixture represented by given EOS object. parameter_types : list[str] - This list of parameter names for the association site calculation, "epsilonHB","K", "rc", and/or "rd" as well as parameters for the specific SAFT variant. + This list of parameter names for the association site calculation, "epsilonHB", + "K", "rc", and/or "rd" as well as parameters for the specific SAFT variant. parameter_bound_extreme : dict - With each parameter names as an entry representing a list with the minimum and maximum feasible parameter value. + With each parameter names as an entry representing a list with the minimum and + maximum feasible parameter value. - epsilonHB: [100.,5000.] - K: [1e-5,10000.] @@ -123,25 +152,39 @@ class EosType(EosTemplate): - etc., other parameters from SAFT variant saft_name : str, Optional, default="gamma_mie" - Define the SAFT variant, options listed in :func:`~despasito.equations_of_state.saft.saft.saft_type`. + Define the SAFT variant, options listed in + :func:`~despasito.equations_of_state.saft.saft.saft_type`. saft_source : obj - Object representing SAFT variant. This attribute can be used to access intermediate calculations. + Object representing SAFT variant. This attribute can be used to access + intermediate calculations. eos_dict : dict - Temperature value is initially defined as NaN for a placeholder until temperature dependent attributes are initialized by using a method of this class. - - - molecular_composition (numpy.ndarray) - :math:`\nu_{i,k}/k_B`. Array containing the number of components by the number of bead types. Defines the number of each type of group in each component - - residual_helmholtz_contributions (list[str]) - List of methods from the specified saft_source representing contributions to the Helmholtz energy that are functions of density, temperature, and composition + Temperature value is initially defined as NaN for a placeholder until + temperature dependent attributes are initialized by using a method of this + class. + + - molecular_composition (numpy.ndarray) - :math:`\nu_{i,k}/k_B`. Array + containing the number of components by the number of bead types. Defines the + number of each type of group in each component + - residual_helmholtz_contributions (list[str]) - List of methods from the + specified saft_source representing contributions to the Helmholtz energy that + are functions of density, temperature, and composition - Aideal_method (str) - Method defined in SAFT variant - massi (list) - List of molecular weight for each group in ``beads`` array - - flag_assoc (bool) - flag indicating whether there is an association site contribution to the Helmholtz energy - - sitenames (list[str]) - List of sitenames for association site interactions. This array is extracted from ``bead_library`` entries - - nk (numpy.ndarray) - A matrix of (Nbeads x Nsites) Contains for each bead the number of each type of site - - epsilonHB (numpy.ndarray) - Optional, Interaction energy between each bead and association site. + - flag_assoc (bool) - flag indicating whether there is an association site + contribution to the Helmholtz energy + - sitenames (list[str]) - List of sitenames for association site interactions. + This array is extracted from ``bead_library`` entries + - nk (numpy.ndarray) - A matrix of (Nbeads x Nsites) Contains for each bead the + number of each type of site + - epsilonHB (numpy.ndarray) - Optional, Interaction energy between each bead and + association site. - Kklab (numpy.ndarray) - Optional, Bonding volume between each association site - rc_klab, Optional, Cutoff distance for association sites - rd_klab, Optional, Association site position - - reduction_ratio (float) - Reduced distance of the sites from the center of the sphere of interaction. This value is used when site position, ``eos_dict['rd_klab'] == None``. - + - reduction_ratio (float) - Reduced distance of the sites from the center of the + sphere of interaction. This value is used when site position, + ``eos_dict['rd_klab'] == None``. + """ def __init__( @@ -192,7 +235,7 @@ def __init__( for res in self.eos_dict["residual_helmholtz_contributions"]: setattr(self, res, getattr(self.saft_source, res)) - if Aideal_method != None: + if Aideal_method is not None: logger.info( "Switching Aideal method from {} to {}.".format( self.eos_dict["Aideal_method"], Aideal_method @@ -230,10 +273,10 @@ def __init__( self.eos_dict["reduction_ratio"] = kwargs["reduction_ratio"] # Initiate association site terms - self.eos_dict["sitenames"], self.eos_dict["nk"], self.eos_dict[ - "flag_assoc" - ] = Aassoc.initiate_assoc_matrices( - self.beads, self.bead_library, self.eos_dict["molecular_composition"] + self.eos_dict["sitenames"], self.eos_dict["nk"], self.eos_dict["flag_assoc"] = ( + Aassoc.initiate_assoc_matrices( + self.beads, self.bead_library, self.eos_dict["molecular_composition"] + ) ) assoc_output = Aassoc.calc_assoc_matrices( self.beads, @@ -250,7 +293,7 @@ def __init__( if self.eos_dict["flag_assoc"]: self.T = None - if combining_rules != None: + if combining_rules is not None: logger.info("Accepted new combining rule definitions") self.saft_source.combining_rules.update(combining_rules) self.parameter_refresh() @@ -258,9 +301,9 @@ def __init__( def residual_helmholtz_energy(self, rho, T, xi): r""" Return a vector of residual Helmholtz energy. - + :math:`\frac{A^{res}}{N k_{B} T}` - + Parameters ---------- rho : numpy.ndarray @@ -270,7 +313,7 @@ def residual_helmholtz_energy(self, rho, T, xi): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- Ares : numpy.ndarray @@ -278,8 +321,9 @@ def residual_helmholtz_energy(self, rho, T, xi): """ if len(xi) != self.number_of_components: raise ValueError( - "Number of components in mole fraction list, {}, doesn't match self.number_of_components, {}".format( - len(xi), self.number_of_components + "Number of components in mole fraction list, {}, ".format(len(xi)) + + "doesn't match self.number_of_components, {}".format( + self.number_of_components ) ) @@ -300,9 +344,9 @@ def residual_helmholtz_energy(self, rho, T, xi): def helmholtz_energy(self, rho, T, xi): r""" Return a vector of Helmholtz energy. - + :math:`\frac{A}{N k_{B} T}` - + Parameters ---------- rho : numpy.ndarray @@ -311,7 +355,7 @@ def helmholtz_energy(self, rho, T, xi): Temperature of the system [K] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- A : numpy.ndarray @@ -319,8 +363,9 @@ def helmholtz_energy(self, rho, T, xi): """ if len(xi) != self.number_of_components: raise ValueError( - "Number of components in mole fraction list, {}, doesn't match self.number_of_components, {}".format( - len(xi), self.number_of_components + "Number of components in mole fraction list, {}, ".format(len(xi)) + + "doesn't match self.number_of_components, {}".format( + self.number_of_components ) ) @@ -335,9 +380,9 @@ def helmholtz_energy(self, rho, T, xi): def Aideal(self, rho, T, xi, method="Abroglie"): r""" Return a vector of ideal contribution of Helmholtz energy. - + :math:`\frac{A^{ideal}}{N k_{B} T}` - + Parameters ---------- rho : numpy.ndarray @@ -347,8 +392,10 @@ def Aideal(self, rho, T, xi, method="Abroglie"): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 method : str, Optional, default=Abroglie - The function name of the method to calculate the ideal contribution of the Helmholtz energy. To add a new one, add a function to: despasito.equations_of_state.saft.Aideal.py - + The function name of the method to calculate the ideal contribution of + the Helmholtz energy. To add a new one, add a function to: + despasito.equations_of_state.saft.Aideal.py + Returns ------- Aideal : numpy.ndarray @@ -357,8 +404,9 @@ def Aideal(self, rho, T, xi, method="Abroglie"): """ if len(xi) != self.number_of_components: raise ValueError( - "Number of components in mole fraction list, {}, doesn't match self.number_of_components, {}".format( - len(xi), self.number_of_components + "Number of components in mole fraction list, {}, ".format(len(xi)) + + "doesn't match self.number_of_components, {}".format( + self.number_of_components ) ) @@ -371,9 +419,9 @@ def Aideal(self, rho, T, xi, method="Abroglie"): def Aassoc(self, rho, T, xi): r""" Return a vector of association site contribution of Helmholtz energy. - + :math:`\frac{A^{association}}{N k_{B} T}` - + Parameters ---------- rho : numpy.ndarray @@ -382,7 +430,7 @@ def Aassoc(self, rho, T, xi): Temperature of the system [K] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- Aassoc : numpy.ndarray @@ -392,8 +440,9 @@ def Aassoc(self, rho, T, xi): if self.eos_dict["flag_assoc"]: if len(xi) != self.number_of_components: raise ValueError( - "Number of components in mole fraction list, {}, doesn't match self.number_of_components, {}".format( - len(xi), self.number_of_components + "Number of components in mole fraction list, {}, ".format(len(xi)) + + "doesn't match self.number_of_components, {}".format( + self.number_of_components ) ) @@ -412,7 +461,9 @@ def Aassoc(self, rho, T, xi): for key in keys: if key in self.eos_dict: opts[key] = self.eos_dict[key] - self.eos_dict["Kijklab"] = self.saft_source.calc_Kijklab(T, self.eos_dict["rc_klab"], **opts) + self.eos_dict["Kijklab"] = self.saft_source.calc_Kijklab( + T, self.eos_dict["rc_klab"], **opts + ) self.T = T Kklab = self.eos_dict["Kijklab"] Ktype = "ijklab" @@ -433,7 +484,7 @@ def Aassoc(self, rho, T, xi): Fklab, Kklab, gr_assoc, - method_stat=self.method_stat + method_stat=self.method_stat, ) # Compute A_assoc @@ -449,7 +500,10 @@ def Aassoc(self, rho, T, xi): ) else: - logger.warning("Association Site contribution was called when the appropriate parameters were not provided. This should not occur.") + logger.warning( + "Association Site contribution was called when the appropriate " + "parameters were not provided. This should not occur." + ) Assoc_contribution = None return Assoc_contribution @@ -457,7 +511,7 @@ def Aassoc(self, rho, T, xi): def pressure(self, rho, T, xi, step_size=1e-6): """ Compute pressure given system information. - + Parameters ---------- rho : numpy.ndarray @@ -466,16 +520,18 @@ def pressure(self, rho, T, xi, step_size=1e-6): Temperature of the system [K] xi : list[float] Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- P : numpy.ndarray - Array of pressure values [Pa] associated with each density and so equal in length + Array of pressure values [Pa] associated with each density and so equal in + length """ if len(xi) != self.number_of_components: raise ValueError( - "Number of components in mole fraction list, {}, doesn't match self.number_of_components, {}".format( - len(xi), self.number_of_components + "Number of components in mole fraction list, {}, ".format(len(xi)) + + "doesn't match self.number_of_components, {}".format( + self.number_of_components ) ) @@ -484,15 +540,14 @@ def pressure(self, rho, T, xi, step_size=1e-6): P_tmp = gtb.central_difference( rho, self.helmholtz_energy, args=(T, xi), step_size=step_size, relative=True ) - pressure = P_tmp * T * constants.R * rho ** 2 + pressure = P_tmp * T * constants.R * rho**2 return pressure def fugacity_coefficient(self, P, rho, xi, T, dy=1e-5, log_method=True): - """ Compute fugacity coefficient. - + Parameters ---------- P : float @@ -504,8 +559,9 @@ def fugacity_coefficient(self, P, rho, xi, T, dy=1e-5, log_method=True): xi : list[float] Mole fraction of each component, sum(xi) should equal 1.0 log_method : bool, Optional, default=False - Choose to use a log transform in central difference method. This allows easier calculations for very small numbers. - + Choose to use a log transform in central difference method. This allows + easier calculations for very small numbers. + Returns ------- fugacity_coefficient : numpy.ndarray @@ -513,8 +569,9 @@ def fugacity_coefficient(self, P, rho, xi, T, dy=1e-5, log_method=True): """ if len(xi) != self.number_of_components: raise ValueError( - "Number of components in mole fraction list, {}, doesn't match self.number_of_components, {}".format( - len(xi), self.number_of_components + "Number of components in mole fraction list, {}, ".format(len(xi)) + + "doesn't match self.number_of_components, {}".format( + self.number_of_components ) ) @@ -546,10 +603,9 @@ def fugacity_coefficient(self, P, rho, xi, T, dy=1e-5, log_method=True): return phi def density_max(self, xi, T, maxpack=0.65): - """ Estimate the maximum density based on the hard sphere packing fraction. - + Parameters ---------- xi : list[float] @@ -558,7 +614,7 @@ def density_max(self, xi, T, maxpack=0.65): Temperature of the system [K] maxpack : float, Optional, default=0.65 Maximum packing fraction - + Returns ------- max_density : float @@ -566,8 +622,9 @@ def density_max(self, xi, T, maxpack=0.65): """ if len(xi) != self.number_of_components: raise ValueError( - "Number of components in mole fraction list, {}, doesn't match self.number_of_components, {}".format( - len(xi), self.number_of_components + "Number of components in mole fraction list, {}, ".format(len(xi)) + + "doesn't match self.number_of_components, {}".format( + self.number_of_components ) ) @@ -577,21 +634,26 @@ def density_max(self, xi, T, maxpack=0.65): def check_bounds(self, parameter, param_name, bounds): """ - Ensures that bounds given for parameter are within the range of feasibility defined in this class. If the bounds are outside of the allowed range, they are adjusted. - + Ensures that bounds given for parameter are within the range of feasibility + defined in this class. If the bounds are outside of the allowed range, they + are adjusted. + Parameters ---------- parameter : str - Parameter type to be fit (e.g. for 'epsilon_CO2', this argument will be 'epsilon'). + Parameter type to be fit (e.g. for 'epsilon_CO2', this argument will be + 'epsilon'). param_name : str - Full parameter string to be fit. See EOS documentation for supported parameter names. + Full parameter string to be fit. See EOS documentation for supported + parameter names. bounds : list Upper and lower bound for given parameter type - + Returns ------- bounds : list - A screened and possibly corrected low and a high value for the parameter, param_name + A screened and possibly corrected low and a high value for the parameter, + param_name """ # Remove association site names @@ -604,14 +666,18 @@ def update_parameter(self, param_name, bead_names, param_value): r""" Update a single parameter value during parameter fitting process. - To refresh those parameters that are dependent on to bead_library or cross_library, use method ``parameter_refresh``. - + To refresh those parameters that are dependent on to bead_library or + cross_library, use method ``parameter_refresh``. + Parameters ---------- param_name : str - Parameter to be fit. See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + Parameter to be fit. See EOS documentation for supported parameter names. + Cross interaction parameter names should be composed of parameter name and + the other bead type, separated by an underscore (e.g. epsilon_CO2). bead_names : list - Bead names to be changed. For a self interaction parameter, the length will be one, for a cross interaction parameter, the length will be two. + Bead names to be changed. For a self interaction parameter, the length + will be one, for a cross interaction parameter, the length will be two. param_value : float Value of parameter """ @@ -619,7 +685,8 @@ def update_parameter(self, param_name, bead_names, param_value): parameter_list = param_name.split("-") if len(parameter_list) > 1 and len(parameter_list[1:]) != 2: raise ValueError( - "Sitenames should be two different sites in the list: {}. You gave: {}".format( + "Sitenames should be two different sites in the list:" + + " {}. You gave: {}".format( self.eos_dict["sitenames"], ", ".join(parameter_list[1:]) ) ) @@ -627,10 +694,12 @@ def update_parameter(self, param_name, bead_names, param_value): super().update_parameter(param_name, bead_names, param_value) def parameter_refresh(self): - r""" + r""" To refresh dependent parameters - - Those parameters that are dependent on bead_library and cross_library attributes **must** be updated by running this function after all parameters have been adjusted with ``update_parameters`` method. + + Those parameters that are dependent on bead_library and cross_library attributes + **must** be updated by running this function after all parameters have been + adjusted with ``update_parameters`` method. """ @@ -651,7 +720,7 @@ def parameter_refresh(self): def _check_density(self, rho): r""" This function checks the attributes of the density array - + Parameters ---------- rho : numpy.ndarray @@ -660,7 +729,7 @@ def _check_density(self, rho): if np.isscalar(rho): rho = np.array([rho]) - elif type(rho) != np.ndarray: + elif not isinstance(rho, np.ndarray): rho = np.array(rho) if len(np.shape(rho)) == 2: rho = rho[0] diff --git a/despasito/equations_of_state/saft/saft_toolbox.py b/despasito/equations_of_state/saft/saft_toolbox.py index ea24ba9..3c7e67c 100644 --- a/despasito/equations_of_state/saft/saft_toolbox.py +++ b/despasito/equations_of_state/saft/saft_toolbox.py @@ -1,6 +1,7 @@ """ General functions that are applicable to multiple SAFT variants """ + import numpy as np import logging @@ -12,9 +13,9 @@ def calc_hard_sphere_matricies(T, sigmakl, bead_library, beads, Cprefactor_funcion): r""" Computes matrix of hard sphere interaction parameters dkk, dkl, and x0kl. - + This does not include function specific or association terms. - + Parameters ---------- T : float @@ -22,19 +23,23 @@ def calc_hard_sphere_matricies(T, sigmakl, bead_library, beads, Cprefactor_funci sigmakl : numpy.ndarray Matrix of Mie diameter for groups (k,l) bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters: - - - epsilon: :math:`\epsilon_{k,k}/k_B`, Energy well depth scaled by Boltzmann constant + A dictionary where bead names are the keys to access EOS self interaction + parameters: + + - epsilon: :math:`\epsilon_{k,k}/k_B`, Energy well depth scaled by Boltzmann + constant - sigma: :math:`\sigma_{k,k}`, Size parameter [m] - mass: Bead mass [kg/mol] - - lambdar: :math:`\lambda^{r}_{k,k}`, Exponent of repulsive term between groups of type k - - lambdaa: :math:`\lambda^{a}_{k,k}`, Exponent of attractive term between groups of type k - + - lambdar: :math:`\lambda^{r}_{k,k}`, Exponent of repulsive term between groups + of type k + - lambdaa: :math:`\lambda^{a}_{k,k}`, Exponent of attractive term between + groups of type k + beads : list[str] List of unique bead names used among components Cprefactor_funcion : function Function used to calculate prefactor for potential - + Returns ------- dkl : numpy.ndarray @@ -70,9 +75,9 @@ def calc_hard_sphere_matricies(T, sigmakl, bead_library, beads, Cprefactor_funci def _dkk_int(r, Ce_kT, sigma, lambdar, lambdaa): r""" Return integrand used to calculate the hard sphere diameter. - + :math:`d_{k,k}` of a group k. See eq. 10. - + Parameters ---------- r : numpy.ndarray @@ -85,7 +90,7 @@ def _dkk_int(r, Ce_kT, sigma, lambdar, lambdaa): :math:`\lambda^{r}_{k,k}`, Exponent of repulsive term between groups of type k lambdaa : float :math:`\lambda^{r}_{k,k}`, Exponent of repulsive term between groups of type k - + Returns ------- dkk_int_tmp : numpy.ndarray @@ -101,8 +106,9 @@ def _dkk_int(r, Ce_kT, sigma, lambdar, lambdaa): def calc_dkk(epsilon, sigma, T, Cprefactor, lambdar, lambdaa=6.0): r""" - Calculates hard sphere diameter of a group k, :math:`d_{k,k}`. Defined in eq. 10. using a 40 point Gauss Legendre - + Calculates hard sphere diameter of a group k, :math:`d_{k,k}`. Defined in eq. 10. + using a 40 point Gauss Legendre + Parameters ---------- epsilon : float @@ -117,7 +123,7 @@ def calc_dkk(epsilon, sigma, T, Cprefactor, lambdar, lambdaa=6.0): :math:`\lambda^{r}_{k,k}`, Exponent of repulsive term between groups of type k lambdaa : float, Optional, default=6.0 :math:`\lambda^{r}_{k,k}`, Exponent of repulsive term between groups of type k - + Returns ------- dkk : float @@ -226,30 +232,35 @@ def calc_composition_dependent_variables( xi, molecular_composition, bead_library, beads ): r""" - Calculate the factor for converting molar density to bead density and molecular mole fractions to bead fractions. - + Calculate the factor for converting molar density to bead density and molecular + mole fractions to bead fractions. + Parameters ---------- xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 molecular_composition : numpy.array - :math:`\nu_{i,k}/k_B`, Array of number of components by number of bead types. Defines the number of each type of group in each component. + :math:`\nu_{i,k}/k_B`, Array of number of components by number of bead types. + Defines the number of each type of group in each component. Defined for eq. 11. Note that indices are flipped from definition in reference. bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters: - + A dictionary where bead names are the keys to access EOS self interaction + parameters: + - Vks: :math:`V_{k,s}`, Number of groups, k, in component - Sk: Optional, :math:`S_{k}`, Shape parameter of group k - + beads : list[str] List of unique bead names used among components - + Returns ------- Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 xskl : numpy.ndarray - Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that of bead l + Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that + of bead l """ # compute Conversion factor @@ -287,18 +298,20 @@ def calc_composition_dependent_variables( def calc_zetaxstar(rho, Cmol2seg, xskl, sigmakl): r""" Calculate matrix of hypothetical packing fraction based on sigma for groups (k,l) - + Parameters ---------- rho : numpy.ndarray Number density of system [:math:`mol/m^3`] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 xskl : numpy.ndarray - Matrix of mole fractions of bead (i.e. segment or group) k multiplied by bead l + Matrix of mole fractions of bead (i.e. segment or group) k multiplied by + bead l sigmakl : numpy.ndarray Matrix of Mie diameter for groups (k,l) - + Returns ------- zetaxstar : numpy.ndarray @@ -309,7 +322,7 @@ def calc_zetaxstar(rho, Cmol2seg, xskl, sigmakl): zetaxstar = ( rho * Cmol2seg - * ((np.pi / 6.0) * np.sum(xskl * (sigmakl ** 3 * constants.molecule_per_nm3))) + * ((np.pi / 6.0) * np.sum(xskl * (sigmakl**3 * constants.molecule_per_nm3))) ) return zetaxstar @@ -317,28 +330,31 @@ def calc_zetaxstar(rho, Cmol2seg, xskl, sigmakl): def calc_zetax(rho, Cmol2seg, xskl, dkl): r""" - Calculate matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Calculate matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Parameters ---------- rho : numpy.ndarray Number density of system [:math:`mol/m^3`] Cmol2seg : float - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 + Conversion factor from from molecular number density, :math:`\rho`, to segment + (i.e. group) number density, :math:`\rho_S`. Shown in eq. 13 xskl : numpy.ndarray Matrix of mole fractions of bead (i.e. segment or group) k multiplied by bead l dkl : numpy.ndarray Matrix of hardsphere diameters for groups (k,l) - + Returns ------- zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) """ zetax = ( rho * Cmol2seg - * ((np.pi / 6.0) * np.sum(xskl * (dkl ** 3 * constants.molecule_per_nm3))) + * ((np.pi / 6.0) * np.sum(xskl * (dkl**3 * constants.molecule_per_nm3))) ) return zetax @@ -346,20 +362,23 @@ def calc_zetax(rho, Cmol2seg, xskl, dkl): def calc_KHS(zetax): r""" - Calculate (length of density array) isothermal compressibility of system with packing fraction zetax - + Calculate (length of density array) isothermal compressibility of system with + packing fraction zetax + Parameters ---------- zetax : numpy.ndarray - Matrix of hypothetical packing fraction based on hard sphere diameter for groups (k,l) - + Matrix of hypothetical packing fraction based on hard sphere diameter for + groups (k,l) + Returns ------- KHS : numpy.ndarray - (length of densities) isothermal compressibility of system with packing fraction zetax + (length of densities) isothermal compressibility of system with packing + fraction zetax """ KHS = ((1.0 - zetax) ** 4) / ( - 1.0 + (4.0 * zetax) + (4.0 * (zetax ** 2)) - (4.0 * (zetax ** 3)) + (zetax ** 4) + 1.0 + (4.0 * zetax) + (4.0 * (zetax**2)) - (4.0 * (zetax**3)) + (zetax**4) ) return KHS diff --git a/despasito/equations_of_state/saft/saft_variant_example.py b/despasito/equations_of_state/saft/saft_variant_example.py index 28c9f91..dcd1a53 100644 --- a/despasito/equations_of_state/saft/saft_variant_example.py +++ b/despasito/equations_of_state/saft/saft_variant_example.py @@ -1,9 +1,9 @@ # -- coding: utf8 -- r""" EOS object for SAFT-:math:`\gamma`-SW - - Equations referenced in this code are from Lymperiadis, A. et. al, J. Chem. Phys. 127, 234903 (2007) - + + Equations referenced in this code are from Lymperiadis, A. et. al, J. Chem. Phys. + 127, 234903 (2007) """ import numpy as np @@ -13,10 +13,10 @@ from despasito.equations_of_state import constants import despasito.equations_of_state.saft.saft_toolbox as stb from despasito.equations_of_state.saft import Aassoc +from despasito.equations_of_state import method_stat logger = logging.getLogger(__name__) -from despasito.equations_of_state import method_stat if not method_stat.cython and not method_stat.numba: pass @@ -35,7 +35,6 @@ class SaftType: - r""" Object of SAFT variant @@ -44,34 +43,50 @@ class SaftType: beads : list[str] List of unique bead names used among components molecular_composition : numpy.ndarray - :math:`\\nu_{i,k}/k_B`. Array of number of components by number of bead types. Defines the number of each type of group in each component. + :math:`\\nu_{i,k}/k_B`. Array of number of components by number of bead types. + Defines the number of each type of group in each component. bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters + A dictionary where bead names are the keys to access EOS self interaction + parameters cross_library : dict, Optional, default={} - Optional library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. If this matrix isn't provided, the SAFT mixing rules are used. + Optional library of bead cross interaction parameters. As many or as few of the + desired parameters may be defined for whichever group combinations are desired. + If this matrix isn't provided, the SAFT mixing rules are used. Attributes ---------- beads : list[str] List of unique bead names used among components bead_library : dict - A dictionary where bead names are the keys to access EOS self interaction parameters. See **Parameters** section. + A dictionary where bead names are the keys to access EOS self interaction + parameters. See **Parameters** section. cross_library : dict - Library of bead cross interaction parameters. As many or as few of the desired parameters may be defined for whichever group combinations are desired. See **Parameters** section. + Library of bead cross interaction parameters. As many or as few of the desired + parameters may be defined for whichever group combinations are desired. See + **Parameters** section. Aideal_method : str - The default functional form of the ideal gas contribution of the Helmholtz energy + The default functional form of the ideal gas contribution of the Helmholtz + energy residual_helmholtz_contributions : list[str] - List of methods from the specified saft_source representing contributions to the Helmholtz energy that are functions of density, temperature, and composition. + List of methods from the specified saft_source representing contributions to + the Helmholtz energy that are functions of density, temperature, and + composition. parameter_types : list[str] - This list of parameter names for the specific SAFT variant. + This list of parameter names for the specific SAFT variant. parameter_bound_extreme : dict - With each parameter name as an entry representing a list with the minimum and maximum feasible parameter value. + With each parameter name as an entry representing a list with the minimum and + maximum feasible parameter value. combining_rules : dict - Contains functional form and additional information for calculating cross interaction parameters that are not found in `cross_library`. Function must be one of those contained in :mod:`~despasito.equations_of_state.combining_rule_types`. + Contains functional form and additional information for calculating cross + interaction parameters that are not found in `cross_library`. Function must be + one of those contained in + :mod:`~despasito.equations_of_state.combining_rule_types`. eos_dict : dict - Dictionary of parameters and specific settings + Dictionary of parameters and specific settings - - molecular_composition (numpy.ndarray) - :math:`\\nu_{i,k}/k_B`. Array of number of components by number of bead types. Defines the number of each type of group in each component. + - molecular_composition (numpy.ndarray) - :math:`\\nu_{i,k}/k_B`. Array of + number of components by number of bead types. Defines the number of each type + of group in each component. ncomp : int Number of components in the system @@ -82,30 +97,54 @@ class SaftType: def __init__(self, **kwargs): - ####### The Following lines are **MANDATORY** but can be edited as needed + # ###### The Following lines are **MANDATORY** but can be edited as needed - # Standard attributes of the EosTemplate are 'density_max' and 'parameter_refresh', while all saft versions require 'calc_gr_assoc' for calculating association sites + # Standard attributes of the EosTemplate are 'density_max' and + # 'parameter_refresh', while all saft versions require 'calc_gr_assoc' for + # calculating association sites - # This keyword is used by the saft.py class and subsequently the `Aideal_contribution` function. If a user desired a different default method of calculating the ideal contribution, add a new function to Aideal.py + # This keyword is used by the saft.py class and subsequently the + # `Aideal_contribution` function. If a user desired a different default method + # of calculating the ideal contribution, add a new function to Aideal.py self.Aideal_method = "Abroglie" - # This list of strings are the bead types that the parameter update method can expect. + # This list of strings are the bead types that the parameter update method can + # expect. # The bead library should use these same parameter names. # Parameter names should not contain '-' or '_' self.parameter_types = ["example"] - # This dictionary will contain the feasible bounds of the parameters listed in 'parameter_types'. Bounds are necessary for each type. + # This dictionary will contain the feasible bounds of the parameters listed in + # 'parameter_types'. Bounds are necessary for each type. self.parameter_bound_extreme = {"example": [0, 1]} - # When calculating the cross-interaction term between two beads, the parameter name and the combining rule type should be listed here. The combining rule keyword can be any that are supported in saft_toolbox.combining_rules. This attribute must exist. + # When calculating the cross-interaction term between two beads, the parameter + # name and the combining rule type should be listed here. The combining rule + # keyword can be any that are supported in saft_toolbox.combining_rules. This + # attribute must exist. self.combining_rules = {"example": {"function": "mean"}} - # This list contains the Helmholtz energy contributions contained in this class below. The class in saft.py will add these as it's own attributes to calculate the total Helmholtz energy. + # This list contains the Helmholtz energy contributions contained in this class + # below. The class in saft.py will add these as it's own attributes to + # calculate the total Helmholtz energy. self.residual_helmholtz_contributions = ["Amonomer", "Achain"] - # Note that these strings must represent methods for the Helmholtz contribution below and are added to Aideal and Aassoc in the main SAFT class. If a function is to be optimized with Cython or Numba, an extensions library can be added to the 'compiled_modules' directory. A python version with the same function names should also be present as well. The import if-structure at the beginning of this module is then used to import the desired form, see gamma_mie.py for an example. - # When deciding whether to include an additional function as a class method, or if it should simply be imported from a library. Think about whether accessing that function would be nice when handling an EOS object in a python script. For instance, the 'reduced_density', 'effective_packing_fraction', 'Ahard_sphere', 'Afirst_order', and other methods in gamma_mie.py would be nice to have as attributes. - - # Now we start processing the given variables. The following three attributes are always needs for the saft.py class. If other inputs are needed for the specific SAFT type at hand, feel free to add them to this list. + # Note that these strings must represent methods for the Helmholtz + # contribution below and are added to Aideal and Aassoc in the main SAFT class. + # If a function is to be optimized with Cython or Numba, an extensions library + # can be added to the 'compiled_modules' directory. A python version with the + # same function names should also be present as well. The import if-structure + # at the beginning of this module is then used to import the desired form, see + # gamma_mie.py for an example. + # When deciding whether to include an additional function as a class method, or + # if it should simply be imported from a library. Think about whether accessing + # that function would be nice when handling an EOS object in a python script. + # For instance, the 'reduced_density', 'effective_packing_fraction', + # 'Ahard_sphere', 'Afirst_order', and other methods in gamma_mie.py would be + # nice to have as attributes. + + # Now we start processing the given variables. The following three attributes + # are always needs for the saft.py class. If other inputs are needed for the + # specific SAFT type at hand, feel free to add them to this list. if not hasattr(self, "eos_dict"): self.eos_dict = {} @@ -114,7 +153,7 @@ def __init__(self, **kwargs): if key not in kwargs: raise ValueError( "The one of the following inputs is missing: {}".format( - ", ".join(tmp) + ", ".join(needed_attributes) ) ) elif key == "molecular_composition": @@ -123,7 +162,8 @@ def __init__(self, **kwargs): elif not hasattr(self, key): setattr(self, key, kwargs[key]) - # Check bead_library to be sure all parameters are present. If one is missing that has a standard default then that is added + # Check bead_library to be sure all parameters are present. If one is missing + # that has a standard default then that is added self._parameter_defaults = { "epsilon": None, "lambdar": None, @@ -141,17 +181,23 @@ def __init__(self, **kwargs): else: self.cross_library = kwargs["cross_library"] - ###### The following lines are *OPTIONAL* and are completely for internal use for this specific SAFT type and aren't mandatory + # ##### The following lines are *OPTIONAL* and are completely for internal use + # for this specific SAFT type and aren't mandatory - # Initialize composition attribute. This is for composition dependent properties. By recording this, we can avoid recalculating those parameters unnecessarily. In saft-gamma_mie we also have temperature dependent parameters and so self.T is included. + # Initialize composition attribute. This is for composition dependent + # properties. By recording this, we can avoid recalculating those + # parameters unnecessarily. In saft-gamma_mie we also have temperature + # dependent parameters and so self.T is included. if not hasattr(self, "xi"): self.xi = np.nan - # These are initialized for loops used in some of the methods below. Depending on how you choose to break things up, these might not be needed. + # These are initialized for loops used in some of the methods below. Depending + # on how you choose to break things up, these might not be needed. if not hasattr(self, "nbeads") or not hasattr(self, "ncomp"): self.ncomp, self.nbeads = np.shape(self.eos_dict["molecular_composition"]) - # Initiate cross interaction terms, as mentioned above, some combining rules use a particular combination of parameters and these are handled here. + # Initiate cross interaction terms, as mentioned above, some combining rules use + # a particular combination of parameters and these are handled here. output = tb.cross_interaction_from_dict( self.beads, self.bead_library, @@ -162,7 +208,10 @@ def __init__(self, **kwargs): self.eos_dict["epsilon_kl"] = output["epsilon"] self.eos_dict["lambda_kl"] = output["lambda"] - # This optional keyword can be passed in an input file as "eos_num_rings". If your chosen version of SAFT needs special keyword passed that don't fall under a category above. Make a note in the doc string to pass it as "eos_somekeyword" + # This optional keyword can be passed in an input file as "eos_num_rings". If + # your chosen version of SAFT needs special keyword passed that don't fall + # under a category above. Make a note in the doc string to pass it as + # "eos_somekeyword" if "num_rings" in kwargs: self.eos_dict["num_rings"] = kwargs["num_rings"] logger.info( @@ -176,10 +225,12 @@ def __init__(self, **kwargs): def Amonomer(self, rho, T, xi): r""" - ** Example of Helmholtz contribution to put in residual_helmholtz_contributions ** + ** Example of Helmholtz contribution to put in + residual_helmholtz_contributions ** + + Outputs the monomer contribution of the Helmholtz energy + :math:`A^{mono.}/Nk_{b}T`. - Outputs the monomer contribution of the Helmholtz energy :math:`A^{mono.}/Nk_{b}T`. - Parameters ---------- rho : numpy.ndarray @@ -188,7 +239,7 @@ def Amonomer(self, rho, T, xi): Temperature of the system [K] xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 - + Returns ------- Amonomer : numpy.ndarray @@ -197,9 +248,9 @@ def Amonomer(self, rho, T, xi): if np.all(rho > self.density_max(xi, T)): raise ValueError( - "Density values should not all be greater than {}, or calc_Amono will fail in log calculation.".format( - self.density_max(xi, T) - ) + "Density values should not all be greater than " + + "{}, or calc_Amono".format(self.density_max(xi, T)) + + " will fail in log calculation." ) rho = self._check_density(rho) @@ -216,12 +267,11 @@ def Amonomer(self, rho, T, xi): return Amonomer def density_max(self, xi, T, maxpack=0.65): - """ ** Mandatory ** Estimate the maximum density based on the hard sphere packing fraction. - + Parameters ---------- xi : list[float] @@ -230,7 +280,7 @@ def density_max(self, xi, T, maxpack=0.65): Temperature of the system [K] maxpack : float, Optional, default=0.65 Maximum packing fraction - + Returns ------- max_density : float @@ -258,8 +308,9 @@ def calc_gr_assoc(self, rho, T, xi, Ktype="ijklab"): r""" ** Mandatory ** - Reference fluid pair correlation function used in calculating association sites - + Reference fluid pair correlation function used in calculating association + sites + Parameters ---------- rho : numpy.ndarray @@ -269,12 +320,15 @@ def calc_gr_assoc(self, rho, T, xi, Ktype="ijklab"): xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 Ktype : str, Optional, default='ijklab' - Indicates which radial distribution function to return. The only option is 'ijklab': The bonding volume was calculated from self.calc_Kijklab, return gHS_dij) - + Indicates which radial distribution function to return. The only option is + 'ijklab': The bonding volume was calculated from self.calc_Kijklab, return + gHS_dij) + Returns ------- gr : numpy.ndarray - A temperature-density polynomial correlation of the association integral for a Lennard-Jones monomer. This matrix is (len(rho) x Ncomp x Ncomp) + A temperature-density polynomial correlation of the association integral + for a Lennard-Jones monomer. This matrix is (len(rho) x Ncomp x Ncomp) """ rho = self._check_density(rho) @@ -286,21 +340,25 @@ def calc_Kijklab(self, T, rc_klab, rd_klab=None, reduction_ratio=0.25): r""" ** Mandatory ** - Calculation of association site bonding volume, dependent on molecule in addition to group + Calculation of association site bonding volume, dependent on molecule in + addition to group Lymperiadis Fluid Phase Equilibria 274 (2008) 85–104 - + Parameters ---------- T : float - Temperature of the system [K], Note used in this version of saft, but included to allow saft.py to be general + Temperature of the system [K], Note used in this version of saft, but + included to allow saft.py to be general rc_klab : numpy.ndarray - This matrix of cutoff distances for association sites for each site type in each group type + This matrix of cutoff distances for association sites for each site type in + each group type rd_klab : numpy.ndarray, Optional, default=None Position of association site in each group (nbead, nbead, nsite, nsite) reduction_ratio : float, Optional, default=0.25 - Reduced distance of the sites from the center of the sphere of interaction. This value is used when site position, rd_klab is None - + Reduced distance of the sites from the center of the sphere of interaction. + This value is used when site position, rd_klab is None + Returns ------- Kijklab : numpy.ndarray @@ -321,25 +379,31 @@ def calc_Kijklab(self, T, rc_klab, rd_klab=None, reduction_ratio=0.25): return Kijklab def parameter_refresh(self, bead_library, cross_library): - r""" + r""" ** Mandatory ** To refresh dependent parameters - - Those parameters that are dependent on bead_library and cross_library attributes **must** be updated by running this function after all parameters from update_parameters method have been changed. + + Those parameters that are dependent on bead_library and cross_library attributes + **must** be updated by running this function after all parameters from + update_parameters method have been changed. Attributes ---------- alpha : np.array - van der Waals attractive parameter for square-well segments, equal to :math:`\alpha_{k,l}/k_B`. + van der Waals attractive parameter for square-well segments, equal to + :math:`\alpha_{k,l}/k_B`. eos_dict : dict The following entries are updated: - epsilon_kl (numpy.ndarray) - Matrix of well depths for groups (k,l) - sigma_kl (numpy.ndarray) - Matrix of bead diameters (k,l) - - lambda_kl (numpy.ndarray) - Matrix of range of potential well depth (k,l) - - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or group) k multiplied by that of bead l - - Cmol2seg (float) - Conversion factor from from molecular number density, :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. + - lambda_kl (numpy.ndarray) - Matrix of range of potential well + depth (k,l) + - xskl (numpy.ndarray) - Matrix of mole fractions of bead (i.e. segment or + group) k multiplied by that of bead l + - Cmol2seg (float) - Conversion factor from from molecular number density, + :math:`\rho`, to segment (i.e. group) number density, :math:`\rho_S`. """ @@ -359,13 +423,13 @@ def parameter_refresh(self, bead_library, cross_library): self.calc_component_averaged_properties() if not np.any(np.isnan(self.xi)): - self.eos_dict["Cmol2seg"], self.eos_dict[ - "xskl" - ] = stb.calc_composition_dependent_variables( - xi, - self.eos_dict["molecular_composition"], - self.bead_library, - self.beads, + self.eos_dict["Cmol2seg"], self.eos_dict["xskl"] = ( + stb.calc_composition_dependent_variables( + self.xi, + self.eos_dict["molecular_composition"], + self.bead_library, + self.beads, + ) ) self.alphakl = ( 2.0 @@ -379,9 +443,10 @@ def parameter_refresh(self, bead_library, cross_library): def _check_density(self, rho): r""" ** Mandatory ** - - This function checks that the density array is in the correct format for further calculations. - + + This function checks that the density array is in the correct format for + further calculations. + Parameters ---------- rho : numpy.ndarray @@ -395,7 +460,7 @@ def _check_density(self, rho): if np.isscalar(rho): rho = np.array([rho]) - elif type(rho) != np.ndarray: + elif not isinstance(rho, np.ndarray): rho = np.array(rho) if len(np.shape(rho)) == 2: rho = rho[0] diff --git a/despasito/examples/saft_gamma_mie/decane_helmholtz/run_test.py b/despasito/examples/saft_gamma_mie/decane_helmholtz/run_test.py index e111ecd..928cf63 100644 --- a/despasito/examples/saft_gamma_mie/decane_helmholtz/run_test.py +++ b/despasito/examples/saft_gamma_mie/decane_helmholtz/run_test.py @@ -41,7 +41,6 @@ Am1 = AHS + A1 + A2 + A3 Am2 = Eos.Amonomer(rho, T, xi) print( - "The Monomer Contribution for Helmholtz Energy: {},\n equals the sum of its components: {}".format( - Am2, Am1 - ) + "The Monomer Contribution for Helmholtz Energy: {},\n".format(Am2) + + " equals the sum of its components: {}".format(Am1) ) diff --git a/despasito/examples/saft_gamma_mie/hexane_heptane_activity_coeff/hexane_heptane_test.py b/despasito/examples/saft_gamma_mie/hexane_heptane_activity_coeff/hexane_heptane_test.py index 48e49e8..44eee6e 100755 --- a/despasito/examples/saft_gamma_mie/hexane_heptane_activity_coeff/hexane_heptane_test.py +++ b/despasito/examples/saft_gamma_mie/hexane_heptane_activity_coeff/hexane_heptane_test.py @@ -1,4 +1,3 @@ - import numpy as np import despasito @@ -17,17 +16,17 @@ ) output = thermo.thermo( - Eos, - calculation_type="liquid_properties", - Tlist=320.0, - Plist=1e+5, + Eos, + calculation_type="liquid_properties", + Tlist=320.0, + Plist=1e5, xilist=np.array([0.4, 0.6]), ) -print("Thermo Output",output) -args = (output["rhol"][0],320.0,[0.4, 0.6]) +print("Thermo Output", output) +args = (output["rhol"][0], 320.0, [0.4, 0.6]) print("Helmholtz Contributions:") -print(" Ideal: ",Eos.Aideal(*args)) -print(" Monomer: ",Eos.saft_source.Amonomer(*args)) -print(" Chain: ",Eos.saft_source.Achain(*args)) -print(" Aassoc: ",Eos.Aassoc(*args)) +print(" Ideal: ", Eos.Aideal(*args)) +print(" Monomer: ", Eos.saft_source.Amonomer(*args)) +print(" Chain: ", Eos.saft_source.Achain(*args)) +print(" Aassoc: ", Eos.Aassoc(*args)) diff --git a/despasito/input_output/__init__.py b/despasito/input_output/__init__.py index 139597f..e69de29 100644 --- a/despasito/input_output/__init__.py +++ b/despasito/input_output/__init__.py @@ -1,2 +0,0 @@ - - diff --git a/despasito/input_output/read_input.py b/despasito/input_output/read_input.py index 4fd458e..ecb7b72 100755 --- a/despasito/input_output/read_input.py +++ b/despasito/input_output/read_input.py @@ -11,15 +11,15 @@ def append_data_file_path(input_dict, path="."): r""" - Appends path from command line option to data file(s). - - Parameters - ---------- - input_dict - Dictionary of input data - path: str - relative path to append to existing data files - """ + Appends path from command line option to data file(s). + + Parameters + ---------- + input_dict + Dictionary of input data + path: str + relative path to append to existing data files + """ if "EOSgroup" in input_dict: input_dict["EOSgroup"] = os.path.join(path, input_dict["EOSgroup"]) @@ -39,7 +39,7 @@ def json_to_dict(filename): ---------- filename : str File name and path leading to file (in JSON format) location - + Returns ------- dictionary : dict @@ -54,16 +54,20 @@ def json_to_dict(filename): def extract_calc_data(input_fname, path=".", **thermo_dict): - r""" Parse dictionary from input file in JSON format into a dictionary. - Resulting dictionaries are used for creating the equation of state object, and for passing instructions for thermodynamic calculations. + Resulting dictionaries are used for creating the equation of state object, and for + passing instructions for thermodynamic calculations. Parameters ---------- input_fname : str - The file name of a file in JSON format in the current directory containing (1) the paths to equation of state parameters, (2) :mod:`~despasito.thermodynamics.calculation_types` and inputs for thermodynamic calculations (e.g. density options for :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays`). + The file name of a file in JSON format in the current directory containing + (1) the paths to equation of state parameters, + (2) :mod:`~despasito.thermodynamics.calculation_types` and inputs for + thermodynamic calculations (e.g. density options for + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays`). path : str, Optional, default="." Path to ``input_fname`` thermo_dict @@ -72,14 +76,18 @@ def extract_calc_data(input_fname, path=".", **thermo_dict): Returns ------- eos_dict : dict - Dictionary of keyword arguments representing bead definitions and parameters used to later initialize Eos object. :func:`despasito.equations_of_state.initiate_eos` + Dictionary of keyword arguments representing bead definitions and parameters + used to later initialize Eos object. + :func:`despasito.equations_of_state.initiate_eos` thermo_dict : dict - Dictionary of keyword arguments for thermodynamic calculations or parameter fitting. :func:`despasito.thermodynamics.thermo` + Dictionary of keyword arguments for thermodynamic calculations or parameter + fitting. :func:`despasito.thermodynamics.thermo` output_file : str - Output from calculation. Default is None, but an alternative can be defined as output_file keyword argument. + Output from calculation. Default is None, but an alternative can be defined + as output_file keyword argument. """ - ## Extract dictionary from input file + # Extract dictionary from input file input_dict = json_to_dict(input_fname) # Look for data (library) files in the user-supplied path @@ -90,7 +98,7 @@ def extract_calc_data(input_fname, path=".", **thermo_dict): else: output_file = None - ## Make bead data dictionary for EOS + # Make bead data dictionary for EOS # process input file if "bead_configuration" in input_dict: beads, molecular_composition = process_bead_data( @@ -113,12 +121,7 @@ def extract_calc_data(input_fname, path=".", **thermo_dict): logger.info("No EOScross file specified") # Extract relevant system state inputs - EOS_dict_keys = [ - "bead_configuration", - "EOSgroup", - "EOScross", - "output_file" - ] + EOS_dict_keys = ["bead_configuration", "EOSgroup", "EOScross", "output_file"] for key, value in input_dict.items(): if key.startswith("eos_"): new_key = "_".join(key.split("_")[1:]) @@ -135,9 +138,8 @@ def extract_calc_data(input_fname, path=".", **thermo_dict): if "optimization_parameters" not in thermo_dict: logger.info( - "The following thermo calculation parameters have been provided: {}\n".format( - (", ".join(thermo_dict.keys())) - ) + "The following thermo calculation parameters have been " + "provided: {}\n".format((", ".join(thermo_dict.keys()))) ) else: # parameter fitting thermo_dict = process_param_fit_inputs(thermo_dict) @@ -151,7 +153,8 @@ def extract_calc_data(input_fname, path=".", **thermo_dict): for key, value in thermo_dict["exp_data"].items(): tmp += " {} ({}),".format(key, value["data_class_type"]) logger.info( - "The bead, {}, will have the parameters {}, fit using the following data:\n {}".format( + "The bead, {}, will have the parameters {}, fit using the following " + "data:\n {}".format( thermo_dict["optimization_parameters"]["fit_bead"], thermo_dict["optimization_parameters"]["fit_parameter_names"], tmp, @@ -162,11 +165,11 @@ def extract_calc_data(input_fname, path=".", **thermo_dict): def file2paramdict(filename, delimiter=" "): - r""" Converted text file into a dictionary. - Each line in the input file is a key followed by a value in the resulting dictionary. + Each line in the input file is a key followed by a value in the resulting + dictionary. Parameters ---------- @@ -199,21 +202,23 @@ def file2paramdict(filename, delimiter=" "): def process_bead_data(bead_data): - r""" Process system information from input file. Parameters ---------- bead_data : list[list] - List of molecules in the system. Each molecule is represented as a list of beads and each bead is represented as a list with the bead name followed by an integer with the number of beads in that molecule. + List of molecules in the system. Each molecule is represented as a list of + beads and each bead is represented as a list with the bead name followed by an + integer with the number of beads in that molecule. Returns ------- beads : list[str] List of unique bead names used among components molecular_composition : numpy.ndarray - Array of number of components by number of bead types. Defines the number of each type of group in each component. + Array of number of components by number of bead types. Defines the number of + each type of group in each component. """ # find list of unique beads @@ -234,43 +239,76 @@ def process_bead_data(bead_data): def process_param_fit_inputs(thermo_dict): - r""" Process parameter fitting information. Parameters ---------- thermo_dict : dict - Dictionary of instructions for thermodynamic calculations or parameter fitting. This dictionary is directly from the input file. + Dictionary of instructions for thermodynamic calculations or parameter + fitting. This dictionary is directly from the input file. - optimization_parameters (dict) - Parameters used in basin fitting algorithm - - fit_bead (str) - Name of bead whose parameters are being fit, should be in bead list of bead_configuration - - fit_parameter_names (list[str]) - This list of contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for limitations on supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). - - \*_bounds (list[float]), Optional - This list contains the minimum and maximum of a parameter from fit_parameter_names, represented by the asterisk. See input file instructions for more information. - - parameters_guess (list[float]), Optional - Initial guess in parameters being fit. Should be the same length at fit_parameter_names and contain a reasonable guess for each parameter. If this is not provided, a guess is made based on the type of parameter from the Eos object. - - - *name* (dict) - Dictionary representing a data set that the parameters are fit to. Each dictionary is added to the exp_data dictionary before being passed to the fitting algorithm. Each *name* is used as the key in exp_data. *name* is an arbitrary string used to identify the data set and used later in reporting objective function values during the fitting process. See data type objects for more details. - - - file (str) - File of experimental data - - data_class_type (str) - One of the supported data type objects to fit parameters - - calculation_type (str) - One of the supported thermo calculation types that would be associated with the chosen data_class_type + - fit_bead (str) - Name of bead whose parameters are being fit, should be + in bead list of bead_configuration + - fit_parameter_names (list[str]) - This list of contains the name of the + parameter being fit (e.g. epsilon). See EOS documentation for limitations + on supported parameter names. Cross interaction parameter names should be + composed of parameter name and the other bead type, separated by an + underscore (e.g. epsilon_CO2). + - \*_bounds (list[float]), Optional - This list contains the minimum and + maximum of a parameter from fit_parameter_names, represented by the + asterisk. See input file instructions for more information. + - parameters_guess (list[float]), Optional - Initial guess in parameters + being fit. Should be the same length at fit_parameter_names and contain a + reasonable guess for each parameter. If this is not provided, a guess is + made based on the type of parameter from the Eos object. + + - *name* (dict) - Dictionary representing a data set that the parameters are + fit to. Each dictionary is added to the exp_data dictionary before being passed + to the fitting algorithm. Each *name* is used as the key in exp_data. *name* is + an arbitrary string used to identify the data set and used later in reporting + objective function values during the fitting process. See data type objects for + more details. + + - file (str) - File of experimental data + - data_class_type (str) - One of the supported data type objects to fit + parameters + - calculation_type (str) - One of the supported thermo calculation types + that would be associated with the chosen data_class_type Returns ------- new_thermo_dict : dict - Dictionary of instructions for thermodynamic calculations or parameter fitting. This dictionary is reformatted and includes imported data. Dictionary values below are altered before being passed on, all other key and value sets are blindly passed. + Dictionary of instructions for thermodynamic calculations or parameter fitting. + This dictionary is reformatted and includes imported data. Dictionary values + below are altered before being passed on, all other key and value sets are + blindly passed. - optimization_parameters (dict) - Parameters used in basin fitting algorithm - - fit_bead (str) - Name of bead whose parameters are being fit, should be in bead list of bead_configuration - - fit_parameter_names (list[str]) - This list of contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for restrictions on supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). - - parameters_guess (list[float]), Optional - Initial guess in parameter. If one is not provided, a guess is made based on the type of parameter from Eos object. - - \*_bounds (list[float]), Optional - This list contains the minimum and maximum of a parameter in fit_parameter_names, represented in place of the asterisk. See input file instructions for more information. - - - exp_data (dict) - This dictionary is made up of a dictionary for each data set that the parameters are fit to. Each key is an arbitrary string used to identify the data set and used later in reporting objective function values during the fitting process. See data type objects for more details. - - - data_class_type (obj) - One of the supported data type objects to fit parameters + - fit_bead (str) - Name of bead whose parameters are being fit, should be + in bead list of bead_configuration + - fit_parameter_names (list[str]) - This list of contains the name of the + parameter being fit (e.g. epsilon). See EOS documentation for restrictions + on supported parameter names. Cross interaction parameter names should be + composed of parameter name and the other bead type, separated by an + underscore (e.g. epsilon_CO2). + - parameters_guess (list[float]), Optional - Initial guess in parameter. + If one is not provided, a guess is made based on the type of parameter + from Eos object. + - \*_bounds (list[float]), Optional - This list contains the minimum and + maximum of a parameter in fit_parameter_names, represented in place of the + asterisk. See input file instructions for more information. + + - exp_data (dict) - This dictionary is made up of a dictionary for each data + set that the parameters are fit to. Each key is an arbitrary string used to + identify the data set and used later in reporting objective function values + during the fitting process. See data type objects for more details. + + - data_class_type (obj) - One of the supported data type objects to fit + parameters """ # Initial new dictionary that will have dictionary for extracted data @@ -290,33 +328,47 @@ def process_param_fit_inputs(thermo_dict): ) if not all([test1, test2]): raise ValueError( - "An exp_data dictionary (dictionary with 'data_class_type' key) as well as an optimization_parameters dictionary with 'fit_bead' and 'fit_parameter_names' must be provided." + "An exp_data dictionary (dictionary with 'data_class_type' key) as well as" + " an optimization_parameters dictionary with 'fit_bead' and " + "'fit_parameter_names' must be provided." ) return new_thermo_dict def process_exp_data(exp_data_dict): - r""" - Convert experimental data dictionary into form used by parameter fitting module. + Convert experimental data dictionary into form used by parameter fitting module. Note that there should be one dictionary per data set. Parameters ---------- exp_data : dict - This dictionary is made up of a dictionary for each data set that the parameters are fit to. Each key is an arbitrary string used to identify the data set and used later in reporting objective function values during the fitting process. See data type objects for more details. Dictionary values below are altered before being passed on, all other key and value sets are blindly passed on. + This dictionary is made up of a dictionary for each data set that the parameters + are fit to. Each key is an arbitrary string used to identify the data set and + used later in reporting objective function values during the fitting process. + See data type objects for more details. Dictionary values below are altered + before being passed on, all other key and value sets are blindly passed on. - - data_class_type (str) - One of the supported data type objects to fit parameters - - file (str) - File name in current working directory, or path to desired experimental data. See experimental data page for examples of acceptable format. + - data_class_type (str) - One of the supported data type objects to fit + parameters + - file (str) - File name in current working directory, or path to desired + experimental data. See experimental data page for examples of acceptable format. Returns ------- exp_data : dict - Reformatted dictionary of experimental data. This dictionary is made up of a dictionary for each data set that the parameters are fit to. Each key is an arbitrary string used to identify the data set and used later in reporting objective function values during the fitting process. See data type objects for more details. Dictionary values below are altered from input dictionary, all other key and value sets are blindly passed on, or extracted from data file with process_exp_data_file function. - - - data_class_type (str) - One of the supported data type objects to fit parameters + Reformatted dictionary of experimental data. This dictionary is made up of + a dictionary for each data set that the parameters are fit to. Each key is + an arbitrary string used to identify the data set and used later in reporting + objective function values during the fitting process. See data type objects + for more details. Dictionary values below are altered from input dictionary, + all other key and value sets are blindly passed on, or extracted from data + file with process_exp_data_file function. + + - data_class_type (str) - One of the supported data type objects to fit + parameters """ exp_data = {} @@ -339,11 +391,14 @@ def process_exp_data(exp_data_dict): def process_exp_data_file(fname): - r""" Import data file and convert columns into dictionary entries. - The headers in the file are the dictionary keys. The top line is skipped, and column headers are the second line. Note that column headers should be thermo properties defined in :ref:`data-types` (e.g. T, P, x1, x2, y1, y2). Mole fractions x1, x2, ... should be in the same order as in the bead_configuration line of the input file. No mole fractions should be left out. + The headers in the file are the dictionary keys. The top line is skipped, and + column headers are the second line. Note that column headers should be thermo + properties defined in :ref:`data-types` (e.g. T, P, x1, x2, y1, y2). Mole + fractions x1, x2, ... should be in the same order as in the bead_configuration + line of the input file. No mole fractions should be left out. Parameters ---------- diff --git a/despasito/input_output/write_output.py b/despasito/input_output/write_output.py index d163f4d..b3a12d1 100644 --- a/despasito/input_output/write_output.py +++ b/despasito/input_output/write_output.py @@ -10,7 +10,6 @@ def write_EOSparameters(library, filename): - """ Sort and export dictionary of input parameters into file in JSON format. @@ -36,7 +35,11 @@ def writeout_thermo_dict(output_dict, calctype, output_file="thermo_output.txt") """ Write out result of thermodynamic calculation. - This file is automatically saved for a thermodynamic calculation (not parameter fitting) using the default ``output_file`` name. Import dictionary of both input and output data to produce a file. A line in the top clarifies the calculation type done. Each system input and output properties (e.g. T, P, etc.) are expressed as a columns. + This file is automatically saved for a thermodynamic calculation (not parameter + fitting) using the default ``output_file`` name. Import dictionary of both input + and output data to produce a file. A line in the top clarifies the calculation + type done. Each system input and output properties (e.g. T, P, etc.) are expressed + as a columns. Parameters ---------- @@ -60,9 +63,8 @@ def writeout_thermo_dict(output_dict, calctype, output_file="thermo_output.txt") } # Make comment line - comment = "# This data was generated in DESPASITO using the thermodynamic calculation: {}".format( - calctype - ) + comment = "# This data was generated in DESPASITO using the thermodynamic " + "calculation: {}".format(calctype) # Make results matrix keys = [] @@ -116,9 +118,8 @@ def writeout_fit_dict(output_dict, output_file="fit_output.txt"): """ header = ( - "DESPASITO was used to fit parameters for the bead {} Obj. Value: {}\n".format( - output_dict["fit_bead"], output_dict["objective_value"] - ) + "DESPASITO was used to fit parameters for the bead {} Obj. Value:" + " {}\n".format(output_dict["fit_bead"], output_dict["objective_value"]) + "Parameter, Value\n" ) with open(output_file, "w") as f: diff --git a/despasito/main.py b/despasito/main.py index a12334f..9eeb09e 100644 --- a/despasito/main.py +++ b/despasito/main.py @@ -3,43 +3,54 @@ """ Handles the primary functions -In any directory with the appropriate input files in the JSON format, run DESPASITO with ``python -m despasito input.json`` - +In any directory with the appropriate input files in the JSON format, run DESPASITO +with ``python -m despasito input.json`` """ import logging import argparse import numpy as np -from .input_output import read_input -from .input_output import write_output -from .equations_of_state import initiate_eos -from .thermodynamics import thermo -from .parameter_fitting import fit +from despasito.input_output import read_input, write_output +from despasito.equations_of_state import initiate_eos +from despasito.thermodynamics import thermo +from despasito.parameter_fitting import fit logger = logging.getLogger(__name__) def get_parser(): - """ Process line arguments - """ + """Process line arguments""" - ## Define parser functions and arguments + # Define parser functions and arguments parser = argparse.ArgumentParser( - description=r"DESPASITO: Determining Equilibrium State and Parametrization Application for SAFT, Intended for Thermodynamic Output. This is an open-source application for thermodynamic calculations and parameter fitting for equations of state (EOS) like the Statistical Associating Fluid Theory (SAFT) EOS." + description=( + r"DESPASITO: Determining Equilibrium State and Parametrization " + + "Application for SAFT, Intended for Thermodynamic Output. This is an " + + "open-source application for thermodynamic calculations and parameter" + + " fitting for equations of state (EOS) like the Statistical " + + "Associating Fluid Theory (SAFT) EOS." + ) ) parser.add_argument( "-i", "--input", dest="input", - help="Input file in JSON format with calculation instructions and path(s) to equation of state parameters. See documentation for explicit explanation. Compile docs or visit https://despasito.readthedocs.io", + help=( + "Input file in JSON format with calculation instructions and path(s) to" + + " equation of state parameters. See documentation for explicit " + + "explanation. Compile docs or visit https://despasito.readthedocs.io" + ), ) parser.add_argument( "-v", "--verbose", action="count", default=0, - help="Verbose level: repeat up to three times for Warning, Info, or Debug levels.", + help=( + "Verbose level: repeat up to three times for Warning, Info, or Debug " + + "levels." + ), ) parser.add_argument( "--log", @@ -53,14 +64,20 @@ def get_parser(): "--ncores", dest="ncores", type=int, - help="Set the number of cores used. A value of -1 will request all possible resources.", + help=( + "Set the number of cores used. A value of -1 will request all possible " + + "resources." + ), default=1, ) parser.add_argument( "-p", "--path", default=".", - help="Set the location of the data/library files (e.g. SAFTcross, etc.) where despasito will look.", + help=( + "Set the location of the data/library files (e.g. SAFTcross, etc.) where" + + " despasito will look." + ), ) parser.add_argument( "-c", @@ -76,7 +93,7 @@ def get_parser(): parser.add_argument( "--python", action="store_true", - help="Remove default Fortran module for association site calculations.", + help="Run calculation in python without compiled modules.", ) parser.add_argument( "--cython", @@ -88,7 +105,7 @@ def get_parser(): def run(filename="input.json", path=".", **kwargs): - """ Main function for running despasito calculations. + """Main function for running despasito calculations. All inputs and settings should be in the supplied JSON file(s). @@ -102,8 +119,8 @@ def run(filename="input.json", path=".", **kwargs): Keywords for other aspects of calculation """ - np.seterr(divide = 'ignore') - #np.warnings.filterwarnings('error', category=np.VisibleDeprecationWarning) + np.seterr(divide="ignore") + # np.warnings.filterwarnings('error', category=np.VisibleDeprecationWarning) # read input file (need to add command line specification) logger.info("Begin processing input file: %s" % filename) diff --git a/despasito/parameter_fitting/__init__.py b/despasito/parameter_fitting/__init__.py index 717d35e..a70d155 100644 --- a/despasito/parameter_fitting/__init__.py +++ b/despasito/parameter_fitting/__init__.py @@ -2,13 +2,13 @@ Fit Parameters -------------- -This package uses functions from ``input_output``, ``equations_of_state``, and ``thermodynamics`` to fit parameters to experimental data. - -input.json files have a different dictionary structure that is processed by :func:`~despasito.input_output.read_input.process_param_fit_inputs`. +This package uses functions from ``input_output``, ``equations_of_state``, and +``thermodynamics`` to fit parameters to experimental data. +input.json files have a different dictionary structure that is processed by +:func:`~despasito.input_output.read_input.process_param_fit_inputs`. """ -import sys import os import numpy as np from importlib import import_module @@ -29,70 +29,98 @@ def fit( **kwargs ): r""" - Fit parameters for an equation of state object with given experimental data. + Fit parameters for an equation of state object with given experimental data. - Each set of experimental data is converted to an object with the built in ability to evaluate its part of objective function. - To add another type of supported experimental data, add a class to the fit_classes.py file. + Each set of experimental data is converted to an object with the built in ability + to evaluate its part of objective function. + To add another type of supported experimental data, add a class to the + fit_classes.py file. Parameters ---------- optimization_parameters : dict, Optional, default=None Parameters used in global fitting algorithm. - - fit_bead (str) - Name of bead whose parameters are being fit. Should be within bead_configuration. - - fit_parameter_names (list[str]) - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of a parameter name followed by the interacting bead's name, separated by an underscore (e.g. epsilon_CO2). - - parameters_guess (list[float]), Optional - Initial guess for all parameters being fit. If this is not provided, the Eos object provides a guesses based on the parameter types. - - \*_bounds (list[float]), Optional - This list contains the minimum and maximum of the parameter from a parameter listed in fit_parameter_names, represented in place of the asterisk. See , :ref:`startfitting-label`, for more information. + - fit_bead (str) - Name of bead whose parameters are being fit. Should be + within bead_configuration. + - fit_parameter_names (list[str]) - This list contains the name of the + parameter being fit (e.g. epsilon). See EOS documentation for supported + parameter names. Cross interaction parameter names should be composed of a + parameter name followed by the interacting bead's name, separated by an + underscore (e.g. epsilon_CO2). + - parameters_guess (list[float]), Optional - Initial guess for all parameters + being fit. If this is not provided, the Eos object provides a guesses based on + the parameter types. + - \*_bounds (list[float]), Optional - This list contains the minimum and + maximum of the parameter from a parameter listed in fit_parameter_names, + represented in place of the asterisk. See , :ref:`startfitting-label`, for + more information. exp_data : dict, Optional, default=None - This dictionary is made up of a dictionary for each data set that the parameters are fit to. Each dictionary is converted into an object and saved back to this structure before parameter fitting begins. Each key is an arbitrary string used to identify the data set and used later in reporting objective function values during the fitting process. See data type objects for more details. - - - data_class_type (str) - One of the supported data type objects to fit parameters, :ref:`data-types`. - - eos_obj (obj) - Equation of state output that writes pressure, max density, fugacity coefficient, updates parameters, and evaluates parameter fitting objective function. See equation of state documentation for more details. + This dictionary is made up of a dictionary for each data set that the + parameters are fit to. Each dictionary is converted into an object and saved + back to this structure before parameter fitting begins. Each key is an + arbitrary string used to identify the data set and used later in reporting + objective function values during the fitting process. See data type objects + for more details. + + - data_class_type (str) - One of the supported data type objects to fit + parameters, :ref:`data-types`. + - eos_obj (obj) - Equation of state output that writes pressure, max density, + fugacity coefficient, updates parameters, and evaluates parameter fitting + objective function. See equation of state documentation for more details. global_opts : dict, Optional, default={} - Method and keyword arguments used in global optimization method. See :func:`~despasito.parameter_fitting.fit_functions.global_minimization`. + Method and keyword arguments used in global optimization method. See + :func:`~despasito.parameter_fitting.fit_functions.global_minimization`. - - method (str), Optional - default='differential_evolution', Global optimization method used to fit parameters. See :func:`~despasito.parameter_fitting.fit_functions.global_minimization`. + - method (str), Optional - default='differential_evolution', Global + optimization method used to fit parameters. See + :func:`~despasito.parameter_fitting.fit_functions.global_minimization`. - Additional options, specific to the global optimization method minimizer_opts : dict, Optional, default=None Dictionary used to define minimization type and the associated options. - method (str) - Method available to ``scipy.optimize.minimize`` - - options (dict) - This dictionary contains the keyword arguments available to the chosen method + - options (dict) - This dictionary contains the keyword arguments available + to the chosen method MultiprocessingObject : obj, Optional - Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` - kwargs : - Other keywords of instructions for thermodynamic calculations and parameter fitting. - + Multiprocessing object, + :class:`~despasito.utils.parallelization.MultiprocessingJob` + kwargs : + Other keywords of instructions for thermodynamic calculations and parameter + fitting. + Returns ------- output : dict Results from parameter optimization - - parameters_final (numpy.ndarray) - Array of the same length as `fit_parameter_names` containing the parameters resulting from the chosen global optimization method. + - parameters_final (numpy.ndarray) - Array of the same length as + `fit_parameter_names` containing the parameters resulting from the chosen + global optimization method. - objective_value (float) - Objective value resulting from `parameters_final` - + """ # Extract relevant quantities from kwargs dicts = {} # Extract inputs - if optimization_parameters == None: + if optimization_parameters is None: raise ValueError("Required input, optimization_parameters, is missing.") dicts["global_opts"] = global_opts - if minimizer_opts != None: + if minimizer_opts is not None: dicts["minimizer_opts"] = minimizer_opts - if exp_data == None: + if exp_data is None: raise ValueError("Required input, exp_data, is missing.") # Add multiprocessing object to exp_data objects and global_optss - if MultiprocessingObject != None: + if MultiprocessingObject is not None: for k2 in list(exp_data.keys()): exp_data[k2]["MultiprocessingObject"] = MultiprocessingObject dicts["global_opts"]["MultiprocessingObject"] = MultiprocessingObject @@ -119,7 +147,8 @@ def fit( parameters_guess = optimization_parameters["parameters_guess"] if len(parameters_guess) != len(optimization_parameters["fit_parameter_names"]): raise ValueError( - "The number of initial parameters given isn't the same number of parameters to be fit." + "The number of initial parameters given isn't the same number of " + "parameters to be fit." ) else: parameters_guess = ff.initial_guess(optimization_parameters, Eos) @@ -131,7 +160,7 @@ def fit( exp_dict = {} pkgpath = os.path.dirname(data_classes.__file__) type_list = [f for f in os.listdir(pkgpath) if f.endswith(".py")] - type_list = type_list.remove("__init__.py") + type_list.remove("__init__.py") for key, data_dict in exp_data.items(): fittype = data_dict["data_class_type"] @@ -148,9 +177,8 @@ def fit( else: tmp = ", ".join(type_list) raise ImportError( - "The experimental data type, '{}', was not found\nThe following calculation types are supported: {}".format( - fittype, tmp - ) + "The experimental data type, '{}', was not found\nThe following " + "calculation types are supported: {}".format(fittype, tmp) ) try: diff --git a/despasito/parameter_fitting/constraint_types.py b/despasito/parameter_fitting/constraint_types.py index 0a86a39..8ee78a7 100644 --- a/despasito/parameter_fitting/constraint_types.py +++ b/despasito/parameter_fitting/constraint_types.py @@ -1,10 +1,11 @@ -""" Optional constraint functions extracted for :func:`~despasito.parameter_fitting.fit_functions.global_minimization` method. +""" Optional constraint functions extracted for +:func:`~despasito.parameter_fitting.fit_functions.global_minimization` method. """ def type1(x, *args): - """ Example constraint. Notice that if this is used by a constraint class, there are no other args, while there are for dict constraints. - """ + """Example constraint. Notice that if this is used by a constraint class, there + are no other args, while there are for dict constraints.""" output = None diff --git a/despasito/parameter_fitting/data_classes/TLVE.py b/despasito/parameter_fitting/data_classes/TLVE.py index 4b166f8..720b832 100644 --- a/despasito/parameter_fitting/data_classes/TLVE.py +++ b/despasito/parameter_fitting/data_classes/TLVE.py @@ -1,5 +1,6 @@ r""" -Objects for storing and producing objective values for comparing experimental data to EOS predictions. +Objects for storing and producing objective values for comparing experimental data to +EOS predictions. """ import numpy as np @@ -13,34 +14,47 @@ logger = logging.getLogger(__name__) + ################################################################## # # # TLVE # # # ################################################################## class Data(ExpDataTemplate): - r""" - Object for Temperature dependent VLE data. + Object for Temperature dependent VLE data. - This object is initiated in :func:`~despasito.parameter_fitting.fit` with the keyword, ``exp_data[*]["data_class_type"]="TLVE"``. + This object is initiated in :func:`~despasito.parameter_fitting.fit` with the + keyword, ``exp_data[*]["data_class_type"]="TLVE"``. - This data could be evaluated with bubble_pressure or dew_pressure. Most entries in the exp. dictionary are converted to attributes. + This data could be evaluated with + :func:`~despasito.thermodynamics.calculation_types.bubble_pressure` or + :func:`~despasito.thermodynamics.calculation_types.dew_pressure`. Most entries in + the exp. dictionary are converted to attributes. Parameters ---------- data_dict : dict Dictionary of exp data of TLVE temperature dependent liquid vapor equilibria - * calculation_type (str) - Optional, default='bubble_pressure', However, 'dew_pressure' is also acceptable - * MultiprocessingObject (obj) - Optional, Initiated :class:`~despasito.utils.parallelization.MultiprocessingJob` + * calculation_type (str) - Optional, default='bubble_pressure', However, + 'dew_pressure' is also acceptable + * MultiprocessingObject (obj) - Optional, Initiated + :class:`~despasito.utils.parallelization.MultiprocessingJob` * eos_obj (obj) - Equation of state object * T (list) - List of temperature values for calculation * P (list) - [Pa] List of pressure values for evaluation - * xi(yi) (list) - List of liquid (or vapor) mole fractions used in bubble_pressure (or dew_pressure) calculation. - * weights (dict) - A dictionary where each key is a system constraint (e.g. T or xi) which is also a header used in an optional exp. data file. The value associated with a header can be a list as long as the number of data points to multiply by the objective value associated with each point, or a float to multiply the objective value of this data set. - * density_opts (dict) - Optional, default={}, Dictionary of options used in calculating pressure vs. mole fraction curves. - * kwargs for :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` + * xi(yi) (list) - List of liquid (or vapor) mole fractions used in + bubble_pressure (or dew_pressure) calculation. + * weights (dict) - A dictionary where each key is a system constraint + (e.g. T or xi) which is also a header used in an optional exp. data file. + The value associated with a header can be a list as long as the number of + data points to multiply by the objective value associated with each point, + or a float to multiply the objective value of this data set. + * density_opts (dict) - Optional, default={}, Dictionary of options used in + calculating pressure vs. mole fraction curves. + * kwargs for + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` Attributes ---------- @@ -49,20 +63,24 @@ class Data(ExpDataTemplate): Eos : obj Equation of state object obj_opts : dict - Keywords to compute the objective function with :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. + Keywords to compute the objective function with + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. npoints : int Number of sets of system conditions this object computes result_keys : list - Thermodynamic property names used in calculation of objective function. In in this case: ["Plist", 'xilist'] or ["Plist", 'yilist'] + Thermodynamic property names used in calculation of objective function. In in + this case: ["Plist", 'xilist'] or ["Plist", 'yilist'] thermodict : dict Dictionary of inputs needed for thermodynamic calculations - + - calculation_type (str) default=bubble_pressure or dew_pressure - - density_opts (dict) default={"min_density_fraction":(1.0 / 300000.0), "density_increment":10.0, "max_volume_increment":1.0E-4} + - density_opts (dict) default={"min_density_fraction":(1.0 / 300000.0), + "density_increment":10.0, "max_volume_increment":1.0E-4} weights : dict, Optional, default: {"some_property": 1.0 ...} - Dictionary with keys corresponding to those in thermodict, with weighting factor or vector for each system property used in fitting - + Dictionary with keys corresponding to those in thermodict, with weighting + factor or vector for each system property used in fitting + """ def __init__(self, data_dict): @@ -127,19 +145,22 @@ def __init__(self, data_dict): thermo_keys = ["xilist", "yilist", "Plist"] if not any([key in self.thermodict for key in thermo_keys]): raise ImportError( - "Given TLVE data, mole fractions and/or pressure should have been provided." + "Given TLVE data, mole fractions and/or pressure should have been " + "provided." ) - if self.thermodict["calculation_type"] == None: + if self.thermodict["calculation_type"] is None: if self.thermodict["xilist"]: self.thermodict["calculation_type"] = "bubble_pressure" logger.warning( - "No calculation type has been provided. Assume a calculation type of bubble_pressure" + "No calculation type has been provided. Assume a calculation type " + "of bubble_pressure" ) elif self.thermodict["yilist"]: self.thermodict["calculation_type"] = "dew_pressure" logger.warning( - "No calculation type has been provided. Assume a calculation type of dew_pressure" + "No calculation type has been provided. Assume a calculation type" + " of dew_pressure" ) else: raise ValueError("Unknown calculation instructions") @@ -152,7 +173,8 @@ def __init__(self, data_dict): del self.weights["yilist"] logger.info( - "Data type 'TLVE' initiated with calculation_type, {}, and data types: {}.\nWeight data by: {}".format( + "Data type 'TLVE' initiated with calculation_type, {}, and data " + "types: {}.\nWeight data by: {}".format( self.thermodict["calculation_type"], ", ".join(self.result_keys), self.weights, @@ -160,14 +182,14 @@ def __init__(self, data_dict): ) def _thermo_wrapper(self): - """ Generate thermodynamic predictions from Eos object Returns ------- phase_list : float - A list of the predicted thermodynamic values estimated from thermo calculation. This list can be composed of lists or floats + A list of the predicted thermodynamic values estimated from thermo + calculation. This list can be composed of lists or floats """ # Remove results @@ -194,7 +216,6 @@ def _thermo_wrapper(self): return output def objective(self): - """ Generate objective function value from this dataset diff --git a/despasito/parameter_fitting/data_classes/flash.py b/despasito/parameter_fitting/data_classes/flash.py index 06743af..024adf3 100644 --- a/despasito/parameter_fitting/data_classes/flash.py +++ b/despasito/parameter_fitting/data_classes/flash.py @@ -1,5 +1,6 @@ r""" -Objects for storing and producing objective values for comparing experimental data to EOS predictions. +Objects for storing and producing objective values for comparing experimental data to +EOS predictions. """ import numpy as np @@ -13,17 +14,20 @@ logger = logging.getLogger(__name__) + ################################################################## # # # TLVE # # # ################################################################## class Data(ExpDataTemplate): - r""" Object for flash calculation. This data could be evaluated with flash. - This object is initiated in :func:`~despasito.parameter_fitting.fit` with the keyword, ``exp_data[*]["data_class_type"]="flash"``. + This object is initiated in :func:`~despasito.parameter_fitting.fit` with the + keyword, ``exp_data[*]["data_class_type"]="flash"``. + The data could be evaluated with + :func:`~despasito.thermodynamics.calculation_types.flash` Parameters ---------- @@ -31,15 +35,24 @@ class Data(ExpDataTemplate): Dictionary of exp data of type flash. * calculation_type (str) - Optional, default='flash' - * MultiprocessingObject (obj) - Optional, Initiated :class:`~despasito.utils.parallelization.MultiprocessingJob` + * MultiprocessingObject (obj) - Optional, Initiated + :class:`~despasito.utils.parallelization.MultiprocessingJob` * eos_obj (obj) - Equation of state object * T (list) - [K] List of temperature values for calculation * P (list) - [Pa] List of pressure values for calculation - * xi (list) - List of liquid compositions - * yi (list) - List of vapor compositions - * weights (dict) - A dictionary where each key is a system constraint (e.g. T or xi) which is also a header used in an optional exp. data file. The value associated with a header can be a list as long as the number of data points to multiply by the objective value associated with each point, or a float to multiply the objective value of this data set. - * density_opts (dict) - Optional, default={"min_density_fraction":(1.0 / 300000.0), "density_increment":10.0, "max_volume_increment":1.0E-4}, Dictionary of options used in calculating pressure vs. mole fraction curves. - * kwargs for :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` + * xi (list) - List of liquid compositions, evaluated in this calculation + * yi (list) - List of vapor compositions, evalulated in this calculation + * weights (dict) - A dictionary where each key is a system constraint + (e.g. T or xi) which is also a header used in an optional exp. data file. The + value associated with a header can be a list as long as the number of data + points to multiply by the objective value associated with each point, or a + float to multiply the objective value of this data set. + * density_opts (dict) - Optional, + default={"min_density_fraction":(1.0 / 300000.0), "density_increment":10.0, + "max_volume_increment":1.0E-4}, Dictionary of options used in calculating + pressure vs. mole fraction curves. + * kwargs for + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` Attributes ---------- @@ -48,19 +61,23 @@ class Data(ExpDataTemplate): Eos : obj Equation of state object weights : dict, Optional, default: {"some_property": 1.0 ...} - Dictionary with keys corresponding to those in thermodict, with weighting factor or vector for each system property used in fitting. + Dictionary with keys corresponding to those in thermodict, with weighting + factor or vector for each system property used in fitting. obj_opts : dict - Keywords to compute the objective function with :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. + Keywords to compute the objective function with + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. npoints : int Number of sets of system conditions this object computes result_keys : list - Thermodynamic property names used in calculation of objective function. In in this case: ["xilist", 'yilist'] + Thermodynamic property names used in calculation of objective function. In in + this case: ["xilist", 'yilist'] thermodict : dict Dictionary of inputs needed for thermodynamic calculations - + - calculation_type (str) default=flash - - density_opts (dict) default={"min_density_fraction":(1.0 / 300000.0), "density_increment":10.0, "max_volume_increment":1.0E-4} - + - density_opts (dict) default={"min_density_fraction":(1.0 / 300000.0), + "density_increment":10.0, "max_volume_increment":1.0E-4} + """ def __init__(self, data_dict): @@ -70,7 +87,7 @@ def __init__(self, data_dict): super().__init__(data_dict) self.name = "flash" - if self.thermodict["calculation_type"] == None: + if self.thermodict["calculation_type"] is None: logger.warning("No calculation type has been provided.") self.thermodict["calculation_type"] = "flash" @@ -132,7 +149,8 @@ def __init__(self, data_dict): ) logger.info( - "Data type 'flash' initiated with calculation_type, {}, and data types: {}.\nWeight data by: {}".format( + "Data type 'flash' initiated with calculation_type, {}, and data " + "types: {}.\nWeight data by: {}".format( self.thermodict["calculation_type"], ", ".join(self.result_keys), self.weights, @@ -140,14 +158,14 @@ def __init__(self, data_dict): ) def _thermo_wrapper(self): - """ Generate thermodynamic predictions from Eos object Returns ------- phase_list : float - A list of the predicted thermodynamic values estimated from thermo calculation. This list can be composed of lists or floats + A list of the predicted thermodynamic values estimated from thermo + calculation. This list can be composed of lists or floats """ # Remove results @@ -166,7 +184,6 @@ def _thermo_wrapper(self): return output def objective(self): - """ Generate objective function value from this dataset diff --git a/despasito/parameter_fitting/data_classes/liquid_density.py b/despasito/parameter_fitting/data_classes/liquid_density.py index 7b89dc5..31408e0 100644 --- a/despasito/parameter_fitting/data_classes/liquid_density.py +++ b/despasito/parameter_fitting/data_classes/liquid_density.py @@ -1,30 +1,33 @@ r""" -Objects for storing and producing objective values for comparing experimental data to EOS predictions. +Objects for storing and producing objective values for comparing experimental data +to EOS predictions. """ import numpy as np import logging -from despasito import fundamental_constants as constants from despasito.thermodynamics import thermo from despasito.parameter_fitting import fit_functions as ff from despasito.parameter_fitting.interface import ExpDataTemplate -from despasito import fundamental_constants as constants +import despasito.fundamental_constants as constants import despasito.utils.general_toolbox as gtb logger = logging.getLogger(__name__) + ################################################################## # # # Liquid Density # # # ################################################################## class Data(ExpDataTemplate): - r""" - Object for liquid density data. This data is evaluated with "liquid_properties". + Object for liquid density data. This data is evaluated with "liquid_properties". - This object is initiated in :func:`~despasito.parameter_fitting.fit` with the keyword, ``exp_data[*]["data_class_type"]="liquid_density"``. + This object is initiated in :func:`~despasito.parameter_fitting.fit` with the + keyword, ``exp_data[*]["data_class_type"]="liquid_density"``. + The data could be evaluated with + :func:`~despasito.thermodynamics.calculation_types.liquid_properties` Parameters ---------- @@ -32,16 +35,27 @@ class Data(ExpDataTemplate): Dictionary of exp data of type liquid density. * calculation_type (str) - Optional, default='liquid_properties' - * MultiprocessingObject (obj) - Optional, Initiated :class:`~despasito.utils.parallelization.MultiprocessingJob` + * MultiprocessingObject (obj) - Optional, Initiated + :class:`~despasito.utils.parallelization.MultiprocessingJob` * eos_obj (obj) - Equation of state object * T (list) - [K] List of temperature values for calculation * P (list) - [Pa] List of pressure values for calculation - * xi (list) - List of liquid mole fractions used in liquid_properties calculations + * xi (list) - List of liquid mole fractions used in liquid_properties + calculations * rhol (list) - [mol/:math:`m^3`] Evaluated liquid density values - * weights (dict) - A dictionary where each key is a system constraint (e.g. T or xi) which is also a header used in an optional exp. data file. The value associated with a header can be a list as long as the number of data points to multiply by the objective value associated with each point, or a float to multiply the objective value of this data set. - * objective_method (str) - The 'method' keyword in function despasito.parameter_fitting.fit_functions.obj_function_form. - * density_opts (dict) - Optional, default={"min_density_fraction":(1.0 / 60000.0), "density_increment":10.0, "max_volume_increment":1.0E-4}, Dictionary of options used in calculating pressure vs. mole fraction curves. - * kwargs for :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` + * weights (dict) - A dictionary where each key is a system constraint + (e.g. T or xi) which is also a header used in an optional exp. data file. + The value associated with a header can be a list as long as the number of + data points to multiply by the objective value associated with each point, + or a float to multiply the objective value of this data set. + * objective_method (str) - The 'method' keyword in function + despasito.parameter_fitting.fit_functions.obj_function_form. + * density_opts (dict) - Optional, + default={"min_density_fraction":(1.0 / 60000.0), "density_increment":10.0, + "max_volume_increment":1.0E-4}, Dictionary of options used in calculating + pressure vs. mole fraction curves. + * kwargs for + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` Attributes ---------- @@ -50,19 +64,23 @@ class Data(ExpDataTemplate): Eos : obj Equation of state object weights : dict, Optional, default: {"some_property": 1.0 ...} - Dictionary with keys corresponding to those in thermodict, with weighting factor or vector for each system property used in fitting + Dictionary with keys corresponding to those in thermodict, with weighting + factor or vector for each system property used in fitting obj_opts : dict - Keywords to compute the objective function with :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. + Keywords to compute the objective function with + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. npoints : int Number of sets of system conditions this object computes result_keys : list - Thermodynamic property names used in calculation of objective function. In in this case: ["rhol", "phil"] + Thermodynamic property names used in calculation of objective function. In in + this case: ["rhol", "phil"] thermodict : dict Dictionary of inputs needed for thermodynamic calculations - + - calculation_type (str) default=liquid_properties - - density_opts (dict) default={"min_density_fraction":(1.0 / 300000.0), "density_increment":10.0, "max_volume_increment":1.0E-4} - + - density_opts (dict) default={"min_density_fraction":(1.0 / 300000.0), + "density_increment":10.0, "max_volume_increment":1.0E-4} + """ def __init__(self, data_dict): @@ -81,7 +99,7 @@ def __init__(self, data_dict): tmp.update(self.thermodict["density_opts"]) self.thermodict["density_opts"] = tmp - if self.thermodict["calculation_type"] == None: + if self.thermodict["calculation_type"] is None: self.thermodict["calculation_type"] = "liquid_properties" if "xi" in data_dict: @@ -123,11 +141,13 @@ def __init__(self, data_dict): if "Tlist" not in self.thermodict and "rhol" not in self.thermodict: raise ImportError( - "Given liquid property data, values for T, xi, and rhol should have been provided." + "Given liquid property data, values for T, xi, and rhol should have" + " been provided." ) logger.info( - "Data type 'liquid_properties' initiated with calculation_type, {}, and data types: {}.\nWeight data by: {}".format( + "Data type 'liquid_properties' initiated with calculation_type, {}, and " + "data types: {}.\nWeight data by: {}".format( self.thermodict["calculation_type"], ", ".join(self.result_keys), self.weights, @@ -135,14 +155,14 @@ def __init__(self, data_dict): ) def _thermo_wrapper(self): - """ Generate thermodynamic predictions from Eos object Returns ------- phase_list : float - A list of the predicted thermodynamic values estimated from thermo calculation. This list can be composed of lists or floats + A list of the predicted thermodynamic values estimated from thermo + calculation. This list can be composed of lists or floats """ # Remove results @@ -160,7 +180,6 @@ def _thermo_wrapper(self): return output def objective(self): - """ Generate objective function value from this dataset diff --git a/despasito/parameter_fitting/data_classes/saturation_properties.py b/despasito/parameter_fitting/data_classes/saturation_properties.py index 0da7732..eb2f25e 100644 --- a/despasito/parameter_fitting/data_classes/saturation_properties.py +++ b/despasito/parameter_fitting/data_classes/saturation_properties.py @@ -1,5 +1,6 @@ r""" -Objects for storing and producing objective values for comparing experimental data to EOS predictions. +Objects for storing and producing objective values for comparing experimental data to +EOS predictions. """ import numpy as np @@ -9,22 +10,24 @@ from despasito.thermodynamics import thermo from despasito.parameter_fitting import fit_functions as ff from despasito.parameter_fitting.interface import ExpDataTemplate -from despasito import fundamental_constants as constants import despasito.utils.general_toolbox as gtb logger = logging.getLogger(__name__) + ################################################################## # # # Saturation Props # # # ################################################################## class Data(ExpDataTemplate): - r""" - Object for saturation data. This data is evaluated with "saturation_properties". + Object for saturation data. This data is evaluated with "saturation_properties". - This object is initiated in :func:`~despasito.parameter_fitting.fit` with the keyword, ``exp_data[*]["data_class_type"]="saturation_properties"``. + This object is initiated in :func:`~despasito.parameter_fitting.fit` with the + keyword, ``exp_data[*]["data_class_type"]="saturation_properties"``. + The data could be evaluated with + :func:`~despasito.thermodynamics.calculation_types.saturation_properties` Parameters ---------- @@ -32,16 +35,28 @@ class Data(ExpDataTemplate): Dictionary of exp data of saturation properties. * calculation_type (str) - Optional, default='saturation_properties' - * MultiprocessingObject (obj) - Optional, Initiated :class:`~despasito.utils.parallelization.MultiprocessingJob` + * MultiprocessingObject (obj) - Optional, Initiated + :class:`~despasito.utils.parallelization.MultiprocessingJob` * eos_obj (obj) - Equation of state object * T (list) - [K] List of temperature values for calculation - * xi (list) - (or yi) List of liquid mole fractions used in saturation properties calculations, should be one for the molecule of focus and zero for the remainder. - * Psat (list) - [Pa] List of saturation pressure values - * rhov (list) - [mol/:math:`m^3`] List of vapor density values - * rhol (list) - [mol/:math:`m^3`] List of liquid density values - * weights (dict) - A dictionary where each key is a system constraint (e.g. T or xi) which is also a header used in an optional exp. data file. The value associated with a header can be a list as long as the number of data points to multiply by the objective value associated with each point, or a float to multiply the objective value of this data set. - * density_opts (dict) - Optional, default={"min_density_fraction":(1.0 / 60000.0), "density_increment":10.0, "max_volume_increment":1.0E-4}, Dictionary of options used in calculating pressure vs. mole fraction curves. - * kwargs for :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` + * xi (list) - (or yi) List of liquid mole fractions used in saturation + properties calculations, should be one for the molecule of focus and zero for + the remainder. + * Psat (list) - [Pa] List of saturation pressure values to evaluate against + * rhov (list) - [mol/:math:`m^3`] List of vapor density values to evaluate + against + * rhol (list) - [mol/:math:`m^3`] List of liquid density values to evaluate + against + * weights (dict) - A dictionary where each key is a system constraint + (e.g. T or xi) which is also a header used in an optional exp. data file. The + value associated with a header can be a list as long as the number of data + points to multiply by the objective value associated with each point, or a + float to multiply the objective value of this data set. + * density_opts (dict) - Optional, default={"min_density_fraction": + (1.0 / 60000.0), "density_increment":10.0, "max_volume_increment":1.0E-4}, + Dictionary of options used in calculating pressure vs. mole fraction curves. + * kwargs for + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` Attributes ---------- @@ -50,20 +65,24 @@ class Data(ExpDataTemplate): Eos : obj Equation of state object obj_opts : dict - Keywords to compute the objective function with :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. + Keywords to compute the objective function with + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. npoints : int Number of sets of system conditions this object computes result_keys : list - Thermodynamic property names used in calculation of objective function. In in this case: ["rhol", "rhov", "Psat"] + Thermodynamic property names used in calculation of objective function. In in + this case: ["rhol", "rhov", "Psat"] thermodict : dict Dictionary of inputs needed for thermodynamic calculations - + - calculation_type (str) default=saturation_properties - - density_opts (dict) default={"min_density_fraction":(1.0 / 80000.0), "density_increment":10.0, "max_volume_increment":1.0E-4} + - density_opts (dict) default={"min_density_fraction":(1.0 / 80000.0), + "density_increment":10.0, "max_volume_increment":1.0E-4} weights : dict, Optional, default: {"some_property": 1.0 ...} - Dictionary corresponding to thermodict, with weighting factor or vector for each system property used in fitting - + Dictionary corresponding to thermodict, with weighting factor or vector for + each system property used in fitting + """ def __init__(self, data_dict): @@ -74,7 +93,7 @@ def __init__(self, data_dict): # If required items weren't defined, set defaults self.name = "saturation_properties" - if self.thermodict["calculation_type"] == None: + if self.thermodict["calculation_type"] is None: self.thermodict["calculation_type"] = "saturation_properties" tmp = { @@ -105,9 +124,8 @@ def __init__(self, data_dict): self.thermodict["Psat"] ): raise ValueError( - "Array of weights for '{}' values not equal to number of experimental values given.".format( - "P" - ) + "Array of weights for '{}' values not equal to number of " + "experimental values given.".format("P") ) else: self.weights["Psat"] = self.weights.pop("P") @@ -126,7 +144,8 @@ def __init__(self, data_dict): if "xilist" not in self.thermodict and self.Eos.number_of_components > 1: raise ValueError( - "Ambiguous instructions. Include xi to define intended component to obtain saturation properties" + "Ambiguous instructions. Include xi to define intended component to " + "obtain saturation properties" ) thermo_defaults = [ np.array([[1.0] for x in range(self.npoints)]), @@ -151,11 +170,13 @@ def __init__(self, data_dict): tmp = ["Psat", "rhol", "rhov"] if not any([x in self.thermodict for x in tmp]): raise ImportError( - "Given saturation data, values for Psat, rhol, and/or rhov should have been provided." + "Given saturation data, values for Psat, rhol, and/or rhov should have " + "been provided." ) logger.info( - "Data type 'saturation_properties' initiated with calculation_type, {}, and data types: {}.\nWeight data by: {}".format( + "Data type 'saturation_properties' initiated with calculation_type, {}, and" + " data types: {}.\nWeight data by: {}".format( self.thermodict["calculation_type"], ", ".join(self.result_keys), self.weights, @@ -163,14 +184,14 @@ def __init__(self, data_dict): ) def _thermo_wrapper(self): - """ Generate thermodynamic predictions from Eos object Returns ------- phase_list : float - A list of the predicted thermodynamic values estimated from thermo calculation. This list can be composed of lists or floats + A list of the predicted thermodynamic values estimated from thermo + calculation. This list can be composed of lists or floats """ # Remove results @@ -190,7 +211,6 @@ def _thermo_wrapper(self): return output def objective(self): - """ Generate objective function value from this dataset @@ -202,7 +222,7 @@ def objective(self): phase_list = self._thermo_wrapper() - ## Reformat array of results + # Reformat array of results phase_list, len_list = ff.reformat_output(phase_list) phase_list = np.transpose(np.array(phase_list)) diff --git a/despasito/parameter_fitting/data_classes/solubility_parameter.py b/despasito/parameter_fitting/data_classes/solubility_parameter.py index 41f81c0..1ca1596 100644 --- a/despasito/parameter_fitting/data_classes/solubility_parameter.py +++ b/despasito/parameter_fitting/data_classes/solubility_parameter.py @@ -1,5 +1,6 @@ r""" -Objects for storing and producing objective values for comparing experimental data to EOS predictions. +Objects for storing and producing objective values for comparing experimental data to +EOS predictions. """ import numpy as np @@ -14,17 +15,20 @@ logger = logging.getLogger(__name__) + ################################################################## # # # Saturation Props # # # ################################################################## class Data(ExpDataTemplate): - r""" - Object for Hildebrand solubility parameters. Evaluated with "solubility_parameter". + Object for Hildebrand solubility parameters. Evaluated with "solubility_parameter". - This object is initiated in :func:`~despasito.parameter_fitting.fit` with the keyword, ``exp_data[*]["data_class_type"]="solubility_parameter"``. + This object is initiated in :func:`~despasito.parameter_fitting.fit` with the + keyword, ``exp_data[*]["data_class_type"]="solubility_parameter"``. + The data could be evaluated with + :func:`~despasito.thermodynamics.calculation_types.solubility_parameter` Parameters ---------- @@ -32,16 +36,25 @@ class Data(ExpDataTemplate): Dictionary of exp data of saturation properties. * calculation_type (str) - Optional, default='solubility_parameter' - * MultiprocessingObject (obj) - Optional, Initiated :class:`~despasito.utils.parallelization.MultiprocessingJob` + * MultiprocessingObject (obj) - Optional, Initiated + :class:`~despasito.utils.parallelization.MultiprocessingJob` * eos_obj (obj) - Equation of state object * T (list) - List of temperature values for calculation * P (list) - List of pressure values used in calculations * xi (list) - List of liquid mole fractions used in calculations. - * delta (list) - Hidebrand solubility parameter given system conditions + * delta (list) - Hidebrand solubility parameter values of given system + conditions to evaluate against * rhol (list) - [mol/:math:`m^3`] Evaluated liquid density - * weights (dict) - A dictionary where each key is a system constraint (e.g. T or xi) which is also a header used in an optional exp. data file. The value associated with a header can be a list as long as the number of data points to multiply by the objective value associated with each point, or a float to multiply the objective value of this data set. - * density_opts (dict) - Optional, default={"min_density_fraction":(1.0 / 60000.0), "density_increment":10.0, "max_volume_increment":1.0E-4}, Dictionary of options used in calculating pressure vs. mole fraction curves. - * kwargs for :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` + * weights (dict) - A dictionary where each key is a system constraint + (e.g. T or xi) which is also a header used in an optional exp. data file. The + value associated with a header can be a list as long as the number of data + points to multiply by the objective value associated with each point, or a + float to multiply the objective value of this data set. + * density_opts (dict) - Optional, default={"min_density_fraction": + (1.0 / 60000.0), "density_increment":10.0, "max_volume_increment":1.0E-4}, + Dictionary of options used in calculating pressure vs. mole fraction curves. + * kwargs for + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` Attributes ---------- @@ -50,18 +63,22 @@ class Data(ExpDataTemplate): Eos : obj Equation of state object weights : dict, Optional, default: {"some_property": 1.0 ...} - Dictionary with keys corresponding to those in thermodict, with weighting factor or vector for each system property used in fitting + Dictionary with keys corresponding to those in thermodict, with weighting + factor or vector for each system property used in fitting obj_opts : dict - Keywords to compute the objective function with :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. + Keywords to compute the objective function with + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. npoints : int Number of sets of system conditions this object computes result_keys : list - Thermodynamic property names used in calculation of objective function. In in this case: ["rhol", "delta"] + Thermodynamic property names used in calculation of objective function. In in + this case: ["rhol", "delta"] thermodict : dict Dictionary of inputs needed for thermodynamic calculations - + - calculation_type (str) default=solubility_parameter - - density_opts (dict) default={"min_density_fraction":(1.0 / 300000.0), "density_increment":10.0, "max_volume_increment":1.0E-4} + - density_opts (dict) default={"min_density_fraction":(1.0 / 300000.0), + "density_increment":10.0, "max_volume_increment":1.0E-4} """ def __init__(self, data_dict): @@ -71,7 +88,7 @@ def __init__(self, data_dict): super().__init__(data_dict) self.name = "solubility_parameter" - if self.thermodict["calculation_type"] == None: + if self.thermodict["calculation_type"] is None: self.thermodict["calculation_type"] = "solubility_parameter" if "density_opts" not in self.thermodict: @@ -122,11 +139,13 @@ def __init__(self, data_dict): if "Tlist" not in self.thermodict and "delta" not in self.thermodict: raise ImportError( - "Given solubility data, value(s) for T and delta should have been provided." + "Given solubility data, value(s) for T and delta should have been " + "provided." ) logger.info( - "Data type 'solubility parameter' initiated with calculation_type, {}, and data types: {}.\nWeight data by: {}".format( + "Data type 'solubility parameter' initiated with calculation_type, {}, and" + " data types: {}.\nWeight data by: {}".format( self.thermodict["calculation_type"], ", ".join(self.result_keys), self.weights, @@ -134,14 +153,14 @@ def __init__(self, data_dict): ) def _thermo_wrapper(self): - """ Generate thermodynamic predictions from Eos object Returns ------- phase_list : float - A list of the predicted thermodynamic values estimated from thermo calculation. This list can be composed of lists or floats + A list of the predicted thermodynamic values estimated from thermo + calculation. This list can be composed of lists or floats """ # Remove results @@ -161,7 +180,6 @@ def _thermo_wrapper(self): return output def objective(self): - """ Generate objective function value from this dataset @@ -173,7 +191,7 @@ def objective(self): phase_list = self._thermo_wrapper() - ## Reformat array of results + # Reformat array of results phase_list, len_list = ff.reformat_output(phase_list) phase_list = np.transpose(np.array(phase_list)) diff --git a/despasito/parameter_fitting/fit_functions.py b/despasito/parameter_fitting/fit_functions.py index 9bfd59f..670527d 100644 --- a/despasito/parameter_fitting/fit_functions.py +++ b/despasito/parameter_fitting/fit_functions.py @@ -23,16 +23,24 @@ def initial_guess(optimization_parameters, Eos): Parameters used in global optimization algorithm. - fit_bead (str) - Name of bead whose parameters are being fit. - - fit_parameter_names (list[str]) - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + - fit_parameter_names (list[str]) - This list contains the name of the + parameter being fit (e.g. epsilon). See EOS documentation for supported + parameter names. Cross interaction parameter names should be composed of + parameter name and the other bead type, separated by an underscore (e.g. + epsilon_CO2). Eos : obj - Equation of state object that writes pressure, max density, fugacity coefficient, updates parameters, and evaluates objective functions. For parameter fitting algorithm See equation of state documentation for more details. + Equation of state object that writes pressure, max density, fugacity + coefficient, updates parameters, and evaluates objective functions. For + parameter fitting algorithm See equation of state documentation for more + details. Returns ------- - parameters_guess : numpy.ndarray, - An array of initial guesses for parameters to be optimized throughout the process. - + parameters_guess : numpy.ndarray, + An array of initial guesses for parameters to be optimized throughout the + process. + """ # Update bead_library with test parameters @@ -51,9 +59,9 @@ def initial_guess(optimization_parameters, Eos): ) else: raise ValueError( - "Parameters for only one bead are allowed to be fit. Multiple underscores in a parameter name suggest more than one bead type in your fit parameter name, {}".format( - param - ) + "Parameters for only one bead are allowed to be fit. Multiple " + + "underscores in a parameter name suggest more than one bead type " + + "in your fit parameter name, {}".format(param) ) return parameters_guess @@ -61,25 +69,37 @@ def initial_guess(optimization_parameters, Eos): def check_parameter_bounds(optimization_parameters, Eos, bounds): r""" - Check that provided parameter bounds are within reasonable limits defined in Eos object. - + Check that provided parameter bounds are within reasonable limits defined in Eos + object. + Parameters ---------- optimization_parameters : dict Parameters used in global optimization algorithm - - - fit_parameter_names (list[str]) - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead name separated by an underscore (e.g. epsilon_CO2). - + + - fit_parameter_names (list[str]) - This list contains the name of the + parameter being fit (e.g. epsilon). See EOS documentation for supported + parameter names. Cross interaction parameter names should be composed of + parameter name and the other bead name separated by an underscore (e.g. + epsilon_CO2). + Eos : obj - Equation of state object that writes pressure, max density, fugacity coefficient, updates parameters, and evaluates parameter fitting objective function. See equation of state documentation for more details. + Equation of state object that writes pressure, max density, fugacity + coefficient, updates parameters, and evaluates parameter fitting objective + function. See equation of state documentation for more details. bounds : numpy.ndarray - List of length equal to fit_parameter_names with lists of pairs for minimum and maximum bounds of parameter being fit. Defaults defined in Eos object are broad, so we recommend specification. - + List of length equal to fit_parameter_names with lists of pairs for minimum + and maximum bounds of parameter being fit. Defaults defined in Eos object are + broad, so we recommend specification. + Returns ------- new_bounds : list[tuple] - Checked with Eos object method, this list has a length equal to fit_parameter_names with lists of pairs for minimum and maximum bounds of parameter being fit. Bounds are adjusted to remain within limits set by Eos object. - + Checked with Eos object method, this list has a length equal to + fit_parameter_names with lists of pairs for minimum and maximum bounds of + parameter being fit. Bounds are adjusted to remain within limits set by Eos + object. + """ new_bounds = [ @@ -99,35 +119,53 @@ def consolidate_bounds(optimization_parameters): r""" Parse parameter bounds in the ``optimization_parameters`` dictionary. - The resulting bounds form a 2D numpy array with a length equal to the number of parameters being fit. - + The resulting bounds form a 2D numpy array with a length equal to the number of + parameters being fit. + Parameters ---------- optimization_parameters : dict Parameters used in basin fitting algorithm - - - fit_bead (str) - Name of bead whose parameters are being fit, should be in bead list of bead_configuration - - fit_parameter_names (list[str]) - This list contains the name of the parameter being fit (e.g. epsilon). See Eos object documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). - - \*_bounds (list[float]), Optional - This list contains the minimum and maximum of the parameter from a parameter listed in fit_parameter_names, represented in place of the asterisk. See , :ref:`startfitting-label`, for more information. - + + - fit_bead (str) - Name of bead whose parameters are being fit, should be in + bead list of bead_configuration + - fit_parameter_names (list[str]) - This list contains the name of the + parameter being fit (e.g. epsilon). See Eos object documentation for supported + parameter names. Cross interaction parameter names should be composed of + parameter name and the other bead type, separated by an underscore (e.g. + epsilon_CO2). + - \*_bounds (list[float]), Optional - This list contains the minimum and + maximum of the parameter from a parameter listed in fit_parameter_names, + represented in place of the asterisk. See , :ref:`startfitting-label`, for + more information. + Returns ------- new_optimization_parameters : dict Parameters used in basin fitting algorithm - + - fit_bead (str) - Name of bead whose parameters are being fit. - - fit_parameter_names (list[str]) - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). - - bounds (numpy.ndarray) - List of lists of length two, of length equal to ``fit_parameter_names``. If no bounds were given then the default parameter boundaries are [0,1e+4], else bounds given as \*_bounds in input dictionary are used. + - fit_parameter_names (list[str]) - This list contains the name of the + parameter being fit (e.g. epsilon). See EOS documentation for supported + parameter names. Cross interaction parameter names should be composed of + parameter name and the other bead type, separated by an underscore (e.g. + epsilon_CO2). + - bounds (numpy.ndarray) - List of lists of length two, of length equal to + ``fit_parameter_names``. If no bounds were given then the default parameter + boundaries are [0,1e+4], else bounds given as \*_bounds in input dictionary + are used. """ if "fit_bead" not in optimization_parameters: raise ValueError( - "optimization_parameters dictionary should include keyword, fit_bead, defining the name of the bead whose parameters are to be fit." + "optimization_parameters dictionary should include keyword, fit_bead, " + "defining the name of the bead whose parameters are to be fit." ) if "fit_parameter_names" not in optimization_parameters: raise ValueError( - "optimization_parameters dictionary should include keyword, fit_parameter_names, defining the parameters to be fit." + "optimization_parameters dictionary should include keyword, " + "fit_parameter_names, defining the parameters to be fit." ) new_optimization_parameters = {} @@ -145,9 +183,8 @@ def consolidate_bounds(optimization_parameters): new_optimization_parameters["bounds"][ind] = value2 else: logger.error( - "Bounds for parameter type '{}' were given, but this parameter is not defined to be fit.".format( - tmp - ) + "Bounds for parameter type '{}' were given, but this parameter is" + " not defined to be fit.".format(tmp) ) else: new_optimization_parameters[key2] = value2 @@ -158,8 +195,9 @@ def consolidate_bounds(optimization_parameters): def reformat_output(cluster): r""" - Takes a list of lists that contain thermo output of lists and floats and reformats it into a 2D numpy array. - + Takes a list of lists that contain thermo output of lists and floats and reformats" + " it into a 2D numpy array. + Parameters ---------- cluster : list[list[list/floats]] @@ -171,7 +209,7 @@ def reformat_output(cluster): A 2D matrix len_cluster : list a list of lengths for each of the columns (whether 1 for float, or len(list)) - + """ # if input is a list or array @@ -200,9 +238,7 @@ def reformat_output(cluster): matrix = np.zeros([len(val[0]), len(val)]) except Exception: matrix = np.zeros([1, len(val)]) - for j, tmp in enumerate( - val - ): # yes, this is a simple transpose, but for some reason a numpy array of np arrays wouldn't transpose + for j, tmp in enumerate(val): matrix[:, j] = tmp l = len_cluster[i] if l == 1: @@ -223,41 +259,57 @@ def reformat_output(cluster): def global_minimization(global_method, *args, **kwargs): r""" - Fit defined parameters for equation of state object with given experimental data. + Fit defined parameters for equation of state object with given experimental data. - Each set of experimental data is converted to an object with the build in ability to evaluate its part of objective function. + Each set of experimental data is converted to an object with the build in ability + to evaluate its part of objective function. To add another type of supported experimental data, see :ref:`contribute-fitting`. Parameters ---------- global_method : str - Global optimization method used to fit parameters. See supported :mod:`~despasito.parameter_fitting.global_methods`. - parameters_guess : numpy.ndarray, - An array of initial guesses for parameters, these will be optimized throughout the process. + Global optimization method used to fit parameters. See supported + :mod:`~despasito.parameter_fitting.global_methods`. + parameters_guess : numpy.ndarray, + An array of initial guesses for parameters, these will be optimized throughout + the process. bounds : list[tuple] - List of length equal to fit_parameter_names with lists of pairs for minimum and maximum bounds of parameter being fit. Defaults are broad, recommend specification. + List of length equal to fit_parameter_names with lists of pairs for minimum + and maximum bounds of parameter being fit. Defaults are broad, recommend + specification. fit_bead : str - Name of bead whose parameters are being fit, should be in bead list of bead_configuration + Name of bead whose parameters are being fit, should be in bead list of + bead_configuration fit_parameter_names : list[str] - This list contains the name of the parameter being fit (e.g. epsilon). See Eos object documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + This list contains the name of the parameter being fit (e.g. epsilon). See Eos + object documentation for supported parameter names. Cross interaction + parameter names should be composed of parameter name and the other bead type, + separated by an underscore (e.g. epsilon_CO2). exp_dict : dict Dictionary of experimental data objects. global_opts : dict, Optional - Keyword arguments of global optimization algorithm. See specific options in :mod:`~despasito.parameter_fitting.global_methods`. Note that unless in the keyword, ``workers`` is provided, the thermodynamic calculations will we split among the cores. Check the global optimization method to ensure it uses the ``workers`` keyword. + Keyword arguments of global optimization algorithm. See specific options in + :mod:`~despasito.parameter_fitting.global_methods`. Note that unless in the + keyword, ``workers`` is provided, the thermodynamic calculations will we split + among the cores. Check the global optimization method to ensure it uses the + ``workers`` keyword. minimizer_opts : dict, Optional Dictionary used to define minimization type and the associated options. - method (str) - Method available to scipy.optimize.minimize - - options (dict) - This dictionary contains the kwargs available to the chosen method + - options (dict) - This dictionary contains the kwargs available to the chosen + method constraints : dict, Optional - This dictionary of constraint types and their arguments will be converted into the appropriate form for the chosen optimization method. + This dictionary of constraint types and their arguments will be converted into + the appropriate form for the chosen optimization method. Returns ------- Objective : float - Sum of objective values according to appropriate weights. Output file saved in current working directory. - + Sum of objective values according to appropriate weights. Output file saved in + current working directory. + """ logger.info("Using global optimization method: {}".format(global_method)) @@ -270,8 +322,10 @@ def global_minimization(global_method, *args, **kwargs): func = getattr(global_methods_mod, global_method) except Exception: raise ImportError( - "The global minimization type, '{}', was not found\nThe following calculation types are supported: {}".format( - function, ", ".join(calc_list) + "The global minimization type, '{}',".format(global_method) + + " was not found\nThe following " + + "calculation types are supported: {}".format( + ", ".join(calc_list) ) ) @@ -282,28 +336,35 @@ def global_minimization(global_method, *args, **kwargs): def initialize_constraints(constraints, constraint_type): r""" - A tuple of either constraint classes or dictionaries as required by :mod:`~despasito.parameter_fitting.global_methods`. + A tuple of either constraint classes or dictionaries as required by + :mod:`~despasito.parameter_fitting.global_methods`. Parameters ---------- constraints : dict - This dictionary of constraint types and their arguments will be converted into the appropriate form for the chosen optimization method. Although the key can be anything, it must represent a dictionary containing: + This dictionary of constraint types and their arguments will be converted into + the appropriate form for the chosen optimization method. Although the key can + be anything, it must represent a dictionary containing: - * function (str) - must be found in the dictionary and represent a valid function name from :mod:`~despasito.parameter_fitting.constraint_types` + * function (str) - must be found in the dictionary and represent a valid + function name from :mod:`~despasito.parameter_fitting.constraint_types` * type - entries depends on ``constraint_type`` * args - Inputs into the functions (keys) constraint_type : str - Either 'dict' or 'class'. Changes the constraint to the specified form. - - * 'dict': Allowed types, "eq" or "ineq", eq means must be zero, ineq means it must be non-negative - * 'class': Allowed types, "nonlinear" or "linear", a keyword argument may also be added for the constraint class + Either 'dict' or 'class'. Changes the constraint to the specified form. + + * 'dict': Allowed types, "eq" or "ineq", eq means must be zero, ineq means it + must be non-negative + * 'class': Allowed types, "nonlinear" or "linear", a keyword argument may also + be added for the constraint class Returns ------- new_constraints : tuple - A tuple of either constraint classes or dictionaries as required by global optimization methods - + A tuple of either constraint classes or dictionaries as required by global + optimization methods + """ calc_list = [ @@ -322,14 +383,14 @@ def initialize_constraints(constraints, constraint_type): func = getattr(constraints_mod, kwargs["function"]) except Exception: raise ImportError( - "The constraint type, '{}', was not found\nThe following types are supported: {}".format( - function, ", ".join(calc_list) - ) + "The constraint type, '{}', was not".format(kwargs["function"]) + + " found\nThe following types are " + + "supported: {}".format(", ".join(calc_list)) ) if "args" not in kwargs: raise ValueError( - "Constraint function, {}, is missing arguements".format( + "Constraint function, {}, is missing arguments".format( kwargs["function"] ) ) @@ -337,27 +398,33 @@ def initialize_constraints(constraints, constraint_type): if constraint_type == "class": if "type" not in kwargs or kwargs["type"] in ["linear", "nonlinear"]: raise ValueError( - "Constraint, {}, does not have type. Type can be 'linear' or 'nonlinear'.".format( - kwargs["function"] - ) + "Constraint, {}, does not have type. ".format(kwargs["function"]) + + "Type can be 'linear' or 'nonlinear'." ) if kwargs["type"] == "linear": if "kwargs" not in kwargs: - output = LinearConstraint(func, args[0], args[1]) + output = LinearConstraint( + func, kwargs["args"][0], kwargs["args"][1] + ) else: - output = LinearConstraint(func, args[0], args[1], **kwargs) + output = LinearConstraint( + func, kwargs["args"][0], kwargs["args"][1], **kwargs + ) elif kwargs["type"] == "nonlinear": if "kwargs" not in kwargs: - output = NonlinearConstraint(func, args[0], args[1]) + output = NonlinearConstraint( + func, kwargs["args"][0], kwargs["args"][1] + ) else: - output = NonlinearConstraint(func, args[0], args[1], **kwargs) + output = NonlinearConstraint( + func, kwargs["args"][0], kwargs["args"][1], **kwargs + ) elif constraint_type == "dict": if "type" not in kwargs or kwargs["type"] in ["eq", "ineq"]: raise ValueError( - "Constraint, {}, does not have type. Type can be 'eq' or 'ineq'.".format( - kwargs["function"] - ) + "Constraint, {}, does not".format(kwargs["function"]) + + " have type. Type can be 'eq' or 'ineq'." ) output = {"type": kwargs["type"], "function": func, "args": kwargs["args"]} else: @@ -368,43 +435,56 @@ def initialize_constraints(constraints, constraint_type): return tuple(new_constraints) -def compute_obj(beadparams, fit_bead, fit_parameter_names, exp_dict, bounds, frozen_parameters=None): +def compute_obj( + beadparams, fit_bead, fit_parameter_names, exp_dict, bounds, frozen_parameters=None +): r""" - Fit defined parameters for equation of state object with given experimental data. + Fit defined parameters for equation of state object with given experimental data. - Each set of experimental data is converted to an object with the built-in ability to evaluate its part of objective function. + Each set of experimental data is converted to an object with the built-in ability + to evaluate its part of objective function. To add another type of supported experimental data, see :ref:`contribute-fitting`. Parameters ---------- - parameters_guess : numpy.ndarray, - An array of initial guesses for parameters, these will be optimized throughout the process. + parameters_guess : numpy.ndarray, + An array of initial guesses for parameters, these will be optimized throughout + the process. fit_bead : str Name of bead whose parameters are being fit. fit_parameter_names : list[str] - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + This list contains the name of the parameter being fit (e.g. epsilon). See EOS + documentation for supported parameter names. Cross interaction parameter names + should be composed of parameter name and the other bead type, separated by an + underscore (e.g. epsilon_CO2). exp_dict : dict Dictionary of experimental data objects. bounds : list[tuple] - List of length equal to fit_parameter_names with lists of pairs containing minimum and maximum bounds of parameters being fit. Defaults from Eos object are broad, so we recommend specification. + List of length equal to fit_parameter_names with lists of pairs containing + minimum and maximum bounds of parameters being fit. Defaults from Eos object + are broad, so we recommend specification. frozen_parameters : numpy.ndarray, Optional, default=None - List of first parameters in the fit_parameter_names list that are frozen during minimization. This feature is currently used in the :func:`~despasito.parameter_fitting.global_methods.grid_minimization` method, enabled with the ``split_grid_minimization`` feature. + List of first parameters in the fit_parameter_names list that are frozen during + minimization. This feature is currently used in the + :func:`~despasito.parameter_fitting.global_methods.grid_minimization` method, + enabled with the ``split_grid_minimization`` feature. Returns ------- Objective : float Sum of objective values according to appropriate weights. - + """ # Update bead_library with test parameters if len(beadparams) != len(fit_parameter_names): - if np.any(frozen_parameters != None): + if frozen_parameters is not None: beadparams = np.array(list(frozen_parameters) + list(beadparams)) else: raise ValueError( - "The length of initial guess vector should be the same number of parameters to be fit." + "The length of initial guess vector should be the same number of " + + "parameters to be fit." ) logger.info( @@ -448,18 +528,19 @@ def compute_obj(beadparams, fit_bead, fit_parameter_names, exp_dict, bounds, fro obj_total += (1e3 * (param - bounds[i][1])) ** 8 else: logger.info("One of provided parameters, {}, is NaN".format(beadparams)) - obj_function = [np.nan for _ in exp_dict] - obj_total = np.nansum(obj_function) + obj_total = np.inf if obj_total == 0.0 and np.isnan(np.sum(obj_function)): obj_total = np.inf # Write out parameters and objective functions for each dataset logger.info( - "\nParameters: {}Total Obj.\nValues: {}{}\nExp. Data: {}\nObj. Values: {}".format( - ("{}, "*len(fit_parameter_names)).format(*fit_parameter_names), - ("{}, "*len(beadparams)).format(*beadparams), + "\nParameters: {}Total Obj.\nValues: {}{}\n".format( + ("{}, " * len(fit_parameter_names)).format(*fit_parameter_names), + ("{}, " * len(beadparams)).format(*beadparams), obj_total, + ) + + "Exp. Data: {}\nObj. Values: {}".format( list(exp_dict.keys()), obj_function, ) @@ -476,10 +557,11 @@ def obj_function_form( nan_number=1000, nan_ratio=0.1, ): - """ - Sets objective functional form + r""" + Sets objective functional form - Note that if the result is np.nan, that point is removed from the list for the purposes of averaging. + Note that if the result is np.nan, that point is removed from the list for the + purposes of averaging. Parameters ---------- @@ -488,20 +570,31 @@ def obj_function_form( data0 : numpy.ndarray Reference data for comparison weights : (numpy.ndarray or float), Optional, default=1.0 - Can be a float or array of data of the same length as ``data_test``. Allows the user to tune the importance of various data points. + Can be a float or array of data of the same length as ``data_test``. Allows + the user to tune the importance of various data points. method : str, Optional, default="mean-squared-relative-error" Keyword used to choose the functional form. Can be: - - average-squared-deviation: :math:`\sum{(\\frac{data\_test-data0}{data0})^2}/N` + - average-squared-deviation: + :math:`\sum{(\\frac{data\_test-data0}{data0})^2}/N` - sum-squared-deviation: :math:`\sum{(\\frac{data\_test-data0}{data0})^2}` - - sum-squared-deviation-boltz: :math:`\sum{(\\frac{data\_test-data0}{data0})^2 exp(\\frac{data\_test\_min-data\_test}{|data\_test\_min|})}` [DOI: 10.1063/1.2181979] - - sum-deviation-boltz: :math:`\sum{\\frac{data\_test-data0}{data0} exp(\\frac{data\_test\_min-data\_test}{|data\_test\_min|})}` [DOI: 10.1063/1.2181979] - - percent-absolute-average-deviation: :math:`\sum{(\\frac{data\_test-data0}{data0})^2}/N \\times 100` + - sum-squared-deviation-boltz: :math:`\sum{(\\frac{data\_test-data0}{data0})^2` + :math:`exp(\\frac{data\_test\_min-data\_test}{|data\_test\_min|})}` + [DOI: 10.1063/1.2181979] + - sum-deviation-boltz: :math:`\sum{\\frac{data\_test-data0}{data0}` + :math:`exp(\\frac{data\_test\_min-data\_test}{|data\_test\_min|})}` + [DOI: 10.1063/1.2181979] + - percent-absolute-average-deviation: + :math:`\sum{(\\frac{data\_test-data0}{data0})^2}/N \\times 100` nan_ratio : float, Optional, default=0.1 - If more than "nan_ratio*100" percent of the calculated data failed to produce NaN, increase the objective value by the number of entries where data_test is NaN times ``nan_number``. + If more than "nan_ratio*100" percent of the calculated data failed to produce + NaN, increase the objective value by the number of entries where data_test is + NaN times ``nan_number``. nan_number : float, Optional, default=1000 - If a thermodynamic calculation produces NaN, add this quantity to the objective value. (See nan_ratio) + If a thermodynamic calculation produces NaN, add this quantity to the + objective + value. (See nan_ratio) Returns ------- @@ -511,16 +604,14 @@ def obj_function_form( if np.size(data0) != np.size(data_test): raise ValueError( - "Input data of length, {}, must be the same length as reference data of length {}".format( - len(data_test), len(data0) - ) + "Input data of length, {}, must be the same length".format(len(data_test)) + + " as reference data of length {}".format(len(data0)) ) if np.size(weights) > 1 and np.size(weights) != np.size(data_test): raise ValueError( - "Weight for data is provided as an array of length, {}, but must be length, {}.".format( - len(weights), len(data_test) - ) + "Weight for data is provided as an array of length, " + + "{}, but must be length, {}.".format(len(weights), len(data_test)) ) data_tmp = np.array( @@ -542,15 +633,15 @@ def obj_function_form( weight_tmp = weights if method == "average-squared-deviation": - obj_value = np.mean(data_tmp ** 2 * weight_tmp) + obj_value = np.mean(data_tmp**2 * weight_tmp) elif method == "sum-squared-deviation": - obj_value = np.sum(data_tmp ** 2 * weight_tmp) + obj_value = np.sum(data_tmp**2 * weight_tmp) elif method == "sum-squared-deviation-boltz": data_min = np.min(data_tmp) obj_value = np.sum( - data_tmp ** 2 * weight_tmp * np.exp((data_min - data_tmp) / np.abs(data_min)) + data_tmp**2 * weight_tmp * np.exp((data_min - data_tmp) / np.abs(data_min)) ) elif method == "sum-deviation-boltz": @@ -570,15 +661,13 @@ def obj_function_form( if tmp > nan_ratio: obj_value += (len(data_test) - len(data_tmp)) * nan_number logger.debug( - "Values of NaN were removed from objective value calculation, nan_ratio {} > {}, augment obj. value".format( - tmp, nan_ratio - ) + "Values of NaN were removed from objective value calculation, " + + "nan_ratio {} > {}, augment obj. value".format(tmp, nan_ratio) ) else: logger.debug( - "Values of NaN were removed from objective value calculation, nan_ratio {} < {}".format( - tmp, nan_ratio - ) + "Values of NaN were removed from objective value calculation, " + + "nan_ratio {} < {}".format(tmp, nan_ratio) ) return obj_value diff --git a/despasito/parameter_fitting/global_methods.py b/despasito/parameter_fitting/global_methods.py index 4a33ca6..4754097 100644 --- a/despasito/parameter_fitting/global_methods.py +++ b/despasito/parameter_fitting/global_methods.py @@ -1,6 +1,7 @@ """ -The function names of this module represents global optimization methods that can be specified as ``global_opts["method"]`` in :func:`~despasito.parameter_fitting.fit`. +The function names of this module represents global optimization methods that can be +specified as ``global_opts["method"]`` in :func:`~despasito.parameter_fitting.fit`. """ @@ -24,18 +25,24 @@ def single_objective( Parameters ---------- - parameters_guess : numpy.ndarray + parameters_guess : numpy.ndarray An array of initial guesses for parameters. bounds : list[tuple] - List of length equal to fit_parameter_names with lists of pairs for minimum and maximum bounds of parameter being fit. Defaults from Eos object are broad, so we recommend specification. + List of length equal to fit_parameter_names with lists of pairs for minimum + and maximum bounds of parameter being fit. Defaults from Eos object are broad, + so we recommend specification. fit_bead : str Name of bead whose parameters are being fit. fit_parameter_names : list[str] - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + This list contains the name of the parameter being fit (e.g. epsilon). See + EOS documentation for supported parameter names. Cross interaction parameter + names should be composed of parameter name and the other bead type, separated + by an underscore (e.g. epsilon_CO2). exp_dict : dict Dictionary of experimental data objects. global_opts : dict, Optional, default={} - This dictionary is included for continuity with other global optimization methods, although this method doesn't have options. + This dictionary is included for continuity with other global optimization + methods, although this method doesn't have options. Returns ------- @@ -74,30 +81,41 @@ def differential_evolution( constraints=None, ): r""" - Fit defined parameters for equation of state object using scipy.optimize.differential_evolution with given experimental data. + Fit defined parameters for equation of state object using + scipy.optimize.differential_evolution with given experimental data. Parameters ---------- - parameters_guess : numpy.ndarray - An array of initial guesses for parameters. + parameters_guess : numpy.ndarray + An array of initial guesses for parameters. Not used in this method. bounds : list[tuple] - List of length equal to fit_parameter_names with lists of pairs for minimum and maximum bounds of parameter being fit. Defaults from Eos object are broad, so we recommend specification. + List of length equal to fit_parameter_names with lists of pairs for minimum + and maximum bounds of parameter being fit. Defaults from Eos object are broad, + so we recommend specification. fit_bead : str Name of bead whose parameters are being fit. fit_parameter_names : list[str] - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + This list contains the name of the parameter being fit (e.g. epsilon). See EOS + documentation for supported parameter names. Cross interaction parameter names + should be composed of parameter name and the other bead type, separated by an + underscore (e.g. epsilon_CO2). exp_dict : dict Dictionary of experimental data objects. global_opts : dict, Optional - init (str) - Optional, default="random", type of initiation for population - - write_intermediate_file (str) - Optional, default=False, If True, an intermediate file will be written from the method callback - - filename (str) - Optional, default=None, filename for callback output, if provided, ``write_intermediate_file`` will be set to True - - obj_cut (float) - Optional, default=None, Cut-off objective value to write the parameters, if provided, ``write_intermediate_file`` will be set to True - - etc. Other keywords for scipy.optimize.differential_evolution use the function defaults + - write_intermediate_file (str) - Optional, default=False, If True, an + intermediate file will be written from the method callback + - filename (str) - Optional, default=None, filename for callback output, if + provided, ``write_intermediate_file`` will be set to True + - obj_cut (float) - Optional, default=None, Cut-off objective value to write + the parameters, if provided, ``write_intermediate_file`` will be set to True + - etc. Other keywords for scipy.optimize.differential_evolution use the + function defaults constraints : dict, Optional, default=None - This dictionary of constraint types and their arguments will be converted into a tuple of constraint classes that is compatible + This dictionary of constraint types and their arguments will be converted into + a tuple of constraint classes that is compatible Returns ------- @@ -175,18 +193,24 @@ def shgo( constraints=None, ): r""" - Fit defined parameters for equation of state object using scipy.optimize.shgo with given experimental data. + Fit defined parameters for equation of state object using scipy.optimize.shgo with + given experimental data. Parameters ---------- - parameters_guess : numpy.ndarray + parameters_guess : numpy.ndarray An array of initial guesses for parameters. bounds : list[tuple] - List of length equal to fit_parameter_names with lists of pairs for minimum and maximum bounds of parameter being fit. Defaults from Eos object are broad, so we recommend specification. + List of length equal to fit_parameter_names with lists of pairs for minimum + and maximum bounds of parameter being fit. Defaults from Eos object are broad, + so we recommend specification. fit_bead : str Name of bead whose parameters are being fit. fit_parameter_names : list[str] - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + This list contains the name of the parameter being fit (e.g. epsilon). See EOS + documentation for supported parameter names. Cross interaction parameter names + should be composed of parameter name and the other bead type, separated by an + underscore (e.g. epsilon_CO2). exp_dict : dict Dictionary of experimental data objects. global_opts : dict, Optional, default={} @@ -197,11 +221,14 @@ def shgo( minimizer_opts : dict, Optional, default={} Dictionary used to define minimization type and the associated options. - - method (str) - Optional, default=nelder-mead, Method available to scipy.optimize.minimize - - options (dict) - Optional, default={'maxiter': 50}, This dictionary contains the kwargs available to the chosen method + - method (str) - Optional, default=nelder-mead, Method available to + scipy.optimize.minimize + - options (dict) - Optional, default={'maxiter': 50}, This dictionary contains + the kwargs available to the chosen method constraints : dict, Optional, default=None - This dictionary of constraint types and their arguments will be converted into a tuple of dictionaries that is compatible + This dictionary of constraint types and their arguments will be converted into + a tuple of dictionaries that is compatible Returns ------- @@ -213,6 +240,7 @@ def shgo( global_opts = global_opts.copy() # Options for differential evolution, set defaults in new_global_opts + obj_kwargs = ["obj_cut", "filename", "write_intermediate_file"] new_global_opts = {"sampling_method": "sobol"} if global_opts: for key, value in global_opts.items(): @@ -245,15 +273,11 @@ def shgo( ]: del minimizer_opts["options"][key] - if minimizer_opts: - logger.warning( - "Minimization options were given but aren't used in this method." - ) - result = spo.shgo( ff.compute_obj, bounds, args=(fit_bead, fit_parameter_names, exp_dict, bounds), + minimizer_kwargs=minimizer_opts, **global_opts ) @@ -271,35 +295,50 @@ def grid_minimization( constraints=None, ): r""" - Fit defined parameters for equation of state object using a custom adaptation of scipy.optimize.brute with given experimental data. + Fit defined parameters for equation of state object using a custom adaptation of + scipy.optimize.brute with given experimental data. Parameters ---------- - parameters_guess : numpy.ndarray + parameters_guess : numpy.ndarray An array of initial guesses for parameters. bounds : list[tuple] - List of length equal to fit_parameter_names with lists of pairs for minimum and maximum bounds of parameter being fit. Defaults from Eos object are broad, so we recommend specification. + List of length equal to fit_parameter_names with lists of pairs for minimum + and maximum bounds of parameter being fit. Defaults from Eos object are broad, + so we recommend specification. fit_bead : str Name of bead whose parameters are being fit. fit_parameter_names : list[str] - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + This list contains the name of the parameter being fit (e.g. epsilon). See EOS + documentation for supported parameter names. Cross interaction parameter names + should be composed of parameter name and the other bead type, separated by an + underscore (e.g. epsilon_CO2). exp_dict : dict Dictionary of experimental data objects. global_opts : dict, Optional, default={} - Ns (int) - Optional, default=5, Number of grid points along the axes - - finish (callable) - Optional, default=scipy.optimize.minimize, A minimization function - - initial_guesses (list) - Optional, Replaces grid of values generated with bounds and Ns - - split_grid_minimization (int) - Optional, default=0, Choose index of first parameter to fit, while the grid is formed from those before. For example, if 4 parameters are defined and ``split_grid_minimization==2``, then a grid is formed for the first two parameters ``parameters_guess[:2]``, and the remaining parameters, ``parameters_guess[2:]`` are minimized. + - finish (callable) - Optional, default=scipy.optimize.minimize, A minimization + function + - initial_guesses (list) - Optional, Replaces grid of values generated with + bounds and Ns + - split_grid_minimization (int) - Optional, default=0, Choose index of first + parameter to fit, while the grid is formed from those before. For example, if + 4 parameters are defined and ``split_grid_minimization==2``, then a grid is + formed for the first two parameters ``parameters_guess[:2]``, and the remaining + parameters, ``parameters_guess[2:]`` are minimized. minimizer_opts : dict, Optional, default={} Dictionary used to define minimization type and the associated options. - - method (str) - Optional, default="least_squares", Method available to our :func:`~despasito.utils.general_toolbox.solve_root` function - - options (dict) - Optional, default={}, This dictionary contains the kwargs available to the chosen method + - method (str) - Optional, default="least_squares", Method available to our + :func:`~despasito.utils.general_toolbox.solve_root` function + - options (dict) - Optional, default={}, This dictionary contains the kwargs + available to the chosen method constraints : dict, Optional, default=None - This dictionary of constraint types and their arguments will be converted into a tuple of constraint classes that is compatible + This dictionary of constraint types and their arguments will be converted into + a tuple of constraint classes that is compatible Returns ------- @@ -348,16 +387,22 @@ def grid_minimization( # Set up inputs if "initial_guesses" in global_opts: + del global_opts["Ns"] x0_array = global_opts["initial_guesses"] if global_opts["split_grid_minimization"] != 0: inputs = [] + bounds = bounds[global_opts["split_grid_minimization"]:] for x0 in x0_array: tmp1 = x0[global_opts["split_grid_minimization"]:] - tmp2 = x0[:global_opts["split_grid_minimization"]] - inputs.append(tmp1, (*args, tmp2), bounds, constraints, minimizer_opts) + tmp2 = x0[: global_opts["split_grid_minimization"]] + inputs.append( + (tmp1, (*args, tmp2), bounds, constraints, minimizer_opts) + ) else: - inputs = [(x0, args, bounds, constraints, minimizer_opts) for x0 in x0_array] + inputs = [ + (x0, args, bounds, constraints, minimizer_opts) for x0 in x0_array + ] else: # Initialization based on implementation in scipy.optimize.brute @@ -365,17 +410,17 @@ def grid_minimization( N = len(bounds) lrange = list(bounds) for k in range(N): - if type(lrange[k]) is not type(slice(None)): + if lrange[k] is not None: if len(lrange[k]) < 3: lrange[k] = tuple(lrange[k]) + (complex(global_opts["Ns"]),) lrange[k] = slice(*lrange[k]) else: - if type(global_opts["split_grid_minimization"]) != int: + if not isinstance(global_opts["split_grid_minimization"], int): raise ValueError("Option, split_grid_minimization, must be an integer") - N = len(bounds[:global_opts["split_grid_minimization"]]) - lrange = list(bounds[:global_opts["split_grid_minimization"]]) + N = len(bounds[: global_opts["split_grid_minimization"]]) + lrange = list(bounds[: global_opts["split_grid_minimization"]]) for k in range(N): - if type(lrange[k]) is not type(slice(None)): + if lrange[k] is not None: if len(lrange[k]) < 3: lrange[k] = tuple(lrange[k]) + (complex(global_opts["Ns"]),) lrange[k] = slice(*lrange[k]) @@ -390,11 +435,17 @@ def grid_minimization( x0_array = np.reshape(x0_array, (inpt_shape[0], np.prod(inpt_shape[1:]))).T if global_opts["split_grid_minimization"] != 0: - min_parameters = list(parameters_guess[global_opts["split_grid_minimization"]:]) - inputs = [(min_parameters, (*args, x0), bounds, constraints, minimizer_opts) for x0 in x0_array] - + min_parameters = list( + parameters_guess[global_opts["split_grid_minimization"]:] + ) + inputs = [ + (min_parameters, (*args, x0), bounds, constraints, minimizer_opts) + for x0 in x0_array + ] else: - inputs = [(x0, args, bounds, constraints, minimizer_opts) for x0 in x0_array] + inputs = [ + (x0, args, bounds, constraints, minimizer_opts) for x0 in x0_array + ] lx = len(x0_array) @@ -410,13 +461,21 @@ def grid_minimization( # Choose final output if global_opts["split_grid_minimization"] != 0: - x0_new = np.zeros((lx,len(parameters_guess))) - results_new = np.zeros((lx,len(parameters_guess))) + if "initial_guesses" not in global_opts: + x0_new = np.zeros((lx, len(parameters_guess))) + results_new = np.zeros((lx, len(parameters_guess))) for i in range(len(x0_array)): - x0_new[i] = np.array(list(x0_array[i])+list(min_parameters)) - results_new[i] = np.array(list(x0_array[i])+list(results[i])) + if "initial_guesses" not in global_opts: + x0_new[i] = np.array(list(x0_array[i]) + list(min_parameters)) + results_new[i] = np.array(list(x0_array[i]) + list(results[i])) + else: + results_new[i] = np.array( + list(x0_array[i][: global_opts["split_grid_minimization"]]) + + list(results[i]) + ) results = results_new - x0_array = x0_new + if "initial_guesses" not in global_opts: + x0_array = x0_new result = [fval[0], results[0]] logger.info("For bead: {} and parameters {}".format(fit_bead, fit_parameter_names)) @@ -430,9 +489,11 @@ def grid_minimization( x=result[1], fun=result[0], success=True, - nit=lx * global_opts["Ns"], - message="Termination successful with {} grid points and the minimum value minimized. Note that parameters may be outside of the given bounds because of the minimizing function.".format( - lx * global_opts["Ns"] + nit=lx, + message=( + "Termination successful with {} grid points and".format(lx) + + " the minimum value minimized. Note that parameters may be outside of" + " the given bounds because of the minimizing function." ), ) @@ -443,24 +504,31 @@ def brute( parameters_guess, bounds, fit_bead, fit_parameter_names, exp_dict, global_opts={} ): r""" - Fit defined parameters for equation of state object using scipy.optimize.brute with given experimental data. + Fit defined parameters for equation of state object using scipy.optimize.brute with + given experimental data. Parameters ---------- - parameters_guess : numpy.ndarray + parameters_guess : numpy.ndarray An array of initial guesses for parameters. bounds : list[tuple] - List of length equal to fit_parameter_names with lists of pairs for minimum and maximum bounds of parameter being fit. Defaults from Eos object are broad, so we recommend specification. + List of length equal to fit_parameter_names with lists of pairs for minimum and + maximum bounds of parameter being fit. Defaults from Eos object are broad, so we + recommend specification. fit_bead : str Name of bead whose parameters are being fit. fit_parameter_names : list[str] - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + This list contains the name of the parameter being fit (e.g. epsilon). See EOS + documentation for supported parameter names. Cross interaction parameter names + should be composed of parameter name and the other bead type, separated by an + underscore (e.g. epsilon_CO2). exp_dict : dict Dictionary of experimental data objects. global_opts : dict, Optional, default={} - Ns (int) - Optional, default=5, Number of grid points along the axes - - finish (callable) - Optional, default=scipy.optimize.minimize, An optimization function + - finish (callable) - Optional, default=scipy.optimize.minimize, An optimization + function - etc. Other keywords for scipy.optimize.brute use the function defaults Returns @@ -499,8 +567,10 @@ def brute( fun=fval, success=True, nit=len(x0) * global_opts["Ns"], - message="Termination successful with {} grid points and the minimum value minimized. Note that parameters may be outside of the given bounds because of the minimizing function.".format( - len(x0) * global_opts["Ns"] + message=( + "Termination successful with {} grid points and the minimum value " + "minimized. Note that parameters may be outside of the given bounds " + "because of the minimizing function.".format(len(x0) * global_opts["Ns"]) ), ) @@ -517,43 +587,58 @@ def basinhopping( minimizer_opts={}, ): r""" - Fit defined parameters for equation of state object using scipy.optimize.basinhopping with given experimental data. + Fit defined parameters for equation of state object using + scipy.optimize.basinhopping with given experimental data. Parameters ---------- - parameters_guess : numpy.ndarray + parameters_guess : numpy.ndarray An array of initial guesses for parameters. bounds : list[tuple] - List of length equal to fit_parameter_names with lists of pairs for minimum and maximum bounds of parameter being fit. Defaults from Eos object are broad, so we recommend specification. + List of length equal to fit_parameter_names with lists of pairs for minimum and + maximum bounds of parameter being fit. Defaults from Eos object are broad, so + we recommend specification. fit_bead : str Name of bead whose parameters are being fit. fit_parameter_names : list[str] - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + This list contains the name of the parameter being fit (e.g. epsilon). See EOS + documentation for supported parameter names. Cross interaction parameter names + should be composed of parameter name and the other bead type, separated by an + underscore (e.g. epsilon_CO2). exp_dict : dict Dictionary of experimental data objects. global_opts : dict, Optional, default={} - - niter (int) - Optional, default=10, The number of basin-hopping iterations - - T (float) - Optional, default=0.5, The "temperature" parameter for the accept or reject criterion. For best results T should be comparable to the separation (in function value) between local minima. - - niter_success (int) Optional, default=3, Stop the run if the global minimum candidate remains the same for this number of iterations. - - stepsize (float) - Optional, default=0.1, Maximum step size for use in the random displacement. + - niter (int) - Optional, default=10, The number of basin-hopping iterations + - T (float) - Optional, default=0.5, The "temperature" parameter for the accept + or reject criterion. For best results T should be comparable to the separation + (in function value) between local minima. + - niter_success (int) Optional, default=3, Stop the run if the global minimum + candidate remains the same for this number of iterations. + - stepsize (float) - Optional, default=0.1, Maximum step size for use in the + random displacement. - take_step (callable) - Set with custom BasinStep class - - write_intermediate_file (str) - Optional, default=False, If True, an intermediate file will be written from the method callback - - filename (str) - Optional, default=None, filename for callback output, if provided, `write_intermediate_file` will be set to True - - obj_cut (float) - Optional, default=None, Cut-off objective value to write the parameters, if provided, `write_intermediate_file` will be set to True - - etc. Other keywords for scipy.optimize.basinhopping use the function defaults + - write_intermediate_file (str) - Optional, default=False, If True, an + intermediate file will be written from the method callback + - filename (str) - Optional, default=None, filename for callback output, if + provided, `write_intermediate_file` will be set to True + - obj_cut (float) - Optional, default=None, Cut-off objective value to write + the parameters, if provided, `write_intermediate_file` will be set to True + - etc. Other keywords for scipy.optimize.basinhopping use the function + defaults minimizer_opts : dict, Optional, default={} Dictionary used to define minimization type and the associated options. - method (str) - Method available to scipy.optimize.minimize - - options (dict) - This dictionary contains the kwargs available to the chosen method + - options (dict) - This dictionary contains the kwargs available to the chosen + method Returns ------- Objective : obj scipy OptimizedResult object - + """ global_opts = global_opts.copy() @@ -630,21 +715,22 @@ def basinhopping( return result -# ___________ Supporting Classes and Functions ___________________________________________ +# ___________ Supporting Classes and Functions _________________ def _grid_minimization_wrapper(args): - """ Wrapper for minimization method in grid_minimization - """ + """Wrapper for minimization method in grid_minimization""" x0, obj_args, bounds, constraints, opts = args - if constraints != None: - logger.warning("Constraints defined, but grid_minimization does not support their use.") + if constraints is not None: + logger.warning( + "Constraints defined, but grid_minimization does not support their use." + ) opts = opts.copy() if "method" in opts: method = opts["method"] del opts["method"] - + try: result = gtb.solve_root( ff.compute_obj, @@ -655,12 +741,12 @@ def _grid_minimization_wrapper(args): options=opts, ) except Exception: - logger.debug("Minimization Failed:", exc_info=True) + logger.info("Minimization Failed:", exc_info=True) result = np.nan * np.ones(len(x0)) # Return NaN if the parameters didn't change if np.sum(np.abs(result - x0)) < 1e-6: - logger.debug("Minimization Failed:", exc_info=True) + logger.info("Minimization Failed:", exc_info=True) result = np.nan * np.ones(len(x0)) logger.info("Starting parameters: {}, converged to: {}".format(x0, result)) @@ -671,18 +757,17 @@ def _grid_minimization_wrapper(args): class _BasinStep(object): - r""" Custom basin step used by scipy.optimize.basinhopping function. - """ + r"""Custom basin step used by scipy.optimize.basinhopping function.""" def __init__(self, stepmag, stepsize=0.05): r""" - + Parameters ---------- stepmag : list List of step magnitudes stepsize : float, Optional, default=0.05 - Step size + Step size Attributes ---------- @@ -690,16 +775,15 @@ def __init__(self, stepmag, stepsize=0.05): List of step magnitudes stepsize : float, Optional, default=0.05 Step size - + """ self._stepsize = stepsize self._stepmag = stepmag def __call__(self, x): - r""" - + Parameters ---------- x : numpy.ndarray @@ -709,7 +793,7 @@ def __call__(self, x): ------- basinstep : numpy.ndarray Suggested basin step used in scipy.optimize.basinhopping algorithm - + """ # Save initial guess in array @@ -722,7 +806,6 @@ def __call__(self, x): breakloop = True # Iterate through array of step magnitudes for i, mag in enumerate(self._stepmag): - # Add or subtract a random number within distribution of +- mag*stepsize x[i] += np.random.uniform(-mag * self._stepsize, mag * self._stepsize) # If a value of x is negative, don't break the cycle if x[i] < 0.0: @@ -734,24 +817,23 @@ def __call__(self, x): class _BasinBounds(object): - r""" Object used by scipy.optimize.basinhopping to set bounds of parameters. - """ + r"""Object used by scipy.optimize.basinhopping to set bounds of parameters.""" def __init__(self, bounds): r""" - + Parameters ---------- bounds : numpy.ndarray Bounds on parameters - + Attributes ---------- xmin : numpy.ndarray Array of minimum values for each parameter xman : numpy.ndarray Array of maximum values for each parameter - + """ bounds = np.transpose(np.array(bounds)) self.xmin = bounds[0] @@ -759,20 +841,22 @@ def __init__(self, bounds): def __call__(self, **kwargs): r""" - + Parameters ---------- kwargs - Keyword arguments used in BasinBounds object for scipy.optimize.basinhopping - + Keyword arguments used in BasinBounds object for + scipy.optimize.basinhopping + - x_new (numpy.ndarray) - Guess in parameters values - f_new (numpy.ndarray) - Objective value for given parameters Returns ------- value : bool - A true or false value that says whether the guess in parameter value is within bounds - + A true or false value that says whether the guess in parameter value is + within bounds + """ x = kwargs["x_new"] tmax = bool(np.all(x <= self.xmax)) @@ -798,12 +882,11 @@ def __call__(self, **kwargs): class _WriteParameterResults(object): - r""" Object used by scipy.optimize.basinhopping to set bounds of parameters. - """ + r"""Object used by scipy.optimize.basinhopping to set bounds of parameters.""" def __init__(self, beadnames, obj_cut=None, filename=None): r""" - + Attributes ---------- beadnames : list[str] @@ -816,15 +899,15 @@ def __init__(self, beadnames, obj_cut=None, filename=None): Returns ------- Initiate file with parameters - + """ - if obj_cut == None: + if obj_cut is None: self.obj_cut = np.inf else: self.obj_cut = obj_cut - if filename == None: + if filename is None: filename = "parameters.txt" if os.path.isfile(filename): @@ -845,27 +928,35 @@ def __init__(self, beadnames, obj_cut=None, filename=None): def __call__(self, *args, **kwargs): r""" - The provided args and kwargs change depending on the global optimization method. This class is equipped to distinguish the callback function for differential_evolution (length equal to ) and basinhopping. - + The provided args and kwargs change depending on the global optimization + method. This class is equipped to distinguish the callback function for + differential_evolution (length equal to ) and basinhopping. + Parameters ---------- args The provided args change depending on the global optimization method. - - - x_new (numpy.ndarray) - Current parameter values being evaluated, used in both algorithms - - f_new (float) - Current object function value for x_new, used in basinhopping - - accept (bool) - Whether or not that minimum was accepted, used in basinhopping + + - x_new (numpy.ndarray) - Current parameter values being evaluated, used + in both algorithms + - f_new (float) - Current object function value for x_new, used in + basinhopping + - accept (bool) - Whether or not that minimum was accepted, used in + basinhopping kwargs The provided kwargs change depending on the global optimization method. - - convergence (float) - Used in differential evolution, the fractional value of the population convergence. When greater than one the function halts. - + - convergence (float) - Used in differential evolution, the fractional + value of the population convergence. When greater than one the function + halts. + Returns ------- value : bool - A true or false value that says whether the guess in parameter value is within bounds - + A true or false value that says whether the guess in parameter value is + within bounds + """ if "convergence" in kwargs: # Used in differential_evolution @@ -895,7 +986,8 @@ def __call__(self, *args, **kwargs): f.write(("{}, " * len(tmp)).format(*tmp) + "\n") else: raise ValueError( - "Unknown inputs. This function is equipped to handle differential_evolution and basinhopping algorithms." + "Unknown inputs. This function is equipped to handle " + "differential_evolution and basinhopping algorithms." ) self.ninit += 1 @@ -904,7 +996,8 @@ def __call__(self, *args, **kwargs): def _del_Data_MultiprocessingObject(dictionary): - r""" A dictionary of fitting objects will remove MultiprocessingObject attributes so that the multiprocessing pool can be used by the fitting algorithm. + r"""A dictionary of fitting objects will remove MultiprocessingObject attributes + so that the multiprocessing pool can be used by the fitting algorithm. Parameters ---------- diff --git a/despasito/parameter_fitting/interface.py b/despasito/parameter_fitting/interface.py index 38b4e99..b53fd1b 100644 --- a/despasito/parameter_fitting/interface.py +++ b/despasito/parameter_fitting/interface.py @@ -19,12 +19,18 @@ class ExpDataTemplate(ABC): Dictionary of exp data * calculation_type (str) - Optional, default=*to be set* - * MultiprocessingObject (obj) - Optional, Initiated :class:`~despasito.utils.parallelization.MultiprocessingJob` + * MultiprocessingObject (obj) - Optional, Initiated + :class:`~despasito.utils.parallelization.MultiprocessingJob` * eos_obj (obj) - Equation of state object - * weights (dict) - A dictionary where each key is the header used in the exp. data file. The value associated with a header can be a list as long as the number of data points to multiply by the objective value associated with each point, or a float to multiply the objective value of this data set. - * density_opts (dict) - Optional, default={}, Dictionary of options used in calculating pressure vs. mole fraction curves. + * weights (dict) - A dictionary where each key is the header used in the exp. + data file. The value associated with a header can be a list as long as the + number of data points to multiply by the objective value associated with each + point, or a float to multiply the objective value of this data set. + * density_opts (dict) - Optional, default={}, Dictionary of options used in + calculating pressure vs. mole fraction curves. * Allowed property keys and associated values - * kwargs for :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` + * kwargs for + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form` Attributes ---------- @@ -33,17 +39,19 @@ class ExpDataTemplate(ABC): Eos : obj Equation of state object weights : dict, Optional, default: {"some_property": 1.0 ...} - Dictionary corresponding to thermo_dict, with weighting factor or vector for each system property used in fitting + Dictionary corresponding to thermo_dict, with weighting factor or vector for + each system property used in fitting obj_opts : dict - Keywords to compute the objective function with :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. + Keywords to compute the objective function with + :func:`~despasito.parameter_fitting.fit_functions.obj_function_form`. npoints : int Number of sets of system conditions this object computes thermodict : dict Dictionary of inputs needed for thermodynamic calculations - + - calculation_type (str) default=*to be set* - density_opts (dict) default={} - + """ def __init__(self, data_dict): @@ -88,17 +96,20 @@ def update_parameters(self, fit_bead, param_names, param_values): r""" Update a single parameter value during parameter fitting process. - To refresh those parameters that are dependent on to bead_library or cross_library, use method "parameter refresh". - + To refresh those parameters that are dependent on to bead_library or + cross_library, use method "parameter refresh". + Parameters ---------- fit_bead : str Name of bead being fit param_names : list - Parameters to be fit. See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of parameter name and the other bead type, separated by an underscore (e.g. epsilon_CO2). + Parameters to be fit. See EOS documentation for supported parameter names. + Cross interaction parameter names should be composed of parameter name and + the other bead type, separated by an underscore (e.g. epsilon_CO2). param_values : list Value of parameter - + """ for i, param in enumerate(param_names): @@ -120,9 +131,9 @@ def update_parameters(self, fit_bead, param_names, param_values): ) else: raise ValueError( - "Parameters for only one bead are allowed to be fit. Multiple underscores in a parameter name suggest more than one bead type in your fit parameter name, {}".format( - param - ) + "Parameters for only one bead are allowed to be fit. Multiple " + "underscores in a parameter name suggest more than one bead type " + "in your fit parameter name, {}".format(param) ) if hasattr(self.Eos, "parameter_refresh"): @@ -130,13 +141,16 @@ def update_parameters(self, fit_bead, param_names, param_values): @abstractmethod def objective(self): - """ Float representing objective function of from comparing predictions to experimental data. - """ + """Float representing objective function of from comparing predictions to + experimental data.""" pass def __str__(self): - string = "Data Set Object\nName: {}\nCalculation_type: {}\nNumber of Points: {}".format( - self.name, self.thermodict["calculation_type"], self.npoints + string = ( + "Data Set Object\nName: {}\nCalculation_type: {}\nNumber of " + "Points: {}".format( + self.name, self.thermodict["calculation_type"], self.npoints + ) ) return string diff --git a/despasito/tests/__init__.py b/despasito/tests/__init__.py index 325e75d..448a524 100644 --- a/despasito/tests/__init__.py +++ b/despasito/tests/__init__.py @@ -1,3 +1,4 @@ """ -Empty init file in case you choose a package besides PyTest such as Nose which may look for such a file +Empty init file in case you choose a package besides PyTest such as Nose which may look +for such a file """ diff --git a/despasito/tests/test_fit_mix.py b/despasito/tests/test_fit_mix.py index e3eca7c..70c9d75 100644 --- a/despasito/tests/test_fit_mix.py +++ b/despasito/tests/test_fit_mix.py @@ -40,7 +40,7 @@ cross_library=cross_library, ) -## Exp Data dict +# Exp Data dict Tlist = np.array([353.0]) xilist = np.array([[0.0128, 0.9872]]) yilist = np.array([[0.9896, 0.0104]]) @@ -74,7 +74,7 @@ } } -## Optimization options +# Optimization options optimization_parameters = { "fit_bead": "CO2", "fit_parameter_names": ["epsilon_H2O353"], @@ -94,12 +94,16 @@ "global_opts": {"method": "single_objective"}, } -def test_dew_pressure(Eos=Eos,thermo_dict=thermo_dict_dew.copy()): + +def test_dew_pressure(Eos=Eos, thermo_dict=thermo_dict_dew.copy()): thermo_dict = ri.process_param_fit_inputs(thermo_dict) output = fit.fit(**thermo_dict) - assert output["parameters_final"][0]==pytest.approx(432.69,abs=1.0) and output["objective_value"]==pytest.approx(854.19,abs=1.0) + assert output["parameters_final"][0] == pytest.approx(432.69, abs=1.0) and output[ + "objective_value" + ] == pytest.approx(854.19, abs=1.0) + def test_flash(Eos=Eos, thermo_dict=thermo_dict_flash.copy()): diff --git a/despasito/tests/test_fit_pure.py b/despasito/tests/test_fit_pure.py index 29f7507..bb946e7 100644 --- a/despasito/tests/test_fit_pure.py +++ b/despasito/tests/test_fit_pure.py @@ -10,7 +10,7 @@ import sys import numpy as np -## EOS Object +# EOS Object beads = ["CH3OH"] molecular_composition = np.array([[1.0]]) bead_library = { @@ -63,7 +63,7 @@ ) -## Exp Data dict +# Exp Data dict exp_data_sat = { "Wiley": { "data_class_type": "saturation_properties", @@ -107,7 +107,7 @@ } } -## Optimization options +# Optimization options optimization_parameters = { "fit_bead": "CH3OH", "fit_parameter_names": ["epsilon"], diff --git a/despasito/tests/test_input_output.py b/despasito/tests/test_input_output.py index fbcf01f..e24140b 100644 --- a/despasito/tests/test_input_output.py +++ b/despasito/tests/test_input_output.py @@ -16,6 +16,7 @@ import pytest import numpy as np + # Not Used because we shouldn't reference an external file @pytest.mark.parametrize("data", [([[["CH4_2", 1]], [["eCH3", 2]]])]) def test_process_bead_data(data): @@ -33,11 +34,3 @@ def test_process_bead_data(data): ) assert not errors, "errors occured:\n{}".format("\n".join(errors)) - - -# Not Used because we shouldn't reference an external file -# @pytest.mark.parametrize('key, answer', [("density_increment",2.0),("min_density_fraction",2.5e-06)]) -# def test_file2paramdict(key,answer): -# """Test conversion of txt file to dictionary""" -# rho_dict = ri.file2paramdict("example/dens_params.txt") -# assert rho_dict[key] == pytest.approx(answer,abs=1e-7) diff --git a/despasito/tests/test_logging.py b/despasito/tests/test_logging.py index c69da06..eba96db 100644 --- a/despasito/tests/test_logging.py +++ b/despasito/tests/test_logging.py @@ -15,6 +15,7 @@ def test_despasito_log_file(): """Test enabling of logging""" fname = "despasito_{}.log".format(random.randint(1, 10)) + print(fname) despasito.initiate_logger(log_file=fname, verbose=10) logger.info("test") diff --git a/despasito/tests/test_peng_robinson.py b/despasito/tests/test_peng_robinson.py index a55dee6..5a9df06 100644 --- a/despasito/tests/test_peng_robinson.py +++ b/despasito/tests/test_peng_robinson.py @@ -42,7 +42,7 @@ def test_PR_coefficients( beads=beads, molecular_composition=molecular_composition, bead_library=bead_library, -): # """Test ability to create EOS object without association sites""" +): # Test ability to create EOS object without association sites Eos_class = despasito.equations_of_state.initiate_eos( eos="cubic.peng_robinson", xi=xi, diff --git a/despasito/tests/test_saft_gamma_mie.py b/despasito/tests/test_saft_gamma_mie.py index e0445dc..7c129fc 100644 --- a/despasito/tests/test_saft_gamma_mie.py +++ b/despasito/tests/test_saft_gamma_mie.py @@ -10,11 +10,13 @@ import despasito.equations_of_state -try: - import cython - flag_cython = True -except ModuleNotFoundError: +if "cython" not in sys.modules: + print("Cython package is unavailable, using Numba") flag_cython = False +else: + flag_cython = True + +path = "despasito.equations_of_state.saft.compiled_modules" xi_co2_ben = np.array([0.2, 0.2]) beads_co2_ben = ["CO2", "benzene"] @@ -116,6 +118,7 @@ rho_co2_h2o = np.array([21146.16997993]) P = np.array([15727315.77]) + def test_saft_gamma_mie_imported(): # """Sample test, will always pass so long as import statement worked""" assert "despasito.equations_of_state" in sys.modules @@ -135,17 +138,6 @@ def test_saft_gamma_mie_class_noassoc( ) assert (Eos_class.eos_dict["massi"] == np.array([0.04401, 0.07811])).all() -@pytest.mark.skipif(hasattr(sys, 'getwindowsversion'), reason="Issue with f2py Fortran modules on Windows") -def test_fortran_available(): - from despasito.equations_of_state.saft.compiled_modules import ext_Aassoc_fortran - try: - from despasito.equations_of_state.saft.compiled_modules import ext_Aassoc_fortran - flag = True - except Exception: - flag = False - - assert flag - def test_saft_gamma_mie_class_assoc( beads=beads_co2_h2o, @@ -166,8 +158,8 @@ def test_saft_gamma_mie_class_assoc( def test_saft_gamma_mie_class_assoc_P( - T=T, - xi=xi_co2_h2o, + T=T, + xi=xi_co2_h2o, rho=rho_co2_h2o, beads=beads_co2_h2o, molecular_composition=molecular_composition_co2_h2o, @@ -190,8 +182,8 @@ def test_saft_gamma_mie_class_assoc_P( def test_saft_gamma_mie_class_assoc_fugacity_coeff( P=P, - T=T, - xi=xi_co2_h2o, + T=T, + xi=xi_co2_h2o, rho=rho_co2_h2o, beads=beads_co2_h2o, molecular_composition=molecular_composition_co2_h2o, @@ -210,27 +202,11 @@ def test_saft_gamma_mie_class_assoc_fugacity_coeff( phi = Eos_class.fugacity_coefficient(P, rho, xi, T) assert phi == pytest.approx(np.array([0.48972481, 0.00281112]), abs=1e-4) -def test_numba_available(): - try: - from despasito.equations_of_state.saft.compiled_modules.ext_Aassoc_numba import ( - calc_Xika, - ) - from despasito.equations_of_state.saft.compiled_modules.ext_gamma_mie_numba import ( - calc_a1s, - calc_Bkl, - calc_a1ii, - calc_a1s_eff, - calc_Bkl_eff, - calc_da1iidrhos, - calc_da2ii_1pchi_drhos, - ) - - flag = True - except Exception: - flag = False +def test_numba_available(): + assert (path + ".ext_Aassoc_numba" in sys.modules + and path + ".ext_gamma_mie_numba" in sys.modules) - assert flag def test_saft_gamma_mie_class_assoc_P_numba( T=T, @@ -241,59 +217,33 @@ def test_saft_gamma_mie_class_assoc_P_numba( bead_library=bead_library_co2_h2o, cross_library=cross_library_co2_h2o, ): -# """Test ability to predict P with association sites""" + # """Test ability to predict P with association sites""" Eos = despasito.equations_of_state.initiate_eos( eos="saft.gamma_mie", beads=beads, molecular_composition=molecular_composition, bead_library=copy.deepcopy(bead_library), cross_library=copy.deepcopy(cross_library), - numba=True + numba=True, ) -# """Test ability to predict P with association sites""" - P = Eos.pressure(rho,T,xi)[0] - - assert P == pytest.approx(15727315.77,abs=1e+3) - -@pytest.mark.skipif(not flag_cython, reason="Cython is not installed with this version of python.") -def test_cython_available(): + # """Test ability to predict P with association sites""" + P = Eos.pressure(rho, T, xi)[0] - from despasito.equations_of_state.saft.compiled_modules.ext_Aassoc_cython import ( - calc_Xika, - ) - from despasito.equations_of_state.saft.compiled_modules.ext_gamma_mie_cython import ( - calc_a1s, - calc_Bkl, - calc_a1ii, - calc_a1s_eff, - calc_Bkl_eff, - calc_da1iidrhos, - calc_da2ii_1pchi_drhos, - ) + assert P == pytest.approx(15727315.77, abs=1e3) - try: - from despasito.equations_of_state.saft.compiled_modules.ext_Aassoc_cython import ( - calc_Xika, - ) - from despasito.equations_of_state.saft.compiled_modules.ext_gamma_mie_cython import ( - calc_a1s, - calc_Bkl, - calc_a1ii, - calc_a1s_eff, - calc_Bkl_eff, - calc_da1iidrhos, - calc_da2ii_1pchi_drhos, - ) - flag = True - except Exception: - print("Cython is available on this machine, but the modules haven't been compiled.") - flag = False +@pytest.mark.skipif( + not flag_cython, reason="Cython is not installed with this version of python." +) +def test_cython_available(): + assert (path + ".ext_Aassoc_cython" in sys.modules + and path + ".ext_gamma_mie_cython" in sys.modules) - assert flag -@pytest.mark.skipif(not flag_cython, reason="Cython is not installed with this version of python.") +@pytest.mark.skipif( + not flag_cython, reason="Cython is not installed with this version of python." +) def test_saft_gamma_mie_class_assoc_P_cython( T=T, xi=xi_co2_h2o, @@ -303,15 +253,14 @@ def test_saft_gamma_mie_class_assoc_P_cython( bead_library=bead_library_co2_h2o, cross_library=cross_library_co2_h2o, ): -# """Test ability to predict P with association sites""" + # """Test ability to predict P with association sites""" Eos = despasito.equations_of_state.initiate_eos( eos="saft.gamma_mie", beads=beads, molecular_composition=molecular_composition, bead_library=copy.deepcopy(bead_library), cross_library=copy.deepcopy(cross_library), - cython=True + cython=True, ) - P = Eos.pressure(rho,T,xi)[0] - assert P == pytest.approx(15727315.77,abs=1e+3) - + P = Eos.pressure(rho, T, xi)[0] + assert P == pytest.approx(15727315.77, abs=1e3) diff --git a/despasito/tests/test_saft_gamma_sw.py b/despasito/tests/test_saft_gamma_sw.py index 9fcc3cd..94860ca 100644 --- a/despasito/tests/test_saft_gamma_sw.py +++ b/despasito/tests/test_saft_gamma_sw.py @@ -68,21 +68,26 @@ def test_saft_gamma_sw_class_assoc_P(T=T, xi=[1.0], Eos=Eos, density=density): assert P == pytest.approx(9447510.360679299, abs=1e3) -def test_saft_gamma_sw_class_assoc_fugacity_coefficient(P=P, xi=[1.0], T=T, Eos=Eos, density=density): +def test_saft_gamma_sw_class_assoc_fugacity_coefficient( + P=P, xi=[1.0], T=T, Eos=Eos, density=density +): # """Test ability to predict P with association sites""" phi = Eos.fugacity_coefficient(P, density, xi, T) assert phi == pytest.approx(np.array([0.8293442]), abs=1e-4) + Eos = despasito.equations_of_state.initiate_eos( eos="saft.gamma_sw", beads=bead, molecular_composition=molecular_composition, bead_library=copy.deepcopy(bead_library), - numba=True + numba=True, ) -def test_saft_gamma_sw_class_assoc_P_numba(T=T, xi=np.array([1.0]), Eos=Eos, density=density): + +def test_saft_gamma_sw_class_assoc_P_numba( + T=T, xi=np.array([1.0]), Eos=Eos, density=density +): # """Test ability to predict P with association sites""" P = Eos.pressure(density, T, xi)[0] assert P == pytest.approx(9447510.360679299, abs=1e3) - diff --git a/despasito/tests/test_thermo.py b/despasito/tests/test_thermo.py index 1a0bab4..32488e2 100644 --- a/despasito/tests/test_thermo.py +++ b/despasito/tests/test_thermo.py @@ -3,7 +3,6 @@ """ # Import package, test suite, and other packages as needed -import despasito.thermodynamics.calc as calc import despasito.thermodynamics as thermo import despasito.equations_of_state import pytest diff --git a/despasito/thermodynamics/__init__.py b/despasito/thermodynamics/__init__.py index 866f230..40995f3 100644 --- a/despasito/thermodynamics/__init__.py +++ b/despasito/thermodynamics/__init__.py @@ -1,7 +1,9 @@ """ Thermodynamics -This package will take in an equation of state object, and any user defined variables for calculation. The calculation type will then be compared to those available in the thermo.py file and be executed. +This package will take in an equation of state object, and any user defined variables +for calculation. The calculation type will then be compared to those available in the +thermo.py file and be executed. """ @@ -16,16 +18,20 @@ def thermo(Eos, calculation_type=None, **kwargs): """ - Use factory design pattern to search for matching calculation_type with those supported in this module. - - To add a new calculation type, add a new wrapper function to ``calculation_types.py``. + Use factory design pattern to search for matching calculation_type with those + supported in this module. + + To add a new calculation type, add a new wrapper function to + ``calculation_types.py``. Parameters ---------- Eos : obj - Equation of state object with the following methods: ``pressure``, ``density_max``, and ``fugacity_coefficient``. + Equation of state object with the following methods: ``pressure``, + ``density_max``, and ``fugacity_coefficient``. calculation_type : str - Calculation type supported in :mod:`~despasito.thermodynamics.calculation_types` + Calculation type supported in + :mod:`~despasito.thermodynamics.calculation_types` kwargs Other keywords passed to the function, depends on calculation type @@ -35,7 +41,7 @@ def thermo(Eos, calculation_type=None, **kwargs): Output of dictionary containing given and calculated values """ - if calculation_type == None: + if calculation_type is None: raise ValueError("No calculation type specified") # Extract available calculation types @@ -47,9 +53,8 @@ def thermo(Eos, calculation_type=None, **kwargs): except Exception: raise ImportError( - "The calculation type, '{}', was not found\nThe following calculation types are supported: {}".format( - calculation_type, ", ".join(calc_list) - ) + "The calculation type, '{}', was not found\nThe following calculation " + "types are supported: {}".format(calculation_type, ", ".join(calc_list)) ) try: diff --git a/despasito/thermodynamics/calc.py b/despasito/thermodynamics/calc.py index 74ceec8..e00332a 100644 --- a/despasito/thermodynamics/calc.py +++ b/despasito/thermodynamics/calc.py @@ -1,6 +1,8 @@ """ -This module contains our thermodynamic calculations. Calculation of pressure, fugacity coefficient, and max density are handled by an Eos object so that these functions can be used with any EOS. The thermodynamics module contains a series of wrapper to handle the inputs and outputs of these functions. - +This module contains our thermodynamic calculations. Calculation of pressure, +fugacity coefficient, and max density are handled by an Eos object so that these +functions can be used with any EOS. The thermodynamics module contains a series of +wrapper to handle the inputs and outputs of these functions. """ import numpy as np @@ -12,7 +14,6 @@ import despasito.utils.general_toolbox as gtb from despasito import fundamental_constants as constants -import despasito.utils.general_toolbox as gtb logger = logging.getLogger(__name__) @@ -32,12 +33,13 @@ def pressure_vs_volume_arrays( density_max_opts={}, **kwargs ): - r""" - Output arrays with specific volume and pressure arrays calculated from the given EOS. + Output arrays with specific volume and pressure arrays calculated from the given + EOS. + + This function is fundamental to every calculation, the options of which are passed + through higher level calculation with the keyword variable ``density_opts``. - This function is fundamental to every calculation, the options of which are passed through higher level calculation with the keyword variable ``density_opts``. - Parameters ---------- T : float @@ -47,11 +49,14 @@ def pressure_vs_volume_arrays( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. min_density_fraction : float, Optional, default=(1.0/500000.0) - Fraction of the maximum density used to calculate, and is equal to, the minimum density of the density array. The minimum density is the reciprocal of the maximum specific volume used to calculate the roots. + Fraction of the maximum density used to calculate, and is equal to, the + minimum density of the density array. The minimum density is the reciprocal + of the maximum specific volume used to calculate the roots. density_increment : float, Optional, default=5.0 The increment between density values in the density array. max_volume_increment : float, Optional, default=1.0E-4 - Maximum increment between specific volume array values. After conversion from density to specific volume, the increment values are compared to this value. + Maximum increment between specific volume array values. After conversion from + density to specific volume, the increment values are compared to this value. pressure_min : float, Optional, default=100 Ensure pressure curve reaches down to this value multfactor : int, Optional, default=2 @@ -59,9 +64,11 @@ def pressure_vs_volume_arrays( extended_npts : int, Optional, default=20 Number of points in extended range maxiter : int, Optional, default=25 - Number of times to multiply range by to obtain full pressure vs. specific volume curve + Number of times to multiply range by to obtain full pressure vs. specific + volume curve max_density : float, Optional, default=None - [mol/:math:`m^3`] Maximum molar density defined, if default of None is used then the Eos object method, density_max is used. + [mol/:math:`m^3`] Maximum molar density defined, if default of None is used + then the Eos object method, density_max is used. density_max_opts : dict, Optional, default={} Keyword arguments for density_max method for EOS object @@ -70,14 +77,14 @@ def pressure_vs_volume_arrays( vlist : numpy.ndarray [:math:`m^3`/mol] Specific volume array. Plist : numpy.ndarray - [Pa] Pressure associated with specific volume of system with given temperature and composition + [Pa] Pressure associated with specific volume of system with given temperature + and composition """ if len(kwargs) > 0: logger.debug( - " 'pressure_vs_volume_arrays' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + " 'pressure_vs_volume_arrays' does not use the following keyword " + "arguments: {}".format(", ".join(list(kwargs.keys()))) ) if np.any(np.isnan(xi)): @@ -86,7 +93,7 @@ def pressure_vs_volume_arrays( if isinstance(xi, list): xi = np.array(xi) - # estimate the maximum density based on the hard sphere packing fraction, part of EOS + # estimate the maximum density based on the hard sphere packing fraction if not max_density: max_density = Eos.density_max(xi, T, **density_max_opts) elif gtb.isiterable(max_density): @@ -107,9 +114,8 @@ def pressure_vs_volume_arrays( # list of densities for P,rho and P,v if (max_density - minrho) < density_increment: raise ValueError( - "Density range, {}, is less than increment, {}. Check parameters used in Eos.density_max().".format( - (max_density - minrho), density_increment - ) + "Density range, {}, is less than increment, {}. Check parameters used " + "in Eos.density_max().".format((max_density - minrho), density_increment) ) rholist = np.arange(minrho, max_density, density_increment) @@ -123,7 +129,7 @@ def pressure_vs_volume_arrays( 1.0 / rholist[vspaceswitch + 1], 1.0 / minrho, max_volume_increment )[::-1] ) - rholist = np.append(rholist_2, rholist[vspaceswitch + 2 :]) + rholist = np.append(rholist_2, rholist[(vspaceswitch + 2):]) # compute Pressures (Plist) for rholist Plist = Eos.pressure(rholist, T, xi) @@ -149,20 +155,22 @@ def pressure_vs_volume_arrays( def pressure_vs_volume_spline(vlist, Plist): r""" Fit arrays of specific volume and pressure values to a cubic Univariate Spline. - + Parameters ---------- vlist : numpy.ndarray [:math:`m^3`/mol] Specific volume array. Plist : numpy.ndarray - [Pa] Pressure associated with specific volume of system with given temperature and composition - + [Pa] Pressure associated with specific volume of system with given temperature + and composition + Returns ------- Pvspline : obj Function object of pressure vs. specific volume roots : list - List of specific volume roots. Subtract a system pressure from the output of Pvsrho to find density of vapor and/or liquid densities. + List of specific volume roots. Subtract a system pressure from the output of + Pvsrho to find density of vapor and/or liquid densities. extrema : list List of specific volume values corresponding to local minima and maxima. """ @@ -189,13 +197,14 @@ def pressure_vs_volume_spline(vlist, Plist): def pressure_vs_volume_plot(vlist, Plist, Pvspline, markers=[], **kwargs): r""" Plot pressure vs. specific volume. - + Parameters ---------- vlist : numpy.ndarray [:math:`m^3`/mol] Specific volume array. Plist : numpy.ndarray - [Pa] Pressure associated with specific volume of system with given temperature and composition + [Pa] Pressure associated with specific volume of system with given temperature + and composition Pvspline : obj Function object of pressure vs. specific volume markers : list, Optional, default=[] @@ -204,9 +213,8 @@ def pressure_vs_volume_plot(vlist, Plist, Pvspline, markers=[], **kwargs): if len(kwargs) > 0: logger.debug( - " 'pressure_vs_volume_plot' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + " 'pressure_vs_volume_plot' does not use the following keyword " + "arguments: {}".format(", ".join(list(kwargs.keys()))) ) try: @@ -231,8 +239,9 @@ def calc_saturation_properties( T, xi, Eos, density_opts={}, tol=1e-6, Pconverged=1, **kwargs ): r""" - Computes the saturated pressure, gas and liquid densities for a single component system. - + Computes the saturated pressure, gas and liquid densities for a single component + system. + Parameters ---------- T : float @@ -242,11 +251,13 @@ def calc_saturation_properties( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` tol : float, Optional, default=1e-6 Tolerance to accept pressure value Pconverged : float, Optional, default=1.0 - If the pressure is negative (under tension), we search from a value just above vacuum + If the pressure is negative (under tension), we search from a value just above + vacuum Returns ------- @@ -260,22 +271,23 @@ def calc_saturation_properties( if len(kwargs) > 0: logger.debug( - " 'calc_saturation_properties' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + " 'calc_saturation_properties' does not use the following keyword " + "arguments: {}".format(", ".join(list(kwargs.keys()))) ) if np.count_nonzero(xi) != 1: if np.count_nonzero(xi > 0.1) != 1: raise ValueError( - "Multiple components have compositions greater than 10%, check code for source" + "Multiple components have compositions greater than 10%, check code " + "for source" ) else: - ind = np.where((xi > 0.1) == True)[0] + ind = np.where(xi > 0.1)[0] raise ValueError( - "Multiple components have compositions greater than 0. Do you mean to obtain the saturation pressure of {} with a mole fraction of {}?".format( - Eos.beads[ind], xi[ind] - ) + "Multiple components have compositions greater than 0. Do you mean to" + " obtain the saturation pressure of" + " {} with a mole fraction " + "of {}?".format(Eos.beads[ind], xi[ind]) ) vlist, Plist = pressure_vs_volume_arrays(T, xi, Eos, **density_opts) @@ -292,7 +304,8 @@ def calc_saturation_properties( Pmaxsearch = Plist[ind_Pmax1] Pminsearch = max(Pconverged, np.amin(Plist[ind_Pmin1:ind_Pmax1])) - # Using computed Psat find the roots in the maxwell construction to give liquid (first root) and vapor (last root) densities + # Using computed Psat find the roots in the maxwell construction to give + # liquid (first root) and vapor (last root) densities Psat = spo.minimize_scalar( objective_saturation_pressure, args=(Plist, vlist), @@ -307,7 +320,8 @@ def calc_saturation_properties( if obj_value < tol: logger.debug( - " Psat found: {} Pa, obj value: {}, with {} roots and {} extrema".format( + " Psat found: " + "{} Pa, obj value: {}, with {} roots and {} extrema".format( Psat, obj_value, np.size(roots), np.size(extrema) ) ) @@ -330,9 +344,8 @@ def calc_saturation_properties( else: logger.warning( - " Psat NOT found: {} Pa, obj value: {}, consider decreasing 'pressure_min' option in density_opts".format( - Psat, obj_value - ) + " Psat NOT found: {} Pa, obj value: {},".format(Psat, obj_value) + + " consider decreasing 'pressure_min' option in density_opts" ) Psat, rhol, rhov = np.nan, np.nan, np.nan @@ -354,16 +367,21 @@ def objective_saturation_pressure(shift, Pv, vlist): Parameters ---------- shift : float - [Pa] Guess in Psat value used to translate the pressure vs. specific volume curve + [Pa] Guess in Psat value used to translate the pressure vs. specific volume + curve Pv : numpy.ndarray - [Pa] Pressure associated with specific volume of system with given temperature and composition + [Pa] Pressure associated with specific volume of system with given temperature + and composition vlist : numpy.ndarray - [mol/:math:`m^3`] Specific volume array. Length depends on values in density_opts passed to :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + [mol/:math:`m^3`] Specific volume array. Length depends on values in + density_opts passed to + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` Returns ------- obj_value : float - Output of objective function, the addition of the positive area between first two roots, and negative area between second and third roots, quantity squared. + Output of objective function, the addition of the positive area between first + two roots, and negative area between second and third roots, quantity squared. """ @@ -374,20 +392,27 @@ def objective_saturation_pressure(shift, Pv, vlist): b = Pvspline.integral(roots[1], roots[2]) elif len(roots) == 2: a = Pvspline.integral(roots[0], roots[1]) - # If the curve hasn't decayed to 0 yet, estimate the remaining area as a triangle. This isn't super accurate but we are just using the saturation pressure to get started. + # If the curve hasn't decayed to 0 yet, estimate the remaining area as a + # triangle. This isn't super accurate but we are just using the saturation + # pressure to get started. slope, yroot = np.polyfit(vlist[-4:], Pv[-4:] - shift, 1) b = ( Pvspline.integral(roots[1], vlist[-1]) + (Pv[-1] - shift) * (-yroot / slope - vlist[-1]) / 2 ) - # raise ValueError("Pressure curve only has two roots. If the curve hasn't fully decayed, either increase maximum specific volume or decrease 'pressure_min' in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays`.") + # raise ValueError("Pressure curve only has two roots. If the curve hasn't + # fully decayed, either increase maximum specific volume or decrease + # 'pressure_min' in + # :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays`.") elif np.any(np.isnan(roots)): raise ValueError( - "Pressure curve without cubic properties has wrongly been accepted. Try decreasing pressure." + "Pressure curve without cubic properties has wrongly been accepted. Try " + + "decreasing pressure." ) else: raise ValueError( - "Pressure curve without cubic properties has wrongly been accepted. Try decreasing min_density_fraction" + "Pressure curve without cubic properties has wrongly been accepted. Try " + "decreasing min_density_fraction" ) # pressure_vs_volume_plot(vlist, Pv-shift, Pvspline, markers=extrema) @@ -397,7 +422,7 @@ def objective_saturation_pressure(shift, Pv, vlist): def calc_vapor_density(P, T, xi, Eos, density_opts={}, **kwargs): r""" Computes vapor density under system conditions. - + Parameters ---------- P : float @@ -409,21 +434,22 @@ def calc_vapor_density(P, T, xi, Eos, density_opts={}, **kwargs): Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` Returns ------- rhov : float [mol/:math:`m^3`] Density of vapor at system pressure flag : int - A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas """ if len(kwargs) > 0: logger.debug( - " 'calc_vapor_density' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + " 'calc_vapor_density' does not use the following keyword arguments:" + " {}".format(", ".join(list(kwargs.keys()))) ) vlist, Plist = pressure_vs_volume_arrays(T, xi, Eos, **density_opts) @@ -438,9 +464,8 @@ def calc_vapor_density(P, T, xi, Eos, density_opts={}, **kwargs): rho_tmp = np.nan flag = 3 logger.warning( - " Flag 3: The T and yi, {} {}, won't produce a fluid (vapor or liquid) at this pressure".format( - T, xi - ) + " Flag 3: The T and yi, {} {}, won't produce a fluid (vapor or liquid)" + " at this pressure".format(T, xi) ) elif l_roots == 0: if Pvspline(1 / vlist[-1]) < 0: @@ -458,24 +483,22 @@ def calc_vapor_density(P, T, xi, Eos, density_opts={}, **kwargs): if not len(extrema): flag = 2 logger.debug( - " Flag 2: The T and yi, {} {}, combination produces a critical fluid at this pressure".format( - T, xi - ) + " Flag 2: The T and yi, {} {}, combination produces a " + "critical fluid at this pressure".format(T, xi) ) else: flag = 1 logger.debug( - " Flag 1: The T and yi, {} {}, combination produces a liquid at this pressure".format( - T, xi - ) + " Flag 1: The T and yi, {} {}, combination produces a " + "liquid at this pressure".format(T, xi) ) except Exception: rho_tmp = np.nan flag = 3 logger.warning( - " Flag 3: The T and xi, {} {}, won't produce a fluid (vapor or liquid) at this pressure, without density greater than max, {}".format( - T, xi, Eos.density_max(xi, T, maxpack=0.99) - ) + " Flag 3: The T and xi, {} {}, won't produce a fluid (vapor " + "or liquid) at this pressure, without density greater than max," + " {}".format(T, xi, Eos.density_max(xi, T, maxpack=0.99)) ) flag_NoOpt = True elif min(Plist) + P > 0: @@ -496,21 +519,20 @@ def calc_vapor_density(P, T, xi, Eos, density_opts={}, **kwargs): if not len(extrema): logger.debug( - " Flag 2: The T and yi, {} {}, combination produces a critical fluid at this pressure".format( - T, xi - ) + " Flag 2: The T and yi, {} {}, combination produces a critical" + " fluid at this pressure".format(T, xi) ) else: logger.debug( - " Flag 0: This T and yi, {} {}, combination produces a vapor at this pressure. Warning! approaching critical fluid".format( + " Flag 0: This T and yi, {} {}, combination produces a vapor" + " at this pressure. Warning! approaching critical fluid".format( T, xi ) ) else: logger.warning( - " Flag 3: The T and yi, {} {}, won't produce a fluid (vapor or liquid) at this pressure".format( - T, xi - ) + " Flag 3: The T and yi, {} {}, won't produce a fluid (vapor or " + "liquid) at this pressure".format(T, xi) ) flag = 3 rho_tmp = np.nan @@ -519,34 +541,30 @@ def calc_vapor_density(P, T, xi, Eos, density_opts={}, **kwargs): flag = 2 rho_tmp = 1.0 / roots[0] logger.debug( - " Flag 2: The T and yi, {} {}, combination produces a critical fluid at this pressure".format( - T, xi - ) + " Flag 2: The T and yi, {} {}, combination produces a critical " + "fluid at this pressure".format(T, xi) ) elif (Pvspline(roots[0]) + P) > (Pvspline(max(extrema)) + P): flag = 1 rho_tmp = 1.0 / roots[0] logger.debug( - " Flag 1: The T and yi, {} {}, combination produces a liquid at this pressure".format( - T, xi - ) + " Flag 1: The T and yi, {} {}, combination produces a liquid at " + "this pressure".format(T, xi) ) elif len(extrema) > 1: flag = 0 rho_tmp = 1.0 / roots[0] logger.debug( - " Flag 0: This T and yi, {} {}, combination produces a vapor at this pressure. Warning! approaching critical fluid".format( - T, xi - ) + " Flag 0: This T and yi, {} {}, combination produces a vapor at " + "this pressure. Warning! approaching critical fluid".format(T, xi) ) elif l_roots == 2: if (Pvspline(roots[0]) + P) < 0.0: flag = 1 rho_tmp = 1.0 / roots[0] logger.debug( - " Flag 1: This T and yi, {} {}, combination produces a liquid under tension at this pressure".format( - T, xi - ) + " Flag 1: This T and yi, {} {}, combination produces a liquid " + "under tension at this pressure".format(T, xi) ) else: slope, yroot = np.polyfit(vlist[-4:], Plist[-4:], 1) @@ -566,21 +584,20 @@ def calc_vapor_density(P, T, xi, Eos, density_opts={}, **kwargs): if not len(extrema): logger.debug( - " Flag 2: The T and yi, {} {}, combination produces a critical fluid at this pressure".format( - T, xi - ) + " Flag 2: The T and yi, {} {}, combination produces a critical" + " fluid at this pressure".format(T, xi) ) else: logger.debug( - " Flag 0: This T and yi, {} {}, combination produces a vapor at this pressure. Warning! approaching critical fluid".format( + " Flag 0: This T and yi, {} {}, combination produces a vapor " + "at this pressure. Warning! approaching critical fluid".format( T, xi ) ) else: # 3 roots logger.debug( - " Flag 0: This T and yi, {} {}, combination produces a vapor at this pressure.".format( - T, xi - ) + " Flag 0: This T and yi, {} {}, combination produces a vapor at this" + " pressure.".format(T, xi) ) rho_tmp = 1.0 / roots[2] flag = 0 @@ -604,9 +621,8 @@ def calc_vapor_density(P, T, xi, Eos, density_opts={}, **kwargs): else: if Plist[0] < 0: logger.warning( - " Density value could not be bounded with (rhomin,rhomax), {}. Using approximate density value".format( - tmp - ) + " Density value could not be bounded with (rhomin,rhomax), " + "{}. Using approximate density value".format(tmp) ) elif not flag_NoOpt: rho_tmp = spo.least_squares( @@ -624,14 +640,15 @@ def calc_vapor_density(P, T, xi, Eos, density_opts={}, **kwargs): # pressure_vs_volume_plot(vlist, Plist, Pvspline, markers=extrema) - # Flag: 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + # Flag: 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is + # true, 4 means we should assume ideal gas return rho_tmp, flag def calc_liquid_density(P, T, xi, Eos, density_opts={}, **kwargs): r""" Computes liquid density under system conditions. - + Parameters ---------- P : float @@ -643,21 +660,22 @@ def calc_liquid_density(P, T, xi, Eos, density_opts={}, **kwargs): Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` Returns ------- rhol : float [mol/:math:`m^3`] Density of liquid at system pressure flag : int - A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true + A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true """ if len(kwargs) > 0: logger.debug( - " 'calc_liquid_density' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + " 'calc_liquid_density' does not use the following keyword arguments: " + "{}".format(", ".join(list(kwargs.keys()))) ) # Get roots and local minima and maxima @@ -671,9 +689,8 @@ def calc_liquid_density(P, T, xi, Eos, density_opts={}, **kwargs): if extrema: if len(extrema) == 1: logger.warning( - " One extrema at {}, assume weird minima behavior. Check your parameters.".format( - 1 / extrema[0] - ) + " One extrema at {}, assume weird minima behavior. Check your " + "parameters.".format(1 / extrema[0]) ) # Assess roots, what is the liquid density @@ -682,9 +699,8 @@ def calc_liquid_density(P, T, xi, Eos, density_opts={}, **kwargs): rho_tmp = np.nan flag = 3 logger.warning( - " Flag 3: The T and xi, {} {}, won't produce a fluid (vapor or liquid) at this pressure".format( - T, xi - ) + " Flag 3: The T and xi, {} {}, won't produce a fluid (vapor or liquid) " + "at this pressure".format(T, xi) ) elif l_roots == 0: if Pvspline(1 / vlist[-1]): @@ -700,23 +716,26 @@ def calc_liquid_density(P, T, xi, Eos, density_opts={}, **kwargs): if not len(extrema): flag = 2 logger.debug( - " Flag 2: The T and xi, {} {}, combination produces a critical fluid at this pressure".format( - T, xi - ) + " Flag 2: The T and xi, {} {}, ".format(T, xi) + + "combination produces a critical fluid at this pressure" ) else: flag = 1 logger.debug( - " Flag 1: The T and xi, {} {}, combination produces a liquid at this pressure".format( - T, xi - ) + " Flag 1: The T and xi, {} {}, ".format(T, xi) + + "combination produces a liquid at this pressure" ) except Exception: rho_tmp = np.nan flag = 3 logger.warning( - " Flag 3: The T and xi, {} {}, won't produce a fluid (vapor or liquid) at this pressure, without density greater than max, {}".format( - T, xi, Eos.density_max(xi, T, maxpack=0.99) + " Flag 3: The T and xi, {} {}, ".format( + T, + xi, + ) + + "won't produce a fluid (vapor or liquid) at this pressure, " + + "without density greater than max, {}".format( + Eos.density_max(xi, T, maxpack=0.99) ) ) flag_NoOpt = True @@ -738,22 +757,20 @@ def calc_liquid_density(P, T, xi, Eos, density_opts={}, **kwargs): if not len(extrema): logger.debug( - " Flag 2: The T and xi, {} {}, combination produces a critical fluid at this pressure".format( - T, xi - ) + " Flag 2: The T and xi, {} {}, ".format(T, xi) + + "combination produces a critical fluid at this pressure" ) else: logger.debug( - " Flag 0: This T and xi, {} {}, combination produces a vapor at this pressure. Warning! approaching critical fluid".format( - T, xi - ) + " Flag 0: This T and xi, {} {},".format(T, xi) + + " combination produces a vapor at this pressure. Warning!" + + " approaching critical fluid" ) else: flag = 3 logger.error( - " Flag 3: The T and xi, {} {}, won't produce a fluid (vapor or liquid) at this pressure".format( - str(T), str(xi) - ) + " Flag 3: The T and xi, {} {},".format(T, xi) + + " won't produce a fluid (vapor or liquid) at this pressure" ) rho_tmp = np.nan # pressure_vs_volume_plot(vlist, Plist, Pvspline, markers=extrema) @@ -762,11 +779,11 @@ def calc_liquid_density(P, T, xi, Eos, density_opts={}, **kwargs): flag = 1 rho_tmp = 1.0 / roots[0] logger.debug( - " Flag 1: This T and xi, {} {}, combination produces a liquid under tension at this pressure".format( - T, xi - ) + " Flag 1: This T and xi, {} {},".format(T, xi) + + "combination produces a liquid under tension at this pressure" ) - else: # There should be three roots, but the values of specific volume don't go far enough to pick up the last one + else: # There should be three roots, but the values of specific volume + # don't go far enough to pick up the last one flag = 1 rho_tmp = 1.0 / roots[0] elif l_roots == 1: # 1 root @@ -774,33 +791,30 @@ def calc_liquid_density(P, T, xi, Eos, density_opts={}, **kwargs): flag = 2 rho_tmp = 1.0 / roots[0] logger.debug( - " Flag 2: The T and xi, {} {}, combination produces a critical fluid at this pressure".format( - T, xi - ) + " Flag 2: The T and xi, {} {},".format(T, xi) + + "combination produces a critical fluid at this pressure" ) elif (Pvspline(roots[0]) + P) > (Pvspline(max(extrema)) + P): flag = 1 rho_tmp = 1.0 / roots[0] logger.debug( - " Flag 1: The T and xi, {} {}, combination produces a liquid at this pressure".format( - T, xi - ) + " Flag 1: The T and xi, {} {},".format(T, xi) + + "combination produces a liquid at this pressure" ) elif len(extrema) > 1: flag = 0 rho_tmp = 1.0 / roots[0] logger.debug( - " Flag 0: This T and xi, {} {}, combination produces a vapor at this pressure. Warning! approaching critical fluid".format( - T, xi - ) + " Flag 0: This T and xi, {} {},".format(T, xi) + + " combination produces a vapor at this pressure. Warning! " + + "approaching critical fluid" ) else: # 3 roots rho_tmp = 1.0 / roots[0] flag = 1 logger.debug( - " Flag 1: The T and xi, {} {}, combination produces a liquid at this pressure".format( - T, xi - ) + " Flag 1: The T and xi, {} {},".format(T, xi) + + "combination produces a liquid at this pressure" ) if flag in [1, 2]: # liquid or critical fluid @@ -816,9 +830,8 @@ def calc_liquid_density(P, T, xi, Eos, density_opts={}, **kwargs): else: if P_tmp[0] < 0: logger.warning( - " Density value could not be bounded with (rhomin,rhomax), {}. Using approximate density value".format( - tmp - ) + " Density value could not be bounded with (rhomin,rhomax)," + + " {}. Using approximate density value".format(tmp) ) elif not flag_NoOpt: rho_tmp = spo.least_squares( @@ -833,16 +846,18 @@ def calc_liquid_density(P, T, xi, Eos, density_opts={}, **kwargs): rho_tmp = rho_tmp.x[0] logger.debug(" Liquid Density: {} mol/m^3, flag {}".format(rho_tmp, flag)) - # Flag: 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true + # Flag: 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither + # is true return rho_tmp, flag def pressure_spline_error(rho, Pset, T, xi, Eos): """ - Calculate difference between set point pressure and computed pressure for a given density. + Calculate difference between set point pressure and computed pressure for a given + density. Used to ensure an accurate value from the EOS rather than an estimate from a spline. - + Parameters ---------- rho : float @@ -855,7 +870,7 @@ def pressure_spline_error(rho, Pset, T, xi, Eos): Mole fraction of each component, sum(xi) should equal 1.0 Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. - + Returns ------- pressure_spline_error : float @@ -870,7 +885,7 @@ def pressure_spline_error(rho, Pset, T, xi, Eos): def calc_vapor_fugacity_coefficient(P, T, yi, Eos, density_opts={}, **kwargs): r""" Computes vapor fugacity coefficient under system conditions. - + Parameters ---------- P : float @@ -882,7 +897,8 @@ def calc_vapor_fugacity_coefficient(P, T, yi, Eos, density_opts={}, **kwargs): Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` Returns ------- @@ -891,14 +907,14 @@ def calc_vapor_fugacity_coefficient(P, T, yi, Eos, density_opts={}, **kwargs): rhov : float [mol/:math:`m^3`] Density of vapor at system pressure flag : int - Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed + Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean + a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed """ if len(kwargs) > 0: logger.debug( - " 'calc_vapor_fugacity_coefficient' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + " 'calc_vapor_fugacity_coefficient' does not use the following " + + "keyword arguments: {}".format(", ".join(list(kwargs.keys()))) ) rhov, flagv = calc_vapor_density(P, T, yi, Eos, density_opts) @@ -917,7 +933,7 @@ def calc_vapor_fugacity_coefficient(P, T, yi, Eos, density_opts={}, **kwargs): def calc_liquid_fugacity_coefficient(P, T, xi, Eos, density_opts={}, **kwargs): r""" Computes liquid fugacity coefficient under system conditions. - + Parameters ---------- P : float @@ -929,7 +945,8 @@ def calc_liquid_fugacity_coefficient(P, T, xi, Eos, density_opts={}, **kwargs): Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` Returns ------- @@ -938,14 +955,14 @@ def calc_liquid_fugacity_coefficient(P, T, xi, Eos, density_opts={}, **kwargs): rhol : float [mol/:math:`m^3`] Density of liquid at system pressure flag : int - Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true. + Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean a + critical fluid, 3 means that neither is true. """ if len(kwargs) > 0: logger.debug( - " 'calc_liquid_fugacity_coefficient' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + " 'calc_liquid_fugacity_coefficient' does not use the following keyword" + " arguments: {}".format(", ".join(list(kwargs.keys()))) ) rhol, flagl = calc_liquid_density(P, T, xi, Eos, density_opts) @@ -960,8 +977,9 @@ def calc_liquid_fugacity_coefficient(P, T, xi, Eos, density_opts={}, **kwargs): def calc_new_mole_fractions(phase_1_mole_fraction, phil, phiv, phase=None): r""" - Calculate the alternative phase composition given the composition and fugacity coefficients of one phase, and the fugacity coefficients of the target phase. - + Calculate the alternative phase composition given the composition and fugacity + coefficients of one phase, and the fugacity coefficients of the target phase. + Parameters ---------- phase_1_mole_fraction : numpy.ndarray @@ -971,17 +989,22 @@ def calc_new_mole_fractions(phase_1_mole_fraction, phil, phiv, phase=None): phiv : float Fugacity coefficient of vapor at system pressure phase : str, default=None - Use either 'vapor' or 'liquid' to define the mole fraction **being computed**. Default is None and it will fail to ensure the user specifies the correct phase + Use either 'vapor' or 'liquid' to define the mole fraction **being computed**. + Default is None and it will fail to ensure the user specifies the correct + phase Returns ------- phase_2_mole_fraction : numpy.ndarray - Mole fraction of each component computed from fugacity coefficients, sum(xi) should equal 1.0 when the solution is found, but the resulting values may not during an equilibrium calculation (e.g. bubble point). + Mole fraction of each component computed from fugacity coefficients, sum(xi) + should equal 1.0 when the solution is found, but the resulting values may not + during an equilibrium calculation (e.g. bubble point). """ - if phase == None or phase not in ["vapor", "liquid"]: + if phase is None or phase not in ["vapor", "liquid"]: raise ValueError( - "The user must specify the desired mole fraction as either 'vapor' or 'liquid'." + "The user must specify the desired mole fraction as either 'vapor' or " + + "'liquid'." ) if np.sum(phase_1_mole_fraction) != 1.0: @@ -1008,8 +1031,10 @@ def calc_new_mole_fractions(phase_1_mole_fraction, phil, phiv, phase=None): def equilibrium_objective(phase_1_mole_fraction, phil, phiv, phase=None): r""" - Computes the objective value used to determine equilibrium between phases. sum(phase_1_mole_fraction * phase_1_phi / phase_2_phi ) - 1.0, where `phase` is phase 2. - + Computes the objective value used to determine equilibrium between phases. + sum(phase_1_mole_fraction * phase_1_phi / phase_2_phi ) - 1.0, where `phase` is + phase 2. + Parameters ---------- phase_1_mole_fraction : numpy.ndarray @@ -1019,7 +1044,9 @@ def equilibrium_objective(phase_1_mole_fraction, phil, phiv, phase=None): phiv : float Fugacity coefficient of vapor at system pressure phase : str, default=None - Use either 'vapor' or 'liquid' to define the mole fraction **being computed**. Default is None and it will fail to ensure the user specifies the correct phase + Use either 'vapor' or 'liquid' to define the mole fraction **being computed**. + Default is None and it will fail to ensure the user specifies the correct + phase Returns ------- @@ -1027,9 +1054,10 @@ def equilibrium_objective(phase_1_mole_fraction, phil, phiv, phase=None): Objective value indicating how close to equilibrium we are """ - if phase == None or phase not in ["vapor", "liquid"]: + if phase is None or phase not in ["vapor", "liquid"]: raise ValueError( - "The user must specify the desired mole fraction as either 'vapor' or 'liquid'." + "The user must specify the desired mole fraction as either 'vapor' or " + + "'liquid'." ) if np.sum(phase_1_mole_fraction) != 1.0: raise ValueError("Given mole fractions must add up to one.") @@ -1050,7 +1078,8 @@ def equilibrium_objective(phase_1_mole_fraction, phil, phiv, phase=None): def _clean_plot_data(x_old, y_old): r""" - Reorder array and remove duplicates, then repeat process for the corresponding array. + Reorder array and remove duplicates, then repeat process for the corresponding + array. Parameters ---------- @@ -1093,8 +1122,9 @@ def calc_Prange_xi( r""" Obtain minimum and maximum pressure values for bubble point calculation. - The liquid mole fraction is set and the objective function at each of those values is of opposite sign. - + The liquid mole fraction is set and the objective function at each of those values + is of opposite sign. + Parameters ---------- T : float @@ -1106,25 +1136,35 @@ def calc_Prange_xi( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` maxiter : float, Optional, default=200 - Maximum number of iterations in both the loop to find Pmin and the loop to find Pmax + Maximum number of iterations in both the loop to find Pmin and the loop to + find Pmax Pmin : float, Optional, default=1000.0 [Pa] Minimum pressure in pressure range that restricts searched space. Pmax : float, Optional, default=100000 - If no local minima or maxima are identified for the liquid composition at this temperature, this value is used as an initial estimate of the maximum pressure range. + If no local minima or maxima are identified for the liquid composition at this + temperature, this value is used as an initial estimate of the maximum pressure + range. Pmin_allowed : float, Optional, default=100 Minimum allowed pressure in search, before looking for a super critical fluid mole_fraction_options : dict, Optional, default={} Options used to solve the inner loop in the solving algorithm ptol : float, Optional, default=1e-2 - If two iterations in the search for the maximum pressure are within this tolerance, the search is discontinued + If two iterations in the search for the maximum pressure are within this + tolerance, the search is discontinued xytol : float, Optional, default=0.01 - If the sum of absolute relative difference between the vapor and liquid mole fractions are less than this total, the pressure is assumed to be super critical and the maximum pressure is sought at a lower value. + If the sum of absolute relative difference between the vapor and liquid mole + fractions are less than this total, the pressure is assumed to be super + critical and the maximum pressure is sought at a lower value. maxfactor : float, Optional, default=2 - Factor to multiply by the pressure if it is too low (produces liquid or positive objective value). Not used if an unfeasible maximum pressure is found to bound the problem (critical for NaN result). + Factor to multiply by the pressure if it is too low (produces liquid or + positive objective value). Not used if an unfeasible maximum pressure is + found to bound the problem (critical for NaN result). minfactor : float, Optional, default=0.5 - Factor to multiply by the minimum pressure if it is too high (produces critical value). + Factor to multiply by the minimum pressure if it is too high (produces + critical value). Returns ------- @@ -1148,7 +1188,7 @@ def calc_Prange_xi( Pvspline, roots, extrema = pressure_vs_volume_spline(vlist, Plist) flag_hard_min = False - if Pmin != None: + if Pmin is not None: flag_hard_min = True if gtb.isiterable(Pmin): Pmin = Pmin[0] @@ -1160,7 +1200,7 @@ def calc_Prange_xi( Pmin = 1e3 flag_hard_max = False - if Pmax != None: + if Pmax is not None: flag_hard_max = True if gtb.isiterable(Pmax): Pmax = Pmax[0] @@ -1173,7 +1213,7 @@ def calc_Prange_xi( Prange = np.array([Pmin, Pmax]) - #################### Find Minimum Pressure and Objective Function Value ############### + # ############## Find Minimum Pressure and Objective Function Value ############### # Root of min from liquid curve is absolute minimum ObjRange = np.zeros(2) @@ -1234,13 +1274,13 @@ def calc_Prange_xi( # If within tolerance of liquid mole fraction elif np.sum(np.abs(xi - yi_range) / xi) < xytol and flagv_min == 2: logger.info( - "Estimated minimum pressure reproduces xi: {}, Obj. Func: {}, Range {}".format( - p, obj, Prange - ) + "Estimated minimum pressure reproduces xi: " + "{}, Obj. Func: {}, Range {}".format(p, obj, Prange) ) if ( flag_max or flag_hard_max - ) and flag_liquid: # If a liquid phase exists at a higher pressure, this must bound the lower pressure + ) and flag_liquid: # If a liquid phase exists at a higher pressure, + # this must bound the lower pressure flag_min = True ObjRange[0] = obj Prange[0] = p @@ -1259,7 +1299,8 @@ def calc_Prange_xi( ObjRange[1] = np.nan elif ( flag_min or flag_hard_min - ) and flag_vapor: # If the 'liquid' phase is vapor at a lower pressure, this must bound the upper pressure + ) and flag_vapor: # If the 'liquid' phase is vapor at a lower pressure, + # this must bound the upper pressure flag_max = True ObjRange[1] = obj Prange[1] = p @@ -1306,9 +1347,8 @@ def calc_Prange_xi( # If 'vapor' phase is liquid or unattainable elif flagv_min not in [0, 2, 4]: logger.info( - "Estimated minimum pressure produces liquid: {}, Obj. Func: {}, Range {}".format( - p, obj, Prange - ) + "Estimated minimum pressure produces liquid: " + "{}, Obj. Func: {}, Range {}".format(p, obj, Prange) ) if flag_hard_min and p <= Pmin: flag_critical = True @@ -1343,18 +1383,16 @@ def calc_Prange_xi( # Found minimum pressure! elif obj > 0: logger.info( - "Found estimated minimum pressure: {}, Obj. Func: {}, Range {}".format( - p, obj, Prange - ) + "Found estimated minimum pressure: " + "{}, Obj. Func: {}, Range {}".format(p, obj, Prange) ) Prange[0] = p ObjRange[0] = obj break elif obj < 0: logger.info( - "Estimated minimum pressure too high: {}, Obj. Func: {}, Range {}".format( - p, obj, Prange - ) + "Estimated minimum pressure too high: " + "{}, Obj. Func: {}, Range {}".format(p, obj, Prange) ) flag_liquid = True flag_max = True @@ -1370,11 +1408,11 @@ def calc_Prange_xi( ObjRange[0] = np.nan else: raise ValueError( - "This shouldn't happen: xi {}, phil {}, flagl {}, yi {}, phiv {}, flagv {}, obj {}, flags: {} {} {}".format( - xi, - phil, - flagl, - yi_range, + "This shouldn't happen: " + + "xi {}, phil {}, flagl {}, yi {},".format( + xi, phil, flagl, yi_range + ) + + " phiv {}, flagv {}, obj {}, flags: {} {} {}".format( phiv_min, flagv_min, obj, @@ -1385,9 +1423,8 @@ def calc_Prange_xi( ) else: logger.info( - "Estimated minimum pressure produced vapor as a 'liquid' phase: {}, Range {}".format( - p, Prange - ) + "Estimated minimum pressure produced vapor as a 'liquid' phase: " + "{}, Range {}".format(p, Prange) ) flag_vapor = True flag_min = True @@ -1403,40 +1440,40 @@ def calc_Prange_xi( and (flag_hard_max or flag_max) and (p < Prange[0] or p > Prange[1]) ): - # if (p < Prange[0] and Prange[0] != Prange[1]) or (flag_max and p > Prange[1]): + # if (p < Prange[0] and Prange[0] != Prange[1]) or (flag_max and + # p > Prange[1]): p = (Prange[1] - Prange[0]) / 1 + Prange[0] if p <= 0.0: raise ValueError( - "Pressure, {}, cannot be equal to or less than zero. Given composition, {}, and T {}".format( - p, xi, T - ) + "Pressure, {}, cannot be equal to or less than zero. Given " + "composition, {}, and T {}".format(p, xi, T) ) if flag_hard_min and Pmin == p: raise ValueError( - "In searching for the minimum pressure, the range {}, converged without a solution".format( - Prange - ) + "In searching for the minimum pressure, the range " + "{}, converged without a solution".format(Prange) ) if z == maxiter - 1: raise ValueError( - "Maximum Number of Iterations Reached: Proper minimum pressure for liquid density could not be found" + "Maximum Number of Iterations Reached: Proper minimum pressure for " + "liquid density could not be found" ) - # A flag value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + # A flag value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means + # that neither is true, 4 means we should assume ideal gas - #################### Find Maximum Pressure and Objective Function Value ############### + # ############## Find Maximum Pressure and Objective Function Value ############# # Be sure guess in upper bound is larger than lower bound if Prange[1] <= Prange[0]: Prange[1] = Prange[0] * maxfactor ObjRange[1] == 0.0 - flag_min = ( - False - ) # Signals that the objective value starts to increase again and we must go back + flag_min = False # Signals that the objective value starts to increase again + # and we must go back p = Prange[1] Parray = [Prange[1]] ObjArray = [ObjRange[1]] @@ -1449,7 +1486,8 @@ def calc_Prange_xi( if any(np.isnan(phil)): logger.info( - "Liquid fugacity coefficient should not be NaN, pressure could be too high." + "Liquid fugacity coefficient should not be NaN, pressure could be " + + "too high." ) flag_max = True Prange[1] = p @@ -1473,7 +1511,8 @@ def calc_Prange_xi( # If 'vapor' phase is a liquid if flagv_max not in [0, 2, 4] or np.any(np.isnan(yi_range)): logger.info( - "New Maximum Pressure: {} isn't vapor, flag={}, Obj Func: {}, Range {}".format( + "New Maximum Pressure: " + + "{} isn't vapor, flag={}, Obj Func: {}, Range {}".format( p, flagv_max, obj, Prange ) ) @@ -1496,9 +1535,8 @@ def calc_Prange_xi( # If 'liquid' composition is reproduced elif np.sum(np.abs(xi - yi_range) / xi) < xytol: # If less than 2% logger.info( - "Estimated Maximum Pressure Reproduces xi: {}, Obj. Func: {}".format( - p, obj - ) + "Estimated Maximum Pressure Reproduces xi: " + + "{}, Obj. Func: {}".format(p, obj) ) flag_max = True ObjRange[1] = obj @@ -1535,23 +1573,15 @@ def calc_Prange_xi( ObjRange[1] = obj logger.info( - "Maximum Pressure (if it exists) between Pressure: {} and Obj Range: {}".format( - Prange, ObjRange - ) + "Maximum Pressure (if it exists) between Pressure: " + + "{} and Obj Range: {}".format(Prange, ObjRange) ) P0 = np.mean(Prange) scale_factor = 10 ** (np.ceil(np.log10(P0))) - args = (xi, T, Eos, density_opts, mole_fraction_options, scale_factor) + args = (xi, T, Eos, density_opts, mole_fraction_options) p = gtb.solve_root( - lambda x, xi, T, Eos, density_opts, mole_fraction_options, scale_factor: objective_bubble_pressure( - x * scale_factor, - xi, - T, - Eos, - density_opts, - mole_fraction_options, - ), + objective_bubble_pressure, args=args, x0=P0 / scale_factor, method="TNC", @@ -1598,9 +1628,8 @@ def calc_Prange_xi( p_min = p_min[obj_tmp == np.nanmin(obj_tmp)] elif len(p_min) == 0: logger.error( - "Could not find minimum in pressure range:\n Pressure: {}\n Obj Value: {}".format( - parray, obj_array - ) + "Could not find minimum in pressure range:\n Pressure:" + + " {}\n Obj Value: {}".format(parray, obj_array) ) p = p_min obj = objective_bubble_pressure( @@ -1622,7 +1651,8 @@ def calc_Prange_xi( flag_min = False else: logger.error( - "Could not find maximum in pressure range:\n Pressure range {} best {}\n Obj Value range {} best {}".format( + "Could not find maximum in pressure range:\n Pressure " + + "range {} best {}\n Obj Value range {} best {}".format( Prange, p, ObjRange, obj ) ) @@ -1659,28 +1689,26 @@ def calc_Prange_xi( if p <= 0.0: raise ValueError( - "Pressure, {}, cannot be equal to or less than zero. Given composition, {}, and T {}".format( - p, xi, T - ) + "Pressure, {}, cannot be equal to or less".format(p) + + " than zero. Given composition, {}, and T {}".format(xi, T) ) if np.abs(Prange[1] - Prange[0]) < ptol: raise ValueError( - "In searching for the minimum pressure, the range {}, converged without a solution".format( - Prange - ) + "In searching for the minimum pressure, the range " + + "{}, converged without a solution".format(Prange) ) if z == maxiter - 1 or flag_min: if flag_min: logger.error( - "Cannot reach objective value of zero. Final Pressure: {}, Obj. Func: {}".format( - p, obj - ) + "Cannot reach objective value of zero. Final Pressure: " + + "{}, Obj. Func: {}".format(p, obj) ) else: logger.error( - "Maximum Number of Iterations Reached: A change in sign for the objective function could not be found, inspect progress" + "Maximum Number of Iterations Reached: A change in sign for the" + + " objective function could not be found, inspect progress" ) Prange = np.array([np.nan, np.nan]) Pguess = np.nan @@ -1715,8 +1743,9 @@ def calc_Prange_yi( r""" Obtain min and max pressure values. - The vapor mole fraction is set and the objective function at each of those values is of opposite sign. - + The vapor mole fraction is set and the objective function at each of those + values is of opposite sign. + Parameters ---------- T : float @@ -1728,25 +1757,36 @@ def calc_Prange_yi( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` maxiter : float, Optional, default=200 - Maximum number of iterations in both the loop to find Pmin and the loop to find Pmax + Maximum number of iterations in both the loop to find Pmin and the loop to + find Pmax Pmin : float, Optional, default=1000.0 - [Pa] Minimum pressure in pressure range that restricts searched space. Used if local minimum isn't available for pressure curve for vapor composition. + [Pa] Minimum pressure in pressure range that restricts searched space. Used + if local minimum isn't available for pressure curve for vapor composition. Pmax : float, Optional, default=100000 - If no local minima or maxima are identified for the liquid composition at this temperature, this value is used as an initial estimate of the maximum pressure range. + If no local minima or maxima are identified for the liquid composition at this + temperature, this value is used as an initial estimate of the maximum pressure + range. Pmin_allowed : float, Optional, default=100 Minimum allowed pressure in search, before looking for a super critical fluid mole_fraction_options : dict, Optional, default={} Options used to solve the inner loop in the solving algorithm ptol : float, Optional, default=1e-2 - If two iterations in the search for the maximum pressure are within this tolerance, the search is discontinued + If two iterations in the search for the maximum pressure are within this + tolerance, the search is discontinued xytol : float, Optional, default=0.01 - If the sum of absolute relative difference between the vapor and liquid mole fractions are less than this total, the pressure is assumed to be super critical and the maximum pressure is sought at a lower value. + If the sum of absolute relative difference between the vapor and liquid mole + fractions are less than this total, the pressure is assumed to be super critical + and the maximum pressure is sought at a lower value. maxfactor : float, Optional, default=2 - Factor to multiply by the pressure if it is too low (produces liquid or positive objective value). Not used if an unfeasible maximum pressure is found to bound the problem (critical for NaN result). + Factor to multiply by the pressure if it is too low (produces liquid or positive + objective value). Not used if an unfeasible maximum pressure is found to bound + the problem (critical for NaN result). minfactor : float, Optional, default=0.5 - Factor to multiply by the minimum pressure if it is too high (produces critical value). + Factor to multiply by the minimum pressure if it is too high (produces critical + value). Returns ------- @@ -1772,7 +1812,7 @@ def calc_Prange_yi( # Calculation the highest pressure possible flag_hard_min = False - if Pmin != None: + if Pmin is not None: flag_hard_min = True if gtb.isiterable(Pmin): Pmin = Pmin[0] @@ -1784,7 +1824,7 @@ def calc_Prange_yi( Pmin = 1e3 flag_hard_max = False - if Pmax != None: + if Pmax is not None: flag_hard_max = True if gtb.isiterable(Pmax): Pmax = Pmax[0] @@ -1800,7 +1840,7 @@ def calc_Prange_yi( ObjRange = np.zeros(2) xi_range = xi - #################### Find Minimum Pressure and Objective Function Value ############### + # ############ Find Minimum Pressure and Objective Function Value ############ flag_min = False flag_max = False @@ -1857,9 +1897,8 @@ def calc_Prange_yi( np.sum(np.abs(yi - xi_range) / yi) < xytol and flagl_min == 2 ): # If within 2% of liquid mole fraction logger.info( - "Estimated Minimum Pressure Reproduces yi: {}, Obj. Func: {}, Range {}".format( - p, obj, Prange - ) + "Estimated Minimum Pressure Reproduces yi: " + + "{}, Obj. Func: {}, Range {}".format(p, obj, Prange) ) if ( @@ -1878,7 +1917,6 @@ def calc_Prange_yi( flag_max = True ObjRange[1] = obj Prange[1] = p - phil_max, flagl_max = phil_min, flagl_min if flag_min or flag_hard_min: p = (Prange[1] - Prange[0]) / 2 + Prange[0] else: @@ -1900,27 +1938,23 @@ def calc_Prange_yi( Prange[0] = p ObjRange[0] = obj logger.info( - "Obtained estimated Minimum Pressure: {}, Obj. Func: {}, Range {}".format( - p, obj, Prange - ) + "Obtained estimated Minimum Pressure: " + + "{}, Obj. Func: {}, Range {}".format(p, obj, Prange) ) break elif obj > 0: flag_max = True logger.info( - "Estimated Minimum Pressure too High: {}, Obj. Func: {}, Range {}".format( - p, obj, Prange - ) + "Estimated Minimum Pressure too High: " + + "{}, Obj. Func: {}, Range {}".format(p, obj, Prange) ) ObjRange[1] = obj Prange[1] = p - phil_max, flagl_max = phil_min, flagl_min p = (Prange[1] - Prange[0]) * minfactor + Prange[0] else: logger.info( - "Estimated Minimum Pressure Produced Liquid instead of Vapor Phase: {}, Range {}".format( - p, Prange - ) + "Estimated Minimum Pressure Produced Liquid instead of Vapor Phase:" + + " {}, Range {}".format(p, Prange) ) if flag_hard_min and p <= Pmin: flag_critical = True @@ -1969,21 +2003,21 @@ def calc_Prange_yi( if flag_hard_min and Pmin == p: raise ValueError( - "In searching for the minimum pressure, the range {}, converged without a solution".format( - Prange - ) + "In searching for the minimum pressure, the range " + + "{}, converged without a solution".format(Prange) ) if p <= 0.0: raise ValueError( - "Pressure, {}, cannot be equal to or less than zero. Given composition, {}, and T {}, results in a supercritical value without a coexistent fluid.".format( - p, xi, T - ) + "Pressure, {}, cannot be equal to or less".format(p) + + " than zero. Given composition, {}, and T {}, results".format(xi, T) + + " in a supercritical value without a coexistent fluid." ) if z == maxiter - 1: raise ValueError( - "Maximum Number of Iterations Reached: Proper minimum pressure for liquid density could not be found" + "Maximum Number of Iterations Reached: Proper minimum pressure for" + + " liquid density could not be found" ) # Be sure guess in pressure is larger than lower bound @@ -1992,7 +2026,7 @@ def calc_Prange_yi( if z == 0: ObjRange[1] == 0.0 - ## Check Pmax + # Check Pmax flag_sol = False flag_vapor = False flag_min = False @@ -2024,9 +2058,8 @@ def calc_Prange_yi( Prange[1] = p ObjRange[1] = obj logger.info( - "New Max Pressure: {} doesn't produce vapor, flag={}, Obj Func: {}, Range {}".format( - Prange[1], flagv, ObjRange[1], Prange - ) + "New Max Pressure: {} doesn't produce vapor,".format(Prange[1]) + + " flag={}, Obj Func: {}, Range {}".format(flagv, ObjRange[1], Prange) ) p = (Prange[1] - Prange[0]) / 2.0 + Prange[0] elif obj > 0: # Check pressure range @@ -2069,23 +2102,15 @@ def calc_Prange_yi( ObjRange[1] = obj logger.info( - "Maximum Pressure (if it exists) between Pressure: {} and Obj Range: {}".format( - Prange, ObjRange - ) + "Maximum Pressure (if it exists) between Pressure: " + + "{} and Obj Range: {}".format(Prange, ObjRange) ) P0 = np.mean(Prange) scale_factor = 10 ** (np.ceil(np.log10(P0))) - args = (yi, T, Eos, density_opts, mole_fraction_options, scale_factor) + args = (yi, T, Eos, density_opts, mole_fraction_options) p = gtb.solve_root( - lambda x, yi, T, Eos, density_opts, mole_fraction_options, scale_factor: -objective_dew_pressure( - x * scale_factor, - yi, - T, - Eos, - density_opts, - mole_fraction_options, - ), + -objective_dew_pressure, args=args, x0=P0 / scale_factor, method="TNC", @@ -2132,7 +2157,8 @@ def calc_Prange_yi( p_min = p_min[obj_tmp == np.nanmin(obj_tmp)] elif len(p_min) == 0: logger.error( - "Could not find minimum in pressure range:\n Pressure: {}\n Obj Value: {}".format( + "Could not find minimum in pressure range:\n " + + "Pressure: {}\n Obj Value: {}".format( parray, obj_array ) ) @@ -2156,7 +2182,8 @@ def calc_Prange_yi( flag_min = False else: logger.error( - "Could not find maximum in pressure range:\n Pressure range {} best {}\n Obj Value range {} best {}".format( + "Could not find maximum in pressure range:\n Pressure " + + "range {} best {}\n Obj Value range {} best {}".format( Prange, p, ObjRange, obj ) ) @@ -2190,13 +2217,13 @@ def calc_Prange_yi( if z == maxiter - 1 or flag_min: if flag_min: logger.error( - "Cannot reach objective value of zero. Final Pressure: {}, Obj. Func: {}".format( - p, obj - ) + "Cannot reach objective value of zero. Final Pressure: " + + "{}, Obj. Func: {}".format(p, obj) ) else: logger.error( - "Maximum Number of Iterations Reached: A change in sign for the objective function could not be found, inspect progress" + "Maximum Number of Iterations Reached: A change in sign for the " + + "objective function could not be found, inspect progress" ) Prange = np.array([np.nan, np.nan]) Pguess = np.nan @@ -2207,7 +2234,8 @@ def calc_Prange_yi( logger.info("Initial guess in pressure: {} Pa".format(Pguess)) else: logger.error( - "Maximum Number of Iterations Reached: A change in sign for the objective function could not be found, inspect progress" + "Maximum Number of Iterations Reached: A change in sign for the " + + "objective function could not be found, inspect progress" ) _xi_global = xi_range @@ -2231,9 +2259,15 @@ def calc_vapor_composition( r""" Find vapor mole fraction given pressure, liquid mole fraction, and temperature. - Objective function is the sum of the predicted "mole numbers" predicted by the computed fugacity coefficients. Note that by "mole number" we mean that the prediction will only sum to one when the correct pressure is chosen in the outer loop. In this inner loop, we seek to find a mole fraction that is converged to reproduce itself in a prediction. If it hasn't, the new "mole numbers" are normalized into mole fractions and used as the next guess. - In the case that a guess doesn't produce a gas or critical fluid, we use another function to produce a new guess. - + Objective function is the sum of the predicted "mole numbers" predicted by the + computed fugacity coefficients. Note that by "mole number" we mean that the + prediction will only sum to one when the correct pressure is chosen in the outer + loop. In this inner loop, we seek to find a mole fraction that is converged to + reproduce itself in a prediction. If it hasn't, the new "mole numbers" are + normalized into mole fractions and used as the next guess. + In the case that a guess doesn't produce a gas or critical fluid, we use another + function to produce a new guess. + Parameters ---------- yi : numpy.ndarray @@ -2249,15 +2283,19 @@ def calc_vapor_composition( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` maxiter : int, Optional, default=50 - Maximum number of iteration for both the outer pressure and inner vapor mole fraction loops + Maximum number of iteration for both the outer pressure and inner vapor mole + fraction loops tol : float, Optional, default=1e-6 Tolerance in sum of predicted yi "mole numbers" tol_trivial : float, Optional, default=0.05 - If the vapor and liquid mole fractions are within this tolerance, search for a different composition + If the vapor and liquid mole fractions are within this tolerance, search for + a different composition kwargs : NA, Optional - Other other keyword arguments for :func:`~despasito.thermodynamics.calc.find_new_yi` + Other other keyword arguments for + :func:`~despasito.thermodynamics.calc.find_new_yi` Returns ------- @@ -2266,7 +2304,8 @@ def calc_vapor_composition( phiv : float Fugacity coefficient of vapor at system pressure flag : int - Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed + Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean + a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed """ if np.any(np.isnan(phil)): @@ -2280,8 +2319,8 @@ def calc_vapor_composition( yi /= np.sum(yi) flag_check_vapor = True # Make sure we only search for vapor compositions once flag_trivial_sol = ( - True - ) # Make sure we only try to find alternative to trivial solution once + True # Make sure we only try to find alternative to trivial solution once + ) logger.info(" Solve yi: P {}, T {}, xi {}, phil {}".format(P, T, xi, phil)) for z in range(maxiter): @@ -2314,14 +2353,16 @@ def calc_vapor_composition( yinew = calc_new_mole_fractions(xi, phil, phiv, phase="vapor") else: logger.debug( - " Composition doesn't produce a vapor, we need a function to search compositions for more than two components." + " Composition doesn't produce a vapor, we need a function to" + + " search compositions for more than two components." ) yinew = yi elif np.sum(np.abs(xi - yi_tmp) / xi) < tol_trivial and flag_trivial_sol: flag_trivial_sol = False if all(yi_tmp != 0.0) and len(yi_tmp) == 2: logger.debug( - " Composition produces trivial solution, let's find a different one!" + " Composition produces trivial solution, let's find a " + + "different one!" ) yi_tmp = find_new_yi( P, T, phil, xi, Eos, density_opts=density_opts, **kwargs @@ -2329,7 +2370,8 @@ def calc_vapor_composition( flag_check_vapor = False else: logger.debug( - " Composition produces trivial solution, using random guess to reset" + " Composition produces trivial solution, using random guess" + + " to reset" ) yi_tmp = np.random.rand(len(yi_tmp)) yi_tmp /= np.sum(yi_tmp) @@ -2355,7 +2397,8 @@ def calc_vapor_composition( if any(np.isnan(phiv)): phiv = np.nan logger.error( - "Fugacity coefficient of vapor should not be NaN, pressure could be too high." + "Fugacity coefficient of vapor should not be NaN, pressure could be" + + " too high." ) # Check for bouncing between values @@ -2376,9 +2419,8 @@ def calc_vapor_composition( ) _yi_global = yi2 logger.info( - " Inner Loop Final (from bracketing bouncing values) yi: {}, Final Error on Smallest Fraction: {}".format( - yi2, obj - ) + " Inner Loop Final (from bracketing bouncing values) yi: " + + "{}, Final Error on Smallest Fraction: {}".format(yi2, obj) ) break @@ -2399,7 +2441,8 @@ def calc_vapor_composition( if np.abs(yi2[ind_tmp] - yi_tmp[ind_tmp]) / yi_tmp[ind_tmp] < tol: _yi_global = yi2 logger.info( - " Inner Loop Final yi: {}, Final Error on Smallest Fraction: {}%".format( + " Inner Loop Final yi: " + + "{}, Final Error on Smallest Fraction: {}%".format( yi2, np.abs(yi2[ind_tmp] - yi_tmp[ind_tmp]) / yi_tmp[ind_tmp] * 100, ) @@ -2410,7 +2453,7 @@ def calc_vapor_composition( yi_total.append(np.sum(yinew)) yi = yinew - ## If yi wasn't found in defined number of iterations + # If yi wasn't found in defined number of iterations ind_tmp = np.where(yi_tmp == min(yi_tmp[yi_tmp > 0.0]))[0] if flagv == 3: yi2 = yinew / np.sum(yinew) @@ -2421,9 +2464,8 @@ def calc_vapor_composition( yi2 = yinew / np.sum(yinew) tmp = np.abs(yi2[ind_tmp] - yi_tmp[ind_tmp]) / yi_tmp[ind_tmp] logger.warning( - " More than {} iterations needed. Error in Smallest Fraction: {}%".format( - maxiter, tmp * 100 - ) + " More than {} iterations needed.".format(maxiter) + + " Error in Smallest Fraction: {}%".format(tmp * 100) ) if tmp > 0.1: # If difference is greater than 10% yinew = find_new_yi( @@ -2467,11 +2509,17 @@ def calc_liquid_composition( **kwargs ): r""" - Find liquid mole fraction given pressure, vapor mole fraction, and temperature. + Find liquid mole fraction given pressure, vapor mole fraction, and temperature. + + Objective function is the sum of the predicted "mole numbers" predicted by the + computed fugacity coefficients. Note that by "mole number" we mean that the + prediction will only sum to one when the correct pressure is chosen in the outer + loop. In this inner loop, we seek to find a mole fraction that is converged to + reproduce itself in a prediction. If it hasn't, the new "mole numbers" are + normalized into mole fractions and used as the next guess. + In the case that a guess doesn't produce a liquid or critical fluid, we use + another function to produce a new guess. - Objective function is the sum of the predicted "mole numbers" predicted by the computed fugacity coefficients. Note that by "mole number" we mean that the prediction will only sum to one when the correct pressure is chosen in the outer loop. In this inner loop, we seek to find a mole fraction that is converged to reproduce itself in a prediction. If it hasn't, the new "mole numbers" are normalized into mole fractions and used as the next guess. - In the case that a guess doesn't produce a liquid or critical fluid, we use another function to produce a new guess. - Parameters ---------- xi : numpy.ndarray @@ -2487,13 +2535,16 @@ def calc_liquid_composition( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` maxiter : int, Optional, default=20 - Maximum number of iteration for both the outer pressure and inner vapor mole fraction loops + Maximum number of iteration for both the outer pressure and inner vapor mole + fraction loops tol : float, Optional, default=1e-6 Tolerance in sum of predicted xi "mole numbers" tol_trivial : float, Optional, default=0.05 - If the vapor and liquid mole fractions are within this tolerance, search for a different composition + If the vapor and liquid mole fractions are within this tolerance, search for + a different composition kwargs : dict, Optional Optional keywords for :func:`~despasito.thermodynamics.calc.find_new_xi` @@ -2504,7 +2555,8 @@ def calc_liquid_composition( phil : float Fugacity coefficient of liquid at system pressure flag : int - Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true + Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean + a critical fluid, 3 means that neither is true """ global _xi_global @@ -2518,8 +2570,8 @@ def calc_liquid_composition( xi_total = [np.sum(xi)] flag_check_liquid = True # Make sure we only search for liquid compositions once flag_trivial_sol = ( - True - ) # Make sure we only try to find alternative to trivial solution once + True # Make sure we only try to find alternative to trivial solution once + ) logger.info(" Solve xi: P {}, T {}, yi {}, phiv {}".format(P, T, yi, phiv)) for z in range(maxiter): @@ -2552,14 +2604,16 @@ def calc_liquid_composition( xinew = calc_new_mole_fractions(yi, phil, phiv, phase="liquid") else: logger.debug( - " Composition doesn't produce a liquid, we need a function to search compositions for more than two components." + " Composition doesn't produce a liquid, we need a function" + + " to search compositions for more than two components." ) xinew = xi elif np.sum(np.abs(yi - xi_tmp) / yi) < tol_trivial and flag_trivial_sol: flag_trivial_sol = False if all(xi_tmp != 0.0) and len(xi_tmp) == 2: logger.debug( - " Composition produces trivial solution, let's find a different one!" + " Composition produces trivial solution, let's find a" + + " different one!" ) xi_tmp = find_new_xi( P, T, phiv, yi, Eos, density_opts=density_opts, **kwargs @@ -2567,7 +2621,8 @@ def calc_liquid_composition( flag_check_liquid = False else: logger.debug( - " Composition produces trivial solution, using random guess to reset" + " Composition produces trivial solution, using random guess" + + " to reset" ) xi_tmp = np.random.rand(len(xi_tmp)) xi_tmp /= np.sum(xi_tmp) @@ -2603,8 +2658,8 @@ def calc_liquid_composition( if np.abs(xi2[ind_tmp] - xi_tmp[ind_tmp]) / xi_tmp[ind_tmp] < tol: _xi_global = xi2 logger.info( - " Inner Loop Final xi: {}, Final Error on Smallest Fraction: {}%".format( - xi2, + " Inner Loop Final xi: {}, Final".format(xi2) + + " Error on Smallest Fraction: {}%".format( np.abs(xi2[ind_tmp] - xi_tmp[ind_tmp]) / xi_tmp[ind_tmp] * 100, ) ) @@ -2620,9 +2675,8 @@ def calc_liquid_composition( if z == maxiter - 1: tmp = np.abs(xi2[ind_tmp] - xi_tmp[ind_tmp]) / xi_tmp[ind_tmp] logger.warning( - " More than {} iterations needed. Error in Smallest Fraction: {} %%".format( - maxiter, tmp * 100 - ) + " More than {} iterations needed. ".format(maxiter) + + "Error in Smallest Fraction: {} %%".format(tmp * 100) ) if tmp > 0.1: # If difference is greater than 10% xinew = find_new_xi( @@ -2648,8 +2702,9 @@ def find_new_yi( P, T, phil, xi, Eos, bounds=(0.01, 0.99), npoints=30, density_opts={}, **kwargs ): r""" - Search vapor mole fraction combinations for a new estimate that produces a vapor density. - + Search vapor mole fraction combinations for a new estimate that produces a vapor + density. + Parameters ---------- P : float @@ -2661,13 +2716,16 @@ def find_new_yi( xi : numpy.ndarray Liquid mole fraction of each component, sum(xi) should equal 1.0 Eos : obj - An instance of the defined EOS class to be used in thermodynamic computations. + An instance of the defined EOS class to be used in thermodynamic + computations. bounds : tuple, Optional, default=(0.01, 0.99) - These bounds dictate the lower and upper boundary for the first component in a binary system. + These bounds dictate the lower and upper boundary for the first component in a + binary system. npoints : float, Optional, default=30 Number of points to test between the bounds. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` Returns ------- @@ -2677,9 +2735,8 @@ def find_new_yi( if len(kwargs) > 0: logger.debug( - " 'find_new_yi' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + " 'find_new_yi' does not use the following keyword arguments:" + + " {}".format(", ".join(list(kwargs.keys()))) ) yi_ext = np.linspace(bounds[0], bounds[1], npoints) # Guess for yi @@ -2722,7 +2779,7 @@ def find_new_yi( yi_min.append(yi_tmp[-1]) yi_min = np.array(yi_min) - ## Remove trivial solution + # Remove trivial solution obj_trivial = np.abs(yi_min - xi[0]) / xi[0] ind = np.where(obj_trivial == min(obj_trivial))[0][0] logger.debug( @@ -2790,8 +2847,9 @@ def bracket_bounding_yi( **kwargs ): r""" - Search binary vapor mole fraction combinations for a new estimate that produces a vapor density. - + Search binary vapor mole fraction combinations for a new estimate that produces a + vapor density. + Parameters ---------- P : float @@ -2805,27 +2863,29 @@ def bracket_bounding_yi( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. bounds : tuple, Optional, default=(0.01, 0.99) - These bounds dictate the lower and upper boundary for the first component in a binary system. + These bounds dictate the lower and upper boundary for the first component in a + binary system. maxiter : int, Optional, default=50 Maximum number of iterations tol : float, Optional, default=1e-7 Tolerance to quit search for yi density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` Returns ------- yi : numpy.ndarray Vapor mole fraction of each component, sum(yi) should equal 1.0 flag : int - Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed + Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean a + critical fluid, 3 means that neither is true, 4 means ideal gas is assumed """ if len(kwargs) > 0: logger.debug( - " 'calc_saturation_properties' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + " 'calc_saturation_properties' does not use the following keyword " + "arguments: {}".format(", ".join(list(kwargs.keys()))) ) if np.size(bounds) != 2: @@ -2843,9 +2903,8 @@ def bracket_bounding_yi( if flag_bounds[0] == flag_bounds[1]: logger.error( - " Both mole fractions have flag, {}, continue seeking convergence".format( - flag_bounds[0] - ) + " Both mole fractions have flag, " + + "{}, continue seeking convergence".format(flag_bounds[0]) ) y1 = bounds[1] flagv = flag_bounds[1] @@ -2895,13 +2954,14 @@ def bracket_bounding_yi( y1, flagv = bounds[ind], flag_bounds[ind] if i == maxiter - 1: logger.debug( - " Bouncing mole fraction, max iterations ended with, y1={}, flagv={}".format( - y1, flagv - ) + " Bouncing mole fraction, max iterations ended with, " + + "y1={}, flagv={}".format(y1, flagv) ) else: logger.debug( - " Bouncing mole fractions converged to y1={}, flagv={}".format(y1, flagv) + " Bouncing mole fractions converged to y1={}, flagv={}".format( + y1, flagv + ) ) return np.array([y1, 1 - y1]), flagv @@ -2910,13 +2970,13 @@ def bracket_bounding_yi( def objective_find_yi(yi, P, T, phil, xi, Eos, density_opts={}, return_flag=False): r""" Objective function for solving for stable vapor mole fraction. - + Parameters ---------- yi : numpy.ndarray Vapor mole fraction of each component, sum(yi) should equal 1.0 P : float - [Pa] Pressure of the system + [Pa] Pressure of the system T : float [K] Temperature of the system phil : float @@ -2926,19 +2986,23 @@ def objective_find_yi(yi, P, T, phil, xi, Eos, density_opts={}, return_flag=Fals Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` return_flag : bool, Optional, default=False - If True, the objective value and flagv is returned, otherwise, just the objective value is returned + If True, the objective value and flagv is returned, otherwise, just the + objective value is returned Returns ------- obj : numpy.ndarray Objective function for solving for vapor mole fractions flag : int, Optional - Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed. Only outputted when `return_flag` is True + Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean + a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed. + Only outputted when `return_flag` is True """ - if type(yi) == float or np.size(yi) == 1: + if isinstance(yi, float) or np.size(yi) == 1: if gtb.isiterable(yi): yi = np.array([yi[0], 1 - yi[0]]) else: @@ -2976,8 +3040,9 @@ def find_new_xi( P, T, phiv, yi, Eos, density_opts={}, bounds=(0.001, 0.999), npoints=30, **kwargs ): r""" - Search liquid mole fraction combinations for a new estimate that produces a liquid density. - + Search liquid mole fraction combinations for a new estimate that produces a liquid + density. + Parameters ---------- P : float @@ -2991,12 +3056,14 @@ def find_new_xi( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` bounds : tuple, Optional, default=(0.001, 0.999) - These bounds dictate the lower and upper boundary for the first component in a binary system. + These bounds dictate the lower and upper boundary for the first component in a + binary system. npoints : float, Optional, default=30 Number of points to test between the bounds. - + Returns ------- xi : numpy.ndarray @@ -3076,7 +3143,7 @@ def find_new_xi( def objective_find_xi(xi, P, T, phiv, yi, Eos, density_opts={}, return_flag=False): r""" Objective function for solving for stable vapor mole fraction. - + Parameters ---------- xi : numpy.ndarray @@ -3092,16 +3159,20 @@ def objective_find_xi(xi, P, T, phiv, yi, Eos, density_opts={}, return_flag=Fals Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` return_flag : bool, Optional, default=False - If True, the objective value and flagl is returned, otherwise, just the objective value is returned - + If True, the objective value and flagl is returned, otherwise, just the + objective value is returned + Returns ------- obj : numpy.ndarray Objective function for solving for liquid mole fractions flag : int, Optional - Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed. Only outputted when `return_flag` is True + Flag identifying the fluid type. A value of 0 is vapor, 1 is liquid, 2 mean + a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed. + Only outputted when `return_flag` is True """ if isinstance(xi, float) or len(xi) == 1: @@ -3142,8 +3213,9 @@ def objective_bubble_pressure( P, xi, T, Eos, density_opts={}, mole_fraction_options={}, **kwargs ): r""" - Objective function used to search pressure values and solve outer loop of constant temperature bubble point calculations. - + Objective function used to search pressure values and solve outer loop of constant + temperature bubble point calculations. + Parameters ---------- P : float @@ -3155,7 +3227,8 @@ def objective_bubble_pressure( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` mole_fraction_options : dict, Optional, default={} Options used to solve the inner loop in the solving algorithm @@ -3167,9 +3240,8 @@ def objective_bubble_pressure( if len(kwargs) > 0: logger.debug( - "'objective_bubble_pressure' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + "'objective_bubble_pressure' does not use the following keyword " + "arguments: {}".format(", ".join(list(kwargs.keys()))) ) global _yi_global @@ -3212,8 +3284,9 @@ def objective_dew_pressure( P, yi, T, Eos, density_opts={}, mole_fraction_options={}, **kwargs ): r""" - Objective function used to search pressure values and solve outer loop of constant temperature dew point calculations. - + Objective function used to search pressure values and solve outer loop of constant + temperature dew point calculations. + Parameters ---------- P : float @@ -3225,7 +3298,8 @@ def objective_dew_pressure( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` mole_fraction_options : dict, Optional, default={} Options used to solve the inner loop in the solving algorithm @@ -3237,9 +3311,8 @@ def objective_dew_pressure( if len(kwargs) > 0: logger.debug( - "'objective_dew_pressure' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + "'objective_dew_pressure' does not use the following keyword " + + "arguments: {}".format(", ".join(list(kwargs.keys()))) ) global _xi_global @@ -3291,8 +3364,9 @@ def calc_dew_pressure( **kwargs ): r""" - Calculate dew point mole fraction and pressure given system vapor mole fraction and temperature. - + Calculate dew point mole fraction and pressure given system vapor mole fraction + and temperature. + Parameters ---------- yi : numpy.ndarray @@ -3302,19 +3376,24 @@ def calc_dew_pressure( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` mole_fraction_options : dict, Optional, default={} Options used to solve the inner loop in the solving algorithm Pguess : float, Optional, default=None - [Pa] Guess the system pressure at the dew point. A negative value will force an estimation based on the saturation pressure of each component. + [Pa] Guess the system pressure at the dew point. A negative value will force + an estimation based on the saturation pressure of each component. Psat_set : float, Optional, default=1e+7 - [Pa] Set the saturation pressure if the pure component is above the critical point in these conditions + [Pa] Set the saturation pressure if the pure component is above the critical + point in these conditions method : str, Optional, default="bisect" Choose the method used to solve the dew point calculation pressure_options : dict, Optional, default={} - Options used in the given method, "method", to solve the outer loop in the solving algorithm + Options used in the given method, "method", to solve the outer loop in the + solving algorithm kwargs - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_saturation_properties` + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_saturation_properties` Returns ------- @@ -3323,18 +3402,21 @@ def calc_dew_pressure( xi : numpy.ndarray Mole fraction of each component, sum(xi) should equal 1.0 flagl : int - Flag identifying the fluid type for the liquid mole fractions, expected is liquid, 1. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true + Flag identifying the fluid type for the liquid mole fractions, expected is + liquid, 1. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 + means that neither is true flagv : int - Flag identifying the fluid type for the vapor mole fractions, expected is vapor or 0. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed + Flag identifying the fluid type for the vapor mole fractions, expected is + vapor or 0. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 + means that neither is true, 4 means ideal gas is assumed obj : float Objective function value """ if len(kwargs) > 0: logger.debug( - "'calc_dew_pressure' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + "'calc_dew_pressure' does not use the following keyword arguments: " + + "{}".format(", ".join(list(kwargs.keys()))) ) global _xi_global @@ -3350,9 +3432,8 @@ def calc_dew_pressure( if np.isnan(Psat[i]): Psat[i] = Psat_set logger.warning( - "Component, {}, is above its critical point. Psat is assumed to be {}.".format( - i + 1, Psat[i] - ) + "Component, {}, is above its critical point. Psat is".format(i + 1) + + " assumed to be {}.".format(Psat[i]) ) # Estimate initial pressure @@ -3380,22 +3461,22 @@ def calc_dew_pressure( ) if np.any(np.isnan(Prange)): raise ValueError( - "Neither a suitable pressure range, or guess in pressure could be found nor was given." + "Neither a suitable pressure range, or guess in pressure could be found " + "nor was given." ) else: if Pguess is not None: if Pguess > Prange[1] or Pguess < Prange[0]: logger.warning( - "Given guess in pressure, {}, is outside of the identified pressure range, {}. Using estimated pressure, {}.".format( - Pguess, Prange, Pestimate - ) + "Given guess in pressure, {}, is outside of the ".format(Pguess) + + "identified pressure range, " + + "{}. Using estimated pressure, {}.".format(Prange, Pestimate) ) P = Pestimate else: logger.warning( - "Using given guess in pressure, {}, that is inside identified pressure range.".format( - Pguess - ) + "Using given guess in pressure, {},".format(Pguess) + + " that is inside identified pressure range." ) P = Pguess else: @@ -3451,8 +3532,9 @@ def calc_bubble_pressure( **kwargs ): r""" - Calculate bubble point mole fraction and pressure given system liquid mole fraction and temperature. - + Calculate bubble point mole fraction and pressure given system liquid mole + fraction and temperature. + Parameters ---------- xi : numpy.ndarray @@ -3460,21 +3542,27 @@ def calc_bubble_pressure( T : float [K] Temperature of the system Eos : obj - An instance of the defined EOS class to be used in thermodynamic computations. + An instance of the defined EOS class to be used in thermodynamic + computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` mole_fraction_options : dict, Optional, default={} Options used to solve the inner loop in the solving algorithm Pguess : float, Optional, default=None - [Pa] Guess the system pressure at the dew point. A value of None will force an estimation based on the saturation pressure of each component. + [Pa] Guess the system pressure at the dew point. A value of None will force + an estimation based on the saturation pressure of each component. Psat_set : float, Optional, default=1e+7 - [Pa] Set the saturation pressure if the pure component is above the critical point in these conditions + [Pa] Set the saturation pressure if the pure component is above the critical + point in these conditions method : str, Optional, default="bisect" Choose the method used to solve the dew point calculation pressure_options : dict, Optional, default={} - Options used in the given method, ``method``, to solve the outer loop in the solving algorithm + Options used in the given method, ``method``, to solve the outer loop in the + solving algorithm kwargs - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_saturation_properties` + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_saturation_properties` Returns ------- @@ -3483,18 +3571,21 @@ def calc_bubble_pressure( yi : numpy.ndarray Mole fraction of each component, sum(yi) should equal 1.0 flagv : int - Flag identifying the fluid type for the vapor mole fractions, expected is vapor or 0. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed + Flag identifying the fluid type for the vapor mole fractions, expected is + vapor or 0. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 + means that neither is true, 4 means ideal gas is assumed flagl : int - Flag identifying the fluid type for the liquid mole fractions, expected is liquid, 1. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true + Flag identifying the fluid type for the liquid mole fractions, expected is + liquid, 1. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 + means that neither is true obj : float Objective function value """ if len(kwargs) > 0: logger.debug( - "'calc_bubble_pressure' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + "'calc_bubble_pressure' does not use the following keyword arguments: " + + "{}".format(", ".join(list(kwargs.keys()))) ) global _yi_global @@ -3509,13 +3600,12 @@ def calc_bubble_pressure( if np.isnan(Psat[i]): Psat[i] = Psat_set logger.warning( - "Component, {}, is above its critical point. Psat is assumed to be {}.".format( - i + 1, Psat[i] - ) + "Component, {}, is above its critical point.".format(i + 1) + + " Psat is assumed to be {}.".format(Psat[i]) ) # Estimate initial pressure - if Pguess == None: + if Pguess is None: P = 1.0 / np.sum(xi / Psat) else: P = Pguess @@ -3524,7 +3614,9 @@ def calc_bubble_pressure( _yi_global = xi * Psat / P _yi_global /= np.nansum(_yi_global) _yi_global = copy.deepcopy(_yi_global) - logger.info("Guess yi in calc_bubble_pressure with Psat: {}".format(_yi_global)) + logger.info( + "Guess yi in calc_bubble_pressure with Psat: " "{}".format(_yi_global) + ) yi = _yi_global Prange, Pestimate = calc_Prange_xi( @@ -3538,22 +3630,22 @@ def calc_bubble_pressure( ) if np.any(np.isnan(Prange)): raise ValueError( - "Neither a suitable pressure range, or guess in pressure could be found nor was given." + "Neither a suitable pressure range, or guess in pressure could be " + + "found nor was given." ) else: - if Pguess != None: + if Pguess is not None: if Pguess > Prange[1] or Pguess < Prange[0]: logger.warning( - "Given guess in pressure, {}, is outside of the identified pressure range, {}. Using estimated pressure, {}.".format( - Pguess, Prange, Pestimate - ) + "Given guess in pressure, {}, is outside of ".format(Pguess) + + "the identified pressure range, " + + "{}. Using estimated pressure, {}.".format(Prange, Pestimate) ) P = Pestimate else: logger.warning( - "Using given guess in pressure, {}, that is inside identified pressure range.".format( - Pguess - ) + "Using given guess in pressure, {}, that".format(Pguess) + + " is inside identified pressure range." ) P = Pguess else: @@ -3600,10 +3692,13 @@ def hildebrand_solubility( rhol, xi, T, Eos, dT=0.1, tol=1e-4, density_opts={}, **kwargs ): r""" - Calculate the solubility parameter based on temperature and composition. + Calculate the solubility parameter based on temperature and composition. + + This function is based on the method used in Zeng, Z., Y. Xi, and Y. Li + *Calculation of Solubility Parameter Using Perturbed-Chain SAFT and* + *Cubic-Plus-Association Equations of State* Ind. Eng. Chem. Res. 2008. 47, + 9663-9669. - This function is based on the method used in Zeng, Z., Y. Xi, and Y. Li *Calculation of Solubility Parameter Using Perturbed-Chain SAFT and Cubic-Plus-Association Equations of State* Ind. Eng. Chem. Res. 2008. 47, 9663-9669. - Parameters ---------- rhol : float @@ -3613,24 +3708,30 @@ def hildebrand_solubility( T : float Temperature of the system [K] Eos : obj - An instance of the defined EOS class to be used in thermodynamic computations. + An instance of the defined EOS class to be used in thermodynamic + computations. dT : float, Optional, default=0.1 - Change in temperature used in calculating the derivative with central difference method + Change in temperature used in calculating the derivative with central + difference method tol : float, Optional, default=1e-4 - This cutoff value evaluates the extent to which the integrand of the calculation has decayed. If the last value if the array is greater than tol, then the remaining area is estimated as a triangle, where the intercept is estimated from an interpolation of the previous four points. + This cutoff value evaluates the extent to which the integrand of the + calculation has decayed. If the last value if the array is greater than tol, + then the remaining area is estimated as a triangle, where the intercept is + estimated from an interpolation of the previous four points. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` Returns ------- delta : float - Solubility parameter [:math:`Pa^(1/2)`], ratio of cohesive energy and molar volume + Solubility parameter [:math:`Pa^(1/2)`], ratio of cohesive energy and molar + volume """ if len(kwargs) > 0: logger.debug( - "'hildebrand_solubility' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + "'hildebrand_solubility' does not use the following keyword arguments: " + + "{}".format(", ".join(list(kwargs.keys()))) ) R = constants.Nav * constants.kb @@ -3690,7 +3791,7 @@ def calc_flash( ): r""" Binary flash calculation of vapor and liquid mole fractions. - + Parameters ---------- P : numpy.ndarray @@ -3700,30 +3801,42 @@ def calc_flash( Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. density_opts : dict, Optional, default={} - Dictionary of options used in calculating pressure vs. specific volume in :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` + Dictionary of options used in calculating pressure vs. specific volume in + :func:`~despasito.thermodynamics.calc.pressure_vs_volume_arrays` maxiter : int, Optional, default=200 Maximum number of iterations in updating Ki values tol : float, Optional, tol: 1e-9 - Tolerance to break loop. The error is defined as the absolute value of the summed difference in Ki values between iterations. + Tolerance to break loop. The error is defined as the absolute value of the + summed difference in Ki values between iterations. min_mole_fraction0 : float, Optional, default=0 - Set the vapor and liquid mole fraction of component one to be greater than this number. Useful for diagrams with multiple solutions, such as those with an azeotrope. + Set the vapor and liquid mole fraction of component one to be greater than + this number. Useful for diagrams with multiple solutions, such as those with + an azeotrope. max_mole_fraction0 : float, Optional, default=1 - Set the vapor and liquid mole fraction of component one to be less than this number. Useful for diagrams with multiple solutions, such as those with an azeotrope. + Set the vapor and liquid mole fraction of component one to be less than this + number. Useful for diagrams with multiple solutions, such as those with an + azeotrope. Psat_set : float, Optional, default=1e+7 - [Pa] Set the saturation pressure if the pure component is above the critical point in these conditions + [Pa] Set the saturation pressure if the pure component is above the critical + point in these conditions kwargs - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_saturation_properties` + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_saturation_properties` Returns ------- xi : numpy.ndarray Liquid mole fraction of each component, sum(xi) should equal 1.0 flagl : int - Flag identifying the fluid type for the liquid mole fractions, expected is liquid, 1. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true + Flag identifying the fluid type for the liquid mole fractions, expected is + liquid, 1. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 + means that neither is true yi : numpy.ndarray Vapor mole fraction of each component, sum(yi) should equal 1.0 flagv : int - Flag identifying the fluid type for the vapor mole fractions, expected is vapor or 0. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means ideal gas is assumed + Flag identifying the fluid type for the vapor mole fractions, expected is + vapor or 0. A value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 + means that neither is true, 4 means ideal gas is assumed obj : float Objective function value """ @@ -3738,9 +3851,8 @@ def calc_flash( # Initialize Variables if Eos.number_of_components != 2: raise ValueError( - "Only binary systems are currently supported for flash calculations, {} were given.".format( - Eos.number_of_components - ) + "Only binary systems are currently supported for flash calculations, " + + "{} were given.".format(Eos.number_of_components) ) Psat, Ki0, xi, yi, phil, phiv = [ @@ -3757,9 +3869,8 @@ def calc_flash( if np.isnan(Psat[i]): Psat[i] = Psat_set logger.warning( - "Component, {}, is above its critical point. Psat is assumed to be {}.".format( - i + 1, Psat[i] - ) + "Component, {}, is above its critical point.".format(i + 1) + + " Psat is assumed to be {}.".format(Psat[i]) ) Ki0[i] = Psat[i] / P @@ -3788,9 +3899,8 @@ def calc_flash( if np.sum(yi) != 1.0: if np.abs(np.sum(yi) != 1.0) < np.sqrt(np.finfo(float).eps): raise ValueError( - "Vapor mole fractions do not add up to 1. Ki {}, xi {} produces {} = {}".format( - Ki, xi, yi, np.sum(yi) - ) + "Vapor mole fractions do not add up to 1. Ki " + + "{}, xi {} produces {} = {}".format(Ki, xi, yi, np.sum(yi)) ) else: yi /= np.sum(yi) @@ -3822,8 +3932,9 @@ def calc_flash( count_reset += 1 if not (Kinew == Ki_tmp).all(): logger.info( - " Reset Ki values, {}, according to mole fraction constraint, {} to {}, to produce {}".format( - Kinew, min_mole_fraction0, max_mole_fraction0, Ki_tmp + " Reset Ki values, {}, according to mole".format(Kinew) + + " fraction constraint, {} to {}, to produce {}".format( + min_mole_fraction0, max_mole_fraction0, Ki_tmp ) ) Ki = Ki_tmp @@ -3835,9 +3946,8 @@ def calc_flash( ind = np.where(Kiprev == min(Kiprev[Kiprev > 0]))[0][0] err = np.abs(Ki[ind] - Kiprev[ind]) / Kiprev[ind] logger.warning( - " Reset Ki values more than {} times. Remaining error, {}. These constraints may not be feasible".format( - 20, err - ) + " Reset Ki values more than {} times. Remaining ".format(20) + + "error, {}. These constraints may not be feasible".format(err) ) break elif np.all(np.abs(Ki - 1.0) < 1e-6) and flag_critical < 2: @@ -3851,9 +3961,8 @@ def calc_flash( Ki[flag_critical] = eps flag_critical += 1 logger.info( - " Liquid and vapor mole fractions are equal, let search from Ki = {}".format( - Ki - ) + " Liquid and vapor mole fractions are equal, let search from Ki =" + + " {}".format(Ki) ) elif err < tol: ind = np.where(Ki == min(Ki[Ki > 0]))[0][0] @@ -3900,18 +4009,23 @@ def calc_flash( def constrain_Ki(Ki0, min_mole_fraction0=0, max_mole_fraction0=1, **kwargs): r""" - For a binary mixture, determine whether the K values will produce properly constrained mole fractions. + For a binary mixture, determine whether the K values will produce properly + constrained mole fractions. If not, randomly choose a value of Ki[1] within the allowed range. - + Parameters ---------- Ki : numpy.ndarray K values for a binary mixture min_mole_fraction0 : float, Optional, default=0 - Set the vapor and liquid mole fraction of component one to be greater than this number. Useful for diagrams with multiple solutions, such as those with an azeotrope. + Set the vapor and liquid mole fraction of component one to be greater than + this number. Useful for diagrams with multiple solutions, such as those with + an azeotrope. max_mole_fraction0 : float, Optional, default=1 - Set the vapor and liquid mole fraction of component one to be less than this number. Useful for diagrams with multiple solutions, such as those with an azeotrope. + Set the vapor and liquid mole fraction of component one to be less than this + number. Useful for diagrams with multiple solutions, such as those with an + azeotrope. Returns ------- @@ -4029,10 +4143,11 @@ def constrain_Ki(Ki0, min_mole_fraction0=0, max_mole_fraction0=1, **kwargs): return Ki, flag_reset + def mixture_fugacity_coefficient(P, T, xi, rho, Eos): r""" Mixture fugacity coefficient d(ln(φ)) = np.sum(xi*ln(φi)) - + Parameters ---------- P : float @@ -4054,27 +4169,37 @@ def mixture_fugacity_coefficient(P, T, xi, rho, Eos): tmp_test = [gtb.isiterable(x) for x in [P, T, xi[0], rho]] if sum(tmp_test) > 1: - raise ValueError("Only one input may be an array representing different system conditions.") + raise ValueError( + "Only one input may be an array representing different system conditions." + ) coefficient = [] if tmp_test[0]: for p in P: - coefficient.append(np.sum(xi*np.log(Eos.fugacity_coefficient(p, rho, xi, T)))) + coefficient.append( + np.sum(xi * np.log(Eos.fugacity_coefficient(p, rho, xi, T))) + ) coefficient = np.array(coefficient) elif tmp_test[1]: for t in T: - coefficient.append(np.sum(xi*np.log(Eos.fugacity_coefficient(P, rho, xi, t)))) + coefficient.append( + np.sum(xi * np.log(Eos.fugacity_coefficient(P, rho, xi, t))) + ) coefficient = np.array(coefficient) elif tmp_test[2]: for xi_tmp in xi: - coefficient.append(np.sum(xi*np.log(Eos.fugacity_coefficient(P, rho, xi_tmp, T)))) + coefficient.append( + np.sum(xi * np.log(Eos.fugacity_coefficient(P, rho, xi_tmp, T))) + ) coefficient = np.array(coefficient) elif tmp_test[3]: for rho_tmp in rho: - coefficient.append(np.sum(xi*np.log(Eos.fugacity_coefficient(P, rho_tmp, xi, T)))) + coefficient.append( + np.sum(xi * np.log(Eos.fugacity_coefficient(P, rho_tmp, xi, T))) + ) coefficient = np.array(coefficient) else: - coefficient = np.sum(xi*np.log(Eos.fugacity_coefficient(P, rho, xi, T))) + coefficient = np.sum(xi * np.log(Eos.fugacity_coefficient(P, rho, xi, T))) return coefficient @@ -4082,7 +4207,7 @@ def mixture_fugacity_coefficient(P, T, xi, rho, Eos): def fugacity_test_1(P, T, xi, rho, Eos, step_size=1e-5, **kwargs): r""" A consistency test where d(ln φ)/dP = (Z-1)/P. - + Parameters ---------- P : float @@ -4101,8 +4226,8 @@ def fugacity_test_1(P, T, xi, rho, Eos, step_size=1e-5, **kwargs): Returns ------- Residual : float - Residual from thermodynamic identity - + Residual from thermodynamic identity + """ tmp_test = [gtb.isiterable(x) for x in [P, T, xi[0], rho]] @@ -4111,13 +4236,14 @@ def fugacity_test_1(P, T, xi, rho, Eos, step_size=1e-5, **kwargs): if len(kwargs) > 0: logger.debug( - "'fugacity_test_1' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + "'fugacity_test_1' does not use the following keyword arguments:" + + " {}".format(", ".join(list(kwargs.keys()))) ) Z = P / (rho * T * constants.R) - dlnPhidP = gtb.central_difference(P, mixture_fugacity_coefficient, step_size=step_size, args=(T, xi, rho, Eos)) + dlnPhidP = gtb.central_difference( + P, mixture_fugacity_coefficient, step_size=step_size, args=(T, xi, rho, Eos) + ) residual = dlnPhidP - (Z - 1) / P return residual @@ -4125,8 +4251,9 @@ def fugacity_test_1(P, T, xi, rho, Eos, step_size=1e-5, **kwargs): def fugacity_test_2(P, T, xi, rho, Eos, step_size=1e-3, n0=1, **kwargs): r""" - A consistency test where np.sum( xi * d(ln φ)/dn1) = 0 at constant temperature and pressure. - + A consistency test where np.sum( xi * d(ln φ)/dn1) = 0 at constant temperature + and pressure. + Parameters ---------- P : float @@ -4140,19 +4267,25 @@ def fugacity_test_2(P, T, xi, rho, Eos, step_size=1e-3, n0=1, **kwargs): Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. step_size : float, Optional, default=1e-3 - Step size in central difference method be aware that changing the step_size can change the inherent error in the derivative. + Step size in central difference method be aware that changing the step_size + can change the inherent error in the derivative. n0 : float, Optional, default=1.0 - Assumed number of moles in derivative, be aware that changing the step_size can change the inherent error in the derivative. For this example, n0 should be three orders of magnitude larger than the step_size to minimize error. + Assumed number of moles in derivative, be aware that changing the step_size + can change the inherent error in the derivative. For this example, n0 should + be three orders of magnitude larger than the step_size to minimize error. Returns ------- Residual : float - Thermodynamically consistency residual, should be zero. + Thermodynamically consistency residual, should be zero. """ if step_size >= n0: - raise ValueError("Central difference of n0: {}, cannot be comparable to step_size: {}".format(n0, step_size)) + raise ValueError( + "Central difference of n0: {}, cannot be".format(n0) + + " comparable to step_size: {}".format(step_size) + ) tmp_test = [gtb.isiterable(x) for x in [P, T, xi[0], rho]] if sum(tmp_test) > 0: @@ -4160,9 +4293,8 @@ def fugacity_test_2(P, T, xi, rho, Eos, step_size=1e-3, n0=1, **kwargs): if len(kwargs) > 0: logger.debug( - "'fugacity_test_2' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + "'fugacity_test_2' does not use the following keyword arguments:" + + "{}".format(", ".join(list(kwargs.keys()))) ) ncomp = len(xi) @@ -4177,27 +4309,27 @@ def fugacity_test_2(P, T, xi, rho, Eos, step_size=1e-3, n0=1, **kwargs): ) dlnPhidrho = gtb.central_difference( - n0, - _fugacity_test_2, - args=(n0, P, rho, xi, T, Eos), - step_size=step_size + n0, _fugacity_test_2, args=(n0, P, rho, xi, T, Eos), step_size=step_size ) - return np.sum(xi * dlnPhidrho)*2*step_size + return np.sum(xi * dlnPhidrho) * 2 * step_size -def _fugacity_test_2(N, n0, P, rho, xi, T, Eos): - """ Intermediate function for calculating the derivative with respect to the number of mole of component 1. - """ +def _fugacity_test_2(N, n0, P, rho, xi, T, Eos): + """Intermediate function for calculating the derivative with respect to the number + of mole of component 1.""" lnPhi_tmp = [] for n_tmp in N: - ni = xi*n0 - ni[0] = ni[0]+(n_tmp-n0) + ni = xi * n0 + ni[0] = ni[0] + (n_tmp - n0) if ni[0] < 0.0: - raise ValueError("Chosen step_size, {}, is larger than assumed amount of component 1: n0*x1={}".format(abs(n_tmp-n0),xi[0]*n0)) + raise ValueError( + "Chosen step_size, {}, is larger than".format(abs(n_tmp - n0)) + + " assumed amount of component 1: n0*x1={}".format(xi[0] * n0) + ) else: - xi_new = ni/np.sum(ni) + xi_new = ni / np.sum(ni) tmp = np.log(Eos.fugacity_coefficient(P, rho, xi_new, T)) lnPhi_tmp.extend(tmp) @@ -4208,7 +4340,7 @@ def activity_coefficient(P, T, xi, yi, Eos, **kwargs): r""" Calculate activity coefficient given T, P, yi, and xi. - + Parameters ---------- P : float @@ -4222,21 +4354,21 @@ def activity_coefficient(P, T, xi, yi, Eos, **kwargs): Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. kwargs - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_saturation_properties` + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_saturation_properties` Returns ------- activity_coefficient : numpy.ndarray Activity coefficient for given composition of mixtures Psat : numpy.ndarray - Saturation pressure + Saturation pressure """ if len(kwargs) > 0: logger.debug( - "'activity_coefficient' does not use the following keyword arguments: {}".format( - ", ".join(list(kwargs.keys())) - ) + "'activity_coefficient' does not use the following keyword arguments: " + + "{}".format(", ".join(list(kwargs.keys()))) ) ncomp = len(xi) diff --git a/despasito/thermodynamics/calculation_types.py b/despasito/thermodynamics/calculation_types.py index f3c1c0d..c76a7d3 100644 --- a/despasito/thermodynamics/calculation_types.py +++ b/despasito/thermodynamics/calculation_types.py @@ -1,8 +1,13 @@ """ - This thermodynamic module contains a series of wrappers to handle the inputs and outputs of these functions. - - The ``calc`` module contains the thermodynamic calculations. Calculation of pressure, fugacity_coefficient, and maximum allowed density are handled by an Eos object so that these functions can be used with any EOS. None of the functions in this folder need to be handled directly, as a function factory is included in our ``__init__.py`` file. Add "from thermodynamics import thermo" and use ``thermo("calc_type",Eos,input_dict)`` to get started. - + This thermodynamic module contains a series of wrappers to handle the inputs and + outputs of these functions. + + The ``calc`` module contains the thermodynamic calculations. Calculation of + pressure, fugacity_coefficient, and maximum allowed density are handled by an Eos + object so that these functions can be used with any EOS. None of the functions in + this folder need to be handled directly, as a function factory is included in our + ``__init__.py`` file. Add "from thermodynamics import thermo" and use + `thermo("calc_type",Eos,input_dict)`` to get started. """ import numpy as np @@ -18,38 +23,58 @@ def bubble_pressure(Eos, **sys_dict): r""" - Calculates pressure and vapor compositions given liquid mole fractions and temperature. + Calculates pressure and vapor compositions given liquid mole fractions and + temperature. Parameters ---------- Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. xilist : list - List of component mole fraction sets, where ``sum(xi)=1.0`` for each set. Each set of components corresponds to a temperature in Tlist, or if one set is given, this composition will be used for all temperatures. + List of component mole fraction sets, where ``sum(xi)=1.0`` for each set. Each + set of components corresponds to a temperature in Tlist, or if one set is + given, this composition will be used for all temperatures. Tlist : list, Optional, default=298.15 - [K] Temperature of the system corresponding to composition in xilist. If one set is given, this temperature will be used for all compositions. + [K] Temperature of the system corresponding to composition in xilist. If one + set is given, this temperature will be used for all compositions. Pguess : list/float, Optional - [Pa] Guess the system pressure at the bubble point. If a list of length one is provided, that value is used for all temperature-composition sets given. + [Pa] Guess the system pressure at the bubble point. If a list of length one + is provided, that value is used for all temperature-composition sets given. Pmin : list, Optional - [Pa] Set the upper bound for the minimum system pressure at the bubble point. If not defined, the default in :func:`~despasito.thermodynamics.calc.calc_bubble_pressure` is used. If a list of length one is provided, that value is used for all temperature-composition sets given. + [Pa] Set the upper bound for the minimum system pressure at the bubble point. + If not defined, the default in + :func:`~despasito.thermodynamics.calc.calc_bubble_pressure` is used. If a list + of length one is provided, that value is used for all temperature-composition + sets given. Pmax : list, Optional - [Pa] Set the upper bound for the maximum system pressure at the bubble point. If not defined, the default in :func:`~despasito.thermodynamics.calc.calc_bubble_pressure` is used. If a list of length one is provided, that value is used for all temperature-composition sets given. + [Pa] Set the upper bound for the maximum system pressure at the bubble point. + If not defined, the default in + :func:`~despasito.thermodynamics.calc.calc_bubble_pressure` is used. If a list + of length one is provided, that value is used for all temperature-composition + sets given. MultiprocessingObject : obj, Optional - Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` + Multiprocessing object, + :class:`~despasito.utils.parallelization.MultiprocessingJob` kwargs : Optional - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_saturation_properties` and :func:`~despasito.thermodynamics.calc.calc_bubble_pressure` + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_saturation_properties` and + :func:`~despasito.thermodynamics.calc.calc_bubble_pressure` Returns ------- output_dict : dict - Output dictionary containing given and calculated arrays + Output dictionary containing given and calculated arrays - - T: Temperature array generated from given instructions + - T: [K] Temperature array generated from given instructions - xi: Given array of liquid compositions - - flagl: Phase flag for liquid given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas - - P: Pressure values evaluated for given conditions + - flagl: Phase flag for liquid given temperature and calculated pressure, a + value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas + - P: [Pa] Pressure values evaluated for given conditions - yi: Vapor mole fraction evaluated for given conditions - - flagv: Phase flag for vapor given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + - flagv: Phase flag for vapor given temperature and calculated pressure, a + value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas - obj: List of objective values """ @@ -59,7 +84,7 @@ def bubble_pressure(Eos, **sys_dict): elif not gtb.isiterable(sys_dict["xilist"][0]): sys_dict["xilist"] = np.array([sys_dict["xilist"]], float) - ## Extract and check input data + # Extract and check input data thermo_keys = ["Tlist", "xilist"] thermo_dict = gtb.check_length_dict(sys_dict, thermo_keys) npoints = len(thermo_dict[list(thermo_dict.keys())[0]]) @@ -69,7 +94,7 @@ def bubble_pressure(Eos, **sys_dict): gtb.set_defaults(thermo_dict, "Tlist", thermo_defaults, lx=npoints) ) - ## Optional values + # Optional values optional_keys = ["Pguess", "Pmin", "Pmax"] opts = gtb.check_length_dict(sys_dict, optional_keys, lx=npoints) if opts: @@ -91,7 +116,7 @@ def bubble_pressure(Eos, **sys_dict): opts.update(sys_dict) # Add unprocessed options - ## Calculate P and yi + # Calculate P and yi per_job_var = ["Pmin", "Pmax", "Pguess"] inputs = [] for i in range(npoints): @@ -104,12 +129,12 @@ def bubble_pressure(Eos, **sys_dict): ) if flag_use_mp_object: - P_list, yi_list, flagv_list, flagl_list, obj_list = MultiprocessingObject.pool_job( - _bubble_pressure_wrapper, inputs + P_list, yi_list, flagv_list, flagl_list, obj_list = ( + MultiprocessingObject.pool_job(_bubble_pressure_wrapper, inputs) ) else: - P_list, yi_list, flagv_list, flagl_list, obj_list = MultiprocessingJob.serial_job( - _bubble_pressure_wrapper, inputs + P_list, yi_list, flagv_list, flagl_list, obj_list = ( + MultiprocessingJob.serial_job(_bubble_pressure_wrapper, inputs) ) logger.info("--- Calculation bubble_pressure Complete ---") @@ -126,8 +151,7 @@ def bubble_pressure(Eos, **sys_dict): def _bubble_pressure_wrapper(args): - r""" Wrapper for parallelized use of 'bubble_pressure' calculation type. - """ + r"""Wrapper for parallelized use of 'bubble_pressure' calculation type.""" T, xi, Eos, opts = args logger.info("T (K), xi: {} {}, Let's Begin!".format(T, xi)) @@ -156,40 +180,59 @@ def _bubble_pressure_wrapper(args): def dew_pressure(Eos, **sys_dict): - r""" - Calculate pressure and liquid composition given vapor mole fractions, yi, and temperature. - + Calculate pressure and liquid composition given vapor mole fractions, yi, and + temperature. + Parameters ---------- Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. yilist : list - List of component mole fraction sets, where ``sum(yi)=1.0`` for each set. Each set of components corresponds to a temperature in Tlist, or if one set is given, this composition will be used for all temperatures. + List of component mole fraction sets, where ``sum(yi)=1.0`` for each set. + Each set of components corresponds to a temperature in Tlist, or if one set + is given, this composition will be used for all temperatures. Tlist : list, Optional, default=298.15 - [K] Temperature of the system corresponding to composition in yilist. If one set is given, this pressure will be used for all compositions. + [K] Temperature of the system corresponding to composition in yilist. If one + set is given, this pressure will be used for all compositions. Pguess : list/float, Optional - [Pa] Guess the system pressure at the dew point. If a list of length one is provided, that value is used for all temperature-composition sets given. + [Pa] Guess the system pressure at the dew point. If a list of length one is + provided, that value is used for all temperature-composition sets given. Pmin : list, Optional - [Pa] Set the upper bound for the minimum system pressure at the dew point. If not defined, the default in :func:`~despasito.thermodynamics.calc.calc_dew_pressure` is used. If a list of length one is provided, that value is used for all temperature-composition sets given. + [Pa] Set the upper bound for the minimum system pressure at the dew point. + If not defined, the default in + :func:`~despasito.thermodynamics.calc.calc_dew_pressure` is used. If a list of + length one is provided, that value is used for all temperature-composition + sets given. Pmax : list, Optional - [Pa] Set the upper bound for the maximum system pressure at the dew point. If not defined, the default in :func:`~despasito.thermodynamics.calc.calc_dew_pressure` is used. If a list of length one is provided, that value is used for all temperature-composition sets given. + [Pa] Set the upper bound for the maximum system pressure at the dew point. + If not defined, the default in + :func:`~despasito.thermodynamics.calc.calc_dew_pressure` is used. If a list + of length one is provided, that value is used for all temperature-composition + sets given. MultiprocessingObject : obj, Optional - Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` + Multiprocessing object, + :class:`~despasito.utils.parallelization.MultiprocessingJob` kwargs : Optional - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_saturation_properties` and :func:`~despasito.thermodynamics.calc.calc_dew_pressure` + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_saturation_properties` and + :func:`~despasito.thermodynamics.calc.calc_dew_pressure` Returns ------- output_dict : dict Output dictionary containing given and calculated values - - T: Temperature array generated from given instructions + - T: [K] Temperature array generated from given instructions - yi: Given array of vapor compositions - - flagv: Phase flag for vapor given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas - - P: Pressure values evaluated for given conditions + - flagv: Phase flag for vapor given temperature and calculated pressure, a + value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas + - P: [Pa] Pressure values evaluated for given conditions - xi: Liquid mole fraction evaluated for given conditions - - flagl: Phase flag for liquid given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + - flagl: Phase flag for liquid given temperature and calculated pressure, a + value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas - obj: List of objective values """ @@ -199,7 +242,7 @@ def dew_pressure(Eos, **sys_dict): elif not gtb.isiterable(sys_dict["yilist"][0]): sys_dict["yilist"] = np.array([sys_dict["yilist"]], float) - ## Extract and check input data + # Extract and check input data thermo_keys = ["Tlist", "yilist"] thermo_dict = gtb.check_length_dict(sys_dict, thermo_keys) npoints = len(thermo_dict[list(thermo_dict.keys())[0]]) @@ -209,7 +252,7 @@ def dew_pressure(Eos, **sys_dict): gtb.set_defaults(thermo_dict, "Tlist", thermo_defaults, lx=npoints) ) - ## Optional values + # Optional values optional_keys = ["Pguess", "Pmin", "Pmax"] opts = gtb.check_length_dict(sys_dict, optional_keys, lx=npoints) if opts: @@ -231,7 +274,7 @@ def dew_pressure(Eos, **sys_dict): opts.update(sys_dict) # Add unprocessed options - ## Calculate P and yi + # Calculate P and yi per_job_var = ["Pguess", "Pmin", "Pmax"] inputs = [] for i in range(npoints): @@ -244,12 +287,12 @@ def dew_pressure(Eos, **sys_dict): ) if flag_use_mp_object: - P_list, xi_list, flagv_list, flagl_list, obj_list = MultiprocessingObject.pool_job( - _dew_pressure_wrapper, inputs + P_list, xi_list, flagv_list, flagl_list, obj_list = ( + MultiprocessingObject.pool_job(_dew_pressure_wrapper, inputs) ) else: - P_list, xi_list, flagv_list, flagl_list, obj_list = MultiprocessingJob.serial_job( - _dew_pressure_wrapper, inputs + P_list, xi_list, flagv_list, flagl_list, obj_list = ( + MultiprocessingJob.serial_job(_dew_pressure_wrapper, inputs) ) logger.info("--- Calculation dew_pressure Complete ---") @@ -266,8 +309,7 @@ def dew_pressure(Eos, **sys_dict): def _dew_pressure_wrapper(args): - r""" Wrapper for parallelized use of 'dew_pressure' calculation type. - """ + r"""Wrapper for parallelized use of 'dew_pressure' calculation type.""" T, yi, Eos, opts = args logger.info("T (K), yi: {} {}, Let's Begin!".format(T, yi)) @@ -296,48 +338,80 @@ def _dew_pressure_wrapper(args): def activity_coefficient(Eos, **sys_dict): - r""" Calculate the activity coefficient with a variety of methods. - Obtaining the activity coefficient requires the temperature, pressure, vapor composition, and liquid composition, but if one is missing, the necessary calculation computes it. - + Obtaining the activity coefficient requires the temperature, pressure, vapor + composition, and liquid composition, but if one is missing, the necessary + calculation computes it. + Parameters ---------- Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. Tlist : list, Optional, default=298.15 - [K] Temperature of the system corresponding to composition in yilist. If one set is given, this pressure will be used for all compositions. + [K] Temperature of the system corresponding to composition in yilist. If one + set is given, this pressure will be used for all compositions. xilist : list, Optional - List of component mole fraction sets, where ``sum(yi)=1.0`` for each set. Each set of components corresponds to a temperature in Tlist, or if one set is given, this composition will be used for all temperatures. If this value is not provided, a dew point calculation is initiated if the pressure is also absent, and a flash calculation if yilist is absent. + List of component mole fraction sets, where ``sum(yi)=1.0`` for each set. + Each set of components corresponds to a temperature in Tlist, or if one set + is given, this composition will be used for all temperatures. If this value + is not provided, a dew point calculation is initiated if the pressure is also + absent, and a flash calculation if yilist is absent. yilist : list, Optional - List of component mole fraction sets, where ``sum(yi)=1.0`` for each set. Each set of components corresponds to a temperature in Tlist, or if one set is given, this composition will be used for all temperatures. If this value is not provided, a bubble point calculation is initiated if the pressure is also absent, and a flash calculation if xilist is absent. + List of component mole fraction sets, where ``sum(yi)=1.0`` for each set. Each + set of components corresponds to a temperature in Tlist, or if one set is + given, this composition will be used for all temperatures. If this value is + not provided, a bubble point calculation is initiated if the pressure is also + absent, and a flash calculation if xilist is absent. Plist : list, Optional - [Pa] Pressure of the system corresponding to Tlist. If one value is given, this pressure will be used for all temperatures. If this value is not provided, a dew or bubble point calculation is initiated to find it. + [Pa] Pressure of the system corresponding to Tlist. If one value is given, + this pressure will be used for all temperatures. If this value is not + provided, a dew or bubble point calculation is initiated to find it. Pguess : list/float, Optional - [Pa] If the pressure and either xilist or yilist is not provided, guess the system pressure at the dew/bubble point respectively. If a list of length one is provided, that value is used for all temperature-composition sets given. + [Pa] If the pressure and either xilist or yilist is not provided, guess the + system pressure at the dew/bubble point respectively. If a list of length one + is provided, that value is used for all temperature-composition sets given. Pmin : list, Optional - [Pa] If the pressure and either xilist or yilist is not provided, set a minimum bounds in pressure for the dew/bubble point respectively. If not defined, the default in :func:`~despasito.thermodynamics.calc.calc_dew_pressure` is used. If a list of length one is provided, that value is used for all temperature-composition sets given. + [Pa] If the pressure and either xilist or yilist is not provided, set a + minimum bounds in pressure for the dew/bubble point respectively. If not + defined, the default in :func:`~despasito.thermodynamics.calc.calc_dew_pressure` + is used. If a list of length one is provided, that value is used for all + temperature-composition sets given. Pmax : list, Optional - [Pa] If the pressure and either xilist or yilist is not provided, set a maximum bounds in pressure for the dew/bubble point respectively. If not defined, the default in :func:`~despasito.thermodynamics.calc.calc_dew_pressure` is used. If a list of length one is provided, that value is used for all temperature-composition sets given. + [Pa] If the pressure and either xilist or yilist is not provided, set a maximum + bounds in pressure for the dew/bubble point respectively. If not defined, the + default in :func:`~despasito.thermodynamics.calc.calc_dew_pressure` is used. + If a list of length one is provided, that value is used for all + temperature-composition sets given. MultiprocessingObject : obj, Optional - Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` + Multiprocessing object, + :class:`~despasito.utils.parallelization.MultiprocessingJob` kwargs : Optional - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_saturation_properties` and either :func:`~despasito.thermodynamics.calc.calc_dew_pressure`, :func:`~despasito.thermodynamics.calc.calc_bubble_pressure`, or :func:`~despasito.thermodynamics.calc.calc_flash`, depending on which two of xi, yi, or P is missing. + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_saturation_properties` and either + :func:`~despasito.thermodynamics.calc.calc_dew_pressure`, + :func:`~despasito.thermodynamics.calc.calc_bubble_pressure`, or + :func:`~despasito.thermodynamics.calc.calc_flash`, depending on which two of + xi, yi, or P is missing. Returns ------- output_dict : dict Output dictionary containing given and calculated values - - T: Temperature array generated from given instructions - - P: Given or calculated array of pressures + - T: [K] Temperature array generated from given instructions + - P: [Pa] Given or calculated array of pressures - xi: Given or calculated array of liquid compositions - yi: Given or calculated array of vapor compositions - - flagv: Phase flag for vapor given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas - - flagl: Phase flag for liquid given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + - flagv: Phase flag for vapor given temperature and calculated pressure, a + value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas + - flagl: Phase flag for liquid given temperature and calculated pressure, a + value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas - obj: List of objective values if a calculation was needed - - Psat: List of calculated saturation pressures + - Psat: [Pa] List of calculated saturation pressures - gamma: List of calculated activity coefficients """ @@ -347,12 +421,12 @@ def activity_coefficient(Eos, **sys_dict): if "yilist" in sys_dict and not gtb.isiterable(sys_dict["yilist"][0]): sys_dict["yilist"] = np.array([sys_dict["yilist"]], float) - ## Extract and check input data + # Extract and check input data thermo_keys = ["Tlist", "yilist", "xilist", "Plist"] thermo_dict = gtb.check_length_dict(sys_dict, thermo_keys) npoints = len(thermo_dict[list(thermo_dict.keys())[0]]) - ## Determine Mode + # Determine Mode if "xilist" not in thermo_dict: if "yilist" not in thermo_dict: if "Plist" in thermo_dict: @@ -361,7 +435,7 @@ def activity_coefficient(Eos, **sys_dict): mode = None else: if "Plist" in thermo_dict: - thermodict["Pguess"] = thermo_dict["Plist"] + thermo_dict["Pguess"] = thermo_dict["Plist"] logger.info("Using given pressure as initial guess") mode = "dew_point" elif "yilist" not in thermo_dict: @@ -372,14 +446,15 @@ def activity_coefficient(Eos, **sys_dict): else: mode = "standard" - if mode == None: + if mode is None: raise ValueError( - "Two of the following system properties must be provided: Tlist, Plist, xilist, or yilist" + "Two of the following system properties must be provided: Tlist, Plist," + " xilist, or yilist" ) else: logger.info("Activity coefficient being calculated in {} mode.".format(mode)) - ## Set Defaults + # Set Defaults thermo_defaults = [ constants.standard_temperature, np.nan * np.ones(Eos.number_of_components), @@ -390,7 +465,7 @@ def activity_coefficient(Eos, **sys_dict): gtb.set_defaults(thermo_dict, thermo_keys, thermo_defaults, lx=npoints) ) - ## Optional values + # Optional values optional_keys = ["Pguess", "Pmin", "Pmax"] opts = gtb.check_length_dict(sys_dict, optional_keys, lx=npoints) if opts: @@ -412,7 +487,7 @@ def activity_coefficient(Eos, **sys_dict): opts.update(sys_dict) # Add unprocessed options - ## Calculate P and yi + # Calculate P and yi per_job_var = ["Pguess", "Pmin", "Pmax"] inputs = [] for i in range(npoints): @@ -436,9 +511,17 @@ def activity_coefficient(Eos, **sys_dict): tmp = MultiprocessingObject.pool_job(_activity_coefficient_wrapper, inputs) else: tmp = MultiprocessingJob.serial_job(_activity_coefficient_wrapper, inputs) - T_list, P_list, xi_list, yi_list, flagv_list, flagl_list, obj_list, Psat_list, gamma_list = ( - tmp - ) + ( + T_list, + P_list, + xi_list, + yi_list, + flagv_list, + flagl_list, + obj_list, + Psat_list, + gamma_list, + ) = tmp logger.info("--- Calculation dew_pressure Complete ---") @@ -456,8 +539,7 @@ def activity_coefficient(Eos, **sys_dict): def _activity_coefficient_wrapper(args): - r""" Wrapper for parallelized use of 'dew_pressure' calculation type. - """ + r"""Wrapper for parallelized use of 'dew_pressure' calculation type.""" T, P, xi, yi, Eos, opts, mode = args logger.info("T (K), yi: {} {}, Let's Begin!".format(T, yi)) @@ -480,9 +562,9 @@ def _activity_coefficient_wrapper(args): except Exception: logger.warning( - "In calculating the activity coefficient, {} calculation with T, P, xi, and yi inputs: {}, {}, {}, {} did not produce a valid result.".format( - mode, T, P, xi, yi - ) + "In calculating the activity coefficient, {} ".format(mode) + + "calculation with T, P, xi, and yi inputs: " + + "{}, {}, {}, {} did not produce a valid result.".format(T, P, xi, yi) ) logger.debug("Calculation Failed:", exc_info=True) @@ -525,22 +607,26 @@ def _activity_coefficient_wrapper(args): def flash(Eos, **sys_dict): - r""" - Flash calculation of vapor and liquid mole fractions. Only binary systems are currently supported + Flash calculation of vapor and liquid mole fractions. Only binary systems are + currently supported + + Input and system information are assessed first. An output file is generated with + T, yi, and corresponding P and xi. - Input and system information are assessed first. An output file is generated with T, yi, and corresponding P and xi. - Parameters ---------- Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. Tlist : list, Optional, default=298.15 - [K] Temperature of the system corresponding Plist. If one value is given, this temperature will be used for all temperatures. + [K] Temperature of the system corresponding Plist. If one value is given, this + temperature will be used for all temperatures. Plist : list, Optional, default=101325.0 - [Pa] Pressure of the system corresponding to Tlist. If one value is given, this pressure will be used for all temperatures. + [Pa] Pressure of the system corresponding to Tlist. If one value is given, + this pressure will be used for all temperatures. MultiprocessingObject : obj, Optional - Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` + Multiprocessing object, + :class:`~despasito.utils.parallelization.MultiprocessingJob` kwargs : Optional Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_flash` @@ -549,17 +635,21 @@ def flash(Eos, **sys_dict): output_dict : dict Output dictionary containing given and calculated values - - T: Temperature array generated from given instructions - - P: Pressure array generated from given instructions + - T: [K] Temperature array generated from given instructions + - P: [Pa] Pressure array generated from given instructions - xi: Given array of liquid compositions - - flagl: Phase flag for liquid given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + - flagl: Phase flag for liquid given temperature and calculated pressure, a + value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas - yi: Vapor mole fraction evaluated for given conditions - - flagv: Phase flag for vapor given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + - flagv: Phase flag for vapor given temperature and calculated pressure, + a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas - obj: List of objective values """ - ## Extract and check input data + # Extract and check input data thermo_keys = ["Tlist", "Plist"] thermo_dict = gtb.check_length_dict(sys_dict, thermo_keys) npoints = len(thermo_dict[list(thermo_dict.keys())[0]]) @@ -585,9 +675,8 @@ def flash(Eos, **sys_dict): if Eos.number_of_components != 2: raise ValueError( - "Only binary systems are currently supported for flash calculations, {} were given.".format( - Eos.number_of_components - ) + "Only binary systems are currently supported for flash calculations, " + "{} were given.".format(Eos.number_of_components) ) inputs = [ @@ -596,12 +685,12 @@ def flash(Eos, **sys_dict): ] if flag_use_mp_object: - xi_list, yi_list, flagv_list, flagl_list, obj_list = MultiprocessingObject.pool_job( - _flash_wrapper, inputs + xi_list, yi_list, flagv_list, flagl_list, obj_list = ( + MultiprocessingObject.pool_job(_flash_wrapper, inputs) ) else: - xi_list, yi_list, flagv_list, flagl_list, obj_list = MultiprocessingJob.serial_job( - _flash_wrapper, inputs + xi_list, yi_list, flagv_list, flagl_list, obj_list = ( + MultiprocessingJob.serial_job(_flash_wrapper, inputs) ) logger.info("--- Calculation flash Complete ---") @@ -618,8 +707,7 @@ def flash(Eos, **sys_dict): def _flash_wrapper(args): - r""" Wrapper for parallelized use of 'flash' calculation type. - """ + r"""Wrapper for parallelized use of 'flash' calculation type.""" T, P, Eos, opts = args @@ -627,9 +715,8 @@ def _flash_wrapper(args): xi, flagl, yi, flagv, obj = calc.calc_flash(P, T, Eos, **opts) except Exception: logger.warning( - "T (K), P (Pa): {} {}, calculation did not produce a valid result.".format( - T, P - ) + "T (K), P (Pa): {} {}, calculation".format(T, P) + + " did not produce a valid result." ) logger.debug("Calculation Failed:", exc_info=True) xi, yi, flagl, flagv, obj = [ @@ -646,36 +733,40 @@ def _flash_wrapper(args): def saturation_properties(Eos, **sys_dict): - r""" - Computes the saturated pressure, liquid, and gas density a one component phase at a temperature. + Computes the saturated pressure, liquid, and gas density a one component phase + at a temperature. + + Input and system information are assessed first. An output file is generated + with T, :math:`P^{sat}`, :math:`\rho^{sat}_{l}`, and :math:`\rho^{sat}_{v}`. - Input and system information are assessed first. An output file is generated with T, :math:`P^{sat}`, :math:`\rho^{sat}_{l}`, and :math:`\rho^{sat}_{v}`. - Parameters ---------- Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. - Tlist : list, Optional, default=298.15 - [K] Temperature of the system corresponding Plist. If one value is given, this temperature will be used for all temperatures. + Tlist : list, Optional, default=298.15 + [K] Temperature of the system corresponding Plist. If one value is given, this + temperature will be used for all temperatures. MultiprocessingObject : obj, Optional - Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` + Multiprocessing object, + :class:`~despasito.utils.parallelization.MultiprocessingJob` kwargs : Optional - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_saturation_properties` + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_saturation_properties` Returns ------- output_dict : dict Output dictionary containing given and calculated values - - T: Temperature array generated from given instructions - - Psat: Saturation pressure - - rhol: Liquid saturation density - - rholv: Vapor saturation density + - T: [K] Temperature array generated from given instructions + - Psat: [Pa] Saturation pressure + - rhol: [mol/:math:`m^3`] Liquid saturation density + - rholv: [mol/:math:`m^3`] Vapor saturation density """ - ## Extract and check input data + # Extract and check input data if "xilist" in sys_dict and not gtb.isiterable(sys_dict["xilist"][0]): sys_dict["xilist"] = np.array([sys_dict["xilist"]], float) @@ -730,8 +821,7 @@ def saturation_properties(Eos, **sys_dict): def _saturation_properties_wrapper(args): - r""" Wrapper for parallelized use of 'saturation_properties' calculation type. - """ + r"""Wrapper for parallelized use of 'saturation_properties' calculation type.""" T, xi, Eos, opts = args @@ -762,42 +852,51 @@ def _saturation_properties_wrapper(args): def liquid_properties(Eos, **sys_dict): - r""" - Computes the liquid density and fugacity coefficient given a temperature, pressure, and liquid mole fractions. + Computes the liquid density and fugacity coefficient given a temperature, pressure, + and liquid mole fractions. + + Input and system information are assessed first. An output file is generated with P, + T, xi, :math:`\rho_{l}`, and :math:`\phi_{l}`. - Input and system information are assessed first. An output file is generated with P, T, xi, :math:`\rho_{l}`, and :math:`\phi_{l}`. - Parameters ---------- Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. xilist : list - List of component mole fraction sets, where ``sum(xi)=1.0`` for each set. Each set of components corresponds to a temperature in Tlist, or if one set is given, this composition will be used for all temperatures. - Tlist : list, Optional default=298.15 - [K] Temperature of the system corresponding Plist. If one value is given, this temperature will be used for all temperatures. - Plist : list, Optional, default=101325.0 - [Pa] Pressure of the system corresponding to Tlist. If one value is given, this pressure will be used for all temperatures. + List of component mole fraction sets, where ``sum(xi)=1.0`` for each set. Each + set of components corresponds to a temperature in Tlist, or if one set is + given, this composition will be used for all temperatures. + Tlist : list, Optional default=298.15 + [K] Temperature of the system corresponding Plist. If one value is given, this + temperature will be used for all temperatures. + Plist : list, Optional, default=101325.0 + [Pa] Pressure of the system corresponding to Tlist. If one value is given, + this pressure will be used for all temperatures. MultiprocessingObject : obj, Optional - Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` + Multiprocessing object, + :class:`~despasito.utils.parallelization.MultiprocessingJob` kwargs : Optional - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_liquid_fugacity_coefficient` + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_liquid_fugacity_coefficient` Returns ------- output_dict : dict Output dictionary containing given and calculated values - - T: Temperature array generated from given instructions - - P: Pressure array generated from given instructions + - T: [K] Temperature array generated from given instructions + - P: [Pa] Pressure array generated from given instructions - xi: Composition generated from given instructions - - rhol: Evaluated liquid density + - rhol: [mol/:math:`m^3`] Evaluated liquid density - phil: Evaluated liquid fugacity coefficient - - flagl: Phase flag for liquid given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + - flagl: Phase flag for liquid given temperature and calculated pressure, a + value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas """ - ## Extract and check input data + # Extract and check input data if "xilist" in sys_dict and not gtb.isiterable(sys_dict["xilist"][0]): sys_dict["xilist"] = np.array([sys_dict["xilist"]], float) @@ -864,8 +963,7 @@ def liquid_properties(Eos, **sys_dict): def _liquid_properties_wrapper(args): - r""" Wrapper for parallelized use of 'liquid_properties' calculation type. - """ + r"""Wrapper for parallelized use of 'liquid_properties' calculation type.""" P, T, xi, Eos, opts = args @@ -888,42 +986,51 @@ def _liquid_properties_wrapper(args): def vapor_properties(Eos, **sys_dict): - r""" - Computes the vapor density and fugacity coefficient given a temperature, pressure, and vapor mole fractions. + Computes the vapor density and fugacity coefficient given a temperature, pressure, + and vapor mole fractions. + + Input and system information are assessed first. An output file is generated with + P, T, yi, :math:`\rho_{v}`, and :math:`\phi_{v}`. - Input and system information are assessed first. An output file is generated with P, T, yi, :math:`\rho_{v}`, and :math:`\phi_{v}`. - Parameters ---------- Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. yilist: : list - List of component mole fraction sets, where ``sum(yi)=1.0`` for each set. Each set of components corresponds to a temperature in Tlist, or if one set is given, this composition will be used for all temperatures. - Tlist : list, Optional, default=298.15 - [K] Temperature of the system corresponding Plist. If one value is given, this temperature will be used for all temperatures. + List of component mole fraction sets, where ``sum(yi)=1.0`` for each set. Each + set of components corresponds to a temperature in Tlist, or if one set is + given, this composition will be used for all temperatures. + Tlist : list, Optional, default=298.15 + [K] Temperature of the system corresponding Plist. If one value is given, this + temperature will be used for all temperatures. Plist : list, Optional, default=101325.0 - [Pa] Pressure of the system corresponding to Tlist. If one value is given, this pressure will be used for all temperatures. + [Pa] Pressure of the system corresponding to Tlist. If one value is given, + this pressure will be used for all temperatures. MultiprocessingObject : obj, Optional - Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` + Multiprocessing object, + :class:`~despasito.utils.parallelization.MultiprocessingJob` kwargs : Optional - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_vapor_fugacity_coefficient` + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_vapor_fugacity_coefficient` Returns ------- output_dict : dict Output dictionary containing given and calculated values - - T: Temperature array generated from given instructions - - P: Pressure array generated from given instructions + - T: [K] Temperature array generated from given instructions + - P: [Pa] Pressure array generated from given instructions - yi: Composition generated from given instructions - - rhov: Evaluated vapor density + - rhov: [mol/:math:`m^3`] Evaluated vapor density - phiv: Evaluated vapor fugacity coefficient - - flagv: Phase flag for vapor given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + - flagv: Phase flag for vapor given temperature and calculated pressure, a + value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas """ - ## Extract and check input data + # Extract and check input data if "yilist" in sys_dict and not gtb.isiterable(sys_dict["yilist"][0]): sys_dict["yilist"] = np.array([sys_dict["yilist"]], float) @@ -990,8 +1097,7 @@ def vapor_properties(Eos, **sys_dict): def _vapor_properties_wrapper(args): - r""" Wrapper for parallelized use of 'vapor_properties' calculation type. - """ + r"""Wrapper for parallelized use of 'vapor_properties' calculation type.""" P, T, yi, Eos, opts = args @@ -1014,38 +1120,49 @@ def _vapor_properties_wrapper(args): def solubility_parameter(Eos, **sys_dict): - r""" - Calculate the Hildebrand solubility parameter based on temperature and composition. + Calculate the Hildebrand solubility parameter based on temperature and composition. + + This function is based on the method used in Zeng, Z., Y. Xi, and Y. Li + *Calculation of Solubility Parameter Using Perturbed-Chain SAFT and* + *Cubic-Plus-Association Equations of State* Ind. Eng. Chem. Res. 2008. 47, + 9663-9669. - This function is based on the method used in Zeng, Z., Y. Xi, and Y. Li *Calculation of Solubility Parameter Using Perturbed-Chain SAFT and Cubic-Plus-Association Equations of State* Ind. Eng. Chem. Res. 2008. 47, 9663-9669. + Input and system information are assessed first. An output file is generated + with T, xi, :math:`\rho_{l}`, and :math:`\delta`. - Input and system information are assessed first. An output file is generated with T, xi, :math:`\rho_{l}`, and :math:`\delta`. - Parameters ---------- Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. - Tlist : list, Optional, default=298.15 - [K] Temperature of the system corresponding Plist. If one value is given, this temperature will be used for all temperatures. - xilist : list, Optional, default=[1.0] - Default assumes all of one component. List of component mole fraction sets, where ``sum(xi)=1.0`` for each set. Each set of components corresponds to a temperature in Tlist, or if one set is given, this composition will be used for all temperatures. + Tlist : list, Optional, default=298.15 + [K] Temperature of the system corresponding Plist. If one value is given, this + temperature will be used for all temperatures. + xilist : list, Optional, default=[1.0] + Default assumes all of one component. List of component mole fraction sets, + where ``sum(xi)=1.0`` for each set. Each set of components corresponds to a + temperature in Tlist, or if one set is given, this composition will be used + for all temperatures. Plist : list, Optional, default=101325.0 - [Pa] Pressure of the system corresponding to Tlist. If one value is given, this pressure will be used for all temperatures. + [Pa] Pressure of the system corresponding to Tlist. If one value is given, + this pressure will be used for all temperatures. MultiprocessingObject : obj, Optional - Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` + Multiprocessing object, + :class:`~despasito.utils.parallelization.MultiprocessingJob` kwargs : Optional - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_liquid_density` and :func:`~despasito.thermodynamics.calc.calc_hildebrand_solubility` + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_liquid_density` and + :func:`~despasito.thermodynamics.calc.calc_hildebrand_solubility` Returns ------- output_dict : dict Output dictionary containing given and calculated values - - T: Temperature array generated from given instructions - - P: Pressure array generated from given instructions + - T: [K] Temperature array generated from given instructions + - P: [Pa] Pressure array generated from given instructions - xi: Composition generated from given instructions - - rhol: Evaluated liquid density + - rhol: [mol/:math:`m^3`] Evaluated liquid density - delta: Hidebrand solubility parameter given system conditions """ @@ -1053,7 +1170,7 @@ def solubility_parameter(Eos, **sys_dict): if "xilist" in sys_dict and not gtb.isiterable(sys_dict["xilist"][0]): sys_dict["xilist"] = np.array([sys_dict["xilist"]], float) - ## Extract and check input data + # Extract and check input data thermo_keys = ["Tlist", "xilist", "Plist"] if any([x in sys_dict for x in thermo_keys]): thermo_dict = gtb.check_length_dict(sys_dict, thermo_keys) @@ -1087,7 +1204,7 @@ def solubility_parameter(Eos, **sys_dict): opts = sys_dict.copy() - ## Calculate solubility parameter + # Calculate solubility parameter inputs = [ ( thermo_dict["Plist"][i], @@ -1119,8 +1236,7 @@ def solubility_parameter(Eos, **sys_dict): def _solubility_parameter_wrapper(args): - r""" Wrapper for parallelized use of 'solubility_parameter' calculation type. - """ + r"""Wrapper for parallelized use of 'solubility_parameter' calculation type.""" P, T, xi, Eos, opts = args @@ -1143,56 +1259,71 @@ def _solubility_parameter_wrapper(args): def verify_eos(Eos, **sys_dict): - r""" - The following consistency checks are performed to ensure the calculated fugacity coefficients are thermodynamically consistent. + The following consistency checks are performed to ensure the calculated fugacity + coefficients are thermodynamically consistent. - 1. :math:`d(ln φ)/dP = (Z-1)/P` - 2. :math:`\sum{x_i d(ln φ)/d n_1 )} = 0` - + Parameters ---------- Eos : obj An instance of the defined EOS class to be used in thermodynamic computations. - Tlist : list, Optional, default=298.15 - [K] Temperature of the system corresponding Plist. If one value is given, this temperature will be used for all temperatures. + Tlist : list, Optional, default=298.15 + [K] Temperature of the system corresponding Plist. If one value is given, this + temperature will be used for all temperatures. xilist : list, Optional - Default array of 11 values from x1=0 to x1=1 for binary array. List of component mole fraction sets, where ``sum(xi)=1.0`` for each set. Each set of components corresponds to a temperature in Tlist, or if one set is given, this composition will be used for all temperatures. - Plist : list, Optional, default=101325.0 - [Pa] Pressure of the system corresponding to Tlist. If one value is given, this pressure will be used for all temperatures. + Default array of 11 values from x1=0 to x1=1 for binary array. List of + component mole fraction sets, where ``sum(xi)=1.0`` for each set. Each set + of components corresponds to a temperature in Tlist, or if one set is given, + this composition will be used for all temperatures. + Plist : list, Optional, default=101325.0 + [Pa] Pressure of the system corresponding to Tlist. If one value is given, + this pressure will be used for all temperatures. MultiprocessingObject : obj, Optional - Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` + Multiprocessing object, + :class:`~despasito.utils.parallelization.MultiprocessingJob` kwargs : Optional - Keyword arguments for :func:`~despasito.thermodynamics.calc.calc_vapor_density`, :func:`~despasito.thermodynamics.calc.calc_liquid_density`, :func:`~despasito.thermodynamics.calc.calc_fugacity_test_1`, and :func:`~despasito.thermodynamics.calc.calc_fugacity_test_2`, + Keyword arguments for + :func:`~despasito.thermodynamics.calc.calc_vapor_density`, + :func:`~despasito.thermodynamics.calc.calc_liquid_density`, + :func:`~despasito.thermodynamics.calc.calc_fugacity_test_1`, and + :func:`~despasito.thermodynamics.calc.calc_fugacity_test_2`, Returns ------- output_dict : dict Output dictionary containing given and calculated values - - T: Temperature array generated from given instructions - - P: Pressure array generated from given instructions + - T: [K] Temperature array generated from given instructions + - P: [Pa] Pressure array generated from given instructions - xi: Composition generated from given instructions - residual_v1: Residual of vapor from method one - residual_v2: Residual of vapor from method two - - flagv: Phase flag for vapor given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + - flagv: Phase flag for vapor given temperature and calculated pressure, + a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas - log_phivi: Log of the partial vapor fugacity coefficient for each component - residual_l1: Residual of liquid from method one - residual_l2: Residual of liquid from method two - - flagl: Phase flag for liquid given temperature and calculated pressure, a value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that neither is true, 4 means we should assume ideal gas + - flagl: Phase flag for liquid given temperature and calculated pressure, a + value of 0 is vapor, 1 is liquid, 2 mean a critical fluid, 3 means that + neither is true, 4 means we should assume ideal gas - log_phili: Log of the partial liquid fugacity coefficient for each component """ - ## Extract and check input data + # Extract and check input data if "xilist" not in sys_dict: if Eos.number_of_components == 2: tmp = np.linspace(0.01, 0.99, 11) sys_dict["xilist"] = np.array([[x, 1.0 - x] for x in tmp]) logger.info("Use array of mole fractions") - else: + else: raise ValueError( - "Must have at least 2 components. With more that 2 components, the mole fractions need to be specified" + "Must have at least 2 components. With more that 2 components, the" + " mole fractions need to be specified" ) elif "xilist" in sys_dict and not gtb.isiterable(sys_dict["xilist"][0]): sys_dict["xilist"] = np.array([sys_dict["xilist"]], float) @@ -1231,13 +1362,27 @@ def verify_eos(Eos, **sys_dict): for i in range(npoints) ] if flag_use_mp_object: - residual_v1, residual_v2, flagv, log_phiv, residual_l1, residual_l2, flagl, log_phil = MultiprocessingObject.pool_job( - _verify_eos_wrapper, inputs - ) + ( + residual_v1, + residual_v2, + flagv, + log_phiv, + residual_l1, + residual_l2, + flagl, + log_phil, + ) = MultiprocessingObject.pool_job(_verify_eos_wrapper, inputs) else: - residual_v1, residual_v2, flagv, log_phiv, residual_l1, residual_l2, flagl, log_phil = MultiprocessingJob.serial_job( - _verify_eos_wrapper, inputs - ) + ( + residual_v1, + residual_v2, + flagv, + log_phiv, + residual_l1, + residual_l2, + flagl, + log_phil, + ) = MultiprocessingJob.serial_job(_verify_eos_wrapper, inputs) logger.info("--- Calculation verify_eos Complete ---") @@ -1257,8 +1402,7 @@ def verify_eos(Eos, **sys_dict): def _verify_eos_wrapper(args): - r""" Wrapper for parallelized use of 'verify_eos' calculation type. - """ + r"""Wrapper for parallelized use of 'verify_eos' calculation type.""" P, T, xi, Eos, opts = args @@ -1275,8 +1419,12 @@ def _verify_eos_wrapper(args): residual_v1 = calc.fugacity_test_1(P, T, xi, rhov, Eos, **opts) residual_v2 = calc.fugacity_test_2(P, T, xi, rhov, Eos, **opts) logger.info( - "rhov {}, flagv {}, log_phiv {}, log_phiv {}, residual1 {}, residual2 {}".format( - rhov, flagv, np.sum(xi * log_phiv), log_phiv, residual_v1, residual_v2 + "rhov {}, flagv {}, ".format( + rhov, + flagv, + ) + + "log_phiv {}, log_phiv {}, residual1 {}, residual2 {}".format( + np.sum(xi * log_phiv), log_phiv, residual_v1, residual_v2 ) ) @@ -1291,8 +1439,9 @@ def _verify_eos_wrapper(args): residual_l1 = calc.fugacity_test_1(P, T, xi, rhol, Eos, **opts) residual_l2 = calc.fugacity_test_2(P, T, xi, rhol, Eos, **opts) logger.info( - "rhol {}, flagl {}, log_phil {}, log_phil {}, residual1 {}, residual2 {}".format( - rhol, flagl, np.sum(xi * log_phil), log_phil, residual_l1, residual_l2 + "rhol {}, flagl {}, ".format(rhol, flagl) + + "log_phil {}, log_phil {}, residual1 {}, residual2 {}".format( + np.sum(xi * log_phil), log_phil, residual_l1, residual_l2 ) ) diff --git a/despasito/utils/general_toolbox.py b/despasito/utils/general_toolbox.py index 5aedfea..aaeb843 100644 --- a/despasito/utils/general_toolbox.py +++ b/despasito/utils/general_toolbox.py @@ -15,7 +15,10 @@ def solve_root(func, args=(), method="bisect", x0=None, bounds=None, options={}) Parameters ---------- func : function - Function used in job. Can be any of the following scipy methods: "brent", "least_squares", "TNC", "L-BFGS-B", "SLSQP", 'hybr', 'lm', 'linearmixing', 'diagbroyden', 'excitingmixing', 'krylov', 'df-sane', 'anderson', 'hybr_broyden1', 'hybr_broyden2', 'broyden1', 'broyden2', 'bisect'. + Function used in job. Can be any of the following scipy methods: "brent", + "least_squares", "TNC", "L-BFGS-B", "SLSQP", 'hybr', 'lm', 'linearmixing', + 'diagbroyden', 'excitingmixing', 'krylov', 'df-sane', 'anderson', + 'hybr_broyden1', 'hybr_broyden2', 'broyden1', 'broyden2', 'bisect'. args : tuple, Optional, default=() Each entry of this list contains the input arguments for each job method : str, Optional, default="bisect" @@ -23,7 +26,7 @@ def solve_root(func, args=(), method="bisect", x0=None, bounds=None, options={}) x0 : float, Optional, default=None Initial guess in parameter to be optimized bounds : tuple, Optional, default=None - Parameter boundaries + Parameter boundaries options : dict, Optional, default={} These options are used in the scipy method @@ -75,9 +78,8 @@ def solve_root(func, args=(), method="bisect", x0=None, bounds=None, options={}) ]: if np.any(bounds is None): raise ValueError( - "Optimization method, {}, requires x0. Because bounds were not provided, so problem cannot be solved.".format( - method - ) + "Optimization method, {}, requires x0. Because bounds were" + " not provided, so problem cannot be solved.".format(method) ) else: logger.error( @@ -105,9 +107,8 @@ def solve_root(func, args=(), method="bisect", x0=None, bounds=None, options={}) if np.any(bounds is None) and method in ["brentq", "bisect"]: if x0 is None: raise ValueError( - "Optimization method, {}, requires bounds. Because x0 was not provided, so problem cannot be solved.".format( - method - ) + "Optimization method, {}, requires bounds. ".format(method) + + "Because x0 was not provided, so problem cannot be solved." ) else: logger.error( @@ -120,7 +121,7 @@ def solve_root(func, args=(), method="bisect", x0=None, bounds=None, options={}) if len(bnd) != 2: raise ValueError("bounds are not of length two") - #################### Root Finding without Boundaries ################### + # ################### Root Finding without Boundaries ################### if method in ["broyden1", "broyden2"]: outer_dict = { "fatol": 1e-5, @@ -164,7 +165,7 @@ def solve_root(func, args=(), method="bisect", x0=None, bounds=None, options={}) ) sol = spo.root(func, x0, args=args, method=method, options=outer_dict) - #################### Minimization Methods with Boundaries ################### + # ################### Minimization Methods with Boundaries ################### elif method in ["TNC", "L-BFGS-B"]: outer_dict = { "gtol": 1e-2 * np.sqrt(np.finfo("float").eps), @@ -209,7 +210,7 @@ def solve_root(func, args=(), method="bisect", x0=None, bounds=None, options={}) else: sol = spo.minimize(func, x0, args=args, method=method, options=outer_dict) - #################### Root Finding with Boundaries ################### + # ################### Root Finding with Boundaries ################### elif method == "brentq": outer_dict = {"rtol": 1e-7} for key, value in options.items(): @@ -266,25 +267,33 @@ def solve_root(func, args=(), method="bisect", x0=None, bounds=None, options={}) def central_difference(x, func, step_size=1e-5, relative=False, args=()): """ - Take the derivative of a dependent variable calculated with a given function using the central difference method. - + Take the derivative of a dependent variable calculated with a given function using + the central difference method. + Parameters ---------- x : numpy.ndarray - Independent variable to take derivative with respect too, using the central difference method. Must be first input of the function. + Independent variable to take derivative with respect too, using the central + difference method. Must be first input of the function. func : function - Function used in job to calculate dependent factor. This function should have a single output. + Function used in job to calculate dependent factor. This function should have + a single output. step_size : float, Optional, default=1E-5 - Either the step size used in the central difference method, or if ``relative=True``, this variable is a scaling factor so that the step size for each value of x is x * step_size. + Either the step size used in the central difference method, or if + ``relative=True``, this variable is a scaling factor so that the step size for + each value of x is x * step_size. args : tuple, Optional, default=() Each entry of this list contains the input arguments for each job relative : bool, Optional, default=False - If False, the step_size is directly used to calculate the derivative. If true, step_size becomes a scaling factor, where the step size for each value of x becomes step_size*x. + If False, the step_size is directly used to calculate the derivative. If true, + step_size becomes a scaling factor, where the step size for each value of x + becomes step_size*x. Returns ------- dydx : numpy.ndarray - Array of derivative of y with respect to x, given an array of independent variables. + Array of derivative of y with respect to x, given an array of independent + variables. """ if not isiterable(x): @@ -303,7 +312,7 @@ def central_difference(x, func, step_size=1e-5, relative=False, args=()): step = step_size y = func(np.append(x + step, x - step), *args) - lx = int(len(y)/2) + lx = int(len(y) / 2) dydx = (y[:lx] - y[lx:]) / (2.0 * step) return dydx @@ -313,7 +322,8 @@ def isiterable(array): """ Check if variable is an iterable type with a length (e.g. np.array or list). - Note that this could be tested with ``isinstance(array, Iterable)``, however ``array=np.array(1.0)`` would pass that test and then fail in ``len(array)``. + Note that this could be tested with ``isinstance(array, Iterable)``, however + ``array=np.array(1.0)`` would pass that test and then fail in ``len(array)``. Parameters ---------- @@ -337,16 +347,19 @@ def isiterable(array): def check_length_dict(dictionary, keys, lx=None): - """ - This function compared the entries, keys, in the provided dictionary to ensure they're the same length. + This function compared the entries, keys, in the provided dictionary to ensure + they're the same length. - All entries in the list ``keys``, will be made into numpy arrays (if present). If a float or array of length one is provided, it will be expanded to the length of other arrays. + All entries in the list ``keys``, will be made into numpy arrays (if present). + If a float or array of length one is provided, it will be expanded to the length + of other arrays. Parameters ---------- dictionary : dict - Dictionary containing all or some of the keywords, ``keys``, of what should be arrays of identical size. + Dictionary containing all or some of the keywords, ``keys``, of what should + be arrays of identical size. keys : list Possible keywords representing array entries lx : int, Optional, default=None @@ -359,7 +372,7 @@ def check_length_dict(dictionary, keys, lx=None): """ - if lx == None: + if lx is None: lx_array = [] for key in keys: if key in dictionary: @@ -395,9 +408,9 @@ def check_length_dict(dictionary, keys, lx=None): def set_defaults(dictionary, keys, values, lx=None): - """ - This function checks a dictionary for the given keys, and if it's not there, the appropriate value is added to the dictionary. + This function checks a dictionary for the given keys, and if it's not there, the + appropriate value is added to the dictionary. Parameters ---------- @@ -408,7 +421,8 @@ def set_defaults(dictionary, keys, values, lx=None): values : list Default values for the keys that aren't in dictionary lx : int, Optional, default=None - If not None, and values[i] is a float, the key will be set to an array of length, ``lx``, populated by ``values[i]`` + If not None, and values[i] is a float, the key will be set to an array of + length, ``lx``, populated by ``values[i]`` Returns ------- @@ -442,7 +456,7 @@ def set_defaults(dictionary, keys, values, lx=None): for i, key in enumerate(keys): if key not in dictionary: tmp = values[i] - if not isiterable(tmp) and lx != None: + if not isiterable(tmp) and lx is not None: new_dictionary[key] = np.ones(lx) * tmp else: new_dictionary[key] = tmp diff --git a/despasito/utils/parallelization.py b/despasito/utils/parallelization.py index 04d650b..108cdd9 100644 --- a/despasito/utils/parallelization.py +++ b/despasito/utils/parallelization.py @@ -15,14 +15,14 @@ class MultiprocessingJob: - """ This object initiates the pool for multiprocessing jobs. Parameters ---------- ncores : int, Optional, default=-1 - Number of processes used. If the default value of -1, the system cpu count is used. + Number of processes used. If the default value of -1, the system cpu count + is used. Attributes ---------- @@ -32,6 +32,7 @@ class MultiprocessingJob: def __init__(self, ncores=-1): + multiprocessing.freeze_support() self.flag_use_mp = True if ncores == -1: ncores = multiprocessing.cpu_count() # includes logical cores! @@ -68,8 +69,8 @@ def __init__(self, ncores=-1): logger.info("MP log files: {}".format(", ".join(self.logfiles))) def _extract_root_logging(self): - """ Swap root handlers defined in despasito.__main__ with process specific log handlers - """ + """Swap root handlers defined in despasito.__main__ with process specific log + handlers""" for handler in logging.root.handlers: if "baseFilename" in handler.__dict__: self._logformat = handler.formatter._fmt @@ -86,9 +87,17 @@ def _initialize_mp_handler(level, logformat): Parameters ---------- level : int - The verbosity level of logging information can be set to any supported representation of the `logging level `_. + The verbosity level of logging information can be set to any supported + representation of the + `logging level `_ + . logformat : str - Formating of logging information can be set to any supported representation of the `formatting class `_. + Formating of logging information can be set to any supported representation + of the + `formatting class `_. + + .. _LOGGING: https://docs.python.org/3/library/logging.html#logging-levels/ + .. _FORMAT: https://docs.python.org/3/library/logging.html#logging.Formatter/ """ logger = logging.getLogger() @@ -159,22 +168,19 @@ def serial_job(func, inputs): return np.transpose(output) def _consolidate_mp_logs(self): - """ Consolidate multiprocessing logs into main log - """ + """Consolidate multiprocessing logs into main log""" for i, fn in enumerate(self.logfiles): with open(fn) as f: logger.info("Log from thread {0}:\n{1}".format(i, f.read())) open(fn, "w").write("") def _remove_mp_logs(self): - """ Ensure all previous mp logs are removed - """ + """Ensure all previous mp logs are removed""" for i, fn in enumerate(self.logfiles): os.remove(fn) def end_pool(self): - """ Close multiprocessing pool - """ + """Close multiprocessing pool""" if self.flag_use_mp: self._pool.close() self._pool.join() @@ -182,14 +188,20 @@ def end_pool(self): def initialize_mp_handler(level, logformat): - """ Wraps the handlers in the given Logger with an MultiProcessingHandler. + """Wraps the handlers in the given Logger with an MultiProcessingHandler. Parameters ---------- level : int - The verbosity level of logging information can be set to any supported representation of the `logging level `_. + The verbosity level of logging information can be set to any supported + representation of the + `logging level `_ + . logformat : str - Formating of logging information can be set to any supported representation of the `formatting class `_. + Formating of logging information can be set to any supported representation of + the + `formatting class `_ + . """ logger = logging.getLogger() diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 361077b..e769894 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -15,6 +15,7 @@ dependencies: - pytest - pytest-cov - codecov + - flake8 # Documentation - sphinx @@ -22,7 +23,7 @@ dependencies: - sphinx-argparse - m2r2 - # Pip-only installs + # Pip-only installs #- pip: - # - codecov + # - flake8 diff --git a/devtools/travis-ci/before_install.sh b/devtools/travis-ci/before_install.sh index 73960a0..1b84f2d 100755 --- a/devtools/travis-ci/before_install.sh +++ b/devtools/travis-ci/before_install.sh @@ -11,7 +11,6 @@ if [ "$TRAVIS_OS_NAME" == "osx" ]; then command md5 -r "$@" } MINICONDA=Miniconda3-latest-MacOSX-x86_64.sh - brew cask install gfortran > /dev/null else MINICONDA=Miniconda3-latest-Linux-x86_64.sh fi diff --git a/docs/.settingup.rst.swo b/docs/.settingup.rst.swo new file mode 100644 index 0000000..1f2ad5d Binary files /dev/null and b/docs/.settingup.rst.swo differ diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 43cb267..5ce903e 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -16,7 +16,7 @@ Install Package - Step 1: Install the prerequisites listed above. - Step 2: Download the master branch from our github page as a zip file, or clone it with git via ``git clone https://github.com/jaclark5/despasito`` to your working directory. - - Step 3: Install with ``pip install despasito/.``, or change directories and run ``pip install .``. + - Step 3: Install with ``pip install despasito/.``, or change directories and run ``pip install -e .``. **NOTE** If `pip `_: is unavailable, follow the instructions outlined `here `_: for installation. diff --git a/docs/requirements b/docs/requirements index affb417..39d3b8f 100644 --- a/docs/requirements +++ b/docs/requirements @@ -1,6 +1,8 @@ numpy scipy +numba matplotlib sphinx-argparse sphinx_rtd_theme m2r2 + diff --git a/docs/settingup.rst b/docs/settingup.rst index 288beb7..4703c8f 100644 --- a/docs/settingup.rst +++ b/docs/settingup.rst @@ -43,8 +43,8 @@ See :ref:`calculation-types` for a list of available types of thermodynamic calc { "CO2": { "epsilon": 207.891, - "l_a": 5.0550, - "l_r": 26.408, + "lambdaa": 5.0550, + "lambdar": 26.408, "sigma": 3.05e-1, "Sk": 0.84680, "Vks": 2, @@ -54,8 +54,8 @@ See :ref:`calculation-types` for a list of available types of thermodynamic calc }, "H2O": { "epsilon": 266.68, - "l_a": 6.0, - "l_r": 17.020, + "lambdaa": 6.0, + "lambdar": 17.020, "sigma": 3.0063e-1, "Sk": 1.0, "Vks": 1, @@ -67,13 +67,14 @@ See :ref:`calculation-types` for a list of available types of thermodynamic calc } } -In the `SAFTgroup.json` file, As many, groups as desired may be defined. Those used in the calculation are specified in the ``bead_configuration`` line. The parameter keys used are defined in the documentation for the chosen EOS object. As an example, the cross-interactions can be defined as follows: +In the `SAFTgroup.json` file, define as many groups as desired. Those used in the calculation are specified in the ``bead_configuration`` line. The parameter keys used are defined in the documentation for the chosen EOS object. As an example, the cross-interactions can be defined as follows: `SAFTcross.json`:: { "CO2": { "H2O": {"epsilon": 226.38, "epsilonHB-H-e1":2200.0, "K-H-e1":91.419e-3} + } } After creating each of these files, go ahead and run the calculation with: diff --git a/docs/startfitting.rst b/docs/startfitting.rst index 348a332..10ef551 100644 --- a/docs/startfitting.rst +++ b/docs/startfitting.rst @@ -27,12 +27,12 @@ Parameters can be fit for one component at a time, and for as many parameters as "epsilon_bounds" : [150.0, 400.0] }, "Wiley": { - "data_class_type": "sat_props", + "data_class_type": "saturation_properties", "calctype": "sat_props", "file": "methanol_saturation.csv" }, "Gibbard": { - "data_class_type": "sat_props", + "data_class_type": "saturation_properties", "file": "methanol_psat.csv", "weights" : { "Psat" : 0.3, diff --git a/paper/Figures/Figure1.png b/paper/Figures/Figure1.png new file mode 100644 index 0000000..96b3891 Binary files /dev/null and b/paper/Figures/Figure1.png differ diff --git a/paper/Figures/Figure2.png b/paper/Figures/Figure2.png new file mode 100644 index 0000000..52def63 Binary files /dev/null and b/paper/Figures/Figure2.png differ diff --git a/paper/paper.bib b/paper/paper.bib new file mode 100644 index 0000000..64d397d --- /dev/null +++ b/paper/paper.bib @@ -0,0 +1,554 @@ + +@book{plunkett_plunketts_2017, + title = {Plunkett's Nanotechnology \& MEMS Industry Almanac 2017}, + isbn = {978-1-62831-439-7}, + language = {English}, + publisher = {PLUNKETT RESEARCH}, + author = {Plunkett, Jack W}, + year = {2017}, +} + +@article{lee_review_2019, + title = {A Review on Recent Progress in the Aspect of Stability of Oxygen Reduction Electrocatalysts for Proton-Exchange Membrane Fuel Cell: Quantum Mechanics and Experimental Approaches}, + language = {en}, + author = {Lee, Jong Min}, + doi = {10.1002/ente.201900312}, + year = {2019}, + volume = {7}, + number = {9}, + pages = {22}, +} + +@article{fermeglia_multiscale_2020, + title = {Multiscale molecular modelling for the design of nanostructured polymer systems: industrial applications}, + volume = {5}, + doi = {10.1039/d0me00109k}, + language = {en}, + number = {9}, + journal = {Molecular Systems Design \& Engineering}, + author = {Fermeglia, Maurizio and Mio, Andrea and Aulic, Suzana and Marson, Domenico and Laurini, Erik and Pricl, Sabrina}, + year = {2020}, + pages = {1447}, +} + +@article{nikkhah_molecular_2021, + title = {Molecular Modelling Guided Modulation of Molecular Shape and Charge for Design of Smart Self-Assembled Polymeric Drug Transporters}, + volume = {13}, + doi={10.3390/pharmaceutics13020141}, + language = {en}, + number = {2}, + journal = {Pharmaceutics}, + author = {Nikkhah, Sousa Javan and Thompson, Damien}, + year = {2021}, + pages = {141}, +} + +@article{miwatani_performance_2020, + title = {Performance of Coarse Graining in Estimating Polymer Properties: Comparison with the Atomistic Model}, + volume = {12}, + doi = {10.3390/polym12020382}, + language = {en}, + number = {2}, + journal = {Polymers}, + author = {Miwatani, Ryota and Takahashi, Kazuaki Z. and Arai, Noriyoshi}, + year = {2020}, + pages = {382}, +} + +@article{lee_multiscale_2009, + title = {Multiscale Modeling of Dendrimers and Their Interactions with Bilayers and Polyelectrolytes}, + volume = {14}, + doi = {10.3390/molecules14010423}, + language = {en}, + journal = {Molecules}, + author = {Lee, Hwankyu and Larson, Ronald}, + year = {2009}, + pages = {423}, +} + +@article{joshi_review_2020, + title = {A Review of Advancements in Coarse-Grained Molecular Dynamics Simulations}, + doi = {10.1080/08927022.2020.1828583}, + volume = {47}, + number = {10-11}, + language = {en}, + journal = {Molecular Simulation}, + author = {Joshi, Soumil Y. and Deshmukh, Sanket A.}, + year = {2020}, + pages = {786}, +} + +@article{muller_force-field_2014, + title = {Force-Field Parameters from the SAFT-$\gamma$ Equation of State for Use in Coarse-Grained Molecular Simulations}, + volume = {5}, + doi = {10.1146/annurev-chembioeng-061312-103314}, + language = {en}, + journal = {Annual Review of Chemical and Biomolecular Engineering}, + author = {Müller, Erich A. and Jackson, George}, + year = {2014}, + pages = {405}, +} + +@article{pervaje_molecular_2019, + title = {Molecular Simulation of Polymers with a SAFT-$\gamma$-Mie Approach}, + volume = {45}, + doi = {10.1080/08927022.2019.1645331}, + language = {en}, + number = {14-15}, + journal = {Molecular Simulation}, + author = {Pervaje, Amulya K. and Walker, Christopher C. and Santiso, Erik E.}, + year = {2019}, + pages = {1223}, +} + +@article{clark_saft-mie_2021, + title = {SAFT-$\gamma$-Mie Cross-Interaction Parameters from Density Functional Theory-Predicted Multipoles of Molecular Fragments for Carbon Dioxide, Benzene, Alkanes, and Water}, + volume = {125}, + doi = {10.1021/acs.jpcb.1c00851}, + language = {en}, + number = {15}, + journal = {Journal of Physical Chemistry B}, + author = {Clark, Jennifer A and Santiso, Erik E}, + year = {2021}, + pages = {3867}, +} + +@article{chapman_saft_1989, + title = {SAFT: Equation-of-State Solution Model for Associating Fluids}, + volume = {52}, + issn = {0378-3812}, + doi = {10.1016/0378-3812(89)80308-5}, + journal = {Fluid Phase Equilibria}, + author = {Chapman, W.G. and Gubbins, K.E. and Jackson, G. and Radosz, M.}, + year = {1989}, + pages = {31}, +} + +@article{chapman_new_1990, + title = {New Reference Equation of State for Associating Liquids}, + volume = {29}, + doi = {10.1021/ie00104a021}, + number = {8}, + journal = {Industrial \& Engineering Chemistry Research}, + author = {Chapman, Walter G. and Gubbins, Keith E. and Jackson, George and Radosz, Maciej}, + year = {1990}, + pages = {1709}, +} + +@article{chapman_phase_1988, + title = {Phase Equilibria of Associating Fluids: Chain Molecules with Multiple Bonding Sites}, + volume = {65}, + doi = {10.1080/00268978800101601}, + language = {en}, + number = {5}, + journal = {Molecular Physics}, + author = {Chapman, Walter G. and Jackson, George and Gubbins, Keith E.}, + year = {1988}, + pages = {1057}, +} + +@article{tan_recent_2008, + title = {Recent Advances and Applications of Statistical Associating Fluid Theory}, + volume = {47}, + doi = {10.1021/ie8008764}, + language = {en}, + number = {21}, + journal = {Industrial \& Engineering Chemistry Research}, + author = {Tan, Sugata P. and Adidharma, Hertanto and Radosz, Maciej}, + year = {2008}, + pages = {8063}, +} + +@article{muller_molecular-based_2001, + title = {Molecular-Based Equations of State for Associating Fluids: A Review of SAFT and Related Approaches}, + volume = {40}, + doi = {10.1021/ie000773w}, + language = {en}, + number = {10}, + journal = {Industrial \& Engineering Chemistry Research}, + author = {Müller, Erich A. and Gubbins, Keith E.}, + year = {2001}, + pages = {2193}, +} + +@article{economou_statistical_2002, + title = {Statistical Associating Fluid Theory: A Successful Model for the Calculation of Thermodynamic and Phase Equilibrium Properties of Complex Fluid Mixtures}, + volume = {41}, + doi = {10.1021/ie0102201}, + language = {en}, + number = {5}, + journal = {Industrial \& Engineering Chemistry Research}, + author = {Economou, Ioannis G.}, + year = {2002}, + pages = {953}, +} + +@article{avendano_saft-_2011, + title = {SAFT-$\gamma$ Force Field for the Simulation of Molecular Fluids. 1. A Single-Site Coarse Grained Model of Carbon Dioxide}, + volume = {115}, + doi = {10.1021/jp204908d}, + language = {en}, + number = {38}, + journal = {The Journal of Physical Chemistry B}, + author = {Avendaño, Carlos and Lafitte, Thomas and Galindo, Amparo and Adjiman, Claire S. and Jackson, George and Müller, Erich A.}, + year = {2011}, + pages = {11154}, +} + +@article{mejia_force_2014, + title = {Force Fields for Coarse-Grained Molecular Simulations from a Corresponding States Correlation}, + volume = {53}, + doi = {10.1021/ie404247e}, + language = {en}, + number = {10}, + journal = {Industrial \& Engineering Chemistry Research}, + author = {Mejía, Andrés and Herdes, Carmelo and Müller, Erich A.}, + year = {2014}, + pages = {4131}, +} + +@article{ervik_bottled_2016, + title = {Bottled SAFT: A Web App Providing SAFT-$\gamma$-Mie Force Field Parameters for Thousands of Molecular Fluids}, + volume = {56}, + doi = {10.1021/acs.jcim.6b00149}, + language = {en}, + number = {9}, + journal = {Journal of Chemical Information and Modeling}, + author = {Ervik, Åsmund and Mejía, Andrés and Müller, Erich A.}, + year = {2016}, + pages = {1609}, +} + +@incollection{papaioannou_simultaneous_2011, + title = {Simultaneous Prediction of Phase Behaviour and Second Derivative Properties with a Group Contribution Approach (SAFT-$\gamma$-Mie)}, + editor = {E.N. Pistikopoulos and M.C. Georgiadis and A.C. Kokossis}, + series = {Computer Aided Chemical Engineering}, + publisher = {Elsevier}, + volume = {29}, + year = {2011}, + booktitle = {21st European Symposium on Computer Aided Process Engineering}, + issn = {1570-7946}, + doi = {10.1016/B978-0-444-54298-4.50097-0}, + language = {en}, + author = {Papaioannou, Vasileios and Lafitte, Thomas and Adjiman, Claire S. and Galindo, Amparo and Jackson, George}, + pages = {1593}, +} + +@article{papaioannou_group_2014, + title = {Group Contribution Methodology Based on the Statistical Associating Fluid Theory for Heteronuclear Molecules Formed from Mie Segments}, + volume = {140}, + doi = {10.1063/1.4851455}, + language = {en}, + number = {5}, + journal = {The Journal of Chemical Physics}, + author = {Papaioannou, Vasileios and Lafitte, Thomas and Avendaño, Carlos and Adjiman, Claire S. and Jackson, George and Müller, Erich A. and Galindo, Amparo}, + year = {2014}, + pages = {054107}, +} + +@article{dufal_prediction_2014, + title = {Prediction of Thermodynamic Properties and Phase Behavior of Fluids and Mixtures with the SAFT-$\gamma$-Mie Group-Contribution Equation of State}, + volume = {59}, + doi = {10.1021/je500248h}, + language = {en}, + number = {10}, + journal = {Journal of Chemical \& Engineering Data}, + author = {Dufal, Simon and Papaioannou, Vasileios and Sadeqzadeh, Majid and Pogiatzis, Thomas and Chremos, Alexandros and Adjiman, Claire S. and Jackson, George and Galindo, Amparo}, + year = {2014}, + pages = {3272}, +} + +@article{cardenas_phase_2016, + title = {Phase Behaviour and Interfacial Properties of Ternary System {CO}$_{\textrm{2}}$ + n-Butane + n-Decane: Coarse-Grained Theoretical Modelling and Molecular Simulations}, + volume = {114}, + doi = {10.1080/00268976.2016.1170221}, + language = {en}, + number = {18}, + journal = {Molecular Physics}, + author = {Cárdenas, Harry and Mejía, Andrés}, + year = {2016}, + pages = {2627}, +} + +@article{galliero_thermodynamic_2007, + title = {Thermodynamic Properties of the Mie n-6 fluid: A Comparison between Statistical Associating Fluid Theory of Variable Range Approach and Molecular Dynamics Results}, + volume = {127}, + doi = {10.1063/1.2801997}, + language = {en}, + number = {18}, + journal = {The Journal of Chemical Physics}, + author = {Galliero, Guillaume and Lafitte, Thomas and Bessieres, David and Boned, Christian}, + year = {2007}, + pages = {184506}, +} + +@article{garrido_interfacial_2016, + title = {Interfacial Tensions of Industrial Fluids from a Molecular-Based Square Gradient Theory}, + volume = {62}, + doi = {10.1002/aic.15190}, + language = {en}, + number = {5}, + journal = {AIChE Journal}, + author = {Garrido, José Matías and Mejía, Andrés and Piñeiro, Manuel M. and Blas, Felipe J. and Müller, Erich A.}, + year = {2016}, + pages = {1781}, +} + +@article{sauer_parametric_2003, + title = {A Parametric Study of Dipolar Chain Theory with Applications to Ketone Mixtures}, + volume = {42}, + doi = {10.1021/ie034035u}, + language = {en}, + number = {22}, + journal = {Industrial \& Engineering Chemistry Research}, + author = {Sauer, Sharon G. and Chapman, Walter G.}, + year = {2003}, + pages = {5687}, +} + +@article{avendano_saft_2013, + title = {SAFT-$\gamma$ Force Field for the Simulation of Molecular Fluids: 2. Coarse-Grained Models of Greenhouse Gases, Refrigerants, and Long Alkanes}, + volume = {117}, + doi = {10.1021/jp306442b}, + language = {en}, + number = {9}, + journal = {The Journal of Physical Chemistry B}, + author = {Avendaño, Carlos and Lafitte, Thomas and Adjiman, Claire S. and Galindo, Amparo and Müller, Erich A. and Jackson, George}, + year = {2013}, + pages = {2717}, +} + +@article{lafitte_saft-_2012, + title = {SAFT-$\gamma$ Force Field for the Simulation of Molecular Fluids: 3. Coarse-Grained Models of Benzene and Hetero-Group Models of n-Decylbenzene}, + volume = {110}, + doi = {10.1080/00268976.2012.662303}, + language = {en}, + number = {11-12}, + journal = {Molecular Physics}, + author = {Lafitte, Thomas and Avendaño, Carlos and Papaioannou, Vasileios and Galindo, Amparo and Adjiman, Claire S. and Jackson, George and Müller, Erich A.}, + year = {2012}, + pages = {1189}, +} + +@article{garrido_interfacial_2016-1, + title = {On Interfacial Properties of Tetrahydrofuran: Atomistic and Coarse-Grained Models from Molecular Dynamics Simulation}, + volume = {144}, + doi = {10.1063/1.4945385}, + language = {en}, + number = {14}, + journal = {The Journal of Chemical Physics}, + author = {Garrido, J. M. and Algaba, J. and Míguez, J. M. and Mendiboure, B. and Moreno-Ventas Bravo, A. I. and Piñeiro, M. M. and Blas, F. J.}, + year = {2016}, + pages = {144702}, +} + +@article{morgado_saft-_2016, + title = {SAFT-$\gamma$ Force Field for the Simulation of Molecular Fluids: 8. Hetero-Segmented Coarse-Grained Models of Perfluoroalkylalkanes Assessed with New Vapour-Liquid Interfacial Tension Data}, + volume = {114}, + doi = {10.1080/00268976.2016.1218077}, + language = {en}, + number = {18}, + journal = {Molecular Physics}, + author = {Morgado, Pedro and Lobanova, Olga and Müller, Erich A. and Jackson, George and Almeida, Miguel and Filipe, Eduardo J. M.}, + year = {2016}, + pages = {2597}, +} + +@article{pervaje_modeling_2018, + title = {Modeling Polymer Glass Transition Properties from Empirical Monomer Data with the SAFT-$\gamma$ Mie Force Field}, + volume = {51}, + issn = {0024-9297, 1520-5835}, + doi = {10.1021/acs.macromol.8b01734}, + language = {en}, + number = {23}, + journal = {Macromolecules}, + author = {Pervaje, Amulya K. and Tilly, Joseph C. and Inglefield, David L. and Spontak, Richard J. and Khan, Saad A. and Santiso, Erik E.}, + year = {2018}, + pages = {9526}, +} + +@article{pervaje_molecular_2020, + title = {Molecular Simulations of Thermoset Polymers Implementing Theoretical Kinetics with Top-Down Coarse-Grained Models}, + volume = {53}, + doi = {10.1021/acs.macromol.9b02255}, + language = {en}, + number = {7}, + journal = {Macromolecules}, + author = {Pervaje, Amulya K. and Tilly, Joseph C. and Detwiler, Andrew T. and Spontak, Richard J. and Khan, Saad A. and Santiso, Erik E.}, + month = apr, + year = {2020}, + pages = {2310}, +} + +@article{walker_development_2019, + title = {Development of a Fused-Sphere SAFT-$\gamma$ Mie Force Field for Poly(Vinyl Alcohol) and Poly(Ethylene)}, + volume = {150}, + doi = {10.1063/1.5078742}, + language = {en}, + number = {3}, + journal = {The Journal of Chemical Physics}, + author = {Walker, Christopher C. and Genzer, Jan and Santiso, Erik E.}, + year = {2019}, + pages = {034901}, +} + +@article{walker_extending_2020, + title = {Extending the Fused-Sphere SAFT-$\gamma$ Mie Force Field Parameterization Approach to Poly(Vinyl Butyral) Copolymers}, + volume = {152}, + doi = {10.1063/1.5126213}, + language = {en}, + number = {4}, + journal = {The Journal of Chemical Physics}, + author = {Walker, Christopher C. and Genzer, Jan and Santiso, Erik E.}, + year = {2020}, + pages = {044903}, +} + +@article{galindo_thermodynamics_1998, + title = {The Thermodynamics of Mixtures and the Corresponding Mixing Rules in the SAFT-VR Approach for Potentials of Variable Range}, + volume = {93}, + doi = {10.1080/002689798169249}, + language = {en}, + number = {2}, + journal = {Molecular Physics}, + author = {Galindo, Amparo and Davies, Lowri A. and Gil-Villegas, Alejandro and Jackson, George}, + year = {1998}, + pages = {241}, +} + +@article{gil-villegas_statistical_1997, + title = {Statistical Associating Fluid Theory for Chain Molecules with Attractive Potentials of Variable Range}, + volume = {106}, + doi = {10.1063/1.473101}, + journal = {The Journal of Chemical Physics}, + author = {Gil-Villegas, Alejandro and Galindo, Amparo and Whitehead, Paul J. and Mills, Stuart J. and Jackson, George and Burgess, Andrew N.}, + year = {1997}, + pages = {4168}, +} + +@article{bell_pure_2014, + title = {Pure and Pseudo-Pure Fluid Thermophysical Property Evaluation and the Open-Source Thermophysical Property Library CoolProp}, + volume = {53}, + doi = {10.1021/ie4033999}, + language = {en}, + number = {6}, + journal = {Ind. Eng. Chem. Res.}, + author = {Bell, Ian H. and Wronski, Jorrit and Quoilin, Sylvain and Lemort, Vincent}, + year = {2014}, + pages = {2498}, +} + +@misc{baird_pcsaft_2021, + title = {pcsaft}, + url = {https://pypi.org/project/pcsaft/1.2.0/}, + author = {Baird, Zach}, + year = {2021}, + note = {https://pypi.org/project/pcsaft/1.2.0/}, +} + +@article{martin_teaching_2011, + title = {Teaching Advanced Equations of State in Applied Thermodynamics Courses Using Open Source Programs}, + volume = {6}, + doi = {10.1016/j.ece.2011.08.003}, + language = {en}, + number = {4}, + journal = {Education for Chemical Engineers}, + author = {Martín, Ángel and Bermejo, María Dolores and Mato, Fidel A. and Cocero, María José}, + year = {2011}, + pages = {e114}, +} + +@misc{clark_despasito_2021, + title = {DESPASITO: Determining Equilibrium State and Parametrization Application for SAFT, Intended for Thermodynamic Output}, + url = {https://github.com/jaclark5/despasito}, + author = {Clark, Jennifer A and Duff, Nathan and Abi-Mansour, Andrew and Santiso, Erik E.}, + year = {2021}, + doi = {10.5281/zenodo.4663126}, +} + +@misc{clark_mapsci_2020, + title = {MAPSCI: Multipole Approach of Predicting and Scaling Cross Interactions}, + shorttitle = {MAPSCI}}, + url = {https://github.com/jaclark5/mapsci}, + author = {Clark, Jennifer A and Santiso, Erik E.}, + year = {2020}, +} + +@article{herdes_coarse_2015, + title = {Coarse Grained Force Field for the Molecular Simulation of Natural Gases and Condensates}, + volume = {406}, + doi = {10.1016/j.fluid.2015.07.014}, + language = {en}, + journal = {Fluid Phase Equilibria}, + author = {Herdes, Carmelo and Totton, Tim S. and Müller, Erich A.}, + year = {2015}, + pages = {91}, +} + +@article{herdes_predicting_2016, + title = {Predicting the Adsorption of n-Perfluorohexane in BAM-P109 Standard Activated Carbon by Molecular Simulation using SAFT-$\gamma$ Mie Coarse-Grained Force Fields}, + volume = {34}, + doi = {10.1177/0263617415619528}, + language = {en}, + journal = {Adsorption Science \& Technology}, + author = {Herdes, Carmelo and Forte, Esther and Jackson, George and Müller, Erich A}, + year = {2016}, + pages = {64}, +} + +@article{mejia_use_2014, + title = {Use of Equations of State and Coarse Grained Simulations to Complement Experiments: Describing the Interfacial Properties of Carbon Dioxide + Decane and Carbon Dioxide + Eicosane Mixtures}, + volume = {59}, + doi = {10.1021/je5000764}, + language = {en}, + number = {10}, + journal = {Journal of Chemical \& Engineering Data}, + author = {Mejía, Andrés and Cartes, Marcela and Segura, Hugo and Müller, Erich A.}, + year = {2014}, + pages = {2928}, +} + +@article{lobanova_saft-_2016, + title = {SAFT-$\gamma$ Force Field for the Simulation of Molecular Fluids 6: Binary and Ternary Mixtures Comprising Water, Carbon Dioxide, and n-Alkanes}, + volume = {93}, + doi = {10.1016/j.jct.2015.10.011}, + language = {en}, + journal = {The Journal of Chemical Thermodynamics}, + author = {Lobanova, Olga and Mejía, Andrés and Jackson, George and Müller, Erich A.}, + year = {2016}, + pages = {320}, +} + +@article{peng_new_1976, + title = {A New Two-Constant Equation of State}, + volume = {15}, + doi = {10.1021/i160057a011}, + language = {en}, + journal = {Industrial & Engineering Chemistry Fundamentals}, + author = {Peng, Ding-Yu and Robinson, Donald B.}, + year = {1976}, + pages = {59}, +} + +@article{shahriari_new_2018, + title = {New Electrolyte SAFT-VR Morse EOS for Prediction of Solid-Liquid Equilibrium in Aqueous Electrolyte Solutions}, + volume = {463}, + doi = {10.1016/j.fluid.2018.02.006}, + language = {en}, + journal = {Fluid Phase Equilibria}, + author = {Shahriari, Reza and Dehghani, Mohammad Reza}, + year = {2018}, + pages = {128}, +} + +@article{dominik_modeling_2005, + title = {Modeling of Polar Systems with the Perturbed-Chain SAFT Equation of State. Investigation of the Performance of Two Polar Terms}, + volume = {44}, + doi = {10.1021/ie050071c}, + language = {en}, + number = {17}, + journal = {Industrial \& Engineering Chemistry Research}, + author = {Dominik, Aleksandra and Chapman, Walter G. and Kleiner, Matthias and Sadowski, Gabriele}, + year = {2005}, + pages = {6928}, +} diff --git a/paper/paper.md b/paper/paper.md new file mode 100644 index 0000000..d4a9e34 --- /dev/null +++ b/paper/paper.md @@ -0,0 +1,107 @@ +--- +title: 'DESPASITO: A Python Package for SAFT EOS Parametrization and Thermodynamic Calculations' +tags: + - Python + - thermodynamics + - equations of state + - saft + - coarse-grain +authors: + - name: Jennifer A. Clark + orcid: 0000-0003-4897-5651 + affiliation: 1 + corresponding: true + - name: Nathan Duff + affiliation: 1 + - name: Andrew Abi-Mansour + affiliation: 2 + - given-names: Erik E. Santiso + orcid: 0000-0003-1768-8414 + corresponding: true + affiliation: 1 +affiliations: + - name: Department of Chemical and Biomolecular Engineering, North Carolina State University, Raleigh, NC, 27695, USA + index: 1 + - name: The Molecular Sciences Software Institute, Blacksburg, VA, 24060, USA + index: 2 +date: 05 May 2024 +bibliography: paper.bib +--- + +# Summary + +DESAPSITO is an open-source Python package that provides access to complex thermodynamic calculations that are not currently freely available. The modular design of DESPASITO provides an extensible architecture that could be easily customized or extended by other researchers. The currently supported equations of state (SAFT-$\gamma$-Mie, SAFT-$\gamma$-SW, and Peng-Robinson) thoroughly represent the dynamic use of this platform and its intentional design for expansion. This growth mindset extends into the other modules, as additional thermodynamic calculations and parameter fitting routines may be included. Nonetheless, the current state of DESPASITO can be employed by the simulation community for coarse-grained forcefield development with SAFT-$\gamma$-Mie. The DESPASITO package is available from PyPi and its source code is hosted on GitHub, which provides a platform for community-driven contributions and feedback. + +# 1. Statement of Need + +To improve on the 20+ years needed between material development and market use,[@plunkett_plunketts_2017] theoretical studies have accelerated material design endeavors.[@lee_review_2019; @fermeglia_multiscale_2020] Of the theoretical methods available, coarse-grained molecular dynamics (CG-MD) simulations have gained attention for their ability to access transport and mechanical properties, as well as phase separations of materials.[@nikkhah_molecular_2021; @miwatani_performance_2020; @lee_multiscale_2009; @joshi_review_2020] CG methods represent an atomistic ensemble as a single “bead” that must sufficiently capture the degrees of freedom of the underlying structure to produce realistic results.[@fermeglia_multiscale_2020] Although CG-MD yields computational efficiency in comparison to all-atom molecular dynamics (AA-MD), generating parameters that are representative of the system’s physics is a continuing struggle. An appealing method of obtaining parameters entails a top-down approach to reproduce some macroscopic property in a simulation. Comparison to macroscopic properties entails an iterative procedure which is time consuming, as many lengthy simulations are needed. + +The SAFT-𝛾-Mie CG methodology has emerged as an appealing CG formalism as the parameters are linked to the SAFT equation of state (EOS).[@muller_force-field_2014; @pervaje_molecular_2019] The Statistical Associating Fluid Theory (SAFT) equation of state (EOS)[@chapman_saft_1989; @chapman_new_1990; @chapman_phase_1988] inherently connects macroscopic properties with the nonbonded interactions of segments through the Helmholtz free energy. Details of theory, applications, and limitations of SAFT variants up to this point have been thoroughly described elsewhere.[@pervaje_molecular_2019; @tan_recent_2008; @muller_molecular-based_2001; @economou_statistical_2002] SAFT has been historically applied to the complex fluids encountered by the oil and gas industry as an EOS, but recently it has received attention from computational scientists and engineers for molecular simulations. SAFT has many advantages due to its deep-rooted basis in statistical mechanics. SAFT-$\gamma$ [@muller_force-field_2014; @avendano_saft-_2011; @mejia_force_2014; @ervik_bottled_2016] offers transferable heteronuclear segments that have been developed into a CG formalism. [@papaioannou_simultaneous_2011; @papaioannou_group_2014; @dufal_prediction_2014] Other homonuclear versions of SAFT have also been applied to molecular simulations.[@cardenas_phase_2016; @galliero_thermodynamic_2007; @garrido_interfacial_2016; @sauer_parametric_2003] Use of SAFT improves CG parametrization, as the parameters fit to an EOS using experimental data can be applied in simulations, avoiding the much more computationally intensive alternative of iterative parametrization simulations with various system conditions. The result is a less computationally intensive methodology yielding chemical specificity. Both SAFT-$\gamma$-Mie [@avendano_saft_2013; @garrido_interfacial_2016-1; @morgado_saft-_2016; @pervaje_modeling_2018; @pervaje_molecular_2020; @walker_development_2019; @walker_extending_2020] and SAFT-$\gamma$-square-well (SW) [@galindo_thermodynamics_1998; @gil-villegas_statistical_1997] have been successfully applied to molecular simulations. + +Due, to its complexity, specialized software is needed to use SAFT either as an EOS or to predict simulation parameters. Bottled SAFT [@ervik_bottled_2016] is a web application meant to remedy issues of parametrization by applying corresponding states for homonuclear molecules. This method relies on possessing the critical properties, acentric factor, and liquid density at 0.7 reduced temperature. Once bead parameters are acquired, whether from the literature or by other means, MD simulations can be carried out. Nevertheless, properties derived from simulations using SAFT are usually accompanied by the EOS prediction, reintroducing the need for specialized software. There are three fundamental requirements for a software that uses SAFT. The first is programming the EOS to generate the Helmholtz free energy. Second, thermodynamic functions must use the EOS to generate system properties. Third, a parametrization package should support both the EOS and thermodynamic functions to facilitate parameter fitting to macroscopic data. Writing this software is not a trivial task and serves as a barrier in development and application of related research. For a specific variant, PC-SAFT, the commercial package, Aspen, handles thermodynamic calculations and parameter estimation. Similarly, the commercial package gSAFT from Process Systems Enterprise, PSE, supports SAFT-$\gamma$-Mie. Another open-source thermodynamic package, CoolProp has been working toward implementing PC-SAFT.[@bell_pure_2014] Additionally, a Python package[@baird_pcsaft_2021] that handles thermodynamic calculations with PC-SAFT was developed in the same time period as this work. This Python package is based on a set of open-source MATLAB scripts[@martin_teaching_2011] that support the original SAFT implementation and PC-SAFT. However, these open-source options do not support parametrization. To produce an open-source software that fulfills the three requirements, this work describes the Python package: Determining Equilibrium State and Parametrization Application for SAFT, Intended for Thermodynamic Output (DESPASITO).[@clark_despasito_2021] + +Herein, we describe DESPASITO, a tool that (1) performs thermodynamic calculations of complex mixtures and (2) fits parameters to experimental data with the potential of accelerating and improving CG simulation research. DESPASITO is capable of reproducing studies published by groups with similar codes.[@muller_force-field_2014; @avendano_saft-_2011; @avendano_saft_2013; @garrido_interfacial_2016-1; @morgado_saft-_2016; @herdes_coarse_2015; @herdes_predicting_2016; @mejia_use_2014; @lobanova_saft-_2016] Although DESPASITO focuses on the SAFT-$\gamma$ variant, it is equipped with general thermodynamic and parameterization modules that any supported EOS can use. In creating this platform to fit parameters for the SAFT EOS, we consequently equipped DESPASITO to handle other equations of state that cannot be solved explicitly. This platform is designed to allow easy and intuitive extension of each module and supported features. It is our purpose to build a base that will supply community accessibility to complex thermodynamic calculations. + +# 2. Implementation and Architecture + +## 2.1 Overview + +The central purpose of DESPASITO is to calculate thermodynamic properties with equations of state (EOS). This package is implemented in the Python 3 programming language with four modules that use a combination of procedural and object-oriented approaches. DESPASITO incorporates the features supplied by the MolSSI Computational Molecular Science Python Cookiecutter (e.g., git, continuous integration, pytest, and readthedocs). The simplest use of DESPASITO involves passing a JSON input file to the command-line interface (CLI), which reads an input file and distributes relevant input data to appropriate modules. An input file may contain instructions for either a thermodynamic or parameter fitting calculation. Alternatively, DESPASITO can be used as a library via its application programming interface (API) for designing complex workflows. In the following sections we will first discuss the CLI, and in the subsequent sections discuss the API as it pertains to the core modules: *Equations of State*, *Thermodynamics*, and *Parameter Fitting*. + +## 2.2 Command-Line Interface + +DESPASITO provides a CLI that is useful when it is executed as a program. This simplifies the user experience as calculations are initiated in an input file. Here we describe some differences between the two interfaces. Once the input file is assembled, a calculation is submitted with: + ``python -m despasito -i input.json`` +The input file uses the JSON format to import the relevant information as a dictionary and then distributes relevant information to the other modules. Upon completing the provided instructions, the results are written to an output file. At a minimum, the input file contains a definition of the molecular components (i.e., bead definitions and quantities) and the bead self- and cross-interaction parameter files in the JSON format, as well the required instructions for either a thermodynamic or parameter fitting calculation. Whether the input file contains instructions for a thermodynamic or parameter fitting calculation is indicated by the presence of the keyword `optimization_parameters`. This section must then be accompanied by dictionaries containing experimental data and thermodynamic calculation requirements for a specific `data_class_type` defined within the dictionary structure. When defining experimental data, a user may list the relevant data in arrays within the input file or include the path to a comma separated file. Rather than further elaborating on the input keyword options or definitions here, the interested user will find the package documentation and tutorials useful. The ability to initiate either a thermodynamic or parameter fitting calculation from a single input file fosters a simple user experience for two complex processes. The logic followed by the CLI is summarized in Figure 1. This diagram shows two workflows that cover typical calculations. However, a user may choose to use DESPASITO as a library to construct a customized workflow or use the module components individually. + + + +*Flowchart of `despasito.main.run`, the function called by the command line interface. This process can distinguish between a thermodynamic and parameter fitting calculation. The colors orange, red, green, and purple, respectively represent the input_output, equations_of_state, thermodynamics, and parameter_fitting modules. Gray color indicates actions taken within `despasito.main.run`.* + +## 2.3 Computations Available in Current Version + +The currently available capabilities in DESPASITO (v0.2.0) can be separated into three categories: *Equations of State*, *Thermodynamics*, and *Parameter Fitting*. Each of these categories is distinguished in this package as a separate module. Here we will summarize the options available to each category (i.e., module), supplemented with knowledge of program architecture where necessary. + +Currently three EOSs are supported in DESPASITO: SAFT- $\gamma$-Mie [@avendano_saft_2013; @garrido_interfacial_2016-1; @morgado_saft-_2016; @pervaje_modeling_2018; @pervaje_molecular_2020; @walker_development_2019; @walker_extending_2020], SAFT- $\gamma$-SW [@galindo_thermodynamics_1998; @gil-villegas_statistical_1997], and Peng-Robinson [@peng_new_1976]. The latter was included as an example of an implementation of a non-SAFT EOS to demonstrate the extensible design in DESPASITO. An EOS object can encapsulate a variety of equations of state, and any compatible EOS object can interact with the *Thermodynamics* and *Parameter Fitting* modules. Details of EOS classes are elaborated on in Section 2.4. Even though DESPASITO is sufficient for CG-MD development with SAFT-𝛾-Mie or SAFT-𝛾-SW, the current EOS options are admittedly limited. Adding another EOS, or family is straightforward in DESPASITO. Further details can be found in the Reuse Policy section or in the documentation. + +DESPASITO provides accessibility to many common thermodynamic calculation types, such as: multicomponent bubble point and dew point, binary flash, Hildebrand solubility, as well as vapor, liquid, and saturation properties. We also included a `verify_eos` function to carry out thermodynamic consistency tests for contributed EOSs. Our EOS classes are equipped to handle any number of components, however, some of the thermodynamic calculations are only robust for binary fluids (i.e., bubble point and dew point). Modeling binary systems with this set of thermodynamic calculations are sufficient for our initial purpose in fitting interaction parameters from experimental data. Second derivative properties are a priority in the next release. + +To fit parameters to experimental data, many options are available to refine the process. A user can customize the parameter fitting routine by defining the objective functional form, choosing a global optimization method and its associated options, refining minimization method and its associated options, and defining nonlinear constraints. The default global optimization method is differential evolution, although many others are available as shown in Table 1. The custom method, `grid_minimization`, is based off the `scipy` optimization function, `brute`. Like `brute`, a grid of parameter values is created, but in this custom function the parameter sets are then changed with a minimization route to fit the experimental data. Some parameters can also be frozen. For example, a grid of two parameters may be defined, while those remaining are minimized. Global optimization of EOS parameters is agnostic to the EOS or thermodynamic data being used. Like the EOS is encapsulated in an object, the thermodynamic calculations are also abstracted from the parameter fitting process with Data classes. The Data class passes updates EOS parameters and returns the objective value used in the fitting procedure. The available data classes supported by DESPASITO, are found in Table 1. Further details on Data classes can be found in the Reuse Policy section or documentation. + +## 2.4 Application Programming Interface + +DESPASITO provides an application programming interface (API) where the capabilities of each of the core modules (i.e., *Equations of State*, *Thermodynamics*, and *Parameter Fitting*) may be accessed through their respective entry-points shown in Table 2. The *Equations of State* module is meant to supply an EOS object with a consistent set of methods exposed for the convenience of the *Thermodynamics* and *Parameter Fitting* modules. Other methods may be included for the internal function of the EOS object or independent use as an imported library. For the *Thermodynamics* module, any supported thermodynamic calculation can be computed through its entry point given the system information and an EOS object. The result of a thermodynamic calculation is a dictionary of both the system inputs and calculated outputs. The entry-point for the *Parameter Fitting* module handles both the interaction between EOS objects and thermodynamic calculations and the interaction between thermodynamic calculations and an objective function used in the parameter fitting method. These interactions are handled with the initiation of a `Data` class for each dataset. However, because each dataset may represent a different molecular composition containing a common target bead, they must have an EOS object initiated beforehand. With these three core functions the major features of DESPASITO can be imported and used as a library. + +Aside from the entry-points for the core modules previously described, the *Input / Output* module provides the functionality of importing and exporting the JSON files handled in by the CLI. Additionally, logging can be enabled to view intermediate values with the function, `despasito.initiate_logger`. Although all functions in DESPASITO are accessible to the user, we expect the functions described in this section to have streamlined the majority of use cases. + +Although the outcome from using module entry-points for the *Thermodynamic* and *Parameter Fitting* modules are comparable to the CLI use of DESPASITO, exposing an EOS object and its available methods can have broader use cases. Figure 2 shows the inheritance structure of EOS classes in DESPASITO using an abstract template class to standardize EOS object methods and attributes. This standardization ensures compatibility with the *Thermodynamics* and *Parameter Fitting* modules. The SAFT EOS object uses another level of organization. As described earlier, SAFT relates nonbonded interactions to macroscopic properties through the Helmholtz free energy. Contributions to the Helmholtz free energy are segmented into terms, often these include the ideal, monomer, chain, and association site contributions. However, different SAFT variants may or may not include other terms (e.g. electrostatic and solvation terms [@shahriari_new_2018] or polar terms [@dominik_modeling_2005]). Often the ideal and association terms are consistent throughout the variants of SAFT and so are grouped in a common SAFT class, while the others are expressed in a `SaftType` class. An EOS object can be initiated, by using DESPASITO as a library, to allow each of these contributions to be handled individually. The *Parameter Fitting* module uses an abstract template class to standardize experimental `Data` classes. The role of Data objects is to handle EOS and thermodynamic calculations and output an objective value representing the ability of a given parameter set to reproduce experimental data. + + + +*UML diagram of *Equation of State* classes used in DESPASITO. Required attributes and methods are shown in abstract class templates. Other methods and attributes are removed for clarity, except in the special case of SAFT. Here we illustrate how the common Helmholtz energy contributions are expressed in a shared class, while the specialized contributions of a SAFT variant are contained in an imported object.* + +## 2.5 Compiled Modules + +Operational speed can be increased with the optional use of compiled code within EOS classes. Apart from the association site contribution of the SAFT EOS, DESPASITO defaults to a pure Python implementation for the supported EOSs. It is heavily recommended that the association term calculation is then computed using `cython` or `numba`. The `method_stat` class is passed to an EOS class to indicate which method should be used. First, the keyword `numba` initiates just-in-time compilation for EOS functions with that supported option. Second, the `cython` flag initiates use of supported EOS functions that were optionally compiled upon installation. Third, the `python` flag signals that only Python should be used, *including* association site calculations, although its use is not recommended except for an exercise in patience. All three of these options have been implemented for the SAFT- 𝛾-Mie EOS. + +## 2.6 Parallelization + +Thermodynamic calculations of the same type can be run in parallel. Python is equipped with a shared memory multiprocessing module that is handled in the utility directory, `despasito.utils`. Here, the `MultiprocessingJob` class handles initiation of a processing pool, calculation dispatch, individual thread logging and consolidation into a job log. The addition of this shared memory utility allows a large set of calculations to run in parallel using high-performance computing (HPC) resources. Both the *Thermodynamics* and *Parameter Fitting* modules are equipped to use this class. If given numerous state conditions at which to perform a thermodynamic calculation (e.g., a list of liquid compositions to perform bubble point calculations), the calculations would be split and reassembled by the multiprocessing pool. A parameter fitting procedure may use the multiprocessing class in one of two ways. First, a single parameter set could be assessed at a time, where the thermodynamic calculations needed to compare to experimental data are split among the processing threads. Although, if a population of parameter sets are to be assessed for every iteration (as in differential evolution), the multiprocessing object may split the evaluation of the parameter sets among the processing threads. If the global algorithm supports workers, this is the default behavior. + +# 3. Additional Features: Prediction of Cross-Interaction Parameters for SAFT-$\gamma$-Mie + +DESPASITO is equipped to predict cross-interaction parameters (between segments of different types) using the open-source package, Multipole Approach of Predicting and Scaling Cross Interactions (MAPSCI) [@clark_mapsci_2020] as a plug-in. As the name implies, MAPSCI uses the multipole moments of molecular fragments in conjunction with their SAFT self-interaction parameters to estimate their cross-interactions. Currently, this package supports SAFT-𝛾-Mie. Use of this plugin reduces the degrees of freedom during parametrization, as SAFT self-interaction parameters are often fit to experimental data, and cross-interaction parameters usually require mixture data. Relying purely on experimental data for cross-interaction parameters is difficult due to the combinatorial explosion in the number of interaction parameters needed for an increasing number of components. MAPSCI is based on a derivation of the combining rules (in this case applied to SAFT-𝛾-Mie) that were extended to utilize multipole moments of molecular fragments.[@clark_saft-mie_2021] Although the multipole moments from small molecules can be found experimentally, the multipole moments of fragments can be derived from density functional theory (DFT).[@clark_saft-mie_2021] The difficulty in coarse-graining systems while retaining chemical specificity is a constant challenge that we seek to remedy. + +# 4. Quality Control + +Initial EOS development and module verification has been achieved through replicating the thermodynamic and parameter fitting results for existing publications. Unit testing is available through `pytest`, where continuous integration of MacOS, Windows, and Linux are handled by GitHub Actions. For each operating system, Python 3.7 and 3.8 are tested. These tests cover over fifty percent of the package according to `codecov`. Much of what is unaccounted for includes exception handling and redundant functions for alternative compilation through Numba and Cython. + +# Acknowledgements + +We thank Jacob Kausler for his aid in logging multiple processes. + +# Funding statement + +Jennifer Clark was supported by a fellowship from The Molecular Sciences Software Institute under NSF grant OAC-1547580. + +# References \ No newline at end of file diff --git a/setup.py b/setup.py index b82226d..6a29ac7 100644 --- a/setup.py +++ b/setup.py @@ -4,28 +4,21 @@ """ import sys import os -from setuptools import find_packages +from setuptools import find_packages, Extension, setup import versioneer -from numpy.distutils.core import Extension, setup -from numpy.distutils.fcompiler import get_default_fcompiler import glob +import numpy as np short_description = __doc__.split("\n") -fpath = os.path.join("despasito", "equations_of_state", "saft", - "compiled_modules") +fpath = os.path.join("despasito", "equations_of_state", "saft", "compiled_modules") extensions = [] -#if sys.version_info.minor > 10: -# raise ValueError( -# "DESPASITO cannot run on python versions greater than 3.8 due to incompatibilities between python 3.9 and numba." -# ) - try: from Cython.Build import cythonize flag_cython = True except Exception: print( - 'Cython not available on your system. Proceeding without C-extensions.' + 'Cython not available on your system. Dependencies will be run with numba.' ) flag_cython = False @@ -33,77 +26,48 @@ cython_list = glob.glob(os.path.join(fpath, "*.pyx")) for cyext in cython_list: name = os.path.split(cyext)[-1].split(".")[-2] - cy_ext_1 = Extension(name=name, sources=[cyext], include_dirs=[fpath]) + cy_ext_1 = Extension( + name=name, + sources=[cyext], + include_dirs=[fpath, np.get_include()], + ) extensions.extend( - cythonize([cy_ext_1], - compiler_directives={ - 'language_level': 3, - 'cdivision': False, - "boundscheck": True - })) + cythonize( + [cy_ext_1], + compiler_directives={ + 'language_level': 3, + 'cdivision': False, + "boundscheck": True + })) # from https://github.com/pytest-dev/pytest-runner#conditional-requirement needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv) pytest_runner = ["pytest-runner"] if needs_pytest else [] -try: - with open("README.md", "r") as handle: - long_description = handle.read() -except Exception: - long_description = "\n".join(short_description[2:]) - -if get_default_fcompiler() != None: - fortran_list = glob.glob(os.path.join(fpath, "*.f90")) - for fext in fortran_list: - name = os.path.split(fext)[-1].split(".")[-2] - ext1 = Extension(name=name, sources=[fext], include_dirs=[fpath]) - extensions.append(ext1) -else: - print("Fortran compiler is not found, default will use numba") - -# try Extension and compile -# !!!! Note that we have Fortran modules that need to be compiled with "f2py3 -m solv_assoc -c solve_assoc.f90" and the same with solve_assoc_matrix.f90 - setup( - # Self-descriptive entries which should always be present name="despasito", author="Jennifer A Clark", author_email="jennifer.clark@gnarlyoak.com", description=short_description[0], - long_description=long_description, long_description_content_type="text/markdown", version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), license="BSD-3-Clause", - # Which Python importable modules should be included when your package is installed - # Handled automatically by setuptools. Use 'exclude' to prevent some specific - # subpackage(s) from being added, if needed packages=find_packages(), - # Optional include package data to ship with your package - # Customize MANIFEST.in if the general case does not suit your needs - # Comment out this line to prevent the files from being packaged with your software include_package_data=True, - # Allows `setup.py test` to work correctly with pytest - setup_requires=["numpy", "scipy"] + pytest_runner, + setup_requires=["numpy", "scipy",] + pytest_runner, ext_package=fpath, ext_modules=extensions, extras_require={ "extra": ["cython"], "tests": ["pytest"], }, - # Additional entries you may want simply uncomment the lines you want and fill in the data - # url='http://www.my_package.com', # Website install_requires=[ "numpy", "scipy", "numba", - ], # Required packages, pulls from pip if needed; do not use for Conda deployment - # platforms=['Linux', - # 'Mac OS-X', - # 'Unix', - # 'Windows'], # Valid platforms your code works on, adjust to your flavor - python_requires=">=3.6, <3.11", # Python version restrictions - - # Manual control if final package is compressible or not, set False to prevent the .egg from being made + "flake8" + ], + python_requires=">=3.6, <3.12", zip_safe=False, )