diff --git a/.travis.yml b/.travis.yml index 01c5f5aa2..0cbe443a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,10 @@ matrix: env: - PYTHON_VER=3.6 - PROG=ANI + - os: linux + env: + - PYTHON_VER=3.7 + - PROG=ADCC - os: linux env: - PYTHON_VER=3.6 @@ -63,6 +67,8 @@ install: python devtools/scripts/conda_env.py -n=test -p=$PYTHON_VER devtools/conda-envs/psi-nightly.yaml elif [ $PROG == "ANI" ]; then python devtools/scripts/conda_env.py -n=test -p=$PYTHON_VER devtools/conda-envs/torchani.yaml + elif [ $PROG == "ADCC" ]; then + python devtools/scripts/conda_env.py -n=test -p=$PYTHON_VER devtools/conda-envs/adcc.yaml elif [ $PROG == "OPENMM" ]; then python devtools/scripts/conda_env.py -n=test -p=$PYTHON_VER devtools/conda-envs/openmm.yaml elif [ $PROG == "NWCHEM" ]; then @@ -81,7 +87,7 @@ install: # Any kind of post env ceation - | - if [ $PROG == "PSI4DEV" ]; then + if [[ $PROG == "PSI4DEV" || $PROG == "ADCC" ]]; then conda remove qcengine --force elif [ $PROG == "QCORE" ]; then qcore --accept-license diff --git a/codemeta.json b/codemeta.json index d498d42c6..2f85b1863 100644 --- a/codemeta.json +++ b/codemeta.json @@ -289,6 +289,23 @@ "affiliation": "Hylleraas Centre for Quantum Molecular Sciences, UiT -- The Arctic University of Norway", "@id": "https://orcid.org/0000-0002-5452-9239", "identifier": "https://github.com/robertodr" + }, + { + "@type": "Person", + "givenName": "Maximilian", + "familyName": "Scheurer", + "affiliation": "Interdisciplinary Center for Scientific Computing, Heidelberg University", + "@id": "https://orcid.org/0000-0003-0592-3464", + "identifier": "https://github.com/maxscheurer" + }, + { + "@type": "Person", + "givenName": "Michael", + "additionalName": "F.", + "familyName": "Herbst", + "affiliation": "Inria Paris and CERMICS, \u00C9cole des Ponts ParisTech", + "@id": "https://orcid.org/0000-0003-0378-7921", + "identifier": "https://github.com/mfherbst" } ], "developmentStatus": "active", diff --git a/devtools/conda-envs/adcc.yaml b/devtools/conda-envs/adcc.yaml new file mode 100644 index 000000000..7a5457633 --- /dev/null +++ b/devtools/conda-envs/adcc.yaml @@ -0,0 +1,25 @@ +name: test +channels: + - adcc + - psi4/label/dev + - conda-forge +dependencies: + - adcc>=0.15.7 + - psi4 + - blas=*=mkl # not needed but an example of disuading solver from openblas and old psi + - intel-openmp!=2019.5 + + # Core + - python + - pyyaml + - py-cpuinfo + - psutil + - qcelemental >=0.13.0 + - pydantic>=1.0.0 + - msgpack-python + + # Testing + - pytest + - pytest-cov + - codecov + diff --git a/docs/source/index.rst b/docs/source/index.rst index a38140beb..3ca7a4105 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -58,6 +58,7 @@ Currently available compute backends for single results are as follow: - Quantum Chemistry: + - `adcc `_ - `Entos `_ - `Molpro `_ - `Psi4 `_ diff --git a/docs/source/program_overview.rst b/docs/source/program_overview.rst index 472d05fc8..75375012b 100644 --- a/docs/source/program_overview.rst +++ b/docs/source/program_overview.rst @@ -10,6 +10,8 @@ Quantum Chemistry +---------------+------------+---+---+---+------------+--------------+ | Program | Production | E | G | H | Properties | Wavefunction + +===============+============+===+===+===+============+==============+ +| adcc | ✘ | ✓ | ✘ | ✘ | ✓ | ✘ | ++---------------+------------+---+---+---+------------+--------------+ | CFOUR | ✘ | ✓ | ✓ | ✘ | ✓ | ✘ | +---------------+------------+---+---+---+------------+--------------+ | Qcore (Entos) | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ | diff --git a/qcengine/programs/adcc.py b/qcengine/programs/adcc.py new file mode 100644 index 000000000..a8a0ebb6c --- /dev/null +++ b/qcengine/programs/adcc.py @@ -0,0 +1,128 @@ +""" +Calls adcc +""" +from typing import Dict, TYPE_CHECKING + +from qcelemental.util import safe_version, which_import +from qcelemental.models import AtomicResult, Provenance +from .qcvar_identities_resources import build_atomicproperties + +from .model import ProgramHarness +from ..exceptions import InputError, UnknownError + +if TYPE_CHECKING: + from qcelemental.models import AtomicInput + + from ..config import TaskConfig + +# + + +class AdccHarness(ProgramHarness): + _defaults = { + "name": "adcc", + "scratch": False, + "thread_safe": False, + "thread_parallel": True, + "node_parallel": False, + "managed_memory": True, + } + version_cache: Dict[str, str] = {} + + class Config(ProgramHarness.Config): + pass + + @staticmethod + def found(raise_error: bool = False) -> bool: + """Whether adcc harness is ready for operation. + Parameters + ---------- + raise_error: bool + Passed on to control negative return between False and ModuleNotFoundError raised. + Returns + ------- + bool + If adcc and psi4 are found, returns True. + If raise_error is False and adcc or psi4 is missing, returns False. + If raise_error is True and adcc or psi4 are missing, the error message is raised. + """ + found_adcc = which_import( + "adcc", + return_bool=True, + raise_error=raise_error, + raise_msg="Please install via `conda install adcc -c adcc`.", + ) + found_psi4 = which_import( + "psi4", + return_bool=True, + raise_error=raise_error, + raise_msg="Please install psi4 for adcc harness via `conda install psi4 -c psi4`.", + ) + return found_adcc and found_psi4 + + def get_version(self) -> str: + """Return the currently used version of adcc""" + self.found(raise_error=True) + + which_prog = which_import("adcc") + if which_prog not in self.version_cache: + import adcc + + self.version_cache[which_prog] = safe_version(adcc.__version__) + return self.version_cache[which_prog] + + def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicResult": + """ + Runs adcc + """ + self.found(raise_error=True) + + import adcc + import psi4 + + mol = input_model.molecule + model = input_model.model + conv_tol = input_model.keywords.get("conv_tol", 1e-6) + + psi4_molecule = psi4.core.Molecule.from_schema(dict(mol.dict(), fix_symmetry="c1")) + psi4.core.clean() + psi4.core.be_quiet() + psi4.set_options( + { + "basis": model.basis, + "scf_type": "pk", + "e_convergence": conv_tol / 100, + "d_convergence": conv_tol / 10, + # 'maxiter': max_iter, + "reference": "RHF" if mol.molecular_multiplicity == 1 else "UHF", + } + ) + _, wfn = psi4.energy("HF", return_wfn=True, molecule=psi4_molecule) + adcc.set_n_threads(config.ncores) + compute_success = False + try: + adcc_state = adcc.run_adc(wfn, method=model.method, **input_model.keywords) + compute_success = adcc_state.converged + except adcc.InputError as e: + raise InputError(str(e)) + except Exception as e: + raise UnknownError(str(e)) + + input_data = input_model.dict(encoding="json") + output_data = input_data.copy() + output_data["success"] = compute_success + + if compute_success: + output_data["return_result"] = adcc_state.excitation_energy[0] + + extract_props = input_model.driver == "properties" + qcvars = adcc_state.to_qcvars(recurse=True, properties=extract_props) + atprop = build_atomicproperties(qcvars) + output_data["extras"]["qcvars"] = qcvars + output_data["properties"] = atprop + + provenance = Provenance(creator="adcc", version=self.get_version(), routine="adcc").dict() + provenance["nthreads"] = adcc.get_n_threads() + output_data["provenance"] = provenance + + return AtomicResult(**output_data) diff --git a/qcengine/programs/base.py b/qcengine/programs/base.py index 47ec6495e..5a9423400 100644 --- a/qcengine/programs/base.py +++ b/qcengine/programs/base.py @@ -5,6 +5,7 @@ from typing import Set from ..exceptions import InputError, ResourceError +from .adcc import AdccHarness from .cfour import CFOURHarness from .dftd3 import DFTD3Harness from .gcp import GCPHarness @@ -99,6 +100,7 @@ def list_available_programs() -> Set[str]: # Quantum +register_program(AdccHarness()) register_program(CFOURHarness()) register_program(EntosHarness()) # Duplicate of Qcore harness to transition the namespace, to be deprecated register_program(GAMESSHarness()) diff --git a/qcengine/programs/tests/test_adcc.py b/qcengine/programs/tests/test_adcc.py new file mode 100644 index 000000000..c76aea616 --- /dev/null +++ b/qcengine/programs/tests/test_adcc.py @@ -0,0 +1,40 @@ +"""Tests for adcc functionality""" +import pytest +import numpy as np +import qcengine as qcng +import qcelemental as qcel + +from qcengine.testing import using +from qcelemental.testing import compare_values + + +@pytest.fixture +def h2o(): + return qcel.models.Molecule.from_data( + """ + O 0.0 0.000 -0.129 + H 0.0 -1.494 1.027 + H 0.0 1.494 1.027 + """ + ) + + +@using("adcc") +def test_run(h2o): + inp = qcel.models.AtomicInput( + molecule=h2o, driver="properties", model={"method": "adc2", "basis": "sto-3g"}, keywords={"n_singlets": 3} + ) + ret = qcng.compute(inp, "adcc", raise_error=True, local_options={"ncores": 1}, return_dict=True) + + ref_excitations = np.array([0.0693704245883876, 0.09773854881340478, 0.21481589246935925]) + ref_hf_energy = -74.45975898670224 + ref_mp2_energy = -74.67111187456267 + assert ret["success"] is True + + qcvars = ret["extras"]["qcvars"] + + assert qcvars["EXCITATION KIND"] == "SINGLET" + assert compare_values(ref_excitations[0], ret["return_result"]) + assert compare_values(ref_hf_energy, ret["properties"]["scf_total_energy"]) + assert compare_values(ref_mp2_energy, ret["properties"]["mp2_total_energy"]) + assert compare_values(ref_excitations, qcvars["ADC2 EXCITATION ENERGIES"]) diff --git a/qcengine/testing.py b/qcengine/testing.py index cfb6e5490..c2e00072c 100644 --- a/qcengine/testing.py +++ b/qcengine/testing.py @@ -140,6 +140,7 @@ def get_job(self): # Figure out what is imported _programs = { + "adcc": is_program_new_enough("adcc", "0.15.7"), "cfour": which("xcfour", return_bool=True), "dftd3": which("dftd3", return_bool=True), "dftd3_321": is_program_new_enough("dftd3", "3.2.1"),