Source code for esipy.atomicfiles

import os
import re

import numpy as np

from esipy.tools import wf_type, format_short_partition


[docs]def read_aoms(path='.'): """ Reads the AOMs from ESIpy's writeaoms() method or from an AIMAll calculation. :param path: Path of the directory of the files. :type path: str :returns: The AOMs in ESIpy format stored in 'ESI.aom'. :rtype: list """ aom = [] aom_alpha, aom_beta = [], [] start_string = 'The Atomic Overlap Matrix' mul = False path = os.path.join(os.getcwd(), path) if not os.path.exists(path): raise ValueError(f"The provided path '{path}' does not exist.") ints = [intfile for intfile in os.listdir(path) if intfile.endswith('.int') and os.path.isfile(os.path.join(path, intfile))] ordered = sorted(ints, key=lambda x: int(re.search(r'\d+', x).group())) for intfile in ordered: intfile_path = os.path.join(path, intfile) with open(intfile_path, 'r') as f: mat_lines = [] for line in f: if "Mulliken" in line: mul = True if start_string in line: next(f) calcinfo = next(f) next(f) while True: line = next(f).strip() if not line: break mat_lines.extend([float(num) for num in line.split()]) # Mulliken works on non-symmetric, square AOMs if mul: mat_size = int(np.sqrt(len(mat_lines))) matrix = np.array(mat_lines).reshape((mat_size, mat_size)) # Symmetric AOMs work on lower-triangular matrices else: mat_size = int(np.sqrt(2 * len(mat_lines) + 1 / 4) - 1 / 2) low_matrix = np.zeros((mat_size, mat_size)) low_matrix[np.tril_indices(mat_size)] = mat_lines matrix = low_matrix + low_matrix.T - np.diag(low_matrix.diagonal()) # We first get the number of shape of the alpha-alpha matrix if 'First Beta MO' in line: shape_aom_alpha = int(line.split()[-1]) - 1 else: shape_aom_alpha = 0 for num in mat_lines: if num == 0.0: shape_aom_alpha += 1 elif shape_aom_alpha > 0: break if 'Restricted' in calcinfo: aom.append(matrix) if 'Unrestricted' in calcinfo: SCR_alpha = matrix[:shape_aom_alpha, :shape_aom_alpha] SCR_beta = matrix[shape_aom_alpha:, shape_aom_alpha:] aom_alpha.append(SCR_alpha) aom_beta.append(SCR_beta) if 'Restricted' in calcinfo: return aom elif 'Unrestricted' in calcinfo: return [aom_alpha, aom_beta]
########### WRITING THE INPUT FOR THE ESI-3D CODE FROM THE AOMS ###########
[docs]def write_aoms(mol, mf, name, aom, ring=None, partition=None): """ Writes the input for the ESI-3D code from the AOMs. :param mol: Molecule object from PySCF. :type mol: PySCF instance :param mf: Calculation object from PySCF. :type mf: PySCF instance :param name: Name of the calculation. :type name: str :param aom: Concatenated list of Atomic Overlap Matrices (AOMs) in the MO basis. :type aom: list :param ring: Connectivity of the atoms in the ring. Can be more than one ring as a list of lists. :type ring: list of int, optional :param partition: Partition scheme for the AOMs. Options are "mulliken", "lowdin", "meta_lowdin", "nao", "iao". :type partition: str, optional :returns: None :generates: - A '_atomicfiles/' directory with all the files created. - A '.int' file for each atom with its corresponding AOM. - A 'name.files' file with a list of the names of the '.int' files. - A 'name.bad' file with a standard input for the ESI-3D code. - For Natural Orbitals, a 'name.wfx' file with the occupancies for the ESI-3D code. """ if isinstance(aom, str): aom = load_aoms(aom) wf = wf_type(aom) if wf == "no": aom, occ = aom # Separating AOMs and orbital occupations # Obtaining information for the files symbols = [mol.atom_symbol(i) for i in range(mol.natm)] atom_numbers = [i + 1 for i in range(mol.natm)] if wf == "unrest": nocc_alpha = mf.mo_occ[0].astype(int) nocc_beta = mf.mo_occ[1].astype(int) nalpha = [np.trace(aom_alpha) for aom_alpha in aom[0]] nbeta = [np.trace(aom_beta) for aom_beta in aom[1]] aoms = [] fill = np.zeros((nocc_beta.sum(), nocc_alpha.sum())) for i in range(mol.natm): left = np.vstack((aom[0][i], fill)) right = np.vstack((fill.T, aom[1][i])) matrix = np.hstack((left, right)) aoms.append(matrix) else: nalpha = nbeta = [np.trace(aom) for aom in aom] # Creating a new directory for the calculation shortpart = format_short_partition(partition) new_dir_name = name + "_" + shortpart symbols = [s.lower() for s in symbols] titles = [symbols[i] + str(atom_numbers[i]) for i in range(mol.natm)] # Setting the title of the files new_dir_path = os.path.join(os.getcwd(), new_dir_name) os.makedirs(new_dir_path, exist_ok=True) # Creating and writing the atomic .int files for i, item in enumerate(titles): with open(os.path.join(new_dir_path, item + ".int"), "w+") as f: f.write(" Created by ESIpy\n") if partition == "mulliken": f.write(" Using Mulliken atomic definition\n") elif partition == "lowdin": f.write(" Using Lowdin atomic definition\n") elif partition == "meta_lowdin": f.write(" Using Meta-Lowdin atomic definition\n") elif partition == "nao": f.write(" Using NAO atomic definition\n") elif partition == "iao": f.write(" Using IAO atomic definition\n") f.write(" Single-determinant wave function\n") f.write(" Molecular SCF ENERGY (AU) = {:.11f}\n\n".format(mf.e_tot)) f.write(" INTEGRATION IS OVER ATOM {}\n".format(titles[i])) f.write(" RESULTS OF THE INTEGRATION\n") if wf == "unrest": f.write(" N {:.10E} NET CHARGE 0.0000000000E+00\n".format( np.trace(aom[0][i]) + np.trace(aom[1][i]))) elif wf == "rest": f.write(" N {:.10E} NET CHARGE 0.0000000000E+00\n".format(2 * np.trace(aom[i]))) else: f.write(" N {:.10E} NET CHARGE 0.0000000000E+00\n".format(np.trace(np.dot(occ, aom[i])))) f.write(" G\n") f.write(" K 1.00000000000000E+01 E(ATOM) 1.00000000000000E+00\n") f.write(" L 0.00000000000000E+01\n\n") if wf == "unrest": f.write("\n The Atomic Overlap Matrix:\n\n Unrestricted\n\n") if partition == "mulliken": f.write(" \n".join([" ".join(["{:.16E}".format(num, 16) for num in row]) for row in aoms[i]]) + "\n") else: f.write("\n".join([" ".join([("{:.16E}".format(aoms[i][j][k]) if j >= k else "") for k in range(len(aoms[i][j]))]) for j in range(len(aoms[i]))]) + "\n") else: f.write("\n The Atomic Overlap Matrix:\n\n Restricted Closed-Shell Wavefunction\n\n ") if partition == "mulliken": f.write( " \n".join([" ".join(["{:.16E}".format(num, 16) for num in row]) for row in aom[i]]) + "\n") else: f.write("\n".join([" ".join([("{:.16E}".format(aom[i][j][k], 16) if j >= k else "") for k in range(len(aom[i][j]))]) for j in range(len(aom[i]))]) + "\n") f.write("\n Alpha electrons (NAlpha) {:.10E}".format(nalpha[i])) f.write("\n Beta electrons (NBeta) {:.10E}\n".format(nbeta[i])) f.write(" NORMAL TERMINATION OF PROAIMV") f.close() # Writing the file containing the title of the atomic .int files with open(os.path.join(new_dir_path, name + shortpart + ".files"), "w") as f: for i in titles: f.write(i + ".int\n") f.close() domci = False if isinstance(ring[0], int): ring = [ring] for r in ring: if len(r) < 12: domci = True # Single-determinant input file if wf == "rest" or wf == "unrest": # Creating the input for the ESI-3D code filename = os.path.join(new_dir_path, name + ".bad") with open(filename, "w") as f: f.write("$TITLE\n") f.write(name + "\n") f.write("$TYPE\n") if wf == "unrest": f.write("uhf\n{}\n".format(mol.nelec[0] + 1)) else: f.write("hf\n") if not domci: f.write("$NOMCI\n") f.write("$RING\n") if ring is not None: if isinstance(ring[0], int): # If only one ring is specified f.write("1\n{}\n".format(len(ring))) f.write(" ".join(str(value) for value in ring)) f.write("\n") else: f.write("{}\n".format(len(ring))) # If two or more rings are specified as a list of lists for sublist in ring: f.write(str(len(sublist)) + "\n") f.write(" ".join(str(value) for value in sublist)) f.write("\n") else: f.write("\n") # No ring specified, write it manually f.write("$ATOMS\n") f.write(str(mol.natm) + "\n") for title in titles: f.write(title + ".int\n") f.write("$BASIS\n") if wf == "unrest": f.write(str(int(np.shape(aom[0])[1]) + int(np.shape(aom[1])[1])) + "\n") else: f.write(str(np.shape(aom)[1]) + "\n") f.write("$AV1245\n") f.write("$FULLOUT\n") if partition == "mulliken": f.write("$MULLIKEN\n") f.close() # Natural orbitals input file elif wf == "no": # Creating the input for the ESI-3D code filename = os.path.join(new_dir_path, name + ".bad") with open(filename, "w") as f: f.write("$READWFN\n") f.write(name + ".wfx\n") if not domci: f.write("$NOMCI\n") f.write("$RING\n") if ring is not None: if isinstance(ring[0], int): # If only one ring is specified f.write("1\n{}\n".format(len(ring))) f.write(" ".join(str(value) for value in ring)) f.write("\n") else: f.write("{}\n".format(len(ring))) # If two or more rings are specified as a list of lists for sublist in ring: f.write(str(len(sublist)) + "\n") f.write(" ".join(str(value) for value in sublist)) f.write("\n") else: f.write("\n") # No ring specified, write it manually f.write("$AV1245\n") f.write("$FULLOUT\n") if partition == "mulliken": f.write("$MULLIKEN\n") f.close() # Creating a custom .wfx file for the ESI-3D code with the occupation numbers filename = os.path.join(new_dir_path, name + ".wfx") with open(filename, "w") as f: f.write('<Number of Nuclei>\n') f.write(f" {mol.natm}\n") f.write('</Number of Nuclei>\n') f.write('<Number of Occupied Molecular Orbitals>\n') f.write(f" {str(len(mf.mo_coeff))}\n") f.write('</Number of Occupied Molecular Orbitals>\n') f.write('<Number of Electrons>\n') f.write(f" {sum(mol.nelec)}\n") f.write('</Number of Electrons>\n') f.write('<Number of Core Electrons>\n') f.write(f" {0}\n") f.write('</Number of Core Electrons>\n') nalpha, nbeta = mol.nelec f.write('<Number of Alpha Electrons>\n') f.write(f" {nalpha}\n") f.write('</Number of Alpha Electrons>\n') f.write('<Number of Beta Electrons>\n') f.write(f" {nbeta}\n") f.write('</Number of Beta Electrons>\n') f.write("<Electronic Spin Multiplicity>\n") mult = 2 * mol.spin + 1 f.write(f" {str(mult)}\n") f.write("</Electronic Spin Multiplicity>\n") f.write("<Nuclear Names>\n") for i in titles: f.write(f" {i.upper()}\n") f.write("</Nuclear Names>\n") f.write("<Nuclear Cartesian Coordinates>\n") for coord in mol.atom_coords(): f.write(" {: .12e} {: .12e} {: .12e}\n".format(coord[0], coord[1], coord[2])) f.write("</Nuclear Cartesian Coordinates>\n") f.write("<Molecular Orbital Occupation Numbers>\n") for occupation in np.diag(occ): f.write(f" {occupation:.12e}\n") f.write("</Molecular Orbital Occupation Numbers>\n") f.write("<Molecular Orbital Spin Types>\n") for i in range(0, len(aom[0])): f.write(" Alpha and Beta\n") f.write("</Molecular Orbital Spin Types>\n") f.write("<Molecular Orbital Energies>\n") for i in range(0, len(aom[0])): f.write(" 0.000000000000e+00\n") f.write("</Molecular Orbital Energies>\n") f.close()