"""Module for linear response TDDFT class with indexed K-matrix storage."""
import os
import datetime
import glob
import numpy as np
from ase.units import Hartree
from ase.utils import IOContext
from gpaw.xc import XC
# a KS determinant with a single occ-uncc excitation
# from gpaw.lrtddft2.ks_singles import KohnShamSingleExcitation
# a list of KS determinants with single occ-uncc excitations
from gpaw.lrtddft2.ks_singles import KohnShamSingles
# Matrix Kip,jq <ia|f_Hxc|jq>
from gpaw.lrtddft2.k_matrix import Kmatrix
# a set of linear combinations of KS single determinants
from gpaw.lrtddft2.lr_transitions import LrtddftTransitions
# a linear combination of KS single determinants
# for a CW laser with Lorentzian width (in energy)
from gpaw.lrtddft2.lr_response import LrResponse
# communicators
from gpaw.lrtddft2.lr_communicators import LrCommunicators
[docs]class LrTDDFT2:
"""Linear response TDDFT (Casida) class with indexed K-matrix storage."""
def __init__(self,
basefilename,
gs_calc,
fxc=None,
min_occ=None,
max_occ=None,
min_unocc=None,
max_unocc=None,
max_energy_diff=None,
recalculate=None,
lr_communicators=None,
txt='-'):
"""Initialize linear response TDDFT without calculating anything.
Note
----
Does NOT support spin polarized calculations yet.
Tip
---
If K_matrix file is too large and you keep running out of
memory when trying to calculate spectrum or response wavefunction,
you can try
``split -l 100000
xxx.K_matrix.ddddddofDDDDDD xxx.K_matrix.ddddddofDDDDDD``.
Parameters
----------
basefilename
All files associated with this calculation are stored as
*<basefilename>.<extension>*
gs_calc
Ground state calculator (if you are using eh_communicator,
you need to take care that calc has suitable dd_communicator.)
fxc
Name of the exchange-correlation kernel (fxc) used in calculation.
(optional)
min_occ
Index of the first occupied state to be included in the calculation.
(optional)
max_occ
Index of the last occupied state (inclusive) to be included in the
calculation. (optional)
min_unocc
Index of the first unoccupied state to be included in the
calculation. (optional)
max_unocc
Index of the last unoccupied state (inclusive) to be included in the
calculation. (optional)
max_energy_diff
Noninteracting Kohn-Sham excitations above this value are not
included in the calculation. Units: eV (optional)
recalculate
| Force recalculation.
| 'eigen' : recalculate only eigensystem (useful for on-the-fly
| spectrum calculations and convergence checking)
| 'matrix' : recalculate matrix without solving the eigensystem
| 'all' : recalculate everything
| None : do not recalculate anything if not needed (default)
lr_communicators
Communicators for parallelizing over electron-hole pairs (i.e.,
rows of K-matrix) and domain. Note that ground state calculator
must have a matching (domain decomposition) communicator, which
can be assured by using lr_communicators
to create both communicators.
txt
Filename for text output
"""
# Save input params
self.basefilename = basefilename
self.fxc_name = fxc
self.xc = XC(self.fxc_name)
self.min_occ = min_occ
self.max_occ = max_occ
self.min_unocc = min_unocc
self.max_unocc = max_unocc
if max_energy_diff is not None:
self.max_energy_diff = max_energy_diff / Hartree
else:
self.max_energy_diff = None
self.recalculate = recalculate
# Don't init calculator yet if it's not needed (to save memory)
self.calc = gs_calc
self.calc_ready = False
# FIXME: SUPPORT ALSO SPIN POLARIZED
self.kpt_ind = 0
# Input paramers?
self.deriv_scale = 1e-5 # fxc finite difference step
# ignore transition if population difference is below this value:
self.min_pop_diff = 1e-3
# set up communicators
self.lr_comms = lr_communicators
if self.lr_comms is None:
self.lr_comms = LrCommunicators(None, None)
self.lr_comms.initialize(gs_calc)
# Init text output
self.iocontext = IOContext()
self.txt = self.iocontext.openfile(txt, self.lr_comms.parent_comm)
# Check and set unset params
kpt = self.calc.wfs.kpt_u[self.kpt_ind]
nbands = len(kpt.f_n)
# If min/max_occ/unocc were not given, but max_energy_diff was,
# check that calc has enough states for max_energy_diff
# (i.e., KS eigenvalue difference between HOMO and highest
# state is below max_energy_diff)
if ((self.min_occ is None or self.min_unocc is None
or self.max_occ is None or self.max_unocc is None)
and self.max_energy_diff is not None):
n_homo = np.sum(kpt.f_n > self.min_pop_diff) - 1
n_highest = nbands - 1 # XXX use highest converged state instead
eps_n = kpt.eps_n
eps_diff = eps_n[n_highest] - eps_n[n_homo]
if eps_diff <= self.max_energy_diff:
msg = ('Error in LrTDDFT2: not enough states in '
'the calculator for the requested max_energy_diff='
f'{self.max_energy_diff * Hartree:.4f} eV. '
f'Max eigenvalue difference from HOMO (n={n_homo}) is '
f'{eps_diff * Hartree:.4f} eV.')
raise RuntimeError(msg)
# If min/max_occ/unocc were not given, initialized them to include
# everything: min_occ/unocc => 0, max_occ/unocc to nubmer of wfs,
# energy diff to numerical infinity
if self.min_occ is None:
self.min_occ = 0
if self.min_unocc is None:
self.min_unocc = self.min_occ
if self.max_occ is None:
self.max_occ = nbands - 1
if self.max_unocc is None:
self.max_unocc = self.max_occ
if self.max_energy_diff is None:
self.max_energy_diff = np.inf
self.min_occ = max(self.min_occ, 0)
self.min_unocc = max(self.min_unocc, 0)
if self.max_occ >= nbands:
raise RuntimeError('Error in LrTDDFT2: max_occ >= nbands')
if self.max_unocc >= nbands:
raise RuntimeError('Error in LrTDDFT2: max_unocc >= nbands')
# Only spin unpolarized calculations are supported atm
# > FIXME
assert len(self.calc.wfs.kpt_u) == 1, \
'LrTDDFT2 does not support more than one k-point/spin.'
self.kpt_ind = 0
# <
# Internal classes
# list of singly excited Kohn-Sham Slater determinants
# (ascending KS energy difference)
self.ks_singles = KohnShamSingles(self.basefilename, self.calc,
self.kpt_ind, self.min_occ,
self.max_occ, self.min_unocc,
self.max_unocc, self.max_energy_diff,
self.min_pop_diff, self.lr_comms,
self.txt)
# Response kernel matrix K = <ip|f_Hxc|jq>
# Note: this is not the Casida matrix
self.K_matrix = Kmatrix(self.ks_singles, self.xc, self.deriv_scale)
self.sl_lrtddft = self.calc.parallel['sl_lrtddft']
# LR-TDDFT transitions
self.lr_transitions = LrtddftTransitions(self.ks_singles,
self.K_matrix,
self.sl_lrtddft)
# Response wavefunction
self.lr_response_wf = None
# Pair density
self.pair_density = None # pair density class
# Timer
self.timer = self.calc.timer
self.timer.start('LrTDDFT')
# If a previous calculation exist, read info file
# self.read(self.basefilename)
# Write info file
# self.parent_comm.barrier()
# if self.parent_comm.rank == 0:
# self.write_info(self.basefilename)
# self.custom_axes = None
# self.K_matrix_scaling_factor = 1.0
self.K_matrix_values_ready = False
[docs] def get_transitions(self,
filename=None,
min_energy=0.0,
max_energy=30.0,
units='eVcgs'):
"""Get transitions: energy, dipole strength and rotatory strength.
Returns transitions as (w,S,R, Sx,Sy,Sz) where
w is an array of frequencies,
S is an array of corresponding dipole strengths,
and R is an array of corresponding rotatory strengths.
Parameters
----------
min_energy
Minimum energy
min_energy
Maximum energy
units
Units for spectrum: 'au' or 'eVcgs'
"""
self.calculate()
self.txt.write('Calculating transitions (%s).\n' %
str(datetime.datetime.now()))
trans = self.lr_transitions.get_transitions(filename, min_energy,
max_energy, units)
self.txt.write('Transitions calculated (%s).\n' %
str(datetime.datetime.now()))
return trans
#################################################################
[docs] def get_spectrum(self,
filename=None,
min_energy=0.0,
max_energy=30.0,
energy_step=0.01,
width=0.1,
units='eVcgs'):
"""Get spectrum for dipole and rotatory strength.
Returns folded spectrum as (w,S,R) where w is an array of frequencies,
S is an array of corresponding dipole strengths, and R is an array of
corresponding rotatory strengths.
Parameters
----------
min_energy
Minimum energy
min_energy
Maximum energy
energy_step
Spacing between calculated energies
width
Width of the Gaussian
units
Units for spectrum: 'au' or 'eVcgs'
"""
self.calculate()
self.txt.write('Calculating spectrum (%s).\n' %
str(datetime.datetime.now()))
spec = self.lr_transitions.get_spectrum(filename, min_energy,
max_energy, energy_step, width,
units)
self.txt.write('Spectrum calculated (%s).\n' %
str(datetime.datetime.now()))
return spec
#################################################################
[docs] def get_transition_contributions(self, index_of_transition):
"""Get contributions of Kohn-Sham singles to a given transition
as number of electrons contributing.
Includes population difference.
This method is meant to be used for small number of transitions.
It is not suitable for analysing densely packed transitions of
large systems. Use transition contribution map (TCM) or similar
approach for this.
Parameters
----------
index_of_transition:
index of transition starting from zero
"""
self.calculate()
return self.lr_transitions.get_transition_contributions(
index_of_transition)
[docs] def calculate_response(self,
excitation_energy,
excitation_direction,
lorentzian_width,
units='eVang'):
"""Calculates and returns response using TD-DFPT.
Parameters
----------
excitation_energy
Energy of the laser in given units
excitation_direction
Vector for direction (will be normalized)
lorentzian_width
Life time or width parameter. Larger width results in wider
energy envelope around excitation energy.
"""
# S_z(omega) = 2 * omega sum_ip n_ip C^(im)_ip(omega) * mu^(z)_ip
self.calculate()
omega_au = excitation_energy
width_au = lorentzian_width
# always unit field in au !!!
direction_au = np.array(excitation_direction)
direction_au = direction_au / np.sqrt(
np.vdot(direction_au, direction_au))
if units == 'au':
pass
elif units == 'eVang':
omega_au /= Hartree
width_au /= Hartree
else:
raise RuntimeError(
'Error in calculate_response_wavefunction: Invalid units.')
lr_response = LrResponse(self, omega_au, direction_au, width_au,
self.sl_lrtddft)
lr_response.solve()
return lr_response
[docs] def calculate(self):
"""Calculates linear response matrix and properties of KS
electron-hole pairs.
This is called implicitly by get_spectrum, get_transitions, etc.
but there is no harm for calling this explicitly.
"""
if not self.calc_ready:
# Initialize wfs, paw corrections and xc, if not done yet
# FIXME: CHECK THIS STUFF, DOES GLLB WORK???
if not self.calc_ready:
mode = self.calc.wfs.mode
C_nM = self.calc.wfs.kpt_u[self.kpt_ind].C_nM
psit_nG = self.calc.wfs.kpt_u[self.kpt_ind].psit_nG
if ((mode == 'lcao' and C_nM is None)
or (mode == 'fd' and psit_nG is None)):
raise RuntimeError('Use ground state calculator ' +
'containing the wave functions.')
if not self.calc.scf.converged:
# Do not allow new scf cycles, it could change
# the wave function signs after every restart.
raise RuntimeError('Converge wave functions first.')
spos_ac = self.calc.initialize_positions()
self.calc.wfs.calculate_occupation_numbers()
self.calc.wfs.initialize(self.calc.density,
self.calc.hamiltonian, spos_ac)
self.xc.initialize(self.calc.density, self.calc.hamiltonian,
self.calc.wfs)
if mode == 'lcao':
self.calc.wfs.initialize_wave_functions_from_lcao()
self.calc_ready = True
# Singles logic
if self.recalculate == 'all' or self.recalculate == 'matrix':
self.ks_singles.update_list()
self.ks_singles.calculate()
elif self.recalculate == 'eigen':
self.ks_singles.read()
self.ks_singles.kss_list_ready = True
self.ks_singles.kss_prop_ready = True
elif ((not self.ks_singles.kss_list_ready)
or (not self.ks_singles.kss_prop_ready)):
self.ks_singles.read()
self.ks_singles.update_list()
self.ks_singles.calculate()
else:
pass
# K-matrix logic
if self.recalculate == 'all' or self.recalculate == 'matrix':
# delete files
if self.lr_comms.parent_comm.rank == 0:
for ready_file in glob.glob(self.basefilename +
'.ready_rows.*'):
os.remove(ready_file)
for K_file in glob.glob(self.basefilename + '.K_matrix.*'):
os.remove(K_file)
self.K_matrix.calculate()
elif self.recalculate == 'eigen':
self.K_matrix.read_indices()
self.K_matrix.K_matrix_ready = True
elif not self.K_matrix.K_matrix_ready:
self.K_matrix.read_indices()
self.K_matrix.calculate()
else:
pass
# Wait... we don't want to read incomplete files
self.lr_comms.parent_comm.barrier()
if not self.K_matrix_values_ready:
self.K_matrix.read_values()
# lr_transitions logic
if not self.lr_transitions.trans_prop_ready:
trans_file = self.basefilename + '.transitions'
if os.path.exists(trans_file) and os.path.isfile(trans_file):
os.remove(trans_file)
self.lr_transitions.calculate()
# recalculate only once
self.recalculate = None
[docs] def read(self, basename):
"""Does not do much at the moment."""
info_file = open(basename + '.lr_info', 'r')
for line in info_file:
if line[0] == '#':
continue
if len(line.split()) <= 2:
continue
# key = line.split('=')[0]
# value = line.split('=')[1]
# .....
# FIXME: do something, like warn if changed
# ...
info_file.close()
[docs] def write_info(self, basename):
"""Writes used parameters to a file."""
f = open(basename + '.lr_info', 'a+')
f.write('# LrTDDFTindexed\n')
f.write('%20s = %s\n' % ('xc_name', self.xc_name))
f.write('%20s = %d\n' % ('min_occ', self.min_occ))
f.write('%20s = %d\n' % ('min_unocc', self.min_unocc))
f.write('%20s = %d\n' % ('max_occ', self.max_occ))
f.write('%20s = %d\n' % ('max_unocc', self.max_unocc))
f.write('%20s = %18.12lf\n' %
('max_energy_diff', self.max_energy_diff))
f.write('%20s = %18.12lf\n' % ('deriv_scale', self.deriv_scale))
f.write('%20s = %18.12lf\n' % ('min_pop_diff', self.min_pop_diff))
f.close()
def __del__(self):
try:
self.iocontext.close()
self.timer.stop('LrTDDFT')
except AttributeError:
pass