Spin-polarized iron

Table of Contents

Introduction

This tutorial demonstrates how to perform a spin-polarized calculation for a bulk system namely bcc iron. In summary, this will involve

  • Creating a bulk sample.

  • Specifying an initial spin density.

  • Calculating the band structure.

  • Plotting the band structure.

Prerequisites

Here, we will assume that you have read the tutorial Spin-polarized oxygen and are familiar with the contents in Python basics. The plotting of the band structure will be done from data stored in a simple text file, which could easily be used by several different plotting tools. Thus no prior knowledge of a specific plotting tool is required. This section makes heavy use of imported modules and if you are unfamiliar with this concept, we encourage you to read the section Importing modules in the Python basics primer. We also use some more advanced Python methods and functions in the modules that are imported. It is not essential to understand what happens, in order to benefit from these additional modules.

Creating the sample

We start out by creating our sample. If you have read the tutorial The band structure of bulk silicon, you will already know these basic steps.

import ATK
from ATK.KohnSham import *

# Create BCC iron crystal
lattice = BodyCenteredCubic( 2.866*Ang )
bcc_iron = BulkConfiguration(lattice,[Iron],[3*(0.0,)*Ang])

We create a bcc lattice using the BodyCenteredCubic method of NanoLanguage and assign a single iron atom as the basis of our BulkConfiguration.

Defining and executing the calculation

The parameters used for configuring the self-consistent field (SCF) calculation will now be specified. You could optimize these parameters, as is done in the tutorial Calculating molecular properties. However, this example will not go into the details of optimizing parameters for a DFT calculation. The following parameters are changed from their default values:

# Specify parameters for spin-polarized bulk calculation 
brillouin_zone_sampling = brillouinZoneIntegrationParameters((3,3,3))

electron_density = electronDensityParameters(
    mesh_cutoff = 100*Rydberg,
    initial_spin = 1.0*hbar
    )

dft_iron = KohnShamMethod(
    brillouin_zone_integration_parameters = brillouin_zone_sampling,
    electron_density_parameters = electron_density,
    )

By setting the initial_spin in the electronDensityParameters(), we instruct ATK to perform a spin-polarized calculation. In this case, since we do not know the spin-polarization of iron (we wish to calculate it!), we set the value to 1.0 hbar. For more details on the initial spin, see the discussion in the reference section for electronDensityParameters(). We also lower the mesh cut-off to reduce the calculation time.

With the necessary parameters for our SCF-calculation set, we instruct ATK to store the calculation in a checkpoint file using the setCheckpointFilename() method of the ATK module. We also increase the verbosityLevel() to follow the convergence of our calculation.

# Specify NetCDF file for storing results
# and increase verbosity
nc_file = 'dft_Febcc.nc'
ATK.setCheckpointFilename(nc_file)
ATK.setVerbosityLevel(1)

scf_iron = executeSelfConsistentCalculation(
    atomic_configuration = bcc_iron,
    method = dft_iron,
    )

We now have a converged spin-polarized electron density for a bulk bcc iron sample. If we increase the verbosityLevel() to 9 (or above), we see that the majority of the iron electrons are spin-up. The SCF-calculations is initialized with 5 electrons having spin-up and 3 electrons spin-down (as we specified by setting the initial_spin to 1.0 hbar) whereas the converged result has a Mulliken population of 5.5 electrons with spin-up and 2.5 electrons with spin-down (on average). That is, 3 more spin-up than spin-down electrons per unit cell.

Plotting the band structure

With our converged electron density in place, we want to plot the band structure. Before we can do that, we need to define which directions in the Brillouin zone, along which the energy levels should be plotted. First, we specify the coordinates of the special points for a bcc Brillouin zone. We then specify in which order, the energy levels along these directions should be plotted.

# Setup special points in the Brillouin zone
bcc_symmetry_points = {
   "G"  : (0.00, 0.00, 0.00),
   "H"  : (0.50,-0.50, 0.50),
   "P"  : (0.25, 0.25, 0.25),
   "N"  : (0.00, 0.00, 0.50),
   "N'" : (0.50, 0.50, 0.00),
   "H'" : (0.50, 0.50, 0.50)
  }

# Specify route in the Brillouin zone
bcc_route = ["G","H","N","G","P","N'","P","H'"]

Now, we use the function calculateAndPlotBandstructure(), which we have imported from the module bandstructure. This method is explained in greater detail below:

# Call auxillary function for plotting spin-polarized band structure
from bandstructure import calculateAndPlotSpinBandstructure
calculateAndPlotSpinBandstructure(
    ncfilename = nc_file,
    datafilename = "dft_Febcc.dat",
    plotfilename = "plotBandStructure.py",
    imagename = 'Fe_bcc.png',
    symmetry_points = bcc_symmetry_points,
    route = bcc_route,
    number_of_kpoints = 25

In short, the method takes a NetCDF file and creates the data files, dft_Febcc_up.dat and dft_Febcc_dn.dat, which contain an ASCII representation of the energy levels in the Brillouin zone for spin-up and spin-down electrons, respectively. We also specify the name of the file, which will contain the plotting instructions. This can be changed to suit your plotting program of choice. Here, we have used matplotlib. Similarly, the name of the graphics file, which will eventually contain the band structure, along with the route and the specified points in the Brillouin Zone are given as arguments. The final two arguments instructs the function to use 25 interpolation points between each of the points that we have specified in bcc_route, and that we have a spin-polarized calculation. The resulting plotting file, plotBandstructure.py, is shown below.

Plotting using matplotlib

The two figures below show the band structure of bcc iron for a spin-polarized (Figure 22) and unpolarized (Figure 23) calculation. We see that the spin-up states (red lines) in general are located lower in energy than the spin-down states. This explains why more spin-up states than spin-down states are occupied.

The band structure of spin-polarized bcc iron

Figure 22: The band structure of spin-polarized bcc iron


The band structure of unpolarized bcc iron

Figure 23: The band structure of unpolarized bcc iron


In this example, we have used matplotlib to generate our band structure plot. The auto-generated plotting script is shown below. This script will fail to run if you do not have the right Python tools installed. Please see the homepage of matplotlib for specific details as to what should be installed on your particular platform if you want to use the script below.

import matplotlib
matplotlib.use('Agg')
from pylab import *

import pickle
f = open("dft_Febcc.dat","rb")
X    = pickle.load(f)
Y_up = pickle.load(f)
Y_dn = pickle.load(f)
f.close()

for i in range(len(Y_up)):
    plot(X,Y_up[i],'r-')
    plot(X,Y_dn[i],'b-')
  
ylabel('Energy (eV)')
xticks ([0.0, 0.8660254037844386, 1.5731321849709863, 2.0731321849709863, 
  2.5061448868632055, 2.9391575887554247, 3.3721702906476438, 3.805182992539863],
  ['G', 'H', 'N', 'G', 'P', "N'", 'P', "H'"])
grid(True)
v1 = axis()
v2 = [v1[0],3.80899,v1[2],v1[3]]
axis(v2)

The complete script

Below is the complete script for setting up the DFT calculation and for creating the data files and plotting script, provided the modules listed in the import section of the script are available. You can see a description, and links for downloading these modules, further down.

import ATK
from ATK.KohnSham import *

# Create BCC iron crystal
lattice = BodyCenteredCubic( 2.866*Ang )
bcc_iron = BulkConfiguration(lattice,[Iron],[3*(0.0,)*Ang])

# Specify parameters for spin-polarized bulk calculation 
brillouin_zone_sampling = brillouinZoneIntegrationParameters((3,3,3))

electron_density = electronDensityParameters(
    mesh_cutoff = 100*Rydberg,
    initial_spin = 1.0*hbar
    )

dft_iron = KohnShamMethod(
    brillouin_zone_integration_parameters = brillouin_zone_sampling,
    electron_density_parameters = electron_density,
    )

# Specify NetCDF file for storing results
# and increase verbosity
nc_file = 'dft_Febcc.nc'
ATK.setCheckpointFilename(nc_file)
ATK.setVerbosityLevel(1)

scf_iron = executeSelfConsistentCalculation(
    atomic_configuration = bcc_iron,
    method = dft_iron,
    )

# Setup special points in the Brillouin zone
bcc_symmetry_points = {
   "G"  : (0.00, 0.00, 0.00),
   "H"  : (0.50,-0.50, 0.50),
   "P"  : (0.25, 0.25, 0.25),
   "N"  : (0.00, 0.00, 0.50),
   "N'" : (0.50, 0.50, 0.00),
   "H'" : (0.50, 0.50, 0.50)
  }

# Specify route in the Brillouin zone
bcc_route = ["G","H","N","G","P","N'","P","H'"]

# Call auxillary function for plotting spin-polarized band structure
from bandstructure import calculateAndPlotSpinBandstructure
calculateAndPlotSpinBandstructure(
    ncfilename = nc_file,
    datafilename = "dft_Febcc.dat",
    plotfilename = "plotBandStructure.py",
    imagename = 'Fe_bcc.png',
    symmetry_points = bcc_symmetry_points,
    route = bcc_route,
    number_of_kpoints = 25
    )

iron-spin.py

Summary

This tutorial has shown how to perform a spin-polarized calculation for a bulk material, in this case exemplified by bcc iron. Along the way we

  1. Configured an initial spin-polarized electron density for a solid.

  2. Saved our results for later use.

  3. Imported auxiliary functions used for data manipulation and script generation from modules.

After these steps, you should master how to enhance NanoLanguage with reusable extension modules and perform spin-polarized SCF-calculations for a bulk system.

The rest of this tutorial explains the functionality of the individual methods imported from the modules. It is not necessary to read these explanations in order to use these modules with the above example.

Imported modules

This example relies heavily on the imported modules. These are described in the following. If you would like to know the basic idea about importing modules in Python, we suggest you read the section Importing modules in Python basics.

bandstructure.py

This module imports other modules, which are either built into NanoLanguage or described below. The following sections describe each function contained in the bandstructure.py module. This module is generalized and can be used, in combination with the other modules listed below, to calculate the band structure of a bulk system along user specified directions in the Brillouin zone. For more details, see the code comments in the scripts.

from ATK.KohnSham import *
from numpy import array
from separations import *

def writeBandstructureDataFile (filename,symmetry_points,route,
                                k_points,bandstructure_up,bandstructure_dn):
    """
    Create a Pickle data file with the bandstructure
    Three objects are dumped: kx (1D values of the k-points
    for the horizontal axis), and energies for up and down spin
    # The last symmetry point is of course not included
    # except as endpoint for the last segment;
    # hence the -1 in the range commands
    """
    # Generate 1D k-points for the horizontal axis
    kx_points = []
    separations,separationsums = generateSeparations(symmetry_points,route)
    for point in range(len(route)-1):
        numKPoints = len(k_points[point])
        kx_points += convertVectorPointToX(numKPoints,point,separations,separationsums)
    # Make empty lists of the right size for the bandstructure data
    # Each list has as many elements as there are bands
    # Each element is a list, the length of which matches the total number of k-points
    energies_up = []
    energies_dn = []
    number_of_bands = bandstructure_up[0].numberOfBands()
    for band in range(number_of_bands):
        energies_up.append([])
        energies_dn.append([])
    # Extract the bands, strip off the unit, and store in the lists
    for point in range(len(route)-1):
        for band in range(number_of_bands):
            tmp = bandstructure_up[point].band(band)
            energies_up[band] = energies_up[band] + [E.inUnitsOf(eV) for E in tmp]
            tmp = bandstructure_dn[point].band(band)
            energies_dn[band] = energies_dn[band] + [E.inUnitsOf(eV) for E in tmp]
    # Store data in pickle file
    import pickle
    f = open(filename,"wb")
    pickle.dump(kx_points,f)
    pickle.dump(energies_up,f)
    pickle.dump(energies_dn,f)
    f.close()

def calculateBandstructure(calculation,symmetry_points,route,n_points,spin):
    """
    Calculate the bandstructure for the "calculation" object,
    using the provided symmetry points and the route that
    connects them. This routines defines the k-points,
    calculate the bandstructure, and return both. In principle
    each segment of the bandstructure plot can contain a
    different number of k-points, but for simplicity we choose
    an equal number for now.
    """
    band_k_points = []
    bandstructure = []
    # The last symmetry point is of course not included
    # except as endpoint for the last segment
    for point in range(len(route)-1):
        start_point = symmetry_points[route[point]]
        end_point = symmetry_points[route[point+1]]
        band_k_points.append(generateLineBetweenVectorsPoints(start_point,end_point,n_points))
        bandstructure.append(calculateEnergyBands(calculation,band_k_points[point],spin))
    return band_k_points, bandstructure

def writePyLabScript(plotfilename,datafilename,imagename,symmetry_points,route):
    # Create pyLab script to plot the data
    f = open(plotfilename,"w")
    separations,separationsums = generateSeparations(symmetry_points,route)
    # The text block to be written to the script
    s = """
import matplotlib
matplotlib.use('Agg')
from pylab import *

import pickle
f = open("%s","rb")
X    = pickle.load(f)
Y_up = pickle.load(f)
Y_dn = pickle.load(f)
f.close()

for i in range(len(Y_up)):
    plot(X,Y_up[i],'r-')
    plot(X,Y_dn[i],'b-')
  
ylabel('Energy (eV)')
xticks (%s,%s)
grid(True)
v1 = axis()
v2 = [v1[0],%g,v1[2],v1[3]]
axis(v2)
savefig('%s',dpi=100)
    """ % (datafilename,separationsums,\
    route,separationsums[-1]*1.001,imagename)
    f.write(s)

def calculateAndPlotSpinBandstructure(
        ncfilename,datafilename,plotfilename,imagename,
        symmetry_points,route,number_of_kpoints=25):

    calculation = restoreSelfConsistentCalculation(ncfilename)
    band_k_points,bandstructure_up = calculateBandstructure(
        calculation, symmetry_points, route,
        number_of_kpoints, Spin.Up)

    band_k_points,bandstructure_dn = calculateBandstructure(
        calculation, symmetry_points, route,
        number_of_kpoints, Spin.Down)

    writeBandstructureDataFile(datafilename,symmetry_points,route,
                               band_k_points,bandstructure_up,bandstructure_dn)

    writePyLabScript(plotfilename,datafilename,imagename,symmetry_points,route)

bandstructure.py

separations.py

The functions contained in this module, could just as well have been included in the bandstructure.py module. However, they could be usable in other situations, where you might not be interested in the energy bands. Therefore, they have been placed in a separate module. It is generally a good idea to modularize your scripts once they grow in size and complexity. Furthermore, this allows you to reuse methods, thus saving yourself the time of rewriting a simple library function every time.

generateSeparations()

This function helps us to project a continuous path between a sequence of points in 3D space onto a one-dimensional axis. In our case the points will be k-points in the Brillouin zone, but the script is general, and makes no assumption regarding the nature of the points (only, that they are dimensionless).

The separation between two points on the one-dimensional axis will be proportional to the vector distance between two sequential points. We calculate this using the dot function from NumPy on the vector delta (a NumPy array) between two points on the route. These distances are stored in the variable separations, which is a simple list; the elements in this list will be the one-dimensional values corresponding to the points on the path. We also store the accumulated sum of these values in separationsums, for later use (see the function convertVectorPointToX() below).

convertVectorPointToX()

This function generates the corresponding one-dimensional values for N points, evenly distributed between two points in space. It is necessary to have generated the separations using the function generateSeparations() (see above).

generateLineBetweenVectorsPoints()

Given two points in space, this function generates a sequence of N evenly distributed points between them, and returns these in a list.

from numpy import array, dot
import math 

def generateSeparations (symmetry_points, route):
    """
    Given a dict of 3D vector points and the route which
    connects them, generate the so-called separations and
    separation sums.
    """
    separations = []
    # The first point is always placed at x=0
    separationsums = [0.0]
    # The last symmetry point is of course not included
    # except as endpoint for the last segment
    for p in range(len(route)-1):
        delta = array(symmetry_points[route[p+1]]) - array(symmetry_points[route[p]])
        # Calculate and store the vector distance between subsequent points
        separations.append(math.sqrt(dot(delta,delta)))
        # Cumulative sum of the distances
        separationsums.append(separationsums[-1] + separations[-1])
    return separations, separationsums

def convertVectorPointToX (N, id, separations, separationsums):
    """
    Use the separations and separation sums to map 3D vector
    points onto a one-dimensional dummy coordinate for the
    horizontal axis, e.g. to be used in a bandstructure plot.
    id is the segment number.
    """
    x = []
    for i in range(N):
        # If changes are made to the definition of the vector points
        # in generateLineBetweenVectorsPoints below,
        # the denominator here must be changed accordingly
        x.append(float(i)/(N-1)*separations[id] + separationsums[id])
    return x

def generateLineBetweenVectorsPoints (start_point,end_point,N):
    """
    Given two points (given as lists), generate a sequence of
    N points between them, evenly distributed and return in a list.
    """
    b = []
    for i in range(N):
        # The end-point is included; to exclude it, use N in the denominator instead
        # If this is changed, the change should be mirrored in convertVectorPointToX above
        a = (float(i)/(N-1),)*3*(array(end_point)-array(start_point)) + start_point
        b.append(a)
    return b

separations.py