Table of Contents
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.
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.
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.
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.
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.
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.
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)
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 )
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
Configured an initial spin-polarized electron density for a solid.
Saved our results for later use.
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.
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.
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)
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.
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).
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).
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