Rustina · torsion_field · go/no-go
Decides whether the SE(3) pose-state reformulation of the torsion search is viable before any of it is built. Executed 2026-08-16 after three defects in the first run were corrected. Every gate reports in the safe direction: a pass is never optimistic.
The reformulation is viable, and the binding constraint was never the rotation
grid. Quantization error is dominated by translation lattice spacing,
not by N_rot. Refining spacing from 0.5 Å to 0.2 Å at a fixed 2,048
rotations moves the G2 ratio from 0.587 to 0.237 — through the 0.25
pass threshold — where sixteen-fold more rotations at 0.375 Å had not.
| Spacing | d_g (depth ≥3) | h/2 floor | |ΔE| | ratio |
|---|---|---|---|---|
| 0.500 Å | 0.2783 Å | 0.250 | 0.645 | 0.587 |
| 0.375 Å | 0.2183 Å | 0.188 | 0.475 | 0.394 |
| 0.250 Å | 0.1601 Å | 0.125 | 0.335 | 0.276 |
| 0.200 Å | 0.1367 Å | 0.100 | 0.279 | 0.237 |
N_rot = 2048, 60 targets × 64 sampled poses. d_g tracks the analytic
translation floor √3·h/√12 = h/2 across the whole sweep, which is what
confirms the measurement is real rather than an artefact.
Two levers, two distinct problems. Spacing fixes the typical case — median regret at 8,192 rotations and 0.25 Å is 0.052 kcal/mol and median RMSD is 0.000 Å. It does not fix the tail: P95 regret is still 10.5 kcal/mol and P95 RMSD 3.18 Å, with geometric recovery at 53.4%. The distribution is bimodal — about half the targets are recovered essentially exactly and roughly a tenth are catastrophically wrong. Only top-M retention addresses that, and the data says it will: the correct tuple sits in the true top 5 for 73.3% of targets while greedy argmin captures 44.0%.
The reformulation replaces the DP state τ_path(g) with the fragment pose
M_g ∈ SE(3), collapsing K^depth to |grid| × K per edge.
It only works if the discrete SE(3) grid can resolve torsional decisions. This gate
measures whether it can, and at what grid resolution — which in turn sets the memory
budget, which is the other thing that can kill the design.
Three outcomes, decided by the table at the end: build the global pose-DP as specified; build it with root-outward branch-and-bound instead of materialised messages; or abandon the global formulation and keep pose-DP only for the shallow levels where fragments are large.
Correction to the original framing
I previously proposed reconstructing the molecule through the chain of snapped poses and
measuring RMSD accumulation. That tests the wrong thing. If the DP stores the argmin
torsion index at each transition and recovers τ* by traceback, the output
conformer is rebuilt by exact forward kinematics from real torsion values. Bond
lengths, angles, and planarity are therefore exact by construction, and geometric error
does not accumulate at all.
Quantization instead causes decision error — the DP selecting the wrong member of the discrete torsion set because two distinct poses aliased to the same grid cell. That is what the gate must measure, and it is a much better-posed question. It also means PoseBusters validity is structural rather than something the gate has to establish: the search cannot emit a molecule that isn't one.
Write snap(M) for nearest-neighbour projection onto the grid: rotation to the
nearest Super-Fibonacci quaternion, translation to the nearest lattice point. For
N rotations the covering radius is
ε ≈ (6π/N)^⅓ radians, and an atom at distance r from its
fragment's frame origin is displaced by roughly r·ε.
Those numbers are not reassuring, which is the point:
| N_rot | ε (worst) | d @ r=2 Å | d @ r=3 Å | d @ r=5 Å | A_g / fragment |
|---|---|---|---|---|---|
| 512 | 19.1° | 0.67 Å | 1.00 Å | 1.66 Å | 0.61 GB |
| 2048 | 12.0° | 0.42 Å | 0.63 Å | 1.05 Å | 2.46 GB |
| 8192 | 7.6° | 0.26 Å | 0.40 Å | 0.66 Å | 9.83 GB |
| 32768 | 4.8° | 0.17 Å | 0.25 Å | 0.42 Å | 39.3 GB |
Memory column assumes a 25 Å box at 0.375 Å spacing (67³ ≈ 300k translations) × N_rot × 4 B, for one fragment. Mean nearest-neighbour error runs ≈ 0.65× the covering radius; translation snapping adds ≈ 0.16 Å RMS at 0.375 Å spacing.
Two conclusions fall straight out. First, at today's 2048-rotation grid a mid-sized fragment
carries ~0.4–0.6 Å of pose error, which near a steric wall is worth more than 1 kcal/mol —
comfortably enough to flip a torsion decision. Second, materialising
A_g over the full grid is impossible: eight fragments at N_rot = 2048
is ~20 GB. Memory is a co-equal risk with quantization, and the gate measures both.
There is one structural reason for optimism, and G1 exists to confirm it: fragment
radius shrinks with depth. The root fragment is large but is never composed — it uses
the exact enumerated rotation. Deep fragments inherit compounded snapping error but are
small, so r·ε stays bounded. If measured error falls with depth rather than
rising, the formulation survives.
Numbered in dependency order — G1 gates G2, G2 gates G3. G4 is independent and can run in parallel; it decides the ring/macrocycle claim on its own.
snap()
~1 day
Nothing downstream can run without a canonical SE(3) grid. Grep found no
fibonacci or UnitQuaternion symbol in src/, so the
rotation set is either inside examples/correlation_dock.rs (149 KB, the
driver) or generated inline. Find it first — the gate must snap to the
same rotations the production engine enumerates, or it measures nothing.
Then add src/se3_grid.rs:
pub struct Se3Grid {
rotations: Vec<UnitQuaternion<f32>>, // Super-Fibonacci, N configurable
origin: Point3<f32>,
spacing: f32,
dim: (usize, usize, usize),
}
impl Se3Grid {
/// Nearest rotation index by |q·q_i| (double-cover aware).
pub fn snap_rotation(&self, q: &UnitQuaternion<f32>) -> usize;
pub fn snap_translation(&self, t: &Point3<f32>) -> (usize, usize, usize);
/// Full pose snap + the residual it discarded.
pub fn snap(&self, m: &Isometry3<f32>) -> (Se3Index, PoseResidual);
}
|q·qᵢ|, not q·qᵢ. Missing this
silently doubles measured rotation error and is the single most likely bug in the
whole gate.
The cheapest measurement and the one that can kill the design fastest. No grids, no
scoring, no docking — exactly the philosophy torsion_reach.rs already
establishes.
For each ligand at its native conformer:
let tf = tree.compute_transforms(&state).
tf[g] is M_g — the API already exists.TorsionPlan::with_ring_policy(...).x_i^loc = tf[g].inverse() * coords[i] for
i ∈ node_atom_ranges[g].d_g = rms_i | snap(M_g)·x_i^loc − M_g·x_i^loc |.d_g against depth(g), fragment radius of gyration,
and atom count.examples/pose_quantization_gate.rs--ligand L.pdbqt --n-rot 2048 --spacing 0.375 --depth 6d_g; whole-ligand RMS over all fragments;
scatter of d_g against r_g to confirm the
d ≈ r·ε model holds empirically.h/2 almost immediately and further rotations buy
nothing.State::new. The zero state makes
every node transform the identity, so all fragments snap one single pose and the
measurement never samples SO(3) — the first run's non-monotonicity in N_rot came
from exactly this. Use ≥ 64 sampled states per target.d_g at depth ≥ 3 stays below
0.35 Å, and d_g is flat or falling with depth.
Fail if d_g grows with depth. A non-monotonic response
to N_rot is not a finding — it is proof of a bug, since a finer covering cannot
increase expected snapping error.
h/2 floor to within a few
percent.
G1 measures geometry; this converts it into the currency that decides the search.
Reuse PreparedMaps::from_grids and the existing correlation scoring so the
numbers are in the engine's own units, not a proxy.
For each fragment: ΔΦ_g = Φ_g(snap(M_g)) − Φ_g(M_g), scoring
g's atoms against the receptor maps at both poses. Total
ΔE = Σ_g ΔΦ_g.
The number is meaningless in isolation, so measure the comparator in the same run: the
decision gap, the energy spread between the best and second-best
torsion tuple in the discrete set. Snapping is harmless when
ΔE ≪ gap and fatal when it is comparable.
--receptor R.pdbqt --mapsΔE (median, p95) in kcal/mol; distribution of the
decision gap; the ratio ΔE / gap, which is the actual gate quantity.MapTransform::Raw for the headline number. The
Curl fold applies at node resolution and would confound grid error
with curl error — measure it separately, not by default.ΔE / gap < 0.25.
Conditional at 0.25–0.6 — recoverable by keeping the top-M poses per
fragment instead of the argmin. Fail above 0.6: snapping noise is the
same size as the signal and the DP is choosing torsions by coin flip.
The only end-to-end test, and the one that actually decides the programme. It needs a throwaway pose-DP — no FFT, no optimisation, correctness only — so that it can be compared against exhaustive ground truth.
Restrict to ligands with ≤ 6 free torsions at K ≤ 4 (≤ 4096 tuples), which makes brute force trivial.
τ ∈ Ω^n. Exact forward_kinematics, exact
E_inter at true coordinates — no snapping anywhere.
Keep the full ordering, not just the argmin.(child, parent grid pose), traceback to τ*.τ*;
(b) rank of τ* in the true ordering;
(c) energy regret E(τ*) − E(τ_opt);
(d) heavy-atom RMSD between the two conformers.examples/pose_dp_fidelity.rsDo not gate on tuple identity. Symmetric substituents — a para-phenyl flipped 180°, for one — give a different tuple and a geometrically identical molecule, so exact-match and rank penalise the DP for decisions carrying no physical difference. Report them as diagnostics only. Geometric recovery is what refinement consumes.
And gate on the tail, not the median. A median hides a catastrophic minority, which is precisely the failure mode that matters in production.
Independent of the pose reformulation and worth running first, because it can invalidate
the PoseBusters claim on its own. torsion_field.rs:105-119 records that
0 of 36 macrocycle closure pairs have both ends on a common root path,
which is why the current field tears rings open and why RingPolicy::Frozen
exists. Pose-space closure fixes the factorisation — but only if the discrete
offset set Ω contains a conformer that actually closes the ring.
"Can any member of Ω^K close the ring?" is vacuous. Offset zero is always in the set and offsets are applied relative to the input torsion, so member zero is the input conformer — whose glue atoms coincide by construction. Any test that accepts on a best-over-trials basis reports 100% at every K and measures nothing.
The question RingPolicy actually poses is a different one:
what fraction of Ω^K leaves the ring intact? The field minimises over
the whole product space, so if intact conformers are a vanishing fraction of it,
the minimum lands on a torn ring however many closed members exist.
tree.ring_torsions and the Meeko glue atoms.Ω^K from the input
conformer; count members satisfying closure. That fraction is the headline number.K ∈ {3, 6, 12, 24}.examples/ring_closure_reach.rsRingPolicy docstring spans both benchmark sets.
This is a sharper statement than "the ring claim is dead." Uniform offsets are not
unable to represent closure — they contain closed members. They are unable to
make closure anything but a measure-zero event inside the space the field searches.
RingPolicy::Frozen is therefore correct as a stopgap, and the finding
motivates the pose-space closure constraint rather than killing it: constraining
the DP to the closed sub-manifold is exactly the way to stop searching a space that is
99.6%+ broken molecules.
The table above says naive materialisation costs ~20 GB. This gate measures whether the
reachable-set restriction rescues it. Fragment g's frame origin is confined
to a ball of radius L_g — the chain length from root to g —
around the root translation, so A_g only needs support on that ball, not the
whole box.
Measure L_g across the benchmark ligands and compute true support sizes. A
5 Å ball at 0.375 Å spacing is ~2,500 lattice points against 300k for the full box — a
120× reduction, if it holds.
L_g by depth; resulting per-fragment and total
footprint at each N_rot; the crossover N_rot where total exceeds 8 GB.A_g depend on the root pose and the DP must rerun per
root pose. Quantify that cost here — it is the difference between materialised
messages and root-outward branch-and-bound.A_g.
All four gates draw from scratch/rustina-spindr/manifest-test.json (225 targets,
already carrying ligand_pdbqt, native_sdf, start_sdf,
and center). Three cohorts:
| Cohort | Selection | Size | Used by |
|---|---|---|---|
| Full | all test targets | 225 | G1, G2, G5 |
| Tractable | ≤ 6 free torsions after eligible_nodes | ~60–80 | G3 |
| Macrocyclic | the 36 archived closure pairs | 36 | G4 |
Use native conformers throughout, not ETKDG starts. The gate asks whether the representation can hold the right answer; starting from a wrong conformer confounds representability with sampling and makes a failure unattributable.
| Outcome | Condition | Action |
|---|---|---|
| G1 pass | 0.137 Å at 0.2 Å spacing; falls with depth; monotone | Centroid-centred fragment frames are mandatory and sufficient. Rotation lever arm r_g ≈ 0.7–1.3 Å. |
| G2 pass | ratio 0.237 at 0.2 Å / N_rot 2048 | Spend budget on spacing, not rotations. N_rot 2048 suffices; 32768 was 16× the cost for under 10% of the benefit. |
| G3 tail fail | median 0.052 kcal/mol but P95 10.49; recovery 53.4% | Top-M retention is required, not optional. M = 4–8 per fragment. Top-5 capture 73.3% vs argmin 44.0% says the branch is there to be kept. |
| G4 fail | intact fraction ≤0.005% of Ω^K at K ≥ 12 | Ship RingPolicy::Frozen for macrocycles. Uniform offsets make closure measure-zero — this is the case for a pose-space closure constraint, not against rings. |
| G5 pass | reachable ball ≈ 4.3 GB at N_rot 2048 / 0.25 Å | Never materialise A_g over the full box. Finer spacing costs memory as h⁻³, but dropping N_rot 32768→2048 more than pays for it. |
N_rot = 2048, spacing 0.25 Å, top-M = 4–8, rings frozen. That fits ~4.3 GB under the reachable-ball restriction, puts the G2 ratio at 0.276, and leaves the residual jitter to top-M rather than to brute-force resolution. It is sixteen times cheaper in rotations than the operating point the first run pointed at, because the first run was pushing the axis that wasn't binding.
Build root-outward branch-and-bound with the existing rigid field from
correlate.rs as an admissible bound — erosion.rs already supplies
the erosion machinery for exactly this. The formulation stays linear in torsion count at
O(n·M·K).
se3_grid.rs, unit-test the double cover.q·qᵢ rather than
|q·qᵢ| doubles apparent rotation error and would fail G1 spuriously. Guard
with the G0 unit test.compute_transforms returns
node transforms in the kinematic tree's own convention. If plan nodes contract several
kinematic nodes, M_g must be the transform of the owning node.
Getting this wrong inflates r_g and pessimises everything downstream.torsion_reach.rs is index-wise rather than
symmetry-corrected and is pessimistic for symmetric ligands. Keep that convention for
consistency; it errs safe.