Prerequisites:
- types
- functions
- containers
- numpy
Plotting a simple mathematical function
Graphical representations of data are ubiqutious across all sciences, and python gives us convenient tools to build these. matplotlib
is an accessible and ubiqutous plotting module. We'll begin by using the matplotlib.pyplot.plot()
function to plot the mathematical function $\mathrm{e}^x$.
import numpy as np # just as numpy is often imported as 'np' ...
import matplotlib.pyplot as plt # ... this is the 'standard' way of importing matplotlib.pyplot
x = np.linspace(0,5) # generate a numpy array, this is 'x'
y = np.exp(x) # take the exponential of that array, this is 'y'
plt.plot(x,y) # plot our graph
plt.show() # plt.show() declares we are finished plotting our graph, and that it should be displayed
To go through the logic of what we did above, after generating the x
and y
data using the already familiar numpy
package, the plt.plot()
function allows us to visualise their relationship taking the data as two arguments. The arguments are positional, your x
data should always preceed you y
data.
While the plotting process was straightforward, our graph is ultimatley incomplete, as it is unlabelled. To explore these concepts, let's turn to some more chemically relevant data.
A note on
plt.show()
plt.show()
is called at the end of each cell in which we are plotting a graph. You may come to discover that this is not always strictly necessary in a Jupyter environment as the graph will often be printed to screen regardless. Notice, however, that if it is not included,[<matplotlib.lines.Line2D at xxxxxx]
is returned: this is the plot object, simply includeplt.show()
to prevent this. If you are writing a python script in a text editor as opposed to a Jupyter/Ipython environment without calling theplt.show()
function, no graph will be shown. In summary, despite sometimes being unnecessary for a quick visualisation, it is good practice to always end your plots withplt.show()
.
Plotting chemical data
We will plot some data associated with the Arrhenius equation,
$ k = A \mathrm{exp}\left(\dfrac{-E_a}{k_\mathrm{B} T}\right) $.
where $k$ is the rate constant, $A$ is the pre-exponential factor, $E_a$ is the activation energy, $k_\mathrm{B}$ and $T$ is temperature. This expression should be familiar from plenty of areas of chemistry, but to review the key concepts, Arrhenius plots are used to analyse the effect of temperature on the rates of chemical reactions.
Taking the natural log of this expression gives,
$\ln(k)= -\dfrac{E_a}{k_\mathrm{B}} \dfrac{1}{T} + \ln(A) $.
This expression now describes a straight line with gradient $-\dfrac{E_a}{k_\mathrm{B}}$ and intercept $\ln(A)$. Given some example data, and remembering that np.log
gives the natural log of each value in a numpy array, we can now make a new plot
temperature = np.arange(100,450,50) #
k = np.array([2.078e-04,2.176e-04,2.229e-04,2.260e-04,2.281e-04,2.316e-04,2.331e-04]) # define Arrhenius data
dk = np.array([2.078e-06,2.176e-06,2.229e-06,2.260e-06,2.281e-06,2.316e-06,2.331e-06]) #
ln_k = np.log(k) # take the natural log of the np.array 'k'
inverse_temperature = 1/temperature # take the inverse of the np.array 'temperature'
plt.plot(inverse_temperature, ln_k) # plot these with inverse_temperature on the x-axis, and ln_k on the y-axis
plt.show() # show our plot
As above, this graph is still unsatisfactory; additions to our code will give a much improved graph. Axes labels can be set via plt.xlabel()
and plt.ylabel()
. The points along the graph are also discrete, and therefore to connect them with an arbitrary line is not appropriate. plt.plot()
takes a third argument, which formats the points in your graph, the default is -b
in otherwords, a line (-
), that is blue (b
). To give discrete points, we could change this default value to og
which will give circles (o
) that are green (g
).
plt.plot(inverse_temperature, ln_k, 'og') # plot the data again, but this time using circles, 'o', that are green 'g'.
plt.xlabel('1/T / 1/K ') # add an x-axis label, with both the quantity (1/T) and the units (1/K)
plt.ylabel('$\ln({k})$') # add a y-axis label, ln(k)
plt.show() # show the plot
It is also possible to include error bars using the plt.errorbar()
function. This gives the same functionality as plt.plot()
taking additional arguments that define errorbars to plot. As there are now quite a few positional arguments in our plt.errorbars()
, we will include them as keyword arguments. To clarify, each of these arguments are:
x
- the data to be plotted on the x-axisy
- the data to be plotted on the y-axisyerr
the errors iny
xerr
the errors inx
fmt
line formatting
We can also add a title to a plot with plt.title()
.
plt.errorbar(x=inverse_temperature, y=ln_k, yerr=dk/k, xerr=None, fmt='og') # plot graph as before, but this time inclusive of error bars
plt.xlabel('1/T / 1/K ') # x-label as above
plt.ylabel('ln(k)') # y-label as above
plt.title('Arrhenius plot') # add a title to our graph
plt.show() # show our graph
# Enter your code here
Multiple lines on the same graph
Let's try something a little different and a little more complex. Say we want to compare the probability distribution of three atomic orbitals on the same graph, we need to call plt.plot()
for each function, and then call plt.show()
when we have finished adding the different functions to our plots. The radial wavefucntions, $R$, for the hydrogenic $3s$, $3p$ and $3d$ orbitals respectively are
$R_{3s} = \dfrac{2}{3\sqrt3}\left(1-\dfrac{2r}{3}+\dfrac{2}{27}r^2\right)e^{-r/3}$
$R_{3p} = \dfrac{2\sqrt{2}}{9\sqrt{3}}\left(\dfrac{2r}{3}\right)\left(1-\dfrac{1}{6}r\right)e^{-r/3}$
$R_{3d} = \dfrac{1}{9\sqrt{30}}\left(\dfrac{2r}{3}\right)^2e^{-r/3}$
plotted below, they look like this
r_values = np.linspace(0,30,100) # generate a series of r values which will give a smooth wavefunction
def r3s(r):
"""
Calculates the value of the radial wavefunction of the 3s orbital for a given distance from origin
Args:
r (float): distance from origin
Returns:
(float): the value of the radial wavefunction at r
"""
r3s = 2/(3*np.sqrt(3)) * (1 - 2*r/3 + 2/27*r**2) * np.exp(-r/3)
return r3s
def r3p(r):
"""
Calculates the value of the radial wavefunction of the 3p orbital for a given distance from origin
Args:
r (float): distance from origin
Returns:
(float): the value of the radial wavefunction at r
"""
r3p = (2*np.sqrt(2))/(9*np.sqrt(3)) * ((2*r)/3) * (1 - 1/6*r) * np.exp(-r/3)
return r3p
def r3d(r):
"""
Calculates the value of the radial wavefunction of the 3d orbital for a given distance from origin
Args:
r (float): distance from origin
Returns:
(float): value of radial wavefunction at r
"""
r3d = 1 /(9*np.sqrt(30)) * ( (2*r/3)**2 ) * np.exp(-r/3)
return r3d
wavefunction_3s = r3s(r_values) #
wavefunction_3p = r3p(r_values) # caluclate R(r) values from functions above
wavefunction_3d = r3d(r_values) #
plt.plot(r_values, wavefunction_3s) # plot 3s wavefunction
plt.plot(r_values, wavefunction_3p) # plot 3p wavefunction
plt.plot(r_values, wavefunction_3d) # plot 3d wavefunction
plt.xlabel('r / Bohr radii' ) # label x-axis
plt.ylabel('R') # label y-axis
plt.title('Radial wavefunctions of $3s$, $3p$ and $3d$ orbitals')
plt.show() # plt.show() only after plotting all three wavefunctions
How do we tell which line refers to which orbital? To show how to do this, and give these plots physical meaning, we can plot their radial distribution functions ($4\pi r^2 R(r)^2$). This time we add a label
argument each time we call the plt.plot()
function, the type of this argument must be a string
. These labels are then shown in a legend, which is added to the plot by calling plt.legend()
before calling plt.show()
.
def radial_probablilty_function(r, wavefunction):
""""
Calculate the radial probability of a wavefunction at a given separation
Args:
r (float): value of wavefunction at r
wavefunction (float): value of the wavefunction at r
returns:
float radial probability of the wavefunction at r
"""
return 4*np.pi*r**2*wavefunction**2
plt.plot(r_values, radial_probablilty_function(r_values, wavefunction_3s), label = '$3s$') # plot 3s *labelled* raidial probability function
plt.plot(r_values, radial_probablilty_function(r_values, wavefunction_3p), label = '$3p$') # plot 3p *labelled* raidial probability function
plt.plot(r_values, radial_probablilty_function(r_values, wavefunction_3d), label = '$3d$') # plot 3d *labelled* raidial probability function
plt.xlabel('r / Bohr radii' ) # label x-axis
plt.ylabel('4$\pi$r$^2$R$^2$') # label y-axis
plt.title('Radial distribution functions of $3s$, $3p$ and $3d$ orbitals') # add a title
plt.legend() # show legend
plt.show() # plt.show() after all distributions have been plotted and show a legend
Excercise
Given that the radial wavefunctions of the 1s and 2s orbitals are as follows:
$ R_{1s} = 2\mathrm{e}^{-r} $
$ R_{2s} = \dfrac{1}{2\sqrt{2}}(2-r)\mathrm{e}^{-r/2}$
plot and compare the radial distribution functions of the $1s$, $2s$ and $3s$ orbitals on the same plot. Ensure you label your axes, add a title, and include a legend that clarifies which function represents which orbital.
# Enter your code here