Skip to content

Commit 5391f03

Browse files
authored
Allow modifying cutoff values from ASE calculator (#154)
1 parent 5ddb7c3 commit 5391f03

File tree

2 files changed

+90
-7
lines changed

2 files changed

+90
-7
lines changed

python/dftd3/ase.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
======================== ============ ============================================
4040
method None Method to calculate dispersion for
4141
damping None Damping function to use
42-
params_tweaks None Optional dict with the damping parameters
42+
params_tweaks {} Optional dict with the damping parameters
43+
realspace_cutoff {} Optional dict to override cutoff values
4344
cache_api True Reuse generate API objects (recommended)
4445
======================== ============ ============================================
4546
@@ -63,6 +64,20 @@
6364
for damping parameters of a given method and prefers special two-body only
6465
damping parameters if available!
6566
67+
The realspace cutoff parameters allow adjusting the distance values for which
68+
interactions are considered
69+
70+
================== =========== ==========================================
71+
Realspace cutoff Default Description
72+
================== =========== ==========================================
73+
disp2 60 * Bohr Pairwise dispersion interactions
74+
disp3 40 * Bohr Triple dispersion interactions
75+
cn 40 * Bohr Coordination number counting
76+
================== =========== ==========================================
77+
78+
Values provided in the dict are expected to be in Angstrom. When providing values
79+
in Bohr multiply the inputs by the `ase.units.Bohr` constant.
80+
6681
Example
6782
-------
6883
>>> from ase.build import molecule
@@ -146,6 +161,7 @@ class DFTD3(Calculator):
146161
"method": None,
147162
"damping": None,
148163
"params_tweaks": {},
164+
"realspace_cutoff": {},
149165
"cache_api": True,
150166
}
151167

@@ -237,20 +253,33 @@ def _create_api_calculator(self) -> DispersionModel:
237253
_periodic,
238254
)
239255

240-
except RuntimeError:
241-
raise InputError("Cannot construct dispersion model for dftd3")
256+
except RuntimeError as e:
257+
raise InputError("Cannot construct dispersion model for dftd3") from e
242258

243259
return disp
244260

261+
def _apply_realspace_cutoff(self, disp: DispersionModel) -> None:
262+
"""Apply realspace cutoff parameters to dispersion model"""
263+
264+
try:
265+
if self.parameters.realspace_cutoff:
266+
disp2 = self.parameters.realspace_cutoff.get("disp2", 60.0 * Bohr) / Bohr
267+
disp3 = self.parameters.realspace_cutoff.get("disp3", 40.0 * Bohr) / Bohr
268+
cn = self.parameters.realspace_cutoff.get("cn", 40.0 * Bohr) / Bohr
269+
270+
disp.set_realspace_cutoff(disp2=disp2, disp3=disp3, cn=cn)
271+
except RuntimeError as e:
272+
raise InputError("Cannot update realspace cutoff for dftd3") from e
273+
245274
def _create_damping_param(self) -> DampingParam:
246275
"""Create a new API damping parameter object"""
247276

248277
try:
249278
params_tweaks = self.parameters.params_tweaks if self.parameters.params_tweaks else {"method": self.parameters.get("method")}
250279
dpar = _damping_param[self.parameters.get("damping")](**params_tweaks)
251280

252-
except RuntimeError:
253-
raise InputError("Cannot construct damping parameter for dftd3")
281+
except RuntimeError as e:
282+
raise InputError("Cannot construct damping parameter for dftd3") from e
254283

255284
return dpar
256285

@@ -271,12 +300,15 @@ def calculate(
271300
if self._disp is None:
272301
self._disp = self._create_api_calculator()
273302

303+
# Apply realspace cutoff before evaluation (works with cached calculator)
304+
self._apply_realspace_cutoff(self._disp)
305+
274306
_dpar = self._create_damping_param()
275307

276308
try:
277309
_res = self._disp.get_dispersion(param=_dpar, grad=True)
278-
except RuntimeError:
279-
raise CalculationFailed("dftd3 could not evaluate input")
310+
except RuntimeError as e:
311+
raise CalculationFailed("dftd3 could not evaluate input") from e
280312

281313
# These properties are garanteed to exist for all implemented calculators
282314
self.results["energy"] = _res.get("energy") * Hartree

python/dftd3/test_ase.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,54 @@ def test_ase_tpssd3():
9797
assert atoms.get_potential_energy() == approx(4.963774668847532, abs=thr)
9898
energies = [calc.get_potential_energy() for calc in get_calcs(atoms.calc)]
9999
assert energies == approx([-0.14230914516094673, 5.106083814008478], abs=thr)
100+
101+
102+
@pytest.mark.skipif(ase is None, reason="requires ase")
103+
def test_ase_realspace_cutoff():
104+
"""Test that realspace_cutoff parameter works and can be updated with cache_api=True"""
105+
from ase.units import Bohr
106+
107+
thr = 1.0e-6
108+
atoms = molecule("H2O")
109+
110+
# Test with default cutoffs
111+
calc_default = DFTD3(method="PBE", damping="d3bj", cache_api=True)
112+
atoms.calc = calc_default
113+
energy_default = atoms.get_potential_energy()
114+
forces_default = atoms.get_forces()
115+
116+
# Test with very small cutoffs (smaller than H2O bond length ~1 Angstrom) - should give zero or very small interactions
117+
calc_custom = DFTD3(
118+
method="PBE",
119+
damping="d3bj",
120+
realspace_cutoff={"disp2": 0.5, "disp3": 0.5, "cn": 0.5},
121+
cache_api=True
122+
)
123+
atoms.calc = calc_custom
124+
energy_custom = atoms.get_potential_energy()
125+
forces_custom = atoms.get_forces()
126+
127+
# With very small cutoffs, energy should be much smaller (closer to zero)
128+
assert abs(energy_custom) < abs(energy_default)
129+
assert not np.allclose(forces_custom, forces_default, atol=thr)
130+
131+
# Test updating cutoff via set() with cache_api=True
132+
# Reset to default-like cutoffs using set()
133+
calc_custom.set(realspace_cutoff={"disp2": 60.0 * Bohr, "disp3": 40.0 * Bohr, "cn": 40.0 * Bohr})
134+
energy_updated = atoms.get_potential_energy()
135+
forces_updated = atoms.get_forces()
136+
137+
# Should match the default energy/forces
138+
assert energy_updated == approx(energy_default, abs=thr)
139+
assert forces_updated == approx(forces_default, abs=thr)
140+
141+
# Test with empty dict (should use library defaults)
142+
calc_empty = DFTD3(method="PBE", damping="d3bj", realspace_cutoff={}, cache_api=True)
143+
atoms.calc = calc_empty
144+
energy_empty = atoms.get_potential_energy()
145+
146+
# Empty dict should behave like no cutoff override (same as default)
147+
assert energy_empty == approx(energy_default, abs=thr)
148+
149+
150+

0 commit comments

Comments
 (0)