Source code for thermo.wilson

'''Chemical Engineering Design Library (ChEDL). Utilities for process modeling.
Copyright (C) 2019, 2020 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 a class :obj:`Wilson` for performing activity coefficient
calculations with the Wilson model. An older, functional calculation for
activity coefficients only is also present, :obj:`Wilson_gammas`.

For reporting bugs, adding feature requests, or submitting pull requests,
please use the `GitHub issue tracker <https://github.com/CalebBell/thermo/>`_.

.. contents:: :local:

Wilson Class
============

.. autoclass:: Wilson
    :members: to_T_xs, GE, dGE_dT, d2GE_dT2, d3GE_dT3, d2GE_dTdxs, dGE_dxs, d2GE_dxixjs, d3GE_dxixjxks, lambdas, dlambdas_dT, d2lambdas_dT2, d3lambdas_dT3, from_DDBST, from_DDBST_as_matrix
    :undoc-members:
    :show-inheritance:
    :exclude-members: gammas

Wilson Functional Calculations
==============================
.. autofunction:: Wilson_gammas

Wilson Regression Calculations
==============================
.. autofunction:: wilson_gammas_binaries

'''

from math import exp, log

from fluids.constants import R
from fluids.numerics import numpy as np
from fluids.numerics import trunc_exp

from thermo.activity import GibbsExcess, d2interaction_exp_dT2, d3interaction_exp_dT3, dinteraction_exp_dT, interaction_exp

try:
    array, zeros, npsum, nplog, ones = np.array, np.zeros, np.sum, np.log, np.ones
except (ImportError, AttributeError):
    pass

__all__ = ['Wilson', 'Wilson_gammas', 'wilson_gammas_binaries', 'wilson_gammas_binaries_jac']


def wilson_xj_Lambda_ijs(xs, lambdas, N, xj_Lambda_ijs=None):
    if xj_Lambda_ijs is None:
        xj_Lambda_ijs = [0.0]*N
    for i in range(N):
        tot = 0.0
        lambdasi = lambdas[i]
        for j in range(N):
            tot += xs[j]*lambdasi[j]
        xj_Lambda_ijs[i] = tot
    return xj_Lambda_ijs

def wilson_dGE_dT(xs, T, GE, N, xj_Lambda_ijs_inv, xj_dLambda_dTijs):
    tot = GE/T

    sum1 = 0.0
    for i in range(N):
        sum1 += xs[i]*xj_dLambda_dTijs[i]*xj_Lambda_ijs_inv[i]
    tot -= T*R*sum1
    return tot

def wilson_d2GE_dT2(xs, T, N, xj_Lambda_ijs_inv, xj_dLambda_dTijs, xj_d2Lambda_dT2ijs):
    sum0, sum1 = 0.0, 0.0
    for i in range(N):
        t = xs[i]*xj_Lambda_ijs_inv[i]
        t2 = xj_dLambda_dTijs[i]*t
        sum1 += t2

        sum0 += t*(xj_d2Lambda_dT2ijs[i] - xj_dLambda_dTijs[i]*xj_dLambda_dTijs[i]*xj_Lambda_ijs_inv[i])

    d2GE_dT2 = -R*(T*sum0 + 2.0*sum1)
    return d2GE_dT2

def wilson_d3GE_dT3(xs, T, N, xj_Lambda_ijs_inv, xj_dLambda_dTijs, xj_d2Lambda_dT2ijs, xj_d3Lambda_dT3ijs):
    #Term is directly from the one above it
    sum0 = 0.0
    for i in range(N):
        sum0 += xj_Lambda_ijs_inv[i]*xs[i]*(xj_d2Lambda_dT2ijs[i]
                - xj_dLambda_dTijs[i]*xj_dLambda_dTijs[i]*xj_Lambda_ijs_inv[i])

    sum_d3 = 0.0
    for i in range(N):
        sum_d3 += xs[i]*xj_d3Lambda_dT3ijs[i]*xj_Lambda_ijs_inv[i]

    sum_comb = 0.0
    for i in range(N):
        sum_comb += xs[i]*xj_d2Lambda_dT2ijs[i]*xj_dLambda_dTijs[i]*xj_Lambda_ijs_inv[i]*xj_Lambda_ijs_inv[i]
    sum_comb *= 3.0

    sum_last = 0.0
    for i in range(N):
        v = xj_dLambda_dTijs[i]*xj_Lambda_ijs_inv[i]
        sum_last += xs[i]*v*v*v
    sum_last *= 2.0

    d3GE_dT3 = -R*(3.0*sum0 + T*(sum_d3 - sum_comb + sum_last))
    return d3GE_dT3

def wilson_d2GE_dTdxs(xs, T, N, log_xj_Lambda_ijs, lambdas, dlambdas_dT,
                      xj_Lambda_ijs_inv, xj_dLambda_dTijs, d2GE_dTdxs=None):
    if d2GE_dTdxs is None:
        d2GE_dTdxs = [0.0]*N

    for i in range(N):
        tot1 = xj_dLambda_dTijs[i]*xj_Lambda_ijs_inv[i]
        tot2 = 0.0
        for j in range(N):
            t1 = lambdas[j][i]*xj_Lambda_ijs_inv[j]
            tot1 += xs[j]*xj_Lambda_ijs_inv[j]*(dlambdas_dT[j][i] - xj_dLambda_dTijs[j]*t1)
            tot2 += xs[j]*t1

        dG = -R*(T*tot1 + log_xj_Lambda_ijs[i] + tot2)

        d2GE_dTdxs[i] = dG

    return d2GE_dTdxs

def wilson_dGE_dxs(xs, T, N, log_xj_Lambda_ijs, lambdas, xj_Lambda_ijs_inv, dGE_dxs=None):
    if dGE_dxs is None:
        dGE_dxs = [0.0]*N
    mRT = -T*R
    for k in range(N):
        tot = log_xj_Lambda_ijs[k]
        for i in range(N):
            tot += xs[i]*lambdas[i][k]*xj_Lambda_ijs_inv[i]
        dGE_dxs[k] = mRT*tot
    return dGE_dxs

def wilson_d2GE_dxixjs(xs, T, N, lambdas, xj_Lambda_ijs_inv, d2GE_dxixjs=None):
    if d2GE_dxixjs is None:
        d2GE_dxixjs = [[0.0]*N for i in range(N)] # numba: delete
#        d2GE_dxixjs = zeros((N, N)) # numba: uncomment

    RT = R*T
    for k in range(N):
        dG_row = d2GE_dxixjs[k]
        for m in range(N):
            tot = 0.0
            for i in range(N):
                tot += xs[i]*lambdas[i][k]*lambdas[i][m]*(xj_Lambda_ijs_inv[i]*xj_Lambda_ijs_inv[i])
            tot -= lambdas[k][m]*xj_Lambda_ijs_inv[k]
            tot -= lambdas[m][k]*xj_Lambda_ijs_inv[m]
            dG_row[m] = RT*tot

    return d2GE_dxixjs

def wilson_d3GE_dxixjxks(xs, T, N, lambdas, xj_Lambda_ijs_inv, d3GE_dxixjxks=None):
    if d3GE_dxixjxks is None:
        d3GE_dxixjxks = [[[0.0]*N for i in range(N)] for _ in range(N)]# numba: delete
#        d3GE_dxixjxks = zeros((N, N, N)) # numba: uncomment

    nRT = -R*T
    for k in range(N):
        dG_matrix = d3GE_dxixjxks[k]
        for m in range(N):
            dG_row = dG_matrix[m]
            for n in range(N):
                tot = 0.0
                for i in range(N):
                    num = xs[i]*lambdas[i][k]*lambdas[i][m]*lambdas[i][n]
                    den = xj_Lambda_ijs_inv[i]*xj_Lambda_ijs_inv[i]*xj_Lambda_ijs_inv[i]
                    tot += num*den
                tot *= 2.0

                tot -= lambdas[k][m]*lambdas[k][n]*xj_Lambda_ijs_inv[k]*xj_Lambda_ijs_inv[k]
                tot -= lambdas[m][k]*lambdas[m][n]*xj_Lambda_ijs_inv[m]*xj_Lambda_ijs_inv[m]
                tot -= lambdas[n][m]*lambdas[n][k]*xj_Lambda_ijs_inv[n]*xj_Lambda_ijs_inv[n]
                dG_row[n] = nRT*tot

    return d3GE_dxixjxks

def wilson_gammas(xs, N, lambdas, xj_Lambda_ijs_inv, gammas=None, vec0=None):
    if gammas is None:
        gammas = [0.0]*N
    if vec0 is None:
        vec0 = [0.0]*N

    for i in range(N):
        vec0[i] = xs[i]*xj_Lambda_ijs_inv[i]

    for j in range(N):
        const_j = vec0[j]
        for i in range(N):
            gammas[i] += lambdas[j][i]*const_j
    for i in range(N):
        gammas[i] = exp(1.0 - gammas[i])*xj_Lambda_ijs_inv[i]

    return gammas

def wilson_gammas_from_args(xs, N, lambdas, xj_Lambda_ijs=None, vec0=None, gammas=None,):
    if xj_Lambda_ijs is None:
        xj_Lambda_ijs = [0.0]*N
    xj_Lambda_ijs = wilson_xj_Lambda_ijs(xs, lambdas, N, xj_Lambda_ijs)
    for i in range(N):
        # Can make this optimization here only
        xj_Lambda_ijs[i] = 1.0/xj_Lambda_ijs[i]
    return wilson_gammas(xs, N, lambdas, xj_Lambda_ijs, gammas=gammas, vec0=vec0)

MIN_LAMBDA_WILSON = 1e-20

[docs]def wilson_gammas_binaries(xs, lambda12, lambda21, calc=None): r'''Calculates activity coefficients at fixed `lambda` values for a binary system at a series of mole fractions. This is used for regression of `lambda` parameters. This function is highly optimized, and operates on multiple points at a time. .. math:: \ln \gamma_1 = -\ln(x_1 + \Lambda_{12}x_2) + x_2\left( \frac{\Lambda_{12}}{x_1 + \Lambda_{12}x_2} - \frac{\Lambda_{21}}{x_2 + \Lambda_{21}x_1} \right) .. math:: \ln \gamma_2 = -\ln(x_2 + \Lambda_{21}x_1) - x_1\left( \frac{\Lambda_{12}}{x_1 + \Lambda_{12}x_2} - \frac{\Lambda_{21}}{x_2 + \Lambda_{21}x_1} \right) Parameters ---------- xs : list[float] Liquid mole fractions of each species in the format x0_0, x1_0, (component 1 point1, component 2 point 1), x0_1, x1_1, (component 1 point2, component 2 point 2), ... [-] lambda12 : float `lambda` parameter for 12, [-] lambda21 : float `lambda` parameter for 21, [-] gammas : list[float], optional Array to store the activity coefficient for each species in the liquid mixture, indexed the same as `xs`; can be omitted or provided for slightly better performance [-] Returns ------- gammas : list[float] Activity coefficient for each species in the liquid mixture, indexed the same as `xs`, [-] Notes ----- The lambda values are hard-coded to replace values under zero which are mathematically impossible, with a very small number. This is helpful for regression which might try to make those values negative. Examples -------- >>> wilson_gammas_binaries([.1, .9, 0.3, 0.7, .85, .15], 0.1759, 0.7991) [3.42989, 1.03432, 1.74338, 1.21234, 1.01766, 2.30656] ''' if lambda12 < MIN_LAMBDA_WILSON: lambda12 = MIN_LAMBDA_WILSON if lambda21 < MIN_LAMBDA_WILSON: lambda21 = MIN_LAMBDA_WILSON pts = len(xs)//2 # Always even if calc is None: allocate_size = (pts*2) calc = [0.0]*allocate_size for i in range(pts): i2 = i*2 x1 = xs[i2] x2 = 1.0 - x1 c0 = 1.0/(x1 + x2*lambda12) c1 = 1.0/(x2 + x1*lambda21) c3 = lambda12*c0 - lambda21*c1 calc[i2] = trunc_exp(c3*x2)*c0 calc[i2 + 1] = trunc_exp(-c3*x1)*c1 return calc
""" Actually readable expression of `wilson_gammas_binaries`: if lambda12 < MIN_LAMBDA_WILSON: lambda12 = MIN_LAMBDA_WILSON if lambda21 < MIN_LAMBDA_WILSON: lambda21 = MIN_LAMBDA_WILSON pts = int(len(xs)/2) # Always even allocate_size = (pts*2) # lambdas = ones((2,2)) # numba: uncomment lambdas = [[1.0, 1.0], [1.0, 1.0]] # numba: delete lambdas[0][1] = lambda12 lambdas[1][0] = lambda21 if calc is None: calc = [0.0]*allocate_size xj_Lambda_ijs_vec = [0.0]*2 vec0 = [0.0]*2 gammas = [0.0]*2 xs_pt = [0.0]*2 for i in range(pts): i2 = i*2 xs_pt[0] = xs[i2] xs_pt[1] = 1.0 - xs_pt[0] xj_Lambda_ijs = wilson_xj_Lambda_ijs(xs_pt, lambdas, N=2, xj_Lambda_ijs=xj_Lambda_ijs_vec) xj_Lambda_ijs[0] = 1.0/xj_Lambda_ijs[0] xj_Lambda_ijs[1] = 1.0/xj_Lambda_ijs[1] gammas = wilson_gammas(xs_pt, N=2, lambdas=lambdas, xj_Lambda_ijs_inv=xj_Lambda_ijs, gammas=gammas, vec0=vec0) calc[i2] = gammas[0] calc[i2 + 1] = gammas[1] return calc """ def wilson_gammas_binaries_jac(xs, lambda12, lambda21, calc=None): if lambda12 < MIN_LAMBDA_WILSON: lambda12 = MIN_LAMBDA_WILSON if lambda21 < MIN_LAMBDA_WILSON: lambda21 = MIN_LAMBDA_WILSON pts = len(xs)//2 # Always even if calc is None: allocate_size = (pts*2) calc = np.zeros((allocate_size, 2)) for i in range(pts): i2 = i*2 x1 = xs[i2] x2 = 1.0 - x1 c0 = lambda12*x2 c1 = c0 + x1 c2 = 1.0/c1 c3 = lambda21*x1 c4 = c3 + x2 c5 = 1.0/c4 c6 = c2*lambda12 - c5*lambda21 c7 = trunc_exp(c6*x2) c8 = c2*c5 c9 = trunc_exp(-c6*x1) calc[i2][0] = -c7*lambda12*x2*x2*c2*c2*c2 calc[i2][1] = c7*c8*x2*(c3*c5 - 1.0) calc[i2 + 1][0] = c8*c9*x1*(c0*c2 - 1.0) calc[i2 + 1][1] = -c9*lambda21*x1*x1*c5*c5*c5 return calc
[docs]class Wilson(GibbsExcess): r'''Class for representing an a liquid with excess gibbs energy represented by the Wilson equation. This model is capable of representing most nonideal liquids for vapor-liquid equilibria, but is not recommended for liquid-liquid equilibria. The two basic equations are as follows; all other properties are derived from these. .. math:: g^E = -RT\sum_i x_i \ln\left(\sum_j x_j \lambda_{i,j} \right) .. math:: \Lambda_{ij} = \exp\left[a_{ij}+\frac{b_{ij}}{T}+c_{ij}\ln T + d_{ij}T + \frac{e_{ij}}{T^2} + f_{ij}{T^2}\right] Parameters ---------- T : float Temperature, [K] xs : list[float] Mole fractions, [-] lambda_coeffs : list[list[list[float]]], optional Wilson parameters, indexed by [i][j] and then each value is a 6 element list with parameters (`a`, `b`, `c`, `d`, `e`, `f`); either `lambda_coeffs` or the lambda parameters are required, [various] ABCDEF : tuple(list[list[float]], 6), optional The lamba parameters can be provided as a tuple, [various] lambda_as : list[list[float]], optional `a` parameters used in calculating :obj:`Wilson.lambdas`, [-] lambda_bs : list[list[float]], optional `b` parameters used in calculating :obj:`Wilson.lambdas`, [K] lambda_cs : list[list[float]], optional `c` parameters used in calculating :obj:`Wilson.lambdas`, [-] lambda_ds : list[list[float]], optional `d` paraemeters used in calculating :obj:`Wilson.lambdas`, [1/K] lambda_es : list[list[float]], optional `e` parameters used in calculating :obj:`Wilson.lambdas`, [K^2] lambda_fs : list[list[float]], optional `f` parameters used in calculating :obj:`Wilson.lambdas`, [1/K^2] Attributes ---------- T : float Temperature, [K] xs : list[float] Mole fractions, [-] model_id : int Unique identifier for the Wilson activity model, [-] Notes ----- In addition to the methods presented here, the methods of its base class :obj:`thermo.activity.GibbsExcess` are available as well. .. warning:: If parameters are ommited for all interactions, this model reverts to :obj:`thermo.activity.IdealSolution`. In large systems it is common to only regress parameters for the most important components; set `lambda` parameters for other components to 0 to "ignore" them and treat them as ideal components. This class works with python lists, numpy arrays, and can be accelerated with Numba or PyPy quite effectively. Examples -------- **Example 1** This object-oriented class provides access to many more thermodynamic properties than :obj:`Wilson_gammas`, but it can also be used like that function. In the following example, `gammas` are calculated with both functions. The `lambdas` cannot be specified in this class; but fixed values can be converted with the `log` function so that fixed values will be obtained. >>> Wilson_gammas([0.252, 0.748], [[1, 0.154], [0.888, 1]]) [1.881492608717, 1.165577493112] >>> GE = Wilson(T=300.0, xs=[0.252, 0.748], lambda_as=[[0, log(0.154)], [log(0.888), 0]]) >>> GE.gammas() [1.881492608717, 1.165577493112] We can check that the same lambda values were computed as well, and that there is no temperature dependency: >>> GE.lambdas() [[1.0, 0.154], [0.888, 1.0]] >>> GE.dlambdas_dT() [[0.0, 0.0], [0.0, 0.0]] In this case, there is no temperature dependency in the Wilson model as the `lambda` values are fixed, so the excess enthalpy is always zero. Other properties are not always zero. >>> GE.HE(), GE.CpE() (0.0, 0.0) >>> GE.GE(), GE.SE(), GE.dGE_dT() (683.165839398, -2.277219464, 2.2772194646) **Example 2** ChemSep is a (partially) free program for modeling distillation. Besides being a wonderful program, it also ships with a permissive license several sets of binary interaction parameters. The Wilson parameters in it can be accessed from Thermo as follows. In the following case, we compute activity coefficients of the ethanol-water system at mole fractions of [.252, 0.748]. >>> from thermo.interaction_parameters import IPDB >>> CAS1, CAS2 = '64-17-5', '7732-18-5' >>> lambda_as = IPDB.get_ip_asymmetric_matrix(name='ChemSep Wilson', CASs=[CAS1, CAS2], ip='aij') >>> lambda_bs = IPDB.get_ip_asymmetric_matrix(name='ChemSep Wilson', CASs=[CAS1, CAS2], ip='bij') >>> GE = Wilson(T=273.15+70, xs=[.252, .748], lambda_as=lambda_as, lambda_bs=lambda_bs) >>> GE.gammas() [1.95733110, 1.1600677] In ChemSep, the form of the Wilson `lambda` equation is .. math:: \Lambda_{ij} = \frac{V_j}{V_i}\exp\left( \frac{-A_{ij}}{RT}\right) The parameters were converted to the form used by Thermo as follows: .. math:: a_{ij} = \log\left(\frac{V_j}{V_i}\right) .. math:: b_{ij} = \frac{-A_{ij}}{R}= \frac{-A_{ij}}{ 1.9872042586408316} This system was chosen because there is also a sample problem for the same components from the DDBST which can be found here: http://chemthermo.ddbst.com/Problems_Solutions/Mathcad_Files/P05.01a%20VLE%20Behavior%20of%20Ethanol%20-%20Water%20Using%20Wilson.xps In that example, with different data sets and parameters, they obtain at the same conditions activity coefficients of [1.881, 1.165]. Different sources of parameters for the same system will generally have similar behavior if regressed in the same temperature range. As higher order `lambda` parameters are added, models become more likely to behave differently. It is recommended in [3]_ to regress the minimum number of parameters required. **Example 3** The DDBST has published some sample problems which are fun to work with. Because the DDBST uses a different equation form for the coefficients than this model implements, we must initialize the :obj:`Wilson` object with a different method. >>> T = 331.42 >>> N = 3 >>> Vs_ddbst = [74.04, 80.67, 40.73] >>> as_ddbst = [[0, 375.2835, 31.1208], [-1722.58, 0, -1140.79], [747.217, 3596.17, 0.0]] >>> bs_ddbst = [[0, -3.78434, -0.67704], [6.405502, 0, 2.59359], [-0.256645, -6.2234, 0]] >>> cs_ddbst = [[0.0, 7.91073e-3, 8.68371e-4], [-7.47788e-3, 0.0, 3.1e-5], [-1.24796e-3, 3e-5, 0.0]] >>> dis = eis = fis = [[0.0]*N for _ in range(N)] >>> params = Wilson.from_DDBST_as_matrix(Vs=Vs_ddbst, ais=as_ddbst, bis=bs_ddbst, cis=cs_ddbst, dis=dis, eis=eis, fis=fis, unit_conversion=False) >>> xs = [0.229, 0.175, 0.596] >>> GE = Wilson(T=T, xs=xs, lambda_as=params[0], lambda_bs=params[1], lambda_cs=params[2], lambda_ds=params[3], lambda_es=params[4], lambda_fs=params[5]) >>> GE Wilson(T=331.42, xs=[0.229, 0.175, 0.596], lambda_as=[[0.0, 3.870101271243586, 0.07939943395502425], [-6.491263271243587, 0.0, -3.276991837288562], [0.8542855660449756, 6.906801837288562, 0.0]], lambda_bs=[[0.0, -375.2835, -31.1208], [1722.58, 0.0, 1140.79], [-747.217, -3596.17, -0.0]], lambda_ds=[[-0.0, -0.00791073, -0.000868371], [0.00747788, -0.0, -3.1e-05], [0.00124796, -3e-05, -0.0]]) >>> GE.GE(), GE.dGE_dT(), GE.d2GE_dT2() (480.26392663, 4.35596276623, -0.02913038452501) >>> GE.HE(), GE.SE(), GE.dHE_dT(), GE.dSE_dT() (-963.389253354, -4.3559627662, 9.6543920392, 0.029130384525) >>> GE.gammas() [1.2233934334, 1.100945902470, 1.205289928117] The solution given by the DDBST has the same values [1.223, 1.101, 1.205], and can be found here: http://chemthermo.ddbst.com/Problems_Solutions/Mathcad_Files/05.09%20Compare%20Experimental%20VLE%20to%20Wilson%20Equation%20Results.xps **Example 4** A simple example is given in [1]_; other textbooks sample problems are normally in the same form as this - with only volumes and the `a` term specified. The system is 2-propanol/water at 353.15 K, and the mole fraction of 2-propanol is 0.25. >>> T = 353.15 >>> N = 2 >>> Vs = [76.92, 18.07] # cm^3/mol >>> ais = [[0.0, 437.98],[1238.0, 0.0]] # cal/mol >>> bis = cis = dis = eis = fis = [[0.0]*N for _ in range(N)] >>> params = Wilson.from_DDBST_as_matrix(Vs=Vs, ais=ais, bis=bis, cis=cis, dis=dis, eis=eis, fis=fis, unit_conversion=True) >>> xs = [0.25, 0.75] >>> GE = Wilson(T=T, xs=xs, lambda_as=params[0], lambda_bs=params[1], lambda_cs=params[2], lambda_ds=params[3], lambda_es=params[4], lambda_fs=params[5]) >>> GE.gammas() [2.124064516, 1.1903745834] The activity coefficients given in [1]_ are [2.1244, 1.1904]; matching ( with a slight deviation from their use of 1.987 as a gas constant). References ---------- .. [1] Smith, H. C. Van Ness Joseph M. Introduction to Chemical Engineering Thermodynamics 4th Edition, Joseph M. Smith, H. C. Van Ness, 1987. .. [2] Kooijman, Harry A., and Ross Taylor. The ChemSep Book. Books on Demand Norderstedt, Germany, 2000. .. [3] Gmehling, Jürgen, Michael Kleiber, Bärbel Kolbe, and Jürgen Rarey. Chemical Thermodynamics for Process Simulation. John Wiley & Sons, 2019. ''' model_id = 200 _model_attributes = ('lambda_as', 'lambda_bs', 'lambda_cs', 'lambda_ds', 'lambda_es', 'lambda_fs') _cached_calculated_attributes = ('_d3GE_dxixjxks', '_xj_dLambda_dTijs', '_xj_Lambda_ijs', '_log_xj_Lambda_ijs', '_dlambdas_dT', '_lambdas', '_d3GE_dT3', '_xj_d2Lambda_dT2ijs', '_xj_Lambda_ijs_inv', '_d2lambdas_dT2', '_d3lambdas_dT3', '_xj_d3Lambda_dT3ijs') __slots__ = GibbsExcess.__slots__ + _model_attributes + _cached_calculated_attributes + ('lambda_coeffs_nonzero',) recalculable_attributes = _cached_calculated_attributes + GibbsExcess.recalculable_attributes gammas_from_args = staticmethod(wilson_gammas_from_args) def gammas_args(self, T=None): if T is not None: obj = self.to_T_xs(T=T, xs=self.xs) else: obj = self lambdas = obj.lambdas() N = obj.N if self.vectorized: xj_Lambda_ijs, vec0 = zeros(N), zeros(N) else: xj_Lambda_ijs, vec0 = [0.0]*N, [0.0]*N return (N, lambdas, xj_Lambda_ijs, vec0)
[docs] @staticmethod def from_DDBST(Vi, Vj, a, b, c, d=0.0, e=0.0, f=0.0, unit_conversion=True): r'''Converts parameters for the wilson equation in the DDBST to the basis used in this implementation. .. math:: \Lambda_{ij} = \frac{V_j}{V_i}\exp\left(\frac{-\Delta \lambda_{ij}}{RT} \right) .. math:: \Delta \lambda_{ij} = a_{ij} + b_{ij}T + c T^2 + d_{ij}T\ln T + e_{ij}T^3 + f_{ij}/T Parameters ---------- Vi : float Molar volume of component i; needs only to be in the same units as `Vj`, [cm^3/mol] Vj : float Molar volume of component j; needs only to be in the same units as `Vi`, [cm^3/mol] a : float `a` parameter in DDBST form, [K] b : float `b` parameter in DDBST form, [-] c : float `c` parameter in DDBST form, [1/K] d : float, optional `d` parameter in DDBST form, [-] e : float, optional `e` parameter in DDBST form, [1/K^2] f : float, optional `f` parameter in DDBST form, [K^2] unit_conversion : bool If True, the input coefficients are in units of cal/K/mol, and a `R` gas constant of 1.9872042... is used for the conversion; the DDBST uses this generally, [-] Returns ------- a : float `a` parameter in :obj:`Wilson` form, [-] b : float `b` parameter in :obj:`Wilson` form, [K] c : float `c` parameter in :obj:`Wilson` form, [-] d : float `d` parameter in :obj:`Wilson` form, [1/K] e : float `e` parameter in :obj:`Wilson` form, [K^2] f : float `f` parameter in :obj:`Wilson` form, [1/K^2] Notes ----- The units show how the different variables are related to each other. Examples -------- >>> Wilson.from_DDBST(Vi=74.04, Vj=80.67, a=375.2835, b=-3.78434, c=0.00791073, d=0.0, e=0.0, f=0.0, unit_conversion=False) (3.8701012712, -375.2835, -0.0, -0.00791073, -0.0, -0.0) ''' if unit_conversion: Rg = 1.9872042586408316 # DDBST document suggests 1.9858775 else: Rg = 1.0 # Not used in some cases - be very careful a, b = log(Vj/Vi) - b/Rg, -a/Rg c, d = -d/Rg, -c/Rg e = -e/Rg f = -f/Rg return (a, b, c, d, e, f)
[docs] @staticmethod def from_DDBST_as_matrix(Vs, ais=None, bis=None, cis=None, dis=None, eis=None, fis=None, unit_conversion=True): r'''Converts parameters for the wilson equation in the DDBST to the basis used in this implementation. Matrix wrapper around :obj:`Wilson.from_DDBST`. Parameters ---------- Vs : list[float] Molar volume of component; needs only to be in consistent units, [cm^3/mol] ais : list[list[float]] `a` parameters in DDBST form, [K] bis : list[list[float]] `b` parameters in DDBST form, [-] cis : list[list[float]] `c` parameters in DDBST form, [1/K] dis : list[list[float]], optional `d` parameters in DDBST form, [-] eis : list[list[float]], optional `e` parameters in DDBST form, [1/K^2] fis : list[list[float]], optional `f` parameters in DDBST form, [K^2] unit_conversion : bool If True, the input coefficients are in units of cal/K/mol, and a `R` gas constant of 1.9872042... is used for the conversion; the DDBST uses this generally, [-] Returns ------- a : list[list[float]] `a` parameters in :obj:`Wilson` form, [-] b : list[list[float]] `b` parameters in :obj:`Wilson` form, [K] c : list[list[float]] `c` parameters in :obj:`Wilson` form, [-] d : list[list[float]] `d` paraemeters in :obj:`Wilson` form, [1/K] e : list[list[float]] `e` parameters in :obj:`Wilson` form, [K^2] f : list[list[float]] `f` parameters in :obj:`Wilson` form, [1/K^2] ''' N = len(Vs) cmps = range(N) if ais is None: ais = [[0.0]*N for _ in range(N)] if bis is None: bis = [[0.0]*N for _ in range(N)] if cis is None: cis = [[0.0]*N for _ in range(N)] if dis is None: dis = [[0.0]*N for _ in range(N)] if eis is None: eis = [[0.0]*N for _ in range(N)] if fis is None: fis = [[0.0]*N for _ in range(N)] a_mat, b_mat, c_mat, d_mat, e_mat, f_mat = [], [], [], [], [], [] for i in cmps: a_row, b_row, c_row, d_row, e_row, f_row = [], [], [], [], [], [] for j in cmps: a, b, c, d, e, f = Wilson.from_DDBST(Vs[i], Vs[j], ais[i][j], bis[i][j], cis[i][j], dis[i][j], eis[i][j], fis[i][j], unit_conversion=unit_conversion) a_row.append(a) b_row.append(b) c_row.append(c) d_row.append(d) e_row.append(e) f_row.append(f) a_mat.append(a_row) b_mat.append(b_row) c_mat.append(c_row) d_mat.append(d_row) e_mat.append(e_row) f_mat.append(f_row) return (a_mat, b_mat, c_mat, d_mat, e_mat, f_mat)
def __init__(self, *, xs, T=GibbsExcess.T_DEFAULT, lambda_coeffs=None, ABCDEF=None, lambda_as=None, lambda_bs=None, lambda_cs=None, lambda_ds=None, lambda_es=None, lambda_fs=None): self.T = T self.xs = xs self.vectorized = vectorized = type(xs) is not list self.N = N = len(xs) if ABCDEF is None: ABCDEF = (lambda_as, lambda_bs, lambda_cs, lambda_ds, lambda_es, lambda_fs) if lambda_coeffs is not None: pass else: try: all_lengths = tuple(len(coeffs) for coeffs in ABCDEF if coeffs is not None) if len(set(all_lengths)) > 1: raise ValueError(f"Coefficient arrays of different size found: {all_lengths}") all_lengths_inner = tuple(len(coeffs[0]) for coeffs in ABCDEF if coeffs is not None) if len(set(all_lengths_inner)) > 1: raise ValueError(f"Coefficient arrays of different size found: {all_lengths_inner}") except: raise ValueError("Coefficients not input correctly") if vectorized: zero_coeffs = zeros((N, N)) else: zero_coeffs = [[0.0]*N for _ in range(N)] if lambda_coeffs is not None: if vectorized: self.lambda_as = array(lambda_coeffs[:,:,0], order='C', copy=True) self.lambda_bs = array(lambda_coeffs[:,:,1], order='C', copy=True) self.lambda_cs = array(lambda_coeffs[:,:,2], order='C', copy=True) self.lambda_ds = array(lambda_coeffs[:,:,3], order='C', copy=True) self.lambda_es = array(lambda_coeffs[:,:,4], order='C', copy=True) self.lambda_fs = array(lambda_coeffs[:,:,5], order='C', copy=True) else: self.lambda_as = [[i[0] for i in l] for l in lambda_coeffs] self.lambda_bs = [[i[1] for i in l] for l in lambda_coeffs] self.lambda_cs = [[i[2] for i in l] for l in lambda_coeffs] self.lambda_ds = [[i[3] for i in l] for l in lambda_coeffs] self.lambda_es = [[i[4] for i in l] for l in lambda_coeffs] self.lambda_fs = [[i[5] for i in l] for l in lambda_coeffs] else: len_ABCDEF = len(ABCDEF) if len_ABCDEF == 0 or ABCDEF[0] is None: self.lambda_as = zero_coeffs else: self.lambda_as = ABCDEF[0] if len_ABCDEF < 2 or ABCDEF[1] is None: self.lambda_bs = zero_coeffs else: self.lambda_bs = ABCDEF[1] if len_ABCDEF < 3 or ABCDEF[2] is None: self.lambda_cs = zero_coeffs else: self.lambda_cs = ABCDEF[2] if len_ABCDEF < 4 or ABCDEF[3] is None: self.lambda_ds = zero_coeffs else: self.lambda_ds = ABCDEF[3] if len_ABCDEF < 5 or ABCDEF[4] is None: self.lambda_es = zero_coeffs else: self.lambda_es = ABCDEF[4] if len_ABCDEF < 6 or ABCDEF[5] is None: self.lambda_fs = zero_coeffs else: self.lambda_fs = ABCDEF[5] # Make an array of values identifying what coefficients are zero. # This may be useful for performance optimization in the future but is # especially important for reducing the size of the __repr__ string. self.lambda_coeffs_nonzero = lambda_coeffs_nonzero = ones(6, bool) if vectorized else [True]*6 for k, coeffs in enumerate([self.lambda_as, self.lambda_bs, self.lambda_cs, self.lambda_ds, self.lambda_es, self.lambda_fs]): nonzero = False for i in range(N): r = coeffs[i] for j in range(N): if r[j] != 0.0: nonzero = True break if nonzero: break lambda_coeffs_nonzero[k] = nonzero def __repr__(self): s = f'{self.__class__.__name__}(T={self.T!r}, xs={self.xs!r}' for i, attr in enumerate(self._model_attributes): if self.lambda_coeffs_nonzero[i]: s += f', {attr}={getattr(self, attr)}' s += ')' return s
[docs] def to_T_xs(self, T, xs): r'''Method to construct a new :obj:`Wilson` instance at temperature `T`, and mole fractions `xs` with the same parameters as the existing object. Parameters ---------- T : float Temperature, [K] xs : list[float] Mole fractions of each component, [-] Returns ------- obj : Wilson New :obj:`Wilson` object at the specified conditions [-] Notes ----- If the new temperature is the same temperature as the existing temperature, if the `lambda` terms or their derivatives have been calculated, they will be set to the new object as well. ''' new = self.__class__.__new__(self.__class__) new.T = T new.xs = xs new.vectorized = self.vectorized new.N = self.N (new.lambda_as, new.lambda_bs, new.lambda_cs, new.lambda_ds, new.lambda_es, new.lambda_fs) = ( self.lambda_as, self.lambda_bs, self.lambda_cs, self.lambda_ds, self.lambda_es, self.lambda_fs) new.lambda_coeffs_nonzero = self.lambda_coeffs_nonzero if T == self.T: try: new._lambdas = self._lambdas except AttributeError: pass try: new._dlambdas_dT = self._dlambdas_dT except AttributeError: pass try: new._d2lambdas_dT2 = self._d2lambdas_dT2 except AttributeError: pass try: new._d3lambdas_dT3 = self._d3lambdas_dT3 except AttributeError: pass return new
[docs] def lambdas(self): r'''Calculate and return the `lambda` terms for the Wilson model for at system temperature. .. math:: \Lambda_{ij} = \exp\left[a_{ij}+\frac{b_{ij}}{T}+c_{ij}\ln T + d_{ij}T + \frac{e_{ij}}{T^2} + f_{ij}{T^2}\right] Returns ------- lambdas : list[list[float]] Lambda terms, asymmetric matrix [-] Notes ----- These `Lambda ij` values (and the coefficients) are NOT symmetric. ''' try: return self._lambdas except AttributeError: pass N = self.N if self.vectorized: lambdas = zeros((N, N)) else: lambdas = [[0.0]*N for _ in range(N)] lambdas = interaction_exp(self.T, N, self.lambda_as, self.lambda_bs, self.lambda_cs, self.lambda_ds, self.lambda_es, self.lambda_fs, lambdas) self._lambdas = lambdas return lambdas
[docs] def dlambdas_dT(self): r'''Calculate and return the temperature derivative of the `lambda` terms for the Wilson model at the system temperature. .. math:: \frac{\partial \Lambda_{ij}}{\partial T} = \left(2 T h_{ij} + d_{ij} + \frac{c_{ij}}{T} - \frac{b_{ij}}{T^{2}} - \frac{2 e_{ij}}{T^{3}}\right) e^{T^{2} h_{ij} + T d_{ij} + a_{ij} + c_{ij} \ln{\left(T \right)} + \frac{b_{ij}}{T} + \frac{e_{ij}}{T^{2}}} Returns ------- dlambdas_dT : list[list[float]] Temperature deriavtives of Lambda terms, asymmetric matrix [1/K] Notes ----- These `Lambda ij` values (and the coefficients) are NOT symmetric. ''' try: return self._dlambdas_dT except AttributeError: pass B = self.lambda_bs C = self.lambda_cs D = self.lambda_ds E = self.lambda_es F = self.lambda_fs T, N = self.T, self.N try: lambdas = self._lambdas except AttributeError: lambdas = self.lambdas() if self.vectorized: dlambdas_dT = zeros((N, N)) else: dlambdas_dT = [[0.0]*N for _ in range(N)] self._dlambdas_dT = dinteraction_exp_dT(T, N, B, C, D, E, F, lambdas, dlambdas_dT) return dlambdas_dT
[docs] def d2lambdas_dT2(self): r'''Calculate and return the second temperature derivative of the `lambda` termsfor the Wilson model at the system temperature. .. math:: \frac{\partial^2 \Lambda_{ij}}{\partial^2 T} = \left(2 f_{ij} + \left(2 T f_{ij} + d_{ij} + \frac{c_{ij}}{T} - \frac{b_{ij}}{T^{2}} - \frac{2 e_{ij}}{T^{3}}\right)^{2} - \frac{c_{ij}}{T^{2}} + \frac{2 b_{ij}}{T^{3}} + \frac{6 e_{ij}}{T^{4}}\right) e^{T^{2} f_{ij} + T d_{ij} + a_{ij} + c_{ij} \ln{\left(T \right)} + \frac{b_{ij}}{T} + \frac{e_{ij}}{T^{2}}} Returns ------- d2lambdas_dT2 : list[list[float]] Second temperature deriavtives of Lambda terms, asymmetric matrix, [1/K^2] Notes ----- These `Lambda ij` values (and the coefficients) are NOT symmetric. ''' try: return self._d2lambdas_dT2 except AttributeError: pass T, N = self.T, self.N try: lambdas = self._lambdas except AttributeError: lambdas = self.lambdas() try: dlambdas_dT = self._dlambdas_dT except AttributeError: dlambdas_dT = self.dlambdas_dT() if not self.vectorized: d2lambdas_dT2 = [[0.0]*N for _ in range(N)] else: d2lambdas_dT2 = zeros((N, N)) self._d2lambdas_dT2 = d2interaction_exp_dT2(T, N, self.lambda_bs, self.lambda_cs, self.lambda_es, self.lambda_fs, lambdas, dlambdas_dT, d2lambdas_dT2) return d2lambdas_dT2
[docs] def d3lambdas_dT3(self): r'''Calculate and return the third temperature derivative of the `lambda` terms for the Wilson model at the system temperature. .. math:: \frac{\partial^3 \Lambda_{ij}}{\partial^3 T} = \left(3 \left(2 f_{ij} - \frac{c_{ij}}{T^{2}} + \frac{2 b_{ij}}{T^{3}} + \frac{6 e_{ij}}{T^{4}}\right) \left(2 T f_{ij} + d_{ij} + \frac{c_{ij}}{T} - \frac{b_{ij}}{T^{2}} - \frac{2 e_{ij}}{T^{3}}\right) + \left(2 T f_{ij} + d_{ij} + \frac{c_{ij}}{T} - \frac{b_{ij}}{T^{2}} - \frac{2 e_{ij}}{T^{3}}\right)^{3} - \frac{2 \left(- c_{ij} + \frac{3 b_{ij}}{T} + \frac{12 e_{ij}}{T^{2}}\right)}{T^{3}}\right) e^{T^{2} f_{ij} + T d_{ij} + a_{ij} + c_{ij} \ln{\left(T \right)} + \frac{b_{ij}}{T} + \frac{e_{ij}}{T^{2}}} Returns ------- d3lambdas_dT3 : list[list[float]] Third temperature deriavtives of Lambda terms, asymmetric matrix, [1/K^3] Notes ----- These `Lambda ij` values (and the coefficients) are NOT symmetric. ''' try: return self._d3lambdas_dT3 except AttributeError: pass T, N = self.T, self.N lambda_bs = self.lambda_bs lambda_cs = self.lambda_cs lambda_es = self.lambda_es lambda_fs = self.lambda_fs try: lambdas = self._lambdas except AttributeError: lambdas = self.lambdas() try: dlambdas_dT = self._dlambdas_dT except AttributeError: dlambdas_dT = self.dlambdas_dT() if not self.vectorized: d3lambdas_dT3s = [[0.0]*N for _ in range(N)] else: d3lambdas_dT3s = zeros((N, N)) self._d3lambdas_dT3 = d3interaction_exp_dT3(T, N, lambda_bs, lambda_cs, lambda_es, lambda_fs, lambdas, dlambdas_dT, d3lambdas_dT3s) return d3lambdas_dT3s
def xj_Lambda_ijs(self): ''' ''' try: return self._xj_Lambda_ijs except AttributeError: pass try: lambdas = self._lambdas except AttributeError: lambdas = self.lambdas() if not self.vectorized: xj_Lambda_ijs = [0.0]*self.N else: xj_Lambda_ijs = zeros(self.N) self._xj_Lambda_ijs = wilson_xj_Lambda_ijs(self.xs, lambdas, self.N, xj_Lambda_ijs) return xj_Lambda_ijs def xj_Lambda_ijs_inv(self): ''' ''' try: return self._xj_Lambda_ijs_inv except AttributeError: pass try: xj_Lambda_ijs = self._xj_Lambda_ijs except AttributeError: xj_Lambda_ijs = self.xj_Lambda_ijs() if not self.vectorized: self._xj_Lambda_ijs_inv = [1.0/x for x in xj_Lambda_ijs] else: self._xj_Lambda_ijs_inv = 1.0/xj_Lambda_ijs return self._xj_Lambda_ijs_inv def log_xj_Lambda_ijs(self): ''' ''' try: return self._log_xj_Lambda_ijs except AttributeError: pass try: xj_Lambda_ijs = self._xj_Lambda_ijs except AttributeError: xj_Lambda_ijs = self.xj_Lambda_ijs() if not self.vectorized: self._log_xj_Lambda_ijs = [log(i) for i in xj_Lambda_ijs] else: self._log_xj_Lambda_ijs = nplog(xj_Lambda_ijs) return self._log_xj_Lambda_ijs def xj_dLambda_dTijs(self): ''' ''' try: return self._xj_dLambda_dTijs except AttributeError: pass try: dlambdas_dT = self._dlambdas_dT except AttributeError: dlambdas_dT = self.dlambdas_dT() if not self.vectorized: xj_dLambda_dTijs = [0.0]*self.N else: xj_dLambda_dTijs = zeros(self.N) self._xj_dLambda_dTijs = wilson_xj_Lambda_ijs(self.xs, dlambdas_dT, self.N, xj_dLambda_dTijs) return xj_dLambda_dTijs def xj_d2Lambda_dT2ijs(self): ''' ''' try: return self._xj_d2Lambda_dT2ijs except AttributeError: pass try: d2lambdas_dT2 = self._d2lambdas_dT2 except AttributeError: d2lambdas_dT2 = self.d2lambdas_dT2() if not self.vectorized: xj_d2Lambda_dT2ijs = [0.0]*self.N else: xj_d2Lambda_dT2ijs = zeros(self.N) self._xj_d2Lambda_dT2ijs = wilson_xj_Lambda_ijs(self.xs, d2lambdas_dT2, self.N, xj_d2Lambda_dT2ijs) return xj_d2Lambda_dT2ijs def xj_d3Lambda_dT3ijs(self): ''' ''' try: return self._xj_d3Lambda_dT3ijs except AttributeError: pass try: d3lambdas_dT3 = self._d3lambdas_dT3 except AttributeError: d3lambdas_dT3 = self.d3lambdas_dT3() if not self.vectorized: xj_d3Lambda_dT3ijs = [0.0]*self.N else: xj_d3Lambda_dT3ijs = zeros(self.N) self._xj_d3Lambda_dT3ijs = wilson_xj_Lambda_ijs(self.xs, d3lambdas_dT3, self.N, xj_d3Lambda_dT3ijs) return xj_d3Lambda_dT3ijs
[docs] def GE(self): r'''Calculate and return the excess Gibbs energy of a liquid phase represented with the Wilson model. .. math:: g^E = -RT\sum_i x_i \ln\left(\sum_j x_j \lambda_{i,j} \right) Returns ------- GE : float Excess Gibbs energy of an ideal liquid, [J/mol] Notes ----- ''' try: return self._GE except AttributeError: pass try: log_xj_Lambda_ijs = self._log_xj_Lambda_ijs except AttributeError: log_xj_Lambda_ijs = self.log_xj_Lambda_ijs() if not self.vectorized: xs, N = self.xs, self.N GE = 0.0 for i in range(N): GE += xs[i]*log_xj_Lambda_ijs[i] else: GE = float((self.xs*log_xj_Lambda_ijs).sum()) self._GE = GE = -GE*R*self.T return GE
[docs] def dGE_dT(self): r'''Calculate and return the temperature derivative of excess Gibbs energy of a liquid phase represented by the Wilson model. .. math:: \frac{\partial G^E}{\partial T} = -R\sum_i x_i \ln\left(\sum_j x_i \Lambda_{ij}\right) -RT\sum_i \frac{x_i \sum_j x_j \frac{\Lambda _{ij}}{\partial T}}{\sum_j x_j \Lambda_{ij}} Returns ------- dGE_dT : float First temperature derivative of excess Gibbs energy of a liquid phase represented by the Wilson model, [J/(mol*K)] Notes ----- '''# Derived with: """from sympy import * N = 4 R, T = symbols('R, T') x1, x2, x3, x4 = symbols('x1, x2, x3, x4') xs = [x1, x2, x3, x4] Lambda11, Lambda12, Lambda13, Lambda14, Lambda21, Lambda22, Lambda23, Lambda24, Lambda31, Lambda32, Lambda33, Lambda34, Lambda41, Lambda42, Lambda43, Lambda44 = symbols( 'Lambda11, Lambda12, Lambda13, Lambda14, Lambda21, Lambda22, Lambda23, Lambda24, Lambda31, Lambda32, Lambda33, Lambda34, Lambda41, Lambda42, Lambda43, Lambda44', cls=Function) Lambda_ijs = [[Lambda11(T), Lambda12(T), Lambda13(T), Lambda14(T)], [Lambda21(T), Lambda22(T), Lambda23(T), Lambda24(T)], [Lambda31(T), Lambda32(T), Lambda33(T), Lambda34(T)], [Lambda41(T), Lambda42(T), Lambda43(T), Lambda44(T)]] ge = 0 for i in range(N): num = 0 for j in range(N): num += Lambda_ijs[i][j]*xs[j] ge -= xs[i]*log(num) ge = ge*R*T diff(ge, T) """ try: return self._dGE_dT except AttributeError: pass try: xj_Lambda_ijs_inv = self._xj_Lambda_ijs_inv except AttributeError: xj_Lambda_ijs_inv = self.xj_Lambda_ijs_inv() try: xj_dLambda_dTijs = self._xj_dLambda_dTijs except AttributeError: xj_dLambda_dTijs = self.xj_dLambda_dTijs() try: GE = self._GE except AttributeError: GE = self.GE() self._dGE_dT = dGE_dT = wilson_dGE_dT(self.xs, self.T, GE, self.N, xj_Lambda_ijs_inv, xj_dLambda_dTijs) return dGE_dT
[docs] def d2GE_dT2(self): r'''Calculate and return the second temperature derivative of excess Gibbs energy of a liquid phase using the Wilson activity coefficient model. .. math:: \frac{\partial^2 G^E}{\partial T^2} = -R\left[T\sum_i \left(\frac{x_i \sum_j (x_j \frac{\partial^2 \Lambda_{ij}}{\partial T^2} )}{\sum_j x_j \Lambda_{ij}} - \frac{x_i (\sum_j x_j \frac{\partial \Lambda_{ij}}{\partial T} )^2}{(\sum_j x_j \Lambda_{ij})^2} \right) + 2\sum_i \left(\frac{x_i \sum_j x_j \frac{\partial \Lambda_{ij}}{\partial T}}{\sum_j x_j \Lambda_{ij}} \right) \right] Returns ------- d2GE_dT2 : float Second temperature derivative of excess Gibbs energy, [J/(mol*K^2)] Notes ----- ''' try: return self._d2GE_dT2 except AttributeError: pass try: xj_Lambda_ijs_inv = self._xj_Lambda_ijs_inv except AttributeError: xj_Lambda_ijs_inv = self.xj_Lambda_ijs_inv() try: xj_dLambda_dTijs = self._xj_dLambda_dTijs except AttributeError: xj_dLambda_dTijs = self.xj_dLambda_dTijs() try: xj_d2Lambda_dT2ijs = self._xj_d2Lambda_dT2ijs except AttributeError: xj_d2Lambda_dT2ijs = self.xj_d2Lambda_dT2ijs() self._d2GE_dT2 = wilson_d2GE_dT2(self.xs, self.T, self.N, xj_Lambda_ijs_inv, xj_dLambda_dTijs, xj_d2Lambda_dT2ijs) return self._d2GE_dT2
[docs] def d3GE_dT3(self): r'''Calculate and return the third temperature derivative of excess Gibbs energy of a liquid phase using the Wilson activity coefficient model. .. math:: \frac{\partial^3 G^E}{\partial T^3} = -R\left[3\left(\frac{x_i \sum_j (x_j \frac{\partial^2 \Lambda_{ij}}{\partial T^2} )}{\sum_j x_j \Lambda_{ij}} - \frac{x_i (\sum_j x_j \frac{\partial \Lambda_{ij}}{\partial T} )^2}{(\sum_j x_j \Lambda_{ij})^2} \right) +T\left( \sum_i \frac{x_i (\sum_j x_j \frac{\partial^3 \Lambda _{ij}}{\partial T^3})}{\sum_j x_j \Lambda_{ij}} - \frac{3x_i (\sum_j x_j \frac{\partial \Lambda_{ij}^2}{\partial T^2}) (\sum_j x_j \frac{\partial \Lambda_{ij}}{\partial T})}{(\sum_j x_j \Lambda_{ij})^2} + 2\frac{x_i(\sum_j x_j \frac{\partial \Lambda_{ij}}{\partial T})^3}{(\sum_j x_j \Lambda_{ij})^3} \right)\right] Returns ------- d3GE_dT3 : float Third temperature derivative of excess Gibbs energy, [J/(mol*K^3)] Notes ----- ''' try: return self._d3GE_dT3 except AttributeError: pass xj_Lambda_ijs_inv = self.xj_Lambda_ijs_inv() xj_dLambda_dTijs = self.xj_dLambda_dTijs() xj_d2Lambda_dT2ijs = self.xj_d2Lambda_dT2ijs() xj_d3Lambda_dT3ijs = self.xj_d3Lambda_dT3ijs() self._d3GE_dT3 = wilson_d3GE_dT3(self.xs, self.T, self.N, xj_Lambda_ijs_inv, xj_dLambda_dTijs, xj_d2Lambda_dT2ijs, xj_d3Lambda_dT3ijs) return self._d3GE_dT3
[docs] def d2GE_dTdxs(self): r'''Calculate and return the temperature derivative of mole fraction derivatives of excess Gibbs energy of a liquid represented by the Wilson model. .. math:: \frac{\partial^2 G^E}{\partial x_k \partial T} = -R\left[T\left( \sum_i \left(\frac{x_i \frac{\partial n_{ik}}{\partial T}}{\sum_j x_j \Lambda_{ij}} - \frac{x_i \Lambda_{ik} (\sum_j x_j \frac{\partial \Lambda_{ij}}{\partial T} )}{(\partial_j x_j \Lambda_{ij})^2} \right) + \frac{\sum_i x_i \frac{\partial \Lambda_{ki}}{\partial T}}{\sum_j x_j \Lambda_{kj}} \right) + \ln\left(\sum_i x_i \Lambda_{ki}\right) + \sum_i \frac{x_i \Lambda_{ik}}{\sum_j x_j \Lambda_{ij}} \right] Returns ------- d2GE_dTdxs : list[float] Temperature derivative of mole fraction derivatives of excess Gibbs energy, [J/mol/K] Notes ----- ''' try: return self._d2GE_dTdxs except AttributeError: pass try: log_xj_Lambda_ijs = self._log_xj_Lambda_ijs except AttributeError: log_xj_Lambda_ijs = self.log_xj_Lambda_ijs() try: lambdas = self._lambdas except AttributeError: lambdas = self.lambdas() try: dlambdas_dT = self._dlambdas_dT except AttributeError: dlambdas_dT = self.dlambdas_dT() try: xj_Lambda_ijs_inv = self._xj_Lambda_ijs_inv except AttributeError: xj_Lambda_ijs_inv = self.xj_Lambda_ijs_inv() try: xj_dLambda_dTijs = self._xj_dLambda_dTijs except AttributeError: xj_dLambda_dTijs = self.xj_dLambda_dTijs() if not self.vectorized: d2GE_dTdxs = [0.0]*self.N else: d2GE_dTdxs = zeros(self.N) wilson_d2GE_dTdxs(self.xs, self.T, self.N, log_xj_Lambda_ijs, lambdas, dlambdas_dT, xj_Lambda_ijs_inv, xj_dLambda_dTijs, d2GE_dTdxs) self._d2GE_dTdxs = d2GE_dTdxs return d2GE_dTdxs
[docs] def dGE_dxs(self): r'''Calculate and return the mole fraction derivatives of excess Gibbs energy for the Wilson model. .. math:: \frac{\partial G^E}{\partial x_k} = -RT\left[ \sum_i \frac{x_i \Lambda_{ik}}{\sum_j \Lambda_{ij}x_j } + \ln\left(\sum_j x_j \Lambda_{kj}\right) \right] Returns ------- dGE_dxs : list[float] Mole fraction derivatives of excess Gibbs energy, [J/mol] Notes ----- ''' """ from sympy import * N = 4 R, T = symbols('R, T') x1, x2, x3, x4 = symbols('x1, x2, x3, x4') xs = [x1, x2, x3, x4] Lambda11, Lambda12, Lambda13, Lambda14, Lambda21, Lambda22, Lambda23, Lambda24, Lambda31, Lambda32, Lambda33, Lambda34, Lambda41, Lambda42, Lambda43, Lambda44 = symbols( 'Lambda11, Lambda12, Lambda13, Lambda14, Lambda21, Lambda22, Lambda23, Lambda24, Lambda31, Lambda32, Lambda33, Lambda34, Lambda41, Lambda42, Lambda43, Lambda44', cls=Function) Lambda_ijs = [[Lambda11(T), Lambda12(T), Lambda13(T), Lambda14(T)], [Lambda21(T), Lambda22(T), Lambda23(T), Lambda24(T)], [Lambda31(T), Lambda32(T), Lambda33(T), Lambda34(T)], [Lambda41(T), Lambda42(T), Lambda43(T), Lambda44(T)]] ge = 0 for i in range(N): num = 0 for j in range(N): num += Lambda_ijs[i][j]*xs[j] ge -= xs[i]*log(num) ge = ge*R*T diff(ge, x1)#, diff(ge, x1, x2), diff(ge, x1, x2, x3) """ try: return self._dGE_dxs except AttributeError: pass try: lambdas = self._lambdas except AttributeError: lambdas = self.lambdas() try: log_xj_Lambda_ijs = self._log_xj_Lambda_ijs except AttributeError: log_xj_Lambda_ijs = self.log_xj_Lambda_ijs() try: xj_Lambda_ijs_inv = self._xj_Lambda_ijs_inv except AttributeError: xj_Lambda_ijs_inv = self.xj_Lambda_ijs_inv() if not self.vectorized: dGE_dxs = [0.0]*self.N else: dGE_dxs = zeros(self.N) dGE_dxs = wilson_dGE_dxs(self.xs, self.T, self.N, log_xj_Lambda_ijs, lambdas, xj_Lambda_ijs_inv, dGE_dxs) self._dGE_dxs = dGE_dxs return dGE_dxs
[docs] def d2GE_dxixjs(self): r'''Calculate and return the second mole fraction derivatives of excess Gibbs energy for the Wilson model. .. math:: \frac{\partial^2 G^E}{\partial x_k \partial x_m} = RT\left( \sum_i \frac{x_i \Lambda_{ik} \Lambda_{im}}{(\sum_j x_j \Lambda_{ij})^2} -\frac{\Lambda_{km}}{\sum_j x_j \Lambda_{kj}} -\frac{\Lambda_{mk}}{\sum_j x_j \Lambda_{mj}} \right) Returns ------- d2GE_dxixjs : list[list[float]] Second mole fraction derivatives of excess Gibbs energy, [J/mol] Notes ----- ''' try: return self._d2GE_dxixjs except AttributeError: pass # Correct, tested with hessian try: lambdas = self._lambdas except AttributeError: lambdas = self.lambdas() try: xj_Lambda_ijs_inv = self._xj_Lambda_ijs_inv except AttributeError: xj_Lambda_ijs_inv = self.xj_Lambda_ijs_inv() N = self.N if not self.vectorized: d2GE_dxixjs = [[0.0]*N for _ in range(N)] else: d2GE_dxixjs = zeros((N, N)) d2GE_dxixjs = wilson_d2GE_dxixjs(self.xs, self.T, N, lambdas, xj_Lambda_ijs_inv, d2GE_dxixjs) self._d2GE_dxixjs = d2GE_dxixjs return d2GE_dxixjs
[docs] def d3GE_dxixjxks(self): r'''Calculate and return the third mole fraction derivatives of excess Gibbs energy using the Wilson model. .. math:: \frac{\partial^3 G^E}{\partial x_k \partial x_m \partial x_n} = -RT\left[ \sum_i \left(\frac{2x_i \Lambda_{ik}\Lambda_{im}\Lambda_{in}} {(\sum x_j \Lambda_{ij})^3}\right) - \frac{\Lambda_{km} \Lambda_{kn}}{(\sum_j x_j \Lambda_{kj})^2} - \frac{\Lambda_{mk} \Lambda_{mn}}{(\sum_j x_j \Lambda_{mj})^2} - \frac{\Lambda_{nk} \Lambda_{nm}}{(\sum_j x_j \Lambda_{nj})^2} \right] Returns ------- d3GE_dxixjxks : list[list[list[float]]] Third mole fraction derivatives of excess Gibbs energy, [J/mol] Notes ----- ''' try: return self._d3GE_dxixjxks except AttributeError: pass # Correct, tested with sympy expanding lambdas = self.lambdas() try: xj_Lambda_ijs_inv = self._xj_Lambda_ijs_inv except AttributeError: xj_Lambda_ijs_inv = self.xj_Lambda_ijs_inv() N = self.N if not self.vectorized: d3GE_dxixjxks = [[[0.0]*N for _ in range(N)] for _ in range(N)] else: d3GE_dxixjxks = zeros((N, N, N)) # all the same: analytical[i][j][k] = analytical[i][k][j] = analytical[j][i][k] = analytical[j][k][i] = analytical[k][i][j] = analytical[k][j][i] = float(v) d3GE_dxixjxks = wilson_d3GE_dxixjxks(self.xs, self.T, self.N, lambdas, xj_Lambda_ijs_inv, d3GE_dxixjxks) self._d3GE_dxixjxks = d3GE_dxixjxks return d3GE_dxixjxks
def gammas(self): # With this formula implemented, dgammas_dxs cannot be calculated # numerically. # Don't bother documenting or exposing; implemented only for a bit more # speed and precision. try: return self._gammas except AttributeError: pass try: lambdas = self._lambdas except AttributeError: lambdas = self.lambdas() try: xj_Lambda_ijs_inv = self._xj_Lambda_ijs_inv except AttributeError: xj_Lambda_ijs_inv = self.xj_Lambda_ijs_inv() if not self.vectorized: gammas = [0.0]*self.N else: gammas = zeros(self.N) wilson_gammas(self.xs, self.N, lambdas, xj_Lambda_ijs_inv, gammas) self._gammas = gammas return gammas @classmethod def regress_binary_parameters(cls, gammas, xs, use_numba=False, do_statistics=True, **kwargs): # Load the functions either locally or with numba if use_numba: from thermo.numba import wilson_gammas_binaries as work_func from thermo.numba import wilson_gammas_binaries_jac as jac_func else: work_func = wilson_gammas_binaries jac_func = wilson_gammas_binaries_jac # Allocate all working memory pts = len(xs) pts2 = pts*2 gammas_iter, jac_iter = zeros(pts2), zeros((pts2, 2)) # Plain objective functions def fitting_func(xs, lambda12, lambda21): return work_func(xs, lambda12, lambda21, gammas_iter) def analytical_jac(xs, lambda12, lambda21): return jac_func(xs, lambda12, lambda21, jac_iter) # The extend calls has been tested to be the fastest compared to numpy and list comprehension xs_working = [] for xsi in xs: xs_working.extend(xsi) gammas_working = [] for gammasi in gammas: gammas_working.extend(gammasi) xs_working = array(xs_working) gammas_working = array(gammas_working) # Objective functions for leastsq maximum speed def func_wrapped_for_leastsq(params): return work_func(xs_working, params[0], params[1], gammas_iter) - gammas_working def jac_wrapped_for_leastsq(params): return jac_func(xs_working, params[0], params[1], jac_iter) fit_parameters = ['lambda12', 'lambda21'] return GibbsExcess._regress_binary_parameters(gammas_working, xs_working, fitting_func=fitting_func, fit_parameters=fit_parameters, use_fit_parameters=fit_parameters, initial_guesses=cls._gamma_parameter_guesses, analytical_jac=analytical_jac, use_numba=use_numba, do_statistics=do_statistics, func_wrapped_for_leastsq=func_wrapped_for_leastsq, jac_wrapped_for_leastsq=jac_wrapped_for_leastsq, **kwargs) # Larger value on the right always _gamma_parameter_guesses = [{'lambda12': 1, 'lambda21': 1}, {'lambda12': 2.2, 'lambda21': 3.0}, {'lambda12': 0.015, 'lambda21': 37.0}, {'lambda12': 0.5, 'lambda21': 40.0}, {'lambda12': 1e-7, 'lambda21': .5}, {'lambda12': 1e-12, 'lambda21': 1.9}, {'lambda12': 1e-12, 'lambda21': 10.0}, ] for i in range(len(_gamma_parameter_guesses)): r = _gamma_parameter_guesses[i] _gamma_parameter_guesses.append({'lambda12': r['lambda21'], 'lambda21': r['lambda12']}) del i, r
[docs]def Wilson_gammas(xs, params): r'''Calculates the activity coefficients of each species in a mixture using the Wilson method, given their mole fractions, and dimensionless interaction parameters. Those are normally correlated with temperature, and need to be calculated separately. .. math:: \ln \gamma_i = 1 - \ln \left(\sum_j^N \Lambda_{ij} x_j\right) -\sum_j^N \frac{\Lambda_{ji}x_j}{\displaystyle\sum_k^N \Lambda_{jk}x_k} Parameters ---------- xs : list[float] Liquid mole fractions of each species, [-] params : list[list[float]] Dimensionless interaction parameters of each compound with each other, [-] Returns ------- gammas : list[float] Activity coefficient for each species in the liquid mixture, [-] Notes ----- This model needs N^2 parameters. The original model correlated the interaction parameters using the standard pure-component molar volumes of each species at 25°C, in the following form: .. math:: \Lambda_{ij} = \frac{V_j}{V_i} \exp\left(\frac{-\lambda_{i,j}}{RT}\right) If a compound is not liquid at that temperature, the liquid volume is taken at the saturated pressure; and if the component is supercritical, its liquid molar volume should be extrapolated to 25°C. However, that form has less flexibility and offered no advantage over using only regressed parameters. Most correlations for the interaction parameters include some of the terms shown in the following form: .. math:: \ln \Lambda_{ij} =a_{ij}+\frac{b_{ij}}{T}+c_{ij}\ln T + d_{ij}T + \frac{e_{ij}}{T^2} + h_{ij}{T^2} The Wilson model is not applicable to liquid-liquid systems. For this model to produce ideal acitivty coefficients (gammas = 1), all interaction parameters should be 1. The specific process simulator implementations are as follows: Examples -------- Ethanol-water example, at 343.15 K and 1 MPa, from [2]_ also posted online http://chemthermo.ddbst.com/Problems_Solutions/Mathcad_Files/P05.01a%20VLE%20Behavior%20of%20Ethanol%20-%20Water%20Using%20Wilson.xps : >>> Wilson_gammas([0.252, 0.748], [[1, 0.154], [0.888, 1]]) [1.881492608717, 1.165577493112] References ---------- .. [1] Wilson, Grant M. "Vapor-Liquid Equilibrium. XI. A New Expression for the Excess Free Energy of Mixing." Journal of the American Chemical Society 86, no. 2 (January 1, 1964): 127-130. doi:10.1021/ja01056a002. .. [2] Gmehling, Jurgen, Barbel Kolbe, Michael Kleiber, and Jurgen Rarey. Chemical Thermodynamics for Process Simulation. 1st edition. Weinheim: Wiley-VCH, 2012. ''' gammas = [] cmps = range(len(xs)) sums0 = [] for j in cmps: tot = 0.0 paramsj = params[j] for k in cmps: tot += paramsj[k]*xs[k] sums0.append(tot) for i in cmps: tot2 = 0. for j in cmps: tot2 += params[j][i]*xs[j]/sums0[j] gamma = exp(1. - log(sums0[i]) - tot2) gammas.append(gamma) return gammas