Source code for strongcoca.response.casida

from typing import Optional
import logging

import numpy as np

from .base import BaseResponse
from ..types import Array
from ..units import au_to_eV, eV_to_au, au_to_eA, eA_to_au
from ..utilities import ClassFormatter
from .excitations import Excitations
from .utilities import Broadening, NoArtificialBroadening


logger = logging.getLogger(__name__)


[docs] class CasidaResponse(BaseResponse): """Objects of this class hold different representations of the response functions for a Casida system. Parameters ---------- D Diagonal part of decomposition of Casida matrix (initial-final energy difference of uncoupled single particle levels). Units are eV by default, optionally atomic units (see :attr:`units`). K Coupling matrix of decomposition of Casida matrix (electron-hole coupling). Units are eV by default, optionally atomic units (see :attr:`units`). mu Transition dipoles. Units are eÅ by default, optionally atomic units (see :attr:`units`). occ Occupation number differences, defaults to one, unitless. broadening Artificial broadening used for the continuous response; defaults to no broadening. units `eVA` to specify D, K in eV and mu in eÅ or `au` to specify inputs in atomic units. This parameter determines whether conversion should be performed during initialization and has no effect on instance methods and variables. name Name of response functioN. Examples -------- The following snippet illustrates how a Casida response object can be constructed. Here, artificial Casida matrix components are used. In practice the Casida matrix would usually be imported from, e.g., NWChem calculations: >>> import numpy as np >>> from strongcoca.response import CasidaResponse >>> >>> D = np.array([0.1, 0.3]) >>> K = np.zeros((2, 2)) >>> mu = np.array([[0.1, 0.0, 0.0], [0.0, 0.1, 0.0]]) >>> response = CasidaResponse(D, K, mu, name='Panda') >>> print(response) ---------------------- CasidaResponse ---------------------- name : Panda n_states : 2 D (eV) : [0.1 0.3] K (eV) : [[0. 0.] [0. 0.]] mu (eÅ) : [[0.1 0. 0. ] [0. 0.1 0. ]] broadening : NoArtificialBroadening atoms : None """ def __init__(self, D: np.ndarray, K: np.ndarray, mu: np.ndarray, broadening: Broadening = NoArtificialBroadening(), occ: Optional[np.ndarray] = None, units: str = 'eVA', name: str = 'Casida') -> None: logger.debug(f'Entering {self.__class__.__name__}.__init__') super().__init__(broadening=broadening, pbc=False, name=name) # TODO: explore way to generate these automatically based on type hints if not isinstance(D, np.ndarray): raise TypeError(f'D matrix must be provided as numpy array: {D}') if not isinstance(K, np.ndarray): raise TypeError(f'K matrix must be provided as numpy array: {K}') if not isinstance(mu, np.ndarray): raise TypeError(f'mu matrix must be provided as numpy array: {mu}') if len(D.shape) != 1: raise ValueError(f'D matrix has the wrong shape: {D.shape}') n = len(D) if K.shape != (n, n): raise ValueError(f'K matrix must be {n}x{n}; actual shape: {K.shape}') if mu.shape != (n, 3): raise ValueError(f'mu matrix must be {n}x{3}; actual shape: {mu.shape}') if occ is None: occ = np.ones_like(D) else: # TODO: check that all code supports occupation numbers raise NotImplementedError('Occupation numbers not implemented') evals = np.linalg.eigvalsh(K) if not np.all(evals > -1e-8): raise ValueError(f'K matrix must be positive definite; eigenvalues: {evals}') if units == 'eVA': D = D * eV_to_au K = K * eV_to_au mu = mu * eA_to_au elif units != 'au': raise ValueError(f"units has to be 'eVA' or 'au', not '{units}'") self._D_n = D self._K_nn = K self._mu_nv = mu self._f_n = occ # TODO: do not diagonalize and evaluate the following in init but # use lazy evaluation # See also issue #34 # compose and diagonalize Casida matrix sq_fD_n = np.sqrt(self._f_n * self._D_n) C_nn = np.diag(self._D_n)**2 + 2 * np.outer(sq_fD_n, sq_fD_n) * self._K_nn eigval_I, F_nI = np.linalg.eig(C_nn) # Calculate transition dipole moments omega_I = np.sqrt(eigval_I) mu_Iv = np.einsum('nv,n,nI,I->Iv', self._mu_nv, sq_fD_n, F_nI, 1. / np.sqrt(omega_I), optimize=True) self._excitations = Excitations(omega_I, mu_Iv, broadening=self._broadening, name=self._name, units='au') self._eigvec_nI = F_nI def __str__(self) -> str: fmt = ClassFormatter(self, pad=15) fmt.append_class_name() fmt.append_attr('name') fmt.append_attr('n_states') fmt.append_attr('D', unit='eV') fmt.append_attr('K', unit='eV') fmt.append_attr('mu', unit='eÅ') fmt.append_attr('broadening') formula = self.atoms.get_chemical_formula() if self.atoms is not None else None fmt.append_attr('atoms', formula) return fmt.to_string() @property def n_states(self) -> int: """Number of states in Casida system.""" return len(self._D_n) @property def D(self) -> np.ndarray: """D matrix of Casida system in units of eV.""" return self._D_n * au_to_eV # type: ignore @property def K(self) -> np.ndarray: """K matrix of Casida system in units of eV.""" return self._K_nn * au_to_eV # type: ignore @property def mu(self) -> np.ndarray: """mu matrix of Casida system in units of eÅ.""" return self._mu_nv * au_to_eA # type: ignore @property def U(self) -> np.ndarray: """Unitless eigenvectors of Casida matrix.""" return self._eigvec_nI # type: ignore @property def excitations(self) -> Excitations: """Excitations as :class:`~strongcoca.response.excitations.Excitations`.""" return self._excitations 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)