Source code for qcdb.interface_gcp

#
# @BEGIN LICENSE
#
# Psi4: an open-source quantum chemistry software package
#
# Copyright (c) 2007-2019 The Psi4 Developers.
#
# The copyrights for code used from other parties are included in
# the corresponding files.
#
# This file is part of Psi4.
#
# Psi4 is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3.
#
# Psi4 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with Psi4; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @END LICENSE
#

"""Module with functions that interface with Grimme's GCP code."""
import os
import re
import uuid
import shutil
import socket
import subprocess

try:
    from psi4.driver.p4util.exceptions import *
    from psi4 import core
    isP4regime = True
except ImportError:
    from .exceptions import *
    isP4regime = False
from .util import parse_dertype
from .molecule import Molecule


[docs]def run_gcp(self, func=None, dertype=None, verbose=False): # dashlvl=None, dashparam=None """Function to call Grimme's GCP program https://www.chemie.uni-bonn.de/pctc/mulliken-center/software/gcp/gcp to compute an a posteriori geometrical BSSE correction to *self* for several HF, generic DFT, and specific HF-3c and PBEh-3c method/basis combinations, *func*. Returns energy if *dertype* is 0, gradient if *dertype* is 1, else tuple of energy and gradient if *dertype* unspecified. The gcp executable must be independently compiled and found in :envvar:`PATH` or :envvar:`PSIPATH`. *self* may be either a qcdb.Molecule (sensibly) or a psi4.Molecule (works b/c psi4.Molecule has been extended by this method py-side and only public interface fns used) or a string that can be instantiated into a qcdb.Molecule. """ # Create (if necessary) and update qcdb.Molecule if isinstance(self, Molecule): # called on a qcdb.Molecule pass elif isinstance(self, core.Molecule): # called on a python export of a psi4.core.Molecule (py-side through Psi4's driver) self.create_psi4_string_from_molecule() elif isinstance(self, str): # called on a string representation of a psi4.Molecule (c-side through psi4.Dispersion) self = Molecule(self) else: raise ValidationError("""Argument mol must be psi4string or qcdb.Molecule""") self.update_geometry() # # Validate arguments # dashlvl = dashlvl.lower() # dashlvl = dash_alias['-' + dashlvl][1:] if ('-' + dashlvl) in dash_alias.keys() else dashlvl # if dashlvl not in dashcoeff.keys(): # raise ValidationError("""-D correction level %s is not available. Choose among %s.""" % (dashlvl, dashcoeff.keys())) if dertype is None: derint, derdriver = -1, 'gradient' else: derint, derdriver = parse_dertype(dertype, max_derivative=1) # if func is None: # if dashparam is None: # # defunct case # raise ValidationError("""Parameters for -D correction missing. Provide a func or a dashparam kwarg.""") # else: # # case where all param read from dashparam dict (which must have all correct keys) # func = 'custom' # dashcoeff[dashlvl][func] = {} # dashparam = dict((k.lower(), v) for k, v in dashparam.iteritems()) # for key in dashcoeff[dashlvl]['b3lyp'].keys(): # if key in dashparam.keys(): # dashcoeff[dashlvl][func][key] = dashparam[key] # else: # raise ValidationError("""Parameter %s is missing from dashparam dict %s.""" % (key, dashparam)) # else: # func = func.lower() # if func not in dashcoeff[dashlvl].keys(): # raise ValidationError("""Functional %s is not available for -D level %s.""" % (func, dashlvl)) # if dashparam is None: # # (normal) case where all param taken from dashcoeff above # pass # else: # # case where items in dashparam dict can override param taken from dashcoeff above # dashparam = dict((k.lower(), v) for k, v in dashparam.iteritems()) # for key in dashcoeff[dashlvl]['b3lyp'].keys(): # if key in dashparam.keys(): # dashcoeff[dashlvl][func][key] = dashparam[key] # TODO temp until figure out paramfile allowed_funcs = ['HF/MINIS', 'DFT/MINIS', 'HF/MINIX', 'DFT/MINIX', 'HF/SV', 'DFT/SV', 'HF/def2-SV(P)', 'DFT/def2-SV(P)', 'HF/def2-SVP', 'DFT/def2-SVP', 'HF/DZP', 'DFT/DZP', 'HF/def-TZVP', 'DFT/def-TZVP', 'HF/def2-TZVP', 'DFT/def2-TZVP', 'HF/631Gd', 'DFT/631Gd', 'HF/def2-TZVP', 'DFT/def2-TZVP', 'HF/cc-pVDZ', 'DFT/cc-pVDZ', 'HF/aug-cc-pVDZ', 'DFT/aug-cc-pVDZ', 'DFT/SV(P/h,c)', 'DFT/LANL', 'DFT/pobTZVP', 'TPSS/def2-SVP', 'PW6B95/def2-SVP', # specials 'hf3c', 'pbeh3c'] allowed_funcs = [f.lower() for f in allowed_funcs] if func.lower() not in allowed_funcs: raise Dftd3Error("""bad gCP func: %s. need one of: %r""" % (func, allowed_funcs)) # Move ~/.dftd3par.<hostname> out of the way so it won't interfere defaultfile = os.path.expanduser('~') + '/.dftd3par.' + socket.gethostname() defmoved = False if os.path.isfile(defaultfile): os.rename(defaultfile, defaultfile + '_hide') defmoved = True # Find environment by merging PSIPATH and PATH environment variables lenv = { 'PATH': ':'.join([os.path.abspath(x) for x in os.environ.get('PSIPATH', '').split(':') if x != '']) + \ ':' + os.environ.get('PATH'), 'LD_LIBRARY_PATH': os.environ.get('LD_LIBRARY_PATH') } # Filter out None values as subprocess will fault on them lenv = {k: v for k, v in lenv.items() if v is not None} # Find out if running from Psi4 for scratch details and such try: import psi4 except ImportError as err: isP4regime = False else: isP4regime = True # Setup unique scratch directory and move in current_directory = os.getcwd() if isP4regime: psioh = core.IOManager.shared_object() psio = core.IO.shared_object() os.chdir(psioh.get_default_path()) gcp_tmpdir = 'psi.' + str(os.getpid()) + '.' + psio.get_default_namespace() + \ '.gcp.' + str(uuid.uuid4())[:8] else: gcp_tmpdir = os.environ['HOME'] + os.sep + 'gcp_' + str(uuid.uuid4())[:8] if os.path.exists(gcp_tmpdir) is False: os.mkdir(gcp_tmpdir) os.chdir(gcp_tmpdir) # Write gcp_parameters file that governs cp correction # paramcontents = gcp_server(func, dashlvl, 'dftd3') # paramfile1 = 'dftd3_parameters' # older patched name # with open(paramfile1, 'w') as handle: # handle.write(paramcontents) # paramfile2 = '.gcppar' # with open(paramfile2, 'w') as handle: # handle.write(paramcontents) ###Two kinds of parameter files can be read in: A short and an extended version. Both are read from ###$HOME/.gcppar.$HOSTNAME by default. If the option -local is specified the file is read in from ###the current working directory: .gcppar ###The short version reads in: basis-keywo # Write dftd3_geometry file that supplies geometry to dispersion calc numAtoms = self.natom() geom = self.save_string_xyz() reals = [] for line in geom.splitlines(): lline = line.split() if len(lline) != 4: continue if lline[0] == 'Gh': numAtoms -= 1 else: reals.append(line) geomtext = str(numAtoms) + '\n\n' for line in reals: geomtext += line.strip() + '\n' geomfile = './gcp_geometry.xyz' with open(geomfile, 'w') as handle: handle.write(geomtext) # TODO somehow the variations on save_string_xyz and # whether natom and chgmult does or doesn't get written # have gotten all tangled. I fear this doesn't work # the same btwn libmints and qcdb or for ghosts # Call gcp program command = ['gcp', geomfile] command.extend(['-level', func]) if derint != 0: command.append('-grad') try: #print('command', command) dashout = subprocess.Popen(command, stdout=subprocess.PIPE, env=lenv) except OSError as e: raise ValidationError('Program gcp not found in path. %s' % e) out, err = dashout.communicate() # Parse output success = False for line in out.splitlines(): line = line.decode('utf-8') if re.match(' Egcp:', line): sline = line.split() dashd = float(sline[1]) if re.match(' normal termination of gCP', line): success = True if not success: os.chdir(current_directory) raise Dftd3Error("""Unsuccessful gCP run.""") # Parse grad output if derint != 0: derivfile = './gcp_gradient' dfile = open(derivfile, 'r') dashdderiv = [] for line in geom.splitlines(): lline = line.split() if len(lline) != 4: continue if lline[0] == 'Gh': dashdderiv.append([0.0, 0.0, 0.0]) else: dashdderiv.append([float(x.replace('D', 'E')) for x in dfile.readline().split()]) dfile.close() if len(dashdderiv) != self.natom(): raise ValidationError('Program gcp gradient file has %d atoms- %d expected.' % \ (len(dashdderiv), self.natom())) # Prepare results for Psi4 if isP4regime and derint != 0: core.set_variable('GCP CORRECTION ENERGY', dashd) psi_dashdderiv = core.Matrix.from_list(dashdderiv) # Print program output to file if verbose if not verbose and isP4regime: verbose = True if core.get_option('SCF', 'PRINT') >= 3 else False if verbose: text = '\n ==> GCP Output <==\n' text += out.decode('utf-8') if derint != 0: with open(derivfile, 'r') as handle: text += handle.read().replace('D', 'E') text += '\n' if isP4regime: core.print_out(text) else: print(text) # # Clean up files and remove scratch directory # os.unlink(paramfile1) # os.unlink(paramfile2) # os.unlink(geomfile) # if derint != 0: # os.unlink(derivfile) # if defmoved is True: # os.rename(defaultfile + '_hide', defaultfile) # clean up files and remove scratch directory os.chdir('..') try: shutil.rmtree(gcp_tmpdir) except OSError as err: raise OSError('Unable to remove gcp temporary directory: {}'.format(gcp_tmpdir)) from err os.chdir(current_directory) # return -D & d(-D)/dx if derint == -1: return dashd, dashdderiv elif derint == 0: return dashd elif derint == 1: return psi_dashdderiv
try: # Attach method to libmints psi4.Molecule class core.Molecule.run_gcp = run_gcp except (NameError, AttributeError): # But don't worry if that doesn't work b/c # it'll get attached to qcdb.Molecule class pass