Source code for iDEA.input
""" Stores input parameters for iDEA calculations.
"""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
from six import string_types
import numpy as np
import importlib
import os
import sys
import copy
import time
from . import results as rs
[docs]class SpaceGrid(object):
"""Stores basic real space arrays
These arrays should be helpful in many types of iDEA calculations.
Storing them in the Input object avoids having to recompute them
and reduces code duplication.
"""
[docs] def __init__(self, pm):
self.npt = pm.sys.grid
self.delta = pm.sys.deltax
self.grid = np.linspace(-pm.sys.xmax, pm.sys.xmax, pm.sys.grid)
self.v_ext = np.zeros(self.npt, dtype=np.float)
for i in range(self.npt):
self.v_ext[i] = pm.sys.v_ext(self.grid[i])
self.v_pert = np.zeros(self.npt, dtype=np.float)
if(pm.sys.im == 1):
self.v_pert = self.v_pert.astype(np.cfloat)
for i in range(self.npt):
self.v_pert[i] = pm.sys.v_pert(self.grid[i])
self.v_int = np.zeros((pm.sys.grid,pm.sys.grid),dtype='float')
for i in range(pm.sys.grid):
for k in range(pm.sys.grid):
self.v_int[i,k] = pm.sys.interaction_strength/(abs(self.grid[i]-self.grid[k])+pm.sys.acon)
stencil_first_derivative = pm.re.stencil
if stencil_first_derivative == 5:
self.first_derivative = 1.0/12 * np.array([1,-8,0,8,-1], dtype=np.float) / self.delta
self.first_derivative_indices = [-2,-1,0,1,2]
self.first_derivative_band = 1.0/12 * np.array([0,-8,1], dtype=np.float) / self.delta
elif stencil_first_derivative == 7:
self.first_derivative = 1.0/60 * np.array([-1,9,-45,0,45,-9,1], dtype=np.float) / self.delta
self.first_derivative_indices = [-3,-2,-1,0,1,2,3]
self.first_derivative_band = 1.0/60 * np.array([0,-45,9,-1], dtype=np.float) / self.delta
else:
raise ValueError("re.stencil = {} not implemented. Please select 5 or 7.".format(stencil_first_derivative))
stencil_second_derivative = pm.sys.stencil
if stencil_second_derivative == 3:
self.second_derivative = np.array([1,-2,1], dtype=np.float) / self.delta**2
self.second_derivative_indices = [-1,0,1]
self.second_derivative_band = np.array([-2,1], dtype=np.float) / self.delta**2
elif stencil_second_derivative == 5:
self.second_derivative = 1.0/12 * np.array([-1,16,-30,16,-1], dtype=np.float) / self.delta**2
self.second_derivative_indices = [-2,-1,0,1,2]
self.second_derivative_band = 1.0/12 * np.array([-30,16,-1], dtype=np.float) / self.delta**2
elif stencil_second_derivative == 7:
self.second_derivative = 1.0/180 * np.array([2,-27,270,-490,270,-27,2], dtype=np.float) / self.delta**2
self.second_derivative_indices = [-3,-2,-1,0,1,2,3]
self.second_derivative_band = 1.0/180 * np.array([-490,270,-27,2], dtype=np.float) / self.delta**2
else:
raise ValueError("sys.stencil = {} not implemented. Please select 3, 5 or 7.".format(stencil_second_derivative))
[docs] def __str__(self):
"""Print variables of section and their values"""
s = ""
v = vars(self)
for key,value in v.items():
s += input_string(key, value)
return s
[docs]def input_string(key,value):
"""Prints a line of the input file"""
if isinstance(value, string_types):
s = "{} = '{}'\n".format(key, value)
else:
s = "{} = {}\n".format(key, value)
return s
[docs]class InputSection(object):
"""Generic section of input file"""
[docs] def __str__(self):
"""Print variables of section and their values"""
s = ""
v = vars(self)
for key,value in v.items():
s += input_string(key, value)
return s
[docs]class SystemSection(InputSection):
"""System section of input file
Includes some derived quantities.
"""
@property
def deltax(self):
"""Spacing of real space grid"""
return 2.0*self.xmax/(self.grid-1)
@property
def deltat(self):
"""Spacing of temporal grid"""
return 1.0*self.tmax/(self.imax-1)
@property
def grid_points(self):
"""Real space grid"""
return np.linspace(-self.xmax,self.xmax,self.grid)
[docs]class Input(object):
"""Stores variables of input parameters file
Includes automatic generation of dependent variables,
checking of input parameters, printing back to file and more.
"""
priority_dict = {
'low': 2,
'default': 1,
'high': 0}
[docs] def __init__(self):
"""Sets default values of some properties."""
self.filename = ''
self.log = ''
self.last_print = time.process_time()
### Run parameters
self.run = InputSection()
run = self.run
run.name = 'run_name' #: Name to identify run. Note: Do not use spaces or any special characters (.~[]{}<>?/\)
run.time_dependence = False #: Run time-dependent calculation
run.verbosity = 'default' #: Output verbosity ('low', 'default', 'high')
run.save = True #: Save results to disk when they are generated
run.module = 'iDEA' #: Specify alternative folder (in this directory) containing modified iDEA module
run.NON = False #: Run Non-Interacting approximation
run.LDA = False #: Run LDA approximation
run.HF = False #: Run Hartree-Fock approximation
run.HYB = False #: Run Hybrid (HF-LDA) calculation
run.EXT = False #: Run Exact Many-Body calculation
### System parameters
self.sys = SystemSection()
sys = self.sys
sys.NE = 2 #: Number of electrons
sys.grid = 201 #: Number of grid points (must be odd)
sys.stencil = 3 #: Discretisation of 2nd derivative (3 or 5 or 7).
sys.xmax = 10.0 #: Size of the system
sys.tmax = 1.0 #: Total real time
sys.imax = 1001 #: Number of real time iterations (NB: deltat = tmax/(imax-1))
sys.acon = 1.0 #: Smoothing of the Coloumb interaction
sys.interaction_strength = 1.0 #: Scales the strength of the Coulomb interaction
sys.im = 0 #: Are there imaginary terms in the perturbing potential? (0: no, 1: yes)
def v_ext(x):
"""Ground-state external potential
"""
return 0.5*(0.25**2)*(x**2)
sys.v_ext = v_ext
def v_pert(x):
"""Perturbing potential (switched on at t=0)
"""
return -0.01*x
sys.v_pert = v_pert
### Exact parameters
self.ext = InputSection()
ext = self.ext
ext.itol = 1e-12 #: Tolerance of imaginary time propagation (Recommended: 1e-12)
ext.itol_solver = 1e-14 #: Tolerance of linear solver in imaginary time propagation (Recommended: 1e-14)
ext.rtol_solver = 1e-12 #: Tolerance of linear solver in real time propagation (Recommended: 1e-12)
ext.itmax = 2000.0 #: Total imaginary time
ext.iimax = 1e5 #: Imaginary time iterations
ext.ideltat = ext.itmax/ext.iimax #: Imaginary time step (DERIVED)
ext.RE = False #: Reverse engineer many-body density
ext.psi_gs = False #: Save the reduced ground-state wavefunction to file
ext.initial_gspsi = 'qho' #: Initial ground-state wavefunction ('qho' by default. 'non' can be selected.
#: 'hf', 'lda1', 'lda2', 'lda3', 'ldaheg' or 'ext' can be selected if the orbitals/wavefunction
#: are available. An ext wavefunction from another run can be used, but specify the run.name
#: instead e.g. 'run_name').
#: WARNING: If no reliable starting guess can be provided e.g. wrong number of electrons per well,
#: then choose 'qho' - this will ensure stable convergence to the true ground-state.)
### Non-interacting approximation parameters
self.non = InputSection()
non = self.non
non.rtol_solver = 1e-13 #: Tolerance of linear solver in real time propagation (Recommended: 1e-13)
non.RE = False #: Reverse-engineer non-interacting density
### LDA parameters
self.lda = InputSection()
lda = self.lda
lda.NE = 'heg' #: Number of electrons used in construction of the LDA (1, 2, 3 or 'heg')
lda.scf_type = 'pulay' #: How to perform scf ('pulay', 'linear', 'cg', 'mixh', 'none')
lda.mix = 0.2 #: Mixing parameter for linear & Pulay mixing (float in [0,1])
lda.pulay_order = 20 #: Length of history for Pulay mixing (max: lda.max_iter)
lda.pulay_preconditioner = None #: Preconditioner for pulay mixing (None, 'kerker', rpa')
lda.kerker_length = 0.5 #: Length over which density fluctuations are screened (Kerker only)
lda.tol = 1e-12 #: Convergence tolerance in the density
lda.etol = 1e-12 #: Convergence tolerance in the energy
lda.max_iter = 10000 #: Maximum number of self-consistency iterations
lda.RE = False #: Reverse-engineer LDA density
### HF parameters
self.hf = InputSection()
hf = self.hf
hf.fock = 1 #: Include Fock term (0 = Hartree approximation, 1 = Hartree-Fock approximation)
hf.con = 1e-12 #: Tolerance
hf.nu = 0.9 #: Mixing term
hf.RE = False #: Reverse-engineer hf density
### HYB parameters
self.hyb = InputSection()
hyb = self.hyb
hyb.seperate = False #: Seperate Vx and Vc in the hybrid (False: a*F + (1-a)Vxc, True: a*F + (1-a)Vx + Vc)
hyb.functionality = 'o' #: Functionality of hybrid functionals: 'o' for optimal alpha, 'f' for fractional numbers of electrons,
#: 'a' for single alpha run
hyb.of_array = (0.5,1.0,6) #: If finding optimal alpa, this defines an array going from a->b in c steps whose energies are used for
#: optimisation. If fractional run, this defines the numbers of electrons to calculate
hyb.alpha = 1.0 #: If single alpha run, this defines the alpha
hyb.mix = 0.5 #: Mixing parameter for linear mixing (float in [0,1])
hyb.tol = 1e-12 #: Convergence tolerance in the density
hyb.max_iter = 10000 #: Maximum number of self-consistency iterations
hyb.RE = False #: Calculate the external potential for the HYB density
### RE parameters
self.re = InputSection()
re = self.re
re.stencil = 5 #: Discretisation of 1st derivative (5 or 7)
re.mu = 1.0 #: 1st convergence parameter in the ground-state reverse-engineering algorithm
re.p = 0.05 #: 2nd convergence parameter in the ground-state reverse-engineering algorithm
re.gs_density_tolerance = 1e-12 #: Tolerance of the error in the ground-state density
re.starting_guess = 'extre' #: Starting guess of groud-state Vks (if not available will start with Vxt)
re.nu = 1.0 #: 1st convergence parameter in the time-dependent reverse-engineering algorithm
re.a = 1.0e-6 #: 2nd convergence parameter in the time-dependent reverse-engineering algorithm
re.rtol_solver = 1e-12 #: Tolerance of linear solver in real time propagation (Recommended: 1e-12)
re.td_density_tolerance = 1e-7 #: Tolerance of the error in the time-dependent density
re.cdensity_tolerance = 1e-7 #: Tolerance of the error in the current density
re.max_iterations = 20 #: Maximum number of iterations per time step to find the Kohn-Sham potential
re.damping = True #: Damping term used to filter out the noise in the time-dependent Kohn-Sham vector potential
re.filter_beta = 1.8 #: 1st parameter in the damping term
[docs] def check(self):
"""Checks validity of input parameters."""
pm = self
if pm.run.module != 'iDEA':
raise ValueError("run.module must be set to 'iDEA' (dynamic loading of modules disabled in PYPI version)")
# Time-dependence
if pm.run.time_dependence == True:
if pm.run.HYB == True:
self.sprint('HYB: Warning - time-dependence not implemented!')
if (pm.ext.RE or pm.non.RE or pm.lda.RE or pm.hf.RE or pm.hyb.RE):
self.sprint('RE: Warning - time-dependence not implemented!')
# EXT
if pm.run.EXT == True:
if pm.ext.itol > 1e-6:
self.sprint('EXT: Warning - value of ext.itol is much larger than 1e-13, this can yeild poor KS potentials')
# LDA
if pm.run.LDA == True:
if pm.lda.scf_type not in [None, 'pulay', 'linear', 'cg', 'mixh']:
raise ValueError("LDA: Warning - lda.scf_type must be None, 'linear', 'pulay' or 'cg'")
if pm.lda.pulay_preconditioner not in [None, 'kerker', 'rpa']:
raise ValueError("LDA: Warning - lda.pulay_preconditioner must be None, 'kerker' or 'rpa'")
# HF
if pm.run.HF == True:
if (pm.hf.fock != 1 and pm.hf.fock != 0):
raise ValueError('HF: Error - hf.fock must be set to 0 or 1.')
if (pm.hf.nu > 1 or pm.hf.nu < 0):
self.sprint('HF: Warning - Value of nu should be between 0 and 1')
# HYB
if pm.run.HYB == True:
if (pm.hyb.alpha > 1 or pm.hyb.alpha < 0):
self.sprint('HYB: Warning - Value of alpha should be between 0 and 1')
if (pm.hyb.mix > 1 or pm.hyb.mix < 0):
self.sprint('HYB: Warning - Value of mix should be between 0 and 1')
[docs] def __str__(self):
"""Prints different sections in input file"""
s = ""
v = vars(self)
for key, value in v.items():
if isinstance(value, InputSection):
s += "### {} section\n".format(key)
s += "{}\n".format(value)
else:
s += input_string(key,value)
return s
[docs] def sprint(self, string='', priority=1, newline=True, refresh=0.000005, savelog=True):
"""Customized print function
Prints to screen and appends to log.
If newline == False, overwrites last line,
but refreshes only every refresh seconds.
parameters
----------
string : string
string to be printed
priority: int
priority of message, possible values are
0: debug
1: normal
2: important
newline : bool
If False, overwrite the last line
refresh : float
If newline == False, print only every "refresh" seconds
savelog : bool
If True, save string to log file
"""
verbosity = self.run.verbosity
if(savelog):
self.log += string + '\n'
if priority >= self.priority_dict[verbosity]:
timestamp = time.process_time()
if newline:
print(string)
self.last_print = timestamp
# When overwriting lines, we only print every "refresh" seconds
elif timestamp - self.last_print > refresh:
## this only overwrites, no erase
#print('\r' + string, end='')
# Overwrite line
sys.stdout.write('\r' + string)
# Delete rest of line starting from cursor position (in case
# previous line was longer). See
# https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes
sys.stdout.write(chr(27) + '[K')
sys.stdout.flush()
self.last_print = timestamp
else:
pass
[docs] @classmethod
def from_python_file(cls,filename):
"""Create Input from Python script."""
tmp = Input()
tmp.read_from_python_file(filename)
return tmp
[docs] def read_from_python_file(self,filename):
"""Update Input from Python script."""
if not os.path.isfile(filename):
raise IOError("Could not find input file '{}'".format(filename))
module, ext = os.path.splitext(filename)
if ext != ".py":
raise IOError("Input file '{}' does not have .py extension.".format(filename))
# import module into object
pm = importlib.import_module(module)
# Replace default member variables with those from parameters file.
# The following recursive approach is adapted from
# See http://stackoverflow.com/questions/3232943/update-value-of-a-nested-dictionary-of-varying-depth
def update(d, u, l=1):
for k, v in u.items():
# We need to step into InputSection objects, as those may have varying
# numbers of parameters defined.
if isinstance(v, InputSection):
r = update(d.get(k, {}).__dict__, v.__dict__, l+1)
d[k].__dict__ = r
#d[k] = r
# We only want to copy contents of the input sections
# No need to copy any of the builtin attributes added
elif l > 1:
d[k] = u[k]
return d
self.__dict__ = update(self.__dict__, pm.__dict__)
self.filename = filename
##########################################
####### Here add derived parameters ####
##########################################
@property
def output_dir(self):
"""Returns full path to output directory
"""
return 'outputs/{}'.format(self.run.name)
##########################################
####### Running the input file ####
##########################################
[docs] def make_dirs(self):
"""Set up ouput directory structure"""
import os
import shutil
import errno
pm = self
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc:
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else: raise
output_dirs = ['data', 'raw', 'plots', 'animations']
for d in output_dirs:
path = '{}/{}'.format(pm.output_dir,d)
mkdir_p(path)
setattr(pm,d,path)
# Copy parameters file to output folder, if there is one
if os.path.isfile(pm.filename):
shutil.copy2(pm.filename,pm.output_dir)
# Copy ViDEO file to output folder
vfile = 'scripts/ViDEO.py'
if os.path.isfile(vfile):
# Note: this doesn't work, when using iDEA as a system module
shutil.copy2('scripts/ViDEO.py',pm.output_dir)
else:
pass
# No longer needed as ViDEO.py is in scrips directory and can be added to PATH
#s = "Warning: Unable to copy ViDEO.py since running iDEA as python module."
#s += " Simply add the scripts folder to your PATH variable to use ViDEO.py anywhere"
#pm.sprint(s,1)
[docs] def setup_space(self):
"""Prepares for performing calculations
precomputes quantities on grids, etc.
"""
self.space = SpaceGrid(self)
[docs] def execute(self):
"""Run this job"""
pm = self
pm.check()
pm.setup_space()
if pm.run.save:
pm.make_dirs()
self.results = rs.Results()
# Draw splash to screen
from . import splash
splash.draw(pm)
pm.sprint('run name: {}'.format(pm.run.name),1)
results = pm.results
# Execute required jobs
if(pm.run.NON == True):
from . import NON
results.add(NON.main(pm), name='non')
if(pm.non.RE == True):
from . import RE
results.add(RE.main(pm,'non'), name='nonre')
if(pm.run.LDA == True):
from . import LDA
results.add(LDA.main(pm), name='lda')
if(pm.run.HF == True):
from . import HF
results.add(HF.main(pm), name='hf')
if(pm.hf.RE == True):
from . import RE
results.add(RE.main(pm,'hf'), name='hfre')
if(pm.sys.NE == 1):
if(pm.run.EXT == True):
from . import EXT1
results.add(EXT1.main(pm), name='ext')
if(pm.ext.RE == True):
from . import RE
results.add(RE.main(pm,'ext'), name='extre')
elif(pm.sys.NE == 2):
if(pm.run.EXT == True):
from . import EXT2
results.add(EXT2.main(pm), name='ext')
if(pm.ext.RE == True):
from . import RE
results.add(RE.main(pm,'ext'), name='extre')
elif(pm.sys.NE == 3):
if(pm.run.EXT == True):
from . import EXT3
results.add(EXT3.main(pm), name='ext')
if(pm.ext.RE == True):
from . import RE
results.add(RE.main(pm,'ext'), name='extre')
elif(pm.sys.NE >= 4):
if(pm.run.EXT == True):
print('EXT: cannot run exact with more than 3 electrons')
if(pm.run.HYB == True):
from . import HYB
results.add(HYB.main(pm), name='hyb')
if(pm.hyb.RE == True):
from . import RE
results.add(RE.main(pm,'hyb{}'.format(pm.hyb.alpha).replace('.','_')), name='hybre')
# All jobs done
if pm.run.save:
# store log in file
f = open(pm.output_dir + '/iDEA.log', 'w')
f.write(pm.log)
f.close()
# need to get rid of nested functions as they can't be pickled
tmp = copy.deepcopy(pm)
del tmp.sys.v_ext
del tmp.sys.v_pert
# store pickled version of parameters object
import pickle
f = open(pm.output_dir + '/parameters.p', 'wb')
pickle.dump(tmp, f, protocol=4)
f.close()
del tmp
results.log = pm.log
pm.log = '' # avoid appending, when pm is run again
string = 'all jobs done \n'
pm.sprint(string,1)
return results