@@ -211,6 +211,16 @@ def _build_gto(a, c, l, r):
211211 import numpy as np
212212 g = c * np .exp (- a * r ** 2 ) * r ** l
213213 return g
214+
215+ def _gto_radial_norm (a , l ):
216+ """Normalization factor of r^l exp(-a r^2) with normalized spherical harmonics.
217+
218+ The NAO fit uses unnormalized radial primitives, while Molden readers
219+ usually normalize primitive Gaussian functions internally. Therefore the
220+ fitted coefficient should be divided by this factor when writing Molden.
221+ """
222+ import math
223+ return math .sqrt (2.0 * (2.0 * a )** (l + 1.5 ) / math .gamma (l + 1.5 ))
214224
215225 def __str__ (self ) -> str :
216226 """print the GTOs in the Gaussian format. Different CGTO are printed as different section."""
@@ -247,6 +257,7 @@ def molden_all(self) -> str:
247257 out += f"{ spectra [l ]:>25s} { ngto :>8d} { '1.00' :>8s} \n "
248258 for ig in range (ngto ):
249259 a , c = NumericalRadial [l ][ic ][ig ]
260+ c /= GTORadials ._gto_radial_norm (a , l )
250261 out += f"{ a :>62.3f} { c :>12.3f} \n "
251262 out += "\n "
252263 return out
@@ -261,11 +272,12 @@ def molden(self, it, iat) -> str:
261272 out += f"{ spectra [l ]:>25s} { ngto :>8d} { '1.00' :>8s} \n "
262273 for ig in range (ngto ):
263274 a , c = NumericalRadial [l ][ic ][ig ]
275+ c /= GTORadials ._gto_radial_norm (a , l )
264276 out += f"{ a :>62.3f} { c :>12.3f} \n "
265277 out += "\n "
266278 return out
267279
268- def fit_radial_with_gto (nao , ngto , l , r , rel_r = 2 ):
280+ def _fit_radial_with_gto_result (nao , ngto , l , r , rel_r = 2 ):
269281 """fit one radial function mapped on grid with GTOs
270282
271283 Args:
@@ -334,16 +346,35 @@ def gto_guess(nao, ngto, l, r, rel_r=2):
334346 norm_nao = simpson (nao ** 2 * r ** 2 , x = r )
335347 norm_gto = simpson (out [0 ][l ][0 ]** 2 * r ** 2 , x = r )
336348 factor = np .sqrt (norm_nao / norm_gto )
337- print (f"NAO2GTO: Renormalize the CGTO from NAO2GTO method with factor { factor :.4f} " )
338349 c *= factor # renormalize the coefficients to make the norm of GTO equals to that of NAO
339350
351+ return a , c , err , factor
352+
353+ def _print_fit_radial_with_gto_result (a , c , err , factor , ngto , l , best_rel_r = None ):
354+ print (f"NAO2GTO: Renormalize the CGTO from NAO2GTO method with factor { factor :.4f} " )
355+ best_rel_r_line = "" if best_rel_r is None else f" Best rel_r: { best_rel_r } \n "
356+
340357 print (f"""NAO2GTO: Angular momentum { l } , with { ngto } superposition to fit numerical atomic orbitals on given grid,
341- Nonlinear fitting error: { err :.4e}
358+ { best_rel_r_line } Nonlinear fitting error: { err :.4e}
342359 Exponential and contraction coefficients of primitive GTOs in a.u.:
343360{ "a" :>10} { "c" :>10} \n ---------------------""" )
344361 for i in range (ngto ):
345362 print (f"{ a [i ]:10.6f} { c [i ]:10.6f} " )
346363 print (f"\n NAO2GTO: The fitted GTOs are saved in the CGTO instance." )
364+
365+ def fit_radial_with_gto (nao , ngto , l , r , rel_r = 2 ):
366+ a , c , err , factor = _fit_radial_with_gto_result (nao , ngto , l , r , rel_r )
367+ _print_fit_radial_with_gto_result (a , c , err , factor , ngto , l )
368+ return a , c
369+
370+ def fit_radial_with_gto_multistart (nao , ngto , l , r , rel_rs ):
371+ """Run independent rel_r starts and keep the fit with the lowest error."""
372+ results = []
373+ for rel_r in rel_rs :
374+ a , c , err , factor = _fit_radial_with_gto_result (nao , ngto , l , r , rel_r )
375+ results .append ((err , rel_r , a , c , factor ))
376+ err , best_rel_r , a , c , factor = min (results , key = lambda item : item [0 ])
377+ _print_fit_radial_with_gto_result (a , c , err , factor , ngto , l , best_rel_r )
347378 return a , c
348379
349380def read_nao (fpath ):
@@ -399,6 +430,15 @@ def read_nao(fpath):
399430
400431 return {'elem' : elem , 'ecut' : ecut , 'rcut' : rcut , 'nr' : nr , 'dr' : dr , 'chi' : chi }
401432
433+ def parse_rel_r (rel_r ):
434+ """Return a single rel_r value, or a list for optional multi-start fitting."""
435+ if isinstance (rel_r , str ):
436+ rel_r = rel_r .strip ()
437+ if "," in rel_r :
438+ return [float (item ) for item in rel_r .split ("," ) if item .strip ()]
439+ return float (rel_r )
440+ return rel_r
441+
402442def convert_nao_to_gto (fnao , fgto = None , ngto : int = 7 , rel_r : float = 2 ):
403443 """convert the numerical atomic orbitals to GTOs. Each chi (or say the zeta function)
404444 corresponds to a CGTO (contracted GTO), and the GTOs are fitted to the radial functions.
@@ -408,6 +448,7 @@ def convert_nao_to_gto(fnao, fgto = None, ngto: int = 7, rel_r: float = 2):
408448 import numpy as np
409449 import os
410450
451+ rel_r = parse_rel_r (rel_r )
411452 gto = GTORadials ()
412453 # read the numerical atomic orbitals
413454 nao = read_nao (fnao )
@@ -418,7 +459,10 @@ def convert_nao_to_gto(fnao, fgto = None, ngto: int = 7, rel_r: float = 2):
418459 for l in range (lmax + 1 ):
419460 nchi = len (nao ["chi" ][l ])
420461 for i in range (nchi ):
421- a , c = fit_radial_with_gto (nao ["chi" ][l ][i ], ngto , l , rgrid , rel_r )
462+ if isinstance (rel_r , list ):
463+ a , c = fit_radial_with_gto_multistart (nao ["chi" ][l ][i ], ngto , l , rgrid , rel_r )
464+ else :
465+ a , c = fit_radial_with_gto (nao ["chi" ][l ][i ], ngto , l , rgrid , rel_r )
422466 gto .register_cgto (a , c , l , symbol , 'a' )
423467
424468 # draw the fitted GTOs
@@ -791,6 +835,34 @@ def write_molden_atoms(labels, kinds, labels_kinds_map, coords):
791835 out += f"{ elem :<2s} { i + 1 :>8d} { ptable (elem ):>8d} { coords [i ][0 ]:>15.6f} { coords [i ][1 ]:>15.6f} { coords [i ][2 ]:>15.6f} \n "
792836 return out
793837
838+ def read_upf_z_valence (fupf ):
839+ """Read z_valence from a UPF pseudopotential file."""
840+ import re
841+ with open (fupf , "r" ) as file :
842+ data = file .read ()
843+ m = re .search (r'\bz_valence\s*=\s*[\'"]([^\'"]+)[\'"]' , data )
844+ if not m :
845+ raise ValueError (f"Cannot find z_valence in UPF file { fupf } " )
846+ zval = float (m .group (1 ))
847+ if abs (zval - round (zval )) < 1e-8 :
848+ return int (round (zval ))
849+ return zval
850+
851+ def write_molden_nval (kinds , nvals ):
852+ """Write Molden [Nval] effective valence charges per element."""
853+ out = "[Nval]\n "
854+ for elem , nval in zip (kinds , nvals ):
855+ out += f"{ elem } { nval :g} \n "
856+ return out
857+
858+ def write_molden_nval_from_stru (stru , pseudo_dir ):
859+ """Build [Nval] from STRU species and their UPF z_valence values."""
860+ import os
861+ kinds = [spec ['symbol' ] for spec in stru ['species' ]]
862+ fpps = [os .path .abspath (os .path .join (pseudo_dir , spec ['pp_file' ])) for spec in stru ["species" ]]
863+ nvals = [read_upf_z_valence (fpp ) for fpp in fpps ]
864+ return write_molden_nval (kinds , nvals )
865+
794866def read_abacus_input (finput ):
795867 """Read the ABACUS input file and return the key-value pairs
796868
@@ -889,7 +961,7 @@ def indexing_mo(total_gto: GTORadials, labels: list):
889961 i += 2 * l + 1
890962 return out
891963
892- def moldengen (folder : str , ndigits = 3 , ngto = 7 , rel_r = 2 , fmolden = "ABACUS.molden" ):
964+ def moldengen (folder : str , ndigits = 3 , ngto = 7 , rel_r = 2 , fmolden = "ABACUS.molden" , write_nval = False ):
893965 """Entrance function: generate molden file by reading the outdir of ABACUS, for only LCAO
894966 calculation.
895967
@@ -921,6 +993,8 @@ def moldengen(folder: str, ndigits=3, ngto=7, rel_r=2, fmolden="ABACUS.molden"):
921993 _ = read_abacus_kpt (kv .get ("kpoint_file" , "KPT" ))
922994 stru = read_abacus_stru (kv .get ("stru_file" , "STRU" ))
923995 out += write_molden_cell (stru ['lat' ]['const' ], stru ['lat' ]['vec' ])
996+ if write_nval :
997+ out += write_molden_nval_from_stru (stru , kv .get ("pseudo_dir" , "./" ))
924998
925999 ####################
9261000 # write the atoms #
@@ -941,7 +1015,7 @@ def moldengen(folder: str, ndigits=3, ngto=7, rel_r=2, fmolden="ABACUS.molden"):
9411015 coords = np .dot (coords , vec )
9421016 elif stru ['coord_type' ].startswith ("Cartesian_angstrom" ):
9431017 # including *_center_xy, *_center_z, *_center_xyz, ... cases
944- coords * = 0.529177249
1018+ coords / = 0.529177210903
9451019 else :
9461020 raise NotImplementedError (f"Unknown coordinate type { stru ['coord_type' ]} " )
9471021 out += write_molden_atoms (labels , kinds , labels_kinds_map , coords .tolist ())
@@ -1316,12 +1390,16 @@ def _argparse():
13161390
13171391 -f, --folder: the folder of the ABACUS calculation, in which the STRU, INPUT, KPT, and OUT* folders are located.
13181392 -n, --ndigits: the number of digits for the MO coefficients. For MO coefficients smaller than 10^-n, they will be set to 0.
1319- -g, --ngto: the number of GTOs to fit ABACUS NAOs. The default is 7.
1320- -r, --rel_r: the relative cutoff radius for the GTOs. The default is 2.
1321- -o, --output: the output Molden file name. The default is ABACUS.molden.
1393+ -g, --ngto: the number of GTOs to fit ABACUS NAOs.
1394+ -r, --rel_r: the relative cutoff radius for the GTOs.
1395+ --write-nval: write the Molden [Nval] section from UPF z_valence.
1396+ -o, --output: the output Molden file name.
13221397 """
13231398 import argparse
1324- parser = argparse .ArgumentParser (description = "Generate Molden file from ABACUS LCAO calculation via NAO2GTO method" )
1399+ parser = argparse .ArgumentParser (
1400+ description = "Generate Molden file from ABACUS LCAO calculation via NAO2GTO method" ,
1401+ formatter_class = argparse .ArgumentDefaultsHelpFormatter ,
1402+ )
13251403 welcome = """WARNING: use at your own risk because the NAO2GTO will not always conserve the shape of radial function, therefore
13261404the total number of electrons may not be conserved. Always use after a re-normalization operation.
13271405Once meet any problem, please submit an issue at: https://github.com/deepmodeling/abacus-develop/issues
@@ -1330,15 +1408,16 @@ def _argparse():
13301408 parser .add_argument ("-f" , "--folder" , type = str , help = "the folder of the ABACUS calculation" )
13311409 parser .add_argument ("-n" , "--ndigits" , type = int , default = 3 , help = "the number of digits for the MO coefficients" )
13321410 parser .add_argument ("-g" , "--ngto" , type = int , default = 7 , help = "the number of GTOs to fit ABACUS NAOs" )
1333- parser .add_argument ("-r" , "--rel_r" , type = int , default = 2 , help = "the relative cutoff radius for the GTOs" )
1411+ parser .add_argument ("-r" , "--rel_r" , type = str , default = "2" , help = "the relative cutoff radius for the GTOs; comma-separated values enable multi-start fitting" )
1412+ parser .add_argument ("--write-nval" , action = "store_true" , help = "write the Molden [Nval] section from UPF z_valence" )
13341413 parser .add_argument ("-o" , "--output" , type = str , default = "ABACUS.molden" , help = "the output Molden file name" )
13351414 args = parser .parse_args ()
13361415 return args
13371416
13381417if __name__ == "__main__" :
13391418 #unittest.main(exit=False)
13401419 args = _argparse ()
1341- moldengen (args .folder , args .ndigits , args .ngto , args .rel_r , args .output )
1420+ moldengen (args .folder , args .ndigits , args .ngto , args .rel_r , args .output , args . write_nval )
13421421 print (" " .join ("*" * 10 ).center (80 , " " ))
13431422 print (f"""MOLDEN: Generated Molden file { args .output } from ABACUS calculation in folder { args .folder } .
13441423WARNING: use at your own risk because the NAO2GTO will not always conserve the shape of radial function, therefore
@@ -1352,4 +1431,4 @@ def _argparse():
13521431Qin X, Shang H, Xiang H, et al. HONPAS: A linear scaling open-source solution for large system simulations[J].
13531432International Journal of Quantum Chemistry, 2015, 115(10): 647-655.
13541433"""
1355- print (citation , flush = True )
1434+ print (citation , flush = True )
0 commit comments