Advanced Thermodynamics

advancedthermo.com

Thermodynamic properties of natural gas mixtures containing helium - Part 3

3 July 2021

This is Part 3 of a series.

In Part 2, I showed how to calculate the residual contribution to the reduced Helmholtz energy of mixtures based on the mixing rules developed by Kunz et al. [1] as part of the GERG-2004 EOS. To calculate other fluid properties — such as pressure, enthalpy, or speed of sound — derivatives of the Helmholtz energy are needed. The task of calculating derivatives will be explored below.

One of the main reasons that I chose Julia to implement the Equation of State was that some Automatic Differentiation (AD) packages are available. Although the derivatives could be coded by hand, this will hopefully provide a shortcut. There are actually a large number of available packages: I'll try ForwardDiff.jl at first.

Relationships between the derivatives of the Helmholtz energy and common thermodynamic properties are given in Table B1 of Kunz and Wagner [2]. There are two main things it looks like I need to address:

  1. the derivatives are in terms of Helmholtz energy a, not αr
  2. derivatives are with respect to v, T and x
To address point 1, I'll implement some methods for calculating the overall Helmholtz energy, i.e. a = ao + ar. The main equations for calculating the ideal-gas contribution to the Helmholtz energy are Eqs. (9) and (12) of Kunz and Wagner [2] and their implementation looks like:

function αᵒ(fluid::FluidComponent,ρ,T)
    δᵢ = ρ/fluid.ρc
    τᵢ = fluid.Tc/T
    s = [
        1.0, 
        τᵢ, 
        log(τᵢ), 
        log(abs(sinh(fluid.vi[4]*τᵢ))), 
       -log(abs(cosh(fluid.vi[5]*τᵢ))), 
        log(abs(sinh(fluid.vi[6]*τᵢ))), 
       -log(abs(cosh(fluid.vi[7]*τᵢ)))]
    return log(δᵢ) + fluid.ni's
end
function αᵒ(m::FluidMixture,ρ,T,x)
    result = 0.0
    for i = 1:length(x)
        if x[i]>0.0
            result += x[i]*(αᵒ(m.components[i],ρ,T)+log(x[i]))
        end
    end
    return result
end
where I've added the necessary parameters for helium, methane and nitrogen to the get_fluid_component function and the PureComponent struct now includes

    # ideal-gas terms
    ni::Vector{Float64}
    vi::Vector{Float64}
to store the parameters.

The reduced Helmholtz energy is the sum of the ideal-gas contribution and the residual contribution:


function α(m::FluidMixture,ρ,T,x)
    return αᵒ(m,ρ,T,x) + αʳ(m,ρ,T,x)
end
Multiplying the reduced Helmholtz energy by RT yields a.

Regarding point 2, from Table B1 [2] the properties that will be most readily calculable are the following:

Note that the other thermodynamic state functions will then be computable via Looking at the ForwardDiff documentation it appears that I can get the derivative of a with respect to T, and calculate the entropy s in turn like so:

function a(m::FluidMixture,v,T,x)
  return R*T*α(m,inv(v),T,x)
end
function ∂a∂T(m::FluidMixture,v,T,x)
  f = T -> a(m,v,T,x)
  return ForwardDiff.derivative(f,T)
end
function s(m::FluidMixture,ρ,T,x)
  return -∂a∂T(m,inv(ρ),T,x)
end

Once again, I'll compare with TREND 4.0 [3] and create a unit test:


@testset "entropy (nitrogen+helium)" begin
  nitrogen = AdvancedThermo.get_fluid_component("nitrogen")
  helium = AdvancedThermo.get_fluid_component("helium")
  mixture = AdvancedThermo.create_mixture([nitrogen,helium])
  x = [0.7, 0.3]
  # benchmark values from TREND 4.0 (GERG-2008)
  s = AdvancedThermo.s(mixture,1000.0,300.0,x)
  @test abs(s-(-21.682667884))<1.e-3
end
I'm fairly certain that the size of the difference compared to the value from TREND is due to minor changes in the value of the gas constant R so I'm not concerned by it. In a very similar manner to how s was calculated, I can also calculate p:

function ∂a∂v(m::FluidMixture,v,T,x)
  f = v -> a(m,v,T,x)
  return ForwardDiff.derivative(f,v)
end
function p(m::FluidMixture,ρ,T,x)
  return -∂a∂v(m,inv(ρ),T,x)
end
and in the unit test the comparison to TREND looks great:

@testset "pressure (nitrogen+helium)" begin
  nitrogen = AdvancedThermo.get_fluid_component("nitrogen")
  helium = AdvancedThermo.get_fluid_component("helium")
  mixture = AdvancedThermo.create_mixture([nitrogen,helium])
  x = [0.7, 0.3]
  # benchmark values from TREND 4.0 (GERG-2008)
  p = AdvancedThermo.p(mixture,1000.0,300.0,x)
  @test abs(p-2.514507019437e6)<5.e-3
end

I could continue bulding up the API in the same manner as above to create functions for g, u, h etc. However, when reading over the ForwardDiff advanced documentation it looks like it will be beneficial to use DiffResults.jl in conjunction with ForwardDiff.jl. The documentation at the time I'm writing this can be found here. For example, code to evaluate all derivatives of the Helmholtz energy up to second order with respect to volume, temperature and mole fraction composition looks like:


function derivatives_of_a(m::FluidMixture,ρ,T,x)
  z = vcat(inv(ρ),T,x)
  result = DiffResults.HessianResult(z)
  f = z -> a(m,z[1],z[2],z[3:end])
  result = ForwardDiff.hessian!(result, f, z)
  return result
end

The result from DiffResults.jl is returned in an object of type DiffResults.MutableDiffResult. A better API than what I have implemented so far will be needed. However for now, I have added a unit test that verifies some of the first- and second-order derivatives against TREND 4.0 [3] and serves to demonstrate the functionality:


@testset "derivatives of a" begin
  nitrogen = AdvancedThermo.get_fluid_component("nitrogen")
  helium = AdvancedThermo.get_fluid_component("helium")
  mixture = AdvancedThermo.create_mixture([nitrogen,helium])
  x = [0.7, 0.3]
  result = AdvancedThermo.derivatives_of_a(mixture,1000.0,300.0,x)
  # Benchmark values from TREND 4.0
  # helmholtz energy a
  @test abs(DiffResults.value(result)-3991.1843584)<0.1
  # p = -∂a/∂v
  @test abs(-DiffResults.gradient(result)[1]-2.514507019437e6)<5.e-3
  # s = -∂a/∂T
  @test abs(-DiffResults.gradient(result)[2]-(-21.682667884))<5.e-3
  # ∂p/∂T_v = -∂²a/∂v∂T
  @test abs(-DiffResults.hessian(result)[1,2]-0.00860851125970e6)<1.e-4
  # volume expansivity = 1/v * ∂v/∂T_p = 1/v * -(∂p/∂T)_v/(∂p/∂v)_T
  v_exp = -1000.0*DiffResults.hessian(result)[1,2]/DiffResults.hessian(result)[1,1]
  @test abs(v_exp-0.003392906770418)<1.e-8 
end

Of the quantities that aren't simplistically related to derivatives of a, the partial fugacity coefficients are perhaps the most important for examining phase equilibria, which will be one of the next phases of this series. The relation in [1] for the partial fugacity coefficient of component i is

where the variables being held constant are the temperature T, total volume V, and mole numbers of components j ≠ i. Functions for computing r and its derivatives are:

function nαʳ(m::FluidMixture,VTn)
  ntotal = sum(VTn[3:end])
  return ntotal*αʳ(m,ntotal/VTn[1],VTn[2],VTn[3:end]./ntotal)
end
function derivatives_of_nαʳ(m::FluidMixture,V,T,n)
    z = vcat(V,T,n)
    result = DiffResults.HessianResult(z)
    f = z -> nαʳ(m,z)
    result = ForwardDiff.hessian!(result, f, z)
    return result
end
A unit test comparing with reference fugacity coefficients is given by:

@testset "derivatives of nαʳ" begin
  nitrogen = AdvancedThermo.get_fluid_component("nitrogen")
  helium = AdvancedThermo.get_fluid_component("helium")
  mixture = AdvancedThermo.create_mixture([nitrogen,helium])
  x = [0.7, 0.3]
  ρ = 1000.0
  T = 300.0
  result = AdvancedThermo.derivatives_of_nαʳ(mixture,inv(ρ),T,x)
  Z = AdvancedThermo.Z(mixture,ρ,T,x)
  ϕ_N2 = exp(DiffResults.gradient(result)[3] - log(Z))
  ϕ_He = exp(DiffResults.gradient(result)[4] - log(Z))
  @test abs(ϕ_N2-0.998810946)<1.e-9
  @test abs(ϕ_He-1.028530046)<1.e-9
end
The API needs work but the results are all looking correct and I don't want to re-work the code prematurely. The code so far can be obtained by downloading the zip file. In the next part of the series, I'll implement a solver to calculate the density when the pressure is specified.

References

[1] Kunz et al., GERG Technical Monograph 15: The GERG-2004 Wide-Range Equation of State for Natural Gases and Other Mixtures, Groupe Européen de Recherches Gazières, 2007 (link to PDF)

[2] Kunz and Wagner, J. Chem. Eng. Data, 2012, 57, 3032 (link to publisher)

[3] Span et al., TREND. Thermodynamic Reference and Engineering Data 4.0. Lehrstuhl für Thermodynamik, Ruhr-Universität Bochum, 2019

Go to Home How to cite this page Permalink to this page