You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -40,15 +45,19 @@ Every `compute_*` function follows this contract:
40
45
41
46
## Compute Backend Default Rule
42
47
43
-
Every public analysis function that accepts a `backend=` argument MUST default to `"mdtraj"`. Other backends (`numba`, `torch`, `jax`, `cupy`) exist for performance (50-200x CPU speedup or GPU acceleration) and MUST be opted into explicitly by the caller.
48
+
Every public analysis function that accepts a `backend=` argument MUST default to `"mdtraj"` **when mdtraj provides a native kernel for that computation**. Other backends (`numba`, `torch`, `jax`, `cupy`) exist for performance (50-200x CPU speedup or GPU acceleration) and MUST be opted into explicitly by the caller. The only current exception is `compute_dccm`, which defaults to `"numpy"` because mdtraj has no covariance kernel -- `numpy`'s BLAS GEMM is multi-threaded and ships with the base install.
44
49
45
50
Why:
46
51
47
52
- Only `mdtraj` supports periodic boundary conditions -- defaulting to anything else silently drops PBC.
48
53
- Consistent defaults across all analysis functions avoid surprising behavior when users switch between them.
49
54
- The GPU backends (`[gpu]` extra) must never be required for the common path.
50
55
51
-
**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.
56
+
**Never silently change a public function's default backend away from its current value** (`"mdtraj"`, or `"numpy"` for DCCM). Add performance notes in the docstring pointing users to `backend="numba"` or a GPU backend when they need speed.
57
+
58
+
## Float Dtype Rule
59
+
60
+
mdpp uses **float32 by default**, matching mdtraj's coordinate precision. Never force float64 for "numerical stability". Every `compute_*` function takes `dtype: DtypeArg = None` as the final keyword argument, calls `resolved = resolve_dtype(dtype)` at the top, and casts outputs with `astype(resolved, copy=False)` (zero-copy when the backend's native dtype already matches). Import `DtypeArg` from `mdpp._types`; never inline the union type.
52
61
53
62
## Backend Registry Typing Rule
54
63
@@ -62,11 +71,13 @@ class RMSDMatrixBackendFn(Protocol):
Protocols return `NDArray[np.floating]` (not `NDArray[np.float64]`) so each backend can return its native dtype -- float32 for mdtraj / torch / jax / cupy, float64 for numba. The public `compute_*` wrapper casts with `astype(resolved, copy=False)` so when the backend's native dtype already matches the resolved dtype, no second buffer is allocated. Never add an unconditional `.astype(np.float64)` at a backend boundary -- on an N^2 matrix at N=120k that is 115 GB of pointless copying.
80
+
70
81
**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
82
72
83
**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.
- First positional arg is `traj: md.Trajectory` (or a feature matrix).
69
+
- All remaining args are keyword-only.
70
+
- `dtype: DtypeArg = None` is the **last** keyword argument.
71
+
- Call `resolve_dtype(dtype)` once at the top, then thread `resolved` through every cast.
49
72
- Import trajectory helpers from `mdpp.core.trajectory`, never from `mdpp.analysis.trajectory`.
50
73
- Validate inputs early (raise `ValueError` with descriptive messages).
51
-
- Cast outputs to explicit dtypes: `np.asarray(..., dtype=np.float64)`.
74
+
- Cast with `np.asarray(value, dtype=resolved)` or `.astype(resolved, copy=False)` -- both are no-copy when the source dtype already matches.
75
+
76
+
## Multi-Backend Compute Functions
77
+
78
+
If the function dispatches through a `BackendRegistry`, add `backend: <Kind>Backend = "mdtraj"` (or `"numpy"` if mdtraj has no kernel) and resolve via `compute_fn = <kind>_backends.get(backend)`. Every backend must accept the same call signature; extra kwargs on one backend (e.g. `periodic` on mdtraj) must be silently accepted by all others.
Each class is `@dataclass(frozen=True, slots=True)` with parameters at construction and a `__call__(data) -> ClusteringResult | FeatureClusteringResult` invocation.
137
+
138
+
```python
139
+
result = Gromos(cutoff_nm=0.15)(rmsd_matrix.rmsd_matrix_nm)
140
+
result = KMeans(n_clusters=10)(pca.projections)
141
+
```
142
+
143
+
Do **not** add the old function-form wrappers (`compute_gromos_clusters`, etc.) -- they were removed and there is no backward-compat shim.
144
+
122
145
## Adding a New Analysis
123
146
124
147
1. Create/extend a file in `src/mdpp/analysis/`.
@@ -130,15 +153,15 @@ Tests live in `tests/analysis/`, `tests/plots/`, and `tests/chem/`, mirroring th
130
153
131
154
## Compute Backend Conventions
132
155
133
-
**Default backend rule**: every public compute function that accepts a `backend=` argument MUST default to `"mdtraj"`. Other backends (`numba`, `torch`, `jax`, `cupy`) are performance options that callers must opt into explicitly.
156
+
**Default backend rule**: every public compute function that accepts a `backend=` argument MUST default to `"mdtraj"`**when mdtraj provides a native kernel for that computation**. Other backends (`numba`, `torch`, `jax`, `cupy`) are performance options that callers must opt into explicitly. The only current exception is `compute_dccm`, which defaults to `"numpy"` because mdtraj has no native covariance kernel -- `numpy`'s BLAS GEMM is multi-threaded and works without any optional dependency.
134
157
135
158
Reasons:
136
159
137
160
- Only `mdtraj` supports periodic boundary conditions -- defaulting to anything else would silently drop PBC for users who don't read the backend parameter.
138
-
- All analysis functions sharing the same default keeps API behavior consistent across `compute_distances`, `compute_rmsd_matrix`, `featurize_ca_distances`, etc.
161
+
- All PBC-relevant analysis functions sharing the same default keeps API behavior consistent across `compute_distances`, `compute_rmsd_matrix`, `featurize_ca_distances`, etc.
139
162
- The optional GPU backends (`[gpu]` extra) must never be required for the common path.
140
163
141
-
When reviewing or writing code, never change a public function's default backend away from `"mdtraj"`.
164
+
When reviewing or writing code, never silently change a public function's default backend away from its current value (`"mdtraj"`, or `"numpy"` for DCCM).
142
165
143
166
**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.
1. Decorate torch/cupy GPU kernels with `@clean_torch_cache` / `@clean_cupy_cache` from `_backends/_imports.py` so pooled memory is released in a `finally` block after the kernel runs. Do **not** apply any cleanup decorator to JAX kernels -- `jax.clear_caches()` trashes JIT compilation caches and forces slow recompiles.
183
206
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`).
184
207
1. Register in the module's `BackendRegistry` at the bottom of the file.
185
-
1. Add the backend name to the corresponding `Literal` alias (`DistanceBackend` / `RMSDBackend`) in `_backends/_registry.py`.
208
+
1. Add the backend name to the corresponding `Literal` alias (`DistanceBackend` / `RMSDBackend` / `DCCMBackend`) in `_backends/_registry.py`.
186
209
1. Add agreement tests in `tests/analysis/test_<kind>.py` guarded by the relevant `requires_*` skip marker and `@pytest.mark.gpu` (if GPU-only).
187
-
1.**Do not change the public function's default backend** -- keep it at `"mdtraj"`.
210
+
1.**Do not change the public function's default backend** -- keep `compute_distances` / `compute_rmsd_matrix` / `featurize_ca_distances` defaulting to `"mdtraj"` and `compute_dccm` defaulting to `"numpy"`.
188
211
189
212
## Adding a New Backend Registry
190
213
191
214
To introduce a registry for a new multi-backend compute function:
192
215
193
216
1. Create `src/mdpp/analysis/_backends/_<kind>.py` with a `Protocol` class defining the shared call signature.
194
-
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()`.
217
+
1. Declare the registry as `<kind>_backends: BackendRegistry[<Kind>BackendFn] = BackendRegistry(default="mdtraj")`(or another sensible no-optional-dep default if mdtraj has no kernel for that computation -- e.g. `_dccm.py` uses `default="numpy"`). Always parameterise with the Protocol so callers get typed `compute_fn` from `registry.get()`.
195
218
1. Add a `Literal` alias to `_backends/_registry.py` (`type <Kind>Backend = Literal["mdtraj", "numba", ...]`) and re-export it from `_backends/__init__.py`.
196
219
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.
0 commit comments