Skip to content

Commit 88da9b9

Browse files
docs: add BackendRegistry typing and uniform-signature rules to AI instructions
Document two new conventions in CLAUDE.md, AGENTS.md, and .cursor/rules/project.mdc so future contributors (and AI assistants) don't reintroduce untyped registries or per-backend dispatcher branches: 1. **Registry typing rule** -- every BackendRegistry[F] instance MUST be parameterised with an explicit Protocol type F defined next to the backends it describes. Never declare a bare BackendRegistry without a type parameter (caller loses type inference on compute_fn). Include a concrete example with RMSDMatrixBackendFn. 2. **Uniform signature rule** -- every backend registered in the same registry MUST accept the exact Protocol call signature. If one backend needs an extra kwarg (e.g. periodic on mdtraj), every other backend MUST also accept that kwarg, silently ignoring it with # noqa: ARG001 and a docstring note. This keeps the dispatcher branch-free and preserves type inference. Also add an "Adding a New Backend Registry" checklist to CLAUDE.md and AGENTS.md so contributors know the Protocol/Literal/__init__ bookkeeping needed for a brand-new multi-backend compute function. No code changes.
1 parent 4200897 commit 88da9b9

3 files changed

Lines changed: 96 additions & 4 deletions

File tree

.cursor/rules/project.mdc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,27 @@ Why:
5050

5151
**Never change a public function's default backend away from `"mdtraj"`.** Add performance notes in the docstring pointing users to `backend="numba"` or a GPU backend when they need speed.
5252

53+
## Backend Registry Typing Rule
54+
55+
Every `BackendRegistry[F]` instance MUST be parameterised with an explicit `Protocol` type `F` defined in the same `_backends/_<kind>.py` file. Example:
56+
57+
```python
58+
from typing import Protocol
59+
60+
class RMSDMatrixBackendFn(Protocol):
61+
def __call__(
62+
self,
63+
traj: md.Trajectory,
64+
atom_indices: NDArray[np.int_],
65+
) -> NDArray[np.float64]: ...
66+
67+
rmsd_matrix_backends: BackendRegistry[RMSDMatrixBackendFn] = BackendRegistry(default="mdtraj")
68+
```
69+
70+
**Never** declare a bare `BackendRegistry` without a type parameter -- `registry.get(backend)` would return an unbound `F`, and callers like `compute_fn = rmsd_matrix_backends.get(backend)` would lose type information. The Protocol lives next to the backends it describes (not in the shared `_registry.py`) so the registry module stays decoupled.
71+
72+
**Uniform signature rule**: every backend registered in the same registry MUST accept the exact Protocol call signature. If one backend needs an extra keyword argument (e.g. `periodic` on mdtraj), every other backend MUST also accept that keyword, silently ignoring it if unused (`# noqa: ARG001` + docstring note). This preserves type inference at call sites and keeps dispatchers branch-free.
73+
5374
## Plotting Module Patterns
5475

5576
Every `plot_*` function follows this contract:

AGENTS.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,17 +140,46 @@ Reasons:
140140

141141
When reviewing or writing code, never change a public function's default backend away from `"mdtraj"`.
142142

143+
**Uniform signature rule**: every backend registered in a given `BackendRegistry` MUST accept the exact same call signature as the Protocol type parameter on that registry. If one backend needs an extra keyword argument (e.g. `periodic` on mdtraj), every other backend in the same registry MUST also accept that keyword, silently ignoring it if unused (mark `# noqa: ARG001` and document as "accepted for Protocol uniformity, ignored"). This keeps the dispatcher free of per-backend branching and preserves type inference for callers.
144+
145+
**Registry typing rule**: every `BackendRegistry[F]` instance MUST be parameterised with an explicit `Protocol` type `F`:
146+
147+
```python
148+
from typing import Protocol
149+
150+
class RMSDMatrixBackendFn(Protocol):
151+
def __call__(
152+
self,
153+
traj: md.Trajectory,
154+
atom_indices: NDArray[np.int_],
155+
) -> NDArray[np.float64]: ...
156+
157+
rmsd_matrix_backends: BackendRegistry[RMSDMatrixBackendFn] = BackendRegistry(default="mdtraj")
158+
```
159+
160+
Never declare a bare `BackendRegistry` without a type parameter -- `registry.get(backend)` would return an unbound `F` and the dispatcher would lose the signature of `compute_fn` at the call site. The Protocol lives in the same `_backends/_<kind>.py` file as the backends it describes (not in the shared `_registry.py`) so the registry module stays decoupled from any particular backend signature.
161+
143162
## Adding a New Compute Backend
144163

145164
For existing multi-backend functions (e.g. `compute_rmsd_matrix`, pairwise distances):
146165

147-
1. Add the implementation in the matching `src/mdpp/analysis/_backends/_<kind>.py` file, matching the existing signature.
166+
1. Add the implementation in the matching `src/mdpp/analysis/_backends/_<kind>.py` file, matching the `Protocol` type defined at the top of that file exactly.
148167
1. Use `require_torch()` / `require_jax()` / `require_cupy()` from `_backends/_imports.py` for optional GPU libraries -- never import them at module top-level.
168+
1. If you introduce a new keyword argument, also retrofit every existing backend in the same registry to accept it (silently ignoring when unused, marked `# noqa: ARG001`).
149169
1. Register in the module's `BackendRegistry` at the bottom of the file.
150170
1. Add the backend name to the corresponding `Literal` alias (`DistanceBackend` / `RMSDBackend`) in `_backends/_registry.py`.
151-
1. Add agreement tests in `tests/analysis/test_<kind>.py` guarded by the relevant `requires_*` skip marker.
171+
1. Add agreement tests in `tests/analysis/test_<kind>.py` guarded by the relevant `requires_*` skip marker and `@pytest.mark.gpu` (if GPU-only).
152172
1. **Do not change the public function's default backend** -- keep it at `"mdtraj"`.
153173

174+
## Adding a New Backend Registry
175+
176+
To introduce a registry for a new multi-backend compute function:
177+
178+
1. Create `src/mdpp/analysis/_backends/_<kind>.py` with a `Protocol` class defining the shared call signature.
179+
1. Declare the registry as `<kind>_backends: BackendRegistry[<Kind>BackendFn] = BackendRegistry(default="mdtraj")` -- always parameterise with the Protocol so callers get typed `compute_fn` from `registry.get()`.
180+
1. Add a `Literal` alias to `_backends/_registry.py` (`type <Kind>Backend = Literal["mdtraj", "numba", ...]`) and re-export it from `_backends/__init__.py`.
181+
1. The public wrapper in `src/mdpp/analysis/<kind>.py` imports the registry and delegates via `compute_fn = <kind>_backends.get(backend)`, letting mypy infer the Protocol type at the call site.
182+
154183
## Adding a New Chem Function
155184

156185
1. Create/extend a file in `src/mdpp/chem/`.

CLAUDE.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,18 +298,60 @@ Rationale:
298298
Users who want performance explicitly pass `backend="numba"` (or a GPU
299299
backend) and accept the PBC limitation.
300300

301+
**Uniform signature rule**: every backend registered in a given
302+
`BackendRegistry` MUST accept the exact same call signature as the
303+
Protocol type parameter on that registry. If one backend needs an
304+
extra keyword argument (e.g. `periodic` on mdtraj), every other
305+
backend in the same registry MUST also accept that keyword, silently
306+
ignoring it if unused (mark `# noqa: ARG001` and document in the
307+
docstring that the arg is accepted for Protocol uniformity and
308+
ignored). This keeps the dispatcher free of per-backend branching
309+
and preserves type inference for callers.
310+
311+
**Registry typing rule**: every `BackendRegistry[F]` instance MUST be
312+
parameterised with an explicit `Protocol` type `F`:
313+
314+
```python
315+
from typing import Protocol
316+
317+
class RMSDMatrixBackendFn(Protocol):
318+
def __call__(
319+
self,
320+
traj: md.Trajectory,
321+
atom_indices: NDArray[np.int_],
322+
) -> NDArray[np.float64]: ...
323+
324+
rmsd_matrix_backends: BackendRegistry[RMSDMatrixBackendFn] = BackendRegistry(default="mdtraj")
325+
```
326+
327+
Never declare a bare `BackendRegistry` without a type parameter --
328+
`registry.get(backend)` would return an unbound `F` and the dispatcher
329+
would lose the signature of `compute_fn` at the call site. The
330+
Protocol lives in the same `_backends/_<kind>.py` file as the backends
331+
it describes (not in the shared `_registry.py`) so the registry module
332+
stays decoupled from any particular backend signature.
333+
301334
### New compute backend
302335

303336
To add a new backend (e.g. `cupy`) for an existing compute function like the RMSD matrix or pairwise distances:
304337

305338
1. Add an implementation function in the matching `src/mdpp/analysis/_backends/_<kind>.py` file.
306339
1. Use lazy imports via `require_torch()` / `require_jax()` / `require_cupy()` from `_backends/_imports.py` -- never import optional GPU libraries at module top-level.
307-
1. Keep the signature aligned with existing backends in the same file (e.g. `(traj, pairs)` for distances, `(traj, atom_indices)` for RMSD matrix).
340+
1. Match the `Protocol` type defined at the top of the same file exactly (e.g. `RMSDMatrixBackendFn`, `DistanceBackendFn`). If you introduce a new keyword argument, also retrofit every existing backend in the same registry to accept it (silently ignoring if unused, `# noqa: ARG001`).
308341
1. Register the function in the module's `BackendRegistry` at the bottom of the file.
309342
1. Add the backend name to the corresponding `Literal` alias in `_backends/_registry.py` (`DistanceBackend` or `RMSDBackend`).
310-
1. Add agreement tests in `tests/analysis/test_<kind>.py` guarded by the relevant `requires_*` marker.
343+
1. Add agreement tests in `tests/analysis/test_<kind>.py` guarded by the relevant `requires_*` marker and `@pytest.mark.gpu` (if GPU-only).
311344
1. **Do not change the public function's default backend** -- keep it at `"mdtraj"`.
312345

346+
### New backend registry
347+
348+
To introduce a registry for a new multi-backend compute function:
349+
350+
1. Create `src/mdpp/analysis/_backends/_<kind>.py` with a `Protocol` class defining the shared call signature.
351+
1. Declare the registry as `<kind>_backends: BackendRegistry[<Kind>BackendFn] = BackendRegistry(default="mdtraj")` -- always parameterise with the Protocol so callers get typed `compute_fn` from `registry.get()`.
352+
1. Add a `Literal` alias to `_backends/_registry.py` (`type <Kind>Backend = Literal["mdtraj", "numba", ...]`) and re-export it from `_backends/__init__.py`.
353+
1. The public wrapper in `src/mdpp/analysis/<kind>.py` should import the registry and delegate via `compute_fn = <kind>_backends.get(backend)`, letting mypy infer the Protocol type.
354+
313355
### New cheminformatics function
314356

315357
1. Create or extend a module in `src/mdpp/chem/`.

0 commit comments

Comments
 (0)