How to use the heatfuncs Python library to calculate
groundwater ﬂow from temperature data
Dylan J. Irvine
Last updated: January 15, 2015
Version number: 0.0.1
heatfuncs.py is a Python library that contains functions to calculate groundwater ﬂow from
temperature data. Curently heatfuncs.py includes the analytical solutions by Bredehoeft
and Papadopulos (1965), Mansure and Reiter (1979), and Lu and Ge (1996). The goal of
heatfuncs.py is to produce easy to use Python functions to promote the more widespread
use of temperature data in hydrogeology.
The version number 0.0.1 denotes the fact that the library is still in the testing phase, and
may contain errors. If any errors are noticed, please contact me using the email address
Python is a really great programming language. It’s pretty easy to pick up (and if you don’t
code, I have an example of all the coding required to run some of the functions included at
the end of this document). The other key beneﬁt of Python is that it is free, so there are no
Before we start
heatfuncs.py is a small library of functions to analyse temperature data using the Python
programming language. The heatfuncs.py library uses some functions which are only
available in relatively recent versions of Python. For example, the library was written using
Python version 2.7.6.
The easiest way to obtain Python, relevant libraries and a nice GUI, is to install Python XY
(which is made for scientiﬁc and engineering purposes). To install Python XY, download an
installer, and follow the instructions at:
As a guide, once you’ve installed Python, you will write scripts, use functions, etc. using the
Important! You have to run heatfuncs.py at least once(!)
You may be better at using Python than me. So if there is a better way of using the heat-
funcs.py library than my description below, please let me know.
First, given that you’re reading this, I assume that you’ve downloaded the library. Save the
ﬁles in a sensible location on your computer. And by sensible, I mean somewhere that you
will remember. Python scripts to call functions from the heatfuncs.py library will need to
be stored in the same location on your computer.
To have the functions from the heatfuncs.py library available to use, open the heat-
funcs.py ﬁle in Spyder (the GUI that comes with the Python XY package), and run in once
(either by pressing the green ’play’ button, or pressing F5). Depending on how your Spyder
GUI is set up, some text may appear on the console window. Either way, nothing much
Now create a new python script in the same directory. Python diﬀers from other high level
languages like MATLAB, in that if you want to use a function, you have to import it from
the library where it belongs. I usually just import the entire library, but you can also import
selected functions. e.g. two ways of importing functions from the numpy library (scientiﬁc
computing library) are below:
from numpy import *
from numpy import exp, sum
i.e. the ﬁrst option simply imports all of the numpy functions, and the second only im-
ports the exp and sum functions. This over-thinking things for now. Once you’ve run
the heatfuncs.py script once, the functions contained within it are ready to use. At the
end of this document is a script (that should be saved in the same directory as the heat-
funcs.py script) that will run the BPrun function, and produce an estimate of qzin m s−1
Some other Python quirks
I personally ﬁnd it really annoying, but Python starts counting from 0. So if you had a
column vector of data (let’s say you had a column of data called T). If you want to print the
ﬁrst value from T, you would type:
The other relevant example, would be if your geothermal data were stored in a ﬁle with 2
columns. The ﬁrst column containing depth, and the second containing temperature. You
could load the ﬁle (let’s say it was called Tatiara.dat, and that it has 2 header lines) to a
parameter called Tatdata (or whatever you would like to call it) with the following:
Tatdata = loadtxt(’Tatiara.dat’, skiprows=2)
Now your depth data would be stored as Tatdata[:,0], and your temperature data would be
stored as Tatdata[:,1]. i.e. the : means all rows of data, and the 0 or 1, refers to the 1st or
Alternatively, you could use the snippet of text below to load the data straight into arrays
called z and T (to match the nomencalture used here) e.g.:
z = loadtxt(’Tatiara.dat’, skiprows=2, usecols=(0,))
T = loadtxt(’Tatiara.dat’, skiprows=2, usecols=(1,))
One last point. . . Commands in Python are case sensitive. So T and t are diﬀerent.
There are other quirks, but this will do for now.
Below is a list of all of the parameters used in heatfuncs.py. All parameters use the units:
kg, m, s, ◦C, J and W.
Symbol Code symbol Units Explanation
zz m Depth
z0z0 m Depth of top of geothermal proﬁle for analysis
zLzL m Depth of bottom of geothermal proﬁle for anal-
T0T0 ◦C m−1Temperature at z0
TLTL ◦C m−1Temperature at zL
LL m Distance between z0and zL
qzqest m s−1Estimate of qzor value of qz
qxqx m s−1Value of qx
ρwpw kg m−3Density of water
cwcw J kg−1◦C−1Speciﬁc heat capacity of water
λ0kb W m−1◦C−1Thermal conductivty of solid-ﬂuid matrix
γgam ◦C m−1Horizontal thermal gradient (user entered)
ηeta ◦C m−1Vertical thermal gradient (calculated inter-
nally from T0,TLand L)
P exPex - Horizontal Peclet number
P ezPez - Vertical Peclet number
δdelta - (γ
Analytical solutions included in heatfuncs.py
Note: The explanation for each solution is given in full. There will be repetition. Symbols
used may vary from the original publication for consistency.
Analytical solutions included in heatfuncs.py:
•Bredehoeft and Papadopulos (1965)
•Mansure and Reiter (1979)
•Lu and Ge (1996)
Future plans to add:
Functions included in heatfuncs.py
An example of calling a function in Python would be to assign the square root of a number
to a variable. For example, you may want to assign the square root of 27 to the variable b.
To do this, you could type:
b = sqrt(27.0)
In all of the examples below, the heatfuncs.py functions are assigned to the variable a.
However, you can use whatever variable name that you like.
Normalises Tand zdata. Removes any data above z0and ignores all data after
zL, has been reached. This function deals with the fact that temperature is typically
recorded as a sensor is lowered down, and then raised up through a borehole. Example:
a = Tzprep(T, z, z0, zL)
a = Tzprep(T, z, 10.0,61.0)
e.g. where user has already loaded Tand z, and z0= 10.0m and zL= 61.0 m.
This function is essentially the Tzprep function, but with plotting capabilities. In
addition to T,z,z0,zL, user also enters the output ﬁle name (must be a .png ﬁle),
the output ﬁle destination (e.g. r’C:\this\is\my\folder’), the dpi of the output ﬁle,
whether the plots are to be side by side (’row’), or stacked on top of one another
(’col’), the fontsize, and line colour. An example could be (where the path to the
destination folder has been set to the parameter outfolder):
a = Tzplot(T, z, z0, zL, ’Tatiara.png’, outfolder, 300,’row’,10,’#0066FF’)
An example of the output with the ’row’ option is below:
Fig 1:Plot on the left shows the orignal T−zplot from 0 to 80 m. On the right, the
Temperature and data has been normalised to be used in either the Bredehoeft and
Papadopulos (1965) or Lu and Ge (1996) methods.
Not intended to be called directly by the user. Calculates the weighted sum of squared
error (SSE) between observed and modelled normalised Tusing the Bredehoeft and
Papadopulos (1965) method. This function is not intended to be used directly by the
user. It is called by BPq (details below). See Advanced options to change the
weighting of the SSE.
Not intended to be called directly by the user. Uses the Python minimize function
to ﬁnd the qzthat minimises the objective function (in this case, the sum of squared
errors between observed and modelled normalised temperature).
Calls Tzprep,BPmin and BPq. Produces the qzvalue that minimises the sum of
squared errors between observed normalised temperature. Setting the plotswitch op-
tion to 1 also outputs a graph of normalised zagainst normalised T. Example:
a = BPrun(T, z, z0, zL, qest, pw, cw, kb, plotoption)
An example of the output ﬁgure from the BPrun function:
Fig 2: Example of the normalised T= (Tz−T0)/(TL−T0) and normalised z=z/L
data (blue) and the ﬁtted Bredehoeft and Papadopulos (1965) function (red dash) for
a hyporthetical example
Not intended to be called directly by the user. Like BPmin, but for the Lu and Ge
Not intended to be called directly by the user. Like BPq, but for the Lu and Ge (1996)
Notice the pattern here? Like BPrun, but for the Lu and Ge (1996) method. Example:
a = LGrun(T, z, z0, zL, qest, qx, pw, cw, kb, gam, plotoption)
Not intended to be called directly by the user. Like LGmin, but the user enters δ
rather than estimates of qx,γ, η
Not intended to be called directly by the user. Like LGq, but where, as above, the
user enters δ.
Notice the pattern here? Like LGrun, but where, as above, the user enters δ. Example:
a = LGrundelta(T, z, z0, zL, qest, pw, cw, kb, delta, plotoption)
Runs the Mansure and Reiter (1979) derivative method. This function may require to
be run until the appropiate range of data has been determined by the user. This can
be done by selecting an appropriate depth range of data to ﬁt the shallow (linear) part
of the dT/dz vs Tgraph. Example:
a = MRrun(T, z, z0, zL, kb, dzpref, zﬁtfrom, zﬁtto, pw, cw)
An example of the output ﬁgure from the Mansure and Reiter (1979) method:
Fig 3: Example of the Mansure and Reiter (1979) derivative method. In blue is the
dT/dz vs Tdata, and red dash is the ﬁtted shallow (linear) part of the dT/dz vs T
data. The slope of the red line is mused in Equation 11.
Many of the solutions in heatfuncs.py include bulk (i.e. saturated porous media) thermal
conductivity, which is denoted as λ0, and has units of W m−1◦C m−1.λ0is calculated us-
ing the thermal conductivity of the solid (λs), the thermal conductivity of water (λw) and
porosity (θ) using one of the two approaches below:
λ0=λs(1 −θ) + λwθ, (2)
Equation 1 is a geometric approach, and Equation 2 is an arithmetic approach to calculating
λ0. The approach that you use is up to you. Typically, the value of λ0is going to be an
As a guide, λwis approximately 0.6 W m−1◦C m−1. Sand (i.e. solid) has a relatively high
thermal conductivity λs≈8.0 W m−1◦C m−1. A typical range of λ0would range between
≈1.6 to 4.5 W m−1◦C m−1.
Bredehoeft and Papadopulos (1965) solution
The Bredehoeft and Papadopulos (1965) analytical solution is a 1D solution to calculate
vertical ﬂuid ﬂow (Darcy ﬂux, qz). The solution is straightforward to use, and only requires
a geothermal proﬁle, and an estimate of bulk thermal conductivity (λ0, W m−1◦C−1). The
Bredehoeft and Papadopulos (1965) solution can be written as:
where Tzis a temperature (◦C) at a depth z(m), T0and TLares the temperature boundary
conditions for the top and bottom of the model respectively, and P ezis the thermal Peclet
where ρwis the density of water (kg m−3, e.g. 1000.0), cwis the speciﬁc heat capacity of
water (J kg−1◦C−1, e.g. 4186.0), qzis the vertical Darcy ﬂux (m s−1), Lis the distance
between the points for T0and TL(here these points are referred to as z0and zL), and λ0is
the bulk thermal conductivity (W m−1◦C−1).
The Bredehoeft and Papadopulos (1965) method obtains a qzestimate by ﬁnding the qz
that produces the best ﬁt between the normalised Tdata (i.e. (Tz−T0)/(TL−T0)) from
the measurements, and equation 1. The functions from heatfuncs.py ﬁnd this qzvalue by
minimising the sum of squared errors between the observed and modelled data.
Lu and Ge (1996) solution
The Lu and Ge (1996) solution is a 2D extension of the Bredehoeft and Papadopulos (1965)
solution. In addition to a geothermal proﬁle and an estimate of bulk thermal conductivity
(λ0, W m−1◦C−1), the Lu and Ge (1996) analytical solution also requires a horizontal thermal
gradient (which requires either another geothermal proﬁle, and the gradient is calculated, or
the estimation of a horizontal thermal gradient).
The Lu and Ge (1996) solution can be written as:
P ez exp(P ez(z/L)−1
where Tzis a temperature (◦C) at a depth z(m), T0and TLares the temperature boundary
conditions for the top and bottom of the model respectively, and P exand P ezare thermal
Peclet numbers (below), Lis the distance (m) between T0and TL,γis the horizontal thermal
gradient ( positive when heat ﬂow is leftward, ◦C m−1, requiring another geothemal proﬁle,
or an estiamte of γ) and ηis the vertical thermal gradient (positive when heat ﬂow is upward,
◦C m−1, which can be determined from the geothermal proﬁle of interest).
The P exand P eznumbers are:
Lu and Ge (1996) also suggest that the γ P ex
ηP ezterm can be combined into a single parameter
δthat includes parameters which are often unknown. Following this, the Lu and Ge (1996)
equation can be written as:
exp(P ez)−1+δ exp(P ez(z/L)−1
where qxand qzare the Darcy ﬂux (m s−1) in the horizontal and vertical directions, and λ0
is bulk thermal conductivity (λ0, W m−1◦C−1).
heatfuncs.py provides approaches to use either form of the Lu and Ge (1996) method.
Notice that the ﬁrst term on the right hand side of either form of the Lu and Ge (1996)
solution is the Bredehoeft and Papadopulos (1965) solution. In cases where there is no hor-
izontal thermal gradient (γ), or no horizontal ﬂow (qx), the solution reduces the Bredehoeft
and Papadopulos (1965) solution. An extension of this point is that the solution is not sen-
sitive to the ﬁnal term on the right hand side where the γ, or qxare small.
The next point is important, hence it’s in red. The ﬁrst form of the Lu and Ge (1996)
solultion simultaneously requires the estimation of both qxand qz.The approach taken in
heatfuncs.py is to ﬁx the qxvalue, and estimate qz. This process can be repeated for several
qxvalues to determine the sensitivity of the qzestimate to qx. Alternatively, the user can
run the δbased function, and use several δvalues.
The Lu and Ge (1996)method obtains a qzestimate by ﬁnding the qzthat produces the best
ﬁt between the normalised Tdata (i.e. (Tz−T0)/(TL−T0)) from the measurements, and
equation 5. The functions from heatfuncs.py ﬁnd this qzvalue by minimising the sum of
squared errors between the observed and modelled data. This approach requires both an
initial estimate of qzand a ﬁxed value of qx. As the solution for qzdepends on the value of
qxit is recommended that the function be run with a range of qxvalues and/or horizontal
thermal gradients (γ) if these values are unknown.
Mansure and Reiter (1979) solution
Mansure and Reiter (1979) present a method of determining ﬂuid ﬂow from geotherms us-
ing derivatives of temperautre with depth. This method performs best using high precision
(i.e. 0.0001 ◦C) equipment. Assuming steady state, vertical and uniform ﬂow, Mansure and
Reiter (1979) show that:
d(dT/dz) = cwρw
and that the slope (m) of the plot dT /dz vs. Tyields:
which can be rearranged to obtain:
When purely vertical ﬂow occurs (and the system is thermally homogeneous), the relation-
ship between dT/dz and Twill be linear. When multidimensional ﬂows occurs, typically,
the shallow data will be linear, and deeper data are non-linear. It is recommended to ﬁt the
shallow, linear part of the curve.
To perform calculations of ﬂuid ﬂow using the Mansure and Reiter (1979) method, the
heatfuncs.py library interpolates the geotherm onto a steady depth proﬁle, as typically,
geotherms are measured at a constant time (as the sensor is lowered down the borehole),
rather than at a constant depth.
heatfuncs.py has a number of advanced settings that can be changed by changing the
options and re-running heatfuncs.py enact changes. The optional changes that can be
made to heatfuncs.py include:
1. Changing the weighting of sum of squared errors (e.g. this is used in the Lu and Ge
2. Changing the convergence criteria for the minimize function in the BPq, LGq, or
Change SSE weightings
By default, heatfuncs.py applies a weighted sum of square error (SSE). In the BPq, LGq
and LGqdelta functions, w= the weighting array. This array has the same length as the
input data (i.e. z/L), and ranges from some value, to 1. The warray is set up using the
Python linspace function (which linearly spaces values between a user deﬁned start (ﬁrst
value in function), and end (second value in function). The length of the array is the third
value in the function).
If the user desires to have no weightings, then the ﬁrst value in the linspace function that
sets up the warray can be set to one. Increasing the weight of the shallow data can be
achieved by increasing the ﬁrst value. Similarly, weighting the shallow data can be achieved
by increasing the value of the end value (i.e. second number in the linspace function).
Reduce convergence criteria
The convergence criteria (xtol) are set to a very small value in the BPq, LGq and LGqdelta
functions (these functions are used to minimize the SSE). By default, the convergence criteria
are set to 1 ×10−18 . This value can be increased/decreased by the user by changing the xtol
Detailed description of key functions
In Spyder (a Python GUI), help on all functions can be obtained by typing the function
name, followed by an open bracket. Detailed description of input and output of the function
will appear in the Object inspector. Below are descriptions of two key functions:
This function is used to prepare raw data to be analysed in the Bredehoeft and Papadopulos
(1965) solution (also used in the Lu and Ge (1996) related functions). The user enters the
temperature data (T), depth data (z), the the depths of the top (z0) and bottom (zL) bound-
ary conditions for the Bredehoeft and Papadopulos (1965) or Lu and Ge (1996) solutions.
Geothermal proﬁles are recorded as the sensor is lowered down a borehole, and as it is raised
up again. The Tzprep function overcomes this issue by using the ﬁrst instance of the sensor
equalling or exceeding either z0 or zL values. For zL this means that any data after the
depth zL is ignored.
The user does not necessarily have to call this function (as it is called by other BP related
functions). However, the Tzprep function is a simple way to produce normalised Tand z
data (e.g. Tz−T0/TL−T0, or z/L).
An example of calling Tzprep (where T and z have been read in) would be:
a = Tzprep(T, z, z0, zL)
Tzprep outputs a single variable (in this case ’a’) which contains position 0 = Tnorm, po-
sition 1 = znorm, position 2 = Ttrunc, position 3 = ztrunc, position 4 = T0, postion 5 =
TL, position 6 = L.
i.e. to print the normalised T data, the user could type:
or to plot the normalised T-z proﬁle (i.e. z/L vs. (Tz−T0/TL−T0)), the user could type:
gca().invert yaxis() # Inverts the y axis to have 0 at the top.
show() # The show command may or may not be required to actually show the plot
This function calcualtes the qz(m s−1) that best ﬁts the observed geothermal proﬁle.
Geothermal logs typically record temperature both as the sensor is lowered down the bore-
hole, and as it is raised back up. This is overcome within the BPrun function, where the
ﬁrst depth that exceeds zLis used, and data deeper than this depth is ignored.
Input for the function is:
•T, 1D column of temperature data (◦C)
•z, 1D column the of depths corresponding to temperature data (m)
•z0, Depth for upper boundary condition (m)
•zL, Depth for lower boundary condition (m)
•pw, Density of water (kg m−1)
•cw, Speciﬁc heat capacity of water (J kg−1◦C−1)
•kb, Bulk thermal conductivity (W m−1◦C−1)
•qest, Estimate of Darcy ﬂux (m s−1)
•plotswitch, 0 = no plot, 1 = graph of ﬁtted and observed (Tz−T0)/(TL−T0)
Output for the function is:
qzin m s−1.
Example script to call BPrun
Warning: If you cut and paste the text below, you may have to re-type the ”’ symbols. Also,
you’ll have replace the example ’datafolder’ to be the path to your data
# -*- coding: utf-8 -*-
Created on Fri Dec 12 12:47:15 2014
A script to calculate qz from the Bredehoft and Papadopulos (1965)
method using the heatfuncs library ”””
from matplotlib.pyplot import *# plotting library, by the way # is the symbol for comments
from numpy import *# scientiﬁc computing library
import os # The opperating system library to change folders
from heatfuncs import *# Finally, the heatfuncs library
#The line below asigns the path to your temperature data to the variable ’datafolder’
#The r out the front is to say that this is in raw format. If the r isn’t used, you will
#have to double all of the backslashes.
datafolder = r’C:\your\path\goes\here’
# Changes the directory to where your data is located
# commands to load your data. Annoying point, Python counts from zero. In this example,
# I have depth (m) in the 1st col of a ﬁle, and T (◦C) in the 2nd col. This example has a
# header row, which is skipped. For more complicated cases with gaps in the data, look up
# the genfromtxt() command. To load csv ﬁles, change the delimiter=None to delimiter=’,’
z = loadtxt(’testdata.dat’,skiprows=1, delimiter=None, usecols=(0,))
T = loadtxt(’testdata.dat’,skiprows=1, delimiter=None, usecols=(1,))
# parameter values for functions
z0 = 0.0 # Depth of top of geothermal proﬁle, m
zL = 100.0 # Depth of bot of geothermal proﬁle, m
pw=1000.0 # water density kg m−3
cw = 4186.0 # speciﬁc heat capcity of water J kg−1◦C−1
kb = 3.0 # bulk thermal conductivity W m−1◦C−1
qest = 1.0e-7 # estimate of Darcy ﬂux m s−1
plotswitch = 1# 0 = no plot, 1 = show ﬁtted data
# Below is the BPrun function which will produce an estimate of q(m s−1)
q = BPrun(T,z,z0,zL,qest,pw,cw,kb,plotswitch)
print ’ end of program’
Bredehoeft, J.D., and Papadopulos, I.S. (1965) Rates of vertical groundwater movement es-
timated from the Earth’s thermal proﬁle, Water Resources Research, 1(2), p 325-328.
Lu, N. and Ge., S. (1996) Eﬀect of horizontal heat and ﬂuid ﬂow on the vertical temperature
distribution in a semiconﬁnig layer, Water Resources Research, 32(5), p 1449-1453.
Mansure, A.J., and Reither, M. (1979) A vertical groundwater movement correction for heat
ﬂow, Journal of Geophysical Research, 84(B7), p 3490-3496.
Taniguchi, M. (1993) Evaluation of vertical groundwater ﬂuxes and thermal properties of
aquifers based on transient temperature-depth proﬁles, Water Resources Research, 29(7), p