|
| 1 | +# Space API Reference |
| 2 | + |
| 3 | +`maws.space` — surface-aware sampling region for the MAWS aptamer-design loop. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +`maws.space` builds the geometric region from which MAWS draws candidate poses for the initial nucleotide. Samples are drawn from a user-chosen envelope (cube / sphere / shell) auto-sized to the ligand, and rejected if they fall inside the ligand bulk via a Bondi-vdW + SAS-probe check. |
| 8 | + |
| 9 | +The intended public entry point is the [`make_sampler`](#make_sampler) factory. It picks the envelope shape, sizes it from the ligand atoms, and wraps everything in a [`SurfaceSampler`](#surfacesampler) whose `.generator()` returns a [`Sample`](#sample) you pass to `Complex.translate_global` / `rotate_global`. |
| 10 | + |
| 11 | +```mermaid |
| 12 | +classDiagram |
| 13 | + direction LR |
| 14 | +
|
| 15 | + Sample <-- Cube : returns |
| 16 | + Sample <-- Sphere : returns |
| 17 | + Sample <-- Shell : returns |
| 18 | +
|
| 19 | + SurfaceSampler "1" *-- "1" Excluder : holds |
| 20 | + SurfaceSampler "1" o-- "1" Cube : envelope |
| 21 | + SurfaceSampler "1" o-- "1" Sphere : envelope |
| 22 | + SurfaceSampler "1" o-- "1" Shell : envelope |
| 23 | +
|
| 24 | + class Sample { |
| 25 | + +position |
| 26 | + +axis |
| 27 | + +angle |
| 28 | + } |
| 29 | + class Cube { |
| 30 | + +width |
| 31 | + +centre |
| 32 | + +generator() Sample |
| 33 | + } |
| 34 | + class Sphere { |
| 35 | + +radius |
| 36 | + +centre |
| 37 | + +generator() Sample |
| 38 | + } |
| 39 | + class Shell { |
| 40 | + +inner |
| 41 | + +outer |
| 42 | + +centre |
| 43 | + +generator() Sample |
| 44 | + } |
| 45 | + class Excluder { |
| 46 | + +is_clear(point) bool |
| 47 | + } |
| 48 | + class SurfaceSampler { |
| 49 | + +envelope |
| 50 | + +excluder |
| 51 | + +max_rejections |
| 52 | + +generator() Sample |
| 53 | + } |
| 54 | +``` |
| 55 | + |
| 56 | +## `make_sampler` |
| 57 | + |
| 58 | +```python |
| 59 | +make_sampler( |
| 60 | + shape: str, # "cube" | "sphere" | "shell" |
| 61 | + complex_obj: Complex, # built ligand-only Complex |
| 62 | + *, |
| 63 | + reach: float = 10.0, # Å beyond surface |
| 64 | + probe: float = 1.4, # Å, vdW probe (water-like) |
| 65 | +) -> SurfaceSampler |
| 66 | +``` |
| 67 | + |
| 68 | +The factory. Auto-sizes the envelope from `complex_obj.positions` + `complex_obj.topology.atoms()` and returns a ready-to-use `SurfaceSampler`. Raises `ValueError` for an unknown `shape`. |
| 69 | + |
| 70 | +```python |
| 71 | +import maws.space as space |
| 72 | + |
| 73 | +sampler = space.make_sampler("shell", ligand_only_complex) |
| 74 | +pose = sampler.generator() |
| 75 | +print(pose.position, pose.axis, pose.angle) |
| 76 | +``` |
| 77 | + |
| 78 | +## `Sample` |
| 79 | + |
| 80 | +```python |
| 81 | +@dataclass(frozen=True) |
| 82 | +class Sample: |
| 83 | + position: np.ndarray # shape (3,), Å |
| 84 | + axis: np.ndarray # shape (3,), unit vector |
| 85 | + angle: float # radians |
| 86 | +``` |
| 87 | + |
| 88 | +What every envelope and sampler returns from `.generator()`. Replaces the old 7-element ndarray with positional indexing. |
| 89 | + |
| 90 | +## Envelope dataclasses |
| 91 | + |
| 92 | +All three are `@dataclass(frozen=True)` with `.generator() -> Sample`. Their dimensions are normally computed by [`compute_envelope_dims`](#compute_envelope_dims) — call them directly only if you know what radii you want. |
| 93 | + |
| 94 | +### `Cube` |
| 95 | + |
| 96 | +| Attribute | Type | Description | |
| 97 | +|-----------|------|-------------| |
| 98 | +| `width` | `float` | Side length, Å | |
| 99 | +| `centre` | `np.ndarray` | 3D centre coordinate | |
| 100 | + |
| 101 | +Uniform sampling over the axis-aligned cube. Random rotation axis (unit) and angle in `[0, π]`. |
| 102 | + |
| 103 | +### `Sphere` |
| 104 | + |
| 105 | +| Attribute | Type | Description | |
| 106 | +|-----------|------|-------------| |
| 107 | +| `radius` | `float` | Sphere radius, Å | |
| 108 | +| `centre` | `np.ndarray` | 3D centre coordinate | |
| 109 | + |
| 110 | +Uniform-in-volume radial sampling (`r = R · u^(1/3)`), uniform direction on the sphere (`cos(ψ)` ∈ `[-1, 1]`). Rotation axis (unit) and angle in `[0, 2π]`. |
| 111 | + |
| 112 | +### `Shell` |
| 113 | + |
| 114 | +| Attribute | Type | Description | |
| 115 | +|-----------|------|-------------| |
| 116 | +| `inner` | `float` | Inner radius, Å | |
| 117 | +| `outer` | `float` | Outer radius, Å | |
| 118 | +| `centre` | `np.ndarray` | 3D centre coordinate | |
| 119 | + |
| 120 | +Volume-correct shell sampling (`r = (u·(R_out³ − R_in³) + R_in³)^(1/3)`), uniform direction. The default shape used by `make_sampler` — for non-globular ligands it hugs the surface much more tightly than a `Sphere`. |
| 121 | + |
| 122 | +## `Excluder` |
| 123 | + |
| 124 | +```python |
| 125 | +class Excluder: |
| 126 | + def __init__(self, complex_obj, probe: float = 1.4): ... |
| 127 | + def is_clear(self, point: np.ndarray) -> bool: ... |
| 128 | +``` |
| 129 | + |
| 130 | +KDTree-backed (scipy.spatial.KDTree) SAS-style rejection: `is_clear(point)` is `True` iff `point` lies outside `(vdW + probe)` of every atom in `complex_obj.topology`. vdW radii come from a built-in Bondi table; unknown elements fall back to a carbon-equivalent default (`1.70 Å`) and emit a `RuntimeWarning` once per process per unknown symbol. |
| 131 | + |
| 132 | +| Method | Description | |
| 133 | +|--------|-------------| |
| 134 | +| `is_clear(point)` | True iff `point` is outside every (vdW + probe) sphere | |
| 135 | + |
| 136 | +## `SurfaceSampler` |
| 137 | + |
| 138 | +```python |
| 139 | +@dataclass |
| 140 | +class SurfaceSampler: |
| 141 | + envelope: Envelope |
| 142 | + excluder: Excluder |
| 143 | + max_rejections: int = 1000 |
| 144 | + def generator(self) -> Sample: ... |
| 145 | +``` |
| 146 | + |
| 147 | +Composes one envelope + one excluder into a rejection sampler. `.generator()` draws from the envelope until the excluder accepts; raises `SamplingError` after `max_rejections` attempts if the envelope is fully buried. |
| 148 | + |
| 149 | +## `compute_envelope_dims` |
| 150 | + |
| 151 | +```python |
| 152 | +compute_envelope_dims(complex_obj, shape: str, reach: float) -> dict |
| 153 | +``` |
| 154 | + |
| 155 | +Returns the kwargs for the matching envelope dataclass. Useful in tests / notebooks. Internally: |
| 156 | + |
| 157 | +- Mass-weighted COM of the ligand (`maws.helpers.mass_weighted_center`). |
| 158 | +- `R_max` = farthest atom from COM; `R_min` = closest. |
| 159 | +- Per-shape formulas: |
| 160 | + |
| 161 | +| Shape | Returned dims | |
| 162 | +|-------|---------------| |
| 163 | +| `cube` | `{"width": 2·(R_max + reach), "centre": COM}` | |
| 164 | +| `sphere` | `{"radius": R_max + reach, "centre": COM}` | |
| 165 | +| `shell` | `{"inner": max(0, R_min − 5), "outer": R_max + reach, "centre": COM}` | |
| 166 | + |
| 167 | +## `NAngles` |
| 168 | + |
| 169 | +```python |
| 170 | +@dataclass(frozen=True) |
| 171 | +class NAngles: |
| 172 | + n: int |
| 173 | + def generator(self) -> np.ndarray: ... |
| 174 | +``` |
| 175 | + |
| 176 | +`N` independent angles in `[0, 2π)`. Used for in-residue backbone torsion rotations alongside the surface sampler. |
| 177 | + |
| 178 | +## `SamplingError` |
| 179 | + |
| 180 | +`RuntimeError` subclass. Raised by `SurfaceSampler.generator()` when no clear point can be drawn within `max_rejections` attempts — typically a misconfigured envelope (too small `reach`, too large `probe`, or ligand fully filling the bounding box). |
| 181 | + |
| 182 | +## CLI / `MawsRunner` integration |
| 183 | + |
| 184 | +The same parameters are exposed through both interfaces, with identical defaults: |
| 185 | + |
| 186 | +| Flag (CLI) | Kwarg (`MawsRunner`) | Default | Meaning | |
| 187 | +|-----------|----------------------|---------|---------| |
| 188 | +| `--shape {cube,sphere,shell}` | `shape="shell"` | `"shell"` | Envelope geometry | |
| 189 | +| `--reach FLOAT` | `reach=10.0` | `10.0` | Å beyond surface | |
| 190 | +| `--probe FLOAT` | `probe=1.4` | `1.4` | vdW probe (Å, water-like) | |
| 191 | + |
| 192 | +See [docs/run.md](run.md) for the runner-level table. |
| 193 | + |
| 194 | +## Tuning notes |
| 195 | + |
| 196 | +- **Defaults are right for most targets.** Tweak only if rejection rates are extreme or if you hit a `SamplingError`. |
| 197 | +- **`reach`** is the *outer* boundary — too small starves the sampler, too large wastes compute on solvent. Default `10 Å` ≈ one nucleotide of slack. |
| 198 | +- **`probe`** is the *inner* boundary — `1.4 Å` is the water-equivalent SAS probe used by Chimera, PyMOL, FreeSASA. Larger → conservative (only big pockets accessible); smaller → permissive. |
| 199 | + |
| 200 | +For a visual walkthrough on a real PDB see [`notebooks/maws/space_analysis.ipynb`](../notebooks/maws/space_analysis.ipynb). |
0 commit comments