Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
f58b8f1
Update BioSimSpace development pin.
lohedges Jun 29, 2026
ebcc92c
Update CHANGELOG for 2026.2.0 development.
lohedges Jun 29, 2026
e5c79f4
Add note regarding PyMBAR JAX issues.
lohedges Jun 29, 2026
c2f5660
Remove pre-release warning. [ci skip]
lohedges Jul 1, 2026
5eb74ec
Clarify perturbed_system option. [ci skip]
lohedges Jul 1, 2026
65ccd41
Add support for generating Boresch restraints for ABFE.
lohedges Jul 1, 2026
2c572de
Add support for split restraint levers in ABFE schedules.
lohedges Jul 2, 2026
a496234
Store auto-generated standard state correction in parquet metadata.
lohedges Jul 3, 2026
d276232
Harmonise dependency version formatting.
lohedges Jul 3, 2026
4bf583b
Merge pull request #166 from OpenBioSim/feature_abfe_restraints
lohedges Jul 3, 2026
3ba282a
Give alchemical ions their own plain morph lambda schedule.
lohedges Jul 3, 2026
df7ae19
Merge pull request #169 from OpenBioSim/fix_168
lohedges Jul 3, 2026
5eb4265
Persist alchemical ion identity across restarts, independent of GCMC.
lohedges Jul 3, 2026
7eaab50
Merge pull request #172 from OpenBioSim/fix_171
lohedges Jul 3, 2026
196237b
Seed simulation from least strained restraint search structure.
lohedges Jul 4, 2026
bcc1741
Use persistent thread pools for the production block.
lohedges Jul 21, 2026
74f1590
Merge pull request #175 from OpenBioSim/fix_174
lohedges Jul 21, 2026
c0b7d5b
Add oversubscription_factor to allowed_diffs.
lohedges Jul 21, 2026
2a835b5
Merge pull request #177 from OpenBioSim/fix_176
lohedges Jul 21, 2026
59ae1ec
Restrict energy decomposition to force groups used for integration.
lohedges Jul 22, 2026
2a695c2
Merge pull request #180 from OpenBioSim/fix_178
lohedges Jul 22, 2026
761c691
Parallelise replica mixing.
lohedges Jul 22, 2026
0315201
Merge pull request #181 from OpenBioSim/feature_parallel_mixing
lohedges Jul 22, 2026
00d637e
Query the correct GPU when checking device memory. [closes #182]
lohedges Jul 27, 2026
d312e2d
Merge pull request #183 from OpenBioSim/fix_182
lohedges Jul 27, 2026
688ac1f
Store GCMC statistics per lambda value.
lohedges Jul 28, 2026
54ffab8
Merge pull request #185 from OpenBioSim/feature_gcmc_stats
lohedges Jul 28, 2026
6b21af9
Use pip_check: false to avoid NumPy pin errors with AmberTools.
lohedges Jul 28, 2026
696344f
Link restart systems to the reference end state.
lohedges Aug 4, 2026
774bcf1
Merge pull request #189 from OpenBioSim/fix_188
lohedges Aug 4, 2026
d4e98bd
Cap the number of OpenMM contexts used for replica exchange.
lohedges Jul 27, 2026
602b502
Fix constrained bond lengths at a common lambda value.
lohedges Jul 28, 2026
7aa1d55
Seed the end state from the middle of a group.
lohedges Jul 29, 2026
dbf257c
Check that replicas resume from where they stopped.
lohedges Jul 29, 2026
da97daf
Read positions from a legacy checkpoint when logging the GCMC sphere.
lohedges Jul 29, 2026
dd98085
Set the box vectors before the positions.
lohedges Jul 29, 2026
78c8d68
Accumulate the flags for replicas whose state has moved.
lohedges Jul 29, 2026
4b19dfd
Don't minimise on restart.
lohedges Jul 29, 2026
b05c162
Consolidate the duplicated context count and GPU load warnings.
lohedges Jul 29, 2026
9359110
Document max_contexts in the replica exchange section of the README.
lohedges Jul 29, 2026
e297c24
Fix typos and formatting errors in README.
lohedges Jul 29, 2026
9e97ff1
Pin the perturbed system coordinate path in the seeding test.
lohedges Jul 29, 2026
06534ff
Respect swap_end_states when seeding from the perturbed system.
lohedges Jul 29, 2026
6596422
Clarify what the perturbed_system input must contain.
lohedges Jul 29, 2026
3a95b4b
Suggest a max_contexts value when the replicas don't fit in memory.
lohedges Aug 1, 2026
d28a6df
Remove the redundant post-equilibration GCMC water count.
lohedges Aug 2, 2026
0c2e302
Assert that GCMC water occupancy follows the replica through a mix.
lohedges Aug 2, 2026
23d29fa
Count the GCMC waters against the context in the regular runner.
lohedges Aug 2, 2026
b1d0e6f
Cover GCMC with no sampling region in the regular runner.
lohedges Aug 2, 2026
682e5c9
Pre-equilibrate the water before minimising in the regular runner.
lohedges Aug 2, 2026
6a80168
Give each replica its own terminal flip statistics list.
lohedges Aug 3, 2026
c51911d
Link CHANGELOG entries to PR.
lohedges Aug 4, 2026
ac9b1e6
Add a precision option for GPU platforms.
lohedges Aug 4, 2026
b5b7cb5
Pin the recorded times in the energy trajectory tests.
lohedges Aug 5, 2026
8504cc5
Merge pull request #191 from OpenBioSim/feature_dynamics_cache
lohedges Aug 5, 2026
fdec2e7
Add section on keeping up-to-date during a development cycle. [ci skip]
lohedges Aug 24, 2026
771c8cd
Add support for auto-generating Morse restraints for ring-breaking.
lohedges Aug 24, 2026
57f3693
Document schedules, ABFE, and ring-breaking. [ci skip]
lohedges Aug 24, 2026
f689ad3
Merge pull request #194 from OpenBioSim/feature_auto_rb_restraint
lohedges Aug 24, 2026
666898e
Document intended use of standard_morph schedule. [ci skip]
lohedges Aug 24, 2026
c681d7f
Remove redundant prepareFEP option. [ci skip]
lohedges Aug 24, 2026
0ebf66f
Make package name formatting consistent. [ci skip]
lohedges Aug 24, 2026
7289440
Correct version scheme. [ci skip]
lohedges Aug 24, 2026
beb490c
Remove redundant kappa lever equations from ring-breaking/making sche…
lohedges Aug 26, 2026
0583678
Merge pull request #195 from OpenBioSim/fix_redundant_terms
lohedges Aug 26, 2026
e8c1b13
Don't silently swallow errors when adding coalchemical restraints.
lohedges Aug 28, 2026
95eb838
Add section on charge-change perturbations. [ci skip]
lohedges Aug 28, 2026
97092ac
Add features section to the top of the README. [ci skip]
lohedges Aug 28, 2026
d1bc0d6
Add link to somd2_examples repository. [ci skip]
lohedges Aug 28, 2026
3384ca7
Accept stream file paths for restraints and lambda schedules.
lohedges Aug 28, 2026
9b7651b
Merge pull request #198 from OpenBioSim/feature_stream_file_input
lohedges Aug 28, 2026
9217a47
Fix log message.
lohedges Aug 28, 2026
b462843
Fix platform section. [ci skip]
lohedges Sep 1, 2026
b46b16b
Account for v-site charges when checking for charge change.
lohedges Sep 4, 2026
2aaee8c
Merge pull request #200 from OpenBioSim/fix_199
lohedges Sep 4, 2026
4734d57
Handle num_lambda=1.
lohedges Sep 8, 2026
05e447b
Update CHANGELOG and add missing entries and links.
lohedges Sep 8, 2026
2662187
Merge pull request #203 from OpenBioSim/fix_202
lohedges Sep 8, 2026
08f7b8d
Default to single precision until benchmarks are done.
lohedges Sep 14, 2026
2e4d519
Use the configured platform when detecting available GPUs.
lohedges Sep 15, 2026
e25aa48
Fix the OpenCL constant used to query AMD GPU free memory.
lohedges Sep 15, 2026
a201bc7
Merge pull request #206 from OpenBioSim/fix_204_205
lohedges Sep 15, 2026
c4d831d
Merge branch 'main' into release_2026.2.0
lohedges Sep 15, 2026
ca848fc
Update BioSimSpace pin.
lohedges Sep 15, 2026
b60f2ab
Update CHANGELOG.
lohedges Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
Changelog
=========

[2026.2.0](https://github.com/openbiosim/somd2/compare/2026.1.0...2026.2.0) - Sep 2026
--------------------------------------------------------------------------------------

* Add support for generating Boresch restraints for absolute binding free energy calculations [#166](https://github.com/OpenBioSim/somd2/pull/166).
* Give alchemical ions their own plain morph lambda schedule so they interpolate correctly under non-standard lambda schedules [#169](https://github.com/OpenBioSim/somd2/pull/169).
* Persist alchemical ion identity across restarts so the same molecule is reused regardless of GCMC state [#172](https://github.com/OpenBioSim/somd2/pull/172).
* Use perisistent `ThreadPoolExector` objects within the main replica exchange dynamics block [#175](https://github.com/OpenBioSim/somd2/pull/175).
* Allow `oversubscription_factor` to change on restart [#177](https://github.com/OpenBioSim/somd2/pull/177).
* Restrict energy component decomposition to force groups that are used for integration [#180](https://github.com/OpenBioSim/somd2/pull/180).
* Parallelise replica mixing [#181](https://github.com/OpenBioSim/somd2/pull/181).
* Fixed the replica exchange GPU memory check querying the wrong device when `CUDA_VISIBLE_DEVICES` does not start at zero, since OpenMM numbers devices relative to the visible set whereas `pynvml` enumerates all of them [#183](https://github.com/OpenBioSim/somd2/issues/183).
* Store GCMC sampling statistics per lambda value, converting those from earlier checkpoints on restart [#184](https://github.com/OpenBioSim/somd2/pull/184).
* Link restart systems to the reference end state rather than the perturbed one, since that is the coordinate set that dynamics maintains. Perturbable molecules were otherwise resumed from the coordinates they were built with [#189](https://github.com/OpenBioSim/somd2/pull/189).
* Add `max_contexts` to cap the number of OpenMM contexts used for replica exchange, re-using each across lambda values so that GPU memory no longer limits the number of replicas [#191](https://github.com/OpenBioSim/somd2/pull/191).
* Skip minimisation on restart [#191](https://github.com/OpenBioSim/somd2/pull/191).
* Pre-equilibrate the water with GCMC moves before minimising in the regular `Runner`, making it consistent with the `RepexRunner`, which already did so to stop the geometry relaxing into a dry pocket [#191](https://github.com/OpenBioSim/somd2/pull/191).
* Add a `precision` option for GPU platforms, defaulting to `single` [#191](https://github.com/OpenBioSim/somd2/pull/191).
* Add support for generating Morse restraints for ring-breaking perturbations [#194](https://github.com/OpenBioSim/somd2/pull/194).
* Remove the unused `kappa` lever equations from the ring-breaking/making lambda schedules [#195](https://github.com/OpenBioSim/somd2/pull/195).
* Accept stream file paths for the `restraints` and `lambda_schedule` configuration options, so they can be set from the command line [#198](https://github.com/OpenBioSim/somd2/pull/198).
* Account for off-site charges (virtual sites) when computing the charge difference between the end states. They are held as a molecule property rather than on the atoms, so a charge-preserving perturbation could appear to change charge and be given spurious alchemical ions [#200](https://github.com/OpenBioSim/somd2/pull/200).
* Handle `num_lambda=1`, which previously raised a `ZeroDivisionError` when generating the lambda values. The `RepexRunner` now rejects a single lambda window, since there is nothing to exchange with and the regular `Runner` is faster [#203](https://github.com/OpenBioSim/somd2/pull/203).
* Detect the available GPUs once in the base runner and re-use the list, rather than the `RepexRunner` querying `CUDA_VISIBLE_DEVICES` regardless of the chosen platform. Replica exchange is now also permitted on the HIP platform [#206](https://github.com/OpenBioSim/somd2/pull/206).
* Query the free memory of AMD GPUs with `CL_DEVICE_GLOBAL_FREE_MEMORY_AMD` rather than `CL_DEVICE_BOARD_NAME_AMD`, which returns the device name [#206](https://github.com/OpenBioSim/somd2/pull/206).

[2026.1.0](https://github.com/openbiosim/somd2/compare/2025.1.0...2026.1.0) - Jun 2026
--------------------------------------------------------------------------------------

Expand Down
356 changes: 305 additions & 51 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ platforms = ["linux-64", "osx-arm64"]
[dependencies]
python = ">=3.10"
# main
biosimspace = ">=2026.1.0,<2026.2.0"
biosimspace = ">=2026.2.0,<2026.3.0"
# devel
#biosimspace = "==2026.2.0.dev"
#biosimspace = "==2026.3.0.dev"
filelock = "*"
ghostly = "*"
loch = "*"
Expand Down
8 changes: 6 additions & 2 deletions recipes/somd2/recipe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ requirements:
- versioningit
run:
# main
- biosimspace >=2026.1.0,<2026.2.0
- biosimspace >=2026.2.0,<2026.3.0
# devel
#- biosimspace ==2026.2.0.dev
#- biosimspace ==2026.3.0.dev
- filelock
- ghostly
- loch
Expand All @@ -37,6 +37,10 @@ tests:
- python:
imports:
- somd2
# AmberTools can be pulled in transitively, and installs tools into
# site-packages whose metadata still pins numpy <2, so 'pip check'
# fails even though nothing here uses them.
pip_check: false
- script:
- PYTHONPATH=. pytest -vvv --color=yes --import-mode=importlib ./tests
files:
Expand Down
14 changes: 12 additions & 2 deletions src/somd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,20 @@
# Store the somd2 version.
from ._version import __version__

# Store the sire version.
# Store the sire version. Unlike somd2/BioSimSpace/ghostly/loch (which use
# versioningit and only append a "+g<revisionid>" local version segment
# for non-release (".dev") builds, omitting it entirely for a clean tagged
# release), sire exposes its version and revision id as separate attributes,
# with __revisionid__ always set regardless of release status. Build a
# composite string using the same "+g<revisionid>" convention as the other
# packages, only appending it for non-release (".dev") builds, so that all
# five version strings are formatted consistently.
from sire import __version__ as _sire_version
from sire import __revisionid__ as _sire_revisionid

if ".dev" in _sire_version:
_sire_version = f"{_sire_version}+g{_sire_revisionid}"

# Store the BioSimSpace version.
from BioSimSpace import __version__ as _biosimspace_version

Expand All @@ -61,7 +71,7 @@ def get_versions():
"""
return {
"somd2": __version__,
"sire": f"{_sire_version}+{_sire_revisionid}",
"sire": _sire_version,
"biosimspace": _biosimspace_version,
"ghostly": _ghostly_version,
"loch": _loch_version,
Expand Down
97 changes: 91 additions & 6 deletions src/somd2/_utils/_schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,44 @@
]


def annihilate(fix_epsilon=True):
def _set_boresch_lever_equations(s, stage_dihedral, stage_distance_angle):
"""
Set the equations for a "split" restraint_lever Boresch restraint (see
sire.restraints.boresch's restraint_lever parameter), reproducing the
RXRX protocol's staged restraint turn-on (Table S1 of the RXRX paper's
SI): within each of the two named stages, the corresponding restraint
group ramps from ~0 to 1 following a geometric progression, while the
other group is held fixed. 'stage_dihedral' is the stage over which
the dihedral restraint group ramps on (with the distance/angle group
held at 0); 'stage_distance_angle' is the stage over which the
distance/angle group ramps on (with the dihedral group held at 1,
already fully on).

Note: this aligns the restraint turn-on with SOMD2's own decharge/
annihilate(or decouple) stage boundaries, rather than reproducing the
RXRX paper's exact global 50/50 window split (which falls partway
through the annihilate/decouple stage, since the paper's own decharge
stage is only 21 of 64 total bound-leg windows) - the relative sizes of
SOMD2's stages are controlled by lambda_values weighting, not fixed.
"""
from sire.legacy.CAS import Exp as _Exp
import math as _math

# Geometric progression from ~0.01 (fully off) to 1.0 (fully on), matching
# the ratio observed in the RXRX paper's published lambda schedule.
ramp_on = _Exp((1 - s.lam()) * _math.log(0.01))

s.set_equation(stage=stage_dihedral, lever="restraint_dihedral", equation=ramp_on)
s.set_equation(stage=stage_dihedral, lever="restraint_distance_angle", equation=0)
s.set_equation(stage=stage_distance_angle, lever="restraint_dihedral", equation=1)
s.set_equation(
stage=stage_distance_angle,
lever="restraint_distance_angle",
equation=ramp_on,
)


def annihilate(fix_epsilon=True, restraint_lever="split"):
"""
Build the ABFE lambda schedule using decharge → annihilate.

Expand All @@ -44,12 +81,28 @@ def annihilate(fix_epsilon=True):
If False, epsilon is scaled normally from initial to final and the LRC
follows naturally.

restraint_lever : str, optional
How the Boresch restraint is controlled by this schedule, matching
sire.restraints.boresch's restraint_lever parameter. Either "split"
(default), where the dihedral restraint terms are turned on during
decharge and the distance/angle terms are turned on during
annihilate, reproducing the RXRX protocol's staged restraint
turn-on, or "combined", where the whole restraint is turned on
together during the decharge stage. The Boresch restraint object
passed to the simulation must have a matching restraint_lever value.

Returns
-------

schedule : sire.legacy.CAS.LambdaSchedule
The lambda schedule.
"""
if restraint_lever not in ("combined", "split"):
raise ValueError(
"'restraint_lever' must be either 'combined' or 'split', "
f"got {restraint_lever!r}"
)

from sire.cas import LambdaSchedule as _LambdaSchedule

# Start with the standard decouple schedule and modify the stages and
Expand All @@ -65,14 +118,22 @@ def annihilate(fix_epsilon=True):
lever="charge",
equation=s.lam() * s.final() + s.initial() * (1 - s.lam()),
)
s.set_equation(stage="decharge", lever="restraint", equation=s.lam() * s.final())

s.add_stage(
"annihilate",
equation=(-s.lam() + 1) * s.initial() + s.lam() * s.final(),
)
s.set_equation(stage="annihilate", lever="charge", equation=s.final())
s.set_equation(stage="annihilate", lever="restraint", equation=s.final())

if restraint_lever == "split":
_set_boresch_lever_equations(
s, stage_dihedral="decharge", stage_distance_angle="annihilate"
)
else:
s.set_equation(
stage="decharge", lever="restraint", equation=s.lam() * s.final()
)
s.set_equation(stage="annihilate", lever="restraint", equation=s.final())

if fix_epsilon:
s.set_equation(stage="annihilate", lever="epsilon", equation=s.initial())
Expand All @@ -86,7 +147,7 @@ def annihilate(fix_epsilon=True):
return s


def decouple(fix_epsilon=True):
def decouple(fix_epsilon=True, restraint_lever="split"):
"""
Build the ABFE lambda schedule using decharge → decouple.

Expand All @@ -101,20 +162,35 @@ def decouple(fix_epsilon=True):
ghost-LRC force is then explicitly scaled to zero over the stage.
If False, epsilon is scaled normally and the LRC follows naturally.

restraint_lever : str, optional
How the Boresch restraint is controlled by this schedule, matching
sire.restraints.boresch's restraint_lever parameter. Either "split"
(default), where the dihedral restraint terms are turned on during
decharge and the distance/angle terms are turned on during decouple,
reproducing the RXRX protocol's staged restraint turn-on, or
"combined", where the whole restraint is turned on together during
the decharge stage. The Boresch restraint object passed to the
simulation must have a matching restraint_lever value.

Returns
-------

schedule : sire.legacy.CAS.LambdaSchedule
The lambda schedule.
"""
if restraint_lever not in ("combined", "split"):
raise ValueError(
"'restraint_lever' must be either 'combined' or 'split', "
f"got {restraint_lever!r}"
)

from sire.cas import LambdaSchedule as _LambdaSchedule

# Start with the standard decouple schedule and modify the stages and
# equations as needed. This will be folded into Sire in future, but
# we will use this approach for prototyping.
s = _LambdaSchedule.standard_decouple()

s.set_equation(stage="decouple", lever="restraint", equation=s.final())
s.set_equation(stage="decouple", lever="kappa", force="ghost/ghost", equation=0)
s.set_equation(stage="decouple", lever="kappa", force="ghost-14", equation=0)
s.set_equation(stage="decouple", lever="charge", equation=s.final())
Expand Down Expand Up @@ -142,7 +218,16 @@ def decouple(fix_epsilon=True):
s.set_equation(
stage="decharge", lever="kappa", force="ghost-14", equation=-s.lam() + 1
)
s.set_equation(stage="decharge", lever="restraint", equation=s.initial() * s.lam())

if restraint_lever == "split":
_set_boresch_lever_equations(
s, stage_dihedral="decharge", stage_distance_angle="decouple"
)
else:
s.set_equation(stage="decouple", lever="restraint", equation=s.final())
s.set_equation(
stage="decharge", lever="restraint", equation=s.initial() * s.lam()
)

return s

Expand Down
Loading
Loading