Source code for qmctorch.scf.calculator.adf

import os
import shutil
import warnings
from types import SimpleNamespace
from typing import BinaryIO, List
import numpy as np

from ... import log
from ...utils.constants import BOHR2ANGS
from .calculator_base import CalculatorBase

try:
    from scm import plams
except ModuleNotFoundError:
    warnings.warn("scm python module not found")


[docs] class CalculatorADF(CalculatorBase): def __init__( # pylint: disable=too-many-arguments self, atoms: list, atom_coords: list, basis: str, charge: int, spin: int, scf: str, units: str, molname: str, savefile: str, ) -> None: """ Initialize the ADF calculator. Args: atoms (list): List of atom symbols. atom_coords (list): List of atomic coordinates. basis (str): Basis set name. charge (int): Molecular charge. spin (int): Spin multiplicity. scf (str): Self-consistent field method. units (str): Units for atomic coordinates. molname (str): Molecule name. savefile (str): File name to save results. Raises: ValueError: If charge or spin is not supported. Returns: None """ CalculatorBase.__init__( self, atoms, atom_coords, basis, charge, spin, scf, units, molname, "adf", savefile, ) if charge != 0: raise ValueError( "ADF calculator does not support charge yet, open an issue in the repo :)" ) if spin != 0: raise ValueError( "ADF calculator does not support spin polarization yet, open an issue in the repo :)" ) # basis from the emma paper self.additional_basis_type = ["VB1", "VB2", "VB3", "CVB1", "CVB2", "CVB3"] self.additional_basis_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "atomicdata/adf/" ) self.adf_version = "adf2020+" self.job_name = "".join(self.atoms) + "_" + self.basis_name self.output_file = "adf.rkf"
[docs] def run(self) -> SimpleNamespace: """Run the calculation using ADF.""" # path needed for the calculation plams_wd = "./plams_workdir" outputdir_path = os.path.join( plams_wd, os.path.join(self.job_name, self.output_file) ) # get the correct exec plams_job = {"adf2020+": plams.AMSJob, "adf2019": plams.ADFJob}[ self.adf_version ] # configure plams and run the calculation self.init_plams() mol = self.get_plams_molecule() sett = self.get_plams_settings() job = plams_job(molecule=mol, settings=sett, name=self.job_name) job.run() # extract the data to hdf5 basis = self.get_basis_data(outputdir_path) # remove adf data if self.savefile: shutil.copyfile(outputdir_path, self.output_file) self.savefile = self.output_file # shutil.rmtree(plams_wd) # fnalize plams self.finish_plams() return basis
[docs] def init_plams(self) -> None: """Init PLAMS.""" plams.init() plams.config.log.stdout = -1 plams.config.log.file = -1 plams.config.erase_workdir = True
[docs] def finish_plams(self) -> None: """Finish PLAMS.""" plams.finish()
[docs] def get_plams_molecule(self) -> plams.Molecule: """Returns a plams molecule object.""" mol = plams.Molecule() bohr2angs = BOHR2ANGS # the coordinate are always in bohr for at, xyz in zip(self.atoms, self.atom_coords): xyz = list(bohr2angs * np.array(xyz)) mol.add_atom(plams.Atom(symbol=at, coords=tuple(xyz))) return mol
[docs] def get_plams_settings(self) -> plams.Settings: """ Returns a plams setting object. Returns: plams.Settings: A plams setting object. """ sett = plams.Settings() sett.input.ams.Task = "SinglePoint" if self.basis_name.upper() in self.additional_basis_type: sett.input.adf.basis.type = "DZP" parsed_atoms = [] for at in self.atoms: if at not in parsed_atoms: basis_path = os.path.join( self.additional_basis_path, self.basis_name.upper(), at ) atomtype = f"Symbol={at} File={basis_path}" sett.input.adf.basis.peratomtype = atomtype parsed_atoms.append(at) else: sett.input.adf.basis.type = self.basis_name.upper() sett.input.adf.basis.core = "None" sett.input.adf.symmetry = "nosym" if self.scf.lower() == "hf": sett.input.adf.XC.HartreeFock = "" elif self.scf.lower() == "dft": sett.input.adf.XC.LDA = "VWN" sett.input.adf.relativity.level = "None" # total energy sett.input.adf.totalenergy = True # charge info # sett.input.adf.charge = "%d %d" % (self.charge, self.spin) # spin info sett.input.unrestricted = False # sett.input.spinpolarization = self.spin return sett
[docs] def get_basis_data(self, kffile: str) -> SimpleNamespace: """ Save the basis information needed to compute the AO values. Args: kffile (str): Path to the KF file. Returns: SimpleNamespace: A namespace containing the basis information. """ if not os.path.isfile(kffile): raise FileNotFoundError( "File %s not found, ADF may have crashed, look into the plams_workdir directory" % kffile ) kf = plams.KFFile(kffile) status = kf.read("General", "termination status").strip() if status != "NORMAL TERMINATION": log.info(" WARNING : ADF calculation terminated with status") log.info(" : %s" % status) log.info(" : Proceed with caution") basis = SimpleNamespace() basis.TotalEnergy = kf.read("Total Energy", "Total energy") basis.radial_type = "sto" basis.harmonics_type = "cart" nao = kf.read("Basis", "naos") nmo = kf.read("A", "nmo_A") basis.nao = nao basis.nmo = nmo # number of bas per atom type nbptr = kf.read("Basis", "nbptr") # number of atom per atom typ nqptr = kf.read("Geometry", "nqptr") atom_type = kf.read("Geometry", "atomtype").split() # number of bas per atom type nshells = np.array([nbptr[i] - nbptr[i - 1] for i in range(1, len(nbptr))]) # kx/ky/kz/kr exponent per atom type bas_kx = self.read_array(kf, "Basis", "kx") bas_ky = self.read_array(kf, "Basis", "ky") bas_kz = self.read_array(kf, "Basis", "kz") bas_kr = self.read_array(kf, "Basis", "kr") # bas exp/coeff/norm per atom type bas_exp = self.read_array(kf, "Basis", "alf") bas_norm = self.read_array(kf, "Basis", "bnorm") basis_nshells = [] basis_bas_kx, basis_bas_ky, basis_bas_kz = [], [], [] basis_bas_kr = [] basis_bas_exp, basis_bas_norm = [], [] for iat, _ in enumerate(atom_type): number_copy = nqptr[iat + 1] - nqptr[iat] idx_bos = list(range(nbptr[iat] - 1, nbptr[iat + 1] - 1)) basis_nshells += [nshells[iat]] * number_copy basis_bas_kx += list(bas_kx[idx_bos]) * number_copy basis_bas_ky += list(bas_ky[idx_bos]) * number_copy basis_bas_kz += list(bas_kz[idx_bos]) * number_copy basis_bas_kr += list(bas_kr[idx_bos]) * number_copy basis_bas_exp += list(bas_exp[idx_bos]) * number_copy basis_bas_norm += list(bas_norm[idx_bos]) * number_copy basis.nshells = basis_nshells basis.nao_per_atom = basis_nshells basis.index_ctr = np.arange(nao) basis.nctr_per_ao = np.ones(nao) basis.bas_kx = np.array(basis_bas_kx) basis.bas_ky = np.array(basis_bas_ky) basis.bas_kz = np.array(basis_bas_kz) basis.bas_kr = np.array(basis_bas_kr) basis.bas_exp = np.array(basis_bas_exp) basis.bas_coeffs = np.ones_like(basis_bas_exp) basis.bas_norm = np.array(basis_bas_norm) basis.atom_coords_internal = np.array(kf.read("Geometry", "xyz")).reshape(-1, 3) # Molecular orbitals mos = np.array(kf.read("A", "Eigen-Bas_A")) mos = mos.reshape(nmo, nao).T # normalize the MO # this is not needed !!! # mos = self.normalize_columns(mos) # orbital that take part in the rep npart = np.array(kf.read("A", "npart")) - 1 # create permutation matrix perm_mat = np.zeros((basis.nao, basis.nao)) for i in range(basis.nao): perm_mat[npart[i], i] = 1.0 # reorder the basis function basis.mos = perm_mat @ mos return basis
[docs] @staticmethod def read_array(kf: BinaryIO, section: str, name: str) -> np.ndarray: """read a data from the kf file Args: kf (BinaryIO): kf file section (str): name of the section name (str): name of the property Returns: np.ndarray: data """ data = np.array(kf.read(section, name)) if data.shape == (): data = np.array([data]) return data
[docs] class CalculatorADF2019(CalculatorADF): def __init__( self, atoms: List[str], atom_coords: List[np.ndarray], basis: str, charge: int, spin: int, scf: str, units: str, molname: str, savefile: str, ): """ Initialize the ADF2019 calculator. Args: atoms (list): List of atom symbols. atom_coords (list): List of atomic coordinates. basis (str): Basis set name. charge (int): Molecular charge. spin (int): Spin multiplicity. scf (str): Self-consistent field method. units (str): Units of the coordinates; 'bohr' or 'angs'. molname (str): Molecule name. savefile (str): File name to save results. Returns: None """ CalculatorADF.__init__( self, atoms, atom_coords, basis, charge, spin, scf, units, molname, savefile ) self.adf_version = "adf2019" self.job_name = "".join(self.atoms) + "_" + self.basis_name self.output_file = self.job_name + ".t21"
[docs] def get_plams_molecule(self) -> plams.Molecule: """Returns a plams molecule object. Returns: plams.Molecule: A plams molecule object. """ mol = plams.Molecule() for at, xyz in zip(self.atoms, self.atom_coords): mol.add_atom(plams.Atom(symbol=at, coords=tuple(xyz))) return mol
[docs] def get_plams_settings(self) -> plams.Settings: """Returns a plams setting object. Returns: plams.Settings: A plams setting object. """ sett = plams.Settings() sett.input.basis.type = self.basis_name.upper() if self.basis_name.upper() in self.additional_basis_type: sett.input.basis.path = self.additional_basis_path sett.input.basis.core = "None" sett.input.symmetry = "nosym" if self.scf.lower() == "hf": sett.input.XC.HartreeFock = "" elif self.scf.lower() == "dft": sett.input.XC.LDA = "VWN" # correct unit if self.units == "angs": sett.input.units.length = "Angstrom" elif self.units == "bohr": sett.input.units.length = "Bohr" # total energy sett.input.totalenergy = True # charge info sett.input.charge = "%d %d" % (self.charge, self.spin) return sett