In the series
about helium-containing mixtures, I created a simple
flash procedure for determining vapour-liquid equilibrium in
systems such as (methane + helium) and (nitrogen + helium) described by
the GERG-2008 Equation of State [1]. I found that the
simple solver was not very reliable – sometimes failing to converge
or converging to an incorrect solution. I also tested the flash routines
from TREND 4.0, REFPROP 10.0 and the
julia package ThermoModels.jl
and found them all lacking in some respects. In this series of posts, I'd like
to arrive at a more reliable method of solving the flash problem for
binary mixtures containing helium.
Before writing any code, my first objective is to develop
a set of benchmark results.
In the course of testing TREND 4.0 [2] and REFPROP 10.0 [3], I
flashed (or attempted to flash) the (methane + helium) system at more than 100
distinct pTz conditions with both solvers. Comparing the results, I find
that 56 of the calculations are in close agreement (which I chose
as being within 2×10−6 in the computed vapour fraction)
which means they probably correspond to the correct equilibrium state.
The 56 points are listed in flash_benchmark_GERG2008_methane+helium.csv
the first few rows of which look like:
"T", "p", "z1", "beta", "y1", "x1" 189.01, 6891.2, 0.9086, 0.287420399, 0.784028555, 0.95884614 189.01, 4974.5, 0.97685, 0.295109999, 0.950485776, 0.987887674 188.01, 4819.4, 0.9757, 0.32786611, 0.94800237, 0.989210871with T in Kelvin, p in kPa, and all composition variables in mole fractions where methane is component 1.
The following code runs the flash calculations and compares to the benchmark results:
@testset "pTxy data (methane+helium)" begin
methane = AdvancedThermo.get_fluid_component("methane")
helium = AdvancedThermo.get_fluid_component("helium")
mixture = AdvancedThermo.create_mixture([methane,helium])
file = "flash_benchmark_GERG2008_methane+helium.csv"
correct = 0
errors = 0
for row in CSV.File(file)
z = [row.z1, 1-row.z1]
try
# Uncomment one line below
#(x,y,β) = AdvancedThermo.flash(mixture,row.p*1e3,row.T,z)
(x,y,β) = AdvancedThermo.flash_helium_binary(mixture,row.p*1e3,row.T,z)
#(x,y,β) = AdvancedThermo.flash_initial_K(mixture,row.p*1e3,row.T,z,[row.y1/row.x1, (1-row.y1)/(1-row.x1)])
if β==0.0 || β==1.0
@info "Single phase - row $(row.n)"
elseif abs(β-row.beta)>2.e-6
@info "Incorrect β - row $(row.n)"
elseif abs(x[1]-row.x1)>2.e-6
@info "Incorrect x1 - row $(row.n)"
elseif abs(y[1]-row.y1)>2.e-6
@info "Incorrect y1 - row $(row.n)"
else
correct += 1
end
catch e
@info "Internal error - row $(row.n)"
println(e.msg)
errors += 1
end
end
@info "Correct: $correct, Incorrect: $(56-correct-errors), Errors: $errors"
end
The statistics for the 56 flash cases are given below for flash()
using Wilson K-factors, flash_helium_binary()
(both developed here),
and flash_initial_K()
using the benchmark K factors as
initial estimates:
flash() Correct: 2, Incorrect: 54, Errors: 0 flash_helium_binary() Correct: 37, Incorrect: 19, Errors: 0 flash_initial_K() Correct: 46, Incorrect: 10, Errors: 0At a later time I will look into why some of the flash results differ from the benchmark when using the K factors at the solution. For now, the performance of
flash_helium_binary()
serves as a baseline from
which to make improvements.
I'll improve the flash using some of the techniques described by Michelsen and Mollerup [4]. First I'll implement a stability analysis based on finding minima in the tangent plane distance. The code looks like the following:
function stability_helium_binary(m::FluidMixture,p,T,z)
lnK = lnK_helium_binary(m,p,T)
K = exp.(lnK)
W = K.*z
Wv = minimise_tm(m,p,T,z,W)
W = z./K
Wl = minimise_tm(m,p,T,z,W)
tpd_v = tpd(m,p,T,z,Wv./sum(Wv))
tpd_l = tpd(m,p,T,z,Wl./sum(Wl))
tm_v = tm(m,p,T,z,Wv)
tm_l = tm(m,p,T,z,Wl)
if tpd_v < tpd_l
return (Wv, tpd_v, tm_v)
else
return (Wl, tpd_l, tm_l)
end
end
function minimise_tm(m::FluidMixture,p,T,z,W)
d = log.(z) .+ lnϕ(m,ρ(m,p,T,z),T,z)
for i = 1:200
w = W./sum(W)
# Michelsen/Mollerup (10.51)
Wnew = exp.(d .- lnϕ(m,ρ(m,p,T,w),T,w))
if maximum(abs.(Wnew.-W))<1.e-5
return Wnew
end
W .= Wnew
end
@warn "failed to minimise tm(W)"
return W
end
function tpd(m::FluidMixture,p,T,z,w)
# Michelsen/Mollerup (9.4) and (10.42)
return sum(w.*(log.(w).+lnϕ(m,ρ(m,p,T,w),T,w).-log.(z).-lnϕ(m,ρ(m,p,T,z),T,z)))
end
function tm(m::FluidMixture,p,T,z,W)
WT = sum(W)
w = W./WT
return 1 - WT + WT*log(WT) + WT*tpd(m,p,T,z,w)
end
Note that the minimum of tm is found simply
by applying up to 200 rounds of successive substitution
(50 iterations was not enough for a handful of cases in the benchmark suite).
When applied to the benchmark flash results I hope that all 56 cases
are found to be unstable. The code to check this looks like:
@testset "stability benchmark (methane+helium)" begin
methane = AdvancedThermo.get_fluid_component("methane")
helium = AdvancedThermo.get_fluid_component("helium")
mixture = AdvancedThermo.create_mixture([methane,helium])
file = "flash_benchmark_GERG2008_methane+helium.csv"
for row in CSV.File(file)
z = [row.z1, 1-row.z1]
p = row.p*1.e3
T = row.T
try
(W,tpd,tm) = AdvancedThermo.stability_helium_binary(mixture,p,T,z)
if tm >= 0.0
@info "tm>0 - row $(row.n)"
end
catch e
@info "Internal error - row $(row.n)"
end
end
end
All of the benchmark cases converge to an unstable result except
benchmark 32:
"n", "T", "p", "z1", "beta", "y1", "x1" 32, 154.01, 5560.6, 0.63655, 0.515872068, 0.309908188, 0.984609627This point will not converge regardless of the number of successive substitution iterations. The problematic call is the following
minimise_tm(m, 5560.6e3, 154.01, [0.63655, 1-0.63655],
[2.889229990105307, 1.5737191000899076e-5])
which produces an oscillating W:
# result of "@info Wnew, tmW" ... snip first few iterations ... [ Info: ([1.4812301176977118, 0.009811385532533598], -2.5764174126546933) [ Info: ([8.710330873755286, 1.3834227689297245e-26], 7.721212493767755) [ Info: ([1.4812301176977118, 0.009811385532533598], -2.5764174126546933) [ Info: ([8.710330873755286, 1.3834227689297245e-26], 7.721212493767755)One of the only papers I can recall that discusses non-convergence of successive substitution like this is by Heidemann and Michelsen [5]. They show that mixtures with strong negative deviations from ideality can lead to non-convergent behaviour. I don't expect that is what is occurring here; otherwise I think I would have seen many more failures. However, for peace of mind it's worth verifying.
For stability calculations the convergence behaviour is dictated by eigenvalues of the matrix containing partial derivatives like [5]:
ForwardDiff.jl
and
DiffResults.jl
like so:
function ∂lnϕ∂n(m::FluidMixture,V,T,n)
dna = AdvancedThermo.derivatives_of_nαʳ(m,V,T,n)
dp = AdvancedThermo.derivatives_of_p(m,V,T,n)
∂p∂V = DiffResults.gradient(dp)[1]
∂p∂n = DiffResults.gradient(dp)[3:end]
∂lnϕ∂n = DiffResults.hessian(dna)[3:end,3:end] .+ 1 .+
∂p∂n*∂p∂n'./(R*T*∂p∂V)
return ∂lnϕ∂n
end
I added also some unit tests to validate the function.
As expected, when I form the matrix of partial derivatives and find the eigenvalues
they don't violate the conditions outlined by
Heidemann and Michelsen [5] so it seems like something else is to blame.
I examined parts of the Gibbs energy curve and tangent plane distance with code like
for w=0.01:0.01:0.99
ρ = AdvancedThermo.ρ(mixture, 5560.6e3, 154.01, [w, 1-w])
println(AdvancedThermo.g(mixture, ρ, 154.01, [w, 1-w]))
end
for w=0.01:0.01:0.99
println(AdvancedThermo.tpd(mixture, 5560.6e3, 154.01, [0.63655, 1-0.63655], [w, 1-w]))
end
and nothing stood out as likely to cause problems.
Unfortunately, I still don't know why the successive substitution isn't
converging to the minimum of tm. However, in the interests of
time I'll try a simple safeguard that rejects changes in W
that increase tm. The modified routine looks like (changes in bold):
function minimise_tm(m::FluidMixture,p,T,z,W)
d = log.(z) .+ lnϕ(m,ρ(m,p,T,z),T,z)
tm_ = tm(m,p,T,z,W)
for i = 1:200
w = W./sum(W)
# Michelsen/Mollerup (10.51)
Wnew = exp.(d .- lnϕ(m,ρ(m,p,T,w),T,w))
tmnew = tm(m,p,T,z,Wnew)
if maximum(abs.(Wnew.-W))<1.e-5
return Wnew
end
# reject steps that don't decrease tm
if tmnew >= tm_
return W
end
W .= Wnew
tm_ = tmnew
end
@warn "failed to minimise tm(W)"
return W
end
With this modification, all of the stability benchmark cases are
found successfully to be unstable. I don't know yet that
the results are completely accurate, but I expect that to become
clearer as I construct the next part
of the reliable flash algorithm.
The code so far can be obtained by downloading the zip file.
[1] Kunz and Wagner, J. Chem. Eng. Data, 2012, 57, 3032 (link to publisher)
[2] Span et al., TREND. Thermodynamic Reference and Engineering Data 4.0. Lehrstuhl für Thermodynamik, Ruhr-Universität Bochum, 2019
[3] Lemmon et al., REFPROP Documentation, Release 10.0, NIST (link to FAQ)
[4] Michelsen and Mollerup, Thermodynamic Models: Fundamentals & Computational Aspects, 2nd Edition, Tie-Line Publications, 2007
[5] Heidemann and Michelsen, Ind. Eng. Chem. Res., 1995, 34, 958 (link to publisher)
Go to Home | How to cite this page | Permalink to this page |