From cf31cc4cf5c1b4fed5f8be9557b5ba96c8650b51 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 5 Feb 2026 10:22:29 +0000 Subject: [PATCH 01/24] Switch to pixi and rattler build. --- .github/workflows/devel.yaml | 59 ++++++++++-------- .github/workflows/main.yaml | 57 ++++++++++------- .github/workflows/pr.yaml | 56 +++++++++-------- README.md | 55 ++++++++++++---- actions/update_recipe.py | 90 +++++++++++++++------------ actions/upload_package.py | 60 ++++++++---------- environment.yaml | 15 ----- environment_macos.yaml | 13 ---- pixi.toml | 25 ++++++++ recipes/somd2/conda_build_config.yaml | 3 - recipes/somd2/recipe.yaml | 68 ++++++++++++++++++++ recipes/somd2/template.yaml | 80 ------------------------ 12 files changed, 306 insertions(+), 275 deletions(-) delete mode 100644 environment.yaml delete mode 100644 environment_macos.yaml create mode 100644 pixi.toml delete mode 100644 recipes/somd2/conda_build_config.yaml create mode 100644 recipes/somd2/recipe.yaml delete mode 100644 recipes/somd2/template.yaml diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index d25fb76..48b6203 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -3,7 +3,7 @@ name: Release Devel on: workflow_dispatch: push: - branches: [ devel ] + branches: [devel] jobs: build: @@ -18,11 +18,9 @@ jobs: - { name: "linux", os: "ubuntu-latest", shell: "bash -l {0}" } - { name: "macos", os: "macos-latest", shell: "bash -l {0}" } exclude: - # Exclude all but the latest Python from all - # but Linux - platform: { name: "macos", os: "macos-latest", shell: "bash -l {0}" } - python-version: "3.12" # MacOS can't run 3.12 yet... We want 3.10 and 3.11 + python-version: "3.12" # MacOS can't run 3.12 yet... We want 3.10 and 3.11 environment: name: somd2-build defaults: @@ -32,30 +30,37 @@ jobs: SIRE_DONT_PHONEHOME: 1 SIRE_SILENT_PHONEHOME: 1 steps: - - uses: conda-incubator/setup-miniconda@v3 + # + - uses: actions/checkout@v4 with: - auto-update-conda: true - python-version: ${{ matrix.python-version }} - activate-environment: somd2_build - miniforge-version: latest -# - - name: Clone the devel branch - run: git clone -b devel https://github.com/openbiosim/somd2 -# - - name: Setup Conda - run: conda install -y -c conda-forge boa anaconda-client packaging -# - - name: Update Conda recipe - run: python ${{ github.workspace }}/somd2/actions/update_recipe.py -# - - name: Prepare build location - run: mkdir ${{ github.workspace }}/build -# - - name: Build Conda package using conda build - run: conda build -c conda-forge -c openbiosim/label/dev ${{ github.workspace }}/somd2/recipes/somd2 -# - - name: Upload Conda package - run: python ${{ github.workspace }}/somd2/actions/upload_package.py + fetch-depth: 0 + # + - name: Compute version info + run: python ${{ github.workspace }}/actions/update_recipe.py + # + - name: Create sdist + run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz + working-directory: ${{ github.workspace }} + # + - name: Install rattler-build + uses: prefix-dev/rattler-build-action@v0.2.34 + with: + tool-version: latest + build-args: --help + # + - name: Write Python variant config + shell: bash + run: printf 'python:\n - "${{ matrix.python-version }}"\n' > "${{ github.workspace }}/python_variant.yaml" + # + - name: Build package using rattler-build + shell: bash + run: rattler-build build --recipe "${{ github.workspace }}/recipes/somd2" -c conda-forge -c openbiosim/label/dev --variant-config "${{ github.workspace }}/python_variant.yaml" + # + - name: Install anaconda-client + run: python -m pip install anaconda-client + # + - name: Upload package + run: python ${{ github.workspace }}/actions/upload_package.py env: ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} ANACONDA_LABEL: dev diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 088186e..3ad8384 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -23,7 +23,7 @@ jobs: exclude: - platform: { name: "macos", os: "macos-latest", shell: "bash -l {0}" } - python-version: "3.12" # MacOS can't run 3.12 yet... + python-version: "3.12" # MacOS can't run 3.12 yet... environment: name: somd2-build defaults: @@ -33,30 +33,39 @@ jobs: SIRE_DONT_PHONEHOME: 1 SIRE_SILENT_PHONEHOME: 1 steps: - - uses: conda-incubator/setup-miniconda@v3 + # + - uses: actions/checkout@v4 with: - auto-update-conda: true - python-version: ${{ matrix.python-version }} - activate-environment: somd2_build - miniforge-version: latest -# - - name: Clone the main branch - run: git clone -b main https://github.com/openbiosim/somd2 -# - - name: Setup Conda - run: conda install -y -c conda-forge boa anaconda-client packaging -# - - name: Update Conda recipe - run: python ${{ github.workspace }}/somd2/actions/update_recipe.py -# - - name: Prepare build location - run: mkdir ${{ github.workspace }}/build -# - - name: Build Conda package using conda build - run: conda build -c conda-forge -c openbiosim/label/main ${{ github.workspace }}/somd2/recipes/somd2 -# - - name: Upload Conda package - run: python ${{ github.workspace }}/somd2/actions/upload_package.py + ref: main + fetch-depth: 0 + # + - name: Compute version info + run: python ${{ github.workspace }}/actions/update_recipe.py + # + - name: Create sdist + run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz + working-directory: ${{ github.workspace }} + # + - name: Install rattler-build + uses: prefix-dev/rattler-build-action@v0.2.34 + with: + tool-version: latest + build-args: --help + # + - name: Write Python variant config + shell: bash + run: printf 'python:\n - "${{ matrix.python-version }}"\n' > "${{ github.workspace }}/python_variant.yaml" + # + - name: Build package using rattler-build + shell: bash + run: rattler-build build --recipe "${{ github.workspace }}/recipes/somd2" -c conda-forge -c openbiosim/label/main --variant-config "${{ github.workspace }}/python_variant.yaml" + # + - name: Install anaconda-client + run: python -m pip install anaconda-client + if: github.event.inputs.upload_packages == 'true' + # + - name: Upload package + run: python ${{ github.workspace }}/actions/upload_package.py env: ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} ANACONDA_LABEL: main diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 6191ca5..b6ed02e 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -17,14 +17,12 @@ jobs: - { name: "linux", os: "ubuntu-latest", shell: "bash -l {0}" } - { name: "macos", os: "macos-latest", shell: "bash -l {0}" } exclude: - # Exclude all but the latest Python from all - # but Linux - platform: { name: "macos", os: "macos-latest", shell: "bash -l {0}" } python-version: "3.10" - platform: { name: "macos", os: "macos-latest", shell: "bash -l {0}" } - python-version: "3.12" # MacOS can't run 3.12 yet... + python-version: "3.12" # MacOS can't run 3.12 yet... environment: name: somd2-build defaults: @@ -33,31 +31,35 @@ jobs: env: SIRE_DONT_PHONEHOME: 1 SIRE_SILENT_PHONEHOME: 1 - REPO: "${{ github.event.pull_request.head.repo.full_name || github.repository }}" steps: - - uses: conda-incubator/setup-miniconda@v3 + # + - uses: actions/checkout@v4 with: - auto-update-conda: true - python-version: ${{ matrix.python-version }} - activate-environment: somd2_build - miniforge-version: latest -# - - name: Clone the feature branch - run: git clone -b ${{ github.head_ref }} --single-branch https://github.com/${{ env.REPO }} somd2 -# - - name: Setup Conda - run: conda install -y -c conda-forge boa anaconda-client packaging -# - - name: Update Conda recipe - run: python ${{ github.workspace }}/somd2/actions/update_recipe.py -# - - name: Prepare build location - run: mkdir ${{ github.workspace }}/build -# - - name: Build Conda package using conda build using main channel + fetch-depth: 0 + # + - name: Compute version info + run: python ${{ github.workspace }}/actions/update_recipe.py + # + - name: Create sdist + run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz + working-directory: ${{ github.workspace }} + # + - name: Install rattler-build + uses: prefix-dev/rattler-build-action@v0.2.34 + with: + tool-version: latest + build-args: --help + # + - name: Write Python variant config + shell: bash + run: printf 'python:\n - "${{ matrix.python-version }}"\n' > "${{ github.workspace }}/python_variant.yaml" + # + - name: Build package using rattler-build (main channel) if: ${{ github.base_ref == 'main' }} - run: conda build -c conda-forge -c openbiosim/label/main ${{ github.workspace }}/somd2/recipes/somd2 -# - - name: Build Conda package using conda build using dev channel + shell: bash + run: rattler-build build --recipe "${{ github.workspace }}/recipes/somd2" -c conda-forge -c openbiosim/label/main --variant-config "${{ github.workspace }}/python_variant.yaml" + # + - name: Build package using rattler-build (dev channel) if: ${{ github.base_ref != 'main' }} - run: conda build -c conda-forge -c openbiosim/label/dev ${{ github.workspace }}/somd2/recipes/somd2 + shell: bash + run: rattler-build build --recipe "${{ github.workspace }}/recipes/somd2" -c conda-forge -c openbiosim/label/dev --variant-config "${{ github.workspace }}/python_variant.yaml" diff --git a/README.md b/README.md index f8174c8..3493b61 100644 --- a/README.md +++ b/README.md @@ -9,35 +9,66 @@ simulations. Built on top of [Sire](https://github.com/OpenBioSim/sire) and [Ope ## Installation -First create a conda environment using the provided environment file: +### Conda package + +Install `somd2` directly from the `openbiosim` channel: ``` -conda env create -f environment.yaml +conda install -c conda-forge -c openbiosim somd2 ``` -(We recommend using [Miniforge](https://github.com/conda-forge/miniforge).) +Or, for the development version: -> [!NOTE] -> On macOS, you will need to use the `environment_macos.yaml` file instead. +``` +conda install -c conda-forge -c openbiosim/label/dev somd2 +``` -Now install `somd2` into the environment: +### Installing from source (standalone) + +To install from source using [pixi](https://pixi.sh), which will +automatically create an environment with all required dependencies +(including pre-built [Sire](https://github.com/OpenBioSim/sire), +[BioSimSpace](https://github.com/OpenBioSim/biosimspace), +[Ghostly](https://github.com/OpenBioSim/ghostly), and +[Loch](https://github.com/OpenBioSim/loch)): ``` -conda activate somd2 -pip install --editable . +git clone https://github.com/openbiosim/somd2 +cd somd2 +pixi install +pixi shell +pip install -e . ``` -Alternatively, to install into an existing conda environment: +### Installing from source (full OpenBioSim development) + +If you are developing across the full OpenBioSim stack, first install +[Sire](https://github.com/OpenBioSim/sire) from source by following the +instructions [here](https://github.com/OpenBioSim/sire#installation), then +activate its pixi environment: ``` -conda install -c conda-forge -c openbiosim somd2 +pixi shell --manifest-path /path/to/sire/pixi.toml -e dev ``` -Or, for the development version: +You may also need to install other packages from source, e.g. +[BioSimSpace](https://github.com/OpenBioSim/biosimspace), +[Ghostly](https://github.com/OpenBioSim/ghostly), and +[Loch](https://github.com/OpenBioSim/loch): ``` -conda install -c conda-forge -c openbiosim/label/dev somd2 +pip install -e /path/to/biosimspace/python +pip install -e /path/to/ghostly +pip install -e /path/to/loch +``` + +Then install `somd2` into the environment: + ``` +pip install -e . +``` + +### Testing You should now have a `somd2` executable in your path. To test, run: diff --git a/actions/update_recipe.py b/actions/update_recipe.py index ac3ff96..629369a 100644 --- a/actions/update_recipe.py +++ b/actions/update_recipe.py @@ -1,50 +1,58 @@ -import sys +"""Compute git version info for rattler-build. + +This script computes GIT_DESCRIBE_TAG and GIT_DESCRIBE_NUMBER from the +git history and outputs them in GitHub Actions format for setting +environment variables. + +It also writes a _version.py file so that versioningit has a fallback +when .git is not available (e.g., when rattler-build excludes it). +""" + import os import subprocess +import sys -# Get the name of the script. script = os.path.abspath(sys.argv[0]) - -# we want to import the 'get_requirements' package from this directory -sys.path.insert(0, os.path.dirname(script)) - -# go up one directories to get the source directory -# (this script is in BioSimSpace/actions/) srcdir = os.path.dirname(os.path.dirname(script)) - -condadir = os.path.join(srcdir, "recipes", "somd2") - -print(f"conda recipe in {condadir}") - -# Store the name of the recipe and template YAML files. -recipe = os.path.join(condadir, "meta.yaml") -template = os.path.join(condadir, "template.yaml") - gitdir = os.path.join(srcdir, ".git") def run_cmd(cmd): - p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) - return str(p.stdout.read().decode("utf-8")).lstrip().rstrip() - - -# Get the remote. -remote = run_cmd( - f"git --git-dir={gitdir} --work-tree={srcdir} config --get remote.origin.url" -) -print(remote) - -# Get the branch. -branch = run_cmd( - f"git --git-dir={gitdir} --work-tree={srcdir} rev-parse --abbrev-ref HEAD" -) -print(branch) - -lines = open(template, "r").readlines() - -with open(recipe, "w") as FILE: - for line in lines: - line = line.replace("SOMD2_REMOTE", remote) - line = line.replace("SOMD2_BRANCH", branch) - - FILE.write(line) + p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, _ = p.communicate() + return stdout.decode("utf-8").strip() + + +# Get the full git describe output (e.g., "2024.1.0-5-gabcdef" or "2024.1.0") +describe = run_cmd(f"git --git-dir={gitdir} --work-tree={srcdir} describe --tags") + +if "-" in describe: + # Format: tag-number-hash (e.g., "2024.1.0-5-gabcdef") + parts = describe.rsplit("-", 2) + tag = parts[0] + number = parts[1] + rev = parts[2] # e.g., "gabcdef" + version = f"{tag}+{number}.{rev}" +else: + # Exactly on a tag + tag = describe + number = "0" + version = tag + +print(f"GIT_DESCRIBE_TAG={tag}") +print(f"GIT_DESCRIBE_NUMBER={number}") +print(f"Version={version}") + +# Write to GITHUB_ENV if running in GitHub Actions +github_env = os.environ.get("GITHUB_ENV") +if github_env: + with open(github_env, "a") as f: + f.write(f"GIT_DESCRIBE_TAG={tag}\n") + f.write(f"GIT_DESCRIBE_NUMBER={number}\n") + print("Exported to GITHUB_ENV") + +# Write _version.py for versioningit fallback +version_file = os.path.join(srcdir, "src", "somd2", "_version.py") +with open(version_file, "w") as f: + f.write(f'__version__ = "{version}"\n') +print(f"Wrote {version_file}") diff --git a/actions/upload_package.py b/actions/upload_package.py index 799638c..22547ad 100644 --- a/actions/upload_package.py +++ b/actions/upload_package.py @@ -1,16 +1,18 @@ +"""Upload built packages to the openbiosim Anaconda Cloud channel.""" + import os import sys import glob +import subprocess script = os.path.abspath(sys.argv[0]) -# go up one directories to get the source directory -# (this script is in somd2/actions/) +# Go up one directory to get the source directory. srcdir = os.path.dirname(os.path.dirname(script)) print(f"SOMD2 source is in {srcdir}\n") -# Get the anaconda token to authorise uploads +# Get the anaconda token to authorise uploads. if "ANACONDA_TOKEN" in os.environ: conda_token = os.environ["ANACONDA_TOKEN"] else: @@ -22,42 +24,30 @@ else: conda_label = "dev" -# get the root conda directory -conda = os.environ["CONDA"] - -# Set the path to the conda-bld directory. -conda_bld = os.path.join(conda, "envs", "somd2_build", "conda-bld") - -print(f"conda_bld = {conda_bld}") +# Search for rattler-build output first. +packages = glob.glob(os.path.join("output", "**", "*.conda"), recursive=True) -# Find the packages to upload -somd2_pkg = glob.glob(os.path.join(conda_bld, "*-*", "somd2-*.tar.bz2")) +# Fall back to conda-bld output. +if not packages: + if "CONDA" in os.environ: + conda = os.environ["CONDA"] + conda_bld = os.path.join(conda, "envs", "somd2_build", "conda-bld") + packages = glob.glob( + os.path.join(conda_bld, "**", "somd2-*.tar.bz2"), recursive=True + ) -if len(somd2_pkg) == 0: +if not packages: print("No somd2 packages to upload?") sys.exit(-1) -packages = somd2_pkg - -print(f"Uploading packages:") -print(" * ", "\n * ".join(packages)) - -packages = " ".join(packages) - - -def run_cmd(cmd): - import subprocess - - p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) - return str(p.stdout.read().decode("utf-8")).lstrip().rstrip() +print("Uploading packages:") +for pkg in packages: + print(f" * {pkg}") - -gitdir = os.path.join(srcdir, ".git") - -tag = run_cmd(f"git --git-dir={gitdir} --work-tree={srcdir} tag --contains") +packages_str = " ".join(packages) # Upload the packages to the openbiosim channel on Anaconda Cloud. -cmd = f"anaconda --token {conda_token} upload --user openbiosim --label {conda_label} --force {packages}" +cmd = f"anaconda --token {conda_token} upload --user openbiosim --label {conda_label} --force {packages_str}" print(f"\nUpload command:\n\n{cmd}\n") @@ -65,8 +55,12 @@ def run_cmd(cmd): print("Not uploading as the ANACONDA_TOKEN is not set!") sys.exit(-1) -output = run_cmd(cmd) -print(output) +def run_cmd(cmd): + p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) + return str(p.stdout.read().decode("utf-8")).lstrip().rstrip() + +output = run_cmd(cmd) +print(output) print("Package uploaded!") diff --git a/environment.yaml b/environment.yaml deleted file mode 100644 index e509e02..0000000 --- a/environment.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: somd2 - -channels: - - conda-forge - - openbiosim/label/dev - -dependencies: - - biosimspace - - ghostly - - filelock - - loch - - loguru - - numba - - nvidia-ml-py - - versioningit diff --git a/environment_macos.yaml b/environment_macos.yaml deleted file mode 100644 index 19083a3..0000000 --- a/environment_macos.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: somd2 - -channels: - - conda-forge - - openbiosim/label/dev - -dependencies: - - biosimspace - - ghostly - - filelock - - loguru - - numba - - versioningit diff --git a/pixi.toml b/pixi.toml new file mode 100644 index 0000000..6058f49 --- /dev/null +++ b/pixi.toml @@ -0,0 +1,25 @@ +[workspace] +name = "somd2" +channels = ["conda-forge", "openbiosim/label/dev"] +# No Windows - depends on loch which requires pycuda/pyopencl +platforms = ["linux-64", "osx-arm64"] + +[dependencies] +python = ">=3.10" +biosimspace = "*" +ghostly = "*" +loch = "*" +loguru = "*" +numba = "*" + +[target.linux-64.dependencies] +nvidia-ml-py = "*" + +[feature.test.dependencies] +pytest = "*" +black = "*" + +[environments] +default = [] +test = ["test"] +dev = ["test"] diff --git a/recipes/somd2/conda_build_config.yaml b/recipes/somd2/conda_build_config.yaml deleted file mode 100644 index 3e8e203..0000000 --- a/recipes/somd2/conda_build_config.yaml +++ /dev/null @@ -1,3 +0,0 @@ -pin_run_as_build: - sire: - max_pin: x.x diff --git a/recipes/somd2/recipe.yaml b/recipes/somd2/recipe.yaml new file mode 100644 index 0000000..34bfd3f --- /dev/null +++ b/recipes/somd2/recipe.yaml @@ -0,0 +1,68 @@ +context: + name: somd2 + +package: + name: ${{ name }} + version: ${{ env.get('GIT_DESCRIBE_TAG', default='PR') }} + +source: + path: ../../somd2-source.tar.gz + +build: + number: ${{ env.get('GIT_DESCRIBE_NUMBER', default='0') }} + script: python -m pip install . --no-deps --ignore-installed -vv + +requirements: + host: + - pip + - python + - setuptools + - versioningit + run: + - biosimspace + - ghostly + - loch + - loguru + - numba + - numpy <2.3 # Remove when nglview >=4.1 is released + - python + - if: linux + then: + - nvidia-ml-py + +tests: + - python: + imports: + - if: linux + then: + - somd2 + - script: + - if: linux and x86_64 + then: + - pytest -vvv --color=yes --black src/somd2 + - if: linux + then: + - pytest -vvv --color=yes --import-mode=importlib ./tests + files: + source: + - src/somd2/ + - tests/ + requirements: + run: + - pytest + - if: linux and x86_64 + then: + - black ==25 + - pytest-black + +about: + homepage: https://github.com/openbiosim/somd2 + license: GPL-3.0-or-later + license_file: LICENSE + summary: "GPU accelerated free-energy perturbation simulation engine." + repository: https://github.com/openbiosim/somd2 + documentation: https://github.com/openbiosim/somd2 + +extra: + recipe-maintainers: + - lohedges diff --git a/recipes/somd2/template.yaml b/recipes/somd2/template.yaml deleted file mode 100644 index 27c3f78..0000000 --- a/recipes/somd2/template.yaml +++ /dev/null @@ -1,80 +0,0 @@ -{% set name = "somd2" %} - -package: - name: {{ name }} - version: {{ environ.get('GIT_DESCRIBE_TAG', 'PR') }} - -source: - git_url: SOMD2_REMOTE - git_tag: SOMD2_BRANCH - -build: - number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} - script: {{ PYTHON }} -m pip install . --no-deps --ignore-installed -vv - -requirements: - host: - - biosimspace - - ghostly - - loch - - loguru - - pip - - python - - numba - - nvidia-ml-py # [linux] - - setuptools - - sire - - versioningit - run: - - biosimspace - - ghostly - - loch - - loguru - - numba - - nvidia-ml-py # [linux] - - python - - sire - -test: - script_env: - - SIRE_DONT_PHONEHOME - - SIRE_SILENT_PHONEHOME - requires: - - black == 25 # [linux and x86_64 and py==311] - - pytest - - pytest-black # [linux and x86_64 and py==311] - imports: - - somd2 # [linux] - source_files: - - src/somd2 - - tests - commands: - - pytest -vvv --color=yes --black src/somd2 # [linux and x86_64 and py==311] - - pytest -vvv --color=yes --import-mode=importlib tests # [linux] - -about: - home: https://github.com/openbiosim/somd2 - license: GPL-3.0-or-later - license_file: '{{ environ["RECIPE_DIR"] }}/LICENSE' - summary: "GPU accelerated free-energy pertubation simulation engine" - dev_url: https://github.com/openbiosim/somd2 - doc_url: https://github.com/openbiosim/somd2 - description: | - somd2 is an open-source GPU accelerated molecular dynamics engine for - alchemical free-energy calculations. Built on top of Sire, BioSimSpace, - and OpenMM. - - `conda install -c conda-forge -c openbiosim somd2` - - To install the development version: - - `conda install -c conda-forge -c openbiosim/label/dev somd2` - - When updating the development version it is generally advised to - update Sire at the same time: - - `conda install -c conda-forge -c openbiosim/label/dev somd2 sire` - -extra: - recipe-maintainers: - - lohedges From ec644dbb4e73d7e5ad2ba2d52411e9dbcd9e683e Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 6 Feb 2026 14:10:26 +0000 Subject: [PATCH 02/24] Add auto-generated version file to gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e1a8d07..dd1d570 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ output.yaml # Conda recipe (it is auto-generated) recipes/somd2/meta.yaml + +# Auto-generated version file +src/somd2/_version.py From 814cd4720b9acfe23ed5ffda3416663bd238a265 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 6 Feb 2026 20:09:18 +0000 Subject: [PATCH 03/24] Save the perturbed system to the output directory. --- src/somd2/runner/_base.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/somd2/runner/_base.py b/src/somd2/runner/_base.py index 14912eb..0c9db05 100644 --- a/src/somd2/runner/_base.py +++ b/src/somd2/runner/_base.py @@ -528,7 +528,17 @@ def __init__(self, system, config): self._is_restart = False self._cleanup() - # Save config whenever 'configure' is called to keep it up to date. + if self._config.replica_exchange and self._config.perturbed_system is not None: + # Check whether the perturbed system was loaded from file. If not + # we need to save to the output directory and update the config to + # point to the new file. + if self._config._perturbed_system_file is None: + filename = _Path(self._config.output_directory) / "perturbed_system.s3" + _sr.stream.save(perturbed_system, perturbed_system_file) + self._config._perturbed_system_file = str(filename) + _logger.info(f"Saving perturbed system to {perturbed_system_file}") + + # Write YAML configuration file to the output directory. if self._config.write_config: _dict_to_yaml( self._config.as_dict(), From de4a6a32e0a62de73e54789bdad59274b71e05e4 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 6 Feb 2026 20:09:53 +0000 Subject: [PATCH 04/24] Only convert if a filename has been set. --- src/somd2/config/_config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/somd2/config/_config.py b/src/somd2/config/_config.py index 52babac..fed794c 100644 --- a/src/somd2/config/_config.py +++ b/src/somd2/config/_config.py @@ -655,7 +655,10 @@ def as_dict(self, sire_compatible=False): # Use the path for the perturbed_system option, since the system # isn't serializable. - if self.perturbed_system is not None: + if ( + self.perturbed_system is not None + and self._perturbed_system_file is not None + ): d["perturbed_system"] = str(self._perturbed_system_file) d.pop("perturbed_system_file", None) From 2ac817034b686166b1abbf185e68409a828e32b3 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 9 Feb 2026 09:13:04 +0000 Subject: [PATCH 05/24] Fix install path. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3493b61..3bd2805 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ You may also need to install other packages from source, e.g. [Loch](https://github.com/OpenBioSim/loch): ``` -pip install -e /path/to/biosimspace/python +pip install -e /path/to/biosimspace pip install -e /path/to/ghostly pip install -e /path/to/loch ``` From b34748f73a84c5bb7b371efcc4cd0a971bdef827 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 9 Feb 2026 09:43:27 +0000 Subject: [PATCH 06/24] Remove numpy pin. --- recipes/somd2/recipe.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/recipes/somd2/recipe.yaml b/recipes/somd2/recipe.yaml index 34bfd3f..b57509b 100644 --- a/recipes/somd2/recipe.yaml +++ b/recipes/somd2/recipe.yaml @@ -24,7 +24,6 @@ requirements: - loch - loguru - numba - - numpy <2.3 # Remove when nglview >=4.1 is released - python - if: linux then: From 08f08796dde6ca1c102cd15b2ce015b489b6475d Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 9 Feb 2026 12:57:14 +0000 Subject: [PATCH 07/24] Fix shell quoting. --- .github/workflows/devel.yaml | 7 +++++-- .github/workflows/main.yaml | 7 +++++-- .github/workflows/pr.yaml | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index 48b6203..c936033 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -36,7 +36,8 @@ jobs: fetch-depth: 0 # - name: Compute version info - run: python ${{ github.workspace }}/actions/update_recipe.py + shell: bash + run: python "${{ github.workspace }}/actions/update_recipe.py" # - name: Create sdist run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz @@ -57,10 +58,12 @@ jobs: run: rattler-build build --recipe "${{ github.workspace }}/recipes/somd2" -c conda-forge -c openbiosim/label/dev --variant-config "${{ github.workspace }}/python_variant.yaml" # - name: Install anaconda-client + shell: bash run: python -m pip install anaconda-client # - name: Upload package - run: python ${{ github.workspace }}/actions/upload_package.py + shell: bash + run: python "${{ github.workspace }}/actions/upload_package.py" env: ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} ANACONDA_LABEL: dev diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 3ad8384..5cdcc46 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -40,7 +40,8 @@ jobs: fetch-depth: 0 # - name: Compute version info - run: python ${{ github.workspace }}/actions/update_recipe.py + shell: bash + run: python "${{ github.workspace }}/actions/update_recipe.py" # - name: Create sdist run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz @@ -61,11 +62,13 @@ jobs: run: rattler-build build --recipe "${{ github.workspace }}/recipes/somd2" -c conda-forge -c openbiosim/label/main --variant-config "${{ github.workspace }}/python_variant.yaml" # - name: Install anaconda-client + shell: bash run: python -m pip install anaconda-client if: github.event.inputs.upload_packages == 'true' # - name: Upload package - run: python ${{ github.workspace }}/actions/upload_package.py + shell: bash + run: python "${{ github.workspace }}/actions/upload_package.py" env: ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} ANACONDA_LABEL: main diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index b6ed02e..0cab3e2 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -38,7 +38,8 @@ jobs: fetch-depth: 0 # - name: Compute version info - run: python ${{ github.workspace }}/actions/update_recipe.py + shell: bash + run: python "${{ github.workspace }}/actions/update_recipe.py" # - name: Create sdist run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz From 749f31b6ffe7c008afa3a96f4e1fdee02e55df39 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 9 Feb 2026 13:03:27 +0000 Subject: [PATCH 08/24] Use relative paths to avoid path mangling. --- .github/workflows/devel.yaml | 4 ++-- .github/workflows/main.yaml | 4 ++-- .github/workflows/pr.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index c936033..5248fd1 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -37,7 +37,7 @@ jobs: # - name: Compute version info shell: bash - run: python "${{ github.workspace }}/actions/update_recipe.py" + run: python actions/update_recipe.py # - name: Create sdist run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz @@ -63,7 +63,7 @@ jobs: # - name: Upload package shell: bash - run: python "${{ github.workspace }}/actions/upload_package.py" + run: python actions/upload_package.py env: ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} ANACONDA_LABEL: dev diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5cdcc46..685826e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -41,7 +41,7 @@ jobs: # - name: Compute version info shell: bash - run: python "${{ github.workspace }}/actions/update_recipe.py" + run: python actions/update_recipe.py # - name: Create sdist run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz @@ -68,7 +68,7 @@ jobs: # - name: Upload package shell: bash - run: python "${{ github.workspace }}/actions/upload_package.py" + run: python actions/upload_package.py env: ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} ANACONDA_LABEL: main diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 0cab3e2..d5a7531 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -39,7 +39,7 @@ jobs: # - name: Compute version info shell: bash - run: python "${{ github.workspace }}/actions/update_recipe.py" + run: python actions/update_recipe.py # - name: Create sdist run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz From d375246d40f562210684c3a2742fca0602fcffc6 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 11 Feb 2026 09:26:45 +0000 Subject: [PATCH 09/24] Add linting tools to dev environment. --- pixi.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pixi.toml b/pixi.toml index 6058f49..ccc4f00 100644 --- a/pixi.toml +++ b/pixi.toml @@ -19,7 +19,11 @@ nvidia-ml-py = "*" pytest = "*" black = "*" +[feature.lint.dependencies] +pre-commit = "*" +ruff = "*" + [environments] default = [] test = ["test"] -dev = ["test"] +dev = ["test", "lint"] From eef379073c616c360ab27d32703e743a0b33eb74 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 11 Feb 2026 11:00:09 +0000 Subject: [PATCH 10/24] Add pre-commit. --- .pre-commit-config.yaml | 23 +++++++++++++++++++++++ README.md | 18 ++++++++++++++++++ pyproject.toml | 7 +++++++ 3 files changed, 48 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0043ca4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +files: ^(src|tests)/ +exclude: ^tests/(input|output)/ + +repos: + # General file quality checks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + args: [--maxkb=1000] # Prevent files larger than 1MB + - id: check-merge-conflict + + # Python formatting and linting + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + # Run the formatter + - id: ruff-format + # Run the linter (optional - remove if too strict) + - id: ruff + args: [--fix, --exit-zero] # Auto-fix but don't block commits diff --git a/README.md b/README.md index 3bd2805..d5ababc 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,24 @@ You should now have a `somd2` executable in your path. To test, run: somd2 --help ``` +## Development + +Pre-commit hooks are used to ensure consistent code formatting and linting. +To set up pre-commit in your development environment: + +``` +pixi shell -e dev +pre-commit install +``` + +This will run [ruff](https://docs.astral.sh/ruff/) formatting and linting +checks automatically on each commit. To run the checks manually against all +files: + +``` +pre-commit run --all-files +``` + ## Usage In order to run an alchemical free-energy simulation you will need to diff --git a/pyproject.toml b/pyproject.toml index fd326a6..10a39fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,10 @@ distance-dirty = "{base_version}+{distance}.{vcs}{rev}.dirty" [tool.versioningit.write] file = "src/somd2/_version.py" + +[tool.ruff.lint] +ignore = ["E402"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["F841"] + From a57d00f02aee308f00f902c1e702258e4ca0a767 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 11 Feb 2026 11:08:28 +0000 Subject: [PATCH 11/24] Autoformat and lint with ruff. --- src/somd2/_utils/_somd1.py | 3 +-- src/somd2/config/_config.py | 1 - src/somd2/runner/_base.py | 5 ++--- src/somd2/runner/_repex.py | 7 ++----- src/somd2/runner/_runner.py | 1 - tests/runner/test_config.py | 1 - tests/runner/test_lambda_values.py | 2 -- tests/runner/test_repex.py | 3 --- 8 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/somd2/_utils/_somd1.py b/src/somd2/_utils/_somd1.py index aa379f7..d25b795 100644 --- a/src/somd2/_utils/_somd1.py +++ b/src/somd2/_utils/_somd1.py @@ -548,7 +548,7 @@ def make_compatible(system, fix_perturbable_zero_sigmas=False): for idx0 in impropers0_idx.keys(): if idx1.equivalent(idx0): # Don't store duplicates. - if not idx0 in impropers_shared_idx.keys(): + if idx0 not in impropers_shared_idx.keys(): impropers_shared_idx[idx1] = ( impropers0_idx[idx0], impropers1_idx[idx1], @@ -659,7 +659,6 @@ def reconstruct_system(system): # Loop over all perturbable molecules. for mol in pert_mols: - # Delete any AmberParams properties. try: cursor = mol.cursor() diff --git a/src/somd2/config/_config.py b/src/somd2/config/_config.py index fed794c..f8c2986 100644 --- a/src/somd2/config/_config.py +++ b/src/somd2/config/_config.py @@ -1459,7 +1459,6 @@ def platform(self): @platform.setter def platform(self, platform): import os as _os - import sys as _sys if not isinstance(platform, str): raise TypeError("'platform' must be of type 'str'") diff --git a/src/somd2/runner/_base.py b/src/somd2/runner/_base.py index 0c9db05..91febaa 100644 --- a/src/somd2/runner/_base.py +++ b/src/somd2/runner/_base.py @@ -402,8 +402,8 @@ def __init__(self, system, config): if len(self._config.rest2_scale) != len(self._lambda_energy): msg = f"Length of 'rest2_scale' must match the number of {_lam_sym} values." if is_missing: - msg += f"If you have omitted some 'lambda_values` from `lambda_energy`, please " - f"add them to `lambda_energy`, along with the corresponding `rest2_scale` values." + msg += "If you have omitted some 'lambda_values` from `lambda_energy`, please " + "add them to `lambda_energy`, along with the corresponding `rest2_scale` values." _logger.error(msg) raise ValueError(msg) # Make sure the end states are close to 1.0. @@ -429,7 +429,6 @@ def __init__(self, system, config): # Make sure the REST2 selection is valid. if self._config.rest2_selection is not None: - try: atoms = _sr.mol.selection_to_atoms( self._system, self._config.rest2_selection diff --git a/src/somd2/runner/_repex.py b/src/somd2/runner/_repex.py index fb42401..cdc1851 100644 --- a/src/somd2/runner/_repex.py +++ b/src/somd2/runner/_repex.py @@ -461,7 +461,6 @@ def save_openmm_state(self, index): index: int The index of the replica. """ - from openmm.unit import angstrom # Get the current OpenMM state. state = ( @@ -1039,8 +1038,7 @@ def run(self): for j in range(num_checkpoint_batches): # Get the indices of the replicas in this batch. replicas = replica_list[ - j - * num_checkpoint_workers : (j + 1) + j * num_checkpoint_workers : (j + 1) * num_checkpoint_workers ] with ThreadPoolExecutor(max_workers=num_workers) as executor: @@ -1063,8 +1061,7 @@ def run(self): for j in range(num_checkpoint_batches): # Get the indices of the replicas in this batch. replicas = replica_list[ - j - * num_checkpoint_workers : (j + 1) + j * num_checkpoint_workers : (j + 1) * num_checkpoint_workers ] with ThreadPoolExecutor(max_workers=num_workers) as executor: diff --git a/src/somd2/runner/_runner.py b/src/somd2/runner/_runner.py index 12a637c..561bc13 100644 --- a/src/somd2/runner/_runner.py +++ b/src/somd2/runner/_runner.py @@ -658,7 +658,6 @@ def generate_lam_vals(lambda_base, increment=0.001): # Run the simulation, checkpointing in blocks. if checkpoint_frequency.value() > 0.0: - # Calculate the number of blocks and the remainder time. frac = (time / checkpoint_frequency).value() diff --git a/tests/runner/test_config.py b/tests/runner/test_config.py index cfb14be..05a2f0b 100644 --- a/tests/runner/test_config.py +++ b/tests/runner/test_config.py @@ -1,4 +1,3 @@ -import pytest import tempfile import sire as sr diff --git a/tests/runner/test_lambda_values.py b/tests/runner/test_lambda_values.py index ec6267f..ca63d7a 100644 --- a/tests/runner/test_lambda_values.py +++ b/tests/runner/test_lambda_values.py @@ -1,9 +1,7 @@ from pathlib import Path import tempfile -import pytest -import sire as sr from somd2.runner import Runner from somd2.config import Config diff --git a/tests/runner/test_repex.py b/tests/runner/test_repex.py index a21eb9d..9336053 100644 --- a/tests/runner/test_repex.py +++ b/tests/runner/test_repex.py @@ -17,7 +17,6 @@ def test_repex_output(ethane_methanol): Validate that repex specific simulation output is generated. """ with tempfile.TemporaryDirectory() as tmpdir: - config = { "runtime": "12fs", "restart": False, @@ -92,7 +91,6 @@ def test_rest2_scale(ethane_methanol, rest2_scale, is_valid): """Validate the REST2 scale factor handling.""" with tempfile.TemporaryDirectory() as tmpdir: - config = { "runtime": "12fs", "restart": False, @@ -130,7 +128,6 @@ def test_rest2_selection(ethane_methanol, rest2_selection, is_valid): """Validate the REST2 selection handling.""" with tempfile.TemporaryDirectory() as tmpdir: - config = { "runtime": "12fs", "restart": False, From 71f22e28fb2730ce2d8c375cff9e571419c6011c Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 11 Feb 2026 11:40:07 +0000 Subject: [PATCH 12/24] Remove redundant sections of recipe. --- recipes/somd2/recipe.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/recipes/somd2/recipe.yaml b/recipes/somd2/recipe.yaml index b57509b..d485c89 100644 --- a/recipes/somd2/recipe.yaml +++ b/recipes/somd2/recipe.yaml @@ -44,15 +44,10 @@ tests: - pytest -vvv --color=yes --import-mode=importlib ./tests files: source: - - src/somd2/ - tests/ requirements: run: - pytest - - if: linux and x86_64 - then: - - black ==25 - - pytest-black about: homepage: https://github.com/openbiosim/somd2 From 8d289706f2c77b1c76f02624dc561b7f84e11e30 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 11 Feb 2026 12:06:18 +0000 Subject: [PATCH 13/24] Update gitignore. --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index dd1d570..0084085 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ setup.err dist/ build/ somd2.egg-info -src/somd2/_version.py # Test output. output.yaml @@ -35,8 +34,5 @@ output.yaml # VSCode config .vscode/ -# Conda recipe (it is auto-generated) -recipes/somd2/meta.yaml - # Auto-generated version file src/somd2/_version.py From e117d0ec36567adf3bc5f440c7591878943525a1 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 11 Feb 2026 14:04:38 +0000 Subject: [PATCH 14/24] Add rattler-build to development environment. --- pixi.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pixi.toml b/pixi.toml index ccc4f00..9559a6e 100644 --- a/pixi.toml +++ b/pixi.toml @@ -21,6 +21,7 @@ black = "*" [feature.lint.dependencies] pre-commit = "*" +rattler-build = "*" ruff = "*" [environments] From 45789e490ed05a4195253f1ed27588b0ec99f787 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 11 Feb 2026 15:07:14 +0000 Subject: [PATCH 15/24] Use pixi install of rattler-build. --- .github/workflows/devel.yaml | 11 +++++++---- .github/workflows/main.yaml | 11 +++++++---- .github/workflows/pr.yaml | 11 +++++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index 5248fd1..e45d9e1 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -43,11 +43,14 @@ jobs: run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz working-directory: ${{ github.workspace }} # - - name: Install rattler-build - uses: prefix-dev/rattler-build-action@v0.2.34 + - name: Install pixi + uses: prefix-dev/setup-pixi@v0.9.4 with: - tool-version: latest - build-args: --help + run-install: false + # + - name: Install rattler-build + shell: bash + run: pixi global install rattler-build # - name: Write Python variant config shell: bash diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 685826e..354264e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -47,11 +47,14 @@ jobs: run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz working-directory: ${{ github.workspace }} # - - name: Install rattler-build - uses: prefix-dev/rattler-build-action@v0.2.34 + - name: Install pixi + uses: prefix-dev/setup-pixi@v0.9.4 with: - tool-version: latest - build-args: --help + run-install: false + # + - name: Install rattler-build + shell: bash + run: pixi global install rattler-build # - name: Write Python variant config shell: bash diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index d5a7531..1069e0a 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -45,11 +45,14 @@ jobs: run: pip install build && python -m build --sdist && mv dist/*.tar.gz somd2-source.tar.gz working-directory: ${{ github.workspace }} # - - name: Install rattler-build - uses: prefix-dev/rattler-build-action@v0.2.34 + - name: Install pixi + uses: prefix-dev/setup-pixi@v0.9.4 with: - tool-version: latest - build-args: --help + run-install: false + # + - name: Install rattler-build + shell: bash + run: pixi global install rattler-build # - name: Write Python variant config shell: bash From 6b254d7a398c0b048f2cd749a79a39e9ad2a733f Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 12 Feb 2026 09:08:44 +0000 Subject: [PATCH 16/24] Remove redundant setup.py file. --- setup.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 6068493..0000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() From 47d80e4ee91b08b3c9bd4458ecd59cc26efbd516 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 12 Feb 2026 12:48:04 +0000 Subject: [PATCH 17/24] Add YAML serialisation for custom lambda schedules and restraints. --- src/somd2/config/_config.py | 97 ++++++++++++++++++++++++++++++++++-- src/somd2/runner/_base.py | 70 ++++++++++++++++++++++++-- src/somd2/runner/_runner.py | 2 +- tests/runner/test_restart.py | 47 +++++++++++++++++ 4 files changed, 207 insertions(+), 9 deletions(-) diff --git a/src/somd2/config/_config.py b/src/somd2/config/_config.py index f8c2986..49ce7a0 100644 --- a/src/somd2/config/_config.py +++ b/src/somd2/config/_config.py @@ -652,6 +652,14 @@ def as_dict(self, sire_compatible=False): self._charge_scale_factor ): d["lambda_schedule"] = "charge_scaled_morph" + else: + d["lambda_schedule"] = self._serialise_object(self.lambda_schedule) + + # Serialise restraints. + if self.restraints is not None: + d["restraints"] = [ + self._serialise_object(restraint) for restraint in self.restraints + ] # Use the path for the perturbed_system option, since the system # isn't serializable. @@ -984,14 +992,21 @@ def lambda_schedule(self, lambda_schedule): if isinstance(lambda_schedule, str): # Strip whitespace and convert to lower case. lambda_schedule = lambda_schedule.strip().lower() - if lambda_schedule not in self._choices["lambda_schedule"]: - raise ValueError( - f"Lambda schedule not recognised. Valid lambda schedules are: {self._choices['lambda_schedule']}" - ) if lambda_schedule == "standard_morph": self._lambda_schedule = _LambdaSchedule.standard_morph() elif lambda_schedule == "charge_scaled_morph": self._lambda_schedule = _LambdaSchedule.charge_scaled_morph(0.2) + else: + try: + self._lambda_schedule = self._deserialise_object( + lambda_schedule + ) + except Exception: + raise ValueError( + "Unable to deserialise 'lambda_schedule'. Ensure that this is a " + "hex string representation of a valid LambdaSchedule object, or " + f"one of the following strings: {', '.join(self._choices['lambda_schedule'])}" + ) else: self._lambda_schedule = lambda_schedule else: @@ -1094,12 +1109,27 @@ def restraints(self, restraints): restraints = [restraints] # Check that all restraints are of the correct type. + deserialised_restraints = [] for restraint in restraints: - if not isinstance(restraint, _sr.mm._MM.Restraints): + if isinstance(restraint, _sr.mm._MM.Restraints): + continue + elif isinstance(restraint, str): + try: + restraint = self._deserialise_object(restraint) + except Exception: + raise ValueError( + "Unable to deserialise restraint. Ensure that this " + "is a hex string representation of a valid sire.mm._MM.Restraints object." + ) + deserialised_restraints.append(restraint) + else: raise ValueError( "'restraints' must be a sire.mm._MM.Restraints object, or a list of these objects." ) + if len(deserialised_restraints) > 0: + restraints = deserialised_restraints + self._restraints = restraints @property @@ -2082,6 +2112,63 @@ def overwrite(self, overwrite): raise ValueError("'overwrite' must be of type 'bool'") self._overwrite = overwrite + @staticmethod + def _serialise_object(obj): + """ + Internal method to serialise a Sire object to a hex string representation + for storage in the YAML config file. + + Parameters + ---------- + + obj: object + The Sire object to serialise. + + Returns + -------- + + hex: + The hex string representation of the Sire object. + """ + + from sire.stream import save + from sire.legacy.Qt import QByteArray + + try: + hex = QByteArray(save(obj)).to_hex().data() + except Exception as e: + raise ValueError(f"Unable to serialise object: {e}") + + return hex + + @staticmethod + def _deserialise_object(hex): + """ + Internal method to deserialise a Sire object from a hex string representation. + + Parameters + ---------- + + hex: str + The hex string representation of the Sire object. + + Returns + ------- + + obj: + The deserialised Sire object. + """ + from sire.stream import load + from sire.legacy.Qt import QByteArray + + try: + hex_byte_arrary = QByteArray.from_raw_data(hex, len(hex)) + obj = load(QByteArray.from_hex(hex_byte_arrary)) + except Exception as e: + raise ValueError(f"Unable to deserialise object: {e}") + + return obj + @classmethod def _create_parser(cls): """ diff --git a/src/somd2/runner/_base.py b/src/somd2/runner/_base.py index 91febaa..b6b8c89 100644 --- a/src/somd2/runner/_base.py +++ b/src/somd2/runner/_base.py @@ -1354,6 +1354,70 @@ def _compare_configs(config1, config2): v1 = config1[key] v2 = config2[key] + # None config options stored as a Sire property are converted + # to False, so None and Fasle are equivalent for the purposes of + # comparison. + if v1 is None and not v2: + continue + if v2 is None and not v1: + continue + + # Early exit equivalence check. + if v1 == v2: + continue + + # Custom lambda schedules are stored as a hexademical string of + # serialised object. We need to deserialise them before comparison. + if key == "lambda_schedule": + # Standard schedules are stored as strings, so we can compare these directly. + if v1 == v2: + continue + else: + try: + v1 = _Config._deserialise_object(v1) + except Exception as e: + raise ValueError( + f"Unable to deserialise lambda schedule from config1: {str(e)}" + ) + try: + v2 = _Config._deserialise_object(v2) + except Exception as e: + raise ValueError( + f"Unable to deserialise lambda schedule from config2: {str(e)}" + ) + if v1 != v2: + raise ValueError( + f"{key} has changed since the last run. This is not " + "allowed when using the restart option." + ) + else: + continue + + # Restraints are stored as a list of hexadecimal strings of serialised objects. + # We need to deserialise them before comparison. + elif key == "restraints": + if v1 and v2: + for r1, r2 in zip(v1, v2): + try: + r1 = _Config._deserialise_object(r1) + except Exception as e: + raise ValueError( + f"Unable to deserialise restraint from config1: {str(e)}" + ) + try: + r2 = _Config._deserialise_object(r2) + except Exception as e: + raise ValueError( + f"Unable to deserialise restraint from config2: {str(e)}" + ) + if r1 != r2: + raise ValueError( + f"{key} has changed since the last run. This is not " + "allowed when using the restart option." + ) + else: + continue + # Convert GeneralUnits to strings for comparison. if isinstance(v1, _GeneralUnit): v1 = str(v1) @@ -1363,14 +1427,14 @@ def _compare_configs(config1, config2): # Convert Sire containers to lists for comparison. try: v1 = v1.to_list() - except: + except Exception: pass try: v2 = v2.to_list() - except: + except Exception: pass - if (v1 == None and v2 == False) or (v2 == None and v1 == False): + if (v1 is None and v2 == False) or (v2 is None and v1 == False): continue # The GCMC frequency will be automaticall set if None. elif key == "gcmc_frequency" and v1 is None: diff --git a/src/somd2/runner/_runner.py b/src/somd2/runner/_runner.py index 561bc13..931b534 100644 --- a/src/somd2/runner/_runner.py +++ b/src/somd2/runner/_runner.py @@ -155,7 +155,7 @@ def _initialise_gpu_devices(num_devices, oversubscription_factor=1): Returns ------- - devices : [(str, int)] + devices: [(str, int)] List of available device numbers with oversubscription factor. """ devices = [] diff --git a/tests/runner/test_restart.py b/tests/runner/test_restart.py index 5f9639d..bfb5fd0 100644 --- a/tests/runner/test_restart.py +++ b/tests/runner/test_restart.py @@ -228,3 +228,50 @@ def test_restart(mols, request): # Load the new checkpoint file and make sure the restart fails with pytest.raises(ValueError): runner_badconfig = Runner(mols, Config(**config_new)) + + +def test_restart_custom_schedule(ethane_methanol): + """ + Test that a restart works when using a non-standard lambda schedule. + """ + mols = ethane_methanol.clone() + schedule = sr.cas.LambdaSchedule.standard_decouple() + + with tempfile.TemporaryDirectory() as tmpdir: + config = { + "runtime": "12fs", + "restart": False, + "output_directory": tmpdir, + "energy_frequency": "4fs", + "checkpoint_frequency": "4fs", + "frame_frequency": "4fs", + "lambda_schedule": schedule, + "platform": "CPU", + "max_threads": 1, + "num_lambda": 2, + } + + # Instantiate a runner using the config defined above. + runner = Runner(mols, Config(**config)) + + del runner + + config_new = { + "runtime": "24fs", + "restart": True, + "output_directory": tmpdir, + "energy_frequency": "4fs", + "checkpoint_frequency": "4fs", + "frame_frequency": "4fs", + "lambda_schedule": schedule, + "platform": "CPU", + "max_threads": 1, + "num_lambda": 2, + "overwrite": True, + "log_level": "DEBUG", + } + + runner2 = Runner(mols, Config(**config_new)) + + # Run the simulation. + runner2.run() From 5c768502a375c4d95fb697b1f6a2893e35a055ff Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 12 Feb 2026 15:34:02 +0000 Subject: [PATCH 18/24] Fix handling of different perturbed_system formats. --- src/somd2/runner/_base.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/somd2/runner/_base.py b/src/somd2/runner/_base.py index b6b8c89..7d3469b 100644 --- a/src/somd2/runner/_base.py +++ b/src/somd2/runner/_base.py @@ -532,10 +532,12 @@ def __init__(self, system, config): # we need to save to the output directory and update the config to # point to the new file. if self._config._perturbed_system_file is None: - filename = _Path(self._config.output_directory) / "perturbed_system.s3" - _sr.stream.save(perturbed_system, perturbed_system_file) - self._config._perturbed_system_file = str(filename) - _logger.info(f"Saving perturbed system to {perturbed_system_file}") + filename = str( + _Path(self._config.output_directory) / "perturbed_system.s3" + ) + _sr.stream.save(self._config.perturbed_system, filename) + self._config._perturbed_system_file = filename + _logger.info(f"Saving perturbed system to {filename}") # Write YAML configuration file to the output directory. if self._config.write_config: From d92fa7d3109240a35eb0b2b7dde1440095a11185 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 12 Feb 2026 15:50:36 +0000 Subject: [PATCH 19/24] nvidia-ml-py is noarch. --- pixi.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pixi.toml b/pixi.toml index 9559a6e..7a3ffa4 100644 --- a/pixi.toml +++ b/pixi.toml @@ -11,8 +11,6 @@ ghostly = "*" loch = "*" loguru = "*" numba = "*" - -[target.linux-64.dependencies] nvidia-ml-py = "*" [feature.test.dependencies] From 21085824be79d7ccd3586b53e5cfeea96c991f8b Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 12 Feb 2026 15:51:14 +0000 Subject: [PATCH 20/24] Fix recipe. --- recipes/somd2/recipe.yaml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/recipes/somd2/recipe.yaml b/recipes/somd2/recipe.yaml index d485c89..ed01f17 100644 --- a/recipes/somd2/recipe.yaml +++ b/recipes/somd2/recipe.yaml @@ -24,24 +24,15 @@ requirements: - loch - loguru - numba + - nvidia-ml-py - python - - if: linux - then: - - nvidia-ml-py tests: - python: imports: - - if: linux - then: - - somd2 + - somd2 - script: - - if: linux and x86_64 - then: - - pytest -vvv --color=yes --black src/somd2 - - if: linux - then: - - pytest -vvv --color=yes --import-mode=importlib ./tests + - pytest -vvv --color=yes --import-mode=importlib ./tests files: source: - tests/ From 00416d37b0bfd340677ff0a76e7f574af73e3342 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 12 Feb 2026 16:02:00 +0000 Subject: [PATCH 21/24] Skip tests on macOS. --- recipes/somd2/recipe.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/recipes/somd2/recipe.yaml b/recipes/somd2/recipe.yaml index ed01f17..f97ceb6 100644 --- a/recipes/somd2/recipe.yaml +++ b/recipes/somd2/recipe.yaml @@ -30,9 +30,13 @@ requirements: tests: - python: imports: - - somd2 + - if: linux + then: + - somd2 - script: - - pytest -vvv --color=yes --import-mode=importlib ./tests + - if: linux + then: + - pytest -vvv --color=yes --import-mode=importlib ./tests files: source: - tests/ From f571d026befa43dce3ac7bcf24842205be96d311 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 12 Feb 2026 16:08:07 +0000 Subject: [PATCH 22/24] Add MANIFEST for tests files and refactor recipe. --- MANIFEST.in | 1 + recipes/somd2/recipe.yaml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..96737d3 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +graft tests diff --git a/recipes/somd2/recipe.yaml b/recipes/somd2/recipe.yaml index f97ceb6..2f66f79 100644 --- a/recipes/somd2/recipe.yaml +++ b/recipes/somd2/recipe.yaml @@ -29,9 +29,9 @@ requirements: tests: - python: - imports: - - if: linux - then: + - if: linux + then: + imports: - somd2 - script: - if: linux From cc4c0c6e60ef638821598e12f9303624587dd535 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 12 Feb 2026 16:09:49 +0000 Subject: [PATCH 23/24] Remove import tests since they don't work with conditionals. --- recipes/somd2/recipe.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/recipes/somd2/recipe.yaml b/recipes/somd2/recipe.yaml index 2f66f79..ac9c510 100644 --- a/recipes/somd2/recipe.yaml +++ b/recipes/somd2/recipe.yaml @@ -28,11 +28,6 @@ requirements: - python tests: - - python: - - if: linux - then: - imports: - - somd2 - script: - if: linux then: From 48c5df95034f61db3a5519b353eed8e2b248b57e Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 12 Feb 2026 16:16:00 +0000 Subject: [PATCH 24/24] Fix recipe conditional formatting. --- recipes/somd2/recipe.yaml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/recipes/somd2/recipe.yaml b/recipes/somd2/recipe.yaml index ac9c510..0217074 100644 --- a/recipes/somd2/recipe.yaml +++ b/recipes/somd2/recipe.yaml @@ -28,16 +28,19 @@ requirements: - python tests: - - script: - - if: linux - then: - - pytest -vvv --color=yes --import-mode=importlib ./tests - files: - source: - - tests/ - requirements: - run: - - pytest + - if: linux + then: + - python: + imports: + - somd2 + - script: + - PYTHONPATH=. pytest -vvv --color=yes --import-mode=importlib ./tests + files: + source: + - tests/ + requirements: + run: + - pytest about: homepage: https://github.com/openbiosim/somd2