Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

efp: start pylibefp interface #144

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
efp: start pylibefp interface
  • Loading branch information
loriab committed Aug 28, 2019
commit 165d3d14b42cd071fe1c4646c997972725aaafbc
2 changes: 2 additions & 0 deletions qcengine/programs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..exceptions import InputError, ResourceError
from .cfour import CFOURHarness
from .dftd3 import DFTD3Harness
from .pylibefp import PylibEFPHarness
from .entos import EntosHarness
from .gamess import GAMESSHarness
from .molpro import MolproHarness
Expand Down Expand Up @@ -101,3 +102,4 @@ def list_available_programs() -> Set[str]:
register_program(NWChemHarness())
register_program(CFOURHarness())
register_program(EntosHarness())
register_program(PylibEFPHarness())
165 changes: 165 additions & 0 deletions qcengine/programs/pylibefp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""
Calls the PylibEFP interface to LibEFP.
"""
import pprint
from typing import Dict

import qcelemental as qcel
from qcelemental.models import Provenance, Result
from qcelemental.util import safe_version, which_import

#from ..exceptions import InputError, RandomError, ResourceError, UnknownError
from .model import ProgramHarness

pp = pprint.PrettyPrinter(width=120, compact=True, indent=1)


class PylibEFPHarness(ProgramHarness):

_defaults = {
"name": "PylibEFP",
"scratch": False,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably thread safe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all in memory and all functions acting on same struct instance, so yes.

"thread_safe": False,
"thread_parallel": False, # can be but not the way Psi usually builds it
"node_parallel": False,
"managed_memory": False,
}
version_cache: Dict[str, str] = {}

class Config(ProgramHarness.Config):
pass

@staticmethod
def found(raise_error: bool = False) -> bool:
return which_import('pylibefp',
return_bool=True,
raise_error=raise_error,
raise_msg='Please install via `conda install pylibefp -c psi4`.')

def get_version(self) -> str:
self.found(raise_error=True)

which_prog = which_import('pylibefp')
if which_prog not in self.version_cache:
import pylibefp
self.version_cache[which_prog] = safe_version(pylibefp.__version__)

candidate_version = self.version_cache[which_prog]

if "undef" in candidate_version:
raise TypeError(
"Using custom build without tags. Please pull git tags with `git pull origin master --tags`.")

return candidate_version

def compute(self, input_model: 'ResultInput', config: 'JobConfig') -> 'Result':
"""
Runs PylibEFP in API mode
"""
self.found(raise_error=True)

# if parse_version(self.get_version()) < parse_version("1.2"):
# raise ResourceError("Psi4 version '{}' not understood.".format(self.get_version()))

# Setup the job
input_data = input_model.dict(encoding="json")
# input_data["nthreads"] = config.ncores
# input_data["memory"] = int(config.memory * 1024 * 1024 * 1024 * 0.95) # Memory in bytes
input_data["success"] = False
input_data["return_output"] = True

import pylibefp
efpobj = pylibefp.core.efp()

efp_extras = input_model.molecule.extras['efp_molecule']['extras']
efpobj.add_potential(efp_extras['fragment_files'])
efpobj.add_fragment(efp_extras['fragment_files'])
for ifr, (hint_type, geom_hint) in enumerate(zip(efp_extras['hint_types'], efp_extras['geom_hints'])):
print('SEt_frag_coordinates', ifr, hint_type, geom_hint)
efpobj.set_frag_coordinates(ifr, hint_type, geom_hint)
efpobj.prepare()

# print efp geom in [A]
print(efpobj.banner())
print(efpobj.geometry_summary(units_to_bohr=qcel.constants.bohr2angstroms))

if input_model.model.method != 'efpefp':
raise InputError

# set keywords
efpobj.set_opts(input_model.keywords)
#efpobj.set_opts(efpopts, label='psi', append='psi')

if input_model.driver == 'energy':
do_gradient = False
elif input_model.driver == 'gradient':
do_gradient = True
else:
raise InputError

# compute and report
efpobj.compute(do_gradient=do_gradient)
print(efpobj.energy_summary(label='psi'))

ene = efpobj.get_energy(label='psi')

pp.pprint(ene)
print('<<< get_opts(): ', efpobj.get_opts(), '>>>')
#print('<<< summary(): ', efpobj.summary(), '>>>')
print('<<< get_energy():', ene, '>>>')
print('<<< get_atoms(): ', efpobj.get_atoms(), '>>>')
print(efpobj.energy_summary())
print(efpobj.geometry_summary(units_to_bohr=qcel.constants.bohr2angstroms))
print(efpobj.geometry_summary(units_to_bohr=1.0))

###### psi4 proc
#def run_efp(name, **kwargs):
# try:
# efpobj = efp_molecule.EFP
# except AttributeError:
# raise ValidationError("""Method 'efp' not available without EFP fragments in molecule""")
#
# core.set_variable('EFP ELST ENERGY', ene['electrostatic'] + ene['charge_penetration'] + ene['electrostatic_point_charges'])
# core.set_variable('EFP IND ENERGY', ene['polarization'])
# core.set_variable('EFP DISP ENERGY', ene['dispersion'])
# core.set_variable('EFP EXCH ENERGY', ene['exchange_repulsion'])
# core.set_variable('EFP TOTAL ENERGY', ene['total'])
# core.set_variable('CURRENT ENERGY', ene['total'])
#
# if do_gradient:
# core.print_out(efpobj.gradient_summary())
#
# core.set_variable('EFP TORQUE', torq)
#
# output_data = input_data

if input_model.driver == 'energy':
retres = ene['total']


# elif input_model.driver == 'gradient':
# torq = efpobj.get_gradient()
# #torq = core.Matrix.from_array(np.asarray(torq).reshape(-1, 6))
# retres = torq

output_data = {
'schema_name': 'qcschema_output',
'schema_version': 1,
# 'extras': {
# 'outfiles': outfiles,
# },
'properties': {},
'provenance': Provenance(creator="PylibEFP", version=self.get_version(), routine="pylibefp"),
'return_result': retres,
# 'stdout': stdout,
}

# # got to even out who needs plump/flat/Decimal/float/ndarray/list
# # Decimal --> str preserves precision
# output_data['extras']['qcvars'] = {
# k.upper(): str(v) if isinstance(v, Decimal) else v
# for k, v in qcel.util.unnp(qcvars, flat=True).items()
# }

output_data['success'] = True
return Result(**{**input_model.dict(), **output_data})
62 changes: 62 additions & 0 deletions qcengine/programs/tests/test_pylibefp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from qcelemental.testing import compare, compare_recursive, compare_values

import qcengine as qcng
from qcengine.testing import using_pylibefp

b2a = 0.529177
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume efp uses its own conversion? Can we add a comment here about that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look it up, but this particular one is just to get the geometry in with both round numbers and to match the answers taken from libefp test suite. outputs are all in Eh, a0. So this conversion isn't a broad concern.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Should be fine as long as we note it.

a2b = 1.0 / b2a


@using_pylibefp
def test_total_1a():

resi = {
'molecule': {
'geometry': [0, 0, 0], # dummy
'symbols': ['He'], # dummy
'extras': {
'efp_molecule': {
'geometry': [],
'symbols': [],
'extras': {
'fragment_files': ['h2o', 'nh3'],
'hint_types': ['xyzabc', 'xyzabc'],
'geom_hints': [
[0.0 * a2b, 0.0 * a2b, 0.0 * a2b, 1.0, 2.0, 3.0],
[5.0 * a2b, 0.0 * a2b, 0.0 * a2b, 5.0, 2.0, 8.0],
]
}
}
}
},
'driver': 'energy',
'model': {
'method': 'efpefp',
},
'keywords': {
'elec': True,
'elec_damp': 'screen',
'xr': True,
'pol': True,
'disp': True,
'disp_damp': 'tt',
}
}

res = qcng.compute(resi, 'pylibefp', raise_error=True, return_dict=False)

atol = 1.e-6
assert compare_values(0.0001922903, res.return_result, atol=atol)


# expected_ene = blank_ene()
# expected_ene['elec'] = expected_ene['electrostatic'] = 0.0002900482
# expected_ene['xr'] = expected_ene['exchange_repulsion'] = 0.0000134716
# expected_ene['pol'] = expected_ene['polarization'] = 0.0002777238 - expected_ene['electrostatic']
# expected_ene['disp'] = expected_ene['dispersion'] = -0.0000989033
# expected_ene['total'] = 0.0001922903
# assert compare(2, asdf.get_frag_count(), sys._getframe().f_code.co_name + ': nfrag')
# assert compare_values(0.0, asdf.get_frag_charge(1), sys._getframe().f_code.co_name + ': f_chg', atol=1.e-6)
# assert compare(1, asdf.get_frag_multiplicity(1), sys._getframe().f_code.co_name + ': f_mult')
# assert compare('NH3', asdf.get_frag_name(1), sys._getframe().f_code.co_name + ': f_name')
# assert compare_recursive(expected_ene, ene, sys._getframe().f_code.co_name + ': ene', atol=1.e-6)
2 changes: 2 additions & 0 deletions qcengine/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def get_job(self):
"cfour": which('xcfour', return_bool=True),
"gamess": which('rungms', return_bool=True),
"nwchem": which('nwchem', return_bool=True),
"pylibefp": which_import("pylibefp", return_bool=True),
}


Expand Down Expand Up @@ -158,6 +159,7 @@ def _build_pytest_skip(program):
using_cfour = _build_pytest_skip("cfour")
using_gamess = _build_pytest_skip("gamess")
using_nwchem = _build_pytest_skip("nwchem")
using_pylibefp = _build_pytest_skip("pylibefp")

using_dftd3_321 = pytest.mark.skipif(is_program_new_enough("dftd3", "3.2.1") is False,
reason='DFTD3 does not include 3.2.1 features. Update package and add to PATH')