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:
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:
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
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.
[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 |