Skip to content

Commit 6cb560b

Browse files
committed
Move Molden converter to Multiwfn interface
1 parent eb4ddf7 commit 6cb560b

4 files changed

Lines changed: 165 additions & 15 deletions

File tree

Lines changed: 165 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
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+
563673
def 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+
866991
def 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
14041550
the total number of electrons may not be conserved. Always use after a re-normalization operation.
14051551
Once 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

14171567
if __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}.
14231573
WARNING: use at your own risk because the NAO2GTO will not always conserve the shape of radial function, therefore

0 commit comments

Comments
 (0)