1616# Author: Naoki Kitamura
1717#
1818
19- '''
19+ """
2020Diagonalization-free Hartree-Fock density construction with Chebyshev filters.
2121
2222This module provides an experimental SCF driver that approximates the
2323finite-temperature occupation projector by a Chebyshev polynomial expansion.
2424The density matrix is built from filtered basis vectors in an orthogonalized AO
2525representation, avoiding an explicit Fock diagonalization in each SCF cycle.
26- '''
26+ """
2727
2828
2929import numpy
@@ -81,6 +81,7 @@ def block_inv_sqrt(s, thresh=1e-12):
8181 # ブロックを対角に並べる
8282 return s_inv_sqrt
8383
84+
8485def inv_sqrt (s , thresh = 1e-12 , full_inv = False ):
8586 """
8687 Compute an inverse square root of a symmetric positive-definite matrix.
@@ -107,6 +108,7 @@ def inv_sqrt(s, thresh=1e-12, full_inv=False):
107108 else :
108109 return block_inv_sqrt (s , thresh = thresh )
109110
111+
110112def to_csr_matrix (matrix , threshold = 1e-5 ):
111113 """
112114 Convert a dense array to CSR format after dropping small elements.
@@ -126,8 +128,9 @@ def to_csr_matrix(matrix, threshold=1e-5):
126128 matrix = numpy .where (numpy .abs (matrix ) < threshold , 0.0 , matrix )
127129 return csr_matrix (matrix )
128130
131+
129132class ChebyshevSCF (hf .SCF ):
130- '''
133+ """
131134 Restricted Hartree-Fock SCF driver using Chebyshev density filtering.
132135
133136 The class follows the :class:`pyscf.scf.hf.SCF` interface but replaces the
@@ -156,7 +159,8 @@ class ChebyshevSCF(hf.SCF):
156159 an accepted, documented scope limitation rather than a defect to fix
157160 in this driver -- benchmarking and validation of this class should
158161 likewise be restricted to well-gapped closed-shell systems.
159- '''
162+ """
163+
160164 def __init__ (self , mol ):
161165 """
162166 Initialize the Chebyshev SCF object for a PySCF molecule.
@@ -200,11 +204,15 @@ def build(self, mol=None):
200204 The built SCF object.
201205 """
202206 hf .SCF .build (self , mol )
203- self ._keys = self ._keys .union (['chebyshev_degree' ,
204- 'chebyshev_filter_interval' ,
205- 'auto_degree' ,
206- 'degree_safety_factor' ,
207- 'rng_seed' ])
207+ self ._keys = self ._keys .union (
208+ [
209+ "chebyshev_degree" ,
210+ "chebyshev_filter_interval" ,
211+ "auto_degree" ,
212+ "degree_safety_factor" ,
213+ "rng_seed" ,
214+ ]
215+ )
208216 return self
209217
210218 def get_ovlp_inv_sqrt (self , mol = None , thresh = 1e-14 , full_inv = False ):
@@ -225,8 +233,9 @@ def get_ovlp_inv_sqrt(self, mol=None, thresh=1e-14, full_inv=False):
225233 ndarray
226234 AO overlap inverse square-root matrix.
227235 """
228- if mol is None : mol = self .mol
229- ovlp = mol .intor_symmetric ('int1e_ovlp' )
236+ if mol is None :
237+ mol = self .mol
238+ ovlp = mol .intor_symmetric ("int1e_ovlp" )
230239 ovlp_inv_sqrt = inv_sqrt (ovlp , thresh = thresh , full_inv = full_inv )
231240 return ovlp_inv_sqrt
232241
@@ -259,10 +268,11 @@ def _power_method(self, fock, x0, max_cycle=50, tol=1e-6):
259268 if err < tol :
260269 break
261270 e_max = numpy .dot (x1 .T , numpy .dot (fock , x1 )) / numpy .dot (x1 .T , x1 )
262- return e_max [0 ,0 ]
271+ return e_max [0 , 0 ]
263272
264- def _chebyshev_poly_vec (self , v , fock_ortho , mu , degree , beta , a ,
265- vec_threshold = None ):
273+ def _chebyshev_poly_vec (
274+ self , v , fock_ortho , mu , degree , beta , a , vec_threshold = None
275+ ):
266276 """
267277 Apply the Chebyshev occupation polynomial to a basis vector.
268278
@@ -305,7 +315,7 @@ def _chebyshev_poly_vec(self, v, fock_ortho, mu, degree, beta, a,
305315 Filtered vector contribution to the density matrix.
306316 """
307317 if vec_threshold is None :
308- vec_threshold = getattr (self , ' vec_threshold' , 1e-6 )
318+ vec_threshold = getattr (self , " vec_threshold" , 1e-6 )
309319
310320 coeffs = self ._calc_coeff (degree , beta = beta , mu = mu , a = a )
311321 # T0 v_
@@ -316,7 +326,7 @@ def _chebyshev_poly_vec(self, v, fock_ortho, mu, degree, beta, a,
316326 if vec_threshold :
317327 v1 = to_csr_matrix (v1 .toarray (), threshold = vec_threshold )
318328 y = coeffs [0 ] * v0 + coeffs [1 ] * v1
319- for k in range (2 , degree + 1 ):
329+ for k in range (2 , degree + 1 ):
320330 v2 = 2 * fock_ortho @ v1 - v0
321331 if vec_threshold :
322332 v2 = to_csr_matrix (v2 .toarray (), threshold = vec_threshold )
@@ -349,7 +359,7 @@ def _calc_coeff(self, n, N=8192, beta=100.0, mu=0.0, a=1.0):
349359 theta = numpy .pi * numpy .arange (N ) / (N - 1 )
350360 beta_tilde = beta * a
351361 f = 1.0 / (1.0 + numpy .exp (beta_tilde * (numpy .cos (theta ) - mu )))
352- coeffs = numpy .fft .fft (numpy .r_ [f , f [- 2 :0 :- 1 ]]).real [:n + 1 ]
362+ coeffs = numpy .fft .fft (numpy .r_ [f , f [- 2 :0 :- 1 ]]).real [: n + 1 ]
353363 coeffs *= 2.0 / (N - 1 )
354364 coeffs [0 ] *= 0.5
355365 return coeffs
@@ -391,12 +401,14 @@ def _spectral_bounds(self, fock):
391401 # filtered density matrix, and the final energy are exactly
392402 # reproducible for a given (mol, beta, rng_seed), not just
393403 # statistically similar run to run.
394- rng = numpy .random .RandomState (getattr (self , ' rng_seed' , 0 ))
404+ rng = numpy .random .RandomState (getattr (self , " rng_seed" , 0 ))
395405 random_vec = rng .rand (nmo )
396406 random_vec /= numpy .linalg .norm (random_vec )
397407 from pyscf .lib import davidson
408+
398409 def fock_ortho_mv (v ):
399410 return fock_ortho @ v
411+
400412 emin , _ = davidson (fock_ortho_mv , random_vec , precond = precond )
401413 emin -= 0.1
402414
@@ -409,7 +421,9 @@ def fock_ortho_mv(v):
409421 b = (emax + emin ) / 2.0
410422 return a , b , emin , emax , fock_ortho
411423
412- def _degree_for_beta_a (self , beta , a , safety_factor = 2.0 , min_degree = 10 , max_degree = 2000 ):
424+ def _degree_for_beta_a (
425+ self , beta , a , safety_factor = 2.0 , min_degree = 10 , max_degree = 2000
426+ ):
413427 """
414428 Pure formula: suggested Chebyshev degree for a given ``beta`` and
415429 spectral half-width ``a``, without re-estimating ``a`` from a Fock
@@ -436,7 +450,9 @@ def _degree_for_beta_a(self, beta, a, safety_factor=2.0, min_degree=10, max_degr
436450 degree = int (numpy .ceil (safety_factor * beta_tilde ))
437451 return max (min_degree , min (max_degree , degree ))
438452
439- def _auto_degree (self , fock , beta = None , safety_factor = 2.5 , min_degree = 10 , max_degree = 2000 ):
453+ def _auto_degree (
454+ self , fock , beta = None , safety_factor = 2.5 , min_degree = 10 , max_degree = 2000
455+ ):
440456 """
441457 Estimate a Chebyshev expansion degree sufficient to resolve the
442458 Fermi-Dirac occupation step for the given Fock matrix and ``beta``.
@@ -477,15 +493,28 @@ def _auto_degree(self, fock, beta=None, safety_factor=2.5, min_degree=10, max_de
477493 if beta is None :
478494 beta = self .beta
479495 a , b , emin , emax , _ = self ._spectral_bounds (fock )
480- degree = self ._degree_for_beta_a (beta , a , safety_factor = safety_factor ,
481- min_degree = min_degree , max_degree = max_degree )
482- logger .info (self , 'Auto-selected chebyshev_degree = %d '
483- '(beta=%.3g, a=%.4f, beta*a=%.3g, safety_factor=%.2f)' ,
484- degree , beta , a , beta * a , safety_factor )
496+ degree = self ._degree_for_beta_a (
497+ beta ,
498+ a ,
499+ safety_factor = safety_factor ,
500+ min_degree = min_degree ,
501+ max_degree = max_degree ,
502+ )
503+ logger .info (
504+ self ,
505+ "Auto-selected chebyshev_degree = %d "
506+ "(beta=%.3g, a=%.4f, beta*a=%.3g, safety_factor=%.2f)" ,
507+ degree ,
508+ beta ,
509+ a ,
510+ beta * a ,
511+ safety_factor ,
512+ )
485513 return degree
486514
487-
488- def make_rdm1_chebyshev (self , fock , mu , ovlp = None , nmo = None , degree = None , beta = None ):
515+ def make_rdm1_chebyshev (
516+ self , fock , mu , ovlp = None , nmo = None , degree = None , beta = None
517+ ):
489518 """
490519 Construct an AO-basis one-particle density matrix by Chebyshev filtering.
491520
@@ -527,17 +556,18 @@ def make_rdm1_chebyshev(self, fock, mu, ovlp=None, nmo=None, degree=None, beta=N
527556
528557 dm = numpy .zeros ((fock .shape [0 ], fock .shape [0 ]))
529558
530- identity = csr_matrix ((numpy .ones (fock .shape [0 ]),
531- (numpy .arange (fock .shape [0 ]), numpy .arange (fock .shape [0 ]))),
532- shape = (fock .shape [0 ], fock .shape [0 ]))
559+ identity = csr_matrix (
560+ (
561+ numpy .ones (fock .shape [0 ]),
562+ (numpy .arange (fock .shape [0 ]), numpy .arange (fock .shape [0 ])),
563+ ),
564+ shape = (fock .shape [0 ], fock .shape [0 ]),
565+ )
533566 normalized_fock = (fock_ortho - b * identity ) / a
534567 normalized_fock = to_csr_matrix (normalized_fock .toarray (), threshold = 1e-5 )
535568
536569 for i in range (nmo ):
537- v = coo_matrix (
538- ([1.0 ], ([i ], [0 ])),
539- shape = (nmo , 1 )
540- ).tocsr ()
570+ v = coo_matrix (([1.0 ], ([i ], [0 ])), shape = (nmo , 1 )).tocsr ()
541571 y = self ._chebyshev_poly_vec (v , normalized_fock , mu , degree , beta , a )
542572 dm [:, i ] = y .toarray ().flatten ()
543573
@@ -568,10 +598,11 @@ class never occupies orbitals directly, so the converged density
568598 ndarray
569599 AO-basis one-particle density matrix.
570600 """
571- if getattr (self , ' _dm1' , None ) is None :
601+ if getattr (self , " _dm1" , None ) is None :
572602 raise RuntimeError (
573- 'ChebyshevSCF.make_rdm1() called before kernel() has run; '
574- 'no density matrix is available yet.' )
603+ "ChebyshevSCF.make_rdm1() called before kernel() has run; "
604+ "no density matrix is available yet."
605+ )
575606 return self ._dm1
576607
577608 def _diff_ne (self , nelec , mu , fock ):
@@ -619,13 +650,13 @@ def bisect_search_mu(self, nelectrons, fock):
619650 if fock is None :
620651 fock = self .get_fock (self .get_init_guess (self .mol ))
621652 mu = bisect (
622- lambda mu : self ._diff_ne (nelectrons , mu , fock ),
623- mu_lo , mu_hi ,
624- xtol = 1e-14
653+ lambda mu : self ._diff_ne (nelectrons , mu , fock ), mu_lo , mu_hi , xtol = 1e-14
625654 )
626655 return mu
627656
628- def kernel (self , conv_dm = 1e-3 , conv_e = 1e-6 , max_cycle = 100 , mixing_beta = 0.3 , dm1 = None ):
657+ def kernel (
658+ self , conv_dm = 1e-3 , conv_e = 1e-6 , max_cycle = 100 , mixing_beta = 0.3 , dm1 = None
659+ ):
629660 """
630661 Run the Chebyshev-filtered SCF iteration.
631662
@@ -662,11 +693,17 @@ def kernel(self, conv_dm=1e-3, conv_e=1e-6, max_cycle=100, mixing_beta=0.3, dm1=
662693 ovlp = self .get_ovlp ()
663694
664695 if self .auto_degree :
665- auto_deg = self ._auto_degree (fock , beta = self .beta ,
666- safety_factor = self .degree_safety_factor )
696+ auto_deg = self ._auto_degree (
697+ fock , beta = self .beta , safety_factor = self .degree_safety_factor
698+ )
667699 if auto_deg > self .chebyshev_degree :
668- logger .info (self , 'auto_degree: raising chebyshev_degree '
669- '%d -> %d for this geometry/beta' , self .chebyshev_degree , auto_deg )
700+ logger .info (
701+ self ,
702+ "auto_degree: raising chebyshev_degree "
703+ "%d -> %d for this geometry/beta" ,
704+ self .chebyshev_degree ,
705+ auto_deg ,
706+ )
670707 self .chebyshev_degree = auto_deg
671708
672709 mu = self .bisect_search_mu (self .mol .nelectron , fock )
@@ -689,19 +726,32 @@ def kernel(self, conv_dm=1e-3, conv_e=1e-6, max_cycle=100, mixing_beta=0.3, dm1=
689726 diff = numpy .linalg .norm (dm_new - dm1 )
690727 ## energy_elec_dm
691728 e = self .energy_elec (dm_new , h1e , veff )[0 ] + self .mol .energy_nuc ()
692- logger .info (self , 'Cycle %d mu = %.12f diff = %.8f e = %.8f ne = %.8f' ,
693- cycle , self .mu , diff , e , ne )
729+ logger .info (
730+ self ,
731+ "Cycle %d mu = %.12f diff = %.8f e = %.8f ne = %.8f" ,
732+ cycle ,
733+ self .mu ,
734+ diff ,
735+ e ,
736+ ne ,
737+ )
694738 if diff < conv_dm or abs (e - e_save ) < conv_e :
695739 converged = True
696740 break
697741 dm1 = dm_new
698742 e_save = e
699743
700744 if not converged :
701- logger .warn (self , 'ChebyshevSCF did not converge after %d cycles '
702- '(final |ddm|/|dE| still above conv_dm=%.3g / '
703- 'conv_e=%.3g). Try increasing max_cycle, '
704- 'chebyshev_degree, or beta.' , max_cycle , conv_dm , conv_e )
745+ logger .warn (
746+ self ,
747+ "ChebyshevSCF did not converge after %d cycles "
748+ "(final |ddm|/|dE| still above conv_dm=%.3g / "
749+ "conv_e=%.3g). Try increasing max_cycle, "
750+ "chebyshev_degree, or beta." ,
751+ max_cycle ,
752+ conv_dm ,
753+ conv_e ,
754+ )
705755
706756 ## energy_elec_dm
707757 e_tot = self .energy_elec (dm_new , h1e , veff )[0 ] + self .mol .energy_nuc ()
@@ -727,6 +777,6 @@ def kernel(self, conv_dm=1e-3, conv_e=1e-6, max_cycle=100, mixing_beta=0.3, dm1=
727777 self .mo_coeff = mo_coeff
728778 self .mo_occ = mo_occ
729779
730- logger .info (self , ' ChebyshevSCF converged = %s' , self .converged )
731- logger .info (self , ' Converged SCF energy = %.12f' , e_tot )
732- return e_tot
780+ logger .info (self , " ChebyshevSCF converged = %s" , self .converged )
781+ logger .info (self , " Converged SCF energy = %.12f" , e_tot )
782+ return e_tot
0 commit comments