Coverage for gpaw/directmin/etdm_lcao.py: 85%
555 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-19 00:19 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-19 00:19 +0000
1"""
2A class for finding optimal orbitals corresponding to a stationary point of
3the energy functional using direct optimization and exponential transformation
4in LCAO mode.
6It can be used with Kohn-Sham functionals and can include Perdew-Zunger
7self-interaction correction (PZ-SIC) in the calculations.
9Ground state as well as variational excited state calculations can be
10performed. Ground state calculations involve minimization of the energy
11(direct minimization), while excited state calculations involve convergence
12on a saddle point (direct optimization).
14Implementation of exponential transformation direct minimization (ETDM) for
15ground state calculations LCAO mode:
17 Comput. Phys. Commun. 267, 108047 (2021) :doi:10.1016/j.cpc.2021.108047
18 arXiv:2101.12597 [physics.comp-ph]
20GPAW implementations of direct optimization (DO) for variational excited state
21calculations LCAO mode:
23 DO with preconditioned quasi-Newton algorithms and maximum overlap method
24 (DO-MOM)
25 J. Chem. Theory Comput. 16, 6968 (2020) :doi:10.1021/acs.jctc.0c00597
26 arXiv:2006.15922 [physics.chem-ph]
28 DO with generalized mode following (DO-GMF)
29 J. Chem. Theory Comput. 19, 3634 (2023) :doi:10.1021/acs.jctc.3c00178
30 arXiv:2302.05912 [physics.chem-ph]
31"""
34import numpy as np
35import warnings
36from ase.utils import basestring
37from gpaw.directmin.tools import expm_ed, expm_ed_unit_inv, random_a, \
38 sort_orbitals_according_to_occ, sort_orbitals_according_to_energies
39from gpaw.directmin.lcao.etdm_helper_lcao import ETDMHelperLCAO
40from gpaw.directmin.locfunc.localize_orbitals import localize_orbitals
41from scipy.linalg import expm
42from gpaw.directmin import search_direction, line_search_algorithm
43from gpaw.directmin.tools import get_n_occ
44from gpaw.directmin.derivatives import get_approx_analytical_hessian
45from gpaw import BadParallelization
46from copy import deepcopy
49class LCAOETDM:
51 def __init__(self,
52 excited_state=False,
53 searchdir_algo=None,
54 linesearch_algo=None,
55 partial_diagonalizer='Davidson',
56 update_ref_orbs_counter=20,
57 update_ref_orbs_canonical=False,
58 update_precond_counter=1000,
59 use_prec=True,
60 matrix_exp='pade-approx',
61 representation='sparse',
62 functional='ks',
63 need_init_orbs=None,
64 orthonormalization='gramschmidt',
65 randomizeorbitals=None,
66 checkgraderror=False,
67 need_localization=True,
68 localizationtype=None,
69 localizationseed=None,
70 constraints=None,
71 subspace_convergence=5e-4
72 ):
73 """Class for direct orbital optimization in LCAO mode.
75 Parameters
76 ----------
77 excited_state: bool
78 If False (default), use search direction and line search
79 algorithms for ground state calculation ('l-bfgs-p' and 'swc-awc')
80 if not specified by searchdir_algo linesearch_algo, and set
81 need_init_orbs to True. Otherwise, use search direction and line
82 search algorithms for excited state calculation ('l-sr1p' and
83 'max-step'), and set need_init_orbs to False.
84 searchdir_algo: str, dict or instance
85 Search direction algorithm. Can be one of the algorithms available
86 in sd_etdm.py:
87 'sd': Steepest descent
88 'fr-cg': Fletcher-Reeves conjugate gradient
89 'l-bfgs': Limited-memory BFGS
90 'l-bfgs-p': Limited-memory BFGS with preconditioner presented
91 in :doi:`10.1016/j.cpc.2021.108047` (default when
92 excited_state is False)
93 'l-sr1p': limited-memory SR1 algorithm presented in
94 :doi:`10.1021/acs.jctc.0c00597` (default when excited_state
95 is True)
96 The default memory for 'l-bfgs'/'l-bfgs-p' and 'l-sr1p' is 3 and
97 20, respectively, and can be changed by supplying a dictionary:
98 {'name': name, 'memory': memory}, where name should be 'l-bfgs',
99 'l-bfgs-p' or 'l-sr1p' and memory should be an int.
100 To use the generalized mode following (GMF) method for excited
101 states, append '_gmf' to the search direction algorithm name. E.g.
102 'l-bfgs-p_gmf'.
103 linesearch_algo: str, dict or instance
104 Line search algorithm. Can be one of the algorithms available
105 in ls_etdm.py:
106 'max-step': The quasi-Newton step is scaled if it exceeds a
107 maximum step length (default when excited_state is True).
108 The default maximum step length is 0.20, and can be changed
109 by supplying a dictionary:
110 {'name': 'max-step', 'max_step': max_step}, where max_step
111 should be a float
112 'swc-awc': Line search with Wolfe conditions (default when
113 excited_state is False)
114 partial_diagonalizer: 'str' or dict
115 Algorithm for partial diagonalization of the electronic Hessian if
116 GMF is used. Default is 'Davidson'.
117 update_ref_orbs_counter: int
118 When to update the coefficients of the reference orbitals. Default
119 is 20 iterations.
120 update_ref_orbs_canonical: bool
121 If True, the coefficients of the reference orbitals are updated to
122 canonical orbital coefficients, otherwise use the optimal orbital
123 coefficients (default).
124 use_prec: bool
125 If True (default) use a preconditioner. The preconditioner is
126 calculated as the inverse of a diagonal approximation of the
127 Hessian (see :doi:`10.1021/j100322a012`) apart for 'l-bfgs-p',
128 which uses the composite preconditioner presented in
129 :doi:`10.1016/j.cpc.2021.108047`.
130 update_precond_counter: int
131 When to update the preconditioner. Default is 1000 iterations.
132 representation: 'str'
133 How to store the elements of the anti-Hermitian matrix for the
134 matrix exponential. Can be one of 'sparse' (default), 'full',
135 'u-invar'. The latter can be used only for unitary invariant
136 functionals, such as Kohn-Sham functionals when the occupied
137 orbitals have the same occupation number, but not for orbital
138 density dependent functionals, such as when using PZ-SIC.
139 matrix_exp: 'str'
140 Algorithm for calculating the matrix exponential and the gradient
141 with respect to the elements of the anti-Hermitian matrix for the
142 exponential transformation. Can be one of 'pade-approx' (default),
143 'egdecomp', 'egdecomp-u-invar' (the latter can be used only with
144 'u-invar' representation).
145 functional: str, dict or instance
146 Type of functional. Can be one of:
147 'ks': The functional as specified in the GPAW calculator is
148 used (default)
149 'pz-sic': Apply the Perdew-Zunger self-interaction correction
150 on top of the functional as specified in the GPAW
151 calculator. Dy default full SIC is applied. A scaling
152 factor for SIC can be given by supplying a dictionary:
153 functional={'name': 'pz-sic', 'scaling_factor': (a, a)},
154 where a is the scaling factor (float).
155 need_init_orbs: bool
156 If True (default when excited_state is False), obtain initial
157 orbitals from eigendecomposition of the Hamiltonian matrix. If
158 False (default when excited_state is True), use orbitals stored in
159 wfs object as initial guess.
160 orthonormalization: str
161 Method to orthonormalize the orbitals. Can be one of 'gramschmidt'
162 (Gram-Schmidt orthonormalization, default), 'loewdin' (Loewdin
163 orthonormalization) or 'diag' (eigendecomposition of the
164 Hamiltonian matrix).
165 randomizeorbitals: numpy.random.Generator
166 Optional RNG to add random noise to the initial guess orbitals,
167 default is to not add noise to them
168 checkgraderror: bool
169 If True, can be used to check the error in the estimation of the
170 gradient (only with representation 'full'). Default is False.
171 need_localization: bool
172 If True (default), localize initial guess orbitals. Requires a
173 specification of localizationtype, otherwise it is set to False.
174 Recommended for calculations with PZ-SIC.
175 localizationtype: str
176 Method for localizing the initial guess orbitals. Can be one of:
177 'pz': Unitary optimization among occupied orbitals (subspace
178 optimization) with PZ-SIC
179 'pm': Pipek-Mezey localization (recommended for PZ-SIC)
180 'fb' Foster-Boys localization
181 Default is None, meaning that no localization is performed.
182 localizationseed: int
183 Seed for Pipek-Mezey or Foster-Boys localization. Default is
184 None (no seed is used).
185 constraints: list of lists of int
186 List of constraints on the orbital rotation for each k-point. If
187 a constraint is given as a pair of orbital indices, the rotation
188 between these two orbitals is constrained. If a constraint is
189 given as a single index, the orbital with this index is frozen.
190 E.g.: calc.set(eigensolver=LCAOETDM(constraints=[[[i]], [[j]]]))
191 will freeze orbital i in the first k-point and orbital j in the
192 second k-point.
193 subspace_convergence: float
194 Tolerance on the norm of the gradient for convergence of the
195 subspace optimization with PZ-SIC.
196 """
198 assert representation in ['sparse', 'u-invar', 'full'], 'Value Error'
199 assert matrix_exp in ['egdecomp', 'egdecomp-u-invar', 'pade-approx'], \
200 'Value Error'
201 if matrix_exp == 'egdecomp-u-invar':
202 assert representation == 'u-invar', 'Use u-invar representation ' \
203 'with egdecomp-u-invar'
204 assert orthonormalization in ['gramschmidt', 'loewdin', 'diag'], \
205 'Value Error'
207 self.sda = searchdir_algo
208 self.lsa = linesearch_algo
209 self.partial_diagonalizer = partial_diagonalizer
210 self.localizationseed = localizationseed
211 self.update_ref_orbs_counter = update_ref_orbs_counter
212 self.update_ref_orbs_canonical = update_ref_orbs_canonical
213 self.update_precond_counter = update_precond_counter
214 self.use_prec = use_prec
215 self.matrix_exp = matrix_exp
216 self.localizationtype = localizationtype
217 self.need_localization = need_localization
218 self.need_init_orbs = need_init_orbs
219 self.randomizeorbitals = randomizeorbitals
220 self.representation = representation
221 self.orthonormalization = orthonormalization
222 self.constraints = constraints
223 self.subspace_convergence = subspace_convergence
224 self.released_subspace = False
225 self.excited_state = excited_state
226 self.name = 'etdm-lcao'
227 self.iters = 0
228 self.eg_count = 0
229 self.subspace_iters = 0
230 self.restart = False
231 self.gmf = False
232 self.check_inputs_and_init_search_algo()
234 self.checkgraderror = checkgraderror
235 self._norm_commutator, self._norm_grad = 0., 0.
236 self.error = 0
237 self.e_sic = 0.0
238 self.subspace_optimization = False
240 # these are things we cannot initialize now
241 self.func_settings = functional
242 self.dtype = None
243 self.nkpts = None
244 self.gd = None
245 self.nbands = None
247 # values: vectors of the elements of matrices, keys: kpt number
248 self.a_vec_u = {} # for the elements of the skew-Hermitian matrix A
249 self.a_vec_oo_u = {}
250 self.a_vec_ov_u = {}
251 self.a_vec_all_u = {}
252 self.g_vec_u = {} # for the elements of the gradient matrix G
253 self.g_vec_oo_u = {}
254 self.g_vec_ov_u = {}
255 self.g_vec_all_u = {}
256 self.evecs = {} # eigenvectors for i*a_vec_u
257 self.evals = {} # eigenvalues for i*a_vec_u
258 self.ind_up = {}
259 self.ind_oo_up = {}
260 self.ind_ov_up = {}
261 self.ind_sparse_up = {}
262 self.ind_all_up = {}
263 self.n_dim = {}
264 self.n_dim_oo = {}
265 self.n_dim_all = {}
266 self.alpha = 1.0 # step length
267 self.phi_2i = [None, None] # energy at last two iterations
268 self.der_phi_2i = [None, None] # energy gradient w.r.t. alpha
269 self.hess = {} # approximate Hessian
271 # for mom
272 self.initial_occupation_numbers = None
274 # in this attribute we store the object specific to each mode
275 self.dm_helper = None
277 self.initialized = False
279 def check_inputs_and_init_search_algo(self):
280 defaults = self.set_defaults()
281 if self.sda is None:
282 self.sda = defaults['searchdir_algo']
283 if self.lsa is None:
284 self.lsa = defaults['linesearch_algo']
285 if self.need_init_orbs is None:
286 self.need_init_orbs = defaults['need_init_orbs']
287 if self.localizationtype is None:
288 self.need_localization = False
290 self.searchdir_algo = search_direction(
291 self.sda, self, self.partial_diagonalizer)
292 sd_name = self.searchdir_algo.name.split('_')
293 if sd_name[0] == 'l-bfgs-p' and not self.use_prec:
294 raise ValueError('Use l-bfgs-p with use_prec=True')
295 if len(sd_name) == 2:
296 if sd_name[1] == 'gmf':
297 self.searchdir_algo.name = sd_name[0]
298 self.gmf = True
299 self.g_vec_u_original = None
300 self.pd = self.partial_diagonalizer
302 self.line_search = line_search_algorithm(self.lsa,
303 self.evaluate_phi_and_der_phi,
304 self.searchdir_algo)
306 def set_defaults(self):
307 if self.excited_state:
308 return {'searchdir_algo': 'l-sr1p',
309 'linesearch_algo': 'max-step',
310 'need_init_orbs': False}
311 else:
312 return {'searchdir_algo': 'l-bfgs-p',
313 'linesearch_algo': 'swc-awc',
314 'need_init_orbs': True}
316 def __repr__(self):
318 sda_name = self.searchdir_algo.name
319 lsa_name = self.line_search.name
320 if isinstance(self.func_settings, basestring):
321 func_name = self.func_settings
322 else:
323 func_name = self.func_settings['name']
324 if self.gmf:
325 if isinstance(self.pd, basestring):
326 pd_name = self.pd
327 else:
328 pd_name = self.pd['name']
330 add = ''
331 pd_add = ''
332 if self.gmf:
333 add = ' with minimum mode following'
334 pardi = {'Davidson': 'Finite difference generalized Davidson '
335 'algorithm'}
336 pd_add = ' ' \
337 'Partial diagonalizer: {}\n'.format(
338 pardi[pd_name])
340 sds = {'sd': 'Steepest Descent',
341 'fr-cg': 'Fletcher-Reeves conj. grad. method',
342 'l-bfgs': 'L-BFGS algorithm',
343 'l-bfgs-p': 'L-BFGS algorithm with preconditioning',
344 'l-sr1p': 'Limited-memory SR1P algorithm'}
346 lss = {'max-step': 'step size equals one',
347 'swc-awc': 'Inexact line search based on cubic interpolation,\n'
348 ' strong and approximate Wolfe '
349 'conditions'}
351 repr_string = 'Direct minimisation' + add + ' using exponential ' \
352 'transformation.\n'
353 repr_string += ' ' \
354 'Search ' \
355 'direction: {}\n'.format(sds[sda_name] + add)
356 repr_string += pd_add
357 repr_string += ' ' \
358 'Line ' \
359 'search: {}\n'.format(lss[lsa_name])
360 repr_string += ' ' \
361 'Preconditioning: {}\n'.format(self.use_prec)
362 repr_string += ' ' \
363 'Orbital-density self-interaction ' \
364 'corrections: {}\n'.format(func_name)
365 repr_string += ' ' \
366 'WARNING: do not use it for metals as ' \
367 'occupation numbers are\n' \
368 ' ' \
369 'not found variationally\n'
371 return repr_string
373 def initialize(self, gd, dtype, nbands, nkpts, nao, using_blacs,
374 bd_comm_size, kpt_u):
376 assert nbands == nao, \
377 'Please, use: nbands=\'nao\''
378 if not bd_comm_size == 1:
379 raise BadParallelization(
380 'Band parallelization is not supported')
381 if using_blacs:
382 raise BadParallelization(
383 'ScaLapack parallelization is not supported')
385 self.gd = gd
386 self.dtype = dtype
387 self.nbands = nbands
388 self.nkpts = nkpts
390 for kpt in kpt_u:
391 u = self.kpointval(kpt)
392 # dimensionality of the problem.
393 # this implementation rotates among all bands
394 self.n_dim_all[u] = self.nbands
396 self.initialized = True
398 # need reference orbitals
399 self.dm_helper = None
401 def initialize_dm_helper(self, wfs, ham, dens, log):
403 self.dm_helper = ETDMHelperLCAO(
404 wfs, dens, ham, self.nkpts, self.func_settings,
405 diagonalizer=None,
406 orthonormalization=self.orthonormalization,
407 need_init_orbs=self.need_init_orbs)
408 self.need_init_orbs = self.dm_helper.need_init_orbs
410 # randomize orbitals?
411 if self.randomizeorbitals is not None:
412 for kpt in wfs.kpt_u:
413 self.randomize_orbitals_kpt(wfs, kpt)
414 self.randomizeorbitals = None
416 if not wfs.coefficients_read_from_file:
417 wfs.calculate_occupation_numbers(dens.fixed)
419 self.initial_sort_orbitals(wfs)
421 # initialize matrices
422 self.set_variable_matrices(wfs.kpt_u)
424 # localize orbitals?
425 self.localize(wfs, dens, ham, log)
427 # MOM
428 self.initialize_mom_reference_orbitals(wfs, dens)
430 for kpt in wfs.kpt_u:
431 f_unique = np.unique(kpt.f_n)
432 if len(f_unique) > 2 and self.representation == 'u-invar':
433 warnings.warn("Use representation == 'sparse' when "
434 "there are unequally occupied orbitals "
435 "as the functional is not unitary invariant")
437 # set reference orbitals
438 self.dm_helper.set_reference_orbitals(wfs, self.n_dim)
440 def set_variable_matrices(self, kpt_u):
442 # Matrices are sparse and Skew-Hermitian.
443 # They have this structure:
444 # A_BigMatrix =
445 #
446 # ( A_1 A_2 )
447 # ( -A_2.T.conj() 0 )
448 #
449 # where 0 is a zero-matrix of size of (M-N) * (M-N)
450 #
451 # A_1 i skew-hermitian matrix of N * N,
452 # N-number of occupied states
453 # A_2 is matrix of size of (M-N) * N,
454 # M - number of basis functions
455 #
456 # if the energy functional is unitary invariant
457 # then A_1 = 0
458 # (see Hutter J et. al, J. Chem. Phys. 101, 3862 (1994))
459 #
460 # We will keep A_1 as we would like to work with metals,
461 # SIC, and molecules with different occupation numbers.
462 # this corresponds to 'sparse' representation
463 #
464 # Thus, for the 'sparse' we need to store upper
465 # triangular part of A_1, and matrix A_2, so in total
466 # (M-N) * N + N * (N - 1)/2 = N * (M - (N + 1)/2) elements
467 #
468 # we will store these elements as a vector and
469 # also will store indices of the A_BigMatrix
470 # which correspond to these elements.
471 #
472 # 'u-invar' corresponds to the case when we want to
473 # store only A_2, that is this representation is sparser
475 for kpt in kpt_u:
476 n_occ = get_n_occ(kpt)[0]
477 u = self.kpointval(kpt)
478 self.n_dim[u] = deepcopy(self.n_dim_all[u])
479 self.n_dim_oo[u] = n_occ
480 # M - one dimension of the A_BigMatrix
481 M = self.n_dim_all[u]
482 i1_oo, i2_oo = [], []
483 for i in range(n_occ):
484 for j in range(i + 1, n_occ):
485 i1_oo.append(i)
486 i2_oo.append(j)
487 self.ind_oo_up[u] = (np.asarray(i1_oo), np.asarray(i2_oo))
488 i1_ov, i2_ov = [], []
489 for i in range(n_occ):
490 for j in range(n_occ, M):
491 i1_ov.append(i)
492 i2_ov.append(j)
493 self.ind_ov_up[u] = (np.asarray(i1_ov), np.asarray(i2_ov))
494 i1_sparse, i2_sparse = [], []
495 for i in range(n_occ):
496 for j in range(i + 1, M):
497 i1_sparse.append(i)
498 i2_sparse.append(j)
499 self.ind_sparse_up[u] = (np.asarray(i1_sparse),
500 np.asarray(i2_sparse))
501 if self.representation == 'u-invar':
502 self.ind_all_up[u] = self.ind_ov_up[u]
503 if self.representation == 'sparse':
504 self.ind_all_up[u] = self.ind_sparse_up[u]
505 elif self.representation == 'full' and self.dtype == complex:
506 # Take indices of all upper triangular and diagonal
507 # elements of A_BigMatrix
508 self.ind_all_up[u] = np.triu_indices(self.n_dim[u])
510 shape_of_oo = len(self.ind_oo_up[u][0])
511 shape_of_ov = len(self.ind_ov_up[u][0])
512 shape_of_all = len(self.ind_all_up[u][0])
514 self.a_vec_oo_u[u] = np.zeros(shape=shape_of_oo, dtype=self.dtype)
515 self.a_vec_ov_u[u] = np.zeros(shape=shape_of_ov, dtype=self.dtype)
516 self.a_vec_all_u[u] = np.zeros(
517 shape=shape_of_all, dtype=self.dtype)
518 self.g_vec_oo_u[u] = np.zeros(shape=shape_of_oo, dtype=self.dtype)
519 self.g_vec_ov_u[u] = np.zeros(shape=shape_of_ov, dtype=self.dtype)
520 self.g_vec_all_u[u] = np.zeros(
521 shape=shape_of_all, dtype=self.dtype)
522 self.evecs[u] = None
523 self.evals[u] = None
525 # All constraints passed as a list of a single orbital index are
526 # converted to all lists of orbital pairs involving that orbital
527 # index
528 if self.constraints:
529 self.constraints[u] = convert_constraints(
530 self.constraints[u], self.n_dim[u],
531 len(kpt.f_n[kpt.f_n > 1e-10]), self.representation)
533 # If there are no degrees of freedom no need to optimize.
534 # Indicated by setting n_dim to 0, as n_dim can never be 0
535 # otherwise.
536 for k in self.ind_all_up:
537 if not self.ind_all_up[k][0].size \
538 or not self.ind_all_up[k][1].size:
539 self.n_dim_all[k] = 0 # Skip full space optimization
540 if not self.ind_oo_up[k][0].size \
541 or not self.ind_oo_up[k][1].size:
542 self.n_dim_oo[k] = 0 # Skip PZ localization if requested
544 self.ind_up = deepcopy(self.ind_all_up)
545 self.a_vec_u = deepcopy(self.a_vec_all_u)
546 self.g_vec_u = deepcopy(self.g_vec_all_u)
548 # This conversion makes it so that constraint-related functions can
549 # iterate through a list of no constraints rather than checking for
550 # None every time
551 if self.constraints is None:
552 self.constraints = [[] for _ in range(len(kpt_u))]
554 self.iters = 1
556 def localize(self, wfs, dens, ham, log):
557 if self.need_localization:
558 localize_orbitals(
559 wfs, dens, ham, log, self.localizationtype,
560 seed=self.localizationseed)
561 self.need_localization = False
563 def lock_subspace(self, subspace='oo'):
564 self.subspace_optimization = True
565 self.subspace_iters = 1
566 if subspace == 'oo':
567 self.n_dim = deepcopy(self.n_dim_oo)
568 self.ind_up = deepcopy(self.ind_oo_up)
569 self.a_vec_u = deepcopy(self.a_vec_oo_u)
570 self.g_vec_u = deepcopy(self.g_vec_oo_u)
571 elif subspace == 'ov':
572 self.n_dim = deepcopy(self.n_dim_all)
573 self.ind_up = deepcopy(self.ind_ov_up)
574 self.a_vec_u = deepcopy(self.a_vec_ov_u)
575 self.g_vec_u = deepcopy(self.g_vec_ov_u)
576 self.alpha = 1.0
577 self.phi_2i = [None, None]
578 self.der_phi_2i = [None, None]
580 def release_subspace(self):
581 self.subspace_optimization = False
582 self.released_subspace = True
583 self.n_dim = deepcopy(self.n_dim_all)
584 self.ind_up = deepcopy(self.ind_all_up)
585 self.a_vec_u = deepcopy(self.a_vec_all_u)
586 self.g_vec_u = deepcopy(self.g_vec_all_u)
587 self.alpha = 1.0
588 self.phi_2i = [None, None]
589 self.der_phi_2i = [None, None]
591 def iterate(self, ham, wfs, dens):
592 """
593 One iteration of direct optimization
594 for occupied orbitals
596 :param ham:
597 :param wfs:
598 :param dens:
599 :return:
600 """
601 with wfs.timer('Direct Minimisation step'):
602 self.update_ref_orbitals(wfs, ham, dens)
604 a_vec_u = self.a_vec_u
605 alpha = self.alpha
606 phi_2i = self.phi_2i
607 der_phi_2i = self.der_phi_2i
608 c_ref = self.dm_helper.reference_orbitals
610 if self.iters == 1 or self.released_subspace or \
611 (self.subspace_optimization and self.subspace_iters == 1):
612 phi_2i[0], g_vec_u = \
613 self.get_energy_and_gradients(
614 a_vec_u, ham, wfs, dens, c_ref)
615 else:
616 g_vec_u = self.g_vec_u_original if self.gmf \
617 and not self.subspace_optimization else self.g_vec_u
619 make_pd = False
620 if self.gmf and not self.subspace_optimization:
621 with wfs.timer('Partial Hessian diagonalization'):
622 self.searchdir_algo.update_eigenpairs(
623 g_vec_u, wfs, ham, dens)
624 # The diagonal Hessian approximation must be positive-definite
625 make_pd = True
627 with wfs.timer('Preconditioning:'):
628 precond = self.get_preconditioning(
629 wfs, self.use_prec, make_pd=make_pd)
631 with wfs.timer('Get Search Direction'):
632 # calculate search direction according to chosen
633 # optimization algorithm (e.g. L-BFGS)
634 p_vec_u = self.searchdir_algo.update_data(
635 wfs, a_vec_u, g_vec_u, precond=precond,
636 subspace=self.subspace_optimization)
638 # recalculate derivative with new search direction
639 der_phi_2i[0] = 0.0
640 for k in g_vec_u:
641 der_phi_2i[0] += g_vec_u[k].conj() @ p_vec_u[k]
642 der_phi_2i[0] = der_phi_2i[0].real
643 der_phi_2i[0] = wfs.kd.comm.sum_scalar(der_phi_2i[0])
645 alpha, phi_alpha, der_phi_alpha, g_vec_u = \
646 self.line_search.step_length_update(
647 a_vec_u, p_vec_u, wfs, ham, dens, c_ref, phi_0=phi_2i[0],
648 der_phi_0=der_phi_2i[0], phi_old=phi_2i[1],
649 der_phi_old=der_phi_2i[1], alpha_max=5.0, alpha_old=alpha,
650 kpdescr=wfs.kd)
652 if wfs.gd.comm.size > 1:
653 with wfs.timer('Broadcast gradients'):
654 alpha_phi_der_phi = np.array([alpha, phi_2i[0],
655 der_phi_2i[0]])
656 wfs.gd.comm.broadcast(alpha_phi_der_phi, 0)
657 alpha = alpha_phi_der_phi[0]
658 phi_2i[0] = alpha_phi_der_phi[1]
659 der_phi_2i[0] = alpha_phi_der_phi[2]
660 for kpt in wfs.kpt_u:
661 k = self.kpointval(kpt)
662 wfs.gd.comm.broadcast(g_vec_u[k], 0)
664 # calculate new matrices for optimal step length
665 for k in a_vec_u:
666 a_vec_u[k] += alpha * p_vec_u[k]
667 self.alpha = alpha
668 self.g_vec_u = g_vec_u
669 if self.subspace_optimization:
670 self.subspace_iters += 1
671 else:
672 self.iters += 1
674 # and 'shift' phi, der_phi for the next iteration
675 phi_2i[1], der_phi_2i[1] = phi_2i[0], der_phi_2i[0]
676 phi_2i[0], der_phi_2i[0] = phi_alpha, der_phi_alpha,
678 if self.subspace_optimization:
679 self.error = np.inf # Do not consider this converged!
681 def get_grad_norm(self):
682 norm = 0.0
683 for k in self.g_vec_u.keys():
684 norm += np.linalg.norm(self.g_vec_u[k])
685 return norm
687 def get_energy_and_gradients(self, a_vec_u, ham, wfs, dens,
688 c_ref):
690 """
691 Energy E = E[C_ref exp(A)]. Gradients G_ij[C, A] = dE/dA_ij
693 :param wfs:
694 :param ham:
695 :param dens:
696 :param a_vec_u: A
697 :param c_ref: C_ref
698 :return:
699 """
701 self.rotate_wavefunctions(wfs, a_vec_u, c_ref)
703 e_total = self.update_ks_energy(ham, wfs, dens)
705 with wfs.timer('Calculate gradients'):
706 g_vec_u = {}
707 self.error = 0.0
708 self.e_sic = 0.0 # this is odd energy
709 for kpt in wfs.kpt_u:
710 k = self.kpointval(kpt)
711 if self.n_dim[k] == 0:
712 g_vec_u[k] = np.zeros_like(a_vec_u[k])
713 continue
714 g_vec_u[k], error = self.dm_helper.calc_grad(
715 wfs, ham, kpt, self.evecs[k], self.evals[k],
716 self.matrix_exp, self.representation, self.ind_up[k],
717 self.constraints[k])
719 if hasattr(self.dm_helper.func, 'e_sic_by_orbitals'):
720 self.e_sic \
721 += self.dm_helper.func.e_sic_by_orbitals[k].sum()
723 self.error += error
724 self.error = wfs.kd.comm.sum_scalar(self.error)
725 self.e_sic = wfs.kd.comm.sum_scalar(self.e_sic)
727 self.eg_count += 1
729 if self.representation == 'full' and self.checkgraderror:
730 self._norm_commutator = 0.0
731 for kpt in wfs.kpt_u:
732 u = self.kpointval(kpt)
733 a_mat = vec2skewmat(a_vec_u[u], self.n_dim[u],
734 self.ind_up[u], wfs.dtype)
735 g_mat = vec2skewmat(g_vec_u[u], self.n_dim[u],
736 self.ind_up[u], wfs.dtype)
738 tmp = np.linalg.norm(g_mat @ a_mat - a_mat @ g_mat)
739 if self._norm_commutator < tmp:
740 self._norm_commutator = tmp
742 tmp = np.linalg.norm(g_mat)
743 if self._norm_grad < tmp:
744 self._norm_grad = tmp
746 return e_total + self.e_sic, g_vec_u
748 def update_ks_energy(self, ham, wfs, dens):
749 """
750 Update Kohn-Sham energy
751 It assumes the temperature is zero K.
752 """
754 dens.update(wfs)
755 ham.update(dens, wfs, False)
757 return ham.get_energy(0.0, wfs, False)
759 def evaluate_phi_and_der_phi(self, a_vec_u, p_mat_u, alpha,
760 wfs, ham, dens, c_ref,
761 phi=None, g_vec_u=None):
762 """
763 phi = f(x_k + alpha_k*p_k)
764 der_phi = \\grad f(x_k + alpha_k*p_k) \\cdot p_k
765 :return: phi, der_phi # floats
766 """
767 if phi is None or g_vec_u is None:
768 x_mat_u = {k: a_vec_u[k] + alpha * p_mat_u[k] for k in a_vec_u}
769 phi, g_vec_u = self.get_energy_and_gradients(
770 x_mat_u, ham, wfs, dens, c_ref)
772 # If GMF is used save the original gradient and invert the parallel
773 # projection onto the eigenvectors with negative eigenvalues
774 if self.gmf and not self.subspace_optimization:
775 self.g_vec_u_original = deepcopy(g_vec_u)
776 g_vec_u = self.searchdir_algo.invert_parallel_grad(g_vec_u)
778 der_phi = 0.0
779 for k in p_mat_u:
780 der_phi += g_vec_u[k].conj() @ p_mat_u[k]
782 der_phi = der_phi.real
783 der_phi = wfs.kd.comm.sum_scalar(der_phi)
785 return phi, der_phi, g_vec_u
787 def update_ref_orbitals(self, wfs, ham, dens):
788 """
789 Update reference orbitals
791 :param wfs:
792 :param ham:
793 :return:
794 """
796 if self.representation == 'full':
797 badgrad = self._norm_commutator > self._norm_grad / 3. and \
798 self.checkgraderror
799 else:
800 badgrad = False
801 counter = self.update_ref_orbs_counter
802 if (self.iters % counter == 0 and self.iters > 1) or \
803 (self.restart and self.iters > 1) or badgrad:
804 self.iters = 1
805 if self.update_ref_orbs_canonical or self.restart:
806 self.get_canonical_representation(ham, wfs, dens)
807 else:
808 self.set_ref_orbitals_and_a_vec(wfs)
810 # Erase memory of search direction algorithm
811 self.searchdir_algo.reset()
813 def get_preconditioning(self, wfs, use_prec, make_pd=False):
815 if not use_prec:
816 return None
818 if self.searchdir_algo.name == 'l-bfgs-p':
819 beta0 = self.searchdir_algo.beta_0
820 gamma = 0.25
821 else:
822 beta0 = 1.0
823 gamma = 0.0
825 counter = self.update_precond_counter
826 precond = {}
827 for kpt in wfs.kpt_u:
828 k = self.kpointval(kpt)
829 w = kpt.weight / (3.0 - wfs.nspins)
830 if self.iters % counter == 0 or self.iters == 1:
831 self.hess[k] = get_approx_analytical_hessian(
832 kpt, self.dtype, ind_up=self.ind_up[k])
833 if make_pd:
834 if self.dtype == float:
835 self.hess[k] = np.abs(self.hess[k])
836 else:
837 self.hess[k] = np.abs(self.hess[k].real) \
838 + 1.0j * np.abs(self.hess[k].imag)
839 hess = self.hess[k]
840 precond[k] = np.zeros_like(hess)
841 correction = w * gamma * beta0 ** (-1)
842 if self.searchdir_algo.name != 'l-bfgs-p':
843 correction = np.zeros_like(hess)
844 zeros = abs(hess) < 1.0e-4
845 correction[zeros] = 1.0
846 precond[k] += 1. / ((1 - gamma) * hess.real + correction)
847 if self.dtype == complex:
848 precond[k] += 1.j / ((1 - gamma) * hess.imag + correction)
850 return precond
852 def get_canonical_representation(self, ham, wfs, dens,
853 sort_eigenvalues=False):
854 """
855 Choose canonical orbitals as the orbitals that diagonalize the
856 Lagrange matrix. It is probably necessary to do a subspace rotation
857 with equally occupied orbitals as the total energy is unitary invariant
858 within equally occupied subspaces.
859 """
861 with ((wfs.timer('Get canonical representation'))):
862 for kpt in wfs.kpt_u:
863 self.dm_helper.update_to_canonical_orbitals(
864 wfs, ham, kpt, self.update_ref_orbs_canonical,
865 self.restart)
867 if self.update_ref_orbs_canonical or self.restart:
868 wfs.calculate_occupation_numbers(dens.fixed)
869 sort_orbitals_according_to_occ(
870 wfs, self.constraints, update_mom=True)
872 if sort_eigenvalues:
873 sort_orbitals_according_to_energies(
874 ham, wfs, self.constraints)
876 self.set_ref_orbitals_and_a_vec(wfs)
878 def set_ref_orbitals_and_a_vec(self, wfs):
879 self.dm_helper.set_reference_orbitals(wfs, self.n_dim)
880 for kpt in wfs.kpt_u:
881 u = self.kpointval(kpt)
882 self.a_vec_u[u] = np.zeros_like(self.a_vec_u[u])
884 def reset(self):
885 self.dm_helper = None
886 self.error = np.inf
887 self.initialized = False
888 self.searchdir_algo.reset()
890 def todict(self):
891 ret = {'name': self.name,
892 'searchdir_algo': self.searchdir_algo.todict(),
893 'linesearch_algo': self.line_search.todict(),
894 'update_ref_orbs_counter': self.update_ref_orbs_counter,
895 'update_ref_orbs_canonical': self.update_ref_orbs_canonical,
896 'update_precond_counter': self.update_precond_counter,
897 'use_prec': self.use_prec,
898 'matrix_exp': self.matrix_exp,
899 'representation': self.representation,
900 'functional': self.func_settings,
901 'orthonormalization': self.orthonormalization,
902 'randomizeorbitals': self.randomizeorbitals,
903 'checkgraderror': self.checkgraderror,
904 'localizationtype': self.localizationtype,
905 'localizationseed': self.localizationseed,
906 'need_localization': self.need_localization,
907 'need_init_orbs': self.need_init_orbs,
908 'constraints': self.constraints,
909 'subspace_convergence': self.subspace_convergence,
910 'excited_state': self.excited_state
911 }
912 if self.gmf:
913 ret['partial_diagonalizer'] = \
914 self.searchdir_algo.partial_diagonalizer.todict()
915 return ret
917 def rotate_wavefunctions(self, wfs, a_vec_u, c_ref):
919 """
920 Apply unitary transformation U = exp(A) to
921 the orbitals c_ref
923 :param wfs:
924 :param a_vec_u:
925 :param c_ref:
926 :return:
927 """
929 with wfs.timer('Unitary rotation'):
930 for kpt in wfs.kpt_u:
931 k = self.kpointval(kpt)
932 if self.n_dim[k] == 0:
933 continue
935 u_nn = self.get_exponential_matrix_kpt(wfs, kpt, a_vec_u)
937 self.dm_helper.appy_transformation_kpt(
938 wfs, u_nn.T, kpt, c_ref[k], False, False)
940 with wfs.timer('Calculate projections'):
941 self.dm_helper.update_projections(wfs, kpt)
943 def get_exponential_matrix_kpt(self, wfs, kpt, a_vec_u):
944 """
945 Get unitary matrix U as the exponential of a skew-Hermitian
946 matrix A (U = exp(A))
947 """
949 k = self.kpointval(kpt)
951 if self.gd.comm.rank == 0:
952 if self.matrix_exp == 'egdecomp-u-invar' and \
953 self.representation == 'u-invar':
954 n_occ = get_n_occ(kpt)[0]
955 n_v = self.n_dim[k] - n_occ
956 a_mat = a_vec_u[k].reshape(n_occ, n_v)
957 else:
958 a_mat = vec2skewmat(a_vec_u[k], self.n_dim[k],
959 self.ind_up[k], self.dtype)
961 if self.matrix_exp == 'pade-approx':
962 # this function takes a lot of memory
963 # for large matrices... what can we do?
964 with wfs.timer('Pade Approximants'):
965 u_nn = np.ascontiguousarray(expm(a_mat))
966 elif self.matrix_exp == 'egdecomp':
967 # this method is based on diagonalization
968 with wfs.timer('Eigendecomposition'):
969 u_nn, evecs, evals = expm_ed(a_mat, evalevec=True)
970 elif self.matrix_exp == 'egdecomp-u-invar':
971 with wfs.timer('Eigendecomposition'):
972 u_nn = expm_ed_unit_inv(a_mat, oo_vo_blockonly=False)
974 with wfs.timer('Broadcast u_nn'):
975 if self.gd.comm.rank != 0:
976 u_nn = np.zeros(shape=(self.n_dim[k], self.n_dim[k]),
977 dtype=wfs.dtype)
978 self.gd.comm.broadcast(u_nn, 0)
980 if self.matrix_exp == 'egdecomp':
981 with wfs.timer('Broadcast evecs and evals'):
982 if self.gd.comm.rank != 0:
983 evecs = np.zeros(shape=(self.n_dim[k], self.n_dim[k]),
984 dtype=complex)
985 evals = np.zeros(shape=self.n_dim[k],
986 dtype=float)
987 self.gd.comm.broadcast(evecs, 0)
988 self.gd.comm.broadcast(evals, 0)
989 self.evecs[k], self.evals[k] = evecs, evals
991 return u_nn
993 def check_assertions(self, wfs, dens):
995 assert dens.mixer.driver.basemixerclass.name == 'no-mixing', \
996 'Please, use: mixer={\'backend\': \'no-mixing\'}'
997 if wfs.occupations.name != 'mom':
998 errormsg = \
999 'Please, use occupations={\'name\': \'fixed-uniform\'}'
1000 assert wfs.occupations.name == 'fixed-uniform', errormsg
1002 def initialize_mom_reference_orbitals(self, wfs, dens):
1003 # Reinitialize the MOM reference orbitals
1004 # after orthogonalization/localization
1005 occ_name = getattr(wfs.occupations, 'name', None)
1006 if occ_name == 'mom':
1007 wfs.occupations.initialize_reference_orbitals()
1008 wfs.calculate_occupation_numbers(dens.fixed)
1010 def initial_sort_orbitals(self, wfs):
1011 occ_name = getattr(wfs.occupations, "name", None)
1012 if occ_name == 'mom':
1013 update_mom = True
1014 self.initial_occupation_numbers = wfs.occupations.numbers.copy()
1015 else:
1016 update_mom = False
1017 sort_orbitals_according_to_occ(wfs,
1018 self.constraints,
1019 update_mom=update_mom)
1021 def check_mom(self, wfs, dens):
1022 occ_name = getattr(wfs.occupations, "name", None)
1023 if occ_name == 'mom':
1024 wfs.calculate_occupation_numbers(dens.fixed)
1025 self.restart = sort_orbitals_according_to_occ(
1026 wfs, self.constraints, update_mom=True)
1027 return self.restart
1028 else:
1029 return False
1031 def randomize_orbitals_kpt(self, wfs, kpt):
1032 """
1033 Add random noise to orbitals but keep them orthonormal
1034 """
1035 nst = self.nbands
1036 wt = kpt.weight * 0.01
1037 arand = wt * random_a((nst, nst),
1038 wfs.dtype,
1039 rng=self.randomizeorbitals)
1040 arand = arand - arand.T.conj()
1041 wfs.gd.comm.broadcast(arand, 0)
1042 self.dm_helper.appy_transformation_kpt(wfs, expm(arand), kpt)
1044 def calculate_hamiltonian_matrix(self, hamiltonian, wfs, kpt):
1045 H_MM = self.dm_helper.calculate_hamiltonian_matrix(
1046 hamiltonian, wfs, kpt)
1047 return H_MM
1049 def kpointval(self, kpt):
1050 return self.nkpts * kpt.s + kpt.q
1052 @property
1053 def error(self):
1054 return self._error
1056 @error.setter
1057 def error(self, e):
1058 self._error = e
1061def vec2skewmat(a_vec, dim, ind_up, dtype):
1063 a_mat = np.zeros(shape=(dim, dim), dtype=dtype)
1064 a_mat[ind_up] = a_vec
1065 a_mat -= a_mat.T.conj()
1066 np.fill_diagonal(a_mat, a_mat.diagonal() * 0.5)
1067 return a_mat
1070def convert_constraints(constraints, n_dim, n_occ, representation):
1071 """
1072 Parses and checks the user input of constraints. If constraints are passed
1073 as a list of a single orbital index all pairs of orbitals involving this
1074 index are added to the constraints.
1076 :param constraints: List of constraints for one K-point
1077 :param n_dim:
1078 :param n_occ:
1079 :param representation: Unitary invariant, sparse or full representation
1080 determining the electronic degrees of freedom that
1081 need to be constrained
1083 :return: Converted list of constraints
1084 """
1086 new = constraints.copy()
1087 for con in constraints:
1088 assert isinstance(con, list) or isinstance(con, int), \
1089 'Check constraints.'
1090 if isinstance(con, list):
1091 assert len(con) < 3, 'Check constraints.'
1092 if len(con) == 1:
1093 con = con[0] # List of single index
1094 else:
1095 # Make first index always smaller than second index
1096 if representation != 'full' and con[1] < con[0]:
1097 temp = deepcopy(con[0])
1098 con[0] = deepcopy(con[1])
1099 con[1] = temp
1100 check_indices(
1101 con[0], con[1], n_dim, n_occ, representation)
1102 continue
1103 # Add all pairs containing the single index
1104 if isinstance(con, int):
1105 new += find_all_pairs(con, n_dim, n_occ, representation)
1107 # Delete all list containing a single index
1108 done = False
1109 while not done:
1110 done = True
1111 for i in range(len(new)):
1112 if len(new[i]) < 2:
1113 del new[i]
1114 done = False
1115 break
1116 return new
1119def check_indices(ind1, ind2, n_dim, n_occ, representation):
1120 """
1121 Makes sure the user input for the constraints makes sense.
1122 """
1124 assert ind1 != ind2, 'Check constraints.'
1125 if representation == 'full':
1126 assert ind1 < n_dim and ind2 < n_dim, 'Check constraints.'
1127 elif representation == 'sparse':
1128 assert ind1 < n_occ and ind2 < n_dim, 'Check constraints.'
1129 elif representation == 'u-invar':
1130 assert ind1 < n_occ and ind2 >= n_occ and ind2 < n_dim, \
1131 'Check constraints.'
1134def find_all_pairs(ind, n_dim, n_occ, representation):
1135 """
1136 Creates a list of all orbital pairs corresponding to degrees of freedom of
1137 the system containing an orbital index.
1139 :param ind: The orbital index
1140 :param n_dim:
1141 :param n_occ:
1142 :param representation: Unitary invariant, sparse or full, defining what
1143 index pairs correspond to degrees of freedom of the
1144 system
1145 """
1147 pairs = []
1148 if representation == 'u-invar':
1149 # Only ov rotations are degrees of freedom
1150 if ind < n_occ:
1151 for i in range(n_occ, n_dim):
1152 pairs.append([ind, i])
1153 else:
1154 for i in range(n_occ):
1155 pairs.append([i, ind])
1156 else:
1157 if (ind < n_occ and representation == 'sparse') \
1158 or representation == 'full':
1159 # oo and ov rotations are degrees of freedom
1160 for i in range(n_dim):
1161 if i == ind:
1162 continue
1163 pairs.append([i, ind] if i < ind else [ind, i])
1164 if representation == 'full':
1165 # The orbital rotation matrix is not assumed to be
1166 # antihermitian, so the reverse order of indices must be
1167 # added as a second constraint
1168 pairs.append([ind, i] if i < ind else [i, ind])
1169 else:
1170 for i in range(n_occ):
1171 pairs.append([i, ind])
1172 return pairs