Rustina · torsion_field · go/no-go

Pose-Quantization Gate


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.

Status executed — verdict below Target src/torsion_field.rs CI 271 tests, fmt + clippy clean

Result

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.

Spacingd_g (depth ≥3)h/2 floor|ΔE|ratio
0.500 Å0.2783 Å0.2500.6450.587
0.375 Å0.2183 Å0.1880.4750.394
0.250 Å0.1601 Å0.1250.3350.276
0.200 Å0.1367 Å0.1000.2790.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%.

What this gate decides

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.

The error model, and why the gate is needed

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
51219.1°0.67 Å1.00 Å1.66 Å0.61 GB
204812.0°0.42 Å0.63 Å1.05 Å2.46 GB
81927.6°0.26 Å0.40 Å0.66 Å9.83 GB
327684.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.

The gates

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.

G0 Prerequisites — locate the rotation set, build 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);
}
Correctness
Unit test: snapping an exact grid pose returns zero residual. Snapping 10⁵ random poses gives a max angular residual within 15% of the analytic covering radius — if it exceeds that, the rotation set is not well-distributed and every downstream number is pessimistic for the wrong reason.
Double cover
Compare |q·qᵢ|, not q·qᵢ. Missing this silently doubles measured rotation error and is the single most likely bug in the whole gate.
G1 Pose-snap displacement versus depth ~1 day · no receptor

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:

  1. Build the tree and let tf = tree.compute_transforms(&state). tf[g] is M_g — the API already exists.
  2. Contract onto plan nodes with TorsionPlan::with_ring_policy(...).
  3. Local coordinates: x_i^loc = tf[g].inverse() * coords[i] for i ∈ node_atom_ranges[g].
  4. Displacement d_g = rms_i | snap(M_g)·x_i^loc − M_g·x_i^loc |.
  5. Record d_g against depth(g), fragment radius of gyration, and atom count.
File
examples/pose_quantization_gate.rs
Invocation
--ligand L.pdbqt --n-rot 2048 --spacing 0.375 --depth 6
Reports
Per-depth median and p95 of d_g; whole-ligand RMS over all fragments; scatter of d_g against r_g to confirm the d ≈ r·ε model holds empirically.
Sweep
Spacing is the primary axis, not N_rot: sweep spacing ∈ {0.5, 0.375, 0.25, 0.2} at fixed N_rot = 2048 first, then N_rot only if the spacing sweep leaves a gap. Once fragment frames are centroid-centred the rotation lever arm is r_g ≈ 0.7–1.3 Å, so rotation error falls below the translation floor h/2 almost immediately and further rotations buy nothing.
Sampling
Never evaluate at 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.
Gate Pass if median 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.
Measured PASS. 0.1367 Å at 0.2 Å spacing, 0.2183 Å at 0.375 Å; falling with depth; monotone in both axes. Matches the h/2 floor to within a few percent.
G2 Energy error induced by snapping ~1 day · needs receptor grids

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.

File
same binary as G1, behind --receptor R.pdbqt --maps
Reports
Distribution of ΔE (median, p95) in kcal/mol; distribution of the decision gap; the ratio ΔE / gap, which is the actual gate quantity.
Careful
Score with 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.
Gate Pass if median Δ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.
Measured PASS at 0.2 Å (ratio 0.237), conditional at 0.25 Å (0.276), and failing at 0.5 Å (0.587) — all at N_rot = 2048. The ratio is set almost entirely by spacing; sweeping N_rot at fixed 0.375 Å moves it by under 10%.
G3 Decision fidelity against brute force ~2 days · decisive

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.

Ground truth
Enumerate every τ ∈ Ω^n. Exact forward_kinematics, exact E_inter at true coordinates — no snapping anywhere. Keep the full ordering, not just the argmin.
Under test
Reference pose-DP with snapping at every transition, argmin torsion index stored per (child, parent grid pose), traceback to τ*.
Metrics
(a) exact-match rate of τ*; (b) rank of τ* in the true ordering; (c) energy regret E(τ*) − E(τ_opt); (d) heavy-atom RMSD between the two conformers.
File
examples/pose_dp_fidelity.rs

Do 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.

Gate Pass if median regret < 0.5 kcal/mol, P95 regret < 3.0 kcal/mol, and geometric recovery (RMSD < 0.25 Å) reaches ≥ 80%. Fail if median regret exceeds 1.5 kcal/mol or P95 regret exceeds 10 kcal/mol.
Measured FAIL on the tail (N_rot 8192, spacing 0.25, 116 targets). Median regret 0.052 kcal/mol and median RMSD 0.000 Å — excellent. But P95 regret 10.49 kcal/mol, P95 RMSD 3.18 Å, geometric recovery 53.4%. Bimodal: about half exact, roughly a tenth catastrophic. Top-5 capture is 73.3% against 44.0% for greedy argmin, so the correct branch is usually present but not ranked first — which is the quantitative case for top-M retention.
G4 Ring-closure representability ~1 day · independent

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.

  1. Identify closure pairs via tree.ring_torsions and the Meeko glue atoms.
  2. Enumerate (or systematically sample up to 20k of) Ω^K from the input conformer; count members satisfying closure. That fraction is the headline number.
  3. Separately, measure reachability from perturbed starts — excluding the untouched input conformer, which closes trivially.
  4. Sweep K ∈ {3, 6, 12, 24}.
File
examples/ring_closure_reach.rs
Monotonicity
Required. Ω(K′) ⊆ Ω(K) whenever K′ divides K, so results must be non-decreasing in K. Enforce it by carrying each pair's best solution forward as a seed for the next K. A decreasing pass rate is proof of search failure, never of representability failure — the first run's 62.5% → 12.5% drop came from an exhaustive/heuristic switch whose threshold depended on K.
Epistemics
The search is heuristic above the exhaustive cap, so a FAIL bounds representability from below and is never conclusive on its own. Only a PASS is.
Data
8 macrocyclic targets / 16 closure pairs in the SPINDR test split. The "36 pairs" in the RingPolicy docstring spans both benchmark sets.
Measured FAIL — and decisively. Intact fraction of Ω^K: median 0.41% at K=3, 0.030% at K=6, and ≤0.005% at K≥12 — the sampling floor of 1/20,000, meaning only the input conformer itself survives. Reachability from perturbed starts: 0% at K ≤ 12, 12.5% at K = 24, with median bond deviation falling monotonically 0.483 → 0.202 → 0.126 → 0.073 Å.

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.

G5 Memory and reachable-set cost model ~1 day · analysis only

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.

Reports
Distribution of L_g by depth; resulting per-fragment and total footprint at each N_rot; the crossover N_rot where total exceeds 8 GB.
Watch
The restriction is relative to the root translation, so a relative-frame formulation makes 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.
Gate Pass if total footprint fits 8 GB at the N_rot that G2 and G3 require. Otherwise the design pivots to root-outward branch-and-bound with the existing rigid field as an admissible bound — still pseudo-exhaustive, never materialising A_g.

Data selection

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:

CohortSelectionSizeUsed by
Fullall test targets225G1, G2, G5
Tractable≤ 6 free torsions after eligible_nodes~60–80G3
Macrocyclicthe 36 archived closure pairs36G4

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.

Decision table

OutcomeConditionAction
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.

Recommended operating point

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).

Sequencing

Day 1G0 — find the rotation set, write se3_grid.rs, unit-test the double cover.
Day 1–2G4 in parallel — independent, and can invalidate the ring claim before anything else is built.
Day 2G1 — displacement versus depth. First real kill point.
Day 3G2 — energy error and decision gap.
Day 4–5G3 — reference pose-DP against brute force. Decisive.
Day 5G5 — cost model, folded in from G1 instrumentation.
Day 6Write up against the decision table; record measurements in CLAUDE.md.

Risks