"""Computes ground-state and time-dependent charge density in the Hybrid
Hartree-Fock-LDA approximation.
The code outputs the ground-state charge density, the energy of the
system and the single-quasiparticle orbitals.
"""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import
import copy
import pickle
import numpy as np
import scipy as sp
import scipy.sparse as sps
import scipy.linalg as spla
from iDEA.input import Input
from . import results as rs
import iDEA.LDA
import iDEA.HF
import iDEA.NON
[docs]def hamiltonian(pm, eigf, density, alpha, occupations, perturb=False):
r"""Compute HF Hamiltonian.
Computes HYB Hamiltonian from a given set of single-particle states.
.. math:: H_{\alpha}(x,y) = \delta(x-y)\hat{T} + \delta(x-y)v_{\text{ext}}(y) + \delta(x-y)v_{\text{H}}(y) + \alpha\Sigma_{\text{x}}(x,y) + (1-\alpha)\delta(x-y)v_{\text{xc}}^{\text{LDA}}(y)
parameters
----------
eigf array_like
single-particle states
density array_like
electron density
alpha float
HF-LDA mixing parameter (1 = all HF, 0 = all LDA)
occupations: array_like
orbital occupations
perturb: bool
If True, add perturbation to external potential (for time-dep. runs)
returns array_like
Hamiltonian matrix
"""
# Construct kinetic energy.
sd = pm.space.second_derivative
sd_ind = pm.space.second_derivative_indices
T = -0.5*sps.diags(sd, sd_ind, shape=(pm.sys.grid,pm.sys.grid), format='csr', dtype=complex).toarray()
# Construct external potential.
Vext = sps.diags(pm.space.v_ext, 0, shape=(pm.sys.grid,pm.sys.grid), format='csr', dtype=complex).toarray()
if perturb:
Vext += sps.diags(pm.space.v_pert, 0, shape=(pm.sys.grid,pm.sys.grid), format='csr', dtype=complex).toarray()
# Construct Hartree potential.
Vh = iDEA.HF.hartree(pm, density)
# Construct LDA Vxc.
if not pm.hyb.seperate:
Vxc_LDA = iDEA.LDA.xc_potential(pm, density, pm.hyb.seperate)
else:
Vxc_LDA, Vx_LDA, Vc_LDA = iDEA.LDA.xc_potential(pm, density, pm.hyb.seperate)
# Construct the Fock operator.
if alpha != 0:
F = iDEA.HF.fock(pm, eigf)
F *= pm.space.delta
else:
F = np.zeros((pm.sys.grid, pm.sys.grid))
# Construct hybrid Vxc.
if not pm.hyb.seperate:
Vxc = alpha*F + (1-alpha)*np.diag(Vxc_LDA)
else:
Vxc = alpha*F + (1-alpha)*np.diag(Vx_LDA) + np.diag(Vc_LDA)
# Construct H.
H = T + Vext + np.diag(Vh) + Vxc
if not pm.hyb.seperate:
return H, Vh, Vxc_LDA, F
else:
return H, Vh, Vxc_LDA, Vx_LDA, Vc_LDA, F
[docs]def calc_with_alpha(pm, alpha, occupations):
r"""Calculate with given alpha.
Perform hybrid calculation with given alpha.
parameters
----------
alpha float
HF-LDA mixing parameter (1 = all HF, 0 = all LDA)
occupations: array_like
orbital occupations
returns density, eigf, eigv, E
Hybrid Density, orbitals and total energy
"""
# Initialise self-consistency.
counter = 0
convergence = 1.0
H, Vh, Vxc_LDA, F = hamiltonian(pm, np.zeros((pm.sys.grid,pm.sys.grid)), np.zeros(pm.sys.grid), alpha, occupations)
density, eigf, eigv = iDEA.HF.groundstate(pm, H)
E = 0
# Convergence boolean:
converged = False
# Perform self-consistency.
while not converged and counter < pm.hyb.max_iter:
# Keep copies to check convergence.
density_old = copy.deepcopy(density)
H_old = copy.deepcopy(H)
# Construct hybrid Hamiltonian.
if not pm.hyb.seperate:
H, Vh, Vxc_LDA, F = hamiltonian(pm, eigf, density, alpha, occupations)
else:
H, Vh, Vxc_LDA, Vx_LDA, Vc_LDA, F = hamiltonian(pm, eigf, density, alpha, occupations)
# Mix for stability.
H = pm.hyb.mix*H + (1.0-pm.hyb.mix)*H_old
# Solve single-particle Shrodinger equation.
density, eigf, eigv = iDEA.HF.groundstate(pm, H)
if pm.sys.NE > 0:
# Get a list of occupied wavefunctions.
occupied = copy.deepcopy(eigf[:, :pm.sys.NE])
# Scale HOMO orbital by its occupation.
# Only this orbital can have fractional occupation.
occupied[:,-1] = occupied[:,-1]*np.sqrt(occupations[-1])
# Calculate density associated with occupied orbitals.
# This takes into account fractional occupation.
density = np.sum(occupied*occupied.conj(), axis=1).real
# Test for convergance.
convergence = sum(abs(density - density_old))
converged = convergence < pm.hyb.tol and counter > 20
pm.sprint('HYB: computing GS density with alpha = {:05.4f}, convergence = {:06.5e}'.format(alpha, convergence), 1, newline=False)
counter += 1
E_H = -0.5*np.dot(Vh, density)*pm.sys.deltax
E_SYS = 0
E_F = 0
for i in range(pm.sys.NE):
# Calculate system energy.
E_SYS += eigv[i]*occupations[i]
# Calculate exchange energy.
orb = copy.deepcopy(eigf[:,i]) * np.sqrt(occupations[i])
E_F -= 0.5 * np.dot(orb.conj().T, np.dot(F, orb)) * pm.sys.deltax
# LDA energy:
if pm.hyb.seperate:
Exc_LDA, Ex_LDA, Ec_LDA = iDEA.LDA.xc_energy(pm, density)
Evx_LDA = Ex_LDA - np.dot(density, Vx_LDA)*pm.sys.deltax
Evc_LDA = Ec_LDA - np.dot(density, Vc_LDA)*pm.sys.deltax
E = (E_SYS + E_H + alpha*E_F + (1.0 - alpha)*Evx_LDA + Evc_LDA).real
else:
Evxc_LDA = iDEA.LDA.xc_energy(pm, density) - np.dot(density, Vxc_LDA)*pm.sys.deltax
E = (E_SYS + E_H + alpha*E_F + (1.0 - alpha)*Evxc_LDA).real
# Calculate charges on grid.
grid_charge = np.sum(density)*pm.sys.deltax
# Print iteration values.
pm.sprint('\nHYB: Total charge on grids: {:10.9f}'.format(grid_charge), 1, newline=True)
pm.sprint('HYB: total energy = {0} converged in {1} iterations'.format(E, counter), 1, newline=True)
pm.sprint('HYB: HOMO-LUMO gap = {0}\n'.format(eigv[pm.sys.NE]-eigv[pm.sys.NE-1]), 1, newline=True)
return density, eigf, eigv, E
[docs]def save_results(pm, results, density, E, eigf, eigv, alpha):
r"""Saves hybrid results to outputs directory
"""
results.add(density,'gs_hyb{:05.3f}_den'.format(alpha).replace('.','_'))
results.add(E,'gs_hyb{:05.3f}_E'.format(alpha).replace('.','_'))
results.add(eigf.T,'gs_hyb{:05.3f}_eigf'.format(alpha).replace('.','_'))
results.add(eigv,'gs_hyb{:05.3f}_eigv'.format(alpha).replace('.','_'))
if pm.run.save:
results.save(pm)
[docs]def optimal_alpha(pm, results, alphas, occupations):
r"""Calculate optimal alpha.
Calculate over range of alphas to determine optimal alpha.
parameters
----------
results Results Object
object to add results to
alphas: array_like
range of alphas to use
occupations: array_like
orbital occupations
"""
pm.sprint('HYB: Finding optimal value of alpha', 1, newline=True)
# Run E(N) calculations.
nElect = pm.sys.NE
pm.sprint('HYB: Starting calculations for N electrons (N={})'.format(pm.sys.NE), 1, newline=True)
energies, eigsHOMO = n_run(pm, results, alphas, occupations)
# Run E(N-1) calculations.
pm.sys.NE = nElect - 1
pm.sprint('HYB: Starting calculations for N-1 electrons (N-1={})'.format(pm.sys.NE), 1, newline=True)
energies_minus_one, eigsLUMO = n_minus_one_run(pm, results, alphas, occupations)
# Save all results.
results.add(alphas,'gs_hyb_alphas')
results.add(energies,'gs_hyb_enN')
results.add(energies_minus_one,'gs_hyb_enNm')
results.add(eigsLUMO,'gs_hyb_eigL')
results.add(eigsHOMO,'gs_hyb_eigH')
if pm.run.save:
results.save(pm)
[docs]def n_run(pm, results, alphas, occupations):
r"""Calculate for :math:`N` electron run.
Calculate total energy and HOMO eigenvalue of N electron system.
parameters
----------
results Results Object
object to add results to
alphas: array_like
range of alphas to use
occupations: array_like
orbital occupations
"""
alphas_length = alphas.shape[0]
eigsHOMO = np.empty(alphas_length)
energies = np.empty(alphas_length)
for i in range(alphas_length):
density, eigf, eigv, energy = calc_with_alpha(pm, alphas[i], occupations)
eigsHOMO[i] = eigv[pm.sys.NE - 1]
energies[i] = energy
save_results(pm, results, density, energy, eigf, eigv, alphas[i])
return energies, eigsHOMO
[docs]def n_minus_one_run(pm, results, alphas, occupations):
r"""Calculate for :math:`N-1` electron run.
Calculate total energy and LUMO eigenvalue of :math:`N-1` electron
system.
parameters
----------
results Results Object
object to add results to
alphas: array_like
range of alphas to use
occupations: array_like
orbital occupations
"""
alphas_length = alphas.shape[0]
eigsLUMO = np.empty(alphas_length)
energies = np.empty(alphas_length)
for i in range(alphas_length):
density, eigf, eigv, energy = calc_with_alpha(pm, alphas[i], occupations)
eigsLUMO[i] = eigv[pm.sys.NE]
energies[i] = energy
return energies, eigsLUMO
[docs]def fractional_run(pm, results, occupations, fractions):
energies = np.array([])
eigsHOMO = np.array([])
eigsLUMO = np.array([])
pm.sprint('\nHYB: Running fractional numbers of electrons from {} to {}\n'.format(fractions[0], fractions[-1]), 1, newline=True)
for num_electrons in fractions:
pm.sprint('HYB: Current total number of electrons = {:05.4f}'.format(num_electrons), 1, newline=True)
pm.sys.NE = int(np.ceil(num_electrons))
if pm.sys.NE == 0:
pm.sys.NE = 1
occupations = np.zeros(pm.sys.NE)
else:
occupations = np.ones(pm.sys.NE)
occupations[-1] = num_electrons - int(np.ceil(num_electrons)) + 1
# Run a normal calculation.
density, eigf, eigv, energy = calc_with_alpha(pm, pm.hyb.alpha, occupations)
eigsHOMO = np.append(eigsHOMO, eigv[pm.sys.NE - 1])
eigsLUMO = np.append(eigsLUMO, eigv[pm.sys.NE])
energies = np.append(energies, energy)
results.add(fractions,'gs_hyb_frac{:05.3f}'.format(pm.hyb.alpha).replace('.','_'))
results.add(eigsHOMO, 'gs_hyb_frac{:05.3f}_HOMO'.format(pm.hyb.alpha).replace('.','_'))
results.add(eigsLUMO, 'gs_hyb_frac{:05.3f}_LUMO'.format(pm.hyb.alpha).replace('.','_'))
results.add(energies, 'gs_hyb_frac{:05.3f}_en'.format(pm.hyb.alpha).replace('.','_'))
if (pm.run.save):
results.save(pm)
[docs]def main(parameters):
r"""Performs Hybrid calculation.
parameters
----------
parameters : object
Parameters object
returns object
Results object
"""
# Set up parameters.
pm = parameters
pm.setup_space()
results = rs.Results()
# Choose type of LDA to be run.
if pm.hyb.seperate:
pm.lda.NE = 'heg'
else:
pm.lda.NE = 3
# Occupations of all the orbitals.
# Only the last value should be fractional.
occupations = np.ones(pm.sys.NE)
# Run code to find optimal alpha.
# No fractional occupation in this part.
if pm.hyb.functionality == 'o':
alphas = np.linspace(pm.hyb.of_array[0], pm.hyb.of_array[1], pm.hyb.of_array[2])
optimal_alpha(pm, results, alphas, occupations)
# Run code to get fractional numbers of electrons.
elif pm.hyb.functionality == 'f':
fractions = np.linspace(pm.hyb.of_array[0], pm.hyb.of_array[1], pm.hyb.of_array[2])
#fractions = np.array([1.0, 0.9])
fractional_run(pm, results, occupations, fractions)
# Run code for one given alpha.
elif pm.hyb.functionality == 'a':
occupations[pm.sys.NE - 1] = 1.0
density, eigf, eigv, E = calc_with_alpha(pm, pm.hyb.alpha, occupations)
save_results(pm, results, density, E, eigf, eigv, pm.hyb.alpha)
# Print an error message.
else:
pm.sprint('HYB: Functionality chosen is not valid. Please choose from a, o, and f.', 1, newline=True)
return results