Source code for strongcoca.calculators.casida_calculator

import logging
from typing import Optional
import numpy as np
from scipy.linalg import block_diag
from .base_calculator import BaseCalculator
from ..response.excitations import Excitations
from ..response.utilities import Broadening, NoArtificialBroadening
from .. import CoupledSystem
from .utilities import get_dipole_dipole_tensor
from ..types import Array


logger = logging.getLogger(__name__)


[docs] class CasidaCalculator(BaseCalculator): """Instances of this class enable the calculation of correlation energy and spectrum of a coupled system, all polarizable objects in which have an internal representation in the form of a Casida matrix. The calculations are done for discrete excitations, and continuous spectra calculated with this calculator use the given broadening. Parameters ---------- coupled_system Coupled system for which to carry out calculations. broadening Artificial broadening used for the continuous response; defaults to no broadening. name Name of response. Examples -------- The following code snippet shows the calculation of the correlation energy for a simple demo system: >>> from strongcoca import CoupledSystem, PolarizableUnit >>> from strongcoca.response import build_random_casida >>> from strongcoca.calculators import CasidaCalculator >>> >>> # construct coupled system >>> response = build_random_casida(n_states=2, name='Tiger', random_seed=42) >>> pu1 = PolarizableUnit(response, [0, 0, 0], name='Cheetah') >>> pu2 = PolarizableUnit(response, [3, 0, 0], name='Leopard') >>> cs = CoupledSystem([pu1, pu2]) >>> >>> # set up calculator >>> calc = CasidaCalculator(cs) >>> >>> # ... and compute the correlation energy >>> calc.get_correlation_energy() -0.050798240... """ def __init__(self, coupled_system: CoupledSystem, broadening: Broadening = NoArtificialBroadening(), name: str = 'CasidaCalculator') -> None: super().__init__(coupled_system, broadening=broadening, name=name) self._excitations: Optional[Excitations] = None def _discard_data(self) -> None: super()._discard_data() self._excitations = None @property def excitations(self) -> Excitations: """Excitations as :class:`~strongcoca.response.excitations.Excitations`.""" self._verify_state() if self._excitations is None: self._excitations = self._build_casida_system() return self._excitations def _calculate_correlation_energy(self) -> float: """Returns the correlation energy of the coupled system in atomic units. """ logger.debug('Entering CasidaCalculator._calculate_correlation_energy') energy = 0.5 * (np.sum(self.excitations._energies) - np.trace(self._D)) # type: float return energy def _build_casida_system(self) -> Excitations: """Compile the Casida matrices representing the coupled system. """ logger.debug('Entering CasidaCalculator._build_casida_system') for k, pu in enumerate(self._coupled_system): if not hasattr(pu.response, '_D_n') or \ not hasattr(pu.response, '_K_nn') or \ not hasattr(pu.response, '_mu_nv'): raise ValueError(f'Polarizable unit #{k} has no internal Casida representation.') # The 'type: ignore' statements on the following lines are necessary # to apease mypy since it does not pick up that the loop above ensures # that the components of the Casida representation are in fact available. D = block_diag(*[np.diag(pu.response._D_n) # type: ignore for pu in self._coupled_system]) K_blocks = [] for ii, pu1 in enumerate(self._coupled_system): row = [] mu1 = np.array([pu1.orientation.apply(m) for m in pu1.response._mu_nv]) # type: ignore for jj, pu2 in enumerate(self._coupled_system): if ii == jj: K_sub = pu1.response._K_nn # type: ignore else: # todo: include cell and pbc distance_vector = pu2._position - pu1._position mu2 = np.array([pu2.orientation.apply(m) for m in pu2.response._mu_nv]) # type: ignore K_sub = np.dot(mu1, np.dot(get_dipole_dipole_tensor(distance_vector), mu2.T)) if ii > jj: K_sub = K_sub.conj() row.append(K_sub) K_blocks.append(row) K = np.block(K_blocks) C = D ** 2 + 2 * np.dot(np.sqrt(D), np.dot(K, np.sqrt(D))) U = block_diag(*[pu.response.U for pu in self._coupled_system]) # type: ignore C = np.dot(U.conj().T, np.dot(C, U)) self._D = np.sqrt(np.diag(np.diag(C))) eigval_I, eigvec_NI = np.linalg.eig(C) srt_I = np.argsort(eigval_I) eigval_I = eigval_I[srt_I] eigvec_NI = eigvec_NI[:, srt_I] # Calculate transition dipole moments omega_I = np.sqrt(eigval_I) F_NI = np.dot(U, eigvec_NI) mu_vNN = [] for v in range(3): mu_NN = block_diag(*[np.diag(pu.response._mu_nv[:, v]) # type: ignore for pu in self._coupled_system]) mu_vNN.append(mu_NN) mu_Iv = np.einsum('vNN,NN,NI,I->Iv', mu_vNN, np.sqrt(D), F_NI, 1 / np.sqrt(omega_I), optimize=True) return Excitations(omega_I, mu_Iv, broadening=self._broadening, name=self._name, units='au') def _get_dynamic_polarizability(self, frequencies: Array) -> np.ndarray: return self.excitations._get_dynamic_polarizability(frequencies) def _get_dynamic_polarizability_imaginary_frequency( self, frequencies: Array) -> np.ndarray: return self.excitations._get_dynamic_polarizability_imaginary_frequency( frequencies)