In [ ]:
from __future__ import print_function, division
import numpy as np
from sisl import *
import matplotlib.pyplot as plt
%matplotlib inline
This tutorial will describe a complete walk-through of a large fraction of the sisl
functionalities. It will show you how to generated default geometries, constructing Hamiltonians, calculating eigenstates and plotting various physical quantities.
Our system of interest will be graphene. Instead of creating a graphene flake, or the primary unit-cell of graphene, we will create a vacancy in graphene. We will start by creating a graphene flake
In [ ]:
graphene = geom.graphene().tile(6, 0).tile(6, 1)
This does a lot of things behind the scenes:
geom.graphene
:3x3
, i.e. a nearest neighbour unit-cell.Geometry.tile
tiles the geometry (reps, axis)
by reps
times along the unit cell axis axis
By printing the object one gets basic information regarding the geometry, such as 1) number and species of atoms, 2) number of orbitals, 3) orbitals associated with each atom and 4) number of supercells.
In [ ]:
print(graphene)
In this example we have na=72
atoms, each have one orbital, hence the total number of orbitals is no=72
. The description of the atomic specie in the geometry tells us we have a carbon atom, with a single orbital with a radius of $1.4342\,\mathrm{Å}$. The number of supercells are [3, 3, 1]
which means cells {-1, 0, 1}
along the first and second lattice are taken into account.
Later we will look into the details of orbitals associated with atoms and how they may be used for wavefunctions etc.
Lets visualize the atomic positions (here adding atomic indices)
In [ ]:
plot(graphene, atom_indices=True);
Removing an atom can be done with Geometry.remove
. The routine takes an index, or a list of indices of the atoms to be removed. For instance removing the first atom will result in the following geometry (red atom is the removed atom)
In [ ]:
coord = graphene.xyz[0, :]
temp = graphene.remove(0)
ax = plot(temp)
ax.scatter(temp.xyz[:, 0], temp.xyz[:, 1]);
ax.scatter(coord[0], coord[1], c='r', s=200);
For the following it doesn't matter which atom we remove (since it is peridiodic), however for visualization purposes we will remove an atom in the middle of the unit cell.
Using sisl
it is easy to find atoms close to specific positions. The middle of the atomic coordinates is also the center of atomic coordinates, here Geometry.center
is useful. The complementary method Geometry.close
finds all atomic indices close to a given position or atom. By default, Geometry.close
determines all atoms within a radius equal to the maximum orbital radius. Here we explicitly set the search radius to $1.5\,\mathrm{Å}$.
In [ ]:
ax = plot(graphene.sc)
xyz_center = graphene.center(what='xyz')
ax.scatter(xyz_center[0], xyz_center[1], 100, marker='*', c='k'); # center of geometry
index = graphene.close(xyz_center, 1.5)
for i in index:
ax.scatter(graphene.xyz[i, 0], graphene.xyz[i, 1], 200, marker='s', edgecolors='k', facecolors='none'); # atoms within 1.5 of center
index = index[0]
xyz_remove = graphene.xyz[index]
ax.scatter(xyz_remove[0], xyz_remove[1], c='r'); # plot the removed atom
system = graphene.remove(index)
ax.scatter(system.xyz[:, 0], system.xyz[:, 1]);
In [ ]:
H = Hamiltonian(system)
print(H)
In addition to the Geometry
information it informs us that it is an orthogonal basis (sisl
also allows non-orthogonal basis'). The spin-configuration is an unpolarized configuration (see Spin
for details).
Currently non-zero = 0
specifies that there are no associated Hamiltonian elements stored in this Hamiltonian object.
Hamiltonian.construct
lets one specify the Hamiltonian elements in a consistent way. However, note that the Hamiltonian
objet may be used as though it was a matrix, i.e. Hamiltonian[0, 1] = a
will set the hopping element from the 0th orbital to the 1st orbital to a
.
We will specify all on-site elements to $0.\,\mathrm{eV}$, and all nearest neighbour interactions with $-2.7\,\mathrm{eV}$, this is the most common used graphene tight-binding model.
The arguments for the construct
method is a list of radii and an accompanying list of energies. Here r
tells sisl
to find all atoms within a sphere of $0.1\,\mathrm{Å}$ from each atom and set the corresponding element to $0$, secondly all atoms within a spherical radius of $0.1\,\mathrm{Å}$ to $1.44\,\mathrm{Å}$ are given the matrix element $-2.7\,\mathrm{eV}$
In [ ]:
r = (0.1, 1.44)
t = (0. , -2.7 )
H.construct([r, t])
print(H)
Now the Hamiltonian has $281=71\cdot4-3$ non-zero elements (as expected).
At this point we have 1) a complete geometry describing a supercell structure with nearest cell neighbour interactions, 2) a Hamiltonian with nearest neighbour interactions describing the electronic structure.
This completes what is needed to calculate a great deal of physical quantities, e.g. eigenstates, density of states, projected density of states and bandstructures.
To begin with we calculate the $\Gamma$-point eigenstates and plot a subset of the eigenstates' norm on the geometry.
In [ ]:
es = H.eigenstate()
# Reduce the contained eigenstates to only 3 states around the Fermi-level
es_fermi = es.sub(range(len(H) // 2 - 1, len(H) // 2 + 2))
_, ax = plt.subplots(1, 3, figsize=(14, 2));
for i, (n, c) in enumerate(zip(es_fermi.norm2(sum=False), 'rbg')):
ax[i].scatter(system.xyz[:, 0], system.xyz[:, 1], 600 * n, facecolor=c, alpha=0.5);
ax[i].scatter(xyz_remove[0], xyz_remove[1], c='k', marker='*'); # mark the removed atom
The Hamiltonian.eigenstate
(with an optional $k$-point argument) routine returns an EigenstateElectron
object which holds the eigenvalues and eigenvectors for a given $k$-point. This object can perform several advanced calculations:
EigenstateElectron.DOS
: calculate the DOS at a given set of energy values (in eV), additionally one can pass a distribution function if the default Gaussian with $\sigma=0.1\,\mathrm{eV}$ is not wanted.EigenstateElectron.PDOS
: calculate the projected DOS at a given set of energy values (in eV), additionally one can pass a distribution function if the default Gaussian with $\sigma=0.1\,\mathrm{eV}$ is not wanted.EigenstateElectron.wavefunction
: add all contained eigenstates to a passed real-space grid.Lets first try and calculate the DOS for a given set of energies in the range $-4\,\mathrm{eV}$ : $\mathrm{4}\,\mathrm{eV}$
In [ ]:
E = np.linspace(-4, 4, 400)
plt.plot(E, es.DOS(E));
plt.xlabel(r'$E - E_F$ [eV]'); plt.ylabel(r'DOS at $\Gamma$ [1/eV]');
The projected DOS (in this case) can aptly be plotted on individual atoms as will be seen in the following. We will integrate the PDOS in the range $-1\,\mathrm{eV}$ to $-0.5\,\mathrm{eV}$
In [ ]:
E = np.linspace(-1, -.5, 100)
dE = E[1] - E[0]
PDOS = es.PDOS(E).sum(1) * dE # perform integration
plt.scatter(system.xyz[:, 0], system.xyz[:, 1], 500 * PDOS);
plt.scatter(xyz_remove[0], xyz_remove[1], c='k', marker='*'); # mark the removed atom
This highlights a somewhat localized state around the missing atom.
The above DOS and PDOS analysis are useful for investigating a single $k$-point at a time. However, they are incomplete in the sense of the full Brillouin zone. To leverage this sisl
, implements several classes to handle Brillouin-zones.
In the following we will show how to perform band structure calculations as well as performing $k$-averaged quantities in the Brillouin zone.
An easy and useful analysis is the band structure. In sisl
calculating the band-structure is as easy as anything else.
Begin by defining the path in the Brillouin zone.
In [ ]:
band = BandStructure(H, [[0, 0, 0], [0, 0.5, 0],
[1/3, 2/3, 0], [0, 0, 0]], 400,
[r'$\Gamma$', r'$M$',
r'$K$', r'$\Gamma$'])
Note that the integer 400
determines the total number of $k$-points on the full band. One can define an explicit number between the different points, however we highly encourage only specifying one integer as the divisions will be determined based on the length in the reciprocal space between the points and thus the physical distance in the Brillouin zone will be correct.
A word of note on the BrillouinZone
objects.
The BrillouinZone
objects are extremely handy because they allow to directly call any routine inherent to the passed object. If calling routine band.a()
it is equivalent to:
[band.parent.a(k=k) for k in band]
Note that BrillouinZone
defaults to use BrillouinZone.asarray()
.
However, for large systems this may result in memory problems in which case it may be necessary to return an iterator. To circumvent this we can tell the Brillouin zone object to (always) return an iterator instead:
band.asyield()
for k in band:
yield band.parent.a(k=k)
The last option is to return $k$-point averaged quantities which is roughly equivalent to (internally it is done with minimal memory usage):
band.asaverage()
sum([band.parent.a(k=k) * band.weight[i] for i, k in enumerate(band)])
Now we can calculate the band structure. A band-structure requires all eigenvalues and thus we ask the BrillouinZone
object to return all values using asarray()
.
In [ ]:
bs = band.asarray().eigh()
In [ ]:
lk, kt, kl = band.lineark(True)
plt.xticks(kt, kl)
plt.xlim(0, lk[-1])
plt.ylim([-3, 3])
plt.ylabel('$E-E_F$ [eV]')
for bk in bs.T:
plt.plot(lk, bk)
Now we are in a position to calculate a subset of quantities from the Hamiltonian. Before proceeding we will note that the Hamiltonian also implements the DOS
and PDOS
methods (equivalent to Hamiltonian.eigenvalue().DOS()
), hence to calculate these as $k$-averaged quantities we can create a Brillouin zone object with proper weights, say a Monkhorst-Pack grid, and calculate the averaged quantities.
In [ ]:
bz = MonkhorstPack(H, [35, 35, 1])
bz.asaverage(); # specify the Brillouin zone to perform an average of subsequent method calls
In [ ]:
E = np.linspace(-4, 4, 1000)
plt.plot(E, bz.DOS(E));
plt.xlabel('$E - E_F$ [eV]');
plt.ylabel('DOS [1/eV]');
We can also plot the projected DOS integrated around the Fermi level on the atoms
In [ ]:
E = np.linspace(-1, 1, 1000)
dE = E[1] - E[0]
PDOS = bz.PDOS(E).sum(1) * dE
In [ ]:
plt.scatter(system.xyz[:, 0], system.xyz[:, 1], 400 * PDOS);
plt.scatter(xyz_remove[0], xyz_remove[1], c='k', marker='*'); # mark the removed atom
sisl
also implements methods to plot orbitals on grids. Often it may also be advantageous to plot simple orbitals to check their appearence. sisl
implements a simple variation of spherical atomic orbitals. Other orbitals may easily be added, if so desired.
Since we require orbitals to be zero at some maximum cut-off $r_\mathrm{max}$ a radial function is used to cutoff the spherical harmonics. In this case we will simply use an exponential function with a cutoff radius of $1.6\,\mathrm{Å}$
In [ ]:
r = np.linspace(0, 1.6, 700)
f = 5 * np.exp(-r * 5)
print('Normalization: {}'.format(f.sum() * (r[1] - r[0])))
plt.plot(r, f);
plt.ylim([0, None])
orb = SphericalOrbital(1, (r, f));
To calculate the orbital on a 3D grid, the Orbital.toGrid
function is available. Here we create an orbital with azimuthal quantum number $l=1$ and by default it has angular moment $0$, i.e. the $2p_z$ orbital. To create a $2p_x$ or $2p_y$ orbital requires the use of an AtomicOrbital
to denote the $m$ quantum number. Below we create the grid that describes the $p_z$ orbital and plot the $yz$ plane at $x=0$ (with respect to the position of the orbital).
Note that the real-space orbital will not be normalized and thus $\int|\psi(\mathbf r)|^2\neq 1$, as it should. The following analysis is thus a qualitative analysis.
In [ ]:
grid = orb.toGrid()
index = grid.index(-grid.origo)
In [ ]:
plt.imshow(grid.grid[index[0], :, :].T);
This simple orbital model may be used to plot the real-space tight-binding eigenstates. Our first task will be to tell the geometry that the orbital associated with the Carbon atoms is the $2p_z$ orbital as noted above. First we create the Carbon atom, then we replace the atom in the system geometry.
In [ ]:
C = Atom(6, orb)
print(system)
system.atom.replace(system.atom[0], C)
print(system)
Note the difference between the two print(system)
statements, before the replacement, an intrinsic Orbital
object describes the orbital, in effect no knowledge other than the radius. After replacement, the spherical orbital with azimuthal angular moment $l=1$ is replaced. Now we can plot the real-space grid for an eigenstate.
Lets plot one of the $\Gamma$-point eigenstates close to the Fermi-level.
In [ ]:
es = H.eigenstate(dtype=np.float64).sub([len(H) // 2 + 1])
grid = Grid(0.075, sc=H.sc)
es.wavefunction(grid)
index = grid.index([0, 0, 0.1])
In [ ]:
plt.imshow(grid.grid[:, :, index[2]].T);
plt.colorbar();
index = grid.index(xyz_remove)
plt.scatter(index[0], index[1], c='k', marker='*'); # mark the removed atom
Please try and disregard the skewedness of the plot. Since the geometry is skewed, the wavefunction plot will be transformed to a square, however the graphene lattice may be recognized. The above plot shows $\psi(x, y, 0.1\,\mathrm{Å})$ for all $x$ and $y$. Note the similarity with the norm of the Hamiltonian eigenstates. However, in this plot we see that the wavefunction changes sign on alternating atoms.