Advanced Thermodynamics

advancedthermo.com

Reliable flash for mixtures containing helium - Part 1

23 October 2021

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.989210871
with 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: 0
At 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.984609627
This 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]:

The derivatives can be written based on the equations given by Kunz and Wagner [1] with automatic differentiation performed with 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.

References

[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