CalebBell / thermo

Thermodynamics and Phase Equilibrium component of Chemical Engineering Design Library (ChEDL)
MIT License
594 stars 114 forks source link

Completing activity coefficient partial derivatives #111

Closed marcosfelt closed 2 years ago

marcosfelt commented 2 years ago

Hi! Thanks for creating this amazing package!

I want to get access to the partial derivatives of the activity coefficient with respect to the mole fraction. I see that this has been commented out as incorrect in the current implementation. Any pointers on how I might be able to contribute a fix?

https://github.com/CalebBell/thermo/blob/6df9cbea2c4d8ed63325041a733a87881f515102/thermo/activity.py#L938

CalebBell commented 2 years ago

Hi Kobi,

This is a tough problem. I thought I had the right formula but it turned out not to be correct, and after some more work I'm not less sure there is a proper general formula. My original hope and anticipation was that this could be calculated as a combination of the properties GE, dGE_dxs, and d2GE_dxixjs. I would be greatly appreciative if you could find the proper formula. I haven't found it published anywhere before.

You can check your work by taking the numerical derivative and comparing to your implementation. Right now, there is only one activity model with the correct analytical expression for dgammas_dxs - UNIFAC, which came from the shape of its equation. A sample check is shown below.

from thermo.unifac import UNIFAC, DOUFIP2006, DOUFSG
from fluids.numerics import *
import numpy as np

T = 373.15
xs = [0.2, 0.3, 0.1, 0.4]
chemgroups = [{9:6}, {78:6}, {1:1, 18:1}, {1:1, 2:1, 14:1}]
# m = Mixture(['benzene', 'cyclohexane', 'acetone', 'ethanol'], zs=xs, T=T, P=1e5)
GE = UNIFAC.from_subgroups(T=T, xs=xs, chemgroups=chemgroups, version=1,
                           interaction_data=DOUFIP2006, subgroups=DOUFSG)

def to_jac_gammas(xs):
    return GE.to_T_xs(T, xs).gammas()

dgammas_dxs_num = jacobian(to_jac_gammas, xs, scalar=False, perturbation=1e-6)
dgammas_dxs_num/np.array(GE.dgammas_dxs())

Sincerely, Caleb

marcosfelt commented 2 years ago

Hi Caleb,

I've been thinking about this for the past couple days. I stumbled across this resource from the people who created the CocoSimulator. There is also a more explicit version for difference activity coefficients from this older paper as well (can send individually if anyone doesn't have access).

I am still trying to wrap my head around their derivation, but I think the core idea is to use the relationship between constrained and unconstrained derivatives (equation 19 in the first link) to then find molar derivatives (equation 26).

marcosfelt commented 2 years ago

Spent some time reading in depth today and it seems like this is the correct equation:.

equation

From what I understand you can arbitrarily choose m. This is the component which will be determined by all the others. In the paper, they choose the nth component. From the third page in this paper: image

I am going to try putting this in code and cross-checking with UNIFAC as suggested.

marcosfelt commented 2 years ago

I gave this a try, and it didn't seem to work, so I'm going to close the issue for now. Code below for reference.

from thermo.unifac import UNIFAC, DOUFIP2006, DOUFSG
from thermo import GibbsExcess
from fluids.numerics import *
from fluids.constants import R_inv
import numpy as np

T = 373.15
xs = [0.2, 0.3, 0.1, 0.4]
chemgroups = [{9: 6}, {78: 6}, {1: 1, 18: 1}, {1: 1, 2: 1, 14: 1}]
# m = Mixture(['benzene', 'cyclohexane', 'acetone', 'ethanol'], zs=xs, T=T, P=1e5)
GE = UNIFAC.from_subgroups(
    T=T,
    xs=xs,
    chemgroups=chemgroups,
    version=1,
    interaction_data=DOUFIP2006,
    subgroups=DOUFSG,
)

def to_jac_gammas(xs):
    return GE.to_T_xs(T, xs).gammas()

def dgamma_dxs_constrained(ge: GibbsExcess, xs, N, T):
    q2 = ge.d2GE_dxixjs()
    gammas = ge.gammas()
    matrix = np.zeros((N, N))
    RT_inv = R_inv / T
    for i in range(N):
        for l in range(N):
            matrix[i, l] += q2[i][l]
            matrix[i, l] -= q2[i][N - 2]
            for k in range(N):
                matrix[i, l] += xs[k] * (q2[k][l] - q2[k][N - 2])
            matrix[i, l] *= gammas[i]

    return matrix * RT_inv

def dgamma_dxs_unconstrained(ge: GibbsExcess, xs, N, T):
    q2 = ge.d2GE_dxixjs()
    gammas = ge.gammas()
    matrix = np.zeros((N, N))
    RT_inv = R_inv / T
    for i in range(N):
        for l in range(N):
            matrix[i, l] += q2[i][l]
            for k in range(N):
                matrix[i, l] += xs[k] * q2[k][l]
            matrix[i, l] *= gammas[i]
    return matrix * RT_inv

# print(GE.d2GE_dxixjs())
# print(GE.gammas())
dgammas_dxs_num = jacobian(to_jac_gammas, xs, scalar=False, perturbation=1e-6)
unifac = np.array(GE.dgammas_dxs())
unconstrained = dgamma_dxs_unconstrained(GE, xs, 4, T)
constrained = dgamma_dxs_constrained(GE, xs, 4, T)
print("UNIFAC")
print(unifac)
print("My formula unconstrained")
print(unconstrained)
print("My formula constrained")
print(constrained)

print("Ratio unifac")
print(unifac / dgammas_dxs_num)
print("Ratio constrained")
print(constrained / dgammas_dxs_num)

print("Ratio unconstrained")
print(unconstrained / dgammas_dxs_num)
CalebBell commented 2 years ago

Hi Kobi, This is unfortunate. Not every formula published in the literature is accurate, for lots of reasons. Which activity model were you trying to obtain the derivatives with? I may still be able to offer help if you are more specific. With SymPy, I have always been able to obtain any derivatives for a specific model, although it can take some time.

Caleb

marcosfelt commented 2 years ago

Hi Caleb,

I was working with UNIFAC. I wrote out all the math here that I used to get the general formula I noted above: https://kobifelton.com/notes/composition-derivatives-of-activity-coefficients

Working on the specific activity model is actually a good idea. I'm using NRTL, so I'll try to work straight from that since there are some derivations in the literature. If I get it working, I'll submit a PR.