Coverage for gpaw/directmin/lcao/etdm_helper_lcao.py: 94%
80 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-20 00:19 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-20 00:19 +0000
1"""
2Helper class for LCAOETDM.
4Handles orbital initialization, setting reference orbitals, applying the
5unitary transformation, calculating the gradient, and getting canonical
6representation.
7"""
9import numpy as np
10from gpaw.lcao.eigensolver import DirectLCAO
11from gpaw.directmin.functional.lcao import get_functional
12from gpaw.utilities.tools import tri2full
15class ETDMHelperLCAO(DirectLCAO):
17 def __init__(self, wfs, dens, ham, nkpts, functional, diagonalizer=None,
18 orthonormalization='gramschmidt',
19 need_init_orbs=True):
21 super(ETDMHelperLCAO, self).__init__(diagonalizer)
22 super(ETDMHelperLCAO, self).initialize(wfs.gd, wfs.dtype,
23 wfs.setups.nao, wfs.ksl)
24 self.orthonormalization = orthonormalization
25 self.need_init_orbs = need_init_orbs
26 self.nkpts = nkpts
27 self.func = get_functional(functional, wfs, dens, ham)
28 self.reference_orbitals = {}
29 self.initialize_orbitals(wfs, ham)
31 def __repr__(self):
32 pass
34 def set_reference_orbitals(self, wfs, n_dim):
35 for kpt in wfs.kpt_u:
36 u = self.kpointval(kpt)
37 self.reference_orbitals[u] = np.copy(kpt.C_nM[:n_dim[u]])
39 def appy_transformation_kpt(self, wfs, u_mat, kpt, c_ref=None,
40 broadcast=True,
41 update_proj=True):
42 """
43 If c_ref are not provided then
44 kpt.C_nM <- u_mat kpt.C_nM
45 otherwise kpt.C_nM <- u_mat c_ref
46 """
48 dimens1 = u_mat.shape[1]
49 dimens2 = u_mat.shape[0]
51 if c_ref is None:
52 kpt.C_nM[:dimens2] = u_mat @ kpt.C_nM[:dimens1]
53 else:
54 kpt.C_nM[:dimens2] = u_mat @ c_ref[:dimens1]
56 if broadcast:
57 with wfs.timer('Broadcast coefficients'):
58 wfs.gd.comm.broadcast(kpt.C_nM, 0)
59 if update_proj:
60 with wfs.timer('Calculate projections'):
61 wfs.atomic_correction.calculate_projections(wfs, kpt)
63 def initialize_orbitals(self, wfs, ham):
64 """
65 If it is the first use of the scf then initialize
66 coefficient matrix using eigensolver
67 """
69 orthname = self.orthonormalization
70 need_canon_coef = \
71 (not wfs.coefficients_read_from_file and self.need_init_orbs)
72 if need_canon_coef or orthname == 'diag':
73 super(ETDMHelperLCAO, self).iterate(ham, wfs)
74 else:
75 wfs.orthonormalize(type=orthname)
76 wfs.coefficients_read_from_file = False
77 self.need_init_orbs = False
79 def calc_grad(self, wfs, ham, kpt, evecs, evals, matrix_exp,
80 representation, ind_up, constraints):
81 """
82 Gradient w.r.t. skew-Hermitian matrices
83 """
85 h_mm = self.calculate_hamiltonian_matrix(ham, wfs, kpt)
86 # make matrix hermitian
87 tri2full(h_mm)
88 # calc gradient and eigenstate error
89 g_mat, error = self.func.get_gradients(
90 h_mm, kpt.C_nM, kpt.f_n, evecs, evals,
91 kpt, wfs, wfs.timer, matrix_exp,
92 representation, ind_up, constraints)
94 return g_mat, error
96 def update_to_canonical_orbitals(self, wfs, ham, kpt,
97 update_ref_orbs_canonical, restart):
98 """
99 Choose canonical orbitals
100 """
102 h_mm = self.calculate_hamiltonian_matrix(ham, wfs, kpt)
103 tri2full(h_mm)
105 if self.func.name == 'ks':
106 if update_ref_orbs_canonical or restart:
107 # Diagonalize entire Hamiltonian matrix
108 with wfs.timer('Diagonalize and rotate'):
109 kpt.C_nM, kpt.eps_n = rotate_subspace(h_mm, kpt.C_nM)
110 else:
111 # Diagonalize equally occupied subspaces separately
112 f_unique = np.unique(kpt.f_n)
113 for f in f_unique:
114 with wfs.timer('Diagonalize and rotate'):
115 kpt.C_nM[kpt.f_n == f, :], kpt.eps_n[kpt.f_n == f] = \
116 rotate_subspace(h_mm, kpt.C_nM[kpt.f_n == f, :])
117 elif self.func.name == 'PZ-SIC':
118 self.func.get_lagrange_matrices(
119 h_mm, kpt.C_nM, kpt.f_n, kpt, wfs,
120 update_eigenvalues=True)
122 with wfs.timer('Calculate projections'):
123 self.update_projections(wfs, kpt)
125 def sort_orbitals(self, wfs, kpt, ind):
126 """
127 sort orbitals according to indices stored in ind
128 """
129 kpt.C_nM[np.arange(len(ind)), :] = kpt.C_nM[ind, :]
130 self.update_projections(wfs, kpt)
132 def update_projections(self, wfs, kpt):
133 """
134 calculate projections kpt.P_ani
135 """
137 wfs.atomic_correction.calculate_projections(wfs, kpt)
139 def orbital_energies(self, wfs, ham, kpt):
140 """
141 diagonal elements of hamiltonian matrix in orbital representation
142 """
144 if self.func.name == 'ks':
145 h_mm = self.calculate_hamiltonian_matrix(ham, wfs, kpt)
146 tri2full(h_mm)
147 # you probably need only diagonal terms?
148 # wouldn't "for" be faster?
149 h_mm = kpt.C_nM.conj() @ h_mm.conj() @ kpt.C_nM.T
150 energies = h_mm.diagonal().real.copy()
151 elif self.func.name == 'PZ-SIC':
152 energies = self.func.lagr_diag_s[wfs.eigensolver.kpointval(kpt)]
154 return energies
156 def kpointval(self, kpt):
157 return self.nkpts * kpt.s + kpt.q
160def rotate_subspace(h_mm, c_nm):
161 """
162 choose canonical orbitals
163 """
164 l_nn = c_nm @ h_mm @ c_nm.conj().T
165 # check if diagonal then don't rotate? it could save a bit of time
166 eps, w = np.linalg.eigh(l_nn)
167 return w.T.conj() @ c_nm, eps