Skip to content
    import matplotlib.pyplot as plt

import numpy as np
from math import sqrt,pi

from common import draw_classic_axes, configure_plotting

from plotly.subplots import make_subplots
import plotly.graph_objs as go

configure_plotting()
  

Lecture 9 – Crystal structure

based on chapter 12 of the book

Expected prior knowledge

Before the start of this lecture, you should be able to:

  • use elementary vector calculus

Learning goals

After this lecture you will be able to:

  • Describe any crystal using crystallographic terminology, and interpret this terminology
  • Compute the volume filling factor given a crystal structure
  • Determine the primitive, conventional, and Wigner-Seitz unit cells of a given lattice
  • Determine the Miller planes of a given lattice

Crystal classification

In the past few lectures, we derived some very important physical quantities for phonons and electrons, such as the effective mass and the dispersion relation. The systems we considered were mainly 1D. But most solids, such as crystals, are 3D structures. Describing 3D systems is not much harder than describing 1D systems. It does however, require a new language and framework to fully describe such structures. Therefore the upcoming two lectures will focus on developing, understanding, and applying this language and framework.

Lattices and unit cells

Most of solid state physics deals with crystals, which are periodic multi-atomic structures. To describe such periodic structures, we need a framework. Such a framework is given by the concept of a lattice:

A lattice is an infinite set of points defined by integer sums of a set of linearly independent primitive lattice vectors(will be explained later on).

Which for a 3D system translates to:

\[ \mathbf{R}_{\left[n_{1} n_{2} n_{3}\right]}=n_{1} \mathbf{a}_{1}+n_{2} \mathbf{a}_{2}+n_{3} \mathbf{a}_{3}, \quad \mathrm{for } \:\: n_{1}, n_{2}, n_{3} \in \mathbb{Z}. \]

With \(\mathbf{a}_i\) being the primitive lattice vectors. For a \(n\) dimensional system, we have to define \(n\) linearly independent primitive lattice vectors in order to be able to map out the entire lattice.

This definition is pretty rigorous. But there exist multiple equivalent definitions of a lattice. A more informal definition is:

A lattice is a set of points, where the environment of each point is the same.

In the image below we show a 2D simple square lattice (panel A). The black dots are the lattice points. Because the environment of each point is the same, this set of points forms a lattice.

# Define the lattice vectors
a1 = np.array([1,0])
a2 = np.array([0,1])
a1_alt = -a1-a2
a2_alt = a1_alt + a1
a2_c = np.array([0,sqrt(3)])

# Produces the lattice to be fed into plotly
def lattice_generation(a1,a2,N = 10):
    grid = np.arange(-N//2,N//2,1)
    xGrid, yGrid = np.meshgrid(grid,grid)
    return np.reshape(np.kron(xGrid.flatten(),a1),(-1,2))+np.reshape(np.kron(yGrid.flatten(),a2),(-1,2))


# Produces the dotted lines of the unit cell
def dash_contour(a1,a2, vec_zero = np.array([0,0]), color='Red'):
    dotLine_a1 = np.transpose(np.array([a1,a1+a2])+vec_zero)
    dotLine_a2 = np.transpose(np.array([a2,a1+a2])+vec_zero)

    def dash_trace(vec,color):
        trace = go.Scatter(
            x=vec[0],
            y=vec[1],
            mode = 'lines',
            line_width = 2,
            line_color = color,
            line_dash='dot',
            visible=False
        )
        return trace

    return dash_trace(dotLine_a1,color),dash_trace(dotLine_a2,color)

# Makes the lattice vector arrow
def make_arrow(vec,text,color = 'Red',vec_zero = [0,0], text_shift = [-0.2,-0.1]):
    annot = [dict(
            x = vec[0],
            y = vec[1],
            ax = vec_zero[0],
            ay = vec_zero[1],
            xref = 'x',
            yref = 'y',
            axref = 'x',
            ayref = 'y',
            showarrow=True,
            arrowhead=2,
            arrowsize=1,
            arrowwidth=3,
            arrowcolor = color
                 ),
         dict(
            x=(vec[0]+vec_zero[0])/2+text_shift[0],
            y=(vec[1]+vec_zero[1])/2+text_shift[1],
            xref='x',
            ayref = 'y',
            text = text,
            font = dict(
                color = color,
                size = 20
            ),
            showarrow=False,
             )
        ]
    return annot

# Create the pattern
pattern_points = lattice_generation(a1,a2)

# Lattice Choice A
latticeA = go.Scatter(visible = True,x=pattern_points.T[0],y=pattern_points.T[1],mode='markers',marker=dict(
        color = 'Black',
        size = 10,
        )
    )

# Annotate the lattice vectors
# Button 1
bt1_annot = make_arrow(a1,r'$\mathbf{a}_1$') + make_arrow(a2,r'$\mathbf{a}_2$',text_shift=[-0.3,-0.1], vec_zero = [0,-.1])
# Button 2
bt2_annot = make_arrow(a1,r'$\mathbf{a}_1$') + make_arrow(a2,r'$\mathbf{a}_2$',text_shift=[-0.3,-0.1], vec_zero = [0,-.1]) + make_arrow(a1_alt,r'$\tilde{\mathbf{a}}_1$',color='Black',text_shift=[-0.6,-0.1]) + make_arrow(a2_alt,r'$\tilde{\mathbf{a}}_2$',color='Black',text_shift=[-0.35,-0.1], vec_zero = [0,.1])


# (0)  Button 1, (0, 1-5) Button 2, (0, 6-8) Button 3, (0,1,9,10)
data = [latticeA, *dash_contour(a1,a2), *dash_contour(a1_alt,a2_alt,color='Black'),]

updatemenus = list([
    dict(
        type="buttons",
        direction = "down",
        active=0,
        buttons=list([
            dict(label="A",
                 method="update",
                 args=[{"visible": [True, False, False, False, False,]},
                       {"title": "Lattice",
                        "annotations": []}]),
            dict(label="B",
                 method="update",
                 args=[{"visible": [True, True, True, False, False,]},
                       {"title": "Lattice with a primitive unit cell",
                        "annotations": bt1_annot}]),
            dict(label="C",
                 method="update",
                 args=[{"visible": [True, True, True, True, True, ]},
                       {"title": "Lattice with a two primitive unit cells",
                        "annotations": bt2_annot}]),
        ]),
    )
]
)

# Setting axis to invisible
plot_range = 2.1
axis = dict(
        range=[-plot_range,plot_range],
        visible = False,
        showgrid = False,
        fixedrange = True
    )

# Figure propeties
layout = dict(
    showlegend = False,
    updatemenus = updatemenus,
    plot_bgcolor = 'rgb(254, 254, 254)',
    width = 600,
    height = 600,
    xaxis = axis,
    yaxis = axis
)

# Displaying the figure
go.Figure(data=data, layout=layout)

A vector connecting any two lattice points is called a lattice vector. Suppose we choose two linearly independent lattice vectors \(\mathbf{a}_1\) and \(\mathbf{a}_2\) (panel B). These two lattice vectors span an area which is called a unit cell:

A unit cell is a spatial region that, when many identical unit cells are stacked together, tiles (completely fills) all of space and reconstructs the full structure.

In the case of a 3D lattice, we need to choose three linearly independent lattice vectors. Therefore, the unit cell will be a volume instead of an area. If the chosen unit cell only contains a single lattice point, we speak of a primitive unit cell. The lattice vectors which span a primitive unit cell are called primitive lattice vectors. Because the primitive unit cell is constructed from a set of linearly independent, primitive lattice vectors, it can be repeated infinitely many times to map out the entire lattice!

The unit cell, which was conveniently chosen in panel B of the figure above, is a primitive unit cell because it contains only one lattice point. At first glance, it might seem that there are four lattice points inside the cell, but each point lies only for \(\frac{1}{4}\)'th inside the cell. Therefore, there is exactly \(4 \times \frac{1}{4}=1\) lattice point in the unit cell and thus it is primitive!

The choice of primitive lattice vectors is not unique. In panel C, we show the same lattice plotted with two different unit cells. Both choices are primitive unit cells and can therefore be used to construct the entire lattice!

Examine the primitive unit cell spanned by the lattice vectors \(\tilde{\mathbf{a}}_1\) and \(\tilde{\mathbf{a}}_2\). What fraction of each lattice point lies within the unit cell?

Two points lie for 1/8'th inside the unit cell, and the other two points for 3/8'th.

Periodic structures.

Equipped with the basic definitions of a lattice, (primitive) lattice vectors, and (primitive) unit cells, we now apply our knowledge to an actual periodic structure. In the image below we show a periodic star-like structure (panel A).

# Define the lattice vectors
vec_0 = np.array([-.5, 0])
a1 = np.array([1, 0])
a2 = np.array([.5, np.sqrt(3)/2])
a1_alt = -a1-a2
a2_alt = a1_alt + a1
a2_c = np.array([0,sqrt(3)])

# Create the pattern
pattern_points = lattice_generation(a1,a2)
pattern = go.Scatter(x=pattern_points.T[0],y=pattern_points.T[1],mode='markers',marker=dict(
    color='Black',
    size = 20,
    symbol = 'star-open-dot')
    )

# Lattice Choice A
latticeA = go.Scatter(visible = False, x = pattern_points.T[0],y = pattern_points.T[1],mode='markers',marker=dict(
        color = 'Red',
        size = 10,
        )
    )

# Lattice Choice B
latticeB = go.Scatter(visible = False, x = pattern_points.T[0]+0.5,y = pattern_points.T[1],mode='markers',marker=dict(
        color='Blue',
        size = 10,
        )
    )

# Annotate the lattice vectors
# Button 1
bt1_annot = make_arrow(a1,r'$\mathbf{a}_1$') + make_arrow(a2,r'$\mathbf{a}_2$',text_shift=[-0.3,-0.1], vec_zero = [0,-.1])
# Button 2
bt2_annot = make_arrow(a1+vec_0,r'$\mathbf{a}_1$', vec_zero = vec_0, color = 'Blue') + make_arrow(a2+vec_0,r'$\mathbf{a}_2$',text_shift=[-0.3,-0.1], vec_zero = [-.55,-.1], color = 'Blue')
# Button 3
bt3_annot = make_arrow([1,0], r'$\mathbf{a}_1$') + make_arrow([0, np.sqrt(3)], r'$\mathbf{a}_2$',text_shift=[-0.3,-0.1])

# (0)  Button 1, (0, 1-5) Button 2, (0, 6-8) Button 3, (0,1,9,10)
data = [pattern, latticeA, latticeB, *dash_contour(a1,a2, color = 'Red'),
        *dash_contour(a1, a2, vec_zero = vec_0, color = 'Blue'),
        *dash_contour(np.array([1,0]), a2_c, color = 'Red')]


updatemenus = list([
    dict(
        type="buttons",
        direction = "down",
        active=0,
        buttons=list([
            dict(label="A",
                 method="update",
                 args=[{"visible": [True, False, False, False, False, False, False, False, False,]},
                       {"title": "Periodic structure",
                        "annotations": []}]),
            dict(label="B",
                 method="update",
                 args=[{"visible": [True, True, False, True, True, False, False, False, False,]},
                       {"title": "Periodic structure with lattice and primitive unit cell",
                        "annotations": bt1_annot}]),
            dict(label="C",
                 method="update",
                 args=[{"visible": [True, False, True, False, False, True, True, False, False,]},
                       {"title": "Periodic structure with translated lattice and primitive unit cell",
                        "annotations": bt2_annot}]),
            dict(label="D",
                 method="update",
                 args=[{"visible": [True, True, False, False, False, False, False, True, True,]},
                       {"title": "Periodic structure with lattice and conventional unit cell",
                        "annotations": bt3_annot}]),
        ]),
    )
]
)

# Setting axis to invisible
plot_range = 2.1
axis = dict(
        range=[-plot_range,plot_range],
        visible = False,
        showgrid = False,
        fixedrange = True
    )

# Figure propeties
layout = dict(
    showlegend = False,
    updatemenus = updatemenus,
    plot_bgcolor = 'rgb(254, 254, 254)',
    width = 600,
    height = 600,
    xaxis = axis,
    yaxis = axis
)

go.Figure(data=data, layout=layout)

There are many reasonable ways to assign lattice points to the periodic structure. We can for instance assign the lattice points to the stars themselves. This set of points indeed defines a lattice, because each of the points has the same environment. Because the lattice points form triangles, this lattice is called a triangular lattice.

The choice of a lattice also defines two linearly independent primitive lattice vectors \(\mathbf{a}_1\) and \(\mathbf{a}_2\) (panel B):

\[ \mathbf{a}_1 = \hat{\mathbf{x}} = \left[ 1, 0\right], \quad \mathbf{a}_2 = \frac{1}{2}\hat{\mathbf{x}} + \frac{\sqrt{3}}{2} \hat{\mathbf{y}}= \left[1/2, \sqrt{3}/2\right]. \]

With these primitive lattice vectors, the lattice is given by

\[ \mathbf{R}_{\left[n_{1}, n_{2}\right]}=n_{1} \left[ 1, 0 \right]+n_{2} \left[ 1/2, \sqrt{3}/2 \right], \quad \mathrm{for} \:\: n_{1}, n_{2} \in \mathbb{Z}. \]

However, our description so far is insufficient to describe the periodic structure. Although we mapped out the entire lattice, we still do not have any information about the periodic star-like structure itself. To include the information of the periodic structure, we need to define a basis (do not confuse this with the definition of a basis in linear algebra):

The description of objects with respect to the reference lattice point is known as a basis.

The reference lattice point is the chosen lattice point to which we apply the lattice vectors in order to reconstruct the lattice. In our case, we chose the reference point at \([0, 0]\). With respect to the reference point, the star is located at \([0, 0]\). The location of all the stars in the lattice with respect to the reference lattice point is then given by:

\[ \mathbf{R}_{\left[n_{1}, n_{2}\right]}^{\mathrm{star}} = n_{1} \left[ 1, 0 \right]+n_{2} \left[ 1/2, \sqrt{3}/2 \right], \quad \mathrm{for} \:\: n_{1}, n_{2} \in \mathbb{Z}. \]

Another way to define the basis is in terms of the fractional coordinates of the primitive lattice vectors. In other words, we want to express the basis as a linear combination of the primitive lattice vectors:

\[ (f_1, f_2, \cdots, f_N) = \sum_{i = 1}^{N}f_i \mathbf{a}_i, \]

where \(f_i\) is the fractional coordinate of \(\mathbf{a}_i\) and \(N\) is the dimensionality of the system. In 2D this equation reduces to

\[ (f_1, f_2) = f_1 \mathbf{a}_1 + f_2 \mathbf{a}_2. \]

In our case, the basis is \(\star(0,0) = 0 \mathbf{a}_1 +0 \mathbf{a}_2\). Here \(\star\) means the type of object that is specified by the basis. E.g. if we were to have carbon atoms (C) as well, we would write that down as \(\mathrm{C}(f_1, f_2)\).

Similar to the choice of primitive lattice vectors, the choice of a lattice is not unique. In panel C we show the same periodic structure, but now with the lattice points translated by \(1/2 \mathbf{a}_1\). This set of points still fulfills the definition of a lattice because the environment of each point is the same. Since the lattice is only translated, we can use the same primitive lattice vectors.

However, we must change the basis: the stars are now not located on the lattice points anymore. Instead, they are located at \(1/2 \mathbf{a}_{1}+0\mathbf{a}_{2}\) with respect to the reference lattice point. Therefore, the basis is \(\star(1/2, 0)\).

The location of the stars with respect to the reference lattice point is then described by

\[ \mathbf{R}_{\left[n_{1}, n_{2}\right]}^{\mathrm{star}} = \left[1/2, 0 \right] + n_{1} \left[ 1, 0 \right]+n_{2} \left[ 1/2, \sqrt{3}/2 \right], \quad \mathrm{for} \:\: n_{1}, n_{2} \in \mathbb{Z}. \]

Conventional unit cell

Sometimes the primitive unit cell is not the most practical choice, because it is often non-orthogonal. However, we can define a conventional unit cell with an orthogonal set of lattice vectors (panel D). Conventional unit cells can contain multiple lattice points, as shown in the plot: since there is an additional lattice point at the center, there are $1 + \frac{1}{4} \times 4 = 2 $ lattice points in the conventional unit cell.

There is a slight problem with this definition of the lattice vectors: no integer linear combination of lattice vectors is able to produce the center lattice points in the unit cell. In order to map out the entire lattice, we need to include an extra star in the basis. Since there is one star at the corner of the unit cell and one in the centre, the basis is: \(\star = (0,0),(1/2,1/2)\). The corresponding locations of the stars with respect to the reference lattice point are then given by:

\[\begin{align*} \mathbf{R}_{\left[n_{1}, n_{2}\right]}^{\mathrm{corner}} &= n_{1} \mathbf{a}_1 +n_2 \mathbf{a}_2\\ \mathbf{R}_{\left[n_{1}, n_{2}\right]}^{\mathrm{centre}} &= 1/2\left( \mathbf{a}_1 + \mathbf{a}_2 \right) + n_{1} \mathbf{a}_1 +n_2 \mathbf{a}_2, \end{align*}\]

for $ n_{1}, n_{2} \in \mathbb{Z}$. Alternatively, one can say that the complete crystal structure is made up from two interpenetrating orthogonal lattices - one centred at \([0,0]\) and another at \([1/2,1/2]\), each with basis \(\star = (0,0)\).

For what type of lattice is the conventional unit cell also primitive?

For a simple square lattice, visible in the topmost figure, the conventional unit cell is also primitive.

General procedure to analyze a periodic structure

To summarize what we explained above, we give a short procedure on how to analyze periodic structures:

  1. Choose a point as the origin (this can be an atom, but does not have to be)
  2. Find other points for which the environment is identical to that of the first point.
  3. Choose lattice vectors that connect these lattice points: either primitive or not primitive (in the latter case you need an additional basis point)
  4. Specify the basis

With the definition of the lattice and the basis, the location of every atom in the periodic structure is known and the crystal can be reconstructed. To recall: a crystal structure = lattice + basis

Example: analyzing the graphene crystal structure

Let us apply our knowledge to an actual crystal structure, namely that of graphene. Graphene consists of a single layer of carbon atoms arranged in a honeycomb shape. The nearest-neighbour interatomic distance is \(a\). Below we show the crystal structure of graphene (panel A).

# Define the lattice vectors
a1 = np.array([np.sqrt(3),0])
a2 = np.array([np.sqrt(3)/2,3/2])
a2_c = np.array([0,3])
N_points = 10

# Wigner-Seitz lines
x_dotted = [np.sqrt(3)/2, -np.sqrt(3)/2, None, np.sqrt(3)/2,0, None, np.sqrt(3)/2, np.sqrt(3), None,
            np.sqrt(3)/2, 3*np.sqrt(3)/2, None, np.sqrt(3)/2, np.sqrt(3), None, np.sqrt(3)/2,0]
y_dotted = [3/2, 3/2, None, 3/2, 3, None, 3/2, 3, None,
            3/2, 3/2, None, 3/2, 0, None, 3/2, 0]
WS_x = [0, 0, np.sqrt(3)/2, np.sqrt(3), np.sqrt(3), np.sqrt(3)/2, 0]
WS_y = [1, 2, 2.5, 2, 1, 0.5, 1]

# New lattice generation
def graphene_generation(a1,a2,N = 10):
    grid = np.arange(-N//2,N//2,1)
    xGrid, yGrid = np.meshgrid(grid, grid[np.mod(grid+1,3) != 0])
    return np.reshape(np.kron(xGrid*np.sqrt(3).flatten(),a1),(-1,2))+np.reshape(np.kron(yGrid.flatten(),a2),(-1,2))

# Creating interatomic lines
def create_lines(x, y):
    x_edges = []
    y_edges = []
    for i in range(len(x)-1):
        for j in range(i+1, len(x)-1):
            vec_len = np.sqrt((x[j]-x[i])**2+(y[j]-y[i])**2)
            if vec_len < 1.1:
                x_edges.extend([x[i], x[j], None])
                y_edges.extend([y[i], y[j], None])

    return x_edges, y_edges

# Create the pattern
lat_vec1 = np.array([1,0])
lat_vec2 = np.array([0,1])
pattern_points = graphene_generation(lat_vec1, lat_vec2)

# Extracting data
xx = np.append(pattern_points.T[0], pattern_points.T[0] + np.sqrt(3)/2)
yy = np.append(pattern_points.T[1], pattern_points.T[1] + 3/2)

# Creating graphene structure
graph_struc = go.Scatter(visible = True, x = xx, y = yy, mode = 'markers', marker=dict(
        color = 'Black',
        size = 10,
        )
    )

# Creating lines
x_edges, y_edges = create_lines(xx, yy)
line_trace = go.Scatter(name='edge',
                        x=x_edges,
                        y=y_edges,
                        mode='lines',
                        line_width=2,
                        line_color='black')

# Creating lattice
lat_points = lattice_generation(a1, a2)
x_lat = lat_points.T[0]
y_lat = lat_points.T[1]
lattice = go.Scatter(visible = False, x = x_lat, y = y_lat, mode = 'markers', marker=dict(
        color = 'Red',
        size = 10,
        )
    )

# Constructing the Wigner-Seitz cell
WS_dotted = go.Scatter(visible = False,
                        x = x_dotted,
                        y = y_dotted,
                        mode = 'lines',
                        line = dict(color = 'red', width = 2, dash = 'dash')
                        )

WS_cell = go.Scatter(visible = False,
                        x = WS_x,
                        y = WS_y,
                        mode = 'lines',
                        line = dict(color = 'red', width = 2),
                        fill = 'toself',
                        fillcolor = 'rgba(255, 0, 0, 0.15)'
                        )

# Button 1
bt1_annot = make_arrow(a1, r'$\mathbf{a}_1$') + make_arrow(a2,'$\mathbf{a}_2$', text_shift=[0,-0.3], vec_zero = [-.05, -.05])
# Button 2
bt2_annot = make_arrow(a1,r'$\mathbf{a}_1$') + make_arrow(a2_c,r'$\mathbf{a}_2$', vec_zero = [0, -.05])


# (0)  Button 1, (0, 1-5) Button 2, (0, 6-8) Button 3, (0,1,9,10)
data = [graph_struc, line_trace, lattice, *dash_contour(a1,a2, color = 'Red'), *dash_contour(a1,a2_c, color='Red'), WS_dotted, WS_cell]


updatemenus = list([
    dict(
        type="buttons",
        direction = "down",
        active=0,
        buttons=list([
            dict(label="A",
                 method="update",
                 args=[{"visible": [True, True, False, False, False, False, False, False, False]},
                       {"title": "graphene crystal structure",
                        "annotations": []}]),
            dict(label="B",
                 method="update",
                 args=[{"visible": [True, True, True, False, False, False, False, False, False]},
                       {"title": "lattice",
                        "annotations": []}]),
            dict(label="C",
                 method="update",
                 args=[{"visible": [True, True, True, True, True, False, False, False, False]},
                       {"title": "lattice with primitive unit cell",
                        "annotations": bt1_annot}]),
            dict(label="D",
                 method="update",
                 args=[{"visible": [True, True, True, False, False, True, True, False, False]},
                       {"title": "lattice with conventional unit cell",
                        "annotations": bt2_annot}]),
            dict(label="E",
                 method="update",
                 args=[{"visible": [True, True, True, False, False, False, False, True, True]},
                       {"title": "lattice with the Wigner-Seitz cell",
                        "annotations": []}]),
        ]),
    )
]
)

# Setting axis to invisible
plot_range = N_points//3+0.3
shift = 1
axis = dict(
        range=[-plot_range+shift, plot_range+shift],
        visible = False,
        showgrid = False,
        fixedrange = True
    )

# Figure propeties
layout = dict(
    showlegend = False,
    updatemenus = updatemenus,
    plot_bgcolor = 'rgb(254, 254, 254)',
    width = 600,
    height = 600,
    xaxis = axis,
    yaxis = axis
)

# Displaying the figure
go.Figure(data=data, layout=layout)

Our first task is to find suitable lattice points. We start by choosing a lattice point at \((x,y)=(0,0)\). Crucially, we see that not all atoms have the same environment. Hence, the set of all carbon atoms do not form a lattice! Only those with the same environment are valid lattice points. In panel B we show the lattice of graphene.

Primitive lattice vectors are not unique. In panel C, a possible choice of primitive lattice vectors is shown. Using geometry, we find

\[ \mathbf{a}_1 = [\sqrt{3}, 0] a, \quad \mathbf{a}_2 = [\sqrt{3}/2, 3/2] a. \]

Having found the lattice vectors, the only thing left is to specify the basis. We observe that each primitive unit cell contains two carbon atoms: one at \(\mathbf{r}_0 = (0,0)\) and the other at \(\mathbf{r}_1 = (\sqrt{3},1)a\). We want to express these coordinates as fractional coordinates of our lattice vectors. For the atom at \(\mathbf{r}_0\) this is easy: \(\mathrm{C}(0,0)\). (here C stand for carbon). For the other atom, we use

\[ \mathbf{r_1} = f_1\mathbf{a}_1 + f_2\mathbf{a}_2 \]

We first look at the \(y\)-component of \(\mathbf{r}_1\), which is \(a\). Because only \(\mathbf{a}_2\) has a nonzero \(y\) component, we conclude \(f_2=2/3\). We then look at the \(x\)-component of \(\mathbf{r}_1\), which is \(\sqrt{3}a\). It should satisfy

\[\begin{align*} \sqrt{3}a &= f_1 a_{1,x} + f_2 a_{2,x}\\ \end{align*}\]

which we solve to find \(f_1 = 2/3\). Hence, the fractional coordinates of the second atom are \(\mathrm{C}(2/3, 2/3)\).

In panel D we show the conventional unit cell with the lattice vectors

\[ \mathbf{a}_1 = [\sqrt{3}, 0] a, \quad \mathbf{a}_2 = [0, 3] a. \]

Now the unit cell contains four carbon atoms which need to be specified in the basis. The basis in fractional coordinates is: \(\mathrm{C}(0,0), \: \mathrm{C}(0,1/3), \: \mathrm{C}(1/2, 1/2) \: \mathrm{and} \: \mathrm{C}(1/2, 5/6)\).

Wigner-Seitz unit cell

There exists a very important alternative type of a primitive unit cell - the Wigner-Seitz cell. It is a collection of all points that are closer to one specific lattice point than to any other lattice point. The cell is formed by taking all the perpendicular bisectrices of lines connecting a lattice point to its neighboring lattice points. The Wigner-Seitz cell is constructed as follows:

  1. Find all neighboring lattice points with respect to the reference lattice point.
  2. Draw lines between the reference lattice point and the neighboring lattice points
  3. Draw perpendicular bisectrices of each line. To do so, find the middle of the line connecting two lattice points and draw a line perpendicular to it.
  4. Extend the perpendicular bisectrices until these intersect. Doing so for each bisectrice yields the Wigner-Seitz cell.

In panel E of the figure above we show the Wigner-Seitz cell of graphene by the red shaded area. We see that the Wigner-Seitz cell only contains a single lattice point in the middle. It does however, contain multiple atoms, which should be specified in the basis.

How many carbon atoms are inside the Wigner-Seitz cell of graphene?

There are two methods to calculate this. We either translate the lattice and thus the Wigner-Seitz cell a bit and observe that there are two carbon atoms inside the cell. Another way to calculate the number of atoms inside the cell is by realizing that there is an atom at the lattice point itself and there is 1/3'rd of an atom at three corners of the cell. This results in \(1+3\times 1/3 = 2\) atoms being inside the unit cell.

The reason why we would want to use the Wigner-Seitz cell over other unit cells becomes clear when we study the reciprocal lattice in next lecture.

Simple 3D crystal structures

So far we looked at 2D periodic structures. As we mentioned earlier, most crystal structures are 3D. Therefore, the rest of the lecture will focus on applying our gained knowledge to 3D lattices. We will consider conventional cubic unit cells with lattice vectors \(\mathbf{a_1} = a \mathbf{\hat{x}}\), \(\mathbf{a_2} = a \mathbf{\hat{y}}\) and \(\mathbf{a_3} = a \mathbf{\hat{z}}\), with \(a\) being the lattice constant.

The simplest unit cell possible is the simple cubic cell visible in panel A of the figure below. Each corner contains a lattice point, each of which is 1/8 inside the unit cell. Thus there is \(8 \times 1/8 = 1\) lattice point inside the unit cell and it is thus primitive!

Body-centered cubic lattice

If we add an additional lattice point to the center of the simple cubic cell, we obtain the body-centered cubic (bcc) lattice (panel B). In the bcc lattice, there are 8 lattice points on the corners of the cell and one in the centre. Thus, the conventional unit cell contains \(8\times 1/8+1 = 2\) lattice points and is not primitive.

What is the basis of the bcc lattice?

\((0,0,0)\) and \((1/2, 1/2, 1/2)\)

In the exercises you will figure out a proper set of primitive lattice vectors for the bcc lattice.

Face-centered cubic lattice

If we add an additional point to the middle of every face of the simple cubic cell, we obtain the face-centered cubic (fcc) lattice (panel C). There are is \(1/2\) of a lattice point at each face inside the lattice in addition to the corner lattice point. Hence there are a total of \(8 \times 1/8 + 6\times 1/2 = 4\) lattice points inside the unit cell and thus it is not primitive. Examples of crystal structures with an fcc lattice:

  1. Ionic crystals are usually fcc. E.g. NaCl.
  2. Zincblende & diamond crystals.
# Coordinates of the corner atoms
x = np.tile(np.arange(0,2,1),4)
y = np.repeat(np.arange(0,2,1),4)
z = np.tile(np.repeat(np.arange(0,2,1),2),2)

# Coordinates for the bcc lattice
x_bcc = [.5,0,.5,1,.5,.5]
y_bcc = [.5,.5,0,.5,1,.5]
z_bcc = [0,.5,.5,.5,.5,1]

# creating edge lines
x_edges, y_edges, z_edges = [], [], []
for i in range(len(x)):
    for j in range(i+1, len(x)):
        vec_len = np.sqrt((x[j]-x[i])**2+(y[j]-y[i])**2+(z[j]-z[i])**2)
        if vec_len < 1.1:
            x_edges.extend([x[i], x[j], None])
            y_edges.extend([y[i], y[j], None])
            z_edges.extend([z[i], z[j], None])

# Creating fcc lines
x_fcc, y_fcc, z_fcc = [], [], []
for j in range(len(x)):
    vec_len = np.sqrt((x[j]-0.5)**2+(y[j]-0.5)**2+(z[j]-0.5)**2)
    if vec_len < 1:
        x_fcc.extend([x[j], 0.5, None])
        y_fcc.extend([y[j], 0.5, None])
        z_fcc.extend([z[j], 0.5, None])

# Creating bcc lines
x_bcc_l, y_bcc_l, z_bcc_l = [], [], []
for j in range(len(x)):
    for i in range(len(x_bcc)):
        vec_len = np.sqrt((x[j]-x_bcc[i])**2+(y[j]-y_bcc[i])**2+(z[j]-z_bcc[i])**2)
        if vec_len < 1:
            x_bcc_l.extend([x_bcc[i], x[j], None])
            y_bcc_l.extend([y_bcc[i], y[j], None])
            z_bcc_l.extend([z_bcc[i], z[j], None])

# Initialize figure
c_size = 32
l_width = 2
fig = go.Figure()

# Adding simple qubic traces
fig.add_trace(
    go.Scatter3d(
        x = x,
        y = y,
        z = z,
        mode = 'markers',
        marker = dict(
            sizemode = 'diameter',
            sizeref = c_size,
            size = c_size,
            color = 'rgb(211,211,211)',
            line = dict(
              color = 'rgb(0,0,0)',
              width = 5
            )
            )
    )
)

fig.add_trace(
    go.Scatter3d(
        x = x_edges,
        y = y_edges,
        z = z_edges,
        mode = 'lines',
        line_width=l_width,
        line_color='black',
    )
)


# fcc traces
fig.add_trace(
    go.Scatter3d(visible = False,
        x = [0.5],
        y = [0.5],
        z = [0.5],
        mode = 'markers',
        marker = dict(
            sizemode = 'diameter',
            sizeref = c_size,
            size = c_size,
            color = 'rgb(211,211,211)',
            line = dict(
              color = 'rgb(0,0,0)',
              width = 5
            )
            )
    )
)

fig.add_trace(
    go.Scatter3d(visible = False,
        x = x_fcc,
        y = y_fcc,
        z = z_fcc,
        mode = 'lines',
        line_width=l_width,
        line_color='black'
    )
)


# bcc traces
fig.add_trace(
    go.Scatter3d(visible = False,
        x = x_bcc,
        y = y_bcc,
        z = z_bcc,
        mode = 'markers',
        marker = dict(
            sizemode = 'diameter',
            sizeref = c_size,
            size = c_size,
            color = 'rgb(211,211,211)',
            line = dict(
              color = 'rgb(0,0,0)',
              width = 5
            )
            )
    )
)

fig.add_trace(
    go.Scatter3d(visible = False,
        x = x_bcc_l,
        y = y_bcc_l,
        z = z_bcc_l,
        mode = 'lines',
        line_width=l_width,
        line_color='black',
    )
)

# Defining button
button_data = [
    dict(
        type = "buttons",
        direction = "down",
        active = 0,
        buttons = list([
            dict(label="A",
                 method="update",
                 args=[{"visible": [True, True, False, False, False, False]},
                       {"title": "Simple cubic lattice",
                        }]),
            dict(label="B",
                 method="update",
                 args=[{"visible": [True, True, True, True, False, False]},
                       {"title": "bcc lattice",
                        }]),
            dict(label="C",
                 method='update',
                 args=[{"visible": [True, True, False, False, True, True]},
                       {"title": "fcc lattice",
                        }]),
        ]),
    )
]


# Creating buttons
fig.update_layout(
    scene = dict(
                xaxis = dict(
                title= 'x[a]',
                ticks='',
                ),
                yaxis=dict(
                title= 'y[a]',
                ticks='',
                showgrid = False,
                zeroline = False,
                showline = False,
                ),
                zaxis=dict(
                title= 'z[a]',
                ticks='',
                )
    ),
    showlegend = False,
    width = 600,
    height = 600,
    updatemenus = button_data,
)

# Setting background to white
fig.update_scenes(xaxis_visible = False, yaxis_visible = False,zaxis_visible = False )
fig

Filling factor

Each of the three crystal structures above have a different configuration and number of atoms in the unit cell. This results in a different fraction of an unit cell being occupied by atoms. The filling factor, commonly called the atomic packing factor, measures the fraction of a volume of the unit cell that is occupied by atoms. It assumes that the atoms are solid spheres with a volume \(V_{\mathrm{atom}} = \frac{4 \pi}{3} R^3\), where \(R\) is the radius of the sphere. For mono-atomic lattices, the filling factor \(F\) is defined as follows:

\[ F = \frac{ N_{\mathrm{atom}} V_{\mathrm{atom}} }{V_{\mathrm{cell}}}. \]

Here \(N_{\mathrm{atom}}\) is the number of atoms in the unit cell and \(V_{\mathrm{cell}}\) is the volume of the unit cell. To calculate the filling factor we first need to find out what \(V_{\mathrm{atom}}\) is. To do this, we need to 'blow up' the atoms simultaneously until each atom touches its neighbor. We use this blown up geometry to find an expression for \(R\) in terms of known quantities, such as the lattice constant.

As an example, let us apply this to the fcc lattice. Suppose we look at one of the faces of the fcc lattice, which is shown in panel A of the figure below.

# Coordinates of the corner atoms
x = np.append(np.tile(np.arange(0,2,1),2), 0.5)
y = np.append(np.repeat(np.arange(0,2,1),2), 0.5)

# lines
x_line = [0, 0, 1, 1, 0]
y_line = [0, 1, 1, 0, 0]

# Initialize figure
c_size = 40
l_width = 2
fig = go.Figure()

# Adding x and y axis text
def add_xy(vec,text,color = 'Black', text_shift = [-0.2,-0.1]):
    annot = [
         dict(
            x=vec[0]+text_shift[0],
            y=vec[1]+text_shift[1],
            xref = 'x',
            ayref = 'y',
            text = text,
            font = dict(
                color = color,
                size = 20
            ),
            showarrow=False,
             )
        ]
    return annot

# Adding simple fcc face
fig.add_trace(
    go.Scatter(
        x = x,
        y = y,
        mode = 'markers',
        marker = dict(
            sizemode = 'diameter',
            sizeref = c_size,
            size = c_size,
            color = 'rgb(211,211,211)',
            line = dict(
              color = 'rgb(0,0,0)',
              width = 2
            )
            )
    )
)

# Trace with blown up atoms
fig.add_trace(
    go.Scatter(visible = False,
        x = x,
        y = y,
        mode = 'markers',
        marker = dict(
            sizemode = 'diameter',
            sizeref = c_size,
            size = 151,
            color = 'rgb(211,211,211)',
            line = dict(
              color = 'rgb(0,0,0)',
              width = 2
            )
            )
    )
)

# Adding lines
fig.add_trace(
    go.Scatter(visible = True,
        x = x_line,
        y = y_line,
        mode = 'lines',
        line_width=l_width,
        line_color='black',
    )
)

# Axis
axis_annot = add_xy(np.array([.5, 0]), r'x[a]', text_shift = [0,-.1]) + add_xy(np.array([0, .5]), r'y[a]', text_shift = [-.15,0])
axis_annot2 = axis_annot+make_arrow(np.array([1,1]), r'4R', color = 'Black',vec_zero = np.array([0,0]), text_shift = [.12,-.05])

# Defining button
button_data = [
    dict(
        type = "buttons",
        direction = "down",
        active = 0,
        buttons = list([
            dict(label="A",
                 method="update",
                 args=[{"visible": [True, False, True]},
                       {"title": "Face of fcc lattice",
                        "annotations": axis_annot}]),
            dict(label="B",
                 method="update",
                 args=[{"visible": [False, True, True]},
                       {"title": "Face of fcc lattice with blown up atoms",
                        "annotations": axis_annot2}]),
        ]),
    )
]

# Defining axis
axis = dict(
        range=[-0.5, 1.5],
        visible = False,
        showgrid = False,
        fixedrange = True
    )

# Creating buttons
fig.update_layout(
    showlegend = False,
    width = 600,
    height = 600,
    xaxis = axis,
    yaxis = axis,
    updatemenus = button_data,
    plot_bgcolor = 'rgba(0,0,0,0)'
)

fig

We first need to blow up the atoms until they touch each other (panel B). We see that on the diagonal, the atoms touch each other. Because the atoms have radius \(R\), the length of the diagonal is equal to \(4R\). But, as previously stated, the sides of the unit cell have length \(a\). Therefore, the diagonal of the unit cell is also equal to \(\sqrt{2}a\). This implies that

\[ R = \frac{a}{2\sqrt{2}}. \]

Having expressed the radius of the atom in terms of the lattice constant, the volume of the atom \(V_{\mathrm{atom}}\) can be expressed as:

\[ V_{\mathrm{atom}} = \frac{4\pi}{3}R^3 = \frac{4\pi}{3} \left( \frac{a}{2\sqrt{2}} \right)^3 = \frac{\pi a^3}{12 \sqrt{2}}. \]

We showed earlier that the number of atoms in the fcc unit cell is 4. Therefore, we now have all the information to calculate the filling factor:

\[\begin{align*} F &= \frac{ N_{\mathrm{atom}} V_{\mathrm{atom}} }{V_{\mathrm{cell}}}\\ &= \frac{4 \times \frac{\pi a^3}{12\sqrt{2}}}{a^3}\\ &= \frac{\pi}{3\sqrt{2}} \approx 0.74 \end{align*}\]

This means that appoximately \(74 \%\) of the fcc unit cell is occupied by atoms. One might ask if this is the best we can achieve. Turns out, it is! The packing limit was theorized by Kepler in 1571–1630 and proven by Hales et al. in 1998!

In the exercises you will also calculate the filling factor of the bcc lattice.

Miller planes

When fabricating crystals it is important to know both the orientation and the surface of the crystal. Different cuts of a crystal lead to different surfaces. In the chemical industry, this is especially significant because different surfaces lead to different chemical properties and thus is one of the foundations of research in catalysts. Therefore, we seek a way to describe the different planes of a crystal. This leads us to a very important concept - Miller planes. To explain Miller planes, let's start off with a simple cubic lattice:

Where \(|{\bf a}_1|=|{\bf a}_2|=|{\bf a}_3|\equiv a\).

We can cut multiple planes through the cubic lattice. Miller planes describe such planes with a set of indices. The plane defined by Miller indices \((u,v,w)\) intersects lattice vector \({\bf a}_1\) at \(\frac{|{\bf a}_1|}{u}\), \({\bf a}_2\) at \(\frac{|{\bf a}_2|}{v}\) and \({\bf a}_3\) at \(\frac{|{\bf a}_3|}{w}\).

A Miller index equal to 0 means that the plane is parallel to that axis (intersection at "\(\frac{|{\bf a}_3|}{0}\rightarrow\infty\)"). A bar above a Miller index means intersection at a negative coordinate.

If a crystal is symmetric under \(90^\circ\) rotations, then \((100)\), \((010)\) and \((001)\) are physically indistinguishable. Therefore, we use the notation \(\{100\}\) to indicate a whole family of these symmetry-related planes. In a cubic crystal, \([100]\) (this is a vector) is perpendicular to \((100)\) \(\rightarrow\) proof in problem set.

Why are these Miller planes useful? It enables us to know the exact orientation of a crystal structure if the crystal structure is known.

Summary

  • A crystal is constructed through the definition of a lattice and a basis.
  • We introduced several important concepts that allow us to describe crystal structure: lattice, lattice vectors, basis, primitive & conventional unit cells, and Miller planes.
  • We introduced several common 3D lattices: simple-cubic, FCC, and BCC.
  • We discussed how to compute the filling factor.

Exercises

Warm-up exercises

  1. State the definition of a primitive unit cell.
  2. Suppose a conventional unit cell contains 4 lattice points and has volume \(a^3\), what would be the volume of the primitive unit cell?
  3. Draw the conventional unit cell of the FCC and the BCC lattice. Write down the associated basis vectors. In addition, find a set of primitive lattice vectors.
  4. Suppose you find the primitive unit cell of a diatomic crystal. How many basis vectors do you minimally need to describe the crystal? Can a diatomic crystal require more basis vectors?
  5. Calculate the filling factor of a simple cubic lattice.
  6. Sketch the \((110),(1\bar{1}0),(111)\) Miller planes of a simple cubic lattice.

Exercise 1: Diatomic crystal

Consider the following two-dimensional diatomic crystal:

y = np.repeat(np.arange(0,8,2),4)
x = np.tile(np.arange(0,8,2),4)
plt.figure(figsize=(5,5))
plt.axis('off')

plt.plot(x,y,'ko', markersize=15)
plt.plot(x+1,y+1, 'o', markerfacecolor='none', markeredgecolor='k', markersize=15);

svg

  1. Sketch the Wigner-Seitz unit cell and two other possible primitive unit cells of the crystal.
  2. If the distance between the filled circles is \(a=0.28\) nm, what is the area of the primitive unit cell? What would be the area of the primitive unit cell if all empty and filled circles were identical?
  3. Write down a set of primitive lattice vectors and the corresponding basis for this crystal. Would these primitive lattice vectors still be primitive if all empty and filled circles were identical? If not, identify a new primitive unit cell and the associated basis.
  4. Imagine expanding the lattice into the perpendicular direction \(z\) (out of the page). We define a new three-dimensional crystal by considering a periodic structure in the \(z\) direction with a period \(a\), where the filled circles are additionally displaced by \(a/2\) in the \(z\)-direction. The figure below shows the new arrangement of the atoms. What lattice do we obtain? Write down the basis of this three-dimensional crystal.
  5. If we consider all atoms to be the same, what lattice do we obtain? Compute the filling factor in the case where all atoms are the same.
x = np.tile(np.arange(0,2,1),4)
y = np.repeat(np.arange(0,2,1),4)
z = np.tile(np.repeat(np.arange(0,2,1),2),2)

trace1 = go.Scatter3d(
    x = x,
    y = y,
    z = z,
    mode = 'markers',
    marker = dict(
        sizemode = 'diameter',
        sizeref = 20,
        size = 20,
        color = 'rgb(255, 255, 255)',
        line = dict(
          color = 'rgb(0,0,0)',
          width = 5
        )
        )
)

trace2 = go.Scatter3d(
    x = [0.5],
    y = [0.5],
    z = [0.5],
    mode = 'markers',
    marker = dict(
        sizemode = 'diameter',
        sizeref = 20,
        size = 20,
        color = 'rgb(0,0,0)',
        line = dict(
          color = 'rgb(0,0,0)',
          width = 5
        )
        )
)

data=[trace1, trace2]

layout=go.Layout(showlegend = False,
                scene = dict(
                        xaxis=dict(
                        title= 'x[a]',
                        ticks='',
                        showticklabels=False
                        ),
                        yaxis=dict(
                        title= 'y[a]',
                        ticks='',
                        showticklabels=False
                        ),
                        zaxis=dict(
                        title= 'z[a]',
                        ticks='',
                        showticklabels=False
                        )
                    ))


go.Figure(data=data, layout=layout)

Exercise 2: The crystal structure of diamond

Consider the diamond crystal structure. The illustration shows the arrangement of the carbon atoms in a conventional unit cell.

Xn = np.tile(np.arange(0,2,1),4)
Yn = np.repeat(np.arange(0,2,1),4)
Zn = np.tile(np.repeat(np.arange(0,2,1),2),2)

Xn = np.hstack((Xn, Xn, Xn+0.5, Xn+0.5))
Yn = np.hstack((Yn, Yn+0.5, Yn+0.5, Yn))
Zn = np.hstack((Zn, Zn+0.5, Zn, Zn+0.5))

Xn = np.hstack((Xn, Xn+1/4))
Yn = np.hstack((Yn, Yn+1/4))
Zn = np.hstack((Zn, Zn+1/4))

Xe=[]
Ye=[]
Ze=[]

num_atoms = len(Xn)
for i in range(num_atoms):
    for j in np.arange(i+1, num_atoms, 1):
        pos1 = np.array([Xn[i], Yn[i], Zn[i]])
        pos2 = np.array([Xn[j], Yn[j], Zn[j]])
        if np.linalg.norm(pos1-pos2) == sqrt(3)/4:
            Xe+=[Xn[i], Xn[j], None]
            Ye+=[Yn[i], Yn[j], None]
            Ze+=[Zn[i], Zn[j], None]

trace1=go.Scatter3d(x=Xe,
               y=Ye,
               z=Ze,
               mode='lines',
               line=dict(color='rgb(0,0,0)', width=3),
               hoverinfo='none'
                )

trace2=go.Scatter3d(x=Xn,
               y=Yn,
               z=Zn,
               mode = 'markers',
               marker = dict(
                        sizemode = 'diameter',
                        sizeref = 20,
                        size = 20,
                        color = 'rgb(255,255,255)',
                        line = dict(
                               color = 'rgb(0,0,0)',
                               width = 5
                               )
                       )
                     )

layout=go.Layout(showlegend = False,
                scene = dict(
                        xaxis=dict(
                        title= 'x[a]',
                        range= [0,1],
                        ticks='',
                        showticklabels=False
                        ),
                        yaxis=dict(
                        title= 'y[a]',
                        range= [0,1],
                        ticks='',
                        showticklabels=False
                        ),
                        zaxis=dict(
                        title= 'z[a]',
                        range= [0,1],
                        ticks='',
                        showticklabels=False
                        )
                    ))

data=[trace1, trace2]

go.Figure(data=data, layout=layout)

The side of the cube is \(a = 0.3567\) nm.

  1. How is this crystal structure related to the fcc lattice? Write down a set of primitive lattice vectors and compute the volume of the corresponding primitive unit cell.
  2. How many atoms are in the primitive unit cell? And how many lattice points? Write down the basis.
  3. Determine the number of atoms in the conventional unit cell and compute this unit cell's volume.
  4. What is the distance between nearest-neighbour atoms?
  5. Compute the atom density (expressed in nr of atoms per cubic nanometer) and the filling factor.

Exercise 3: Directions and spacings of Miller planes

(adapted from ex 13.3 of "The Oxford Solid State Basics" by S.Simon)

  1. Explain the terms Miller planes and Miller indices.
  2. Consider a cubic crystal with one atom in the basis and a set of orthogonal primitive lattice vectors \(a\hat{x}\), \(a\hat{y}\) and \(a\hat{z}\). Show that the direction \([hkl]\) in this crystal is normal to the planes with Miller indices \((hkl)\).
  3. Show that this is not true in general. Consider for instance an orthorhombic crystal, for which the primitive lattice vectors are still orthogonal but have different lengths.
  4. Any set of Miller indices corresponds to a family of planes separated by a distance \(d\). Show that the spacing \(d\) of the \((hkl)\) set of planes in a cubic crystal with lattice parameter \(a\) is \(d = \frac{a}{\sqrt{h^2 + k^2 + l^2}}\).

    Hint

    Recall that a family of lattice planes is an infinite set of equally separated parallel planes which taken all together contain all points of the lattice.

    Try computing the distance between the plane that contains the site \((0,0,0)\) of the conventional unit cell and a plane defined by the \((hkl)\) indices.