Skip to content

Commit 81863dd

Browse files
authored
Merge pull request #16 from OpenBioSim/feature_softcore
Add support for different alchemical softcore forms
2 parents 480fffc + a6992fc commit 81863dd

5 files changed

Lines changed: 121 additions & 9 deletions

File tree

src/loch/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
#####################################################################
2121

2222
from ._sampler import *
23+
from ._softcore import *
2324
from ._utils import *
2425
from ._version import __version__

src/loch/_kernels.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -394,9 +394,11 @@
394394
float rf_cutoff,
395395
float rf_kappa,
396396
float rf_correction,
397+
int softcore_form,
397398
float sc_coulomb_power,
398399
float sc_shift_coulomb,
399-
float sc_shift_delta)
400+
float sc_shift_delta,
401+
int sc_taylor_power)
400402
{
401403
// Work out the atom index.
402404
const int idx_atom = GET_GLOBAL_ID(0);
@@ -538,7 +540,7 @@
538540
energy_coul[idx] += (q0 * q1) * (r_inv + (rf_kappa * r2) - rf_correction);
539541
}
540542
541-
// Zacharias soft-core potential.
543+
// Soft-core potential for ghost atoms.
542544
else
543545
{
544546
// Store required parameters.
@@ -558,10 +560,28 @@
558560
r = 0.001f;
559561
}
560562
561-
// Compute the Lennard-Jones interaction.
562-
const float delta_lj = sc_shift_delta * a;
563-
const float s6 = powf(s, 6.0f) / powf((s * delta_lj) + (r * r), 3.0f);
564-
energy_lj[idx] += 4.0f * e * s6 * (s6 - 1.0f);
563+
// Compute the LJ interaction using the chosen soft-core form.
564+
float sig6;
565+
if (softcore_form == 1)
566+
{
567+
// Taylor soft-core LJ:
568+
// sig6 = sigma^6 / (alpha^m * sigma^6 + r^6)
569+
const float s6_val = powf(s, 6.0f);
570+
const float r6 = r * r * r * r * r * r;
571+
const float alpha_m = (sc_taylor_power == 1) ? a
572+
: (sc_taylor_power == 0) ? 1.0f
573+
: powf(a, (float)sc_taylor_power);
574+
sig6 = s6_val / (alpha_m * s6_val + r6);
575+
}
576+
else
577+
{
578+
// Zacharias soft-core LJ:
579+
// sig6 = sigma^6 / (sigma*delta + r^2)^3
580+
// delta = shift_delta * alpha
581+
const float delta_lj = sc_shift_delta * a;
582+
sig6 = powf(s, 6.0f) / powf((s * delta_lj) + (r * r), 3.0f);
583+
}
584+
energy_lj[idx] += 4.0f * e * sig6 * (sig6 - 1.0f);
565585
566586
// Compute the Coulomb power expression.
567587
float cpe;

src/loch/_sampler.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
from ._platforms import create_backend as _create_backend
3939
from ._platforms._rng import RNGManager as _RNGManager
40+
from ._softcore import SoftcoreForm as _SoftcoreForm
4041

4142

4243
def _as_float32(arr: _np.ndarray) -> _np.ndarray:
@@ -81,6 +82,8 @@ def __init__(
8182
coulomb_power: float = 0.0,
8283
shift_coulomb: str = "1 A",
8384
shift_delta: str = "1.5 A",
85+
softcore_form: str = "zacharias",
86+
taylor_power: int = 1,
8487
swap_end_states: bool = False,
8588
restart: bool = False,
8689
overwrite: bool = False,
@@ -207,6 +210,17 @@ def __init__(
207210
The soft-core shift-delta parameter. This is used to soften the
208211
Lennard-Jones interaction.
209212
213+
softcore_form : str
214+
The soft-core potential form to use for alchemical interactions.
215+
This can be either 'zacharias' or 'taylor'. The default is
216+
'zacharias'.
217+
218+
taylor_power : int
219+
The power to use for the alpha term in the Taylor soft-core LJ
220+
expression, i.e. sig6 = sigma^6 / (alpha^m * sigma^6 + r^6).
221+
Must be between 0 and 4. The default is 1. Only used when
222+
softcore_form is 'taylor'.
223+
210224
swap_end_states: bool
211225
Whether to swap the end states of the alchemical systems.
212226
@@ -495,6 +509,26 @@ def __init__(
495509
except Exception as e:
496510
raise ValueError(f"Could not validate the 'shift_delta': {e}")
497511

512+
if not isinstance(softcore_form, str):
513+
raise TypeError("'softcore_form' must be of type 'str'")
514+
softcore_form = softcore_form.lower().replace(" ", "")
515+
_valid_softcore_forms = {m.name.lower(): m for m in _SoftcoreForm}
516+
if softcore_form not in _valid_softcore_forms:
517+
raise ValueError(
518+
f"'softcore_form' not recognised. Valid forms are: "
519+
f"{', '.join(_valid_softcore_forms)}"
520+
)
521+
self._softcore_form = _valid_softcore_forms[softcore_form]
522+
523+
if not isinstance(taylor_power, int):
524+
try:
525+
taylor_power = int(taylor_power)
526+
except Exception:
527+
raise ValueError("'taylor_power' must be of type 'int'")
528+
if not 0 <= taylor_power <= 4:
529+
raise ValueError("'taylor_power' must be between 0 and 4")
530+
self._taylor_power = taylor_power
531+
498532
if not isinstance(swap_end_states, bool):
499533
raise ValueError("'swap_end_states' must be of type 'bool'")
500534
self._swap_end_states = swap_end_states
@@ -1323,9 +1357,11 @@ def move(self, context: _openmm.Context) -> list[int]:
13231357
self._rf_cutoff,
13241358
self._rf_kappa,
13251359
self._rf_correction,
1360+
self._sc_softcore_form,
13261361
self._sc_coulomb_power,
13271362
self._sc_shift_coulomb,
13281363
self._sc_shift_delta,
1364+
self._sc_taylor_power,
13291365
block=(self._num_threads, 1, 1),
13301366
grid=(self._atom_blocks, self._batch_size, 1),
13311367
)
@@ -1902,6 +1938,12 @@ def _initialise_gpu_memory(self):
19021938
# Link to the reference state.
19031939
mols = _sr.morph.link_to_reference(self._system)
19041940

1941+
# Build map of extra options for the dynamics object.
1942+
_map = {}
1943+
if self._softcore_form == _SoftcoreForm.TAYLOR:
1944+
_map["use_taylor_softening"] = True
1945+
_map["taylor_power"] = self._taylor_power
1946+
19051947
# Create a dynamics object.
19061948
d = mols.dynamics(
19071949
cutoff_type=self._cutoff,
@@ -1916,6 +1958,7 @@ def _initialise_gpu_memory(self):
19161958
rest2_selection=self._rest2_selection,
19171959
swap_end_states=self._swap_end_states,
19181960
platform="cpu",
1961+
map=_map,
19191962
)
19201963

19211964
# Flags for the required force.
@@ -2059,9 +2102,11 @@ def _initialise_gpu_memory(self):
20592102
)
20602103

20612104
# Store soft-core parameters as scalars.
2105+
self._sc_softcore_form = _np.int32(int(self._softcore_form))
20622106
self._sc_coulomb_power = _np.float32(self._coulomb_power)
20632107
self._sc_shift_coulomb = _np.float32(self._shift_coulomb.value())
20642108
self._sc_shift_delta = _np.float32(self._shift_delta.value())
2109+
self._sc_taylor_power = _np.int32(self._taylor_power)
20652110

20662111
# Store immutable per-atom buffers on GPU.
20672112
self._gpu_sigma = sigmas

src/loch/_softcore.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
######################################################################
2+
# Loch: GPU accelerated GCMC water sampling engine.
3+
#
4+
# Copyright: 2025-2026
5+
#
6+
# Authors: The OpenBioSim Team <team@openbiosim.org>
7+
#
8+
# Loch is free software: you can redistribute it and/or modify
9+
# it under the terms of the GNU General Public License as published by
10+
# the Free Software Foundation, either version 3 of the License, or
11+
# (at your option) any later version.
12+
#
13+
# Loch is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with Loch. If not, see <http://www.gnu.org/licenses/>.
20+
#####################################################################
21+
22+
__all__ = ["SoftcoreForm"]
23+
24+
from enum import IntEnum
25+
26+
27+
class SoftcoreForm(IntEnum):
28+
"""Enum for the soft-core potential form used for alchemical interactions."""
29+
30+
ZACHARIAS = 0
31+
TAYLOR = 1

tests/test_energy.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,24 @@
55

66
import sire as sr
77

8-
from loch import GCMCSampler
8+
from loch import GCMCSampler, SoftcoreForm
99

1010

1111
@pytest.mark.skipif(
1212
"CUDA_VISIBLE_DEVICES" not in os.environ,
1313
reason="Requires CUDA enabled GPU.",
1414
)
1515
@pytest.mark.parametrize("platform", ["cuda", "opencl"])
16-
@pytest.mark.parametrize("fixture", ["water_box", "bpti", "sd12"])
17-
def test_energy(fixture, platform, request):
16+
@pytest.mark.parametrize(
17+
"fixture,softcore_form",
18+
[
19+
("water_box", "zacharias"),
20+
("bpti", "zacharias"),
21+
("sd12", "zacharias"),
22+
("sd12", "taylor"),
23+
],
24+
)
25+
def test_energy(fixture, softcore_form, platform, request):
1826
"""
1927
Test that the RF energy difference agrees with OpenMM.
2028
"""
@@ -36,13 +44,19 @@ def test_energy(fixture, platform, request):
3644
reference=reference,
3745
lambda_schedule=schedule,
3846
lambda_value=lambda_value,
47+
softcore_form=softcore_form,
3948
log_level="debug",
4049
ghost_file=None,
4150
log_file=None,
4251
test=True,
4352
platform=platform,
4453
)
4554

55+
# Build map of extra options for the dynamics object.
56+
dyn_map = {}
57+
if sampler._softcore_form == SoftcoreForm.TAYLOR:
58+
dyn_map["use_taylor_softening"] = True
59+
4660
# Create a dynamics object using the modified GCMC system.
4761
d = sampler.system().dynamics(
4862
cutoff_type="rf",
@@ -57,6 +71,7 @@ def test_energy(fixture, platform, request):
5771
shift_coulomb=str(sampler._shift_coulomb),
5872
shift_delta=str(sampler._shift_delta),
5973
platform=platform,
74+
map=dyn_map,
6075
)
6176

6277
# Loop until we accept an insertion move.

0 commit comments

Comments
 (0)