Skip to content

Commit 544c5ae

Browse files
authored
Merge pull request #64 from OpenBioSim/feature_repex
Add support for HREX and REST2
2 parents 669e968 + 93292b4 commit 544c5ae

File tree

22 files changed

+3446
-1800
lines changed

22 files changed

+3446
-1800
lines changed

README.md

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,58 @@ This is a `bzip2` compressed file that will need to be extracted before use.
5959

6060
#### Running SOMD2 using one or more GPUs
6161

62-
In order to run using GPUs you will first need to set the relevant environment variable. For example, to run using 4 CUDA enabled GPUS set `CUDA_VISIBLE_DEVICES=0,1,2,3` (for openCL and HIP use `OPENCL_VISIBLE_DEVICES` and `HIP_VISIBLE_DEVICES` respectively).
63-
64-
By default SOMD2 will run using the CPU platform, however if the relevant environment variable has been set (as above) the new platform will be detected and set. In the case that this detection fails, or if there are multiple platforms available, the `--platform` option can be set (for example `--platform cuda`).
65-
66-
By default, SOMD2 will automatically manage the distribution of lambda windows across all listed devices. In order to restrict the number of devices used the `--max_gpus` option can be set, for example setting `max_gpus=2` while `CUDA_VISIBLE_DEVICES` are set as above would restrict SOMD2 to using only GPUs 0 and 1.
62+
In order to run using GPUs you will first need to set the relevant environment
63+
variable. For example, to run using 4 CUDA enabled GPUS set `CUDA_VISIBLE_DEVICES=0,1,2,3`
64+
(for openCL and HIP use `OPENCL_VISIBLE_DEVICES` and `HIP_VISIBLE_DEVICES` respectively).
65+
66+
By default `SOMD2` will run using the CPU platform, however if the relevant
67+
environment variable has been set (as above) the new platform will be detected
68+
and set. In the case that this detection fails, or if there are multiple platforms
69+
available, the `--platform` option can be set (for example `--platform cuda`).
70+
71+
By default, `SOMD2` will automatically manage the distribution of lambda windows
72+
across all listed devices. In order to restrict the number of devices used
73+
the `--max_gpus` option can be set, for example setting `max_gpus=2` while
74+
`CUDA_VISIBLE_DEVICES` are set as above would restrict `SOMD2` to using only
75+
GPUs 0 and 1.
76+
77+
## Replica exchange
78+
79+
`SOMD2` supports Hamiltonian replica exchange (HREX) simulations, which can be
80+
enabled using the `--replica-exchange` option. Note that dynamics contexts will
81+
be created up-front for all replicas, so this can be memory intensive. As such,
82+
replica exchange is intended for use on multi-GPU nodes with a large amount of
83+
memory. For optimal performance, it is recommended that the number of replicas
84+
be a multiple of the number of GPUs. It is also possible to oversubscribe the
85+
GPUs, i.e. have more than one replica running on a GPU at a time. This can be
86+
controlled via the `--oversubscription-factor` option, e.g. a value of 2 would
87+
allow 2 replicas to run on each GPU at a time.
88+
89+
The swap frequency for replica exchange is controlled by the `energy-frequency`
90+
option, i.e. we compute the energies for all replicas at this frequency, then
91+
attempt to mix the replicas. A larger value will improve performance, but may
92+
reduce the efficiency of the exchange.
93+
94+
## REST2
95+
96+
We also support Replica Exchange with Solute Scaling
97+
([REST2](https://pubs.acs.org/doi/10.1021/jp204407d)) simulations to facilitate sampling for perturbations
98+
involving conformational changes, e.g. ring flips. This can be enabled
99+
using the `--rest2-scale` option, which specifies the "temperature" of the
100+
REST2 region relative to the rest of the system. By default, the REST2 region
101+
comprises _all_ atoms in perturbable molecules, but can be controlled via the
102+
`--rest2-selection` option. This should be a `Sire` selection string that specifies
103+
additional atoms of interest, i.e. those in regular, non-perturbable molecules.
104+
If the selection does contain atoms within perturbable molecules, then only
105+
those atoms within the perturbable molecules will be considered as part of the
106+
REST2 region, i.e. you can select a sub-set of atoms within a perturbable
107+
molecule to be scaled.
108+
109+
By default, the REST2 schedule is a triangular function that starts and ends
110+
at 1.0, with a peak at the middle of the lambda schedule corresponding to
111+
the value of `--rest2-scale`. By passing multiple values for `--rest2-scale`, the
112+
user can fully control the schedule. When doing so, the number of values must
113+
match the number of lambda windows.
67114

68115
## Analysis
69116

@@ -89,6 +136,19 @@ pmf2, overlap2 = BSS.FreeEnergy.Relative.analyse("output2")
89136
free_nrg = BSS.FreeEnergy.Relative.difference(pmf1, pmf2)
90137
```
91138

139+
## Truncated MBAR analysis
140+
141+
When running HREX with a large number of replicas it can become computationally
142+
expensive to compute energies. (We need the energies of each replica at each
143+
lamdba value.) As a shortcut, it's possible to truncate the neighbourhood of
144+
windows for which we compute energies, then use a large null energy for the
145+
remaining windows. This can be controlled via the `--num-energy-neighbours` option.
146+
For example, setting this to 2 would compute energies for the current window and
147+
its two neighbours on either side. The value assigned to the remaining windows
148+
can be controlled via the `--null-energy` option. The number of neighbours should
149+
be chosen as a trade off between accuracy and computational cost. A value of around
150+
20% of the number of replicas has been found to be a good starting point.
151+
92152
## Note for SOMD1 users
93153

94154
For existing users of `somd1`, it's possible to generate input for `somd2` by passing
@@ -124,3 +184,7 @@ somd2 somd1.bss --pert-file somd1.pert --somd1-compatibility
124184
```
125185

126186
(This only shows the limited options required. Others will take default values and can be set accordingly.)
187+
188+
If you want to load an existing system from a perturbation file and use the
189+
new `somd2` ghost atom bonded-term modifications, then simply omit the
190+
`--somd1-compatibility` option.

environment.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ dependencies:
88
- biosimspace
99
- git
1010
- loguru
11+
- numba

src/somd2/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
######################################################################
22
# SOMD2: GPU accelerated alchemical free-energy engine.
33
#
4-
# Copyright: 2023-2024
4+
# Copyright: 2023-2025
55
#
66
# Authors: The OpenBioSim Team <team@openbiosim.org>
77
#
@@ -19,6 +19,17 @@
1919
# along with SOMD2. If not, see <http://www.gnu.org/licenses/>.
2020
#####################################################################
2121

22+
# Make sure we used the mixed API so we can use BioSimSpace.
23+
try:
24+
import sire as _sr
25+
26+
_sr.use_mixed_api(support_old_module_names=False)
27+
_sr.convert.supported_formats()
28+
29+
del _sr
30+
except ImportError:
31+
pass
32+
2233
# Disable Sire progress bars until we work out the best way to handle
2334
# them for the SOMD2 runner, i.e. when running multiple dynamics objects
2435
# in parallel.

src/somd2/_utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
######################################################################
22
# SOMD2: GPU accelerated alchemical free-energy engine.
33
#
4-
# Copyright: 2023-2024
4+
# Copyright: 2023-2025
55
#
66
# Authors: The OpenBioSim Team <team@openbiosim.org>
77
#

src/somd2/_utils/_ghosts.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
######################################################################
22
# SOMD2: GPU accelerated alchemical free-energy engine.
33
#
4-
# Copyright: 2023-2024
4+
# Copyright: 2023-2025
55
#
66
# Authors: The OpenBioSim Team <team@openbiosim.org>
77
#
@@ -19,6 +19,8 @@
1919
# along with SOMD2. If not, see <http://www.gnu.org/licenses/>.
2020
#####################################################################
2121

22+
__all__ = ["boresch"]
23+
2224
from sire.system import System as _System
2325
from sire.legacy.System import System as _LegacySystem
2426

@@ -32,7 +34,7 @@
3234
from . import _lam_sym
3335

3436

35-
def _boresch(system, k_hard=100, k_soft=5, optimise_angles=True):
37+
def boresch(system, k_hard=100, k_soft=5, optimise_angles=True):
3638
"""
3739
Apply Boresch modifications to ghost atom bonded terms to avoid non-physical
3840
coupling between the ghost atoms and the physical region.

src/somd2/_utils/_somd1.py

Lines changed: 146 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
######################################################################
22
# SOMD2: GPU accelerated alchemical free-energy engine.
33
#
4-
# Copyright: 2023-2024
4+
# Copyright: 2023-2025
55
#
66
# Authors: The OpenBioSim Team <team@openbiosim.org>
77
#
@@ -19,14 +19,72 @@
1919
# along with SOMD2. If not, see <http://www.gnu.org/licenses/>.
2020
#####################################################################
2121

22+
__all__ = ["apply_pert", "make_compatible", "reconstruct_system"]
23+
2224
from sire.system import System as _System
2325
from sire.legacy.System import System as _LegacySystem
2426

2527
import sire.legacy.MM as _SireMM
2628
import sire.legacy.Mol as _SireMol
2729

2830

29-
def _make_compatible(system):
31+
def apply_pert(system, pert_file):
32+
"""
33+
Helper function to apply a perturbation to a reference system.
34+
35+
Parameters
36+
----------
37+
38+
system: sr.system.System
39+
The reference system.
40+
41+
pert_file: str
42+
Path to a stream file containing the perturbation to apply to the
43+
reference system.
44+
45+
Returns
46+
-------
47+
48+
system: sire.system.System
49+
The perturbable system.
50+
"""
51+
52+
if not isinstance(system, _System):
53+
raise TypeError("'system' must be of type 'sr.system.System'.")
54+
55+
if not isinstance(pert_file, str):
56+
raise TypeError("'pert_file' must be of type 'str'.")
57+
58+
from sire import morph as _morph
59+
60+
# Get the non-water molecules in the system.
61+
non_waters = system["not water"].molecules()
62+
63+
# Try to apply the perturbation to each non-water molecule.
64+
is_pert = False
65+
for mol in non_waters:
66+
# Exclude ions.
67+
if mol.num_atoms() > 1:
68+
try:
69+
pert_mol = _morph.create_from_pertfile(mol, pert_file)
70+
is_pert = True
71+
break
72+
except:
73+
pass
74+
75+
if not is_pert:
76+
raise ValueError(f"Failed to apply the perturbation in '{pert_file}'.")
77+
78+
# Update the molecule.
79+
system.update(pert_mol)
80+
81+
# Link to the reference state.
82+
system = _morph.link_to_reference(system)
83+
84+
return system
85+
86+
87+
def make_compatible(system):
3088
"""
3189
Makes a perturbation SOMD1 compatible.
3290
@@ -547,54 +605,108 @@ def _make_compatible(system):
547605
return system
548606

549607

550-
def _apply_pert(system, pert_file):
608+
def reconstruct_system(system):
551609
"""
552-
Helper function to apply a perturbation to a reference system.
610+
Reconstruct a perturbable system to its original state, i.e. extract the
611+
end states for each perturbable molecule and re-merge them using the original
612+
mapping. This removes any ghost atom modifications applied via a perturbation
613+
file.
553614
554615
Parameters
555616
----------
556617
557-
system: sr.system.System
558-
The reference system.
559-
560-
pert_file: str
561-
Path to a stream file containing the perturbation to apply to the
562-
reference system.
618+
system : sire.system.System, sire.legacy.System.System
619+
The system containing the molecules to be perturbed.
563620
564621
Returns
565622
-------
566623
567-
system: sire.system.System
568-
The perturbable system.
624+
system : sire.system.System
625+
The updated system.
569626
"""
570627

571-
if not isinstance(system, _System):
572-
raise TypeError("'system' must be of type 'sr.system.System'.")
573-
574-
if not isinstance(pert_file, str):
575-
raise TypeError("'pert_file' must be of type 'str'.")
628+
import BioSimSpace as _BSS
576629

577630
from sire import morph as _morph
578631

579-
# Get the non-water molecules in the system.
580-
non_waters = system["not water"].molecules()
632+
# Check the system is a Sire system.
633+
if not isinstance(system, (_System, _LegacySystem)):
634+
raise TypeError(
635+
"'system' must of type 'sire.system.System' or 'sire.legacy.System.System'"
636+
)
581637

582-
# Try to apply the perturbation to each non-water molecule.
583-
is_pert = False
584-
for mol in non_waters:
585-
# Exclude ions.
586-
if mol.num_atoms() > 1:
587-
try:
588-
pert_mol = _morph.create_from_pertfile(mol, pert_file)
589-
is_pert = True
590-
break
591-
except:
592-
pass
638+
# Extract the legacy system.
639+
if isinstance(system, _LegacySystem):
640+
system = _System(system)
593641

594-
if not is_pert:
595-
raise ValueError(f"Failed to apply the perturbation in '{pert_file}'.")
642+
# Clone the system.
643+
system = system.clone()
596644

597-
# Update the molecule.
598-
system.update(pert_mol)
645+
# Search for perturbable molecules.
646+
try:
647+
pert_mols = system.molecules("property is_perturbable")
648+
except KeyError:
649+
raise KeyError("No perturbable molecules in the system")
650+
651+
# Store a dummy element for ghost atoms.
652+
ghost = _SireMol.Element(0)
653+
654+
# Loop over all perturbable molecules.
655+
for mol in pert_mols:
656+
657+
# Extract the end states.
658+
ref = _morph.extract_reference(mol)
659+
pert = _morph.extract_perturbed(mol)
660+
661+
# Find the indices for non-ghost atoms.
662+
ref_idxs = []
663+
for x, (a, e) in enumerate(
664+
zip(ref.property("ambertype"), ref.property("element"))
665+
):
666+
if a == "du" or e == ghost:
667+
continue
668+
else:
669+
ref_idxs.append(x)
670+
pert_idxs = []
671+
for x, (a, e) in enumerate(
672+
zip(pert.property("ambertype"), pert.property("element"))
673+
):
674+
if a == "du" or e == ghost:
675+
continue
676+
else:
677+
pert_idxs.append(x)
678+
679+
# Convert to BioSimSpace molecules and extract the non-ghost atoms.
680+
ref = _BSS._SireWrappers.Molecule(ref).extract(ref_idxs)
681+
pert = _BSS._SireWrappers.Molecule(pert).extract(pert_idxs)
682+
683+
# Work out the mapping.
684+
idx0 = 0
685+
idx1 = 0
686+
mapping = {}
687+
for x, atom in enumerate(mol.atoms()):
688+
at0 = atom.property("ambertype0")
689+
at1 = atom.property("ambertype1")
690+
691+
if at0 != "du" and at1 != "du":
692+
mapping[idx0] = idx1
693+
694+
if at0 != "du":
695+
idx0 += 1
696+
697+
if at1 != "du":
698+
idx1 += 1
699+
700+
# Re-merge the molecules.
701+
merged = _BSS.Align.merge(ref, pert, mapping=mapping, force=True)
702+
703+
# Give the molecule the same number as the original.
704+
merged = merged._sire_object.edit().renumber(mol.number()).molecule().commit()
705+
706+
# Update the system.
707+
system.update(merged)
708+
709+
# Link to the reference state.
710+
system = _morph.link_to_reference(system)
599711

600712
return system

src/somd2/app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
######################################################################
22
# SOMD2: GPU accelerated alchemical free-energy engine.
33
#
4-
# Copyright: 2023-2024
4+
# Copyright: 2023-2025
55
#
66
# Authors: The OpenBioSim Team <team@openbiosim.org>
77
#

0 commit comments

Comments
 (0)