Skip to content

Commit 02dbee1

Browse files
authored
Merge pull request #7 from TheochemUI/cacher
feat(cache): Rework for RocksDB cacher
2 parents 21e73ae + 66f96f4 commit 02dbee1

26 files changed

+4366
-164
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SCM syntax highlighting & preventing 3-way merges
2+
pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff

.github/workflows/build_test.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ jobs:
1515
with:
1616
submodules: "recursive"
1717
fetch-depth: 0
18-
- name: Install Conda environment
19-
uses: mamba-org/provision-with-micromamba@main
18+
- name: Setup Pixi
19+
uses: prefix-dev/setup-pixi@v0.8.1
20+
with:
21+
pixi-version: latest
2022
- name: Build
2123
shell: bash -l {0}
2224
run: |
23-
pdm build
25+
pixi run pdm build

.github/workflows/build_wheels.yml

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,101 @@
1-
# Build on every branch push, tag push, and pull request change:
1+
# Build on every pull request change:
22
# From: https://github.com/pypa/cibuildwheel/blob/main/examples/github-deploy.yml
3-
name: Build wheels
4-
on: [push, pull_request]
3+
name: Build and upload to PyPI
4+
5+
on:
6+
workflow_dispatch:
7+
release:
8+
types:
9+
- published
510

611
jobs:
712
build_wheels:
8-
name: Build wheel for ${{ matrix.python }}-${{ matrix.buildplat[1] }}
9-
runs-on: ${{ matrix.buildplat[0] }}
10-
environment: pypi
13+
name: Build wheels for ${{ matrix.os }}
14+
runs-on: ${{ matrix.runs-on }}
1115
strategy:
12-
# Ensure that a wheel builder finishes even if another fails
13-
fail-fast: false
1416
matrix:
15-
# From NumPy
16-
# Github Actions doesn't support pairing matrix values together, let's improvise
17-
# https://github.com/github/feedback/discussions/7835#discussioncomment-1769026
18-
buildplat:
19-
- [ubuntu-20.04, manylinux_x86_64]
20-
- [ubuntu-20.04, musllinux_x86_64] # No OpenBlas, no test
21-
- [macos-12, macosx_x86_64]
22-
# - [windows-2019, win_amd64]
23-
python: ["cp38", "cp39","cp310", "cp311"]
17+
include:
18+
- os: linux-intel
19+
runs-on: ubuntu-latest
20+
- os: linux-arm
21+
runs-on: ubuntu-24.04-arm
22+
- os: windows-intel
23+
runs-on: windows-latest
24+
- os: windows-arm
25+
runs-on: windows-11-arm
26+
- os: macos-intel
27+
# macos-15-intel is the last x86_64 runner
28+
runs-on: macos-15-intel
29+
- os: macos-arm
30+
# macos-14+ (including latest) are ARM64 runners
31+
runs-on: macos-latest
32+
- os: android-intel
33+
runs-on: ubuntu-latest
34+
platform: android
35+
- os: android-arm
36+
# GitHub Actions doesn’t currently support the Android emulator on any ARM
37+
# runner. So we build on a non-ARM runner, which will skip the tests.
38+
runs-on: ubuntu-latest
39+
platform: android
40+
archs: arm64_v8a
41+
- os: ios
42+
runs-on: macos-latest
43+
platform: ios
44+
- os: pyodide
45+
runs-on: ubuntu-latest
46+
platform: pyodide
2447

2548
steps:
26-
- uses: actions/checkout@v3
49+
- uses: actions/checkout@v5
2750

2851
- name: Build wheels
29-
uses: pypa/cibuildwheel@v2.12.3
52+
uses: pypa/cibuildwheel@v3.3.1
3053
env:
31-
CIBW_BUILD: ${{ matrix.python }}-${{ matrix.buildplat[1] }}
54+
CIBW_PLATFORM: ${{ matrix.platform || 'auto' }}
55+
CIBW_ARCHS: ${{ matrix.archs || 'auto' }}
56+
# Can also be configured directly, using `with:`
57+
# with:
58+
# package-dir: .
59+
# output-dir: wheelhouse
60+
# config-file: "{package}/pyproject.toml"
3261

33-
- uses: actions/upload-artifact@v3
62+
- uses: actions/upload-artifact@v4
3463
with:
64+
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
3565
path: ./wheelhouse/*.whl
3666

3767
build_sdist:
3868
name: Build source distribution
3969
runs-on: ubuntu-latest
4070
steps:
41-
- uses: actions/checkout@v3
71+
- uses: actions/checkout@v5
4272

4373
- name: Build sdist
44-
shell: bash -l {0}
4574
run: pipx run build --sdist
4675

47-
- uses: actions/upload-artifact@v3
76+
- uses: actions/upload-artifact@v4
4877
with:
78+
name: cibw-sdist
4979
path: dist/*.tar.gz
5080

5181
upload_pypi:
5282
needs: [build_wheels, build_sdist]
5383
runs-on: ubuntu-latest
54-
# upload to PyPI on every tag starting with 'v'
55-
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
56-
# alternatively, to publish when a GitHub Release is created, use the following rule:
57-
# if: github.event_name == 'release' && github.event.action == 'published'
84+
environment: pypi
85+
permissions:
86+
id-token: write
87+
if: github.event_name == 'release' && github.event.action == 'published'
88+
# or, alternatively, upload to PyPI on every tag starting with 'v' (remove on: release above to use this)
89+
# if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
5890
steps:
59-
- uses: actions/download-artifact@v3
91+
- uses: actions/download-artifact@v5
6092
with:
61-
# unpacks default artifact into dist/
62-
# if `name: artifact` is omitted, the action will create extra parent dir
63-
name: artifact
93+
# unpacks all CIBW artifacts into dist/
94+
pattern: cibw-*
6495
path: dist
96+
merge-multiple: true
6597

6698
- uses: pypa/gh-action-pypi-publish@release/v1
67-
with:
68-
user: __token__
69-
password: ${{ secrets.PYPI_API_TOKEN }}
99+
# To test uploads to TestPyPI, uncomment the following:
100+
# with:
101+
# repository-url: https://test.pypi.org/legacy/

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,6 @@ Temporary Items
328328
.apdisk
329329

330330

331+
# pixi environments
332+
.pixi/*
333+
!.pixi/config.toml

README.md

Lines changed: 131 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The library consists of thin wrappers to `potlib` under `cpot` and a
1111

1212
This is [on PyPI](https://pypi.org/project/pypotlib), with wheels, so usage is simply:
1313

14-
``` bash
14+
```bash
1515
pip install pypotlib
1616
```
1717

@@ -21,13 +21,14 @@ work with.
2121

2222
### Local Development
2323

24-
The easiest way is to use the environment file, compatible with `conda`,
25-
`mamba`, `micromamba` etc.
24+
The easiest way is to use the `pixi` environment.
2625

2726
```bash
28-
micromamba env create -f environment.yml
29-
micromamba activate rgpotpy
27+
pixi s
3028
pdm install
29+
# For tests
30+
pixi s -e with-ase
31+
pytest tests/test_cache.py
3132
```
3233

3334
### Production
@@ -93,6 +94,130 @@ optimizer = BFGS(neb)
9394
optimizer.run(fmax=0.04)
9495
```
9596

97+
## Caching runs
98+
99+
`pypotlib` supports persistent caching via RocksDB. This allows energy and force
100+
evaluations to be stored and retrieved, significantly speeding up repeated
101+
calculations on identical configurations.
102+
103+
```python
104+
import pypotlib.cpot as cpot
105+
import numpy as np
106+
107+
# 1. Initialize the cache with a directory path
108+
# This will create a RocksDB database at the specified location.
109+
cache = cpot.PotentialCache("/tmp/my_pot_cache", create_if_missing=True)
110+
111+
# 2. Create the potential and link the cache
112+
lj = cpot.LJPot()
113+
lj.set_cache(cache)
114+
115+
# 3. Use as normal
116+
pos = np.array([[0.0, 0.0, 0.0], [3.0, 0.0, 0.0]])
117+
types = [1, 1]
118+
box = np.eye(3) * 10.0
119+
120+
# First call: Computes and stores result in DB
121+
e1, f1 = lj(pos, types, box)
122+
123+
# Second call (same inputs): Retrieves result from DB (Instant)
124+
e2, f2 = lj(pos, types, box)
125+
```
126+
127+
### ASE Caching
128+
129+
The ASE calculator provides more sophisticated caching, with the internal checks
130+
for equivalent structures further reducing calls to the underlying compiled
131+
code.
132+
133+
```python
134+
from ase import Atoms
135+
from pypotlib import cpot
136+
from pypotlib.ase_adapters import PyPotLibCalc
137+
138+
# Setup Potential with Cache
139+
cache = cpot.PotentialCache("ase_cache_db")
140+
pot = cpot.CuH2Pot()
141+
pot.set_cache(cache)
142+
143+
# Create Calculator
144+
atoms = Atoms(symbols=["Cu", "H"], positions=[[0, 0, 0], [0.5, 0.5, 0.5]])
145+
calc = PyPotLibCalc(pot)
146+
atoms.set_calculator(calc)
147+
148+
print(atoms.get_potential_energy())
149+
print(atoms.get_forces())
150+
```
151+
152+
### NEB Example with Benchmarking
153+
154+
To really see the power of the cache, we can run an NEB optimization twice. The
155+
first run performs the calculations and populates the RocksDB database. The
156+
second run, performing the exact same optimization, hits the cache for every
157+
step, reducing the computational cost to near zero.
158+
159+
```python
160+
import time
161+
import shutil
162+
from ase import Atoms
163+
from ase.mep import NEB
164+
from ase.optimize import BFGS
165+
from pypotlib import cpot
166+
from pypotlib.ase_adapters import PyPotLibCalc
167+
168+
# Setup a persistent cache
169+
cache_path = "/tmp/neb_demo_cache"
170+
# Clear previous cache to ensure a "cold" start for demonstration
171+
shutil.rmtree(cache_path, ignore_errors=True)
172+
cache = cpot.PotentialCache(cache_path, create_if_missing=True)
173+
174+
175+
def setup_neb_images():
176+
"""Helper to create fresh images for the NEB."""
177+
atoms_initial = Atoms(symbols=["H", "H"], positions=[(0, 0, 0), (0, 0, 1)])
178+
atoms_final = Atoms(symbols=["H", "H"], positions=[(0, 0, 2), (0, 0, 3)])
179+
180+
images = [atoms_initial]
181+
images += [atoms_initial.copy() for _ in range(3)]
182+
images += [atoms_final]
183+
184+
# Attach calculators with the SHARED cache
185+
for image in images:
186+
pot = cpot.LJPot()
187+
pot.set_cache(cache) # All images share the same DB
188+
image.calc = PyPotLibCalc(pot)
189+
190+
return images
191+
192+
193+
# --- Run 1: Cold Cache (Calculates & Writes) ---
194+
print("Starting Run 1 (Cold Cache)...")
195+
images_1 = setup_neb_images()
196+
neb_1 = NEB(images_1)
197+
neb_1.interpolate(method="idpp")
198+
opt_1 = BFGS(neb_1)
199+
200+
start_1 = time.time()
201+
opt_1.run(fmax=0.04)
202+
duration_1 = time.time() - start_1
203+
print(f"Run 1 finished in {duration_1:.4f} seconds.")
204+
205+
# --- Run 2: Warm Cache (Reads only) ---
206+
print("\nStarting Run 2 (Warm Cache)...")
207+
images_2 = setup_neb_images() # Re-create identical initial state
208+
neb_2 = NEB(images_2)
209+
neb_2.interpolate(method="idpp")
210+
opt_2 = BFGS(neb_2)
211+
212+
start_2 = time.time()
213+
opt_2.run(fmax=0.04)
214+
duration_2 = time.time() - start_2
215+
print(f"Run 2 finished in {duration_2:.4f} seconds.")
216+
217+
# --- Results ---
218+
speedup = duration_1 / duration_2 if duration_2 > 0 else 0
219+
print(f"\nSpeedup factor: {speedup:.1f}x")
220+
```
96221

97222
# Contributions
98223

@@ -102,4 +227,5 @@ all contributors to follow our [Code of
102227
Conduct](https://github.com/TheochemUI/pypotlib/blob/main/CODE_OF_CONDUCT.md).
103228

104229
# License
230+
105231
[MIT](https://github.com/TheochemUI/pypotlib/blob/main/LICENSE).
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
feat(pickle): generalize slightly
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
feat(cache): add a RocksDB integration

0 commit comments

Comments
 (0)