Fit Pure Fluid ParametersΒΆ

This example shows how to use the new fitting class of version 0.21 of teqp to fit the model parameters for the SAFT-VR-Mie model based on fitting pseudo-experimental data obtained from a reference equation of state.

In version 0.23, the parameter optimization objects were moved into their own submodule called paramopt

[1]:
import teqp
import numpy as np
import matplotlib.pyplot as plt
import CoolProp.CoolProp as CP
import scipy.optimize
display(teqp.__version__)

nonpolar = {
    "kind": "SAFT-VR-Mie",
    "model": {
        "coeffs": [
        {
          "name": "R32",
          "BibTeXKey": "Bell",
          "m": 1.2476268271391935,
          "sigma_m": 3.6080717234117107e-10,
          "epsilon_over_k": 172.53065054286867,
          "lambda_r": 14.634722358167384,
          "lambda_a": 6
        }
      ]
    }
}
template = {
  'kind': 'genericSAFT',
  'model': {
      'nonpolar': nonpolar
  }
}

# NOTE: '/' inside field names MUST be escaped as ~1; see https://datatracker.ietf.org/doc/html/rfc6901#section-3
pointers = [
    '/model/nonpolar/model/coeffs/0/m',
    '/model/nonpolar/model/coeffs/0/sigma_m',
    '/model/nonpolar/model/coeffs/0/epsilon_over_k',
    '/model/nonpolar/model/coeffs/0/lambda_r',
    '/model/nonpolar/model/coeffs/0/lambda_a'
]
x0 = [1.5, 3e-10, 150, 19, 5.7]
bounds = [(1,5), (2e-10,5e-10), (100,400), (12,50), (5.1, 6.0)]
'0.22.0'
[2]:
FLD = 'Methane'
ppo = teqp.paramopt.PureParameterOptimizer(template, pointers)

Ts = np.linspace(100, 170, 10)

# Generate some artificial pseudo-experimental data from
# the reference EOS as implemented in CoolProp
for T in Ts:
    pt = teqp.paramopt.SatRhoLPWPoint()
    rhoL, rhoV = [CP.PropsSI('Dmolar','Q',Q,'T',T,FLD) for Q in [0,1]]
    p = CP.PropsSI('P','Q',0,'T',T,FLD)
    w = CP.PropsSI('speed_of_sound','Q',0,'T',T,FLD)
    pt.T = T
    # Measurands (here, pseudo-experimental values)
    pt.p_exp = p
    pt.rhoL_exp = rhoL
    pt.w_exp = w

    # Additional parameters
    pt.rhoL_guess = rhoL
    pt.rhoV_guess = rhoV
    pt.R = 8.31446261815324
    pt.M = CP.PropsSI('molemass',FLD)
    AS = CP.AbstractState('HEOS',FLD)
    AS.update(CP.DmolarT_INPUTS, 1e-10, T)
    pt.Ao20 = AS.tau()**2*AS.d2alpha0_dTau2() # -cv0/R

    # Weights (multiplied by 100 to put on a percentage basis)
    pt.weight_p = 1*100
    pt.weight_rho = 1*100
    pt.weight_w = 0.25*100
    ppo.add_one_contribution(pt)

def cost_function(x):
#     return ppo.cost_function_threaded(x, 10) # This is an option if you have lots of threads
    return ppo.cost_function(x)

r = scipy.optimize.differential_evolution(cost_function, bounds=bounds, disp=True, maxiter=10000, popsize=8)
print(r)
x = r.x
model = teqp.make_model(ppo.build_JSON(x))

Tc, rhoc = model.solve_pure_critical(400, 5000)
print(Tc, rhoc)
anc = teqp.build_ancillaries(model, Tc, rhoc, 0.5*Tc)

def _get_SOS(model, T, rho, z, *, R, M, Ao20):
    """ Helper function to calculate speed of sound """
    Ar0n = model.get_Ar02n(T, rho, z)
    Ar01 = Ar0n[1]; Ar02 = Ar0n[2]
    Ar11 = model.get_Ar11(T, rho, z)
    Ar20 = model.get_Ar20(T, rho, z)

    # M*w^2/(R*T) where w is the speed of sound
    # from the definition w = sqrt(dp/drho|s)
    Mw2RT = 1 + 2*Ar01 + Ar02 - (1 + Ar01 - Ar11)**2/(Ao20 + Ar20)
    if Mw2RT < 0:
        return 1e6
    w = (Mw2RT*R*T/M)**0.5
    return w

TcREF = CP.PropsSI('Tcrit', FLD)
Tsverify = np.linspace(100, TcREF*0.99999, 1000)
RHOL, RHOV, PPP, WWW = [],[],[],[]
z = np.array([1.0])
for T in Tsverify:
    rhoL, rhoV = model.pure_VLE_T(T, anc.rhoL(T)*1.01, anc.rhoV(T)*0.9, 10)
    pL = rhoL*model.get_R(z)*T*(1+model.get_Ar01(T, rhoL, z))
    pV = rhoV*model.get_R(z)*T*(1+model.get_Ar01(T, rhoV, z))
    RHOL.append(rhoL)
    RHOV.append(rhoV)
    PPP.append(pL)
    AS = CP.AbstractState('HEOS',FLD)
    AS.update(CP.DmolarT_INPUTS, 1e-10, T)
    Ao20 = AS.d2alpha0_dTau2()*AS.tau()**2
    WWW.append(_get_SOS(model, T, rhoL, z, R=8.314462618,M=CP.PropsSI('molemass',FLD),Ao20=Ao20))

# Plot the T-rho for VLE
line, = plt.plot(np.array(RHOL), Tsverify)
plt.plot(np.array(RHOV), Tsverify, color=line.get_color())
for Q in [0, 1]:
    D = CP.PropsSI('Dmolar','T',Tsverify,'Q',Q,FLD)
    plt.plot(D, Tsverify, lw=2, color='k')
plt.gca().set(xlabel=r'$\rho$ / mol/m$^3$', ylabel='$T$ / K')

# And a deviation plot, much closer to the critical point
# than the fitting region
fig, (ax1, ax2, ax3) = plt.subplots(3,1, figsize=(10,5), sharex=True)

ax1.plot(Tsverify, (np.array(PPP)/CP.PropsSI('P','T',Tsverify,'Q',0,FLD)-1)*100)
ax1.set(ylabel=r'$(p_{fit}/p_{\rm pexp}-1)\times 100$')

ax2.plot(Tsverify, (np.array(RHOL)/CP.PropsSI('Dmolar','T',Tsverify,'Q',0,FLD)-1)*100)
ax2.set(ylabel=r'$(\rho_{fit}/\rho_{\rm pexp}-1)\times 100$')

ax3.plot(Tsverify, (np.array(WWW)/CP.PropsSI('speed_of_sound','T',Tsverify,'Q',0,FLD)-1)*100)
ax3.set(ylabel=r'$(w_{fit}/w_{\rm pexp}-1)\times 100$', xlabel='$T$ / K')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[2], line 2
      1 FLD = 'Methane'
----> 2 ppo = teqp.paramopt.PureParameterOptimizer(template, pointers)
      4 Ts = np.linspace(100, 170, 10)
      6 # Generate some artificial pseudo-experimental data from 
      7 # the reference EOS as implemented in CoolProp

AttributeError: module 'teqp' has no attribute 'paramopt'