Skip to content

Commit 9c8539f

Browse files
authored
Fix Molden GTO normalization and coordinate conversion (#7421)
* Fix Molden GTO normalization and coordinate conversion Title: Improve ABACUS Molden output for wavefunction analysis Summary: This PR fixes several Molden conversion issues in tools/molden/molden.py while keeping the default workflow unchanged as much as possible. Changes: - Correct the primitive Gaussian coefficient convention when writing Molden GTO data. The NAO-to-GTO fit uses unnormalized radial primitives, while Molden readers usually normalize primitive Gaussian functions internally. - Fix Cartesian_angstrom coordinate conversion. Coordinates in Angstrom are now converted to Bohr for the Molden [Atoms] AU section by dividing by 0.529177210903. - Add optional multi-start NAO-to-GTO fitting. A single -r value keeps the old single-start behavior; comma-separated -r values enable multi-start fitting and keep the fit with the lowest nonlinear error. - Add optional Molden [Nval] output via --write-nval. The values are read from UPF z_valence. This option is disabled by default. Notes: - The changes are limited to the Molden converter. Validation: - Ran the existing molden.py unit tests successfully. - Checked that default output does not contain [Nval]. - Checked that --write-nval writes C/O/H valence charges for the PhenolDimer test case. - Checked that Cartesian_angstrom coordinates are written at the correct Bohr scale. * Show default values in molden.py CLI help
1 parent 435c09b commit 9c8539f

1 file changed

Lines changed: 92 additions & 13 deletions

File tree

tools/molden/molden.py

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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"\nNAO2GTO: 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

349380
def 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+
402442
def 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+
794866
def 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
13261404
the total number of electrons may not be conserved. Always use after a re-normalization operation.
13271405
Once 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

13381417
if __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}.
13441423
WARNING: use at your own risk because the NAO2GTO will not always conserve the shape of radial function, therefore
@@ -1352,4 +1431,4 @@ def _argparse():
13521431
Qin X, Shang H, Xiang H, et al. HONPAS: A linear scaling open-source solution for large system simulations[J].
13531432
International Journal of Quantum Chemistry, 2015, 115(10): 647-655.
13541433
"""
1355-
print(citation, flush=True)
1434+
print(citation, flush=True)

0 commit comments

Comments
 (0)