1+ #!/usr/bin/env python3
2+ '''
3+ This script is implemented referring to Molden file standard:
4+ https://www.theochem.ru.nl/molden/molden_format.html
5+ https://zorkzou.github.io/Molden2AIM/readme.html
6+
7+ Users may add this directory to PATH and run chmod +x on this file to use it as a command.
8+ '''
9+
10+ DEFAULT_FOLDER = "$(pwd)"
11+
112####################
213# Impl. of NAO2GTO #
314####################
@@ -500,7 +511,7 @@ def write_molden_gto(total_gto: GTORadials, labels: list):
500511 out += total_gto .molden (l , iat + 1 ) # iat starts from 0, molden starts from 1
501512 return out
502513
503- def read_abacus_lowf (flowf , pat = r'^WFC_NAO_(K\d+|GAMMA(1|2)) .txt$' ):
514+ def read_abacus_lowf (flowf , pat = r'^( WFC_NAO_(K\d+|GAMMA\d+)(_ION\d+)?|wf((k\d+)?(s\d+)?|(s\d+)?(k\d+)?)(g\d+)?_nao)\ .txt$' ):
504515 """Read the ABACUS WFC_NAO_(K|GAMMA) file, which contains the coefficients of MOs.
505516 Please note, only Gamma-only calculation is supported. For ABACUS version earlier
506517 than 3.7.1, the `flowf` file is something like LOWF_K_. Manually change the file
@@ -549,7 +560,8 @@ def read_abacus_lowf(flowf, pat=r'^WFC_NAO_(K\d+|GAMMA(1|2)).txt$'):
549560 i += 1
550561
551562 # check if the data we collected has the correct number of elements
552- if "WFC_NAO_K" in flowf : # multi-k case, the coef will be complex
563+ basename = os .path .basename (flowf )
564+ if "WFC_NAO_K" in basename or re .match (r"^wf(k\d+|s\d+k\d+)" , basename ): # multi-k case, the coef will be complex
553565 data = [complex (data [i ], data [i + 1 ]) for i in range (0 , len (data ), 2 )]
554566 data = [d .real for d in data ]
555567 data = np .array (data ).reshape (nbands , nlocal )
@@ -560,6 +572,104 @@ def read_abacus_lowf(flowf, pat=r'^WFC_NAO_(K\d+|GAMMA(1|2)).txt$'):
560572
561573 return nbands , nlocal , occ , band , ener , data
562574
575+ def find_abacus_lowf_files (nspin ):
576+ """Find ABACUS LCAO wavefunction files in OUT.*.
577+
578+ Keep compatibility with older WFC_NAO_* names and newer wf*_nao.txt names.
579+ The KPT parser still restricts this converter to gamma-only single-k runs.
580+ """
581+ import glob
582+ import os
583+ import re
584+
585+ if nspin == 4 :
586+ raise NotImplementedError ("nspin=4/SOC spinor wavefunctions are not supported by this Molden writer" )
587+ if nspin not in (1 , 2 ):
588+ raise NotImplementedError (f"Unsupported nspin={ nspin } " )
589+
590+ def newest (pattern ):
591+ def key (path ):
592+ nums = [int (x ) for x in re .findall (r"\d+" , os .path .basename (path ))]
593+ return nums , path
594+ matches = sorted (glob .glob (pattern ), key = key )
595+ return matches [- 1 ] if matches else None
596+
597+ def old_k_file (spin ):
598+ """Select the first-k old WFC_NAO_K* file for a spin channel."""
599+ files_by_k = {}
600+ for path in glob .glob ("WFC_NAO_K*.txt" ):
601+ match = re .match (r"^WFC_NAO_K(\d+)(?:_ION(\d+))?\.txt$" , os .path .basename (path ))
602+ if match is None :
603+ continue
604+ ik = int (match .group (1 ))
605+ ion = int (match .group (2 ) or 0 )
606+ if ik not in files_by_k or (ion , path ) > files_by_k [ik ][0 ]:
607+ files_by_k [ik ] = ((ion , path ), path )
608+ k_indices = sorted (files_by_k )
609+ if not k_indices :
610+ return None
611+ if nspin == 1 :
612+ target = k_indices [0 ]
613+ else :
614+ nk = len (k_indices ) // 2
615+ if nk == 0 :
616+ return None
617+ target = k_indices [0 ] if spin == 1 else k_indices [nk ]
618+ return files_by_k .get (target , (None , None ))[1 ]
619+
620+ if nspin == 1 :
621+ pattern_groups = [[
622+ "WFC_NAO_GAMMA1.txt" ,
623+ "WFC_NAO_GAMMA1_ION*.txt" ,
624+ "OLD_WFC_NAO_K_SPIN1" ,
625+ "wf_nao.txt" ,
626+ "wfk1_nao.txt" ,
627+ "WFC/wfg*_nao.txt" ,
628+ "WFC/wfk1g*_nao.txt" ,
629+ ]]
630+ else :
631+ pattern_groups = [
632+ [
633+ "WFC_NAO_GAMMA1.txt" ,
634+ "WFC_NAO_GAMMA1_ION*.txt" ,
635+ "OLD_WFC_NAO_K_SPIN1" ,
636+ "wfs1_nao.txt" ,
637+ "wfs1k1_nao.txt" ,
638+ "wfk1s1_nao.txt" ,
639+ "WFC/wfs1g*_nao.txt" ,
640+ "WFC/wfs1k1g*_nao.txt" ,
641+ "WFC/wfk1s1g*_nao.txt" ,
642+ ],
643+ [
644+ "WFC_NAO_GAMMA2.txt" ,
645+ "WFC_NAO_GAMMA2_ION*.txt" ,
646+ "OLD_WFC_NAO_K_SPIN2" ,
647+ "wfs2_nao.txt" ,
648+ "wfs2k1_nao.txt" ,
649+ "wfk1s2_nao.txt" ,
650+ "WFC/wfs2g*_nao.txt" ,
651+ "WFC/wfs2k1g*_nao.txt" ,
652+ "WFC/wfk1s2g*_nao.txt" ,
653+ ],
654+ ]
655+
656+ files = []
657+ for patterns in pattern_groups :
658+ found = None
659+ for pattern in patterns :
660+ if pattern == "OLD_WFC_NAO_K_SPIN1" :
661+ found = old_k_file (1 )
662+ elif pattern == "OLD_WFC_NAO_K_SPIN2" :
663+ found = old_k_file (2 )
664+ else :
665+ found = newest (pattern )
666+ if found is not None :
667+ break
668+ if found is None :
669+ raise FileNotFoundError (f"Cannot find ABACUS LCAO wavefunction file. Tried: { ', ' .join (patterns )} " )
670+ files .append (found )
671+ return files
672+
563673def write_molden_mo (coefs , mo_map , occ , ener , spin = 0 , ndigits = 3 ):
564674 """Write Molden [MO] section from the coefficients, occupations and energies.
565675
@@ -863,6 +973,21 @@ def write_molden_nval_from_stru(stru, pseudo_dir):
863973 nvals = [read_upf_z_valence (fpp ) for fpp in fpps ]
864974 return write_molden_nval (kinds , nvals )
865975
976+ def write_molden_pseudo (kinds , labels_kinds_map , zvals ):
977+ """Write Molden [Pseudo] effective nuclear charges per atom."""
978+ out = "[Pseudo]\n "
979+ for i , itype in enumerate (labels_kinds_map ):
980+ out += f"{ kinds [itype ]} { i + 1 } { zvals [itype ]:g} \n "
981+ return out
982+
983+ def write_molden_pseudo_from_stru (stru , pseudo_dir , labels_kinds_map ):
984+ """Build [Pseudo] from STRU species and their UPF z_valence values."""
985+ import os
986+ kinds = [spec ['symbol' ] for spec in stru ['species' ]]
987+ fpps = [os .path .abspath (os .path .join (pseudo_dir , spec ['pp_file' ])) for spec in stru ["species" ]]
988+ zvals = [read_upf_z_valence (fpp ) for fpp in fpps ]
989+ return write_molden_pseudo (kinds , labels_kinds_map , zvals )
990+
866991def read_abacus_input (finput ):
867992 """Read the ABACUS input file and return the key-value pairs
868993
@@ -961,7 +1086,7 @@ def indexing_mo(total_gto: GTORadials, labels: list):
9611086 i += 2 * l + 1
9621087 return out
9631088
964- def moldengen (folder : str , ndigits = 3 , ngto = 7 , rel_r = 2 , fmolden = "ABACUS .molden" , write_nval = False ):
1089+ def moldengen (folder : str , ndigits = 3 , ngto = 7 , rel_r = 2 , fmolden = "ABACUS_Multiwfn .molden" , with_cell = True , with_nval = True , with_pseudo = False ):
9651090 """Entrance function: generate molden file by reading the outdir of ABACUS, for only LCAO
9661091 calculation.
9671092
@@ -976,6 +1101,7 @@ def moldengen(folder: str, ndigits=3, ngto=7, rel_r=2, fmolden="ABACUS.molden",
9761101 import os
9771102 import numpy as np
9781103
1104+ folder = os .path .abspath (folder )
9791105 files = os .listdir (folder )
9801106 assert ("STRU" in files ) and ("INPUT" in files ) and ("KPT" in files ),\
9811107 "STRU, INPUT, KPT files are required in the folder."
@@ -989,12 +1115,12 @@ def moldengen(folder: str, ndigits=3, ngto=7, rel_r=2, fmolden="ABACUS.molden",
9891115 ####################
9901116 # write the cell #
9911117 ####################
1118+ print (f"MOLDEN: Processing ABACUS calculation directory { folder } " )
9921119 kv = read_abacus_input ("INPUT" )
9931120 _ = read_abacus_kpt (kv .get ("kpoint_file" , "KPT" ))
9941121 stru = read_abacus_stru (kv .get ("stru_file" , "STRU" ))
995- 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" , "./" ))
1122+ if with_cell :
1123+ out += write_molden_cell (stru ['lat' ]['const' ], stru ['lat' ]['vec' ])
9981124
9991125 ####################
10001126 # write the atoms #
@@ -1019,6 +1145,10 @@ def moldengen(folder: str, ndigits=3, ngto=7, rel_r=2, fmolden="ABACUS.molden",
10191145 else :
10201146 raise NotImplementedError (f"Unknown coordinate type { stru ['coord_type' ]} " )
10211147 out += write_molden_atoms (labels , kinds , labels_kinds_map , coords .tolist ())
1148+ if with_nval :
1149+ out += write_molden_nval_from_stru (stru , kv .get ("pseudo_dir" , "./" ))
1150+ if with_pseudo :
1151+ out += write_molden_pseudo_from_stru (stru , kv .get ("pseudo_dir" , "./" ), labels_kinds_map )
10221152
10231153 ####################
10241154 # write the basis #
@@ -1039,17 +1169,19 @@ def moldengen(folder: str, ndigits=3, ngto=7, rel_r=2, fmolden="ABACUS.molden",
10391169 ####################
10401170 suffix = kv .get ("suffix" , "ABACUS" )
10411171 out_dir = "." .join (["OUT" , suffix ])
1172+ print (f"MOLDEN: Parsing ABACUS output directory { os .path .abspath (out_dir )} " )
10421173 os .chdir (out_dir )
10431174 nspin = int (kv .get ("nspin" , 1 ))
1044- mo_files = "WFC_NAO_GAMMA1.txt"
1045- mo_files = "WFC_NAO_K1.txt" if mo_files not in os .listdir () else mo_files
1046- mo_files = [mo_files ] if nspin == 1 else [mo_files , mo_files .replace ("1.txt" , "2.txt" )]
1175+ mo_files = find_abacus_lowf_files (nspin )
10471176 for ispin , mo_file in enumerate (mo_files ):
1177+ print (f"MOLDEN: Parsing wavefunction file { os .path .abspath (mo_file )} " )
10481178 nbands , nlocal , occ , band , ener , data_np = read_abacus_lowf (mo_file )
10491179 coefs = data_np .tolist ()
10501180 out += write_molden_mo (coefs , mo_map , occ , ener , spin = ispin , ndigits = ndigits )
10511181
10521182 os .chdir (cwd )
1183+ fmolden = os .path .abspath (fmolden )
1184+ print (f"MOLDEN: Writing Molden file { fmolden } " )
10531185 with open (fmolden , "w" ) as file :
10541186 file .write (out )
10551187 return out
@@ -1392,32 +1524,50 @@ def _argparse():
13921524 -n, --ndigits: the number of digits for the MO coefficients. For MO coefficients smaller than 10^-n, they will be set to 0.
13931525 -g, --ngto: the number of GTOs to fit ABACUS NAOs.
13941526 -r, --rel_r: the relative cutoff radius for the GTOs.
1395- --write-nval: write the Molden [Nval] section from UPF z_valence.
1527+ --with-cell: whether to write the Molden [Cell] section.
1528+ --with-Nval: whether to write the Molden [Nval] section from UPF z_valence.
1529+ --with-pseudo: whether to write the Molden [Pseudo] section from UPF z_valence.
13961530 -o, --output: the output Molden file name.
13971531 """
13981532 import argparse
1533+ import os
1534+
1535+ def str_to_bool (value ):
1536+ if isinstance (value , bool ):
1537+ return value
1538+ value = value .lower ()
1539+ if value in ("true" , "t" , "yes" , "y" , "1" ):
1540+ return True
1541+ if value in ("false" , "f" , "no" , "n" , "0" ):
1542+ return False
1543+ raise argparse .ArgumentTypeError ("expected true or false" )
1544+
13991545 parser = argparse .ArgumentParser (
1400- description = "Generate Molden file from ABACUS LCAO calculation via NAO2GTO method " ,
1546+ description = "Generate Molden file from ABACUS LCAO calculation for Multiwfn " ,
14011547 formatter_class = argparse .ArgumentDefaultsHelpFormatter ,
14021548 )
14031549 welcome = """WARNING: use at your own risk because the NAO2GTO will not always conserve the shape of radial function, therefore
14041550the total number of electrons may not be conserved. Always use after a re-normalization operation.
14051551Once meet any problem, please submit an issue at: https://github.com/deepmodeling/abacus-develop/issues
14061552 """
14071553 parser .epilog = welcome
1408- parser .add_argument ("-f" , "--folder" , type = str , help = "the folder of the ABACUS calculation" )
1554+ parser .add_argument ("-f" , "--folder" , type = str , default = DEFAULT_FOLDER , help = "the folder of the ABACUS calculation" )
14091555 parser .add_argument ("-n" , "--ndigits" , type = int , default = 3 , help = "the number of digits for the MO coefficients" )
14101556 parser .add_argument ("-g" , "--ngto" , type = int , default = 7 , help = "the number of GTOs to fit ABACUS NAOs" )
14111557 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" )
1413- parser .add_argument ("-o" , "--output" , type = str , default = "ABACUS.molden" , help = "the output Molden file name" )
1558+ parser .add_argument ("--with-cell" , type = str_to_bool , default = True , help = "whether to write the Molden [Cell] section" )
1559+ parser .add_argument ("--with-Nval" , dest = "with_nval" , type = str_to_bool , default = True , help = "whether to write the Molden [Nval] section" )
1560+ parser .add_argument ("--with-pseudo" , type = str_to_bool , default = False , help = "whether to write the Molden [Pseudo] section" )
1561+ parser .add_argument ("-o" , "--output" , type = str , default = "ABACUS_Multiwfn.molden" , help = "the output Molden file name" )
14141562 args = parser .parse_args ()
1563+ if args .folder == DEFAULT_FOLDER :
1564+ args .folder = os .getcwd ()
14151565 return args
14161566
14171567if __name__ == "__main__" :
14181568 #unittest.main(exit=False)
14191569 args = _argparse ()
1420- moldengen (args .folder , args .ndigits , args .ngto , args .rel_r , args .output , args .write_nval )
1570+ moldengen (args .folder , args .ndigits , args .ngto , args .rel_r , args .output , args .with_cell , args . with_nval , args . with_pseudo )
14211571 print (" " .join ("*" * 10 ).center (80 , " " ))
14221572 print (f"""MOLDEN: Generated Molden file { args .output } from ABACUS calculation in folder { args .folder } .
14231573WARNING: use at your own risk because the NAO2GTO will not always conserve the shape of radial function, therefore
0 commit comments