Coverage for strongcoca/response/casida.py: 100%
83 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-26 18:44 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-26 18:44 +0000
1from typing import Optional
2import logging
4import numpy as np
6from .base import BaseResponse
7from ..types import Array
8from ..units import au_to_eV, eV_to_au, au_to_eA, eA_to_au
9from ..utilities import ClassFormatter
10from .excitations import Excitations
11from .utilities import Broadening, NoArtificialBroadening
14logger = logging.getLogger(__name__)
17class CasidaResponse(BaseResponse):
18 """Objects of this class hold different representations of the response
19 functions for a Casida system.
21 Parameters
22 ----------
23 D
24 Diagonal part of decomposition of Casida matrix
25 (initial-final energy difference of uncoupled single particle levels).
27 Units are eV by default, optionally atomic units (see :attr:`units`).
28 K
29 Coupling matrix of decomposition of Casida matrix
30 (electron-hole coupling).
32 Units are eV by default, optionally atomic units (see :attr:`units`).
33 mu
34 Transition dipoles.
36 Units are eÅ by default, optionally atomic units (see :attr:`units`).
37 occ
38 Occupation number differences, defaults to one, unitless.
39 broadening
40 Artificial broadening used for the continuous response;
41 defaults to no broadening.
42 units
43 `eVA` to specify D, K in eV and mu in eÅ or `au` to specify inputs
44 in atomic units.
46 This parameter determines whether conversion should be performed during
47 initialization and has no effect on instance methods and variables.
48 name
49 Name of response functioN.
51 Examples
52 --------
54 The following snippet illustrates how a Casida response object can be constructed.
55 Here, artificial Casida matrix components are used. In practice the Casida matrix
56 would usually be imported from, e.g., NWChem calculations:
58 >>> import numpy as np
59 >>> from strongcoca.response import CasidaResponse
60 >>>
61 >>> D = np.array([0.1, 0.3])
62 >>> K = np.zeros((2, 2))
63 >>> mu = np.array([[0.1, 0.0, 0.0], [0.0, 0.1, 0.0]])
64 >>> response = CasidaResponse(D, K, mu, name='Panda')
65 >>> print(response)
66 ---------------------- CasidaResponse ----------------------
67 name : Panda
68 n_states : 2
69 D (eV) : [0.1 0.3]
70 K (eV) : [[0. 0.]
71 [0. 0.]]
72 mu (eÅ) : [[0.1 0. 0. ]
73 [0. 0.1 0. ]]
74 broadening : NoArtificialBroadening
75 atoms : None
76 """
78 def __init__(self,
79 D: np.ndarray,
80 K: np.ndarray,
81 mu: np.ndarray,
82 broadening: Broadening = NoArtificialBroadening(),
83 occ: Optional[np.ndarray] = None,
84 units: str = 'eVA',
85 name: str = 'Casida') -> None:
86 logger.debug(f'Entering {self.__class__.__name__}.__init__')
87 super().__init__(broadening=broadening, pbc=False, name=name)
89 # TODO: explore way to generate these automatically based on type hints
90 if not isinstance(D, np.ndarray):
91 raise TypeError(f'D matrix must be provided as numpy array: {D}')
92 if not isinstance(K, np.ndarray):
93 raise TypeError(f'K matrix must be provided as numpy array: {K}')
94 if not isinstance(mu, np.ndarray):
95 raise TypeError(f'mu matrix must be provided as numpy array: {mu}')
96 if len(D.shape) != 1:
97 raise ValueError(f'D matrix has the wrong shape: {D.shape}')
98 n = len(D)
99 if K.shape != (n, n):
100 raise ValueError(f'K matrix must be {n}x{n}; actual shape: {K.shape}')
101 if mu.shape != (n, 3):
102 raise ValueError(f'mu matrix must be {n}x{3}; actual shape: {mu.shape}')
103 if occ is None:
104 occ = np.ones_like(D)
105 else:
106 # TODO: check that all code supports occupation numbers
107 raise NotImplementedError('Occupation numbers not implemented')
108 evals = np.linalg.eigvalsh(K)
109 if not np.all(evals > -1e-8):
110 raise ValueError(f'K matrix must be positive definite; eigenvalues: {evals}')
112 if units == 'eVA':
113 D = D * eV_to_au
114 K = K * eV_to_au
115 mu = mu * eA_to_au
116 elif units != 'au':
117 raise ValueError(f"units has to be 'eVA' or 'au', not '{units}'")
119 self._D_n = D
120 self._K_nn = K
121 self._mu_nv = mu
122 self._f_n = occ
124 # TODO: do not diagonalize and evaluate the following in init but
125 # use lazy evaluation
126 # See also issue #34
128 # compose and diagonalize Casida matrix
129 sq_fD_n = np.sqrt(self._f_n * self._D_n)
130 C_nn = np.diag(self._D_n)**2 + 2 * np.outer(sq_fD_n, sq_fD_n) * self._K_nn
131 eigval_I, F_nI = np.linalg.eig(C_nn)
133 # Calculate transition dipole moments
134 omega_I = np.sqrt(eigval_I)
135 mu_Iv = np.einsum('nv,n,nI,I->Iv', self._mu_nv, sq_fD_n,
136 F_nI, 1. / np.sqrt(omega_I),
137 optimize=True)
138 self._excitations = Excitations(omega_I, mu_Iv, broadening=self._broadening,
139 name=self._name, units='au')
140 self._eigvec_nI = F_nI
142 def __str__(self) -> str:
143 fmt = ClassFormatter(self, pad=15)
144 fmt.append_class_name()
146 fmt.append_attr('name')
147 fmt.append_attr('n_states')
149 fmt.append_attr('D', unit='eV')
150 fmt.append_attr('K', unit='eV')
151 fmt.append_attr('mu', unit='eÅ')
152 fmt.append_attr('broadening')
153 formula = self.atoms.get_chemical_formula() if self.atoms is not None else None
154 fmt.append_attr('atoms', formula)
156 return fmt.to_string()
158 @property
159 def n_states(self) -> int:
160 """Number of states in Casida system."""
161 return len(self._D_n)
163 @property
164 def D(self) -> np.ndarray:
165 """D matrix of Casida system in units of eV."""
166 return self._D_n * au_to_eV # type: ignore
168 @property
169 def K(self) -> np.ndarray:
170 """K matrix of Casida system in units of eV."""
171 return self._K_nn * au_to_eV # type: ignore
173 @property
174 def mu(self) -> np.ndarray:
175 """mu matrix of Casida system in units of eÅ."""
176 return self._mu_nv * au_to_eA # type: ignore
178 @property
179 def U(self) -> np.ndarray:
180 """Unitless eigenvectors of Casida matrix."""
181 return self._eigvec_nI # type: ignore
183 @property
184 def excitations(self) -> Excitations:
185 """Excitations as :class:`~strongcoca.response.excitations.Excitations`."""
186 return self._excitations
188 def _get_dynamic_polarizability(self, frequencies: Array) -> np.ndarray:
189 return self._excitations._get_dynamic_polarizability(frequencies)
191 def _get_dynamic_polarizability_imaginary_frequency(
192 self, frequencies: Array) -> np.ndarray:
193 return self._excitations._get_dynamic_polarizability_imaginary_frequency(
194 frequencies)