Table of Contents
Once you have finished a calculation, there will be many situations where you would like to re-issue the obtained results for sub-sequent calculations. Here we will cover the following scenarios
Improve and refine calculations from a previous run
Restart an ATK run after a system failure
In some situations, for example, you might not know the right choice of parameter settings for a given simulation beforehand. Often, a set of trial-and-error runs are performed until the simulation has been fine tuned to match a given system. This, however, has the disadvantage that you waste a lot of computer resources, since all the information obtained through these test runs is lost. To avoid this, ATK offers the possibility of restoring the result of a previous calculation and use this as the initial condition for a subsequent calculation.
In addition, ATK can also restart an aborted calculation, which typically is needed if your ATK job terminated due to a system crash. Notice that this latter option requires that you have specified a NetCDF checkpoint file.
We will cover both scenarios in the next sub-sections.
Suppose you wanted to calculate the bond length of a hydrogen molecule in its ground state. This could be tackled using the ATK function calculateOptimizedAtomicGeometry().
If, however, the molecular optimization performed by calculateOptimizedAtomicGeometry() for some reason
does not converge within a desired accuracy, we can avoid a complete restart of
the optimization by specifying that the final state obtained by calculateOptimizedAtomicGeometry() should be saved
to a checkpoint file. We do this by
specifying the name of the checkpoint file in the runtimeParameters() and assign the returned object
to the optional variable runtime_parameters when we call the
calculateOptimizedAtomicGeometry()
function. Below is an example of how the checkpoint file and
verbosity_level (i.e degree of program output to the terminal)
is specified in the runtimeParameters():
params = runtimeParameters( verbosity_level=10, checkpoint_filename='h2.nc' )
Here is a complete script for solving the task of optimizing the geometry of the hydrogen molecule
# Import the KohnSham module from ATK from ATK.KohnSham import * # Set up elements and positions elm = [ Hydrogen, Hydrogen ] pos = [ ( +0.4, 0.0, 0.0)*Angstrom, ( -0.4, 0.0, 0.0)*Angstrom ] # Add them to a configuration h2 = MoleculeConfiguration(elm,pos) # Setup Kohn-Sham method dft_method = KohnShamMethod() # Set name of check point file params = runtimeParameters( verbosity_level=10, checkpoint_filename='h2.nc' ) # Calculate the optimized atomic geometry h2_opt = calculateOptimizedAtomicGeometry( atomic_configuration=h2, method=dft_method, runtime_parameters=params ) # Print the coordinates of the individual atoms print '\nAtom coordinates\n---------------------------' for coord in h2_opt.cartesianCoordinates(): for c in coord: print '%7.4f' % (c.inUnitsOf(Angstrom)), print
The key elements in the script are:
Set up a hydrogen molecule with an initial bond length of 0.8 Å.
Define the Kohn-Sham equations using a KohnShamMethod object.
Define the variable params for
holding the runtime parameter settings that should be passed on to calculateOptimizedAtomicGeometry(). Here, we
have chosen the name h2.nc for the checkpoint file
(which is written in the NetCDF
file format). In addition, we also choose a high verbosity setting in order to
follow the progress of the optimization process.
Do the actual geometry optimization by calling calculateOptimizedAtomicGeometry(). The
function calculateOptimizedAtomicGeometry() returns an optimized molecule configuration, which we store
in the variable h2_opt.
Print out the optimized coordinates of the hydrogen molecule.
If you run the script, the last few lines of the output will be
Atom coordinates --------------------------- 0.4000 0.0000 0.0000 -0.4000 0.0000 0.0000
This is not exactly what we had in mind. The initial coordinates of the two hydrogen atoms already satisfy the accuracy of the algorithm used by calculateOptimizedAtomicGeometry(), implying that the atom positions are left untouched by the optimization process. To improve the geometry of the hydrogen molecule, we should have specified a smaller value for the force tolerance criteria used by calculateOptimizedAtomicGeometry(). This is done using the helper function geometricOptimizationParameters() as shown below.
opt = geometricOptimizationParameters( force_tolerance=0.00001*electronVolt/Angstrom )
Here, we have specified a force tolerance of
eV/Å contrary to
the default setting of
eV/Å. The defined variable
opt is then passed as an argument to
calculateOptimizedAtomicGeometry() using the
optional variable optimization_parameters.
Now, to avoid a complete restart of the calculation, we use the ATK function
restoreSelfConsistentCalculation() to load
the checkpoint file h2.nc. This is done as
scf = restoreSelfConsistentCalculation('h2.nc')
So, in summary, here is what we should do
Load the checkpoint file using restoreSelfConsistentCalculation().
Set the desired force tolerance by calling the helper function geometricOptimizationParameters().
Resume the calculation by calling calculateOptimizedAtomicGeometry().
The full script implementing the above strategy becomes
# Import the KohnSham module from ATK from ATK.KohnSham import * # Read self consistent calculation from NetCDF file scf = restoreSelfConsistentCalculation('h2.nc') # Set the force tolerance opt = geometricOptimizationParameters( force_tolerance=0.00001*electronVolt/Angstrom ) # Set name of check point file params = runtimeParameters( verbosity_level=10 ) # Calculate the optimized atomic geometry h2_opt = calculateOptimizedAtomicGeometry( self_consistent_calculation=scf, optimization_parameters=opt, runtime_parameters=params ) # Print the coordinates of the individual atoms print '\nAtom coordinates\n---------------------------' for coord in h2_opt.cartesianCoordinates(): for c in coord: print '%7.4f' % (c.inUnitsOf(Angstrom)), print
Running the script gives rise to the following output:
Atom coordinates --------------------------- 0.3962 -0.0000 -0.0000 -0.3962 0.0000 0.0000
As you probably have noticed, ATK goes through a lot of iterations before the molecule geometry satisfies the prescribed force tolerance.
It is also possible to restart a complete self-consistent field calculation using the executeSelfConsistentCalculation() method in ATK. Using this approach, an almost converged calculation can be restored and the extra cycles needed for convergence can be applied, thereby salvaging precious computing time.
Sometimes when you execute a script, the execution may terminate due to a system crash, or when running the job through a queue system the job may be terminated because the queue wall clock time is exceeded. In both these situations, if you have specified a NetCDF checkpoint file there are certain built-in features of ATK that may help you to minimize the amount of damage.
During a self-consistent calculation, if the checkpoint file has been specified, ATK will automatically save the system state in the checkpoint file after each completed iteration step.
To see how this works in practice, try to run the following script and simulate a system crash by pressing Ctrl-C before the script finishes:
# Import the KohnSham module from ATK from ATK.KohnSham import * # Set up elements and positions elm = [ Hydrogen, Hydrogen ] pos = [ ( +0.4, 0.0, 0.0)*Angstrom, ( -0.4, 0.0, 0.0)*Angstrom ] # Add them to a configuration h2 = MoleculeConfiguration(elm,pos) # Setup Kohn-Sham method dft_method = KohnShamMethod() # Set name of check point file params = runtimeParameters( verbosity_level=10, checkpoint_filename='h2.nc' ) # Calculate the optimized atomic geometry h2_opt = calculateOptimizedAtomicGeometry( atomic_configuration=h2, method=dft_method, runtime_parameters=params )
Now, if you look in your working directory, you will find a checkpoint file
h2.nc
The state of the system after the last successful iteration step is then reloaded using the command
scf = restoreSelfConsistentCalculation('h2.nc')
The variable scf now contains the previously obtained
SCF-results and the calculation can be resumed using the NanoLanguage command
executeSelfConsistentCalculation().
In many situations, you often use a VNLFile object for storing your molecules, structures, and calculations. Data is added to the VNLFile using the method addToSample(). A typical scenario could for example be
vnl_file = VNLFile('h2.vnl') vnl_file.addToSample(h2, 'h2')
If you rerun a script that uses this construction, ATK will overwrite the existing sample in the VNLFile. To avoid this , you may either
choose a new name for the sample added in the call to the method addToSample().
physically rename or copy the VNLFile on the disk
A more elegant solution for making an automatic backup of a
VNLFile could be to define your own
class, say MyVNLFile, which inherits the VNLFile class. The check for an existing VNLFile can then be performed in the
__init__ constructor of MyVNLFile. Here is
a working implementation of MyVNLFile
class MyVNLFile(VNLFile): def __init__(self, filename, backup=False): # Check if VNL file exists if ( path.isfile(filename) and backup ): rename(filename,filename+'.bak') VNLFile.__init__(self,filename)
Here we have added an additional (optional) variable
backup to the constructor of MyVNLFile. The
VNLFile is only backed up to
when MyVNLFile is constructed like
vnl_file = MyVNLFile('h2.vnl', True) vnl_file.addToSample(h2, 'h2')
It is important to note that removing the old file is just one possible implementation. Alternatively, one could also have chosen to for example rename the old file. The complete example described above is shown here.
# Import the KohnSham module from ATK from ATK.KohnSham import * from os import path, rename class MyVNLFile(VNLFile): def __init__(self, filename, backup=False): # Check if VNL file exists if ( path.isfile(filename) and backup ): rename(filename,filename+'.bak') VNLFile.__init__(self,filename) # Set up elements and positions elm = [ Hydrogen, Hydrogen ] pos = [ ( +0.4, 0.0, 0.0)*Angstrom, ( -0.4, 0.0, 0.0)*Angstrom ] # Add them to a configuration h2 = MoleculeConfiguration(elm,pos) # Open a VNL file and add the molecule to it vnl_file = MyVNLFile('h2.vnl', True) vnl_file.addToSample(h2, 'h2')
Now a VNLFile named h2.vnl.bak should exist.
It is possible to specify an individual basis set for each atomic element in a given structure either by using the function basisSetParameters()
basis_set_params_Li = basisSetParameters( type = DoubleZetaPolarized, element = Lithium ) basis_set_params_H = basisSetParameters( type = SingleZetaPolarized, element = Hydrogen )
or by constructing a basis set variable for each element and using them in a list in the SCF-calculation method in the following way
dft_method = KohnShamMethod( [basis_set_params_Li, basis_set_params_H], exchange_correlation_type = LDA.PZ )
The SCF-calculation will now be performed using a SingleZetaPolarized and DoubleZetaPolarized basis set for hydrogen and lithium, respectively. Had we not explicitly specified those basis sets, ATK would have used the DoubleZeta basis set as default for both elements.
A calculated transmission spectrum can be visualized in VNL, but it is also possible to use other visualization tools.
The data points representing a calculated transmission spectrum can be extracted in the following way:
from ATK.TwoProbe import * old_scf_zero_bias = \ restoreSelfConsistentCalculation("old_scf_file.nc") temporary_list = range(-50,50,1) energy_list = [] for i in temporary_list: energy_list.append(i/10.0) trans_spectrum = calculateTransmissionSpectrum( old_scf_zero_bias, energies = energy_list*electronVolt ) trans_energy = trans_spectrum.energies() trans_coeff = trans_spectrum.coefficients() trans_energy = trans_energy.tolist() trans_energy.reverse() trans_coeff = trans_coeff.tolist() trans_coeff.reverse() while len(trans_energy)>0: print trans_energy.pop(),'\t',trans_coeff.pop()
The energy range and file name old_scf_file.nc filename should,
of course, be adapted to your specific case. This script will print the calculated
transmission coefficients and energies in two columns to the screen. The column
based output can then be used as input for your favorite visualization.
The trick is to use the methods energies() and
coefficients() of the TransmissionSpectrum object. These allow the extraction of
the values contained within the object into a NumPy array. We then turn these NumPy
arrays into simple lists using the
tolist() method of the NumPy array. The lists are then
reversed (using the reverse() method) which allows us to
extract values from the lists using the list method pop().
If you wish to store data in a VNLFile when executing ATK in parallel, some additional
precautions must be taken, since only the master process should be responsible for
handling VNLFile related I/O
tasks. This, however, can easily be controlled from within ATK by using the
Boolean function processIsMaster(), which is
included in the ATK module ATK.MPI. processIsMaster() returns True if
called by the master process and False otherwise. A very simple
in-line fix to handle VNLFile
I/O could therefore be
# Open a VNL file (MPI-safe) from ATK.MPI import * ... if ( processIsMaster() ): vnl_file = ParallelVNLFile('h2.vnl') vnl_file.addToSample(h2, 'h2')
A more elegant solution would be to define a new class
ParallelVNLFile which inherits the original VNLFile class. Both in the
__init__ constructor and in the user-defined implementation of
addToSample(), you will have to check whether
these methods are called by the master or a slave process. A possible implementation
of the class ParallelVNLFile is
class ParallelVNLFile(VNLFile): # New constructor with support for removing the VNLFile def __init__(self, filename, force=False): # Only master process should enter here if ( not processIsMaster() ): return # Check if VNL file exists if ( path.isfile(filename) ): # and remove it if force = True if (force): remove(filename) VNLFile.__init__(self, filename) # MPI-safe addToSample() def addToSample(self, sample, sample_name): # Only master process should enter here if ( not processIsMaster() ): return VNLFile.addToSample(self, sample, sample_name)
which then can be used in your ATK script as
vnl_file = ParallelVNLFile('h2.vnl', True) vnl_file.addToSample(h2, 'h2')
Observe that the ParallelVNLFile class also includes support
for overwriting a VNLFile. Consult Overwriting VNLFile samples for a discussion of when and why this functionality is
relevant.
Since NanoLanguage is a Python based scripting language it is easy to create extensions which will make your everyday life easier. A script is provided below which transforms the structural data contained in an XYZ-file into the VNLFile format.
from ATK.KohnSham import * ''' NanoLanguage XYZ import filters Description: 3 utility routines for specifying atom positions in XYZ format in NanoLanguage - xyzBlockToLists: Converts a sequence of lines with the format "element x y z" to two lists [elements,positions] - importXYZFile: Reads an XYZ file from disk and uses xyzBlockToLists to convert the atom positions to two lists [elements,positions] - importXYZFile: Same as above, only the XYZ file should not contain the two initial lines (number of atoms and comment). Very useful for copy/pasted atom lists! For more details, see each individual routine Notes: * At the moment, these routines can only read single-configuration xyz files. * If the xyz file contains more atoms than specified on line 1, the remaining ones will be ignored. * If the xyz file contains fewer atoms than specified on line 1, an error will be raised. * Errors will be raised on all format errors. * No units are attached to the positions; this is up to the user to handle. ''' def convert(line,types=()): ''' Utility routine used by xyzBlockToLists to parse a single line in the xyz format This is the place where we enforce the xyz format element x y z where element is an arbitrary string, and x y z are floats We allow an arbitrary amount of leading and trailing white spaces, plus any number and any kind of white spaces as separators ''' import re a = re.split('\s*',line.strip()) for i in range(len(types)): a[i] = apply(types[i],[a[i]]) return tuple(a) def xyzBlockToLists (xyz_block,number_of_atoms=None): ''' Converts a block (input argument "xyz_block") of the format element1 x1 y1 z1 element2 x2 y2 z2 ... elementN xN yN zN to two lists, one with the elements (in NanoLanguage format, i.e. as objects) and one with positions (without unit!), and returns these as [elements,positions] If N is given as input (input argument "number_of_atoms), the xyz block must contain at least this number of lines (additional ones will be ignored). If N is not given, all lines are converted. Strict checking of the xyz format is performed. This means, that only initial and/or trailing blank lines are allowed (internal blanks lines are not allowed in the xyz format), and any line not conforming to the xyz format will raise an error. This can easily be modified, to allow for e.g. comments (start line with #), as indicated in a commented-out piece of code below. ''' import re # Handle DOS files as well as Unix all_lines = re.split('\n',xyz_block.replace('\r','')) # Strip off all leading and trailing blank lines while all_lines[-1]=='': all_lines = all_lines[0:-1] while all_lines[0]=='': all_lines = all_lines[1:] # If number of atoms is not known, attempt to convert all lines if number_of_atoms==None: number_of_atoms = len(all_lines) # Otherwise, convert as many as requested # (first we check that there are these many lines in the block) elif len(all_lines)<number_of_atoms: raise Exception('Number of atoms is %i, but only %i lines found in the xyz block' % (number_of_atoms,len(all_lines))) # Count lines for reference in error messages # and to exit when the number of wanted lines is read line_ix = 0 # Store elements and positions elements = [] positions = [] for line in all_lines: line_ix = line_ix+1 # Read until we have the desired number of atoms if line_ix>number_of_atoms: break try: element,x,y,z = convert(line,(str,)+(float,)*3) positions.append([x,y,z]) except: raise Exception('Invalid format ("element x y z" expected) on line %i (%s)' % (line_ix,line)) try: elements.append(eval("PeriodicTable.%s" % element)) except: raise Exception('Invalid element on line %i (%s)' % (line_ix,line)) return (elements,positions) def importXYZFile (filename): ''' Read an xyz file from disk (file name given as "filename"), and convert it to two lists (returned) [elements,positions] by using xyzBlockToLists above. The file must correspond to the xyz format (right now, this can only handle single configuration files!), meaning N (number of atoms) comment element1 x1 y1 z1 element1 x2 y2 z2 ... elementN xN yN zN The comment is discarded. If the file contains more atoms than N, the additional ones will be ignored. No blank lines are allowed. ''' f = open(filename,'r') try: s = f.readline() number_of_atoms = int(s) except: raise Exception('Invalid format (expected an integer number) on line 1: %s' % s) try: comment = f.readline() except: raise Exception('Invalid format on line 2: File ended abruptly') try: xyz_block = f.read() except: raise Exception('Invalid format on line 3: File ended abruptly') return xyzBlockToLists(xyz_block,number_of_atoms) def importSimplifiedXYZFile (filename): ''' Read a "simplified" xyz file from disk (file name given as "filename"), and convert it to two lists (returned) [elements,positions] by using xyzBlockToLists above. The file must correspond to the xyz format (right now, this can only handle single configuration files!), but should NOT contain the initial two lines (number of atoms and comment) meaning element1 x1 y1 z1 element1 x2 y2 z2 ... elementN xN yN zN All lines are read; no blank lines are allowed. ''' f = open(filename,'r') xyz_block = f.read() if xyz_block=='': raise Exception('Invalid format on line 1: File ended abruptly') return xyzBlockToLists(xyz_block)
Now, suppose you have a file 3-octanol.xyz which contains the
chemical structure of 3-octanol in an XYZ-format as shown below:
27 3-octanol H 8.75072 -2.4733 2.54656 H 11.2604 -4.14431 3.82607 H 11.715 -5.8038 3.84934 H 11.6474 -4.95623 2.27507 C 11.1617 -5.07225 3.25095 H 9.33521 -5.55013 4.12477 H 9.62311 -6.44956 2.6051 O 9.21734 -3.19852 2.99613 C 9.69108 -5.46985 3.09219 H 9.29706 -4.35725 1.28281 C 8.91344 -4.40821 2.30845 H 7.16157 -4.71225 3.38853 H 7.20964 -5.67055 1.87836 H 6.99798 -2.66424 2.05897 C 7.40753 -4.68768 2.32166 H 6.90682 -3.59278 0.52793 C 6.63492 -3.58604 1.58988 H 5.00393 -3.82421 2.85921 H 4.78426 -4.66796 1.29681 H 4.87008 -1.67629 1.69415 C 5.12218 -3.74055 1.7737 H 4.51297 -2.47743 0.130426 C 4.37134 -2.52746 1.21631 H 2.72864 -2.48623 2.65878 H 2.41386 -3.49026 1.21167 C 2.88355 -2.56995 1.5779 H 2.35393 -1.6987 1.17539
This structure can easily be converted into a VNL file format using the following short NanoLanguage script:
from ATK.KohnSham import * # Import 3-octanol XYZ-file from import_xyz import * (new_vnl_elements,new_vnl_positions) = importXYZFile ('3-octanol.xyz') # Create NanoLanguage molecule new_vnl_molecule = MoleculeConfiguration( elements=new_vnl_elements, cartesian_coordinates=new_vnl_positions*Angstrom ) # Save in VNL file vnlfile = VNLFile('3-octanol.vnl') vnlfile.addToSample(new_vnl_molecule,'3-octanol')
Of course you should have downloaded the import_xyz.py file and
have it in the same directory as your other files. This procedure could be extended
to convert entire directories filled with structures in an XYZ-format into the VNL
file format. This way it is easy to convert all of your old structural files into a
format compatible with ATK and VNL.
The version of Python supplied with ATK includes the datetime
module which can be used for timing a calculation. Here, we will show you how this
can be done.
Suppose you want to perform a series of calculations and keep track of how long it takes to perform these calculations. Below is a script which returns an object which can display the time, at which the object was created.
def atkTimestamp(): ''' Create a timestamp string without the microseconds @return A datetime object which does not display microseconds as they have been set to zero. ''' import datetime current_time=datetime.datetime.now() current_time=current_time.replace(microsecond=0) return current_time
By placing the timestamp.py file in the same directory as our
ATK script we can input it and get access to the function
atkTimestamp. Say, you would like to time the
self-consistent calculation of a Hydrogen using different basis-sets.
from ATK.KohnSham import * from timestamp import atkTimestamp basis_sets = [ SingleZeta, SingleZetaPolarized, DoubleZeta, DoubleZetaPolarized, DoubleZetaDoublePolarized ] atoms = [Hydrogen]*2 positions = [(0.0,0.0,0.0),(0.0,0.0,0.802)]*Angstrom hydrogen_molecule = MoleculeConfiguration( elements=atoms, cartesian_coordinates=positions ) for basisset in basis_sets: basis_set_params = basisSetParameters( type=basisset, element=Hydrogen ) dft_method = KohnShamMethod( basis_set_parameters=basis_set_params ) start = atkTimestamp() print 'SCF calculation started at ', print start scf = executeSelfConsistentCalculation( method=dft_method, atomic_configuration=hydrogen_molecule ) end = atkTimestamp() print 'SCF calculation ended at ', print end print 'Calculation took: %s' % str(end-start) print 50*'-'
Here, we import our atkTimestamp() function, which will return
the time of the system in seconds as a datetime. Datetime
objects can be subtracted and added (which we do in the latter part of the for-loop. Running the above script should
result in output similar to what is shown below (with different dates and times of
course):
SCF calculation started at 2007-04-16 12:37:27 SCF calculation ended at 2007-04-16 12:37:30 Calculation took: 0:00:03 -------------------------------------------------- SCF calculation started at 2007-04-16 12:37:30 SCF calculation ended at 2007-04-16 12:37:34 Calculation took: 0:00:04 -------------------------------------------------- SCF calculation started at 2007-04-16 12:37:34 SCF calculation ended at 2007-04-16 12:37:38 Calculation took: 0:00:04 -------------------------------------------------- SCF calculation started at 2007-04-16 12:37:38 SCF calculation ended at 2007-04-16 12:37:44 Calculation took: 0:00:06 -------------------------------------------------- SCF calculation started at 2007-04-16 12:37:44 SCF calculation ended at 2007-04-16 12:37:51 Calculation took: 0:00:07 --------------------------------------------------
If you have a directory where you like to keep all your 'helper' scripts, you can add the path to this directory using the following construction:
import sys sys.append('<em class="replaceable"><code>/home/$username/python_library_functions/</code></em>')
If you place the above two lines at the top of your ATK script, you can easily
import anything in the /home/$username/python_library_functions/.
The convergence of a calculation for a two-probe system is not a trivial issue. "Simple" systems (such as the two semi-infinite linear chains of lithium atoms with a single hydrogen molecule positioned between the two electrode chains described in Li-H2-Li two-probe system) converge after slightly more than a dozen iterations. However, the distinction between "simple" and "complex" systems is not straightforward and cannot directly be connected with the dimensionality of the system in study. In fact, more "complex" systems exist which would require hundreds of iterations before convergence is achieved, according to the default choice of the tolerance and convergence criterion. In some special case, the convergence to the desired tolerance appears to be so slow that the quantity that is used to measure whether the convergence tolerance has been reached seems rather to oscillate indefinitely around a given value than becoming smaller and smaller.
In this section, we give some general tips on the choice of parameters and procedures, which may facilitate faster convergence of two probe systems that turn out to be challenging or slow to converge with default parameters.
The convergence behavior of a two-probe calculation can depend to a large
extent on the parameters which are set up for the calculation.
For several system, a speed up of the convergence can be achieved by
increasing the electron temperature.
As an example, the increase of the electron temperature from a value of
300 K to 1000-2000 K in the case of the spin polarized system Fe-MgO-Fe presented in
Spin-polarized two-probe system improves in a drastic way
the convergence behavior of this two-probe system.
The initialization of the self-consistent calculation for a two-probe system requires a guess for the initial real-space electron density. This can be taken to be as one of the two following choices:
Equivalent bulk: The initial density is derived from a self-consistent calculation for the equivalent bulk system of the two-probe system.
Neutral atom density: The initial density is obtained as a superposition of the atomic electron densities of the atoms in the two-probe system.
Since the release of ATK 2.3, the first choice is taken as default for both
systems where the left and right electrode parameters have been set-up identically
and for two-probe systems with heterogeneous electrodes. This choice has been
shown to improve (even drastically) the convergence behavior of a large number of
two-probe calculations. However, the memory usage in performing the calculation
for the equivalent bulk (including the central region and one unit cell of the
left and right electrodes) could be quite large. In order to make feasible
calculations for large systems which would require a very large memory usage (the
critical size being dependent on the available computer resources), the
initialization might be performed using the option neutral
atom density.
The following script shows how to explicitly perform a calculation using as initial
guess the neutral atom density
for a two-probe system with a Li-H2-Li set-up.
It requires the VNLFile
lih2li.vnl to exist.
from ATK.TwoProbe import * import ATK ATK.setVerbosityLevel(2) # Read the atomic configuration from an VNL file. vnl_file = VNLFile("lih2li.vnl") configurations = vnl_file.readAtomicConfigurations() two_probe_conf = configurations["lih2li"] # Reduce basis set size for Li basis_set_params = basisSetParameters( type = SingleZeta, element = Lithium ) # Define electrode method electrode_bz = brillouinZoneIntegrationParameters( (1,1,100) ) electrode_params = ElectrodeParameters( brillouin_zone_integration_parameters = electrode_bz ) # Initialize the electron density two_probe_params = twoProbeAlgorithmParameters( initial_density_type = InitialDensityType.NeutralAtom ) # Define the two-probe method twoprobe_method=TwoProbeMethod( electrode_parameters = (electrode_params, electrode_params), basis_set_parameters = basis_set_params, algorithm_parameters = two_probe_params ) # Specify checkpoint file runtime_params = runtimeParameters( checkpoint_filename = 'lih2li-scf-neutral-atom-density.nc' ) # Perform the self-consistent two-probe calculation scf = executeSelfConsistentCalculation( atomic_configuration = two_probe_conf, method = twoprobe_method, runtime_parameters = runtime_params )
Since the release of ATK 2.3, the default for the
self-consistent calculation of a two-probe system uses an
unconstrained algorithm which does
not impose any constraint on the real-space density of the system. This choice differs
from the one used in the ATK versions up to ATK 2.2, in which the real-space density
in the electrode region is constrained to be identical to the one of the
corresponding bulk. Even if the unconstrained algorithm is more theoretically
grounded than the previous one, in some very special case the convergence of the two-probe
calculation could be affected by some problems like, e.g. a slow convergence behavior
or a tendency to loose electronic charge in the central region. If this happens,
the following procedure might be used to stabilize the calculation.
Step 1 - Constrained run: Perform a
self-consistent constrained calculation keeping fixed the real-space density
in the electrode regions by setting the parameter
electrode_constraint
to ElectrodeConstraints.RealSpaceDensity.
Step 2 - Unconstrained run: Use the result of the previous calculation (Step 1) for starting an unconstrained calculation.
The following script shows how to apply this procedure
to a two-probe system with a Li-H2-Li set-up.
It requires the VNLFile
lih2li.vnl to exist.
from ATK.TwoProbe import * import ATK ATK.setVerbosityLevel(2) # Restore old two-probe configuration vnl_file = VNLFile("lih2li.vnl") configurations = vnl_file.readAtomicConfigurations() two_probe_conf = configurations["lih2li"] # Reduce basis set size for Li basis_set_params = basisSetParameters( type = SingleZeta, element = Lithium ) # Define electrode method electrode_bz = brillouinZoneIntegrationParameters( (1,1,100) ) electrode_params = ElectrodeParameters( brillouin_zone_integration_parameters = electrode_bz ) # Define methods for constrained algorithm constrained_algorithm = twoProbeAlgorithmParameters( electrode_constraint = ElectrodeConstraints.RealSpaceDensity ) constrained_method = TwoProbeMethod( electrode_parameters = (electrode_params,electrode_params), basis_set_parameters = basis_set_params, algorithm_parameters = constrained_algorithm ) # Execute self-consistent constrained calculation constrained_scf = constrained_method.apply(two_probe_conf) # Define methods for unconstrained algorithm (default algorithm) unconstrained_algorithm = twoProbeAlgorithmParameters( electrode_constraint = ElectrodeConstraints.Off ) unconstrained_method = TwoProbeMethod( electrode_parameters = (electrode_params,electrode_params), basis_set_parameters = basis_set_params, algorithm_parameters = unconstrained_algorithm ) # Specify checkpoint file runtime_params = runtimeParameters( checkpoint_filename = 'lih2li-scf-constrained-unconstrained.nc' ) # Execute self-consistent unconstrained calculation unconstrained_scf = executeSelfConsistentCalculation( atomic_configuration = two_probe_conf, method = unconstrained_method, runtime_parameters = runtime_params, initial_calculation = constrained_scf )
Some light-element atom could be difficult to converge using the default parameters set by ATK. In this section, we give some general suggestions for improving the convergence of isolated atoms. These general tips on the choice of parameters and procedures can be used to speed up and enhance the convergence for single atom systems.
Choosing the correct electronic temperature
Atomic and molecular systems have discrete electronic states. In this case, it is physically more correct to use a low electronic temperature, which, at the same time, could improve the convergence behavior.
Fully spin-polarized systems
For fully spin polarized systems, as in the case of the Hydrogen atom, it
could be useful for improving the convergence, to set the initial scaled
spin to 0.9, instead of using the value corresponding to the full
polarized configuration (intial_scaled_spin equals to 1).
Choice of the basis set
When performing calculations on single atoms, the usage of a large basis set is strongly suggested. We recommend the use of basis set not smaller than the default Double Zeta Polarized.
Using density matrix mixing instead of Hamiltonian mixing
The default algorithm for the mixing during the self-consistent iteration is based
on mixing the Hamiltonian
(IterationMixing.Hamiltonian). However, experience has
shown that for light atoms it could be useful to mix the density matrix
(IterationMixing.DensityMatrix) instead. Furthermore,
increasing the diagonal mixing parameter
(diagonal_mixing_parameter) could speed up the
convergence.