Skip to content

Commit 42ca222

Browse files
committed
docs(api): document surface-aware sampler in docs/
Three changes to keep docs/ in sync with the new sampler: - docs/space.md (new) — full per-module reference matching the existing chain.md / complex.md / structure.md / run.md pattern. Covers make_sampler, Sample, Cube/Sphere/Shell envelopes, Excluder, SurfaceSampler, compute_envelope_dims, NAngles, SamplingError, CLI/MawsRunner integration table, tuning notes, and a pointer to the visualization notebook. - docs/README.md — fix the stale "Typical Workflow" snippet that used cube.generator()[:3] / [3:6] / [6] indexing; updated to make_sampler('shell', ...) returning a Sample(position, axis, angle). - docs/run.md — add shape / reach / probe rows to the parameter table with a short note explaining the surface-aware sampling change and pointing to docs/space.md for the underlying API. (Note: docs/run.md still uses the old MAWSConfig / run_maws naming, which doesn't match the actual run.py code — that staleness predates this PR and is out of scope; flagged for a follow-up.)
1 parent a48c51f commit 42ca222

3 files changed

Lines changed: 217 additions & 5 deletions

File tree

docs/README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,14 +260,14 @@ aptamer = cpx.aptamer_chain()
260260
aptamer.create_sequence("G A U C")
261261
cpx.build()
262262

263-
# 5. Sample conformations
263+
# 5. Sample conformations with the surface-aware sampler
264264
import maws.space as space
265-
cube = space.Cube(20.0, center)
265+
sampler = space.make_sampler("shell", ligand_only_cpx) # auto-sized envelope + SAS rejection
266266

267267
for _ in range(1000):
268-
sample = cube.generator()
269-
cpx.translate_global(aptamer.element, sample[:3])
270-
cpx.rotate_global(aptamer.element, sample[3:6], sample[6])
268+
pose = sampler.generator() # Sample(position, axis, angle)
269+
cpx.translate_global(aptamer.element, pose.position)
270+
cpx.rotate_global(aptamer.element, pose.axis, pose.angle)
271271

272272
for i in range(4):
273273
aptamer.rotate_in_residue(0, i, random_angle)
@@ -278,6 +278,9 @@ for _ in range(1000):
278278
cpx.rebuild() # Reset for next sample
279279
```
280280

281+
See [docs/space.md](space.md) for the surface-aware sampler API (`make_sampler`,
282+
the envelope dataclasses, `Excluder`, `SurfaceSampler`).
283+
281284
---
282285

283286
## Coordinate Model

docs/run.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ config = MAWSConfig(
3939
| `remove_h` | `bool` | `False` | Remove hydrogens |
4040
| `drop_hetatm` | `bool` | `False` | Drop HETATM records |
4141
| `verbose` | `bool` | `True` | Log progress to console |
42+
| `shape` | `"cube"`, `"sphere"`, `"shell"` | `"shell"` | Surface-aware sampler envelope geometry |
43+
| `reach` | `float` | `10.0` | Distance the envelope extends past the ligand surface (Å) |
44+
| `probe` | `float` | `1.4` | vdW probe radius for SAS rejection (Å, water-equivalent) |
45+
46+
> **Note (2026):** the initial pose search now restricts samples to the
47+
> region just outside the ligand surface, computed automatically from
48+
> the ligand atoms. The three new parameters above (`shape`, `reach`,
49+
> `probe`) tune the envelope; defaults work for most targets. See
50+
> [docs/space.md](space.md) for the underlying API.
4251
4352
### MAWSResult
4453

docs/space.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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

Comments
 (0)