"""Calculates the exact Kohn-Sham potential and exchange-correlation potential for a given electron
density using the reverse-engineering algorithm. This works for both a ground-state and time-dependent
density.
"""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import
import math
import numpy as np
import scipy.sparse as sps
import scipy.linalg as spla
import scipy.sparse.linalg as spsla
from scipy.optimize import curve_fit
from . import RE_cython
from . import results as rs
[docs]def read_density(pm, approx):
r"""Reads in the electron density that was calculated using the selected approximation.
parameters
----------
pm : objectmath
Parameters object
approx : string
The approximation used to calculate the electron density
returns array_like
2D array of the ground-state/time-dependent electron density from the approximation,
indexed as density_approx[time_index,space_index]
"""
if(pm.run.time_dependence):
density_approx = np.zeros((pm.sys.imax,pm.space.npt), dtype=np.float)
name = 'td_{}_den'.format(approx)
density_approx[:,:] = rs.Results.read(name, pm)
else:
density_approx = np.zeros((1,pm.space.npt), dtype=np.float)
name = 'gs_{}_den'.format(approx)
density_approx[0,:] = rs.Results.read(name, pm)
return density_approx
[docs]def read_current_density(pm, approx):
r"""Reads in the electron current density that was calculated using the selected approximation.
parameters
----------
pm : object
Parameters object
approx : string
The approximation used to calculate the electron current density
returns array_like
2D array of the electron current density from the approximation, indexed as
current_density_approx[time_index,space_index]
"""
current_density_approx = np.zeros((pm.sys.imax,pm.space.npt), dtype=np.float)
name = 'td_{}_cur'.format(approx)
current_density_approx[:,:] = rs.Results.read(name, pm)
return current_density_approx
[docs]def construct_K(pm):
r"""Stores the band elements of the kinetic energy matrix in lower form. The kinetic energy matrix
is constructed using a three-point, five-point or seven-point stencil. This yields an NxN band
matrix (where N is the number of grid points). For example with N=6 and a three-point stencil:
.. math::
K = -\frac{1}{2} \frac{d^2}{dx^2}=
-\frac{1}{2} \begin{pmatrix}
-2 & 1 & 0 & 0 & 0 & 0 \\
1 & -2 & 1 & 0 & 0 & 0 \\
0 & 1 & -2 & 1 & 0 & 0 \\
0 & 0 & 1 & -2 & 1 & 0 \\
0 & 0 & 0 & 1 & -2 & 1 \\
0 & 0 & 0 & 0 & 1 & -2
\end{pmatrix}
\frac{1}{\delta x^2}
= [\frac{1}{\delta x^2},-\frac{1}{2 \delta x^2}]
parameters
----------
pm : object
Parameters object
returns array_like
2D array containing the band elements of the kinetic energy matrix, indexed as
K[band,space_index]
"""
# Stencil to use
sd = pm.space.second_derivative_band
nbnd = len(sd)
# Band elements
K = np.zeros((nbnd, pm.space.npt), dtype=np.float)
for i in range(nbnd):
K[i,:] = -0.5 * sd[i]
return K
[docs]def construct_momentum(pm):
r"""Stores the band elements of the momentum matrix in lower form.
The momentum matrix is constructed using a five-point or seven-point
stencil. This yields an NxN band matrix (where N is the number of grid
points). For example with N=6 and a five-point stencil:
.. math::
p = -i \frac{d}{dx}=
-\frac{i}{12} \begin{pmatrix}
0 & 8 & -1 & 0 & 0 & 0 \\
-8 & 0 & 8 & -1 & 0 & 0 \\
1 & -8 & 0 & 8 & -1 & 0 \\
0 & 1 & -8 & 0 & 8 & -1 \\
0 & 0 & 1 & -8 & 0 & 8 \\
0 & 0 & 0 & 1 & -8 & 0
\end{pmatrix}
\frac{1}{\delta x}
= \frac{1}{\delta x} \bigg[0,\frac{2}{3},-\frac{1}{12}\bigg]
parameters
----------
pm : object
Parameters object
returns array_like
1D array of the lower band elements of the momentum matrix, index as
momentum[band]
"""
fd = pm.space.first_derivative_band
nbnd = len(fd)
momentum = np.zeros(nbnd, dtype=np.cfloat)
for i in range(nbnd):
momentum[i] = -1.0j * fd[i]
return momentum
[docs]def construct_damping(pm):
r"""Stores the damping function which is used to filter out noise in the
Kohn-Sham vector potential.
.. math::
f_{\mathrm{damping}}(x) = e^{-10^{-12}(\beta x)^{\sigma}}
parameters
----------
pm : object
Parameters object
returns array_like
1D array of the damping function used to filter out noise, indexed as
damping[frequency_index]
"""
# Only valid for x>=0
damping_length = int((pm.space.npt+1)/2)
damping = np.zeros(damping_length, dtype=np.float)
# Damping term
for j in range(damping_length):
x = j*pm.space.delta
damping[j] = np.exp(-pm.re.filter_beta*x**2)
return damping
[docs]def construct_A_initial(pm, K, v_ks):
r"""Constructs the sparse matrix A at t=0, once the external perturbation
has been applied.
.. math::
A_{\mathrm{initial}} = I + i\dfrac{\delta t}{2}H(t=0) \\
H(t=0) = K + V_{\mathrm{KS}}(t=0)
parameters
----------
pm : object
Parameters object
K : array_like
2D array of the kinetic energy matrix, index as
K[band,space_index]
v_ks : array_like
1D array of the ground-state Kohn-Sham potential + the external
perturbation, indexed as v_ks[space_index]
returns sparse_matrix
The sparse matrix A at t=0, A_initial, used when solving the equation
Ax=b
"""
A_initial = np.zeros((pm.space.npt,pm.space.npt), dtype=np.cfloat)
nbnd = K.shape[0]
prefactor = 0.5j*pm.sys.deltat
# Assign the main-diagonal elements
for j in range(pm.space.npt):
A_initial[j,j] = prefactor*(K[0,j] + v_ks[j])
# Assign the off-diagonal elements
for k in range(1, nbnd):
if((j+k) < pm.space.npt):
A_initial[j,j+k] = prefactor*K[k,j]
A_initial[j+k,j] = prefactor*K[k,j]
# Add the identity matrix
A_initial += sps.identity(pm.space.npt, dtype=np.cfloat)
return A_initial
[docs]def construct_A(pm, A_initial, A_ks, momentum):
r"""Constructs the sparse matrix A at time t.
.. math::
A = I + i\dfrac{\delta t}{2}H \\
H = \frac{1}{2}(p-A_{\mathrm{KS}})^{2} + V_{\mathrm{KS}}(t=0) \\
= K + V_{\mathrm{KS}}(t=0) + \frac{A_{\mathrm{KS}}^{2}}{2} -
\frac{pA_{\mathrm{KS}}}{2} - \frac{A_{\mathrm{KS}}p}{2} \\
= H(t=0) + \frac{A_{\mathrm{KS}}^{2}}{2} - \frac{pA_{\mathrm{KS}}}{2}
- \frac{A_{\mathrm{KS}}p}{2}
parameters
----------
pm : object
Parameters object
A_initial : sparse_matrix
The sparse matrix A at t=0
A_ks : array_like
1D array of the time-dependent Kohn-Sham vector potential, at time t,
indexed as A_ks[space_index]
momentum : array_like
1D array of the lower band elements of the momentum matrix, index as
momentum[band,space_index]
returns sparse_matrix
The sparse matrix A at time t
"""
A = np.copy(A_initial)
nbnd = momentum.shape[0]
prefactor = 0.25j*pm.sys.deltat
# Assign the main diagonal elements of the matrix
for j in range(pm.space.npt):
A[j,j] += prefactor*(A_ks[j]**2)
# Assign the off-diagonal elements
for k in range(1, nbnd):
if((j+k) < pm.space.npt):
A[j+k,j] -= prefactor*(momentum[k]*A_ks[j])
A[j,j+k] += prefactor*(A_ks[j]*momentum[k])
if((j-k) >= 0):
A[j-k,j] += prefactor*(momentum[k]*A_ks[j])
A[j,j-k] -= prefactor*(A_ks[j]*momentum[k])
# Convert to compressed sparse column (csc) format for efficient arithmetic
A = sps.csc_matrix(A)
return A
[docs]def calculate_ground_state(pm, approx, density_approx, v_ext, K):
r"""Calculates the exact ground-state Kohn-Sham potential by solving the
ground-state Kohn-Sham equations and iteratively correcting v_ks. The
exact ground-state Kohn-Sham eigenfunctions, eigenenergies and electron
density are then calculated.
.. math::
V_{\mathrm{KS}}(x) \rightarrow V_{\mathrm{KS}}(x) +
\mu[n_{\mathrm{KS}}^{p}(x)-n_{\mathrm{approx}}^{p}(x)]
parameters
----------
pm : object
Parameters object
approx : string
The approximation used to calculate the electron density
density_approx : array_like
1D array of the ground-state electron density from the approximation,
indexed as density_approx[space_index]
v_ext : array_like
1D array of the unperturbed external potential, indexed as
v_ext[space_index]
K : array_like
2D array of the kinetic energy matrix, index as
K[band,space_index]
returns array_like and array_like and array_like and array_like and Boolean
1D array of the ground-state Kohn-Sham potential, indexed as
v_ks[space_index]. 1D array of the ground-state Kohn-Sham electron
density, indexed as density_ks[space_index]. 2D array of the
ground-state Kohn-Sham eigenfunctions, indexed as
wavefunctions_ks[space_index,eigenfunction]. 1D array containing the
ground-state Kohn-Sham eigenenergies, indexed as
energies_ks[eigenenergies]. Boolean - True if file containing exact
Kohn-Sham potential is found, False if file is not found.
"""
# Read in v_ks or take the external potential as the initial guess
string = 'RE: calculating the ground-state Kohn-Sham potential for the' + \
' {} density'.format(approx)
pm.sprint(string, 1, newline=True)
v_ks = np.zeros(pm.space.npt, dtype=np.float)
try:
# Using the exact Vks as a starting guess results in much quicker
# convergence in most systems.
name = 'gs_{}_vks'.format(pm.re.starting_guess)
v_ks[:] = rs.Results.read(name, pm)
string = ' (found {} ground-state Kohn-Sham potential to start from)'.format(pm.re.starting_guess)
pm.sprint(string, 1, newline=True)
file_exist = True
except:
string = ' (starting from the external potential)'
pm.sprint(string, 1, newline=True)
v_ks[:] += v_ext[:]
file_exist = False
# Construct the Hamiltonian matrix for the initial v_ks and solve the
# ground-state Kohn-Sham equations
hamiltonian = np.copy(K)
hamiltonian[0,:] += v_ks[:]
wavefunctions_ks, energies_ks, density_ks = solve_gsks_equations(
pm, hamiltonian)
density_difference = abs(density_approx-density_ks)
density_error = np.sum(density_difference)*pm.space.delta
string = 'RE: initial guess density error = {}'.format(density_error)
pm.sprint(string, 1, newline=True)
# Solve the ground-state Kohn-Sham equations and iteratively correct v_ks
iterations = 0
mu = pm.re.mu
error_change = 1
while(error_change > 1e-8 and density_error > pm.re.gs_density_tolerance):
# Save the last iteration
density_error_old = density_error
# Iteratively correct v_ks within the Hamiltonian matrix
hamiltonian[0,:] += mu*(density_ks[:]**pm.re.p -
density_approx[:]**pm.re.p)
# Solve the ground-state Kohn-Sham equations
wavefunctions_ks, energies_ks, density_ks = solve_gsks_equations(pm,
hamiltonian)
# Calculate the error in the ground-state Kohn-Sham density
density_difference = abs(density_approx-density_ks)
density_error = np.sum(density_difference)*pm.space.delta
string = 'RE: density error = {}'.format(density_error)
pm.sprint(string, 1, newline=False)
# Ensure stable convergence
error_change = abs(density_error - density_error_old)/density_error
#if((error_change > 0.0) or (abs(error_change)<1e-15)):
# mu /= 2.0
iterations +=1
# Print
if(iterations > 0):
pm.sprint('', 1, newline=True)
string = 'RE: calculated the ground-state Kohn-Sham potential,'
pm.sprint(string, 1, newline=True)
string = ' error = {}'.format(density_error)
pm.sprint(string, 1, newline=True)
if (density_error > pm.re.gs_density_tolerance):
string = 'RE: WARNING: density error above tolerance of {}!'.format(pm.re.gs_density_tolerance)
pm.sprint(string, 2, newline=True)
# Extract the exact v_ks from the Hamiltonian matrix
v_ks[:] = hamiltonian[0,:] - K[0,:]
return v_ks, density_ks, wavefunctions_ks, energies_ks, file_exist
[docs]def solve_gsks_equations(pm, hamiltonian):
r"""Solves the ground-state Kohn-Sham equations to find the ground-state
Kohn-Sham eigenfunctions, energies and electron density.
.. math::
\hat{H}\phi_{j}(x) = \varepsilon_{j}\phi_{j}(x) \\
n_{\mathrm{KS}}(x) = \sum_{j=1}^{N}|\phi_{j}(x)|^{2}
parameters
----------
pm : object
Parameters object
hamiltonian : array_like
2D array of the Hamiltonian matrix, index as
K[band,space_index]
returns array_like and array_like and array_like
2D array of the ground-state Kohn-Sham eigenfunctions, indexed as
wavefunctions_ks[space_index,eigenfunction]. 1D array containing the
ground-state Kohn-Sham eigenenergies, indexed as
energies_ks[eigenenergies]. 1D array of the ground-state Kohn-Sham
electron density, indexed as density_ks[space_index].
"""
# Solve the Kohn-Sham equations
energies_ks, wavefunctions_ks = spla.eig_banded(hamiltonian, lower=True)
# Normalise the wavefunctions
for j in range(pm.space.npt):
norm = np.linalg.norm(wavefunctions_ks[:,j])*pm.space.delta**0.5
wavefunctions_ks[:,j] /= norm
# Calculate the electron density
density_ks = np.sum(wavefunctions_ks[:,:pm.sys.NE]**2, axis=1, dtype=np.float)
return wavefunctions_ks, energies_ks, density_ks
[docs]def calculate_time_dependence(pm, A_initial, momentum, A_ks, damping,
wavefunctions_ks, density_ks, density_approx,
current_density_approx):
r"""Calculates the exact time-dependent Kohn-Sham vector potential, at time
t+dt, by solving the time-dependent Kohn-Sham equations and iteratively
correcting A_ks. The exact time-dependent Kohn-Sham eigenfunctions,
electron density and electron current density are then calculated.
.. math::
A_{\mathrm{KS}}(x,t) \rightarrow A_{\mathrm{KS}}(x,t) +
\nu\bigg[\frac{j_{\mathrm{KS}}(x,t)-j_{\mathrm{approx}}(x,t)}
{n_{\mathrm{approx}}(x,t) + a}\bigg]
parameters
----------
pm : object
Parameters object
A_initial : sparse_matrix
The sparse matrix A at t=0, A_initial, used when solving the equation
Ax=b
momentum : array_like
1D array of the lower band elements of the momentum matrix, index as
momentum[band]
A_ks : array_like
1D array of the time-dependent Kohn-Sham vector potential, at time t+dt,
indexed as A_ks[space_index]
damping : array_like
1D array of the damping function used to filter out noise, indexed as
damping[frequency_index]
wavefunctions_ks : array_like
2D array of the time-dependent Kohn-Sham eigenfunctions, at time t,
indexed as wavefunctions_ks[space_index,eigenfunction]
density_ks : array_like
2D array of the time-dependent Kohn-Sham electron density, at time t
and t+dt, indexed as density_ks[time_index, space_index]
density_approx : array_like
1D array of the time-dependent electron density from the approximation,
at time t+dt, indexed as density_approx[space_index]
current_density_approx : array_like
1D array of the electron current density from the approximation, at
time t+dt, indexed as current_density_approx[space_index]
returns array_like and array_like and array_like and array_like and float
and float
1D array of the time-dependent Kohn-Sham vector potential, at time t+dt,
indexed as A_ks[space_index]. 1D array of the time-dependent Kohn-Sham
electron density, at time t+dt, indexed as density_ks[space_index]. 1D
array of the Kohn-Sham electron current density, at time t+dt, indexed
as current_density_ks[space_index]. 2D array of the time-dependent
Kohn-Sham eigenfunctions, at time t+dt, indexed as
wavefunctions_ks[space_index,eigenfunction]. The error between the
Kohn-Sham electron density and electron density from the approximation.
The error between the Kohn-Sham electron current density and electron
current density from the approximation.
"""
# Create an array to store the A_ks that minimises the error in the current
# density
A_ks[:] = 0
A_ks_best = np.copy(A_ks)
# Create a copy of the time-dependent Kohn-Sham eigenfunctions at the
# beginning of the time step
wavefunctions_ks_copy = np.copy(wavefunctions_ks)
# Solve the time-dependent Kohn-Sham equations and iteratively correct A_ks
iterations = 0
while((iterations < pm.re.max_iterations)):
# Construct the sparse matrix A
A = construct_A(pm, A_initial, A_ks, momentum)
# Solve the time-dependent Kohn-Sham equations
density_ks[1,:], wavefunctions_ks = solve_tdks_equations(pm, A,
wavefunctions_ks)
# Calculate the Kohn-Sham current density
current_density_ks = calculate_current_density(pm, density_ks)
# Calculate the error in the Kohn-Sham charge density
density_error = np.sum(abs(density_approx[:]-density_ks[1,:])
)*pm.space.delta
# Calculate the error in the Kohn-Sham current density
current_density_difference = abs(current_density_approx-current_density_ks)
current_density_error = np.sum(current_density_difference)*pm.space.delta
if(iterations == 0):
current_density_error_min = np.copy(current_density_error)
# Store A_ks if it is an improvement
if(current_density_error < current_density_error_min):
A_ks_best[:] = A_ks[:]
current_density_error_min = np.copy(current_density_error)
# Iteratively correct A_ks
A_ks[:] += pm.re.nu*(current_density_ks[:] - current_density_approx[:]
)/(density_approx[:] + pm.re.a)
# Filter out noise in A_ks
if(pm.re.damping):
A_ks = filter_noise(pm, A_ks, damping)
# Reset the Kohn-Sham eigenfunctions
wavefunctions_ks[:,:] = wavefunctions_ks_copy[:,:]
iterations += 1
# Use the best A_ks
A_ks[:] = A_ks_best[:]
# Construct the sparse matrix A
A = construct_A(pm, A_initial, A_ks, momentum)
# Solve the time-dependent Kohn-Sham equations
density_ks[1,:], wavefunctions_ks = solve_tdks_equations(pm, A,
wavefunctions_ks)
# Calculate the Kohn-Sham current density
current_density_ks = calculate_current_density(pm, density_ks)
# Calculate the error in the Kohn-Sham charge density
density_difference = abs(density_approx-density_ks)
density_error = np.sum(density_difference)*pm.space.delta
# Calculate the error in the Kohn-Sham current density
current_density_difference = abs(current_density_approx-current_density_ks)
current_density_error = np.sum(current_density_difference)*pm.space.delta
return (A_ks, density_ks[1,:], current_density_ks, wavefunctions_ks,
density_error, current_density_error)
[docs]def solve_tdks_equations(pm, A, wavefunctions_ks):
r"""Solves the time-dependent Kohn-Sham equations to find the
time-dependent Kohn-Sham eigenfunctions and electron density.
.. math::
\hat{H}\phi_{j}(x,t) = i\frac{\partial\phi_{j}(x,t)}{\partial t} \\
n_{\mathrm{KS}}(x,t) = \sum_{j=1}^{N}|\phi_{j}(x,t)|^{2}
parameters
----------
pm : object
Parameters object
A : sparse_matrix
The sparse matrix A, used when solving the equation Ax=b
wavefunctions_ks : array_like
2D array of the time-dependent Kohn-Sham eigenfunctions, at time t+dt,
indexed as wavefunctions_ks[space_index,eigenfunction]
returns array_like and array_like
1D array of the time-dependent Kohn-Sham electron density, at time
t+dt, indexed as density_ks[space_index]. 2D array of the
time-dependent Kohn-Sham eigenfunctions, at time t+dt, indexed as
wavefunctions_ks[space_index,eigenfunction]
"""
# Construct the sparse matrix C
C = 2.0*sps.identity(pm.space.npt, dtype=np.cfloat) - A
# Loop over each electron
for j in range(pm.sys.NE):
# Construct the vector b for each wavefunction
b = C*wavefunctions_ks[:,j]
# Solve Ax=b
wavefunctions_ks[:,j], info = spsla.cg(A, b, x0=wavefunctions_ks[:,j], tol=pm.re.rtol_solver)
# Normalise each wavefunction
if(pm.sys.im == 0):
norm = np.linalg.norm(wavefunctions_ks[:,j])*pm.sys.deltax**0.5
wavefunctions_ks[:,j] /= norm
# Calculate the electron density
density_ks = np.sum(abs(wavefunctions_ks[:,:pm.sys.NE])**2, axis=1, dtype=np.float)
return density_ks, wavefunctions_ks
[docs]def filter_noise(pm, A_ks, damping):
r"""Filters out noise in the Kohn-Sham vector potential by suppressing
high-frequency terms in the Fourier transform.
parameters
----------
pm : object
Parameters object
A_ks : array_like
1D array of the time-dependent Kohn-Sham vector potential, at time t+dt,
indexed as A_ks[space_index]
damping : array_like
1D array of the damping function used to filter out noise, indexed as
damping[frequency_index]
returns array_like
1D array of the time-dependent Kohn-Sham vector potential, at time t+dt,
indexed as A_ks[space_index]
"""
# Calculate the Fourier transform of A_ks
A_ks_freq = np.fft.rfft(A_ks)
# Apply the damping function to suppress high-frequency terms
A_ks_freq[:] *= damping[:]
# Calculate the inverse Fourier transform to recover the spatial
# representation of A_ks with the noise filtered out
A_ks[:] = np.fft.irfft(A_ks_freq, len(A_ks))
return A_ks
[docs]def remove_gauge(pm, A_ks, v_ks, v_ks_gs):
r"""Removes the gauge transformation that was applied to the Kohn-Sham
potential, so that it becomes a fully scalar quantity.
.. math::
V_{\mathrm{KS}}(x,t) \rightarrow V_{\mathrm{KS}}(x,t) +
\int_{-x_{\mathrm{max}}}^{x}
\frac{\partial A_{\mathrm{KS}}(x',t)}{\partial t} dx'
parameters
----------
pm : object
Parameters object
A_ks : array_like
2D array of the time-dependent Kohn-Sham vector potential, at time t
and t+dt, indexed as A_ks[time_index, space_index]
v_ks : array_like
1D array of the time-dependent Kohn-Sham potential, at time t+dt,
indexed as v_ks[space_index]
v_ks_gs : array_like
1D array of the ground-state Kohn-Sham potential, indexed as
v_ks[space_index]
returns array_like
1D array of the time-dependent Kohn-Sham potential, at time t +dt,
indexed as v_ks[space_index]
"""
# Change gauge to calculate the full Kohn-Sham (scalar) potential
for j in range(pm.space.npt):
for k in range(j+1):
v_ks[j] += (A_ks[1,k] - A_ks[0,k])*(pm.space.delta/pm.sys.deltat)
# Shift the Kohn-Sham potential to match the ground-state Kohn-Sham
# potential at the centre of the system
shift = v_ks_gs[int((pm.space.npt-1)/2)] - v_ks[int((pm.space.npt-1)/2)]
v_ks[:] += shift
return v_ks[:]
[docs]def calculate_hartree_potential(pm, density_ks):
r"""Calculates the Hartree potential for a given electron density.
.. math::
V_{\mathrm{H}}(x) = \int U(x,x') n(x') dx'
parameters
----------
pm : object
Parameters object
density_ks : array_like
1D array of the Kohn-Sham electron density, either the ground-state or
at a particular time step, indexed as density_ks[space_index]
returns array_like
1D array of the Hartree potential for a given electron density, indexed
as v_h[space_index]
"""
return np.dot(pm.space.v_int,density_ks)*pm.space.delta
[docs]def calculate_hartree_energy(pm, density_ks, v_h):
r"""Calculates the Hartree energy of the ground-state system.
.. math::
E_{\mathrm{H}} = \frac{1}{2} \int \int U(x,x') n(x) n(x') dx dx'
= \frac{1}{2} \int V_{\mathrm{H}}(x) n(x) dx
parameters
----------
pm : object
Parameters object
density_ks : array_like
1D array of the ground-state Kohn-Sham electron density, indexed as
density_ks[space_index]
v_h : array_like
1D array of the ground-state Hartree potential, indexed as
v_h[space_index]
returns float
The Hartree energy of the ground-state system
"""
return 0.5*np.dot(v_h,density_ks)*pm.space.delta
[docs]def calculate_current_density(pm, density_ks):
r"""Calculates the Kohn-Sham electron current density, at time t+dt, from
the time-dependent Kohn-Sham electron density by solving the continuity
equation.
.. math::
\frac{\partial n_{\mathrm{KS}}}{\partial t} + \nabla
\cdot j_{\mathrm{KS}} = 0
parameters
----------
pm : object
Parameters object
density_ks : array_like
2D array of the time-dependent Kohn-Sham electron density, at time t
and t+dt, indexed as density_ks[time_index, space_index]
returns array_like
1D array of the Kohn-Sham electron current density, at time t+dt,
indexed as current_density_ks[space_index]
"""
current_density_ks = np.zeros(pm.space.npt, dtype=np.float)
current_density_ks = RE_cython.continuity_eqn(pm, density_ks[1,:], density_ks[0,:])
return current_density_ks
[docs]def xc_correction(pm, v_xc):
r"""Calculates an approximation to the constant that needs to be added to
the exchange-correlation potential so that it asymptotically approaches
zero at large :math:`|x|`. The approximate error (standard deviation) on the
constant is also calculated.
.. math::
V_{\mathrm{xc}}(x) \rightarrow V_{\mathrm{xc}}(x) + a \ , \ \
\text{s.t.} \ \lim_{|x| \to \infty} V_{\mathrm{xc}}(x) = 0
parameters
----------
pm : object
Parameters object
v_xc : array_like
1D array of the ground-state exchange-correlation potential, indexed as
v_xc[space_index]
returns float and float
An approximation to the constant that needs to be added to the
exchange-correlation potential. The approximate error (standard
deviation) on the constant.
"""
# The range over which the fit will be applied
x_min = int(0.05*pm.space.npt)
x_max = int(0.15*pm.space.npt)
# Calculate the fit and determine the correction to v_xc and its error
fit, variance = curve_fit(xc_fit, pm.space.grid[x_min:x_max],
v_xc[x_min:x_max])
correction = fit[0]
correction_error = variance[0,0]**0.5
string = 'RE: correction to the asymptotic form of V_xc = {},'\
.format(correction)
pm.sprint(string, 1, newline=True)
string = ' error = {}'.format(correction_error)
pm.sprint(string, 1, newline=True)
return correction, correction_error
[docs]def xc_fit(grid, correction):
r"""Applies a fit to the exchange-correlation potential over a specified
range near the edge of the system's grid to determine the correction that
needs to be applied to give the correct asymptotic behaviour at large :math:`|x|`
.. math::
V_{\mathrm{xc}}(x) \approx \frac{1}{x} + a
parameters
----------
grid : array_like
1D array of the spatial grid over a specified range near the edge of
the system
correction : float
An approximation to the constant that needs to be added to the
exchange-correlation potential
returns array_like
A fit to the exchange-correlation potential over the specified range
"""
return 1.0/grid + correction
[docs]def calculate_xc_energy(pm, approx, density_ks, v_h, v_xc, energies_ks):
r"""Calculates the exchange-correlation energy of the ground-state system.
.. math::
E_{\mathrm{xc}} = E_{\mathrm{total}} - \sum_{j=1}^{N}\varepsilon_{j} +
\int \bigg[\frac{1}{2}V_{\mathrm{H}}(x) +
V_{\mathrm{xc}}(x)\bigg]n_{\mathrm{KS}}(x)dx
parameters
----------
pm : object
Parameters object
approx : string
The approximation used to calculate the electron density
density_ks : array_like
1D array of the ground-state Kohn-Sham electron density, indexed as
density_ks[space_index]
v_h : array_like
1D array of the ground-state Hartree potential, indexed as
v_h[space_index]
v_xc : array_like
1D array of the ground-state exchange-correlation potential, indexed as
v_xc[space_index]
energies_ks : array_like
1D array containing the ground-state Kohn-Sham eigenenergies, indexed
as energies_ks[eigenenergies]
returns float
The exchange-correlation energy of the ground-state system
"""
try:
name = 'gs_{}_E'.format(approx)
energy_approx = rs.Results.read(name, pm)
E_xc = energy_approx - np.sum(energies_ks[:pm.sys.NE])
for j in range(pm.space.npt):
E_xc += (density_ks[j])*(0.5*v_h[j] + v_xc[j])*pm.space.delta
except:
E_xc = 0.0
string = 'RE: the exchange-correlation energy could not be ' + \
'calculated as no file containing the total energy of ' + \
'the system could be found'
pm.sprint(string, 1, newline=True)
return E_xc
[docs]def main(parameters, approx):
r"""Calculates the exact Kohn-Sham potential and exchange-correlation potential for a given
electron density using the reverse-engineering algorithm. This works for both a ground-state
and time-dependent system.
parameters
----------
parameters : object
Parameters object
approx : string
The approximation used to calculate the electron density
returns object
Results object
"""
# Array initialisations
pm = parameters
string = 'RE: constructing arrays'
pm.sprint(string, 1)
pm.setup_space()
v_ext = pm.space.v_ext
K = construct_K(pm)
if(pm.run.time_dependence):
density_ks = np.zeros((pm.sys.imax,pm.space.npt), dtype=np.float)
else:
density_ks = np.zeros((1,pm.space.npt), dtype=np.float)
v_ks = np.copy(density_ks)
v_hxc = np.copy(density_ks)
v_h = np.copy(density_ks)
v_xc = np.copy(density_ks)
# Read the density calculated using the approximation
density_approx = read_density(pm, approx)
# Calculate the ground-state Kohn-Sham system
v_ks[0,:], density_ks[0,:], wavefunctions_ks, energies_ks, file_exist = calculate_ground_state(pm, approx,
density_approx[0,:], v_ext, K)
# Calculate the ground-state Hartree potential, exchange-correlation potential and
# Hartree-exchange-correlation potential
v_hxc[0,:] = v_ks[0,:] - v_ext[:]
v_h[0,:] = calculate_hartree_potential(pm, density_ks[0,:])
v_xc[0,:] = v_hxc[0,:] - v_h[0,:]
# Correct the asymptotic form of v_xc
correction, correction_error = xc_correction(pm, v_xc[0,:])
v_ks[0,:] -= correction
v_hxc[0,:] -= correction
v_xc[0,:] -= correction
energies_ks[:] -= correction
# Calculate the ionization potential
IP = -energies_ks[pm.sys.NE-1]
string = 'RE: ionization potential = {0:.3f} +/- {1:.4f}'.format(IP, correction_error)
pm.sprint(string, 1, newline=True)
# Calculate the Kohn-Sham gap
ks_gap = energies_ks[pm.sys.NE] - energies_ks[pm.sys.NE-1]
string = 'RE: Kohn-Sham gap = {0:.3f}'.format(ks_gap)
pm.sprint(string, 1)
# Calculate the Hartree energy
E_h = calculate_hartree_energy(pm, density_ks[0,:], v_h[0,:])
string = 'RE: Hartree energy = {}'.format(E_h)
pm.sprint(string, 1)
# Calculate the exchange-correlation energy
E_xc = calculate_xc_energy(pm, approx, density_ks[0,:], v_h[0,:], v_xc[0,:], energies_ks)
string = 'RE: exchange-correlation energy = {}'.format(E_xc)
pm.sprint(string, 1)
# Calculate the Hartree exchange-correlation energy
E_hxc = E_h + E_xc
string = 'RE: Hartree exchange-correlation energy = {}'.format(E_hxc)
pm.sprint(string, 1)
# Save the ground-state quantities to file
approxre = approx + 're'
results = rs.Results()
results.add(density_ks[0,:],'gs_{}_den'.format(approxre))
results.add(v_ks[0,:],'gs_{}_vks'.format(approxre))
results.add(v_hxc[0,:],'gs_{}_vhxc'.format(approxre))
results.add(v_h[0,:],'gs_{}_vh'.format(approxre))
results.add(v_xc[0,:],'gs_{}_vxc'.format(approxre))
results.add(E_hxc,'gs_{}_Ehxc'.format(approxre))
results.add(E_h,'gs_{}_Eh'.format(approxre))
results.add(E_xc,'gs_{}_Exc'.format(approxre))
results.add(IP,'gs_{}_IP'.format(approxre))
results.add(ks_gap,'gs_{}_GAP'.format(approxre))
results.add(wavefunctions_ks.T,'gs_{}_eigf'.format(approxre))
results.add(energies_ks,'gs_{}_eigv'.format(approxre))
if(pm.run.save):
results.save(pm)
# Reverse-engineer the time-dependent system
# if(pm.run.time_dependence):
# # Array initialisations
# string = 'RE: constructing arrays'
# pm.sprint(string, 1)
# wavefunctions_ks = wavefunctions_ks.astype(np.cfloat)
# if(pm.sys.im == 1):
# v_ks = v_ks.astype(np.cfloat)
# v_ext = v_ext.astype(np.cfloat)
# v_xc = v_xc.astype(np.cfloat)
# v_ext += pm.space.v_pert
# v_ks[1:,:] += (v_ks[0,:] + pm.space.v_pert)
# A_ks = np.zeros((2,pm.space.npt), dtype=np.float)
# current_density_ks = np.zeros((pm.sys.imax,pm.space.npt), dtype=np.float)
# momentum = construct_momentum(pm)
# damping = construct_damping(pm)
# A_initial = construct_A_initial(pm, K, v_ks[1,:])
#
# # Read the current density calculated using the approximation
# current_density_approx = read_current_density(pm, approx)
#
# # Reverse-engineer each time step
# for i in range(1, pm.sys.imax):
#
# # Use A_ks from the last time step as the initial guess
# A_ks[1,:] = A_ks[0,:]
#
# # Calculate the time-dependent Kohn-Sham system
# A_ks[1,:], density_ks[i,:], current_density_ks[i,:], wavefunctions_ks, density_error, current_density_error = (
# calculate_time_dependence(pm, A_initial, momentum, A_ks[1,:],
# damping, wavefunctions_ks, density_ks[i-1:i+1,:],
# density_approx[i,:], current_density_approx[i,:]))
#
# # Print to screen
# string = 'RE: t = {0}, current density error = {1}, density error = {2}'.format(i*pm.sys.deltat,\
# current_density_error, density_error)
# pm.sprint(string,1,newline=False)
#
# # If the required tolerance has been reached
# if((density_error < pm.re.td_density_tolerance) and (current_density_error < pm.re.cdensity_tolerance)):
#
# # Remove the gauge to get the full Kohn-Sham scalar potential
# v_ks[i,:] = remove_gauge(pm, A_ks, v_ks[i,:], v_ks[0,:])
#
# # Calculate the Hartree-potential, exchange-correlation potential and
# # Hartree-exchange-correlation potential
# v_hxc[i,:] = v_ks[i,:] - v_ext[:]
# v_h[i,:] = calculate_hartree_potential(pm, density_ks[i,:])
# v_xc[i,:] = v_hxc[i,:] - v_h[i,:]
#
# else:
# pm.sprint('', 1)
# string = 'RE: The minimum tolerance has not been met. Stopping at t = {}'.format(i*pm.sys.deltat)
# pm.sprint(string, 1)
# break
#
# # Save the current time step
# A_ks[0,:] = A_ks[1,:]
#
# # Print to screen
# if(i == pm.sys.imax-1):
# pm.sprint('', 1)
#
# # Velocity field
# velocity_field_ks = np.zeros((pm.sys.imax,pm.space.npt), dtype=np.float)
# velocity_field = np.zeros((pm.sys.imax,pm.space.npt), dtype=np.float)
# velocity_field_ks[:,:] = current_density_ks[:,:]/density_ks[:,:]
# velocity_field[:,:] = current_density_approx[:,:]/density_approx[:,:]
#
# # Save the time-dependent quantities to file
# results.add(density_ks,'td_{}_den'.format(approxre))
# results.add(current_density_ks,'td_{}_cur'.format(approxre))
# results.add(v_ks,'td_{}_vks'.format(approxre))
# results.add(v_hxc,'td_{}_vhxc'.format(approxre))
# results.add(v_h,'td_{}_vh'.format(approxre))
# results.add(v_xc,'td_{}_vxc'.format(approxre))
# results.add(velocity_field_ks,'td_{}_vel'.format(approxre))
# results.add(velocity_field,'td_ext_vel')
# if(pm.run.save):
# results.save(pm)
return results