Lattice specification

This section covers a few extra features of the Lattice class. It is assumed that you are already familiar with the Tutorial.

Download this page as a Jupyter notebook

First, we set a few constants which are going to be needed in the following examples:

from math import sqrt, pi

a = 0.24595   # [nm] unit cell length
a_cc = 0.142  # [nm] carbon-carbon distance
t = -2.8      # [eV] nearest neighbour hopping

Gamma = [0, 0]
K1 = [-4*pi / (3*sqrt(3)*a_cc), 0]
M = [0, 2*pi / (3*a_cc)]
K2 = [2*pi / (3*sqrt(3)*a_cc), 2*pi / (3*a_cc)]

Intrinsic onsite energy

During the construction of a Lattice object, the full signature of a sublattice is (name, offset, onsite_energy=0.0), where the last argument is optional. The name and offset arguments were already explained in the basic tutorial. The onsite_energy is applied as an intrinsic part of the sublattice site. As an example, we’ll add this term to monolayer graphene:

def monolayer_graphene(onsite_energy=[0, 0]):
    lat = pb.Lattice(a1=[a, 0], a2=[a/2, a/2 * sqrt(3)])
    lat.add_sublattices(('A', [0, -a_cc/2], onsite_energy[0]),
                        ('B', [0,  a_cc/2], onsite_energy[1]))
    lat.add_hoppings(([0,  0], 'A', 'B', t),
                     ([1, -1], 'A', 'B', t),
                     ([0, -1], 'A', 'B', t))
    return lat

lattice = monolayer_graphene()
lattice.plot()
Unit cell of graphene's crystal lattice

The effect of the onsite energy becomes apparent if we set opposite values for the A and B sublattices. This opens a band gap in graphene:

model = pb.Model(
    monolayer_graphene(onsite_energy=[-1, 1]),  # eV
    pb.translational_symmetry()
)
solver = pb.solver.lapack(model)
bands = solver.calc_bands(K1, Gamma, M, K2)
bands.plot(point_labels=['K', r'$\Gamma$', 'M', 'K'])
Graphene band structure with a band gap

An alternative way of doing this was covered in the Opening a band gap section of the basic tutorial. There, an @onsite_energy_modifier was used to produce the same effect. The modifier is applied only after the system is constructed so it can depend on the final (x, y, z) coordinates. Conversely, when the onsite energy is specified directly in a Lattice object, it models an intrinsic part of the lattice and cannot depend on position. If both the intrinsic energy and the modifier are specified, the values are added up.

Constructing a supercell

A primitive cell is the smallest unit cell of a crystal. For graphene, this is the usual 2-atom cell. It’s translated in space to construct a larger system. Sometimes it can be convenient to use a larger unit cell instead, i.e. a supercell consisting of multiple primitive cells. This allows us to slightly adjust the geometry of the lattice. For example, the 2-atom primitive cell of graphene has vectors at an acute angle with regard to each other. On the other hand, a 4-atom supercell is rectangular which makes certain model geometries easier to create. It also makes it possible to realize armchair edges, as shown in Nanoribbons section of the basic tutorial.

We can create a 4-atom cell by adding two more sublattice to the Lattice specification:

def monolayer_graphene_4atom():
    lat = pb.Lattice(a1=[a, 0], a2=[0, 3*a_cc])
    lat.add_sublattices(('A',  [  0, -a_cc/2]),
                        ('B',  [  0,  a_cc/2]),
                        ('A2', [a/2,    a_cc]),
                        ('B2', [a/2,  2*a_cc]))
    lat.add_hoppings(
        # inside the unit sell
        ([0, 0], 'A',  'B',  t),
        ([0, 0], 'B',  'A2', t),
        ([0, 0], 'A2', 'B2', t),
        # between neighbouring unit cells
        ([-1, -1], 'A', 'B2', t),
        ([ 0, -1], 'A', 'B2', t),
        ([-1,  0], 'B', 'A2', t),
    )
    return lat

lattice = monolayer_graphene_4atom()
plt.figure(figsize=(5, 5))
lattice.plot()
../_images/lattice-4.png

Note the additional sublattices A2 and B2, shown in green and red in the figure. As defined above, these are interpreted as new and distinct lattice sites. However, we would like to have sublattices A2 and B2 be equivalent to A and B. Lattice.add_aliases() does exactly that:

def monolayer_graphene_4atom():
    lat = pb.Lattice(a1=[a, 0], a2=[0, 3*a_cc])
    lat.add_sublattices(('A',  [  0, -a_cc/2]),
                        ('B',  [  0,  a_cc/2]))
    lat.add_aliases(('A2', 'A', [a/2, a_cc]),
                    ('B2', 'B', [a/2, 2*a_cc]))
    lat.add_hoppings(
        # inside the unit sell
        ([0, 0], 'A',  'B',  t),
        ([0, 0], 'B',  'A2', t),
        ([0, 0], 'A2', 'B2', t),
        # between neighbouring unit cells
        ([-1, -1], 'A', 'B2', t),
        ([ 0, -1], 'A', 'B2', t),
        ([-1,  0], 'B', 'A2', t),
    )
    return lat

lattice = monolayer_graphene_4atom()
plt.figure(figsize=(5, 5))
lattice.plot()
../_images/lattice-5.png

Now we have a supercell with only two unique sublattices: A and B. The 4-atom graphene unit cell is rectangular which makes it a more convenient building block than the oblique 2-atom cell.

Removing dangling bonds

When a finite-sized graphene system is constructed, it’s possible that it will contain a few dangling bonds on the edge of the system. These are usually not desired and can be removed easily by setting the Lattice.min_neighbors attribute:

plt.figure(figsize=(8, 3))
lattice = monolayer_graphene()
shape = pb.rectangle(x=1.4, y=1.1)

plt.subplot(121, title="min_neighbors == 1 -> dangling bonds")
model = pb.Model(lattice, shape)
model.plot()

plt.subplot(122, title="min_neighbors == 2", ylim=[-0.6, 0.6])
model = pb.Model(lattice.with_min_neighbors(2), shape)
model.plot()
../_images/lattice-6.png

The dangling atoms on the edges have only one neighbor which makes them unique. When we use the Lattice.with_min_neighbors() method, the model is required to remove any atoms which have less than the specified minimum number of neighbors. Note that setting min_neighbors to 3 would produce an empty system since it is impossible for all atoms to have at least 3 neighbors.

Global lattice offset

When we defined monolayer_graphene() at the start of this section, we set the positions of the sublattices as \([x, y] = [0, \pm a_{cc}]\), i.e. the coordinate system origin is at the midpoint between A and B atoms. It can sometimes be convenient to choose a different origin position such as the center of a hexagon formed by the carbon atoms. Rather than define an entirely new lattice with different positions for A and B, we can simply offset the entire lattice by setting the Lattice.offset attribute:

plt.figure(figsize=(8, 3))
shape = pb.regular_polygon(num_sides=6, radius=0.55)

plt.subplot(121, title="Origin between A and B atoms")
model = pb.Model(monolayer_graphene(), shape)
model.plot()
model.shape.plot()

plt.subplot(122, title="Origin in the center of a hexagon")
model = pb.Model(monolayer_graphene().with_offset([a/2, 0]), shape)
model.plot()
model.shape.plot()
../_images/lattice-7.png

Note that the shape remains unchanged, only the lattice shifts position. We could have achieved the same result by only moving the shape, but then the center of the shape would not match the origin of the coordinate system. The Lattice.with_offset() makes it easy to position the lattice as needed. Note that the given offset must be within half the length of a primitive lattice vector (positive or negative). Beyond that length the lattice repeats periodically, so it doesn’t make sense to shift it any father.