Search
Writing documentation

prerequistes:

  • functions
  • types
  • collections
  • reading and finding documentation

Effective communication is essential to learning and research: if no one can understand your work, you may as well have never have done it. Any code produced as part of your research also needs to communicated effectively, if for no other reason that you need to understand what you were trying to do when you revisit your code, days, weeks or months after you originally wrote it. The cell below contains functional code, but is not instantly recognisable as to what it is doing. In this section, we will work through this code, adding layers of simple 'documentation' to communicate what it is doing.

See if you can work out what the code is doing before we write any documentation.

import math

h = 1
k = 1
l = 1
a = 6
b = 3
c = 2 

d = math.sqrt(1/((h*h)/(a*a) + (k*k)/(b*b) + (l*l)/(c*c)))

w = 1.5406
s = math.asin(w/(2*d))
ang =  math.degrees(s)

print(ang)
28.7093380112038

If you couldn't work it out, don't worry. Let's start communicating the code more effectivly by stating the problem we are trying to solve: the code below solves Bragg's law for an orthorombic crystal to determine the Bragg angle associated with a given miller plane. Perhaps the easiest thing we can do is re-write the code, with clearer variable names.

import math

h = 1
k = 1
l = 1
a = 4
b = 4
c = 4 

d_hkl = math.sqrt(1/((h*h)/(a*a) + (k*k)/(b*b) + (l*l)/(c*c)))

xray_wavelength = 1.5406
bragg_angle_rads = math.asin(xray_wavelength/(2*d_hkl))
bragg_angle_degrees =  math.degrees(bragg_angle_rads)

print(bragg_angle_rads)
print(bragg_angle_degrees)
0.3400663935848004
19.484369106643797

While not exactly documentation, using descriptive variable names does improve the readability of the code. Anyone versed in crystallography would likely read the above code and be able to more quickly surmise what it is trying to achieve. That being said, we can be even more helpful buy using 'comments'.

Comments begin with a hash symbol # and will not be read as python in a code cell, for example:

# You don't have to comment out a whole line
print(1+2) # you can just comment out the end of a line
3

The cell above still returns 3, no matter what is included as a comment. We can add some comments to our example to improve the clarity.

import math

# calculate scattering angle of an miller plane in an orthorhombic Bravias lattice

# define miller indices

h = 1
k = 1
l = 1

# define cell lengths in Angstroms

a = 5
b = 5
c = 5 

d_hkl = math.sqrt(1/((h*h)/(a*a) + (k*k)/(b*b) + (l*l)/(c*c)))

xray_wavelength = 1.5406 # wavelength of incident radition in Angstroms

# apply Bragg's law - the math module works in radians as a default, crystallographers in degrees, and so we need to 
# add an additional step to convert from radians to degrees.

bragg_angle_rads = math.asin(xray_wavelength/(2*d_hkl))
bragg_angle_degrees =  math.degrees(bragg_angle_rads)

print(bragg_angle_rads)
print(bragg_angle_degrees)
0.2701123839807746
15.47629959631549

Hopefully the above code is now straightforward to comprehend for anyone with an interest in either the mechanics of the code, or the scientific problem it attempts to solve. It is important to bear in mind that commenting should be used sparingly, and the context should be considered to avoid redundancy. Ultimately, we are trying to convey the function of the code, and not distract from that. Some points in the above text about possible redundancies, and contextual commenting:

  • The first comment # calculate scattering angle of an miller plane in an orthorhombic Bravias lattice is redundant given the context of the code, i.e. in this jupyter book: we have already explained what we are trying to to do, but if this were a stand-alone script, a concise explanation of what the code does - such as this comment - is very helpful. Note this could also come from the name of the script or notebook.

  • Consider the line xray_wavelength = 1.5406 # wavelength of incident radition in Angstroms. If it read xray_wavelength = 1.5406 # wavelength of incident radition this would be a redundant comment: the variable name conveys the same message as the comment itself. As scientists, however, it's imperative to convey the units we are working in, and so the addition of 'in Angstroms' confers meaning to the value.

  • Finally, the comment the math module works in radians as a default, crystallographers in degrees, and so we need to add an additional step to convert from radians to degrees. is purely for instructive purposes, most experienced python users would know this: it is not always necessary to include such descriptive commenting.

Docstrings

We're going to re-write the above using functions to discuss the importance of docstrings.

import math

def calculate_d_hkl(abc,hkl):
    return math.sqrt(1/((hkl[0]**2)/(abc[0]**2) + (hkl[1]**2)/(abc[1]**2) + (hkl[2]**2)/(abc[2]**2)))

def braggs_law(d_hkl,wavelength=1.5406):
    return math.asin(wavelength/(2*d_hkl))

d_hkl = calculate_d_hkl([5,5,5],[1,1,1])
bragg_angle_rads = braggs_law(d_hkl)
bragg_angle_degrees =  math.degrees(bragg_angle_rads)

print(bragg_angle_rads)
print(bragg_angle_degrees)
0.2701123839807746
15.47629959631549

If it weren't for the fact the cell above produces the same ouptut as the cells in the previous section, you may be forgiven for thinking, on first glance, that this reformulation of the code might do something slightly different. This misconception could have been avoided using docstrings, which were breifly discussed in the functions section. We revisit the concept here, to emphasise that even though they are a non-essential part of the function (as shown by the functioning code above), they offer a great deal of clarity. The description of docstrings given in the function section is quoted below:

The docstring is an important (although not essential) component of any function. Describing the purpose of a function is valuable of many reasons, it helps to clarify what the function will do, it offers guidence for others on how to use the function, and it acts to remind future you why it is that you have a particular function and what is does. You may read this last point and roll your eyes, however I promise you that code you write today will not stay present in your memory forever.

In addition to a description of the function, a docstring also typically includes information about the arguments taken by the function and objects that are returned. There are a few common way that these may be formatted, within this book we will use Google Style, however, other styles exist, such as NumPy and Sphinx. Using a standard style of docstring is useful when writting large software packages, due to the availablity of tools to automatically generate software documentation from these docstrings. The Google style indicates the arguments with the keyword Args, before listing the arguments, the expected type and a short description (we find that it can be helpful to include information about the expected units in this section). The Returns keyword is followed by a list of the values returned by the function, since these do not necessary has a name this is omitted.

So, let's write some docstrings for our example above

import math

def calculate_d_hkl(abc,hkl):
    """
    Calculates d_hkl for a given miller plane in an orthorhombic Bravais lattice
    
    Args:
        abc (list): lattice_parameters
        hkl (list): miller indices
    
    Returns:
        (float): d_hkl
    
    """
    return math.sqrt(1/((hkl[0]**2)/(abc[0]**2) + (hkl[1]**2)/(abc[1]**2) + (hkl[2]**2)/(abc[2]**2)))

def braggs_law(d_hkl,wavelength=1.5406):
    """
    Calculates the Bragg angle for a given value of d_hkl and x-ray wavelength
    
    Args:
        d_hkl (float): d_hkl spacing (Angstroms)
        wavelength (float, optional): wavelength of incident radiation (Angstroms). 
            Defaults to 1.5406 - Cu source.
    
    Returns:
        (float): Bragg angle (float)
    
    """
    return math.asin(wavelength/(2*d_hkl))

d_hkl = calculate_d_hkl([5,5,5],[1,1,1])
bragg_angle_rads = braggs_law(d_hkl)
bragg_angle_degrees =  math.degrees(bragg_angle_rads)

print(bragg_angle_rads)
print(bragg_angle_degrees)
0.2701123839807746
15.47629959631549

Not only do the docstrings included in the code above explain the utility of the functions, it also allows us to question what the functions do should we ever forget by executing the following code:

calculate_d_hkl?
Signature: calculate_d_hkl(abc, hkl)
Docstring:
Calculates d_hkl for a given miller plane in an orthorhombic Bravais lattice

Args:
    abc (list): lattice_parameters
    hkl (list): miller indices

Returns:
    (float): d_hkl
File:      /mnt/a/materials_2/intro_python_chemists/content/good_practice/<ipython-input-50-8ec24a070a8b>
Type:      function

This is a really helpful trick that can be applied to any python function, should you forget what it does/what arguments are required, i.e.

from numpy import ones
# note numpy does not use the 'Google' style docstring.
ones?
Signature: ones(shape, dtype=None, order='C')
Docstring:
Return a new array of given shape and type, filled with ones.

Parameters
----------
shape : int or sequence of ints
    Shape of the new array, e.g., ``(2, 3)`` or ``2``.
dtype : data-type, optional
    The desired data-type for the array, e.g., `numpy.int8`.  Default is
    `numpy.float64`.
order : {'C', 'F'}, optional, default: C
    Whether to store multi-dimensional data in row-major
    (C-style) or column-major (Fortran-style) order in
    memory.

Returns
-------
out : ndarray
    Array of ones with the given shape, dtype, and order.

See Also
--------
ones_like : Return an array of ones with shape and type of input.
empty : Return a new uninitialized array.
zeros : Return a new array setting values to zero.
full : Return a new array of given shape filled with value.


Examples
--------
>>> np.ones(5)
array([1., 1., 1., 1., 1.])

>>> np.ones((5,), dtype=int)
array([1, 1, 1, 1, 1])

>>> np.ones((2, 1))
array([[1.],
       [1.]])

>>> s = (2,2)
>>> np.ones(s)
array([[1.,  1.],
       [1.,  1.]])
File:      ~/.pyenv/versions/3.8.2/lib/python3.8/site-packages/numpy/core/numeric.py
Type:      function