Skip to content

Commit 84c6c1b

Browse files
committed
Enable integration with QCSchema
1 parent 8b2a197 commit 84c6c1b

File tree

8 files changed

+677
-7
lines changed

8 files changed

+677
-7
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ env:
1414
pytest-cov
1515
cffi
1616
numpy
17+
qcelemental
1718
1819
jobs:
1920
gcc-build:

python/README.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,39 @@ A more pythonic interface is provided in the ``dftd3.interface`` module which ca
3737
# => -0.029489232932494884
3838
3939
40+
QCSchema Integration
41+
~~~~~~~~~~~~~~~~~~~~
42+
43+
This Python API natively understands QCSchema and the `QCArchive infrastructure <http://docs.qcarchive.molssi.org>`_.
44+
If the QCElemental package is installed the ``dftd3.qcschema`` module becomes importable and provides the ``run_qcschema`` function.
45+
46+
.. code:: python
47+
48+
from dftd3.qcschema import run_qcschema
49+
import qcelemental as qcel
50+
atomic_input = qcel.models.AtomicInput(
51+
molecule = qcel.models.Molecule(
52+
symbols = ["O", "H", "H"],
53+
geometry = [
54+
0.00000000000000, 0.00000000000000, -0.73578586109551,
55+
1.44183152868459, 0.00000000000000, 0.36789293054775,
56+
-1.44183152868459, 0.00000000000000, 0.36789293054775
57+
],
58+
),
59+
driver = "energy",
60+
model = {
61+
"method": "tpss",
62+
},
63+
keywords = {
64+
"level_hint": "d3bj",
65+
},
66+
)
67+
68+
atomic_result = run_qcschema(atomic_input)
69+
print(atomic_result.return_result)
70+
# => -0.0004204244108151285
71+
72+
4073
Building the extension module
4174
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4275

python/dftd3/interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class DampingParam:
136136
def __init__(self, **kwargs):
137137
"""Create new damping parameter from method name or explicit data"""
138138

139-
if "method" in kwargs:
139+
if "method" in kwargs and kwargs["method"] is not None:
140140
self._param = self.load_param(**kwargs)
141141
else:
142142
self._param = self.new_param(**kwargs)

python/dftd3/libdftd3.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def new_structure(natoms, numbers, positions, lattice, periodic):
8181
def _delete_model(disp):
8282
"""Delete a DFT-D3 dispersion model object"""
8383
ptr = ffi.new("dftd3_model *")
84-
ptr[0] = mol
84+
ptr[0] = disp
8585
lib.dftd3_delete_model(ptr)
8686

8787

@@ -93,11 +93,11 @@ def new_d3_model(mol):
9393
return model
9494

9595

96-
def _delete_param(disp):
96+
def _delete_param(param):
9797
"""Delete a DFT-D3 damping parameteter object"""
98-
ptr = ffi.new("dftd3_model *")
99-
ptr[0] = mol
100-
lib.dftd3_delete_model(ptr)
98+
ptr = ffi.new("dftd3_param *")
99+
ptr[0] = param
100+
lib.dftd3_delete_param(ptr)
101101

102102

103103
def new_zero_damping(s6, s8, s9, rs6, rs8, alp):

python/dftd3/qcschema.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# This file is part of s-dftd3.
2+
# SPDX-Identifier: LGPL-3.0-or-later
3+
#
4+
# s-dftd3 is free software: you can redistribute it and/or modify it under
5+
# the terms of the Lesser GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# s-dftd3 is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# Lesser GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the Lesser GNU General Public License
15+
# along with s-dftd3. If not, see <https://www.gnu.org/licenses/>.
16+
"""Integration with the `QCArchive infrastructure <http://docs.qcarchive.molssi.org>`_.
17+
18+
This module provides a way to translate QCSchema or QCElemental Atomic Input
19+
into a format understandable by the ``dftd3`` API which in turn provides the
20+
calculation results in a QCSchema compatible format.
21+
22+
Supported keywords are
23+
24+
======================== =========== ============================================
25+
Keyword Default Description
26+
======================== =========== ============================================
27+
level_hint None Dispersion correction level (allowed: "d4")
28+
params_tweaks None Optional dict with the damping parameters
29+
======================== =========== ============================================
30+
31+
The params_tweaks dict contains the damping parameters, at least s8, a1 and a2
32+
must be provided
33+
34+
======================== =========== ============================================
35+
Tweakable parameter Default Description
36+
======================== =========== ============================================
37+
s6 1.0 Scaling of the dipole-dipole dispersion
38+
s8 None Scaling of the dipole-quadrupole dispersion
39+
s9 1.0 Scaling of the three-body dispersion energy
40+
a1 None Scaling of the critical radii
41+
a2 None Offset of the critical radii
42+
alp 16.0 Exponent of the zero damping (ATM only)
43+
======================== =========== ============================================
44+
45+
Either method or s8, a1 and a2 must be provided, s9 can be used to overwrite
46+
the ATM scaling if the method is provided in the model.
47+
Disabling the three-body dispersion (s9=0.0) changes the internal selection rules
48+
for damping parameters of a given method and prefers special two-body only
49+
damping parameters if available!
50+
If input_data.model.method and input_data.keywords["params_tweaks"] are both
51+
provided, the former "wins" without any consistency checking. Note that with the
52+
QCEngine QCSchema runner for classic ``dftd3``, the latter "wins" without any
53+
consistency checking.
54+
55+
Example
56+
-------
57+
58+
>>> from dftd3.qcschema import run_qcschema
59+
>>> import qcelemental as qcel
60+
>>> atomic_input = qcel.models.AtomicInput(
61+
... molecule = qcel.models.Molecule(
62+
... symbols = ["O", "H", "H"],
63+
... geometry = [
64+
... 0.00000000000000, 0.00000000000000, -0.73578586109551,
65+
... 1.44183152868459, 0.00000000000000, 0.36789293054775,
66+
... -1.44183152868459, 0.00000000000000, 0.36789293054775
67+
... ],
68+
... ),
69+
... driver = "energy",
70+
... model = {
71+
... "method": "TPSS-D3(BJ)",
72+
... },
73+
... keywords = {},
74+
... )
75+
...
76+
>>> atomic_result = run_qcschema(atomic_input)
77+
>>> atomic_result.return_result
78+
-0.0002667885779142513
79+
"""
80+
81+
from typing import Union
82+
from .interface import (
83+
DispersionModel,
84+
RationalDampingParam,
85+
ZeroDampingParam,
86+
ModifiedRationalDampingParam,
87+
ModifiedZeroDampingParam,
88+
)
89+
from .libdftd3 import get_api_version
90+
import numpy as np
91+
import qcelemental as qcel
92+
93+
94+
_supported_drivers = [
95+
"energy",
96+
"gradient",
97+
]
98+
99+
_available_levels = [
100+
"d3bj",
101+
"d3zero",
102+
"d3bjm",
103+
"d3zerom",
104+
]
105+
106+
_damping_param = {
107+
"d3bj": RationalDampingParam,
108+
"d3zero": ZeroDampingParam,
109+
"d3bjm": ModifiedRationalDampingParam,
110+
"d3zerom": ModifiedZeroDampingParam,
111+
}
112+
113+
_clean_dashlevel = str.maketrans("", "", "()")
114+
115+
116+
def run_qcschema(
117+
input_data: Union[dict, qcel.models.AtomicInput]
118+
) -> qcel.models.AtomicResult:
119+
"""Perform disperson correction based on an atomic inputmodel"""
120+
121+
if not isinstance(input_data, qcel.models.AtomicInput):
122+
atomic_input = qcel.models.AtomicInput(**input_data)
123+
else:
124+
atomic_input = input_data
125+
ret_data = atomic_input.dict()
126+
127+
provenance = {
128+
"creator": "s-dftd3",
129+
"version": get_api_version(),
130+
"routine": "dftd4.qcschema.run_qcschema",
131+
}
132+
success = False
133+
return_result = 0.0
134+
properties = {}
135+
136+
# Since it is a level hint we a forgiving if it is not present,
137+
# we are much less forgiving if the wrong level is hinted here.
138+
_level = atomic_input.keywords.get("level_hint", "d3bj")
139+
if _level.lower() not in _available_levels:
140+
ret_data.update(
141+
provenance=provenance,
142+
success=success,
143+
properties=properties,
144+
return_result=return_result,
145+
error=qcel.models.ComputeError(
146+
error_type="input error",
147+
error_message="Level '{}' is invalid for this dispersion correction".format(
148+
_level
149+
),
150+
),
151+
)
152+
return qcel.models.AtomicResult(**ret_data)
153+
154+
# Check if the method is provided and strip the “dashlevel” from the method
155+
_method = atomic_input.model.method
156+
if len(_method) == 0:
157+
_method = None
158+
else:
159+
_method = _method.split("-")
160+
if _method[-1].lower().translate(_clean_dashlevel) == _level.lower():
161+
_method.pop()
162+
_method = "-".join(_method)
163+
164+
# Obtain the parameters for the damping function
165+
_input_param = atomic_input.keywords.get("params_tweaks", {})
166+
167+
try:
168+
param = _damping_param[_level](
169+
method=_method,
170+
**_input_param,
171+
)
172+
173+
disp = DispersionModel(
174+
atomic_input.molecule.atomic_numbers[atomic_input.molecule.real],
175+
atomic_input.molecule.geometry[atomic_input.molecule.real],
176+
)
177+
178+
res = disp.get_dispersion(
179+
param=param,
180+
grad=atomic_input.driver == "gradient",
181+
)
182+
extras = {"dftd3": res}
183+
184+
if atomic_input.driver == "gradient":
185+
if all(atomic_input.molecule.real):
186+
fullgrad = res.get("gradient")
187+
else:
188+
ireal = np.argwhere(atomic_input.molecule.real).reshape((-1))
189+
fullgrad = np.zeros_like(atomic_input.molecule.geometry)
190+
fullgrad[ireal, :] = res.get("gradient")
191+
192+
properties.update(return_energy=res.get("energy"))
193+
194+
success = atomic_input.driver in _supported_drivers
195+
if atomic_input.driver == "energy":
196+
return_result = properties["return_energy"]
197+
elif atomic_input.driver == "gradient":
198+
return_result = fullgrad
199+
else:
200+
ret_data.update(
201+
error=qcel.models.ComputeError(
202+
error_type="input error",
203+
error_message="Calculation succeeded but invalid driver request provided",
204+
),
205+
)
206+
207+
ret_data["extras"].update(extras)
208+
209+
except RuntimeError as e:
210+
ret_data.update(
211+
error=qcel.models.ComputeError(
212+
error_type="input error", error_message=str(e)
213+
),
214+
),
215+
216+
ret_data.update(
217+
provenance=provenance,
218+
success=success,
219+
properties=properties,
220+
return_result=return_result,
221+
)
222+
223+
return qcel.models.AtomicResult(**ret_data)

0 commit comments

Comments
 (0)