'''Chemical Engineering Design Library (ChEDL). Utilities for process modeling.
Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021
Caleb Bell <Caleb.Andrew.Bell@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This module contains implementations of most cubic equations of state for
mixtures. This includes Peng-Robinson, SRK, Van der Waals, PRSV, TWU and
many other variants.
For reporting bugs, adding feature requests, or submitting pull requests,
please use the `GitHub issue tracker <https://github.com/CalebBell/thermo/>`_.
.. contents:: :local:
Base Class
==========
.. autoclass:: thermo.eos_mix.GCEOSMIX
:members:
:undoc-members:
:show-inheritance:
:exclude-members: a_alpha_and_derivatives_numpy, a_alpha_and_derivatives_py, main_derivatives_and_departures, derivatives_and_departures,
sequential_substitution_VL, stability_Michelsen, stability_iteration_Michelsen, newton_VL, broyden2_VL,
d2A_dep_dninjs, d2A_dep_dninjs_Vt, d2A_dninjs_Vt, d2A_dninjs_Vt_another, d2P_dninjs_Vt, d2nA_dninjs_Vt, d3P_dninjnks_Vt,
dScomp_dns, d2Scomp_dninjs, dA_dep_dns_Vt, dP_dns_Vt
Peng-Robinson Family EOSs
=========================
Standard Peng Robinson
----------------------
.. autoclass:: thermo.eos_mix.PRMIX
:show-inheritance:
:members: eos_pure, a_alphas_vectorized, a_alpha_and_derivatives_vectorized,
d3a_alpha_dT3, d3a_alpha_dT3_vectorized, fugacity_coefficients,
dlnphis_dT, dlnphis_dP, dlnphis_dzs, ddelta_dzs, ddelta_dns,
d2delta_dzizjs, d2delta_dninjs, d3delta_dninjnks, depsilon_dzs,
depsilon_dns, d2epsilon_dzizjs, d2epsilon_dninjs,
d3epsilon_dninjnks
Peng Robinson (1978)
--------------------
.. autoclass:: thermo.eos_mix.PR78MIX
:show-inheritance:
:members: eos_pure
Peng Robinson Stryjek-Vera
--------------------------
.. autoclass:: thermo.eos_mix.PRSVMIX
:show-inheritance:
:members: eos_pure, a_alphas_vectorized, a_alpha_and_derivatives_vectorized
Peng Robinson Stryjek-Vera 2
----------------------------
.. autoclass:: thermo.eos_mix.PRSV2MIX
:show-inheritance:
:members: eos_pure, a_alphas_vectorized, a_alpha_and_derivatives_vectorized
Peng Robinson Twu (1995)
------------------------
.. autoclass:: thermo.eos_mix.TWUPRMIX
:show-inheritance:
:members: eos_pure
Peng Robinson Translated
------------------------
.. autoclass:: thermo.eos_mix.PRMIXTranslated
:show-inheritance:
:members: eos_pure, ddelta_dzs, d2delta_dzizjs, d3delta_dzizjzks, ddelta_dns,
d2delta_dninjs, d3delta_dninjnks, depsilon_dzs, depsilon_dns,
d2epsilon_dzizjs, d3epsilon_dzizjzks, d2epsilon_dninjs,
d3epsilon_dninjnks
Peng Robinson Translated-Consistent
-----------------------------------
.. autoclass:: thermo.eos_mix.PRMIXTranslatedConsistent
:show-inheritance:
:members: eos_pure
Peng Robinson Translated (Pina-Martinez, Privat, and Jaubert Variant)
---------------------------------------------------------------------
.. autoclass:: thermo.eos_mix.PRMIXTranslatedPPJP
:show-inheritance:
:members: eos_pure
SRK Family EOSs
===============
Standard SRK
------------
.. autoclass:: thermo.eos_mix.SRKMIX
:show-inheritance:
:members: eos_pure, dlnphis_dT, dlnphis_dP, a_alphas_vectorized,
a_alpha_and_derivatives_vectorized, fugacity_coefficients
:exclude-members:
Twu SRK (1995)
--------------
.. autoclass:: thermo.eos_mix.TWUSRKMIX
:show-inheritance:
:members: eos_pure
API SRK
-------
.. autoclass:: thermo.eos_mix.APISRKMIX
:show-inheritance:
:members: eos_pure
SRK Translated
--------------
.. autoclass:: thermo.eos_mix.SRKMIXTranslated
:show-inheritance:
:members: eos_pure, ddelta_dzs, d2delta_dzizjs, d3delta_dzizjzks, ddelta_dns,
d2delta_dninjs, d3delta_dninjnks, depsilon_dzs, depsilon_dns,
d2epsilon_dzizjs, d3epsilon_dzizjzks, d2epsilon_dninjs,
d3epsilon_dninjnks
SRK Translated-Consistent
-------------------------
.. autoclass:: thermo.eos_mix.SRKMIXTranslatedConsistent
:show-inheritance:
:members: eos_pure
MSRK Translated
---------------
.. autoclass:: thermo.eos_mix.MSRKMIXTranslated
:show-inheritance:
:members: eos_pure
Cubic Equation of State with Activity Coefficients
==================================================
.. autoclass:: thermo.eos_mix.PSRK
:show-inheritance:
:members: eos_pure
Van der Waals Equation of State
===============================
.. autoclass:: thermo.eos_mix.VDWMIX
:show-inheritance:
:members: eos_pure, dlnphis_dT, dlnphis_dP, a_alphas_vectorized,
a_alpha_and_derivatives_vectorized, fugacity_coefficients,
ddelta_dzs, ddelta_dns, d2delta_dzizjs, d2delta_dninjs,
d3delta_dninjnks
Redlich-Kwong Equation of State
===============================
.. autoclass:: thermo.eos_mix.RKMIX
:show-inheritance:
:members: eos_pure, a_alphas_vectorized, a_alpha_and_derivatives_vectorized,
ddelta_dzs, ddelta_dns, d2delta_dzizjs, d2delta_dninjs,
d3delta_dninjnks
Ideal Gas Equation of State
===========================
.. autoclass:: thermo.eos_mix.IGMIX
:show-inheritance:
:members: eos_pure, a_alphas_vectorized, a_alpha_and_derivatives_vectorized
Different Mixing Rules
======================
.. autoclass:: thermo.eos_mix.EpsilonZeroMixingRules
.. autoclass:: thermo.eos_mix.PSRKMixingRules
:members: u, A, a_alpha_and_derivatives
:undoc-members:
:show-inheritance:
Lists of Equations of State
===========================
.. autodata:: thermo.eos_mix.eos_mix_list
.. autodata:: thermo.eos_mix.eos_mix_no_coeffs_list
'''
__all__ = ['GCEOSMIX', 'PRMIX', 'SRKMIX', 'PR78MIX', 'VDWMIX', 'PRSVMIX',
'PRSV2MIX', 'TWUPRMIX', 'TWUSRKMIX', 'APISRKMIX', 'IGMIX', 'RKMIX',
'PRMIXTranslatedConsistent', 'PRMIXTranslatedPPJP', 'PRMIXTranslated',
'SRKMIXTranslatedConsistent', 'PSRK', 'MSRKMIXTranslated',
'eos_mix_list', 'eos_mix_no_coeffs_list', 'SRKMIXTranslated', 'one_minus_kijs']
from cmath import log as clog
from chemicals.flash_basic import K_value, Wilson_K_value
from chemicals.rachford_rice import Rachford_Rice_flash_error, flash_inner_loop
from chemicals.utils import d2ns_to_dn2_partials, d2xs_to_dxdn_partials, dns_to_dn_partials, dxs_to_dn_partials, dxs_to_dns, normalize
from fluids.constants import R
from fluids.numerics import UnconvergedError, broyden2, catanh, exp, log, newton_system, solve_2_direct, sqrt, trunc_exp
from fluids.numerics import numpy as np
from fluids.numerics.arrays import det, subset_matrix
from thermo.eos import (
APISRK,
GCEOS,
IG,
PR,
PR78,
PRSV,
PRSV2,
RK,
SRK,
TWUPR,
TWUSRK,
VDW,
MSRKTranslated,
PRTranslated,
PRTranslatedConsistent,
PRTranslatedPPJP,
SRKTranslated,
SRKTranslatedConsistent,
)
from thermo.eos_alpha_functions import (
APISRK_a_alpha_and_derivatives_vectorized,
APISRK_a_alphas_vectorized,
Mathias_Copeman_poly_a_alpha,
PR_a_alpha_and_derivatives_vectorized,
PR_a_alphas_vectorized,
PRSV2_a_alpha_and_derivatives_vectorized,
PRSV2_a_alphas_vectorized,
PRSV_a_alpha_and_derivatives_vectorized,
PRSV_a_alphas_vectorized,
RK_a_alpha_and_derivatives_vectorized,
RK_a_alphas_vectorized,
Soave_1979_a_alpha,
SRK_a_alpha_and_derivatives_vectorized,
SRK_a_alphas_vectorized,
Twu91_a_alpha,
TwuPR95_a_alpha,
TwuSRK95_a_alpha,
)
from thermo.eos_mix_methods import (
G_dep_lnphi_d_helper,
PR_d2delta_dninjs,
PR_d2epsilon_dninjs,
PR_d2epsilon_dzizjs,
PR_d3delta_dninjnks,
PR_d3epsilon_dninjnks,
PR_ddelta_dns,
PR_ddelta_dzs,
PR_depsilon_dns,
PR_depsilon_dzs,
PR_lnphis,
PR_translated_d2delta_dninjs,
PR_translated_d2epsilon_dninjs,
PR_translated_d2epsilon_dzizjs,
PR_translated_d3delta_dninjnks,
PR_translated_d3epsilon_dninjnks,
PR_translated_ddelta_dns,
PR_translated_ddelta_dzs,
PR_translated_depsilon_dns,
PR_translated_depsilon_dzs,
RK_d3delta_dninjnks,
SRK_lnphis,
SRK_translated_d2delta_dninjs,
SRK_translated_d2epsilon_dninjs,
SRK_translated_d2epsilon_dzizjs,
SRK_translated_d3delta_dninjnks,
SRK_translated_d3epsilon_dninjnks,
SRK_translated_ddelta_dns,
SRK_translated_depsilon_dns,
SRK_translated_depsilon_dzs,
VDW_lnphis,
a_alpha_aijs_composition_independent,
a_alpha_and_derivatives_full,
a_alpha_and_derivatives_quadratic_terms,
a_alpha_quadratic_terms,
eos_mix_dV_dzs,
)
from thermo.serialize import JsonOptEncodable
try:
(zeros, array, npexp, npsqrt, empty, full, npwhere, npmin, npmax, ndarray, dot, prodsum) = (
np.zeros, np.array, np.exp, np.sqrt, np.empty, np.full, np.where, np.min, np.max, np.ndarray, np.dot, np.dot)
except:
pass
R2 = R*R
R_inv = 1.0/R
R2_inv = R_inv*R_inv
two_root_two = 2*2**0.5
root_two = sqrt(2.)
root_two_m1 = root_two - 1.0
root_two_p1 = root_two + 1.0
c1R2_PR = PR.c1R2
c2R_PR = PR.c2R
def one_minus_kijs(kijs):
# return 1.0 - kijs # numba: uncomment
if type(kijs) is ndarray: # numba: delete
return 1.0 - kijs # numba: delete
return [[1.0-v for v in row] for row in kijs] # numba: delete
[docs]class GCEOSMIX(GCEOS):
r'''Class for solving a generic pressure-explicit three-parameter cubic
equation of state for a mixture. Does not implement any parameters itself;
must be subclassed by a mixture equation of state class which subclasses it.
.. math::
P=\frac{RT}{V-b}-\frac{a\alpha(T)}{V^2 + \delta V + \epsilon}
'''
nonstate_constants = ('N', 'cmps', 'Tcs', 'Pcs', 'omegas', 'kijs', 'kwargs', 'ais', 'bs')
mix_kwargs_to_pure = {}
kwargs_square = ('kijs',)
"""Tuple of 2D arguments used by the specific EOS.
"""
kwargs_linear = tuple()
"""Tuple of 1D arguments used by the specific EOS in addition to the conventional ones.
"""
multicomponent = True
"""All inherited classes of GCEOSMIX are multicomponent.
"""
vectorized = False
"""Whether the model is implemented using numpy arrays of float64,
or pure-Python lists of floats.
"""
translated = False
"""Whether or not the model implements volume translation.
"""
[docs] def subset(self, idxs, **state_specs):
r'''Method to construct a new :obj:`GCEOSMIX` that removes all components
not specified in the `idxs` argument.
Parameters
----------
idxs : list[int] or Slice
Indexes of components that should be included, [-]
Returns
-------
subset_eos : :obj:`GCEOSMIX`
Multicomponent :obj:`GCEOSMIX` at the same specified specs but with a
composition normalized to 1 and with fewer components, [-]
state_specs : float
Keyword arguments which can be any of `T`, `P`, `V`, `zs`; `zs`
is optional, as are (`T`, `P`, `V`), but if any of (`T`, `P`, `V`)
are specified, a second one is required as well, [various]
Notes
-----
Subclassing equations of state require their :obj:`kwargs_linear <GCEOSMIX.kwargs_linear>` and
:obj:`kwargs_square <GCEOSMIX.kwargs_square>` attributes to be correct for this to work.
`Tcs`, `Pcs`, and `omegas` are always assumed to be used.
Examples
--------
>>> kijs = [[0.0, 0.00076, 0.00171], [0.00076, 0.0, 0.00061], [0.00171, 0.00061, 0.0]]
>>> PR3 = PRMIX(Tcs=[469.7, 507.4, 540.3], zs=[0.8168, 0.1501, 0.0331], omegas=[0.249, 0.305, 0.349], Pcs=[3.369E6, 3.012E6, 2.736E6], T=322.29, P=101325.0, kijs=kijs)
>>> PR3.subset([1,2])
PRMIX(Tcs=[507.4, 540.3], Pcs=[3012000.0, 2736000.0], omegas=[0.305, 0.349], kijs=[[0.0, 0.00061], [0.00061, 0.0]], zs=[0.8193231441048036, 0.1806768558951965], T=322.29, P=101325.0)
>>> PR3.subset([1,2], T=500.0, P=1e5, zs=[.2, .8])
PRMIX(Tcs=[507.4, 540.3], Pcs=[3012000.0, 2736000.0], omegas=[0.305, 0.349], kijs=[[0.0, 0.00061], [0.00061, 0.0]], zs=[0.2, 0.8], T=500.0, P=100000.0)
>>> PR3.subset([1,2], zs=[.2, .8])
PRMIX(Tcs=[507.4, 540.3], Pcs=[3012000.0, 2736000.0], omegas=[0.305, 0.349], kijs=[[0.0, 0.00061], [0.00061, 0.0]], zs=[0.2, 0.8], T=322.29, P=101325.0)
'''
is_slice = isinstance(idxs, slice)
if is_slice:
def atindexes(values):
return values[idxs]
else:
def atindexes(values):
return [values[i] for i in idxs]
if state_specs:
kwargs = state_specs
if len(kwargs) == 1 and 'zs' in kwargs:
kwargs.update(self.state_specs)
else:
kwargs = self.state_specs
if 'zs' not in kwargs:
zs = atindexes(self.zs)
if not zs:
raise ValueError("Cannot create an EOS without any components selected")
zs_tot_inv = 1.0/sum(zs)
for i in range(len(zs)):
zs[i] *= zs_tot_inv
kwargs['zs'] = zs
kwargs['Tcs'] = atindexes(self.Tcs)
kwargs['Pcs'] = atindexes(self.Pcs)
kwargs['omegas'] = atindexes(self.omegas)
local_kwargs = self.kwargs
for k in self.kwargs_linear:
kwargs[k] = atindexes(local_kwargs[k])
for k in self.kwargs_square:
kwargs[k] = subset_matrix(local_kwargs[k], idxs)
return self.__class__(**kwargs)
def __repr__(self):
s = f'{self.__class__.__name__}(Tcs={self.Tcs!r}, Pcs={self.Pcs!r}, omegas={self.omegas!r}, '
for k, v in self.kwargs.items():
s += f'{k}={v!r}, '
s += f'zs={repr(self.zs)}, '
if hasattr(self, 'no_T_spec') and self.no_T_spec:
s += f'P={self.P!r}, V={self.V!r}'
elif self.V is not None:
s += f'T={self.T!r}, V={self.V!r}'
else:
s += f'T={self.T!r}, P={self.P!r}'
s += ')'
return s
def _custom_from_json(self, *args):
eos_name = self.__full_path__
eos = eos_mix_full_path_dict[eos_name]
try:
self.raw_volumes = tuple(self.raw_volumes)
except:
pass
try:
self.alpha_coeffs = [tuple(v) for v in self.alpha_coeffs]
except:
pass
if eos.kwargs_keys:
self.kwargs = {k: getattr(self, k) for k in eos.kwargs_keys}
try:
self.kwargs['alpha_coeffs'] = [tuple(v) for v in self.kwargs['alpha_coeffs']]
except:
pass
try:
self.one_minus_kijs = one_minus_kijs(self.kijs)
except:
pass
[docs] @classmethod
def from_json(cls, json_repr, cache=None):
r'''Method to create a mixture cubic equation of state from a JSON
friendly serialization of another mixture cubic equation of state.
Parameters
----------
json_repr : dict
Json representation, [-]
Returns
-------
eos_mix : :obj:`GCEOSMIX`
Newly created object from the json serialization, [-]
Notes
-----
It is important that the input string be in the same format as that
created by :obj:`GCEOS.as_json`.
Examples
--------
>>> import pickle
>>> eos = PRSV2MIX(Tcs=[507.6], Pcs=[3025000], omegas=[0.2975], zs=[1], T=299., P=1E6, kappa1s=[0.05104], kappa2s=[0.8634], kappa3s=[0.460])
>>> json_stuff = pickle.dumps(eos.as_json())
>>> new_eos = GCEOSMIX.from_json(pickle.loads(json_stuff))
>>> assert new_eos == eos
'''
return JsonOptEncodable.from_json(json_repr, cache)
[docs] def to_TP_zs_fast(self, T, P, zs, only_l=False, only_g=False, full_alphas=True):
r'''Method to construct a new :obj:`GCEOSMIX` instance with the same
parameters as the existing object. If both instances are at the same
temperature, `a_alphas` and `da_alpha_dTs` and `d2a_alpha_dT2s` are
shared between the instances. It is always assumed the new object has
a differet composition. Optionally, only one set of phase properties
can be solved for, increasing speed. Additionally, if `full_alphas`
is set to False no temperature derivatives of `a_alpha` will be
computed. Those derivatives are not needed in the context of a
PT or PVF flash.
Parameters
----------
T : float
Temperature, [K]
P : float
Pressure, [Pa]
zs : list[float]
Mole fractions of each component, [-]
only_l : bool
When true, if there is a liquid and a vapor root, only the liquid
root (and properties) will be set.
only_g : bool
When true, if there is a liquid and a vapor root, only the vapor
root (and properties) will be set.
Returns
-------
eos : :obj:`GCEOSMIX`
Multicomponent :obj:`GCEOSMIX` at the specified conditions [-]
Notes
-----
Examples
--------
>>> base = RKMIX(T=500.0, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> base.to_TP_zs_fast(T=300, P=1e5, zs=base.zs)
RKMIX(Tcs=[126.1, 190.6], Pcs=[3394000.0, 4604000.0], omegas=[0.04, 0.011], kijs=[[0.0, 0.0], [0.0, 0.0]], zs=[0.6, 0.4], T=300, P=100000.0)
'''
new = self.__class__.__new__(self.__class__) # potentially also object.__new__(self.__class__)
new.N, new.Tcs, new.Pcs, new.omegas, new.kijs, new.one_minus_kijs, new.kwargs, new.ais, new.bs, new.vectorized = (
self.N, self.Tcs, self.Pcs, self.omegas, self.kijs, self.one_minus_kijs, self.kwargs, self.ais, self.bs,
self.vectorized
)
# new.N = self.N
# new.Tcs = self.Tcs
# new.Pcs = self.Pcs
# new.omegas = self.omegas
# new.kijs = self.kijs
# new.one_minus_kijs = self.one_minus_kijs
# new.kwargs = self.kwargs
# new.ais = self.ais
# new.bs = self.bs
# new.vectorized = self.vectorized
if T == self.T:
new.a_alphas = self.a_alphas
try:
new.a_alpha_roots = self.a_alpha_roots # has to be first
new.da_alpha_dTs = self.da_alpha_dTs
new.d2a_alpha_dT2s = self.d2a_alpha_dT2s
except:
pass
new.zs, new.T, new.P, new.V = zs, T, P, None
# new.zs = zs
# new.T = T
# new.P = P
# new.V = None
new._fast_init_specific(self)
new.solve(pure_a_alphas=(T != self.T), only_l=only_l,
only_g=only_g, full_alphas=full_alphas)
return new
[docs] def to_TP_zs(self, T, P, zs, fugacities=True, only_l=False, only_g=False):
r'''Method to construct a new :obj:`GCEOSMIX` instance at `T`, `P`, and `zs`
with the same parameters as the existing object. Optionally, only one
set of phase properties can be solved for, increasing speed. The
fugacities calculation can be be skipped by by setting `fugacities` to
False.
Parameters
----------
T : float
Temperature, [K]
P : float
Pressure, [Pa]
zs : list[float]
Mole fractions of each component, [-]
fugacities : bool
Whether or not to calculate and set the fugacities of each
component, [-]
only_l : bool
When true, if there is a liquid and a vapor root, only the liquid
root (and properties) will be set.
only_g : bool
When true, if there is a liquid and a vapor root, only the vapor
root (and properties) will be set.
Returns
-------
eos : :obj:`GCEOSMIX`
Multicomponent :obj:`GCEOSMIX` at the specified conditions [-]
Notes
-----
A check for whether or not `T`, `P`, and `zs` are the same as the
existing instance is performed; if it is, the existing object is
returned.
Examples
--------
>>> base = RKMIX(T=500.0, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> base.to_TP_zs(T=300, P=1e5, zs=[.1, 0.9])
RKMIX(Tcs=[126.1, 190.6], Pcs=[3394000.0, 4604000.0], omegas=[0.04, 0.011], kijs=[[0.0, 0.0], [0.0, 0.0]], zs=[0.1, 0.9], T=300, P=100000.0)
'''
if T != self.T or P != self.P or zs != self.zs:
return self.__class__(T=T, P=P, zs=zs, Tcs=self.Tcs, Pcs=self.Pcs, omegas=self.omegas, only_l=only_l, only_g=only_g, fugacities=fugacities, **self.kwargs)
else:
return self
[docs] def to_PV_zs(self, P, V, zs, fugacities=True, only_l=False, only_g=False):
r'''Method to construct a new :obj:`GCEOSMIX` instance at `P`, `V`, and `zs`
with the same parameters as the existing object. Optionally, only one
set of phase properties can be solved for, increasing speed. The
fugacities calculation can be be skipped by by setting `fugacities` to
False.
Parameters
----------
P : float
Pressure, [Pa]
V : float
Molar volume, [m^3/mol]
zs : list[float]
Mole fractions of each component, [-]
fugacities : bool
Whether or not to calculate and set the fugacities of each
component, [-]
only_l : bool
When true, if there is a liquid and a vapor root, only the liquid
root (and properties) will be set.
only_g : bool
When true, if there is a liquid and a vapor root, only the vapor
root (and properties) will be set.
Returns
-------
eos : :obj:`GCEOSMIX`
Multicomponent :obj:`GCEOSMIX` at the specified conditions [-]
Notes
-----
A check for whether or not `P`, `V`, and `zs` are the same as the
existing instance is performed; if it is, the existing object is
returned.
Examples
--------
>>> base = RKMIX(T=500.0, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> base.to_PV_zs(V=0.004162, P=1e5, zs=[.1, 0.9])
RKMIX(Tcs=[126.1, 190.6], Pcs=[3394000.0, 4604000.0], omegas=[0.04, 0.011], kijs=[[0.0, 0.0], [0.0, 0.0]], zs=[0.1, 0.9], P=100000.0, V=0.004162)
'''
if P == self.P and V == self.V and zs == self.zs:
return self
return self.__class__(P=P, V=V, zs=zs, Tcs=self.Tcs, Pcs=self.Pcs, omegas=self.omegas, only_l=only_l, only_g=only_g, fugacities=fugacities, **self.kwargs)
[docs] def to(self, zs=None, T=None, P=None, V=None, fugacities=True):
r'''Method to construct a new :obj:`GCEOSMIX` object at two of `T`, `P` or `V`
with the specified composition.
In the event the specs match those of the current object, it will be
returned unchanged.
Parameters
----------
zs : list[float], optional
Mole fractions of EOS, [-]
T : float or None, optional
Temperature, [K]
P : float or None, optional
Pressure, [Pa]
V : float or None, optional
Molar volume, [m^3/mol]
fugacities : bool
Whether or not to calculate fugacities, [-]
Returns
-------
obj : :obj:`GCEOSMIX`
Pure component :obj:`GCEOSMIX` at the two specified specs, [-]
Notes
-----
Constructs the object with parameters `Tcs`, `Pcs`, `omegas`, and
`kwargs`.
Examples
--------
>>> base = PRMIX(T=500.0, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> base.to(T=300.0, P=1e9).state_specs
{'T': 300.0, 'P': 1000000000.0}
>>> base.to(T=300.0, V=1.0).state_specs
{'T': 300.0, 'V': 1.0}
>>> base.to(P=1e5, V=1.0).state_specs
{'P': 100000.0, 'V': 1.0}
'''
if zs is None:
zs = self.zs
if T is not None and P is not None:
try:
sln = self.to_TP_zs_fast(T, P, zs)
if fugacities:
sln.fugacities()
return sln
except:
return self.to_TP_zs(T, P, zs, fugacities)
elif T is not None and V is not None:
if T == self.T and V == self.V and zs == self.zs:
return self
return self.__class__(T=T, V=V, zs=zs, Tcs=self.Tcs, Pcs=self.Pcs, omegas=self.omegas, fugacities=fugacities, **self.kwargs)
elif P is not None and V is not None:
return self.to_PV_zs(P, V, zs, fugacities)
else:
return self.__class__(T=T, P=P, V=V, zs=zs, Tcs=self.Tcs, Pcs=self.Pcs, omegas=self.omegas, fugacities=fugacities, **self.kwargs)
[docs] def to_TP(self, T, P):
r'''Method to construct a new :obj:`GCEOSMIX` object at the spcified `T` and `P`
with the current composition. In the event the `T` and `P` match the
current object's `T` and `P`, it will be returned unchanged.
Parameters
----------
T : float
Temperature, [K]
P : float
Pressure, [Pa]
Returns
-------
obj : :obj:`GCEOSMIX`
Pure component :obj:`GCEOSMIX` at specified `T` and `P`, [-]
Notes
-----
Constructs the object with parameters `Tcs`, `Pcs`, `omegas`, and
`kwargs`.
Examples
--------
>>> base = RKMIX(T=500.0, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> new = base.to_TP(T=10.0, P=2000.0)
>>> base.state_specs, new.state_specs
({'T': 500.0, 'P': 1000000.0}, {'T': 10.0, 'P': 2000.0})
'''
return self.to_TP_zs(T, P, zs=self.zs)
[docs] def to_TV(self, T, V):
r'''Method to construct a new :obj:`GCEOSMIX` object at the spcified `T` and `V`
with the current composition. In the event the `T` and `V` match the
current object's `T` and `V`, it will be returned unchanged.
Parameters
----------
T : float
Temperature, [K]
V : float
Molar volume, [m^3/mol]
Returns
-------
obj : :obj:`GCEOSMIX`
Pure component :obj:`GCEOSMIX` at specified `T` and `V`, [-]
Notes
-----
Constructs the object with parameters `Tcs`, `Pcs`, `omegas`, and
`kwargs`.
Examples
--------
>>> base = RKMIX(T=500.0, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> new = base.to_TV(T=1000000.0, V=1.0)
>>> base.state_specs, new.state_specs
({'T': 500.0, 'P': 1000000.0}, {'T': 1000000.0, 'V': 1.0})
'''
if T == self.T and V == self.V:
return self
return self.__class__(T=T, V=V, zs=self.zs, Tcs=self.Tcs, Pcs=self.Pcs, omegas=self.omegas, fugacities=True, **self.kwargs)
[docs] def to_PV(self, P, V):
r'''Method to construct a new :obj:`GCEOSMIX` object at the spcified `P` and `V`
with the current composition. In the event the `P` and `V` match the
current object's `P` and `V`, it will be returned unchanged.
Parameters
----------
P : float
Pressure, [Pa]
V : float
Molar volume, [m^3/mol]
Returns
-------
obj : :obj:`GCEOSMIX`
Pure component :obj:`GCEOSMIX` at specified `P` and `V`, [-]
Notes
-----
Constructs the object with parameters `Tcs`, `Pcs`, `omegas`, and
`kwargs`.
Examples
--------
>>> base = RKMIX(T=500.0, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> new = base.to_PV(P=1000000.0, V=1.0)
>>> base.state_specs, new.state_specs
({'T': 500.0, 'P': 1000000.0}, {'P': 1000000.0, 'V': 1.0})
'''
if V == self.V and P == self.P:
return self
return self.__class__(V=V, P=P, zs=self.zs, Tcs=self.Tcs, Pcs=self.Pcs, omegas=self.omegas, fugacities=True, **self.kwargs)
[docs] def to_mechanical_critical_point(self):
r'''Method to construct a new :obj:`GCEOSMIX` object at the current object's
properties and composition, but which is at the mechanical critical
point.
Returns
-------
obj : :obj:`GCEOSMIX`
Pure component :obj:`GCEOSMIX` at mechanical critical point [-]
Examples
--------
>>> base = RKMIX(T=500.0, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> base.to_mechanical_critical_point()
RKMIX(Tcs=[126.1, 190.6], Pcs=[3394000.0, 4604000.0], omegas=[0.04, 0.011], kijs=[[0.0, 0.0], [0.0, 0.0]], zs=[0.6, 0.4], T=151.861, P=3908737.9)
'''
T, P = self.mechanical_critical_point()
return self.to_TP_zs(T=T, P=P, zs=self.zs)
[docs] def to_TPV_pure(self, i, T=None, P=None, V=None):
r'''Helper method which returns a pure `EOSs` at the specs (two of `T`,
`P` and `V`) and base EOS as the mixture for a particular index.
Parameters
----------
i : int
Index of specified compound, [-]
T : float or None, optional
Specified temperature, [K]
P : float or None, optional
Specified pressure, [Pa]
V : float or None, optional
Specified volume, [m^3/mol]
Returns
-------
eos_pure : eos
A pure-species EOSs at the two specified `T`, `P`, and `V` for
component `i`, [-]
Notes
-----
'''
kwargs = {}
mix_kwargs_to_pure = self.mix_kwargs_to_pure
for k, v in self.kwargs.items():
if k in mix_kwargs_to_pure:
kwargs[mix_kwargs_to_pure[k]] = v[i]
return self.eos_pure(T=T, P=P, V=V, Tc=self.Tcs[i], Pc=self.Pcs[i],
omega=self.omegas[i], **kwargs)
[docs] def pures(self):
r'''Helper method which returns a list of pure `EOSs` at the same `T`
and `P` and base EOS as the mixture.
Returns
-------
eos_pures : list[eos]
A list of pure-species EOSs at the same `T` and `P` as the system,
[-]
Notes
-----
This is useful for i.e. comparing mixture fugacities with the
Lewis-Randall rule or when using an activity coefficient model which
require pure component fugacities.
'''
T, P, N = self.T, self.P, self.N
return [self.to_TPV_pure(T=T, P=P, V=None, i=i) for i in range(N)]
@property
def pseudo_Tc(self):
'''Apply a linear mole-fraction mixing rule to compute the average
critical temperature, [K].
Examples
--------
>>> base = RKMIX(T=150.0, P=4e6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> base.pseudo_Tc
151.9
'''
zs = self.zs
Tcs = self.Tcs
Tc = 0.0
for i in range(self.N):
Tc += zs[i]*Tcs[i]
return Tc
@property
def pseudo_Pc(self):
'''Apply a linear mole-fraction mixing rule to compute the average
critical pressure, [Pa].
Examples
--------
>>> base = RKMIX(T=150.0, P=4e6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> base.pseudo_Pc
3878000.0
'''
zs = self.zs
Pcs = self.Pcs
Pc = 0.0
for i in range(self.N):
Pc += zs[i]*Pcs[i]
return Pc
@property
def pseudo_omega(self):
'''Apply a linear mole-fraction mixing rule to compute the average
`omega`, [-].
Examples
--------
>>> base = RKMIX(T=150.0, P=4e6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> base.pseudo_omega
0.0284
'''
zs = self.zs
omegas = self.omegas
omega = 0.0
for i in range(self.N):
omega += zs[i]*omegas[i]
return omega
@property
def pseudo_a(self):
'''Apply a linear mole-fraction mixing rule to compute the average
`a` coefficient, [-].
Examples
--------
>>> base = RKMIX(T=150.0, P=4e6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.6, 0.4])
>>> base.pseudo_a
0.17634464184
'''
zs = self.zs
ais = self.ais
a = 0.0
for i in range(self.N):
a += zs[i]*ais[i]
return a
[docs] def Psat(self, T, polish=False):
r'''Generic method to calculate vapor pressure of a pure-component
equation of state for a specified `T`. An explicit solution is used
unless `polish` is True.
The result of this function has no physical meaning for multicomponent
mixtures, and does not represent either a dew point or a bubble point!
Parameters
----------
T : float
Temperature, [K]
polish : bool, optional
Whether to attempt to use a numerical solver to make the solution
more precise or not
Returns
-------
Psat : float
Vapor pressure using the pure-component approach, [Pa]
Notes
-----
For multicomponent mixtures this may serve as a useful guess
for the dew and the bubble pressure.
'''
if self.N == 1:
Tc, Pc, omega, a = self.Tcs[0], self.Pcs[0], self.omegas[0], self.ais[0]
else:
zs = self.zs
Tcs, Pcs, omegas, ais = self.Tcs, self.Pcs, self.omegas, self.ais
Tc, Pc, omega, a = 0.0, 0.0, 0.0, 0.0
for i in range(self.N):
Tc += Tcs[i]*zs[i]
Pc += Pcs[i]*zs[i]
omega += omegas[i]*zs[i]
a += ais[i]*zs[i]
self.Tc, self.Pc, self.omega = Tc, Pc, omega
self.a = a
Psat = GCEOS.Psat(self, T, polish=False)
del self.Tc, self.Pc, self.omega
return Psat
[docs] def a_alpha_and_derivatives(self, T, full=True, quick=True,
pure_a_alphas=True):
r'''Method to calculate `a_alpha` and its first and second
derivatives for an EOS with the Van der Waals mixing rules. Uses the
parent class's interface to compute pure component values. Returns
`a_alpha`, `da_alpha_dT`, and `d2a_alpha_dT2`.
For use in :obj:`solve_T <GCEOSMIX.solve_T>` this returns only
`a_alpha` if `full` is False.
.. math::
a \alpha = \sum_i \sum_j z_i z_j {(a\alpha)}_{ij}
.. math::
(a\alpha)_{ij} = (1-k_{ij})\sqrt{(a\alpha)_{i}(a\alpha)_{j}}
Parameters
----------
T : float
Temperature, [K]
full : bool, optional
If False, calculates and returns only `a_alpha`
quick : bool, optional
Only the quick variant is implemented; it is little faster anyhow
pure_a_alphas : bool, optional
Whether or not to recalculate the a_alpha terms of pure components
(for the case of mixtures only) which stay the same as the
composition changes (i.e in a PT flash), [-]
Returns
-------
a_alpha : float
Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
da_alpha_dT : float
Temperature derivative of coefficient calculated by EOS-specific
method, [J^2/mol^2/Pa/K]
d2a_alpha_dT2 : float
Second temperature derivative of coefficient calculated by
EOS-specific method, [J^2/mol^2/Pa/K**2]
Notes
-----
The exact expressions can be obtained with the following SymPy
expression below, commented out for brevity.
>>> from sympy import * # doctest:+SKIP
>>> kij, T = symbols('kij, T ') # doctest:+SKIP
>>> a_alpha_i, a_alpha_j = symbols('a_alpha_i, a_alpha_j', cls=Function) # doctest:+SKIP
>>> a_alpha_ij = (1-kij)*sqrt(a_alpha_i(T)*a_alpha_j(T)) # doctest:+SKIP
>>> diff(a_alpha_ij, T) # doctest:+SKIP
>>> diff(a_alpha_ij, T, T) # doctest:+SKIP
'''
if pure_a_alphas:
if full:
a_alphas, da_alpha_dTs, d2a_alpha_dT2s = self.a_alpha_and_derivatives_vectorized(T)
self.a_alphas, self.da_alpha_dTs, self.d2a_alpha_dT2s = a_alphas, da_alpha_dTs, d2a_alpha_dT2s
else:
self.a_alphas = a_alphas = self.a_alphas_vectorized(T)
da_alpha_dTs = d2a_alpha_dT2s = None
if self.vectorized:
self.a_alpha_roots = npsqrt(a_alphas)
else:
self.a_alpha_roots = [sqrt(i) for i in a_alphas]
else:
try:
a_alphas, da_alpha_dTs, d2a_alpha_dT2s, = self.a_alphas, self.da_alpha_dTs, self.d2a_alpha_dT2s
except:
if full:
a_alphas, da_alpha_dTs, d2a_alpha_dT2s = self.a_alpha_and_derivatives_vectorized(T)
self.a_alphas, self.da_alpha_dTs, self.d2a_alpha_dT2s = a_alphas, da_alpha_dTs, d2a_alpha_dT2s
else:
self.a_alphas = a_alphas = self.a_alphas_vectorized(T)
da_alpha_dTs = d2a_alpha_dT2s = None
zs, one_minus_kijs, vectorized, N, a_alpha_roots = self.zs, self.one_minus_kijs, self.vectorized, self.N, self.a_alpha_roots
if full:
if vectorized:
a_alpha_j_rows, da_alpha_dT_j_rows = zeros(N), zeros(N)
else:
a_alpha_j_rows, da_alpha_dT_j_rows = [0.0]*N, [0.0]*N
a_alpha, da_alpha_dT, d2a_alpha_dT2, self.a_alpha_j_rows, self.da_alpha_dT_j_rows = (
a_alpha_and_derivatives_quadratic_terms(a_alphas, a_alpha_roots, da_alpha_dTs,
d2a_alpha_dT2s, T, zs, one_minus_kijs,
a_alpha_j_rows, da_alpha_dT_j_rows))
return a_alpha, da_alpha_dT, d2a_alpha_dT2
else:
a_alpha_j_rows = zeros(N) if vectorized else [0.0]*N
a_alpha, self.a_alpha_j_rows = a_alpha_quadratic_terms(a_alphas, a_alpha_roots, T, zs, one_minus_kijs, a_alpha_j_rows)
return a_alpha
# Goahead and inline the method
# if not IS_PYPY and self.N > 2000:
# return self.a_alpha_and_derivatives_numpy(a_alphas, da_alpha_dTs, d2a_alpha_dT2s, T, full=full)
# return self.a_alpha_and_derivatives_py(a_alphas, da_alpha_dTs, d2a_alpha_dT2s, T, full)
# def a_alpha_and_derivatives_py(self, a_alphas, da_alpha_dTs, d2a_alpha_dT2s, T, full=True):
# Did not work out a_alpha_aijs_composition_independent does not save any time bad idea
# zs, one_minus_kijs, N = self.zs, self.one_minus_kijs, self.N
# same_T = T == self.T
# try:
# assert same_T
# a_alpha_ijs, a_alpha_roots, a_alpha_ij_roots_inv = self.a_alpha_ijs, self.a_alpha_roots, self.a_alpha_ij_roots_inv
# except (AttributeError, AssertionError):
# if self.vectorized:
# a_alpha_ijs, a_alpha_roots, a_alpha_ij_roots_inv = zeros((N, N)), zeros(N), zeros((N, N))
# else:
# a_alpha_ijs, a_alpha_roots, a_alpha_ij_roots_inv = [[0.0]*N for _ in range(N)], [0.0]*N, [[0.0]*N for _ in range(N)]
# a_alpha_ijs, a_alpha_roots, a_alpha_ij_roots_inv = a_alpha_aijs_composition_independent(a_alphas, one_minus_kijs, a_alpha_ijs=a_alpha_ijs, a_alpha_roots=a_alpha_roots, a_alpha_ij_roots_inv=a_alpha_ij_roots_inv)
# self.a_alpha_ijs, self.a_alpha_roots, self.a_alpha_ij_roots_inv = a_alpha_ijs, a_alpha_roots, a_alpha_ij_roots_inv
# if full:
# try:
# a_alpha, da_alpha_dT, d2a_alpha_dT2, a_alpha_ijs, da_alpha_dT_ijs, d2a_alpha_dT2_ijs = a_alpha_and_derivatives_full(a_alphas, da_alpha_dTs, d2a_alpha_dT2s, T, zs, one_minus_kijs,
# a_alpha_ijs, a_alpha_roots, a_alpha_ij_roots_inv)
# if self.vectorized:
# d2a_alpha_dT2_ijs = array(d2a_alpha_dT2_ijs)
# except:
# if self.N == 1:
# a_alpha, da_alpha_dT, d2a_alpha_dT2 = a_alphas[0], da_alpha_dTs[0], d2a_alpha_dT2s[0]
# d2a_alpha_dT2_ijs, da_alpha_dT_ijs, a_alpha_ijs = [[d2a_alpha_dT2s[0]]], [[da_alpha_dTs[0]]], [[a_alphas[0]]]
# if self.vectorized:
# d2a_alpha_dT2_ijs, da_alpha_dT_ijs, a_alpha_ijs = array(d2a_alpha_dT2_ijs), array(da_alpha_dT_ijs), array(a_alpha_ijs)
# self.d2a_alpha_dT2_ijs, self.da_alpha_dT_ijs, self.a_alpha_ijs = d2a_alpha_dT2_ijs, da_alpha_dT_ijs, a_alpha_ijs
# return float(a_alpha), float(da_alpha_dT), float(d2a_alpha_dT2)
# else:
# a_alpha, _, a_alpha_ijs = a_alpha_and_derivatives(a_alphas, T, zs, one_minus_kijs, a_alpha_ijs, a_alpha_roots, a_alpha_ij_roots_inv)
# self.da_alpha_dT_ijs = None
# self.a_alpha_ijs = a_alpha_ijs
# return float(a_alpha)
def a_alpha_and_derivatives_py(self, a_alphas, da_alpha_dTs, d2a_alpha_dT2s, T, full=True):
# For 44 components, takes 150 us in PyPy.; 95 in pythran. Much of that is type conversions.
# 4 ms pypy for 44*4, 1.3 ms for pythran, 10 ms python with numpy
# 2 components 1.89 pypy, pythran 1.75 us, regular python 12.7 us.
# 10 components - regular python 148 us, 9.81 us PyPy, 8.37 pythran in PyPy (flags have no effect; 14.3 us in regular python)
zs, one_minus_kijs, vectorized, N, a_alpha_roots = self.zs, self.one_minus_kijs, self.vectorized, self.N, self.a_alpha_roots
if full:
if vectorized:
a_alpha_j_rows, da_alpha_dT_j_rows = zeros(N), zeros(N)
else:
a_alpha_j_rows, da_alpha_dT_j_rows = [0.0]*N, [0.0]*N
a_alpha, da_alpha_dT, d2a_alpha_dT2, self.a_alpha_j_rows, self.da_alpha_dT_j_rows = (
a_alpha_and_derivatives_quadratic_terms(a_alphas, a_alpha_roots, da_alpha_dTs,
d2a_alpha_dT2s, T, zs, one_minus_kijs,
a_alpha_j_rows=a_alpha_j_rows, da_alpha_dT_j_rows=da_alpha_dT_j_rows))
return a_alpha, da_alpha_dT, d2a_alpha_dT2
else:
a_alpha_j_rows = zeros(N) if vectorized else [0.0]*N
a_alpha, self.a_alpha_j_rows = a_alpha_quadratic_terms(a_alphas, a_alpha_roots, T, zs, one_minus_kijs, a_alpha_j_rows=a_alpha_j_rows)
return a_alpha
def a_alpha_and_derivatives_numpy(self, a_alphas, da_alpha_dTs, d2a_alpha_dT2s, T, full=True):
zs, kijs = self.zs, np.array(self.kijs)
a_alphas = np.array(a_alphas)
da_alpha_dTs = np.array(da_alpha_dTs)
one_minus_kijs = 1.0 - kijs
x0 = np.einsum('i,j', a_alphas, a_alphas)
x0_05 = npsqrt(x0)
a_alpha_ijs = (one_minus_kijs)*x0_05
z_products = np.einsum('i,j', zs, zs)
a_alpha = np.einsum('ij,ji', a_alpha_ijs, z_products)
if self.vectorized:
self.a_alpha_ijs = a_alpha_ijs
else:
self.a_alpha_ijs = a_alpha_ijs.tolist()
if full:
term0 = np.einsum('j,i', a_alphas, da_alpha_dTs)
term7 = (one_minus_kijs)/(x0_05)
da_alpha_dT = (z_products*term7*(term0)).sum()
term1 = -x0_05/x0*(one_minus_kijs)
term2 = np.einsum('i, j', a_alphas, da_alpha_dTs)
main3 = da_alpha_dTs/(2.0*a_alphas)*term2
main4 = -np.einsum('i, j', a_alphas, d2a_alpha_dT2s)
main6 = -0.5*np.einsum('i, j', da_alpha_dTs, da_alpha_dTs)
# Needed for fugacity temperature derivative
self.da_alpha_dT_ijs = (0.5*(term7)*(term2 + term0)).tolist()
d2a_alpha_dT2 = (z_products*(term1*(main3 + main4 + main6))).sum()
return float(a_alpha), float(da_alpha_dT), float(d2a_alpha_dT2)
else:
return float(a_alpha)
def _spinodal_f(self, TPV):
# TODO - use `self`, do not create new instance
# Work to do - ethane', 'heptane
# Specify V, solve P; increase V and keep going
# After Effective utilization of equations of state for thermodynamic properties in process simulation
'''eos = PRMIX(P=6e6, T=500, Tcs=[305.32, 540.2], Pcs=[4872000.0, 2740000.0], omegas=[0.098, 0.3457], zs=[.5, .5])
def to_solve(T):
return eos.to(T=T, P=eos.P, zs=eos.zs)._spinodal_f([T, eos.P])
# Very well could be right
eos.to(T=secant(to_solve, eos.T), P=eos.P, zs=eos.zs).rho_l # 3004.715984610371
'''
T, P, V = TPV
eos_instance = self.to(T=T, P=P, V=V, zs=self.zs)
RT_inv = 1.0/(R*eos_instance.T)
if eos_instance.phase == 'l/g':
if eos_instance.G_dep_l < eos_instance.G_dep_g:
v = eos_instance.d2nA_dninjs_Vt('l')
else:
v = eos_instance.d2nA_dninjs_Vt('g')
elif eos_instance.phase == 'g':
v = eos_instance.d2nA_dninjs_Vt('g')
else:
v = eos_instance.d2nA_dninjs_Vt('l')
dGs = [[i*RT_inv for i in row] for row in v]
return det(dGs)
def _spinodal_at(self, T=None, P=None, V=None):
# TODO finish
if T is not None:
def to_solve(V):
return self._spinodal_f((T, None, V))
if 1:
from fluids.numerics import linspace
Vs = linspace(self.b*(1+1e-7), self.b*1000, 1000)
errs = []
for Vi in Vs:
try:
errs.append(abs(to_solve(Vi)))
except:
errs.append(1e5)
import matplotlib.pyplot as plt
plt.semilogy(Vs, errs)
plt.show()
a = 1
elif P is not None:
def to_solve(V):
return self._spinodal_f((None, P, V))
elif V is not None:
def to_solve(T):
return self._spinodal_f((T, None, V))
def _mechanical_critical_point_f_jac(self, TP):
'''The criteria for c_goal and d_goal come from a cubic
'roots_cubic', which uses a `f`, `g`, and `h` parameter. When all of
them are zero, all three roots are equal. For the eos (a=1), this
results in the following system of equations:
from sympy import *
a = 1
b, c, d = symbols('b, c, d')
f = ((3* c / a) - ((b ** 2) / (a ** 2))) / 3
g = (((2 * (b ** 3)) / (a ** 3)) - ((9* b * c) / (a **2)) + (27 * d / a)) /27
h = ((g ** 2) / 4 + (f ** 3) / 27)z
solve([Eq(f, 0), Eq(g, 0), Eq(h, 0)], [b, c, d])
The solution (sympy struggled) is:
c = b^2/3
d = b^3/27
These two variables switch sign at the criteria, so they work well with
a root finding approach.
Derived with:
from sympy import *
P, T, V, R, b_eos, alpha = symbols('P, T, V, R, b_eos, alpha')
Tc, Pc, omega = symbols('Tc, Pc, omega')
delta, epsilon = symbols('delta, epsilon')
a_alpha = alpha(T)
eta = b_eos
B = b_eos*P/(R*T)
deltas = delta*P/(R*T)
thetas = a_alpha*P/(R*T)**2
epsilons = epsilon*(P/(R*T))**2
etas = eta*P/(R*T)
b = (deltas - B - 1)
c = (thetas + epsilons - deltas*(B + 1))
d = -(epsilons*(B + 1) + thetas*etas)
c_goal = b*b/3
d_goal = b*b*b/27
F1 = c - c_goal
F2 = d - d_goal
cse([F1, F2, diff(F1, T), diff(F1, P), diff(F2, T), diff(F2, P)], optimizations='basic')
Performance analysis:
77% of this is getting a_alpha and da_alpha_dT.
71% of the outer solver is getting f and this Jacobian.
Limited results from optimizing the below code, which was derived with
sympy.
'''
T, P = float(TP[0]), float(TP[1])
b_eos, delta, epsilon = self.b, self.delta, self.epsilon
eta = b_eos
try:
del self.a_alpha_ijs
del self.a_alpha_roots
del self.a_alpha_ij_roots_inv
except:
pass
a_alpha, da_alpha_dT, _ = self.a_alpha_and_derivatives(T, full=True)
x6 = R_inv
x7 = 1.0/T
x0 = a_alpha
x1 = R_inv*R_inv
x2 = x7*x7
x3 = x1*x2
x4 = P*P
x5 = epsilon*x3*x4
x8 = P*x6*x7
x9 = delta*x8
x10 = b_eos*x8
x11 = x10 + 1.0
x12 = x11 - x9
x13 = x12*x12
x14 = P*x2*x6
x15 = da_alpha_dT
x16 = x6*x7
x17 = x0*x16
x18 = 2.0*epsilon*x8
x19 = delta*x10
x20 = delta*x11
x21 = b_eos - delta
x22 = 2.0/3.0*x12*x21
x23 = P*b_eos*x0*x1*x2
x24 = b_eos*x5
x25 = x11*x18
x26 = x13*x21/9.0
F1 = P*x0*x3 - x11*x9 - x13*(1/3.0) + x5
F2 = -x11*x5 + x13*x12*(1/27.0) - b_eos*x0*x4*x6*x1*x7*x2
dF1_dT = x14*(x15*x6 - 2.0*x17 - x18 + x19 + x20 + x22)
dF1_dP = x16*(x17 + x18 - x19 - x20 - x22)
dF2_dT = x14*(-P*b_eos*x1*x15*x7 + 3.0*x23 + x24 + x25 - x26)
dF2_dP = x16*(-2.0*x23 - x24 - x25 + x26)
return [F1, F2], [[dF1_dT, dF1_dP], [dF2_dT, dF2_dP]]
[docs] def mechanical_critical_point(self):
r'''Method to calculate the mechanical critical point of a mixture
of defined composition.
The mechanical critical point is where:
.. math::
\frac{\partial P}{\partial \rho}|_T =
\frac{\partial^2 P}{\partial \rho^2}|_T = 0
Returns
-------
T : float
Mechanical critical temperature, [K]
P : float
Mechanical critical temperature, [Pa]
Notes
-----
One useful application of the mechanical critical temperature is that
the phase identification approach of Venkatarathnam is valid only up to
it.
Note that the equation of state, when solved at these conditions, will
have fairly large (1e-3 - 1e-6) results for the derivatives; but they
are the minimum. This is just from floating point precision.
It can also be checked looking at the calculated molar volumes - all
three (available with :obj:`sorted_volumes <GCEOSMIX.sorted_volumes>`) will be very close (1e-5
difference in practice), again differing because of floating point
error.
The algorithm here is a custom implementation, using Newton-Raphson's
method with the initial guesses described in [1] (mole-weighted
critical pressure average, critical temperature average using a
quadratic mixing rule). Normally ~4 iterations are needed to solve the
system. It is relatively fast, as only one evaluation of `a_alpha`
and `da_alpha_dT` are needed per call to function and its jacobian.
References
----------
.. [1] Watson, Harry A. J., and Paul I. Barton. "Reliable Flash
Calculations: Part 3. A Nonsmooth Approach to Density Extrapolation
and Pseudoproperty Evaluation." Industrial & Engineering Chemistry
Research, November 11, 2017.
https://doi.org/10.1021/acs.iecr.7b03233.
.. [2] Mathias P. M., Boston J. F., and Watanasiri S. "Effective
Utilization of Equations of State for Thermodynamic Properties in
Process Simulation." AIChE Journal 30, no. 2 (June 17, 2004):
182-86. https://doi.org/10.1002/aic.690300203.
'''
zs, Tcs, Pcs, N = self.zs, self.Tcs, self.Pcs, self.N
Pmc = sum([Pcs[i]*zs[i] for i in range(N)])
Tmc = sum([sqrt(Tcs[i]*Tcs[j])*zs[j]*zs[i] for i in range(N)
for j in range(N)])
TP, iterations = newton_system(self._mechanical_critical_point_f_jac,
x0=[Tmc, Pmc], jac=True, ytol=1e-10,
xtol=1e-12,
solve_func=solve_2_direct)
T, P = float(TP[0]), float(TP[1])
return T, P
[docs] def fugacities(self, only_l=False, only_g=False):
r'''Helper method for calculating fugacity coefficients for any
phases present, using either the overall mole fractions for both phases
or using specified mole fractions for each phase.
Requires :obj:`fugacity_coefficients <GCEOSMIX.fugacity_coefficients>` to be implemented by each subclassing
EOS.
In addition to setting `fugacities_l` and/or `fugacities_g`, this also
sets the fugacity coefficients `phis_l` and/or `phis_g`.
.. math::
\hat \phi_i^g = \frac{\hat f_i^g}{y_i P}
.. math::
\hat \phi_i^l = \frac{\hat f_i^l}{x_i P}
Note that in a flash calculation, each phase requires their own EOS
object.
Parameters
----------
only_l : bool
When true, if there is a liquid and a vapor root, only the liquid
root (and properties) will be set.
only_g : bool
When true, if there is a liquid and a vapor root, only the vapor
root (and properties) will be set.
Notes
-----
It is helpful to check that :obj:`fugacity_coefficients <GCEOSMIX.fugacity_coefficients>` has been
implemented correctly using the following expression, from [1]_.
.. math::
\ln \hat \phi_i = \left[\frac{\partial (n\ln \phi)}{\partial
n_i}\right]_{T,P,n_j,V_t}
For reference, several expressions for fugacity of a component are as
follows, shown in [1]_ and [2]_.
.. math::
\ln \hat \phi_i = \int_{0}^P\left(\frac{\hat V_i}
{RT} - \frac{1}{P}\right)dP
.. math::
\ln \hat \phi_i = \int_V^\infty \left[
\frac{1}{RT}\frac{\partial P}{ \partial n_i}
- \frac{1}{V}\right] d V - \ln Z
References
----------
.. [1] Hu, Jiawen, Rong Wang, and Shide Mao. "Some Useful Expressions
for Deriving Component Fugacity Coefficients from Mixture Fugacity
Coefficient." Fluid Phase Equilibria 268, no. 1-2 (June 25, 2008):
7-13. doi:10.1016/j.fluid.2008.03.007.
.. [2] Walas, Stanley M. Phase Equilibria in Chemical Engineering.
Butterworth-Heinemann, 1985.
'''
P, zs, vectorized = self.P, self.zs, self.vectorized
if not only_g and hasattr(self, 'V_l'):
self.lnphis_l = lnphis_l = self.fugacity_coefficients(self.Z_l)
if vectorized:
self.phis_l = phis_l = npexp(lnphis_l)
self.fugacities_l = zs*P*phis_l
else:
try:
self.phis_l = [exp(i) for i in lnphis_l]
except:
self.phis_l = [trunc_exp(i, trunc=1e308) for i in lnphis_l]
self.fugacities_l = [phi*x*P for phi, x in zip(self.phis_l, zs)]
if not only_l and hasattr(self, 'V_g'):
self.lnphis_g = lnphis_g = self.fugacity_coefficients(self.Z_g)
if vectorized:
self.phis_g = phis_g = npexp(lnphis_g)
self.fugacities_g = zs*P*phis_g
else:
try:
self.phis_g = phis_g = [exp(i) for i in lnphis_g]
except:
self.phis_g = phis_g = [trunc_exp(i, trunc=1e308) for i in lnphis_g]
self.fugacities_g = [phi*y*P for phi, y in zip(phis_g, zs)]
def _eos_lnphis_lowest_Gibbs(self):
try:
try:
if self.G_dep_l < self.G_dep_g:
return self.lnphis_l, 'l'
else:
return self.lnphis_g, 'g'
except:
# Only one root - take it and set the prefered other phase to be a different type
return (self.lnphis_g, 'g') if hasattr(self, 'Z_g') else (self.lnphis_l, 'l')
except:
self.fugacities()
return self._eos_fugacities_lowest_Gibbs()
def _eos_fugacities_lowest_Gibbs(self):
# TODO delete with property_package.py
try:
try:
if self.G_dep_l < self.G_dep_g:
return self.fugacities_l, 'l'
else:
return self.fugacities_g, 'g'
except:
# Only one root - take it and set the prefered other phase to be a different type
return (self.fugacities_g, 'g') if hasattr(self, 'Z_g') else (self.fugacities_l, 'l')
except:
self.fugacities()
return self._eos_fugacities_lowest_Gibbs()
def _dphi_dn(self, zi, i, phase):
# obsolete, should be deleted
z_copy = list(self.zs)
z_copy.pop(i)
z_sum = sum(z_copy) + zi
z_copy = [j/z_sum if j else 0 for j in z_copy]
z_copy.insert(i, zi)
eos = self.to_TP_zs(self.T, self.P, z_copy)
if phase == 'g':
return eos.phis_g[i]
elif phase == 'l':
return eos.phis_l[i]
def _dfugacity_dn(self, zi, i, phase):
# obsolete, should be deleted
z_copy = list(self.zs)
z_copy.pop(i)
z_sum = sum(z_copy) + zi
z_copy = [j/z_sum if j else 0 for j in z_copy]
z_copy.insert(i, zi)
eos = self.to_TP_zs(self.T, self.P, z_copy)
if phase == 'g':
return eos.fugacities_g[i]
elif phase == 'l':
return eos.fugacities_l[i]
def _Stateva_Tsvetkov_TPDF_broken(self, Zz, Zy, zs, ys):
# TODO: delete
z_log_fugacity_coefficients = self.fugacity_coefficients(Zz)
y_log_fugacity_coefficients = self.fugacity_coefficients(Zy)
kis = []
for yi, phi_yi, zi, phi_zi in zip(ys, y_log_fugacity_coefficients, zs, z_log_fugacity_coefficients):
di = log(zi) + phi_zi
try:
ki = phi_yi + log(yi) - di
except ValueError:
ki = phi_yi + log(1e-200) - di
kis.append(ki)
kis.append(kis[0])
tot = 0.0
for i in range(self.N):
t = kis[i+1] - kis[i]
tot += t*t
return tot
def _d_TPD_Michelson_modified(self, Zz, Zy, zs, alphas):
r'''Modified objective function for locating the minima of the
Tangent Plane Distance function according to [1]_, also shown in [2]_
[2]_. The stationary points of a system are all zeros of this function;
so once all zeroes have been located, the stability can be evaluated
at the stationary points only. It may be required to use multiple
guesses to find all stationary points, and there is no method of
confirming all points have been found.
This method does not alter the state of the object.
.. math::
\frac{\partial \; TPD^*}{\partial \alpha_i} = \sqrt{Y_i} \left[
\ln \phi_i(Y) + \ln(Y_i) - h_i\right]
.. math::
\alpha_i = 2 \sqrt{Y_i}
.. math::
d_i(z) = \ln z_i + \ln \phi_i(z)
Parameters
----------
Zz : float
Compressibility factor of the phase undergoing stability testing,
(`test` phase), [-]
Zy : float
Compressibility factor of the trial phase, [-]
zs : list[float]
Mole fraction composition of the phase undergoing stability
testing (`test` phase), [-]
alphas : list[float]
Twice the square root of the mole numbers of each component,
[mol^0.5]
Returns
-------
err : float
Error in solving for stationary points according to the modified
TPD method in [1]_, [-]
Notes
-----
This method is particularly useful because it is not a constrained
objective function. This has been verified to return the same roots as
other stationary point methods.
References
----------
.. [1] Michelsen, Michael L. "The Isothermal Flash Problem. Part I.
Stability." Fluid Phase Equilibria 9, no. 1 (December 1982): 1-19.
.. [2] Qiu, Lu, Yue Wang, Qi Jiao, Hu Wang, and Rolf D. Reitz.
"Development of a Thermodynamically Consistent, Robust and Efficient
Phase Equilibrium Solver and Its Validations." Fuel 115 (January 1,
2014): 1-16
'''
# TODO: delete
Ys = [(alpha/2.)**2 for alpha in alphas]
ys = normalize(Ys)
z_log_fugacity_coefficients = self.fugacity_coefficients(Zz)
y_log_fugacity_coefficients = self.fugacity_coefficients(Zy)
tot = 0
for Yi, phi_yi, zi, phi_zi in zip(Ys, y_log_fugacity_coefficients, zs, z_log_fugacity_coefficients):
di = log(zi) + phi_zi
if Yi != 0:
diff = Yi**0.5*(log(Yi) + phi_yi - di)
tot += abs(diff)
return tot
# def TDP_Michelsen(self, phase):
#
# z_log_fugacity_coefficients = self.fugacity_coefficients(Zz, zs)
# y_log_fugacity_coefficients = self.fugacity_coefficients(Zy, ys)
# tot = 0
# for yi, phi_yi, zi, phi_zi in zip(ys, y_log_fugacity_coefficients, zs, z_log_fugacity_coefficients):
# hi = di = log(zi) + phi_zi # same as di
#
# k = log(yi) + phi_yi - hi
# # Michaelsum doesn't do the exponents.
# Yi = exp(-k)*yi
# tot += Yi*(log(Yi) + phi_yi - hi - 1.)
#
# return 1. + tot
# def TDP_Michelsen_modified(self, Zz, Zy, zs, Ys):
# # https://www.e-education.psu.edu/png520/m17_p7.html
# # Might as well continue
# Ys = [abs(float(Yi)) for Yi in Ys]
# # Ys only need to be positive
# ys = normalize(Ys)
#
# z_log_fugacity_coefficients = self.fugacity_coefficients(Zz, zs)
# y_log_fugacity_coefficients = self.fugacity_coefficients(Zy, ys)
#
# tot = 0
# for Yi, phi_yi, yi, zi, phi_zi in zip(Ys, y_log_fugacity_coefficients, ys, zs, z_log_fugacity_coefficients):
# hi = di = log(zi) + phi_zi # same as di
# tot += Yi*(log(Yi) + phi_yi - di - 1.)
# return (1. + tot)
# # Another formulation, returns the same answers.
## tot += yi*(log(sum(Ys)) +log(yi)+ log(phi_yi) - di - 1.)
## return (1. + sum(Ys)*tot)*1e15
[docs] def solve_T(self, P, V, quick=True, solution=None):
r'''Generic method to calculate `T` from a specified `P` and `V`.
Provides SciPy's `newton` solver, and iterates to solve the general
equation for `P`, recalculating `a_alpha` as a function of temperature
using :obj:`a_alpha_and_derivatives <GCEOSMIX.a_alpha_and_derivatives>` each iteration.
Parameters
----------
P : float
Pressure, [Pa]
V : float
Molar volume, [m^3/mol]
quick : bool, optional
Unimplemented, although it may be possible to derive explicit
expressions as done for many pure-component EOS
solution : str or None, optional
'l' or 'g' to specify a liquid of vapor solution (if one exists);
if None, will select a solution more likely to be real (closer to
STP, attempting to avoid temperatures like 60000 K or 0.0001 K).
Returns
-------
T : float
Temperature, [K]
'''
# -4 goes back from object, GCEOS
return super(type(self).__mro__[-3], self).solve_T(P=P, V=V, solution=solution)
def _err_VL_jacobian(self, lnKsVF, T, P, zs, near_critical=False,
err_also=False, info=None):
if info is None:
info = []
N = self.N
lnKs = lnKsVF[:-1]
Ks = [exp(lnKi) for lnKi in lnKs]
VF = float(lnKsVF[-1])
xs = [zi/(1.0 + VF*(Ki - 1.0)) for zi, Ki in zip(zs, Ks)]
ys = [Ki*xi for Ki, xi in zip(Ks, xs)]
eos_g = self.to_TP_zs_fast(T=T, P=P, zs=ys, only_g=True)
eos_l = self.to_TP_zs_fast(T=T, P=P, zs=xs, only_l=True)
# eos_g = self.to_TP_zs(T=T, P=P, zs=ys)
# eos_l = self.to_TP_zs(T=T, P=P, zs=xs)
if not near_critical:
# lnphis_g = eos_g.lnphis_g
# lnphis_l = eos_l.lnphis_l
Z_g = eos_g.Z_g
Z_l = eos_l.Z_l
else:
try:
# lnphis_g = eos_g.lnphis_g
Z_g = eos_g.Z_g
except AttributeError:
# lnphis_g = eos_g.lnphis_l
Z_g = eos_g.Z_l
try:
# lnphis_l = eos_l.lnphis_l
Z_l = eos_l.Z_l
except AttributeError:
# lnphis_l = eos_l.lnphis_g
Z_l = eos_l.Z_g
lnphis_g = eos_g.fugacity_coefficients(Z_g)
lnphis_l = eos_l.fugacity_coefficients(Z_l)
size = N + 1
J = [[None]*size for i in range(size)]
# d_lnphi_dzs_basic_num
# d_lnphi_dxs = eos_l.d_lnphi_dzs_basic_num(Z_l, xs)
# d_lnphi_dys = eos_g.d_lnphi_dzs_basic_num(Z_g, ys)
d_lnphi_dxs = eos_l.dlnphis_dzs(Z_l)
d_lnphi_dys = eos_g.dlnphis_dzs(Z_g)
# # Handle the zeros and the ones
# Half of this is probably wrong! Only gets set for one set of variables?
# Numerical jacobian not good enough to tell
# for i in range(self.N):
# J[i][-2] = 0.0
# J[-2][i] = 0.0
J[N][N] = 1.0
# Last column except last value; believed correct
# Was not correct when compared to numerical solution
Ksm1 = [Ki - 1.0 for Ki in Ks]
RR_denoms_inv2 = []
for i in range(N):
t = 1.0 + VF*Ksm1[i]
RR_denoms_inv2.append(1.0/(t*t))
RR_terms = [zs[k]*Ksm1[k]*RR_denoms_inv2[k] for k in range(N)]
for i in range(N):
value = 0.0
d_lnphi_dxs_i, d_lnphi_dys_i = d_lnphi_dxs[i], d_lnphi_dys[i]
for k in range(N):
# pretty sure indexing is right in the below expression
value += RR_terms[k]*(d_lnphi_dxs_i[k] - Ks[k]*d_lnphi_dys_i[k])
J[i][-1] = value
# print(value)
# def delta(k, j):
# if k == j:
# return 1.0
# return 0.0
# Main body - expensive to compute! Lots of elements
# Can flip around the indexing of i, j on the d_lnphi_ds but still no fix
# unsure of correct order!
# Reveals bugs in d_lnphi_dxs though.
zsKsRRinvs2 = [zs[j]*Ks[j]*RR_denoms_inv2[j] for j in range(N)]
one_m_VF = 1.0 - VF
for i in range(N): # to N is CORRECT/MATCHES JACOBIAN NUMERICALLY
Ji = J[i]
d_lnphi_dxs_is, d_lnphi_dys_is = d_lnphi_dxs[i], d_lnphi_dys[i]
for j in range(N): # to N is CORRECT/MATCHES JACOBIAN NUMERICALLY
value = 1.0 if i == j else 0.0
# value = 0.0
# value += delta(i, j)
# print(i, j, value)
# Maybe if i == j, can skip the bit below? Tried it once and the solver never converged
# term = zs[j]*Ks[j]*RR_denoms_inv2[j]
value += zsKsRRinvs2[j]*(VF*d_lnphi_dxs_is[j] + one_m_VF*d_lnphi_dys_is[j])
Ji[j] = value
# Last row except last value - good, working
# Diff of RR w.r.t each log K
bottom_row = J[-1]
for j in range(N):
# value = 0.0
# RR_l =
# RR_l = -Ks[j]*zs[j]*VF/(1.0 + VF*(Ks[j] - 1.0))**2.0
# RR_g = Ks[j]*(1.0 - VF)*zs[j]/(1.0 + VF*(Ks[j] - 1.0))**2.0
# value += # -RR_l
bottom_row[j] = zsKsRRinvs2[j]*(one_m_VF) + VF*zsKsRRinvs2[j]
# Last row except last value - good, working
# bottom_row = J[-1]
# for j in range(self.N):
# value = 0.0
# for k in range(self.N):
# if k == j:
# RR_l = -Ks[j]*zs[k]*VF/(1.0 + VF*(Ks[k] - 1.0))**2.0
# RR_g = Ks[j]*(1.0 - VF)*zs[k]/(1.0 + VF*(Ks[k] - 1.0))**2.0
# value += RR_g - RR_l
# bottom_row[j] = value
#
# Last value - good, working, being overwritten
dF_ncp1_dB = 0.0
for i in range(N):
dF_ncp1_dB -= RR_terms[i]*Ksm1[i]
J[-1][-1] = dF_ncp1_dB
info[:] = VF, xs, ys, eos_l, eos_g
if err_also:
err_RR = Rachford_Rice_flash_error(VF, zs, Ks)
Fs = [lnKi - lnphi_l + lnphi_g for lnphi_l, lnphi_g, lnKi in zip(lnphis_l, lnphis_g, lnKs)]
Fs.append(err_RR)
return Fs, J
return J
def _err_VL(self, lnKsVF, T, P, zs, near_critical=False, info=None):
# import numpy as np
# tried autograd without luck
lnKs = lnKsVF[:-1]
# if isinstance(lnKs, np.ndarray):
# lnKs = lnKs.tolist()
# Ks = np.exp(lnKs)
Ks = [exp(lnKi) for lnKi in lnKs]
VF = float(lnKsVF[-1])
# VF = lnKsVF[-1]
if info is None:
info = []
xs = [zi/(1.0 + VF*(Ki - 1.0)) for zi, Ki in zip(zs, Ks)]
ys = [Ki*xi for Ki, xi in zip(Ks, xs)]
err_RR = Rachford_Rice_flash_error(VF, zs, Ks)
eos_g = self.to_TP_zs_fast(T=T, P=P, zs=ys, only_g=True)
eos_g.fugacities()
eos_l = self.to_TP_zs_fast(T=T, P=P, zs=xs, only_l=True)
eos_l.fugacities()
if not near_critical:
lnphis_g = eos_g.lnphis_g
lnphis_l = eos_l.lnphis_l
else:
try:
lnphis_g = eos_g.lnphis_g
except AttributeError:
lnphis_g = eos_g.lnphis_l
try:
lnphis_l = eos_l.lnphis_l
except AttributeError:
lnphis_l = eos_l.lnphis_g
# Fs = [fl/fg-1.0 for fl, fg in zip(fugacities_l, fugacities_g)]
Fs = [lnKi - lnphi_l + lnphi_g for lnphi_l, lnphi_g, lnKi in zip(lnphis_l, lnphis_g, lnKs)]
Fs.append(err_RR)
info[:] = VF, xs, ys, eos_l, eos_g
return Fs
def newton_VL(self, Ks_initial=None, maxiter=30,
ytol=1E-7, near_critical=True,
xs=None, ys=None, V_over_F=None):
T, P, zs = self.T, self.P, self.zs
if xs is not None and ys is not None and V_over_F is not None:
pass
else:
if Ks_initial is None:
Ks = [Wilson_K_value(T, P, Tci, Pci, omega) for Pci, Tci, omega in zip(self.Pcs, self.Tcs, self.omegas)]
else:
Ks = Ks_initial
V_over_F, xs, ys = flash_inner_loop(zs, Ks)
lnKs_guess = [log(yi/xi) for yi, xi in zip(ys, xs)] + [V_over_F]
info = []
def err_and_jacobian(lnKs_guess):
err = self._err_VL_jacobian(lnKs_guess, T, P, zs, near_critical=True, err_also=True, info=info)
# print(lnKs_guess[-1], err[0])
return err
ans, count = newton_system(err_and_jacobian, jac=True, x0=lnKs_guess, ytol=ytol, maxiter=maxiter)
V_over_F, xs, ys, eos_l, eos_g = info
return V_over_F, xs, ys, eos_l, eos_g
def broyden2_VL(self, Ks_initial=None, maxiter=30,
ytol=1E-7, xtol=1e-8, near_critical=True,
xs=None, ys=None, V_over_F=None):
T, P, zs = self.T, self.P, self.zs
if xs is not None and ys is not None and V_over_F is not None:
pass
else:
if Ks_initial is None:
Ks = [Wilson_K_value(T, P, Tci, Pci, omega) for Pci, Tci, omega in zip(self.Pcs, self.Tcs, self.omegas)]
else:
Ks = Ks_initial
V_over_F, xs, ys = flash_inner_loop(zs, Ks)
lnKs_guess = [log(yi/xi) for yi, xi in zip(ys, xs)] + [V_over_F]
info = []
def err_and_jacobian(lnKs_guess):
err = self._err_VL_jacobian(lnKs_guess, T, P, zs, near_critical=near_critical, err_also=True, info=info)
# print(lnKs_guess[-1], err[0])
return err[0], err[1]
def err(lnKs_guess):
err = self._err_VL(lnKs_guess, T, P, zs, near_critical=near_critical, info=info)
# print(lnKs_guess[-1], err[0])
return err
ans, count = broyden2(fun=err, jac=err_and_jacobian, xs=lnKs_guess, xtol=xtol, maxiter=maxiter, jac_has_fun=True, skip_J=True)
V_over_F, xs, ys, eos_l, eos_g = info
return V_over_F, xs, ys, eos_l, eos_g, count
def sequential_substitution_VL(self, Ks_initial=None, maxiter=1000,
xtol=1E-13, near_critical=True, Ks_extra=None,
xs=None, ys=None, trivial_solution_tol=1e-5, info=None,
full_alphas=False):
# print(self.zs, Ks)
T, P, zs = self.T, self.P, self.zs
V_over_F = None
if xs is not None and ys is not None:
pass
else:
# TODO use flash_wilson here
if Ks_initial is None:
Ks = [Wilson_K_value(T, P, Tci, Pci, omega) for Pci, Tci, omega in zip(self.Pcs, self.Tcs, self.omegas)]
else:
Ks = Ks_initial
xs = None
try:
V_over_F, xs, ys = flash_inner_loop(zs, Ks)
except ValueError as e:
if Ks_extra is not None:
for Ks in Ks_extra:
try:
V_over_F, xs, ys = flash_inner_loop(zs, Ks)
break
except ValueError as e:
pass
if xs is None:
raise(e)
# print(xs, ys, 'innerloop')
# Z_l_prev = None
# Z_g_prev = None
for i in range(maxiter):
if not near_critical:
eos_g = self.to_TP_zs_fast(T=T, P=P, zs=ys, only_l=False, only_g=True, full_alphas=full_alphas)
eos_l = self.to_TP_zs_fast(T=T, P=P, zs=xs, only_l=True, only_g=False, full_alphas=full_alphas)
lnphis_g = eos_g.fugacity_coefficients(eos_g.Z_g)
lnphis_l = eos_l.fugacity_coefficients(eos_l.Z_l)
else:
eos_g = self.to_TP_zs_fast(T=T, P=P, zs=ys, only_l=False, only_g=True, full_alphas=full_alphas)
eos_l = self.to_TP_zs_fast(T=T, P=P, zs=xs, only_l=True, only_g=False, full_alphas=full_alphas)
try:
lnphis_g = eos_g.fugacity_coefficients(eos_g.Z_g)
except AttributeError:
lnphis_g = eos_g.fugacity_coefficients(eos_g.Z_l)
try:
lnphis_l = eos_l.fugacity_coefficients(eos_l.Z_l)
except AttributeError:
lnphis_l = eos_l.fugacity_coefficients(eos_l.Z_g)
# eos_g = self.to_TP_zs(T=self.T, P=self.P, zs=ys)
# eos_l = self.to_TP_zs(T=self.T, P=self.P, zs=xs)
# if 0:
# if hasattr(eos_g, 'lnphis_g') and hasattr(eos_g, 'lnphis_l'):
# if Z_l_prev is not None and Z_g_prev is not None:
# if abs(eos_g.Z_g - Z_g_prev) < abs(eos_g.Z_l - Z_g_prev):
# lnphis_g = eos_g.lnphis_g
# fugacities_g = eos_g.fugacities_g
# Z_g_prev = eos_g.Z_g
# else:
# lnphis_g = eos_g.lnphis_l
# fugacities_g = eos_g.fugacities_l
# Z_g_prev = eos_g.Z_l
# else:
# if eos_g.G_dep_g < eos_g.lnphis_l:
# lnphis_g = eos_g.lnphis_g
# fugacities_g = eos_g.fugacities_g
# Z_g_prev = eos_g.Z_g
# else:
# lnphis_g = eos_g.lnphis_l
# fugacities_g = eos_g.fugacities_l
# Z_g_prev = eos_g.Z_l
# else:
# try:
# lnphis_g = eos_g.lnphis_g#fugacity_coefficients(eos_g.Z_g, ys)
# fugacities_g = eos_g.fugacities_g
# Z_g_prev = eos_g.Z_g
# except AttributeError:
# lnphis_g = eos_g.lnphis_l#fugacity_coefficients(eos_g.Z_l, ys)
# fugacities_g = eos_g.fugacities_l
# Z_g_prev = eos_g.Z_l
# if hasattr(eos_l, 'lnphis_g') and hasattr(eos_l, 'lnphis_l'):
# if Z_l_prev is not None and Z_g_prev is not None:
# if abs(eos_l.Z_l - Z_l_prev) < abs(eos_l.Z_g - Z_l_prev):
# lnphis_l = eos_l.lnphis_g
# fugacities_l = eos_l.fugacities_g
# Z_l_prev = eos_l.Z_g
# else:
# lnphis_l = eos_l.lnphis_l
# fugacities_l = eos_l.fugacities_l
# Z_l_prev = eos_l.Z_l
# else:
# if eos_l.G_dep_g < eos_l.lnphis_l:
# lnphis_l = eos_l.lnphis_g
# fugacities_l = eos_l.fugacities_g
# Z_l_prev = eos_l.Z_g
# else:
# lnphis_l = eos_l.lnphis_l
# fugacities_l = eos_l.fugacities_l
# Z_l_prev = eos_l.Z_l
# else:
# try:
# lnphis_l = eos_l.lnphis_g#fugacity_coefficients(eos_l.Z_g, ys)
# fugacities_l = eos_l.fugacities_g
# Z_l_prev = eos_l.Z_g
# except AttributeError:
# lnphis_l = eos_l.lnphis_l#fugacity_coefficients(eos_l.Z_l, ys)
# fugacities_l = eos_l.fugacities_l
# Z_l_prev = eos_l.Z_l
# elif 0:
# if hasattr(eos_g, 'lnphis_g') and hasattr(eos_g, 'lnphis_l'):
# if eos_g.G_dep_g < eos_g.lnphis_l:
# lnphis_g = eos_g.lnphis_g
# fugacities_g = eos_g.fugacities_g
# else:
# lnphis_g = eos_g.lnphis_l
# fugacities_g = eos_g.fugacities_l
# else:
# try:
# lnphis_g = eos_g.lnphis_g#fugacity_coefficients(eos_g.Z_g, ys)
# fugacities_g = eos_g.fugacities_g
# except AttributeError:
# lnphis_g = eos_g.lnphis_l#fugacity_coefficients(eos_g.Z_l, ys)
# fugacities_g = eos_g.fugacities_l
#
# if hasattr(eos_l, 'lnphis_g') and hasattr(eos_l, 'lnphis_l'):
# if eos_l.G_dep_g < eos_l.lnphis_l:
# lnphis_l = eos_l.lnphis_g
# fugacities_l = eos_l.fugacities_g
# else:
# lnphis_l = eos_l.lnphis_l
# fugacities_l = eos_l.fugacities_l
# else:
# try:
# lnphis_l = eos_l.lnphis_g#fugacity_coefficients(eos_l.Z_g, ys)
# fugacities_l = eos_l.fugacities_g
# except AttributeError:
# lnphis_l = eos_l.lnphis_l#fugacity_coefficients(eos_l.Z_l, ys)
# fugacities_l = eos_l.fugacities_l
#
# else:
# print(phis_l, phis_g, 'phis')
Ks = [exp(l - g) for l, g in zip(lnphis_l, lnphis_g)] # K_value(phi_l=l, phi_g=g)
# print(Ks)
# Hack - no idea if this will work
# maxK = max(Ks)
# if maxK < 1:
# Ks[Ks.index(maxK)] = 1.1
# minK = min(Ks)
# if minK >= 1:
# Ks[Ks.index(minK)] = .9
# print(Ks, 'Ks into RR')
V_over_F, xs_new, ys_new = flash_inner_loop(zs, Ks, guess=V_over_F)
# if any(i < 0 for i in xs_new):
# print('hil', xs_new)
#
# if any(i < 0 for i in ys_new):
# print('hig', ys_new)
for xi in xs_new:
if xi < 0.0:
xs_new_sum = sum(abs(i) for i in xs_new)
xs_new = [abs(i)/xs_new_sum for i in xs_new]
break
for yi in ys_new:
if yi < 0.0:
ys_new_sum = sum(abs(i) for i in ys_new)
ys_new = [abs(i)/ys_new_sum for i in ys_new]
break
# Claimed error function in CONVENTIONAL AND RAPID FLASH CALCULATIONS FOR THE SOAVE-REDLICH-KWONG AND PENG-ROBINSON EQUATIONS OF STATE
err3 = 0.0
# Suggested tolerance 1e-15
for Ki, xi, yi in zip(Ks, xs, ys):
# equivalent of fugacity ratio
# Could divide by the old Ks as well.
err_i = Ki*xi/yi - 1.0
err3 += err_i*err_i
# or use absolute for tolerance...
# err2 = sum([(exp(l-g)-1.0)**2 ])
# err2 = 0.0
# for l, g in zip(fugacities_l, fugacities_g):
# err_i = (l/g-1.0)
# err2 += err_i*err_i
# Suggested tolerance 1e-15
# This is a better metric because it does not involve hysterisis
# print(err3, err2)
# err = (sum([abs(x_new - x_old) for x_new, x_old in zip(xs_new, xs)]) +
# sum([abs(y_new - y_old) for y_new, y_old in zip(ys_new, ys)]))
# print(err, err2)
xs, ys = xs_new, ys_new
# print(i, 'err', err, err2, 'xs, ys', xs, ys, 'VF', V_over_F)
if near_critical:
comp_difference = sum([abs(xi - yi) for xi, yi in zip(xs, ys)])
if comp_difference < trivial_solution_tol:
raise ValueError("Converged to trivial condition, compositions of both phases equal")
# print(xs)
if err3 < xtol:
break
if i == maxiter-1:
raise ValueError('End of SS without convergence')
if info is not None:
info[:] = (i, err3)
return V_over_F, xs, ys, eos_l, eos_g
[docs] def stabiliy_iteration_Michelsen(self, T, P, zs, Ks_initial=None,
maxiter=20, xtol=1E-12, liq=True):
# checks stability vs. the current zs, mole fractions
# liq: whether adding a test liquid phase to see if is stable or not
eos_ref = self#.to_TP_zs(T=T, P=P, zs=zs)
# If one phase is present - use that phase as the reference phase.
# Otherwise, consider the phase with the lowest Gibbs excess energy as
# the stable phase
fugacities_ref, fugacities_ref_phase = eos_ref._eos_fugacities_lowest_Gibbs()
# print(fugacities_ref, fugacities_ref_phase, 'fugacities_ref, fugacities_ref_phase')
if Ks_initial is None:
Ks = [Wilson_K_value(T, P, Tci, Pci, omega) for Pci, Tci, omega in zip(self.Pcs, self.Tcs, self.omegas)]
else:
Ks = Ks_initial
same_phase_count = 0.0
for _ in range(maxiter):
if liq:
zs_test = [zi/Ki for zi, Ki in zip(zs, Ks)]
else:
zs_test = [zi*Ki for zi, Ki in zip(zs, Ks)]
sum_zs_test = sum(zs_test)
zs_test_normalized = [zi/sum_zs_test for zi in zs_test]
# if liq:
# print(zs_test_normalized, sum_zs_test)
# to_TP_zs_fast(self, T, P, zs, only_l=False, only_g=False)
# IT IS NOT PERMISSIBLE TO DO ONLY ONE ROOT! 2019-03-20
# Breaks lots of stabilities.
eos_test = self.to_TP_zs_fast(T=T, P=P, zs=zs_test_normalized, only_l=False, only_g=False, full_alphas=False)
fugacities_test, fugacities_phase = eos_test._eos_fugacities_lowest_Gibbs()
if fugacities_ref_phase == fugacities_phase:
same_phase_count += 1.0
else:
same_phase_count = 0
# if liq:
# print(fugacities_test, fugacities_ref_phase, fugacities_phase)
if liq:
corrections = [fi/f_ref*sum_zs_test for fi, f_ref in zip(fugacities_test, fugacities_ref)]
else:
corrections = [f_ref/(fi*sum_zs_test) for fi, f_ref in zip(fugacities_test, fugacities_ref)]
Ks = [Ki*corr for Ki, corr in zip(Ks, corrections)]
corrections_minus_1 = [corr - 1.0 for corr in corrections]
err = sum([ci*ci for ci in corrections_minus_1])
# print(err, xtol, Ks, corrections)
# print('MM iter Ks =', Ks, 'zs', zs_test_normalized, 'MM err', err, xtol, _)
if err < xtol:
break
# elif same_phase_count > 5:
# break
# It is possible to break if the trivial solution is being approached here also
if _ == maxiter-1 and fugacities_ref_phase != fugacities_phase:
raise UnconvergedError('End of stability_iteration_Michelsen without convergence')
# Fails directly if fugacities_ref_phase == fugacities_phase
# Fugacity error:
# no, the fugacities are not supposed to be equal
# err_equifugacity = 0
# for fi, fref in zip(fugacities_test, fugacities_ref):
# err_equifugacity += abs(fi - fref)
# if err_equifugacity/P > 1e-3:
# sum_zs_test = 1
return sum_zs_test, Ks, fugacities_ref_phase == fugacities_phase
def stability_Michelsen(self, T, P, zs, Ks_initial=None, maxiter=20,
xtol=1E-12, trivial_criteria=1E-4,
stable_criteria=1E-7):
# print('MM starting, Ks=', Ks_initial)
if Ks_initial is None:
Ks = [Wilson_K_value(T, P, Tci, Pci, omega) for Pci, Tci, omega in zip(self.Pcs, self.Tcs, self.omegas)]
else:
Ks = Ks_initial
zs_sum_g, Ks_g, phase_failure_g = self.stabiliy_iteration_Michelsen(T=T, P=P, zs=zs, Ks_initial=Ks,
maxiter=maxiter, xtol=xtol, liq=False)
zs_sum_l, Ks_l, phase_failure_l = self.stabiliy_iteration_Michelsen(T=T, P=P, zs=zs, Ks_initial=Ks,
maxiter=maxiter, xtol=xtol, liq=True)
log_Ks_g = [log(Ki) for Ki in Ks_g]
log_Ks_l = [log(Ki) for Ki in Ks_l]
lnK_2_tot_g = sum(log_Ki*log_Ki for log_Ki in log_Ks_g)
lnK_2_tot_l = sum(log_Ki*log_Ki for log_Ki in log_Ks_l)
sum_g_criteria = zs_sum_g - 1.0
sum_l_criteria = zs_sum_l - 1.0
trivial_g, trivial_l = False, False
if lnK_2_tot_g < trivial_criteria:
trivial_g = True
if lnK_2_tot_l < trivial_criteria:
trivial_l = True
stable = False
# print(Ks_l, Ks_g, 'Ks_l, Ks_g')
# Table 4.6 Summary of Possible Phase Stability Test Results,
# Phase Behavior, Whitson and Brule
# There is a typo where Sl appears in the vapor column; this should be
# liquid; as shown in https://www.e-education.psu.edu/png520/m17_p7.html
g_pass, l_pass = False, False # pass means this phase cannot form another phase
if phase_failure_g:
g_pass = True
if phase_failure_l:
l_pass = True
if trivial_g:
g_pass = True
if trivial_l:
l_pass = True
if sum_g_criteria < stable_criteria:
g_pass = True
if sum_l_criteria < stable_criteria:
l_pass = True
# print(l_pass, g_pass, 'l, g test show stable')
if phase_failure_g and phase_failure_l:
stable = True
elif trivial_g and trivial_l:
stable = True
elif sum_g_criteria < stable_criteria and trivial_l:
stable = True
elif trivial_g and sum_l_criteria < stable_criteria:
stable = True
elif sum_g_criteria < stable_criteria and sum_l_criteria < stable_criteria:
stable = True
# These last two are custom, and it is apparent since they are bad
# Also did not document well enough the cases they fail in
# Disabled 2018-12-29
# elif trivial_l and sum_l_criteria < stable_criteria:
# stable = True
# elif trivial_g and sum_g_criteria < stable_criteria:
# stable = True
# else:
# print('lnK_2_tot_g', lnK_2_tot_g , 'lnK_2_tot_l', lnK_2_tot_l,
# 'sum_g_criteria', sum_g_criteria, 'sum_l_criteria', sum_l_criteria)
# print('stable', stable, 'phase_failure_g', phase_failure_g, 'phase_failure_l', phase_failure_l,
# 'sum_g_criteria', sum_g_criteria, 'sum_l_criteria', sum_l_criteria,
# 'trivial_g', trivial_g, 'trivial_l', trivial_l)
# No need to enumerate unstable results
if not stable: # One set may be trivial, which means the other set is approx
# the only use used
Ks = [K_g*K_l for K_g, K_l in zip(Ks_g, Ks_l)]
# print('MM ended', Ks, stable, Ks_g, Ks_l)
return stable, Ks, [Ks_g, Ks_l]
def _V_over_F_bubble_T_inner(self, T, P, zs, maxiter=20, xtol=1E-3):
eos_l = self.to_TP_zs(T=T, P=P, zs=zs)
if not hasattr(eos_l, 'V_l'):
raise ValueError('At the specified temperature, there is no liquid root')
Ks = [Wilson_K_value(T, P, Tci, Pci, omega) for Pci, Tci, omega in zip(self.Pcs, self.Tcs, self.omegas)]
V_over_F, xs, ys = flash_inner_loop(zs, Ks)
for i in range(maxiter):
eos_g = self.to_TP_zs(T=T, P=P, zs=ys)
if not hasattr(eos_g, 'V_g'):
phis_g = eos_g.phis_l
fugacities_g = eos_g.fugacities_l
else:
phis_g = eos_g.phis_g
fugacities_g = eos_g.fugacities_g
Ks = [K_value(phi_l=l, phi_g=g) for l, g in zip(eos_l.phis_l, phis_g)]
V_over_F, xs, ys = flash_inner_loop(zs, Ks)
err = sum([abs(i-j) for i, j in zip(eos_l.fugacities_l, fugacities_g)])
if err < xtol:
break
if not hasattr(eos_g, 'V_g'):
raise ValueError('At the specified temperature, the solver did not converge to a vapor root')
return V_over_F
# raise Exception('Could not converge to desired tolerance')
def _V_over_F_dew_T_inner(self, T, P, zs, maxiter=20, xtol=1E-10):
eos_g = self.to_TP_zs(T=T, P=P, zs=zs)
if not hasattr(eos_g, 'V_g'):
raise ValueError('At the specified temperature, there is no vapor root')
Ks = [Wilson_K_value(T, P, Tci, Pci, omega) for Pci, Tci, omega in zip(self.Pcs, self.Tcs, self.omegas)]
V_over_F, xs, ys = flash_inner_loop(zs, Ks)
for i in range(maxiter):
eos_l = self.to_TP_zs(T=T, P=P, zs=xs)
if not hasattr(eos_l, 'V_l'):
phis_l = eos_l.phis_g
fugacities_l = eos_l.fugacities_g
else:
phis_l = eos_l.phis_l
fugacities_l = eos_l.fugacities_l
Ks = [K_value(phi_l=l, phi_g=g) for l, g in zip(phis_l, eos_g.phis_g)]
V_over_F, xs_new, ys_new = flash_inner_loop(zs, Ks)
err = (sum([abs(x_new - x_old) for x_new, x_old in zip(xs_new, xs)]) +
sum([abs(y_new - y_old) for y_new, y_old in zip(ys_new, ys)]))
xs, ys = xs_new, ys_new
if xtol < 1E-10:
break
if not hasattr(eos_l, 'V_l'):
raise ValueError('At the specified temperature, the solver did not converge to a liquid root')
return V_over_F-1.0
# return abs(V_over_F-1)
def _V_over_F_dew_T_inner_accelerated(self, T, P, zs, maxiter=20, xtol=1E-10):
'''This is not working.
'''
eos_g = self.to_TP_zs(T=T, P=P, zs=zs)
if not hasattr(eos_g, 'V_g'):
raise ValueError('At the specified temperature, there is no vapor root')
Ks = [Wilson_K_value(T, P, Tci, Pci, omega) for Pci, Tci, omega in zip(self.Pcs, self.Tcs, self.omegas)]
V_over_F_new, xs, ys = flash_inner_loop(zs, Ks)
for i in range(maxiter):
eos_l = self.to_TP_zs(T=T, P=P, zs=xs)
if not hasattr(eos_l, 'V_l'):
phis_l = eos_l.phis_g
fugacities_l = eos_l.fugacities_g
else:
phis_l = eos_l.phis_l
fugacities_l = eos_l.fugacities_l
if 0.0 < V_over_F_new < 1.0 and i > 2:
Rs = [K_value(phi_l=l, phi_g=g) for l, g in zip(phis_l, eos_g.phis_g)]
lambdas = [(Ki - 1.0)/(Ki - Rri) for Rri, Ki in zip(Rs, Ks)]
Ks = [Ki*Ri**lambda_i for Ki, Ri, lambda_i in zip(Ks, Rs, lambdas)]
else:
Ks = [K_value(phi_l=l, phi_g=g) for l, g in zip(phis_l, eos_g.phis_g)]
V_over_F_new, xs_new, ys_new = flash_inner_loop(zs, Ks)
err_new = (sum([abs(x_new - x_old) for x_new, x_old in zip(xs_new, xs)]) +
sum([abs(y_new - y_old) for y_new, y_old in zip(ys_new, ys)]))
xs, ys = xs_new, ys_new
V_over_F_old = V_over_F_new
if i == 0:
err_old = err_new
err_old = err_new
if err_new < xtol:
break
if not hasattr(eos_l, 'V_l'):
raise ValueError('At the specified temperature, the solver did not converge to a liquid root')
return V_over_F_new-1.0
# return abs(V_over_F-1)
# def _a_alpha_j_rows(self):
## try:
## return self.a_alpha_j_rows
## except:
## pass
# zs = self.zs
# N = self.N
# a_alpha_ijs = self.a_alpha_ijs
# a_alpha_j_rows = []
# for i in range(N):
# l = a_alpha_ijs[i]
# sum_term = 0.0
# for j in range(N):
# sum_term += zs[j]*l[j]
# a_alpha_j_rows.append(sum_term)
# self.a_alpha_j_rows = a_alpha_j_rows
# return a_alpha_j_rows
@property
def _a_alpha_j_rows(self):
try:
return self.a_alpha_j_rows
except:
pass
zs, N = self.zs, self.N
a_alpha_ijs = self.a_alpha_ijs
if self.vectorized:
a_alpha_j_rows = zeros(N)
else:
a_alpha_j_rows = [0.0]*N
for i in range(N):
l = a_alpha_ijs[i]
for j in range(i):
a_alpha_j_rows[j] += zs[i]*l[j]
a_alpha_j_rows[i] += zs[j]*l[j]
a_alpha_j_rows[i] += zs[i]*l[i]
self.a_alpha_j_rows = a_alpha_j_rows
return a_alpha_j_rows
def _set_alpha_matrices(self):
N = self.N
if not hasattr(self, 'd2a_alpha_dT2s'):
self.a_alpha_and_derivatives(self.T, full=True, pure_a_alphas=True)
if self.vectorized:
a_alpha_ijs, a_alpha_roots, a_alpha_ij_roots_inv = zeros((N, N)), zeros(N), zeros((N, N))
else:
a_alpha_ijs, a_alpha_roots, a_alpha_ij_roots_inv = [[0.0]*N for _ in range(N)], [0.0]*N, [[0.0]*N for _ in range(N)]
a_alpha_ijs, a_alpha_roots, a_alpha_ij_roots_inv = a_alpha_aijs_composition_independent(self.a_alphas, self.one_minus_kijs,
a_alpha_ijs=a_alpha_ijs,a_alpha_roots=a_alpha_roots, a_alpha_ij_roots_inv=a_alpha_ij_roots_inv)
_, _, _, a_alpha_ijs, da_alpha_dT_ijs, d2a_alpha_dT2_ijs = a_alpha_and_derivatives_full(
self.a_alphas, self.da_alpha_dTs, self.d2a_alpha_dT2s, self.T, self.zs, self.one_minus_kijs,
a_alpha_ijs, self.a_alpha_roots, a_alpha_ij_roots_inv)
if self.vectorized:
d2a_alpha_dT2_ijs = array(d2a_alpha_dT2_ijs)
da_alpha_dT_ijs = array(da_alpha_dT_ijs)
self._d2a_alpha_dT2_ijs = d2a_alpha_dT2_ijs
self._da_alpha_dT_ijs = da_alpha_dT_ijs
self._a_alpha_ijs = a_alpha_ijs
@property
def a_alpha_ijs(self):
r'''Calculate and return the matrix
:math:`(a\alpha)_{ij} = (1-k_{ij})\sqrt{(a\alpha)_{i}(a\alpha)_{j}}`.
Returns
-------
a_alpha_ijs : list[list[float]]
`a_alpha` terms for each component with every other component,
[J^2/mol^2/Pa]
Notes
-----
In an earlier implementation this matrix was stored each EOS solve;
however, allocating that much memory becomes quite expensive for large
number of component cases and this is now calculated on-demand only.
'''
try:
return self._a_alpha_ijs
except:
self._set_alpha_matrices()
return self._a_alpha_ijs
@property
def da_alpha_dT_ijs(self):
r'''Calculate and return the matrix for the temperature derivatives of
the alpha terms.
.. math::
\frac{\partial (a\alpha)_{ij}}{\partial T} =
\frac{\sqrt{\operatorname{a\alpha_{i}}{\left(T \right)} \operatorname{a\alpha_{j}}
{\left(T \right)}} \left(1 - k_{ij}\right) \left(\frac{\operatorname{a\alpha_{i}}
{\left(T \right)} \frac{d}{d T} \operatorname{a\alpha_{j}}{\left(T \right)}}{2}
+ \frac{\operatorname{a\alpha_{j}}{\left(T \right)} \frac{d}{d T} \operatorname{
a\alpha_{i}}{\left(T \right)}}{2}\right)}{\operatorname{a\alpha_{i}}{\left(T \right)}
\operatorname{a\alpha_{j}}{\left(T \right)}}
Returns
-------
da_alpha_dT_ijs : list[list[float]]
First temperature derivative of `a_alpha` terms for each component
with every other component, [J^2/mol^2/Pa/K]
Notes
-----
In an earlier implementation this matrix was stored each EOS solve;
however, allocating that much memory becomes quite expensive for large
number of component cases and this is now calculated on-demand only.
'''
try:
return self._da_alpha_dT_ijs
except:
self._set_alpha_matrices()
return self._da_alpha_dT_ijs
@property
def d2a_alpha_dT2_ijs(self):
r'''Calculate and return the matrix of the second temperature
derivatives of the alpha terms.
.. math::
\frac{\partial^2 (a\alpha)_{ij}}{\partial T^2} =
- \frac{\sqrt{\operatorname{a\alpha_{i}}{\left(T \right)} \operatorname{a\alpha_{j}}
{\left(T \right)}} \left(k_{ij} - 1\right) \left(\frac{\left(\operatorname{
a\alpha_{i}}{\left(T \right)} \frac{d}{d T} \operatorname{a\alpha_{j}}{\left(T \right)}
+ \operatorname{a\alpha_{j}}{\left(T \right)} \frac{d}{d T} \operatorname{a\alpha_{i}}
{\left(T \right)}\right)^{2}}{4 \operatorname{a\alpha_{i}}{\left(T \right)}
\operatorname{a\alpha_{j}}{\left(T \right)}} - \frac{\left(\operatorname{a\alpha_{i}}
{\left(T \right)} \frac{d}{d T} \operatorname{a\alpha_{j}}{\left(T \right)}
+ \operatorname{a\alpha_{j}}{\left(T \right)} \frac{d}{d T}
\operatorname{a\alpha_{i}}{\left(T \right)}\right) \frac{d}{d T}
\operatorname{a\alpha_{j}}{\left(T \right)}}{2 \operatorname{a\alpha_{j}}
{\left(T \right)}} - \frac{\left(\operatorname{a\alpha_{i}}{\left(T \right)}
\frac{d}{d T} \operatorname{a\alpha_{j}}{\left(T \right)}
+ \operatorname{a\alpha_{j}}{\left(T \right)} \frac{d}{d T}
\operatorname{a\alpha_{i}}{\left(T \right)}\right) \frac{d}{d T}
\operatorname{a\alpha_{i}}{\left(T \right)}}{2 \operatorname{a\alpha_{i}}
{\left(T \right)}} + \frac{\operatorname{a\alpha_{i}}{\left(T \right)}
\frac{d^{2}}{d T^{2}} \operatorname{a\alpha_{j}}{\left(T \right)}}{2}
+ \frac{\operatorname{a\alpha_{j}}{\left(T \right)} \frac{d^{2}}{d T^{2}}
\operatorname{a\alpha_{i}}{\left(T \right)}}{2} + \frac{d}{d T}
\operatorname{a\alpha_{i}}{\left(T \right)} \frac{d}{d T}
\operatorname{a\alpha_{j}}{\left(T \right)}\right)}
{\operatorname{a\alpha_{i}}{\left(T \right)} \operatorname{a\alpha_{j}}
{\left(T \right)}}
Returns
-------
d2a_alpha_dT2_ijs : list[list[float]]
Second temperature derivative of `a_alpha` terms for each component
with every other component, [J^2/mol^2/Pa/K^2]
Notes
-----
In an earlier implementation this matrix was stored each EOS solve;
however, allocating that much memory becomes quite expensive for large
number of component cases and this is now calculated on-demand only.
'''
try:
return self._d2a_alpha_dT2_ijs
except:
self._set_alpha_matrices()
return self._d2a_alpha_dT2_ijs
@property
def _da_alpha_dT_j_rows(self):
try:
return self.da_alpha_dT_j_rows
except:
pass
zs, N, vectorized = self.zs, self.N, self.vectorized
da_alpha_dT_ijs = self.da_alpha_dT_ijs
# Handle the case of attempting to avoid a full alpha derivative matrix evaluation
if da_alpha_dT_ijs is None:
self.resolve_full_alphas()
da_alpha_dT_ijs = self.da_alpha_dT_ijs
if vectorized:
da_alpha_dT_j_rows = zeros(N)
else:
da_alpha_dT_j_rows = [0.0]*N
for i in range(N):
l = da_alpha_dT_ijs[i]
for j in range(i):
da_alpha_dT_j_rows[j] += zs[i]*l[j]
da_alpha_dT_j_rows[i] += zs[j]*l[j]
da_alpha_dT_j_rows[i] += zs[i]*l[i]
self.da_alpha_dT_j_rows = da_alpha_dT_j_rows
return da_alpha_dT_j_rows
@property
def _d2a_alpha_dT2_j_rows(self):
try:
return self.d2a_alpha_dT2_j_rows
except AttributeError:
pass
d2a_alpha_dT2_ijs, N, vectorized = self.d2a_alpha_dT2_ijs, self.N, self.vectorized
# Handle the case of attempting to avoid a full alpha derivative matrix evaluation
if d2a_alpha_dT2_ijs is None:
self.resolve_full_alphas()
d2a_alpha_dT2_ijs = self.d2a_alpha_dT2_ijs
zs = self.zs
if vectorized:
d2a_alpha_dT2_j_rows = zeros(N)
else:
d2a_alpha_dT2_j_rows = [0.0]*N
for i in range(N):
l = d2a_alpha_dT2_ijs[i]
for j in range(i):
d2a_alpha_dT2_j_rows[j] += zs[i]*l[j]
d2a_alpha_dT2_j_rows[i] += zs[j]*l[j]
d2a_alpha_dT2_j_rows[i] += zs[i]*l[i]
self.d2a_alpha_dT2_j_rows = d2a_alpha_dT2_j_rows
return d2a_alpha_dT2_j_rows
@property
def db_dzs(self):
r'''Helper method for calculating the composition derivatives of `b`.
Note this is independent of the phase.
.. math::
\left(\frac{\partial b}{\partial x_i}\right)_{T, P, x_{i\ne j}}
= b_i
Returns
-------
db_dzs : list[float]
Composition derivative of `b` of each component, [m^3/mol]
Notes
-----
This derivative is checked numerically.
'''
return self.bs
@property
def db_dns(self):
r'''Helper method for calculating the mole number derivatives of `b`.
Note this is independent of the phase.
.. math::
\left(\frac{\partial b}{\partial n_i}\right)_{T, P, n_{i\ne j}}
= b_i - b
Returns
-------
db_dns : list[float]
Composition derivative of `b` of each component, [m^3/mol^2]
Notes
-----
This derivative is checked numerically.
'''
b = self.b
if self.vectorized:
return self.bs - b
else:
return [bi - b for bi in self.bs]
@property
def dnb_dns(self):
r'''Helper method for calculating the partial molar derivative of `b`.
Note this is independent of the phase.
.. math::
\left(\frac{\partial n \cdot b}{\partial n_i}\right)_{T, P, n_{i\ne j}}
= b_i
Returns
-------
dnb_dns : list[float]
Partial molar derivative of `b` of each component, [m^3/mol]
Notes
-----
This derivative is checked numerically.
'''
return self.bs
@property
def d2b_dzizjs(self):
r'''Helper method for calculating the second partial mole fraction
derivatives of `b`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 b}{\partial x_i \partial x_j}
\right)_{T, P,
n_{k \ne i,j}} = 0
Returns
-------
d2b_dzizjs : list[list[float]]
Second mole fraction derivatives of `b` of each component,
[m^3/mol]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return zeros((N, N)) if self.vectorized else [[0.0]*N for i in range(N)]
@property
def d2b_dninjs(self):
r'''Helper method for calculating the second partial mole number
derivatives of `b`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 b}{\partial n_i \partial n_j}\right)_{T, P,
n_{k\ne i,k}} = 2b - b_i - b_j
Returns
-------
d2b_dninjs : list[list[float]]
Second Composition derivative of `b` of each component,
[m^3/mol^3]
Notes
-----
This derivative is checked numerically.
'''
bb = 2.0*self.b
bs = self.bs
if self.vectorized:
N = self.N
d2b_dninjs = np.full((N, N), bb, float)
d2b_dninjs -= bs
d2b_dninjs = d2b_dninjs.transpose()
d2b_dninjs -= bs
else:
d2b_dninjs = []
for bi in bs:
d2b_dninjs.append([bb - bi - bj for bj in bs])
return d2b_dninjs
@property
def d3b_dzizjzks(self):
r'''Helper method for calculating the third partial mole fraction
derivatives of `b`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 b}{\partial x_i \partial x_j \partial x_k}
\right)_{T, P,
n_{k \ne i,j,k}} = 0
Returns
-------
d3b_dzizjzks : list[list[list[float]]]
Third mole fraction derivatives of `b` of each component,
[m^3/mol]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N)] for _ in range(N)]
@property
def d3b_dninjnks(self):
r'''Helper method for calculating the third partial mole number
derivatives of `b`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 b}{\partial n_i \partial n_j \partial n_k }
\right)_{T, P,
n_{m \ne i,j,k}} = 2(-3b + b_i + b_j + b_k)
Returns
-------
d3b_dninjnks : list[list[list[float]]]
Third mole number derivative of `b` of each component,
[m^3/mol^4]
Notes
-----
This derivative is checked numerically.
'''
bs = self.bs
n6b = -6.0*self.b
if self.vectorized:
bs2 = 2.0*self.bs
N = self.N
d3b_dninjnks = np.full((N, N, N), n6b)
d3b_dninjnks += bs2
d3b_dninjnks = d3b_dninjnks.transpose((2, 1, 0))
d3b_dninjnks += bs2
d3b_dninjnks = d3b_dninjnks.transpose((0, 2, 1))
d3b_dninjnks += bs2
else:
bs2 = [bi + bi for bi in bs]
d3b_dninjnks = []
for bi2 in bs2:
d3b_dnjnks = []
for bj2 in bs2:
base = n6b + bi2 + bj2
d3b_dnjnks.append([base + bk2 for bk2 in bs2])
d3b_dninjnks.append(d3b_dnjnks)
return d3b_dninjnks
@property
def d3epsilon_dzizjzks(self):
r'''Helper method for calculating the third composition derivatives
of `epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 \epsilon}{\partial x_i \partial x_j
\partial x_k }\right)_{T, P, x_{m\ne i,j,k}} = 0
Returns
-------
d2epsilon_dzizjzks : list[list[list[float]]]
Composition derivative of `epsilon` of each component, [m^6/mol^2]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N)] for _ in range(N)]
@property
def d3delta_dzizjzks(self):
r'''Helper method for calculating the third composition derivatives
of `delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 \delta}{\partial x_i \partial x_j
\partial x_k }\right)_{T, P, x_{m\ne i,j,k}} = 0
Returns
-------
d3delta_dzizjzks : list[list[list[float]]]
Third composition derivative of `epsilon` of each component,
[m^6/mol^5]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N)] for _ in range(N)]
@property
def da_alpha_dzs(self):
r'''Helper method for calculating the composition derivatives of
`a_alpha`. Note this is independent of the phase.
.. math::
\left(\frac{\partial a \alpha}{\partial x_i}\right)_{T, P, x_{i\ne j}}
= 2 \cdot \sum_j z_{j} (1 - k_{ij}) \sqrt{ (a \alpha)_i (a \alpha)_j}
Returns
-------
da_alpha_dzs : list[float]
Composition derivative of `alpha` of each component,
[kg*m^5/(mol^2*s^2)]
Notes
-----
This derivative is checked numerically.
'''
try:
a_alpha_j_rows = self.a_alpha_j_rows
except:
a_alpha_j_rows = self._a_alpha_j_rows
return 2.0*a_alpha_j_rows if self.vectorized else [i + i for i in a_alpha_j_rows]
@property
def da_alpha_dns(self):
r'''Helper method for calculating the mole number derivatives of
`a_alpha`. Note this is independent of the phase.
.. math::
\left(\frac{\partial a \alpha}{\partial n_i}\right)_{T, P, n_{i\ne j}}
= 2 (-a\alpha + \sum_j z_{j} (1 - k_{ij}) \sqrt{ (a \alpha)_i (a \alpha)_j})
Returns
-------
da_alpha_dns : list[float]
Mole number derivative of `alpha` of each component,
[kg*m^5/(mol^3*s^2)]
Notes
-----
This derivative is checked numerically.
'''
try:
a_alpha_j_rows = self.a_alpha_j_rows
except:
a_alpha_j_rows = self._a_alpha_j_rows
a_alpha_n_2 = -2.0*self.a_alpha
return 2.0*a_alpha_j_rows + a_alpha_n_2 if self.vectorized else [2.0*t + a_alpha_n_2 for t in a_alpha_j_rows]
@property
def dna_alpha_dns(self):
r'''Helper method for calculating the partial molar derivatives of
`a_alpha`. Note this is independent of the phase.
.. math::
\left(\frac{\partial a \alpha}{\partial n_i}\right)_{T, P, n_{i\ne j}}
= 2 (-0.5 a\alpha + \sum_j z_{j} (1 - k_{ij}) \sqrt{ (a \alpha)_i (a \alpha)_j})
Returns
-------
dna_alpha_dns : list[float]
Partial molar derivative of `alpha` of each component,
[kg*m^5/(mol^2*s^2)]
Notes
-----
This derivative is checked numerically.
'''
try:
a_alpha_j_rows = self.a_alpha_j_rows
except:
a_alpha_j_rows = self._a_alpha_j_rows
a_alpha = self.a_alpha
return 2.0*a_alpha_j_rows - a_alpha if self.vectorized else [t + t - a_alpha for t in a_alpha_j_rows]
@property
def d2a_alpha_dzizjs(self):
r'''Helper method for calculating the second composition derivatives of
`a_alpha` (hessian). Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 a \alpha}{\partial x_i \partial
x_j}\right)_{T, P, x_{k\ne i,j}}
= 2 (1-k_{ij})\sqrt{(a\alpha)_{i}(a\alpha)_{j}}
Returns
-------
d2a_alpha_dzizjs : list[float]
Second composition derivative of `alpha` of each component,
[kg*m^5/(mol^2*s^2)]
Notes
-----
This derivative is checked numerically.
'''
a_alpha_ijs = self.a_alpha_ijs
return 2.0*a_alpha_ijs if self.vectorized else [[i+i for i in row] for row in a_alpha_ijs]
@property
def d2a_alpha_dninjs(self):
r'''Helper method for calculating the second partial molar derivatives
of `a_alpha` (hessian). Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 a \alpha}{\partial n_i \partial n_j }\right)_{T, P, n_{k\ne i,j}}
= 2\left[3(a \alpha) + (a\alpha)_{ij} -2 (\text{term}_{i,j})
\right]
.. math::
\text{term}_{i,j} = \sum_k z_k\left((a\alpha)_{ik} + (a\alpha)_{jk}
\right)
.. math::
(a\alpha)_{ij} = (1-k_{ij})\sqrt{(a\alpha)_{i}(a\alpha)_{j}}
Returns
-------
d2a_alpha_dninjs : list[float]
Second partial molar derivative of `alpha` of each component,
[kg*m^5/(mol^4*s^2)]
Notes
-----
This derivative is checked numerically.
'''
try:
a_alpha_j_rows = self.a_alpha_j_rows
except:
a_alpha_j_rows = self._a_alpha_j_rows
a_alpha = self.a_alpha
a_alpha_ijs = self.a_alpha_ijs
N = self.N
zs = self.zs
a_alpha3 = 3.0*a_alpha
if self.vectorized:
hessian = zeros((N, N))
else:
hessian = [[0.0]*N for _ in range(N)]
for i in range(N):
for j in range(i+1):
if i == j:
term = 2.0*a_alpha_j_rows[i]
else:
term = 0.0
for k in range(N):
term += zs[k]*(a_alpha_ijs[i][k] + a_alpha_ijs[j][k])
hessian[i][j] = hessian[j][i] = 2.0*(a_alpha3 + a_alpha_ijs[i][j] -2.0*term)
# row.append(2.0*(a_alpha3 + a_alpha_ijs[i][j] -2.0*term))
# hessian.append(row)
return hessian
@property
def d3a_alpha_dzizjzks(self):
r'''Helper method for calculating the third composition derivatives of
`a_alpha`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 a \alpha}{\partial x_i \partial x_j
\partial x_k}\right)_{T, P, x_{m\ne i,j,k}}
= 0
Returns
-------
d3a_alpha_dzizjzks : list[float]
Third composition derivative of `alpha` of each component,
[kg*m^5/(mol^2*s^2)]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N)] for _ in range(N)]
@property
def d3a_alpha_dninjnks(self):
r'''Helper method for calculating the third mole number derivatives of
`a_alpha`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 a \alpha}{\partial n_i \partial n_j
\partial n_k}\right)_{T, P, n_{m\ne i,j,k}}
= 4\left(-6 (a \alpha) - [(a \alpha)_{i,j} + (a \alpha)_{i,k}
+ (a \alpha)_{j,k}]
+ 3\sum_m z_m[(a \alpha)_{i,m} + (a \alpha)_{j,m}
+ (a \alpha)_{k,m}]\right)
Returns
-------
d3a_alpha_dninjnks : list[float]
Third mole number derivative of `alpha` of each component,
[kg*m^5/(mol^5*s^2)]
Notes
-----
This derivative is checked numerically.
'''
# Seems correct across diagonal
# Each term is of similar magnitude, so likely would notice if brokwn
a_alpha = self.a_alpha
a_alpha_ijs = self.a_alpha_ijs
N = self.N
zs = self.zs
a_alpha6 = -6.0*a_alpha
matrix = zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N)] for _ in range(N)]
for i in range(N):
l = []
for j in range(N):
row = []
for k in range(N):
mid = a_alpha_ijs[i][j] + a_alpha_ijs[i][k] + a_alpha_ijs[j][k]
last = sum(zs[m]*(a_alpha_ijs[i][m] + a_alpha_ijs[j][m] + a_alpha_ijs[k][m]) for m in range(N))
ele = 4.0*(a_alpha6 - mid + 3.0*last)
matrix[i][j][k] = ele
return matrix
@property
def da_alpha_dT_dzs(self):
r'''Helper method for calculating the composition derivatives of
`da_alpha_dT`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 a \alpha}{\partial x_i \partial T}
\right)_{P, x_{i\ne j}}
= 2 \sum_j -z_{j} (k_{ij} - 1) (a \alpha)_i (a \alpha)_j
\frac{\partial (a \alpha)_i}{\partial T} \frac{\partial (a \alpha)_j}{\partial T}
\left({ (a \alpha)_i (a \alpha)_j}\right)^{-0.5}
Returns
-------
da_alpha_dT_dzs : list[float]
Composition derivative of `da_alpha_dT` of each component,
[kg*m^5/(mol^2*s^2*K)]
Notes
-----
This derivative is checked numerically.
'''
try:
da_alpha_dT_j_rows = self.da_alpha_dT_j_rows
except:
da_alpha_dT_j_rows = self._da_alpha_dT_j_rows
return 2.0*da_alpha_dT_j_rows if self.vectorized else [i + i for i in da_alpha_dT_j_rows]
@property
def da_alpha_dT_dns(self):
r'''Helper method for calculating the mole number derivatives of
`da_alpha_dT`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 a \alpha}{\partial n_i \partial T}
\right)_{P, n_{i\ne j}}
= 2 \left[\sum_j -z_{j} (k_{ij} - 1) (a \alpha)_i (a \alpha)_j
\frac{\partial (a \alpha)_i}{\partial T} \frac{\partial (a \alpha)_j}{\partial T}
\left({ (a \alpha)_i (a \alpha)_j}\right)^{-0.5}
- \frac{\partial a \alpha}{\partial T} \right]
Returns
-------
da_alpha_dT_dns : list[float]
Composition derivative of `da_alpha_dT` of each component,
[kg*m^5/(mol^3*s^2*K)]
Notes
-----
This derivative is checked numerically.
'''
try:
da_alpha_dT_j_rows = self.da_alpha_dT_j_rows
except:
da_alpha_dT_j_rows = self._da_alpha_dT_j_rows
da_alpha_dT = self.da_alpha_dT
return 2.0*(da_alpha_dT_j_rows - da_alpha_dT) if self.vectorized else [2.0*(t - da_alpha_dT) for t in da_alpha_dT_j_rows]
@property
def dna_alpha_dT_dns(self):
r'''Helper method for calculating the mole number derivatives of
`da_alpha_dT`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 n a \alpha}{\partial n_i \partial T}
\right)_{P, n_{i\ne j}}
= 2 \left[\sum_j -z_{j} (k_{ij} - 1) (a \alpha)_i (a \alpha)_j
\frac{\partial (a \alpha)_i}{\partial T} \frac{\partial (a \alpha)_j}{\partial T}
\left({ (a \alpha)_i (a \alpha)_j}\right)^{-0.5}
- 0.5 \frac{\partial a \alpha}{\partial T} \right]
Returns
-------
dna_alpha_dT_dns : list[float]
Composition derivative of `da_alpha_dT` of each component,
[kg*m^5/(mol^2*s^2*K)]
Notes
-----
This derivative is checked numerically.
'''
try:
da_alpha_dT_j_rows = self.da_alpha_dT_j_rows
except:
da_alpha_dT_j_rows = self._da_alpha_dT_j_rows
da_alpha_dT = self.da_alpha_dT
return 2.0*da_alpha_dT_j_rows - da_alpha_dT if self.vectorized else [t + t - da_alpha_dT for t in da_alpha_dT_j_rows]
@property
def d2a_alpha_dT2_dzs(self):
r'''Helper method for calculating the mole number derivatives of
`d2a_alpha_dT2`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 a \alpha}{\partial z_i \partial T^2}
\right)_{P, z_{i\ne j}}
= \text{large expression}
Returns
-------
d2a_alpha_dT2_dzs : list[float]
Composition derivative of `d2a_alpha_dT2` of each component,
[kg*m^5/(mol^2*s^2*K^2)]
Notes
-----
This derivative is checked numerically.
'''
try:
d2a_alpha_dT2_j_rows = self.d2a_alpha_dT2_j_rows
except:
d2a_alpha_dT2_j_rows = self._d2a_alpha_dT2_j_rows
return 2.0*d2a_alpha_dT2_j_rows if self.vectorized else [i + i for i in d2a_alpha_dT2_j_rows]
@property
def d2a_alpha_dT2_dns(self):
r'''Helper method for calculating the mole number derivatives of
`d2a_alpha_dT2`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 a \alpha}{\partial n_i \partial T^2}
\right)_{P, n_{i\ne j}}
= f\left(\left(\frac{\partial^3 a\alpha}{\partial z_i \partial T^2}
\right)_{P, z_{i\ne j}} \right)
Returns
-------
d2a_alpha_dT2_dns : list[float]
Mole number derivative of `d2a_alpha_dT2` of each component,
[kg*m^5/(mol^3*s^2*K^2)]
Notes
-----
This derivative is checked numerically.
'''
try:
d2a_alpha_dT2_j_rows = self.d2a_alpha_dT2_j_rows
except:
d2a_alpha_dT2_j_rows = self._d2a_alpha_dT2_j_rows
d2a_alpha_dT2 = self.d2a_alpha_dT2
return 2.0*(d2a_alpha_dT2_j_rows - d2a_alpha_dT2) if self.vectorized else [2.0*(t - d2a_alpha_dT2) for t in d2a_alpha_dT2_j_rows]
[docs] def dV_dzs(self, Z):
r'''Calculates the molar volume composition derivative
(where the mole fractions do not sum to 1). Verified numerically.
Used in many other derivatives, and for the molar volume mole number
derivative and partial molar volume calculation.
.. math::
\left(\frac{\partial V}{\partial x_i}\right)_{T, P,
x_{i\ne j}} =
\frac{- R T \left(V^{2}{\left(x \right)} + V{\left(x \right)} \delta{\left(x \right)}
+ \epsilon{\left(x \right)}\right)^{3} \frac{d}{d x} b{\left(x \right)} + \left(V{\left(x \right)}
- b{\left(x \right)}\right)^{2} \left(V^{2}{\left(x \right)} + V{\left(x \right)} \delta{\left(x \right)}
+ \epsilon{\left(x \right)}\right)^{2} \frac{d}{d x} \operatorname{a \alpha}{\left(x \right)}
- \left(V{\left(x \right)} - b{\left(x \right)}\right)^{2} V^{3}{\left(x \right)} \operatorname{a
\alpha}{\left(x \right)} \frac{d}{d x} \delta{\left(x \right)} - \left(V{\left(x \right)} - b{\left(x
\right)}\right)^{2} V^{2}{\left(x \right)} \operatorname{a \alpha}{\left(x \right)} \delta{\left(x
\right)} \frac{d}{d x} \delta{\left(x \right)} - \left(V{\left(x \right)} - b{\left(x \right)}
\right)^{2} V^{2}{\left(x \right)} \operatorname{a \alpha}{\left(x \right)} \frac{d}{d x} \epsilon{
\left(x \right)} - \left(V{\left(x \right)} - b{\left(x \right)}\right)^{2} V{\left(x \right)}
\operatorname{a \alpha}{\left(x \right)} \delta{\left(x \right)} \frac{d}{d x} \epsilon{\left(x
\right)} - \left(V{\left(x \right)} - b{\left(x \right)}\right)^{2} V{\left(x \right)} \operatorname{a
\alpha}{\left(x \right)} \epsilon{\left(x \right)} \frac{d}{d x} \delta{\left(x \right)}
- \left(V{\left(x \right)} - b{\left(x \right)}\right)^{2} \operatorname{a \alpha}{\left(x \right)}
\epsilon{\left(x \right)} \frac{d}{d x} \epsilon{\left(x \right)}}{- R T \left(V^{2}{\left(x \right)}
+ V{\left(x \right)} \delta{\left(x \right)} + \epsilon{\left(x \right)}\right)^{3}
+ 2 \left(V{\left(x \right)} - b{\left(x \right)}\right)^{2} V^{3}{\left(x \right)}
\operatorname{a \alpha}{\left(x \right)} + 3 \left(V{\left(x \right)} - b{\left(x \right)}\right)^{2}
V^{2}{\left(x \right)} \operatorname{a \alpha}{\left(x \right)} \delta{\left(x \right)}
+ \left(V{\left(x \right)} - b{\left(x \right)}\right)^{2} V{\left(x \right)} \operatorname{a
\alpha}{\left(x \right)} \delta^{2}{\left(x \right)} + 2 \left(V{\left(x \right)} - b{\left(x
\right)}\right)^{2} V{\left(x \right)} \operatorname{a \alpha}{\left(x \right)} \epsilon{\left(x
\right)} + \left(V{\left(x \right)} - b{\left(x \right)}\right)^{2} \operatorname{a \alpha}{\left(x
\right)} \delta{\left(x \right)} \epsilon{\left(x \right)}}
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dV_dzs : float
Molar volume composition derivatives, [m^3/mol]
Notes
-----
The derivation for the derivative is performed as follows using SymPy.
The function source code is an optimized variant created with the `cse`
SymPy function, and hand optimized further.
>>> from sympy import * # doctest:+SKIP
>>> P, T, R, x = symbols('P, T, R, x') # doctest:+SKIP
>>> V, delta, epsilon, a_alpha, b = symbols('V, delta, epsilon, a\ \\alpha, b', cls=Function) # doctest:+SKIP
>>> CUBIC = R*T/(V(x) - b(x)) - a_alpha(x)/(V(x)*V(x) + delta(x)*V(x) + epsilon(x)) - P # doctest:+SKIP
>>> solve(diff(CUBIC, x), Derivative(V(x), x)) # doctest:+SKIP
[(-R*T*(V(x)**2 + V(x)*delta(x) + epsilon(x))**3*Derivative(b(x), x) + (V(x) - b(x))**2*(V(x)**2 + V(x)*delta(x) + epsilon(x))**2*Derivative(a \alpha(x), x) - (V(x) - b(x))**2*V(x)**3*a \alpha(x)*Derivative(delta(x), x) - (V(x) - b(x))**2*V(x)**2*a \alpha(x)*delta(x)*Derivative(delta(x), x) - (V(x) - b(x))**2*V(x)**2*a \alpha(x)*Derivative(epsilon(x), x) - (V(x) - b(x))**2*V(x)*a \alpha(x)*delta(x)*Derivative(epsilon(x), x) - (V(x) - b(x))**2*V(x)*a \alpha(x)*epsilon(x)*Derivative(delta(x), x) - (V(x) - b(x))**2*a \alpha(x)*epsilon(x)*Derivative(epsilon(x), x))/(-R*T*(V(x)**2 + V(x)*delta(x) + epsilon(x))**3 + 2*(V(x) - b(x))**2*V(x)**3*a \alpha(x) + 3*(V(x) - b(x))**2*V(x)**2*a \alpha(x)*delta(x) + (V(x) - b(x))**2*V(x)*a \alpha(x)*delta(x)**2 + 2*(V(x) - b(x))**2*V(x)*a \alpha(x)*epsilon(x) + (V(x) - b(x))**2*a \alpha(x)*delta(x)*epsilon(x))]
'''
N = self.N
out = zeros(N) if self.vectorized else [0.0]*N
return eos_mix_dV_dzs(self.T, self.P, Z, self.b, self.delta, self.epsilon,
self.a_alpha, self.db_dzs, self.ddelta_dzs,
self.depsilon_dzs, self.da_alpha_dzs, N, out)
[docs] def dV_dns(self, Z):
r'''Calculates the molar volume mole number derivatives
(where the mole fractions sum to 1). No specific formula is implemented
for this property - it is calculated from the mole fraction derivative.
.. math::
\left(\frac{\partial V}{\partial n_i}\right)_{T, P,
n_{i\ne j}} = f\left( \left(\frac{\partial V}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dV_dns : float
Molar volume mole number derivatives, [m^3/mol^2]
'''
out = zeros(self.N) if self.vectorized else [0.0]*self.N
dV_dns = dxs_to_dns(self.dV_dzs(Z), self.zs, out)
return dV_dns
[docs] def dnV_dns(self, Z):
r'''Calculates the partial molar volume of the specified phase
No specific formula is implemented
for this property - it is calculated from the molar
volume mole fraction derivative.
.. math::
\left(\frac{\partial n V}{\partial n_i}\right)_{T, P,
n_{i\ne j}} = f\left( \left(\frac{\partial V}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dnV_dns : float
Partial molar volume of the mixture of the specified phase,
[m^3/mol]
'''
V = Z*R*self.T/self.P
out = zeros(self.N) if self.vectorized else [0.0]*self.N
return dxs_to_dn_partials(self.dV_dzs(Z), self.zs, V, out)
def _d2V_dij_wrapper(self, V, d_Vs, dbs, d2bs, d_epsilons, d2_epsilons,
d_deltas, d2_deltas, da_alphas, d2a_alphas):
T = self.T
x0 = V
x3 = self.b
x4 = x0 - x3
x5 = self.epsilon
x6 = x0*x0
x7 = self.delta
x8 = x0*x7
x9 = x5 + x6 + x8
x10 = self.a_alpha
x11 = x10*x4*x4
x12 = x0 + x0
x13 = x9*x9
x14 = R*T
x17 = x4*x4*x4
x18 = x10*x17
x19 = 2*x18
x22 = 4*x18
x27 = x12*x18
x33 = x14*x13*x9
x34 = x33 + x33
x37 = x19*x8
x38 = x17*x9
x39 = x10*x38
N = self.N
hessian = zeros((N, N)) if self.vectorized else [[0.0]*N for _ in range(N)]
for i in range(N):
for j in range(N):
# TODO optimize this - symmetric, others
x15 = d_epsilons[i]
x16 = d_epsilons[j]
x20 = x16*x19
x21 = d_Vs[i]
x24 = d_Vs[j]
x23 = x21*x22
x25 = x15*x24
x26 = d_deltas[i]
x28 = d_deltas[j]
x29 = x21*x24
x30 = 8*x18*x29
x31 = x28*x6
x32 = x24*x26
x35 = x34*dbs[j]
x36 = dbs[i]
x40 = x38*da_alphas[i]
x41 = x38*da_alphas[j]
x42 = x21*x41
x43 = x24*x40
x44 = x21*x39
d1 = d2_deltas[i][j] # Derivative(x7, x1, x2)
d2 = d2a_alphas[i][j] # Derivative(x10, x1, x2)
d3 = d2bs[i][j] # Derivative(x3, x1, x2)
d4 = d2_epsilons[i][j] # Derivative(x5, x1, x2)
v = ((x0*x16*x23 + x0*x22*x25 - x0*x26*x41 - x0*x28*x40
- x0*x39*d1 - x12*x42 - x12*x43 + x13*x17*d2 + x15*x20
+ x15*x27*x28 - x15*x41 + x16*x26*x27 - x16*x40
+ x19*x25*x7 + x19*x26*x31 + x19*x29*x7**2 + x20*x21*x7
+ x21*x28*x37 + x21*x35 + x22*x32*x6 + x23*x31
+ x24*x34*x36 - 2*x24*x44 - x28*x44 - x29*x34 + x30*x6
+ x30*x8 + x32*x37 - x32*x39 - x33*x4*d3 - x35*x36
- x39*d4 - x42*x7 - x43*x7)/(x4*x9*(x11*x12 + x11*x7 - x13*x14)))
hessian[i][j] = v
return hessian
[docs] def d2V_dzizjs(self, Z):
r'''Calculates the molar volume second composition derivative
(where the mole fractions do not sum to 1). Verified numerically.
Used in many other derivatives, and for the molar volume second mole
number derivative.
.. math::
\left(\frac{\partial^2 V}{\partial x_i \partial x_j}\right)_{T, P,
x_{k \ne i,j}} = \text{run SymPy code to obtain - very long!}
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
d2V_dzizjs : float
Molar volume second composition derivatives, [m^3/mol]
Notes
-----
The derivation for the derivative is performed as follows using SymPy.
The function source code is an optimized variant created with the `cse`
SymPy function, and hand optimized further.
>>> from sympy import * # doctest:+SKIP
>>> P, T, R, x1, x2 = symbols('P, T, R, x1, x2') # doctest:+SKIP
>>> V, delta, epsilon, a_alpha, b = symbols('V, delta, epsilon, a\ \\alpha, b', cls=Function) # doctest:+SKIP
>>> CUBIC = R*T/(V(x1, x2) - b(x1, x2)) - a_alpha(x1, x2)/(V(x1, x2)*V(x1, x2) + delta(x1, x2)*V(x1, x2) + epsilon(x1, x2)) - P # doctest:+SKIP
>>> solve(diff(CUBIC, x1, x2), Derivative(V(x1, x2), x1, x2)) # doctest:+SKIP
'''
V = Z*self.T*R/self.P
dV_dzs = self.dV_dzs(Z)
depsilon_dzs = self.depsilon_dzs
d2epsilon_dzizjs = self.d2epsilon_dzizjs
ddelta_dzs = self.ddelta_dzs
d2delta_dzizjs = self.d2delta_dzizjs
db_dzs = self.db_dzs
d2bs = self.d2b_dzizjs
da_alpha_dzs = self.da_alpha_dzs
d2a_alpha_dzizjs = self.d2a_alpha_dzizjs
return self._d2V_dij_wrapper(V=V, d_Vs=dV_dzs, dbs=db_dzs, d2bs=d2bs,
d_epsilons=depsilon_dzs, d2_epsilons=d2epsilon_dzizjs,
d_deltas=ddelta_dzs, d2_deltas=d2delta_dzizjs,
da_alphas=da_alpha_dzs, d2a_alphas=d2a_alpha_dzizjs)
[docs] def d2V_dninjs(self, Z):
r'''Calculates the molar volume second mole number derivatives
(where the mole fractions sum to 1). No specific formula is implemented
for this property - it is calculated from the second mole fraction
derivatives.
.. math::
\left(\frac{\partial^2 V}{\partial n_i \partial n_j}\right)_{T, P,
n_{k\ne i,j}} = f\left( \left(\frac{\partial^2 V}{\partial
x_i\partial x_j}\right)_{T, P, x_{k\ne i,j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
d2V_dninjs : float
Molar volume second mole number derivatives, [m^3/mol^3]
'''
V = Z*self.T*R/self.P
dV_dns = self.dV_dns(Z)
depsilon_dns = self.depsilon_dns
d2epsilon_dninjs = self.d2epsilon_dninjs
ddelta_dns = self.ddelta_dns
d2delta_dninjs = self.d2delta_dninjs
db_dns = self.db_dns
d2bs = self.d2b_dninjs
da_alpha_dns = self.da_alpha_dns
d2a_alpha_dninjs = self.d2a_alpha_dninjs
return self._d2V_dij_wrapper(V=V, d_Vs=dV_dns, dbs=db_dns, d2bs=d2bs,
d_epsilons=depsilon_dns, d2_epsilons=d2epsilon_dninjs,
d_deltas=ddelta_dns, d2_deltas=d2delta_dninjs,
da_alphas=da_alpha_dns, d2a_alphas=d2a_alpha_dninjs)
[docs] def dZ_dzs(self, Z):
r'''Calculates the compressibility composition derivatives
(where the mole fractions do not sum to 1). No specific formula is
implemented for this property - it is calculated from the
composition derivative of molar volume, which does have its formula
implemented.
.. math::
\left(\frac{\partial Z}{\partial x_i}\right)_{T, P,
x_{i\ne j}} = \frac{P }{RT}
\left(\frac{\partial V}{\partial x_i}\right)_{T, P, x_{i\ne j}}
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dZ_dzs : float
Compressibility composition derivative, [-]
'''
factor = self.P/(self.T*R)
dV_dzs =self.dV_dzs(Z)
return dV_dzs*factor if self.vectorized else [dV*factor for dV in dV_dzs]
[docs] def dZ_dns(self, Z):
r'''Calculates the compressibility mole number derivatives
(where the mole fractions sum to 1). No specific formula is implemented
for this property - it is calculated from the mole fraction derivative.
.. math::
\left(\frac{\partial Z}{\partial n_i}\right)_{T, P,
n_{i\ne j}} = f\left( \left(\frac{\partial Z}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dZ_dns : float
Compressibility number derivatives, [1/mol]
'''
out = zeros(self.N) if self.vectorized else [0.0]*self.N
return dxs_to_dns(self.dZ_dzs(Z), self.zs, out)
[docs] def dnZ_dns(self, Z):
r'''Calculates the partial compressibility of the specified phase
No specific formula is implemented
for this property - it is calculated from the compressibility
mole fraction derivative.
.. math::
\left(\frac{\partial n Z}{\partial n_i}\right)_{T, P,
n_{i\ne j}} = f\left( \left(\frac{\partial Z}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dnZ_dns : float
Partial compressibility of the mixture of the specified phase,
[-]
'''
out = zeros(self.N) if self.vectorized else [0.0]*self.N
return dxs_to_dn_partials(self.dZ_dzs(Z), self.zs, Z, out)
[docs] def dH_dep_dzs(self, Z):
r'''Calculates the molar departure enthalpy composition derivative
(where the mole fractions do not sum to 1). Verified numerically.
Useful in solving for enthalpy specifications in newton-type methods,
and forms the basis for the molar departure enthalpy mole number
derivative and molar partial departure enthalpy.
.. math::
\left(\frac{\partial H_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}} =
P \frac{d}{d x} V{\left(x \right)} + \frac{2 \left(T \frac{\partial}{\partial T}
\operatorname{a \alpha}{\left(T,x \right)} - \operatorname{a \alpha}{\left(x
\right)}\right) \left(- \delta{\left(x \right)} \frac{d}{d x} \delta{\left(x
\right)} + 2 \frac{d}{d x} \epsilon{\left(x \right)}\right) \operatorname{atanh}
{\left(\frac{2 V{\left(x \right)} + \delta{\left(x \right)}}{\sqrt{\delta^{2}
{\left(x \right)} - 4 \epsilon{\left(x \right)}}} \right)}}{\left(\delta^{2}
{\left(x \right)} - 4 \epsilon{\left(x \right)}\right)^{\frac{3}{2}}}
+ \frac{2 \left(T \frac{\partial}{\partial T} \operatorname{a \alpha}
{\left(T,x \right)} - \operatorname{a \alpha}{\left(x \right)}\right)
\left(\frac{\left(- \delta{\left(x \right)} \frac{d}{d x} \delta{\left(x
\right)} + 2 \frac{d}{d x} \epsilon{\left(x \right)}\right) \left(2
V{\left(x \right)} + \delta{\left(x \right)}\right)}{\left(\delta^{2}{\left(x
\right)} - 4 \epsilon{\left(x \right)}\right)^{\frac{3}{2}}} + \frac{2
\frac{d}{d x} V{\left(x \right)} + \frac{d}{d x} \delta{\left(x \right)}}
{\sqrt{\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x \right)}}}\right)}{\left(
- \frac{\left(2 V{\left(x \right)} + \delta{\left(x \right)}\right)^{2}}{
\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x \right)}} + 1\right) \sqrt{
\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x \right)}}} + \frac{2
\left(T \frac{\partial^{2}}{\partial x\partial T} \operatorname{a \alpha}
{\left(T,x \right)} - \frac{d}{d x} \operatorname{a \alpha}{\left(x \right)}
\right) \operatorname{atanh}{\left(\frac{2 V{\left(x \right)} + \delta{\left(x
\right)}}{\sqrt{\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x \right)}}}
\right)}}{\sqrt{\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x \right)}}}
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dH_dep_dzs : float
Departure enthalpy composition derivatives, [J/mol]
Notes
-----
The derivation for the derivative is performed as follows using SymPy.
The function source code is an optimized variant created with the `cse`
SymPy function, and hand optimized further.
>>> from sympy import * # doctest:+SKIP
>>> P, T, V, R, b, a, delta, epsilon, x = symbols('P, T, V, R, b, a, delta, epsilon, x') # doctest:+SKIP
>>> V, delta, epsilon, a_alpha, b = symbols('V, delta, epsilon, a_alpha, b', cls=Function) # doctest:+SKIP
>>> H_dep = (P*V(x) - R*T + 2/sqrt(delta(x)**2 - 4*epsilon(x))*(T*Derivative(a_alpha(T, x), T) # doctest:+SKIP
... - a_alpha(x))*atanh((2*V(x)+delta(x))/sqrt(delta(x)**2-4*epsilon(x))))
>>> diff(H_dep, x) # doctest:+SKIP
P*Derivative(V(x), x) + 2*(T*Derivative(a \alpha(T, x), T) - a \alpha(x))*(-delta(x)*Derivative(delta(x), x) + 2*Derivative(epsilon(x), x))*atanh((2*V(x) + delta(x))/sqrt(delta(x)**2 - 4*epsilon(x)))/(delta(x)**2 - 4*epsilon(x))**(3/2) + 2*(T*Derivative(a \alpha(T, x), T) - a \alpha(x))*((-delta(x)*Derivative(delta(x), x) + 2*Derivative(epsilon(x), x))*(2*V(x) + delta(x))/(delta(x)**2 - 4*epsilon(x))**(3/2) + (2*Derivative(V(x), x) + Derivative(delta(x), x))/sqrt(delta(x)**2 - 4*epsilon(x)))/((-(2*V(x) + delta(x))**2/(delta(x)**2 - 4*epsilon(x)) + 1)*sqrt(delta(x)**2 - 4*epsilon(x))) + 2*(T*Derivative(a \alpha(T, x), T, x) - Derivative(a \alpha(x), x))*atanh((2*V(x) + delta(x))/sqrt(delta(x)**2 - 4*epsilon(x)))/sqrt(delta(x)**2 - 4*epsilon(x))
'''
P = self.P
T = self.T
N = self.N
ddelta_dzs = self.ddelta_dzs
depsilon_dzs = self.depsilon_dzs
da_alpha_dzs = self.da_alpha_dzs
da_alpha_dT_dzs = self.da_alpha_dT_dzs
dV_dzs = self.dV_dzs(Z)
x0 = V = Z*R*T/P
x2 = self.delta
x3 = x0 + x0 + x2
x4 = self.epsilon
x5 = x2*x2 - 4.0*x4
try:
x6 = x5**-0.5
except:
# VDW has x5 as zero as delta, epsilon = 0
x6 = 1e50
x7 = 2.0*catanh(x3*x6).real
x8 = x9 = self.a_alpha
x10 = T*self.da_alpha_dT - x8
x13 = x6*x6# 1.0/x5
t0 = x6*x7
t1 = x10*t0*x13
t2 = 2.0*x10*x13/(x13*x3*x3 - 1.0)
x3_x13 = x3*x13
dH_dzs = zeros(N) if self.vectorized else [0.0]*N
for i in range(self.N):
x1 = dV_dzs[i]
x11 = ddelta_dzs[i]
x12 = x11*x2 - 2.0*depsilon_dzs[i]
value = (P*x1 - x12*t1 + t2*(x12*x3_x13 - x1 - x1 - x11)
+ t0*(T*da_alpha_dT_dzs[i] - da_alpha_dzs[i]))
dH_dzs[i] = value
return dH_dzs
[docs] def dS_dep_dzs(self, Z):
r'''Calculates the molar departure entropy composition derivative
(where the mole fractions do not sum to 1). Verified numerically.
Useful in solving for entropy specifications in newton-type methods,
and forms the basis for the molar departure entropy mole number
derivative and molar partial departure entropy.
.. math::
\left(\frac{\partial S_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}} = \frac{1}{T}\left(
\left(\frac{\partial H_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
- \left(\frac{\partial G_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dS_dep_dzs : float
Departure entropy composition derivatives, [J/mol/K]
Notes
-----
'''
dH_dep_dzs = self.dH_dep_dzs(Z)
dG_dep_dzs = self.dG_dep_dzs(Z)
T_inv = 1.0/self.T
if self.vectorized:
return T_inv*(dH_dep_dzs - dG_dep_dzs)
return [T_inv*(dH_dep_dzs[i] - dG_dep_dzs[i]) for i in range(self.N)]
[docs] def dS_dep_dns(self, Z):
r'''Calculates the molar departure entropy mole number derivatives
(where the mole fractions sum to 1). No specific formula is implemented
for this property - it is calculated from the mole fraction derivative.
.. math::
\left(\frac{\partial S_{dep}}{\partial n_i}\right)_{T, P,
n_{i\ne j}} = f\left( \left(\frac{\partial S_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dS_dep_dns : float
Departure entropy mole number derivatives, [J/mol^2/K]
'''
out = zeros(self.N) if self.vectorized else [0.0]*self.N
return dxs_to_dns(self.dS_dep_dzs(Z), self.zs, out)
def dP_dns_Vt(self, phase):
# Checked numerically, working. Evaluated at constant temperature and total volume.
r'''from sympy import *
Vt, P, T, R, n1, n2, n3, no = symbols('Vt, P, T, R, n1, n2, n3, no') # doctest:+SKIP
n, P, V, a_alpha, delta, epsilon, b = symbols('n, P, V, a\ \\alpha, delta, epsilon, b', cls=Function) # doctest:+SKIP
da_alpha_dT, d2a_alpha_dT2 = symbols('da_alpha_dT, d2a_alpha_dT2', cls=Function) # doctest:+SKIP
n = no + n1 + n2 + n3
P = R*T/(Vt/n-b(n1, n2, n3)) - a_alpha(T, n1, n2, n3)/((Vt/n)**2 + delta(n1, n2, n3)*(Vt/n)+epsilon(n1, n2, n3))
V = Vt/n
cse(diff(P, n1))
'''
if phase == 'g':
Vt = self.V_g
else:
Vt = self.V_l
T = self.T
b = self.b
N = self.N
a_alpha = self.a_alpha
epsilon = self.epsilon
Vt2 = Vt*Vt
delta = self.delta
x9 = Vt2 + Vt*delta + epsilon
depsilon_dns = self.depsilon_dns
ddelta_dns = self.ddelta_dns
db_dns = self.db_dns
da_alpha_dns = self.da_alpha_dns
t1 = R*T*1.0/((Vt - b)*(Vt - b))
t2 = 1.0/x9
t3 = a_alpha*t2*t2
t4 = t1*Vt -t3*(Vt*delta + Vt2 + Vt2)
dP_dns_Vt = zeros(N) if self.vectorized else [0.0]*N
for i in range(N):
v = (t4 + t1*db_dns[i] + t3*(Vt*ddelta_dns[i] + depsilon_dns[i]) - t2*da_alpha_dns[i])
dP_dns_Vt[i] = v
return dP_dns_Vt
def d2P_dninjs_Vt(self, phase):
if phase == 'g':
Vt = self.V_g
else:
Vt = self.V_l
T, N = self.T, self.N
b = self.b
a_alpha = self.a_alpha
epsilon = self.epsilon
depsilon_dns = self.depsilon_dns
ddelta_dns = self.ddelta_dns
db_dns = self.db_dns
da_alpha_dns = self.da_alpha_dns
d2delta_dninjs = self.d2delta_dninjs
d2epsilon_dninjs = self.d2epsilon_dninjs
d2bs = self.d2b_dninjs
d2a_alpha_dninjs = self.d2a_alpha_dninjs
x0 = self.a_alpha
x1 = self.epsilon
x2 = Vt*Vt
x5 = self.delta
x7 = x1 + x2 + x5*Vt
x7_inv = 1.0/x7
x8 = self.b
x9 = Vt - x8
x11 = Vt + Vt
x12 = R*T
x13 = Vt
x14 = x7_inv*x7_inv
x16 = x2 + x2 + x13*x5
t1 = x0*x14
x9_inv = 1.0/x9
x9_inv2 = x9_inv*x9_inv
x9_inv3 = x9_inv*x9_inv2
t2 = t1*(x11*x5 + 6.0*x2) - x12*x11*x9_inv2
t3 = x12*x9_inv2
t4 = 2.0*x12*x9_inv3
t5 = 2.0*x0*x7_inv*x7_inv*x7_inv
hess = zeros((N, N)) if self.vectorized else [[0.0]*N for _ in range(N)]
for i in range(N):
x15 = ddelta_dns[i]
x17 = -x15*Vt + x16 - depsilon_dns[i]
t50 = -x13*x15
t51 = t5*x17
t52 = t4*(x13 + db_dns[i])
t53 = x14*x17
t54 = x14*da_alpha_dns[i]
t55 = (t51 + t54)
iadd = t1*t50 + t52*x13 - x16*t55
for j in range(i+1):
x18 = ddelta_dns[j]
x19 = x18*Vt + depsilon_dns[j]
v = (t2 + iadd + t1*(Vt*d2delta_dninjs[i][j] + d2epsilon_dninjs[i][j] - x13*x18)
+ t52*db_dns[j] - t53*da_alpha_dns[j] + t55*x19
+ t3*d2bs[i][j] - x7_inv*d2a_alpha_dninjs[i][j])
hess[i][j] = hess[j][i] = v
return hess
def d3P_dninjnks_Vt(self, phase):
if phase == 'g':
Vt = self.V_g
else:
Vt = self.V_l
T, N = self.T, self.N
b = self.b
a_alpha = self.a_alpha
epsilon = self.epsilon
depsilon_dns = self.depsilon_dns
ddelta_dns = self.ddelta_dns
db_dns = self.db_dns
da_alpha_dns = self.da_alpha_dns
d2delta_dninjs = self.d2delta_dninjs
d2epsilon_dninjs = self.d2epsilon_dninjs
d2bs = self.d2b_dninjs
d2a_alpha_dninjs = self.d2a_alpha_dninjs
d3epsilon_dninjnks = self.d3epsilon_dninjnks
d3delta_dninjnks = self.d3delta_dninjnks
d3a_alpha_dninjnks = self.d3a_alpha_dninjnks
d3b_dninjnks = self.d3b_dninjnks
mat = zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N)] for _ in range(N)]
for i in range(N):
for j in range(N):
for k in range(N):
x0 = self.b
x1 = 1.0
x2 = Vt/x1
x3 = -x0 + x2
x4 = 6/x1**4
x5 = Vt*x4
x6 = R*T
x7 = self.a_alpha
x8 = self.epsilon
x9 = Vt**2
x10 = x1**(-2)
x11 = self.delta
x12 = x10*x9 + x11*x2 + x8
x13 = 2/x1**3
x14 = Vt*x13
x15 = Vt*x10
x16 = x6*(x15 + db_dns[k])
x17 = 2/x3**3
x18 = x15 + db_dns[j]
x19 = x17*x6
x20 = x15 + db_dns[i]
x21 = x12**(-2)
x22 = ddelta_dns[i]
x23 = x11*x15 + x13*x9
x24 = -x2*x22 + x23 - depsilon_dns[i]
x25 = ddelta_dns[j]
x26 = -x2*x25 + x23 - depsilon_dns[j]
x27 = ddelta_dns[k]
x28 = -x2*x27 + x23 - depsilon_dns[j]
x29 = da_alpha_dns[k]
x30 = d2delta_dninjs[i][j]
x31 = -x15*x25
x32 = x4*x9
x33 = x11*x14
x34 = -x15*x22 + x32 + x33
x35 = x2*x30 + x31 + x34 + d2epsilon_dninjs[i][j]
x36 = da_alpha_dns[j]
x37 = d2delta_dninjs[i][k]
x38 = -x15*x27
x39 = x2*x37 + x34 + x38 + d2epsilon_dninjs[i][k]
x40 = da_alpha_dns[i]
x41 = d2delta_dninjs[j][k]
x42 = x2*x41 + x31 + x32 + x33 + x38 + d2epsilon_dninjs[j][k]
x43 = 2/x12**3
x44 = x24*x26
x45 = x28*x43
x46 = x43*x7
v = (-x16*x17*(x14 - d2bs[i][j]) + 6*x16*x18*x20/x3**4 - x18*x19*(x14 -d2bs[i][k])
- x19*x20*(x14 - d2bs[j][k]) - x21*x24*d2a_alpha_dninjs[j][k]
- x21*x26*d2a_alpha_dninjs[i][k] - x21*x28*d2a_alpha_dninjs[i][j]
+ x21*x29*x35 + x21*x36*x39 + x21*x40*x42
- x21*x7*(x11*x5 - x14*x22 - x14*x25 - x14*x27 + x15*x30 + x15*x37 + x15*x41
- x2*d3delta_dninjnks[i][j][k] - d3epsilon_dninjnks[i][j][k] + 24*x9/x1**5)
- x24*x36*x45 + x24*x42*x46 + x26*x39*x46 - x26*x40*x45 - x29*x43*x44 + x35*x45*x7
+ x6*(x5 + d3b_dninjnks[i][j][k])/x3**2 - d3a_alpha_dninjnks[i][j][k]/x12 - 6*x28*x44*x7/x12**4)
mat[i][j][k] = v
return mat
[docs] def dH_dep_dns(self, Z):
r'''Calculates the molar departure enthalpy mole number derivatives
(where the mole fractions sum to 1). No specific formula is implemented
for this property - it is calculated from the mole fraction derivative.
.. math::
\left(\frac{\partial H_{dep}}{\partial n_i}\right)_{T, P,
n_{i\ne j}} = f\left( \left(\frac{\partial H_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dH_dep_dns : float
Departure enthalpy mole number derivatives, [J/mol^2]
'''
out = zeros(self.N) if self.vectorized else [0.0]*self.N
return dxs_to_dns(self.dH_dep_dzs(Z), self.zs, out)
[docs] def dnH_dep_dns(self, Z):
r'''Calculates the partial molar departure enthalpy. No specific
formula is implemented for this property - it is calculated from the
mole fraction derivative.
.. math::
\left(\frac{\partial n H_{dep}}{\partial n_i}\right)_{T, P,
n_{i\ne j}} = f\left( \left(\frac{\partial H_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dnH_dep_dns : float
Partial molar departure enthalpies of the phase, [J/mol]
'''
try:
if Z == self.Z_l:
F = self.H_dep_l
else:
F = self.H_dep_g
except:
F = self.H_dep_g
out = zeros(self.N) if self.vectorized else [0.0]*self.N
return dxs_to_dn_partials(self.dH_dep_dzs(Z), self.zs, F, out)
def _G_dep_lnphi_d_helper(self, Z, dbs, depsilons, ddelta, dVs, da_alphas,
G=True):
out = zeros(self.N) if self.vectorized else [0.0]*self.N
return G_dep_lnphi_d_helper(self.T, self.P, self.b, self.delta,
self.epsilon, self.a_alpha, self.N,
Z, dbs, depsilons, ddelta, dVs, da_alphas,
G, out=out)
[docs] def dlnphi_dzs(self, Z):
r'''Calculates the mixture log *fugacity coefficient* mole fraction
derivatives (where the mole fractions do not sum to 1). No specific
formula is implemented for this property - it is calculated from the
mole fraction derivative of Gibbs free energy.
.. math::
\left(\frac{\partial \ln \phi }{\partial x_i}\right)_{T, P,
x_{i\ne j}} = \frac{1}{RT}\left( \left(\frac{\partial G_{dep}}
{\partial x_i}\right)_{T, P, x_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dlnphi_dzs : float
Mixture log fugacity coefficient mole fraction derivatives, [-]
'''
return self._G_dep_lnphi_d_helper(Z, dbs=self.db_dzs, depsilons=self.depsilon_dzs,
ddelta=self.ddelta_dzs, dVs=self.dV_dzs(Z),
da_alphas=self.da_alpha_dzs, G=False)
[docs] def dlnphi_dns(self, Z):
r'''Calculates the mixture log *fugacity coefficient* mole number
derivatives (where the mole fractions sum to 1). No specific formula is
implemented for this property - it is calculated from the mole fraction
derivative of Gibbs free energy.
.. math::
\left(\frac{\partial \ln \phi }{\partial n_i}\right)_{T, P,
n_{i\ne j}} = f\left( \left(\frac{\partial G_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
This property can be converted into a partial molar property to obtain
the individual fugacity coefficients.
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dlnphi_dns : float
Mixture log fugacity coefficient mole number derivatives, [1/mol]
'''
return self._G_dep_lnphi_d_helper(Z, dbs=self.db_dns, depsilons=self.depsilon_dns,
ddelta=self.ddelta_dns, dVs=self.dV_dns(Z),
da_alphas=self.da_alpha_dns, G=False)
[docs] def dG_dep_dzs(self, Z):
r'''Calculates the molar departure Gibbs energy composition derivative
(where the mole fractions do not sum to 1). Verified numerically.
Useful in solving for gibbs minimization calculations or for solving
for the true critical point. Also forms the basis for the molar
departure Gibbs energy mole number derivative and molar partial
departure Gibbs energy.
.. math::
\left(\frac{\partial G_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}} =
P \frac{d}{d x} V{\left(x \right)} - \frac{R T \left(\frac{d}{d x}
V{\left(x \right)} - \frac{d}{d x} b{\left(x \right)}\right)}{
V{\left(x \right)} - b{\left(x \right)}} - \frac{2 \left(- \delta{
\left(x \right)} \frac{d}{d x} \delta{\left(x \right)} + 2 \frac{d}
{d x} \epsilon{\left(x \right)}\right) \operatorname{a \alpha}{
\left(x \right)} \operatorname{atanh}{\left(\frac{2 V{\left(x
\right)}}{\sqrt{\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x
\right)}}} + \frac{\delta{\left(x \right)}}{\sqrt{\delta^{2}{\left(
x \right)} - 4 \epsilon{\left(x \right)}}} \right)}}{\left(
\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x \right)}\right)^{
\frac{3}{2}}} - \frac{2 \operatorname{atanh}{\left(\frac{2 V{\left(
x \right)}}{\sqrt{\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x
\right)}}} + \frac{\delta{\left(x \right)}}{\sqrt{\delta^{2}{\left(
x \right)} - 4 \epsilon{\left(x \right)}}} \right)} \frac{d}{d x}
\operatorname{a \alpha}{\left(x \right)}}{\sqrt{\delta^{2}{\left(x
\right)} - 4 \epsilon{\left(x \right)}}} - \frac{2 \left(\frac{2
\left(- \delta{\left(x \right)} \frac{d}{d x} \delta{\left(x
\right)} + 2 \frac{d}{d x} \epsilon{\left(x \right)}\right)
V{\left(x \right)}}{\left(\delta^{2}{\left(x \right)} - 4 \epsilon{
\left(x \right)}\right)^{\frac{3}{2}}} + \frac{\left(- \delta{\left
(x \right)} \frac{d}{d x} \delta{\left(x \right)} + 2 \frac{d}{d x}
\epsilon{\left(x \right)}\right) \delta{\left(x \right)}}{\left(
\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x \right)}\right)^{
\frac{3}{2}}} + \frac{2 \frac{d}{d x} V{\left(x \right)}}{\sqrt{
\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x \right)}}}
+ \frac{\frac{d}{d x} \delta{\left(x \right)}}{\sqrt{\delta^{2}{
\left(x \right)} - 4 \epsilon{\left(x \right)}}}\right)
\operatorname{a \alpha}{\left(x \right)}}{\left(1 - \left(\frac{2
V{\left(x \right)}}{\sqrt{\delta^{2}{\left(x \right)} - 4 \epsilon{
\left(x \right)}}} + \frac{\delta{\left(x \right)}}{\sqrt{
\delta^{2}{\left(x \right)} - 4 \epsilon{\left(x \right)}}}\right
)^{2}\right) \sqrt{\delta^{2}{\left(x \right)}
- 4 \epsilon{\left(x \right)}}}
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dG_dep_dzs : float
Departure Gibbs free energy composition derivatives, [J/mol]
Notes
-----
The derivation for the derivative is performed as follows using SymPy.
The function source code is an optimized variant created with the `cse`
SymPy function, and hand optimized further.
>>> from sympy import * # doctest:+SKIP
>>> P, T, R, x = symbols('P, T, R, x') # doctest:+SKIP
>>> a_alpha, a, delta, epsilon, V, b, da_alpha_dT = symbols('a\ \\alpha, a, delta, epsilon, V, b, da_alpha_dT', cls=Function) # doctest:+SKIP
>>> S_dep = R*log(P*V(x)/(R*T)) + R*log(V(x)-b(x))+2*da_alpha_dT(x)*atanh((2*V(x)+delta(x))/sqrt(delta(x)**2-4*epsilon(x)))/sqrt(delta(x)**2-4*epsilon(x))-R*log(V(x)) # doctest:+SKIP
>>> H_dep = P*V(x) - R*T + 2*atanh((2*V(x)+delta(x))/sqrt(delta(x)**2-4*epsilon(x)))*(da_alpha_dT(x)*T-a_alpha(x))/sqrt(delta(x)**2-4*epsilon(x)) # doctest:+SKIP
>>> G_dep = simplify(H_dep - T*S_dep) # doctest:+SKIP
>>> diff(G_dep, x) # doctest:+SKIP
P*Derivative(V(x), x) - R*T*(Derivative(V(x), x) - Derivative(b(x), x))/(V(x) - b(x)) - 2*(-delta(x)*Derivative(delta(x), x) + 2*Derivative(epsilon(x), x))*a \alpha(x)*atanh(2*V(x)/sqrt(delta(x)**2 - 4*epsilon(x)) + delta(x)/sqrt(delta(x)**2 - 4*epsilon(x)))/(delta(x)**2 - 4*epsilon(x))**(3/2) - 2*atanh(2*V(x)/sqrt(delta(x)**2 - 4*epsilon(x)) + delta(x)/sqrt(delta(x)**2 - 4*epsilon(x)))*Derivative(a \alpha(x), x)/sqrt(delta(x)**2 - 4*epsilon(x)) - 2*(2*(-delta(x)*Derivative(delta(x), x) + 2*Derivative(epsilon(x), x))*V(x)/(delta(x)**2 - 4*epsilon(x))**(3/2) + (-delta(x)*Derivative(delta(x), x) + 2*Derivative(epsilon(x), x))*delta(x)/(delta(x)**2 - 4*epsilon(x))**(3/2) + 2*Derivative(V(x), x)/sqrt(delta(x)**2 - 4*epsilon(x)) + Derivative(delta(x), x)/sqrt(delta(x)**2 - 4*epsilon(x)))*a \alpha(x)/((1 - (2*V(x)/sqrt(delta(x)**2 - 4*epsilon(x)) + delta(x)/sqrt(delta(x)**2 - 4*epsilon(x)))**2)*sqrt(delta(x)**2 - 4*epsilon(x)))
'''
return self._G_dep_lnphi_d_helper(Z, dbs=self.db_dzs, depsilons=self.depsilon_dzs,
ddelta=self.ddelta_dzs, dVs=self.dV_dzs(Z),
da_alphas=self.da_alpha_dzs, G=True)
[docs] def dG_dep_dns(self, Z):
r'''Calculates the molar departure Gibbs energy mole number derivatives
(where the mole fractions sum to 1). No specific formula is implemented
for this property - it is calculated from the mole fraction derivative.
.. math::
\left(\frac{\partial G_{dep}}{\partial n_i}\right)_{T, P,
n_{i\ne j}} = f\left( \left(\frac{\partial G_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
Apart from the ideal term, this is the formulation for chemical
potential.
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dG_dep_dns : float
Departure Gibbs energy mole number derivatives, [J/mol^2]
'''
return self._G_dep_lnphi_d_helper(Z, dbs=self.db_dns, depsilons=self.depsilon_dns,
ddelta=self.ddelta_dns, dVs=self.dV_dns(Z),
da_alphas=self.da_alpha_dns, G=True)
[docs] def dnG_dep_dns(self, Z):
r'''Calculates the partial molar departure Gibbs energy. No specific
formula is implemented for this property - it is calculated from the
mole fraction derivative.
.. math::
\left(\frac{\partial n G_{dep}}{\partial n_i}\right)_{T, P,
n_{i\ne j}} = f\left( \left(\frac{\partial G_{dep}}{\partial x_i}\right)_{T, P,
x_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dnG_dep_dns : float
Partial molar departure Gibbs energy of the phase, [J/mol]
'''
try:
if Z == self.Z_l:
F = self.G_dep_l
else:
F = self.G_dep_g
except:
F = self.G_dep_g
dG_dns = self.dG_dep_dns(Z)
out = zeros(self.N) if self.vectorized else [0.0]*self.N
return dns_to_dn_partials(dG_dns, F, out)
[docs] def fugacity_coefficients(self, Z):
r'''Generic formula for calculating log fugacity coefficients for each
species in a mixture. Verified numerically. Applicable to all cubic
equations of state which can be cast in the form used here.
Normally this routine is slower than EOS-specific ones, as it does not
make assumptions that certain parameters are zero or equal to other
parameters.
.. math::
\left(\frac{\partial n \ln \phi}{\partial n_i}
\right)_{n_{k \ne i}} = \ln \phi _i = \ln \phi +
n \left(\frac{\partial \ln \phi}{\partial n_i}
\right)_{n_{k\ne i}}
.. math::
\left(\frac{\partial \ln \phi }{\partial n_i}\right)_{T, P,
n_{i\ne j}} = \frac{1}{RT}\left( \left(\frac{\partial G_{dep}}
{\partial n_i}\right)_{T, P, n_{i\ne j}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
log_phis : float
Log fugacity coefficient for each species, [-]
'''
zs = self.zs
try:
if Z == self.Z_l:
F = self.phi_l
else:
F = self.phi_g
except:
F = self.phi_g
# This conversion seems numerically safe anyway
try:
logF = log(F)
except:
logF = -690.7755278982137
out = zeros(self.N) if self.vectorized else [0.0]*self.N
log_phis = dns_to_dn_partials(self.dlnphi_dns(Z), logF, out)
return log_phis
def _d2_G_dep_lnphi_d2_helper(self, V, d_Vs, d2Vs, dbs, d2bs, d_epsilons, d2_epsilons,
d_deltas, d2_deltas, da_alphas, d2a_alphas, G=True):
T, P = self.T, self.P
N = self.N
RT = T*R
RT_inv = 1.0/RT
hess = zeros((N, N)) if self.vectorized else [[0.0]*N for _ in range(N)]
for i in range(N):
for j in range(N):
# x1: i
# x2: j
x0 = V# V(x1, x2)
x3 = d2Vs[i][j] #Derivative(x0, x1, x2)
x4 = self.b#b(x1, x2)
x5 = x0 - x4
x6 = R*T
x7 = d_Vs[i] #Derivative(x0, x1)
x8 = d_Vs[j] #Derivative(x0, x2)
x9 = self.delta#delta(x1, x2)
x10 = self.epsilon#epsilon(x1, x2)
x11 = -4*x10 + x9**2
if x11 == 0.0:
x11 = 1e-100
x12 = 1/sqrt(x11)
x13 = self.a_alpha#alpha(x1, x2)
x14 = 2*x0
x15 = x14 + x9
x16 = catanh(x12*x15).real
x17 = 2*x16
x18 = d_deltas[i] #Derivative(x9, x1)
x19 = x18*x9 - 2*d_epsilons[i]#Derivative(x10, x1)
x20 = da_alphas[j]#Derivative(x13, x2)
x21 = x17/x11**(3/2)
x22 = d_deltas[j]#Derivative(x9, x2)
x23 = x22*x9 - 2*d_epsilons[j]#Derivative(x10, x2)
x24 = da_alphas[i]#Derivative(x13, x1)
x25 = d2_deltas[i][j]#Derivative(x9, x1, x2)
x26 = x18*x22 + x25*x9 - 2*d2_epsilons[i][j]#Derivative(x10, x1, x2)
x27 = x13*x23
x28 = 2*x7
x29 = 1/x11
x30 = x29*x9
x31 = x19*x29
x32 = x14*x31 - x18 + x19*x30 - x28
x33 = x15**2*x29 - 1
x34 = 2/x33
x35 = x29*x34
x36 = 2*x8
x37 = x23*x29
x38 = x14*x37 - x22 + x23*x30 - x36
x39 = x11**(-2)
x40 = x19*x39
x41 = x13*x38
x42 = x32*x39
x43 = x23*x40
v = (P*x3 - x12*x17*d2a_alphas[i][j] + x13*x21*x26
- x13*x35*(-6*x0*x43 + x14*x26*x29 + x18*x37 + x22*x31
- x25 + x26*x30 + x28*x37 - 2*x3 + x31*x36
- 3*x43*x9) - 4*x15*x41*x42/x33**2 + x19*x20*x21
- x20*x32*x35 + x21*x23*x24 - x24*x35*x38 + x27*x34*x42
+ x34*x40*x41 - x6*(x3 - d2bs[i][j])/x5
+ x6*(x7 - dbs[i])*(x8 - dbs[j])/x5**2
- 6*x16*x19*x27/x11**(5/2))
if not G:
v *= RT_inv
hess[i][j] = v
return hess
[docs] def d2lnphi_dzizjs(self, Z):
r'''Calculates the mixture log *fugacity coefficient* second mole
fraction derivatives (where the mole fractions do not sum to 1). No
specific formula is implemented for this property - it is calculated
from the second mole fraction derivative of Gibbs free energy.
.. math::
\left(\frac{\partial^2 \ln \phi }{\partial x_i\partial x_j}\right)_{T, P,
x_{i,j\ne k}} = \frac{1}{RT}\left( \left(\frac{\partial^2 G_{dep}}
{\partial x_j \partial x_i}\right)_{T, P, x_{i,j\ne k}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
d2lnphi_dzizjs : float
Mixture log fugacity coefficient second mole fraction derivatives,
[-]
'''
V = Z*self.T*R/self.P
dV_dzs = self.dV_dzs(Z)
d2Vs = self.d2V_dzizjs(Z)
depsilon_dzs = self.depsilon_dzs
d2epsilon_dzizjs = self.d2epsilon_dzizjs
ddelta_dzs = self.ddelta_dzs
d2delta_dzizjs = self.d2delta_dzizjs
db_dzs = self.db_dzs
d2bs = self.d2b_dzizjs
da_alpha_dzs = self.da_alpha_dzs
d2a_alpha_dzizjs = self.d2a_alpha_dzizjs
return self._d2_G_dep_lnphi_d2_helper(V=V, d_Vs=dV_dzs, d2Vs=d2Vs, dbs=db_dzs, d2bs=d2bs,
d_epsilons=depsilon_dzs, d2_epsilons=d2epsilon_dzizjs,
d_deltas=ddelta_dzs, d2_deltas=d2delta_dzizjs,
da_alphas=da_alpha_dzs, d2a_alphas=d2a_alpha_dzizjs,
G=False)
[docs] def d2lnphi_dninjs(self, Z):
r'''Calculates the mixture log *fugacity coefficient* second mole
number derivatives (where the mole fraction sum to 1). No
specific formula is implemented for this property - it is calculated
from the second mole fraction derivative of Gibbs free energy.
.. math::
\left(\frac{\partial^2 \ln \phi }{\partial n_i\partial n_j}\right)_{T, P,
n_{i,j\ne k}} f\left( \left(\frac{\partial^2 G_{dep}}
{\partial x_j \partial x_i}\right)_{T, P, x_{i,j\ne k}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
d2lnphi_dninjs : float
Mixture log fugacity coefficient second mole number derivatives,
[-]
'''
V = Z*self.T*R/self.P
dV_dns = self.dV_dns(Z)
d2Vs = self.d2V_dninjs(Z)
depsilon_dns = self.depsilon_dns
d2epsilon_dninjs = self.d2epsilon_dninjs
ddelta_dns = self.ddelta_dns
d2delta_dninjs = self.d2delta_dninjs
db_dns = self.db_dns
d2bs = self.d2b_dninjs
da_alpha_dns = self.da_alpha_dns
d2a_alpha_dninjs = self.d2a_alpha_dninjs
return self._d2_G_dep_lnphi_d2_helper(V=V, d2Vs=d2Vs, d_Vs=dV_dns, dbs=db_dns, d2bs=d2bs,
d_epsilons=depsilon_dns, d2_epsilons=d2epsilon_dninjs,
d_deltas=ddelta_dns, d2_deltas=d2delta_dninjs,
da_alphas=da_alpha_dns, d2a_alphas=d2a_alpha_dninjs,
G=False)
[docs] def d2G_dep_dzizjs(self, Z):
r'''Calculates the molar departure Gibbs energy second composition
derivative (where the mole fractions do not sum to 1). Verified numerically.
Useful in solving for gibbs minimization calculations or for solving
for the true critical point. Also forms the basis for the molar
departure Gibbs energy mole second number derivative.
.. math::
\left(\frac{\partial^2 G_{dep}}{\partial x_j \partial x_i}\right)_{T, P,
x_{i,j\ne k}} = \text{run SymPy code to obtain - very long!}
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
d2G_dep_dzizjs : float
Departure Gibbs free energy second composition derivatives, [J/mol]
Notes
-----
The derivation for the derivative is performed as follows using SymPy.
The function source code is an optimized variant created with the `cse`
SymPy function, and hand optimized further.
>>> from sympy import * # doctest:+SKIP
>>> P, T, R, x1, x2 = symbols('P, T, R, x1, x2') # doctest:+SKIP
>>> a_alpha, delta, epsilon, V, b = symbols('a\ \\alpha, delta, epsilon, V, b', cls=Function) # doctest:+SKIP
>>> da_alpha_dT, d2a_alpha_dT2 = symbols('da_alpha_dT, d2a_alpha_dT2', cls=Function) # doctest:+SKIP
>>> S_dep = R*log(P*V(x1, x2)/(R*T)) + R*log(V(x1, x2)-b(x1, x2))+2*da_alpha_dT(x1, x2)*atanh((2*V(x1, x2)+delta(x1, x2))/sqrt(delta(x1, x2)**2-4*epsilon(x1, x2)))/sqrt(delta(x1, x2)**2-4*epsilon(x1, x2))-R*log(V(x1, x2)) # doctest:+SKIP
>>> H_dep = P*V(x1, x2) - R*T + 2*atanh((2*V(x1, x2)+delta(x1, x2))/sqrt(delta(x1, x2)**2-4*epsilon(x1, x2)))*(da_alpha_dT(x1, x2)*T-a_alpha(x1, x2))/sqrt(delta(x1, x2)**2-4*epsilon(x1, x2)) # doctest:+SKIP
>>> G_dep = simplify(H_dep - T*S_dep) # doctest:+SKIP
>>> diff(G_dep, x1, x2) # doctest:+SKIP
'''
V = Z*self.T*R/self.P
dV_dzs = self.dV_dzs(Z)
d2Vs = self.d2V_dzizjs(Z)
depsilon_dzs = self.depsilon_dzs
d2epsilon_dzizjs = self.d2epsilon_dzizjs
ddelta_dzs = self.ddelta_dzs
d2delta_dzizjs = self.d2delta_dzizjs
db_dzs = self.db_dzs
d2bs = self.d2b_dzizjs
da_alpha_dzs = self.da_alpha_dzs
d2a_alpha_dzizjs = self.d2a_alpha_dzizjs
return self._d2_G_dep_lnphi_d2_helper(V=V, d_Vs=dV_dzs, d2Vs=d2Vs, dbs=db_dzs, d2bs=d2bs,
d_epsilons=depsilon_dzs, d2_epsilons=d2epsilon_dzizjs,
d_deltas=ddelta_dzs, d2_deltas=d2delta_dzizjs,
da_alphas=da_alpha_dzs, d2a_alphas=d2a_alpha_dzizjs,
G=True)
[docs] def dlnphis_dns(self, Z):
r'''Generic formula for calculating the mole number derivaitves of
log fugacity coefficients for each species in a mixture. Verified
numerically. Applicable to all cubic equations of state which can be
cast in the form used here.
.. math::
\left(\frac{\partial \ln \phi_i}{\partial n_i}\right)_{P,
n_{j \ne i}}
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dlnphis_dns : list[list[float]]
Mole number derivatives of log fugacity coefficient for each
species, [-]
Notes
-----
'''
dns = self.dlnphi_dns(Z)
d2ns = self.d2lnphi_dninjs(Z)
ans = d2ns_to_dn2_partials(d2ns, dns)
if self.vectorized: ans = array(ans)
return ans
[docs] def dlnfugacities_dns(self, phase):
r'''Generic formula for calculating the mole number derivaitves of
log fugacities for each species in a mixture. Verified
numerically. Applicable to all cubic equations of state which can be
cast in the form used here.
.. math::
\left(\frac{\partial \ln f_i}{\partial n_i}\right)_{P,
n_{j \ne i}}
Parameters
----------
phase : str
One of 'l' or 'g', [-]
Returns
-------
dlnfugacities_dns : list[list[float]]
Mole number derivatives of log fugacities for each
species, [-]
Notes
-----
'''
zs, N = self.zs, self.N
if phase == 'l':
Z = self.Z_l
try:
fugacities = self.fugacities_l
except AttributeError:
self.fugacities()
fugacities = self.fugacities_l
else:
Z = self.Z_g
try:
fugacities = self.fugacities_g
except AttributeError:
self.fugacities()
fugacities = self.fugacities_g
dlnfugacities_dns = array([list(i) for i in self.dfugacities_dns(phase)])
fugacities_inv = [1.0/fi for fi in fugacities]
for i in range(N):
r = dlnfugacities_dns[i]
for j in range(N):
r[j]*= fugacities_inv[i]
return dlnfugacities_dns
[docs] def dfugacities_dns(self, phase):
r'''Generic formula for calculating the mole number derivaitves of
fugacities for each species in a mixture. Verified
numerically. Applicable to all cubic equations of state which can be
cast in the form used here.
.. math::
\left(\frac{\partial f_i}{\partial n_i}\right)_{P,
n_{j \ne i}}
Parameters
----------
phase : str
One of 'l' or 'g', [-]
Returns
-------
dfugacities_dns : list[list[float]]
Mole number derivatives of fugacities for each species, [-]
Notes
-----
'''
"""
from sympy import *
phifun1, phifun2 = symbols('phifun1, phifun2', cls=Function)
n1, n2, P = symbols('n1, n2, P')
x1 = n1/(n1+n2)
x2 = n2/(n1+n2)
to_diff = x2*P*exp(phifun1(n1))
diff(to_diff, n1).subs({n1+n1: 1})
"""
zs = self.zs
if phase == 'l':
Z = self.Z_l
try:
phis = self.phis_l
except AttributeError:
self.fugacities()
phis = self.phis_l
else:
Z = self.Z_g
try:
phis = self.phis_g
except AttributeError:
self.fugacities()
phis = self.phis_g
dlnphis_dns = self.dlnphis_dns(Z)
P = self.P
N = self.N
matrix = zeros((N, N)) if self.vectorized else [[0.0]*N for i in range(N)]
for i in range(N):
phi_P = P*phis[i]
ziPphi = phi_P*zs[i]
r = dlnphis_dns[i]
for j in range(N):
matrix[i][j] = ziPphi*(dlnphis_dns[j][i] - 1.0)
matrix[i][i] += phi_P
return matrix
[docs] def d2G_dep_dninjs(self, Z):
r'''Calculates the molar departure Gibbs energy mole number derivatives
(where the mole fractions sum to 1). No specific formula is implemented
for this property - it is calculated from the mole fraction derivative.
.. math::
\left(\frac{\partial^2 G_{dep}}{\partial n_j \partial n_i}\right)_{T, P,
n_{i,j\ne k}} = f\left( \left(\frac{\partial^2 G_{dep}}{\partial x_j \partial x_i}\right)_{T, P,
x_{i,j\ne k}}
\right)
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
d2G_dep_dninjs : float
Departure Gibbs energy second mole number derivatives, [J/mol^3]
'''
V = Z*self.T*R/self.P
dV_dns = self.dV_dns(Z)
d2Vs = self.d2V_dninjs(Z)
depsilon_dns = self.depsilon_dns
d2epsilon_dninjs = self.d2epsilon_dninjs
ddelta_dns = self.ddelta_dns
d2delta_dninjs = self.d2delta_dninjs
db_dns = self.db_dns
d2bs = self.d2b_dninjs
da_alpha_dns = self.da_alpha_dns
d2a_alpha_dninjs = self.d2a_alpha_dninjs
return self._d2_G_dep_lnphi_d2_helper(V=V, d2Vs=d2Vs, d_Vs=dV_dns, dbs=db_dns, d2bs=d2bs,
d_epsilons=depsilon_dns, d2_epsilons=d2epsilon_dninjs,
d_deltas=ddelta_dns, d2_deltas=d2delta_dninjs,
da_alphas=da_alpha_dns, d2a_alphas=d2a_alpha_dninjs,
G=True)
def _d2_A_dep_d2_helper(self, V, d_Vs, d2Vs, dbs, d2bs, d_epsilons,
d2_epsilons, d_deltas, d2_deltas, da_alphas,
d2a_alphas):
# pass
r'''from sympy import * # doctest:+SKIP
P, T, R, x1, x2 = symbols('P, T, R, x1, x2') # doctest:+SKIP
a_alpha, delta, epsilon, V, b = symbols('a\ \\alpha, delta, epsilon, V, b', cls=Function) # doctest:+SKIP
da_alpha_dT, d2a_alpha_dT2 = symbols('da_alpha_dT, d2a_alpha_dT2', cls=Function) # doctest:+SKIP
S_dep = R*log(P*V(x1, x2)/(R*T)) + R*log(V(x1, x2)-b(x1, x2))+2*da_alpha_dT(x1, x2)*atanh((2*V(x1, x2)+delta(x1, x2))/sqrt(delta(x1, x2)**2-4*epsilon(x1, x2)))/sqrt(delta(x1, x2)**2-4*epsilon(x1, x2))-R*log(V(x1, x2)) # doctest:+SKIP
H_dep = P*V(x1, x2) - R*T + 2*atanh((2*V(x1, x2)+delta(x1, x2))/sqrt(delta(x1, x2)**2-4*epsilon(x1, x2)))*(da_alpha_dT(x1, x2)*T-a_alpha(x1, x2))/sqrt(delta(x1, x2)**2-4*epsilon(x1, x2)) # doctest:+SKIP
G_dep = simplify(H_dep - T*S_dep) # doctest:+SKIP
V_dep = V(x1, x2) - R*T/P
U_dep = H_dep - P*V_dep
A_dep = simplify(U_dep - T*S_dep)
'''
T, P = self.T, self.P
b = self.b
N = self.N
RT = T*R
hess = zeros((N, N)) if self.vectorized else [[0.0]*N for _ in range(N)]
for i in range(N):
for j in range(N):
x0 = V
x3 = b
x4 = x0 - x3
x5 = d2Vs[i][j]
x6 = R*T
x7 = d_Vs[i]
x8 = d_Vs[j]
x9 = self.delta
x10 = self.epsilon
x11 = -4*x10 + x9**2
x12 = 1/sqrt(x11)
x13 = self.a_alpha
x14 = 2*x0
x15 = x14 + x9
x16 = catanh(x12*x15).real
x17 = 2*x16
x18 = d_deltas[i]
x19 = x18*x9 - 2*d_epsilons[i]
x20 = da_alphas[j]
x21 = x17/x11**(3/2)
x22 = d_deltas[j]
x23 = x22*x9 - 2*d_epsilons[j]
x24 = da_alphas[i]
x25 = d2_deltas[i][j]
x26 = x18*x22 + x25*x9 - 2*d2_epsilons[i][j]
x27 = x13*x23
x28 = 2*x7
x29 = 1/x11
x30 = x29*x9
x31 = x19*x29
x32 = x14*x31 - x18 + x19*x30 - x28
x33 = x15**2*x29 - 1
x34 = 2/x33
x35 = x29*x34
x36 = 2*x8
x37 = x23*x29
x38 = x14*x37 - x22 + x23*x30 - x36
x39 = x11**(-2)
x40 = x19*x39
x41 = x13*x38
x42 = x32*x39
x43 = x23*x40
v = (-x12*x17*d2a_alphas[i][j] + x13*x21*x26 - x13*x35*(-6*x0*x43
+ x14*x26*x29 + x18*x37 + x22*x31 - x25 + x26*x30 + x28*x37
+ x31*x36 - 3*x43*x9 - 2*x5) - 4*x15*x41*x42/x33**2
+ x19*x20*x21 - x20*x32*x35 + x21*x23*x24 - x24*x35*x38 + x27*x34*x42
+ x34*x40*x41 - x6*(x5 - d2bs[i][j])/x4 + x6*(x7 - dbs[i])*(x8 - dbs[j])/x4**2 - 6*x16*x19*x27/x11**(5/2.))
hess[i][j] = v
return hess
def d2A_dep_dninjs(self, Z):
V = Z*self.T*R/self.P
dV_dns = self.dV_dns(Z)
d2Vs = self.d2V_dninjs(Z)
depsilon_dns = self.depsilon_dns
d2epsilon_dninjs = self.d2epsilon_dninjs
ddelta_dns = self.ddelta_dns
d2delta_dninjs = self.d2delta_dninjs
db_dns = self.db_dns
d2bs = self.d2b_dninjs
da_alpha_dns = self.da_alpha_dns
d2a_alpha_dninjs = self.d2a_alpha_dninjs
return self._d2_A_dep_d2_helper(V=V, d2Vs=d2Vs, d_Vs=dV_dns, dbs=db_dns, d2bs=d2bs,
d_epsilons=depsilon_dns, d2_epsilons=d2epsilon_dninjs,
d_deltas=ddelta_dns, d2_deltas=d2delta_dninjs,
da_alphas=da_alpha_dns, d2a_alphas=d2a_alpha_dninjs)
def dA_dep_dns_Vt(self, phase):
# pass
r'''
from sympy import *
Vt, P, T, R, n1, n2, n3 = symbols('Vt, P, T, R, n1, n2, n3') # doctest:+SKIP
P, V, a_alpha, delta, epsilon, b = symbols('P, V, a\ \\alpha, delta, epsilon, b', cls=Function) # doctest:+SKIP
da_alpha_dT, d2a_alpha_dT2 = symbols('da_alpha_dT, d2a_alpha_dT2', cls=Function) # doctest:+SKIP
ns = [n1, n2, n3]
S_dep = R*log(P(n1, n2, n3)*V(n1, n2, n3)/(R*T)) + R*log(V(n1, n2, n3)-b(n1, n2, n3))+2*da_alpha_dT(n1, n2, n3)*atanh((2*V(n1, n2, n3)+delta(n1, n2, n3))/sqrt(delta(n1, n2, n3)**2-4*epsilon(n1, n2, n3)))/sqrt(delta(n1, n2, n3)**2-4*epsilon(n1, n2, n3))-R*log(V(n1, n2, n3))
H_dep = P(n1, n2, n3)*V(n1, n2, n3) - R*T + 2*atanh((2*V(n1, n2, n3)+delta(n1, n2, n3))/sqrt(delta(n1, n2, n3)**2-4*epsilon(n1, n2, n3)))*(da_alpha_dT(n1, n2, n3)*T-a_alpha(n1, n2, n3))/sqrt(delta(n1, n2, n3)**2-4*epsilon(n1, n2, n3))
G_dep = simplify(H_dep - T*S_dep)
V_dep = V(n1, n2, n3) - R*T/P(n1, n2, n3)
U_dep = H_dep - P(n1, n2, n3)*V_dep
A_dep = simplify(U_dep - T*S_dep)
expr = diff(A_dep, n1)
for ni in ns:
expr = expr.subs(Derivative(V(n1, n2, n3), ni), -Vt)
expr = simplify(expr)
cse(expr, optimizations='basic')
'''
if phase == 'g':
Vt = self.V_g
else:
Vt = self.V_l
T, N = self.T, self.N
b = self.b
a_alpha = self.a_alpha
epsilon = self.epsilon
depsilon_dns = self.depsilon_dns
ddelta_dns = self.ddelta_dns
db_dns = self.db_dns
da_alpha_dns = self.da_alpha_dns
dP_dns_Vt = self.dP_dns_Vt(phase)
x0 = self.P
x1 = Vt
x2 = self.b
x3 = x1 - x2
x4 = self.delta
x5 = x4**2
x6 = self.epsilon
x7 = 4*x6
x8 = x5 - x7
x9 = x8**(7/2)
x10 = 2*x1
x11 = x10 + x4
x12 = x11**2 - x5 + x7
x13 = Vt*x0
x14 = x12*x3
x15 = R*T*x9
x16 = x14*x15
x17 = self.a_alpha
x18 = x0*x10
x19 = x14*catanh(x11*x8**-0.5).real
jac = zeros(N) if self.vectorized else [0.0]*N
for i in range(N):
x20 = ddelta_dns[i]
x21 = x20*x4 - 2*depsilon_dns[i]
x22 = x17*x18
v = (-(-x0*x1*x12*x15*(Vt + db_dns[i]) + x13*x16 - x16*(-x1*dP_dns_Vt[i] + x13)
+ x18*x19*x8**3*da_alpha_dns[i] - x19*x21*x22*x8**2
+ x22*x3*x8**(5/2)*(x11*x21 + x8*(2*Vt - x20)))/(x0*x1*x12*x3*x9))
jac[i] = v
return jac
def d2A_dep_dninjs_Vt(self, phase):
if phase == 'g':
Vt = self.V_g
else:
Vt = self.V_l
T, N = self.T, self.N
b = self.b
a_alpha = self.a_alpha
epsilon = self.epsilon
depsilon_dns = self.depsilon_dns
ddelta_dns = self.ddelta_dns
db_dns = self.db_dns
da_alpha_dns = self.da_alpha_dns
d2delta_dninjs = self.d2delta_dninjs
d2epsilon_dninjs = self.d2epsilon_dninjs
d2bs = self.d2b_dninjs
d2a_alpha_dninjs = self.d2a_alpha_dninjs
dP_dns_Vt = self.dP_dns_Vt(phase)
d2P_dninjs_Vt = self.d2P_dninjs_Vt(phase)
hess = zeros((N, N)) if self.vectorized else [[0.0]*N for i in range(N)]
for i in range(N):
for j in range(i+1):
x0 = self.P
x1 = x0**2
x2 = Vt#V(n1, n2, n3)
x3 = x2**2
x4 = self.b
x5 = x2 - x4
x6 = x5**2
x7 = self.delta
x8 = x7**2
x9 = self.epsilon
x10 = 4*x9
x11 = -x10 + x8
x12 = x11**(25/2)
x13 = 2*x2
x14 = x13 + x7
x15 = x10 + x14**2 - x8
x16 = x15**2
x17 = x1*x6
x18 = R*T*x12*x16
x19 = x17*x18
x20 = x1*x18*x3
x21 = Vt*x0
x22 = dP_dns_Vt[i]
x23 = -x2*x22 + x21
x24 = 2*Vt
x25 = dP_dns_Vt[j]
x26 = x18*x2*x6
x27 = self.a_alpha
x28 = x17*x3
x29 = 2*x28
x30 = x16*catanh(x14/sqrt(x11)).real
x31 = x29*x30
x32 = ddelta_dns[i]
x33 = x32*x7 - 2*depsilon_dns[i]
x34 = ddelta_dns[j]
x35 = x34*x7 - 2*depsilon_dns[j]
x36 = x33*x35
x37 = da_alpha_dns[j]
x38 = da_alpha_dns[i]
x39 = d2delta_dninjs[i][j]
x40 = x32*x34 + x39*x7 - 2*d2epsilon_dninjs[i][j]
x41 = x11*(x24 - x32) + x13*x33 + x33*x7
x42 = x11*(x24 - x34) + x13*x35 + x35*x7
x43 = x11**(21/2)*x27
x44 = x15*x29
x45 = x43*x44
v = (-(Vt**2*x19 - Vt*x13*x19 + x0*x26*(-Vt*x22 - Vt*x25 + x0*x24
+ x2*d2P_dninjs_Vt[i][j]) + x11**(23/2)*x44*(x37*x41 + x38*x42)
+ x11**12*x31*d2a_alpha_dninjs[i][j] - x11**11*x31*(x27*x40 + x33*x37 + x35*x38)
+ 6*x11**10*x27*x28*x30*x36 + 4*x14*x28*x41*x42*x43 - x18*x21*x23*x6
+ x20*x5*(x24 - d2bs[i][j]) - x20*(Vt + db_dns[i])*(Vt + db_dns[j]) + x23*x25*x26
- x45*(x33*x42 + x35*x41) - x45*(x11**2*(4*Vt + x39) - x11*(x13*x40 - x24*x33
- x24*x35 + x32*x35 + x33*x34 + x40*x7) + 3*x14*x36))/(x1*x12*x16*x3*x6))
hess[i][j] = hess[j][i] = v
return hess
# @property
# def SCp0_l(self):
# S_dep = self.S_dep_l
# S_dep -= R*sum([zi*log(zi) for zi in self.zs if zi > 0.0]) # ideal composition entropy composition
# S_dep -= R*log(self.P/101325.0)
# return S_dep
#
# @property
# def ACp0_l(self):
# return self.A_dep_l - self.T*(self.SCp0_l - self.S_dep_l)
#
# @property
# def SCp0_g(self):
# S_dep = self.S_dep_g
# S_dep -= R*sum([zi*log(zi) for zi in self.zs if zi > 0.0]) # ideal composition entropy composition
# S_dep -= R*log(self.P/101325.0)
# return S_dep
#
# @property
# def ACp0_g(self):
# return self.A_dep_g - self.T*(self.SCp0_g - self.S_dep_g)
#
# def Scomp(self, phase):
# v = self.T*R*sum([zi*log(zi) for zi in self.zs if zi > 0.0]) # ideal composition entropy composition
# v += R*self.T*log(self.P/101325.0)
# return v
#
# @property
# def HCp0_g(self):
# return self.H_dep_g
#
# @property
# def HCp0_l(self):
# return self.H_dep_l
#
# @property
# def GCp0_g(self):
# return self.HCp0_g - self.T*self.SCp0_g
#
# @property
# def GCp0_l(self):
# return self.HCp0_l - self.T*self.SCp0_l
def dScomp_dns(self, phase):
dP_dns_Vt = self.dP_dns_Vt(phase)
mRT = -R*self.T
zs, N = self.zs, self.N
logzs = [log(zi) for zi in zs]
tot = 0.0
for i in range(N):
tot += zs[i]*logzs[i]
const = R*self.T/self.P
out = zeros(N) if self.vectorized else [0.0]*N
for i in range(N):
out[i] = mRT*(tot - logzs[i]) + const*dP_dns_Vt[i]
return out
def d2Scomp_dninjs(self, phase):
'''P_ref = symbols('P_ref')
diff(R*T*log(P(n1, n2, n3)/P_ref), n1, n2)
'''
dP_dns_Vt = self.dP_dns_Vt(phase)
d2P_dninjs_Vt = self.d2P_dninjs_Vt(phase)
P = self.P
RT = R*self.T
const = RT/P
zs, N = self.zs, self.N
logzs = [log(zi) for zi in zs]
hess = zeros((N, N)) if self.vectorized else [[0.0]*N for _ in range(N)]
for i in range(N):
row = []
for j in range(N):
t = sum(2.0*zs[i]*logzs[i] + 3.0*zs[i] for i in range(N))
if i != j:
v = RT*(t - logzs[i] - logzs[j] -4.0)
else:
v = RT*(t - 2*logzs[i] - 3 - (zs[i] - 1.0)/zs[i])
v += const*(d2P_dninjs_Vt[i][j] - dP_dns_Vt[i]*dP_dns_Vt[j]/P)
hess[i][j] = v
return hess
# TODO fix the implementation below, make it work
tot = 0.0
for i in range(N):
tot += zs[i]*logzs[i]
tot2m1 = tot + tot - 1.0
hess = [[RT*(tot2m1 - logzs[i] - logzs[j]) for i in range(N)] for j in range(N)]
return hess
# return d2xs_to_dxdn_partials(hess, zs)
# return d2ns_to_dn2_partials(hess, self.dScomp_dns)
def d2A_dninjs_Vt(self, phase):
if phase == 'g':
Vt = self.V_g
else:
Vt = self.V_l
N, zs = self.N, self.zs
d2A_dep_dninjs_Vt = self.d2A_dep_dninjs_Vt(phase)
d2Scomp_dninjs = self.d2Scomp_dninjs(phase)
hess = zeros((N, N)) if self.vectorized else [[0.0]*N for i in range(N)]
for i in range(N):
for j in range(N):
hess[i][j] = d2Scomp_dninjs[i][j] + d2A_dep_dninjs_Vt[i][j]
return hess
def d2nA_dninjs_Vt(self, phase):
d2ns = [[i+j for i, j in zip(r1, r2)] for r1, r2 in zip(self.d2A_dep_dninjs_Vt(phase), self.d2Scomp_dninjs(phase))]
dns = [i+j for i, j in zip(self.dA_dep_dns_Vt(phase), self.dScomp_dns(phase))]
ans = d2ns_to_dn2_partials(d2ns, dns)
if self.vectorized: ans = array(ans)
return ans
def d2A_dninjs_Vt_another(self, phase):
d2ns = [[i+j for i, j in zip(r1, r2)] for r1, r2 in zip(self.d2A_dep_dninjs_Vt(phase), self.d2Scomp_dninjs(phase))]
if self.vectorized: d2ns = array(d2ns)
return d2ns
# dns = [i+j for i, j in zip(self.dA_dep_dns_Vt(phase), self.dScomp_dns(phase))]
# return d2ns_to_dn2_partials(d2ns, dns)
def _d_main_derivatives_and_departures_dnx(self, V, db_dns, ddelta_dns,
depsilon_dns, da_alpha_dns,
da_alpha_dT_dns,
d2a_alpha_dT2_dns, dV_dns):
T = self.T
Z = (self.P*V)/(R*T)
x0 = self.a_alpha
x2 = self.epsilon
x3 = V
x4 = self.delta
x5 = x2 + x3**2 + x3*x4
x6 = 1/x5
x7 = self.b
x8 = x3 - x7
x14 = x5**(-2)
x15 = self.da_alpha_dT
x16 = x14*x15
x18 = 2*x3 + x4
x23 = x5**(-3)
x24 = 2*x23
x27 = x18**2
x28 = x18*x24
dndP_dT_dsn = []
dndP_dV_dns = []
dnd2P_dT2_dns = []
dnd2P_dV2_dns = []
dnd2P_dTdV_dns = []
for i in range(self.N):
x1 = da_alpha_dT_dns[i]
x9 = dV_dns[i]
x10 = R*(x9 - db_dns[i])
x17 = 2*x10/x8**3
x12 = 2*x9
x11 = ddelta_dns[i]
x21 = x11 + x12
x22 = x0*x21
x13 = x11*x3 + x12*x3 + x4*x9 + depsilon_dns[i]
x25 = x0*x13
x26 = x24*x25
x19 = da_alpha_dns[i]
x20 = x14*x19
dndP_dT = -x1*x6 - x10/x8**2 + x13*x16
dndP_dT_dsn.append(dndP_dT)
dndP_dV = T*x17 + x14*x22 + x18*x20 - x18*x26
dndP_dV_dns.append(dndP_dV)
d2a_alpha_dT2_dn = d2a_alpha_dT2_dns[i]
dnd2P_dT2 = x6*(x13*x6*self.d2a_alpha_dT2 - d2a_alpha_dT2_dn)
dnd2P_dT2_dns.append(dnd2P_dT2)
dnd2P_dV2 = -6*T*x10/x8**4 - 2*x19*x23*x27 + 2*x20 - 2*x22*x28 + 6*x25*x27/x5**4 - 2*x26
dnd2P_dV2_dns.append(dnd2P_dV2)
dnd2P_dTdV = x1*x14*x18 - x13*x15*x28 + x16*x21 + x17
dnd2P_dTdV_dns.append(dnd2P_dTdV)
return dndP_dT_dsn, dndP_dV_dns, dnd2P_dT2_dns, dnd2P_dV2_dns, dnd2P_dTdV_dns
def _d_main_derivatives_and_departures_dn(self, V):
Z = (self.P*V)/(R*self.T)
db_dns = self.db_dns
ddelta_dns = self.ddelta_dns
depsilon_dns = self.depsilon_dns
dV_dns = self.dV_dns(Z)
da_alpha_dns = self.da_alpha_dns
da_alpha_dT_dns = self.da_alpha_dT_dns
d2a_alpha_dT2_dns = self.d2a_alpha_dT2_dns
return self._d_main_derivatives_and_departures_dnx(V, db_dns, ddelta_dns,
depsilon_dns, da_alpha_dns,
da_alpha_dT_dns, d2a_alpha_dT2_dns,
dV_dns)
def _d_main_derivatives_and_departures_dz(self, V):
Z = (self.P*V)/(R*self.T)
db_dzs = self.db_dzs
ddelta_dzs = self.ddelta_dzs
depsilon_dzs = self.depsilon_dzs
dV_dzs = self.dV_dzs(Z)
da_alpha_dzs = self.da_alpha_dzs
da_alpha_dT_dzs = self.da_alpha_dT_dzs
d2a_alpha_dT2_dzs = self.d2a_alpha_dT2_dzs
return self._d_main_derivatives_and_departures_dnx(V, db_dzs, ddelta_dzs,
depsilon_dzs, da_alpha_dzs,
da_alpha_dT_dzs, d2a_alpha_dT2_dzs,
dV_dzs)
def _dnz_derivatives_and_departures(self, V, n=True):
try:
if V == self.V_l:
l = True
else:
l = False
except:
l = False
if n:
f = self._d_main_derivatives_and_departures_dn
else:
f = self._d_main_derivatives_and_departures_dz
d2P_dTdns, d2P_dVdns, d3P_dT2dns, d3P_dV2dns, d3P_dTdVdns = f(V)
# Needed in calculation routines
if l:
(dP_dT, dP_dV, dV_dT, dV_dP, dT_dV, dT_dP, d2P_dT2, d2P_dV2, d2V_dT2,
d2V_dP2, d2T_dV2, d2T_dP2, d2V_dPdT, d2P_dTdV, d2T_dPdV) = (self.dP_dT_l,
self.dP_dV_l, self.dV_dT_l, self.dV_dP_l, self.dT_dV_l, self.dT_dP_l,
self.d2P_dT2_l, self.d2P_dV2_l, self.d2V_dT2_l, self.d2V_dP2_l, self.d2T_dV2_l,
self.d2T_dP2_l, self.d2V_dPdT_l, self.d2P_dTdV_l, self.d2T_dPdV_l)
else:
(dP_dT, dP_dV, dV_dT, dV_dP, dT_dV, dT_dP, d2P_dT2, d2P_dV2, d2V_dT2,
d2V_dP2, d2T_dV2, d2T_dP2, d2V_dPdT, d2P_dTdV, d2T_dPdV) = (self.dP_dT_g,
self.dP_dV_g, self.dV_dT_g, self.dV_dP_g, self.dT_dV_g, self.dT_dP_g,
self.d2P_dT2_g, self.d2P_dV2_g, self.d2V_dT2_g, self.d2V_dP2_g, self.d2T_dV2_g,
self.d2T_dP2_g, self.d2V_dPdT_g, self.d2P_dTdV_g, self.d2T_dPdV_g)
d2V_dTdns = []
d2V_dPdns = []
d2T_dVdns = []
d2T_dPdns = []
d3T_dP2dns = []
d3V_dP2dns = []
d3T_dV2dns = []
d3V_dT2dns = []
d3T_dPdVdns = []
d3V_dPdTdns = []
for i in range(self.N):
d2P_dTdn, d2P_dVdn, d3P_dT2dn, d3P_dV2dn, d3P_dTdVdn = (
d2P_dTdns[i], d2P_dVdns[i], d3P_dT2dns[i], d3P_dV2dns[i], d3P_dTdVdns[i])
# First derivative - one over the other
d2V_dTdn = dP_dT*d2P_dVdn/dP_dV**2 - d2P_dTdn/dP_dV
d2V_dTdns.append(d2V_dTdn)
# dP_dT # f
# dP_dV # g
# Second derivative - one over the other
d2V_dPdn = dV_dT*d2P_dTdn/dP_dT**2 - d2V_dTdn/dP_dT
d2V_dPdns.append(d2V_dPdn)
# f = dV_dT
# g = dP_dT
# Third derivative - inverse of other expression
d2T_dVdn = -d2V_dTdn/dV_dT**2
d2T_dVdns.append(d2T_dVdn)
# Fourth derivative - inverse of other expression
d2T_dPdn = -d2P_dTdn/dP_dT**2
d2T_dPdns.append(d2T_dPdn)
# Fifth derivative - starting to get big
f = d2P_dT2
df = d3P_dT2dn
g = dP_dT
dg = d2P_dTdn
d3T_dP2dn = 3*f*dg/g**4 - df/g**3
d3T_dP2dns.append(d3T_dP2dn)
# Sixth derivative
f = d2P_dV2
df = d3P_dV2dn
g = dP_dV
dg = d2P_dVdn
d3V_dP2dn = 3*f*dg/g**4 - df/g**3
d3V_dP2dns.append(d3V_dP2dn)
# Seventh - crazy
f = d2P_dV2
df = d3P_dV2dn
g = dP_dT
dg = d2P_dTdn
h = dP_dV
dh = d2P_dVdn
k = d2P_dTdV
dk = d3P_dTdVdn
j = d2P_dT2
dj = d3P_dT2dn
d3T_dV2dn = (f*g**2*dg - g**3*df + 2*g**2*h*dk + 2*g**2*k*dh - g*h**2*dj - 4*g*h*k*dg - 2*g*h*j*dh + 3*h**2*j*dg)/g**4
d3T_dV2dns.append(d3T_dV2dn)
# ekghth - crazy
f = d2P_dT2
df = d3P_dT2dn
g = dP_dV
dg = d2P_dVdn
h = dP_dT
dh = d2P_dTdn
k = d2P_dTdV
dk = d3P_dTdVdn
j = d2P_dV2
dj = d3P_dV2dn
d3V_dT2dn = (f*g**2*dg - g**3*df + 2*g**2*h*dk + 2*g**2*k*dh - g*h**2*dj - 4*g*h*k*dg - 2*g*h*j*dh + 3*h**2*j*dg)/g**4
d3V_dT2dns.append(d3V_dT2dn)
# nknth
f = d2P_dTdV
df = d3P_dTdVdn
g = dP_dT
dg = d2P_dTdn
h = dP_dV
dh = d2P_dVdn
k = d2P_dT2
dk = d3P_dT2dn
j = dP_dT
dj = d2P_dTdn
d3T_dPdVdn = 3*(f*g - h*k)*dj/j**4 - (f*dg + g*df - h*dk- k*dh)/j**3
d3T_dPdVdns.append(d3T_dPdVdn)
# tenth
f = d2P_dTdV
df = d3P_dTdVdn
g = dP_dV
dg = d2P_dVdn
h = dP_dT
dh = d2P_dTdn
k = d2P_dV2
dk = d3P_dV2dn
j = dP_dV
dj = d2P_dVdn
d3V_dPdTdn = 3*(f*g - h*k)*dj/j**4 - (f*dg + g*df - h*dk- k*dh)/j**3
d3V_dPdTdns.append(d3V_dPdTdn)
return (d2P_dTdns, d2P_dVdns, d2V_dTdns, d2V_dPdns, d2T_dVdns, d2T_dPdns,
d3P_dT2dns, d3P_dV2dns, d3V_dT2dns, d3V_dP2dns, d3T_dV2dns, d3T_dP2dns,
d3V_dPdTdns, d3P_dTdVdns, d3T_dPdVdns)
[docs] def set_dnzs_derivatives_and_departures(self, n=True, x=True, only_l=False,
only_g=False):
r'''Sets a number of mole number and/or composition partial derivatives
of thermodynamic partial derivatives.
The list of properties set is as follows, with all properties suffixed
with '_l' or '_g'
if `n` is True:
d2P_dTdns, d2P_dVdns, d2V_dTdns, d2V_dPdns, d2T_dVdns, d2T_dPdns,
d3P_dT2dns, d3P_dV2dns, d3V_dT2dns, d3V_dP2dns, d3T_dV2dns, d3T_dP2dns,
d3V_dPdTdns, d3P_dTdVdns, d3T_dPdVdns, dV_dep_dns, dG_dep_dns,
dH_dep_dns, dU_dep_dns, dS_dep_dns, dA_dep_dns
if `x` is True:
d2P_dTdzs, d2P_dVdzs, d2V_dTdzs, d2V_dPdzs, d2T_dVdzs, d2T_dPdzs,
d3P_dT2dzs, d3P_dV2dzs, d3V_dT2dzs, d3V_dP2dzs, d3T_dV2dzs, d3T_dP2dzs,
d3V_dPdTdzs, d3P_dTdVdzs, d3T_dPdVdzs, dV_dep_dzs, dG_dep_dzs,
dH_dep_dzs, dU_dep_dzs, dS_dep_dzs, dA_dep_dzs
Parameters
----------
n : bool, optional
Whether or not to set the mole number derivatives (sums up to one),
[-]
x : bool, optional
Whether or not to set the composition derivatives (does not sum up
to one), [-]
only_l : bool, optional
Whether or not to set only the liquid-like phase properties (if
there are two phases), [-]
only_g : bool, optional
Whether or not to set only the gas-like phase properties (if
there are two phases), [-]
Notes
-----
'''
N = self.N
zs = self.zs
T, P = self.T, self.P
if n and x:
ns = [True, False]
elif n:
ns = [True]
elif x:
ns = [False]
else:
return
if only_l:
phases = ['l']
elif only_g:
phases = ['g']
else:
phases = ['l', 'g']
for n in ns:
for phase in phases:
if phase == 'g':
Z, V = self.Z_g, self.V_g
else:
Z, V = self.Z_l, self.V_l
if n:
V_fun, G_fun, H_fun = self.dV_dns, self.dG_dep_dns, self.dH_dep_dns
else:
V_fun, G_fun, H_fun = self.dV_dzs, self.dG_dep_dzs, self.dH_dep_dzs
(d2P_dTdns, d2P_dVdns, d2V_dTdns, d2V_dPdns, d2T_dVdns, d2T_dPdns,
d3P_dT2dns, d3P_dV2dns, d3V_dT2dns, d3V_dP2dns, d3T_dV2dns, d3T_dP2dns,
d3V_dPdTdns, d3P_dTdVdns, d3T_dPdVdns) = self._dnz_derivatives_and_departures(V, n=n)
# V
dV_dep_dns = V_fun(Z)
# G
dG_dep_dns = G_fun(Z)
# H
dH_dep_dns = H_fun(Z)
# U
dU_dep_dns = [dH_dep_dns[i] - P*dV_dep_dns[i] for i in range(N)]
# S
dS_dep_dns = [(dG_dep_dns[i] - dH_dep_dns[i])/-T for i in range(N)]
# A
dA_dep_dns = [dU_dep_dns[i] - T*dS_dep_dns[i] for i in range(N)]
if n and phase == 'l':
self.d2P_dTdns_l, self.d2P_dVdns_l, self.d2V_dTdns_l = d2P_dTdns, d2P_dVdns, d2V_dTdns
self.d2V_dPdns_l, self.d2T_dVdns_l, self.d2T_dPdns_l = d2V_dPdns, d2T_dVdns, d2T_dPdns
self.d3P_dT2dns_l, self.d3P_dV2dns_l, self.d3V_dT2dns_l = d3P_dT2dns, d3P_dV2dns, d3V_dT2dns
self.d3V_dP2dns_l, self.d3T_dV2dns_l, self.d3T_dP2dns_l = d3V_dP2dns, d3T_dV2dns, d3T_dP2dns
self.d3V_dPdTdns_l, self.d3P_dTdVdns_l, self.d3T_dPdVdns_l = d3V_dPdTdns, d3P_dTdVdns, d3T_dPdVdns
self.dV_dep_dns_l, self.dG_dep_dns_l, self.dH_dep_dns_l = dV_dep_dns, dG_dep_dns, dH_dep_dns
self.dU_dep_dns_l, self.dS_dep_dns_l, self.dA_dep_dns_l = dU_dep_dns, dS_dep_dns, dA_dep_dns
if n and phase == 'g':
self.d2P_dTdns_g, self.d2P_dVdns_g, self.d2V_dTdns_g = d2P_dTdns, d2P_dVdns, d2V_dTdns
self.d2V_dPdns_g, self.d2T_dVdns_g, self.d2T_dPdns_g = d2V_dPdns, d2T_dVdns, d2T_dPdns
self.d3P_dT2dns_g, self.d3P_dV2dns_g, self.d3V_dT2dns_g = d3P_dT2dns, d3P_dV2dns, d3V_dT2dns
self.d3V_dP2dns_g, self.d3T_dV2dns_g, self.d3T_dP2dns_g = d3V_dP2dns, d3T_dV2dns, d3T_dP2dns
self.d3V_dPdTdns_g, self.d3P_dTdVdns_g, self.d3T_dPdVdns_g = d3V_dPdTdns, d3P_dTdVdns, d3T_dPdVdns
self.dV_dep_dns_g, self.dG_dep_dns_g, self.dH_dep_dns_g = dV_dep_dns, dG_dep_dns, dH_dep_dns
self.dU_dep_dns_g, self.dS_dep_dns_g, self.dA_dep_dns_g = dU_dep_dns, dS_dep_dns, dA_dep_dns
if not n and phase == 'g':
self.d2P_dTdzs_g, self.d2P_dVdzs_g, self.d2V_dTdzs_g = d2P_dTdns, d2P_dVdns, d2V_dTdns
self.d2V_dPdzs_g, self.d2T_dVdzs_g, self.d2T_dPdzs_g = d2V_dPdns, d2T_dVdns, d2T_dPdns
self.d3P_dT2dzs_g, self.d3P_dV2dzs_g, self.d3V_dT2dzs_g = d3P_dT2dns, d3P_dV2dns, d3V_dT2dns
self.d3V_dP2dzs_g, self.d3T_dV2dzs_g, self.d3T_dP2dzs_g = d3V_dP2dns, d3T_dV2dns, d3T_dP2dns
self.d3V_dPdTdzs_g, self.d3P_dTdVdzs_g, self.d3T_dPdVdzs_g = d3V_dPdTdns, d3P_dTdVdns, d3T_dPdVdns
self.dV_dep_dzs_g, self.dG_dep_dzs_g, self.dH_dep_dzs_g = dV_dep_dns, dG_dep_dns, dH_dep_dns
self.dU_dep_dzs_g, self.dS_dep_dzs_g, self.dA_dep_dzs_g = dU_dep_dns, dS_dep_dns, dA_dep_dns
if not n and phase == 'l':
self.d2P_dTdzs_l, self.d2P_dVdzs_l, self.d2V_dTdzs_l = d2P_dTdns, d2P_dVdns, d2V_dTdns
self.d2V_dPdzs_l, self.d2T_dVdzs_l, self.d2T_dPdzs_l = d2V_dPdns, d2T_dVdns, d2T_dPdns
self.d3P_dT2dzs_l, self.d3P_dV2dzs_l, self.d3V_dT2dzs_l = d3P_dT2dns, d3P_dV2dns, d3V_dT2dns
self.d3V_dP2dzs_l, self.d3T_dV2dzs_l, self.d3T_dP2dzs_l = d3V_dP2dns, d3T_dV2dns, d3T_dP2dns
self.d3V_dPdTdzs_l, self.d3P_dTdVdzs_l, self.d3T_dPdVdzs_l = d3V_dPdTdns, d3P_dTdVdns, d3T_dPdVdns
self.dV_dep_dzs_l, self.dG_dep_dzs_l, self.dH_dep_dzs_l = dV_dep_dns, dG_dep_dns, dH_dep_dns
self.dU_dep_dzs_l, self.dS_dep_dzs_l, self.dA_dep_dzs_l = dU_dep_dns, dS_dep_dns, dA_dep_dns
[docs] def dlnphis_dP(self, phase):
r'''Generic formula for calculating the pressure derivaitve of
log fugacity coefficients for each species in a mixture. Verified
numerically. Applicable to all cubic equations of state which can be
cast in the form used here.
Normally this routine is slower than EOS-specific ones, as it does not
make assumptions that certain parameters are zero or equal to other
parameters.
.. math::
\left(\frac{\partial \ln \phi_i}{\partial P}\right)_{T,
nj \ne i} = \frac{G_{dep}}{\partial P}_{T, n}
+ \left(\frac{\partial^2 \ln \phi}{\partial P \partial n_i}
\right)_{T, P, n_{j \ne i}}
Parameters
----------
phase : str
One of 'l' or 'g', [-]
Returns
-------
dlnphis_dP : float
Pressure derivatives of log fugacity coefficient for each species,
[1/Pa]
Notes
-----
This expression for the partial derivative of the mixture `lnphi` with
respect to pressure and mole number can be derived as follows; to
convert to the partial molar `lnphi` pressure and temperature
derivative, add ::math::`\frac{G_{dep}/(RT)}{\partial P}_{T, n}`.
>>> from sympy import * # doctest:+SKIP
>>> P, T, R, n = symbols('P, T, R, n') # doctest:+SKIP
>>> a_alpha, a, delta, epsilon, V, b, da_alpha_dT, d2a_alpha_dT2 = symbols('a_alpha, a, delta, epsilon, V, b, da_alpha_dT, d2a_alpha_dT2', cls=Function) # doctest:+SKIP
>>> S_dep = R*log(P*V(n, P)/(R*T)) + R*log(V(n, P)-b(n))+2*da_alpha_dT(n, T)*atanh((2*V(n, P)+delta(n))/sqrt(delta(n)**2-4*epsilon(n)))/sqrt(delta(n)**2-4*epsilon(n))-R*log(V(n, P)) # doctest:+SKIP
>>> H_dep = P*V(n, P) - R*T + 2*atanh((2*V(n, P)+delta(n))/sqrt(delta(n)**2-4*epsilon(n)))*(da_alpha_dT(n, T)*T-a_alpha(n, T))/sqrt(delta(n)**2-4*epsilon(n)) # doctest:+SKIP
>>> G_dep = H_dep - T*S_dep # doctest:+SKIP
>>> lnphi = simplify(G_dep/(R*T)) # doctest:+SKIP
>>> diff(diff(lnphi, P), n) # doctest:+SKIP
P*Derivative(V(n, P), P, n)/(R*T) + Derivative(V(n, P), P, n)/V(n, P) - Derivative(V(n, P), P)*Derivative(V(n, P), n)/V(n, P)**2 - Derivative(V(n, P), P, n)/(V(n, P) - b(n)) - (-Derivative(V(n, P), n) + Derivative(b(n), n))*Derivative(V(n, P), P)/(V(n, P) - b(n))**2 + Derivative(V(n, P), n)/(R*T) - 4*(-2*delta(n)*Derivative(delta(n), n) + 4*Derivative(epsilon(n), n))*a_alpha(n, T)*Derivative(V(n, P), P)/(R*T*(1 - (2*V(n, P)/sqrt(delta(n)**2 - 4*epsilon(n)) + delta(n)/sqrt(delta(n)**2 - 4*epsilon(n)))**2)*(delta(n)**2 - 4*epsilon(n))**2) - 4*a_alpha(n, T)*Derivative(V(n, P), P, n)/(R*T*(1 - (2*V(n, P)/sqrt(delta(n)**2 - 4*epsilon(n)) + delta(n)/sqrt(delta(n)**2 - 4*epsilon(n)))**2)*(delta(n)**2 - 4*epsilon(n))) - 4*Derivative(V(n, P), P)*Derivative(a_alpha(n, T), n)/(R*T*(1 - (2*V(n, P)/sqrt(delta(n)**2 - 4*epsilon(n)) + delta(n)/sqrt(delta(n)**2 - 4*epsilon(n)))**2)*(delta(n)**2 - 4*epsilon(n))) - 4*(2*V(n, P)/sqrt(delta(n)**2 - 4*epsilon(n)) + delta(n)/sqrt(delta(n)**2 - 4*epsilon(n)))*(4*(-delta(n)*Derivative(delta(n), n) + 2*Derivative(epsilon(n), n))*V(n, P)/(delta(n)**2 - 4*epsilon(n))**(3/2) + 2*(-delta(n)*Derivative(delta(n), n) + 2*Derivative(epsilon(n), n))*delta(n)/(delta(n)**2 - 4*epsilon(n))**(3/2) + 4*Derivative(V(n, P), n)/sqrt(delta(n)**2 - 4*epsilon(n)) + 2*Derivative(delta(n), n)/sqrt(delta(n)**2 - 4*epsilon(n)))*a_alpha(n, T)*Derivative(V(n, P), P)/(R*T*(1 - (2*V(n, P)/sqrt(delta(n)**2 - 4*epsilon(n)) + delta(n)/sqrt(delta(n)**2 - 4*epsilon(n)))**2)**2*(delta(n)**2 - 4*epsilon(n))) + R*T*(P*Derivative(V(n, P), P)/(R*T) + V(n, P)/(R*T))*Derivative(V(n, P), n)/(P*V(n, P)**2) - R*T*(P*Derivative(V(n, P), P, n)/(R*T) + Derivative(V(n, P), n)/(R*T))/(P*V(n, P))
'''
if phase == 'g':
V = self.V_g
Z = self.Z_g
dV_dP = self.dV_dP_g
dG_dep_dP = (self.dH_dep_dP_g - self.T*self.dS_dep_dP_g)/(R*self.T)
else:
V = self.V_l
Z = self.Z_l
dV_dP = self.dV_dP_l
dG_dep_dP = (self.dH_dep_dP_l - self.T*self.dS_dep_dP_l)/(R*self.T)
T = self.T
P = self.P
dV_dns = self.dV_dns(Z)
ddelta_dns = self.ddelta_dns
depsilon_dns = self.depsilon_dns
da_alpha_dns = self.da_alpha_dns
db_dns = self.db_dns
d2V_dPdns = self._dnz_derivatives_and_departures(V)[3]# self.d2V_dPdn
x0 = V
x2 = 1/(R*T)
x3 = 1/x0
x6 = dV_dP
x8 = self.b
x9 = x0 - x8
x10 = 1/P
x11 = self.delta
x12 = 2*x0
x13 = x11 + x12
x14 = self.epsilon
x15 = x11**2 - 4*x14
try:
x16 = 1/x15
except ZeroDivisionError:
x16 = 1e50
x17 = x13**2*x16 - 1
x18 = 1/x17
x19 = self.a_alpha
x20 = 4*x16
x21 = x2*x6
x22 = x18*x21
x25 = 8*x19*x16*x16
t50 = 1.0/(x0*x0)
N = self.N
dlnphis_dPs = zeros(N) if self.vectorized else [0.0]*N
for i in range(N):
# number dependent calculations
x1 = dV_dns[i] # Derivative(x0, n)
x7 = x1*t50
x4 = d2V_dPdns[i] #Derivative(x0, P, n) # TODO calculate only this - d2V_dPdn; the T one wants d2V_dTdn
x5 = P*x4
x23 = ddelta_dns[i]# Derivative(x11, n)
x24 = x11*x23 - 2.0*depsilon_dns[i]#Derivative(x14, n)
x26 = x16*x24
dlnphi_dP = (x1*x2 - x10*x3*(x1 + x5) + x10*x7*(P*x6 + x0)
- x13*x21*x25*(2*x1 - x11*x26 - x12*x26 + x23)/x17**2
+ x18*x19*x2*x20*x4 + x2*x5 + x20*x22*da_alpha_dns[i]
- x22*x24*x25 + x3*x4 - x4/x9 - x6*x7 + x6*(x1 - db_dns[i])/x9**2)
dlnphis_dPs[i] = (dlnphi_dP + dG_dep_dP)
return dlnphis_dPs
[docs] def dlnphis_dT(self, phase):
r'''Generic formula for calculating the temperature derivaitve of
log fugacity coefficients for each species in a mixture. Verified
numerically. Applicable to all cubic equations of state which can be
cast in the form used here.
Normally this routine is slower than EOS-specific ones, as it does not
make assumptions that certain parameters are zero or equal to other
parameters.
.. math::
\left(\frac{\partial \ln \phi_i}{\partial T}\right)_{P,
nj \ne i} = \frac{\frac{G_{dep}}{RT}}{\partial T}_{P, n}
+ \left(\frac{\partial^2 \ln \phi}{\partial T \partial n_i}
\right)_{P, n_{j \ne i}}
Parameters
----------
phase : str
One of 'l' or 'g', [-]
Returns
-------
dlnphis_dT : float
Temperature derivatives of log fugacity coefficient for each species,
[1/K]
Notes
-----
This expression for the partial derivative of the mixture `lnphi` with
respect to pressure and mole number can be derived as follows; to
convert to the partial molar `lnphi` pressure and temperature
derivative, add ::math::`\frac{G_{dep}/(RT)}{\partial T}_{P, n}`.
>>> from sympy import * # doctest:+SKIP
>>> P, T, R, n = symbols('P, T, R, n') # doctest:+SKIP
>>> a_alpha, a, delta, epsilon, V, b, da_alpha_dT, d2a_alpha_dT2 = symbols('a_alpha, a, delta, epsilon, V, b, da_alpha_dT, d2a_alpha_dT2', cls=Function) # doctest:+SKIP
>>> S_dep = R*log(P*V(n, T)/(R*T)) + R*log(V(n, T)-b(n))+2*da_alpha_dT(n, T)*atanh((2*V(n, T)+delta(n))/sqrt(delta(n)**2-4*epsilon(n)))/sqrt(delta(n)**2-4*epsilon(n))-R*log(V(n, T)) # doctest:+SKIP
>>> H_dep = P*V(n, T) - R*T + 2*atanh((2*V(n, T)+delta(n))/sqrt(delta(n)**2-4*epsilon(n)))*(da_alpha_dT(n, T)*T-a_alpha(n, T))/sqrt(delta(n)**2-4*epsilon(n)) # doctest:+SKIP
>>> G_dep = H_dep - T*S_dep # doctest:+SKIP
>>> lnphi = simplify(G_dep/(R*T)) # doctest:+SKIP
>>> diff(diff(lnphi, T), n) # doctest:+SKIP
'''
T, P, zs, N = self.T, self.P, self.zs, self.N
if phase == 'g':
V = self.V_g
Z = self.Z_g
dV_dT = self.dV_dT_g
dG_dep_dT = (-T*self.dS_dep_dT_g - self.S_dep_g + self.dH_dep_dT_g)/(R*self.T)
dG_dep_dT -= (-T*self.S_dep_g + self.H_dep_g)/(R*self.T*self.T)
else:
V = self.V_l
Z = self.Z_l
dV_dT = self.dV_dT_l
dG_dep_dT = (-T*self.dS_dep_dT_l - self.S_dep_l + self.dH_dep_dT_l)/(R*self.T)
dG_dep_dT -= (-T*self.S_dep_l + self.H_dep_l)/(R*self.T*self.T)
"""R, T = symbols('R, T')
H, S = symbols('H, S', cls=Function)
print(diff((H(T) - T*S(T))/(R*T), T))
# (-T*Derivative(S(T), T) - S(T) + Derivative(H(T), T))/(R*T) - (-T*S(T) + H(T))/(R*T**2)
"""
d2V_dTdns = self._dnz_derivatives_and_departures(V, n=True)[2]
dV_dns = self.dV_dns(Z)
db_dns = self.db_dns
da_alpha_dns = self.da_alpha_dns
da_alpha_dT_dns = self.da_alpha_dT_dns
ddelta_dns = self.ddelta_dns
depsilon_dns = self.depsilon_dns
x0 = V
x1 = 1/x0
x4 = T**(-2)
x5 = 1/R
x6 = P*x5
x7 = 1/T
x9 = dV_dT
x11 = self.b
x12 = x0 - x11
x13 = self.a_alpha
x15 = self.delta
x16 = self.epsilon
x17 = x15*x15 - 4.0*x16
if x17 == 0.0:
x17 = 1e-100
x18 = 1/sqrt(x17)
x19 = 2*x0
x20 = x15 + x19
x21 = 2*x5
x22 = x21*catanh(x18*x20).real
x23 = x18*x22
x24 = 1/x17
x25 = x20**2*x24 - 1
x26 = 1/x25
x27 = x24*x26
x28 = 4*x27*x5
x29 = x7*x9
x30 = x13*x4
x34 = x7*self.da_alpha_dT
x35 = 8*x13*x29*x5/x17**2
dlnphis_dTs = zeros(N) if self.vectorized else [0.0]*N
for i in range(N):
x2 = d2V_dTdns[i]
x8 = x2*x7
x3 = dV_dns[i]
x10 = x3/x0**2
x14 = da_alpha_dns[i]
x31 = ddelta_dns[i]
x32 = x15*x31 - 2.0*depsilon_dns[i]
x33 = x22*x32/x17**(3/2)
x36 = x24*x32
x37 = -x15*x36 - x19*x36 + 2.0*x3 + x31
x38 = x21*x27*x37
dlnphi_dT = (x1*x2 - x1*(x2 - x3*x7) - x10*x9 + x10*(-x0*x7 + x9)
+ x13*x28*x8 + x14*x23*x4 + x14*x28*x29 - x20*x35*x37/x25**2
- x23*x7*da_alpha_dT_dns[i] - x26*x32*x35 - x3*x4*x6 - x30*x33
- x30*x38 + x33*x34 + x34*x38 + x6*x8 - x2/x12 + x9*(x3 - db_dns[i])/x12**2)
dlnphis_dTs[i] = dlnphi_dT + dG_dep_dT
return dlnphis_dTs
[docs] def dlnphis_dzs(self, Z):
r'''Generic formula for calculating the mole fraction derivaitves of
log fugacity coefficients for each species in a mixture. Verified
numerically. Applicable to all cubic equations of state which can be
cast in the form used here.
.. math::
\left(\frac{\partial \ln \phi_i}{\partial z_i}\right)_{P,
z_{j \ne i}}
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dlnphis_dzs : list[list[float]]
Mole fraction derivatives of log fugacity coefficient for each
species (such that the mole fractions do not sum to 1), [-]
Notes
-----
'''
d2dxs = self.d2lnphi_dzizjs(Z)
d2ns = d2xs_to_dxdn_partials(d2dxs, self.zs)
return array(d2ns) if self.vectorized else d2ns
[docs]class EpsilonZeroMixingRules:
@property
def depsilon_dzs(self):
r'''Helper method for calculating the composition derivatives of
`epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \epsilon}{\partial x_i}\right)_{T, P, x_{i\ne j}}
= 0
Returns
-------
depsilon_dzs : list[float]
Composition derivative of `epsilon` of each component, [m^6/mol^2]
Notes
-----
This derivative is checked numerically.
'''
return zeros(self.N) if self.vectorized else [0.0]*self.N
@property
def depsilon_dns(self):
r'''Helper method for calculating the mole number derivatives of
`epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \epsilon}{\partial n_i}\right)_{T, P, n_{i\ne j}}
= 0
Returns
-------
depsilon_dns : list[float]
Composition derivative of `epsilon` of each component, [m^6/mol^3]
Notes
-----
This derivative is checked numerically.
'''
return zeros(self.N) if self.vectorized else [0.0]*self.N
@property
def d2epsilon_dzizjs(self):
r'''Helper method for calculating the second composition derivatives (hessian)
of `epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \epsilon}{\partial x_i \partial x_j}\right)_{T, P, x_{k\ne i,j}}
= 0
Returns
-------
d2epsilon_dzizjs : list[list[float]]
Composition derivative of `epsilon` of each component, [m^6/mol^2]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return zeros((N, N)) if self.vectorized else [[0.0]*N for i in range(N)]
@property
def d2epsilon_dninjs(self):
r'''Helper method for calculating the second mole number derivatives (hessian) of
`epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \epsilon}{\partial n_i n_j}\right)_{T, P, n_{k\ne i,j}}
= 0
Returns
-------
d2epsilon_dninjs : list[list[float]]
Second composition derivative of `epsilon` of each component, [m^6/mol^4]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return zeros((N, N)) if self.vectorized else [[0.0]*N for i in range(N)]
@property
def d3epsilon_dninjnks(self):
r'''Helper method for calculating the third partial mole number
derivatives of `epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 \epsilon}{\partial n_i \partial n_j \partial n_k }
\right)_{T, P,
n_{m \ne i,j,k}} = 0
Returns
-------
d3epsilon_dninjnks : list[list[list[float]]]
Third mole number derivative of `epsilon` of each component,
[m^6/mol^5]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N)] for _ in range(N)]
# # Python 2/3 compatibility
# try:
# eos.__dict__['d3epsilon_dninjnks'] = d3epsilon_dninjnks
# eos.__dict__['d2epsilon_dninjs'] = d2epsilon_dninjs
# eos.__dict__['d2epsilon_dzizjs'] = d2epsilon_dzizjs
# eos.__dict__['depsilon_dns'] = depsilon_dns
# eos.__dict__['depsilon_dzs'] = depsilon_dzs
# except:
# setattr(eos, 'd3epsilon_dninjnks', d3epsilon_dninjnks)
# setattr(eos, 'd2epsilon_dninjs', d2epsilon_dninjs)
# setattr(eos, 'd2epsilon_dzizjs', d2epsilon_dzizjs)
# setattr(eos, 'depsilon_dns', depsilon_dns)
# setattr(eos, 'depsilon_dzs', depsilon_dzs)
[docs]class PSRKMixingRules:
u = 1.1
A = -0.6466271649250525 # log(1.1/(1.1+1))
A_inv = 1.0/A
[docs] def a_alpha_and_derivatives(self, T, full=True, quick=True,
pure_a_alphas=True):
r'''Method to calculate `a_alpha` and its first and second
derivatives for an EOS with the PSRK mixing rules. Returns
`a_alpha`, `da_alpha_dT`, and `d2a_alpha_dT2`.
For use in some methods, this returns only `a_alpha` if `full` is False.
.. math::
\alpha = bRT \left[ \sum_i \frac{z_i \alpha_i}{b_i RT}
+ \frac{1}{A}\left(\frac{G^E}{RT} + \sum_i z_i \ln
\left(\frac{b}{b_i}\right) \right)\right]
.. math::
\frac{\partial \alpha}{\partial T} = RTb\left[
\sum_i \left(\frac{z_i \frac{\partial \alpha_i}{\partial T}}{RTb_i}
-\frac{z_i\alpha_i}{RT^2b_i} \right)
+ \frac{1}{A}\left(\frac{\frac{\partial G^E}{\partial T}}{RT}
- \frac{G^E}{RT^2} \right)
\right] + \frac{\alpha}{T}
.. math::
\frac{\partial^2 \alpha}{\partial T^2} = b\left[\sum_i
\left(\frac{z_i\frac{\partial^2 \alpha_i}{\partial T^2}}{b_i}
- \frac{2z_i \frac{\partial \alpha_i}{\partial T}}{T b_i}
+ \frac{2z_i\alpha_i}{T^2 b_i}
\right)
+ \frac{2}{T}\left[\sum_i \left(\frac{z_i\frac{\partial \alpha_i}
{\partial T}}{b_i}
- \frac{z_i \alpha_i}{T b_i}
\right)
+ \frac{1}{A}\left(\frac{\partial G^E}{\partial T} - \frac{G^E}{T}
\right)
\right]
+ \frac{1}{A}\left(
\frac{\partial^2 G^E}{\partial T^2} - \frac{2}{T}
\frac{\partial G^E}{\partial T} + 2\frac{G^E}{T^2}
\right)
\right]
Parameters
----------
T : float
Temperature, [K]
full : bool, optional
If False, calculates and returns only `a_alpha`
quick : bool, optional
Only the quick variant is implemented; it is little faster anyhow
pure_a_alphas : bool, optional
Whether or not to recalculate the a_alpha terms of pure components
(for the case of mixtures only) which stay the same as the
composition changes (i.e in a PT flash), [-]
Returns
-------
a_alpha : float
Coefficient calculated by PSRK-specific method, [J^2/mol^2/Pa]
da_alpha_dT : float
Temperature derivative of coefficient calculated by PSRK-specific
method, [J^2/mol^2/Pa/K]
d2a_alpha_dT2 : float
Second temperature derivative of coefficient calculated by
PSRK-specific method, [J^2/mol^2/Pa/K**2]
Notes
-----
'''
if pure_a_alphas:
a_alphas, da_alpha_dTs, d2a_alpha_dT2s = self.a_alpha_and_derivatives_vectorized(T)
self.a_alphas, self.da_alpha_dTs, self.d2a_alpha_dT2s = a_alphas, da_alpha_dTs, d2a_alpha_dT2s
else:
a_alphas, da_alpha_dTs, d2a_alpha_dT2s = self.a_alphas, self.da_alpha_dTs, self.d2a_alpha_dT2s
b, zs, bs = self.b, self.zs, self.bs
ge_model = self.ge_model
if T != ge_model.T:
# TODO make sure this gets set when solve_T is called
ge_model = ge_model.to_T_xs(T, zs)
self._last_ge = ge_model
GE = ge_model.GE()
if full:
dGE_dT = ge_model.dGE_dT()
d2GE_dT2 = ge_model.d2GE_dT2()
T_inv = 1.0/T
T2_inv = T_inv*T_inv
RT_inv = R_inv*T_inv
RT2_inv = R_inv*T2_inv
A_inv = self.A_inv
N = self.N
tot0, tot1, d1tot, d2tot, other = 0.0, 0.0, 0.0, 0.0, 0.0
if full:
for i in range(N):
bi_inv = 1.0/bs[i]
# Main component
tot0 += zs[i]*a_alphas[i]*bi_inv*RT_inv
tot1 += zs[i]*log(b*bi_inv)
d1tot += zs[i]*da_alpha_dTs[i]*RT_inv*bi_inv - zs[i]*a_alphas[i]*RT2_inv*bi_inv
# TODO go back to just using d1tot
# TODO optimize all of this
other += zs[i]*da_alpha_dTs[i]*bi_inv - zs[i]*a_alphas[i]*bi_inv*T_inv
d2tot += (zs[i]*d2a_alpha_dT2s[i]*bi_inv
- 2.0*zs[i]*da_alpha_dTs[i]*T_inv*bi_inv
+ 2.0*zs[i]*a_alphas[i]*T2_inv*bi_inv)
else:
for i in range(N):
bi_inv = 1.0/bs[i]
tot0 += zs[i]*a_alphas[i]*bi_inv*RT_inv
tot1 += zs[i]*log(b*bi_inv)
a_alpha = R*T*b*(tot0 + A_inv*(GE*RT_inv + tot1))
if full:
da_alpha_dT = R*T*b*(d1tot + A_inv*(dGE_dT*RT_inv - GE*RT2_inv)) + a_alpha*T_inv
d2a_alpha_dT2 = b*(d2tot + 2.0*T_inv*(other + A_inv*(dGE_dT - GE*T_inv))
+ A_inv*(d2GE_dT2 - 2.0*T_inv*dGE_dT + 2.0*GE*T2_inv))
return a_alpha, da_alpha_dT, d2a_alpha_dT2
return a_alpha
def solve_T(self, P, V, quick=True, solution=None):
T = GCEOS.solve_T(self, P, V, solution=solution)
if hasattr(self, '_last_ge') and self._last_ge.T == T:
self.ge_model = self._last_ge
del self._last_ge
else:
self.ge_model = self.ge_model.to_T_xs(T, self.zs)
return T
@property
def da_alpha_dzs(self):
raise NotImplementedError("TODO")
@property
def da_alpha_dns(self):
raise NotImplementedError("TODO")
@property
def dna_alpha_dns(self):
raise NotImplementedError("TODO")
@property
def d2a_alpha_dzizjs(self):
raise NotImplementedError("TODO")
@property
def d2a_alpha_dninjs(self):
raise NotImplementedError("TODO")
@property
def d3a_alpha_dzizjzks(self):
raise NotImplementedError("TODO")
@property
def d3a_alpha_dninjnks(self):
raise NotImplementedError("TODO")
@property
def da_alpha_dT_dzs(self):
raise NotImplementedError("TODO")
@property
def da_alpha_dT_dns(self):
raise NotImplementedError("TODO")
@property
def dna_alpha_dT_dns(self):
raise NotImplementedError("TODO")
@property
def d2a_alpha_dT2_dzs(self):
raise NotImplementedError("TODO")
@property
def d2a_alpha_dT2_dns(self):
raise NotImplementedError("TODO")
[docs]class IGMIX(EpsilonZeroMixingRules, GCEOSMIX, IG):
r'''Class for solving the ideal gas [1]_ [2]_ equation of state for a
mixture of any number of compounds. Subclasses :obj:`thermo.eos.IG`. Solves
the EOS on initialization.
Two of `T`, `P`, and `V` are needed to solve the EOS.
.. math::
P =\frac{RT}{V}
Parameters
----------
zs : list[float]
Overall mole fractions of all species, [-]
T : float, optional
Temperature, [K]
P : float, optional
Pressure, [Pa]
V : float, optional
Molar volume, [m^3/mol]
Tcs : list[float], optional
Critical temperatures of all compounds, [K]
Pcs : list[float], optional
Critical pressures of all compounds, [Pa]
omegas : list[float], optional
Acentric factors of all compounds - Not used in this equation of
state!, [-]
kijs : list[list[float]], optional
n*n size list of lists with binary interaction parameters for the
Van der Waals mixing rules, default all 0 and not used[-]
fugacities : bool, optional
Whether or not to calculate fugacity related values (phis, log phis,
and fugacities); default True, [-]
only_l : bool, optional
When true, if there is a liquid and a vapor root, only the liquid
root (and properties) will be set; default False, [-]
only_g : bool, optional
When true, if there is a liquid and a vapor root, only the vapor
root (and properties) will be set; default False, [-]
Examples
--------
T-P initialization, nitrogen-methane at 115 K and 1 MPa:
>>> eos = IGMIX(T=115, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, .008], zs=[0.5, 0.5])
>>> eos.phase, eos.V_g
('g', 0.0009561632010876225)
Notes
-----
Many properties of this object are zero. Many of the arguments are not used
and are provided for consistency only.
References
----------
.. [1] Walas, Stanley M. Phase Equilibria in Chemical Engineering.
Butterworth-Heinemann, 1985.
.. [2] Poling, Bruce E. The Properties of Gases and Liquids. 5th
edition. New York: McGraw-Hill Professional, 2000.
'''
eos_pure = IG
a_alphas = None
da_alpha_dTs = None
d2a_alpha_dT2s = None
nonstate_constants_specific = ()
kwargs_keys = ('kijs',)
model_id = 0
def _zeros1d(self):
return self.zeros1d
def _zeros2d(self):
return self.zeros2d
def _zeros3d(self):
N = self.N
return zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N)] for _ in range(N)]
@property
def a_alpha_roots(self):
return self.zeros1d
@property
def a_alpha_j_rows(self):
return self.zeros1d
@property
def ddelta_dzs(self):
r'''Helper method for calculating the composition derivatives of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \delta}{\partial x_i}\right)_{T, P, x_{i\ne j}}
= 0
Returns
-------
ddelta_dzs : list[float]
Composition derivative of `delta` of each component, [m^3/mol]
'''
return self.zeros1d
@property
def ddelta_dns(self):
r'''Helper method for calculating the mole number derivatives of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \delta}{\partial n_i}\right)_{T, P, n_{i\ne j}}
= 0
Returns
-------
ddelta_dns : list[float]
Mole number derivative of `delta` of each component, [m^3/mol^2]
'''
return self.zeros1d
@property
def depsilon_dzs(self):
r'''Helper method for calculating the composition derivatives of
`epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \epsilon}{\partial x_i}\right)_{T, P, x_{i\ne j}}
= 0
Returns
-------
depsilon_dzs : list[float]
Composition derivative of `epsilon` of each component, [m^6/mol^2]
'''
return self.zeros1d
@property
def depsilon_dns(self):
r'''Helper method for calculating the mole number derivatives of
`epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \epsilon}{\partial n_i}\right)_{T, P, n_{i\ne j}}
= 0
Returns
-------
depsilon_dns : list[float]
Composition derivative of `epsilon` of each component, [m^6/mol^3]
'''
return self.zeros1d
@property
def d2delta_dzizjs(self):
r'''Helper method for calculating the second composition derivatives (hessian) of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \delta}{\partial x_i\partial x_j}\right)_{T, P, x_{k\ne i,j}}
= 0
Returns
-------
d2delta_dzizjs : list[float]
Second Composition derivative of `delta` of each component, [m^3/mol]
'''
return self.zeros2d
@property
def d2delta_dninjs(self):
r'''Helper method for calculating the second mole number derivatives (hessian) of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \delta}{\partial n_i \partial n_j}\right)_{T, P, n_{k\ne i,j}}
= 0
Returns
-------
d2delta_dninjs : list[list[float]]
Second mole number derivative of `delta` of each component, [m^3/mol^3]
'''
return self.zeros2d
@property
def d2epsilon_dzizjs(self):
r'''Helper method for calculating the second composition derivatives (hessian)
of `epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \epsilon}{\partial x_i \partial x_j}\right)_{T, P, x_{k\ne i,j}}
= 0
Returns
-------
d2epsilon_dzizjs : list[list[float]]
Second composition derivative of `epsilon` of each component, [m^6/mol^2]
'''
return self.zeros2d
@property
def d2epsilon_dninjs(self):
r'''Helper method for calculating the second mole number derivatives
(hessian) of `epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \epsilon}{\partial n_i n_j}\right)_{T, P,
n_{k\ne i,j}} = 0
Returns
-------
d2epsilon_dninjs : list[list[float]]
Second mole number derivative of `epsilon` of each component,
[m^6/mol^4]
'''
return self.zeros2d
@property
def d3delta_dninjnks(self):
r'''Helper method for calculating the third partial mole number
derivatives of `delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 \delta}{\partial n_i \partial n_j \partial n_k }
\right)_{T, P,
n_{m \ne i,j,k}} = 0
Returns
-------
d3delta_dninjnks : list[list[list[float]]]
Third mole number derivative of `delta` of each component,
[m^3/mol^4]
'''
return self._zeros3d()
@property
def d3epsilon_dninjnks(self):
r'''Helper method for calculating the third partial mole number
derivatives of `epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 \epsilon}{\partial n_i \partial n_j \partial n_k }
\right)_{T, P,
n_{m \ne i,j,k}} = 0
Returns
-------
d3epsilon_dninjnks : list[list[list[float]]]
Third mole number derivative of `epsilon` of each component,
[m^6/mol^5]
'''
return self._zeros3d()
def __init__(self, zs, T=None, P=None, V=None,
Tcs=None, Pcs=None, omegas=None, kijs=None,
fugacities=True, only_l=False, only_g=False):
self.N = N = len(zs)
self.Tcs = Tcs
self.Pcs = Pcs
self.omegas = omegas
self.zs = zs
self.vectorized = vectorized = type(zs) is ndarray
if vectorized:
self.zeros2d = zeros2d = zeros((N, N))
else:
self.zeros2d = zeros2d = [[0.0]*N for _ in range(N)]
if kijs is None:
kijs = zeros2d
self.kijs = kijs
self.one_minus_kijs = one_minus_kijs(kijs)
self.kwargs = {'kijs': kijs}
self.T = T
self.P = P
self.V = V
self.b = 0.0
self.bs = self.ais = self.zeros1d = self.a_alphas = self.da_alpha_dTs = self.d2a_alpha_dT2s = zeros2d[0]
self.solve(only_l=only_l, only_g=only_g)
if fugacities:
self.fugacities()
def _fast_init_specific(self, other):
self.bs = other.bs
self.ais = other.ais
self.b = other.b
self.zeros1d = self.a_alphas = self.da_alpha_dTs = self.d2a_alpha_dT2s = other.zeros1d
self.zeros2d = other.zeros2d
[docs] def a_alphas_vectorized(self, T):
r'''Method to calculate the pure-component `a_alphas` for the Ideal Gas
EOS. This vectorized implementation is added for extra speed.
.. math::
a\alpha = 0
Parameters
----------
T : float
Temperature, [K]
Returns
-------
a_alphas : list[float]
Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
'''
return self.zeros1d
[docs] def a_alpha_and_derivatives_vectorized(self, T):
r'''Method to calculate the pure-component `a_alphas` and their first
and second derivatives for the Ideal Gas EOS. This vectorized
implementation is added for extra speed.
.. math::
a\alpha = 0
.. math::
\frac{d a\alpha}{dT} = 0
.. math::
\frac{d^2 a\alpha}{dT^2} = 0
Parameters
----------
T : float
Temperature, [K]
Returns
-------
a_alphas : list[float]
Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
da_alpha_dTs : list[float]
Temperature derivative of coefficient calculated by EOS-specific
method, [J^2/mol^2/Pa/K]
d2a_alpha_dT2s : list[float]
Second temperature derivative of coefficient calculated by
EOS-specific method, [J^2/mol^2/Pa/K**2]
'''
return self.zeros1d, self.zeros1d, self.zeros1d
def a_alpha_and_derivatives(self, T, full=True, quick=True,
pure_a_alphas=True):
# Saves time
if full:
return 0.0, 0.0, 0.0
return 0.0
try:
a_alpha_and_derivatives.__doc__ = GCEOSMIX.a_alpha_and_derivatives.__doc__
except:
pass
def fugacity_coefficients(self, Z):
r'''Calculate and return the fugacity coefficients of the ideal-gas
phase (0 by definition).
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
log_phis : float
Log fugacity coefficient for each species, [-]
'''
return self.zeros1d
def dlnphis_dT(self, phase):
r'''Calculate and return the temperature derivative of fugacity
coefficients of the ideal-gas phase (0 by definition).
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dlnphis_dT : float
Temperature derivatives of log fugacity coefficient for each
species, [1/K]
'''
return self.zeros1d
def dlnphis_dP(self, phase):
r'''Calculate and return the pressure derivative of fugacity
coefficients of the ideal-gas phase (0 by definition).
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dlnphis_dP : float
Pressure derivatives of log fugacity coefficient for each
species, [1/Pa]
'''
return self.zeros1d
@property
def a_alpha_ijs(self):
return self.zeros2d
try:
a_alpha_ijs.__doc__ = GCEOSMIX.a_alpha_ijs.__doc__
except:
pass
@property
def da_alpha_dT_ijs(self):
return self.zeros2d
try:
da_alpha_dT_ijs.__doc__ = GCEOSMIX.da_alpha_dT_ijs.__doc__
except:
pass
@property
def d2a_alpha_dT2_ijs(self):
return self.zeros2d
try:
d2a_alpha_dT2_ijs.__doc__ = GCEOSMIX.d2a_alpha_dT2_ijs.__doc__
except:
pass
[docs]class RKMIX(EpsilonZeroMixingRules, GCEOSMIX, RK):
r'''Class for solving the Redlich Kwong [1]_ [2]_ cubic equation of state for a
mixture of any number of compounds. Subclasses :obj:`thermo.eos.RK` . Solves the EOS on
initialization and calculates fugacities for all components in all phases.
Two of `T`, `P`, and `V` are needed to solve the EOS.
.. math::
P =\frac{RT}{V-b}-\frac{a}{V\sqrt{T}(V+b)}
.. math::
a = \sum_i \sum_j z_i z_j {a}_{ij}
.. math::
b = \sum_i z_i b_i
.. math::
a_{ij} = (1-k_{ij})\sqrt{a_{i}a_{j}}
.. math::
a_i =\left(\frac{R^2(T_{c,i})^{2}}{9(\sqrt[3]{2}-1)P_{c,i}} \right)
=\frac{0.42748\cdot R^2(T_{c,i})^{2}}{P_{c,i}}
.. math::
b_i=\left( \frac{(\sqrt[3]{2}-1)}{3}\right)\frac{RT_{c,i}}{P_{c,i}}
=\frac{0.08664\cdot R T_{c,i}}{P_{c,i}}
Parameters
----------
Tcs : list[float]
Critical temperatures of all compounds, [K]
Pcs : list[float]
Critical pressures of all compounds, [Pa]
zs : list[float]
Overall mole fractions of all species, [-]
kijs : list[list[float]], optional
n*n size list of lists with binary interaction parameters for the
Van der Waals mixing rules, default all 0 [-]
T : float, optional
Temperature, [K]
P : float, optional
Pressure, [Pa]
V : float, optional
Molar volume, [m^3/mol]
omegas : list[float], optional
Acentric factors of all compounds - Not used in this equation of
state!, [-]
fugacities : bool, optional
Whether or not to calculate fugacity related values (phis, log phis,
and fugacities); default True, [-]
only_l : bool, optional
When true, if there is a liquid and a vapor root, only the liquid
root (and properties) will be set; default False, [-]
only_g : bool, optional
When true, if there is a liquid and a vapor root, only the vapor
root (and properties) will be set; default False, [-]
Examples
--------
T-P initialization, nitrogen-methane at 115 K and 1 MPa:
>>> eos = RKMIX(T=115, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], zs=[0.5, 0.5], kijs=[[0,0],[0,0]])
>>> eos.V_l, eos.V_g
(4.048414781e-05, 0.00070060605863)
Notes
-----
The PV solution for `T` is iterative.
References
----------
.. [1] Walas, Stanley M. Phase Equilibria in Chemical Engineering.
Butterworth-Heinemann, 1985.
.. [2] Poling, Bruce E. The Properties of Gases and Liquids. 5th
edition. New York: McGraw-Hill Professional, 2000.
'''
eos_pure = RK
kwargs_keys = ('kijs',)
model_id = 10002
def __init__(self, Tcs, Pcs, zs, omegas=None, kijs=None, T=None, P=None, V=None,
fugacities=True, only_l=False, only_g=False):
self.N = N = len(Tcs)
cmps = range(N)
self.Tcs = Tcs
self.Pcs = Pcs
self.omegas = omegas
self.zs = zs
self.vectorized = vectorized = type(zs) is ndarray
if kijs is None:
if vectorized:
kijs = zeros((N, N))
else:
kijs = [[0.0]*N for i in cmps]
self.kijs = kijs
self.one_minus_kijs = one_minus_kijs(kijs)
self.kwargs = {'kijs': kijs}
self.T = T
self.P = P
self.V = V
c1R2_c2R, c2R = self.c1R2_c2R, self.c2R
if vectorized:
self.bs = bs = c2R*Tcs/Pcs
self.ais = c1R2_c2R*Tcs*bs
b = float((bs*zs).sum())
else:
self.bs = bs = [c2R*Tcs[i]/Pcs[i] for i in cmps]
self.ais = [c1R2_c2R*Tcs[i]*bs[i] for i in cmps]
b = 0.0
for i in cmps:
b += bs[i]*zs[i]
self.b = self.delta = b
self.solve(only_l=only_l, only_g=only_g)
if fugacities:
self.fugacities()
def _fast_init_specific(self, other):
b = 0.0
if self.vectorized:
b = float((self.bs*self.zs).sum())
else:
for bi, zi in zip(self.bs, self.zs):
b += bi*zi
self.b = self.delta = b
[docs] def a_alphas_vectorized(self, T):
r'''Method to calculate the pure-component `a_alphas` for the RK EOS.
This vectorized implementation is added for extra speed.
.. math::
a\alpha = \frac{a}{\sqrt{\frac{T}{Tc}}}
Parameters
----------
T : float
Temperature, [K]
Returns
-------
a_alphas : list[float]
Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
Examples
--------
>>> eos = RKMIX(T=115, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.5, 0.5], kijs=[[0,0],[0,0]])
>>> eos.a_alphas_vectorized(115)
[0.1449810919468, 0.30019773677]
'''
return RK_a_alphas_vectorized(T, self.Tcs, self.ais,
a_alphas=zeros(self.N) if self.vectorized else [0.0]*self.N)
[docs] def a_alpha_and_derivatives_vectorized(self, T):
r'''Method to calculate the pure-component `a_alphas` and their first
and second derivatives for the RK EOS. This vectorized implementation
is added for extra speed.
.. math::
a\alpha = \frac{a}{\sqrt{\frac{T}{Tc}}}
.. math::
\frac{d a\alpha}{dT} = - \frac{a}{2 T\sqrt{\frac{T}{Tc}}}
.. math::
\frac{d^2 a\alpha}{dT^2} = \frac{3 a}{4 T^{2}\sqrt{\frac{T}{Tc}}}
Parameters
----------
T : float
Temperature, [K]
Returns
-------
a_alphas : list[float]
Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
da_alpha_dTs : list[float]
Temperature derivative of coefficient calculated by EOS-specific
method, [J^2/mol^2/Pa/K]
d2a_alpha_dT2s : list[float]
Second temperature derivative of coefficient calculated by
EOS-specific method, [J^2/mol^2/Pa/K**2]
Examples
--------
>>> eos = RKMIX(T=115, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.5, 0.5], kijs=[[0,0],[0,0]])
>>> eos.a_alpha_and_derivatives_vectorized(115)
([0.1449810919468, 0.30019773677], [-0.000630352573681, -0.00130520755121], [8.2219900915e-06, 1.7024446320e-05])
'''
N = self.N
if self.vectorized:
a_alphas, da_alpha_dTs, d2a_alpha_dT2s = zeros(N), zeros(N), zeros(N)
else:
a_alphas, da_alpha_dTs, d2a_alpha_dT2s = [0.0]*N, [0.0]*N, [0.0]*N
return RK_a_alpha_and_derivatives_vectorized(T, self.Tcs, self.ais,
a_alphas=a_alphas, da_alpha_dTs=da_alpha_dTs,
d2a_alpha_dT2s=d2a_alpha_dT2s)
def solve_T(self, P, V, solution=None):
if self.N == 1 and type(self) is RKMIX:
self.Tc = self.Tcs[0]
self.Pc = self.Pcs[0]
self.a = self.ais[0]
T = super(type(self).__mro__[-4], self).solve_T(P=P, V=V, solution=solution)
del self.Tc
del self.Pc
del self.a
return T
else:
return super(type(self).__mro__[-3], self).solve_T(P=P, V=V, solution=solution)
@property
def ddelta_dzs(self):
r'''Helper method for calculating the composition derivatives of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \delta}{\partial x_i}\right)_{T, P, x_{i\ne j}}
= b_i
Returns
-------
ddelta_dzs : list[float]
Composition derivative of `delta` of each component, [m^3/mol]
Notes
-----
This derivative is checked numerically.
'''
return self.bs
@property
def ddelta_dns(self):
r'''Helper method for calculating the mole number derivatives of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \delta}{\partial n_i}\right)_{T, P, n_{i\ne j}}
= (b_i - b)
Returns
-------
ddelta_dns : list[float]
Mole number derivative of `delta` of each component, [m^3/mol^2]
Notes
-----
This derivative is checked numerically.
'''
b = self.b
return self.bs - b if self.vectorized else [(bi - b) for bi in self.bs]
@property
def d2delta_dzizjs(self):
r'''Helper method for calculating the second composition derivatives (hessian) of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \delta}{\partial x_i\partial x_j}\right)_{T, P, x_{k\ne i,j}}
= 0
Returns
-------
d2delta_dzizjs : list[float]
Second Composition derivative of `delta` of each component, [m^3/mol]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return zeros((N, N)) if self.vectorized else [[0.0]*N for i in range(N)]
@property
def d2delta_dninjs(self):
r'''Helper method for calculating the second mole number derivatives (hessian) of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \delta}{\partial n_i \partial n_j}\right)_{T, P, n_{k\ne i,j}}
= 2b - b_i - b_j
Returns
-------
d2delta_dninjs : list[list[float]]
Second mole number derivative of `delta` of each component, [m^3/mol^3]
Notes
-----
This derivative is checked numerically.
'''
return self.d2b_dninjs
@property
def d3delta_dninjnks(self):
r'''Helper method for calculating the third partial mole number
derivatives of `delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 \delta}{\partial n_i \partial n_j \partial n_k }
\right)_{T, P,
n_{m \ne i,j,k}} = 2(-3b + b_i + b_j + b_k)
Returns
-------
d3delta_dninjnks : list[list[list[float]]]
Third mole number derivative of `delta` of each component,
[m^3/mol^4]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
out = zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N) ] for _ in range(N)]
return RK_d3delta_dninjnks(self.b, self.bs, N, out)
[docs]class PRMIX(GCEOSMIX, PR):
r'''Class for solving the Peng-Robinson [1]_ [2]_ cubic equation of state
for a mixture of any number of compounds. Subclasses `PR`. Solves the EOS
on initialization and calculates fugacities for all components in all
phases.
Two of `T`, `P`, and `V` are needed to solve the EOS.
.. math::
P = \frac{RT}{v-b}-\frac{a\alpha(T)}{v(v+b)+b(v-b)}
.. math::
a \alpha = \sum_i \sum_j z_i z_j {(a\alpha)}_{ij}
.. math::
(a\alpha)_{ij} = (1-k_{ij})\sqrt{(a\alpha)_{i}(a\alpha)_{j}}
.. math::
b = \sum_i z_i b_i
.. math::
a_i=0.45724\frac{R^2T_{c,i}^2}{P_{c,i}}
.. math::
b_i=0.07780\frac{RT_{c,i}}{P_{c,i}}
.. math::
\alpha(T)_i=[1+\kappa_i(1-\sqrt{T_{r,i}})]^2
.. math::
\kappa_i=0.37464+1.54226\omega_i-0.26992\omega^2_i
Parameters
----------
Tcs : list[float]
Critical temperatures of all compounds, [K]
Pcs : list[float]
Critical pressures of all compounds, [Pa]
omegas : list[float]
Acentric factors of all compounds, [-]
zs : list[float]
Overall mole fractions of all species, [-]
kijs : list[list[float]], optional
n*n size list of lists with binary interaction parameters for the
Van der Waals mixing rules, default all 0 [-]
T : float, optional
Temperature, [K]
P : float, optional
Pressure, [Pa]
V : float, optional
Molar volume, [m^3/mol]
fugacities : bool, optional
Whether or not to calculate fugacity related values (phis, log phis,
and fugacities); default True, [-]
only_l : bool, optional
When true, if there is a liquid and a vapor root, only the liquid
root (and properties) will be set; default False, [-]
only_g : bool, optional
When true, if there is a liquid and a vapor root, only the vapor
root (and properties) will be set; default False, [-]
Examples
--------
T-P initialization, nitrogen-methane at 115 K and 1 MPa:
>>> eos = PRMIX(T=115, P=1E6, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.5, 0.5], kijs=[[0,0],[0,0]])
>>> eos.V_l, eos.V_g
(3.6257362939e-05, 0.00070066592313)
>>> eos.fugacities_l, eos.fugacities_g
([793860.83821, 73468.552253], [436530.92470, 358114.63827])
Notes
-----
For P-V initializations, a numerical solver is used to find T.
References
----------
.. [1] Peng, Ding-Yu, and Donald B. Robinson. "A New Two-Constant Equation
of State." Industrial & Engineering Chemistry Fundamentals 15, no. 1
(February 1, 1976): 59-64. doi:10.1021/i160057a011.
.. [2] Robinson, Donald B., Ding-Yu Peng, and Samuel Y-K Chung. "The
Development of the Peng - Robinson Equation and Its Application to Phase
Equilibrium in a System Containing Methanol." Fluid Phase Equilibria 24,
no. 1 (January 1, 1985): 25-41. doi:10.1016/0378-3812(85)87035-7.
'''
eos_pure = PR
nonstate_constants_specific = ('kappas', )
kwargs_keys = ('kijs',)
model_id = 10200
eos_slots = ('V_l', 'Z_l', 'PIP_l', 'dP_dT_l', 'dP_dV_l', 'dV_dT_l', 'dV_dP_l', 'dT_dV_l', 'dT_dP_l',
'd2P_dT2_l', 'd2P_dV2_l', 'd2P_dTdV_l', 'H_dep_l', 'S_dep_l', 'G_dep_l', 'Cp_dep_l', 'Cv_dep_l',
'T', 'P', 'V', 'b', 'delta', 'epsilon', 'a_alpha', 'da_alpha_dT', 'd2a_alpha_dT2', 'raw_volumes',
'V_g', 'Z_g', 'PIP_g', 'dP_dT_g', 'dP_dV_g', 'dV_dT_g', 'dV_dP_g', 'dT_dV_g', 'dT_dP_g',
'd2P_dT2_g', 'd2P_dV2_g', 'd2P_dTdV_g', 'H_dep_g', 'S_dep_g', 'G_dep_g', 'Cp_dep_g', 'Cv_dep_g', 'phase', 'kwargs',)
eos_mix_slots = ('lnphis_l', 'phis_l', 'fugacities_l', 'N', 'Tcs', 'Pcs', 'omegas', 'zs', 'vectorized', 'kijs', 'one_minus_kijs', 'bs',
'bs', 'ais', 'a_alphas', 'da_alpha_dTs', 'd2a_alpha_dT2s', 'a_alpha_roots', 'a_alpha_j_rows', 'da_alpha_dT_j_rows',
'lnphis_g', 'phis_g', 'fugacities_g')
__slots__ = ('kappas',) + eos_slots + eos_mix_slots
def __init__(self, Tcs, Pcs, omegas, zs, kijs=None, T=None, P=None, V=None,
fugacities=True, only_l=False, only_g=False):
self.N = N = len(Tcs)
cmps = range(N)
self.Tcs = Tcs
self.Pcs = Pcs
self.omegas = omegas
self.zs = zs
self.vectorized = vectorized = type(zs) is ndarray
if kijs is None:
if vectorized:
kijs = zeros((N, N))
else:
kijs = [[0.0]*N for i in cmps]
self.kijs = kijs
self.one_minus_kijs = one_minus_kijs(kijs)
self.kwargs = {'kijs': kijs}
self.T = T
self.P = P
self.V = V
# optimization, unfortunately
c1R2_c2R, c2R = self.c1R2_c2R, self.c2R
# Also tried to store the inverse of Pcs, without success - slows it down
self.vectorized = vectorized = type(Tcs) is ndarray
if vectorized:
self.bs = bs = c2R*Tcs/Pcs
self.ais = c1R2_c2R*Tcs*bs
self.kappas = omegas*(-0.26992*omegas + 1.54226) + 0.37464
b = float((bs*zs).sum())
else:
self.bs = bs = [c2R*Tcs[i]/Pcs[i] for i in cmps]
self.ais = [c1R2_c2R*Tcs[i]*bs[i] for i in cmps]
self.kappas = [omega*(-0.26992*omega + 1.54226) + 0.37464 for omega in omegas]
b = 0.0
for i in cmps:
b += bs[i]*zs[i]
self.b = b
self.delta = 2.0*b
self.epsilon = -b*b
self.solve(only_l=only_l, only_g=only_g)
if fugacities:
self.fugacities()
def _fast_init_specific(self, other):
if self.vectorized:
b = float(prodsum(self.bs, self.zs))
else:
b = 0.0
for bi, zi in zip(self.bs, self.zs):
b += bi*zi
self.kappas, self.b, self.delta, self.epsilon = other.kappas, b, 2.0*b, -b*b
[docs] def a_alphas_vectorized(self, T):
r'''Method to calculate the pure-component `a_alphas` for the PR EOS.
This vectorized implementation is added for extra speed.
.. math::
a\alpha = a \left(\kappa \left(- \frac{T^{0.5}}{Tc^{0.5}}
+ 1\right) + 1\right)^{2}
Parameters
----------
T : float
Temperature, [K]
Returns
-------
a_alphas : list[float]
Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
'''
return PR_a_alphas_vectorized(T, self.Tcs, self.ais, self.kappas,
a_alphas=zeros(self.N) if self.vectorized else [0.0]*self.N)
[docs] def a_alpha_and_derivatives_vectorized(self, T):
r'''Method to calculate the pure-component `a_alphas` and their first
and second derivatives for the PR EOS. This vectorized implementation
is added for extra speed.
.. math::
a\alpha = a \left(\kappa \left(- \frac{T^{0.5}}{Tc^{0.5}}
+ 1\right) + 1\right)^{2}
.. math::
\frac{d a\alpha}{dT} = - \frac{1.0 a \kappa}{T^{0.5} Tc^{0.5}}
\left(\kappa \left(- \frac{T^{0.5}}{Tc^{0.5}} + 1\right) + 1\right)
.. math::
\frac{d^2 a\alpha}{dT^2} = 0.5 a \kappa \left(- \frac{1}{T^{1.5}
Tc^{0.5}} \left(\kappa \left(\frac{T^{0.5}}{Tc^{0.5}} - 1\right)
- 1\right) + \frac{\kappa}{T^{1.0} Tc^{1.0}}\right)
Parameters
----------
T : float
Temperature, [K]
Returns
-------
a_alphas : list[float]
Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
da_alpha_dTs : list[float]
Temperature derivative of coefficient calculated by EOS-specific
method, [J^2/mol^2/Pa/K]
d2a_alpha_dT2s : list[float]
Second temperature derivative of coefficient calculated by
EOS-specific method, [J^2/mol^2/Pa/K**2]
'''
N = self.N
if self.vectorized:
a_alphas, da_alpha_dTs, d2a_alpha_dT2s = zeros(N), zeros(N), zeros(N)
else:
a_alphas, da_alpha_dTs, d2a_alpha_dT2s = [0.0]*N, [0.0]*N, [0.0]*N
return PR_a_alpha_and_derivatives_vectorized(T, self.Tcs, self.ais, self.kappas, a_alphas=a_alphas,
da_alpha_dTs=da_alpha_dTs, d2a_alpha_dT2s=d2a_alpha_dT2s)
@property
def d3a_alpha_dT3(self):
r'''Method to calculate approximately the third temperature derivative
of `a_alpha` for the PR EOS. A rigorous calculation has not been
implemented.
Parameters
----------
T : float
Temperature, [K]
Returns
-------
d3a_alpha_dT3 : float
Third temperature derivative :math:`a \alpha`, [J^2/mol^2/Pa/K^3]
'''
try:
return self._d3a_alpha_dT3
except AttributeError:
pass
tot = 0.0
zs = self.zs
vs = self.d3a_alpha_dT3_vectorized(self.T)
if self.vectorized:
tot += float(dot(zs, vs))
else:
for i in range(self.N):
tot += zs[i]*vs[i]
self._d3a_alpha_dT3 = tot
return tot
[docs] def d3a_alpha_dT3_vectorized(self, T):
r'''Method to calculate the third temperature derivative of
pure-component `a_alphas` for the PR EOS. This vectorized implementation
is added for extra speed.
Parameters
----------
T : float
Temperature, [K]
Returns
-------
d3a_alpha_dT3s : list[float]
Third temperature derivative of coefficient calculated by
EOS-specific method, [J^2/mol^2/Pa/K^3]
'''
ais, kappas, Tcs = self.ais, self.kappas, self.Tcs
T_inv = 1.0/T
N = self.N
d3a_alpha_dT3s = zeros(N) if self.vectorized else [0.0]*N
for i in range(N):
kappa = kappas[i]
x0 = 1.0/Tcs[i]
x1 = sqrt(T*x0)
v = (-ais[i]*0.75*kappa*(kappa*x0 - x1*(kappa*(x1 - 1.0) - 1.0)*T_inv)*T_inv*T_inv)
d3a_alpha_dT3s[i] = v
return d3a_alpha_dT3s
[docs] def fugacity_coefficients(self, Z):
r'''Literature formula for calculating fugacity coefficients for each
species in a mixture. Verified numerically. Applicable to most
derivatives of the Peng-Robinson equation of state as well.
Called by :obj:`fugacities <GCEOSMIX.fugacities>` on initialization, or by a solver routine
which is performing a flash calculation.
.. math::
\ln \hat \phi_i = \frac{B_i}{B}(Z-1)-\ln(Z-B) + \frac{A}{2\sqrt{2}B}
\left[\frac{B_i}{B} - \frac{2}{a\alpha}\sum_i y_i(a\alpha)_{ij}\right]
\ln\left[\frac{Z + (1+\sqrt{2})B}{Z-(\sqrt{2}-1)B}\right]
.. math::
A = \frac{(a\alpha)P}{R^2 T^2}
.. math::
B = \frac{b P}{RT}
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
log_phis : float
Log fugacity coefficient for each species, [-]
'''
N = self.N
return PR_lnphis(self.T, self.P, Z, self.b, self.a_alpha, self.bs, self.a_alpha_j_rows, N,
lnphis=zeros(N) if self.vectorized else [0.0]*N)
a_alpha = self.a_alpha
# a_alpha_ijs = self.a_alpha_ijs
T_inv = 1.0/self.T
bs, b = self.bs, self.b
P_T = self.P*T_inv
A = a_alpha*P_T*R2_inv*T_inv
B = b*P_T*R_inv
# The two log terms need to use a complex log; typically these are
# calculated at "liquid" volume solutions which are unstable
# and cannot exist
try:
x0 = log(Z - B)
except ValueError:
# less than zero
x0 = 0.0
root_two_B = B*root_two
two_root_two_B = root_two_B + root_two_B
ZB = Z + B
try:
x4 = A*log((ZB + root_two_B)/(ZB - root_two_B))
except ValueError:
# less than zero
x4 = 0.0
a_alpha_j_rows = self._a_alpha_j_rows
try:
t50 = 2.0*x4/(a_alpha*two_root_two_B)
except ZeroDivisionError:
return [0.0]*self.N
t51 = (x4 + (Z - 1.0)*two_root_two_B)/(b*two_root_two_B)
if self.vectorized:
return bs*t51 - x0 - t50*a_alpha_j_rows
else:
return [bs[i]*t51 - x0 - t50*a_alpha_j_rows[i]
for i in range(self.N)]
[docs] def dlnphis_dT(self, phase):
r'''Formula for calculating the temperature derivaitve of
log fugacity coefficients for each species in a mixture for the
Peng-Robinson equation of state. Verified numerically.
.. math::
\left(\frac{\partial \ln \phi_i}{\partial T}\right)_{P,
nj \ne i}
Parameters
----------
phase : str
One of 'l' or 'g', [-]
Returns
-------
dlnphis_dT : float
Temperature derivatives of log fugacity coefficient for each species,
[1/K]
Notes
-----
This expression was derived using SymPy and optimized with the `cse`
technique.
'''
zs = self.zs
if phase == 'g':
Z = self.Z_g
dZ_dT = self.dZ_dT_g
else:
Z = self.Z_l
dZ_dT = self.dZ_dT_l
bs, b, N = self.bs, self.b, self.N
T_inv = 1.0/self.T
A = self.a_alpha*self.P*R2_inv*T_inv*T_inv
B = b*self.P*R_inv*T_inv
x2 = T_inv*T_inv
x3 = R_inv
x4 = self.P*b*x3
x5 = x2*x4
x8 = x4*T_inv
x10 = self.a_alpha
x11 = 1.0/self.a_alpha
x12 = self.da_alpha_dT
x13 = root_two
x14 = 1.0/b
x15 = x13 + 1.0 # root_two plus 1
x16 = Z + x15*x8
x17 = x13 - 1.0 # root two minus one
x18 = x16/(x17*x8 - Z)
x19 = log(-x18)
x13x14 = x13*x14
x10x13x14_4 = 0.25*x10*x13x14
x19x3 = x19*x3
x24 = x10x13x14_4*x19x3*x2
x25 = 0.25*x12*x13x14*x19x3*T_inv
x26 = x10x13x14_4*x3*T_inv*(-dZ_dT + x15*x5 - x18*(dZ_dT + x17*x5))/(x16)
x50 = -0.5*x13x14*x19x3*T_inv
x51 = -x11*x12
x52 = (dZ_dT + x5)/(x8 - Z)
x53 = 2.0*x11
x54 = x52/x50
x55 = x24 - x25 + x26
x56 = dZ_dT/x55
x57 = x53*x55
x58 = x14*(dZ_dT - x55)
x59 = x57/x50 + x51
# Composition stuff
a_alpha_j_rows = self._a_alpha_j_rows
da_alpha_dT_j_rows = self._da_alpha_dT_j_rows
d_lnphis_dTs = zeros(N) if self.vectorized else [0.0]*N
for i in range(N):
d_lnphis_dTs[i] = x52 + bs[i]*x58 + x50*(x59*a_alpha_j_rows[i] + da_alpha_dT_j_rows[i])
return d_lnphis_dTs
[docs] def dlnphis_dP(self, phase):
r'''Generic formula for calculating the pressure derivaitve of
log fugacity coefficients for each species in a mixture for the
Peng-Robinson EOS. Verified numerically.
.. math::
\left(\frac{\partial \ln \phi_i}{\partial P}\right)_{T,
nj \ne i}
Parameters
----------
phase : str
One of 'l' or 'g', [-]
Returns
-------
dlnphis_dP : float
Pressure derivatives of log fugacity coefficient for each species,
[1/Pa]
Notes
-----
This expression was derived using SymPy and optimized with the `cse`
technique.
'''
zs = self.zs
if phase == 'l':
Z, dZ_dP = self.Z_l, self.dZ_dP_l
else:
Z, dZ_dP = self.Z_g, self.dZ_dP_g
a_alpha = self.a_alpha
bs, b = self.bs, self.b
T_inv = 1.0/self.T
x2 = 1.0/b
x6 = b*R_inv*T_inv
x8 = self.P*x6
x9 = (dZ_dP - x6)/(x8 - Z)
x13 = Z + root_two_p1*x8
x15 = (a_alpha*root_two*x2*R_inv*T_inv*(dZ_dP + root_two_p1*x6
+ x13*(dZ_dP - root_two_m1*x6)/(root_two_m1*x8 - Z))/(4.0*x13))
x16 = dZ_dP + x15
a_alpha_j_rows = self._a_alpha_j_rows
x50 = -2.0/a_alpha
N = self.N
d_lnphi_dPs = zeros(N) if self.vectorized else [0.0]*N
for i in range(N):
x3 = bs[i]*x2
x10 = x50*a_alpha_j_rows[i]
# d_lnphi_dP = dZ_dP*x3 + x15*(x10 + x3) + x9
d_lnphi_dP = x16*x3 + x15*x10 + x9
d_lnphi_dPs[i] = d_lnphi_dP
return d_lnphi_dPs
def d_lnphi_dzs_analytical0(self, Z, zs):
# TODO try to follow "B.5.2.1 Derivatives of Fugacity Coefficient with Respect to Mole Fraction"
# "Development of an Equation-of-State Thermal Flooding Simulator"
N = self.N
cmps_m1 = range(N-1)
a_alpha = self.a_alpha
a_alpha_ijs = self.a_alpha_ijs
T2 = self.T*self.T
b = self.b
A = a_alpha*self.P/(R2*T2)
B = b*self.P/(R*self.T)
B2 = B*B
Z2 = Z*Z
A_B = A/B
ZmB = Z - B
dZ_dA = (B - Z)/(3.0*Z2 - 2.0*(1.0 - B)*Z + (A - 2.0*B - 3.0*B2))
# 2*(3.0*B + 1)*Z may or may not have Z
# Simple phase stability-testing algorithm in the reduction method.
dZ_dB = ((-Z2 + 2*(3.0*B + 1)*Z) + (A - 2.0*B - 3.0*B2))/(
3.0*Z2 - 2.0*(1.0 - B)*Z + (A - 2.0*B - 3.0*B2))
Sis = []
for i in range(N):
tot = 0.0
for j in range(N):
tot += zs[j]*a_alpha_ijs[i][j]
Sis.append(tot)
Sais = [val/a_alpha for val in Sis]
Sbis = [bi/b for bi in self.bs]
Snc = Sis[-1]
const_A = 2.0*self.P/(R2*T2)
dA_dzis = [const_A*(Si - Snc) for Si in Sis[:-1]]
const_B = 2.0*self.P/(R*self.T)
bnc = self.bs[-1]
dB_dzis = [const_B*(self.bs[i] - bnc) for i in range(N)] # Probably wrong, missing
dZ_dzs = [dZ_dA*dA_dz_i + dZ_dB*dB_dzi for dA_dz_i, dB_dzi in zip(dA_dzis, dB_dzis)]
t1 = (Z2 + 2.0*Z*B - B2)
t2 = clog((Z + (root_two + 1.)*B)/(Z - (root_two - 1.)*B)).real
t3 = t2*-A/(B*two_root_two)
t4 = -t2/(two_root_two*B)
a_nc = a_alpha_ijs[-1][-1] # no idea if this is right
# Have some converns of what Snc really is
dlnphis_dzs_all = []
for i in range(self.N):
Diks = [-A_B*(2.0*Sais[i] - Sbis[i])*(Z*dB_dzis[k] - B*dZ_dzs[k])/t1
for k in cmps_m1]
Ciks = [t3*(2.0*(a_alpha_ijs[i][k] - a_nc)/a_alpha
- 4.0*Sais[i]*(Sais[k] - Snc)
+ Sbis[i]*(Sbis[k] - Snc))
for k in cmps_m1]
x5 = t4*(2.0*Sais[i] - Sbis[i])
Biks = [x5*(dA_dzis[k] - A_B*dB_dzis[k])
for k in cmps_m1 ]
Aiks = [Sbis[i]*(dZ_dzs[k] - (Sbis[k] - Snc)*(Z - 1.0))
- (dZ_dzs[k] - dB_dzis[k])/ZmB
for k in cmps_m1 ]
dlnphis_dzs = [Aik + Bik + Cik + Dik for Aik, Bik, Cik, Dik in zip(Aiks, Biks, Ciks, Diks)]
dlnphis_dzs_all.append(dlnphis_dzs)
return dlnphis_dzs_all
def d_lnphi_dzs_basic_num(self, Z, zs):
all_diffs = []
try:
if self.G_dep_l < self.G_dep_g:
lnphis_ref = self.lnphis_l
else:
lnphis_ref = self.lnphis_g
except:
lnphis_ref = self.lnphis_l if hasattr(self, 'G_dep_l') else self.lnphis_g
for i in range(len(zs)):
zs2 = list(zs)
dz = 1e-7#zs2[i]*3e-
zs2[i] = zs2[i]+dz
# sum_one = sum(zs2)
# zs2 = normalize(zs2)
eos2 = self.to_TP_zs(T=self.T, P=self.P, zs=zs2)
diffs = []
for j in range(len(zs)):
try:
dlnphis = (eos2.lnphis_g[j] - lnphis_ref[j])/dz
except:
dlnphis = (eos2.lnphis_l[j] - lnphis_ref[j])/dz
diffs.append(dlnphis)
all_diffs.append(diffs)
import numpy as np
return np.array(all_diffs).T.tolist()
def d_lnphi_dzs_numdifftools(self, Z, zs):
import numdifftools as nd
import numpy as np
def lnphis_from_zs(zs2):
if isinstance(zs2, np.ndarray):
zs2 = zs2.tolist()
zs2 = normalize(zs2)
# Last row suggests the normalization breaks everything!
# zs2 = normalize(zs2)
# if Z == self.Z_l
try:
return np.array(self.to_TP_zs(T=self.T, P=self.P, zs=zs2).lnphis_l)
except:
return np.array(self.to_TP_zs(T=self.T, P=self.P, zs=zs2).lnphis_g)
Jfun_partial = nd.Jacobian(lnphis_from_zs, step=1e-4, order=2, method='central')
return Jfun_partial(zs)
[docs] def dlnphis_dzs(self, Z):
r'''Calculate and return the mole fraction derivaitves of
log fugacity coefficients for each species in a mixture. This formula
is specific to the Peng-Robinson equation of state.
.. math::
\left(\frac{\partial \ln \phi_i}{\partial z_i}\right)_{P,
z_{j \ne i}}
Parameters
----------
Z : float
Compressibility of the mixture for a desired phase, [-]
Returns
-------
dlnphis_dzs : list[list[float]]
Mole fraction derivatives of log fugacity coefficient for each
species (such that the mole fractions do not sum to 1), [-]
Notes
-----
This formula is from [1]_ but is validated to match the generic
implementation.
Examples
--------
>>> kijs = [[0, 0.00076, 0.00171], [0.00076, 0, 0.00061], [0.00171, 0.00061, 0]]
>>> eos = PRMIX(Tcs=[469.7, 507.4, 540.3], zs=[0.8168, 0.1501, 0.0331], omegas=[0.249, 0.305, 0.349], Pcs=[3.369E6, 3.012E6, 2.736E6], T=322.29, P=101325, kijs=kijs)
>>> eos.dlnphis_dzs(eos.Z_l)
[[0.009938069276, 0.0151503498382, 0.018297235797], [-0.038517738793, -0.05958926042, -0.068438990795], [-0.07057106923, -0.10363920720, -0.14116283024]]
References
----------
.. [1] Chang, Yih-Bor. "Development and Application of an Equation of
State Compositional Simulator" 1990.
https://repositories.lib.utexas.edu/handle/2152/80585.
'''
T, P, zs = self.T, self.P, self.zs
T2 = T*T
T_inv = 1.0/T
RT_inv = R_inv*T_inv
bs, b = self.bs, self.b
a_alpha = self.a_alpha
a_alpha_ijs = self.a_alpha_ijs
a_alphas = self.a_alphas
a_alpha_j_rows = self.a_alpha_j_rows
N = len(zs)
b2 = b*b
b_inv = 1.0/b
b2_inv = b_inv*b_inv
a_alpha2 = a_alpha*a_alpha
A = a_alpha*P*RT_inv*RT_inv
B = b*P*RT_inv
B_inv = 1.0/B
C = 1.0/(Z - B)
Zm1 = Z - 1.0
G = (Z + (1.0 + root_two)*B)/(Z + (1.0 - root_two)*B)
t4 = 2.0/a_alpha
t5 = -A/(two_root_two*B)
Eis = [t5*(t4*a_alpha_j_rows[i] - bs[i]*b_inv) for i in range(N)]
# ln_phis = []
# for i in range(N):
# ln_phis.append(log(C) + Dis[i] + Eis[i]*log(G))
# return ln_phis
# Bis = [bi*P/(R*T) for bi in bs]
# maybe with a 2 constant?
t6 = P*RT_inv
dB_dxks = [t6*bk for bk in bs]
# THIS IS WRONG - the sum changes w.r.t (or does it?)
# Believed right now?
const = (P+P)*RT_inv*RT_inv
dA_dxks = [const*term_i for term_i in a_alpha_j_rows]
dF_dZ_inv = 1.0/(3.0*Z*Z - 2.0*Z*(1.0 - B) + (A - 3.0*B*B - 2.0*B))
t15 = (A - 2.0*B - 3.0*B*B + 2.0*(3.0*B + 1.0)*Z - Z*Z)
BmZ = (B - Z)
dZ_dxs = [(BmZ*dA_dxks[i] + t15*dB_dxks[i])*dF_dZ_inv for i in range(N)]
# function only of k
ZmB = Z - B
t20 = -1.0/(ZmB*ZmB)
dC_dxs = [t20*(dZ_dxs[k] - dB_dxks[k]) for k in range(N)]
dD_dxs = []
# dD_dxs = [[0.0]*N for _ in cmps]
t55s = [b*dZ_dxs[k] - bs[k]*Zm1 for k in range(N)]
for i in range(N):
# dD_dxs_i = dD_dxs[i]
b_term_ratio = bs[i]*b2_inv
dD_dxs.append([b_term_ratio*t55s[k] for k in range(N)])
# for k in range(N):
# dD_dxs_i[k] = b_term_ratio*t55s[k]
# dD_dxs = []
# for i in range(N):
# term = bs[i]/(b*b)*(b*dZ_dxs[i] - b*(Z - 1.0))
# dD_dxs.append(term)
# ? Believe this is the only one with multi indexes?
t1 = 1.0/(two_root_two*a_alpha*b*B)
t2 = t1*A/(a_alpha*b)
t50s = [B*dA_dxks[k] - A*dB_dxks[k] for k in range(N)]
# problem is in here, tested numerically
b_two = b + b
t32 = 2.0*a_alpha*b2
t33 = 4.0*b2
t34 = t1*B_inv*a_alpha
t35 = -t1*B_inv*b_two
# Symmetric matrix!
dE_dxs = [[0.0]*N for _ in range(N)] # TODO - makes little sense. Too many i indexes.
for i in range(N):
zm_aim_tot = a_alpha_j_rows[i]
t30 = t34*bs[i] + t35*zm_aim_tot
t31 = t33*zm_aim_tot
dE_dxs_i = []
a_alpha_ijs_i = a_alpha_ijs[i]
for k in range(i+1):
# Sign was wrong in article - should be a plus
second = t2*(t31*a_alpha_j_rows[k] - t32*a_alpha_ijs_i[k] - bs[i]*bs[k]*a_alpha2)
dE_dxs[i][k] = dE_dxs[k][i] = t30*t50s[k] + second
# dE_dxs_i.append(t1*(first + second))
# dE_dxs.append(dE_dxs_i)
t59 = (Z + (1.0 - root_two)*B)
t60 = two_root_two/(t59*t59)
dG_dxs = [t60*(Z*dB_dxks[k] - B*dZ_dxs[k]) for k in range(N)]
G_inv = 1.0/G
logG = log(G)
C_inv = 1.0/C
dlnphis_dxs = zeros((N, N)) if self.vectorized else [[0.0]*N for _ in range(N)]
t61s = [C_inv*dC_dxi for dC_dxi in dC_dxs]
for i in range(N):
dD_dxs_i = dD_dxs[i]
dE_dxs_i = dE_dxs[i]
E_G = Eis[i]*G_inv
# dlnphis_dxs_i = dlnphis_dxs[i]
for k in range(N):
dlnphis_dxs[i][k] = t61s[k] + dD_dxs_i[k] + logG*dE_dxs_i[k] + E_G*dG_dxs[k]
# dlnphis_dxs_i = [t61s[k] + dD_dxs_i[k] + logG*dE_dxs_i[k] + E_G*dG_dxs[k]
# for k in range(N)]
# dlnphis_dxs.append(dlnphis_dxs_i)
# return dlnphis_dxs
return dlnphis_dxs#, dZ_dxs, dA_dxks, dB_dxks, dC_dxs, dD_dxs, dE_dxs, dG_dxs
@property
def ddelta_dzs(self):
r'''Helper method for calculating the composition derivatives of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \delta}{\partial x_i}\right)_{T, P, x_{i\ne j}}
= 2 b_i
Returns
-------
ddelta_dzs : list[float]
Composition derivative of `delta` of each component, [m^3/mol]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return PR_ddelta_dzs(self.bs, N, out=zeros(N) if self.vectorized else [0.0]*N)
@property
def ddelta_dns(self):
r'''Helper method for calculating the mole number derivatives of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \delta}{\partial n_i}\right)_{T, P, n_{i\ne j}}
= 2 (b_i - b)
Returns
-------
ddelta_dns : list[float]
Mole number derivative of `delta` of each component, [m^3/mol^2]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return PR_ddelta_dns(self.bs, self.b, N, out=zeros(N) if self.vectorized else [0.0]*N)
@property
def d2delta_dzizjs(self):
r'''Helper method for calculating the second composition derivatives (hessian) of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \delta}{\partial x_i\partial x_j}\right)_{T, P, x_{k\ne i,j}}
= 0
Returns
-------
d2delta_dzizjs : list[float]
Second Composition derivative of `delta` of each component, [m^3/mol]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return zeros((N, N)) if self.vectorized else [[0.0]*N for i in range(N)]
@property
def d2delta_dninjs(self):
r'''Helper method for calculating the second mole number derivatives (hessian) of
`delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \delta}{\partial n_i \partial n_j}\right)_{T, P, n_{k\ne i,j}}
= 4b - 2b_i - 2b_j
Returns
-------
d2delta_dninjs : list[list[float]]
Second mole number derivative of `delta` of each component, [m^3/mol^3]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
out = zeros((N, N)) if self.vectorized else [[0.0]*N for _ in range(N)]
return PR_d2delta_dninjs(self.b, self.bs, N, out)
@property
def d3delta_dninjnks(self):
r'''Helper method for calculating the third partial mole number
derivatives of `delta`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 \delta}{\partial n_i \partial n_j \partial n_k }
\right)_{T, P,
n_{m \ne i,j,k}} = 4(-3b + b_i + b_j + b_k)
Returns
-------
d3delta_dninjnks : list[list[list[float]]]
Third mole number derivative of `delta` of each component,
[m^3/mol^4]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
out = zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N) ] for _ in range(N)]
return PR_d3delta_dninjnks(self.b, self.bs, N, out)
@property
def depsilon_dzs(self):
r'''Helper method for calculating the composition derivatives of
`epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \epsilon}{\partial x_i}\right)_{T, P, x_{i\ne j}}
= -2 b_i\cdot b
Returns
-------
depsilon_dzs : list[float]
Composition derivative of `epsilon` of each component, [m^6/mol^2]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return PR_depsilon_dzs(self.b, self.bs, N, out=zeros(N) if self.vectorized else [0.0]*N)
@property
def depsilon_dns(self):
r'''Helper method for calculating the mole number derivatives of
`epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial \epsilon}{\partial n_i}\right)_{T, P, n_{i\ne j}}
= 2b(b - b_i)
Returns
-------
depsilon_dns : list[float]
Composition derivative of `epsilon` of each component, [m^6/mol^3]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
return PR_depsilon_dns(self.b, self.bs, N, out=zeros(N) if self.vectorized else [0.0]*N)
@property
def d2epsilon_dzizjs(self):
r'''Helper method for calculating the second composition derivatives (hessian)
of `epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \epsilon}{\partial x_i \partial x_j}\right)_{T, P, x_{k\ne i,j}}
= 2 b_i b_j
Returns
-------
d2epsilon_dzizjs : list[list[float]]
Second composition derivative of `epsilon` of each component, [m^6/mol^2]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
out = zeros((N, N)) if self.vectorized else [[0.0]*N for _ in range(N)]
return PR_d2epsilon_dzizjs(self.b, self.bs, N, out)
@property
def d2epsilon_dninjs(self):
r'''Helper method for calculating the second mole number derivatives
(hessian) of `epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^2 \epsilon}{\partial n_i n_j}\right)_{T, P,
n_{k\ne i,j}} = -2b(2b - b_i - b_j) - 2(b - b_i)(b - b_j)
Returns
-------
d2epsilon_dninjs : list[list[float]]
Second mole number derivative of `epsilon` of each component,
[m^6/mol^4]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
out = zeros((N, N)) if self.vectorized else [[0.0]*N for _ in range(N)]
return PR_d2epsilon_dninjs(self.b, self.bs, N, out)
@property
def d3epsilon_dninjnks(self):
r'''Helper method for calculating the third partial mole number
derivatives of `epsilon`. Note this is independent of the phase.
.. math::
\left(\frac{\partial^3 \epsilon}{\partial n_i \partial n_j \partial n_k }
\right)_{T, P,
n_{m \ne i,j,k}} = 24b^2 - 12b(b_i + b_j + b_k)
+ 4(b_i b_j + b_i b_k + b_j b_k)
Returns
-------
d3epsilon_dninjnks : list[list[list[float]]]
Third mole number derivative of `epsilon` of each component,
[m^6/mol^5]
Notes
-----
This derivative is checked numerically.
'''
N = self.N
out = zeros((N, N, N)) if self.vectorized else [[[0.0]*N for _ in range(N) ] for _ in range(N)]
return PR_d3epsilon_dninjnks(self.b, self.bs, N, out)
def solve_T(self, P, V, quick=True, solution=None):
if self.N == 1 and type(self) is PRMIX:
self.Tc = self.Tcs[0]
self.Pc = self.Pcs[0]
self.kappa = self.kappas[0]
self.a = self.ais[0]
T = super(type(self).__mro__[-4], self).solve_T(P=P, V=V, solution=solution)
del self.Tc
del self.Pc
del self.kappa
del self.a
return T
else:
return super(type(self).__mro__[-3], self).solve_T(P=P, V=V, solution=solution)
[docs]class PRMIXTranslated(PRMIX):
r'''Class for solving the Peng-Robinson [1]_ [2]_ translated cubic equation
of state for a mixture of any number of compounds. Solves the EOS
on initialization and calculates fugacities for all components in all
phases.
Two of `T`, `P`, and `V` are needed to solve the EOS.
.. math::
P = \frac{RT}{v + c - b} - \frac{a\alpha(T)}{(v+c)(v + c + b)+b(v
+ c - b)}
.. math::
a \alpha = \sum_i \sum_j z_i z_j {(a\alpha)}_{ij}
.. math::
(a\alpha)_{ij} = (1-k_{ij})\sqrt{(a\alpha)_{i}(a\alpha)_{j}}
.. math::
b = \sum_i z_i b_i
.. math::
a_i=0.45724\frac{R^2T_{c,i}^2}{P_{c,i}}
.. math::
b_i=0.07780\frac{RT_{c,i}}{P_{c,i}}
.. math::
\alpha(T)_i=[1+\kappa_i(1-\sqrt{T_{r,i}})]^2
.. math::
\kappa_i=0.37464+1.54226\omega_i-0.26992\omega^2_i
Parameters
----------
Tcs : list[float]
Critical temperatures of all compounds, [K]
Pcs : list[float]
Critical pressures of all compounds, [Pa]
omegas : list[float]
Acentric factors of all compounds, [-]
zs : list[float]
Overall mole fractions of all species, [-]
kijs : list[list[float]], optional
n*n size list of lists with binary interaction parameters for the
Van der Waals mixing rules, default all 0 [-]
cs : list[float], optional
Volume translation parameters; always zero in the original
implementation, [m^3/mol]
T : float, optional
Temperature, [K]
P : float, optional
Pressure, [Pa]
V : float, optional
Molar volume, [m^3/mol]
fugacities : bool, optional
Whether or not to calculate fugacity related values (phis, log phis,
and fugacities); default True, [-]
only_l : bool, optional
When true, if there is a liquid and a vapor root, only the liquid
root (and properties) will be set; default False, [-]
only_g : bool, optional
When true, if there is a liquid and a vapor root, only the vapor
root (and properties) will be set; default False, [-]
Examples
--------
T-P initialization, nitrogen-methane at 115 K and 1 MPa:
>>> eos = PRMIXTranslated(T=115, P=1E6, cs=[-4.4e-6, -4.35e-6], Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.2, 0.8], kijs=[[0,0.03],[0.03,0]])
>>> eos.V_l, eos.V_g
(3.9079056337e-05, 0.00060231393016)
>>> eos.fugacities_l, eos.fugacities_g
([442838.8615, 108854.48589], [184396.972, 565531.7709])
Notes
-----
For P-V initializations, a numerical solver is used to find T.
References
----------
.. [1] Peng, Ding-Yu, and Donald B. Robinson. "A New Two-Constant Equation
of State." Industrial & Engineering Chemistry Fundamentals 15, no. 1
(February 1, 1976): 59-64. doi:10.1021/i160057a011.
.. [2] Robinson, Donald B., Ding-Yu Peng, and Samuel Y-K Chung. "The
Development of the Peng - Robinson Equation and Its Application to Phase
Equilibrium in a System Containing Methanol." Fluid Phase Equilibria 24,
no. 1 (January 1, 1985): 25-41. doi:10.1016/0378-3812(85)87035-7.
'''
translated = True
eos_pure = PRTranslated
mix_kwargs_to_pure = {'cs': 'c'}
kwargs_linear = ('cs',)
fugacity_coefficients = GCEOSMIX.fugacity_coefficients
dlnphis_dT = GCEOSMIX.dlnphis_dT
dlnphis_dP = GCEOSMIX.dlnphis_dP
d_lnphi_dzs = GCEOSMIX.dlnphis_dzs
P_max_at_V = GCEOSMIX.P_max_at_V
model_id = 11202
# All the b derivatives happen to work out to be the same, and are checked numerically
solve_T = GCEOS.solve_T
kwargs_keys = ('kijs', 'cs')
def __init__(self, Tcs, Pcs, omegas, zs, kijs=None, cs=None,
T=None, P=None, V=None,
fugacities=True, only_l=False, only_g=False):
self.N = N = len(Tcs)
cmps = range(N)
self.Tcs = Tcs
self.Pcs = Pcs
self.omegas = omegas
self.zs = zs
self.vectorized = vectorized = type(zs) is ndarray
if kijs is None:
if vectorized:
kijs = zeros((N, N))
else:
kijs = [[0.0]*N for i in range(N)]
self.kijs = kijs
self.one_minus_kijs = one_minus_kijs(kijs)
self.T = T
self.