Backbone-only side-chain completion: scorable PoseStack from N/CA/C/O (follow-up to #465) - #380
Conversation
|
Current-code audit: the backbone-only use case is useful, but this implementation should not be merged as-is. It duplicates format-specific canonical-ordering/parameter caches now generalized by the Atom37/Biotite path in #465, always materializes float32 output, supports only a hard-coded protein subset, and hides a full score-function/Dunbrack/OptH side-chain packing operation inside what looks like a tensor conversion. The tests mostly assert shape/device rather than preservation of the supplied backbone and deterministic side-chain-completion semantics. The branch is conflicting and has no current CI. I recommend a small follow-up after #465: prepare topology once, make missing-side-chain completion an explicit policy/step, preserve input dtype/autograd where supported, and test CPU/CUDA equivalence plus repeated prepared builds. |
85d590e to
a0821ce
Compare
|
Thanks — reworked rather than patched, and rebased onto master now that #465 has landed. Duplication. Gone. Hidden packing. Now an explicit Two things the new tests turned up:
Not changed, for transparency: Two questions now that #465 is in:
|
a0821ce to
475ddae
Compare
Every structure-prediction adapter on master requires side-chain heavy atoms to already be present: pose_stack_from_canonical_form builds missing leaf atoms but rejects blocks missing non-leaf atoms. Backbone generators emit only N/CA/C/O, so today the only way to score one is a PDB round-trip. Add pose_stack_from_backbone_coords, which takes a bare (L, 4, 3) backbone plus integer residue types and returns a scorable PoseStack. Side-chain completion is an explicit sidechain_completion="pack"|"none" policy rather than a hidden step: "pack" runs a score-function-driven Dunbrack/OptH job and is documented as such, "none" does the conversion only and stays differentiable. Under both, the supplied backbone is returned bit-for-bit and stays on the autograd tape. Chemistry is reused from the atomworks adapter rather than duplicated: its parameter database, canonical ordering, packed block types and atom mappings already cover exactly this restype subset. Packing setup is held in a memoized PoseBuildContext so the score function and Dunbrack sampler are built once per device. A leaf atom whose ideal geometry reaches into the side chain cannot be placed from a bare backbone -- HA is built from CA/N/CB -- and leaf building runs before packing, so those atoms were left NaN on every non-GLY residue. Rebuild the pose once after packing, when the heavy atoms exist.
475ddae to
513609e
Compare
The gap
Every structure-prediction adapter on
masterrequires the side-chain heavyatoms to already be there.
pose_stack_from_canonical_formbuilds missingleaf atoms but rejects blocks missing non-leaf atoms
(
fail_on_missing_nonleaf_atoms=not return_block_has_missing_atoms), sopose_stack_from_openfold,pose_stack_from_atomworksandpose_stack_from_atom37_and_biotiteall reject backbone-only input. Onlypose_stack_from_biotitecompletes side chains, and it needs a caller-suppliedAtomArray.Backbone generators — RFdiffusion, mosaic, partial-diffusion and hallucination
loops — emit exactly N/CA/C/O and an integer sequence, and nothing else. Today
the only way to score one is to write a PDB and read it back.
This PR adds that one missing step: bare
(L, 4, 3)backbone plus residue-typeindices in, scorable
PoseStackout.API
sidechain_completionis a required decision, spelled out rather than implied:"pack"(default) runsbuild_missing_sidechains— a fullscore-function-driven Dunbrack + OptH packing job. The docstring says so at
the call site: expensive, dependent on beta2016 and the Dunbrack library, and
the rebuilt atoms are not a differentiable function of the input.
"none"does the coordinate conversion only. Absent side chains stayNaN, the result is not scorable, and the conversion is cheap and fully
differentiable — for callers that place side chains themselves or want to
batch the packing.
Under both modes the supplied backbone is returned bit-for-bit and stays on
the autograd tape: after packing, every finite input atom is routed back to the
canonical tensor through the same
_restore_canonical_input_coordsmechanismpose_stack_from_atom37_and_biotiteuses.Two things the new tests turned up
Backbone-only input leaves a NaN
HAon every non-GLY residue.HA'sinternal-coordinate frame is
CA / N / CB, sobuild_missing_leaf_atoms— whichruns before packing — cannot place it when CB does not exist yet, and the
packer only rebuilds heavy atoms and never revisits it. On ubiquitin that was 70
NaN atoms in the 70 blocks flagged as incomplete.
"pack"therefore rebuilds thepose once after packing, when the heavy atoms exist; that second pass costs
~0.00 s against ~0.70 s for the packing itself and deliberately does not pass
return_block_has_missing_atoms, so anything still absent raises rather thansilently returning NaN. This is not specific to this adapter — any backbone-only
input reaching
pose_stack_from_canonical_formhits it.Side-chain placement is not reproducible from a torch seed on CPU. The CPU
simulated annealer draws from libc
rand()(
tmol/pack/compiled/compiled.cpu.cpp, lines 29 / 109 / 220), whichtorch.manual_seeddoes not control; repeated packs of the same input differ byup to ~10 Å.
test_default_pose_builds_reuse_packing_setuppasses only becausea complete 1UBQ needs no Dunbrack packing and takes the OptH-only path.
That reaches topology too, not just coordinates: the HIS tautomer is resolved
from the packed side chain, so ubiquitin's His68 comes back as
HISon somebuilds and
HIS_Don others. The test here therefore asserts what is actuallyguaranteed — setup reuse, atom layout, and a bit-identical backbone — and says
why in the docstring, rather than asserting a reproducibility the library does
not currently provide. Happy to tighten it if you would rather seed the
annealer.
Reuse — no new format-specific caches
The chemistry is shared verbatim with the atomworks adapter, whose protein
subset is already exactly what a backbone needs:
_paramdb_for_atomworks/_restype_set_for_atomworks/canonical_ordering_for_atomworks/packed_block_types_for_atomworks_get_aw_2_tmol_mappingsfor the atom and residue-type tablesTwo facts make that reuse exact, and both are asserted in the tests rather than
assumed: AlphaFold2's
"ARNDCQEGHILKMFPSTWYV"is alphabetical by three-lettercode and therefore equals
ATOMWORKS_NAME3S[1:21], and N/CA/C/O occupy atom37slots
(0, 1, 2, 4)for all 20 canonical amino acids. Padding maps to theatomworks
<M>token, which the existing tables already send to tmol restype-1with no real atoms.The packing setup is held in a memoized
PoseBuildContext, so the scorefunction and Dunbrack sampler are built once per device across repeated calls,
not once per structure.
Tests
tmol/tests/io/test_pose_stack_from_backbone_coords.pytest_build_context_reuses_atomworks_chemistrytest_default_aa_order_matches_atomworks_protein_tokenstest_backbone_slots_are_uniform_across_restypestest_packing_preserves_the_supplied_backboneatol=0test_sidechains_are_built_and_no_nans_remaintest_gradients_reach_the_input_backbone_through_packingcoordsdespite the value-level packing steptest_completion_none_leaves_sidechains_absent"none"leaves NaN where"pack"fillstest_completion_none_is_differentiabletest_repeated_builds_are_stable_and_reuse_setuptest_cpu_and_cuda_agreetest_single_pose_input_is_unsqueezed,test_output_is_on_the_requested_device,test_padding_positions_are_excluded,test_two_chains_are_separatedtest_shape_validation,test_out_of_range_res_types_are_rejected,test_aa_order_rejects_non_canonical_and_repeats,test_invalid_completion_policy_is_rejectedChanges from the previous revision of this PR
master; thePoseStack.split/PDBInfo.splitcherry-pick is dropped now that Add code to split out a single Pose from a PoseStack #379 is merged (it was the source of the
conflicts).
_paramdb_for_backbone_coords,canonical_ordering_for_backbone_coordsand
packed_block_types_for_backbone_coordsin favour of the atomworks ones,and dropped the module's own backbone atom-name table in favour of
_get_aw_2_tmol_mappings. Public surface is down from five names to two.sidechain_completionargumentinstead of an unconditional hidden step.
device equivalence and setup reuse rather than shape and device alone. Those
tests are what caught the NaN
HA, which the previous revision shipped.Two notes on points raised in review that I did not change, for
transparency:
CanonicalForm.coordsisTensor[torch.float32]by contract, sothe float32 cast is the framework's, not this adapter's; and the protein-only
restype subset is the same one
pose_stack_from_atomworksandpose_stack_from_openfoldenforce. Happy to follow if either is generalizedrepo-wide.
Finally — if you'd rather this live inside
_pose_stack_from_atomworks.pyas asibling of
pose_stack_from_atomworks, or would rather relaxcanonical_form_from_atomworksto accept its own<M>mask token as padding(which would reduce this to a thin wrapper), say the word and I'll move it.