Skip to content

Commit 89bbb17

Browse files
committed
Refactor Python API
- make error handling cleaner by using functools - error out if both a method and parameters are provided to damping function constructor
1 parent 982bb90 commit 89bbb17

File tree

9 files changed

+263
-224
lines changed

9 files changed

+263
-224
lines changed

python/README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Python interface for the generally applicable atomic-charge dependent London dis
55
This Python project is targeted at developers who want to interface their project via Python with ``dftd4``.
66

77
This interface provides access to the C-API of ``dftd4`` via the CFFI module.
8-
The low-level CFFI interface is available in the ``dftd4.libdftd4`` module and only required for implementing other interfaces.
8+
The low-level CFFI interface is available in the ``dftd4.library`` module and only required for implementing other interfaces.
99
A more pythonic interface is provided in the ``dftd4.interface`` module which can be used to build more specific interfaces.
1010

1111
.. code:: python

python/dftd4/interface.py

Lines changed: 63 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,20 @@
1313
#
1414
# You should have received a copy of the Lesser GNU General Public License
1515
# along with dftd4. If not, see <https://www.gnu.org/licenses/>.
16-
"""Wrapper around the C-API of the dftd4 shared library."""
16+
"""
17+
Wrapper around the C-API of the dftd4 shared library.
18+
It provides the definition the basic interface to the library for most further integration
19+
in other Python frameworks.
20+
21+
The classes defined here allow a more Pythonic usage of the API object provided by the
22+
library in actual workflows than the low-level access provided in the CFFI generated wrappers.
23+
"""
1724

1825
from typing import Optional
1926
import numpy as np
2027

2128

22-
from .libdftd4 import (
23-
ffi as _ffi,
24-
lib as _lib,
25-
new_error,
26-
new_structure,
27-
new_d4_model,
28-
custom_d4_model,
29-
new_rational_damping,
30-
load_rational_damping,
31-
handle_error,
32-
)
29+
from . import library
3330

3431

3532
class Structure:
@@ -41,7 +38,7 @@ class Structure:
4138
and immutable atomic identifiers
4239
"""
4340

44-
_mol = _ffi.NULL
41+
_mol = library.ffi.NULL
4542

4643
def __init__(
4744
self,
@@ -78,7 +75,7 @@ def __init__(
7875
else:
7976
_periodic = None
8077

81-
self._mol = new_structure(
78+
self._mol = library.new_structure(
8279
self._natoms,
8380
_cast("int*", _numbers),
8481
_cast("double*", _positions),
@@ -116,43 +113,57 @@ def update(
116113
else:
117114
_lattice = None
118115

119-
_error = new_error()
120-
_lib.dftd4_update_structure(
121-
_error,
116+
library.update_structure(
122117
self._mol,
123118
_cast("double*", _positions),
124119
_cast("double*", _lattice),
125120
)
126121

127-
handle_error(_error)
128-
129122

130123
class DampingParam:
131-
"""Damping parameters for the dispersion correction"""
124+
"""
125+
Rational damping function for DFT-D4.
126+
127+
The damping parameters contained in the object are immutable. To change the
128+
parametrization, a new object must be created. Furthermore, the object is
129+
opaque to the user and the contained data cannot be accessed directly.
130+
131+
There are two main ways provided to generate a new damping parameter object:
132+
133+
1. a method name is passed to the constructor, the library will load the
134+
required data from the *s-dftd3* shared library.
135+
136+
2. all required parameters are passed to the constructor and the library will
137+
generate an object from the given parameters.
138+
139+
.. note::
140+
141+
Mixing of the two methods is not allowed to avoid partial initialization
142+
of any created objects. Users who need full control over the creation
143+
of the object should use the second method.
144+
"""
132145

133-
_param = _ffi.NULL
146+
_param = library.ffi.NULL
134147

135148
def __init__(self, *, method=None, **kwargs):
136149
"""Create new damping parameter from method name or explicit data"""
137150

138-
if method is not None:
139-
_method = _ffi.new("char[]", method.encode())
140-
self._param = load_rational_damping(
141-
_method,
142-
kwargs.get("s9", 1.0) > 0.0,
143-
)
151+
if "method" in kwargs and kwargs["method"] is None:
152+
del kwargs["method"]
153+
154+
if "method" in kwargs:
155+
self._param = self.load_param(**kwargs)
144156
else:
145-
try:
146-
self._param = new_rational_damping(
147-
kwargs.get("s6", 1.0),
148-
kwargs["s8"],
149-
kwargs.get("s9", 1.0),
150-
kwargs["a1"],
151-
kwargs["a2"],
152-
kwargs.get("alp", 16.0),
153-
)
154-
except KeyError as e:
155-
raise RuntimeError("Constructor requires argument for " + str(e))
157+
self._param = self.new_param(**kwargs)
158+
159+
@staticmethod
160+
def load_param(method, atm=True):
161+
_method = library.ffi.new("char[]", method.encode())
162+
return library.load_rational_damping(_method, atm)
163+
164+
@staticmethod
165+
def new_param(*, s6=1.0, s8, s9=1.0, a1, a2, alp=16.0):
166+
return library.new_rational_damping(s6, s8, s9, a1, a2, alp)
156167

157168

158169
class DispersionModel(Structure):
@@ -165,7 +176,7 @@ class DispersionModel(Structure):
165176
recreating it.
166177
"""
167178

168-
_disp = _ffi.NULL
179+
_disp = library.ffi.NULL
169180

170181
def __init__(
171182
self,
@@ -181,29 +192,27 @@ def __init__(
181192
Structure.__init__(self, numbers, positions, charge, lattice, periodic)
182193

183194
if "ga" in kwargs or "gc" in kwargs or "wf" in kwargs:
184-
self._disp = custom_d4_model(
195+
self._disp = library.custom_d4_model(
185196
self._mol,
186197
kwargs.get("ga", 3.0),
187198
kwargs.get("gc", 2.0),
188199
kwargs.get("wf", 6.0),
189200
)
190201
else:
191-
self._disp = new_d4_model(self._mol)
202+
self._disp = library.new_d4_model(self._mol)
192203

193204
def get_dispersion(self, param: DampingParam, grad: bool) -> dict:
194205
"""Perform actual evaluation of the dispersion correction"""
195206

196-
_error = new_error()
197-
_energy = _ffi.new("double *")
207+
_energy = library.ffi.new("double *")
198208
if grad:
199209
_gradient = np.zeros((len(self), 3))
200210
_sigma = np.zeros((3, 3))
201211
else:
202212
_gradient = None
203213
_sigma = None
204214

205-
_lib.dftd4_get_dispersion(
206-
_error,
215+
library.get_dispersion(
207216
self._mol,
208217
self._disp,
209218
param._param,
@@ -212,8 +221,6 @@ def get_dispersion(self, param: DampingParam, grad: bool) -> dict:
212221
_cast("double*", _sigma),
213222
)
214223

215-
handle_error(_error)
216-
217224
results = dict(energy=_energy[0])
218225
if _gradient is not None:
219226
results.update(gradient=_gradient)
@@ -224,13 +231,12 @@ def get_dispersion(self, param: DampingParam, grad: bool) -> dict:
224231
def get_properties(self) -> dict:
225232
"""Evaluate dispersion related properties"""
226233

227-
_error = new_error()
228234
_c6 = np.zeros((len(self), len(self)))
229235
_cn = np.zeros((len(self)))
230236
_charges = np.zeros((len(self)))
231237
_alpha = np.zeros((len(self)))
232238

233-
_lib.dftd4_get_properties(
239+
library.get_properties(
234240
_error,
235241
self._mol,
236242
self._disp,
@@ -240,8 +246,6 @@ def get_properties(self) -> dict:
240246
_cast("double*", _alpha),
241247
)
242248

243-
handle_error(_error)
244-
245249
return {
246250
"coordination numbers": _cn,
247251
"partial charges": _charges,
@@ -252,21 +256,17 @@ def get_properties(self) -> dict:
252256
def get_pairwise_dispersion(self, param: DampingParam) -> dict:
253257
"""Evaluate pairwise representation of the dispersion energy"""
254258

255-
_error = new_error()
256259
_pair_disp2 = np.zeros((len(self), len(self)))
257260
_pair_disp3 = np.zeros((len(self), len(self)))
258261

259-
_lib.dftd4_get_pairwise_dispersion(
260-
_error,
262+
library.get_pairwise_dispersion(
261263
self._mol,
262264
self._disp,
263265
param._param,
264266
_cast("double*", _pair_disp2),
265267
_cast("double*", _pair_disp3),
266268
)
267269

268-
handle_error(_error)
269-
270270
return {
271271
"additive pairwise energy": _pair_disp2,
272272
"non-additive pairwise energy": _pair_disp3,
@@ -275,13 +275,17 @@ def get_pairwise_dispersion(self, param: DampingParam) -> dict:
275275

276276
def _cast(ctype, array):
277277
"""Cast a numpy array to a FFI pointer"""
278-
return _ffi.NULL if array is None else _ffi.cast(ctype, array.ctypes.data)
278+
return (
279+
library.ffi.NULL
280+
if array is None
281+
else library.ffi.cast(ctype, array.ctypes.data)
282+
)
279283

280284

281285
def _ref(ctype, value):
282286
"""Create a reference to a value"""
283287
if value is None:
284-
return _ffi.NULL
285-
ref = _ffi.new(ctype + "*")
288+
return library.ffi.NULL
289+
ref = library.ffi.new(ctype + "*")
286290
ref[0] = value
287291
return ref

0 commit comments

Comments
 (0)