Feature Extraction
Transformers
PyTorch
Safetensors
boltz2_automodel
protein-language-model
fastplms
custom_code
Instructions to use Synthyra/Boltz2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Synthyra/Boltz2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="Synthyra/Boltz2", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Synthyra/Boltz2", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Update FastPLMs runtime and model cards
Browse filesAdd-only FastPLMs files-only publication. Checkpoint weights and complete-artifact attestations are unchanged.
- README.md +70 -29
- config.json +5 -5
- fastplms/__init__.py +1 -0
- fastplms/models/__init__.py +1 -0
- fastplms/models/boltz/__init__.py +1 -0
- fastplms/models/boltz/_pair_attention.py +14 -10
- fastplms/models/boltz/cif_writer.py +28 -22
- fastplms/models/boltz/minimal_featurizer.py +175 -111
- fastplms/models/boltz/modeling_boltz2.py +131 -104
- fastplms/models/boltz/vb_layers_attention.py +22 -17
- fastplms/models/boltz/vb_layers_attentionv2.py +11 -8
- fastplms/models/boltz/vb_layers_confidence_utils.py +104 -58
- fastplms/models/boltz/vb_layers_dropout.py +4 -2
- fastplms/models/boltz/vb_layers_initialize.py +13 -2
- fastplms/models/boltz/vb_layers_outer_product_mean.py +36 -20
- fastplms/models/boltz/vb_layers_pair_averaging.py +45 -25
- fastplms/models/boltz/vb_layers_pairformer.py +36 -24
- fastplms/models/boltz/vb_layers_transition.py +17 -8
- fastplms/models/boltz/vb_layers_triangular_mult.py +15 -9
- fastplms/models/boltz/vb_loss_diffusionv2.py +40 -33
- fastplms/models/boltz/vb_modules_diffusion_conditioning.py +15 -7
- fastplms/models/boltz/vb_modules_encodersv2.py +174 -104
- fastplms/models/boltz/vb_modules_transformersv2.py +54 -32
- fastplms/models/boltz/vb_modules_trunkv2.py +110 -82
- fastplms/models/boltz/vb_modules_utils.py +64 -39
- fastplms/models/boltz/vb_potentials_potentials.py +215 -165
- fastplms/models/boltz/vb_tri_attn_attention.py +20 -15
- fastplms/models/boltz/vb_tri_attn_primitives.py +57 -29
- fastplms/models/boltz/vb_tri_attn_utils.py +36 -20
- fastplms/registry.py +45 -44
- fastplms/runtime.py +1 -0
- fastplms_bundle.py +0 -0
- modeling_fastplms.py +26 -70
- requirements.txt +20 -0
- runtime-attestation.json +39 -38
README.md
CHANGED
|
@@ -16,16 +16,35 @@ Accepted inputs are raw amino-acid sequences through the convenience API, or
|
|
| 16 |
prepared model features.
|
| 17 |
Supported Transformers entry points are `AutoConfig`, `AutoModel`.
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
## Install and platform requirements
|
| 20 |
|
| 21 |
-
Install the
|
| 22 |
|
| 23 |
```bash
|
| 24 |
-
python -m pip install \
|
| 25 |
-
"
|
| 26 |
```
|
| 27 |
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
| 29 |
access on first download. For an air-gapped run, first build the manifest-pinned
|
| 30 |
local artifact and use the offline form shown in the example.
|
| 31 |
|
|
@@ -38,22 +57,50 @@ model_id = "Synthyra/Boltz2"
|
|
| 38 |
model = AutoModel.from_pretrained(
|
| 39 |
model_id,
|
| 40 |
trust_remote_code=True,
|
|
|
|
| 41 |
).eval()
|
| 42 |
```
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
## Protein structure prediction
|
| 59 |
|
|
@@ -100,7 +147,7 @@ continues independently of the ESM++ and ESMFold2 release gates.
|
|
| 100 |
- Precision policies: `default`
|
| 101 |
- BF16 execution: `fp32_parameters_autocast`
|
| 102 |
- Generation contract: `not_applicable`
|
| 103 |
-
-
|
| 104 |
- Weight publication allowed: `true`
|
| 105 |
- Weight license status: `resolved`
|
| 106 |
- Redistributable: `true`
|
|
@@ -111,28 +158,22 @@ continues independently of the ESM++ and ESMFold2 release gates.
|
|
| 111 |
- FastPLMs weights: `Synthyra/Boltz2`
|
| 112 |
- Runtime revision: recorded separately in the built artifact and published commit
|
| 113 |
- Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
|
| 114 |
-
- Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
|
| 115 |
- Official checkpoint: `boltz-community/boltz-2`
|
| 116 |
- Artifact source: `fast`
|
| 117 |
- State transform: `boltz2_inference_core_v1`
|
| 118 |
-
- BF16 execution: `fp32_parameters_autocast`
|
| 119 |
- Pinned upstreams: `boltz`
|
| 120 |
-
- Reference container: `reference-boltz2`
|
| 121 |
- Release tiers: `structure`, `artifact`, `benchmark`
|
| 122 |
- Unresolved required file identities: `0`
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
release blocker.
|
| 127 |
|
| 128 |
## Validation boundary
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
passed, that one backend is faster, or that an output has biological or
|
| 135 |
-
therapeutic validity.
|
| 136 |
|
| 137 |
## License
|
| 138 |
|
|
|
|
| 16 |
prepared model features.
|
| 17 |
Supported Transformers entry points are `AutoConfig`, `AutoModel`.
|
| 18 |
|
| 19 |
+
## Capabilities
|
| 20 |
+
|
| 21 |
+
| Feature | Status |
|
| 22 |
+
| --- | --- |
|
| 23 |
+
| Sequence classification | Unavailable: no advertised AutoClass |
|
| 24 |
+
| Token classification | Unavailable: no advertised AutoClass |
|
| 25 |
+
| PEFT fine-tuning | Supported pattern: attach LoRA to the pretrained model |
|
| 26 |
+
| Embeddings | Unavailable for this structure-only checkpoint |
|
| 27 |
+
| Test-time training | Unavailable for this inference-only checkpoint |
|
| 28 |
+
| Attention variants | Supported: `eager` |
|
| 29 |
+
| Compliance | Unavailable: this provisional family has no compliance tier |
|
| 30 |
+
|
| 31 |
+
A supported interface is not a pretrained downstream predictor. Classification
|
| 32 |
+
heads start untrained, and declared compliance metadata is not a claim that an
|
| 33 |
+
arbitrary local build passed its release gate.
|
| 34 |
+
|
| 35 |
## Install and platform requirements
|
| 36 |
|
| 37 |
+
Install the direct dependencies published with this model:
|
| 38 |
|
| 39 |
```bash
|
| 40 |
+
python -m pip install -r \
|
| 41 |
+
"https://huggingface.co/Synthyra/Boltz2/resolve/main/requirements.txt"
|
| 42 |
```
|
| 43 |
|
| 44 |
+
The FastPLMs implementation itself is embedded in the model repository and loaded
|
| 45 |
+
by Transformers through `trust_remote_code=True`.
|
| 46 |
+
|
| 47 |
+
Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. The artifact requirements include the direct structure dependencies. The published execution contract requires a CUDA device. The current validated release target is the exact NVIDIA GH200 on Linux aarch64; Linux x86-64, CPU-only, Windows, and macOS structure runs are not current release evidence. The Hub quick start below requires network
|
| 48 |
access on first download. For an air-gapped run, first build the manifest-pinned
|
| 49 |
local artifact and use the offline form shown in the example.
|
| 50 |
|
|
|
|
| 57 |
model = AutoModel.from_pretrained(
|
| 58 |
model_id,
|
| 59 |
trust_remote_code=True,
|
| 60 |
+
attn_implementation="eager",
|
| 61 |
).eval()
|
| 62 |
```
|
| 63 |
|
| 64 |
+
For offline validation, replace `model_id` with the manifest-built
|
| 65 |
+
`dist/hub/Boltz2` path and pass `local_files_only=True`.
|
| 66 |
+
|
| 67 |
+
## Attention and compliance
|
| 68 |
+
|
| 69 |
+
The quick start selects `eager` explicitly. Declared variants are `eager`. An unavailable requested backend raises instead
|
| 70 |
+
of silently switching implementations.
|
| 71 |
+
`output_attentions=True` may use the documented, one-call eager fallback solely
|
| 72 |
+
to materialize attention tensors; the configured backend remains unchanged.
|
| 73 |
|
| 74 |
+
This family does not declare the `compliance` tier. Boltz2 remains provisional
|
| 75 |
+
and its structure checks must not be broadened into parity claims.
|
| 76 |
+
|
| 77 |
+
## PEFT fine-tuning
|
| 78 |
+
|
| 79 |
+
Install the direct training dependencies, then attach LoRA to the loaded checkpoint:
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
python -m pip install "datasets>=4.8,<5" "peft>=0.19,<0.20"
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
from peft import LoraConfig, get_peft_model
|
| 87 |
+
|
| 88 |
+
peft_model = get_peft_model(
|
| 89 |
+
model,
|
| 90 |
+
LoraConfig(
|
| 91 |
+
r=8,
|
| 92 |
+
lora_alpha=16,
|
| 93 |
+
target_modules="all-linear",
|
| 94 |
+
),
|
| 95 |
+
)
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
This checkpoint has no advertised classifier. Supply the task-specific
|
| 99 |
+
objective and preserve any new head through `modules_to_save`.
|
| 100 |
+
All FastPLMs checkpoints follow the Transformers `PreTrainedModel` contract and
|
| 101 |
+
can be adapted with PEFT. The ESM2-specific shipped CLI is an example, not a
|
| 102 |
+
support boundary. Record the target modules, base revision, data identity, and
|
| 103 |
+
trainable parameter scope.
|
| 104 |
|
| 105 |
## Protein structure prediction
|
| 106 |
|
|
|
|
| 147 |
- Precision policies: `default`
|
| 148 |
- BF16 execution: `fp32_parameters_autocast`
|
| 149 |
- Generation contract: `not_applicable`
|
| 150 |
+
- Artifact dependency set: `core + structure`
|
| 151 |
- Weight publication allowed: `true`
|
| 152 |
- Weight license status: `resolved`
|
| 153 |
- Redistributable: `true`
|
|
|
|
| 158 |
- FastPLMs weights: `Synthyra/Boltz2`
|
| 159 |
- Runtime revision: recorded separately in the built artifact and published commit
|
| 160 |
- Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
|
|
|
|
| 161 |
- Official checkpoint: `boltz-community/boltz-2`
|
| 162 |
- Artifact source: `fast`
|
| 163 |
- State transform: `boltz2_inference_core_v1`
|
|
|
|
| 164 |
- Pinned upstreams: `boltz`
|
|
|
|
| 165 |
- Release tiers: `structure`, `artifact`, `benchmark`
|
| 166 |
- Unresolved required file identities: `0`
|
| 167 |
|
| 168 |
+
`provenance.json` records exact file identities, conversion, source revisions,
|
| 169 |
+
legal texts, schema, and attestations. A nonzero unresolved count blocks release.
|
|
|
|
| 170 |
|
| 171 |
## Validation boundary
|
| 172 |
|
| 173 |
+
Declared tiers compare applicable configuration, tokenizer behavior, state,
|
| 174 |
+
and representative inference with the pinned reference. Metadata alone does
|
| 175 |
+
not claim a build passed, a backend is faster, or an output is biologically
|
| 176 |
+
valid.
|
|
|
|
|
|
|
| 177 |
|
| 178 |
## License
|
| 179 |
|
config.json
CHANGED
|
@@ -137,11 +137,11 @@
|
|
| 137 |
"fastplms_checkpoint_repo_id": "Synthyra/Boltz2",
|
| 138 |
"fastplms_checkpoint_revision": "3b148fc5efea109c065ec82ba8683d024de7134e",
|
| 139 |
"fastplms_model_id": "boltz2",
|
| 140 |
-
"fastplms_release_tool_revision": "
|
| 141 |
-
"fastplms_release_tool_sha256": "
|
| 142 |
-
"fastplms_runtime_bundle_sha256": "
|
| 143 |
-
"fastplms_runtime_revision": "
|
| 144 |
-
"fastplms_source_tree_sha256": "
|
| 145 |
"fastplms_weights_revision": "3b148fc5efea109c065ec82ba8683d024de7134e",
|
| 146 |
"model_type": "boltz2_automodel",
|
| 147 |
"num_bins": 64,
|
|
|
|
| 137 |
"fastplms_checkpoint_repo_id": "Synthyra/Boltz2",
|
| 138 |
"fastplms_checkpoint_revision": "3b148fc5efea109c065ec82ba8683d024de7134e",
|
| 139 |
"fastplms_model_id": "boltz2",
|
| 140 |
+
"fastplms_release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 141 |
+
"fastplms_release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
|
| 142 |
+
"fastplms_runtime_bundle_sha256": "3aebb85d11edb01329d2aafb4c8260bd67339785f0f252582246ae7c33206143",
|
| 143 |
+
"fastplms_runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 144 |
+
"fastplms_source_tree_sha256": "9d2fc9586cfbd1d2d2cc49d745311acb81d4e3740e59ead09e633917589806a8",
|
| 145 |
"fastplms_weights_revision": "3b148fc5efea109c065ec82ba8683d024de7134e",
|
| 146 |
"model_type": "boltz2_automodel",
|
| 147 |
"num_bins": 64,
|
fastplms/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
| 9 |
from importlib import import_module
|
| 10 |
from typing import Any
|
| 11 |
|
|
|
|
| 12 |
__version__ = "1.0.0"
|
| 13 |
|
| 14 |
_LAZY_EXPORTS = {
|
|
|
|
| 9 |
from importlib import import_module
|
| 10 |
from typing import Any
|
| 11 |
|
| 12 |
+
|
| 13 |
__version__ = "1.0.0"
|
| 14 |
|
| 15 |
_LAZY_EXPORTS = {
|
fastplms/models/__init__.py
CHANGED
|
@@ -7,4 +7,5 @@ tokenizers, compile kernels, or initialize an accelerator runtime.
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
|
|
|
| 10 |
__all__: tuple[str, ...] = ()
|
|
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
+
|
| 11 |
__all__: tuple[str, ...] = ()
|
fastplms/models/boltz/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ from fastplms.models.boltz.modeling_boltz2 import (
|
|
| 5 |
Boltz2StructureOutput,
|
| 6 |
)
|
| 7 |
|
|
|
|
| 8 |
__all__ = [
|
| 9 |
"Boltz2Config",
|
| 10 |
"Boltz2Model",
|
|
|
|
| 5 |
Boltz2StructureOutput,
|
| 6 |
)
|
| 7 |
|
| 8 |
+
|
| 9 |
__all__ = [
|
| 10 |
"Boltz2Config",
|
| 11 |
"Boltz2Model",
|
fastplms/models/boltz/_pair_attention.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import math
|
| 6 |
-
|
| 7 |
import torch
|
| 8 |
from torch import Tensor
|
| 9 |
|
|
@@ -11,9 +10,11 @@ from torch import Tensor
|
|
| 11 |
def reshape_heads(states: Tensor, num_heads: int) -> Tensor:
|
| 12 |
"""Reshape X from ``(b, l, d)`` to ``(b, l, h, d_head)``."""
|
| 13 |
|
| 14 |
-
|
|
|
|
| 15 |
if width % num_heads:
|
| 16 |
raise ValueError(f"width {width} is not divisible by {num_heads} heads")
|
|
|
|
| 17 |
return states.view(batch_size, sequence_length, num_heads, width // num_heads)
|
| 18 |
|
| 19 |
|
|
@@ -27,20 +28,23 @@ def pair_biased_attention(
|
|
| 27 |
) -> Tensor:
|
| 28 |
"""Return pair-biased attention values with shape ``(b, l_q, h, d_head)``."""
|
| 29 |
|
| 30 |
-
|
|
|
|
|
|
|
| 31 |
with torch.autocast("cuda", enabled=False):
|
| 32 |
-
# S is the FP32 attention-score tensor with shape (b, h, l_q, l_k).
|
| 33 |
scores = torch.einsum(
|
| 34 |
"bihd,bjhd->bhij",
|
| 35 |
query_states.float(),
|
| 36 |
key_states.float(),
|
| 37 |
-
)
|
| 38 |
-
scores = scores / math.sqrt(head_dim) + pair_bias.float()
|
| 39 |
-
scores =
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
output = torch.einsum(
|
| 42 |
"bhij,bjhd->bihd",
|
| 43 |
probabilities,
|
| 44 |
value_states.float(),
|
| 45 |
-
)
|
| 46 |
-
return output.to(value_states.dtype)
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import math
|
|
|
|
| 6 |
import torch
|
| 7 |
from torch import Tensor
|
| 8 |
|
|
|
|
| 10 |
def reshape_heads(states: Tensor, num_heads: int) -> Tensor:
|
| 11 |
"""Reshape X from ``(b, l, d)`` to ``(b, l, h, d_head)``."""
|
| 12 |
|
| 13 |
+
# states: (b, l, d); num_heads: h
|
| 14 |
+
batch_size, sequence_length, width = states.shape # b, l, d
|
| 15 |
if width % num_heads:
|
| 16 |
raise ValueError(f"width {width} is not divisible by {num_heads} heads")
|
| 17 |
+
# states.view(...): (b, l, h, d_h)
|
| 18 |
return states.view(batch_size, sequence_length, num_heads, width // num_heads)
|
| 19 |
|
| 20 |
|
|
|
|
| 28 |
) -> Tensor:
|
| 29 |
"""Return pair-biased attention values with shape ``(b, l_q, h, d_head)``."""
|
| 30 |
|
| 31 |
+
# query_states: (b, l_q, h, d_h); key_states, value_states: (b, l_k, h, d_h)
|
| 32 |
+
# pair_bias: (b, h, l_q, l_k); key_mask: (b, l_k)
|
| 33 |
+
head_dim = query_states.shape[-1] # d_h
|
| 34 |
with torch.autocast("cuda", enabled=False):
|
|
|
|
| 35 |
scores = torch.einsum(
|
| 36 |
"bihd,bjhd->bhij",
|
| 37 |
query_states.float(),
|
| 38 |
key_states.float(),
|
| 39 |
+
) # (b, h, l_q, l_k)
|
| 40 |
+
scores = scores / math.sqrt(head_dim) + pair_bias.float() # (b, h, l_q, l_k)
|
| 41 |
+
scores = (
|
| 42 |
+
scores + (1 - key_mask[:, None, None].float()) * -mask_value
|
| 43 |
+
) # (b, h, l_q, l_k)
|
| 44 |
+
probabilities = scores.softmax(dim=-1) # (b, h, l_q, l_k)
|
| 45 |
output = torch.einsum(
|
| 46 |
"bhij,bjhd->bihd",
|
| 47 |
probabilities,
|
| 48 |
value_states.float(),
|
| 49 |
+
) # (b, l_q, h, d_h)
|
| 50 |
+
return output.to(value_states.dtype) # (b, l_q, h, d_h)
|
fastplms/models/boltz/cif_writer.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
-
|
| 3 |
import numpy as np
|
| 4 |
import torch
|
|
|
|
| 5 |
|
| 6 |
from .minimal_structures import ProteinStructureTemplate
|
| 7 |
|
|
@@ -12,28 +11,30 @@ def _confidence_per_atom(
|
|
| 12 |
num_atoms: int,
|
| 13 |
sample_index: int,
|
| 14 |
) -> np.ndarray:
|
|
|
|
| 15 |
if plddt is None:
|
| 16 |
-
return np.ones((num_atoms,), dtype=np.float32) * 100.0
|
| 17 |
|
| 18 |
-
values = plddt.detach().cpu()
|
| 19 |
if values.ndim == 1:
|
| 20 |
-
values = values.unsqueeze(0)
|
| 21 |
if values.ndim != 2:
|
| 22 |
raise ValueError("Expected pLDDT tensor S with shape (n_samples, n_items).")
|
|
|
|
| 23 |
if not 0 <= sample_index < values.shape[0]:
|
| 24 |
raise IndexError("sample_index out of range for pLDDT.")
|
| 25 |
|
| 26 |
-
selected = values[sample_index]
|
| 27 |
if selected.shape[0] == num_atoms:
|
| 28 |
-
return (selected.numpy() * 100.0).astype(np.float32)
|
| 29 |
|
| 30 |
-
num_residues = max(atom_to_residue) + 1
|
| 31 |
if selected.shape[0] == num_residues:
|
| 32 |
-
expanded = np.zeros((num_atoms,), dtype=np.float32)
|
| 33 |
-
selected_np = selected.numpy()
|
| 34 |
for atom_idx, residue_idx in enumerate(atom_to_residue):
|
| 35 |
-
expanded[atom_idx] = selected_np[residue_idx] * 100.0
|
| 36 |
-
return expanded
|
| 37 |
|
| 38 |
raise ValueError(
|
| 39 |
"pLDDT item count must match either atoms or residues: "
|
|
@@ -50,34 +51,39 @@ def write_cif(
|
|
| 50 |
plddt: torch.Tensor | None = None,
|
| 51 |
sample_index: int = 0,
|
| 52 |
) -> str:
|
| 53 |
-
|
|
|
|
|
|
|
| 54 |
if coords.ndim == 2:
|
| 55 |
-
coords = coords.unsqueeze(0)
|
| 56 |
if coords.ndim != 3 or coords.shape[-1] != 3:
|
| 57 |
raise ValueError(
|
| 58 |
"Expected coordinate tensor X with shape (n_samples, n_atoms, 3)."
|
| 59 |
)
|
|
|
|
| 60 |
if not 0 <= sample_index < coords.shape[0]:
|
| 61 |
raise IndexError("sample_index out of range.")
|
| 62 |
-
selected_coords_tensor = coords[sample_index]
|
| 63 |
-
all_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor))
|
| 64 |
if torch.any(all_non_finite):
|
| 65 |
raise ValueError(
|
| 66 |
"CIF export received non-finite coordinates. "
|
| 67 |
f"Non-finite count: {int(all_non_finite.sum().item())}"
|
| 68 |
)
|
| 69 |
-
selected_coords = selected_coords_tensor.numpy()
|
| 70 |
|
| 71 |
-
mask = atom_mask.detach().cpu()
|
| 72 |
if mask.ndim == 2:
|
| 73 |
-
mask = mask[0]
|
| 74 |
if mask.ndim != 1:
|
| 75 |
raise ValueError("Expected atom mask M with shape (n_atoms,).")
|
|
|
|
| 76 |
if mask.shape[0] != selected_coords.shape[0]:
|
| 77 |
raise ValueError("Atom mask/coord size mismatch.")
|
| 78 |
if not torch.any(mask > 0):
|
| 79 |
raise ValueError("Atom mask has no valid atoms for CIF export.")
|
| 80 |
valid_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor[mask > 0]))
|
|
|
|
| 81 |
if torch.any(valid_non_finite):
|
| 82 |
raise ValueError(
|
| 83 |
"CIF export has non-finite coordinates in unmasked atoms. "
|
|
@@ -89,7 +95,7 @@ def write_cif(
|
|
| 89 |
atom_to_residue=structure_template.atom_residue_index,
|
| 90 |
num_atoms=structure_template.num_atoms,
|
| 91 |
sample_index=sample_index,
|
| 92 |
-
)
|
| 93 |
if b_iso.shape[0] != structure_template.num_atoms:
|
| 94 |
raise RuntimeError("CIF confidence values do not match the structure atom count.")
|
| 95 |
|
|
@@ -122,8 +128,8 @@ def write_cif(
|
|
| 122 |
atom_name = structure_template.atom_names[idx]
|
| 123 |
element = structure_template.atom_elements[idx]
|
| 124 |
chain_id = structure_template.atom_chain_id[idx]
|
| 125 |
-
x_val, y_val, z_val = selected_coords[idx].tolist()
|
| 126 |
-
b_factor = float(b_iso[idx])
|
| 127 |
|
| 128 |
line = (
|
| 129 |
f"ATOM {atom_id} {element} {atom_name} {residue_name} {chain_id} "
|
|
|
|
|
|
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
import torch
|
| 3 |
+
from pathlib import Path
|
| 4 |
|
| 5 |
from .minimal_structures import ProteinStructureTemplate
|
| 6 |
|
|
|
|
| 11 |
num_atoms: int,
|
| 12 |
sample_index: int,
|
| 13 |
) -> np.ndarray:
|
| 14 |
+
# plddt: (n_i,) or (n_s, n_i); atom_to_residue: length n_a; num_atoms: n_a
|
| 15 |
if plddt is None:
|
| 16 |
+
return np.ones((num_atoms,), dtype=np.float32) * 100.0 # (n_a,)
|
| 17 |
|
| 18 |
+
values = plddt.detach().cpu() # (n_i,) or (n_s, n_i)
|
| 19 |
if values.ndim == 1:
|
| 20 |
+
values = values.unsqueeze(0) # (1, n_i)
|
| 21 |
if values.ndim != 2:
|
| 22 |
raise ValueError("Expected pLDDT tensor S with shape (n_samples, n_items).")
|
| 23 |
+
# values: (n_s, n_i) beyond this point
|
| 24 |
if not 0 <= sample_index < values.shape[0]:
|
| 25 |
raise IndexError("sample_index out of range for pLDDT.")
|
| 26 |
|
| 27 |
+
selected = values[sample_index] # (n_i,)
|
| 28 |
if selected.shape[0] == num_atoms:
|
| 29 |
+
return (selected.numpy() * 100.0).astype(np.float32) # (n_a,)
|
| 30 |
|
| 31 |
+
num_residues = max(atom_to_residue) + 1 # n_r
|
| 32 |
if selected.shape[0] == num_residues:
|
| 33 |
+
expanded = np.zeros((num_atoms,), dtype=np.float32) # (n_a,)
|
| 34 |
+
selected_np = selected.numpy() # (n_r,)
|
| 35 |
for atom_idx, residue_idx in enumerate(atom_to_residue):
|
| 36 |
+
expanded[atom_idx] = selected_np[residue_idx] * 100.0 # () -> ()
|
| 37 |
+
return expanded # (n_a,)
|
| 38 |
|
| 39 |
raise ValueError(
|
| 40 |
"pLDDT item count must match either atoms or residues: "
|
|
|
|
| 51 |
plddt: torch.Tensor | None = None,
|
| 52 |
sample_index: int = 0,
|
| 53 |
) -> str:
|
| 54 |
+
# atom_coords: (n_a, 3) or (n_s, n_a, 3)
|
| 55 |
+
# atom_mask: (n_a,) or (n_m, n_a); plddt: (n_i,) or (n_s, n_i)
|
| 56 |
+
coords = atom_coords.detach().cpu() # (n_a, 3) or (n_s, n_a, 3)
|
| 57 |
if coords.ndim == 2:
|
| 58 |
+
coords = coords.unsqueeze(0) # (1, n_a, 3)
|
| 59 |
if coords.ndim != 3 or coords.shape[-1] != 3:
|
| 60 |
raise ValueError(
|
| 61 |
"Expected coordinate tensor X with shape (n_samples, n_atoms, 3)."
|
| 62 |
)
|
| 63 |
+
# coords: (n_s, n_a, 3) beyond this point
|
| 64 |
if not 0 <= sample_index < coords.shape[0]:
|
| 65 |
raise IndexError("sample_index out of range.")
|
| 66 |
+
selected_coords_tensor = coords[sample_index] # (n_a, 3)
|
| 67 |
+
all_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor)) # (n_a, 3)
|
| 68 |
if torch.any(all_non_finite):
|
| 69 |
raise ValueError(
|
| 70 |
"CIF export received non-finite coordinates. "
|
| 71 |
f"Non-finite count: {int(all_non_finite.sum().item())}"
|
| 72 |
)
|
| 73 |
+
selected_coords = selected_coords_tensor.numpy() # (n_a, 3)
|
| 74 |
|
| 75 |
+
mask = atom_mask.detach().cpu() # (n_a,) or (n_m, n_a)
|
| 76 |
if mask.ndim == 2:
|
| 77 |
+
mask = mask[0] # (n_a,)
|
| 78 |
if mask.ndim != 1:
|
| 79 |
raise ValueError("Expected atom mask M with shape (n_atoms,).")
|
| 80 |
+
# mask: (n_a,) beyond this point
|
| 81 |
if mask.shape[0] != selected_coords.shape[0]:
|
| 82 |
raise ValueError("Atom mask/coord size mismatch.")
|
| 83 |
if not torch.any(mask > 0):
|
| 84 |
raise ValueError("Atom mask has no valid atoms for CIF export.")
|
| 85 |
valid_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor[mask > 0]))
|
| 86 |
+
# valid_non_finite: (n_v, 3), where n_v is the valid-atom count
|
| 87 |
if torch.any(valid_non_finite):
|
| 88 |
raise ValueError(
|
| 89 |
"CIF export has non-finite coordinates in unmasked atoms. "
|
|
|
|
| 95 |
atom_to_residue=structure_template.atom_residue_index,
|
| 96 |
num_atoms=structure_template.num_atoms,
|
| 97 |
sample_index=sample_index,
|
| 98 |
+
) # (n_a,)
|
| 99 |
if b_iso.shape[0] != structure_template.num_atoms:
|
| 100 |
raise RuntimeError("CIF confidence values do not match the structure atom count.")
|
| 101 |
|
|
|
|
| 128 |
atom_name = structure_template.atom_names[idx]
|
| 129 |
element = structure_template.atom_elements[idx]
|
| 130 |
chain_id = structure_template.atom_chain_id[idx]
|
| 131 |
+
x_val, y_val, z_val = selected_coords[idx].tolist() # (3,) -> three scalars
|
| 132 |
+
b_factor = float(b_iso[idx]) # () -> Python float
|
| 133 |
|
| 134 |
line = (
|
| 135 |
f"ATOM {atom_id} {element} {atom_name} {residue_name} {chain_id} "
|
fastplms/models/boltz/minimal_featurizer.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import math
|
| 2 |
-
|
| 3 |
import numpy as np
|
| 4 |
import torch
|
| 5 |
from torch.nn.functional import one_hot
|
|
@@ -7,6 +6,7 @@ from torch.nn.functional import one_hot
|
|
| 7 |
from . import vb_const as const
|
| 8 |
from .minimal_structures import ProteinStructureTemplate
|
| 9 |
|
|
|
|
| 10 |
_ELEMENT_TO_Z = {
|
| 11 |
"H": 1,
|
| 12 |
"C": 6,
|
|
@@ -75,7 +75,7 @@ def _atom_name_to_codes(atom_name: str) -> torch.Tensor:
|
|
| 75 |
vals = [ord(ch) - 32 for ch in clipped]
|
| 76 |
while len(vals) < 4:
|
| 77 |
vals.append(0)
|
| 78 |
-
out = torch.tensor(vals, dtype=torch.long)
|
| 79 |
if not (torch.all(out >= 0) and torch.all(out < 64)):
|
| 80 |
raise ValueError(f"Invalid atom-name encoding for '{atom_name}'.")
|
| 81 |
return out
|
|
@@ -309,7 +309,7 @@ def _get_atom_position(res_name: str, atom_name: str, atom_idx: int) -> np.ndarr
|
|
| 309 |
to a simple geometric placement for unknown residue/atom combinations.
|
| 310 |
"""
|
| 311 |
if res_name in _RDKIT_CONFORMERS and atom_name in _RDKIT_CONFORMERS[res_name]:
|
| 312 |
-
return np.array(_RDKIT_CONFORMERS[res_name][atom_name], dtype=np.float32)
|
| 313 |
# Fallback for unknown atoms (should not happen for canonical AAs)
|
| 314 |
angle = (atom_idx + 1) * 0.7
|
| 315 |
radius = 1.4 + 0.03 * atom_idx
|
|
@@ -320,7 +320,7 @@ def _get_atom_position(res_name: str, atom_name: str, atom_idx: int) -> np.ndarr
|
|
| 320 |
0.1 * ((atom_idx % 5) - 2),
|
| 321 |
],
|
| 322 |
dtype=np.float32,
|
| 323 |
-
)
|
| 324 |
|
| 325 |
|
| 326 |
def _build_template(
|
|
@@ -340,7 +340,7 @@ def _build_template(
|
|
| 340 |
atom_elements: list[str] = []
|
| 341 |
atom_residue_index: list[int] = []
|
| 342 |
atom_chain_id: list[str] = []
|
| 343 |
-
atom_positions: list[np.ndarray] = []
|
| 344 |
residue_center_atom_idx: list[int] = []
|
| 345 |
residue_disto_atom_idx: list[int] = []
|
| 346 |
residue_frame_atom_idx: list[int] = []
|
|
@@ -370,7 +370,7 @@ def _build_template(
|
|
| 370 |
atom_residue_index.append(res_idx)
|
| 371 |
atom_chain_id.append("A")
|
| 372 |
|
| 373 |
-
atom_pos = _get_atom_position(token_name, atom_name, local_idx)
|
| 374 |
atom_positions.append(atom_pos)
|
| 375 |
|
| 376 |
if atom_name == center_atom_name:
|
|
@@ -422,14 +422,14 @@ def _build_template(
|
|
| 422 |
|
| 423 |
def _random_rotation_matrix() -> torch.Tensor:
|
| 424 |
"""Sample a uniform random 3x3 rotation matrix (Algorithm 19 from AF2/Boltz)."""
|
| 425 |
-
quaternion = torch.randn((1, 4), dtype=torch.float32)
|
| 426 |
-
squared_norm = (quaternion * quaternion).sum(dim=1)
|
| 427 |
-
norm = torch.sqrt(squared_norm)
|
| 428 |
-
signed_norm = torch.where(quaternion[:, 0] < 0, -norm, norm)
|
| 429 |
-
quaternion = quaternion / signed_norm[:, None]
|
| 430 |
-
|
| 431 |
-
real, i, j, k = torch.unbind(quaternion, dim=-1)
|
| 432 |
-
two_s = 2.0 / (quaternion * quaternion).sum(dim=-1)
|
| 433 |
rotation = torch.stack(
|
| 434 |
(
|
| 435 |
1 - two_s * (j * j + k * k),
|
|
@@ -443,8 +443,8 @@ def _random_rotation_matrix() -> torch.Tensor:
|
|
| 443 |
1 - two_s * (i * i + j * j),
|
| 444 |
),
|
| 445 |
dim=-1,
|
| 446 |
-
)
|
| 447 |
-
return rotation.reshape(1, 3, 3)[0]
|
| 448 |
|
| 449 |
|
| 450 |
def _center_and_augment_atoms_per_residue(
|
|
@@ -457,25 +457,30 @@ def _center_and_augment_atoms_per_residue(
|
|
| 457 |
The pinned implementation intentionally excludes the final residue because
|
| 458 |
it iterates to the maximum reference-space identifier rather than through it.
|
| 459 |
"""
|
| 460 |
-
|
| 461 |
-
|
|
|
|
| 462 |
for residue_idx in range(max(num_residues - 1, 0)):
|
| 463 |
-
residue_mask = residue_index_tensor == residue_idx
|
| 464 |
if not torch.any(residue_mask):
|
| 465 |
raise RuntimeError(f"Residue index {residue_idx} has no atoms.")
|
| 466 |
-
residue_coords = result[residue_mask][None]
|
| 467 |
resolved_mask = torch.ones(
|
| 468 |
residue_coords.shape[:2], dtype=torch.bool, device=residue_coords.device
|
| 469 |
-
)
|
| 470 |
residue_center = torch.sum(
|
| 471 |
residue_coords * resolved_mask[:, :, None], dim=1, keepdim=True
|
| 472 |
-
) / torch.sum(resolved_mask[:, :, None], dim=1, keepdim=True)
|
| 473 |
-
residue_coords = residue_coords - residue_center
|
| 474 |
-
rotation = _random_rotation_matrix()[None]
|
| 475 |
-
residue_coords = torch.einsum(
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
|
| 480 |
|
| 481 |
def build_boltz2_features(
|
|
@@ -499,39 +504,44 @@ def build_boltz2_features(
|
|
| 499 |
if num_tokens <= 0 or num_atoms <= 0:
|
| 500 |
raise RuntimeError("Boltz2 feature construction produced an empty protein template.")
|
| 501 |
|
| 502 |
-
|
|
|
|
|
|
|
|
|
|
| 503 |
atom_positions = _center_and_augment_atoms_per_residue(
|
| 504 |
atom_positions=atom_positions,
|
| 505 |
atom_residue_index=template.atom_residue_index,
|
| 506 |
num_residues=num_tokens,
|
| 507 |
-
)
|
| 508 |
|
| 509 |
-
token_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0)
|
| 510 |
-
residue_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0)
|
| 511 |
-
asym_id = torch.zeros((1, num_tokens), dtype=torch.long)
|
| 512 |
-
entity_id = torch.zeros((1, num_tokens), dtype=torch.long)
|
| 513 |
-
sym_id = torch.zeros((1, num_tokens), dtype=torch.long)
|
| 514 |
mol_type = torch.full(
|
| 515 |
(1, num_tokens),
|
| 516 |
fill_value=const.chain_type_ids["PROTEIN"],
|
| 517 |
dtype=torch.long,
|
| 518 |
-
)
|
| 519 |
|
| 520 |
-
res_type_ids = torch.tensor(residue_token_ids, dtype=torch.long)
|
| 521 |
-
res_type = one_hot(
|
|
|
|
|
|
|
| 522 |
|
| 523 |
# token_bonds encodes explicit covalent cross-links from structure bonds,
|
| 524 |
# NOT backbone peptide bonds (those are implicit via residue_index + asym_id).
|
| 525 |
# For a standard single-chain protein without cross-links, this is all zeros.
|
| 526 |
# This matches the official Boltz2 featurizer (featurizerv2.py lines 696-705).
|
| 527 |
-
token_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.float32)
|
| 528 |
-
type_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.long)
|
| 529 |
-
token_bonds = token_bonds.unsqueeze(0).unsqueeze(-1)
|
| 530 |
-
type_bonds = type_bonds.unsqueeze(0)
|
| 531 |
|
| 532 |
-
token_pad_mask = torch.ones((1, num_tokens), dtype=torch.float32)
|
| 533 |
-
token_resolved_mask = torch.ones((1, num_tokens), dtype=torch.float32)
|
| 534 |
-
token_disto_mask = torch.ones((1, num_tokens), dtype=torch.float32)
|
| 535 |
|
| 536 |
num_contact_classes = len(const.contact_conditioning_info)
|
| 537 |
unspecified_id = const.contact_conditioning_info["UNSPECIFIED"]
|
|
@@ -539,12 +549,14 @@ def build_boltz2_features(
|
|
| 539 |
(num_tokens, num_tokens),
|
| 540 |
fill_value=unspecified_id,
|
| 541 |
dtype=torch.long,
|
| 542 |
-
)
|
| 543 |
contact_conditioning = one_hot(
|
| 544 |
contact_ids,
|
| 545 |
num_classes=num_contact_classes,
|
| 546 |
-
).unsqueeze(0)
|
| 547 |
-
contact_threshold = torch.zeros(
|
|
|
|
|
|
|
| 548 |
|
| 549 |
if "x-ray diffraction" not in const.method_types_ids:
|
| 550 |
raise RuntimeError("Boltz2 method metadata omits x-ray diffraction.")
|
|
@@ -552,20 +564,22 @@ def build_boltz2_features(
|
|
| 552 |
(1, num_tokens),
|
| 553 |
fill_value=const.method_types_ids["x-ray diffraction"],
|
| 554 |
dtype=torch.long,
|
| 555 |
-
)
|
| 556 |
-
modified = torch.zeros((1, num_tokens), dtype=torch.long)
|
| 557 |
-
cyclic_period = torch.zeros((1, num_tokens), dtype=torch.float32)
|
| 558 |
-
affinity_token_mask = torch.zeros((1, num_tokens), dtype=torch.float32)
|
| 559 |
|
| 560 |
-
ref_pos = atom_positions.unsqueeze(0)
|
| 561 |
-
atom_pad_mask = torch.ones((1, num_atoms), dtype=torch.float32)
|
| 562 |
-
atom_resolved_mask = torch.ones((1, num_atoms), dtype=torch.bool)
|
| 563 |
|
| 564 |
atom_name_codes = torch.stack(
|
| 565 |
[_atom_name_to_codes(atom_name) for atom_name in template.atom_names],
|
| 566 |
dim=0,
|
| 567 |
-
)
|
| 568 |
-
ref_atom_name_chars = one_hot(
|
|
|
|
|
|
|
| 569 |
|
| 570 |
atomic_numbers = []
|
| 571 |
for element in template.atom_elements:
|
|
@@ -578,7 +592,7 @@ def build_boltz2_features(
|
|
| 578 |
ref_element = one_hot(
|
| 579 |
torch.tensor(atomic_numbers, dtype=torch.long),
|
| 580 |
num_classes=const.num_elements,
|
| 581 |
-
).unsqueeze(0)
|
| 582 |
|
| 583 |
ref_charge_values = []
|
| 584 |
ref_chirality_values = []
|
|
@@ -592,23 +606,27 @@ def build_boltz2_features(
|
|
| 592 |
ref_chirality_values.append(
|
| 593 |
2 if atom_name in _CHIRAL_ATOMS.get(residue_name, frozenset()) else 0
|
| 594 |
)
|
| 595 |
-
ref_charge = torch.tensor(ref_charge_values, dtype=torch.float32).unsqueeze(0)
|
| 596 |
-
ref_chirality = torch.tensor(
|
| 597 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 598 |
|
| 599 |
atom_to_token = one_hot(
|
| 600 |
torch.tensor(template.atom_residue_index, dtype=torch.long),
|
| 601 |
num_classes=num_tokens,
|
| 602 |
-
).unsqueeze(0)
|
| 603 |
token_to_rep_atom = one_hot(
|
| 604 |
torch.tensor(residue_disto_atom_idx, dtype=torch.long),
|
| 605 |
num_classes=num_atoms,
|
| 606 |
-
).unsqueeze(0)
|
| 607 |
token_to_center_atom = one_hot(
|
| 608 |
torch.tensor(residue_center_atom_idx, dtype=torch.long),
|
| 609 |
num_classes=num_atoms,
|
| 610 |
-
).unsqueeze(0)
|
| 611 |
-
r_set_to_rep_atom = token_to_center_atom.clone()
|
| 612 |
|
| 613 |
num_backbone_classes = (
|
| 614 |
1 + len(const.protein_backbone_atom_index) + len(const.nucleic_backbone_atom_index)
|
|
@@ -622,17 +640,17 @@ def build_boltz2_features(
|
|
| 622 |
atom_backbone_feat = one_hot(
|
| 623 |
torch.tensor(backbone_ids, dtype=torch.long),
|
| 624 |
num_classes=num_backbone_classes,
|
| 625 |
-
).unsqueeze(0)
|
| 626 |
|
| 627 |
# X contains no observed coordinates for sequence-only inference.
|
| 628 |
-
coords = torch.zeros((1, 1, num_atoms, 3), dtype=torch.float32)
|
| 629 |
disto_coords_ensemble = torch.zeros(
|
| 630 |
(1, 1, num_tokens, 3),
|
| 631 |
dtype=torch.float32,
|
| 632 |
-
)
|
| 633 |
|
| 634 |
-
bfactor = torch.zeros((1, num_atoms), dtype=torch.float32)
|
| 635 |
-
atom_plddt = torch.ones((1, num_atoms), dtype=torch.float32)
|
| 636 |
|
| 637 |
if atoms_per_window_queries <= 0:
|
| 638 |
raise ValueError("atoms_per_window_queries must be positive.")
|
|
@@ -640,60 +658,86 @@ def build_boltz2_features(
|
|
| 640 |
(num_atoms - 1) // atoms_per_window_queries + 1
|
| 641 |
) * atoms_per_window_queries - num_atoms
|
| 642 |
if pad_atoms > 0:
|
| 643 |
-
ref_pos = torch.nn.functional.pad(
|
| 644 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
atom_resolved_mask = torch.nn.functional.pad(
|
| 646 |
atom_resolved_mask,
|
| 647 |
(0, pad_atoms),
|
| 648 |
value=0.0,
|
| 649 |
-
)
|
| 650 |
ref_atom_name_chars = torch.nn.functional.pad(
|
| 651 |
ref_atom_name_chars,
|
| 652 |
(0, 0, 0, 0, 0, pad_atoms),
|
| 653 |
value=0.0,
|
| 654 |
-
)
|
| 655 |
-
ref_element = torch.nn.functional.pad(
|
| 656 |
-
|
| 657 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
atom_backbone_feat = torch.nn.functional.pad(
|
| 659 |
atom_backbone_feat,
|
| 660 |
(0, 0, 0, pad_atoms),
|
| 661 |
value=0.0,
|
| 662 |
-
)
|
| 663 |
-
ref_space_uid = torch.nn.functional.pad(
|
| 664 |
-
|
| 665 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
token_to_rep_atom = torch.nn.functional.pad(
|
| 667 |
token_to_rep_atom,
|
| 668 |
(0, pad_atoms),
|
| 669 |
value=0.0,
|
| 670 |
-
)
|
| 671 |
token_to_center_atom = torch.nn.functional.pad(
|
| 672 |
token_to_center_atom,
|
| 673 |
(0, pad_atoms),
|
| 674 |
value=0.0,
|
| 675 |
-
)
|
| 676 |
r_set_to_rep_atom = torch.nn.functional.pad(
|
| 677 |
r_set_to_rep_atom,
|
| 678 |
(0, pad_atoms),
|
| 679 |
value=0.0,
|
| 680 |
-
)
|
| 681 |
-
bfactor = torch.nn.functional.pad(
|
| 682 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
|
| 684 |
frames_idx = torch.tensor(
|
| 685 |
residue_frame_atom_idx_flat,
|
| 686 |
dtype=torch.long,
|
| 687 |
-
).reshape(num_tokens, 3)
|
| 688 |
-
frames_idx = frames_idx.unsqueeze(0).unsqueeze(1)
|
| 689 |
-
frame_resolved_mask = torch.zeros(
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
profile = (
|
| 698 |
one_hot(
|
| 699 |
torch.tensor(residue_token_ids, dtype=torch.long),
|
|
@@ -701,28 +745,48 @@ def build_boltz2_features(
|
|
| 701 |
)
|
| 702 |
.float()
|
| 703 |
.unsqueeze(0)
|
| 704 |
-
)
|
| 705 |
|
| 706 |
template_restype = one_hot(
|
| 707 |
torch.zeros((1, 1, num_tokens), dtype=torch.long),
|
| 708 |
num_classes=const.num_tokens,
|
| 709 |
-
)
|
| 710 |
-
template_frame_rot = torch.zeros(
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
|
| 720 |
disto_target = torch.zeros(
|
| 721 |
(1, num_tokens, num_tokens, 1, num_bins),
|
| 722 |
dtype=torch.float32,
|
| 723 |
-
)
|
| 724 |
-
disto_target[..., 0] = 1.0
|
| 725 |
-
disto_center = torch.zeros(
|
|
|
|
|
|
|
| 726 |
|
| 727 |
features: dict[str, torch.Tensor] = {
|
| 728 |
"token_index": token_index,
|
|
@@ -783,4 +847,4 @@ def build_boltz2_features(
|
|
| 783 |
"visibility_ids": visibility_ids,
|
| 784 |
}
|
| 785 |
|
| 786 |
-
return features, template
|
|
|
|
| 1 |
import math
|
|
|
|
| 2 |
import numpy as np
|
| 3 |
import torch
|
| 4 |
from torch.nn.functional import one_hot
|
|
|
|
| 6 |
from . import vb_const as const
|
| 7 |
from .minimal_structures import ProteinStructureTemplate
|
| 8 |
|
| 9 |
+
|
| 10 |
_ELEMENT_TO_Z = {
|
| 11 |
"H": 1,
|
| 12 |
"C": 6,
|
|
|
|
| 75 |
vals = [ord(ch) - 32 for ch in clipped]
|
| 76 |
while len(vals) < 4:
|
| 77 |
vals.append(0)
|
| 78 |
+
out = torch.tensor(vals, dtype=torch.long) # (4,)
|
| 79 |
if not (torch.all(out >= 0) and torch.all(out < 64)):
|
| 80 |
raise ValueError(f"Invalid atom-name encoding for '{atom_name}'.")
|
| 81 |
return out
|
|
|
|
| 309 |
to a simple geometric placement for unknown residue/atom combinations.
|
| 310 |
"""
|
| 311 |
if res_name in _RDKIT_CONFORMERS and atom_name in _RDKIT_CONFORMERS[res_name]:
|
| 312 |
+
return np.array(_RDKIT_CONFORMERS[res_name][atom_name], dtype=np.float32) # (3,)
|
| 313 |
# Fallback for unknown atoms (should not happen for canonical AAs)
|
| 314 |
angle = (atom_idx + 1) * 0.7
|
| 315 |
radius = 1.4 + 0.03 * atom_idx
|
|
|
|
| 320 |
0.1 * ((atom_idx % 5) - 2),
|
| 321 |
],
|
| 322 |
dtype=np.float32,
|
| 323 |
+
) # (3,)
|
| 324 |
|
| 325 |
|
| 326 |
def _build_template(
|
|
|
|
| 340 |
atom_elements: list[str] = []
|
| 341 |
atom_residue_index: list[int] = []
|
| 342 |
atom_chain_id: list[str] = []
|
| 343 |
+
atom_positions: list[np.ndarray] = [] # each: (3,)
|
| 344 |
residue_center_atom_idx: list[int] = []
|
| 345 |
residue_disto_atom_idx: list[int] = []
|
| 346 |
residue_frame_atom_idx: list[int] = []
|
|
|
|
| 370 |
atom_residue_index.append(res_idx)
|
| 371 |
atom_chain_id.append("A")
|
| 372 |
|
| 373 |
+
atom_pos = _get_atom_position(token_name, atom_name, local_idx) # (3,)
|
| 374 |
atom_positions.append(atom_pos)
|
| 375 |
|
| 376 |
if atom_name == center_atom_name:
|
|
|
|
| 422 |
|
| 423 |
def _random_rotation_matrix() -> torch.Tensor:
|
| 424 |
"""Sample a uniform random 3x3 rotation matrix (Algorithm 19 from AF2/Boltz)."""
|
| 425 |
+
quaternion = torch.randn((1, 4), dtype=torch.float32) # (1, 4)
|
| 426 |
+
squared_norm = (quaternion * quaternion).sum(dim=1) # (1,)
|
| 427 |
+
norm = torch.sqrt(squared_norm) # (1,)
|
| 428 |
+
signed_norm = torch.where(quaternion[:, 0] < 0, -norm, norm) # (1,)
|
| 429 |
+
quaternion = quaternion / signed_norm[:, None] # (1, 4)
|
| 430 |
+
|
| 431 |
+
real, i, j, k = torch.unbind(quaternion, dim=-1) # each: (1,)
|
| 432 |
+
two_s = 2.0 / (quaternion * quaternion).sum(dim=-1) # (1,)
|
| 433 |
rotation = torch.stack(
|
| 434 |
(
|
| 435 |
1 - two_s * (j * j + k * k),
|
|
|
|
| 443 |
1 - two_s * (i * i + j * j),
|
| 444 |
),
|
| 445 |
dim=-1,
|
| 446 |
+
) # (1, 9)
|
| 447 |
+
return rotation.reshape(1, 3, 3)[0] # (3, 3)
|
| 448 |
|
| 449 |
|
| 450 |
def _center_and_augment_atoms_per_residue(
|
|
|
|
| 457 |
The pinned implementation intentionally excludes the final residue because
|
| 458 |
it iterates to the maximum reference-space identifier rather than through it.
|
| 459 |
"""
|
| 460 |
+
# atom_positions: (a, 3), where a is the unpadded atom count.
|
| 461 |
+
result = atom_positions.clone() # (a, 3)
|
| 462 |
+
residue_index_tensor = torch.tensor(atom_residue_index, dtype=torch.long) # (a,)
|
| 463 |
for residue_idx in range(max(num_residues - 1, 0)):
|
| 464 |
+
residue_mask = residue_index_tensor == residue_idx # (a,)
|
| 465 |
if not torch.any(residue_mask):
|
| 466 |
raise RuntimeError(f"Residue index {residue_idx} has no atoms.")
|
| 467 |
+
residue_coords = result[residue_mask][None] # (1, a_r, 3)
|
| 468 |
resolved_mask = torch.ones(
|
| 469 |
residue_coords.shape[:2], dtype=torch.bool, device=residue_coords.device
|
| 470 |
+
) # (1, a_r)
|
| 471 |
residue_center = torch.sum(
|
| 472 |
residue_coords * resolved_mask[:, :, None], dim=1, keepdim=True
|
| 473 |
+
) / torch.sum(resolved_mask[:, :, None], dim=1, keepdim=True) # (1, 1, 3)
|
| 474 |
+
residue_coords = residue_coords - residue_center # (1, a_r, 3)
|
| 475 |
+
rotation = _random_rotation_matrix()[None] # (1, 3, 3)
|
| 476 |
+
residue_coords = torch.einsum(
|
| 477 |
+
"bmd,bds->bms", residue_coords, rotation
|
| 478 |
+
) # (1, a_r, 3)
|
| 479 |
+
residue_coords = (
|
| 480 |
+
residue_coords + torch.randn_like(residue_coords[:, 0:1, :])
|
| 481 |
+
) # (1, a_r, 3)
|
| 482 |
+
result[residue_mask] = residue_coords[0] # (a_r, 3)
|
| 483 |
+
return result # (a, 3)
|
| 484 |
|
| 485 |
|
| 486 |
def build_boltz2_features(
|
|
|
|
| 504 |
if num_tokens <= 0 or num_atoms <= 0:
|
| 505 |
raise RuntimeError("Boltz2 feature construction produced an empty protein template.")
|
| 506 |
|
| 507 |
+
# t is the token count, a is the unpadded atom count, and a_p is the padded atom count.
|
| 508 |
+
atom_positions = torch.tensor(
|
| 509 |
+
np.asarray(atom_positions_np), dtype=torch.float32
|
| 510 |
+
) # (a, 3)
|
| 511 |
atom_positions = _center_and_augment_atoms_per_residue(
|
| 512 |
atom_positions=atom_positions,
|
| 513 |
atom_residue_index=template.atom_residue_index,
|
| 514 |
num_residues=num_tokens,
|
| 515 |
+
) # (a, 3)
|
| 516 |
|
| 517 |
+
token_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0) # (1, t)
|
| 518 |
+
residue_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0) # (1, t)
|
| 519 |
+
asym_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 520 |
+
entity_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 521 |
+
sym_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 522 |
mol_type = torch.full(
|
| 523 |
(1, num_tokens),
|
| 524 |
fill_value=const.chain_type_ids["PROTEIN"],
|
| 525 |
dtype=torch.long,
|
| 526 |
+
) # (1, t)
|
| 527 |
|
| 528 |
+
res_type_ids = torch.tensor(residue_token_ids, dtype=torch.long) # (t,)
|
| 529 |
+
res_type = one_hot(
|
| 530 |
+
res_type_ids, num_classes=const.num_tokens
|
| 531 |
+
).unsqueeze(0) # (1, t, n_res_type)
|
| 532 |
|
| 533 |
# token_bonds encodes explicit covalent cross-links from structure bonds,
|
| 534 |
# NOT backbone peptide bonds (those are implicit via residue_index + asym_id).
|
| 535 |
# For a standard single-chain protein without cross-links, this is all zeros.
|
| 536 |
# This matches the official Boltz2 featurizer (featurizerv2.py lines 696-705).
|
| 537 |
+
token_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.float32) # (t, t)
|
| 538 |
+
type_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.long) # (t, t)
|
| 539 |
+
token_bonds = token_bonds.unsqueeze(0).unsqueeze(-1) # (1, t, t, 1)
|
| 540 |
+
type_bonds = type_bonds.unsqueeze(0) # (1, t, t)
|
| 541 |
|
| 542 |
+
token_pad_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 543 |
+
token_resolved_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 544 |
+
token_disto_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 545 |
|
| 546 |
num_contact_classes = len(const.contact_conditioning_info)
|
| 547 |
unspecified_id = const.contact_conditioning_info["UNSPECIFIED"]
|
|
|
|
| 549 |
(num_tokens, num_tokens),
|
| 550 |
fill_value=unspecified_id,
|
| 551 |
dtype=torch.long,
|
| 552 |
+
) # (t, t)
|
| 553 |
contact_conditioning = one_hot(
|
| 554 |
contact_ids,
|
| 555 |
num_classes=num_contact_classes,
|
| 556 |
+
).unsqueeze(0) # (1, t, t, n_contact)
|
| 557 |
+
contact_threshold = torch.zeros(
|
| 558 |
+
(1, num_tokens, num_tokens), dtype=torch.float32
|
| 559 |
+
) # (1, t, t)
|
| 560 |
|
| 561 |
if "x-ray diffraction" not in const.method_types_ids:
|
| 562 |
raise RuntimeError("Boltz2 method metadata omits x-ray diffraction.")
|
|
|
|
| 564 |
(1, num_tokens),
|
| 565 |
fill_value=const.method_types_ids["x-ray diffraction"],
|
| 566 |
dtype=torch.long,
|
| 567 |
+
) # (1, t)
|
| 568 |
+
modified = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 569 |
+
cyclic_period = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 570 |
+
affinity_token_mask = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 571 |
|
| 572 |
+
ref_pos = atom_positions.unsqueeze(0) # (1, a, 3)
|
| 573 |
+
atom_pad_mask = torch.ones((1, num_atoms), dtype=torch.float32) # (1, a)
|
| 574 |
+
atom_resolved_mask = torch.ones((1, num_atoms), dtype=torch.bool) # (1, a)
|
| 575 |
|
| 576 |
atom_name_codes = torch.stack(
|
| 577 |
[_atom_name_to_codes(atom_name) for atom_name in template.atom_names],
|
| 578 |
dim=0,
|
| 579 |
+
) # (a, 4)
|
| 580 |
+
ref_atom_name_chars = one_hot(
|
| 581 |
+
atom_name_codes, num_classes=64
|
| 582 |
+
).unsqueeze(0) # (1, a, 4, 64)
|
| 583 |
|
| 584 |
atomic_numbers = []
|
| 585 |
for element in template.atom_elements:
|
|
|
|
| 592 |
ref_element = one_hot(
|
| 593 |
torch.tensor(atomic_numbers, dtype=torch.long),
|
| 594 |
num_classes=const.num_elements,
|
| 595 |
+
).unsqueeze(0) # (1, a, n_element)
|
| 596 |
|
| 597 |
ref_charge_values = []
|
| 598 |
ref_chirality_values = []
|
|
|
|
| 606 |
ref_chirality_values.append(
|
| 607 |
2 if atom_name in _CHIRAL_ATOMS.get(residue_name, frozenset()) else 0
|
| 608 |
)
|
| 609 |
+
ref_charge = torch.tensor(ref_charge_values, dtype=torch.float32).unsqueeze(0) # (1, a)
|
| 610 |
+
ref_chirality = torch.tensor(
|
| 611 |
+
ref_chirality_values, dtype=torch.long
|
| 612 |
+
).unsqueeze(0) # (1, a)
|
| 613 |
+
ref_space_uid = torch.tensor(
|
| 614 |
+
template.atom_residue_index, dtype=torch.long
|
| 615 |
+
).unsqueeze(0) # (1, a)
|
| 616 |
|
| 617 |
atom_to_token = one_hot(
|
| 618 |
torch.tensor(template.atom_residue_index, dtype=torch.long),
|
| 619 |
num_classes=num_tokens,
|
| 620 |
+
).unsqueeze(0) # (1, a, t)
|
| 621 |
token_to_rep_atom = one_hot(
|
| 622 |
torch.tensor(residue_disto_atom_idx, dtype=torch.long),
|
| 623 |
num_classes=num_atoms,
|
| 624 |
+
).unsqueeze(0) # (1, t, a)
|
| 625 |
token_to_center_atom = one_hot(
|
| 626 |
torch.tensor(residue_center_atom_idx, dtype=torch.long),
|
| 627 |
num_classes=num_atoms,
|
| 628 |
+
).unsqueeze(0) # (1, t, a)
|
| 629 |
+
r_set_to_rep_atom = token_to_center_atom.clone() # (1, t, a)
|
| 630 |
|
| 631 |
num_backbone_classes = (
|
| 632 |
1 + len(const.protein_backbone_atom_index) + len(const.nucleic_backbone_atom_index)
|
|
|
|
| 640 |
atom_backbone_feat = one_hot(
|
| 641 |
torch.tensor(backbone_ids, dtype=torch.long),
|
| 642 |
num_classes=num_backbone_classes,
|
| 643 |
+
).unsqueeze(0) # (1, a, n_backbone)
|
| 644 |
|
| 645 |
# X contains no observed coordinates for sequence-only inference.
|
| 646 |
+
coords = torch.zeros((1, 1, num_atoms, 3), dtype=torch.float32) # (1, 1, a, 3)
|
| 647 |
disto_coords_ensemble = torch.zeros(
|
| 648 |
(1, 1, num_tokens, 3),
|
| 649 |
dtype=torch.float32,
|
| 650 |
+
) # (1, 1, t, 3)
|
| 651 |
|
| 652 |
+
bfactor = torch.zeros((1, num_atoms), dtype=torch.float32) # (1, a)
|
| 653 |
+
atom_plddt = torch.ones((1, num_atoms), dtype=torch.float32) # (1, a)
|
| 654 |
|
| 655 |
if atoms_per_window_queries <= 0:
|
| 656 |
raise ValueError("atoms_per_window_queries must be positive.")
|
|
|
|
| 658 |
(num_atoms - 1) // atoms_per_window_queries + 1
|
| 659 |
) * atoms_per_window_queries - num_atoms
|
| 660 |
if pad_atoms > 0:
|
| 661 |
+
ref_pos = torch.nn.functional.pad(
|
| 662 |
+
ref_pos, (0, 0, 0, pad_atoms), value=0.0
|
| 663 |
+
) # (1, a_p, 3)
|
| 664 |
+
atom_pad_mask = torch.nn.functional.pad(
|
| 665 |
+
atom_pad_mask, (0, pad_atoms), value=0.0
|
| 666 |
+
) # (1, a_p)
|
| 667 |
atom_resolved_mask = torch.nn.functional.pad(
|
| 668 |
atom_resolved_mask,
|
| 669 |
(0, pad_atoms),
|
| 670 |
value=0.0,
|
| 671 |
+
) # (1, a_p)
|
| 672 |
ref_atom_name_chars = torch.nn.functional.pad(
|
| 673 |
ref_atom_name_chars,
|
| 674 |
(0, 0, 0, 0, 0, pad_atoms),
|
| 675 |
value=0.0,
|
| 676 |
+
) # (1, a_p, 4, 64)
|
| 677 |
+
ref_element = torch.nn.functional.pad(
|
| 678 |
+
ref_element, (0, 0, 0, pad_atoms), value=0.0
|
| 679 |
+
) # (1, a_p, n_element)
|
| 680 |
+
ref_charge = torch.nn.functional.pad(
|
| 681 |
+
ref_charge, (0, pad_atoms), value=0.0
|
| 682 |
+
) # (1, a_p)
|
| 683 |
+
ref_chirality = torch.nn.functional.pad(
|
| 684 |
+
ref_chirality, (0, pad_atoms), value=0
|
| 685 |
+
) # (1, a_p)
|
| 686 |
atom_backbone_feat = torch.nn.functional.pad(
|
| 687 |
atom_backbone_feat,
|
| 688 |
(0, 0, 0, pad_atoms),
|
| 689 |
value=0.0,
|
| 690 |
+
) # (1, a_p, n_backbone)
|
| 691 |
+
ref_space_uid = torch.nn.functional.pad(
|
| 692 |
+
ref_space_uid, (0, pad_atoms), value=0
|
| 693 |
+
) # (1, a_p)
|
| 694 |
+
coords = torch.nn.functional.pad(
|
| 695 |
+
coords, (0, 0, 0, pad_atoms), value=0.0
|
| 696 |
+
) # (1, 1, a_p, 3)
|
| 697 |
+
atom_to_token = torch.nn.functional.pad(
|
| 698 |
+
atom_to_token, (0, 0, 0, pad_atoms), value=0.0
|
| 699 |
+
) # (1, a_p, t)
|
| 700 |
token_to_rep_atom = torch.nn.functional.pad(
|
| 701 |
token_to_rep_atom,
|
| 702 |
(0, pad_atoms),
|
| 703 |
value=0.0,
|
| 704 |
+
) # (1, t, a_p)
|
| 705 |
token_to_center_atom = torch.nn.functional.pad(
|
| 706 |
token_to_center_atom,
|
| 707 |
(0, pad_atoms),
|
| 708 |
value=0.0,
|
| 709 |
+
) # (1, t, a_p)
|
| 710 |
r_set_to_rep_atom = torch.nn.functional.pad(
|
| 711 |
r_set_to_rep_atom,
|
| 712 |
(0, pad_atoms),
|
| 713 |
value=0.0,
|
| 714 |
+
) # (1, t, a_p)
|
| 715 |
+
bfactor = torch.nn.functional.pad(
|
| 716 |
+
bfactor, (0, pad_atoms), value=0.0
|
| 717 |
+
) # (1, a_p)
|
| 718 |
+
atom_plddt = torch.nn.functional.pad(
|
| 719 |
+
atom_plddt, (0, pad_atoms), value=0.0
|
| 720 |
+
) # (1, a_p)
|
| 721 |
|
| 722 |
frames_idx = torch.tensor(
|
| 723 |
residue_frame_atom_idx_flat,
|
| 724 |
dtype=torch.long,
|
| 725 |
+
).reshape(num_tokens, 3) # (t, 3)
|
| 726 |
+
frames_idx = frames_idx.unsqueeze(0).unsqueeze(1) # (1, 1, t, 3)
|
| 727 |
+
frame_resolved_mask = torch.zeros(
|
| 728 |
+
(1, 1, num_tokens), dtype=torch.bool
|
| 729 |
+
) # (1, 1, t)
|
| 730 |
+
|
| 731 |
+
msa = (
|
| 732 |
+
torch.tensor(residue_token_ids, dtype=torch.long).unsqueeze(0).unsqueeze(0)
|
| 733 |
+
) # (1, 1, t)
|
| 734 |
+
msa_paired = torch.ones((1, 1, num_tokens), dtype=torch.float32) # (1, 1, t)
|
| 735 |
+
deletion_value = torch.zeros(
|
| 736 |
+
(1, 1, num_tokens), dtype=torch.float32
|
| 737 |
+
) # (1, 1, t)
|
| 738 |
+
has_deletion = torch.zeros((1, 1, num_tokens), dtype=torch.bool) # (1, 1, t)
|
| 739 |
+
msa_mask = torch.ones((1, 1, num_tokens), dtype=torch.long) # (1, 1, t)
|
| 740 |
+
deletion_mean = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 741 |
profile = (
|
| 742 |
one_hot(
|
| 743 |
torch.tensor(residue_token_ids, dtype=torch.long),
|
|
|
|
| 745 |
)
|
| 746 |
.float()
|
| 747 |
.unsqueeze(0)
|
| 748 |
+
) # (1, t, n_res_type)
|
| 749 |
|
| 750 |
template_restype = one_hot(
|
| 751 |
torch.zeros((1, 1, num_tokens), dtype=torch.long),
|
| 752 |
num_classes=const.num_tokens,
|
| 753 |
+
) # (1, 1, t, n_res_type)
|
| 754 |
+
template_frame_rot = torch.zeros(
|
| 755 |
+
(1, 1, num_tokens, 3, 3), dtype=torch.float32
|
| 756 |
+
) # (1, 1, t, 3, 3)
|
| 757 |
+
template_frame_t = torch.zeros(
|
| 758 |
+
(1, 1, num_tokens, 3), dtype=torch.float32
|
| 759 |
+
) # (1, 1, t, 3)
|
| 760 |
+
template_cb = torch.zeros(
|
| 761 |
+
(1, 1, num_tokens, 3), dtype=torch.float32
|
| 762 |
+
) # (1, 1, t, 3)
|
| 763 |
+
template_ca = torch.zeros(
|
| 764 |
+
(1, 1, num_tokens, 3), dtype=torch.float32
|
| 765 |
+
) # (1, 1, t, 3)
|
| 766 |
+
template_mask_cb = torch.zeros(
|
| 767 |
+
(1, 1, num_tokens), dtype=torch.float32
|
| 768 |
+
) # (1, 1, t)
|
| 769 |
+
template_mask_frame = torch.zeros(
|
| 770 |
+
(1, 1, num_tokens), dtype=torch.float32
|
| 771 |
+
) # (1, 1, t)
|
| 772 |
+
template_mask = torch.zeros(
|
| 773 |
+
(1, 1, num_tokens), dtype=torch.float32
|
| 774 |
+
) # (1, 1, t)
|
| 775 |
+
query_to_template = torch.zeros(
|
| 776 |
+
(1, 1, num_tokens), dtype=torch.long
|
| 777 |
+
) # (1, 1, t)
|
| 778 |
+
visibility_ids = torch.zeros(
|
| 779 |
+
(1, 1, num_tokens), dtype=torch.float32
|
| 780 |
+
) # (1, 1, t)
|
| 781 |
|
| 782 |
disto_target = torch.zeros(
|
| 783 |
(1, num_tokens, num_tokens, 1, num_bins),
|
| 784 |
dtype=torch.float32,
|
| 785 |
+
) # (1, t, t, 1, n_bin)
|
| 786 |
+
disto_target[..., 0] = 1.0 # (1, t, t, 1)
|
| 787 |
+
disto_center = torch.zeros(
|
| 788 |
+
(1, num_tokens, 3), dtype=torch.float32
|
| 789 |
+
) # (1, t, 3)
|
| 790 |
|
| 791 |
features: dict[str, torch.Tensor] = {
|
| 792 |
"token_index": token_index,
|
|
|
|
| 847 |
"visibility_ids": visibility_ids,
|
| 848 |
}
|
| 849 |
|
| 850 |
+
return features, template # each feature shape is traced at its binding above
|
fastplms/models/boltz/modeling_boltz2.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
| 1 |
import copy
|
| 2 |
import inspect
|
| 3 |
import random
|
|
|
|
|
|
|
|
|
|
| 4 |
from collections.abc import Iterator, Mapping, Sequence
|
| 5 |
from contextlib import contextmanager, nullcontext
|
| 6 |
from dataclasses import dataclass
|
| 7 |
from typing import Any, ClassVar
|
| 8 |
-
|
| 9 |
-
import numpy as np
|
| 10 |
-
import torch
|
| 11 |
-
import torch.nn as nn
|
| 12 |
from torch import Tensor
|
| 13 |
from transformers import PretrainedConfig, PreTrainedModel
|
| 14 |
from transformers.modeling_outputs import ModelOutput
|
|
@@ -113,6 +112,7 @@ def _require_key(mapping: dict[str, Any], key: str) -> Any:
|
|
| 113 |
|
| 114 |
|
| 115 |
def _state_dict_without_wrappers(state_dict: dict[str, Tensor]) -> dict[str, Tensor]:
|
|
|
|
| 116 |
cleaned: dict[str, Tensor] = {}
|
| 117 |
for key, value in state_dict.items():
|
| 118 |
if key.startswith("ema."):
|
|
@@ -122,13 +122,13 @@ def _state_dict_without_wrappers(state_dict: dict[str, Tensor]) -> dict[str, Ten
|
|
| 122 |
new_key = new_key[len("model.") :]
|
| 123 |
if new_key.startswith("module."):
|
| 124 |
new_key = new_key[len("module.") :]
|
| 125 |
-
cleaned[new_key] = value
|
| 126 |
return cleaned
|
| 127 |
|
| 128 |
|
| 129 |
def _to_cpu_detached(value: Any) -> Any:
|
| 130 |
if torch.is_tensor(value):
|
| 131 |
-
return value.detach().cpu()
|
| 132 |
if isinstance(value, dict):
|
| 133 |
out: dict[Any, Any] = {}
|
| 134 |
for key, nested_value in value.items():
|
|
@@ -145,8 +145,8 @@ def _to_cpu_detached(value: Any) -> Any:
|
|
| 145 |
class _RandomState:
|
| 146 |
python: object
|
| 147 |
numpy: tuple[Any, ...]
|
| 148 |
-
torch_cpu: Tensor
|
| 149 |
-
torch_cuda: list[Tensor] | None
|
| 150 |
|
| 151 |
|
| 152 |
@contextmanager
|
|
@@ -212,46 +212,46 @@ def _filtered_kwargs(target: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
| 212 |
class Boltz2ModelOutput(ModelOutput):
|
| 213 |
"""Raw Boltz2 inference output with standard AutoModel controls."""
|
| 214 |
|
| 215 |
-
last_hidden_state: torch.Tensor | None = None
|
| 216 |
-
hidden_states: tuple[torch.Tensor, ...] | None = None
|
| 217 |
-
attentions: tuple[torch.Tensor, ...] | None = None
|
| 218 |
-
pdistogram: torch.Tensor | None = None
|
| 219 |
-
s: torch.Tensor | None = None
|
| 220 |
-
z: torch.Tensor | None = None
|
| 221 |
-
sample_atom_coords: torch.Tensor | None = None
|
| 222 |
-
diff_token_repr: torch.Tensor | None = None
|
| 223 |
-
s_conf: torch.Tensor | None = None
|
| 224 |
-
z_conf: torch.Tensor | None = None
|
| 225 |
-
pde_logits: torch.Tensor | None = None
|
| 226 |
-
plddt_logits: torch.Tensor | None = None
|
| 227 |
-
resolved_logits: torch.Tensor | None = None
|
| 228 |
-
pde: torch.Tensor | None = None
|
| 229 |
-
plddt: torch.Tensor | None = None
|
| 230 |
-
complex_plddt: torch.Tensor | None = None
|
| 231 |
-
complex_iplddt: torch.Tensor | None = None
|
| 232 |
-
complex_pde: torch.Tensor | None = None
|
| 233 |
-
complex_ipde: torch.Tensor | None = None
|
| 234 |
-
pae_logits: torch.Tensor | None = None
|
| 235 |
-
pae: torch.Tensor | None = None
|
| 236 |
-
ptm: torch.Tensor | None = None
|
| 237 |
-
iptm: torch.Tensor | None = None
|
| 238 |
-
ligand_iptm: torch.Tensor | None = None
|
| 239 |
-
protein_iptm: torch.Tensor | None = None
|
| 240 |
pair_chains_iptm: Any = None
|
| 241 |
|
| 242 |
|
| 243 |
@dataclass
|
| 244 |
class Boltz2StructureOutput(ModelOutput):
|
| 245 |
-
sample_atom_coords: torch.Tensor | None = None
|
| 246 |
-
atom_pad_mask: torch.Tensor | None = None
|
| 247 |
-
plddt: torch.Tensor | None = None
|
| 248 |
-
confidence_score: torch.Tensor | None = None
|
| 249 |
-
complex_plddt: torch.Tensor | None = None
|
| 250 |
-
iptm: torch.Tensor | None = None
|
| 251 |
-
ptm: torch.Tensor | None = None
|
| 252 |
sequence: str | None = None
|
| 253 |
structure_template: ProteinStructureTemplate | None = None
|
| 254 |
-
raw_output: dict[str, torch.Tensor] | None = None
|
| 255 |
seed: int | None = None
|
| 256 |
|
| 257 |
|
|
@@ -573,41 +573,49 @@ class Boltz2InferenceCore(nn.Module):
|
|
| 573 |
run_confidence_sequentially: bool = True,
|
| 574 |
detach_confidence: bool = True,
|
| 575 |
) -> dict[str, Tensor]:
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
z_init =
|
| 582 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
if self.bond_type_feature:
|
| 584 |
-
z_init = z_init + self.token_bonds_type(
|
| 585 |
-
|
|
|
|
|
|
|
| 586 |
|
| 587 |
-
s = torch.zeros_like(s_init)
|
| 588 |
-
z = torch.zeros_like(z_init)
|
| 589 |
-
mask = feats["token_pad_mask"].float()
|
| 590 |
-
pair_mask = mask[:, :, None] * mask[:, None, :]
|
| 591 |
|
| 592 |
if self.run_trunk_and_structure:
|
| 593 |
for _ in range(recycling_steps + 1):
|
| 594 |
-
s = s_init + self.s_recycle(self.s_norm(s))
|
| 595 |
-
z = z_init + self.z_recycle(self.z_norm(z))
|
| 596 |
z = z + self.msa_module(
|
| 597 |
z,
|
| 598 |
s_inputs,
|
| 599 |
feats,
|
| 600 |
use_kernels=self.use_kernels,
|
| 601 |
-
)
|
| 602 |
s, z = self.pairformer_module(
|
| 603 |
s,
|
| 604 |
z,
|
| 605 |
mask=mask,
|
| 606 |
pair_mask=pair_mask,
|
| 607 |
use_kernels=self.use_kernels,
|
| 608 |
-
)
|
| 609 |
|
| 610 |
-
pdistogram = self.distogram_module(z)
|
| 611 |
output: dict[str, Tensor] = {
|
| 612 |
"pdistogram": pdistogram,
|
| 613 |
"s": s,
|
|
@@ -622,7 +630,7 @@ class Boltz2InferenceCore(nn.Module):
|
|
| 622 |
relative_position_encoding=relative_position_encoding,
|
| 623 |
feats=feats,
|
| 624 |
)
|
| 625 |
-
)
|
| 626 |
diffusion_conditioning = {
|
| 627 |
"q": q,
|
| 628 |
"c": c,
|
|
@@ -642,29 +650,31 @@ class Boltz2InferenceCore(nn.Module):
|
|
| 642 |
max_parallel_samples=max_parallel_samples,
|
| 643 |
steering_args=self.steering_args,
|
| 644 |
diffusion_conditioning=diffusion_conditioning,
|
| 645 |
-
)
|
| 646 |
output.update(struct_out)
|
| 647 |
|
| 648 |
if self.confidence_prediction:
|
| 649 |
if self.skip_run_structure:
|
| 650 |
-
x_pred = feats["coords"].repeat_interleave(
|
|
|
|
|
|
|
| 651 |
else:
|
| 652 |
if "sample_atom_coords" not in output:
|
| 653 |
raise RuntimeError("Structure sampling did not produce sample_atom_coords.")
|
| 654 |
-
x_pred = output["sample_atom_coords"]
|
| 655 |
|
| 656 |
if detach_confidence:
|
| 657 |
-
s_inputs_c = s_inputs.detach()
|
| 658 |
-
s_c = s.detach()
|
| 659 |
-
z_c = z.detach()
|
| 660 |
-
x_pred_c = x_pred.detach()
|
| 661 |
-
pdist_c = output["pdistogram"][:, :, :, 0].detach()
|
| 662 |
else:
|
| 663 |
-
s_inputs_c = s_inputs
|
| 664 |
-
s_c = s
|
| 665 |
-
z_c = z
|
| 666 |
-
x_pred_c = x_pred
|
| 667 |
-
pdist_c = output["pdistogram"][:, :, :, 0]
|
| 668 |
|
| 669 |
output.update(
|
| 670 |
self.confidence_module(
|
|
@@ -680,7 +690,7 @@ class Boltz2InferenceCore(nn.Module):
|
|
| 680 |
)
|
| 681 |
)
|
| 682 |
|
| 683 |
-
return output
|
| 684 |
|
| 685 |
|
| 686 |
class Boltz2Model(PreTrainedModel):
|
|
@@ -698,6 +708,7 @@ class Boltz2Model(PreTrainedModel):
|
|
| 698 |
return
|
| 699 |
|
| 700 |
def _detied_state_dict(self) -> dict[str, Tensor]:
|
|
|
|
| 701 |
raw_state = self.state_dict()
|
| 702 |
seen_ptrs: dict[int, str] = {}
|
| 703 |
out: dict[str, Tensor] = {}
|
|
@@ -705,13 +716,13 @@ class Boltz2Model(PreTrainedModel):
|
|
| 705 |
if torch.is_tensor(tensor):
|
| 706 |
ptr = tensor.untyped_storage().data_ptr()
|
| 707 |
if ptr in seen_ptrs:
|
| 708 |
-
out[key] = tensor.clone()
|
| 709 |
else:
|
| 710 |
seen_ptrs[ptr] = key
|
| 711 |
-
out[key] = tensor
|
| 712 |
else:
|
| 713 |
-
out[key] = tensor
|
| 714 |
-
return out
|
| 715 |
|
| 716 |
def save_pretrained(self, save_directory: str, **kwargs: Any) -> None:
|
| 717 |
if "safe_serialization" not in kwargs:
|
|
@@ -757,7 +768,7 @@ class Boltz2Model(PreTrainedModel):
|
|
| 757 |
checkpoint_path,
|
| 758 |
map_location=map_location,
|
| 759 |
weights_only=False,
|
| 760 |
-
)
|
| 761 |
if not isinstance(checkpoint, dict):
|
| 762 |
raise TypeError("Checkpoint must deserialize to a dictionary.")
|
| 763 |
_require_key(checkpoint, "hyper_parameters")
|
|
@@ -766,7 +777,7 @@ class Boltz2Model(PreTrainedModel):
|
|
| 766 |
hparams = checkpoint["hyper_parameters"]
|
| 767 |
if not isinstance(hparams, dict):
|
| 768 |
raise TypeError("Checkpoint hyper_parameters must be a dictionary.")
|
| 769 |
-
state_dict = checkpoint["state_dict"]
|
| 770 |
if not isinstance(state_dict, dict):
|
| 771 |
raise TypeError("Checkpoint state_dict must be a dictionary.")
|
| 772 |
|
|
@@ -778,7 +789,7 @@ class Boltz2Model(PreTrainedModel):
|
|
| 778 |
default_diffusion_samples=default_diffusion_samples,
|
| 779 |
)
|
| 780 |
model = cls(config)
|
| 781 |
-
cleaned = _state_dict_without_wrappers(state_dict)
|
| 782 |
target_keys = set(model.core.state_dict().keys())
|
| 783 |
for key in target_keys:
|
| 784 |
if ".attention.norm_s." in key:
|
|
@@ -789,7 +800,7 @@ class Boltz2Model(PreTrainedModel):
|
|
| 789 |
filtered: dict[str, Tensor] = {}
|
| 790 |
for key, value in cleaned.items():
|
| 791 |
if key in target_keys:
|
| 792 |
-
filtered[key] = value
|
| 793 |
|
| 794 |
missing = sorted(target_keys.difference(filtered.keys()))
|
| 795 |
if missing:
|
|
@@ -826,6 +837,7 @@ class Boltz2Model(PreTrainedModel):
|
|
| 826 |
output_hidden_states: bool | None = None,
|
| 827 |
return_dict: bool | None = None,
|
| 828 |
) -> Boltz2ModelOutput | tuple[Any, ...]:
|
|
|
|
| 829 |
output_attentions = (
|
| 830 |
self.config.output_attentions if output_attentions is None else output_attentions
|
| 831 |
)
|
|
@@ -854,9 +866,9 @@ class Boltz2Model(PreTrainedModel):
|
|
| 854 |
max_parallel_samples=max_parallel_samples,
|
| 855 |
run_confidence_sequentially=run_confidence_sequentially,
|
| 856 |
detach_confidence=detach_confidence,
|
| 857 |
-
)
|
| 858 |
-
token_state = raw_output.get("s")
|
| 859 |
-
pair_state = raw_output.get("z")
|
| 860 |
model_output = Boltz2ModelOutput(
|
| 861 |
last_hidden_state=token_state,
|
| 862 |
hidden_states=(token_state, pair_state)
|
|
@@ -871,16 +883,19 @@ class Boltz2Model(PreTrainedModel):
|
|
| 871 |
feats: dict[str, Tensor],
|
| 872 |
float_dtype: torch.dtype = torch.float32,
|
| 873 |
) -> dict[str, Tensor]:
|
|
|
|
| 874 |
moved: dict[str, Tensor] = {}
|
| 875 |
for key, value in feats.items():
|
| 876 |
if torch.is_tensor(value):
|
| 877 |
if value.is_floating_point():
|
| 878 |
-
moved[key] = value.to(
|
|
|
|
|
|
|
| 879 |
else:
|
| 880 |
-
moved[key] = value.to(device=self.device)
|
| 881 |
else:
|
| 882 |
-
moved[key] = value
|
| 883 |
-
return moved
|
| 884 |
|
| 885 |
def predict_structure(
|
| 886 |
self,
|
|
@@ -914,8 +929,10 @@ class Boltz2Model(PreTrainedModel):
|
|
| 914 |
atoms_per_window_queries=(
|
| 915 |
self.core.input_embedder.atom_encoder.atoms_per_window_queries
|
| 916 |
),
|
| 917 |
-
)
|
| 918 |
-
feats = self._to_model_device(
|
|
|
|
|
|
|
| 919 |
autocast_context = (
|
| 920 |
torch.autocast(device_type="cuda", dtype=torch.bfloat16)
|
| 921 |
if self.device.type == "cuda"
|
|
@@ -930,32 +947,42 @@ class Boltz2Model(PreTrainedModel):
|
|
| 930 |
max_parallel_samples=max_parallel_samples,
|
| 931 |
run_confidence_sequentially=run_confidence_sequentially,
|
| 932 |
return_dict=True,
|
| 933 |
-
)
|
| 934 |
|
| 935 |
-
sample_atom_coords_value = output.get(
|
|
|
|
|
|
|
| 936 |
if not isinstance(sample_atom_coords_value, torch.Tensor):
|
| 937 |
raise RuntimeError("Boltz2 structure sampling did not return coordinate tensors.")
|
| 938 |
-
sample_atom_coords = sample_atom_coords_value.detach().cpu()
|
| 939 |
-
non_finite_mask = torch.logical_not(
|
|
|
|
|
|
|
| 940 |
if torch.any(non_finite_mask):
|
| 941 |
raise RuntimeError(
|
| 942 |
"sample_atom_coords contains non-finite values. "
|
| 943 |
f"Non-finite count: {int(non_finite_mask.sum().item())}"
|
| 944 |
)
|
| 945 |
-
atom_pad_mask = feats["atom_pad_mask"][0].detach().cpu()
|
| 946 |
-
plddt =
|
|
|
|
|
|
|
| 947 |
complex_plddt = (
|
| 948 |
output["complex_plddt"].detach().cpu() if "complex_plddt" in output else None
|
| 949 |
-
)
|
| 950 |
-
iptm =
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
if (complex_plddt is not None) and (iptm is not None) and (ptm is not None):
|
| 955 |
if torch.allclose(iptm, torch.zeros_like(iptm)):
|
| 956 |
-
confidence_score = (4 * complex_plddt + ptm) / 5
|
| 957 |
else:
|
| 958 |
-
confidence_score = (4 * complex_plddt + iptm) / 5
|
| 959 |
|
| 960 |
return Boltz2StructureOutput(
|
| 961 |
sample_atom_coords=sample_atom_coords,
|
|
|
|
| 1 |
import copy
|
| 2 |
import inspect
|
| 3 |
import random
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
from collections.abc import Iterator, Mapping, Sequence
|
| 8 |
from contextlib import contextmanager, nullcontext
|
| 9 |
from dataclasses import dataclass
|
| 10 |
from typing import Any, ClassVar
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
from torch import Tensor
|
| 12 |
from transformers import PretrainedConfig, PreTrainedModel
|
| 13 |
from transformers.modeling_outputs import ModelOutput
|
|
|
|
| 112 |
|
| 113 |
|
| 114 |
def _state_dict_without_wrappers(state_dict: dict[str, Tensor]) -> dict[str, Tensor]:
|
| 115 |
+
# Every state tensor has its parameter-defined shape (...).
|
| 116 |
cleaned: dict[str, Tensor] = {}
|
| 117 |
for key, value in state_dict.items():
|
| 118 |
if key.startswith("ema."):
|
|
|
|
| 122 |
new_key = new_key[len("model.") :]
|
| 123 |
if new_key.startswith("module."):
|
| 124 |
new_key = new_key[len("module.") :]
|
| 125 |
+
cleaned[new_key] = value # (...)
|
| 126 |
return cleaned
|
| 127 |
|
| 128 |
|
| 129 |
def _to_cpu_detached(value: Any) -> Any:
|
| 130 |
if torch.is_tensor(value):
|
| 131 |
+
return value.detach().cpu() # same shape (...)
|
| 132 |
if isinstance(value, dict):
|
| 133 |
out: dict[Any, Any] = {}
|
| 134 |
for key, nested_value in value.items():
|
|
|
|
| 145 |
class _RandomState:
|
| 146 |
python: object
|
| 147 |
numpy: tuple[Any, ...]
|
| 148 |
+
torch_cpu: Tensor # (n_cpu_rng_state,)
|
| 149 |
+
torch_cuda: list[Tensor] | None # each: (n_cuda_rng_state,)
|
| 150 |
|
| 151 |
|
| 152 |
@contextmanager
|
|
|
|
| 212 |
class Boltz2ModelOutput(ModelOutput):
|
| 213 |
"""Raw Boltz2 inference output with standard AutoModel controls."""
|
| 214 |
|
| 215 |
+
last_hidden_state: torch.Tensor | None = None # (b, t, d_s) or None
|
| 216 |
+
hidden_states: tuple[torch.Tensor, ...] | None = None # each shape traced at source
|
| 217 |
+
attentions: tuple[torch.Tensor, ...] | None = None # unsupported; always None
|
| 218 |
+
pdistogram: torch.Tensor | None = None # (b, t, t, n_distogram, n_bin) or None
|
| 219 |
+
s: torch.Tensor | None = None # (b, t, d_s) or None
|
| 220 |
+
z: torch.Tensor | None = None # (b, t, t, d_z) or None
|
| 221 |
+
sample_atom_coords: torch.Tensor | None = None # (b * m, a, 3) or None
|
| 222 |
+
diff_token_repr: torch.Tensor | None = None # (...) or None
|
| 223 |
+
s_conf: torch.Tensor | None = None # (...) or None
|
| 224 |
+
z_conf: torch.Tensor | None = None # (...) or None
|
| 225 |
+
pde_logits: torch.Tensor | None = None # (...) or None
|
| 226 |
+
plddt_logits: torch.Tensor | None = None # (...) or None
|
| 227 |
+
resolved_logits: torch.Tensor | None = None # (...) or None
|
| 228 |
+
pde: torch.Tensor | None = None # (...) or None
|
| 229 |
+
plddt: torch.Tensor | None = None # (...) or None
|
| 230 |
+
complex_plddt: torch.Tensor | None = None # (...) or None
|
| 231 |
+
complex_iplddt: torch.Tensor | None = None # (...) or None
|
| 232 |
+
complex_pde: torch.Tensor | None = None # (...) or None
|
| 233 |
+
complex_ipde: torch.Tensor | None = None # (...) or None
|
| 234 |
+
pae_logits: torch.Tensor | None = None # (...) or None
|
| 235 |
+
pae: torch.Tensor | None = None # (...) or None
|
| 236 |
+
ptm: torch.Tensor | None = None # (...) or None
|
| 237 |
+
iptm: torch.Tensor | None = None # (...) or None
|
| 238 |
+
ligand_iptm: torch.Tensor | None = None # (...) or None
|
| 239 |
+
protein_iptm: torch.Tensor | None = None # (...) or None
|
| 240 |
pair_chains_iptm: Any = None
|
| 241 |
|
| 242 |
|
| 243 |
@dataclass
|
| 244 |
class Boltz2StructureOutput(ModelOutput):
|
| 245 |
+
sample_atom_coords: torch.Tensor | None = None # (m, a_p, 3) or None
|
| 246 |
+
atom_pad_mask: torch.Tensor | None = None # (a_p,) or None
|
| 247 |
+
plddt: torch.Tensor | None = None # (m, a_p) or None
|
| 248 |
+
confidence_score: torch.Tensor | None = None # (m,) or None
|
| 249 |
+
complex_plddt: torch.Tensor | None = None # (m,) or None
|
| 250 |
+
iptm: torch.Tensor | None = None # (m,) or None
|
| 251 |
+
ptm: torch.Tensor | None = None # (m,) or None
|
| 252 |
sequence: str | None = None
|
| 253 |
structure_template: ProteinStructureTemplate | None = None
|
| 254 |
+
raw_output: dict[str, torch.Tensor] | None = None # values keep model-output shapes
|
| 255 |
seed: int | None = None
|
| 256 |
|
| 257 |
|
|
|
|
| 573 |
run_confidence_sequentially: bool = True,
|
| 574 |
detach_confidence: bool = True,
|
| 575 |
) -> dict[str, Tensor]:
|
| 576 |
+
# b is the batch size, t the token count, a the padded atom count,
|
| 577 |
+
# d_s the token width, d_z the pair width, and m the diffusion multiplicity.
|
| 578 |
+
s_inputs = self.input_embedder(feats) # (b, t, d_s)
|
| 579 |
+
s_init = self.s_init(s_inputs) # (b, t, d_s)
|
| 580 |
+
|
| 581 |
+
z_init = (
|
| 582 |
+
self.z_init_1(s_inputs)[:, :, None] + self.z_init_2(s_inputs)[:, None, :]
|
| 583 |
+
) # (b, t, t, d_z)
|
| 584 |
+
relative_position_encoding = self.rel_pos(feats) # (b, t, t, d_z)
|
| 585 |
+
z_init = z_init + relative_position_encoding # (b, t, t, d_z)
|
| 586 |
+
z_init = z_init + self.token_bonds(
|
| 587 |
+
feats["token_bonds"].float()
|
| 588 |
+
) # (b, t, t, d_z)
|
| 589 |
if self.bond_type_feature:
|
| 590 |
+
z_init = z_init + self.token_bonds_type(
|
| 591 |
+
feats["type_bonds"].long()
|
| 592 |
+
) # (b, t, t, d_z)
|
| 593 |
+
z_init = z_init + self.contact_conditioning(feats) # (b, t, t, d_z)
|
| 594 |
|
| 595 |
+
s = torch.zeros_like(s_init) # (b, t, d_s)
|
| 596 |
+
z = torch.zeros_like(z_init) # (b, t, t, d_z)
|
| 597 |
+
mask = feats["token_pad_mask"].float() # (b, t)
|
| 598 |
+
pair_mask = mask[:, :, None] * mask[:, None, :] # (b, t, t)
|
| 599 |
|
| 600 |
if self.run_trunk_and_structure:
|
| 601 |
for _ in range(recycling_steps + 1):
|
| 602 |
+
s = s_init + self.s_recycle(self.s_norm(s)) # (b, t, d_s)
|
| 603 |
+
z = z_init + self.z_recycle(self.z_norm(z)) # (b, t, t, d_z)
|
| 604 |
z = z + self.msa_module(
|
| 605 |
z,
|
| 606 |
s_inputs,
|
| 607 |
feats,
|
| 608 |
use_kernels=self.use_kernels,
|
| 609 |
+
) # (b, t, t, d_z)
|
| 610 |
s, z = self.pairformer_module(
|
| 611 |
s,
|
| 612 |
z,
|
| 613 |
mask=mask,
|
| 614 |
pair_mask=pair_mask,
|
| 615 |
use_kernels=self.use_kernels,
|
| 616 |
+
) # (b, t, d_s), (b, t, t, d_z)
|
| 617 |
|
| 618 |
+
pdistogram = self.distogram_module(z) # (b, t, t, n_distogram, n_bin)
|
| 619 |
output: dict[str, Tensor] = {
|
| 620 |
"pdistogram": pdistogram,
|
| 621 |
"s": s,
|
|
|
|
| 630 |
relative_position_encoding=relative_position_encoding,
|
| 631 |
feats=feats,
|
| 632 |
)
|
| 633 |
+
) # q/c: (b, a, d_a); biases retain their attention-specific axes
|
| 634 |
diffusion_conditioning = {
|
| 635 |
"q": q,
|
| 636 |
"c": c,
|
|
|
|
| 650 |
max_parallel_samples=max_parallel_samples,
|
| 651 |
steering_args=self.steering_args,
|
| 652 |
diffusion_conditioning=diffusion_conditioning,
|
| 653 |
+
) # tensor values include sample_atom_coords: (b * m, a, 3)
|
| 654 |
output.update(struct_out)
|
| 655 |
|
| 656 |
if self.confidence_prediction:
|
| 657 |
if self.skip_run_structure:
|
| 658 |
+
x_pred = feats["coords"].repeat_interleave(
|
| 659 |
+
diffusion_samples, 0
|
| 660 |
+
) # (b * m, ..., a, 3)
|
| 661 |
else:
|
| 662 |
if "sample_atom_coords" not in output:
|
| 663 |
raise RuntimeError("Structure sampling did not produce sample_atom_coords.")
|
| 664 |
+
x_pred = output["sample_atom_coords"] # (b * m, a, 3)
|
| 665 |
|
| 666 |
if detach_confidence:
|
| 667 |
+
s_inputs_c = s_inputs.detach() # (b, t, d_s)
|
| 668 |
+
s_c = s.detach() # (b, t, d_s)
|
| 669 |
+
z_c = z.detach() # (b, t, t, d_z)
|
| 670 |
+
x_pred_c = x_pred.detach() # (b * m, ..., a, 3)
|
| 671 |
+
pdist_c = output["pdistogram"][:, :, :, 0].detach() # (b, t, t, n_bin)
|
| 672 |
else:
|
| 673 |
+
s_inputs_c = s_inputs # (b, t, d_s)
|
| 674 |
+
s_c = s # (b, t, d_s)
|
| 675 |
+
z_c = z # (b, t, t, d_z)
|
| 676 |
+
x_pred_c = x_pred # (b * m, ..., a, 3)
|
| 677 |
+
pdist_c = output["pdistogram"][:, :, :, 0] # (b, t, t, n_bin)
|
| 678 |
|
| 679 |
output.update(
|
| 680 |
self.confidence_module(
|
|
|
|
| 690 |
)
|
| 691 |
)
|
| 692 |
|
| 693 |
+
return output # named tensors retain the shapes traced above
|
| 694 |
|
| 695 |
|
| 696 |
class Boltz2Model(PreTrainedModel):
|
|
|
|
| 708 |
return
|
| 709 |
|
| 710 |
def _detied_state_dict(self) -> dict[str, Tensor]:
|
| 711 |
+
# Every state tensor has its parameter-defined shape (...).
|
| 712 |
raw_state = self.state_dict()
|
| 713 |
seen_ptrs: dict[int, str] = {}
|
| 714 |
out: dict[str, Tensor] = {}
|
|
|
|
| 716 |
if torch.is_tensor(tensor):
|
| 717 |
ptr = tensor.untyped_storage().data_ptr()
|
| 718 |
if ptr in seen_ptrs:
|
| 719 |
+
out[key] = tensor.clone() # (...)
|
| 720 |
else:
|
| 721 |
seen_ptrs[ptr] = key
|
| 722 |
+
out[key] = tensor # (...)
|
| 723 |
else:
|
| 724 |
+
out[key] = tensor # (...)
|
| 725 |
+
return out # each tensor: (...)
|
| 726 |
|
| 727 |
def save_pretrained(self, save_directory: str, **kwargs: Any) -> None:
|
| 728 |
if "safe_serialization" not in kwargs:
|
|
|
|
| 768 |
checkpoint_path,
|
| 769 |
map_location=map_location,
|
| 770 |
weights_only=False,
|
| 771 |
+
) # mapping values include state tensors with parameter-defined shapes (...)
|
| 772 |
if not isinstance(checkpoint, dict):
|
| 773 |
raise TypeError("Checkpoint must deserialize to a dictionary.")
|
| 774 |
_require_key(checkpoint, "hyper_parameters")
|
|
|
|
| 777 |
hparams = checkpoint["hyper_parameters"]
|
| 778 |
if not isinstance(hparams, dict):
|
| 779 |
raise TypeError("Checkpoint hyper_parameters must be a dictionary.")
|
| 780 |
+
state_dict = checkpoint["state_dict"] # each tensor: (...)
|
| 781 |
if not isinstance(state_dict, dict):
|
| 782 |
raise TypeError("Checkpoint state_dict must be a dictionary.")
|
| 783 |
|
|
|
|
| 789 |
default_diffusion_samples=default_diffusion_samples,
|
| 790 |
)
|
| 791 |
model = cls(config)
|
| 792 |
+
cleaned = _state_dict_without_wrappers(state_dict) # each tensor: (...)
|
| 793 |
target_keys = set(model.core.state_dict().keys())
|
| 794 |
for key in target_keys:
|
| 795 |
if ".attention.norm_s." in key:
|
|
|
|
| 800 |
filtered: dict[str, Tensor] = {}
|
| 801 |
for key, value in cleaned.items():
|
| 802 |
if key in target_keys:
|
| 803 |
+
filtered[key] = value # (...)
|
| 804 |
|
| 805 |
missing = sorted(target_keys.difference(filtered.keys()))
|
| 806 |
if missing:
|
|
|
|
| 837 |
output_hidden_states: bool | None = None,
|
| 838 |
return_dict: bool | None = None,
|
| 839 |
) -> Boltz2ModelOutput | tuple[Any, ...]:
|
| 840 |
+
# Feature shapes follow build_boltz2_features; b and t are batch and token counts.
|
| 841 |
output_attentions = (
|
| 842 |
self.config.output_attentions if output_attentions is None else output_attentions
|
| 843 |
)
|
|
|
|
| 866 |
max_parallel_samples=max_parallel_samples,
|
| 867 |
run_confidence_sequentially=run_confidence_sequentially,
|
| 868 |
detach_confidence=detach_confidence,
|
| 869 |
+
) # named tensors use the Boltz2InferenceCore.forward shapes
|
| 870 |
+
token_state = raw_output.get("s") # (b, t, d_s) or None
|
| 871 |
+
pair_state = raw_output.get("z") # (b, t, t, d_z) or None
|
| 872 |
model_output = Boltz2ModelOutput(
|
| 873 |
last_hidden_state=token_state,
|
| 874 |
hidden_states=(token_state, pair_state)
|
|
|
|
| 883 |
feats: dict[str, Tensor],
|
| 884 |
float_dtype: torch.dtype = torch.float32,
|
| 885 |
) -> dict[str, Tensor]:
|
| 886 |
+
# Each feature tensor keeps its existing shape (...).
|
| 887 |
moved: dict[str, Tensor] = {}
|
| 888 |
for key, value in feats.items():
|
| 889 |
if torch.is_tensor(value):
|
| 890 |
if value.is_floating_point():
|
| 891 |
+
moved[key] = value.to(
|
| 892 |
+
device=self.device, dtype=float_dtype
|
| 893 |
+
) # same shape (...)
|
| 894 |
else:
|
| 895 |
+
moved[key] = value.to(device=self.device) # same shape (...)
|
| 896 |
else:
|
| 897 |
+
moved[key] = value # same shape (...)
|
| 898 |
+
return moved # each tensor: same shape (...)
|
| 899 |
|
| 900 |
def predict_structure(
|
| 901 |
self,
|
|
|
|
| 929 |
atoms_per_window_queries=(
|
| 930 |
self.core.input_embedder.atom_encoder.atoms_per_window_queries
|
| 931 |
),
|
| 932 |
+
) # feature shapes are traced in build_boltz2_features
|
| 933 |
+
feats = self._to_model_device(
|
| 934 |
+
feats, float_dtype=torch.float32
|
| 935 |
+
) # unchanged feature shapes
|
| 936 |
autocast_context = (
|
| 937 |
torch.autocast(device_type="cuda", dtype=torch.bfloat16)
|
| 938 |
if self.device.type == "cuda"
|
|
|
|
| 947 |
max_parallel_samples=max_parallel_samples,
|
| 948 |
run_confidence_sequentially=run_confidence_sequentially,
|
| 949 |
return_dict=True,
|
| 950 |
+
) # named tensors use Boltz2InferenceCore.forward shapes
|
| 951 |
|
| 952 |
+
sample_atom_coords_value = output.get(
|
| 953 |
+
"sample_atom_coords"
|
| 954 |
+
) # (m, a_p, 3) or absent
|
| 955 |
if not isinstance(sample_atom_coords_value, torch.Tensor):
|
| 956 |
raise RuntimeError("Boltz2 structure sampling did not return coordinate tensors.")
|
| 957 |
+
sample_atom_coords = sample_atom_coords_value.detach().cpu() # (m, a_p, 3)
|
| 958 |
+
non_finite_mask = torch.logical_not(
|
| 959 |
+
torch.isfinite(sample_atom_coords)
|
| 960 |
+
) # (m, a_p, 3)
|
| 961 |
if torch.any(non_finite_mask):
|
| 962 |
raise RuntimeError(
|
| 963 |
"sample_atom_coords contains non-finite values. "
|
| 964 |
f"Non-finite count: {int(non_finite_mask.sum().item())}"
|
| 965 |
)
|
| 966 |
+
atom_pad_mask = feats["atom_pad_mask"][0].detach().cpu() # (a_p,)
|
| 967 |
+
plddt = (
|
| 968 |
+
output["plddt"].detach().cpu() if "plddt" in output else None
|
| 969 |
+
) # (m, a_p) or None
|
| 970 |
complex_plddt = (
|
| 971 |
output["complex_plddt"].detach().cpu() if "complex_plddt" in output else None
|
| 972 |
+
) # (m,) or None
|
| 973 |
+
iptm = (
|
| 974 |
+
output["iptm"].detach().cpu() if "iptm" in output else None
|
| 975 |
+
) # (m,) or None
|
| 976 |
+
ptm = (
|
| 977 |
+
output["ptm"].detach().cpu() if "ptm" in output else None
|
| 978 |
+
) # (m,) or None
|
| 979 |
+
|
| 980 |
+
confidence_score = None # (m,) or None
|
| 981 |
if (complex_plddt is not None) and (iptm is not None) and (ptm is not None):
|
| 982 |
if torch.allclose(iptm, torch.zeros_like(iptm)):
|
| 983 |
+
confidence_score = (4 * complex_plddt + ptm) / 5 # (m,)
|
| 984 |
else:
|
| 985 |
+
confidence_score = (4 * complex_plddt + iptm) / 5 # (m,)
|
| 986 |
|
| 987 |
return Boltz2StructureOutput(
|
| 988 |
sample_atom_coords=sample_atom_coords,
|
fastplms/models/boltz/vb_layers_attention.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from collections.abc import Callable, MutableMapping
|
| 6 |
-
|
| 7 |
from einops.layers.torch import Rearrange
|
| 8 |
from torch import Tensor, nn
|
| 9 |
|
|
@@ -51,12 +50,13 @@ class AttentionPairBias(nn.Module):
|
|
| 51 |
pair_states: Tensor,
|
| 52 |
model_cache: MutableMapping[str, Tensor] | None,
|
| 53 |
) -> Tensor:
|
|
|
|
| 54 |
if model_cache is not None and "z" in model_cache:
|
| 55 |
-
return model_cache["z"]
|
| 56 |
-
pair_bias = self.proj_z(pair_states)
|
| 57 |
if model_cache is not None:
|
| 58 |
model_cache["z"] = pair_bias
|
| 59 |
-
return pair_bias
|
| 60 |
|
| 61 |
def forward(
|
| 62 |
self,
|
|
@@ -70,21 +70,26 @@ class AttentionPairBias(nn.Module):
|
|
| 70 |
) -> Tensor:
|
| 71 |
"""Transform S with shapes ``S: (b, l_q, d)`` and ``Z: (b, l, l, d_z)``."""
|
| 72 |
|
| 73 |
-
|
|
|
|
| 74 |
if to_keys is not None:
|
| 75 |
-
key_states = to_keys(sequence_states)
|
| 76 |
-
key_mask = to_keys(mask.unsqueeze(-1)).squeeze(-1)
|
| 77 |
else:
|
| 78 |
-
key_states = sequence_states if k_in is None else k_in
|
| 79 |
-
key_mask = mask
|
| 80 |
|
| 81 |
-
query = reshape_heads(
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
pair_bias = self._resolve_pair_bias(z, model_cache).repeat_interleave(
|
| 85 |
multiplicity,
|
| 86 |
dim=0,
|
| 87 |
-
)
|
| 88 |
attended = pair_biased_attention(
|
| 89 |
query,
|
| 90 |
key,
|
|
@@ -92,7 +97,7 @@ class AttentionPairBias(nn.Module):
|
|
| 92 |
pair_bias,
|
| 93 |
key_mask,
|
| 94 |
self.inf,
|
| 95 |
-
)
|
| 96 |
-
attended = attended.reshape(s.shape[0], -1, self.c_s)
|
| 97 |
-
gate = self.proj_g(sequence_states).sigmoid()
|
| 98 |
-
return self.proj_o(gate * attended)
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from collections.abc import Callable, MutableMapping
|
|
|
|
| 6 |
from einops.layers.torch import Rearrange
|
| 7 |
from torch import Tensor, nn
|
| 8 |
|
|
|
|
| 50 |
pair_states: Tensor,
|
| 51 |
model_cache: MutableMapping[str, Tensor] | None,
|
| 52 |
) -> Tensor:
|
| 53 |
+
# pair_states: (b, l_q, l_k, d_z)
|
| 54 |
if model_cache is not None and "z" in model_cache:
|
| 55 |
+
return model_cache["z"] # (b, h, l_q, l_k)
|
| 56 |
+
pair_bias = self.proj_z(pair_states) # (b, h, l_q, l_k)
|
| 57 |
if model_cache is not None:
|
| 58 |
model_cache["z"] = pair_bias
|
| 59 |
+
return pair_bias # (b, h, l_q, l_k)
|
| 60 |
|
| 61 |
def forward(
|
| 62 |
self,
|
|
|
|
| 70 |
) -> Tensor:
|
| 71 |
"""Transform S with shapes ``S: (b, l_q, d)`` and ``Z: (b, l, l, d_z)``."""
|
| 72 |
|
| 73 |
+
# s: (b, l_q, d); z: (b_z, l_q, l_k, d_z); mask: (b_k, l_k).
|
| 74 |
+
sequence_states = self.norm_s(s) if self.initial_norm else s # (b, l_q, d)
|
| 75 |
if to_keys is not None:
|
| 76 |
+
key_states = to_keys(sequence_states) # (b, l_k, d)
|
| 77 |
+
key_mask = to_keys(mask.unsqueeze(-1)).squeeze(-1) # (b_k, l_k)
|
| 78 |
else:
|
| 79 |
+
key_states = sequence_states if k_in is None else k_in # (b, l_k, d)
|
| 80 |
+
key_mask = mask # (b_k, l_k)
|
| 81 |
|
| 82 |
+
query = reshape_heads(
|
| 83 |
+
self.proj_q(sequence_states), self.num_heads
|
| 84 |
+
) # (b, l_q, h, d_h)
|
| 85 |
+
key = reshape_heads(self.proj_k(key_states), self.num_heads) # (b, l_k, h, d_h)
|
| 86 |
+
value = reshape_heads(
|
| 87 |
+
self.proj_v(key_states), self.num_heads
|
| 88 |
+
) # (b, l_k, h, d_h)
|
| 89 |
pair_bias = self._resolve_pair_bias(z, model_cache).repeat_interleave(
|
| 90 |
multiplicity,
|
| 91 |
dim=0,
|
| 92 |
+
) # (b, h, l_q, l_k)
|
| 93 |
attended = pair_biased_attention(
|
| 94 |
query,
|
| 95 |
key,
|
|
|
|
| 97 |
pair_bias,
|
| 98 |
key_mask,
|
| 99 |
self.inf,
|
| 100 |
+
) # (b, l_q, h, d_h)
|
| 101 |
+
attended = attended.reshape(s.shape[0], -1, self.c_s) # (b, l_q, d)
|
| 102 |
+
gate = self.proj_g(sequence_states).sigmoid() # (b, l_q, d)
|
| 103 |
+
return self.proj_o(gate * attended) # (b, l_q, d)
|
fastplms/models/boltz/vb_layers_attentionv2.py
CHANGED
|
@@ -56,10 +56,13 @@ class AttentionPairBias(nn.Module):
|
|
| 56 |
) -> Tensor:
|
| 57 |
"""Transform S with K and pair input Z, returning shape ``(b, l_q, d)``."""
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
attended = pair_biased_attention(
|
| 64 |
query,
|
| 65 |
key,
|
|
@@ -67,7 +70,7 @@ class AttentionPairBias(nn.Module):
|
|
| 67 |
pair_bias,
|
| 68 |
mask,
|
| 69 |
self.inf,
|
| 70 |
-
)
|
| 71 |
-
attended = attended.reshape(s.shape[0], -1, self.c_s)
|
| 72 |
-
gate = self.proj_g(s).sigmoid()
|
| 73 |
-
return self.proj_o(gate * attended)
|
|
|
|
| 56 |
) -> Tensor:
|
| 57 |
"""Transform S with K and pair input Z, returning shape ``(b, l_q, d)``."""
|
| 58 |
|
| 59 |
+
# s: (b, l_q, d); k_in: (b, l_k, d); z: (b_z, l_q, l_k, d_z or h).
|
| 60 |
+
query = reshape_heads(self.proj_q(s), self.num_heads) # (b, l_q, h, d_h)
|
| 61 |
+
key = reshape_heads(self.proj_k(k_in), self.num_heads) # (b, l_k, h, d_h)
|
| 62 |
+
value = reshape_heads(self.proj_v(k_in), self.num_heads) # (b, l_k, h, d_h)
|
| 63 |
+
pair_bias = self.proj_z(z).repeat_interleave(
|
| 64 |
+
multiplicity, dim=0
|
| 65 |
+
) # (b, h, l_q, l_k)
|
| 66 |
attended = pair_biased_attention(
|
| 67 |
query,
|
| 68 |
key,
|
|
|
|
| 70 |
pair_bias,
|
| 71 |
mask,
|
| 72 |
self.inf,
|
| 73 |
+
) # (b, l_q, h, d_h)
|
| 74 |
+
attended = attended.reshape(s.shape[0], -1, self.c_s) # (b, l_q, d)
|
| 75 |
+
gate = self.proj_g(s).sigmoid() # (b, l_q, d)
|
| 76 |
+
return self.proj_o(gate * attended) # (b, l_q, d)
|
fastplms/models/boltz/vb_layers_confidence_utils.py
CHANGED
|
@@ -11,12 +11,15 @@ from . import vb_const as const
|
|
| 11 |
def compute_collinear_mask(v1: torch.Tensor, v2: torch.Tensor) -> torch.Tensor:
|
| 12 |
"""Mark nondegenerate vector pairs whose angle defines a stable frame."""
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
|
@@ -24,7 +27,7 @@ def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
|
| 24 |
return torch.bmm(
|
| 25 |
feats["atom_to_token"].float(),
|
| 26 |
feats["asym_id"].unsqueeze(-1).float(),
|
| 27 |
-
).squeeze(-1)
|
| 28 |
|
| 29 |
|
| 30 |
def _replace_nonpolymer_frames(
|
|
@@ -36,17 +39,19 @@ def _replace_nonpolymer_frames(
|
|
| 36 |
*,
|
| 37 |
inference: bool,
|
| 38 |
) -> None:
|
| 39 |
-
|
|
|
|
| 40 |
for batch_index, sample_coordinates in enumerate(coordinates):
|
|
|
|
| 41 |
token_offset = 0
|
| 42 |
atom_offset = 0
|
| 43 |
for chain_id in torch.unique(token_chain_ids[batch_index]):
|
| 44 |
-
token_mask = (token_chain_ids[batch_index] == chain_id) * feats[
|
| 45 |
-
|
| 46 |
-
]
|
| 47 |
-
atom_mask = (atom_chain_ids[batch_index] == chain_id) * feats[
|
| 48 |
-
|
| 49 |
-
]
|
| 50 |
token_count = int(token_mask.sum().item())
|
| 51 |
atom_count = int(atom_mask.sum().item())
|
| 52 |
is_nonpolymer = (
|
|
@@ -62,21 +67,27 @@ def _replace_nonpolymer_frames(
|
|
| 62 |
else resolved_mask[batch_index]
|
| 63 |
)
|
| 64 |
)
|
| 65 |
-
chain_atom_mask = atom_mask.bool()
|
| 66 |
-
chain_coordinates = sample_coordinates[:, chain_atom_mask]
|
| 67 |
-
differences =
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
frames = (
|
| 74 |
torch.cat(
|
| 75 |
(nearest[:, :, 1:2], nearest[:, :, 0:1], nearest[:, :, 2:3]),
|
| 76 |
dim=2,
|
| 77 |
)
|
| 78 |
+ atom_offset
|
| 79 |
-
)
|
| 80 |
frame_indices[
|
| 81 |
batch_index,
|
| 82 |
:,
|
|
@@ -102,12 +113,18 @@ def compute_frame_pred(
|
|
| 102 |
atom's three nearest resolved neighbors.
|
| 103 |
"""
|
| 104 |
|
| 105 |
-
atom_chain_ids = _atom_chain_ids(feats)
|
| 106 |
expanded_batch, _, _ = pred_atom_coords.shape
|
| 107 |
base_batch = expanded_batch // multiplicity
|
| 108 |
-
coordinates = pred_atom_coords.reshape(
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
_replace_nonpolymer_frames(
|
| 112 |
coordinates,
|
| 113 |
frame_indices,
|
|
@@ -117,18 +134,25 @@ def compute_frame_pred(
|
|
| 117 |
inference=inference,
|
| 118 |
)
|
| 119 |
|
| 120 |
-
batch_indices = torch.arange(
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
frame_coordinates = coordinates[
|
| 123 |
batch_indices,
|
| 124 |
sample_indices,
|
| 125 |
frame_indices,
|
| 126 |
-
].reshape(-1, 3, 3)
|
| 127 |
valid_frames = compute_collinear_mask(
|
| 128 |
frame_coordinates[:, 1] - frame_coordinates[:, 0],
|
| 129 |
frame_coordinates[:, 1] - frame_coordinates[:, 2],
|
| 130 |
-
).reshape(base_batch, multiplicity, -1)
|
| 131 |
-
return
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
|
| 134 |
def compute_aggregated_metric(logits: torch.Tensor, end: float = 1.0) -> torch.Tensor:
|
|
@@ -141,25 +165,32 @@ def compute_aggregated_metric(logits: torch.Tensor, end: float = 1.0) -> torch.T
|
|
| 141 |
end,
|
| 142 |
bin_width,
|
| 143 |
device=logits.device,
|
| 144 |
-
)
|
| 145 |
-
|
|
|
|
| 146 |
broadcast_shape = (1,) * (probabilities.ndim - 1) + centers.shape
|
| 147 |
-
return torch.sum(
|
|
|
|
|
|
|
| 148 |
|
| 149 |
|
| 150 |
def tm_function(d: torch.Tensor, n_res: torch.Tensor) -> torch.Tensor:
|
| 151 |
"""Evaluate the TM-score distance kernel."""
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
|
|
|
| 155 |
|
| 156 |
|
| 157 |
def _maximum_masked_tm(
|
| 158 |
expected_tm: torch.Tensor,
|
| 159 |
pair_mask: torch.Tensor,
|
| 160 |
) -> torch.Tensor:
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
|
| 165 |
def compute_ptms(
|
|
@@ -176,18 +207,25 @@ def compute_ptms(
|
|
| 176 |
]:
|
| 177 |
"""Compute pTM, ipTM, interface-type, and chain-pair confidence scores."""
|
| 178 |
|
|
|
|
| 179 |
_, frame_mask = compute_frame_pred(
|
| 180 |
x_preds,
|
| 181 |
feats["frames_idx"],
|
| 182 |
feats,
|
| 183 |
multiplicity,
|
| 184 |
inference=True,
|
| 185 |
-
)
|
| 186 |
-
token_mask = feats["token_pad_mask"].repeat_interleave(
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
num_bins = logits.shape[-1]
|
| 193 |
pae_centers = torch.arange(
|
|
@@ -195,23 +233,31 @@ def compute_ptms(
|
|
| 195 |
32.0,
|
| 196 |
32.0 / num_bins,
|
| 197 |
device=logits.device,
|
| 198 |
-
).unsqueeze(0)
|
| 199 |
-
n_res = token_mask.sum(dim=-1, keepdim=True)
|
| 200 |
-
tm_values = tm_function(pae_centers, n_res).unsqueeze(1).unsqueeze(
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
-
ptm = _maximum_masked_tm(expected_tm, base_pair_mask)
|
| 204 |
-
iptm = _maximum_masked_tm(expected_tm, interface_mask)
|
| 205 |
|
| 206 |
-
token_type = feats["mol_type"].repeat_interleave(multiplicity, 0)
|
| 207 |
-
ligand = (token_type == const.chain_type_ids["NONPOLYMER"]).float()
|
| 208 |
-
protein = (token_type == const.chain_type_ids["PROTEIN"]).float()
|
| 209 |
ligand_protein = (
|
| 210 |
ligand[:, :, None] * protein[:, None, :] + protein[:, :, None] * ligand[:, None, :]
|
| 211 |
-
)
|
| 212 |
-
protein_protein = protein[:, :, None] * protein[:, None, :]
|
| 213 |
-
ligand_iptm = _maximum_masked_tm(
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
chain_pair_iptm: dict[int, dict[int, torch.Tensor]] = {}
|
| 217 |
for first_chain in torch.unique(asym_id).tolist():
|
|
|
|
| 11 |
def compute_collinear_mask(v1: torch.Tensor, v2: torch.Tensor) -> torch.Tensor:
|
| 12 |
"""Mark nondegenerate vector pairs whose angle defines a stable frame."""
|
| 13 |
|
| 14 |
+
# v1/v2: (n, 3)
|
| 15 |
+
norm1 = torch.norm(v1, dim=1, keepdim=True) # (n, 1)
|
| 16 |
+
norm2 = torch.norm(v2, dim=1, keepdim=True) # (n, 1)
|
| 17 |
+
unit1 = v1 / (norm1 + 1e-6) # (n, 3)
|
| 18 |
+
unit2 = v2 / (norm2 + 1e-6) # (n, 3)
|
| 19 |
+
separated = torch.abs(torch.sum(unit1 * unit2, dim=1)) < 0.9063 # (n,)
|
| 20 |
+
return (
|
| 21 |
+
separated & (norm1.reshape(-1) > 1e-2) & (norm2.reshape(-1) > 1e-2)
|
| 22 |
+
) # (n,)
|
| 23 |
|
| 24 |
|
| 25 |
def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
|
|
|
| 27 |
return torch.bmm(
|
| 28 |
feats["atom_to_token"].float(),
|
| 29 |
feats["asym_id"].unsqueeze(-1).float(),
|
| 30 |
+
).squeeze(-1) # (b, a)
|
| 31 |
|
| 32 |
|
| 33 |
def _replace_nonpolymer_frames(
|
|
|
|
| 39 |
*,
|
| 40 |
inference: bool,
|
| 41 |
) -> None:
|
| 42 |
+
# coordinates: (b, m, a, 3); frame_indices: (b, m, t_or_a, 3).
|
| 43 |
+
token_chain_ids = feats["asym_id"] # (b, t)
|
| 44 |
for batch_index, sample_coordinates in enumerate(coordinates):
|
| 45 |
+
# sample_coordinates: (m, a, 3)
|
| 46 |
token_offset = 0
|
| 47 |
atom_offset = 0
|
| 48 |
for chain_id in torch.unique(token_chain_ids[batch_index]):
|
| 49 |
+
token_mask = (token_chain_ids[batch_index] == chain_id) * feats[
|
| 50 |
+
"token_pad_mask"
|
| 51 |
+
][batch_index] # (t,)
|
| 52 |
+
atom_mask = (atom_chain_ids[batch_index] == chain_id) * feats[
|
| 53 |
+
"atom_pad_mask"
|
| 54 |
+
][batch_index] # (a,)
|
| 55 |
token_count = int(token_mask.sum().item())
|
| 56 |
atom_count = int(atom_mask.sum().item())
|
| 57 |
is_nonpolymer = (
|
|
|
|
| 67 |
else resolved_mask[batch_index]
|
| 68 |
)
|
| 69 |
)
|
| 70 |
+
chain_atom_mask = atom_mask.bool() # (a,)
|
| 71 |
+
chain_coordinates = sample_coordinates[:, chain_atom_mask] # (m, a_c, 3)
|
| 72 |
+
differences = (
|
| 73 |
+
chain_coordinates[:, None, :, :] - chain_coordinates[:, :, None, :]
|
| 74 |
+
) # (m, a_c, a_c, 3)
|
| 75 |
+
distances = differences.square().sum(dim=-1) ** 0.5 # (m, a_c, a_c)
|
| 76 |
+
valid = chain_resolved[chain_atom_mask] # (a_c,)
|
| 77 |
+
invalid_pairs = 1 - (valid[None, :] * valid[:, None]).to(
|
| 78 |
+
torch.float32
|
| 79 |
+
) # (a_c, a_c)
|
| 80 |
+
invalid_pairs[invalid_pairs == 1] = torch.inf # (a_c, a_c)
|
| 81 |
+
nearest = torch.sort(
|
| 82 |
+
distances + invalid_pairs, dim=2
|
| 83 |
+
).indices # (m, a_c, a_c)
|
| 84 |
frames = (
|
| 85 |
torch.cat(
|
| 86 |
(nearest[:, :, 1:2], nearest[:, :, 0:1], nearest[:, :, 2:3]),
|
| 87 |
dim=2,
|
| 88 |
)
|
| 89 |
+ atom_offset
|
| 90 |
+
) # (m, a_c, 3)
|
| 91 |
frame_indices[
|
| 92 |
batch_index,
|
| 93 |
:,
|
|
|
|
| 113 |
atom's three nearest resolved neighbors.
|
| 114 |
"""
|
| 115 |
|
| 116 |
+
atom_chain_ids = _atom_chain_ids(feats) # (b, a)
|
| 117 |
expanded_batch, _, _ = pred_atom_coords.shape
|
| 118 |
base_batch = expanded_batch // multiplicity
|
| 119 |
+
coordinates = pred_atom_coords.reshape(
|
| 120 |
+
base_batch, multiplicity, -1, 3
|
| 121 |
+
) # (b, m, a, 3)
|
| 122 |
+
frame_indices = frames_idx_true.clone().repeat_interleave(
|
| 123 |
+
multiplicity, 0
|
| 124 |
+
) # (b * m, 1, t, 3) or (b * m, t, 3)
|
| 125 |
+
frame_indices = frame_indices.reshape(
|
| 126 |
+
base_batch, multiplicity, -1, 3
|
| 127 |
+
) # (b, m, t, 3)
|
| 128 |
_replace_nonpolymer_frames(
|
| 129 |
coordinates,
|
| 130 |
frame_indices,
|
|
|
|
| 134 |
inference=inference,
|
| 135 |
)
|
| 136 |
|
| 137 |
+
batch_indices = torch.arange(
|
| 138 |
+
base_batch, device=frame_indices.device
|
| 139 |
+
)[:, None, None, None] # (b, 1, 1, 1)
|
| 140 |
+
sample_indices = torch.arange(
|
| 141 |
+
multiplicity, device=frame_indices.device
|
| 142 |
+
)[None, :, None, None] # (1, m, 1, 1)
|
| 143 |
frame_coordinates = coordinates[
|
| 144 |
batch_indices,
|
| 145 |
sample_indices,
|
| 146 |
frame_indices,
|
| 147 |
+
].reshape(-1, 3, 3) # (b * m * t, 3, 3)
|
| 148 |
valid_frames = compute_collinear_mask(
|
| 149 |
frame_coordinates[:, 1] - frame_coordinates[:, 0],
|
| 150 |
frame_coordinates[:, 1] - frame_coordinates[:, 2],
|
| 151 |
+
).reshape(base_batch, multiplicity, -1) # (b, m, t)
|
| 152 |
+
return (
|
| 153 |
+
frame_indices,
|
| 154 |
+
valid_frames * feats["token_pad_mask"][:, None, :],
|
| 155 |
+
) # (b, m, t, 3), (b, m, t)
|
| 156 |
|
| 157 |
|
| 158 |
def compute_aggregated_metric(logits: torch.Tensor, end: float = 1.0) -> torch.Tensor:
|
|
|
|
| 165 |
end,
|
| 166 |
bin_width,
|
| 167 |
device=logits.device,
|
| 168 |
+
) # (n_bin,)
|
| 169 |
+
# logits: (..., n_bin)
|
| 170 |
+
probabilities = nn.functional.softmax(logits, dim=-1) # (..., n_bin)
|
| 171 |
broadcast_shape = (1,) * (probabilities.ndim - 1) + centers.shape
|
| 172 |
+
return torch.sum(
|
| 173 |
+
probabilities * centers.view(broadcast_shape), dim=-1
|
| 174 |
+
) # (...)
|
| 175 |
|
| 176 |
|
| 177 |
def tm_function(d: torch.Tensor, n_res: torch.Tensor) -> torch.Tensor:
|
| 178 |
"""Evaluate the TM-score distance kernel."""
|
| 179 |
|
| 180 |
+
# d and n_res broadcast to a shared shape (...).
|
| 181 |
+
distance_scale = 1.24 * (torch.clip(n_res, min=19) - 15) ** (1 / 3) - 1.8 # (...)
|
| 182 |
+
return 1 / (1 + (d / distance_scale) ** 2) # (...)
|
| 183 |
|
| 184 |
|
| 185 |
def _maximum_masked_tm(
|
| 186 |
expected_tm: torch.Tensor,
|
| 187 |
pair_mask: torch.Tensor,
|
| 188 |
) -> torch.Tensor:
|
| 189 |
+
# expected_tm/pair_mask: (b * m, t, t).
|
| 190 |
+
per_anchor = torch.sum(expected_tm * pair_mask, dim=-1) / (
|
| 191 |
+
torch.sum(pair_mask, dim=-1) + 1e-5
|
| 192 |
+
) # (b * m, t)
|
| 193 |
+
return torch.max(per_anchor, dim=1).values # (b * m,)
|
| 194 |
|
| 195 |
|
| 196 |
def compute_ptms(
|
|
|
|
| 207 |
]:
|
| 208 |
"""Compute pTM, ipTM, interface-type, and chain-pair confidence scores."""
|
| 209 |
|
| 210 |
+
# logits: (b * m, t, t, n_bin); x_preds: (b * m, a, 3).
|
| 211 |
_, frame_mask = compute_frame_pred(
|
| 212 |
x_preds,
|
| 213 |
feats["frames_idx"],
|
| 214 |
feats,
|
| 215 |
multiplicity,
|
| 216 |
inference=True,
|
| 217 |
+
) # frame_mask: (b, m, t)
|
| 218 |
+
token_mask = feats["token_pad_mask"].repeat_interleave(
|
| 219 |
+
multiplicity, 0
|
| 220 |
+
) # (b * m, t)
|
| 221 |
+
valid_anchor = frame_mask.reshape(-1, frame_mask.shape[-1]) # (b * m, t)
|
| 222 |
+
base_pair_mask = (
|
| 223 |
+
valid_anchor[:, :, None] * token_mask[:, None, :] * token_mask[:, :, None]
|
| 224 |
+
) # (b * m, t, t)
|
| 225 |
+
asym_id = feats["asym_id"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 226 |
+
interface_mask = base_pair_mask * (
|
| 227 |
+
asym_id[:, None, :] != asym_id[:, :, None]
|
| 228 |
+
) # (b * m, t, t)
|
| 229 |
|
| 230 |
num_bins = logits.shape[-1]
|
| 231 |
pae_centers = torch.arange(
|
|
|
|
| 233 |
32.0,
|
| 234 |
32.0 / num_bins,
|
| 235 |
device=logits.device,
|
| 236 |
+
).unsqueeze(0) # (1, n_bin)
|
| 237 |
+
n_res = token_mask.sum(dim=-1, keepdim=True) # (b * m, 1)
|
| 238 |
+
tm_values = tm_function(pae_centers, n_res).unsqueeze(1).unsqueeze(
|
| 239 |
+
2
|
| 240 |
+
) # (b * m, 1, 1, n_bin)
|
| 241 |
+
expected_tm = torch.sum(
|
| 242 |
+
nn.functional.softmax(logits, dim=-1) * tm_values, dim=-1
|
| 243 |
+
) # (b * m, t, t)
|
| 244 |
|
| 245 |
+
ptm = _maximum_masked_tm(expected_tm, base_pair_mask) # (b * m,)
|
| 246 |
+
iptm = _maximum_masked_tm(expected_tm, interface_mask) # (b * m,)
|
| 247 |
|
| 248 |
+
token_type = feats["mol_type"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 249 |
+
ligand = (token_type == const.chain_type_ids["NONPOLYMER"]).float() # (b * m, t)
|
| 250 |
+
protein = (token_type == const.chain_type_ids["PROTEIN"]).float() # (b * m, t)
|
| 251 |
ligand_protein = (
|
| 252 |
ligand[:, :, None] * protein[:, None, :] + protein[:, :, None] * ligand[:, None, :]
|
| 253 |
+
) # (b * m, t, t)
|
| 254 |
+
protein_protein = protein[:, :, None] * protein[:, None, :] # (b * m, t, t)
|
| 255 |
+
ligand_iptm = _maximum_masked_tm(
|
| 256 |
+
expected_tm, interface_mask * ligand_protein
|
| 257 |
+
) # (b * m,)
|
| 258 |
+
protein_iptm = _maximum_masked_tm(
|
| 259 |
+
expected_tm, interface_mask * protein_protein
|
| 260 |
+
) # (b * m,)
|
| 261 |
|
| 262 |
chain_pair_iptm: dict[int, dict[int, torch.Tensor]] = {}
|
| 263 |
for first_chain in torch.unique(asym_id).tolist():
|
fastplms/models/boltz/vb_layers_dropout.py
CHANGED
|
@@ -36,5 +36,7 @@ def get_dropout_mask(
|
|
| 36 |
|
| 37 |
probability = float(dropout) if training else 0.0
|
| 38 |
sample_shape = _broadcast_mask_shape(z, columnwise=columnwise)
|
| 39 |
-
keep =
|
| 40 |
-
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
probability = float(dropout) if training else 0.0
|
| 38 |
sample_shape = _broadcast_mask_shape(z, columnwise=columnwise)
|
| 39 |
+
keep = (
|
| 40 |
+
torch.rand(sample_shape, dtype=torch.float32, device=z.device) >= probability
|
| 41 |
+
) # (b, 1, n, 1) or (b, n, 1, 1)
|
| 42 |
+
return keep * (1.0 / (1.0 - probability)) # same broadcast-mask shape
|
fastplms/models/boltz/vb_layers_initialize.py
CHANGED
|
@@ -3,11 +3,11 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import math
|
| 6 |
-
from typing import Literal
|
| 7 |
-
|
| 8 |
import torch
|
|
|
|
| 9 |
from torch import Tensor
|
| 10 |
|
|
|
|
| 11 |
FanMode = Literal["fan_in", "fan_out", "fan_avg"]
|
| 12 |
|
| 13 |
|
|
@@ -33,6 +33,7 @@ def trunc_normal_init_(
|
|
| 33 |
) -> None:
|
| 34 |
"""Fill W from a normal distribution truncated at two standard deviations."""
|
| 35 |
|
|
|
|
| 36 |
variance = scale / max(1.0, _calculate_fan(weights.shape, fan))
|
| 37 |
std = math.sqrt(variance)
|
| 38 |
with torch.no_grad():
|
|
@@ -42,6 +43,7 @@ def trunc_normal_init_(
|
|
| 42 |
def lecun_normal_init_(weights: Tensor) -> None:
|
| 43 |
"""Initialize W using fan-in-scaled truncated normal values."""
|
| 44 |
|
|
|
|
| 45 |
trunc_normal_init_(weights)
|
| 46 |
|
| 47 |
|
|
@@ -58,6 +60,7 @@ def glorot_uniform_init_(weights: Tensor) -> None:
|
|
| 58 |
|
| 59 |
|
| 60 |
def _fill_(tensor: Tensor, value: float) -> None:
|
|
|
|
| 61 |
with torch.no_grad():
|
| 62 |
tensor.fill_(value)
|
| 63 |
|
|
@@ -96,3 +99,11 @@ def ipa_point_weights_init_(weights: Tensor) -> None:
|
|
| 96 |
"""Initialize W so applying softplus yields one."""
|
| 97 |
|
| 98 |
_fill_(weights, 0.541324854612918)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import math
|
|
|
|
|
|
|
| 6 |
import torch
|
| 7 |
+
from typing import Literal
|
| 8 |
from torch import Tensor
|
| 9 |
|
| 10 |
+
|
| 11 |
FanMode = Literal["fan_in", "fan_out", "fan_avg"]
|
| 12 |
|
| 13 |
|
|
|
|
| 33 |
) -> None:
|
| 34 |
"""Fill W from a normal distribution truncated at two standard deviations."""
|
| 35 |
|
| 36 |
+
# weights: (d_out, d_in); initialization preserves this shape in place.
|
| 37 |
variance = scale / max(1.0, _calculate_fan(weights.shape, fan))
|
| 38 |
std = math.sqrt(variance)
|
| 39 |
with torch.no_grad():
|
|
|
|
| 43 |
def lecun_normal_init_(weights: Tensor) -> None:
|
| 44 |
"""Initialize W using fan-in-scaled truncated normal values."""
|
| 45 |
|
| 46 |
+
# weights: (d_out, d_in), mutated in place.
|
| 47 |
trunc_normal_init_(weights)
|
| 48 |
|
| 49 |
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
def _fill_(tensor: Tensor, value: float) -> None:
|
| 63 |
+
# tensor: (...), mutated in place without changing shape.
|
| 64 |
with torch.no_grad():
|
| 65 |
tensor.fill_(value)
|
| 66 |
|
|
|
|
| 99 |
"""Initialize W so applying softplus yields one."""
|
| 100 |
|
| 101 |
_fill_(weights, 0.541324854612918)
|
| 102 |
+
# weights: (d_out, d_in), mutated in place.
|
| 103 |
+
# weights: (d_out, d_in), mutated in place.
|
| 104 |
+
# weights: (...), mutated in place.
|
| 105 |
+
# weights: (...), mutated in place.
|
| 106 |
+
# bias: (...), mutated in place.
|
| 107 |
+
# bias: (...), mutated in place.
|
| 108 |
+
# weights: (d_out, d_in), mutated in place.
|
| 109 |
+
# weights: (...), mutated in place.
|
fastplms/models/boltz/vb_layers_outer_product_mean.py
CHANGED
|
@@ -25,19 +25,25 @@ class OuterProductMean(nn.Module):
|
|
| 25 |
def _pair_counts(mask: Tensor) -> Tensor:
|
| 26 |
"""Count valid MSA observations for each residue pair."""
|
| 27 |
|
|
|
|
| 28 |
counts: Tensor | None = None
|
| 29 |
for start in range(0, mask.shape[1], 64):
|
| 30 |
-
mask_slice = mask[:, start : start + 64]
|
| 31 |
-
contribution = (
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
if counts is None:
|
| 34 |
raise ValueError("MSA depth must be positive")
|
| 35 |
-
return counts.clamp(min=1)
|
| 36 |
|
| 37 |
@staticmethod
|
| 38 |
def _outer_product(left: Tensor, right: Tensor, counts: Tensor) -> Tensor:
|
| 39 |
-
|
| 40 |
-
|
|
|
|
| 41 |
|
| 42 |
def _chunked_projection(
|
| 43 |
self,
|
|
@@ -47,32 +53,38 @@ class OuterProductMean(nn.Module):
|
|
| 47 |
chunk_size: int,
|
| 48 |
target: Tensor,
|
| 49 |
) -> Tensor:
|
|
|
|
| 50 |
output: Tensor | None = None
|
| 51 |
for start in range(0, self.c_hidden, chunk_size):
|
| 52 |
stop = min(start + chunk_size, self.c_hidden)
|
| 53 |
-
outer = self._outer_product(
|
|
|
|
|
|
|
| 54 |
weight = self.proj_o.weight[
|
| 55 |
:,
|
| 56 |
start * self.c_hidden : stop * self.c_hidden,
|
| 57 |
-
]
|
| 58 |
-
contribution = outer.to(target) @ weight.T
|
| 59 |
-
output =
|
|
|
|
|
|
|
| 60 |
if output is None:
|
| 61 |
raise ValueError("c_hidden must be positive")
|
| 62 |
-
return output + self.proj_o.bias
|
| 63 |
|
| 64 |
def forward(self, m: Tensor, mask: Tensor, chunk_size: int | None = None) -> Tensor:
|
| 65 |
"""Return pair tensor Z with shape ``(b, l, l, d_z)``."""
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
| 71 |
|
| 72 |
if chunk_size is not None and not self.training:
|
| 73 |
if chunk_size <= 0:
|
| 74 |
raise ValueError("chunk_size must be positive")
|
| 75 |
-
counts = self._pair_counts(expanded_mask)
|
| 76 |
return self._chunked_projection(
|
| 77 |
left,
|
| 78 |
right,
|
|
@@ -81,7 +93,11 @@ class OuterProductMean(nn.Module):
|
|
| 81 |
normalized,
|
| 82 |
)
|
| 83 |
|
| 84 |
-
pair_mask =
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
def _pair_counts(mask: Tensor) -> Tensor:
|
| 26 |
"""Count valid MSA observations for each residue pair."""
|
| 27 |
|
| 28 |
+
# mask: (b, s, l, 1)
|
| 29 |
counts: Tensor | None = None
|
| 30 |
for start in range(0, mask.shape[1], 64):
|
| 31 |
+
mask_slice = mask[:, start : start + 64] # (b, s_chunk, l, 1)
|
| 32 |
+
contribution = (
|
| 33 |
+
mask_slice[:, :, None, :] * mask_slice[:, :, :, None]
|
| 34 |
+
).sum(dim=1) # (b, l, l, 1)
|
| 35 |
+
counts = (
|
| 36 |
+
contribution if counts is None else counts + contribution
|
| 37 |
+
) # (b, l, l, 1)
|
| 38 |
if counts is None:
|
| 39 |
raise ValueError("MSA depth must be positive")
|
| 40 |
+
return counts.clamp(min=1) # (b, l, l, 1)
|
| 41 |
|
| 42 |
@staticmethod
|
| 43 |
def _outer_product(left: Tensor, right: Tensor, counts: Tensor) -> Tensor:
|
| 44 |
+
# left/right: (b, s, l, d_h); counts: (b, l, l, 1).
|
| 45 |
+
outer = torch.einsum("bsic,bsjd->bijcd", left, right) # (b, l, l, d_h, d_h)
|
| 46 |
+
return outer.reshape(*outer.shape[:3], -1) / counts # (b, l, l, d_h**2)
|
| 47 |
|
| 48 |
def _chunked_projection(
|
| 49 |
self,
|
|
|
|
| 53 |
chunk_size: int,
|
| 54 |
target: Tensor,
|
| 55 |
) -> Tensor:
|
| 56 |
+
# left/right: (b, s, l, d_h); counts: (b, l, l, 1).
|
| 57 |
output: Tensor | None = None
|
| 58 |
for start in range(0, self.c_hidden, chunk_size):
|
| 59 |
stop = min(start + chunk_size, self.c_hidden)
|
| 60 |
+
outer = self._outer_product(
|
| 61 |
+
left[..., start:stop], right, counts
|
| 62 |
+
) # (b, l, l, d_chunk * d_h)
|
| 63 |
weight = self.proj_o.weight[
|
| 64 |
:,
|
| 65 |
start * self.c_hidden : stop * self.c_hidden,
|
| 66 |
+
] # (d_z, d_chunk * d_h)
|
| 67 |
+
contribution = outer.to(target) @ weight.T # (b, l, l, d_z)
|
| 68 |
+
output = (
|
| 69 |
+
contribution if output is None else output + contribution
|
| 70 |
+
) # (b, l, l, d_z)
|
| 71 |
if output is None:
|
| 72 |
raise ValueError("c_hidden must be positive")
|
| 73 |
+
return output + self.proj_o.bias # (b, l, l, d_z)
|
| 74 |
|
| 75 |
def forward(self, m: Tensor, mask: Tensor, chunk_size: int | None = None) -> Tensor:
|
| 76 |
"""Return pair tensor Z with shape ``(b, l, l, d_z)``."""
|
| 77 |
|
| 78 |
+
# m: (b, s, l, d_m); mask: (b, s, l).
|
| 79 |
+
expanded_mask = mask.unsqueeze(-1).to(m) # (b, s, l, 1)
|
| 80 |
+
normalized = self.norm(m) # (b, s, l, d_m)
|
| 81 |
+
left = self.proj_a(normalized) * expanded_mask # (b, s, l, d_h)
|
| 82 |
+
right = self.proj_b(normalized) * expanded_mask # (b, s, l, d_h)
|
| 83 |
|
| 84 |
if chunk_size is not None and not self.training:
|
| 85 |
if chunk_size <= 0:
|
| 86 |
raise ValueError("chunk_size must be positive")
|
| 87 |
+
counts = self._pair_counts(expanded_mask) # (b, l, l, 1)
|
| 88 |
return self._chunked_projection(
|
| 89 |
left,
|
| 90 |
right,
|
|
|
|
| 93 |
normalized,
|
| 94 |
)
|
| 95 |
|
| 96 |
+
pair_mask = (
|
| 97 |
+
expanded_mask[:, :, None, :] * expanded_mask[:, :, :, None]
|
| 98 |
+
) # (b, s, l, l, 1)
|
| 99 |
+
counts = pair_mask.sum(dim=1).clamp(min=1) # (b, l, l, 1)
|
| 100 |
+
outer = self._outer_product(
|
| 101 |
+
left.float(), right.float(), counts
|
| 102 |
+
) # (b, l, l, d_h**2)
|
| 103 |
+
return self.proj_o(outer.to(normalized)) # (b, l, l, d_z)
|
fastplms/models/boltz/vb_layers_pair_averaging.py
CHANGED
|
@@ -34,39 +34,54 @@ class PairWeightedAveraging(nn.Module):
|
|
| 34 |
init.final_init_(self.proj_o.weight)
|
| 35 |
|
| 36 |
def _attention_weights(self, pair_states: Tensor, mask: Tensor) -> Tensor:
|
| 37 |
-
|
| 38 |
-
logits =
|
| 39 |
-
|
|
|
|
| 40 |
|
| 41 |
def _all_heads(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor) -> Tensor:
|
|
|
|
| 42 |
values = self.proj_m(msa_states).reshape(
|
| 43 |
*msa_states.shape[:3],
|
| 44 |
self.num_heads,
|
| 45 |
self.c_h,
|
| 46 |
-
)
|
| 47 |
-
values = values.permute(0, 3, 1, 2, 4)
|
| 48 |
-
weights = self._attention_weights(pair_states, mask)
|
| 49 |
-
gate = self.proj_g(msa_states).sigmoid()
|
| 50 |
-
attended = torch.einsum(
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
def _one_head(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor, head: int) -> Tensor:
|
|
|
|
| 56 |
start = head * self.c_h
|
| 57 |
stop = start + self.c_h
|
| 58 |
-
value = msa_states @ self.proj_m.weight[start:stop].T
|
| 59 |
-
value = value.reshape(*value.shape[:3], 1, self.c_h).permute(
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
attended = attended.permute(0, 2, 3, 1, 4).reshape(
|
| 66 |
*msa_states.shape[:3],
|
| 67 |
self.c_h,
|
| 68 |
-
)
|
| 69 |
-
return (gate * attended) @ self.proj_o.weight[:, start:stop].T
|
| 70 |
|
| 71 |
def forward(
|
| 72 |
self,
|
|
@@ -77,15 +92,20 @@ class PairWeightedAveraging(nn.Module):
|
|
| 77 |
) -> Tensor:
|
| 78 |
"""Return updated M with shape ``(b, s, l, d_m)``."""
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
|
|
|
| 82 |
if not chunk_heads or self.training:
|
| 83 |
return self._all_heads(msa_states, pair_states, mask)
|
| 84 |
|
| 85 |
output: Tensor | None = None
|
| 86 |
for head in range(self.num_heads):
|
| 87 |
-
contribution = self._one_head(
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
if output is None:
|
| 90 |
raise ValueError("num_heads must be positive")
|
| 91 |
-
return output
|
|
|
|
| 34 |
init.final_init_(self.proj_o.weight)
|
| 35 |
|
| 36 |
def _attention_weights(self, pair_states: Tensor, mask: Tensor) -> Tensor:
|
| 37 |
+
# pair_states: (b, l, l, d_z); mask: (b, l).
|
| 38 |
+
logits = self.proj_z(pair_states).permute(0, 3, 1, 2) # (b, h, l, l)
|
| 39 |
+
logits = logits + (1 - mask[:, None]) * -self.inf # (b, h, l, l)
|
| 40 |
+
return torch.softmax(logits, dim=-1) # (b, h, l, l)
|
| 41 |
|
| 42 |
def _all_heads(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor) -> Tensor:
|
| 43 |
+
# msa_states: (b, s, l, d_m); pair_states: (b, l, l, d_z).
|
| 44 |
values = self.proj_m(msa_states).reshape(
|
| 45 |
*msa_states.shape[:3],
|
| 46 |
self.num_heads,
|
| 47 |
self.c_h,
|
| 48 |
+
) # (b, s, l, h, d_h)
|
| 49 |
+
values = values.permute(0, 3, 1, 2, 4) # (b, h, s, l, d_h)
|
| 50 |
+
weights = self._attention_weights(pair_states, mask) # (b, h, l, l)
|
| 51 |
+
gate = self.proj_g(msa_states).sigmoid() # (b, s, l, h * d_h)
|
| 52 |
+
attended = torch.einsum(
|
| 53 |
+
"bhij,bhsjd->bhsid", weights, values
|
| 54 |
+
) # (b, h, s, l, d_h)
|
| 55 |
+
attended = attended.permute(0, 2, 3, 1, 4) # (b, s, l, h, d_h)
|
| 56 |
+
attended = attended.reshape(
|
| 57 |
+
*attended.shape[:3], self.num_heads * self.c_h
|
| 58 |
+
) # (b, s, l, h * d_h)
|
| 59 |
+
return self.proj_o(gate * attended) # (b, s, l, d_m)
|
| 60 |
|
| 61 |
def _one_head(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor, head: int) -> Tensor:
|
| 62 |
+
# msa_states: (b, s, l, d_m); pair_states: (b, l, l, d_z).
|
| 63 |
start = head * self.c_h
|
| 64 |
stop = start + self.c_h
|
| 65 |
+
value = msa_states @ self.proj_m.weight[start:stop].T # (b, s, l, d_h)
|
| 66 |
+
value = value.reshape(*value.shape[:3], 1, self.c_h).permute(
|
| 67 |
+
0, 3, 1, 2, 4
|
| 68 |
+
) # (b, 1, s, l, d_h)
|
| 69 |
+
logits = pair_states @ self.proj_z.weight[head : head + 1].T # (b, l, l, 1)
|
| 70 |
+
logits = logits.permute(0, 3, 1, 2) # (b, 1, l, l)
|
| 71 |
+
weights = torch.softmax(
|
| 72 |
+
logits + (1 - mask[:, None]) * -self.inf, dim=-1
|
| 73 |
+
) # (b, 1, l, l)
|
| 74 |
+
gate = (
|
| 75 |
+
msa_states @ self.proj_g.weight[start:stop].T
|
| 76 |
+
).sigmoid() # (b, s, l, d_h)
|
| 77 |
+
attended = torch.einsum(
|
| 78 |
+
"bhij,bhsjd->bhsid", weights, value
|
| 79 |
+
) # (b, 1, s, l, d_h)
|
| 80 |
attended = attended.permute(0, 2, 3, 1, 4).reshape(
|
| 81 |
*msa_states.shape[:3],
|
| 82 |
self.c_h,
|
| 83 |
+
) # (b, s, l, d_h)
|
| 84 |
+
return (gate * attended) @ self.proj_o.weight[:, start:stop].T # (b, s, l, d_m)
|
| 85 |
|
| 86 |
def forward(
|
| 87 |
self,
|
|
|
|
| 92 |
) -> Tensor:
|
| 93 |
"""Return updated M with shape ``(b, s, l, d_m)``."""
|
| 94 |
|
| 95 |
+
# m: (b, s, l, d_m); z: (b, l, l, d_z); mask: (b, l).
|
| 96 |
+
msa_states = self.norm_m(m) # (b, s, l, d_m)
|
| 97 |
+
pair_states = self.norm_z(z) # (b, l, l, d_z)
|
| 98 |
if not chunk_heads or self.training:
|
| 99 |
return self._all_heads(msa_states, pair_states, mask)
|
| 100 |
|
| 101 |
output: Tensor | None = None
|
| 102 |
for head in range(self.num_heads):
|
| 103 |
+
contribution = self._one_head(
|
| 104 |
+
msa_states, pair_states, mask, head
|
| 105 |
+
) # (b, s, l, d_m)
|
| 106 |
+
output = (
|
| 107 |
+
contribution if output is None else output + contribution
|
| 108 |
+
) # (b, s, l, d_m)
|
| 109 |
if output is None:
|
| 110 |
raise ValueError("num_heads must be positive")
|
| 111 |
+
return output # (b, s, l, d_m)
|
fastplms/models/boltz/vb_layers_pairformer.py
CHANGED
|
@@ -2,9 +2,8 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
from typing import Any, cast
|
| 6 |
-
|
| 7 |
import torch
|
|
|
|
| 8 |
from torch import Tensor, nn
|
| 9 |
from torch.utils.checkpoint import checkpoint
|
| 10 |
|
|
@@ -34,37 +33,42 @@ def _pair_update(
|
|
| 34 |
) -> Tensor:
|
| 35 |
"""Apply multiplicative, attention, and transition updates to pair tensor Z."""
|
| 36 |
|
|
|
|
| 37 |
multiplication_kernel = use_kernels or use_cuequiv_mul
|
| 38 |
attention_kernel = use_kernels or use_cuequiv_attn
|
| 39 |
-
output = pair_states
|
| 40 |
for update in (modules.tri_mul_out, modules.tri_mul_in):
|
| 41 |
-
dropout = get_dropout_mask(
|
|
|
|
|
|
|
| 42 |
output = output + dropout * update(
|
| 43 |
output,
|
| 44 |
mask=pair_mask,
|
| 45 |
use_kernels=multiplication_kernel,
|
| 46 |
-
)
|
| 47 |
|
| 48 |
-
dropout = get_dropout_mask(
|
|
|
|
|
|
|
| 49 |
output = output + dropout * modules.tri_att_start(
|
| 50 |
output,
|
| 51 |
mask=pair_mask,
|
| 52 |
chunk_size=chunk_size,
|
| 53 |
use_kernels=attention_kernel,
|
| 54 |
-
)
|
| 55 |
dropout = get_dropout_mask(
|
| 56 |
modules.dropout,
|
| 57 |
output,
|
| 58 |
modules.training,
|
| 59 |
columnwise=True,
|
| 60 |
-
)
|
| 61 |
output = output + dropout * modules.tri_att_end(
|
| 62 |
output,
|
| 63 |
mask=pair_mask,
|
| 64 |
chunk_size=chunk_size,
|
| 65 |
use_kernels=attention_kernel,
|
| 66 |
-
)
|
| 67 |
-
return cast(Tensor, output + modules.transition_z(output))
|
| 68 |
|
| 69 |
|
| 70 |
def _triangle_chunk_size(pair_states: Tensor, training: bool) -> int | None:
|
|
@@ -134,18 +138,22 @@ class PairformerLayer(nn.Module):
|
|
| 134 |
use_kernels,
|
| 135 |
use_cuequiv_mul,
|
| 136 |
use_cuequiv_attn,
|
| 137 |
-
)
|
| 138 |
with torch.autocast("cuda", enabled=False):
|
| 139 |
-
normalized = self.pre_norm_s(s.float())
|
| 140 |
sequence_output = s.float() + self.attention(
|
| 141 |
s=normalized,
|
| 142 |
z=pair_output.float(),
|
| 143 |
mask=mask.float(),
|
| 144 |
k_in=normalized,
|
| 145 |
-
)
|
| 146 |
-
sequence_output =
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
|
| 151 |
class PairformerModule(nn.Module):
|
|
@@ -199,8 +207,9 @@ class PairformerModule(nn.Module):
|
|
| 199 |
) -> tuple[Tensor, Tensor]:
|
| 200 |
"""Return S and Z after every configured pairformer block."""
|
| 201 |
|
|
|
|
| 202 |
chunk_size = _triangle_chunk_size(z, self.training)
|
| 203 |
-
sequence_output, pair_output = s, z
|
| 204 |
for layer in self.layers:
|
| 205 |
if self.activation_checkpointing:
|
| 206 |
sequence_output, pair_output = checkpoint(
|
|
@@ -212,7 +221,7 @@ class PairformerModule(nn.Module):
|
|
| 212 |
chunk_size,
|
| 213 |
use_kernels,
|
| 214 |
use_reentrant=False,
|
| 215 |
-
)
|
| 216 |
else:
|
| 217 |
sequence_output, pair_output = layer(
|
| 218 |
sequence_output,
|
|
@@ -221,8 +230,8 @@ class PairformerModule(nn.Module):
|
|
| 221 |
pair_mask,
|
| 222 |
chunk_size,
|
| 223 |
use_kernels,
|
| 224 |
-
)
|
| 225 |
-
return sequence_output, pair_output
|
| 226 |
|
| 227 |
|
| 228 |
class PairformerNoSeqLayer(nn.Module):
|
|
@@ -320,8 +329,9 @@ class PairformerNoSeqModule(nn.Module):
|
|
| 320 |
) -> Tensor:
|
| 321 |
"""Return Z after every configured pair-only block."""
|
| 322 |
|
|
|
|
| 323 |
chunk_size = _triangle_chunk_size(z, self.training)
|
| 324 |
-
output = z
|
| 325 |
for layer in self.layers:
|
| 326 |
if self.activation_checkpointing:
|
| 327 |
output = checkpoint(
|
|
@@ -331,7 +341,9 @@ class PairformerNoSeqModule(nn.Module):
|
|
| 331 |
chunk_size,
|
| 332 |
use_kernels,
|
| 333 |
use_reentrant=False,
|
| 334 |
-
)
|
| 335 |
else:
|
| 336 |
-
output = layer(
|
| 337 |
-
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
| 5 |
import torch
|
| 6 |
+
from typing import Any, cast
|
| 7 |
from torch import Tensor, nn
|
| 8 |
from torch.utils.checkpoint import checkpoint
|
| 9 |
|
|
|
|
| 33 |
) -> Tensor:
|
| 34 |
"""Apply multiplicative, attention, and transition updates to pair tensor Z."""
|
| 35 |
|
| 36 |
+
# pair_states: (b, l, l, d_z); pair_mask: (b, l, l).
|
| 37 |
multiplication_kernel = use_kernels or use_cuequiv_mul
|
| 38 |
attention_kernel = use_kernels or use_cuequiv_attn
|
| 39 |
+
output = pair_states # (b, l, l, d_z)
|
| 40 |
for update in (modules.tri_mul_out, modules.tri_mul_in):
|
| 41 |
+
dropout = get_dropout_mask(
|
| 42 |
+
modules.dropout, output, modules.training
|
| 43 |
+
) # broadcastable to (b, l, l, d_z)
|
| 44 |
output = output + dropout * update(
|
| 45 |
output,
|
| 46 |
mask=pair_mask,
|
| 47 |
use_kernels=multiplication_kernel,
|
| 48 |
+
) # (b, l, l, d_z)
|
| 49 |
|
| 50 |
+
dropout = get_dropout_mask(
|
| 51 |
+
modules.dropout, output, modules.training
|
| 52 |
+
) # broadcastable to (b, l, l, d_z)
|
| 53 |
output = output + dropout * modules.tri_att_start(
|
| 54 |
output,
|
| 55 |
mask=pair_mask,
|
| 56 |
chunk_size=chunk_size,
|
| 57 |
use_kernels=attention_kernel,
|
| 58 |
+
) # (b, l, l, d_z)
|
| 59 |
dropout = get_dropout_mask(
|
| 60 |
modules.dropout,
|
| 61 |
output,
|
| 62 |
modules.training,
|
| 63 |
columnwise=True,
|
| 64 |
+
) # broadcastable to (b, l, l, d_z)
|
| 65 |
output = output + dropout * modules.tri_att_end(
|
| 66 |
output,
|
| 67 |
mask=pair_mask,
|
| 68 |
chunk_size=chunk_size,
|
| 69 |
use_kernels=attention_kernel,
|
| 70 |
+
) # (b, l, l, d_z)
|
| 71 |
+
return cast(Tensor, output + modules.transition_z(output)) # (b, l, l, d_z)
|
| 72 |
|
| 73 |
|
| 74 |
def _triangle_chunk_size(pair_states: Tensor, training: bool) -> int | None:
|
|
|
|
| 138 |
use_kernels,
|
| 139 |
use_cuequiv_mul,
|
| 140 |
use_cuequiv_attn,
|
| 141 |
+
) # (b, l, l, d_z)
|
| 142 |
with torch.autocast("cuda", enabled=False):
|
| 143 |
+
normalized = self.pre_norm_s(s.float()) # (b, l, d_s)
|
| 144 |
sequence_output = s.float() + self.attention(
|
| 145 |
s=normalized,
|
| 146 |
z=pair_output.float(),
|
| 147 |
mask=mask.float(),
|
| 148 |
k_in=normalized,
|
| 149 |
+
) # (b, l, d_s)
|
| 150 |
+
sequence_output = (
|
| 151 |
+
sequence_output + self.transition_s(sequence_output)
|
| 152 |
+
) # (b, l, d_s)
|
| 153 |
+
sequence_output = cast(
|
| 154 |
+
Tensor, self.s_post_norm(sequence_output)
|
| 155 |
+
) # (b, l, d_s)
|
| 156 |
+
return sequence_output, pair_output # (b, l, d_s), (b, l, l, d_z)
|
| 157 |
|
| 158 |
|
| 159 |
class PairformerModule(nn.Module):
|
|
|
|
| 207 |
) -> tuple[Tensor, Tensor]:
|
| 208 |
"""Return S and Z after every configured pairformer block."""
|
| 209 |
|
| 210 |
+
# s: (b, l, d_s); z: (b, l, l, d_z).
|
| 211 |
chunk_size = _triangle_chunk_size(z, self.training)
|
| 212 |
+
sequence_output, pair_output = s, z # (b, l, d_s), (b, l, l, d_z)
|
| 213 |
for layer in self.layers:
|
| 214 |
if self.activation_checkpointing:
|
| 215 |
sequence_output, pair_output = checkpoint(
|
|
|
|
| 221 |
chunk_size,
|
| 222 |
use_kernels,
|
| 223 |
use_reentrant=False,
|
| 224 |
+
) # (b, l, d_s), (b, l, l, d_z)
|
| 225 |
else:
|
| 226 |
sequence_output, pair_output = layer(
|
| 227 |
sequence_output,
|
|
|
|
| 230 |
pair_mask,
|
| 231 |
chunk_size,
|
| 232 |
use_kernels,
|
| 233 |
+
) # (b, l, d_s), (b, l, l, d_z)
|
| 234 |
+
return sequence_output, pair_output # (b, l, d_s), (b, l, l, d_z)
|
| 235 |
|
| 236 |
|
| 237 |
class PairformerNoSeqLayer(nn.Module):
|
|
|
|
| 329 |
) -> Tensor:
|
| 330 |
"""Return Z after every configured pair-only block."""
|
| 331 |
|
| 332 |
+
# z: (b, l, l, d_z); pair_mask: (b, l, l).
|
| 333 |
chunk_size = _triangle_chunk_size(z, self.training)
|
| 334 |
+
output = z # (b, l, l, d_z)
|
| 335 |
for layer in self.layers:
|
| 336 |
if self.activation_checkpointing:
|
| 337 |
output = checkpoint(
|
|
|
|
| 341 |
chunk_size,
|
| 342 |
use_kernels,
|
| 343 |
use_reentrant=False,
|
| 344 |
+
) # (b, l, l, d_z)
|
| 345 |
else:
|
| 346 |
+
output = layer(
|
| 347 |
+
output, pair_mask, chunk_size, use_kernels
|
| 348 |
+
) # (b, l, l, d_z)
|
| 349 |
+
return output # (b, l, l, d_z)
|
fastplms/models/boltz/vb_layers_transition.py
CHANGED
|
@@ -40,19 +40,26 @@ class Transition(nn.Module):
|
|
| 40 |
def _project_hidden_slice(self, normalized: Tensor, start: int, stop: int) -> Tensor:
|
| 41 |
"""Return one hidden-width contribution to the output projection."""
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
def forward(self, x: Tensor, chunk_size: int | None = None) -> Tensor:
|
| 48 |
"""Transform X, optionally accumulating the hidden dimension in chunks."""
|
| 49 |
|
| 50 |
# X is the normalized input tensor with shape (..., d).
|
| 51 |
-
normalized = self.norm(x)
|
| 52 |
if chunk_size is None or self.training:
|
| 53 |
# H is the gated hidden tensor with shape (..., d_hidden).
|
| 54 |
hidden_states = self.silu(self.fc1(normalized)) * self.fc2(normalized)
|
| 55 |
-
return self.fc3(hidden_states)
|
| 56 |
if chunk_size <= 0:
|
| 57 |
raise ValueError("chunk_size must be positive")
|
| 58 |
|
|
@@ -62,8 +69,10 @@ class Transition(nn.Module):
|
|
| 62 |
normalized,
|
| 63 |
start,
|
| 64 |
min(start + chunk_size, self.hidden),
|
| 65 |
-
)
|
| 66 |
-
output =
|
|
|
|
|
|
|
| 67 |
if output is None: # hidden is normally positive; keep malformed configs explicit.
|
| 68 |
raise ValueError("hidden must be positive")
|
| 69 |
-
return output
|
|
|
|
| 40 |
def _project_hidden_slice(self, normalized: Tensor, start: int, stop: int) -> Tensor:
|
| 41 |
"""Return one hidden-width contribution to the output projection."""
|
| 42 |
|
| 43 |
+
# normalized: (..., d); the selected hidden width is d_chunk.
|
| 44 |
+
gate = self.silu(
|
| 45 |
+
torch.matmul(normalized, self.fc1.weight[start:stop].T)
|
| 46 |
+
) # (..., d_chunk)
|
| 47 |
+
value = torch.matmul(
|
| 48 |
+
normalized, self.fc2.weight[start:stop].T
|
| 49 |
+
) # (..., d_chunk)
|
| 50 |
+
return torch.matmul(
|
| 51 |
+
gate * value, self.fc3.weight[:, start:stop].T
|
| 52 |
+
) # (..., d_out)
|
| 53 |
|
| 54 |
def forward(self, x: Tensor, chunk_size: int | None = None) -> Tensor:
|
| 55 |
"""Transform X, optionally accumulating the hidden dimension in chunks."""
|
| 56 |
|
| 57 |
# X is the normalized input tensor with shape (..., d).
|
| 58 |
+
normalized = self.norm(x) # (..., d)
|
| 59 |
if chunk_size is None or self.training:
|
| 60 |
# H is the gated hidden tensor with shape (..., d_hidden).
|
| 61 |
hidden_states = self.silu(self.fc1(normalized)) * self.fc2(normalized)
|
| 62 |
+
return self.fc3(hidden_states) # (..., d_out)
|
| 63 |
if chunk_size <= 0:
|
| 64 |
raise ValueError("chunk_size must be positive")
|
| 65 |
|
|
|
|
| 69 |
normalized,
|
| 70 |
start,
|
| 71 |
min(start + chunk_size, self.hidden),
|
| 72 |
+
) # (..., d_out)
|
| 73 |
+
output = (
|
| 74 |
+
contribution if output is None else output + contribution
|
| 75 |
+
) # (..., d_out)
|
| 76 |
if output is None: # hidden is normally positive; keep malformed configs explicit.
|
| 77 |
raise ValueError("hidden must be positive")
|
| 78 |
+
return output # (..., d_out)
|
fastplms/models/boltz/vb_layers_triangular_mult.py
CHANGED
|
@@ -3,14 +3,14 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import importlib
|
|
|
|
| 6 |
from importlib.util import find_spec
|
| 7 |
from typing import Literal
|
| 8 |
-
|
| 9 |
-
import torch
|
| 10 |
from torch import Tensor, nn
|
| 11 |
|
| 12 |
from . import vb_layers_initialize as init
|
| 13 |
|
|
|
|
| 14 |
TriangleDirection = Literal["incoming", "outgoing"]
|
| 15 |
|
| 16 |
|
|
@@ -31,6 +31,7 @@ def kernel_triangular_mult(
|
|
| 31 |
) -> Tensor:
|
| 32 |
"""Dispatch the optional cuEquivariance triangle primitive lazily."""
|
| 33 |
|
|
|
|
| 34 |
if (
|
| 35 |
find_spec("cuequivariance_torch") is None
|
| 36 |
or find_spec("cuequivariance_ops_torch") is None
|
|
@@ -81,6 +82,7 @@ class _TriangleMultiplication(nn.Module):
|
|
| 81 |
init.gating_init_(self.g_out.weight)
|
| 82 |
|
| 83 |
def _kernel_forward(self, pair_states: Tensor, mask: Tensor) -> Tensor:
|
|
|
|
| 84 |
return kernel_triangular_mult(
|
| 85 |
pair_states,
|
| 86 |
direction=self.direction,
|
|
@@ -94,7 +96,7 @@ class _TriangleMultiplication(nn.Module):
|
|
| 94 |
p_out_weight=self.p_out.weight,
|
| 95 |
g_out_weight=self.g_out.weight,
|
| 96 |
eps=1e-5,
|
| 97 |
-
)
|
| 98 |
|
| 99 |
def forward(self, x: Tensor, mask: Tensor, use_kernels: bool = False) -> Tensor:
|
| 100 |
"""Transform pair tensor X with shape ``(b, l, l, d)``."""
|
|
@@ -103,12 +105,16 @@ class _TriangleMultiplication(nn.Module):
|
|
| 103 |
return self._kernel_forward(x, mask)
|
| 104 |
|
| 105 |
# X_norm is the normalized pair tensor used by the output gate.
|
| 106 |
-
normalized = self.norm_in(x)
|
| 107 |
-
projected =
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
|
| 114 |
class TriangleMultiplicationOutgoing(_TriangleMultiplication):
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import importlib
|
| 6 |
+
import torch
|
| 7 |
from importlib.util import find_spec
|
| 8 |
from typing import Literal
|
|
|
|
|
|
|
| 9 |
from torch import Tensor, nn
|
| 10 |
|
| 11 |
from . import vb_layers_initialize as init
|
| 12 |
|
| 13 |
+
|
| 14 |
TriangleDirection = Literal["incoming", "outgoing"]
|
| 15 |
|
| 16 |
|
|
|
|
| 31 |
) -> Tensor:
|
| 32 |
"""Dispatch the optional cuEquivariance triangle primitive lazily."""
|
| 33 |
|
| 34 |
+
# x: (b, l, l, d); mask: (b, l, l); returned tensor: (b, l, l, d).
|
| 35 |
if (
|
| 36 |
find_spec("cuequivariance_torch") is None
|
| 37 |
or find_spec("cuequivariance_ops_torch") is None
|
|
|
|
| 82 |
init.gating_init_(self.g_out.weight)
|
| 83 |
|
| 84 |
def _kernel_forward(self, pair_states: Tensor, mask: Tensor) -> Tensor:
|
| 85 |
+
# pair_states: (b, l, l, d); mask: (b, l, l).
|
| 86 |
return kernel_triangular_mult(
|
| 87 |
pair_states,
|
| 88 |
direction=self.direction,
|
|
|
|
| 96 |
p_out_weight=self.p_out.weight,
|
| 97 |
g_out_weight=self.g_out.weight,
|
| 98 |
eps=1e-5,
|
| 99 |
+
) # (b, l, l, d)
|
| 100 |
|
| 101 |
def forward(self, x: Tensor, mask: Tensor, use_kernels: bool = False) -> Tensor:
|
| 102 |
"""Transform pair tensor X with shape ``(b, l, l, d)``."""
|
|
|
|
| 105 |
return self._kernel_forward(x, mask)
|
| 106 |
|
| 107 |
# X_norm is the normalized pair tensor used by the output gate.
|
| 108 |
+
normalized = self.norm_in(x) # (b, l, l, d)
|
| 109 |
+
projected = (
|
| 110 |
+
self.p_in(normalized) * self.g_in(normalized).sigmoid()
|
| 111 |
+
) # (b, l, l, 2 * d)
|
| 112 |
+
projected = projected * mask.unsqueeze(-1) # (b, l, l, 2 * d)
|
| 113 |
+
left, right = torch.chunk(projected.float(), 2, dim=-1) # each: (b, l, l, d)
|
| 114 |
+
combined = torch.einsum(self.equation, left, right) # (b, l, l, d)
|
| 115 |
+
return (
|
| 116 |
+
self.p_out(self.norm_out(combined)) * self.g_out(normalized).sigmoid()
|
| 117 |
+
) # (b, l, l, d)
|
| 118 |
|
| 119 |
|
| 120 |
class TriangleMultiplicationOutgoing(_TriangleMultiplication):
|
fastplms/models/boltz/vb_loss_diffusionv2.py
CHANGED
|
@@ -8,7 +8,6 @@ does not import an upstream runtime package.
|
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
import warnings
|
| 11 |
-
|
| 12 |
import torch
|
| 13 |
import torch.nn.functional as functional
|
| 14 |
from einops import einsum
|
|
@@ -18,10 +17,11 @@ def _weighted_centroid(
|
|
| 18 |
coordinates: torch.Tensor,
|
| 19 |
weights: torch.Tensor,
|
| 20 |
) -> torch.Tensor:
|
|
|
|
| 21 |
return (coordinates * weights).sum(dim=-2, keepdim=True) / weights.sum(
|
| 22 |
dim=-2,
|
| 23 |
keepdim=True,
|
| 24 |
-
)
|
| 25 |
|
| 26 |
|
| 27 |
def _warn_if_alignment_is_ambiguous(
|
|
@@ -62,25 +62,25 @@ def weighted_rigid_align(
|
|
| 62 |
|
| 63 |
output_shape = torch.broadcast_shapes(true_coords.shape, pred_coords.shape)
|
| 64 |
*batch_shape, num_points, coordinate_dim = output_shape
|
| 65 |
-
point_weights = (mask * weights).unsqueeze(-1)
|
| 66 |
|
| 67 |
-
true_centroid = _weighted_centroid(true_coords, point_weights)
|
| 68 |
-
pred_centroid = _weighted_centroid(pred_coords, point_weights)
|
| 69 |
-
true_centered = true_coords - true_centroid
|
| 70 |
-
pred_centered = pred_coords - pred_centroid
|
| 71 |
|
| 72 |
covariance = einsum(
|
| 73 |
point_weights * pred_centered,
|
| 74 |
true_centered,
|
| 75 |
"... n i, ... n j -> ... i j",
|
| 76 |
-
)
|
| 77 |
original_dtype = covariance.dtype
|
| 78 |
-
covariance_fp32 = covariance.to(torch.float32)
|
| 79 |
left_vectors, singular_values, right_vectors_h = torch.linalg.svd(
|
| 80 |
covariance_fp32,
|
| 81 |
driver="gesvd" if covariance_fp32.is_cuda else None,
|
| 82 |
-
)
|
| 83 |
-
right_vectors = right_vectors_h.mH
|
| 84 |
_warn_if_alignment_is_ambiguous(
|
| 85 |
mask,
|
| 86 |
singular_values,
|
|
@@ -92,23 +92,25 @@ def weighted_rigid_align(
|
|
| 92 |
"... i j, ... k j -> ... i k",
|
| 93 |
left_vectors,
|
| 94 |
right_vectors,
|
| 95 |
-
).to(torch.float32)
|
| 96 |
orientation = torch.eye(
|
| 97 |
coordinate_dim,
|
| 98 |
dtype=covariance_fp32.dtype,
|
| 99 |
device=covariance.device,
|
| 100 |
-
)[None].repeat(*batch_shape, 1, 1)
|
| 101 |
-
orientation[..., -1, -1] = torch.det(preliminary_rotation)
|
| 102 |
rotation = einsum(
|
| 103 |
left_vectors,
|
| 104 |
orientation,
|
| 105 |
right_vectors,
|
| 106 |
"... i j, ... j k, ... l k -> ... i l",
|
| 107 |
-
).to(original_dtype)
|
| 108 |
|
| 109 |
-
aligned =
|
|
|
|
|
|
|
| 110 |
aligned.detach_()
|
| 111 |
-
return aligned
|
| 112 |
|
| 113 |
|
| 114 |
def _smooth_lddt_for_example(
|
|
@@ -120,33 +122,38 @@ def _smooth_lddt_for_example(
|
|
| 120 |
nucleic_acid_cutoff: float,
|
| 121 |
other_cutoff: float,
|
| 122 |
) -> torch.Tensor:
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
| 125 |
pair_mask = torch.where(
|
| 126 |
nucleotide_rows,
|
| 127 |
true_distances < nucleic_acid_cutoff,
|
| 128 |
true_distances < other_cutoff,
|
| 129 |
-
)
|
| 130 |
pair_mask &= ~torch.eye(
|
| 131 |
pred_coords.shape[0],
|
| 132 |
dtype=torch.bool,
|
| 133 |
device=pred_coords.device,
|
| 134 |
-
)
|
| 135 |
-
coordinate_rows = coords_mask.bool()
|
| 136 |
-
pair_mask &= coordinate_rows.unsqueeze(-1)
|
| 137 |
-
pair_mask &= coordinate_rows.unsqueeze(-2)
|
| 138 |
-
|
| 139 |
-
pair_indices = pair_mask.nonzero()
|
| 140 |
-
true_pair_distances = true_distances[
|
|
|
|
|
|
|
| 141 |
pred_pair_distances = functional.pairwise_distance(
|
| 142 |
pred_coords[pair_indices[:, 0]],
|
| 143 |
pred_coords[pair_indices[:, 1]],
|
| 144 |
-
)
|
| 145 |
-
distance_error = torch.abs(true_pair_distances - pred_pair_distances)
|
| 146 |
smooth_agreement = (
|
| 147 |
sum(torch.sigmoid(threshold - distance_error) for threshold in (0.5, 1.0, 2.0, 4.0)) / 4.0
|
| 148 |
-
)
|
| 149 |
-
return smooth_agreement.sum() / (pair_indices.shape[0] + 1e-5)
|
| 150 |
|
| 151 |
|
| 152 |
def smooth_lddt_loss(
|
|
@@ -174,5 +181,5 @@ def smooth_lddt_loss(
|
|
| 174 |
other_cutoff=other_cutoff,
|
| 175 |
)
|
| 176 |
for index in range(true_coords.shape[0])
|
| 177 |
-
]
|
| 178 |
-
return 1.0 - torch.stack(agreements).mean(dim=0)
|
|
|
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
import warnings
|
|
|
|
| 11 |
import torch
|
| 12 |
import torch.nn.functional as functional
|
| 13 |
from einops import einsum
|
|
|
|
| 17 |
coordinates: torch.Tensor,
|
| 18 |
weights: torch.Tensor,
|
| 19 |
) -> torch.Tensor:
|
| 20 |
+
# coordinates: (..., n, 3); weights: (..., n, 1).
|
| 21 |
return (coordinates * weights).sum(dim=-2, keepdim=True) / weights.sum(
|
| 22 |
dim=-2,
|
| 23 |
keepdim=True,
|
| 24 |
+
) # (..., 1, 3)
|
| 25 |
|
| 26 |
|
| 27 |
def _warn_if_alignment_is_ambiguous(
|
|
|
|
| 62 |
|
| 63 |
output_shape = torch.broadcast_shapes(true_coords.shape, pred_coords.shape)
|
| 64 |
*batch_shape, num_points, coordinate_dim = output_shape
|
| 65 |
+
point_weights = (mask * weights).unsqueeze(-1) # (..., n, 1)
|
| 66 |
|
| 67 |
+
true_centroid = _weighted_centroid(true_coords, point_weights) # (..., 1, 3)
|
| 68 |
+
pred_centroid = _weighted_centroid(pred_coords, point_weights) # (..., 1, 3)
|
| 69 |
+
true_centered = true_coords - true_centroid # (..., n, 3)
|
| 70 |
+
pred_centered = pred_coords - pred_centroid # (..., n, 3)
|
| 71 |
|
| 72 |
covariance = einsum(
|
| 73 |
point_weights * pred_centered,
|
| 74 |
true_centered,
|
| 75 |
"... n i, ... n j -> ... i j",
|
| 76 |
+
) # (..., 3, 3)
|
| 77 |
original_dtype = covariance.dtype
|
| 78 |
+
covariance_fp32 = covariance.to(torch.float32) # (..., 3, 3)
|
| 79 |
left_vectors, singular_values, right_vectors_h = torch.linalg.svd(
|
| 80 |
covariance_fp32,
|
| 81 |
driver="gesvd" if covariance_fp32.is_cuda else None,
|
| 82 |
+
) # left/right: (..., 3, 3); singular_values: (..., 3)
|
| 83 |
+
right_vectors = right_vectors_h.mH # (..., 3, 3)
|
| 84 |
_warn_if_alignment_is_ambiguous(
|
| 85 |
mask,
|
| 86 |
singular_values,
|
|
|
|
| 92 |
"... i j, ... k j -> ... i k",
|
| 93 |
left_vectors,
|
| 94 |
right_vectors,
|
| 95 |
+
).to(torch.float32) # (..., 3, 3)
|
| 96 |
orientation = torch.eye(
|
| 97 |
coordinate_dim,
|
| 98 |
dtype=covariance_fp32.dtype,
|
| 99 |
device=covariance.device,
|
| 100 |
+
)[None].repeat(*batch_shape, 1, 1) # (..., 3, 3)
|
| 101 |
+
orientation[..., -1, -1] = torch.det(preliminary_rotation) # (...)
|
| 102 |
rotation = einsum(
|
| 103 |
left_vectors,
|
| 104 |
orientation,
|
| 105 |
right_vectors,
|
| 106 |
"... i j, ... j k, ... l k -> ... i l",
|
| 107 |
+
).to(original_dtype) # (..., 3, 3)
|
| 108 |
|
| 109 |
+
aligned = (
|
| 110 |
+
einsum(true_centered, rotation, "... n i, ... j i -> ... n j") + pred_centroid
|
| 111 |
+
) # (..., n, 3)
|
| 112 |
aligned.detach_()
|
| 113 |
+
return aligned # (..., n, 3)
|
| 114 |
|
| 115 |
|
| 116 |
def _smooth_lddt_for_example(
|
|
|
|
| 122 |
nucleic_acid_cutoff: float,
|
| 123 |
other_cutoff: float,
|
| 124 |
) -> torch.Tensor:
|
| 125 |
+
# pred_coords/true_coords: (n, 3); is_nucleotide/coords_mask: (n,).
|
| 126 |
+
true_distances = torch.cdist(true_coords, true_coords) # (n, n)
|
| 127 |
+
nucleotide_rows = is_nucleotide.bool().unsqueeze(-1).expand_as(
|
| 128 |
+
true_distances
|
| 129 |
+
) # (n, n)
|
| 130 |
pair_mask = torch.where(
|
| 131 |
nucleotide_rows,
|
| 132 |
true_distances < nucleic_acid_cutoff,
|
| 133 |
true_distances < other_cutoff,
|
| 134 |
+
) # (n, n)
|
| 135 |
pair_mask &= ~torch.eye(
|
| 136 |
pred_coords.shape[0],
|
| 137 |
dtype=torch.bool,
|
| 138 |
device=pred_coords.device,
|
| 139 |
+
) # (n, n)
|
| 140 |
+
coordinate_rows = coords_mask.bool() # (n,)
|
| 141 |
+
pair_mask &= coordinate_rows.unsqueeze(-1) # (n, n)
|
| 142 |
+
pair_mask &= coordinate_rows.unsqueeze(-2) # (n, n)
|
| 143 |
+
|
| 144 |
+
pair_indices = pair_mask.nonzero() # (n_pair, 2)
|
| 145 |
+
true_pair_distances = true_distances[
|
| 146 |
+
pair_indices[:, 0], pair_indices[:, 1]
|
| 147 |
+
] # (n_pair,)
|
| 148 |
pred_pair_distances = functional.pairwise_distance(
|
| 149 |
pred_coords[pair_indices[:, 0]],
|
| 150 |
pred_coords[pair_indices[:, 1]],
|
| 151 |
+
) # (n_pair,)
|
| 152 |
+
distance_error = torch.abs(true_pair_distances - pred_pair_distances) # (n_pair,)
|
| 153 |
smooth_agreement = (
|
| 154 |
sum(torch.sigmoid(threshold - distance_error) for threshold in (0.5, 1.0, 2.0, 4.0)) / 4.0
|
| 155 |
+
) # (n_pair,)
|
| 156 |
+
return smooth_agreement.sum() / (pair_indices.shape[0] + 1e-5) # ()
|
| 157 |
|
| 158 |
|
| 159 |
def smooth_lddt_loss(
|
|
|
|
| 181 |
other_cutoff=other_cutoff,
|
| 182 |
)
|
| 183 |
for index in range(true_coords.shape[0])
|
| 184 |
+
] # each: ()
|
| 185 |
+
return 1.0 - torch.stack(agreements).mean(dim=0) # ()
|
fastplms/models/boltz/vb_modules_diffusion_conditioning.py
CHANGED
|
@@ -30,7 +30,10 @@ def _concatenate_biases(
|
|
| 30 |
projections: nn.ModuleList,
|
| 31 |
pair_features: torch.Tensor,
|
| 32 |
) -> torch.Tensor:
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
class DiffusionConditioning(nn.Module):
|
|
@@ -112,21 +115,26 @@ class DiffusionConditioning(nn.Module):
|
|
| 112 |
order so downstream code can select them by layer.
|
| 113 |
"""
|
| 114 |
|
|
|
|
| 115 |
z_conditioned = self.pairwise_conditioner(
|
| 116 |
z_trunk,
|
| 117 |
relative_position_encoding,
|
| 118 |
-
)
|
| 119 |
q, c, p, to_keys = self.atom_encoder(
|
| 120 |
feats=feats,
|
| 121 |
s_trunk=s_trunk,
|
| 122 |
z=z_conditioned,
|
| 123 |
-
)
|
| 124 |
-
atom_encoder_bias = _concatenate_biases(
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
token_transformer_bias = _concatenate_biases(
|
| 127 |
self.token_trans_proj_z,
|
| 128 |
z_conditioned,
|
| 129 |
-
)
|
| 130 |
return (
|
| 131 |
q,
|
| 132 |
c,
|
|
@@ -134,4 +142,4 @@ class DiffusionConditioning(nn.Module):
|
|
| 134 |
atom_encoder_bias,
|
| 135 |
atom_decoder_bias,
|
| 136 |
token_transformer_bias,
|
| 137 |
-
)
|
|
|
|
| 30 |
projections: nn.ModuleList,
|
| 31 |
pair_features: torch.Tensor,
|
| 32 |
) -> torch.Tensor:
|
| 33 |
+
# pair_features: (..., d_pair); each projection: (..., h).
|
| 34 |
+
return torch.cat(
|
| 35 |
+
[projection(pair_features) for projection in projections], dim=-1
|
| 36 |
+
) # (..., depth * h)
|
| 37 |
|
| 38 |
|
| 39 |
class DiffusionConditioning(nn.Module):
|
|
|
|
| 115 |
order so downstream code can select them by layer.
|
| 116 |
"""
|
| 117 |
|
| 118 |
+
# b is batch size, t token count, a atom count, and k the atom-window count.
|
| 119 |
z_conditioned = self.pairwise_conditioner(
|
| 120 |
z_trunk,
|
| 121 |
relative_position_encoding,
|
| 122 |
+
) # (b, t, t, d_z)
|
| 123 |
q, c, p, to_keys = self.atom_encoder(
|
| 124 |
feats=feats,
|
| 125 |
s_trunk=s_trunk,
|
| 126 |
z=z_conditioned,
|
| 127 |
+
) # q/c: (b, a, d_a); p: (b, k, w, h_k, d_p); to_keys: callable
|
| 128 |
+
atom_encoder_bias = _concatenate_biases(
|
| 129 |
+
self.atom_enc_proj_z, p
|
| 130 |
+
) # (b, k, w, h_k, depth_enc * heads_enc)
|
| 131 |
+
atom_decoder_bias = _concatenate_biases(
|
| 132 |
+
self.atom_dec_proj_z, p
|
| 133 |
+
) # (b, k, w, h_k, depth_dec * heads_dec)
|
| 134 |
token_transformer_bias = _concatenate_biases(
|
| 135 |
self.token_trans_proj_z,
|
| 136 |
z_conditioned,
|
| 137 |
+
) # (b, t, t, depth_token * heads_token)
|
| 138 |
return (
|
| 139 |
q,
|
| 140 |
c,
|
|
|
|
| 142 |
atom_encoder_bias,
|
| 143 |
atom_decoder_bias,
|
| 144 |
token_transformer_bias,
|
| 145 |
+
) # tensor shapes are traced above; to_keys is the atom-key gatherer
|
fastplms/models/boltz/vb_modules_encodersv2.py
CHANGED
|
@@ -11,11 +11,10 @@ implementations under MIT terms. See ``THIRD_PARTY_NOTICES.md``.
|
|
| 11 |
|
| 12 |
from __future__ import annotations
|
| 13 |
|
|
|
|
| 14 |
from collections.abc import Callable
|
| 15 |
from functools import partial
|
| 16 |
from math import pi
|
| 17 |
-
|
| 18 |
-
import torch
|
| 19 |
from torch import nn
|
| 20 |
from torch.nn.functional import one_hot
|
| 21 |
|
|
@@ -46,12 +45,13 @@ class FourierEmbedding(nn.Module):
|
|
| 46 |
def forward(self, times: torch.Tensor) -> torch.Tensor:
|
| 47 |
"""Map times with shape ``(b,)`` to an embedding ``H: (b, d)``."""
|
| 48 |
|
| 49 |
-
random_phase = self.proj(times.reshape(-1, 1))
|
| 50 |
-
return torch.cos(2 * pi * random_phase)
|
| 51 |
|
| 52 |
|
| 53 |
def _pairwise_difference(values: torch.Tensor) -> torch.Tensor:
|
| 54 |
-
|
|
|
|
| 55 |
|
| 56 |
|
| 57 |
class RelativePositionEncoder(nn.Module):
|
|
@@ -76,54 +76,68 @@ class RelativePositionEncoder(nn.Module):
|
|
| 76 |
def forward(self, feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 77 |
"""Return relative pair features ``Z: (b, n, n, token_z)``."""
|
| 78 |
|
| 79 |
-
same_chain =
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
if self.cyclic_pos_enc and torch.any(feats["cyclic_period"] > 0):
|
| 85 |
period = torch.where(
|
| 86 |
feats["cyclic_period"] > 0,
|
| 87 |
feats["cyclic_period"],
|
| 88 |
torch.zeros_like(feats["cyclic_period"]) + 10000,
|
| 89 |
-
)
|
| 90 |
-
residue_offset = (
|
|
|
|
|
|
|
| 91 |
residue_offset = torch.clip(
|
| 92 |
residue_offset + self.r_max,
|
| 93 |
0,
|
| 94 |
2 * self.r_max,
|
| 95 |
-
)
|
| 96 |
residue_offset = torch.where(
|
| 97 |
same_chain,
|
| 98 |
residue_offset,
|
| 99 |
torch.zeros_like(residue_offset) + 2 * self.r_max + 1,
|
| 100 |
-
)
|
| 101 |
-
residue_features = one_hot(
|
|
|
|
|
|
|
| 102 |
|
| 103 |
token_offset = torch.clip(
|
| 104 |
_pairwise_difference(feats["token_index"]) + self.r_max,
|
| 105 |
0,
|
| 106 |
2 * self.r_max,
|
| 107 |
-
)
|
| 108 |
token_offset = torch.where(
|
| 109 |
same_chain & same_residue,
|
| 110 |
token_offset,
|
| 111 |
torch.zeros_like(token_offset) + 2 * self.r_max + 1,
|
| 112 |
-
)
|
| 113 |
-
token_features = one_hot(
|
|
|
|
|
|
|
| 114 |
|
| 115 |
symmetry_offset = torch.clip(
|
| 116 |
_pairwise_difference(feats["sym_id"]) + self.s_max,
|
| 117 |
0,
|
| 118 |
2 * self.s_max,
|
| 119 |
-
)
|
| 120 |
-
invalid_symmetry = ~same_entity if self.fix_sym_check else same_chain
|
| 121 |
symmetry_offset = torch.where(
|
| 122 |
invalid_symmetry,
|
| 123 |
torch.zeros_like(symmetry_offset) + 2 * self.s_max + 1,
|
| 124 |
symmetry_offset,
|
| 125 |
-
)
|
| 126 |
-
symmetry_features = one_hot(
|
|
|
|
|
|
|
| 127 |
|
| 128 |
pair_features = torch.cat(
|
| 129 |
(
|
|
@@ -133,8 +147,8 @@ class RelativePositionEncoder(nn.Module):
|
|
| 133 |
symmetry_features.float(),
|
| 134 |
),
|
| 135 |
dim=-1,
|
| 136 |
-
)
|
| 137 |
-
return self.linear_layer(pair_features)
|
| 138 |
|
| 139 |
|
| 140 |
class SingleConditioning(nn.Module):
|
|
@@ -175,17 +189,20 @@ class SingleConditioning(nn.Module):
|
|
| 175 |
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
| 176 |
"""Return conditioned token features ``S: (b, n, 2 * token_s)``."""
|
| 177 |
|
| 178 |
-
|
| 179 |
-
single =
|
| 180 |
-
|
|
|
|
| 181 |
if not self.disable_times:
|
| 182 |
-
fourier = self.fourier_embed(times)
|
| 183 |
-
normalized_fourier = self.norm_fourier(fourier)
|
| 184 |
-
time_condition = self.fourier_to_single(
|
| 185 |
-
|
|
|
|
|
|
|
| 186 |
for transition in self.transitions:
|
| 187 |
-
single = transition(single) + single
|
| 188 |
-
return single, normalized_fourier
|
| 189 |
|
| 190 |
|
| 191 |
class PairwiseConditioning(nn.Module):
|
|
@@ -217,10 +234,12 @@ class PairwiseConditioning(nn.Module):
|
|
| 217 |
) -> torch.Tensor:
|
| 218 |
"""Return conditioned pair features ``Z: (b, n, n, token_z)``."""
|
| 219 |
|
| 220 |
-
pair = self.dim_pairwise_init_proj(
|
|
|
|
|
|
|
| 221 |
for transition in self.transitions:
|
| 222 |
-
pair = transition(pair) + pair
|
| 223 |
-
return pair
|
| 224 |
|
| 225 |
|
| 226 |
def get_indexing_matrix(
|
|
@@ -247,13 +266,17 @@ def get_indexing_matrix(
|
|
| 247 |
f"h must contain an even number of half-window key blocks; received {key_blocks}."
|
| 248 |
)
|
| 249 |
|
| 250 |
-
positions = torch.arange(2 * k, device=device)
|
| 251 |
-
relative_blocks = (
|
| 252 |
-
|
| 253 |
-
)
|
| 254 |
-
relative_blocks = relative_blocks.view(k, 2, 2 * k)[:, 0, :]
|
| 255 |
-
selectors = one_hot(relative_blocks, num_classes=key_blocks + 2)[
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
|
| 259 |
def single_to_keys(
|
|
@@ -266,9 +289,11 @@ def single_to_keys(
|
|
| 266 |
|
| 267 |
b, n, d = single.shape
|
| 268 |
k = n // w
|
| 269 |
-
half_windows = single.view(b, 2 * k, w // 2, d)
|
| 270 |
-
gathered = torch.einsum(
|
| 271 |
-
|
|
|
|
|
|
|
| 272 |
|
| 273 |
|
| 274 |
class AtomEncoder(nn.Module):
|
|
@@ -332,11 +357,12 @@ class AtomEncoder(nn.Module):
|
|
| 332 |
b: int,
|
| 333 |
n: int,
|
| 334 |
) -> torch.Tensor:
|
|
|
|
| 335 |
feature_parts = [
|
| 336 |
feats["ref_pos"],
|
| 337 |
feats["ref_charge"].unsqueeze(-1),
|
| 338 |
feats["ref_element"],
|
| 339 |
-
]
|
| 340 |
if not self.use_no_atom_char:
|
| 341 |
feature_parts.append(feats["ref_atom_name_chars"].reshape(b, n, 4 * 64))
|
| 342 |
if self.use_atom_backbone_feat:
|
|
@@ -349,9 +375,11 @@ class AtomEncoder(nn.Module):
|
|
| 349 |
one_hot(feats["mol_type"], num_classes=4).float(),
|
| 350 |
),
|
| 351 |
dim=-1,
|
| 352 |
-
)
|
| 353 |
-
feature_parts.append(
|
| 354 |
-
|
|
|
|
|
|
|
| 355 |
|
| 356 |
def forward(
|
| 357 |
self,
|
|
@@ -368,61 +396,81 @@ class AtomEncoder(nn.Module):
|
|
| 368 |
|
| 369 |
with torch.autocast("cuda", enabled=False):
|
| 370 |
b, n, _ = feats["ref_pos"].shape
|
| 371 |
-
atom_mask = feats["atom_pad_mask"].bool()
|
| 372 |
-
atom_positions = feats["ref_pos"]
|
| 373 |
-
atom_space = feats["ref_space_uid"]
|
| 374 |
-
atom_features = self._assemble_atom_features(
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
w = self.atoms_per_window_queries
|
| 378 |
h = self.atoms_per_window_keys
|
| 379 |
b, n = conditioned_single.shape[:2]
|
| 380 |
k = n // w
|
| 381 |
-
indexing = get_indexing_matrix(
|
|
|
|
|
|
|
| 382 |
to_keys = partial(single_to_keys, indexing_matrix=indexing, w=w, h=h)
|
| 383 |
|
| 384 |
-
position_queries = atom_positions.view(b, k, w, 1, 3)
|
| 385 |
-
position_keys = to_keys(atom_positions).view(
|
| 386 |
-
|
|
|
|
|
|
|
| 387 |
inverse_squared_distance = 1 / (
|
| 388 |
1 + torch.sum(displacement * displacement, dim=-1, keepdim=True)
|
| 389 |
-
)
|
| 390 |
-
|
| 391 |
-
mask_queries = atom_mask.view(b, k, w, 1)
|
| 392 |
-
mask_keys =
|
| 393 |
-
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
valid_pair = (
|
| 396 |
(mask_queries & mask_keys & (space_queries == space_keys)).float().unsqueeze(-1)
|
| 397 |
-
)
|
| 398 |
|
| 399 |
-
pair = self.embed_atompair_ref_pos(displacement) * valid_pair
|
| 400 |
-
pair =
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
| 403 |
|
| 404 |
if self.structure_prediction:
|
| 405 |
if s_trunk is None or z is None:
|
| 406 |
raise ValueError("structure prediction requires S and Z trunk tensors")
|
| 407 |
-
atom_to_token = feats["atom_to_token"].float()
|
| 408 |
-
single_update = self.s_to_c_trans(s_trunk.float())
|
| 409 |
-
single_update = torch.bmm(
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
pair_update = torch.einsum(
|
| 416 |
"bijd,bwki,bwlj->bwkld",
|
| 417 |
pair_update,
|
| 418 |
token_queries,
|
| 419 |
token_keys,
|
| 420 |
-
)
|
| 421 |
-
pair = pair + pair_update.to(pair)
|
| 422 |
|
| 423 |
pair = pair + self.c_to_p_trans_q(
|
| 424 |
conditioned_single.view(b, k, w, 1, conditioned_single.shape[-1])
|
| 425 |
-
)
|
| 426 |
pair = pair + self.c_to_p_trans_k(
|
| 427 |
to_keys(conditioned_single).view(
|
| 428 |
b,
|
|
@@ -431,9 +479,14 @@ class AtomEncoder(nn.Module):
|
|
| 431 |
h,
|
| 432 |
conditioned_single.shape[-1],
|
| 433 |
)
|
| 434 |
-
)
|
| 435 |
-
pair = pair + self.p_mlp(pair)
|
| 436 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
|
| 439 |
class AtomAttentionEncoder(nn.Module):
|
|
@@ -484,14 +537,15 @@ class AtomAttentionEncoder(nn.Module):
|
|
| 484 |
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, Callable]:
|
| 485 |
b, n, _ = feats["ref_pos"].shape
|
| 486 |
del b, n
|
| 487 |
-
|
|
|
|
| 488 |
if self.structure_prediction:
|
| 489 |
if r is None:
|
| 490 |
raise ValueError("structure prediction requires atom coordinates R")
|
| 491 |
-
q = q.repeat_interleave(multiplicity, 0)
|
| 492 |
-
q = q + self.r_to_q_trans(r)
|
| 493 |
-
c = c.repeat_interleave(multiplicity, 0)
|
| 494 |
-
atom_mask = atom_mask.repeat_interleave(multiplicity, 0)
|
| 495 |
q = self.atom_encoder(
|
| 496 |
q=q,
|
| 497 |
mask=atom_mask,
|
|
@@ -499,15 +553,26 @@ class AtomAttentionEncoder(nn.Module):
|
|
| 499 |
bias=atom_enc_bias,
|
| 500 |
multiplicity=multiplicity,
|
| 501 |
to_keys=to_keys,
|
| 502 |
-
)
|
| 503 |
|
| 504 |
with torch.autocast("cuda", enabled=False):
|
| 505 |
-
atom_update = self.atom_to_token_trans(q).float()
|
| 506 |
-
atom_to_token = feats["atom_to_token"].float()
|
| 507 |
-
atom_to_token = atom_to_token.repeat_interleave(
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
|
| 512 |
|
| 513 |
class AtomAttentionDecoder(nn.Module):
|
|
@@ -560,12 +625,17 @@ class AtomAttentionDecoder(nn.Module):
|
|
| 560 |
"""Return atom-coordinate updates ``R_update: (b, a, 3)``."""
|
| 561 |
|
| 562 |
with torch.autocast("cuda", enabled=False):
|
| 563 |
-
|
| 564 |
-
atom_to_token = atom_to_token.
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
q = self.atom_decoder(
|
| 570 |
q=q,
|
| 571 |
mask=atom_mask,
|
|
@@ -573,5 +643,5 @@ class AtomAttentionDecoder(nn.Module):
|
|
| 573 |
bias=atom_dec_bias,
|
| 574 |
multiplicity=multiplicity,
|
| 575 |
to_keys=to_keys,
|
| 576 |
-
)
|
| 577 |
-
return self.atom_feat_to_atom_pos_update(q)
|
|
|
|
| 11 |
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
+
import torch
|
| 15 |
from collections.abc import Callable
|
| 16 |
from functools import partial
|
| 17 |
from math import pi
|
|
|
|
|
|
|
| 18 |
from torch import nn
|
| 19 |
from torch.nn.functional import one_hot
|
| 20 |
|
|
|
|
| 45 |
def forward(self, times: torch.Tensor) -> torch.Tensor:
|
| 46 |
"""Map times with shape ``(b,)`` to an embedding ``H: (b, d)``."""
|
| 47 |
|
| 48 |
+
random_phase = self.proj(times.reshape(-1, 1)) # (b, d)
|
| 49 |
+
return torch.cos(2 * pi * random_phase) # (b, d)
|
| 50 |
|
| 51 |
|
| 52 |
def _pairwise_difference(values: torch.Tensor) -> torch.Tensor:
|
| 53 |
+
# values: (b, n)
|
| 54 |
+
return values[:, :, None] - values[:, None, :] # (b, n, n)
|
| 55 |
|
| 56 |
|
| 57 |
class RelativePositionEncoder(nn.Module):
|
|
|
|
| 76 |
def forward(self, feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 77 |
"""Return relative pair features ``Z: (b, n, n, token_z)``."""
|
| 78 |
|
| 79 |
+
same_chain = (
|
| 80 |
+
feats["asym_id"][:, :, None] == feats["asym_id"][:, None, :]
|
| 81 |
+
) # (b, n, n)
|
| 82 |
+
same_residue = (
|
| 83 |
+
feats["residue_index"][:, :, None] == feats["residue_index"][:, None, :]
|
| 84 |
+
) # (b, n, n)
|
| 85 |
+
same_entity = (
|
| 86 |
+
feats["entity_id"][:, :, None] == feats["entity_id"][:, None, :]
|
| 87 |
+
) # (b, n, n)
|
| 88 |
+
|
| 89 |
+
residue_offset = _pairwise_difference(feats["residue_index"]) # (b, n, n)
|
| 90 |
if self.cyclic_pos_enc and torch.any(feats["cyclic_period"] > 0):
|
| 91 |
period = torch.where(
|
| 92 |
feats["cyclic_period"] > 0,
|
| 93 |
feats["cyclic_period"],
|
| 94 |
torch.zeros_like(feats["cyclic_period"]) + 10000,
|
| 95 |
+
) # (b, n)
|
| 96 |
+
residue_offset = (
|
| 97 |
+
residue_offset - period * torch.round(residue_offset / period)
|
| 98 |
+
).long() # (b, n, n)
|
| 99 |
residue_offset = torch.clip(
|
| 100 |
residue_offset + self.r_max,
|
| 101 |
0,
|
| 102 |
2 * self.r_max,
|
| 103 |
+
) # (b, n, n)
|
| 104 |
residue_offset = torch.where(
|
| 105 |
same_chain,
|
| 106 |
residue_offset,
|
| 107 |
torch.zeros_like(residue_offset) + 2 * self.r_max + 1,
|
| 108 |
+
) # (b, n, n)
|
| 109 |
+
residue_features = one_hot(
|
| 110 |
+
residue_offset, 2 * self.r_max + 2
|
| 111 |
+
) # (b, n, n, 2 * r_max + 2)
|
| 112 |
|
| 113 |
token_offset = torch.clip(
|
| 114 |
_pairwise_difference(feats["token_index"]) + self.r_max,
|
| 115 |
0,
|
| 116 |
2 * self.r_max,
|
| 117 |
+
) # (b, n, n)
|
| 118 |
token_offset = torch.where(
|
| 119 |
same_chain & same_residue,
|
| 120 |
token_offset,
|
| 121 |
torch.zeros_like(token_offset) + 2 * self.r_max + 1,
|
| 122 |
+
) # (b, n, n)
|
| 123 |
+
token_features = one_hot(
|
| 124 |
+
token_offset, 2 * self.r_max + 2
|
| 125 |
+
) # (b, n, n, 2 * r_max + 2)
|
| 126 |
|
| 127 |
symmetry_offset = torch.clip(
|
| 128 |
_pairwise_difference(feats["sym_id"]) + self.s_max,
|
| 129 |
0,
|
| 130 |
2 * self.s_max,
|
| 131 |
+
) # (b, n, n)
|
| 132 |
+
invalid_symmetry = ~same_entity if self.fix_sym_check else same_chain # (b, n, n)
|
| 133 |
symmetry_offset = torch.where(
|
| 134 |
invalid_symmetry,
|
| 135 |
torch.zeros_like(symmetry_offset) + 2 * self.s_max + 1,
|
| 136 |
symmetry_offset,
|
| 137 |
+
) # (b, n, n)
|
| 138 |
+
symmetry_features = one_hot(
|
| 139 |
+
symmetry_offset, 2 * self.s_max + 2
|
| 140 |
+
) # (b, n, n, 2 * s_max + 2)
|
| 141 |
|
| 142 |
pair_features = torch.cat(
|
| 143 |
(
|
|
|
|
| 147 |
symmetry_features.float(),
|
| 148 |
),
|
| 149 |
dim=-1,
|
| 150 |
+
) # (b, n, n, d_rel)
|
| 151 |
+
return self.linear_layer(pair_features) # (b, n, n, token_z)
|
| 152 |
|
| 153 |
|
| 154 |
class SingleConditioning(nn.Module):
|
|
|
|
| 189 |
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
| 190 |
"""Return conditioned token features ``S: (b, n, 2 * token_s)``."""
|
| 191 |
|
| 192 |
+
# times: (b,); s_trunk/s_inputs: (b, n, token_s).
|
| 193 |
+
single = torch.cat((s_trunk, s_inputs), dim=-1) # (b, n, 2 * token_s)
|
| 194 |
+
single = self.single_embed(self.norm_single(single)) # (b, n, 2 * token_s)
|
| 195 |
+
normalized_fourier = None # (b, d_fourier) or None
|
| 196 |
if not self.disable_times:
|
| 197 |
+
fourier = self.fourier_embed(times) # (b, d_fourier)
|
| 198 |
+
normalized_fourier = self.norm_fourier(fourier) # (b, d_fourier)
|
| 199 |
+
time_condition = self.fourier_to_single(
|
| 200 |
+
normalized_fourier
|
| 201 |
+
) # (b, 2 * token_s)
|
| 202 |
+
single = time_condition[:, None, :] + single # (b, n, 2 * token_s)
|
| 203 |
for transition in self.transitions:
|
| 204 |
+
single = transition(single) + single # (b, n, 2 * token_s)
|
| 205 |
+
return single, normalized_fourier # (b, n, 2 * token_s), (b, d_fourier) or None
|
| 206 |
|
| 207 |
|
| 208 |
class PairwiseConditioning(nn.Module):
|
|
|
|
| 234 |
) -> torch.Tensor:
|
| 235 |
"""Return conditioned pair features ``Z: (b, n, n, token_z)``."""
|
| 236 |
|
| 237 |
+
pair = self.dim_pairwise_init_proj(
|
| 238 |
+
torch.cat((z_trunk, token_rel_pos_feats), dim=-1)
|
| 239 |
+
) # (b, n, n, token_z)
|
| 240 |
for transition in self.transitions:
|
| 241 |
+
pair = transition(pair) + pair # (b, n, n, token_z)
|
| 242 |
+
return pair # (b, n, n, token_z)
|
| 243 |
|
| 244 |
|
| 245 |
def get_indexing_matrix(
|
|
|
|
| 266 |
f"h must contain an even number of half-window key blocks; received {key_blocks}."
|
| 267 |
)
|
| 268 |
|
| 269 |
+
positions = torch.arange(2 * k, device=device) # (2 * k,)
|
| 270 |
+
relative_blocks = (
|
| 271 |
+
(positions.unsqueeze(0) - positions.unsqueeze(1)) + key_blocks // 2
|
| 272 |
+
).clamp(min=0, max=key_blocks + 1) # (2 * k, 2 * k)
|
| 273 |
+
relative_blocks = relative_blocks.view(k, 2, 2 * k)[:, 0, :] # (k, 2 * k)
|
| 274 |
+
selectors = one_hot(relative_blocks, num_classes=key_blocks + 2)[
|
| 275 |
+
..., 1:-1
|
| 276 |
+
].transpose(1, 0) # (2 * k, k, key_blocks)
|
| 277 |
+
return selectors.reshape(
|
| 278 |
+
2 * k, key_blocks * k
|
| 279 |
+
).float() # (2 * k, key_blocks * k)
|
| 280 |
|
| 281 |
|
| 282 |
def single_to_keys(
|
|
|
|
| 289 |
|
| 290 |
b, n, d = single.shape
|
| 291 |
k = n // w
|
| 292 |
+
half_windows = single.view(b, 2 * k, w // 2, d) # (b, 2 * k, w / 2, d)
|
| 293 |
+
gathered = torch.einsum(
|
| 294 |
+
"b j i d, j k -> b k i d", half_windows, indexing_matrix
|
| 295 |
+
) # (b, key_blocks * k, w / 2, d)
|
| 296 |
+
return gathered.reshape(b, k, h, d) # (b, k, h, d)
|
| 297 |
|
| 298 |
|
| 299 |
class AtomEncoder(nn.Module):
|
|
|
|
| 357 |
b: int,
|
| 358 |
n: int,
|
| 359 |
) -> torch.Tensor:
|
| 360 |
+
# Feature tensors share leading shape (b, n).
|
| 361 |
feature_parts = [
|
| 362 |
feats["ref_pos"],
|
| 363 |
feats["ref_charge"].unsqueeze(-1),
|
| 364 |
feats["ref_element"],
|
| 365 |
+
] # each: (b, n, d_feature)
|
| 366 |
if not self.use_no_atom_char:
|
| 367 |
feature_parts.append(feats["ref_atom_name_chars"].reshape(b, n, 4 * 64))
|
| 368 |
if self.use_atom_backbone_feat:
|
|
|
|
| 375 |
one_hot(feats["mol_type"], num_classes=4).float(),
|
| 376 |
),
|
| 377 |
dim=-1,
|
| 378 |
+
) # (b, t, d_residue)
|
| 379 |
+
feature_parts.append(
|
| 380 |
+
torch.bmm(feats["atom_to_token"].float(), residue_features)
|
| 381 |
+
) # (b, n, d_residue)
|
| 382 |
+
return torch.cat(feature_parts, dim=-1) # (b, n, atom_feature_dim)
|
| 383 |
|
| 384 |
def forward(
|
| 385 |
self,
|
|
|
|
| 396 |
|
| 397 |
with torch.autocast("cuda", enabled=False):
|
| 398 |
b, n, _ = feats["ref_pos"].shape
|
| 399 |
+
atom_mask = feats["atom_pad_mask"].bool() # (b, n)
|
| 400 |
+
atom_positions = feats["ref_pos"] # (b, n, 3)
|
| 401 |
+
atom_space = feats["ref_space_uid"] # (b, n)
|
| 402 |
+
atom_features = self._assemble_atom_features(
|
| 403 |
+
feats, b, n
|
| 404 |
+
) # (b, n, atom_feature_dim)
|
| 405 |
+
conditioned_single = self.embed_atom_features(
|
| 406 |
+
atom_features
|
| 407 |
+
) # (b, n, atom_s)
|
| 408 |
|
| 409 |
w = self.atoms_per_window_queries
|
| 410 |
h = self.atoms_per_window_keys
|
| 411 |
b, n = conditioned_single.shape[:2]
|
| 412 |
k = n // w
|
| 413 |
+
indexing = get_indexing_matrix(
|
| 414 |
+
k, w, h, conditioned_single.device
|
| 415 |
+
) # (2 * k, key_blocks * k)
|
| 416 |
to_keys = partial(single_to_keys, indexing_matrix=indexing, w=w, h=h)
|
| 417 |
|
| 418 |
+
position_queries = atom_positions.view(b, k, w, 1, 3) # (b, k, w, 1, 3)
|
| 419 |
+
position_keys = to_keys(atom_positions).view(
|
| 420 |
+
b, k, 1, h, 3
|
| 421 |
+
) # (b, k, 1, h, 3)
|
| 422 |
+
displacement = position_keys - position_queries # (b, k, w, h, 3)
|
| 423 |
inverse_squared_distance = 1 / (
|
| 424 |
1 + torch.sum(displacement * displacement, dim=-1, keepdim=True)
|
| 425 |
+
) # (b, k, w, h, 1)
|
| 426 |
+
|
| 427 |
+
mask_queries = atom_mask.view(b, k, w, 1) # (b, k, w, 1)
|
| 428 |
+
mask_keys = (
|
| 429 |
+
to_keys(atom_mask.unsqueeze(-1).float()).view(b, k, 1, h).bool()
|
| 430 |
+
) # (b, k, 1, h)
|
| 431 |
+
space_queries = atom_space.view(b, k, w, 1) # (b, k, w, 1)
|
| 432 |
+
space_keys = (
|
| 433 |
+
to_keys(atom_space.unsqueeze(-1).float()).view(b, k, 1, h).long()
|
| 434 |
+
) # (b, k, 1, h)
|
| 435 |
valid_pair = (
|
| 436 |
(mask_queries & mask_keys & (space_queries == space_keys)).float().unsqueeze(-1)
|
| 437 |
+
) # (b, k, w, h, 1)
|
| 438 |
|
| 439 |
+
pair = self.embed_atompair_ref_pos(displacement) * valid_pair # (b, k, w, h, atom_z)
|
| 440 |
+
pair = (
|
| 441 |
+
pair + self.embed_atompair_ref_dist(inverse_squared_distance) * valid_pair
|
| 442 |
+
) # (b, k, w, h, atom_z)
|
| 443 |
+
pair = pair + self.embed_atompair_mask(valid_pair) * valid_pair # (b, k, w, h, atom_z)
|
| 444 |
+
query = conditioned_single # (b, n, atom_s)
|
| 445 |
|
| 446 |
if self.structure_prediction:
|
| 447 |
if s_trunk is None or z is None:
|
| 448 |
raise ValueError("structure prediction requires S and Z trunk tensors")
|
| 449 |
+
atom_to_token = feats["atom_to_token"].float() # (b, n, t)
|
| 450 |
+
single_update = self.s_to_c_trans(s_trunk.float()) # (b, t, atom_s)
|
| 451 |
+
single_update = torch.bmm(
|
| 452 |
+
atom_to_token, single_update
|
| 453 |
+
) # (b, n, atom_s)
|
| 454 |
+
conditioned_single = conditioned_single + single_update.to(
|
| 455 |
+
conditioned_single
|
| 456 |
+
) # (b, n, atom_s)
|
| 457 |
+
|
| 458 |
+
token_queries = atom_to_token.view(
|
| 459 |
+
b, k, w, atom_to_token.shape[-1]
|
| 460 |
+
) # (b, k, w, t)
|
| 461 |
+
token_keys = to_keys(atom_to_token) # (b, k, h, t)
|
| 462 |
+
pair_update = self.z_to_p_trans(z.float()) # (b, t, t, atom_z)
|
| 463 |
pair_update = torch.einsum(
|
| 464 |
"bijd,bwki,bwlj->bwkld",
|
| 465 |
pair_update,
|
| 466 |
token_queries,
|
| 467 |
token_keys,
|
| 468 |
+
) # (b, k, w, h, atom_z)
|
| 469 |
+
pair = pair + pair_update.to(pair) # (b, k, w, h, atom_z)
|
| 470 |
|
| 471 |
pair = pair + self.c_to_p_trans_q(
|
| 472 |
conditioned_single.view(b, k, w, 1, conditioned_single.shape[-1])
|
| 473 |
+
) # (b, k, w, h, atom_z)
|
| 474 |
pair = pair + self.c_to_p_trans_k(
|
| 475 |
to_keys(conditioned_single).view(
|
| 476 |
b,
|
|
|
|
| 479 |
h,
|
| 480 |
conditioned_single.shape[-1],
|
| 481 |
)
|
| 482 |
+
) # (b, k, w, h, atom_z)
|
| 483 |
+
pair = pair + self.p_mlp(pair) # (b, k, w, h, atom_z)
|
| 484 |
+
return (
|
| 485 |
+
query,
|
| 486 |
+
conditioned_single,
|
| 487 |
+
pair,
|
| 488 |
+
to_keys,
|
| 489 |
+
) # (b, n, atom_s), (b, n, atom_s), (b, k, w, h, atom_z), callable
|
| 490 |
|
| 491 |
|
| 492 |
class AtomAttentionEncoder(nn.Module):
|
|
|
|
| 537 |
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, Callable]:
|
| 538 |
b, n, _ = feats["ref_pos"].shape
|
| 539 |
del b, n
|
| 540 |
+
# q/c: (b, a, atom_s); atom_enc_bias uses local-window attention axes.
|
| 541 |
+
atom_mask = feats["atom_pad_mask"].bool() # (b, a)
|
| 542 |
if self.structure_prediction:
|
| 543 |
if r is None:
|
| 544 |
raise ValueError("structure prediction requires atom coordinates R")
|
| 545 |
+
q = q.repeat_interleave(multiplicity, 0) # (b * m, a, atom_s)
|
| 546 |
+
q = q + self.r_to_q_trans(r) # (b * m, a, atom_s)
|
| 547 |
+
c = c.repeat_interleave(multiplicity, 0) # (b * m, a, atom_s)
|
| 548 |
+
atom_mask = atom_mask.repeat_interleave(multiplicity, 0) # (b * m, a)
|
| 549 |
q = self.atom_encoder(
|
| 550 |
q=q,
|
| 551 |
mask=atom_mask,
|
|
|
|
| 553 |
bias=atom_enc_bias,
|
| 554 |
multiplicity=multiplicity,
|
| 555 |
to_keys=to_keys,
|
| 556 |
+
) # (b * m, a, atom_s)
|
| 557 |
|
| 558 |
with torch.autocast("cuda", enabled=False):
|
| 559 |
+
atom_update = self.atom_to_token_trans(q).float() # (b * m, a, d_token)
|
| 560 |
+
atom_to_token = feats["atom_to_token"].float() # (b, a, t)
|
| 561 |
+
atom_to_token = atom_to_token.repeat_interleave(
|
| 562 |
+
multiplicity, 0
|
| 563 |
+
) # (b * m, a, t)
|
| 564 |
+
atom_to_token_mean = atom_to_token / (
|
| 565 |
+
atom_to_token.sum(dim=1, keepdim=True) + 1e-6
|
| 566 |
+
) # (b * m, a, t)
|
| 567 |
+
token_update = torch.bmm(
|
| 568 |
+
atom_to_token_mean.transpose(1, 2), atom_update
|
| 569 |
+
) # (b * m, t, d_token)
|
| 570 |
+
return (
|
| 571 |
+
token_update.to(q),
|
| 572 |
+
q,
|
| 573 |
+
c,
|
| 574 |
+
to_keys,
|
| 575 |
+
) # (b * m, t, d_token), two (b * m, a, atom_s), callable
|
| 576 |
|
| 577 |
|
| 578 |
class AtomAttentionDecoder(nn.Module):
|
|
|
|
| 625 |
"""Return atom-coordinate updates ``R_update: (b, a, 3)``."""
|
| 626 |
|
| 627 |
with torch.autocast("cuda", enabled=False):
|
| 628 |
+
# a: (b * m, t, 2 * token_s); q/c: (b * m, a, atom_s).
|
| 629 |
+
atom_to_token = feats["atom_to_token"].float() # (b, a, t)
|
| 630 |
+
atom_to_token = atom_to_token.repeat_interleave(
|
| 631 |
+
multiplicity, 0
|
| 632 |
+
) # (b * m, a, t)
|
| 633 |
+
token_update = self.a_to_q_trans(a.float()) # (b * m, t, atom_s)
|
| 634 |
+
atom_update = torch.bmm(atom_to_token, token_update) # (b * m, a, atom_s)
|
| 635 |
+
q = q + atom_update.to(q) # (b * m, a, atom_s)
|
| 636 |
+
atom_mask = feats["atom_pad_mask"].repeat_interleave(
|
| 637 |
+
multiplicity, 0
|
| 638 |
+
) # (b * m, a)
|
| 639 |
q = self.atom_decoder(
|
| 640 |
q=q,
|
| 641 |
mask=atom_mask,
|
|
|
|
| 643 |
bias=atom_dec_bias,
|
| 644 |
multiplicity=multiplicity,
|
| 645 |
to_keys=to_keys,
|
| 646 |
+
) # (b * m, a, atom_s)
|
| 647 |
+
return self.atom_feat_to_atom_pos_update(q) # (b * m, a, 3)
|
fastplms/models/boltz/vb_modules_transformersv2.py
CHANGED
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
| 4 |
|
| 5 |
from collections.abc import Callable
|
| 6 |
from typing import Any, cast
|
| 7 |
-
|
| 8 |
from torch import Tensor, nn
|
| 9 |
from torch.utils.checkpoint import checkpoint
|
| 10 |
|
|
@@ -23,10 +22,12 @@ class AdaLN(nn.Module):
|
|
| 23 |
self.s_bias = LinearNoBias(dim_single_cond, dim)
|
| 24 |
|
| 25 |
def forward(self, a: Tensor, s: Tensor) -> Tensor:
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
output = self.s_scale(conditioning).sigmoid() * normalized + self.s_bias(conditioning)
|
| 29 |
-
return cast(Tensor, output)
|
| 30 |
|
| 31 |
|
| 32 |
class ConditionedTransitionBlock(nn.Module):
|
|
@@ -49,13 +50,15 @@ class ConditionedTransitionBlock(nn.Module):
|
|
| 49 |
self.b_to_a = LinearNoBias(inner_dim, dim_single)
|
| 50 |
|
| 51 |
projection = nn.Linear(dim_single_cond, dim_single)
|
| 52 |
-
nn.init.zeros_(projection.weight)
|
| 53 |
-
nn.init.constant_(projection.bias, -2.0)
|
| 54 |
self.output_projection = nn.Sequential(projection, nn.Sigmoid())
|
| 55 |
|
| 56 |
def forward(self, a: Tensor, s: Tensor) -> Tensor:
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
return cast(Tensor, self.output_projection(s) * self.b_to_a(hidden))
|
| 60 |
|
| 61 |
|
|
@@ -89,14 +92,16 @@ class DiffusionTransformer(nn.Module):
|
|
| 89 |
)
|
| 90 |
|
| 91 |
def _split_pair_bias(self, bias: Tensor | None) -> Tensor | None:
|
|
|
|
| 92 |
if not self.pair_bias_attn:
|
| 93 |
return None
|
| 94 |
if bias is None:
|
| 95 |
raise ValueError("pair bias is required when pair_bias_attn=True")
|
| 96 |
-
batch_size, query_length, key_length, width = bias.shape
|
| 97 |
-
depth = len(self.layers)
|
| 98 |
if depth == 0 or width % depth:
|
| 99 |
raise ValueError("pair-bias width must be divisible by transformer depth")
|
|
|
|
| 100 |
return bias.view(batch_size, query_length, key_length, depth, width // depth)
|
| 101 |
|
| 102 |
def forward(
|
|
@@ -110,12 +115,16 @@ class DiffusionTransformer(nn.Module):
|
|
| 110 |
) -> Tensor:
|
| 111 |
"""Transform A and preserve shape ``(b * m, l, d)``."""
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
for index, layer in enumerate(self.layers):
|
|
|
|
| 116 |
layer_bias = None if layer_biases is None else layer_biases[..., index, :]
|
| 117 |
if self.activation_checkpointing:
|
| 118 |
-
output = checkpoint(
|
| 119 |
layer,
|
| 120 |
output,
|
| 121 |
s,
|
|
@@ -126,7 +135,7 @@ class DiffusionTransformer(nn.Module):
|
|
| 126 |
use_reentrant=False,
|
| 127 |
)
|
| 128 |
else:
|
| 129 |
-
output = layer(
|
| 130 |
output,
|
| 131 |
s,
|
| 132 |
layer_bias,
|
|
@@ -134,7 +143,7 @@ class DiffusionTransformer(nn.Module):
|
|
| 134 |
to_keys,
|
| 135 |
multiplicity,
|
| 136 |
)
|
| 137 |
-
return output
|
| 138 |
|
| 139 |
|
| 140 |
class DiffusionTransformerLayer(nn.Module):
|
|
@@ -156,8 +165,8 @@ class DiffusionTransformerLayer(nn.Module):
|
|
| 156 |
compute_pair_bias=False,
|
| 157 |
)
|
| 158 |
self.output_projection_linear = nn.Linear(conditioning_dim, dim)
|
| 159 |
-
nn.init.zeros_(self.output_projection_linear.weight)
|
| 160 |
-
nn.init.constant_(self.output_projection_linear.bias, -2.0)
|
| 161 |
self.output_projection = nn.Sequential(
|
| 162 |
self.output_projection_linear,
|
| 163 |
nn.Sigmoid(),
|
|
@@ -176,25 +185,28 @@ class DiffusionTransformerLayer(nn.Module):
|
|
| 176 |
) -> Tensor:
|
| 177 |
"""Update activation tensor A with conditioning S and pair bias."""
|
| 178 |
|
|
|
|
|
|
|
|
|
|
| 179 |
if bias is None or mask is None:
|
| 180 |
raise ValueError("diffusion attention requires pair bias and a key mask")
|
| 181 |
-
normalized = self.adaln(a, s)
|
| 182 |
-
key_states = normalized
|
| 183 |
-
key_mask = mask
|
| 184 |
if to_keys is not None:
|
| 185 |
-
key_states = to_keys(normalized)
|
| 186 |
-
key_mask = to_keys(mask.unsqueeze(-1)).squeeze(-1)
|
| 187 |
|
| 188 |
-
attended = self.pair_bias_attn(
|
| 189 |
s=normalized,
|
| 190 |
z=bias,
|
| 191 |
mask=key_mask,
|
| 192 |
multiplicity=multiplicity,
|
| 193 |
k_in=key_states,
|
| 194 |
)
|
| 195 |
-
output = a + self.output_projection(s) * attended
|
| 196 |
-
output = output + self.transition(output, s)
|
| 197 |
-
return cast(Tensor, self.post_lnorm(output))
|
| 198 |
|
| 199 |
|
| 200 |
class AtomTransformer(nn.Module):
|
|
@@ -224,15 +236,21 @@ class AtomTransformer(nn.Module):
|
|
| 224 |
) -> Tensor:
|
| 225 |
"""Transform atom tensor Q with shape ``(b, n_atoms, d)``."""
|
| 226 |
|
| 227 |
-
query_window = self.attn_window_queries
|
| 228 |
-
key_window = self.attn_window_keys
|
| 229 |
-
batch_size, atom_count, width = q.shape
|
| 230 |
-
window_count = atom_count // query_window
|
|
|
|
|
|
|
|
|
|
| 231 |
query_states = q.view(batch_size * window_count, query_window, -1)
|
|
|
|
| 232 |
conditioning = c.view(batch_size * window_count, query_window, -1)
|
|
|
|
| 233 |
query_mask = mask.view(batch_size * window_count, query_window)
|
|
|
|
| 234 |
pair_bias = bias.repeat_interleave(multiplicity, dim=0)
|
| 235 |
-
pair_bias = pair_bias.view(
|
| 236 |
pair_bias.shape[0] * window_count,
|
| 237 |
query_window,
|
| 238 |
key_window,
|
|
@@ -240,10 +258,13 @@ class AtomTransformer(nn.Module):
|
|
| 240 |
)
|
| 241 |
|
| 242 |
def windowed_keys(states: Tensor) -> Tensor:
|
|
|
|
|
|
|
| 243 |
merged = states.view(batch_size, window_count * query_window, -1)
|
|
|
|
| 244 |
return to_keys(merged).view(batch_size * window_count, key_window, -1)
|
| 245 |
|
| 246 |
-
output = self.diffusion_transformer(
|
| 247 |
a=query_states,
|
| 248 |
s=conditioning,
|
| 249 |
bias=pair_bias,
|
|
@@ -251,4 +272,5 @@ class AtomTransformer(nn.Module):
|
|
| 251 |
multiplicity=1,
|
| 252 |
to_keys=windowed_keys,
|
| 253 |
)
|
|
|
|
| 254 |
return cast(Tensor, output.view(batch_size, window_count * query_window, width))
|
|
|
|
| 4 |
|
| 5 |
from collections.abc import Callable
|
| 6 |
from typing import Any, cast
|
|
|
|
| 7 |
from torch import Tensor, nn
|
| 8 |
from torch.utils.checkpoint import checkpoint
|
| 9 |
|
|
|
|
| 22 |
self.s_bias = LinearNoBias(dim_single_cond, dim)
|
| 23 |
|
| 24 |
def forward(self, a: Tensor, s: Tensor) -> Tensor:
|
| 25 |
+
# a: (..., d); s: (..., d_c).
|
| 26 |
+
normalized = self.a_norm(a) # (..., d)
|
| 27 |
+
conditioning = self.s_norm(s) # (..., d_c)
|
| 28 |
+
# (..., d)
|
| 29 |
output = self.s_scale(conditioning).sigmoid() * normalized + self.s_bias(conditioning)
|
| 30 |
+
return cast(Tensor, output) # (..., d)
|
| 31 |
|
| 32 |
|
| 33 |
class ConditionedTransitionBlock(nn.Module):
|
|
|
|
| 50 |
self.b_to_a = LinearNoBias(inner_dim, dim_single)
|
| 51 |
|
| 52 |
projection = nn.Linear(dim_single_cond, dim_single)
|
| 53 |
+
nn.init.zeros_(projection.weight) # (d, d_c)
|
| 54 |
+
nn.init.constant_(projection.bias, -2.0) # (d,)
|
| 55 |
self.output_projection = nn.Sequential(projection, nn.Sigmoid())
|
| 56 |
|
| 57 |
def forward(self, a: Tensor, s: Tensor) -> Tensor:
|
| 58 |
+
# a: (..., d); s: (..., d_c).
|
| 59 |
+
normalized = self.adaln(a, s) # (..., d)
|
| 60 |
+
hidden = self.swish_gate(normalized) * self.a_to_b(normalized) # (..., d_inner)
|
| 61 |
+
# (..., d)
|
| 62 |
return cast(Tensor, self.output_projection(s) * self.b_to_a(hidden))
|
| 63 |
|
| 64 |
|
|
|
|
| 92 |
)
|
| 93 |
|
| 94 |
def _split_pair_bias(self, bias: Tensor | None) -> Tensor | None:
|
| 95 |
+
# bias: (b_z, l_q, l_k, n_layer * h) or None.
|
| 96 |
if not self.pair_bias_attn:
|
| 97 |
return None
|
| 98 |
if bias is None:
|
| 99 |
raise ValueError("pair bias is required when pair_bias_attn=True")
|
| 100 |
+
batch_size, query_length, key_length, width = bias.shape # b_z, l_q, l_k, n_layer * h
|
| 101 |
+
depth = len(self.layers) # n_layer
|
| 102 |
if depth == 0 or width % depth:
|
| 103 |
raise ValueError("pair-bias width must be divisible by transformer depth")
|
| 104 |
+
# (b_z, l_q, l_k, n_layer, h)
|
| 105 |
return bias.view(batch_size, query_length, key_length, depth, width // depth)
|
| 106 |
|
| 107 |
def forward(
|
|
|
|
| 115 |
) -> Tensor:
|
| 116 |
"""Transform A and preserve shape ``(b * m, l, d)``."""
|
| 117 |
|
| 118 |
+
# a: (b * m, l_q, d); s: (b * m, l_q, d_c).
|
| 119 |
+
# bias: (b, l_q, l_k, n_layer * h) or None.
|
| 120 |
+
# mask: (b * m, l_q) before an optional to_keys mapping; to_keys preserves d.
|
| 121 |
+
layer_biases = self._split_pair_bias(bias) # (b, l_q, l_k, n_layer, h) or None
|
| 122 |
+
output = a # (b * m, l_q, d)
|
| 123 |
for index, layer in enumerate(self.layers):
|
| 124 |
+
# (b, l_q, l_k, h) or None
|
| 125 |
layer_bias = None if layer_biases is None else layer_biases[..., index, :]
|
| 126 |
if self.activation_checkpointing:
|
| 127 |
+
output = checkpoint( # (b * m, l_q, d)
|
| 128 |
layer,
|
| 129 |
output,
|
| 130 |
s,
|
|
|
|
| 135 |
use_reentrant=False,
|
| 136 |
)
|
| 137 |
else:
|
| 138 |
+
output = layer( # (b * m, l_q, d)
|
| 139 |
output,
|
| 140 |
s,
|
| 141 |
layer_bias,
|
|
|
|
| 143 |
to_keys,
|
| 144 |
multiplicity,
|
| 145 |
)
|
| 146 |
+
return output # (b * m, l_q, d)
|
| 147 |
|
| 148 |
|
| 149 |
class DiffusionTransformerLayer(nn.Module):
|
|
|
|
| 165 |
compute_pair_bias=False,
|
| 166 |
)
|
| 167 |
self.output_projection_linear = nn.Linear(conditioning_dim, dim)
|
| 168 |
+
nn.init.zeros_(self.output_projection_linear.weight) # (d, d_c)
|
| 169 |
+
nn.init.constant_(self.output_projection_linear.bias, -2.0) # (d,)
|
| 170 |
self.output_projection = nn.Sequential(
|
| 171 |
self.output_projection_linear,
|
| 172 |
nn.Sigmoid(),
|
|
|
|
| 185 |
) -> Tensor:
|
| 186 |
"""Update activation tensor A with conditioning S and pair bias."""
|
| 187 |
|
| 188 |
+
# a: (b * m, l_q, d); s: (b * m, l_q, d_c).
|
| 189 |
+
# bias: (b, l_q, l_k, h) or None.
|
| 190 |
+
# mask: (b * m, l_q) or None; to_keys maps l_q to l_k.
|
| 191 |
if bias is None or mask is None:
|
| 192 |
raise ValueError("diffusion attention requires pair bias and a key mask")
|
| 193 |
+
normalized = self.adaln(a, s) # (b * m, l_q, d)
|
| 194 |
+
key_states = normalized # (b * m, l_q, d)
|
| 195 |
+
key_mask = mask # (b * m, l_q)
|
| 196 |
if to_keys is not None:
|
| 197 |
+
key_states = to_keys(normalized) # (b * m, l_k, d)
|
| 198 |
+
key_mask = to_keys(mask.unsqueeze(-1)).squeeze(-1) # (b * m, l_k)
|
| 199 |
|
| 200 |
+
attended = self.pair_bias_attn( # (b * m, l_q, d)
|
| 201 |
s=normalized,
|
| 202 |
z=bias,
|
| 203 |
mask=key_mask,
|
| 204 |
multiplicity=multiplicity,
|
| 205 |
k_in=key_states,
|
| 206 |
)
|
| 207 |
+
output = a + self.output_projection(s) * attended # (b * m, l_q, d)
|
| 208 |
+
output = output + self.transition(output, s) # (b * m, l_q, d)
|
| 209 |
+
return cast(Tensor, self.post_lnorm(output)) # (b * m, l_q, d)
|
| 210 |
|
| 211 |
|
| 212 |
class AtomTransformer(nn.Module):
|
|
|
|
| 236 |
) -> Tensor:
|
| 237 |
"""Transform atom tensor Q with shape ``(b, n_atoms, d)``."""
|
| 238 |
|
| 239 |
+
query_window = self.attn_window_queries # w_q
|
| 240 |
+
key_window = self.attn_window_keys # w_k
|
| 241 |
+
batch_size, atom_count, width = q.shape # b * m, n_atom, d
|
| 242 |
+
window_count = atom_count // query_window # k
|
| 243 |
+
# q, c: (b * m, n_atom, d); bias: (b, k, w_q, w_k, n_layer * h).
|
| 244 |
+
# mask: (b * m, n_atom); k = n_atom // w_q.
|
| 245 |
+
# (b * m * k, w_q, d)
|
| 246 |
query_states = q.view(batch_size * window_count, query_window, -1)
|
| 247 |
+
# (b * m * k, w_q, d_c)
|
| 248 |
conditioning = c.view(batch_size * window_count, query_window, -1)
|
| 249 |
+
# (b * m * k, w_q)
|
| 250 |
query_mask = mask.view(batch_size * window_count, query_window)
|
| 251 |
+
# (b * m, k, w_q, w_k, n_layer * h)
|
| 252 |
pair_bias = bias.repeat_interleave(multiplicity, dim=0)
|
| 253 |
+
pair_bias = pair_bias.view( # (b * m * k, w_q, w_k, n_layer * h)
|
| 254 |
pair_bias.shape[0] * window_count,
|
| 255 |
query_window,
|
| 256 |
key_window,
|
|
|
|
| 258 |
)
|
| 259 |
|
| 260 |
def windowed_keys(states: Tensor) -> Tensor:
|
| 261 |
+
# states: (b * m * k, w_q, d_x).
|
| 262 |
+
# (b * m, n_atom, d_x)
|
| 263 |
merged = states.view(batch_size, window_count * query_window, -1)
|
| 264 |
+
# (b * m * k, w_k, d_x)
|
| 265 |
return to_keys(merged).view(batch_size * window_count, key_window, -1)
|
| 266 |
|
| 267 |
+
output = self.diffusion_transformer( # (b * m * k, w_q, d)
|
| 268 |
a=query_states,
|
| 269 |
s=conditioning,
|
| 270 |
bias=pair_bias,
|
|
|
|
| 272 |
multiplicity=1,
|
| 273 |
to_keys=windowed_keys,
|
| 274 |
)
|
| 275 |
+
# (b * m, n_atom, d)
|
| 276 |
return cast(Tensor, output.view(batch_size, window_count * query_window, width))
|
fastplms/models/boltz/vb_modules_trunkv2.py
CHANGED
|
@@ -2,9 +2,8 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
from typing import Any, cast
|
| 6 |
-
|
| 7 |
import torch
|
|
|
|
| 8 |
from torch import Tensor, nn
|
| 9 |
from torch.nn import functional as F
|
| 10 |
from torch.utils.checkpoint import checkpoint
|
|
@@ -30,8 +29,8 @@ class ContactConditioning(nn.Module):
|
|
| 30 |
self.fourier_embedding = FourierEmbedding(token_z)
|
| 31 |
input_width = token_z + len(const.contact_conditioning_info) - 1
|
| 32 |
self.encoder = nn.Linear(input_width, token_z)
|
| 33 |
-
self.encoding_unspecified = nn.Parameter(torch.zeros(token_z))
|
| 34 |
-
self.encoding_unselected = nn.Parameter(torch.zeros(token_z))
|
| 35 |
self.cutoff_min = cutoff_min
|
| 36 |
self.cutoff_max = cutoff_max
|
| 37 |
|
|
@@ -43,22 +42,25 @@ class ContactConditioning(nn.Module):
|
|
| 43 |
if const.contact_conditioning_info["UNSELECTED"] != 1:
|
| 44 |
raise ValueError("UNSELECTED contact conditioning must use channel one")
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
| 48 |
normalized = (threshold - self.cutoff_min) / (self.cutoff_max - self.cutoff_min)
|
|
|
|
| 49 |
fourier = self.fourier_embedding(normalized.flatten()).reshape((*normalized.shape, -1))
|
| 50 |
-
selected_features = torch.cat(
|
| 51 |
[categories[..., 2:], normalized.unsqueeze(-1), fourier],
|
| 52 |
dim=-1,
|
| 53 |
)
|
| 54 |
-
selected = self.encoder(selected_features)
|
| 55 |
-
special = categories[..., :2]
|
| 56 |
return cast(
|
| 57 |
Tensor,
|
| 58 |
selected * (1 - special.sum(dim=-1, keepdim=True))
|
| 59 |
+ self.encoding_unspecified * special[..., 0:1]
|
| 60 |
+ self.encoding_unselected * special[..., 1:2],
|
| 61 |
-
)
|
| 62 |
|
| 63 |
|
| 64 |
class InputEmbedder(nn.Module):
|
|
@@ -122,51 +124,56 @@ class InputEmbedder(nn.Module):
|
|
| 122 |
|
| 123 |
if add_method_conditioning:
|
| 124 |
self.method_conditioning_init = nn.Embedding(const.num_method_types, token_s)
|
| 125 |
-
self.method_conditioning_init.weight.data.fill_(0)
|
| 126 |
if add_modified_flag:
|
| 127 |
self.modified_conditioning_init = nn.Embedding(2, token_s)
|
| 128 |
-
self.modified_conditioning_init.weight.data.fill_(0)
|
| 129 |
if add_cyclic_flag:
|
| 130 |
self.cyclic_conditioning_init = nn.Linear(1, token_s, bias=False)
|
| 131 |
-
self.cyclic_conditioning_init.weight.data.fill_(0)
|
| 132 |
if add_mol_type_feat:
|
| 133 |
self.mol_type_conditioning_init = nn.Embedding(
|
| 134 |
len(const.chain_type_ids),
|
| 135 |
token_s,
|
| 136 |
)
|
| 137 |
-
self.mol_type_conditioning_init.weight.data.fill_(0)
|
| 138 |
|
| 139 |
def forward(self, feats: dict[str, Tensor], affinity: bool = False) -> Tensor:
|
| 140 |
"""Return embedded sequence tensor S with shape ``(b, l, d_s)``."""
|
| 141 |
|
| 142 |
-
|
|
|
|
| 143 |
suffix = "_affinity" if affinity else ""
|
| 144 |
-
profile = feats[f"profile{suffix}"]
|
| 145 |
-
deletion_mean = feats[f"deletion_mean{suffix}"].unsqueeze(-1)
|
|
|
|
| 146 |
atom_queries, atom_conditioning, atom_pairs, to_keys = self.atom_encoder(feats)
|
| 147 |
-
atom_bias = self.atom_enc_proj_z(atom_pairs)
|
| 148 |
atom_output, _, _, _ = self.atom_attention_encoder(
|
| 149 |
feats=feats,
|
| 150 |
q=atom_queries,
|
| 151 |
c=atom_conditioning,
|
| 152 |
atom_enc_bias=atom_bias,
|
| 153 |
to_keys=to_keys,
|
| 154 |
-
)
|
| 155 |
-
output = (
|
| 156 |
atom_output
|
| 157 |
+ self.res_type_encoding(residue_type)
|
| 158 |
+ self.msa_profile_encoding(torch.cat([profile, deletion_mean], dim=-1))
|
| 159 |
)
|
| 160 |
if self.add_method_conditioning:
|
|
|
|
| 161 |
output = output + self.method_conditioning_init(feats["method_feature"])
|
| 162 |
if self.add_modified_flag:
|
|
|
|
| 163 |
output = output + self.modified_conditioning_init(feats["modified"])
|
| 164 |
if self.add_cyclic_flag:
|
| 165 |
-
cyclic = feats["cyclic_period"].clamp(max=1.0).unsqueeze(-1)
|
| 166 |
-
output = output + self.cyclic_conditioning_init(cyclic)
|
| 167 |
if self.add_mol_type_feat:
|
|
|
|
| 168 |
output = output + self.mol_type_conditioning_init(feats["mol_type"])
|
| 169 |
-
return cast(Tensor, output)
|
| 170 |
|
| 171 |
|
| 172 |
class _TemplateBase(nn.Module):
|
|
@@ -213,44 +220,54 @@ class _TemplateBase(nn.Module):
|
|
| 213 |
feats: dict[str, Tensor],
|
| 214 |
template_pair_mask: Tensor,
|
| 215 |
) -> Tensor:
|
| 216 |
-
residue_type
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
| 219 |
cb_pair_mask = (cb_mask[..., :, None] * cb_mask[..., None, :]).unsqueeze(-1)
|
|
|
|
| 220 |
frame_pair_mask = (frame_mask[..., :, None] * frame_mask[..., None, :]).unsqueeze(-1)
|
| 221 |
with torch.autocast(device_type="cuda", enabled=False):
|
|
|
|
|
|
|
| 222 |
cb_distances = torch.cdist(feats["template_cb"], feats["template_cb"])
|
| 223 |
-
boundaries = torch.linspace(
|
| 224 |
self.min_dist,
|
| 225 |
self.max_dist,
|
| 226 |
self.num_bins - 1,
|
| 227 |
).to(cb_distances.device)
|
| 228 |
-
bins = (cb_distances[..., None] > boundaries).sum(dim=-1).long()
|
| 229 |
-
distogram = F.one_hot(bins, num_classes=self.num_bins)
|
| 230 |
|
|
|
|
| 231 |
rotations = feats["template_frame_rot"].unsqueeze(2).transpose(-1, -2)
|
|
|
|
| 232 |
translations = feats["template_frame_t"].unsqueeze(2).unsqueeze(-1)
|
|
|
|
| 233 |
ca_coordinates = feats["template_ca"].unsqueeze(3).unsqueeze(-1)
|
|
|
|
| 234 |
vectors = torch.matmul(rotations, ca_coordinates - translations)
|
| 235 |
-
norms = torch.norm(vectors, dim=-1, keepdim=True)
|
| 236 |
-
unit_vectors = torch.where(
|
| 237 |
norms > 0,
|
| 238 |
vectors / norms,
|
| 239 |
torch.zeros_like(vectors),
|
| 240 |
).squeeze(-1)
|
| 241 |
-
pair_features = torch.cat(
|
| 242 |
[distogram, cb_pair_mask, unit_vectors, frame_pair_mask],
|
| 243 |
dim=-1,
|
| 244 |
)
|
|
|
|
| 245 |
pair_features = pair_features * template_pair_mask.unsqueeze(-1)
|
| 246 |
-
residue_i = residue_type[:, :, :, None].expand(
|
| 247 |
-1,
|
| 248 |
-1,
|
| 249 |
-1,
|
| 250 |
residue_type.size(2),
|
| 251 |
-1,
|
| 252 |
)
|
| 253 |
-
residue_j = residue_type[:, :, None, :].expand(
|
| 254 |
-1,
|
| 255 |
-1,
|
| 256 |
residue_type.size(2),
|
|
@@ -260,7 +277,7 @@ class _TemplateBase(nn.Module):
|
|
| 260 |
return cast(
|
| 261 |
Tensor,
|
| 262 |
self.a_proj(torch.cat([pair_features, residue_i, residue_j], dim=-1)),
|
| 263 |
-
)
|
| 264 |
|
| 265 |
def forward(
|
| 266 |
self,
|
|
@@ -271,38 +288,40 @@ class _TemplateBase(nn.Module):
|
|
| 271 |
) -> Tensor:
|
| 272 |
"""Aggregate template pair tensor V into trunk update U."""
|
| 273 |
|
| 274 |
-
|
|
|
|
| 275 |
batch_size, template_count = residue_type.shape[:2]
|
| 276 |
-
template_present = feats["template_mask"].any(dim=2).float()
|
| 277 |
-
present_count = template_present.sum(dim=1).clamp(min=1)
|
| 278 |
-
features = self._template_features(
|
| 279 |
feats,
|
| 280 |
self._template_pair_mask(feats, template_count),
|
| 281 |
)
|
| 282 |
-
expanded_mask = pair_mask[:, None].expand(-1, template_count, -1, -1)
|
| 283 |
-
expanded_mask = expanded_mask.reshape(
|
| 284 |
batch_size * template_count,
|
| 285 |
*expanded_mask.shape[2:],
|
| 286 |
)
|
| 287 |
-
template_states = self.z_proj(self.z_norm(z[:, None])) + features
|
| 288 |
-
template_states = template_states.view(
|
| 289 |
batch_size * template_count,
|
| 290 |
*template_states.shape[2:],
|
| 291 |
)
|
| 292 |
-
template_states = template_states + self.pairformer(
|
| 293 |
template_states,
|
| 294 |
expanded_mask,
|
| 295 |
use_kernels=use_kernels,
|
| 296 |
)
|
| 297 |
-
template_states = self.v_norm(template_states).view(
|
| 298 |
batch_size,
|
| 299 |
template_count,
|
| 300 |
*template_states.shape[1:],
|
| 301 |
)
|
| 302 |
-
weights = template_present[:, :, None, None, None]
|
| 303 |
-
aggregate = (template_states * weights).sum(dim=1)
|
|
|
|
| 304 |
aggregate = aggregate / present_count[:, None, None, None].to(template_states)
|
| 305 |
-
return cast(Tensor, self.u_proj(self.relu(aggregate)))
|
| 306 |
|
| 307 |
|
| 308 |
class TemplateModule(_TemplateBase):
|
|
@@ -339,9 +358,9 @@ class TemplateModule(_TemplateBase):
|
|
| 339 |
)
|
| 340 |
|
| 341 |
def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
|
| 342 |
-
asym_id = feats["asym_id"]
|
| 343 |
-
same_chain = (asym_id[:, :, None] == asym_id[:, None, :]).float()
|
| 344 |
-
return same_chain[:, None].expand(-1, count, -1, -1)
|
| 345 |
|
| 346 |
|
| 347 |
class TemplateV2Module(_TemplateBase):
|
|
@@ -379,8 +398,8 @@ class TemplateV2Module(_TemplateBase):
|
|
| 379 |
|
| 380 |
def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
|
| 381 |
del count
|
| 382 |
-
visibility = feats["visibility_ids"]
|
| 383 |
-
return (visibility[..., :, None] == visibility[..., None, :]).float()
|
| 384 |
|
| 385 |
|
| 386 |
class MSAModule(nn.Module):
|
|
@@ -433,6 +452,7 @@ class MSAModule(nn.Module):
|
|
| 433 |
pair_states: Tensor,
|
| 434 |
training: bool,
|
| 435 |
) -> tuple[bool, int | None, int | None, int | None, int | None]:
|
|
|
|
| 436 |
if training:
|
| 437 |
return False, None, None, None, None
|
| 438 |
if pair_states.shape[1] > const.chunk_size_threshold:
|
|
@@ -449,28 +469,32 @@ class MSAModule(nn.Module):
|
|
| 449 |
"""Return updated pair tensor Z after every MSA block."""
|
| 450 |
|
| 451 |
chunking = self._chunk_configuration(z, self.training)
|
| 452 |
-
|
|
|
|
|
|
|
| 453 |
if msa.dtype in (torch.long, torch.int32, torch.int64):
|
| 454 |
-
msa = F.one_hot(msa, num_classes=const.num_tokens).float()
|
| 455 |
-
msa_mask = feats["msa_mask"]
|
| 456 |
-
components = [
|
| 457 |
msa,
|
| 458 |
-
feats["has_deletion"].unsqueeze(-1),
|
| 459 |
-
feats["deletion_value"].unsqueeze(-1),
|
| 460 |
]
|
| 461 |
if self.use_paired_feature:
|
| 462 |
-
components.append(feats["msa_paired"].unsqueeze(-1))
|
| 463 |
-
msa_input = torch.cat(components, dim=-1)
|
| 464 |
if self.subsample_msa:
|
| 465 |
-
indices = torch.randperm(msa.shape[1])[: self.num_subsampled_msa]
|
| 466 |
-
msa_input = msa_input[:, indices]
|
| 467 |
-
msa_mask = msa_mask[:, indices]
|
| 468 |
-
|
| 469 |
-
msa_states = self.msa_proj(msa_input) + self.s_proj(emb).unsqueeze(1)
|
| 470 |
-
token_mask = feats["token_pad_mask"].float()
|
| 471 |
-
pair_mask = token_mask[:, :, None] * token_mask[:, None, :]
|
| 472 |
-
pair_states = z
|
| 473 |
for layer in self.layers:
|
|
|
|
|
|
|
| 474 |
arguments = (
|
| 475 |
pair_states,
|
| 476 |
msa_states,
|
|
@@ -480,13 +504,13 @@ class MSAModule(nn.Module):
|
|
| 480 |
use_kernels,
|
| 481 |
)
|
| 482 |
if self.activation_checkpointing and self.training:
|
| 483 |
-
pair_states, msa_states = checkpoint(
|
| 484 |
layer,
|
| 485 |
*arguments,
|
| 486 |
)
|
| 487 |
else:
|
| 488 |
-
pair_states, msa_states = layer(*arguments)
|
| 489 |
-
return pair_states
|
| 490 |
|
| 491 |
|
| 492 |
class MSALayer(nn.Module):
|
|
@@ -534,29 +558,31 @@ class MSALayer(nn.Module):
|
|
| 534 |
"""Return updated Z and M tensors."""
|
| 535 |
|
| 536 |
del chunk_size_transition_z
|
| 537 |
-
|
| 538 |
-
|
|
|
|
|
|
|
| 539 |
m,
|
| 540 |
z,
|
| 541 |
token_mask,
|
| 542 |
chunk_heads_pwa,
|
| 543 |
)
|
| 544 |
-
msa_states = msa_states + self.msa_transition(
|
| 545 |
msa_states,
|
| 546 |
chunk_size_transition_msa,
|
| 547 |
)
|
| 548 |
-
pair_states = z + self.outer_product_mean(
|
| 549 |
msa_states,
|
| 550 |
msa_mask,
|
| 551 |
chunk_size_outer_product,
|
| 552 |
)
|
| 553 |
-
pair_states = self.pairformer_layer(
|
| 554 |
pair_states,
|
| 555 |
token_mask,
|
| 556 |
chunk_size_tri_attn,
|
| 557 |
use_kernels=use_kernels,
|
| 558 |
)
|
| 559 |
-
return pair_states, msa_states
|
| 560 |
|
| 561 |
|
| 562 |
class BFactorModule(nn.Module):
|
|
@@ -568,7 +594,8 @@ class BFactorModule(nn.Module):
|
|
| 568 |
self.num_bins = num_bins
|
| 569 |
|
| 570 |
def forward(self, s: Tensor) -> Tensor:
|
| 571 |
-
|
|
|
|
| 572 |
|
| 573 |
|
| 574 |
class DistogramModule(nn.Module):
|
|
@@ -581,8 +608,9 @@ class DistogramModule(nn.Module):
|
|
| 581 |
self.num_bins = num_bins
|
| 582 |
|
| 583 |
def forward(self, z: Tensor) -> Tensor:
|
| 584 |
-
|
| 585 |
-
|
|
|
|
| 586 |
return cast(
|
| 587 |
Tensor,
|
| 588 |
logits.reshape(
|
|
@@ -592,4 +620,4 @@ class DistogramModule(nn.Module):
|
|
| 592 |
self.num_distograms,
|
| 593 |
self.num_bins,
|
| 594 |
),
|
| 595 |
-
)
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
| 5 |
import torch
|
| 6 |
+
from typing import Any, cast
|
| 7 |
from torch import Tensor, nn
|
| 8 |
from torch.nn import functional as F
|
| 9 |
from torch.utils.checkpoint import checkpoint
|
|
|
|
| 29 |
self.fourier_embedding = FourierEmbedding(token_z)
|
| 30 |
input_width = token_z + len(const.contact_conditioning_info) - 1
|
| 31 |
self.encoder = nn.Linear(input_width, token_z)
|
| 32 |
+
self.encoding_unspecified = nn.Parameter(torch.zeros(token_z)) # (d_z,)
|
| 33 |
+
self.encoding_unselected = nn.Parameter(torch.zeros(token_z)) # (d_z,)
|
| 34 |
self.cutoff_min = cutoff_min
|
| 35 |
self.cutoff_max = cutoff_max
|
| 36 |
|
|
|
|
| 42 |
if const.contact_conditioning_info["UNSELECTED"] != 1:
|
| 43 |
raise ValueError("UNSELECTED contact conditioning must use channel one")
|
| 44 |
|
| 45 |
+
# c_contact is the number of contact-conditioning categories.
|
| 46 |
+
categories = feats["contact_conditioning"] # (b, l, l, c_contact)
|
| 47 |
+
threshold = feats["contact_threshold"] # (b, l, l)
|
| 48 |
+
# (b, l, l)
|
| 49 |
normalized = (threshold - self.cutoff_min) / (self.cutoff_max - self.cutoff_min)
|
| 50 |
+
# (b, l, l, d_z)
|
| 51 |
fourier = self.fourier_embedding(normalized.flatten()).reshape((*normalized.shape, -1))
|
| 52 |
+
selected_features = torch.cat( # (b, l, l, c_contact - 1 + d_z)
|
| 53 |
[categories[..., 2:], normalized.unsqueeze(-1), fourier],
|
| 54 |
dim=-1,
|
| 55 |
)
|
| 56 |
+
selected = self.encoder(selected_features) # (b, l, l, d_z)
|
| 57 |
+
special = categories[..., :2] # (b, l, l, 2)
|
| 58 |
return cast(
|
| 59 |
Tensor,
|
| 60 |
selected * (1 - special.sum(dim=-1, keepdim=True))
|
| 61 |
+ self.encoding_unspecified * special[..., 0:1]
|
| 62 |
+ self.encoding_unselected * special[..., 1:2],
|
| 63 |
+
) # (b, l, l, d_z)
|
| 64 |
|
| 65 |
|
| 66 |
class InputEmbedder(nn.Module):
|
|
|
|
| 124 |
|
| 125 |
if add_method_conditioning:
|
| 126 |
self.method_conditioning_init = nn.Embedding(const.num_method_types, token_s)
|
| 127 |
+
self.method_conditioning_init.weight.data.fill_(0) # (n_method, d_s)
|
| 128 |
if add_modified_flag:
|
| 129 |
self.modified_conditioning_init = nn.Embedding(2, token_s)
|
| 130 |
+
self.modified_conditioning_init.weight.data.fill_(0) # (2, d_s)
|
| 131 |
if add_cyclic_flag:
|
| 132 |
self.cyclic_conditioning_init = nn.Linear(1, token_s, bias=False)
|
| 133 |
+
self.cyclic_conditioning_init.weight.data.fill_(0) # (d_s, 1)
|
| 134 |
if add_mol_type_feat:
|
| 135 |
self.mol_type_conditioning_init = nn.Embedding(
|
| 136 |
len(const.chain_type_ids),
|
| 137 |
token_s,
|
| 138 |
)
|
| 139 |
+
self.mol_type_conditioning_init.weight.data.fill_(0) # (n_mol_type, d_s)
|
| 140 |
|
| 141 |
def forward(self, feats: dict[str, Tensor], affinity: bool = False) -> Tensor:
|
| 142 |
"""Return embedded sequence tensor S with shape ``(b, l, d_s)``."""
|
| 143 |
|
| 144 |
+
# n_atom is the padded atom count; k is the number of atom windows.
|
| 145 |
+
residue_type = feats["res_type"].float() # (b, l, n_token_type)
|
| 146 |
suffix = "_affinity" if affinity else ""
|
| 147 |
+
profile = feats[f"profile{suffix}"] # (b, l, n_token_type)
|
| 148 |
+
deletion_mean = feats[f"deletion_mean{suffix}"].unsqueeze(-1) # (b, l, 1)
|
| 149 |
+
# (b, n_atom, d_a), (b, n_atom, d_a), (b, k, w_q, w_k, d_az), callable
|
| 150 |
atom_queries, atom_conditioning, atom_pairs, to_keys = self.atom_encoder(feats)
|
| 151 |
+
atom_bias = self.atom_enc_proj_z(atom_pairs) # (b, k, w_q, w_k, n_layer * h)
|
| 152 |
atom_output, _, _, _ = self.atom_attention_encoder(
|
| 153 |
feats=feats,
|
| 154 |
q=atom_queries,
|
| 155 |
c=atom_conditioning,
|
| 156 |
atom_enc_bias=atom_bias,
|
| 157 |
to_keys=to_keys,
|
| 158 |
+
) # (b, l, d_s), (b, n_atom, d_a), (b, n_atom, d_a), callable
|
| 159 |
+
output = ( # (b, l, d_s)
|
| 160 |
atom_output
|
| 161 |
+ self.res_type_encoding(residue_type)
|
| 162 |
+ self.msa_profile_encoding(torch.cat([profile, deletion_mean], dim=-1))
|
| 163 |
)
|
| 164 |
if self.add_method_conditioning:
|
| 165 |
+
# method_feature: (b, l); output: (b, l, d_s).
|
| 166 |
output = output + self.method_conditioning_init(feats["method_feature"])
|
| 167 |
if self.add_modified_flag:
|
| 168 |
+
# modified: (b, l); output: (b, l, d_s).
|
| 169 |
output = output + self.modified_conditioning_init(feats["modified"])
|
| 170 |
if self.add_cyclic_flag:
|
| 171 |
+
cyclic = feats["cyclic_period"].clamp(max=1.0).unsqueeze(-1) # (b, l, 1)
|
| 172 |
+
output = output + self.cyclic_conditioning_init(cyclic) # (b, l, d_s)
|
| 173 |
if self.add_mol_type_feat:
|
| 174 |
+
# mol_type: (b, l); output: (b, l, d_s).
|
| 175 |
output = output + self.mol_type_conditioning_init(feats["mol_type"])
|
| 176 |
+
return cast(Tensor, output) # (b, l, d_s)
|
| 177 |
|
| 178 |
|
| 179 |
class _TemplateBase(nn.Module):
|
|
|
|
| 220 |
feats: dict[str, Tensor],
|
| 221 |
template_pair_mask: Tensor,
|
| 222 |
) -> Tensor:
|
| 223 |
+
# residue_type: (b, t, l, n_token_type); template_pair_mask: (b, t, l, l).
|
| 224 |
+
residue_type = feats["template_restype"] # (b, t, l, n_token_type)
|
| 225 |
+
cb_mask = feats["template_mask_cb"] # (b, t, l)
|
| 226 |
+
frame_mask = feats["template_mask_frame"] # (b, t, l)
|
| 227 |
+
# (b, t, l, l, 1)
|
| 228 |
cb_pair_mask = (cb_mask[..., :, None] * cb_mask[..., None, :]).unsqueeze(-1)
|
| 229 |
+
# (b, t, l, l, 1)
|
| 230 |
frame_pair_mask = (frame_mask[..., :, None] * frame_mask[..., None, :]).unsqueeze(-1)
|
| 231 |
with torch.autocast(device_type="cuda", enabled=False):
|
| 232 |
+
# template_cb: (b, t, l, 3).
|
| 233 |
+
# (b, t, l, l)
|
| 234 |
cb_distances = torch.cdist(feats["template_cb"], feats["template_cb"])
|
| 235 |
+
boundaries = torch.linspace( # (n_bin - 1,)
|
| 236 |
self.min_dist,
|
| 237 |
self.max_dist,
|
| 238 |
self.num_bins - 1,
|
| 239 |
).to(cb_distances.device)
|
| 240 |
+
bins = (cb_distances[..., None] > boundaries).sum(dim=-1).long() # (b, t, l, l)
|
| 241 |
+
distogram = F.one_hot(bins, num_classes=self.num_bins) # (b, t, l, l, n_bin)
|
| 242 |
|
| 243 |
+
# (b, t, 1, l, 3, 3)
|
| 244 |
rotations = feats["template_frame_rot"].unsqueeze(2).transpose(-1, -2)
|
| 245 |
+
# (b, t, 1, l, 3, 1)
|
| 246 |
translations = feats["template_frame_t"].unsqueeze(2).unsqueeze(-1)
|
| 247 |
+
# (b, t, l, 1, 3, 1)
|
| 248 |
ca_coordinates = feats["template_ca"].unsqueeze(3).unsqueeze(-1)
|
| 249 |
+
# (b, t, l, l, 3, 1)
|
| 250 |
vectors = torch.matmul(rotations, ca_coordinates - translations)
|
| 251 |
+
norms = torch.norm(vectors, dim=-1, keepdim=True) # (b, t, l, l, 3, 1)
|
| 252 |
+
unit_vectors = torch.where( # (b, t, l, l, 3)
|
| 253 |
norms > 0,
|
| 254 |
vectors / norms,
|
| 255 |
torch.zeros_like(vectors),
|
| 256 |
).squeeze(-1)
|
| 257 |
+
pair_features = torch.cat( # (b, t, l, l, n_bin + 5)
|
| 258 |
[distogram, cb_pair_mask, unit_vectors, frame_pair_mask],
|
| 259 |
dim=-1,
|
| 260 |
)
|
| 261 |
+
# (b, t, l, l, n_bin + 5)
|
| 262 |
pair_features = pair_features * template_pair_mask.unsqueeze(-1)
|
| 263 |
+
residue_i = residue_type[:, :, :, None].expand( # (b, t, l, l, n_token_type)
|
| 264 |
-1,
|
| 265 |
-1,
|
| 266 |
-1,
|
| 267 |
residue_type.size(2),
|
| 268 |
-1,
|
| 269 |
)
|
| 270 |
+
residue_j = residue_type[:, :, None, :].expand( # (b, t, l, l, n_token_type)
|
| 271 |
-1,
|
| 272 |
-1,
|
| 273 |
residue_type.size(2),
|
|
|
|
| 277 |
return cast(
|
| 278 |
Tensor,
|
| 279 |
self.a_proj(torch.cat([pair_features, residue_i, residue_j], dim=-1)),
|
| 280 |
+
) # (b, t, l, l, d_t)
|
| 281 |
|
| 282 |
def forward(
|
| 283 |
self,
|
|
|
|
| 288 |
) -> Tensor:
|
| 289 |
"""Aggregate template pair tensor V into trunk update U."""
|
| 290 |
|
| 291 |
+
# z: (b, l, l, d_z); pair_mask: (b, l, l).
|
| 292 |
+
residue_type = feats["template_restype"] # (b, t, l, n_token_type)
|
| 293 |
batch_size, template_count = residue_type.shape[:2]
|
| 294 |
+
template_present = feats["template_mask"].any(dim=2).float() # (b, t)
|
| 295 |
+
present_count = template_present.sum(dim=1).clamp(min=1) # (b,)
|
| 296 |
+
features = self._template_features( # (b, t, l, l, d_t)
|
| 297 |
feats,
|
| 298 |
self._template_pair_mask(feats, template_count),
|
| 299 |
)
|
| 300 |
+
expanded_mask = pair_mask[:, None].expand(-1, template_count, -1, -1) # (b, t, l, l)
|
| 301 |
+
expanded_mask = expanded_mask.reshape( # (b * t, l, l)
|
| 302 |
batch_size * template_count,
|
| 303 |
*expanded_mask.shape[2:],
|
| 304 |
)
|
| 305 |
+
template_states = self.z_proj(self.z_norm(z[:, None])) + features # (b, t, l, l, d_t)
|
| 306 |
+
template_states = template_states.view( # (b * t, l, l, d_t)
|
| 307 |
batch_size * template_count,
|
| 308 |
*template_states.shape[2:],
|
| 309 |
)
|
| 310 |
+
template_states = template_states + self.pairformer( # (b * t, l, l, d_t)
|
| 311 |
template_states,
|
| 312 |
expanded_mask,
|
| 313 |
use_kernels=use_kernels,
|
| 314 |
)
|
| 315 |
+
template_states = self.v_norm(template_states).view( # (b, t, l, l, d_t)
|
| 316 |
batch_size,
|
| 317 |
template_count,
|
| 318 |
*template_states.shape[1:],
|
| 319 |
)
|
| 320 |
+
weights = template_present[:, :, None, None, None] # (b, t, 1, 1, 1)
|
| 321 |
+
aggregate = (template_states * weights).sum(dim=1) # (b, l, l, d_t)
|
| 322 |
+
# (b, l, l, d_t)
|
| 323 |
aggregate = aggregate / present_count[:, None, None, None].to(template_states)
|
| 324 |
+
return cast(Tensor, self.u_proj(self.relu(aggregate))) # (b, l, l, d_z)
|
| 325 |
|
| 326 |
|
| 327 |
class TemplateModule(_TemplateBase):
|
|
|
|
| 358 |
)
|
| 359 |
|
| 360 |
def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
|
| 361 |
+
asym_id = feats["asym_id"] # (b, l)
|
| 362 |
+
same_chain = (asym_id[:, :, None] == asym_id[:, None, :]).float() # (b, l, l)
|
| 363 |
+
return same_chain[:, None].expand(-1, count, -1, -1) # (b, t, l, l)
|
| 364 |
|
| 365 |
|
| 366 |
class TemplateV2Module(_TemplateBase):
|
|
|
|
| 398 |
|
| 399 |
def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
|
| 400 |
del count
|
| 401 |
+
visibility = feats["visibility_ids"] # (b, t, l)
|
| 402 |
+
return (visibility[..., :, None] == visibility[..., None, :]).float() # (b, t, l, l)
|
| 403 |
|
| 404 |
|
| 405 |
class MSAModule(nn.Module):
|
|
|
|
| 452 |
pair_states: Tensor,
|
| 453 |
training: bool,
|
| 454 |
) -> tuple[bool, int | None, int | None, int | None, int | None]:
|
| 455 |
+
# pair_states: (b, l, l, d_z).
|
| 456 |
if training:
|
| 457 |
return False, None, None, None, None
|
| 458 |
if pair_states.shape[1] > const.chunk_size_threshold:
|
|
|
|
| 469 |
"""Return updated pair tensor Z after every MSA block."""
|
| 470 |
|
| 471 |
chunking = self._chunk_configuration(z, self.training)
|
| 472 |
+
# z: (b, l, l, d_z); emb: (b, l, d_s).
|
| 473 |
+
# s is MSA depth; n_token_type is the residue vocabulary size.
|
| 474 |
+
msa = feats["msa"] # (b, s, l) or (b, s, l, n_token_type)
|
| 475 |
if msa.dtype in (torch.long, torch.int32, torch.int64):
|
| 476 |
+
msa = F.one_hot(msa, num_classes=const.num_tokens).float() # (b, s, l, n_token_type)
|
| 477 |
+
msa_mask = feats["msa_mask"] # (b, s, l)
|
| 478 |
+
components = [ # (b, s, l, n_token_type), then two (b, s, l, 1) tensors
|
| 479 |
msa,
|
| 480 |
+
feats["has_deletion"].unsqueeze(-1), # (b, s, l, 1)
|
| 481 |
+
feats["deletion_value"].unsqueeze(-1), # (b, s, l, 1)
|
| 482 |
]
|
| 483 |
if self.use_paired_feature:
|
| 484 |
+
components.append(feats["msa_paired"].unsqueeze(-1)) # (b, s, l, 1)
|
| 485 |
+
msa_input = torch.cat(components, dim=-1) # (b, s, l, n_token_type + 2 or 3)
|
| 486 |
if self.subsample_msa:
|
| 487 |
+
indices = torch.randperm(msa.shape[1])[: self.num_subsampled_msa] # (s_sub,)
|
| 488 |
+
msa_input = msa_input[:, indices] # (b, s_sub, l, n_token_type + 2 or 3)
|
| 489 |
+
msa_mask = msa_mask[:, indices] # (b, s_sub, l)
|
| 490 |
+
|
| 491 |
+
msa_states = self.msa_proj(msa_input) + self.s_proj(emb).unsqueeze(1) # (b, s, l, d_m)
|
| 492 |
+
token_mask = feats["token_pad_mask"].float() # (b, l)
|
| 493 |
+
pair_mask = token_mask[:, :, None] * token_mask[:, None, :] # (b, l, l)
|
| 494 |
+
pair_states = z # (b, l, l, d_z)
|
| 495 |
for layer in self.layers:
|
| 496 |
+
# Tensor arguments: pair_states (b, l, l, d_z), msa_states (b, s, l, d_m),
|
| 497 |
+
# pair_mask (b, l, l), msa_mask (b, s, l).
|
| 498 |
arguments = (
|
| 499 |
pair_states,
|
| 500 |
msa_states,
|
|
|
|
| 504 |
use_kernels,
|
| 505 |
)
|
| 506 |
if self.activation_checkpointing and self.training:
|
| 507 |
+
pair_states, msa_states = checkpoint( # (b, l, l, d_z), (b, s, l, d_m)
|
| 508 |
layer,
|
| 509 |
*arguments,
|
| 510 |
)
|
| 511 |
else:
|
| 512 |
+
pair_states, msa_states = layer(*arguments) # (b, l, l, d_z), (b, s, l, d_m)
|
| 513 |
+
return pair_states # (b, l, l, d_z)
|
| 514 |
|
| 515 |
|
| 516 |
class MSALayer(nn.Module):
|
|
|
|
| 558 |
"""Return updated Z and M tensors."""
|
| 559 |
|
| 560 |
del chunk_size_transition_z
|
| 561 |
+
# z: (b, l, l, d_z); m: (b, s, l, d_m).
|
| 562 |
+
# token_mask: (b, l, l); msa_mask: (b, s, l).
|
| 563 |
+
dropout = get_dropout_mask(self.msa_dropout, m, self.training) # (b, s, 1, 1)
|
| 564 |
+
msa_states = m + dropout * self.pair_weighted_averaging( # (b, s, l, d_m)
|
| 565 |
m,
|
| 566 |
z,
|
| 567 |
token_mask,
|
| 568 |
chunk_heads_pwa,
|
| 569 |
)
|
| 570 |
+
msa_states = msa_states + self.msa_transition( # (b, s, l, d_m)
|
| 571 |
msa_states,
|
| 572 |
chunk_size_transition_msa,
|
| 573 |
)
|
| 574 |
+
pair_states = z + self.outer_product_mean( # (b, l, l, d_z)
|
| 575 |
msa_states,
|
| 576 |
msa_mask,
|
| 577 |
chunk_size_outer_product,
|
| 578 |
)
|
| 579 |
+
pair_states = self.pairformer_layer( # (b, l, l, d_z)
|
| 580 |
pair_states,
|
| 581 |
token_mask,
|
| 582 |
chunk_size_tri_attn,
|
| 583 |
use_kernels=use_kernels,
|
| 584 |
)
|
| 585 |
+
return pair_states, msa_states # (b, l, l, d_z), (b, s, l, d_m)
|
| 586 |
|
| 587 |
|
| 588 |
class BFactorModule(nn.Module):
|
|
|
|
| 594 |
self.num_bins = num_bins
|
| 595 |
|
| 596 |
def forward(self, s: Tensor) -> Tensor:
|
| 597 |
+
# s: (..., d_s).
|
| 598 |
+
return cast(Tensor, self.bfactor(s)) # (..., n_bin)
|
| 599 |
|
| 600 |
|
| 601 |
class DistogramModule(nn.Module):
|
|
|
|
| 608 |
self.num_bins = num_bins
|
| 609 |
|
| 610 |
def forward(self, z: Tensor) -> Tensor:
|
| 611 |
+
# z: (b, l, l, d_z).
|
| 612 |
+
symmetric = z + z.transpose(1, 2) # (b, l, l, d_z)
|
| 613 |
+
logits = self.distogram(symmetric) # (b, l, l, n_distogram * n_bin)
|
| 614 |
return cast(
|
| 615 |
Tensor,
|
| 616 |
logits.reshape(
|
|
|
|
| 620 |
self.num_distograms,
|
| 621 |
self.num_bins,
|
| 622 |
),
|
| 623 |
+
) # (b, l, l, n_distogram, n_bin)
|
fastplms/models/boltz/vb_modules_utils.py
CHANGED
|
@@ -6,14 +6,14 @@ and attribution records are retained in ``THIRD_PARTY_NOTICES.md``.
|
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
|
|
|
| 9 |
from collections.abc import Iterable, Mapping, Sequence
|
| 10 |
from functools import partial
|
| 11 |
from typing import Any
|
| 12 |
-
|
| 13 |
-
import torch
|
| 14 |
from torch import Tensor, nn
|
| 15 |
from torch.nn import functional as F
|
| 16 |
|
|
|
|
| 17 |
LinearNoBias = partial(nn.Linear, bias=False)
|
| 18 |
|
| 19 |
|
|
@@ -32,19 +32,23 @@ def default(value: Any, fallback: Any) -> Any:
|
|
| 32 |
def log(tensor: Tensor, eps: float = 1e-20) -> Tensor:
|
| 33 |
"""Compute a finite logarithm by applying a scalar lower bound."""
|
| 34 |
|
| 35 |
-
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
class SwiGLU(nn.Module):
|
| 39 |
"""Split X in half and apply a SiLU-gated linear unit."""
|
| 40 |
|
| 41 |
def forward(self, x: Tensor) -> Tensor:
|
| 42 |
-
|
| 43 |
-
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
def _masked_center(coordinates: Tensor, mask: Tensor) -> Tensor:
|
| 47 |
-
|
|
|
|
|
|
|
| 48 |
return (coordinates * weights).sum(dim=1, keepdim=True) / weights.sum(
|
| 49 |
dim=1,
|
| 50 |
keepdim=True,
|
|
@@ -54,21 +58,24 @@ def _masked_center(coordinates: Tensor, mask: Tensor) -> Tensor:
|
|
| 54 |
def center(atom_coords: Tensor, atom_mask: Tensor) -> Tensor:
|
| 55 |
"""Center coordinate tensor X with shape ``(b, n_atoms, 3)``."""
|
| 56 |
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
def _copysign(magnitudes: Tensor, signs: Tensor) -> Tensor:
|
| 61 |
"""Apply the elementwise sign of S to magnitude tensor M."""
|
| 62 |
|
| 63 |
-
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def quaternion_to_matrix(quaternions: Tensor) -> Tensor:
|
| 67 |
"""Convert real-first quaternion tensor Q from ``(..., 4)`` to ``(..., 3, 3)``."""
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
| 72 |
1 - scale * (j_axis * j_axis + k_axis * k_axis),
|
| 73 |
scale * (i_axis * j_axis - k_axis * real),
|
| 74 |
scale * (i_axis * k_axis + j_axis * real),
|
|
@@ -79,6 +86,7 @@ def quaternion_to_matrix(quaternions: Tensor) -> Tensor:
|
|
| 79 |
scale * (j_axis * k_axis + i_axis * real),
|
| 80 |
1 - scale * (i_axis * i_axis + j_axis * j_axis),
|
| 81 |
)
|
|
|
|
| 82 |
return torch.stack(entries, dim=-1).reshape((*quaternions.shape[:-1], 3, 3))
|
| 83 |
|
| 84 |
|
|
@@ -90,10 +98,10 @@ def random_quaternions(
|
|
| 90 |
"""Draw ``n`` uniformly distributed unit quaternions with nonnegative real part."""
|
| 91 |
|
| 92 |
resolved_device = torch.device(device) if isinstance(device, str) else device
|
| 93 |
-
samples = torch.randn((n, 4), dtype=dtype, device=resolved_device)
|
| 94 |
-
squared_norm = (samples * samples).sum(dim=1)
|
| 95 |
-
signed_norm = _copysign(torch.sqrt(squared_norm), samples[:, 0])
|
| 96 |
-
return samples / signed_norm[:, None]
|
| 97 |
|
| 98 |
|
| 99 |
def random_rotations(
|
|
@@ -103,6 +111,7 @@ def random_rotations(
|
|
| 103 |
) -> Tensor:
|
| 104 |
"""Draw rotation tensor R with shape ``(n, 3, 3)``."""
|
| 105 |
|
|
|
|
| 106 |
return quaternion_to_matrix(random_quaternions(n, dtype=dtype, device=device))
|
| 107 |
|
| 108 |
|
|
@@ -114,8 +123,8 @@ def compute_random_augmentation(
|
|
| 114 |
) -> tuple[Tensor, Tensor]:
|
| 115 |
"""Draw independent rotations R and translations T for a replicated batch."""
|
| 116 |
|
| 117 |
-
rotations = random_rotations(multiplicity, dtype=dtype, device=device)
|
| 118 |
-
translations = (
|
| 119 |
torch.randn(
|
| 120 |
(multiplicity, 1, 3),
|
| 121 |
dtype=dtype,
|
|
@@ -123,7 +132,7 @@ def compute_random_augmentation(
|
|
| 123 |
)
|
| 124 |
* s_trans
|
| 125 |
)
|
| 126 |
-
return rotations, translations
|
| 127 |
|
| 128 |
|
| 129 |
def randomly_rotate(
|
|
@@ -133,14 +142,15 @@ def randomly_rotate(
|
|
| 133 |
) -> Tensor | tuple[Tensor, Tensor | None]:
|
| 134 |
"""Rotate X and optionally Y using the same sampled rotation tensor R."""
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
|
|
|
| 138 |
if not return_second_coords:
|
| 139 |
-
return rotated
|
| 140 |
-
rotated_second = (
|
| 141 |
None if second_coords is None else torch.einsum("bmd,bds->bms", second_coords, rotations)
|
| 142 |
)
|
| 143 |
-
return rotated, rotated_second
|
| 144 |
|
| 145 |
|
| 146 |
def center_random_augmentation(
|
|
@@ -154,25 +164,28 @@ def center_random_augmentation(
|
|
| 154 |
) -> Tensor | tuple[Tensor, Tensor | None]:
|
| 155 |
"""Center and rigidly augment coordinate tensors X and optional Y."""
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
| 159 |
if centering:
|
| 160 |
-
centroid = _masked_center(primary, atom_mask)
|
| 161 |
-
primary = primary - centroid
|
| 162 |
if secondary is not None:
|
| 163 |
-
secondary = secondary - centroid
|
| 164 |
|
| 165 |
if augmentation:
|
| 166 |
-
primary, secondary = randomly_rotate(
|
| 167 |
primary,
|
| 168 |
return_second_coords=True,
|
| 169 |
second_coords=secondary,
|
| 170 |
)
|
| 171 |
-
translation = torch.randn_like(primary[:, 0:1, :]) * s_trans
|
| 172 |
-
primary = primary + translation
|
| 173 |
if secondary is not None:
|
| 174 |
-
secondary = secondary + translation
|
| 175 |
|
|
|
|
| 176 |
return (primary, secondary) if return_second_coords else primary
|
| 177 |
|
| 178 |
|
|
@@ -185,21 +198,24 @@ class ExponentialMovingAverage:
|
|
| 185 |
decay: float,
|
| 186 |
use_num_updates: bool = True,
|
| 187 |
) -> None:
|
|
|
|
| 188 |
if not 0.0 <= decay <= 1.0:
|
| 189 |
raise ValueError("decay must lie in [0, 1]")
|
| 190 |
self.decay = decay
|
| 191 |
self.num_updates: int | None = 0 if use_num_updates else None
|
| 192 |
-
self.shadow_params = [
|
| 193 |
parameter.clone().detach() for parameter in parameters if parameter.requires_grad
|
| 194 |
]
|
| 195 |
self.collected_params: list[Tensor] = []
|
| 196 |
|
| 197 |
@staticmethod
|
| 198 |
def _trainable(parameters: Iterable[nn.Parameter]) -> list[nn.Parameter]:
|
|
|
|
| 199 |
return [parameter for parameter in parameters if parameter.requires_grad]
|
| 200 |
|
| 201 |
def update(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 202 |
-
|
|
|
|
| 203 |
if len(trainable) != len(self.shadow_params):
|
| 204 |
raise ValueError("EMA parameter count changed")
|
| 205 |
decay = self.decay
|
|
@@ -209,34 +225,40 @@ class ExponentialMovingAverage:
|
|
| 209 |
update_weight = 1.0 - decay
|
| 210 |
with torch.no_grad():
|
| 211 |
for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
|
| 212 |
-
shadow.sub_(update_weight * (shadow - parameter))
|
| 213 |
|
| 214 |
def compatible(self, parameters: Sequence[Tensor]) -> bool:
|
| 215 |
"""Return whether parameter count and tensor shapes match the EMA state."""
|
| 216 |
|
|
|
|
| 217 |
return len(self.shadow_params) == len(parameters) and all(
|
| 218 |
shadow.shape == parameter.shape
|
| 219 |
for shadow, parameter in zip(self.shadow_params, parameters, strict=True)
|
| 220 |
)
|
| 221 |
|
| 222 |
def copy_to(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 223 |
-
|
|
|
|
| 224 |
if len(trainable) != len(self.shadow_params):
|
| 225 |
raise ValueError("EMA parameter count changed")
|
| 226 |
for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
|
| 227 |
-
parameter.data.copy_(shadow.data)
|
| 228 |
|
| 229 |
def store(self, parameters: Iterable[nn.Parameter]) -> None:
|
|
|
|
|
|
|
| 230 |
self.collected_params = [parameter.clone() for parameter in parameters]
|
| 231 |
|
| 232 |
def restore(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 233 |
-
|
|
|
|
| 234 |
if len(current) != len(self.collected_params):
|
| 235 |
raise ValueError("stored EMA parameter count changed")
|
| 236 |
for collected, parameter in zip(self.collected_params, current, strict=True):
|
| 237 |
-
parameter.data.copy_(collected.data)
|
| 238 |
|
| 239 |
def state_dict(self) -> dict[str, Any]:
|
|
|
|
| 240 |
return {
|
| 241 |
"decay": self.decay,
|
| 242 |
"num_updates": self.num_updates,
|
|
@@ -248,10 +270,13 @@ class ExponentialMovingAverage:
|
|
| 248 |
state_dict: Mapping[str, Any],
|
| 249 |
device: torch.device | str,
|
| 250 |
) -> None:
|
|
|
|
| 251 |
self.decay = float(state_dict["decay"])
|
| 252 |
updates = state_dict["num_updates"]
|
| 253 |
self.num_updates = None if updates is None else int(updates)
|
|
|
|
| 254 |
self.shadow_params = [tensor.to(device) for tensor in state_dict["shadow_params"]]
|
| 255 |
|
| 256 |
def to(self, device: torch.device | str) -> None:
|
|
|
|
| 257 |
self.shadow_params = [tensor.to(device) for tensor in self.shadow_params]
|
|
|
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
import torch
|
| 10 |
from collections.abc import Iterable, Mapping, Sequence
|
| 11 |
from functools import partial
|
| 12 |
from typing import Any
|
|
|
|
|
|
|
| 13 |
from torch import Tensor, nn
|
| 14 |
from torch.nn import functional as F
|
| 15 |
|
| 16 |
+
|
| 17 |
LinearNoBias = partial(nn.Linear, bias=False)
|
| 18 |
|
| 19 |
|
|
|
|
| 32 |
def log(tensor: Tensor, eps: float = 1e-20) -> Tensor:
|
| 33 |
"""Compute a finite logarithm by applying a scalar lower bound."""
|
| 34 |
|
| 35 |
+
# tensor: (...).
|
| 36 |
+
return torch.log(tensor.clamp(min=eps)) # (...)
|
| 37 |
|
| 38 |
|
| 39 |
class SwiGLU(nn.Module):
|
| 40 |
"""Split X in half and apply a SiLU-gated linear unit."""
|
| 41 |
|
| 42 |
def forward(self, x: Tensor) -> Tensor:
|
| 43 |
+
# x: (..., 2 * d).
|
| 44 |
+
values, gates = x.chunk(2, dim=-1) # each: (..., d)
|
| 45 |
+
return F.silu(gates) * values # (..., d)
|
| 46 |
|
| 47 |
|
| 48 |
def _masked_center(coordinates: Tensor, mask: Tensor) -> Tensor:
|
| 49 |
+
# coordinates: (b, n_atom, 3); mask: (b, n_atom).
|
| 50 |
+
weights = mask[:, :, None] # (b, n_atom, 1)
|
| 51 |
+
# (b, 1, 3)
|
| 52 |
return (coordinates * weights).sum(dim=1, keepdim=True) / weights.sum(
|
| 53 |
dim=1,
|
| 54 |
keepdim=True,
|
|
|
|
| 58 |
def center(atom_coords: Tensor, atom_mask: Tensor) -> Tensor:
|
| 59 |
"""Center coordinate tensor X with shape ``(b, n_atoms, 3)``."""
|
| 60 |
|
| 61 |
+
# atom_coords: (b, n_atom, 3); atom_mask: (b, n_atom).
|
| 62 |
+
return atom_coords - _masked_center(atom_coords, atom_mask) # (b, n_atom, 3)
|
| 63 |
|
| 64 |
|
| 65 |
def _copysign(magnitudes: Tensor, signs: Tensor) -> Tensor:
|
| 66 |
"""Apply the elementwise sign of S to magnitude tensor M."""
|
| 67 |
|
| 68 |
+
# magnitudes, signs: broadcast-compatible (...).
|
| 69 |
+
return torch.where((magnitudes < 0) != (signs < 0), -magnitudes, magnitudes) # (...)
|
| 70 |
|
| 71 |
|
| 72 |
def quaternion_to_matrix(quaternions: Tensor) -> Tensor:
|
| 73 |
"""Convert real-first quaternion tensor Q from ``(..., 4)`` to ``(..., 3, 3)``."""
|
| 74 |
|
| 75 |
+
# quaternions: (..., 4).
|
| 76 |
+
real, i_axis, j_axis, k_axis = torch.unbind(quaternions, dim=-1) # each: (...)
|
| 77 |
+
scale = 2.0 / (quaternions * quaternions).sum(dim=-1) # (...)
|
| 78 |
+
entries = ( # nine tensors, each (...)
|
| 79 |
1 - scale * (j_axis * j_axis + k_axis * k_axis),
|
| 80 |
scale * (i_axis * j_axis - k_axis * real),
|
| 81 |
scale * (i_axis * k_axis + j_axis * real),
|
|
|
|
| 86 |
scale * (j_axis * k_axis + i_axis * real),
|
| 87 |
1 - scale * (i_axis * i_axis + j_axis * j_axis),
|
| 88 |
)
|
| 89 |
+
# (..., 3, 3)
|
| 90 |
return torch.stack(entries, dim=-1).reshape((*quaternions.shape[:-1], 3, 3))
|
| 91 |
|
| 92 |
|
|
|
|
| 98 |
"""Draw ``n`` uniformly distributed unit quaternions with nonnegative real part."""
|
| 99 |
|
| 100 |
resolved_device = torch.device(device) if isinstance(device, str) else device
|
| 101 |
+
samples = torch.randn((n, 4), dtype=dtype, device=resolved_device) # (n, 4)
|
| 102 |
+
squared_norm = (samples * samples).sum(dim=1) # (n,)
|
| 103 |
+
signed_norm = _copysign(torch.sqrt(squared_norm), samples[:, 0]) # (n,)
|
| 104 |
+
return samples / signed_norm[:, None] # (n, 4)
|
| 105 |
|
| 106 |
|
| 107 |
def random_rotations(
|
|
|
|
| 111 |
) -> Tensor:
|
| 112 |
"""Draw rotation tensor R with shape ``(n, 3, 3)``."""
|
| 113 |
|
| 114 |
+
# (n, 3, 3)
|
| 115 |
return quaternion_to_matrix(random_quaternions(n, dtype=dtype, device=device))
|
| 116 |
|
| 117 |
|
|
|
|
| 123 |
) -> tuple[Tensor, Tensor]:
|
| 124 |
"""Draw independent rotations R and translations T for a replicated batch."""
|
| 125 |
|
| 126 |
+
rotations = random_rotations(multiplicity, dtype=dtype, device=device) # (m, 3, 3)
|
| 127 |
+
translations = ( # (m, 1, 3)
|
| 128 |
torch.randn(
|
| 129 |
(multiplicity, 1, 3),
|
| 130 |
dtype=dtype,
|
|
|
|
| 132 |
)
|
| 133 |
* s_trans
|
| 134 |
)
|
| 135 |
+
return rotations, translations # (m, 3, 3), (m, 1, 3)
|
| 136 |
|
| 137 |
|
| 138 |
def randomly_rotate(
|
|
|
|
| 142 |
) -> Tensor | tuple[Tensor, Tensor | None]:
|
| 143 |
"""Rotate X and optionally Y using the same sampled rotation tensor R."""
|
| 144 |
|
| 145 |
+
# coords: (b, n, 3); second_coords: (b, n_second, 3) or None.
|
| 146 |
+
rotations = random_rotations(len(coords), coords.dtype, coords.device) # (b, 3, 3)
|
| 147 |
+
rotated = torch.einsum("bmd,bds->bms", coords, rotations) # (b, n, 3)
|
| 148 |
if not return_second_coords:
|
| 149 |
+
return rotated # (b, n, 3)
|
| 150 |
+
rotated_second = ( # (b, n_second, 3) or None
|
| 151 |
None if second_coords is None else torch.einsum("bmd,bds->bms", second_coords, rotations)
|
| 152 |
)
|
| 153 |
+
return rotated, rotated_second # (b, n, 3), (b, n_second, 3) or None
|
| 154 |
|
| 155 |
|
| 156 |
def center_random_augmentation(
|
|
|
|
| 164 |
) -> Tensor | tuple[Tensor, Tensor | None]:
|
| 165 |
"""Center and rigidly augment coordinate tensors X and optional Y."""
|
| 166 |
|
| 167 |
+
# atom_coords: (b, n_atom, 3); atom_mask: (b, n_atom).
|
| 168 |
+
# second_coords: (b, n_second, 3) or None.
|
| 169 |
+
primary = atom_coords # (b, n_atom, 3)
|
| 170 |
+
secondary = second_coords # (b, n_second, 3) or None
|
| 171 |
if centering:
|
| 172 |
+
centroid = _masked_center(primary, atom_mask) # (b, 1, 3)
|
| 173 |
+
primary = primary - centroid # (b, n_atom, 3)
|
| 174 |
if secondary is not None:
|
| 175 |
+
secondary = secondary - centroid # (b, n_second, 3)
|
| 176 |
|
| 177 |
if augmentation:
|
| 178 |
+
primary, secondary = randomly_rotate( # (b, n_atom, 3), (b, n_second, 3) or None
|
| 179 |
primary,
|
| 180 |
return_second_coords=True,
|
| 181 |
second_coords=secondary,
|
| 182 |
)
|
| 183 |
+
translation = torch.randn_like(primary[:, 0:1, :]) * s_trans # (b, 1, 3)
|
| 184 |
+
primary = primary + translation # (b, n_atom, 3)
|
| 185 |
if secondary is not None:
|
| 186 |
+
secondary = secondary + translation # (b, n_second, 3)
|
| 187 |
|
| 188 |
+
# (b, n_atom, 3), optionally paired with (b, n_second, 3) or None.
|
| 189 |
return (primary, secondary) if return_second_coords else primary
|
| 190 |
|
| 191 |
|
|
|
|
| 198 |
decay: float,
|
| 199 |
use_num_updates: bool = True,
|
| 200 |
) -> None:
|
| 201 |
+
# parameters: one independently shaped tensor (...) per trainable parameter.
|
| 202 |
if not 0.0 <= decay <= 1.0:
|
| 203 |
raise ValueError("decay must lie in [0, 1]")
|
| 204 |
self.decay = decay
|
| 205 |
self.num_updates: int | None = 0 if use_num_updates else None
|
| 206 |
+
self.shadow_params = [ # each: same shape as one trainable parameter, (...)
|
| 207 |
parameter.clone().detach() for parameter in parameters if parameter.requires_grad
|
| 208 |
]
|
| 209 |
self.collected_params: list[Tensor] = []
|
| 210 |
|
| 211 |
@staticmethod
|
| 212 |
def _trainable(parameters: Iterable[nn.Parameter]) -> list[nn.Parameter]:
|
| 213 |
+
# Each item is one trainable parameter with an independently varying shape (...).
|
| 214 |
return [parameter for parameter in parameters if parameter.requires_grad]
|
| 215 |
|
| 216 |
def update(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 217 |
+
# parameters: one independently shaped tensor (...) per trainable parameter.
|
| 218 |
+
trainable = self._trainable(parameters) # each: one trainable parameter, (...)
|
| 219 |
if len(trainable) != len(self.shadow_params):
|
| 220 |
raise ValueError("EMA parameter count changed")
|
| 221 |
decay = self.decay
|
|
|
|
| 225 |
update_weight = 1.0 - decay
|
| 226 |
with torch.no_grad():
|
| 227 |
for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
|
| 228 |
+
shadow.sub_(update_weight * (shadow - parameter)) # (...) unchanged in place
|
| 229 |
|
| 230 |
def compatible(self, parameters: Sequence[Tensor]) -> bool:
|
| 231 |
"""Return whether parameter count and tensor shapes match the EMA state."""
|
| 232 |
|
| 233 |
+
# Each shadow and corresponding parameter has an independently varying shape (...).
|
| 234 |
return len(self.shadow_params) == len(parameters) and all(
|
| 235 |
shadow.shape == parameter.shape
|
| 236 |
for shadow, parameter in zip(self.shadow_params, parameters, strict=True)
|
| 237 |
)
|
| 238 |
|
| 239 |
def copy_to(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 240 |
+
# parameters: one independently shaped tensor (...) per trainable parameter.
|
| 241 |
+
trainable = self._trainable(parameters) # each: one trainable parameter, (...)
|
| 242 |
if len(trainable) != len(self.shadow_params):
|
| 243 |
raise ValueError("EMA parameter count changed")
|
| 244 |
for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
|
| 245 |
+
parameter.data.copy_(shadow.data) # (...) unchanged in place
|
| 246 |
|
| 247 |
def store(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 248 |
+
# parameters: one independently shaped tensor (...) per parameter.
|
| 249 |
+
# Each clone has the same shape (...) as its corresponding parameter.
|
| 250 |
self.collected_params = [parameter.clone() for parameter in parameters]
|
| 251 |
|
| 252 |
def restore(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 253 |
+
# parameters: one independently shaped tensor (...) per parameter.
|
| 254 |
+
current = list(parameters) # each: one parameter, (...)
|
| 255 |
if len(current) != len(self.collected_params):
|
| 256 |
raise ValueError("stored EMA parameter count changed")
|
| 257 |
for collected, parameter in zip(self.collected_params, current, strict=True):
|
| 258 |
+
parameter.data.copy_(collected.data) # (...) unchanged in place
|
| 259 |
|
| 260 |
def state_dict(self) -> dict[str, Any]:
|
| 261 |
+
# shadow_params contains independently shaped tensors (...).
|
| 262 |
return {
|
| 263 |
"decay": self.decay,
|
| 264 |
"num_updates": self.num_updates,
|
|
|
|
| 270 |
state_dict: Mapping[str, Any],
|
| 271 |
device: torch.device | str,
|
| 272 |
) -> None:
|
| 273 |
+
# state_dict["shadow_params"] contains independently shaped tensors (...).
|
| 274 |
self.decay = float(state_dict["decay"])
|
| 275 |
updates = state_dict["num_updates"]
|
| 276 |
self.num_updates = None if updates is None else int(updates)
|
| 277 |
+
# Each tensor retains its shape (...).
|
| 278 |
self.shadow_params = [tensor.to(device) for tensor in state_dict["shadow_params"]]
|
| 279 |
|
| 280 |
def to(self, device: torch.device | str) -> None:
|
| 281 |
+
# Each shadow parameter retains its independently varying shape (...).
|
| 282 |
self.shadow_params = [tensor.to(device) for tensor in self.shadow_params]
|
fastplms/models/boltz/vb_potentials_potentials.py
CHANGED
|
@@ -9,12 +9,11 @@ oracle while preserving the public class names used by converted checkpoints.
|
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
|
|
|
|
| 12 |
from abc import ABC, abstractmethod
|
| 13 |
from dataclasses import dataclass
|
| 14 |
from typing import Any
|
| 15 |
|
| 16 |
-
import torch
|
| 17 |
-
|
| 18 |
from . import vb_const as const
|
| 19 |
from .vb_loss_diffusionv2 import weighted_rigid_align
|
| 20 |
from .vb_potentials_schedules import (
|
|
@@ -23,22 +22,23 @@ from .vb_potentials_schedules import (
|
|
| 23 |
PiecewiseStepFunction,
|
| 24 |
)
|
| 25 |
|
|
|
|
| 26 |
Parameter = ParameterSchedule | float | int | bool
|
| 27 |
ParameterMap = dict[str, Parameter]
|
| 28 |
|
| 29 |
|
| 30 |
@dataclass(frozen=True, slots=True)
|
| 31 |
class _PreparedPotential:
|
| 32 |
-
coordinates: torch.Tensor
|
| 33 |
-
index: torch.Tensor
|
| 34 |
function_args: tuple[Any, ...]
|
| 35 |
-
com_index: torch.Tensor | None
|
| 36 |
-
atom_pad_mask: torch.Tensor | None
|
| 37 |
-
ref_coords: torch.Tensor | None
|
| 38 |
-
ref_mask: torch.Tensor | None
|
| 39 |
-
ref_token_index: torch.Tensor | None
|
| 40 |
-
negation_mask: torch.Tensor | None
|
| 41 |
-
union_index: torch.Tensor | None
|
| 42 |
|
| 43 |
|
| 44 |
def _reduce_atoms_to_centers(
|
|
@@ -46,15 +46,16 @@ def _reduce_atoms_to_centers(
|
|
| 46 |
com_index: torch.Tensor,
|
| 47 |
atom_pad_mask: torch.Tensor,
|
| 48 |
) -> torch.Tensor:
|
| 49 |
-
|
| 50 |
-
|
|
|
|
| 51 |
center_shape = (*unpadded_coordinates.shape[:-2], unpadded_index.max() + 1, 3)
|
| 52 |
return torch.zeros(center_shape, device=coordinates.device).scatter_reduce(
|
| 53 |
-2,
|
| 54 |
unpadded_index.unsqueeze(-1).expand_as(unpadded_coordinates),
|
| 55 |
unpadded_coordinates,
|
| 56 |
"mean",
|
| 57 |
-
)
|
| 58 |
|
| 59 |
|
| 60 |
def _union_weights(
|
|
@@ -62,7 +63,8 @@ def _union_weights(
|
|
| 62 |
union_index: torch.Tensor,
|
| 63 |
union_lambda: float,
|
| 64 |
) -> torch.Tensor:
|
| 65 |
-
|
|
|
|
| 66 |
partition = torch.zeros(
|
| 67 |
(*energy.shape[:-1], union_index.max() + 1),
|
| 68 |
device=union_index.device,
|
|
@@ -71,10 +73,10 @@ def _union_weights(
|
|
| 71 |
union_index.expand_as(unnormalized),
|
| 72 |
unnormalized,
|
| 73 |
"sum",
|
| 74 |
-
)
|
| 75 |
-
weights = unnormalized / partition[..., union_index]
|
| 76 |
-
weights[partition[..., union_index] == 0] = 0
|
| 77 |
-
return weights
|
| 78 |
|
| 79 |
|
| 80 |
def _scatter_constraint_gradients(
|
|
@@ -83,20 +85,22 @@ def _scatter_constraint_gradients(
|
|
| 83 |
index: torch.Tensor,
|
| 84 |
coordinates: torch.Tensor,
|
| 85 |
) -> torch.Tensor:
|
|
|
|
|
|
|
| 86 |
product = coefficients.tile(variable_gradient.shape[-3]).unsqueeze(
|
| 87 |
-1
|
| 88 |
-
) * variable_gradient.flatten(start_dim=-3, end_dim=-2)
|
| 89 |
if product.dim() > 3:
|
| 90 |
-
product = product.sum(dim=list(range(1, product.dim() - 2)))
|
| 91 |
scatter_index = (
|
| 92 |
index.flatten(start_dim=0, end_dim=1).unsqueeze(-1).expand((*coordinates.shape[:-2], -1, 3))
|
| 93 |
-
)
|
| 94 |
return torch.zeros_like(coordinates).scatter_reduce(
|
| 95 |
-2,
|
| 96 |
scatter_index,
|
| 97 |
product,
|
| 98 |
"sum",
|
| 99 |
-
)
|
| 100 |
|
| 101 |
|
| 102 |
class Potential(ABC):
|
|
@@ -112,26 +116,29 @@ class Potential(ABC):
|
|
| 112 |
parameters: dict[str, Any],
|
| 113 |
computed_args: tuple[Any, ...] | None = None,
|
| 114 |
) -> _PreparedPotential:
|
|
|
|
| 115 |
if computed_args is None:
|
| 116 |
computed_args = self.compute_args(feats, parameters)
|
| 117 |
-
index, args, com_args, ref_args, operator_args = computed_args
|
| 118 |
com_index = atom_pad_mask = None
|
| 119 |
if com_args is not None:
|
| 120 |
-
com_index, atom_pad_mask = com_args
|
| 121 |
coordinates = _reduce_atoms_to_centers(
|
| 122 |
coordinates,
|
| 123 |
com_index,
|
| 124 |
atom_pad_mask,
|
| 125 |
-
)
|
| 126 |
|
| 127 |
ref_coords = ref_mask = ref_token_index = None
|
| 128 |
if ref_args is not None:
|
|
|
|
|
|
|
| 129 |
ref_coords, ref_mask, ref_atom_index, ref_token_index = ref_args
|
| 130 |
-
coordinates = coordinates[..., ref_atom_index, :]
|
| 131 |
|
| 132 |
negation_mask = union_index = None
|
| 133 |
if operator_args is not None:
|
| 134 |
-
negation_mask, union_index = operator_args
|
| 135 |
return _PreparedPotential(
|
| 136 |
coordinates=coordinates,
|
| 137 |
index=index,
|
|
@@ -153,9 +160,10 @@ class Potential(ABC):
|
|
| 153 |
) -> torch.Tensor:
|
| 154 |
"""Evaluate one energy per coordinate sample."""
|
| 155 |
|
|
|
|
| 156 |
computed_args = self.compute_args(feats, parameters)
|
| 157 |
if computed_args[0].shape[1] == 0:
|
| 158 |
-
return torch.zeros(coords.shape[:-2], device=coords.device)
|
| 159 |
prepared = self._prepare(coords, feats, parameters, computed_args)
|
| 160 |
value = self.compute_variable(
|
| 161 |
prepared.coordinates,
|
|
@@ -163,21 +171,21 @@ class Potential(ABC):
|
|
| 163 |
ref_coords=prepared.ref_coords,
|
| 164 |
ref_mask=prepared.ref_mask,
|
| 165 |
compute_gradient=False,
|
| 166 |
-
)
|
| 167 |
energy = self.compute_function(
|
| 168 |
value,
|
| 169 |
*prepared.function_args,
|
| 170 |
negation_mask=prepared.negation_mask,
|
| 171 |
compute_derivative=False,
|
| 172 |
-
)
|
| 173 |
if prepared.union_index is not None:
|
| 174 |
weights = _union_weights(
|
| 175 |
energy,
|
| 176 |
prepared.union_index,
|
| 177 |
parameters["union_lambda"],
|
| 178 |
-
)
|
| 179 |
-
return (energy * weights).sum(dim=-1)
|
| 180 |
-
return energy.sum(dim=tuple(range(1, energy.dim())))
|
| 181 |
|
| 182 |
def compute_gradient(
|
| 183 |
self,
|
|
@@ -187,9 +195,10 @@ class Potential(ABC):
|
|
| 187 |
) -> torch.Tensor:
|
| 188 |
"""Return the analytic coordinate gradient of the potential energy."""
|
| 189 |
|
|
|
|
| 190 |
computed_args = self.compute_args(feats, parameters)
|
| 191 |
if computed_args[0].shape[1] == 0:
|
| 192 |
-
return torch.zeros_like(coords)
|
| 193 |
prepared = self._prepare(coords, feats, parameters, computed_args)
|
| 194 |
value, variable_gradient = self.compute_variable(
|
| 195 |
prepared.coordinates,
|
|
@@ -197,19 +206,19 @@ class Potential(ABC):
|
|
| 197 |
ref_coords=prepared.ref_coords,
|
| 198 |
ref_mask=prepared.ref_mask,
|
| 199 |
compute_gradient=True,
|
| 200 |
-
)
|
| 201 |
energy, energy_derivative = self.compute_function(
|
| 202 |
value,
|
| 203 |
*prepared.function_args,
|
| 204 |
negation_mask=prepared.negation_mask,
|
| 205 |
compute_derivative=True,
|
| 206 |
-
)
|
| 207 |
if prepared.union_index is not None:
|
| 208 |
weights = _union_weights(
|
| 209 |
energy,
|
| 210 |
prepared.union_index,
|
| 211 |
parameters["union_lambda"],
|
| 212 |
-
)
|
| 213 |
union_energy = torch.zeros(
|
| 214 |
(*energy.shape[:-1], prepared.union_index.max() + 1),
|
| 215 |
device=prepared.union_index.device,
|
|
@@ -218,7 +227,7 @@ class Potential(ABC):
|
|
| 218 |
prepared.union_index.expand_as(energy),
|
| 219 |
energy * weights,
|
| 220 |
"sum",
|
| 221 |
-
)
|
| 222 |
coefficients = (
|
| 223 |
energy_derivative
|
| 224 |
* weights
|
|
@@ -227,21 +236,21 @@ class Potential(ABC):
|
|
| 227 |
+ parameters["union_lambda"]
|
| 228 |
* (energy - union_energy[..., prepared.union_index])
|
| 229 |
)
|
| 230 |
-
)
|
| 231 |
else:
|
| 232 |
-
coefficients = energy_derivative
|
| 233 |
|
| 234 |
atom_gradient = _scatter_constraint_gradients(
|
| 235 |
coefficients,
|
| 236 |
variable_gradient,
|
| 237 |
prepared.index,
|
| 238 |
prepared.coordinates,
|
| 239 |
-
)
|
| 240 |
if prepared.com_index is not None:
|
| 241 |
-
atom_gradient = atom_gradient[..., prepared.com_index, :]
|
| 242 |
elif prepared.ref_token_index is not None:
|
| 243 |
-
atom_gradient = atom_gradient[..., prepared.ref_token_index, :]
|
| 244 |
-
return atom_gradient
|
| 245 |
|
| 246 |
def compute_parameters(self, t: float) -> dict[str, Any] | None:
|
| 247 |
"""Resolve scheduled parameters at diffusion time ``t``."""
|
|
@@ -261,6 +270,7 @@ class Potential(ABC):
|
|
| 261 |
negation_mask: torch.Tensor | None = None,
|
| 262 |
compute_derivative: bool = False,
|
| 263 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
|
|
|
| 264 |
raise NotImplementedError
|
| 265 |
|
| 266 |
@abstractmethod
|
|
@@ -272,6 +282,7 @@ class Potential(ABC):
|
|
| 272 |
ref_mask: torch.Tensor | None = None,
|
| 273 |
compute_gradient: bool = False,
|
| 274 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
|
|
|
| 275 |
raise NotImplementedError
|
| 276 |
|
| 277 |
@abstractmethod
|
|
@@ -303,16 +314,18 @@ class FlatBottomPotential(Potential):
|
|
| 303 |
negation_mask: torch.Tensor | None = None,
|
| 304 |
compute_derivative: bool = False,
|
| 305 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
|
|
|
|
|
|
| 306 |
lower = (
|
| 307 |
(torch.full_like(value, float("-inf")) if lower_bounds is None else lower_bounds)
|
| 308 |
.expand_as(value)
|
| 309 |
.clone()
|
| 310 |
-
)
|
| 311 |
upper = (
|
| 312 |
(torch.full_like(value, float("inf")) if upper_bounds is None else upper_bounds)
|
| 313 |
.expand_as(value)
|
| 314 |
.clone()
|
| 315 |
-
)
|
| 316 |
|
| 317 |
if negation_mask is not None:
|
| 318 |
if not torch.is_tensor(negation_mask):
|
|
@@ -329,37 +342,38 @@ class FlatBottomPotential(Potential):
|
|
| 329 |
f"got {negation_mask.device} and {value.device}."
|
| 330 |
)
|
| 331 |
try:
|
| 332 |
-
expanded_negation_mask = negation_mask.expand_as(value)
|
| 333 |
except RuntimeError as error:
|
| 334 |
raise ValueError(
|
| 335 |
"negation_mask must be broadcastable to value shape "
|
| 336 |
f"{tuple(value.shape)}, got {tuple(negation_mask.shape)}."
|
| 337 |
) from error
|
| 338 |
-
unbounded_below = torch.isneginf(lower)
|
| 339 |
-
unbounded_above = torch.isposinf(upper)
|
|
|
|
| 340 |
valid_negation = unbounded_below | unbounded_above | expanded_negation_mask
|
| 341 |
if not bool(torch.all(valid_negation).item()):
|
| 342 |
raise ValueError(
|
| 343 |
"negation_mask may be false only where at least one bound is infinite."
|
| 344 |
)
|
| 345 |
-
select_upper = ~unbounded_above & ~expanded_negation_mask
|
| 346 |
-
lower[select_upper] = upper[select_upper]
|
| 347 |
-
upper[select_upper] = float("inf")
|
| 348 |
-
select_lower = ~unbounded_below & ~expanded_negation_mask
|
| 349 |
-
upper[select_lower] = lower[select_lower]
|
| 350 |
-
lower[select_lower] = float("-inf")
|
| 351 |
-
|
| 352 |
-
below = value < lower
|
| 353 |
-
above = value > upper
|
| 354 |
-
energy = torch.zeros_like(value)
|
| 355 |
-
energy[below] = (k * (lower - value))[below]
|
| 356 |
-
energy[above] = (k * (value - upper))[above]
|
| 357 |
if not compute_derivative:
|
| 358 |
-
return energy
|
| 359 |
-
derivative = torch.zeros_like(value)
|
| 360 |
-
derivative[below] = -k.expand_as(below)[below]
|
| 361 |
-
derivative[above] = k.expand_as(above)[above]
|
| 362 |
-
return energy, derivative
|
| 363 |
|
| 364 |
|
| 365 |
class ReferencePotential(Potential):
|
|
@@ -373,19 +387,22 @@ class ReferencePotential(Potential):
|
|
| 373 |
ref_mask: torch.Tensor,
|
| 374 |
compute_gradient: bool = False,
|
| 375 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
|
|
|
|
|
|
| 376 |
aligned_reference = weighted_rigid_align(
|
| 377 |
ref_coords.float(),
|
| 378 |
coords[:, index].float(),
|
| 379 |
ref_mask,
|
| 380 |
ref_mask,
|
| 381 |
-
)
|
| 382 |
-
displacement = coords[:, index] - aligned_reference
|
| 383 |
-
distance = torch.linalg.norm(displacement, dim=-1)
|
| 384 |
if not compute_gradient:
|
| 385 |
-
return distance
|
| 386 |
-
unit_displacement = displacement / distance.unsqueeze(-1)
|
|
|
|
| 387 |
gradient = (unit_displacement * ref_mask.unsqueeze(-1)).unsqueeze(1)
|
| 388 |
-
return distance, gradient
|
| 389 |
|
| 390 |
|
| 391 |
class DistancePotential(Potential):
|
|
@@ -399,15 +416,17 @@ class DistancePotential(Potential):
|
|
| 399 |
ref_mask: torch.Tensor | None = None,
|
| 400 |
compute_gradient: bool = False,
|
| 401 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
|
|
|
| 402 |
del ref_coords, ref_mask
|
| 403 |
displacement = coords.index_select(-2, index[0]) - coords.index_select(
|
| 404 |
-2,
|
| 405 |
index[1],
|
| 406 |
-
)
|
| 407 |
-
distance = torch.linalg.norm(displacement, dim=-1)
|
| 408 |
if not compute_gradient:
|
| 409 |
-
return distance
|
| 410 |
-
unit_displacement = displacement / distance.unsqueeze(-1)
|
|
|
|
| 411 |
return distance, torch.stack((unit_displacement, -unit_displacement), dim=1)
|
| 412 |
|
| 413 |
|
|
@@ -422,36 +441,40 @@ class DihedralPotential(Potential):
|
|
| 422 |
ref_mask: torch.Tensor | None = None,
|
| 423 |
compute_gradient: bool = False,
|
| 424 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
|
|
|
| 425 |
del ref_coords, ref_mask
|
|
|
|
| 426 |
r_ij = coords.index_select(-2, index[0]) - coords.index_select(-2, index[1])
|
| 427 |
r_kj = coords.index_select(-2, index[2]) - coords.index_select(-2, index[1])
|
| 428 |
r_kl = coords.index_select(-2, index[2]) - coords.index_select(-2, index[3])
|
| 429 |
-
n_ijk = torch.cross(r_ij, r_kj, dim=-1)
|
| 430 |
-
n_jkl = torch.cross(r_kj, r_kl, dim=-1)
|
| 431 |
-
r_kj_norm = torch.linalg.norm(r_kj, dim=-1)
|
| 432 |
-
n_ijk_norm = torch.linalg.norm(n_ijk, dim=-1)
|
| 433 |
-
n_jkl_norm = torch.linalg.norm(n_jkl, dim=-1)
|
| 434 |
|
| 435 |
orientation = torch.sign(
|
| 436 |
r_kj.unsqueeze(-2) @ torch.cross(n_ijk, n_jkl, dim=-1).unsqueeze(-1)
|
| 437 |
-
).squeeze(-1, -2)
|
| 438 |
cosine = (n_ijk.unsqueeze(-2) @ n_jkl.unsqueeze(-1)).squeeze(-1, -2) / (
|
| 439 |
n_ijk_norm * n_jkl_norm
|
| 440 |
-
)
|
|
|
|
| 441 |
angle = orientation * torch.arccos(torch.clamp(cosine, -1 + 1e-8, 1 - 1e-8))
|
| 442 |
if not compute_gradient:
|
| 443 |
-
return angle
|
| 444 |
|
| 445 |
projection_i = (
|
| 446 |
(r_ij.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
|
| 447 |
-
).unsqueeze(-1)
|
| 448 |
projection_l = (
|
| 449 |
(r_kl.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
|
| 450 |
-
).unsqueeze(-1)
|
| 451 |
-
grad_i = n_ijk * (r_kj_norm / n_ijk_norm**2).unsqueeze(-1)
|
| 452 |
-
grad_l = -n_jkl * (r_kj_norm / n_jkl_norm**2).unsqueeze(-1)
|
| 453 |
-
grad_j = (projection_i - 1) * grad_i - projection_l * grad_l
|
| 454 |
-
grad_k = (projection_l - 1) * grad_l - projection_i * grad_i
|
|
|
|
| 455 |
return angle, torch.stack((grad_i, grad_j, grad_k, grad_l), dim=1)
|
| 456 |
|
| 457 |
|
|
@@ -466,33 +489,37 @@ class AbsDihedralPotential(DihedralPotential):
|
|
| 466 |
ref_mask: torch.Tensor | None = None,
|
| 467 |
compute_gradient: bool = False,
|
| 468 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
|
|
|
| 469 |
del ref_coords, ref_mask
|
| 470 |
if not compute_gradient:
|
| 471 |
-
return torch.abs(super().compute_variable(coords, index))
|
| 472 |
angle, gradient = super().compute_variable(
|
| 473 |
coords,
|
| 474 |
index,
|
| 475 |
compute_gradient=True,
|
| 476 |
-
)
|
| 477 |
-
gradient[(angle < 0)[..., None, :, None].expand_as(gradient)] *= -1
|
| 478 |
-
return torch.abs(angle), gradient
|
| 479 |
|
| 480 |
|
| 481 |
def _element_radii(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
|
|
|
| 482 |
element_radii = torch.zeros(
|
| 483 |
const.num_elements,
|
| 484 |
dtype=torch.float32,
|
| 485 |
device=feats["ref_element"].device,
|
| 486 |
-
)
|
| 487 |
element_radii[1:119] = torch.tensor(
|
| 488 |
const.vdw_radii,
|
| 489 |
dtype=torch.float32,
|
| 490 |
device=element_radii.device,
|
| 491 |
-
)
|
|
|
|
| 492 |
return (feats["ref_element"].float() @ element_radii.unsqueeze(-1)).squeeze(-1)[0]
|
| 493 |
|
| 494 |
|
| 495 |
def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
|
|
|
| 496 |
return (
|
| 497 |
torch.bmm(
|
| 498 |
feats["atom_to_token"].float(),
|
|
@@ -500,94 +527,103 @@ def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
|
| 500 |
)
|
| 501 |
.squeeze(-1)
|
| 502 |
.long()
|
| 503 |
-
)[0]
|
| 504 |
|
| 505 |
|
| 506 |
class PoseBustersPotential(FlatBottomPotential, DistancePotential):
|
| 507 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
|
|
|
| 517 |
shared_buffer = min(parameters["bond_buffer"], parameters["angle_buffer"])
|
| 518 |
-
lower[bond * angle] *= 1.0 - shared_buffer
|
| 519 |
-
upper[bond * angle] *= 1.0 + shared_buffer
|
| 520 |
-
lower[~bond * ~angle] *= 1.0 - parameters["clash_buffer"]
|
| 521 |
-
upper[~bond * ~angle] = float("inf")
|
| 522 |
-
|
| 523 |
-
atom_radii = _element_radii(feats)
|
| 524 |
-
bond_cutoff = 0.35 + atom_radii[pair_index].mean(dim=0)
|
| 525 |
-
lower[~bond] = torch.max(lower[~bond], bond_cutoff[~bond])
|
| 526 |
-
upper[bond] = torch.min(upper[bond], bond_cutoff[bond])
|
|
|
|
| 527 |
return pair_index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 528 |
|
| 529 |
|
| 530 |
class ConnectionsPotential(FlatBottomPotential, DistancePotential):
|
| 531 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 532 |
-
|
|
|
|
| 533 |
upper = torch.full(
|
| 534 |
(pair_index.shape[1],),
|
| 535 |
parameters["buffer"],
|
| 536 |
device=pair_index.device,
|
| 537 |
-
)
|
|
|
|
| 538 |
return pair_index, (torch.ones_like(upper), None, upper), None, None, None
|
| 539 |
|
| 540 |
|
| 541 |
class VDWOverlapPotential(FlatBottomPotential, DistancePotential):
|
| 542 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 543 |
-
atom_chain_id = _atom_chain_ids(feats)
|
| 544 |
-
atom_pad_mask = feats["atom_pad_mask"][0].bool()
|
| 545 |
-
chain_sizes = torch.bincount(atom_chain_id[atom_pad_mask])
|
| 546 |
-
nonion_atom = (chain_sizes > 1)[atom_chain_id]
|
| 547 |
-
atom_radii = _element_radii(feats)
|
| 548 |
pair_index = torch.triu_indices(
|
| 549 |
atom_chain_id.shape[0],
|
| 550 |
atom_chain_id.shape[0],
|
| 551 |
1,
|
| 552 |
device=atom_chain_id.device,
|
| 553 |
-
)
|
| 554 |
-
pair_pad_mask = atom_pad_mask[pair_index].all(dim=0)
|
|
|
|
| 555 |
pair_ion_mask = nonion_atom[pair_index[0]] * nonion_atom[pair_index[1]]
|
| 556 |
|
| 557 |
-
num_chains = atom_chain_id.max() + 1
|
| 558 |
-
connected = feats["connected_chain_index"][0]
|
| 559 |
connected_matrix = torch.eye(
|
| 560 |
num_chains,
|
| 561 |
device=atom_chain_id.device,
|
| 562 |
dtype=torch.bool,
|
| 563 |
-
)
|
| 564 |
-
connected_matrix[connected[0], connected[1]] = True
|
| 565 |
-
connected_matrix[connected[1], connected[0]] = True
|
| 566 |
connected_pair = connected_matrix[
|
| 567 |
atom_chain_id[pair_index[0]],
|
| 568 |
atom_chain_id[pair_index[1]],
|
| 569 |
-
]
|
|
|
|
| 570 |
pair_index = pair_index[:, pair_pad_mask * pair_ion_mask * ~connected_pair]
|
|
|
|
| 571 |
lower = atom_radii[pair_index].sum(dim=0) * (1.0 - parameters["buffer"])
|
|
|
|
| 572 |
return pair_index, (torch.ones_like(lower), lower, None), None, None, None
|
| 573 |
|
| 574 |
|
| 575 |
class SymmetricChainCOMPotential(FlatBottomPotential, DistancePotential):
|
| 576 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 577 |
-
atom_chain_id = _atom_chain_ids(feats)
|
| 578 |
-
atom_pad_mask = feats["atom_pad_mask"][0].bool()
|
| 579 |
-
nonion_chain = torch.bincount(atom_chain_id[atom_pad_mask]) > 1
|
| 580 |
-
pair_index = feats["symmetric_chain_index"][0]
|
| 581 |
pair_index = pair_index[
|
| 582 |
:,
|
| 583 |
nonion_chain[pair_index[0]] * nonion_chain[pair_index[1]],
|
| 584 |
-
]
|
| 585 |
lower = torch.full(
|
| 586 |
(pair_index.shape[1],),
|
| 587 |
parameters["buffer"],
|
| 588 |
dtype=torch.float32,
|
| 589 |
device=pair_index.device,
|
| 590 |
-
)
|
|
|
|
| 591 |
return (
|
| 592 |
pair_index,
|
| 593 |
(torch.ones_like(lower), lower, None),
|
|
@@ -602,52 +638,60 @@ def _oriented_bounds(
|
|
| 602 |
positive_lower: float,
|
| 603 |
negative_upper: float,
|
| 604 |
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
|
|
|
| 612 |
|
| 613 |
|
| 614 |
class StereoBondPotential(FlatBottomPotential, AbsDihedralPotential):
|
| 615 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 616 |
-
|
| 617 |
-
|
|
|
|
| 618 |
lower, upper = _oriented_bounds(
|
| 619 |
orientation,
|
| 620 |
torch.pi - parameters["buffer"],
|
| 621 |
parameters["buffer"],
|
| 622 |
-
)
|
|
|
|
| 623 |
return index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 624 |
|
| 625 |
|
| 626 |
class ChiralAtomPotential(FlatBottomPotential, DihedralPotential):
|
| 627 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 628 |
-
|
| 629 |
-
|
|
|
|
| 630 |
lower, upper = _oriented_bounds(
|
| 631 |
orientation,
|
| 632 |
parameters["buffer"],
|
| 633 |
-parameters["buffer"],
|
| 634 |
-
)
|
|
|
|
| 635 |
return index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 636 |
|
| 637 |
|
| 638 |
class PlanarBondPotential(FlatBottomPotential, AbsDihedralPotential):
|
| 639 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 640 |
-
|
|
|
|
| 641 |
improper_pattern = torch.tensor(
|
| 642 |
[[1, 2, 3, 0], [4, 5, 0, 3]],
|
| 643 |
device=bond_index.device,
|
| 644 |
-
).T
|
|
|
|
| 645 |
improper_index = bond_index[:, improper_pattern].swapaxes(0, 1).flatten(start_dim=1)
|
| 646 |
upper = torch.full(
|
| 647 |
(improper_index.shape[1],),
|
| 648 |
parameters["buffer"],
|
| 649 |
device=improper_index.device,
|
| 650 |
-
)
|
|
|
|
| 651 |
return (
|
| 652 |
improper_index,
|
| 653 |
(torch.ones_like(upper), None, upper),
|
|
@@ -661,25 +705,28 @@ class TemplateReferencePotential(FlatBottomPotential, ReferencePotential):
|
|
| 661 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 662 |
del parameters
|
| 663 |
if "template_mask_cb" not in feats or "template_force" not in feats:
|
|
|
|
| 664 |
return torch.empty((1, 0)), None, None, None, None
|
| 665 |
-
|
|
|
|
| 666 |
if template_mask.shape[0] == 0:
|
|
|
|
| 667 |
return torch.empty((1, 0)), None, None, None, None
|
| 668 |
|
| 669 |
-
ref_coords = feats["template_cb"][feats["template_force"]].clone()
|
| 670 |
-
ref_mask = feats["template_mask_cb"][feats["template_force"]].clone()
|
| 671 |
atom_indices = torch.arange(
|
| 672 |
feats["atom_pad_mask"].shape[1],
|
| 673 |
device=feats["atom_pad_mask"].device,
|
| 674 |
dtype=torch.float32,
|
| 675 |
-
)[None, :, None]
|
| 676 |
ref_atom_index = (
|
| 677 |
torch.bmm(
|
| 678 |
feats["token_to_rep_atom"].float(),
|
| 679 |
atom_indices,
|
| 680 |
)
|
| 681 |
.squeeze(-1)
|
| 682 |
-
.long()[0]
|
| 683 |
)
|
| 684 |
ref_token_index = (
|
| 685 |
torch.bmm(
|
|
@@ -687,24 +734,25 @@ class TemplateReferencePotential(FlatBottomPotential, ReferencePotential):
|
|
| 687 |
feats["token_index"].unsqueeze(-1).float(),
|
| 688 |
)
|
| 689 |
.squeeze(-1)
|
| 690 |
-
.long()[0]
|
| 691 |
)
|
| 692 |
|
| 693 |
index = torch.arange(
|
| 694 |
template_mask.shape[-1],
|
| 695 |
dtype=torch.long,
|
| 696 |
device=template_mask.device,
|
| 697 |
-
)[None]
|
| 698 |
upper = torch.full(
|
| 699 |
template_mask.shape,
|
| 700 |
float("inf"),
|
| 701 |
device=index.device,
|
| 702 |
dtype=torch.float32,
|
| 703 |
-
)
|
| 704 |
-
reference_indices = torch.argwhere(template_mask).T
|
| 705 |
upper[reference_indices.unbind()] = feats["template_force_threshold"][
|
| 706 |
feats["template_force"]
|
| 707 |
-
][reference_indices[0]]
|
|
|
|
| 708 |
return (
|
| 709 |
index,
|
| 710 |
(torch.ones_like(upper), None, upper),
|
|
@@ -719,8 +767,10 @@ class ContactPotentital(FlatBottomPotential, DistancePotential):
|
|
| 719 |
|
| 720 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 721 |
del parameters
|
| 722 |
-
index
|
| 723 |
-
|
|
|
|
|
|
|
| 724 |
return (
|
| 725 |
index,
|
| 726 |
(torch.ones_like(upper), None, upper),
|
|
|
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
+
import torch
|
| 13 |
from abc import ABC, abstractmethod
|
| 14 |
from dataclasses import dataclass
|
| 15 |
from typing import Any
|
| 16 |
|
|
|
|
|
|
|
| 17 |
from . import vb_const as const
|
| 18 |
from .vb_loss_diffusionv2 import weighted_rigid_align
|
| 19 |
from .vb_potentials_schedules import (
|
|
|
|
| 22 |
PiecewiseStepFunction,
|
| 23 |
)
|
| 24 |
|
| 25 |
+
|
| 26 |
Parameter = ParameterSchedule | float | int | bool
|
| 27 |
ParameterMap = dict[str, Parameter]
|
| 28 |
|
| 29 |
|
| 30 |
@dataclass(frozen=True, slots=True)
|
| 31 |
class _PreparedPotential:
|
| 32 |
+
coordinates: torch.Tensor # (..., a, 3)
|
| 33 |
+
index: torch.Tensor # (q, n)
|
| 34 |
function_args: tuple[Any, ...]
|
| 35 |
+
com_index: torch.Tensor | None # (a_input,)
|
| 36 |
+
atom_pad_mask: torch.Tensor | None # (a_input,)
|
| 37 |
+
ref_coords: torch.Tensor | None # (n_ref, n, 3)
|
| 38 |
+
ref_mask: torch.Tensor | None # (n_ref, n)
|
| 39 |
+
ref_token_index: torch.Tensor | None # (a_input,)
|
| 40 |
+
negation_mask: torch.Tensor | None # (n,)
|
| 41 |
+
union_index: torch.Tensor | None # (n,)
|
| 42 |
|
| 43 |
|
| 44 |
def _reduce_atoms_to_centers(
|
|
|
|
| 46 |
com_index: torch.Tensor,
|
| 47 |
atom_pad_mask: torch.Tensor,
|
| 48 |
) -> torch.Tensor:
|
| 49 |
+
# coordinates: (..., a, 3); com_index/atom_pad_mask: (a,).
|
| 50 |
+
unpadded_index = com_index[atom_pad_mask] # (a_valid,)
|
| 51 |
+
unpadded_coordinates = coordinates[..., atom_pad_mask, :] # (..., a_valid, 3)
|
| 52 |
center_shape = (*unpadded_coordinates.shape[:-2], unpadded_index.max() + 1, 3)
|
| 53 |
return torch.zeros(center_shape, device=coordinates.device).scatter_reduce(
|
| 54 |
-2,
|
| 55 |
unpadded_index.unsqueeze(-1).expand_as(unpadded_coordinates),
|
| 56 |
unpadded_coordinates,
|
| 57 |
"mean",
|
| 58 |
+
) # (..., n_center, 3)
|
| 59 |
|
| 60 |
|
| 61 |
def _union_weights(
|
|
|
|
| 63 |
union_index: torch.Tensor,
|
| 64 |
union_lambda: float,
|
| 65 |
) -> torch.Tensor:
|
| 66 |
+
# energy: (..., n); union_index: (n,).
|
| 67 |
+
unnormalized = torch.exp(-union_lambda * energy) # (..., n)
|
| 68 |
partition = torch.zeros(
|
| 69 |
(*energy.shape[:-1], union_index.max() + 1),
|
| 70 |
device=union_index.device,
|
|
|
|
| 73 |
union_index.expand_as(unnormalized),
|
| 74 |
unnormalized,
|
| 75 |
"sum",
|
| 76 |
+
) # (..., n_union)
|
| 77 |
+
weights = unnormalized / partition[..., union_index] # (..., n)
|
| 78 |
+
weights[partition[..., union_index] == 0] = 0 # (..., n)
|
| 79 |
+
return weights # (..., n)
|
| 80 |
|
| 81 |
|
| 82 |
def _scatter_constraint_gradients(
|
|
|
|
| 85 |
index: torch.Tensor,
|
| 86 |
coordinates: torch.Tensor,
|
| 87 |
) -> torch.Tensor:
|
| 88 |
+
# coefficients: (..., n); variable_gradient: (..., q, n, 3).
|
| 89 |
+
# index: (q, n); coordinates: (b, a, 3).
|
| 90 |
product = coefficients.tile(variable_gradient.shape[-3]).unsqueeze(
|
| 91 |
-1
|
| 92 |
+
) * variable_gradient.flatten(start_dim=-3, end_dim=-2) # (..., q * n, 3)
|
| 93 |
if product.dim() > 3:
|
| 94 |
+
product = product.sum(dim=list(range(1, product.dim() - 2))) # (b, q * n, 3)
|
| 95 |
scatter_index = (
|
| 96 |
index.flatten(start_dim=0, end_dim=1).unsqueeze(-1).expand((*coordinates.shape[:-2], -1, 3))
|
| 97 |
+
) # (b, q * n, 3)
|
| 98 |
return torch.zeros_like(coordinates).scatter_reduce(
|
| 99 |
-2,
|
| 100 |
scatter_index,
|
| 101 |
product,
|
| 102 |
"sum",
|
| 103 |
+
) # (b, a, 3)
|
| 104 |
|
| 105 |
|
| 106 |
class Potential(ABC):
|
|
|
|
| 116 |
parameters: dict[str, Any],
|
| 117 |
computed_args: tuple[Any, ...] | None = None,
|
| 118 |
) -> _PreparedPotential:
|
| 119 |
+
# coordinates: (..., a_input, 3).
|
| 120 |
if computed_args is None:
|
| 121 |
computed_args = self.compute_args(feats, parameters)
|
| 122 |
+
index, args, com_args, ref_args, operator_args = computed_args # index: (q, n)
|
| 123 |
com_index = atom_pad_mask = None
|
| 124 |
if com_args is not None:
|
| 125 |
+
com_index, atom_pad_mask = com_args # each (a_input,)
|
| 126 |
coordinates = _reduce_atoms_to_centers(
|
| 127 |
coordinates,
|
| 128 |
com_index,
|
| 129 |
atom_pad_mask,
|
| 130 |
+
) # (..., n_center, 3)
|
| 131 |
|
| 132 |
ref_coords = ref_mask = ref_token_index = None
|
| 133 |
if ref_args is not None:
|
| 134 |
+
# ref_coords: (n_ref, n, 3); ref_mask: (n_ref, n).
|
| 135 |
+
# ref_atom_index: (n,); ref_token_index: (a_input,).
|
| 136 |
ref_coords, ref_mask, ref_atom_index, ref_token_index = ref_args
|
| 137 |
+
coordinates = coordinates[..., ref_atom_index, :] # (..., n, 3)
|
| 138 |
|
| 139 |
negation_mask = union_index = None
|
| 140 |
if operator_args is not None:
|
| 141 |
+
negation_mask, union_index = operator_args # each (n,)
|
| 142 |
return _PreparedPotential(
|
| 143 |
coordinates=coordinates,
|
| 144 |
index=index,
|
|
|
|
| 160 |
) -> torch.Tensor:
|
| 161 |
"""Evaluate one energy per coordinate sample."""
|
| 162 |
|
| 163 |
+
# coords: (..., a, 3).
|
| 164 |
computed_args = self.compute_args(feats, parameters)
|
| 165 |
if computed_args[0].shape[1] == 0:
|
| 166 |
+
return torch.zeros(coords.shape[:-2], device=coords.device) # (...)
|
| 167 |
prepared = self._prepare(coords, feats, parameters, computed_args)
|
| 168 |
value = self.compute_variable(
|
| 169 |
prepared.coordinates,
|
|
|
|
| 171 |
ref_coords=prepared.ref_coords,
|
| 172 |
ref_mask=prepared.ref_mask,
|
| 173 |
compute_gradient=False,
|
| 174 |
+
) # (..., n)
|
| 175 |
energy = self.compute_function(
|
| 176 |
value,
|
| 177 |
*prepared.function_args,
|
| 178 |
negation_mask=prepared.negation_mask,
|
| 179 |
compute_derivative=False,
|
| 180 |
+
) # (..., n)
|
| 181 |
if prepared.union_index is not None:
|
| 182 |
weights = _union_weights(
|
| 183 |
energy,
|
| 184 |
prepared.union_index,
|
| 185 |
parameters["union_lambda"],
|
| 186 |
+
) # (..., n)
|
| 187 |
+
return (energy * weights).sum(dim=-1) # (...)
|
| 188 |
+
return energy.sum(dim=tuple(range(1, energy.dim()))) # (b,)
|
| 189 |
|
| 190 |
def compute_gradient(
|
| 191 |
self,
|
|
|
|
| 195 |
) -> torch.Tensor:
|
| 196 |
"""Return the analytic coordinate gradient of the potential energy."""
|
| 197 |
|
| 198 |
+
# coords: (b, a, 3).
|
| 199 |
computed_args = self.compute_args(feats, parameters)
|
| 200 |
if computed_args[0].shape[1] == 0:
|
| 201 |
+
return torch.zeros_like(coords) # (b, a, 3)
|
| 202 |
prepared = self._prepare(coords, feats, parameters, computed_args)
|
| 203 |
value, variable_gradient = self.compute_variable(
|
| 204 |
prepared.coordinates,
|
|
|
|
| 206 |
ref_coords=prepared.ref_coords,
|
| 207 |
ref_mask=prepared.ref_mask,
|
| 208 |
compute_gradient=True,
|
| 209 |
+
) # value: (..., n); variable_gradient: (..., q, n, 3)
|
| 210 |
energy, energy_derivative = self.compute_function(
|
| 211 |
value,
|
| 212 |
*prepared.function_args,
|
| 213 |
negation_mask=prepared.negation_mask,
|
| 214 |
compute_derivative=True,
|
| 215 |
+
) # each (..., n)
|
| 216 |
if prepared.union_index is not None:
|
| 217 |
weights = _union_weights(
|
| 218 |
energy,
|
| 219 |
prepared.union_index,
|
| 220 |
parameters["union_lambda"],
|
| 221 |
+
) # (..., n)
|
| 222 |
union_energy = torch.zeros(
|
| 223 |
(*energy.shape[:-1], prepared.union_index.max() + 1),
|
| 224 |
device=prepared.union_index.device,
|
|
|
|
| 227 |
prepared.union_index.expand_as(energy),
|
| 228 |
energy * weights,
|
| 229 |
"sum",
|
| 230 |
+
) # (..., n_union)
|
| 231 |
coefficients = (
|
| 232 |
energy_derivative
|
| 233 |
* weights
|
|
|
|
| 236 |
+ parameters["union_lambda"]
|
| 237 |
* (energy - union_energy[..., prepared.union_index])
|
| 238 |
)
|
| 239 |
+
) # (..., n)
|
| 240 |
else:
|
| 241 |
+
coefficients = energy_derivative # (..., n)
|
| 242 |
|
| 243 |
atom_gradient = _scatter_constraint_gradients(
|
| 244 |
coefficients,
|
| 245 |
variable_gradient,
|
| 246 |
prepared.index,
|
| 247 |
prepared.coordinates,
|
| 248 |
+
) # (b, a_prepared, 3)
|
| 249 |
if prepared.com_index is not None:
|
| 250 |
+
atom_gradient = atom_gradient[..., prepared.com_index, :] # (b, a, 3)
|
| 251 |
elif prepared.ref_token_index is not None:
|
| 252 |
+
atom_gradient = atom_gradient[..., prepared.ref_token_index, :] # (b, a, 3)
|
| 253 |
+
return atom_gradient # (b, a, 3)
|
| 254 |
|
| 255 |
def compute_parameters(self, t: float) -> dict[str, Any] | None:
|
| 256 |
"""Resolve scheduled parameters at diffusion time ``t``."""
|
|
|
|
| 270 |
negation_mask: torch.Tensor | None = None,
|
| 271 |
compute_derivative: bool = False,
|
| 272 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 273 |
+
# value: (..., n); negation_mask: (n,).
|
| 274 |
raise NotImplementedError
|
| 275 |
|
| 276 |
@abstractmethod
|
|
|
|
| 282 |
ref_mask: torch.Tensor | None = None,
|
| 283 |
compute_gradient: bool = False,
|
| 284 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 285 |
+
# coords: (..., a, 3); index: (q, n).
|
| 286 |
raise NotImplementedError
|
| 287 |
|
| 288 |
@abstractmethod
|
|
|
|
| 314 |
negation_mask: torch.Tensor | None = None,
|
| 315 |
compute_derivative: bool = False,
|
| 316 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 317 |
+
# value: (..., n); k/lower_bounds/upper_bounds: (n,) or (..., n).
|
| 318 |
+
# negation_mask: (n,) or a shape broadcastable to (..., n).
|
| 319 |
lower = (
|
| 320 |
(torch.full_like(value, float("-inf")) if lower_bounds is None else lower_bounds)
|
| 321 |
.expand_as(value)
|
| 322 |
.clone()
|
| 323 |
+
) # (..., n)
|
| 324 |
upper = (
|
| 325 |
(torch.full_like(value, float("inf")) if upper_bounds is None else upper_bounds)
|
| 326 |
.expand_as(value)
|
| 327 |
.clone()
|
| 328 |
+
) # (..., n)
|
| 329 |
|
| 330 |
if negation_mask is not None:
|
| 331 |
if not torch.is_tensor(negation_mask):
|
|
|
|
| 342 |
f"got {negation_mask.device} and {value.device}."
|
| 343 |
)
|
| 344 |
try:
|
| 345 |
+
expanded_negation_mask = negation_mask.expand_as(value) # (..., n)
|
| 346 |
except RuntimeError as error:
|
| 347 |
raise ValueError(
|
| 348 |
"negation_mask must be broadcastable to value shape "
|
| 349 |
f"{tuple(value.shape)}, got {tuple(negation_mask.shape)}."
|
| 350 |
) from error
|
| 351 |
+
unbounded_below = torch.isneginf(lower) # (..., n)
|
| 352 |
+
unbounded_above = torch.isposinf(upper) # (..., n)
|
| 353 |
+
# (..., n)
|
| 354 |
valid_negation = unbounded_below | unbounded_above | expanded_negation_mask
|
| 355 |
if not bool(torch.all(valid_negation).item()):
|
| 356 |
raise ValueError(
|
| 357 |
"negation_mask may be false only where at least one bound is infinite."
|
| 358 |
)
|
| 359 |
+
select_upper = ~unbounded_above & ~expanded_negation_mask # (..., n)
|
| 360 |
+
lower[select_upper] = upper[select_upper] # (..., n)
|
| 361 |
+
upper[select_upper] = float("inf") # (..., n)
|
| 362 |
+
select_lower = ~unbounded_below & ~expanded_negation_mask # (..., n)
|
| 363 |
+
upper[select_lower] = lower[select_lower] # (..., n)
|
| 364 |
+
lower[select_lower] = float("-inf") # (..., n)
|
| 365 |
+
|
| 366 |
+
below = value < lower # (..., n)
|
| 367 |
+
above = value > upper # (..., n)
|
| 368 |
+
energy = torch.zeros_like(value) # (..., n)
|
| 369 |
+
energy[below] = (k * (lower - value))[below] # (..., n)
|
| 370 |
+
energy[above] = (k * (value - upper))[above] # (..., n)
|
| 371 |
if not compute_derivative:
|
| 372 |
+
return energy # (..., n)
|
| 373 |
+
derivative = torch.zeros_like(value) # (..., n)
|
| 374 |
+
derivative[below] = -k.expand_as(below)[below] # (..., n)
|
| 375 |
+
derivative[above] = k.expand_as(above)[above] # (..., n)
|
| 376 |
+
return energy, derivative # each (..., n)
|
| 377 |
|
| 378 |
|
| 379 |
class ReferencePotential(Potential):
|
|
|
|
| 387 |
ref_mask: torch.Tensor,
|
| 388 |
compute_gradient: bool = False,
|
| 389 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 390 |
+
# coords: (b, a, 3); index: (1, n).
|
| 391 |
+
# ref_coords: (n_ref, n, 3); ref_mask: (n_ref, n).
|
| 392 |
aligned_reference = weighted_rigid_align(
|
| 393 |
ref_coords.float(),
|
| 394 |
coords[:, index].float(),
|
| 395 |
ref_mask,
|
| 396 |
ref_mask,
|
| 397 |
+
) # (b, n_ref, n, 3)
|
| 398 |
+
displacement = coords[:, index] - aligned_reference # (b, n_ref, n, 3)
|
| 399 |
+
distance = torch.linalg.norm(displacement, dim=-1) # (b, n_ref, n)
|
| 400 |
if not compute_gradient:
|
| 401 |
+
return distance # (b, n_ref, n)
|
| 402 |
+
unit_displacement = displacement / distance.unsqueeze(-1) # (b, n_ref, n, 3)
|
| 403 |
+
# (b, 1, n_ref, n, 3)
|
| 404 |
gradient = (unit_displacement * ref_mask.unsqueeze(-1)).unsqueeze(1)
|
| 405 |
+
return distance, gradient # (b, n_ref, n); (b, 1, n_ref, n, 3)
|
| 406 |
|
| 407 |
|
| 408 |
class DistancePotential(Potential):
|
|
|
|
| 416 |
ref_mask: torch.Tensor | None = None,
|
| 417 |
compute_gradient: bool = False,
|
| 418 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 419 |
+
# coords: (..., a, 3); index: (2, n).
|
| 420 |
del ref_coords, ref_mask
|
| 421 |
displacement = coords.index_select(-2, index[0]) - coords.index_select(
|
| 422 |
-2,
|
| 423 |
index[1],
|
| 424 |
+
) # (..., n, 3)
|
| 425 |
+
distance = torch.linalg.norm(displacement, dim=-1) # (..., n)
|
| 426 |
if not compute_gradient:
|
| 427 |
+
return distance # (..., n)
|
| 428 |
+
unit_displacement = displacement / distance.unsqueeze(-1) # (..., n, 3)
|
| 429 |
+
# Returns (..., n); (..., 2, n, 3).
|
| 430 |
return distance, torch.stack((unit_displacement, -unit_displacement), dim=1)
|
| 431 |
|
| 432 |
|
|
|
|
| 441 |
ref_mask: torch.Tensor | None = None,
|
| 442 |
compute_gradient: bool = False,
|
| 443 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 444 |
+
# coords: (..., a, 3); index: (4, n).
|
| 445 |
del ref_coords, ref_mask
|
| 446 |
+
# Each displacement has shape (..., n, 3).
|
| 447 |
r_ij = coords.index_select(-2, index[0]) - coords.index_select(-2, index[1])
|
| 448 |
r_kj = coords.index_select(-2, index[2]) - coords.index_select(-2, index[1])
|
| 449 |
r_kl = coords.index_select(-2, index[2]) - coords.index_select(-2, index[3])
|
| 450 |
+
n_ijk = torch.cross(r_ij, r_kj, dim=-1) # (..., n, 3)
|
| 451 |
+
n_jkl = torch.cross(r_kj, r_kl, dim=-1) # (..., n, 3)
|
| 452 |
+
r_kj_norm = torch.linalg.norm(r_kj, dim=-1) # (..., n)
|
| 453 |
+
n_ijk_norm = torch.linalg.norm(n_ijk, dim=-1) # (..., n)
|
| 454 |
+
n_jkl_norm = torch.linalg.norm(n_jkl, dim=-1) # (..., n)
|
| 455 |
|
| 456 |
orientation = torch.sign(
|
| 457 |
r_kj.unsqueeze(-2) @ torch.cross(n_ijk, n_jkl, dim=-1).unsqueeze(-1)
|
| 458 |
+
).squeeze(-1, -2) # (..., n)
|
| 459 |
cosine = (n_ijk.unsqueeze(-2) @ n_jkl.unsqueeze(-1)).squeeze(-1, -2) / (
|
| 460 |
n_ijk_norm * n_jkl_norm
|
| 461 |
+
) # (..., n)
|
| 462 |
+
# (..., n)
|
| 463 |
angle = orientation * torch.arccos(torch.clamp(cosine, -1 + 1e-8, 1 - 1e-8))
|
| 464 |
if not compute_gradient:
|
| 465 |
+
return angle # (..., n)
|
| 466 |
|
| 467 |
projection_i = (
|
| 468 |
(r_ij.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
|
| 469 |
+
).unsqueeze(-1) # (..., n, 1)
|
| 470 |
projection_l = (
|
| 471 |
(r_kl.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
|
| 472 |
+
).unsqueeze(-1) # (..., n, 1)
|
| 473 |
+
grad_i = n_ijk * (r_kj_norm / n_ijk_norm**2).unsqueeze(-1) # (..., n, 3)
|
| 474 |
+
grad_l = -n_jkl * (r_kj_norm / n_jkl_norm**2).unsqueeze(-1) # (..., n, 3)
|
| 475 |
+
grad_j = (projection_i - 1) * grad_i - projection_l * grad_l # (..., n, 3)
|
| 476 |
+
grad_k = (projection_l - 1) * grad_l - projection_i * grad_i # (..., n, 3)
|
| 477 |
+
# Returns (..., n); (..., 4, n, 3).
|
| 478 |
return angle, torch.stack((grad_i, grad_j, grad_k, grad_l), dim=1)
|
| 479 |
|
| 480 |
|
|
|
|
| 489 |
ref_mask: torch.Tensor | None = None,
|
| 490 |
compute_gradient: bool = False,
|
| 491 |
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 492 |
+
# coords: (..., a, 3); index: (4, n).
|
| 493 |
del ref_coords, ref_mask
|
| 494 |
if not compute_gradient:
|
| 495 |
+
return torch.abs(super().compute_variable(coords, index)) # (..., n)
|
| 496 |
angle, gradient = super().compute_variable(
|
| 497 |
coords,
|
| 498 |
index,
|
| 499 |
compute_gradient=True,
|
| 500 |
+
) # (..., n); (..., 4, n, 3)
|
| 501 |
+
gradient[(angle < 0)[..., None, :, None].expand_as(gradient)] *= -1 # same
|
| 502 |
+
return torch.abs(angle), gradient # (..., n); (..., 4, n, 3)
|
| 503 |
|
| 504 |
|
| 505 |
def _element_radii(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 506 |
+
# feats["ref_element"]: (1, a, n_element).
|
| 507 |
element_radii = torch.zeros(
|
| 508 |
const.num_elements,
|
| 509 |
dtype=torch.float32,
|
| 510 |
device=feats["ref_element"].device,
|
| 511 |
+
) # (n_element,)
|
| 512 |
element_radii[1:119] = torch.tensor(
|
| 513 |
const.vdw_radii,
|
| 514 |
dtype=torch.float32,
|
| 515 |
device=element_radii.device,
|
| 516 |
+
) # (n_element,)
|
| 517 |
+
# (a,)
|
| 518 |
return (feats["ref_element"].float() @ element_radii.unsqueeze(-1)).squeeze(-1)[0]
|
| 519 |
|
| 520 |
|
| 521 |
def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 522 |
+
# atom_to_token: (1, a, l); asym_id: (1, l).
|
| 523 |
return (
|
| 524 |
torch.bmm(
|
| 525 |
feats["atom_to_token"].float(),
|
|
|
|
| 527 |
)
|
| 528 |
.squeeze(-1)
|
| 529 |
.long()
|
| 530 |
+
)[0] # (a,)
|
| 531 |
|
| 532 |
|
| 533 |
class PoseBustersPotential(FlatBottomPotential, DistancePotential):
|
| 534 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 535 |
+
# rdkit_bounds_index: (1, 2, n); bound and mask features: (1, n).
|
| 536 |
+
pair_index = feats["rdkit_bounds_index"][0] # (2, n)
|
| 537 |
+
lower = feats["rdkit_lower_bounds"][0].clone() # (n,)
|
| 538 |
+
upper = feats["rdkit_upper_bounds"][0].clone() # (n,)
|
| 539 |
+
bond = feats["rdkit_bounds_bond_mask"][0] # (n,)
|
| 540 |
+
angle = feats["rdkit_bounds_angle_mask"][0] # (n,)
|
| 541 |
+
lower[bond * ~angle] *= 1.0 - parameters["bond_buffer"] # (n,)
|
| 542 |
+
upper[bond * ~angle] *= 1.0 + parameters["bond_buffer"] # (n,)
|
| 543 |
+
lower[~bond * angle] *= 1.0 - parameters["angle_buffer"] # (n,)
|
| 544 |
+
upper[~bond * angle] *= 1.0 + parameters["angle_buffer"] # (n,)
|
| 545 |
shared_buffer = min(parameters["bond_buffer"], parameters["angle_buffer"])
|
| 546 |
+
lower[bond * angle] *= 1.0 - shared_buffer # (n,)
|
| 547 |
+
upper[bond * angle] *= 1.0 + shared_buffer # (n,)
|
| 548 |
+
lower[~bond * ~angle] *= 1.0 - parameters["clash_buffer"] # (n,)
|
| 549 |
+
upper[~bond * ~angle] = float("inf") # (n,)
|
| 550 |
+
|
| 551 |
+
atom_radii = _element_radii(feats) # (a,)
|
| 552 |
+
bond_cutoff = 0.35 + atom_radii[pair_index].mean(dim=0) # (n,)
|
| 553 |
+
lower[~bond] = torch.max(lower[~bond], bond_cutoff[~bond]) # (n,)
|
| 554 |
+
upper[bond] = torch.min(upper[bond], bond_cutoff[bond]) # (n,)
|
| 555 |
+
# Returns index (2, n) and three function arguments of shape (n,).
|
| 556 |
return pair_index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 557 |
|
| 558 |
|
| 559 |
class ConnectionsPotential(FlatBottomPotential, DistancePotential):
|
| 560 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 561 |
+
# connected_atom_index: (1, 2, n).
|
| 562 |
+
pair_index = feats["connected_atom_index"][0] # (2, n)
|
| 563 |
upper = torch.full(
|
| 564 |
(pair_index.shape[1],),
|
| 565 |
parameters["buffer"],
|
| 566 |
device=pair_index.device,
|
| 567 |
+
) # (n,)
|
| 568 |
+
# Returns index (2, n), k (n,), and upper bound (n,).
|
| 569 |
return pair_index, (torch.ones_like(upper), None, upper), None, None, None
|
| 570 |
|
| 571 |
|
| 572 |
class VDWOverlapPotential(FlatBottomPotential, DistancePotential):
|
| 573 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 574 |
+
atom_chain_id = _atom_chain_ids(feats) # (a,)
|
| 575 |
+
atom_pad_mask = feats["atom_pad_mask"][0].bool() # (a,)
|
| 576 |
+
chain_sizes = torch.bincount(atom_chain_id[atom_pad_mask]) # (n_chain,)
|
| 577 |
+
nonion_atom = (chain_sizes > 1)[atom_chain_id] # (a,)
|
| 578 |
+
atom_radii = _element_radii(feats) # (a,)
|
| 579 |
pair_index = torch.triu_indices(
|
| 580 |
atom_chain_id.shape[0],
|
| 581 |
atom_chain_id.shape[0],
|
| 582 |
1,
|
| 583 |
device=atom_chain_id.device,
|
| 584 |
+
) # (2, n_pair)
|
| 585 |
+
pair_pad_mask = atom_pad_mask[pair_index].all(dim=0) # (n_pair,)
|
| 586 |
+
# (n_pair,)
|
| 587 |
pair_ion_mask = nonion_atom[pair_index[0]] * nonion_atom[pair_index[1]]
|
| 588 |
|
| 589 |
+
num_chains = atom_chain_id.max() + 1 # ()
|
| 590 |
+
connected = feats["connected_chain_index"][0] # (2, n_connection)
|
| 591 |
connected_matrix = torch.eye(
|
| 592 |
num_chains,
|
| 593 |
device=atom_chain_id.device,
|
| 594 |
dtype=torch.bool,
|
| 595 |
+
) # (n_chain, n_chain)
|
| 596 |
+
connected_matrix[connected[0], connected[1]] = True # (n_chain, n_chain)
|
| 597 |
+
connected_matrix[connected[1], connected[0]] = True # (n_chain, n_chain)
|
| 598 |
connected_pair = connected_matrix[
|
| 599 |
atom_chain_id[pair_index[0]],
|
| 600 |
atom_chain_id[pair_index[1]],
|
| 601 |
+
] # (n_pair,)
|
| 602 |
+
# (2, n)
|
| 603 |
pair_index = pair_index[:, pair_pad_mask * pair_ion_mask * ~connected_pair]
|
| 604 |
+
# (n,)
|
| 605 |
lower = atom_radii[pair_index].sum(dim=0) * (1.0 - parameters["buffer"])
|
| 606 |
+
# Returns index (2, n), k (n,), and lower bound (n,).
|
| 607 |
return pair_index, (torch.ones_like(lower), lower, None), None, None, None
|
| 608 |
|
| 609 |
|
| 610 |
class SymmetricChainCOMPotential(FlatBottomPotential, DistancePotential):
|
| 611 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 612 |
+
atom_chain_id = _atom_chain_ids(feats) # (a,)
|
| 613 |
+
atom_pad_mask = feats["atom_pad_mask"][0].bool() # (a,)
|
| 614 |
+
nonion_chain = torch.bincount(atom_chain_id[atom_pad_mask]) > 1 # (n_chain,)
|
| 615 |
+
pair_index = feats["symmetric_chain_index"][0] # (2, n_candidate)
|
| 616 |
pair_index = pair_index[
|
| 617 |
:,
|
| 618 |
nonion_chain[pair_index[0]] * nonion_chain[pair_index[1]],
|
| 619 |
+
] # (2, n)
|
| 620 |
lower = torch.full(
|
| 621 |
(pair_index.shape[1],),
|
| 622 |
parameters["buffer"],
|
| 623 |
dtype=torch.float32,
|
| 624 |
device=pair_index.device,
|
| 625 |
+
) # (n,)
|
| 626 |
+
# Returns center index (2, n), k/lower (n,), and atom-to-center maps (a,).
|
| 627 |
return (
|
| 628 |
pair_index,
|
| 629 |
(torch.ones_like(lower), lower, None),
|
|
|
|
| 638 |
positive_lower: float,
|
| 639 |
negative_upper: float,
|
| 640 |
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 641 |
+
# orientations: (n,).
|
| 642 |
+
lower = torch.zeros(orientations.shape, device=orientations.device) # (n,)
|
| 643 |
+
upper = torch.zeros(orientations.shape, device=orientations.device) # (n,)
|
| 644 |
+
lower[orientations] = positive_lower # (n,)
|
| 645 |
+
upper[orientations] = float("inf") # (n,)
|
| 646 |
+
lower[~orientations] = float("-inf") # (n,)
|
| 647 |
+
upper[~orientations] = negative_upper # (n,)
|
| 648 |
+
return lower, upper # each (n,)
|
| 649 |
|
| 650 |
|
| 651 |
class StereoBondPotential(FlatBottomPotential, AbsDihedralPotential):
|
| 652 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 653 |
+
# stereo_bond_index: (1, 4, n); orientations: (1, n).
|
| 654 |
+
index = feats["stereo_bond_index"][0] # (4, n)
|
| 655 |
+
orientation = feats["stereo_bond_orientations"][0].bool() # (n,)
|
| 656 |
lower, upper = _oriented_bounds(
|
| 657 |
orientation,
|
| 658 |
torch.pi - parameters["buffer"],
|
| 659 |
parameters["buffer"],
|
| 660 |
+
) # each (n,)
|
| 661 |
+
# Returns index (4, n) and three function arguments of shape (n,).
|
| 662 |
return index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 663 |
|
| 664 |
|
| 665 |
class ChiralAtomPotential(FlatBottomPotential, DihedralPotential):
|
| 666 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 667 |
+
# chiral_atom_index: (1, 4, n); orientations: (1, n).
|
| 668 |
+
index = feats["chiral_atom_index"][0] # (4, n)
|
| 669 |
+
orientation = feats["chiral_atom_orientations"][0].bool() # (n,)
|
| 670 |
lower, upper = _oriented_bounds(
|
| 671 |
orientation,
|
| 672 |
parameters["buffer"],
|
| 673 |
-parameters["buffer"],
|
| 674 |
+
) # each (n,)
|
| 675 |
+
# Returns index (4, n) and three function arguments of shape (n,).
|
| 676 |
return index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 677 |
|
| 678 |
|
| 679 |
class PlanarBondPotential(FlatBottomPotential, AbsDihedralPotential):
|
| 680 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 681 |
+
# The feature stores two atom-index rows for each six-entry bond pattern.
|
| 682 |
+
bond_index = feats["planar_bond_index"][0].T # (2, n_bond_entry)
|
| 683 |
improper_pattern = torch.tensor(
|
| 684 |
[[1, 2, 3, 0], [4, 5, 0, 3]],
|
| 685 |
device=bond_index.device,
|
| 686 |
+
).T # (4, 2)
|
| 687 |
+
# (4, n_improper)
|
| 688 |
improper_index = bond_index[:, improper_pattern].swapaxes(0, 1).flatten(start_dim=1)
|
| 689 |
upper = torch.full(
|
| 690 |
(improper_index.shape[1],),
|
| 691 |
parameters["buffer"],
|
| 692 |
device=improper_index.device,
|
| 693 |
+
) # (n_improper,)
|
| 694 |
+
# Returns index (4, n_improper), k (n_improper,), and upper (n_improper,).
|
| 695 |
return (
|
| 696 |
improper_index,
|
| 697 |
(torch.ones_like(upper), None, upper),
|
|
|
|
| 705 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 706 |
del parameters
|
| 707 |
if "template_mask_cb" not in feats or "template_force" not in feats:
|
| 708 |
+
# Empty sentinel index: (1, 0).
|
| 709 |
return torch.empty((1, 0)), None, None, None, None
|
| 710 |
+
# template_mask_cb/template_force: (b, n_template, n)/(b, n_template).
|
| 711 |
+
template_mask = feats["template_mask_cb"][feats["template_force"]] # (n_ref, n)
|
| 712 |
if template_mask.shape[0] == 0:
|
| 713 |
+
# Empty sentinel index: (1, 0).
|
| 714 |
return torch.empty((1, 0)), None, None, None, None
|
| 715 |
|
| 716 |
+
ref_coords = feats["template_cb"][feats["template_force"]].clone() # (n_ref, n, 3)
|
| 717 |
+
ref_mask = feats["template_mask_cb"][feats["template_force"]].clone() # (n_ref, n)
|
| 718 |
atom_indices = torch.arange(
|
| 719 |
feats["atom_pad_mask"].shape[1],
|
| 720 |
device=feats["atom_pad_mask"].device,
|
| 721 |
dtype=torch.float32,
|
| 722 |
+
)[None, :, None] # (1, a, 1)
|
| 723 |
ref_atom_index = (
|
| 724 |
torch.bmm(
|
| 725 |
feats["token_to_rep_atom"].float(),
|
| 726 |
atom_indices,
|
| 727 |
)
|
| 728 |
.squeeze(-1)
|
| 729 |
+
.long()[0] # (n,)
|
| 730 |
)
|
| 731 |
ref_token_index = (
|
| 732 |
torch.bmm(
|
|
|
|
| 734 |
feats["token_index"].unsqueeze(-1).float(),
|
| 735 |
)
|
| 736 |
.squeeze(-1)
|
| 737 |
+
.long()[0] # (a,)
|
| 738 |
)
|
| 739 |
|
| 740 |
index = torch.arange(
|
| 741 |
template_mask.shape[-1],
|
| 742 |
dtype=torch.long,
|
| 743 |
device=template_mask.device,
|
| 744 |
+
)[None] # (1, n)
|
| 745 |
upper = torch.full(
|
| 746 |
template_mask.shape,
|
| 747 |
float("inf"),
|
| 748 |
device=index.device,
|
| 749 |
dtype=torch.float32,
|
| 750 |
+
) # (n_ref, n)
|
| 751 |
+
reference_indices = torch.argwhere(template_mask).T # (2, n_active)
|
| 752 |
upper[reference_indices.unbind()] = feats["template_force_threshold"][
|
| 753 |
feats["template_force"]
|
| 754 |
+
][reference_indices[0]] # (n_ref, n)
|
| 755 |
+
# Returns index (1, n), function args (n_ref, n), and reference mappings.
|
| 756 |
return (
|
| 757 |
index,
|
| 758 |
(torch.ones_like(upper), None, upper),
|
|
|
|
| 767 |
|
| 768 |
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
|
| 769 |
del parameters
|
| 770 |
+
# pair index: (1, 2, n); threshold/operator features: (1, n).
|
| 771 |
+
index = feats["contact_pair_index"][0] # (2, n)
|
| 772 |
+
upper = feats["contact_thresholds"][0].clone() # (n,)
|
| 773 |
+
# Returns index (2, n), k/upper (n,), and two operator tensors (n,).
|
| 774 |
return (
|
| 775 |
index,
|
| 776 |
(torch.ones_like(upper), None, upper),
|
fastplms/models/boltz/vb_tri_attn_attention.py
CHANGED
|
@@ -2,9 +2,8 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
from typing import cast
|
| 6 |
-
|
| 7 |
import torch
|
|
|
|
| 8 |
from torch import Tensor, nn
|
| 9 |
|
| 10 |
from .vb_tri_attn_primitives import Attention, LayerNorm, Linear
|
|
@@ -44,6 +43,7 @@ class TriangleAttention(nn.Module):
|
|
| 44 |
) -> Tensor:
|
| 45 |
"""Evaluate attention in slices of the batch-like leading axes."""
|
| 46 |
|
|
|
|
| 47 |
def attention_call(**inputs: Tensor) -> Tensor:
|
| 48 |
return cast(Tensor, self.mha(**inputs, use_kernels=use_kernels))
|
| 49 |
|
|
@@ -53,7 +53,7 @@ class TriangleAttention(nn.Module):
|
|
| 53 |
"tri_bias": tri_bias,
|
| 54 |
"mask_bias": mask_bias,
|
| 55 |
"mask": mask,
|
| 56 |
-
}
|
| 57 |
return cast(
|
| 58 |
Tensor,
|
| 59 |
chunk_layer(
|
|
@@ -74,6 +74,7 @@ class TriangleAttention(nn.Module):
|
|
| 74 |
chunk_size: int | None,
|
| 75 |
use_kernels: bool,
|
| 76 |
) -> Tensor:
|
|
|
|
| 77 |
if chunk_size is not None and not use_kernels:
|
| 78 |
return cast(
|
| 79 |
Tensor,
|
|
@@ -107,17 +108,19 @@ class TriangleAttention(nn.Module):
|
|
| 107 |
) -> Tensor:
|
| 108 |
"""Transform X with shape ``(..., l, l, d)`` under mask M."""
|
| 109 |
|
| 110 |
-
pair_mask = x.new_ones(x.shape[:-1]) if mask is None else mask
|
| 111 |
-
pair_states = x
|
| 112 |
if not self.starting:
|
| 113 |
-
pair_states = pair_states.transpose(-2, -3)
|
| 114 |
-
pair_mask = pair_mask.transpose(-1, -2)
|
| 115 |
-
|
| 116 |
-
normalized = self.layer_norm(pair_states)
|
| 117 |
-
expanded_mask = pair_mask[..., :, None, None, :]
|
| 118 |
-
mask_bias = self.inf * (expanded_mask - 1)
|
| 119 |
-
triangle_bias = permute_final_dims(
|
| 120 |
-
|
|
|
|
|
|
|
| 121 |
output = self._run_attention(
|
| 122 |
normalized,
|
| 123 |
triangle_bias,
|
|
@@ -125,8 +128,10 @@ class TriangleAttention(nn.Module):
|
|
| 125 |
expanded_mask,
|
| 126 |
chunk_size,
|
| 127 |
use_kernels,
|
| 128 |
-
)
|
| 129 |
-
return
|
|
|
|
|
|
|
| 130 |
|
| 131 |
|
| 132 |
TriangleAttentionStartingNode = TriangleAttention
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
| 5 |
import torch
|
| 6 |
+
from typing import cast
|
| 7 |
from torch import Tensor, nn
|
| 8 |
|
| 9 |
from .vb_tri_attn_primitives import Attention, LayerNorm, Linear
|
|
|
|
| 43 |
) -> Tensor:
|
| 44 |
"""Evaluate attention in slices of the batch-like leading axes."""
|
| 45 |
|
| 46 |
+
# x: (..., l, l, d); biases/mask retain broadcastable triangular axes.
|
| 47 |
def attention_call(**inputs: Tensor) -> Tensor:
|
| 48 |
return cast(Tensor, self.mha(**inputs, use_kernels=use_kernels))
|
| 49 |
|
|
|
|
| 53 |
"tri_bias": tri_bias,
|
| 54 |
"mask_bias": mask_bias,
|
| 55 |
"mask": mask,
|
| 56 |
+
} # tensor values retain the input shapes above
|
| 57 |
return cast(
|
| 58 |
Tensor,
|
| 59 |
chunk_layer(
|
|
|
|
| 74 |
chunk_size: int | None,
|
| 75 |
use_kernels: bool,
|
| 76 |
) -> Tensor:
|
| 77 |
+
# states: (..., l, l, d); returned tensor has the same shape.
|
| 78 |
if chunk_size is not None and not use_kernels:
|
| 79 |
return cast(
|
| 80 |
Tensor,
|
|
|
|
| 108 |
) -> Tensor:
|
| 109 |
"""Transform X with shape ``(..., l, l, d)`` under mask M."""
|
| 110 |
|
| 111 |
+
pair_mask = x.new_ones(x.shape[:-1]) if mask is None else mask # (..., l, l)
|
| 112 |
+
pair_states = x # (..., l, l, d)
|
| 113 |
if not self.starting:
|
| 114 |
+
pair_states = pair_states.transpose(-2, -3) # (..., l, l, d)
|
| 115 |
+
pair_mask = pair_mask.transpose(-1, -2) # (..., l, l)
|
| 116 |
+
|
| 117 |
+
normalized = self.layer_norm(pair_states) # (..., l, l, d)
|
| 118 |
+
expanded_mask = pair_mask[..., :, None, None, :] # (..., l, 1, 1, l)
|
| 119 |
+
mask_bias = self.inf * (expanded_mask - 1) # (..., l, 1, 1, l)
|
| 120 |
+
triangle_bias = permute_final_dims(
|
| 121 |
+
self.linear(normalized), (2, 0, 1)
|
| 122 |
+
) # (..., h, l, l)
|
| 123 |
+
triangle_bias = triangle_bias.unsqueeze(-4) # (..., 1, h, l, l)
|
| 124 |
output = self._run_attention(
|
| 125 |
normalized,
|
| 126 |
triangle_bias,
|
|
|
|
| 128 |
expanded_mask,
|
| 129 |
chunk_size,
|
| 130 |
use_kernels,
|
| 131 |
+
) # (..., l, l, d)
|
| 132 |
+
return (
|
| 133 |
+
output if self.starting else output.transpose(-2, -3)
|
| 134 |
+
) # (..., l, l, d)
|
| 135 |
|
| 136 |
|
| 137 |
TriangleAttentionStartingNode = TriangleAttention
|
fastplms/models/boltz/vb_tri_attn_primitives.py
CHANGED
|
@@ -12,10 +12,9 @@ from __future__ import annotations
|
|
| 12 |
|
| 13 |
import importlib
|
| 14 |
import math
|
|
|
|
| 15 |
from collections.abc import Callable, Sequence
|
| 16 |
from importlib.util import find_spec
|
| 17 |
-
|
| 18 |
-
import torch
|
| 19 |
from torch import nn
|
| 20 |
|
| 21 |
from . import vb_layers_initialize as initialize
|
|
@@ -27,6 +26,7 @@ def _initialize_weight(
|
|
| 27 |
bias: torch.Tensor | None,
|
| 28 |
method: str,
|
| 29 |
) -> None:
|
|
|
|
| 30 |
initializers = {
|
| 31 |
"default": initialize.lecun_normal_init_,
|
| 32 |
"relu": initialize.he_normal_init_,
|
|
@@ -68,21 +68,28 @@ class Linear(nn.Linear):
|
|
| 68 |
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
| 69 |
"""Project ``input`` while preserving explicit precision policies."""
|
| 70 |
|
|
|
|
| 71 |
input_dtype = input.dtype
|
| 72 |
if self.precision is not None:
|
| 73 |
with torch.autocast("cuda", enabled=False):
|
| 74 |
-
bias =
|
|
|
|
|
|
|
| 75 |
projected = nn.functional.linear(
|
| 76 |
input.to(self.precision),
|
| 77 |
self.weight.to(self.precision),
|
| 78 |
bias,
|
| 79 |
-
)
|
| 80 |
-
return projected.to(input_dtype)
|
| 81 |
if input_dtype is torch.bfloat16:
|
| 82 |
with torch.autocast("cuda", enabled=False):
|
| 83 |
-
bias =
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
class LayerNorm(nn.Module):
|
|
@@ -92,10 +99,11 @@ class LayerNorm(nn.Module):
|
|
| 92 |
super().__init__()
|
| 93 |
self.c_in = (c_in,)
|
| 94 |
self.eps = eps
|
| 95 |
-
self.weight = nn.Parameter(torch.ones(c_in))
|
| 96 |
-
self.bias = nn.Parameter(torch.zeros(c_in))
|
| 97 |
|
| 98 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
|
|
| 99 |
if x.dtype is torch.bfloat16:
|
| 100 |
with torch.autocast("cuda", enabled=False):
|
| 101 |
return nn.functional.layer_norm(
|
|
@@ -126,11 +134,14 @@ def _attention(
|
|
| 126 |
) -> torch.Tensor:
|
| 127 |
"""Compute biased scaled attention after query scaling."""
|
| 128 |
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
| 130 |
for bias in biases:
|
| 131 |
-
scores += bias
|
| 132 |
-
probabilities = softmax_no_cast(scores, dim=-1)
|
| 133 |
-
return torch.matmul(probabilities, value)
|
| 134 |
|
| 135 |
|
| 136 |
@torch.compiler.disable
|
|
@@ -144,6 +155,7 @@ def kernel_triangular_attn(
|
|
| 144 |
) -> torch.Tensor:
|
| 145 |
"""Call the optional cuEquivariance triangle-attention primitive."""
|
| 146 |
|
|
|
|
| 147 |
if (
|
| 148 |
find_spec("cuequivariance_torch") is None
|
| 149 |
or find_spec("cuequivariance_ops_torch") is None
|
|
@@ -193,23 +205,32 @@ class Attention(nn.Module):
|
|
| 193 |
"""Project Q, K, and V to ``(..., h, n, d)`` tensors."""
|
| 194 |
|
| 195 |
def split_heads(X: torch.Tensor) -> torch.Tensor:
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
if apply_scale:
|
| 202 |
-
q /= math.sqrt(self.c_hidden)
|
| 203 |
-
return q, k, v
|
| 204 |
|
| 205 |
def _wrap_up(self, output: torch.Tensor, q_x: torch.Tensor) -> torch.Tensor:
|
| 206 |
"""Apply the optional gate, merge heads, and project the update."""
|
| 207 |
|
|
|
|
| 208 |
if self.linear_g is not None:
|
| 209 |
-
gate = self.sigmoid(self.linear_g(q_x))
|
| 210 |
-
gate = gate.view(
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
def forward(
|
| 215 |
self,
|
|
@@ -222,7 +243,9 @@ class Attention(nn.Module):
|
|
| 222 |
) -> torch.Tensor:
|
| 223 |
"""Return a gated pair update with shape ``(..., n, c_q)``."""
|
| 224 |
|
| 225 |
-
q, k, v = self._prep_qkv(
|
|
|
|
|
|
|
| 226 |
if use_kernels:
|
| 227 |
output = kernel_triangular_attn(
|
| 228 |
q,
|
|
@@ -231,7 +254,12 @@ class Attention(nn.Module):
|
|
| 231 |
tri_bias=tri_bias,
|
| 232 |
mask=mask.bool(),
|
| 233 |
scale=1.0 / math.sqrt(self.c_hidden),
|
| 234 |
-
)
|
| 235 |
else:
|
| 236 |
-
output = _attention(
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
import importlib
|
| 14 |
import math
|
| 15 |
+
import torch
|
| 16 |
from collections.abc import Callable, Sequence
|
| 17 |
from importlib.util import find_spec
|
|
|
|
|
|
|
| 18 |
from torch import nn
|
| 19 |
|
| 20 |
from . import vb_layers_initialize as initialize
|
|
|
|
| 26 |
bias: torch.Tensor | None,
|
| 27 |
method: str,
|
| 28 |
) -> None:
|
| 29 |
+
# weight: (d_out, d_in); bias: (d_out,) or None; both retain shape in place.
|
| 30 |
initializers = {
|
| 31 |
"default": initialize.lecun_normal_init_,
|
| 32 |
"relu": initialize.he_normal_init_,
|
|
|
|
| 68 |
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
| 69 |
"""Project ``input`` while preserving explicit precision policies."""
|
| 70 |
|
| 71 |
+
# input: (..., d_in); every branch returns (..., d_out).
|
| 72 |
input_dtype = input.dtype
|
| 73 |
if self.precision is not None:
|
| 74 |
with torch.autocast("cuda", enabled=False):
|
| 75 |
+
bias = (
|
| 76 |
+
None if self.bias is None else self.bias.to(self.precision)
|
| 77 |
+
) # (d_out,) or None
|
| 78 |
projected = nn.functional.linear(
|
| 79 |
input.to(self.precision),
|
| 80 |
self.weight.to(self.precision),
|
| 81 |
bias,
|
| 82 |
+
) # (..., d_out)
|
| 83 |
+
return projected.to(input_dtype) # (..., d_out)
|
| 84 |
if input_dtype is torch.bfloat16:
|
| 85 |
with torch.autocast("cuda", enabled=False):
|
| 86 |
+
bias = (
|
| 87 |
+
None if self.bias is None else self.bias.to(input_dtype)
|
| 88 |
+
) # (d_out,) or None
|
| 89 |
+
return nn.functional.linear(
|
| 90 |
+
input, self.weight.to(input_dtype), bias
|
| 91 |
+
) # (..., d_out)
|
| 92 |
+
return nn.functional.linear(input, self.weight, self.bias) # (..., d_out)
|
| 93 |
|
| 94 |
|
| 95 |
class LayerNorm(nn.Module):
|
|
|
|
| 99 |
super().__init__()
|
| 100 |
self.c_in = (c_in,)
|
| 101 |
self.eps = eps
|
| 102 |
+
self.weight = nn.Parameter(torch.ones(c_in)) # (c_in,)
|
| 103 |
+
self.bias = nn.Parameter(torch.zeros(c_in)) # (c_in,)
|
| 104 |
|
| 105 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 106 |
+
# x: (..., c_in); normalization preserves shape.
|
| 107 |
if x.dtype is torch.bfloat16:
|
| 108 |
with torch.autocast("cuda", enabled=False):
|
| 109 |
return nn.functional.layer_norm(
|
|
|
|
| 134 |
) -> torch.Tensor:
|
| 135 |
"""Compute biased scaled attention after query scaling."""
|
| 136 |
|
| 137 |
+
# query/key/value: (..., h, n_q_or_k, d_h).
|
| 138 |
+
scores = torch.matmul(
|
| 139 |
+
query, permute_final_dims(key, (1, 0))
|
| 140 |
+
) # (..., h, n_q, n_k)
|
| 141 |
for bias in biases:
|
| 142 |
+
scores += bias # (..., h, n_q, n_k)
|
| 143 |
+
probabilities = softmax_no_cast(scores, dim=-1) # (..., h, n_q, n_k)
|
| 144 |
+
return torch.matmul(probabilities, value) # (..., h, n_q, d_h)
|
| 145 |
|
| 146 |
|
| 147 |
@torch.compiler.disable
|
|
|
|
| 155 |
) -> torch.Tensor:
|
| 156 |
"""Call the optional cuEquivariance triangle-attention primitive."""
|
| 157 |
|
| 158 |
+
# q/k/v: (..., h, n, d_h); returned tensor uses the kernel's matching layout.
|
| 159 |
if (
|
| 160 |
find_spec("cuequivariance_torch") is None
|
| 161 |
or find_spec("cuequivariance_ops_torch") is None
|
|
|
|
| 205 |
"""Project Q, K, and V to ``(..., h, n, d)`` tensors."""
|
| 206 |
|
| 207 |
def split_heads(X: torch.Tensor) -> torch.Tensor:
|
| 208 |
+
# X: (..., n, h * d_h)
|
| 209 |
+
return X.view(*X.shape[:-1], self.no_heads, -1).transpose(
|
| 210 |
+
-2, -3
|
| 211 |
+
) # (..., h, n, d_h)
|
| 212 |
+
|
| 213 |
+
# q_x: (..., n_q, c_q); kv_x: (..., n_k, c_k).
|
| 214 |
+
q = split_heads(self.linear_q(q_x)) # (..., h, n_q, d_h)
|
| 215 |
+
k = split_heads(self.linear_k(kv_x)) # (..., h, n_k, d_h)
|
| 216 |
+
v = split_heads(self.linear_v(kv_x)) # (..., h, n_k, d_h)
|
| 217 |
if apply_scale:
|
| 218 |
+
q /= math.sqrt(self.c_hidden) # (..., h, n_q, d_h)
|
| 219 |
+
return q, k, v # (..., h, n_q, d_h), two (..., h, n_k, d_h)
|
| 220 |
|
| 221 |
def _wrap_up(self, output: torch.Tensor, q_x: torch.Tensor) -> torch.Tensor:
|
| 222 |
"""Apply the optional gate, merge heads, and project the update."""
|
| 223 |
|
| 224 |
+
# output: (..., n_q, h, d_h); q_x: (..., n_q, c_q).
|
| 225 |
if self.linear_g is not None:
|
| 226 |
+
gate = self.sigmoid(self.linear_g(q_x)) # (..., n_q, h * d_h)
|
| 227 |
+
gate = gate.view(
|
| 228 |
+
*gate.shape[:-1], self.no_heads, -1
|
| 229 |
+
) # (..., n_q, h, d_h)
|
| 230 |
+
output = output * gate # (..., n_q, h, d_h)
|
| 231 |
+
return self.linear_o(
|
| 232 |
+
flatten_final_dims(output, 2)
|
| 233 |
+
) # (..., n_q, c_q)
|
| 234 |
|
| 235 |
def forward(
|
| 236 |
self,
|
|
|
|
| 243 |
) -> torch.Tensor:
|
| 244 |
"""Return a gated pair update with shape ``(..., n, c_q)``."""
|
| 245 |
|
| 246 |
+
q, k, v = self._prep_qkv(
|
| 247 |
+
q_x, kv_x, apply_scale=not use_kernels
|
| 248 |
+
) # q: (..., h, n_q, d_h); k/v: (..., h, n_k, d_h)
|
| 249 |
if use_kernels:
|
| 250 |
output = kernel_triangular_attn(
|
| 251 |
q,
|
|
|
|
| 254 |
tri_bias=tri_bias,
|
| 255 |
mask=mask.bool(),
|
| 256 |
scale=1.0 / math.sqrt(self.c_hidden),
|
| 257 |
+
) # (..., h, n_q, d_h)
|
| 258 |
else:
|
| 259 |
+
output = _attention(
|
| 260 |
+
q, k, v, (mask_bias, tri_bias)
|
| 261 |
+
) # (..., h, n_q, d_h)
|
| 262 |
+
return self._wrap_up(
|
| 263 |
+
output.transpose(-2, -3), q_x
|
| 264 |
+
) # (..., n_q, c_q)
|
| 265 |
+
# tensor: (...); softmax preserves shape.
|
fastplms/models/boltz/vb_tri_attn_utils.py
CHANGED
|
@@ -7,21 +7,21 @@ the public function names expected by converted checkpoints.
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
|
|
|
| 10 |
from collections.abc import Callable, Sequence
|
| 11 |
from functools import partial
|
| 12 |
from math import prod
|
| 13 |
from typing import Any
|
| 14 |
|
| 15 |
-
import torch
|
| 16 |
-
|
| 17 |
|
| 18 |
def add(left: torch.Tensor, right: torch.Tensor, inplace: bool) -> torch.Tensor:
|
| 19 |
"""Add ``right`` to ``left``, optionally reusing ``left`` storage."""
|
| 20 |
|
|
|
|
| 21 |
if inplace:
|
| 22 |
-
left += right
|
| 23 |
-
return left
|
| 24 |
-
return left + right
|
| 25 |
|
| 26 |
|
| 27 |
def permute_final_dims(tensor: torch.Tensor, inds: Sequence[int]) -> torch.Tensor:
|
|
@@ -30,7 +30,7 @@ def permute_final_dims(tensor: torch.Tensor, inds: Sequence[int]) -> torch.Tenso
|
|
| 30 |
final_count = len(inds)
|
| 31 |
leading = list(range(tensor.ndim - final_count))
|
| 32 |
final = [tensor.ndim - final_count + index for index in inds]
|
| 33 |
-
return tensor.permute(leading + final)
|
| 34 |
|
| 35 |
|
| 36 |
def is_fp16_enabled() -> bool:
|
|
@@ -73,7 +73,8 @@ tensor_tree_map = partial(tree_map, leaf_type=torch.Tensor)
|
|
| 73 |
def flatten_final_dims(tensor: torch.Tensor, no_dims: int) -> torch.Tensor:
|
| 74 |
"""Collapse the final ``no_dims`` axes into one axis."""
|
| 75 |
|
| 76 |
-
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
def _fetch_dims(tree: Any) -> list[torch.Size]:
|
|
@@ -194,9 +195,13 @@ def _chunk_slice(
|
|
| 194 |
batch_shape = tuple(tensor.shape[:no_batch_dims])
|
| 195 |
start = _flat_idx_to_idx(flat_start, batch_shape)
|
| 196 |
end = _flat_idx_to_idx(flat_end - 1, batch_shape)
|
| 197 |
-
pieces = [
|
|
|
|
|
|
|
| 198 |
feature_shape = tuple(tensor.shape[no_batch_dims:])
|
| 199 |
-
return torch.cat(
|
|
|
|
|
|
|
| 200 |
|
| 201 |
|
| 202 |
def _prepare_input(
|
|
@@ -206,12 +211,15 @@ def _prepare_input(
|
|
| 206 |
*,
|
| 207 |
low_mem: bool,
|
| 208 |
) -> torch.Tensor:
|
|
|
|
| 209 |
feature_shape = tuple(tensor.shape[no_batch_dims:])
|
| 210 |
if low_mem:
|
| 211 |
-
return tensor.expand(batch_shape + feature_shape)
|
| 212 |
if any(size != 1 for size in tensor.shape[:no_batch_dims]):
|
| 213 |
-
tensor = tensor.expand(
|
| 214 |
-
|
|
|
|
|
|
|
| 215 |
|
| 216 |
|
| 217 |
def _write_chunk(
|
|
@@ -245,9 +253,9 @@ def _write_chunk(
|
|
| 245 |
if not isinstance(source, torch.Tensor):
|
| 246 |
raise ValueError(f"output type {type(source)!r} is not supported")
|
| 247 |
if add_into_out:
|
| 248 |
-
destination[start:stop] += source
|
| 249 |
else:
|
| 250 |
-
destination[start:stop] = source
|
| 251 |
|
| 252 |
|
| 253 |
def chunk_layer(
|
|
@@ -274,14 +282,16 @@ def chunk_layer(
|
|
| 274 |
no_batch_dims=no_batch_dims,
|
| 275 |
low_mem=low_mem,
|
| 276 |
)
|
| 277 |
-
prepared_inputs = tensor_tree_map(
|
|
|
|
|
|
|
| 278 |
|
| 279 |
output = None
|
| 280 |
if _out is not None:
|
| 281 |
output = tensor_tree_map(
|
| 282 |
lambda tensor: tensor.reshape(-1, *tensor.shape[no_batch_dims:]),
|
| 283 |
_out,
|
| 284 |
-
)
|
| 285 |
|
| 286 |
flat_batch_size = prod(batch_shape)
|
| 287 |
for start in range(0, flat_batch_size, chunk_size):
|
|
@@ -302,14 +312,18 @@ def chunk_layer(
|
|
| 302 |
) -> torch.Tensor:
|
| 303 |
return tensor if tensor.shape[0] == 1 else tensor[start:stop]
|
| 304 |
|
| 305 |
-
input_chunk = tensor_tree_map(
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
if output is None:
|
| 309 |
output = tensor_tree_map(
|
| 310 |
lambda tensor: tensor.new_zeros((flat_batch_size, *tensor.shape[1:])),
|
| 311 |
output_chunk,
|
| 312 |
-
)
|
| 313 |
_write_chunk(
|
| 314 |
output,
|
| 315 |
output_chunk,
|
|
@@ -321,4 +335,6 @@ def chunk_layer(
|
|
| 321 |
return tensor_tree_map(
|
| 322 |
lambda tensor: tensor.reshape(batch_shape + tuple(tensor.shape[1:])),
|
| 323 |
output,
|
| 324 |
-
)
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
+
import torch
|
| 11 |
from collections.abc import Callable, Sequence
|
| 12 |
from functools import partial
|
| 13 |
from math import prod
|
| 14 |
from typing import Any
|
| 15 |
|
|
|
|
|
|
|
| 16 |
|
| 17 |
def add(left: torch.Tensor, right: torch.Tensor, inplace: bool) -> torch.Tensor:
|
| 18 |
"""Add ``right`` to ``left``, optionally reusing ``left`` storage."""
|
| 19 |
|
| 20 |
+
# left/right: broadcast-compatible shapes; result has their broadcast shape.
|
| 21 |
if inplace:
|
| 22 |
+
left += right # same shape as left
|
| 23 |
+
return left # same shape as left
|
| 24 |
+
return left + right # broadcast shape
|
| 25 |
|
| 26 |
|
| 27 |
def permute_final_dims(tensor: torch.Tensor, inds: Sequence[int]) -> torch.Tensor:
|
|
|
|
| 30 |
final_count = len(inds)
|
| 31 |
leading = list(range(tensor.ndim - final_count))
|
| 32 |
final = [tensor.ndim - final_count + index for index in inds]
|
| 33 |
+
return tensor.permute(leading + final) # (..., d_0, ..., d_n)
|
| 34 |
|
| 35 |
|
| 36 |
def is_fp16_enabled() -> bool:
|
|
|
|
| 73 |
def flatten_final_dims(tensor: torch.Tensor, no_dims: int) -> torch.Tensor:
|
| 74 |
"""Collapse the final ``no_dims`` axes into one axis."""
|
| 75 |
|
| 76 |
+
# tensor: (..., d_0, ..., d_n)
|
| 77 |
+
return tensor.reshape(*tensor.shape[:-no_dims], -1) # (..., prod(final dims))
|
| 78 |
|
| 79 |
|
| 80 |
def _fetch_dims(tree: Any) -> list[torch.Size]:
|
|
|
|
| 195 |
batch_shape = tuple(tensor.shape[:no_batch_dims])
|
| 196 |
start = _flat_idx_to_idx(flat_start, batch_shape)
|
| 197 |
end = _flat_idx_to_idx(flat_end - 1, batch_shape)
|
| 198 |
+
pieces = [
|
| 199 |
+
tensor[item] for item in _get_minimal_slice_set(start, end, batch_shape)
|
| 200 |
+
] # each: (*covered_batch_shape, *feature_shape)
|
| 201 |
feature_shape = tuple(tensor.shape[no_batch_dims:])
|
| 202 |
+
return torch.cat(
|
| 203 |
+
[piece.reshape(-1, *feature_shape) for piece in pieces]
|
| 204 |
+
) # (flat_end - flat_start, *feature_shape)
|
| 205 |
|
| 206 |
|
| 207 |
def _prepare_input(
|
|
|
|
| 211 |
*,
|
| 212 |
low_mem: bool,
|
| 213 |
) -> torch.Tensor:
|
| 214 |
+
# tensor: (*input_batch_shape, *feature_shape)
|
| 215 |
feature_shape = tuple(tensor.shape[no_batch_dims:])
|
| 216 |
if low_mem:
|
| 217 |
+
return tensor.expand(batch_shape + feature_shape) # (*batch_shape, *feature_shape)
|
| 218 |
if any(size != 1 for size in tensor.shape[:no_batch_dims]):
|
| 219 |
+
tensor = tensor.expand(
|
| 220 |
+
batch_shape + feature_shape
|
| 221 |
+
) # (*batch_shape, *feature_shape)
|
| 222 |
+
return tensor.reshape(-1, *feature_shape) # (flat_batch, *feature_shape)
|
| 223 |
|
| 224 |
|
| 225 |
def _write_chunk(
|
|
|
|
| 253 |
if not isinstance(source, torch.Tensor):
|
| 254 |
raise ValueError(f"output type {type(source)!r} is not supported")
|
| 255 |
if add_into_out:
|
| 256 |
+
destination[start:stop] += source # (stop - start, *feature_shape)
|
| 257 |
else:
|
| 258 |
+
destination[start:stop] = source # (stop - start, *feature_shape)
|
| 259 |
|
| 260 |
|
| 261 |
def chunk_layer(
|
|
|
|
| 282 |
no_batch_dims=no_batch_dims,
|
| 283 |
low_mem=low_mem,
|
| 284 |
)
|
| 285 |
+
prepared_inputs = tensor_tree_map(
|
| 286 |
+
prepare, inputs
|
| 287 |
+
) # each tensor: (flat_batch, *feature_shape), or broadcast batch in low-memory mode
|
| 288 |
|
| 289 |
output = None
|
| 290 |
if _out is not None:
|
| 291 |
output = tensor_tree_map(
|
| 292 |
lambda tensor: tensor.reshape(-1, *tensor.shape[no_batch_dims:]),
|
| 293 |
_out,
|
| 294 |
+
) # each tensor: (flat_batch, *feature_shape)
|
| 295 |
|
| 296 |
flat_batch_size = prod(batch_shape)
|
| 297 |
for start in range(0, flat_batch_size, chunk_size):
|
|
|
|
| 312 |
) -> torch.Tensor:
|
| 313 |
return tensor if tensor.shape[0] == 1 else tensor[start:stop]
|
| 314 |
|
| 315 |
+
input_chunk = tensor_tree_map(
|
| 316 |
+
select, prepared_inputs
|
| 317 |
+
) # each tensor: (chunk, *feature_shape)
|
| 318 |
+
output_chunk = layer(
|
| 319 |
+
**input_chunk
|
| 320 |
+
) # each output tensor: (chunk, *output_feature_shape)
|
| 321 |
|
| 322 |
if output is None:
|
| 323 |
output = tensor_tree_map(
|
| 324 |
lambda tensor: tensor.new_zeros((flat_batch_size, *tensor.shape[1:])),
|
| 325 |
output_chunk,
|
| 326 |
+
) # each tensor: (flat_batch, *output_feature_shape)
|
| 327 |
_write_chunk(
|
| 328 |
output,
|
| 329 |
output_chunk,
|
|
|
|
| 335 |
return tensor_tree_map(
|
| 336 |
lambda tensor: tensor.reshape(batch_shape + tuple(tensor.shape[1:])),
|
| 337 |
output,
|
| 338 |
+
) # each tensor: (*batch_shape, *output_feature_shape)
|
| 339 |
+
# tensor: (..., d_0, ..., d_n); only the named final dimensions are reordered.
|
| 340 |
+
# tensor: (*batch_shape, *feature_shape)
|
fastplms/registry.py
CHANGED
|
@@ -18,6 +18,7 @@ from types import MappingProxyType
|
|
| 18 |
from typing import Any, Literal, cast
|
| 19 |
from urllib.parse import urlparse
|
| 20 |
|
|
|
|
| 21 |
_HEX_RE = re.compile(r"^[0-9a-f]+$")
|
| 22 |
_IDENTIFIER_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
| 23 |
_HUB_LICENSE_NAME_RE = re.compile(r"[^a-z0-9.]+")
|
|
@@ -559,20 +560,20 @@ def _require_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple
|
|
| 559 |
value = table.get(key)
|
| 560 |
if not isinstance(value, list) or not value or any(not isinstance(item, str) for item in value):
|
| 561 |
raise RegistryError(f"{context}.{key} must be a non-empty string array.")
|
| 562 |
-
|
| 563 |
-
if len(set(
|
| 564 |
raise RegistryError(f"{context}.{key} contains duplicate values.")
|
| 565 |
-
return
|
| 566 |
|
| 567 |
|
| 568 |
def _optional_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
|
| 569 |
value = table.get(key, [])
|
| 570 |
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
| 571 |
raise RegistryError(f"{context}.{key} must be a string array.")
|
| 572 |
-
|
| 573 |
-
if len(set(
|
| 574 |
raise RegistryError(f"{context}.{key} contains duplicate values.")
|
| 575 |
-
return
|
| 576 |
|
| 577 |
|
| 578 |
def _optional_str(table: Mapping[str, Any], key: str, context: str) -> str | None:
|
|
@@ -657,11 +658,11 @@ def _require_digest_list(
|
|
| 657 |
table: Mapping[str, Any], key: str, context: str
|
| 658 |
) -> tuple[FileDigest, ...]:
|
| 659 |
encoded = _require_str_list(table, key, context)
|
| 660 |
-
|
| 661 |
-
paths = [item.path for item in
|
| 662 |
if len(paths) != len(set(paths)):
|
| 663 |
raise RegistryError(f"{context}.{key} contains duplicate paths.")
|
| 664 |
-
return
|
| 665 |
|
| 666 |
|
| 667 |
def _validate_revision(revision: str, context: str) -> None:
|
|
@@ -701,7 +702,7 @@ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[Oracle
|
|
| 701 |
raw = table.get("oracle_assets", [])
|
| 702 |
if not isinstance(raw, list):
|
| 703 |
raise RegistryError(f"{context}.oracle_assets must be an array of tables.")
|
| 704 |
-
|
| 705 |
for index, value in enumerate(raw):
|
| 706 |
asset_context = f"{context}.oracle_assets[{index}]"
|
| 707 |
if not isinstance(value, dict):
|
|
@@ -736,7 +737,7 @@ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[Oracle
|
|
| 736 |
size = value.get("size")
|
| 737 |
if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
|
| 738 |
raise RegistryError(f"{asset_context}.size must be a positive byte count.")
|
| 739 |
-
|
| 740 |
OracleAsset(
|
| 741 |
role=role,
|
| 742 |
path=path,
|
|
@@ -745,16 +746,16 @@ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[Oracle
|
|
| 745 |
size=size,
|
| 746 |
)
|
| 747 |
)
|
| 748 |
-
roles = [asset.role for asset in
|
| 749 |
-
paths = [asset.path for asset in
|
| 750 |
-
urls = [asset.url for asset in
|
| 751 |
if (
|
| 752 |
len(roles) != len(set(roles))
|
| 753 |
or len(paths) != len(set(paths))
|
| 754 |
or len(urls) != len(set(urls))
|
| 755 |
):
|
| 756 |
raise RegistryError(f"{context}.oracle_assets contains duplicate identities.")
|
| 757 |
-
return tuple(
|
| 758 |
|
| 759 |
|
| 760 |
def _parse_official_golden(
|
|
@@ -789,7 +790,7 @@ def _parse_official_golden(
|
|
| 789 |
def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
|
| 790 |
if not isinstance(raw, list) or not raw:
|
| 791 |
raise RegistryError("The manifest must contain [[attention_kernels]] entries.")
|
| 792 |
-
|
| 793 |
expected_variants = {
|
| 794 |
"flash_attention_2": "flash_attn2",
|
| 795 |
"flash_attention_3": "flash_attn3",
|
|
@@ -812,7 +813,7 @@ def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
|
|
| 812 |
implementation = _require_str(value, "implementation", context)
|
| 813 |
if implementation not in expected_variants:
|
| 814 |
raise RegistryError(f"Unsupported attention kernel {implementation!r}.")
|
| 815 |
-
if implementation in
|
| 816 |
raise RegistryError(f"Duplicate attention kernel {implementation!r}.")
|
| 817 |
repository = _require_str(value, "repository", context)
|
| 818 |
if _REPOSITORY_ID_RE.fullmatch(repository) is None:
|
|
@@ -834,7 +835,7 @@ def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
|
|
| 834 |
dtypes = _require_str_list(value, "dtypes", context)
|
| 835 |
if not set(dtypes).issubset(_ALLOWED_DTYPES):
|
| 836 |
raise RegistryError(f"{context}.dtypes contains unsupported dtypes.")
|
| 837 |
-
|
| 838 |
implementation=implementation,
|
| 839 |
repository=repository,
|
| 840 |
revision=revision,
|
|
@@ -842,15 +843,15 @@ def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
|
|
| 842 |
expected_variant=expected_variant,
|
| 843 |
dtypes=cast(tuple[DtypeName, ...], dtypes),
|
| 844 |
)
|
| 845 |
-
if set(
|
| 846 |
raise RegistryError("The manifest must pin both FlashAttention kernel versions.")
|
| 847 |
-
return
|
| 848 |
|
| 849 |
|
| 850 |
def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
|
| 851 |
if not isinstance(raw, list) or not raw:
|
| 852 |
raise RegistryError("The manifest must contain at least one [[upstreams]] entry.")
|
| 853 |
-
|
| 854 |
paths: set[str] = set()
|
| 855 |
for index, value in enumerate(raw):
|
| 856 |
context = f"upstreams[{index}]"
|
|
@@ -860,7 +861,7 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
|
|
| 860 |
source_id = _require_str(value, "id", context)
|
| 861 |
if _IDENTIFIER_RE.fullmatch(source_id) is None:
|
| 862 |
raise RegistryError(f"Invalid upstream ID: {source_id!r}")
|
| 863 |
-
if source_id in
|
| 864 |
raise RegistryError(f"Duplicate upstream ID: {source_id!r}")
|
| 865 |
revision = _require_str(value, "revision", context)
|
| 866 |
_validate_revision(revision, f"{context}.revision")
|
|
@@ -913,7 +914,7 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
|
|
| 913 |
missing_e1 = sorted(required_e1.difference(distribution_map))
|
| 914 |
if missing_e1:
|
| 915 |
raise RegistryError(f"{context} is missing E1 legal files: {missing_e1}")
|
| 916 |
-
|
| 917 |
id=source_id,
|
| 918 |
path=path,
|
| 919 |
url=url,
|
|
@@ -923,7 +924,7 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
|
|
| 923 |
license_digests=license_digests,
|
| 924 |
distribution_files=distribution_files,
|
| 925 |
)
|
| 926 |
-
return
|
| 927 |
|
| 928 |
|
| 929 |
def _parse_families(
|
|
@@ -932,7 +933,7 @@ def _parse_families(
|
|
| 932 |
) -> dict[str, ModelFamily]:
|
| 933 |
if not isinstance(raw, dict) or not raw:
|
| 934 |
raise RegistryError("The manifest must contain [families.<id>] tables.")
|
| 935 |
-
|
| 936 |
for family_id, value in raw.items():
|
| 937 |
context = f"families.{family_id}"
|
| 938 |
if _IDENTIFIER_RE.fullmatch(family_id) is None or not isinstance(value, dict):
|
|
@@ -1068,7 +1069,7 @@ def _parse_families(
|
|
| 1068 |
f"{context}.conversion_provenance must identify {state_transform!r} and "
|
| 1069 |
f"contain mechanism-first sections; missing {missing_sections}."
|
| 1070 |
)
|
| 1071 |
-
|
| 1072 |
id=family_id,
|
| 1073 |
architecture=_require_str(value, "architecture", context),
|
| 1074 |
upstreams=source_ids,
|
|
@@ -1099,7 +1100,7 @@ def _parse_families(
|
|
| 1099 |
conversion_provenance=conversion_provenance,
|
| 1100 |
backbone_model=backbone_model,
|
| 1101 |
)
|
| 1102 |
-
return
|
| 1103 |
|
| 1104 |
|
| 1105 |
def _parse_runtime_assets(
|
|
@@ -1108,7 +1109,7 @@ def _parse_runtime_assets(
|
|
| 1108 |
) -> dict[str, RuntimeAsset]:
|
| 1109 |
if not isinstance(raw, list) or not raw:
|
| 1110 |
raise RegistryError("The manifest must contain at least one [[runtime_assets]] entry.")
|
| 1111 |
-
|
| 1112 |
identities: set[tuple[str, str, str]] = set()
|
| 1113 |
for index, value in enumerate(raw):
|
| 1114 |
context = f"runtime_assets[{index}]"
|
|
@@ -1118,7 +1119,7 @@ def _parse_runtime_assets(
|
|
| 1118 |
asset_id = _require_str(value, "id", context)
|
| 1119 |
if _IDENTIFIER_RE.fullmatch(asset_id) is None:
|
| 1120 |
raise RegistryError(f"Invalid runtime asset ID: {asset_id!r}")
|
| 1121 |
-
if asset_id in
|
| 1122 |
raise RegistryError(f"Duplicate runtime asset ID: {asset_id!r}")
|
| 1123 |
repository = _require_str(value, "repository", context)
|
| 1124 |
if _REPOSITORY_ID_RE.fullmatch(repository) is None:
|
|
@@ -1164,7 +1165,7 @@ def _parse_runtime_assets(
|
|
| 1164 |
if identity in identities:
|
| 1165 |
raise RegistryError(f"Duplicate runtime asset identity: {identity!r}")
|
| 1166 |
identities.add(identity)
|
| 1167 |
-
|
| 1168 |
id=asset_id,
|
| 1169 |
repository=repository,
|
| 1170 |
revision=revision,
|
|
@@ -1176,7 +1177,7 @@ def _parse_runtime_assets(
|
|
| 1176 |
license_expression=license_expression,
|
| 1177 |
offline_behavior=offline_behavior,
|
| 1178 |
)
|
| 1179 |
-
return
|
| 1180 |
|
| 1181 |
|
| 1182 |
def _parse_models(
|
|
@@ -1185,7 +1186,7 @@ def _parse_models(
|
|
| 1185 |
) -> dict[str, ModelSpec]:
|
| 1186 |
if not isinstance(raw, list) or not raw:
|
| 1187 |
raise RegistryError("The manifest must contain at least one [[models]] entry.")
|
| 1188 |
-
|
| 1189 |
fast_repositories: set[str] = set()
|
| 1190 |
for index, value in enumerate(raw):
|
| 1191 |
context = f"models[{index}]"
|
|
@@ -1195,7 +1196,7 @@ def _parse_models(
|
|
| 1195 |
model_id = _require_str(value, "id", context)
|
| 1196 |
if _IDENTIFIER_RE.fullmatch(model_id) is None:
|
| 1197 |
raise RegistryError(f"Invalid model ID: {model_id!r}")
|
| 1198 |
-
if model_id in
|
| 1199 |
raise RegistryError(f"Duplicate model ID: {model_id!r}")
|
| 1200 |
family_id = _require_str(value, "family", context)
|
| 1201 |
if family_id not in families:
|
|
@@ -1278,7 +1279,7 @@ def _parse_models(
|
|
| 1278 |
if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
|
| 1279 |
raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
|
| 1280 |
auto_map.append((auto_class, class_path))
|
| 1281 |
-
|
| 1282 |
id=model_id,
|
| 1283 |
family=family,
|
| 1284 |
fast=fast,
|
|
@@ -1294,7 +1295,7 @@ def _parse_models(
|
|
| 1294 |
notes=notes,
|
| 1295 |
msa_conditioning=msa_conditioning,
|
| 1296 |
)
|
| 1297 |
-
return
|
| 1298 |
|
| 1299 |
|
| 1300 |
def _validate_registry(
|
|
@@ -1409,21 +1410,21 @@ def _validate_registry(
|
|
| 1409 |
|
| 1410 |
def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
|
| 1411 |
try:
|
| 1412 |
-
|
| 1413 |
except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
|
| 1414 |
raise RegistryError(f"Unable to parse model manifest: {error}") from error
|
| 1415 |
-
_reject_unknown_fields(
|
| 1416 |
-
if
|
| 1417 |
raise RegistryError("Unsupported model manifest schema_version; expected 1.")
|
| 1418 |
-
legal_files = _require_digest_list(
|
| 1419 |
required_legal_paths = {"LICENSE", "THIRD_PARTY_NOTICES.md"}
|
| 1420 |
if {item.path for item in legal_files} != required_legal_paths:
|
| 1421 |
raise RegistryError("manifest.legal_files must contain LICENSE and THIRD_PARTY_NOTICES.md.")
|
| 1422 |
-
attention_kernels = _parse_attention_kernels(
|
| 1423 |
-
upstreams = _parse_upstreams(
|
| 1424 |
-
families = _parse_families(
|
| 1425 |
-
runtime_assets = _parse_runtime_assets(
|
| 1426 |
-
models = _parse_models(
|
| 1427 |
_validate_registry(upstreams, attention_kernels, families, models)
|
| 1428 |
return ModelRegistry(
|
| 1429 |
schema_version=1,
|
|
|
|
| 18 |
from typing import Any, Literal, cast
|
| 19 |
from urllib.parse import urlparse
|
| 20 |
|
| 21 |
+
|
| 22 |
_HEX_RE = re.compile(r"^[0-9a-f]+$")
|
| 23 |
_IDENTIFIER_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
| 24 |
_HUB_LICENSE_NAME_RE = re.compile(r"[^a-z0-9.]+")
|
|
|
|
| 560 |
value = table.get(key)
|
| 561 |
if not isinstance(value, list) or not value or any(not isinstance(item, str) for item in value):
|
| 562 |
raise RegistryError(f"{context}.{key} must be a non-empty string array.")
|
| 563 |
+
strings = tuple(value)
|
| 564 |
+
if len(set(strings)) != len(strings):
|
| 565 |
raise RegistryError(f"{context}.{key} contains duplicate values.")
|
| 566 |
+
return strings
|
| 567 |
|
| 568 |
|
| 569 |
def _optional_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
|
| 570 |
value = table.get(key, [])
|
| 571 |
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
| 572 |
raise RegistryError(f"{context}.{key} must be a string array.")
|
| 573 |
+
strings = tuple(value)
|
| 574 |
+
if len(set(strings)) != len(strings):
|
| 575 |
raise RegistryError(f"{context}.{key} contains duplicate values.")
|
| 576 |
+
return strings
|
| 577 |
|
| 578 |
|
| 579 |
def _optional_str(table: Mapping[str, Any], key: str, context: str) -> str | None:
|
|
|
|
| 658 |
table: Mapping[str, Any], key: str, context: str
|
| 659 |
) -> tuple[FileDigest, ...]:
|
| 660 |
encoded = _require_str_list(table, key, context)
|
| 661 |
+
digests = tuple(FileDigest.parse(value) for value in encoded)
|
| 662 |
+
paths = [item.path for item in digests]
|
| 663 |
if len(paths) != len(set(paths)):
|
| 664 |
raise RegistryError(f"{context}.{key} contains duplicate paths.")
|
| 665 |
+
return digests
|
| 666 |
|
| 667 |
|
| 668 |
def _validate_revision(revision: str, context: str) -> None:
|
|
|
|
| 702 |
raw = table.get("oracle_assets", [])
|
| 703 |
if not isinstance(raw, list):
|
| 704 |
raise RegistryError(f"{context}.oracle_assets must be an array of tables.")
|
| 705 |
+
assets: list[OracleAsset] = []
|
| 706 |
for index, value in enumerate(raw):
|
| 707 |
asset_context = f"{context}.oracle_assets[{index}]"
|
| 708 |
if not isinstance(value, dict):
|
|
|
|
| 737 |
size = value.get("size")
|
| 738 |
if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
|
| 739 |
raise RegistryError(f"{asset_context}.size must be a positive byte count.")
|
| 740 |
+
assets.append(
|
| 741 |
OracleAsset(
|
| 742 |
role=role,
|
| 743 |
path=path,
|
|
|
|
| 746 |
size=size,
|
| 747 |
)
|
| 748 |
)
|
| 749 |
+
roles = [asset.role for asset in assets]
|
| 750 |
+
paths = [asset.path for asset in assets]
|
| 751 |
+
urls = [asset.url for asset in assets]
|
| 752 |
if (
|
| 753 |
len(roles) != len(set(roles))
|
| 754 |
or len(paths) != len(set(paths))
|
| 755 |
or len(urls) != len(set(urls))
|
| 756 |
):
|
| 757 |
raise RegistryError(f"{context}.oracle_assets contains duplicate identities.")
|
| 758 |
+
return tuple(assets)
|
| 759 |
|
| 760 |
|
| 761 |
def _parse_official_golden(
|
|
|
|
| 790 |
def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
|
| 791 |
if not isinstance(raw, list) or not raw:
|
| 792 |
raise RegistryError("The manifest must contain [[attention_kernels]] entries.")
|
| 793 |
+
kernels: dict[str, AttentionKernelSpec] = {}
|
| 794 |
expected_variants = {
|
| 795 |
"flash_attention_2": "flash_attn2",
|
| 796 |
"flash_attention_3": "flash_attn3",
|
|
|
|
| 813 |
implementation = _require_str(value, "implementation", context)
|
| 814 |
if implementation not in expected_variants:
|
| 815 |
raise RegistryError(f"Unsupported attention kernel {implementation!r}.")
|
| 816 |
+
if implementation in kernels:
|
| 817 |
raise RegistryError(f"Duplicate attention kernel {implementation!r}.")
|
| 818 |
repository = _require_str(value, "repository", context)
|
| 819 |
if _REPOSITORY_ID_RE.fullmatch(repository) is None:
|
|
|
|
| 835 |
dtypes = _require_str_list(value, "dtypes", context)
|
| 836 |
if not set(dtypes).issubset(_ALLOWED_DTYPES):
|
| 837 |
raise RegistryError(f"{context}.dtypes contains unsupported dtypes.")
|
| 838 |
+
kernels[implementation] = AttentionKernelSpec(
|
| 839 |
implementation=implementation,
|
| 840 |
repository=repository,
|
| 841 |
revision=revision,
|
|
|
|
| 843 |
expected_variant=expected_variant,
|
| 844 |
dtypes=cast(tuple[DtypeName, ...], dtypes),
|
| 845 |
)
|
| 846 |
+
if set(kernels) != set(expected_variants):
|
| 847 |
raise RegistryError("The manifest must pin both FlashAttention kernel versions.")
|
| 848 |
+
return kernels
|
| 849 |
|
| 850 |
|
| 851 |
def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
|
| 852 |
if not isinstance(raw, list) or not raw:
|
| 853 |
raise RegistryError("The manifest must contain at least one [[upstreams]] entry.")
|
| 854 |
+
upstreams: dict[str, UpstreamSource] = {}
|
| 855 |
paths: set[str] = set()
|
| 856 |
for index, value in enumerate(raw):
|
| 857 |
context = f"upstreams[{index}]"
|
|
|
|
| 861 |
source_id = _require_str(value, "id", context)
|
| 862 |
if _IDENTIFIER_RE.fullmatch(source_id) is None:
|
| 863 |
raise RegistryError(f"Invalid upstream ID: {source_id!r}")
|
| 864 |
+
if source_id in upstreams:
|
| 865 |
raise RegistryError(f"Duplicate upstream ID: {source_id!r}")
|
| 866 |
revision = _require_str(value, "revision", context)
|
| 867 |
_validate_revision(revision, f"{context}.revision")
|
|
|
|
| 914 |
missing_e1 = sorted(required_e1.difference(distribution_map))
|
| 915 |
if missing_e1:
|
| 916 |
raise RegistryError(f"{context} is missing E1 legal files: {missing_e1}")
|
| 917 |
+
upstreams[source_id] = UpstreamSource(
|
| 918 |
id=source_id,
|
| 919 |
path=path,
|
| 920 |
url=url,
|
|
|
|
| 924 |
license_digests=license_digests,
|
| 925 |
distribution_files=distribution_files,
|
| 926 |
)
|
| 927 |
+
return upstreams
|
| 928 |
|
| 929 |
|
| 930 |
def _parse_families(
|
|
|
|
| 933 |
) -> dict[str, ModelFamily]:
|
| 934 |
if not isinstance(raw, dict) or not raw:
|
| 935 |
raise RegistryError("The manifest must contain [families.<id>] tables.")
|
| 936 |
+
families: dict[str, ModelFamily] = {}
|
| 937 |
for family_id, value in raw.items():
|
| 938 |
context = f"families.{family_id}"
|
| 939 |
if _IDENTIFIER_RE.fullmatch(family_id) is None or not isinstance(value, dict):
|
|
|
|
| 1069 |
f"{context}.conversion_provenance must identify {state_transform!r} and "
|
| 1070 |
f"contain mechanism-first sections; missing {missing_sections}."
|
| 1071 |
)
|
| 1072 |
+
families[family_id] = ModelFamily(
|
| 1073 |
id=family_id,
|
| 1074 |
architecture=_require_str(value, "architecture", context),
|
| 1075 |
upstreams=source_ids,
|
|
|
|
| 1100 |
conversion_provenance=conversion_provenance,
|
| 1101 |
backbone_model=backbone_model,
|
| 1102 |
)
|
| 1103 |
+
return families
|
| 1104 |
|
| 1105 |
|
| 1106 |
def _parse_runtime_assets(
|
|
|
|
| 1109 |
) -> dict[str, RuntimeAsset]:
|
| 1110 |
if not isinstance(raw, list) or not raw:
|
| 1111 |
raise RegistryError("The manifest must contain at least one [[runtime_assets]] entry.")
|
| 1112 |
+
runtime_assets: dict[str, RuntimeAsset] = {}
|
| 1113 |
identities: set[tuple[str, str, str]] = set()
|
| 1114 |
for index, value in enumerate(raw):
|
| 1115 |
context = f"runtime_assets[{index}]"
|
|
|
|
| 1119 |
asset_id = _require_str(value, "id", context)
|
| 1120 |
if _IDENTIFIER_RE.fullmatch(asset_id) is None:
|
| 1121 |
raise RegistryError(f"Invalid runtime asset ID: {asset_id!r}")
|
| 1122 |
+
if asset_id in runtime_assets:
|
| 1123 |
raise RegistryError(f"Duplicate runtime asset ID: {asset_id!r}")
|
| 1124 |
repository = _require_str(value, "repository", context)
|
| 1125 |
if _REPOSITORY_ID_RE.fullmatch(repository) is None:
|
|
|
|
| 1165 |
if identity in identities:
|
| 1166 |
raise RegistryError(f"Duplicate runtime asset identity: {identity!r}")
|
| 1167 |
identities.add(identity)
|
| 1168 |
+
runtime_assets[asset_id] = RuntimeAsset(
|
| 1169 |
id=asset_id,
|
| 1170 |
repository=repository,
|
| 1171 |
revision=revision,
|
|
|
|
| 1177 |
license_expression=license_expression,
|
| 1178 |
offline_behavior=offline_behavior,
|
| 1179 |
)
|
| 1180 |
+
return runtime_assets
|
| 1181 |
|
| 1182 |
|
| 1183 |
def _parse_models(
|
|
|
|
| 1186 |
) -> dict[str, ModelSpec]:
|
| 1187 |
if not isinstance(raw, list) or not raw:
|
| 1188 |
raise RegistryError("The manifest must contain at least one [[models]] entry.")
|
| 1189 |
+
models: dict[str, ModelSpec] = {}
|
| 1190 |
fast_repositories: set[str] = set()
|
| 1191 |
for index, value in enumerate(raw):
|
| 1192 |
context = f"models[{index}]"
|
|
|
|
| 1196 |
model_id = _require_str(value, "id", context)
|
| 1197 |
if _IDENTIFIER_RE.fullmatch(model_id) is None:
|
| 1198 |
raise RegistryError(f"Invalid model ID: {model_id!r}")
|
| 1199 |
+
if model_id in models:
|
| 1200 |
raise RegistryError(f"Duplicate model ID: {model_id!r}")
|
| 1201 |
family_id = _require_str(value, "family", context)
|
| 1202 |
if family_id not in families:
|
|
|
|
| 1279 |
if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
|
| 1280 |
raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
|
| 1281 |
auto_map.append((auto_class, class_path))
|
| 1282 |
+
models[model_id] = ModelSpec(
|
| 1283 |
id=model_id,
|
| 1284 |
family=family,
|
| 1285 |
fast=fast,
|
|
|
|
| 1295 |
notes=notes,
|
| 1296 |
msa_conditioning=msa_conditioning,
|
| 1297 |
)
|
| 1298 |
+
return models
|
| 1299 |
|
| 1300 |
|
| 1301 |
def _validate_registry(
|
|
|
|
| 1410 |
|
| 1411 |
def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
|
| 1412 |
try:
|
| 1413 |
+
manifest = tomllib.loads(raw_bytes.decode("utf-8"))
|
| 1414 |
except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
|
| 1415 |
raise RegistryError(f"Unable to parse model manifest: {error}") from error
|
| 1416 |
+
_reject_unknown_fields(manifest, _ROOT_FIELDS, "manifest")
|
| 1417 |
+
if manifest.get("schema_version") != 1:
|
| 1418 |
raise RegistryError("Unsupported model manifest schema_version; expected 1.")
|
| 1419 |
+
legal_files = _require_digest_list(manifest, "legal_files", "manifest")
|
| 1420 |
required_legal_paths = {"LICENSE", "THIRD_PARTY_NOTICES.md"}
|
| 1421 |
if {item.path for item in legal_files} != required_legal_paths:
|
| 1422 |
raise RegistryError("manifest.legal_files must contain LICENSE and THIRD_PARTY_NOTICES.md.")
|
| 1423 |
+
attention_kernels = _parse_attention_kernels(manifest.get("attention_kernels"))
|
| 1424 |
+
upstreams = _parse_upstreams(manifest.get("upstreams"))
|
| 1425 |
+
families = _parse_families(manifest.get("families"), upstreams)
|
| 1426 |
+
runtime_assets = _parse_runtime_assets(manifest.get("runtime_assets"), families)
|
| 1427 |
+
models = _parse_models(manifest.get("models"), families)
|
| 1428 |
_validate_registry(upstreams, attention_kernels, families, models)
|
| 1429 |
return ModelRegistry(
|
| 1430 |
schema_version=1,
|
fastplms/runtime.py
CHANGED
|
@@ -11,6 +11,7 @@ from contextlib import contextmanager
|
|
| 11 |
from dataclasses import dataclass
|
| 12 |
from typing import TYPE_CHECKING, Literal
|
| 13 |
|
|
|
|
| 14 |
if TYPE_CHECKING:
|
| 15 |
from collections.abc import Iterator
|
| 16 |
|
|
|
|
| 11 |
from dataclasses import dataclass
|
| 12 |
from typing import TYPE_CHECKING, Literal
|
| 13 |
|
| 14 |
+
|
| 15 |
if TYPE_CHECKING:
|
| 16 |
from collections.abc import Iterator
|
| 17 |
|
fastplms_bundle.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
modeling_fastplms.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Generated bridge to the
|
| 2 |
|
| 3 |
import base64
|
| 4 |
import hashlib
|
|
@@ -6,14 +6,13 @@ import importlib
|
|
| 6 |
import importlib.util
|
| 7 |
import sys
|
| 8 |
import tempfile
|
| 9 |
-
from importlib.metadata import PackageNotFoundError, distribution
|
| 10 |
from io import BytesIO
|
| 11 |
from pathlib import Path
|
| 12 |
from zipfile import ZIP_DEFLATED, ZipFile
|
| 13 |
|
| 14 |
from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
|
| 15 |
|
| 16 |
-
if RUNTIME_HASH != "
|
| 17 |
raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
|
| 18 |
|
| 19 |
_RUNTIME_TEMPORARIES = []
|
|
@@ -83,24 +82,6 @@ def _runtime_file_hashes(package_root):
|
|
| 83 |
result[relative.as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
|
| 84 |
return result
|
| 85 |
|
| 86 |
-
def _installed_runtime_digest(installed_root, relative):
|
| 87 |
-
candidate = installed_root / relative
|
| 88 |
-
if candidate.is_file():
|
| 89 |
-
return hashlib.sha256(candidate.read_bytes()).hexdigest()
|
| 90 |
-
if relative != "kernels.lock":
|
| 91 |
-
return None
|
| 92 |
-
try:
|
| 93 |
-
installed_distribution = distribution("fastplms")
|
| 94 |
-
except PackageNotFoundError:
|
| 95 |
-
return None
|
| 96 |
-
for entry in installed_distribution.files or ():
|
| 97 |
-
normalized = str(entry).replace("\\", "/")
|
| 98 |
-
if normalized.endswith(".dist-info/kernels.lock"):
|
| 99 |
-
lock_path = Path(installed_distribution.locate_file(entry))
|
| 100 |
-
if lock_path.is_file():
|
| 101 |
-
return hashlib.sha256(lock_path.read_bytes()).hexdigest()
|
| 102 |
-
return None
|
| 103 |
-
|
| 104 |
def _extend_loaded_package_paths(package_root):
|
| 105 |
for name, module in list(sys.modules.items()):
|
| 106 |
if name != "fastplms" and not name.startswith("fastplms."):
|
|
@@ -114,29 +95,14 @@ def _extend_loaded_package_paths(package_root):
|
|
| 114 |
if candidate.is_dir() and candidate_text not in paths:
|
| 115 |
paths.append(candidate_text)
|
| 116 |
|
| 117 |
-
def _merge_runtime(
|
| 118 |
incoming = _runtime_file_hashes(package_root)
|
| 119 |
-
known =
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
if installed_file is None:
|
| 126 |
-
raise RuntimeError(
|
| 127 |
-
"The loaded fastplms package has no source path and cannot be verified "
|
| 128 |
-
"against the embedded artifact runtime."
|
| 129 |
-
)
|
| 130 |
-
installed_root = Path(installed_file).resolve().parent
|
| 131 |
-
for relative, digest in incoming.items():
|
| 132 |
-
if _installed_runtime_digest(installed_root, relative) != digest:
|
| 133 |
-
raise RuntimeError(
|
| 134 |
-
"The installed FastPLMs runtime differs from this artifact at "
|
| 135 |
-
f"{relative!r}. Install the artifact's matching FastPLMs release "
|
| 136 |
-
"or use a separate Python process."
|
| 137 |
-
)
|
| 138 |
-
installed_root_text = str(installed_root)
|
| 139 |
-
installed.__fastplms_artifact_installed_root__ = installed_root_text
|
| 140 |
conflicts = sorted(
|
| 141 |
relative
|
| 142 |
for relative, digest in incoming.items()
|
|
@@ -148,35 +114,25 @@ def _merge_runtime(installed, package_root):
|
|
| 148 |
+ ", ".join(repr(path) for path in conflicts[:5])
|
| 149 |
+ ". Load incompatible releases in separate Python processes."
|
| 150 |
)
|
| 151 |
-
|
| 152 |
-
installed_root = Path(installed_root_text)
|
| 153 |
-
for relative, digest in incoming.items():
|
| 154 |
-
if relative in known:
|
| 155 |
-
continue
|
| 156 |
-
if _installed_runtime_digest(installed_root, relative) != digest:
|
| 157 |
-
raise RuntimeError(
|
| 158 |
-
"The installed FastPLMs runtime differs from this artifact at "
|
| 159 |
-
f"{relative!r}. Install the artifact's matching FastPLMs release "
|
| 160 |
-
"or use a separate Python process."
|
| 161 |
-
)
|
| 162 |
known.update(incoming)
|
| 163 |
-
|
| 164 |
-
roots = list(getattr(
|
| 165 |
if str(package_root) not in roots:
|
| 166 |
roots.append(str(package_root))
|
| 167 |
-
|
| 168 |
temporaries = list(
|
| 169 |
-
getattr(
|
| 170 |
)
|
| 171 |
for temporary in _RUNTIME_TEMPORARIES:
|
| 172 |
if temporary not in temporaries:
|
| 173 |
temporaries.append(temporary)
|
| 174 |
-
|
| 175 |
-
hashes = set(getattr(
|
| 176 |
hashes.add(RUNTIME_HASH)
|
| 177 |
-
|
| 178 |
_extend_loaded_package_paths(package_root)
|
| 179 |
-
return
|
| 180 |
|
| 181 |
def _import_without_bytecode(module_name):
|
| 182 |
previous = sys.dont_write_bytecode
|
|
@@ -187,13 +143,13 @@ def _import_without_bytecode(module_name):
|
|
| 187 |
sys.dont_write_bytecode = previous
|
| 188 |
|
| 189 |
def _install_runtime():
|
| 190 |
-
|
| 191 |
-
hashes = getattr(
|
| 192 |
if RUNTIME_HASH in hashes:
|
| 193 |
-
return
|
| 194 |
package_root = _ensure_runtime()
|
| 195 |
-
if
|
| 196 |
-
return _merge_runtime(
|
| 197 |
spec = importlib.util.spec_from_file_location(
|
| 198 |
"fastplms",
|
| 199 |
package_root / "__init__.py",
|
|
@@ -223,8 +179,8 @@ def _install_runtime():
|
|
| 223 |
return package
|
| 224 |
|
| 225 |
_install_runtime()
|
| 226 |
-
|
| 227 |
-
Boltz2Config =
|
| 228 |
Boltz2Config.__module__ = __name__
|
| 229 |
-
Boltz2Model =
|
| 230 |
Boltz2Model.__module__ = __name__
|
|
|
|
| 1 |
+
"""Generated bridge to the embedded FastPLMs runtime sources."""
|
| 2 |
|
| 3 |
import base64
|
| 4 |
import hashlib
|
|
|
|
| 6 |
import importlib.util
|
| 7 |
import sys
|
| 8 |
import tempfile
|
|
|
|
| 9 |
from io import BytesIO
|
| 10 |
from pathlib import Path
|
| 11 |
from zipfile import ZIP_DEFLATED, ZipFile
|
| 12 |
|
| 13 |
from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
|
| 14 |
|
| 15 |
+
if RUNTIME_HASH != "3aebb85d11edb01329d2aafb4c8260bd67339785f0f252582246ae7c33206143":
|
| 16 |
raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
|
| 17 |
|
| 18 |
_RUNTIME_TEMPORARIES = []
|
|
|
|
| 82 |
result[relative.as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
|
| 83 |
return result
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def _extend_loaded_package_paths(package_root):
|
| 86 |
for name, module in list(sys.modules.items()):
|
| 87 |
if name != "fastplms" and not name.startswith("fastplms."):
|
|
|
|
| 95 |
if candidate.is_dir() and candidate_text not in paths:
|
| 96 |
paths.append(candidate_text)
|
| 97 |
|
| 98 |
+
def _merge_runtime(package, package_root):
|
| 99 |
incoming = _runtime_file_hashes(package_root)
|
| 100 |
+
known = getattr(package, "__fastplms_artifact_runtime_files__", None)
|
| 101 |
+
if not isinstance(known, dict):
|
| 102 |
+
raise RuntimeError(
|
| 103 |
+
"A non-artifact fastplms module is already loaded. Load the Hub artifact "
|
| 104 |
+
"in a separate Python process."
|
| 105 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
conflicts = sorted(
|
| 107 |
relative
|
| 108 |
for relative, digest in incoming.items()
|
|
|
|
| 114 |
+ ", ".join(repr(path) for path in conflicts[:5])
|
| 115 |
+ ". Load incompatible releases in separate Python processes."
|
| 116 |
)
|
| 117 |
+
known = dict(known)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
known.update(incoming)
|
| 119 |
+
package.__fastplms_artifact_runtime_files__ = known
|
| 120 |
+
roots = list(getattr(package, "__fastplms_artifact_runtime_roots__", ()))
|
| 121 |
if str(package_root) not in roots:
|
| 122 |
roots.append(str(package_root))
|
| 123 |
+
package.__fastplms_artifact_runtime_roots__ = tuple(roots)
|
| 124 |
temporaries = list(
|
| 125 |
+
getattr(package, "__fastplms_artifact_runtime_temporaries__", ())
|
| 126 |
)
|
| 127 |
for temporary in _RUNTIME_TEMPORARIES:
|
| 128 |
if temporary not in temporaries:
|
| 129 |
temporaries.append(temporary)
|
| 130 |
+
package.__fastplms_artifact_runtime_temporaries__ = tuple(temporaries)
|
| 131 |
+
hashes = set(getattr(package, "__fastplms_artifact_runtime_hashes__", ()))
|
| 132 |
hashes.add(RUNTIME_HASH)
|
| 133 |
+
package.__fastplms_artifact_runtime_hashes__ = frozenset(hashes)
|
| 134 |
_extend_loaded_package_paths(package_root)
|
| 135 |
+
return package
|
| 136 |
|
| 137 |
def _import_without_bytecode(module_name):
|
| 138 |
previous = sys.dont_write_bytecode
|
|
|
|
| 143 |
sys.dont_write_bytecode = previous
|
| 144 |
|
| 145 |
def _install_runtime():
|
| 146 |
+
package = sys.modules.get("fastplms")
|
| 147 |
+
hashes = getattr(package, "__fastplms_artifact_runtime_hashes__", ())
|
| 148 |
if RUNTIME_HASH in hashes:
|
| 149 |
+
return package
|
| 150 |
package_root = _ensure_runtime()
|
| 151 |
+
if package is not None:
|
| 152 |
+
return _merge_runtime(package, package_root)
|
| 153 |
spec = importlib.util.spec_from_file_location(
|
| 154 |
"fastplms",
|
| 155 |
package_root / "__init__.py",
|
|
|
|
| 179 |
return package
|
| 180 |
|
| 181 |
_install_runtime()
|
| 182 |
+
_module_181 = _import_without_bytecode("fastplms.models.boltz.modeling_boltz2")
|
| 183 |
+
Boltz2Config = _module_181.Boltz2Config
|
| 184 |
Boltz2Config.__module__ = __name__
|
| 185 |
+
Boltz2Model = _module_181.Boltz2Model
|
| 186 |
Boltz2Model.__module__ = __name__
|
requirements.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Direct runtime dependencies for Synthyra/Boltz2.
|
| 2 |
+
# FastPLMs source is embedded in this model repository.
|
| 3 |
+
torch>=2.13,<2.14
|
| 4 |
+
transformers>=5.13,<5.14
|
| 5 |
+
huggingface-hub>=0.34,<2
|
| 6 |
+
tokenizers>=0.22,<0.23
|
| 7 |
+
safetensors>=0.5,<1
|
| 8 |
+
numpy>=1.26,<3
|
| 9 |
+
einops>=0.8,<1
|
| 10 |
+
tqdm>=4.67,<5
|
| 11 |
+
accelerate>=1.10,<2
|
| 12 |
+
biopython>=1.85,<2
|
| 13 |
+
biotite>=1.4,<2
|
| 14 |
+
brotli>=1.1,<2
|
| 15 |
+
msgpack>=1.1,<2
|
| 16 |
+
msgpack-numpy>=0.4.8,<1
|
| 17 |
+
omegaconf>=2.3,<3
|
| 18 |
+
rdkit>=2025.9,<2027
|
| 19 |
+
scipy>=1.15,<2
|
| 20 |
+
zstandard>=0.23,<1
|
runtime-attestation.json
CHANGED
|
@@ -2,56 +2,57 @@
|
|
| 2 |
"files": {
|
| 3 |
"LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
|
| 4 |
"LICENSES/boltz/LICENSE": "sha256:f0667fd5e66c51e1ba8ddaa0249c6d7225b30037e02c45782d8f2c2943ac2617",
|
| 5 |
-
"README.md": "sha256:
|
| 6 |
"THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 7 |
-
"config.json": "sha256:
|
| 8 |
-
"fastplms/__init__.py": "sha256:
|
| 9 |
"fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
|
| 10 |
-
"fastplms/models/__init__.py": "sha256:
|
| 11 |
-
"fastplms/models/boltz/__init__.py": "sha256:
|
| 12 |
-
"fastplms/models/boltz/_pair_attention.py": "sha256:
|
| 13 |
-
"fastplms/models/boltz/cif_writer.py": "sha256:
|
| 14 |
-
"fastplms/models/boltz/minimal_featurizer.py": "sha256:
|
| 15 |
"fastplms/models/boltz/minimal_structures.py": "sha256:df7f1ec765ddbb5a730edb9f304fe312a5d3c431ae564ca9b97e6537e0ebf476",
|
| 16 |
-
"fastplms/models/boltz/modeling_boltz2.py": "sha256:
|
| 17 |
"fastplms/models/boltz/vb_const.py": "sha256:67d3cfdb7f0facdb217fe9220404d598ac7924a672f1a2720f7d2d82b23270df",
|
| 18 |
-
"fastplms/models/boltz/vb_layers_attention.py": "sha256:
|
| 19 |
-
"fastplms/models/boltz/vb_layers_attentionv2.py": "sha256:
|
| 20 |
-
"fastplms/models/boltz/vb_layers_confidence_utils.py": "sha256:
|
| 21 |
-
"fastplms/models/boltz/vb_layers_dropout.py": "sha256:
|
| 22 |
-
"fastplms/models/boltz/vb_layers_initialize.py": "sha256:
|
| 23 |
-
"fastplms/models/boltz/vb_layers_outer_product_mean.py": "sha256:
|
| 24 |
-
"fastplms/models/boltz/vb_layers_pair_averaging.py": "sha256:
|
| 25 |
-
"fastplms/models/boltz/vb_layers_pairformer.py": "sha256:
|
| 26 |
-
"fastplms/models/boltz/vb_layers_transition.py": "sha256:
|
| 27 |
-
"fastplms/models/boltz/vb_layers_triangular_mult.py": "sha256:
|
| 28 |
-
"fastplms/models/boltz/vb_loss_diffusionv2.py": "sha256:
|
| 29 |
"fastplms/models/boltz/vb_modules_confidencev2.py": "sha256:a80a6f711e9ea021c45d89967fd37f7e668eea9ab209f9c70f2562d695a15041",
|
| 30 |
-
"fastplms/models/boltz/vb_modules_diffusion_conditioning.py": "sha256:
|
| 31 |
"fastplms/models/boltz/vb_modules_diffusionv2.py": "sha256:0077b40a72d9bf19904e0ab04fdb78b2c642ec6169b688732a1d221aa20a3637",
|
| 32 |
-
"fastplms/models/boltz/vb_modules_encodersv2.py": "sha256:
|
| 33 |
-
"fastplms/models/boltz/vb_modules_transformersv2.py": "sha256:
|
| 34 |
-
"fastplms/models/boltz/vb_modules_trunkv2.py": "sha256:
|
| 35 |
-
"fastplms/models/boltz/vb_modules_utils.py": "sha256:
|
| 36 |
-
"fastplms/models/boltz/vb_potentials_potentials.py": "sha256:
|
| 37 |
"fastplms/models/boltz/vb_potentials_schedules.py": "sha256:91997ee4b7a2a5ba1f40475c9e4fe6a58927f5ef7b51012743615b0f05e278d1",
|
| 38 |
-
"fastplms/models/boltz/vb_tri_attn_attention.py": "sha256:
|
| 39 |
-
"fastplms/models/boltz/vb_tri_attn_primitives.py": "sha256:
|
| 40 |
-
"fastplms/models/boltz/vb_tri_attn_utils.py": "sha256:
|
| 41 |
-
"fastplms/registry.py": "sha256:
|
| 42 |
-
"fastplms/runtime.py": "sha256:
|
| 43 |
-
"fastplms_bundle.py": "sha256:
|
| 44 |
-
"modeling_fastplms.py": "sha256:
|
|
|
|
| 45 |
},
|
| 46 |
"model_id": "boltz2",
|
| 47 |
"redistributable": true,
|
| 48 |
-
"release_tool_revision": "
|
| 49 |
-
"release_tool_sha256": "
|
| 50 |
-
"runtime_bundle_sha256": "
|
| 51 |
-
"runtime_revision": "
|
| 52 |
"schema_version": 2,
|
| 53 |
"scope": "runtime-only",
|
| 54 |
-
"source_tree_sha256": "
|
| 55 |
"weights": {
|
| 56 |
"repo_id": "Synthyra/Boltz2",
|
| 57 |
"revision": "3b148fc5efea109c065ec82ba8683d024de7134e"
|
|
|
|
| 2 |
"files": {
|
| 3 |
"LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
|
| 4 |
"LICENSES/boltz/LICENSE": "sha256:f0667fd5e66c51e1ba8ddaa0249c6d7225b30037e02c45782d8f2c2943ac2617",
|
| 5 |
+
"README.md": "sha256:4d8e31396f39d7b2e1f5b131a4d688c391091531477e896005b2b28f418ef8e5",
|
| 6 |
"THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 7 |
+
"config.json": "sha256:cefe82f26d2a25cbff6958749f3e8727efdb42e8a5eae8c8677498482f45933a",
|
| 8 |
+
"fastplms/__init__.py": "sha256:8503e02debf24abc6d33c1913ff3eb9f245e63f7e62017e220d072c7393fdf2c",
|
| 9 |
"fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
|
| 10 |
+
"fastplms/models/__init__.py": "sha256:d3dd084f5e8fafcf2d0fe4a26e4ab9284d3f9414b1f9a5bd86e308a406447c43",
|
| 11 |
+
"fastplms/models/boltz/__init__.py": "sha256:ef2bad434c2cb2dd171051ce13765ac364651839748670ad2affb5ac816256e9",
|
| 12 |
+
"fastplms/models/boltz/_pair_attention.py": "sha256:da466a398fd30427758c937d49c570dc5319f792bf954091e51ded1c0b51e0ff",
|
| 13 |
+
"fastplms/models/boltz/cif_writer.py": "sha256:f15329f21a0dc070b564dfd95114f145ef638ecd93024d62f342b3b1b26b0334",
|
| 14 |
+
"fastplms/models/boltz/minimal_featurizer.py": "sha256:e7647e2cb62e7608fdfce986d773609ae3a749c63818039bb1c4f77edb5f8415",
|
| 15 |
"fastplms/models/boltz/minimal_structures.py": "sha256:df7f1ec765ddbb5a730edb9f304fe312a5d3c431ae564ca9b97e6537e0ebf476",
|
| 16 |
+
"fastplms/models/boltz/modeling_boltz2.py": "sha256:98fa1c9d307b9595ccd53021ac678293c7fecaf7ea12e851b107fc369dc172b5",
|
| 17 |
"fastplms/models/boltz/vb_const.py": "sha256:67d3cfdb7f0facdb217fe9220404d598ac7924a672f1a2720f7d2d82b23270df",
|
| 18 |
+
"fastplms/models/boltz/vb_layers_attention.py": "sha256:c0fc4b0b5b9a38d56f18af85d0ed506e0c45826d8d4c797da4a281dc35801390",
|
| 19 |
+
"fastplms/models/boltz/vb_layers_attentionv2.py": "sha256:f0509a5be8ee19c3d01797317bdcb6caaf1d37bde695bd6e6be33d06e19e46d7",
|
| 20 |
+
"fastplms/models/boltz/vb_layers_confidence_utils.py": "sha256:ee802fa8c364a38b17dfd0c141bc0b87ac03c8731e8029129756511aa7194e5b",
|
| 21 |
+
"fastplms/models/boltz/vb_layers_dropout.py": "sha256:e962d7fd699c27808f0348ccfc57b719e801ac443328c2fcd799a9cd44753241",
|
| 22 |
+
"fastplms/models/boltz/vb_layers_initialize.py": "sha256:e05108acaf92497f02d6a5567862f4bfe53265ac8f0117d7263dfa0e396ddac3",
|
| 23 |
+
"fastplms/models/boltz/vb_layers_outer_product_mean.py": "sha256:e435b827c110ae54bde63b0a184b65fe3b87cebdd50f3cea6d39e4818ce324da",
|
| 24 |
+
"fastplms/models/boltz/vb_layers_pair_averaging.py": "sha256:73b93eaa790f77d03bf68200ed505d3b2a40afd0f2d806709ecc2c277fe58b13",
|
| 25 |
+
"fastplms/models/boltz/vb_layers_pairformer.py": "sha256:932566fbffd07ab817af1cd5d2b71de98f8ab2bf9c0f6b270e21bf6adca2bb99",
|
| 26 |
+
"fastplms/models/boltz/vb_layers_transition.py": "sha256:e3aad7419fbe86b833e5f2991a779d12fc99d329f797a3b33ce836d8a4abb2e7",
|
| 27 |
+
"fastplms/models/boltz/vb_layers_triangular_mult.py": "sha256:13f55152ad8478ca6fc1dcc5c56bb9028ba8f4a030e5402001399e9359cb643e",
|
| 28 |
+
"fastplms/models/boltz/vb_loss_diffusionv2.py": "sha256:77137ab5da4393af127c3022c43ca4721906cba0c3cdc1d2b6b73fd70352c573",
|
| 29 |
"fastplms/models/boltz/vb_modules_confidencev2.py": "sha256:a80a6f711e9ea021c45d89967fd37f7e668eea9ab209f9c70f2562d695a15041",
|
| 30 |
+
"fastplms/models/boltz/vb_modules_diffusion_conditioning.py": "sha256:dcbabd880c2682e910973512b875fc43e08ddf1a98ee13ab0b5ada01ca5a71d4",
|
| 31 |
"fastplms/models/boltz/vb_modules_diffusionv2.py": "sha256:0077b40a72d9bf19904e0ab04fdb78b2c642ec6169b688732a1d221aa20a3637",
|
| 32 |
+
"fastplms/models/boltz/vb_modules_encodersv2.py": "sha256:135a20ce20c7fda10b92f2770464cd81be4899a3960ae4f5ef0a96dc6f6df105",
|
| 33 |
+
"fastplms/models/boltz/vb_modules_transformersv2.py": "sha256:22a77bdb8af6ee1235369cbe0f0c987eba74ae5cbf10b2fdfcfc5ad1b081aae1",
|
| 34 |
+
"fastplms/models/boltz/vb_modules_trunkv2.py": "sha256:cbd222f1c939f7d1728c5de97535ab1b940205dbdc23bb1895e71d405568d192",
|
| 35 |
+
"fastplms/models/boltz/vb_modules_utils.py": "sha256:10a69588abb92424007971a5c5bb91294a07d3a0850932f7145276d132a73041",
|
| 36 |
+
"fastplms/models/boltz/vb_potentials_potentials.py": "sha256:ca2aacf0c2e5b8de13c5c6456967fb4d9b1cd72f74e13584e9248bbfc8b1a918",
|
| 37 |
"fastplms/models/boltz/vb_potentials_schedules.py": "sha256:91997ee4b7a2a5ba1f40475c9e4fe6a58927f5ef7b51012743615b0f05e278d1",
|
| 38 |
+
"fastplms/models/boltz/vb_tri_attn_attention.py": "sha256:70afafa17bb4c6fa1151a188dfe8a635b1224a6d814bf16b0a4d2949edbdd29a",
|
| 39 |
+
"fastplms/models/boltz/vb_tri_attn_primitives.py": "sha256:3d4beefb52ee69ee98422c3d9058f8e394c2125fc029e79eb959eee9d0eea4ce",
|
| 40 |
+
"fastplms/models/boltz/vb_tri_attn_utils.py": "sha256:bd0566f3c6d5e36d7e6c1a3f79a95fa2e06c1d18d554d4f9331a7a69ac7ab2e1",
|
| 41 |
+
"fastplms/registry.py": "sha256:cb37799dae5a4c0c780089ab2cca4cc56300c1c5f515b275892aa4e1c4b97eb6",
|
| 42 |
+
"fastplms/runtime.py": "sha256:9521c37fcd168bf9a4137277fb73672403ccf30d70d4845aa879fb22f42ddeb6",
|
| 43 |
+
"fastplms_bundle.py": "sha256:e23521271a92290a4da0a66314fba4a3c499826b802817c16340922f85e02421",
|
| 44 |
+
"modeling_fastplms.py": "sha256:39540ac66a63879fac53c2cf365f4d378bc9f81814f7ea6c4261c41a75e789e0",
|
| 45 |
+
"requirements.txt": "sha256:bf8969fb385af9448d39a9574a024d20e1e65609d60c28b5b3e58e6bf55bc351"
|
| 46 |
},
|
| 47 |
"model_id": "boltz2",
|
| 48 |
"redistributable": true,
|
| 49 |
+
"release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 50 |
+
"release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
|
| 51 |
+
"runtime_bundle_sha256": "3aebb85d11edb01329d2aafb4c8260bd67339785f0f252582246ae7c33206143",
|
| 52 |
+
"runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 53 |
"schema_version": 2,
|
| 54 |
"scope": "runtime-only",
|
| 55 |
+
"source_tree_sha256": "9d2fc9586cfbd1d2d2cc49d745311acb81d4e3740e59ead09e633917589806a8",
|
| 56 |
"weights": {
|
| 57 |
"repo_id": "Synthyra/Boltz2",
|
| 58 |
"revision": "3b148fc5efea109c065ec82ba8683d024de7134e"
|