openmm-opes 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Charlles Abreu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: openmm-opes
3
+ Version: 0.1.0
4
+ Summary: OpenMM implementation of On-the-fly Probability Enhanced Sampling (OPES)
5
+ Author: Charlles Abreu
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy>=1.24
11
+ Requires-Dist: scipy>=1.10
12
+ Requires-Dist: openmm>=8.1
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=7.4; extra == "dev"
15
+ Requires-Dist: ruff>=0.16; extra == "dev"
16
+ Requires-Dist: ty==0.0.72; extra == "dev"
17
+ Provides-Extra: docs
18
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
19
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
20
+ Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
21
+ Dynamic: license-file
22
+
23
+ # openmm-opes
24
+
25
+ [![CI](https://github.com/craabreu/openmm-opes/actions/workflows/ci.yml/badge.svg)](https://github.com/craabreu/openmm-opes/actions/workflows/ci.yml)
26
+
27
+ An [OpenMM](https://openmm.org) implementation of On-the-fly Probability
28
+ Enhanced Sampling (OPES) and OPES-explore.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install openmm-opes
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from openmm_opes import OPES
40
+
41
+ sampler = OPES(system, variables, temperature, barrier, frequency=500,
42
+ varianceFrequency=50)
43
+ sampler.step(simulation, 1_000_000)
44
+ fes = sampler.getFreeEnergy()
45
+ ```
46
+
47
+ Full documentation: <https://craabreu.github.io/openmm-opes>
48
+
49
+ ## Origin
50
+
51
+ This library packages and hardens the prototype implementation developed in
52
+ [craabreu/opes-simulations](https://github.com/craabreu/opes-simulations),
53
+ porting its `OPES`/`OnlineKDE` classes into a tested, documented package.
54
+
55
+ ## References
56
+
57
+ - Invernizzi & Parrinello, J. Phys. Chem. Lett. 2020,
58
+ [doi:10.1021/acs.jpclett.0c00497](https://doi.org/10.1021/acs.jpclett.0c00497)
59
+ - Invernizzi & Parrinello, J. Chem. Theory Comput. 2022,
60
+ [doi:10.1021/acs.jctc.2c00152](https://doi.org/10.1021/acs.jctc.2c00152)
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,42 @@
1
+ # openmm-opes
2
+
3
+ [![CI](https://github.com/craabreu/openmm-opes/actions/workflows/ci.yml/badge.svg)](https://github.com/craabreu/openmm-opes/actions/workflows/ci.yml)
4
+
5
+ An [OpenMM](https://openmm.org) implementation of On-the-fly Probability
6
+ Enhanced Sampling (OPES) and OPES-explore.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install openmm-opes
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```python
17
+ from openmm_opes import OPES
18
+
19
+ sampler = OPES(system, variables, temperature, barrier, frequency=500,
20
+ varianceFrequency=50)
21
+ sampler.step(simulation, 1_000_000)
22
+ fes = sampler.getFreeEnergy()
23
+ ```
24
+
25
+ Full documentation: <https://craabreu.github.io/openmm-opes>
26
+
27
+ ## Origin
28
+
29
+ This library packages and hardens the prototype implementation developed in
30
+ [craabreu/opes-simulations](https://github.com/craabreu/opes-simulations),
31
+ porting its `OPES`/`OnlineKDE` classes into a tested, documented package.
32
+
33
+ ## References
34
+
35
+ - Invernizzi & Parrinello, J. Phys. Chem. Lett. 2020,
36
+ [doi:10.1021/acs.jpclett.0c00497](https://doi.org/10.1021/acs.jpclett.0c00497)
37
+ - Invernizzi & Parrinello, J. Chem. Theory Comput. 2022,
38
+ [doi:10.1021/acs.jctc.2c00152](https://doi.org/10.1021/acs.jctc.2c00152)
39
+
40
+ ## License
41
+
42
+ MIT
@@ -0,0 +1,91 @@
1
+ [project]
2
+ name = "openmm-opes"
3
+ version = "0.1.0"
4
+ description = "OpenMM implementation of On-the-fly Probability Enhanced Sampling (OPES)"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Charlles Abreu" }]
9
+ requires-python = ">=3.11"
10
+ dependencies = ["numpy>=1.24", "scipy>=1.10", "openmm>=8.1"]
11
+
12
+ [project.optional-dependencies]
13
+ dev = ["pytest>=7.4", "ruff>=0.16", "ty==0.0.72"]
14
+ # mkdocs >= 1.6 is required for the exclude_docs setting used by mkdocs.yml
15
+ docs = ["mkdocs>=1.6", "mkdocs-material>=9.5", "mkdocstrings[python]>=0.24"]
16
+
17
+ [build-system]
18
+ requires = ["setuptools>=68"]
19
+ build-backend = "setuptools.build_meta"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = ["tests"]
26
+ markers = ["slow: end-to-end simulations that take minutes"]
27
+
28
+ [tool.ruff]
29
+ line-length = 88
30
+ target-version = "py311"
31
+ include = ["src/**/*.py", "tests/**/*.py"]
32
+
33
+ [tool.ruff.lint]
34
+ # Deliberately no "N" rules: the public API is camelCase to match OpenMM.
35
+ select = ["E", "F", "I", "UP", "B", "C4", "RUF"]
36
+
37
+ [tool.ty.analysis]
38
+ # OpenMM ships no type stubs, so the lint job never installs it: doing so
39
+ # would still leave every attribute access untyped, for no benefit.
40
+ # online_kde is the ORIGINAL implementation, sys.path-shimmed at runtime from
41
+ # a sibling repo (tests/data/make_reference.py) rather than installed, so it
42
+ # is never resolvable statically.
43
+ allowed-unresolved-imports = ["openmm", "openmm.**", "online_kde"]
44
+
45
+ [[tool.ty.overrides]]
46
+ # numpy-stubs gap, confirmed a false positive: np.logaddexp.reduce(array) is
47
+ # ordinary numpy usage that runs correctly (see the passing test suite), but
48
+ # the stub's reduce() overloads for binary ufuncs cannot be satisfied without
49
+ # an explicit out= argument, so ty reports every plain reduce() call here as
50
+ # having no matching overload.
51
+ include = ["src/openmm_opes/kde.py", "tests/test_kde.py"]
52
+
53
+ [tool.ty.overrides.rules]
54
+ no-matching-overload = "ignore"
55
+
56
+ [[tool.ty.overrides]]
57
+ # numpy-stubs gap, confirmed a false positive: np.savez(path, **arrays) passes
58
+ # arbitrary ndarrays through **kwds, but the stub resolves that against
59
+ # allow_pickle: bool instead of **kwds: ArrayLike, reporting every array
60
+ # argument as "expected bool". Same class of gap sieve's own pyproject.toml
61
+ # documents for the identical call shape.
62
+ include = ["tests/data/make_reference.py"]
63
+
64
+ [tool.ty.overrides.rules]
65
+ invalid-argument-type = "ignore"
66
+
67
+ [[tool.ty.overrides]]
68
+ # openmm.unit stub gap, confirmed a false positive: unit.kelvin and
69
+ # unit.kilojoules_per_mole resolve fine, but unit.picosecond/picoseconds do
70
+ # not, even though `dir(unit)` and runtime multiplication both confirm they
71
+ # exist -- these unit constants are evidently built at import time in a way
72
+ # ty's static analysis can't trace, unlike the ones assigned as literal
73
+ # module-level names.
74
+ include = ["tests/test_opes.py", "tests/test_integration.py"]
75
+
76
+ [tool.ty.overrides.rules]
77
+ unresolved-attribute = "ignore"
78
+ unsupported-operator = "ignore"
79
+
80
+ [[tool.ty.overrides]]
81
+ # numpy-stubs gap, confirmed a false positive: ndarray.min() with no arguments
82
+ # on an ordinary float64 array (e.g. analytic[mask].min() where analytic is
83
+ # built as BARRIER_HEIGHT * (1 - grid**2) ** 2) runs correctly and returns a
84
+ # float64 -- ty's own diagnostic even reports the array's dtype correctly as
85
+ # float64 -- yet resolution picks the unrelated NDArray[object_] overload
86
+ # instead of a float64-compatible one. Reproduced in isolation outside this
87
+ # file, so this is a stub resolution gap, not anything about this code.
88
+ include = ["tests/test_integration.py"]
89
+
90
+ [tool.ty.overrides.rules]
91
+ invalid-argument-type = "ignore"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ """OpenMM implementation of On-the-fly Probability Enhanced Sampling (OPES)."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["OPES", "__version__"]
6
+
7
+
8
+ def __getattr__(name):
9
+ """Import OPES lazily (PEP 562).
10
+
11
+ An eager ``from .opes import OPES`` would also run on
12
+ ``import openmm_opes.kde``, dragging openmm in and breaking the guarantee
13
+ that kde and io stay OpenMM-free. Task 1's test enforces that.
14
+ """
15
+ if name == "OPES":
16
+ from .opes import OPES
17
+
18
+ return OPES
19
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,102 @@
1
+ """Multi-walker bias sharing through a shared directory.
2
+
3
+ Pure NumPy. This module must not import openmm, and deliberately knows
4
+ nothing about kernels: it moves flat dicts of arrays so it stays testable
5
+ without a simulation.
6
+
7
+ The format is ``np.savez``, not pickle. A walker reads files written by other
8
+ processes out of a shared directory, and ``pickle.load`` on data from another
9
+ process would be a remote-code-execution primitive. OpenMM's own Metadynamics
10
+ writes .npy for the same reason.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import re
17
+ import warnings
18
+
19
+ import numpy as np
20
+
21
+ FILENAME_PATTERN = re.compile(r"kde_(\d+)_(\d+)\.npz")
22
+
23
+
24
+ class _LoadedBias:
25
+ """A peer's most recently seen state. Mirrors Metadynamics' own namedtuple."""
26
+
27
+ __slots__ = ("index", "state", "walkerId")
28
+
29
+ def __init__(self, walkerId: int, index: int, state: dict):
30
+ self.walkerId = walkerId
31
+ self.index = index
32
+ self.state = state
33
+
34
+
35
+ class BiasSharer:
36
+ """Reads and writes walker bias states in a shared directory.
37
+
38
+ Parameters
39
+ ----------
40
+ biasDir
41
+ Directory shared by every walker.
42
+ walkerId
43
+ This walker's identifier. Drawn at random when omitted, from a fresh
44
+ generator so the global NumPy random state is left undisturbed.
45
+ """
46
+
47
+ def __init__(self, biasDir: str, walkerId: int | None = None):
48
+ self.biasDir = biasDir
49
+ self.walkerId = (
50
+ int(np.random.default_rng().integers(0x7FFFFFFF))
51
+ if walkerId is None
52
+ else walkerId
53
+ )
54
+ self._saveIndex = 0
55
+ self._loaded: dict[int, _LoadedBias] = {}
56
+
57
+ def _path(self, prefix: str, index: int) -> str:
58
+ return os.path.join(self.biasDir, f"{prefix}_{self.walkerId}_{index}.npz")
59
+
60
+ def save(self, state: dict) -> None:
61
+ """Write this walker's state atomically and drop the previous index."""
62
+ oldName = self._path("kde", self._saveIndex)
63
+ self._saveIndex += 1
64
+ tempName = self._path("temp", self._saveIndex)
65
+ fileName = self._path("kde", self._saveIndex)
66
+ np.savez(tempName, **state)
67
+ os.rename(tempName, fileName)
68
+ if os.path.exists(oldName):
69
+ os.remove(oldName)
70
+
71
+ def load(self) -> dict[int, dict]:
72
+ """Return peer states that are new or have advanced since the last call."""
73
+ updated: dict[int, dict] = {}
74
+ for filename in os.listdir(self.biasDir):
75
+ match = FILENAME_PATTERN.match(filename)
76
+ if match is None:
77
+ continue
78
+ walkerId, index = int(match.group(1)), int(match.group(2))
79
+ if walkerId == self.walkerId:
80
+ continue
81
+ known = self._loaded.get(walkerId)
82
+ if known is not None and index <= known.index:
83
+ continue
84
+ try:
85
+ with np.load(
86
+ os.path.join(self.biasDir, filename), allow_pickle=False
87
+ ) as data:
88
+ state = {key: data[key] for key in data.files}
89
+ except (OSError, ValueError):
90
+ warnings.warn(
91
+ f"The file {filename} seems to have been deleted. Using the "
92
+ "latest loaded data from the same walker.",
93
+ stacklevel=2,
94
+ )
95
+ continue
96
+ self._loaded[walkerId] = _LoadedBias(walkerId, index, state)
97
+ updated[walkerId] = state
98
+ return updated
99
+
100
+ def getLoadedStates(self) -> dict[int, dict]:
101
+ """Every peer state seen so far, whether or not it changed recently."""
102
+ return {walkerId: bias.state for walkerId, bias in self._loaded.items()}