Thanks for your interest in contributing! This guide covers setup, testing, and submitting changes.
git clone https://github.com/ericchansen/q2mm.git
cd q2mm
pip install -e ".[dev]"The [dev] extras include pytest, ruff, other development tools, and core
backend/optimizer dependencies such as openmm and scipy.
If you need all optional backends for local testing, you can install the
broader all extra:
pip install -e ".[dev,all]" # everything (OpenMM, JAX, scipy, etc.)Tests are organized into four tiers so you can iterate quickly:
pytest # core only (~13s, no backends)
pytest --run-integration # core + integration (~49s)
pytest --run-validation # core + integration + validation (~80s)
pytest --run-nightly # everything (~330s+, optimizer loops)By default, all tests run on CPU only — no GPU memory is allocated.
Use --gpu or set Q2MM_USE_GPU=1 to opt into GPU execution for
benchmarks or GPU-specific tests.
Tests requiring specific backends are auto-skipped when the dependency is
missing. Use -m to filter:
pytest -m openmm # only OpenMM tests
pytest -m tinker # only Tinker tests
pytest -m "not tinker" # skip Tinker testsAvailable markers: openmm, tinker, jax, jax_md, psi4.
If you don't have OpenMM, Tinker, or other backends installed locally, you can use the pre-built CI images from GitHub Container Registry:
# OpenMM tests
docker run --rm -v $PWD:/workspace -w /workspace \
ghcr.io/ericchansen/q2mm/ci-openmm:latest \
pytest -m openmm --run-nightly
# Tinker tests
docker run --rm -v $PWD:/workspace -w /workspace \
ghcr.io/ericchansen/q2mm/ci-tinker:latest \
pytest -m tinker --run-nightly
# JAX tests
docker run --rm -v $PWD:/workspace -w /workspace \
ghcr.io/ericchansen/q2mm/ci-jax:latest \
pytest -m jax --run-nightly
# All backends (full image)
docker run --rm -v $PWD:/workspace -w /workspace \
ghcr.io/ericchansen/q2mm/ci-full:latest \
pytest --run-nightlyOn Windows (PowerShell), replace $PWD with ${PWD}.
Images are built from .github/docker/Dockerfile using environment files in
.github/envs/. To rebuild locally from the repository root:
docker build -f .github/docker/Dockerfile --build-arg ENV_FILE=openmm.yml -t q2mm-openmm .We use Ruff for linting and formatting. Install pre-commit hooks so formatting and lint errors are caught before they reach CI:
# Option A: prek (fast, Rust-based — https://github.com/j178/prek)
prek install
# Option B: Python pre-commit framework
pip install pre-commit
pre-commit installEither tool reads the same .pre-commit-config.yaml in the repo root. Once
installed, hooks run automatically on every git commit.
The ruff hook auto-fixes simple lint issues during commit. When fixes are
applied, the hook exits with an error so you can review the changes. Stage the
updated files with git add and commit again.
To check the entire codebase manually:
ruff check q2mm/ test/ scripts/
ruff format --check q2mm test scripts examplesWhen running benchmarks with q2mm-benchmark, the runner always saves
its output — every requested candidate is written to <output>/candidates/
and each accepted candidate is promoted to <output>/accepted/ and
<output>/forcefields/. Benchmark runs can take minutes to hours depending
on the system and optimizer. With SciPy L-BFGS-B + the JAX objective
executor on GPU, all 5 TS systems complete in ~15 minutes total (dominated
by JIT compilation, not optimization). Results are not reproducible
bit-for-bit due to floating-point reduction-order differences between
devices.
# Convergence run for one system (writes candidate + promoted artifacts)
q2mm-benchmark single --system rh-enamide --backend jax --optimizer scipy-lbfgsb-jax \
--workflow method-e2 --output results/rh-enamide
# Convergence batch across all systems
q2mm-benchmark batch --output results
# Backend x form x optimizer matrix for one system
q2mm-benchmark matrix --system rh-enamide --backend jax --optimizer scipy-lbfgsb \
--output results/rh-enamideRules:
- Output is always persisted — there is no
--no-save; pass--outputto choose the location (defaults to./results). - Commit output force fields to the separate
q2mm-datarepo underbenchmarks/<system>/forcefields/so they are tracked in version control. - Commit result JSON files to
q2mm-data/benchmarks/<system>/for reproducibility and future analysis. - If running GPU vs CPU comparisons, run them sequentially on an idle system — parallel benchmark runs produce invalid timing data.
- Fork and create a feature branch from
master - Make your changes with clear, focused commits
- Ensure
ruff checkandruff format --checkpass - Ensure
pytestpasses (at minimum the core tier;--run-integrationrecommended) - Open a pull request against
master
Pull requests trigger CI automatically across six jobs:
- Lint — ruff check + format + dependency parity (
check_env_dep_parity.py) - Core — pure-Python tests on Python 3.10–3.13 (no backends)
- OpenMM — OpenMM backend tests in Docker
- Tinker — Tinker backend tests in Docker
- JAX — JAX backend tests in Docker
- JAX-MD — JAX-MD backend tests in Docker
All jobs must pass before merging.
To support a new MM backend:
- Implement the prepared-session backend contract in
q2mm.backends.contracts: the backend exposesinfoandprepare(PreparationRequest(...)), and each prepared session handles typed requests such asEnergyRequest,HessianRequest, andFrequencyRequest. - Handle unit conversion between canonical units (kcal/mol, Å, hartree/bohr² as appropriate) and whatever the underlying engine expects internally.
- Declare capabilities in the backend
BackendInfoso objective executors can fail clearly when a requested operation is unsupported. - Register the backend in
q2mm.backends.registryif it should be loaded by name. - Add tests with the appropriate backend marker (e.g.,
@pytest.mark.mybackend).
Optimizers consume objective executors, so a backend that satisfies the typed
contract works with ObjectivePlan, PythonObjectiveExecutor, and any
optimizer using optimize(evaluator, space).
Backend API version 1 is a stable public authoring contract. Built-in and
out-of-tree backends both use JSON-safe manifests with exactly
backend_api_version, name, role, capability_ceiling,
functional_form_ceiling, factory, and optional probe. The one validator,
q2mm.backends.discovery.validate_manifest, produces a BackendDescriptor.
Unknown keys and pre-v1 names are rejected rather than aliased.
An out-of-tree backend advertises one entry point in the exact
q2mm.backends group. Its value targets a lightweight descriptor module
(a manifest mapping or zero-argument provider), not the implementation:
[project.entry-points."q2mm.backends"]
my-backend = "my_backend.descriptor:MANIFEST"Discovery is lazy: importing q2mm.backends.registry enumerates nothing, and
cataloging imports only the descriptor module. The backend implementation named
by the manifest's factory import string is imported only on an explicit
load_backend("my-backend"). The first query creates a deterministic cached
snapshot; registry.refresh() rebuilds it. Missing dependencies, import
errors, incompatible versions, invalid claims, and broken factories are
isolated. Built-ins win name conflicts, and all external plugins sharing one
name are rejected.
Manifest capability/form fields are static ceilings. Loaded BackendInfo
provides authoritative exact subsets; its role must equal the descriptor role
and it may not overclaim. Run the typed, dependency-light
q2mm.backends.conformance checks against a bounded deterministic case.
See the author guide and the independently
installable examples/backend-plugin/ reference implementation.
To support a new file format:
- Add a module in
q2mm/io/(e.g.,q2mm/io/charmm.py) with a loader (e.g.,load_charmm_rtf()) that reads the file and returns aForceFieldwith parameters in canonical units. - Add a saver in the same module (e.g.,
save_charmm_rtf()) that converts canonical → format units on write. - Re-export the public functions from
q2mm/io/__init__.py. - Add convenience methods on
ForceField(e.g.,from_charmm_rtf(),to_charmm_rtf()).