This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
alchemiscale is a high-throughput alchemical free energy execution system for HPC, cloud, bare metal, and Folding@Home. It's a Python 3.11+ service-oriented system using Neo4j as a graph database state store and AWS S3 for object storage.
# Install (editable, no-deps since conda handles deps)
pip install --no-deps -e .
# Environment setup via conda/mamba
micromamba create -f devtools/conda-envs/test.yml
micromamba activate alchemiscale-test
# Run all tests (parallel)
pytest -n auto -v --cov=alchemiscale alchemiscale/tests
# Run a single test file
pytest alchemiscale/tests/unit/test_models.py
# Run a single test by name
pytest -k "test_name_pattern" alchemiscale/tests
# Unit tests only
pytest alchemiscale/tests/unit
# Integration tests only (requires Docker for Neo4j via grolt)
pytest alchemiscale/tests/integration
# Formatting
black alchemiscale
black --check --diff alchemiscale # check only (CI uses this)
# Type checking
mypy alchemiscaleThe system has four main services communicating through Neo4j and S3:
- AlchemiscaleAPI (
interface/api.py): User-facing FastAPI REST API for submitting networks, actioning tasks, retrieving results. Deployed with Gunicorn + Uvicorn. - AlchemiscaleComputeAPI (
compute/api.py): Compute-facing FastAPI REST API for task claiming and result submission. - SynchronousComputeService (
compute/service.py): Runs on HPC/GPU nodes, claims tasks, executes GUFE ProtocolDAGs, submits results. - StrategistService (
strategist/service.py): Automated strategy execution service using thestratocasterframework.
models.py: Core data models —Scope(org/campaign/project hierarchy) andScopedKey(globally unique identifier within a scope).storage/statestore.py:Neo4jStore— the largest module, contains all Neo4j Cypher query logic for state management. This is the source of truth for the system.storage/objectstore.py:S3ObjectStore— stores serialized ProtocolDAG results.base/api.py: Shared FastAPI router logic, JWT auth, scope validation, response handling. Both APIs inherit from this.base/client.py: Shared HTTP client with retry logic. BothAlchemiscaleClientandAlchemiscaleComputeClientinherit from this.security/auth.py: JWT token creation/validation, password hashing with bcrypt.settings.py: PydanticBaseSettingsclasses that auto-populate from environment variables (Neo4j, S3, JWT, API settings).compression.py: Zstandard compression/decompression for GUFE objects.cli.py: Click-based CLI entry point (alchemiscalecommand) for starting APIs, compute services, database management, and identity management.
- Users interact via
AlchemiscaleClient→AlchemiscaleAPI - Tasks are created and stored in Neo4j with scope-based access control
- Compute services claim tasks via
AlchemiscaleComputeAPI - Results (ProtocolDAGResults) are stored in S3, references in Neo4j
- Users retrieve results through the client
- Settings from environment: All
*Settingsclasses usepydantic-settingsto auto-populate from env vars (case-insensitive). - Scope-based authorization: All entities are organized by
Scope(org/campaign/project). JWT tokens encode permitted scopes. - Frozen Pydantic models: Core models use
frozen=Truefor immutability. - Module docstrings: Follow the pattern
:mod:\alchemiscale.module_name` --- description`. - GUFE integration: Core chemistry types (
AlchemicalNetwork,Transformation,ChemicalSystem,Protocol) come from thegufelibrary and are stored/retrieved via their tokenization system (GufeKey,GufeTokenizable). GufeKeyis not stable acrossgufeversions: AScopedKeyis stable once created (it carries the ingestion-timegufe_key), but aGufeTokenizabledeserialized later may recompute a differentGufeKeyif thegufetokenization has changed since ingestion. Never derive aScopedKeyfrom a freshly-deserialized object's.key(e.g.ScopedKey(gufe_key=obj.key, **scope.to_dict())) to join against stored data — such a join can silently miss instead of erroring. Instead, obtain the authoritativeScopedKeyfrom the server (e.g.get_network_transformations, or the keys ofget_network_results) and fetch the object by thatScopedKey(e.g.get_transformation(sk)). Comparisons purely among objects deserialized in the same process (e.g. "is this transformation innetwork.edges?") are safe, since both sides use the current-stack key consistently.
Integration tests require Docker (for Neo4j via grolt) and use moto for AWS S3 mocking. The conftest.py in tests/integration/ handles port management for pytest-xdist parallel workers. Test environments are defined in devtools/conda-envs/test.yml.
The alchemiscale CLI (defined in cli.py) provides subcommands: api, compute synchronous, strategist, database init/check/reset/migrate, and identity add/list/remove/add_scope.
Always update documentation as part of any PR that warrants it. When a change adds, alters, or removes user-facing behavior:
- Update the relevant Sphinx docs under
docs/(e.g.docs/user_guide/) so guides and API references stay accurate. - Add a news fragment under
news/fromnews/TEMPLATE.rst, filling in only the relevant sections (Added/Changed/Deprecated/Removed/Fixed/Security) and removing the rest.
Treat missing or stale documentation as part of the change, not a follow-up.