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 files from Synthyra/FastPLMs 32d9951 (PR 52: ESMFold2 noise cap, DPLM2 parity)
Browse files- README.md +2 -2
- fastplms/atomic_files.py +65 -0
- fastplms/digests.py +35 -0
- fastplms/json_files.py +45 -0
- fastplms/models.toml +366 -39
- fastplms/models/boltz/__init__.py +16 -16
- fastplms/models/boltz/_pair_attention.py +51 -51
- fastplms/models/boltz/cif_writer.py +150 -150
- fastplms/models/boltz/minimal_featurizer.py +853 -853
- fastplms/models/boltz/minimal_structures.py +21 -21
- fastplms/models/boltz/modeling_boltz2.py +1066 -1061
- fastplms/models/boltz/vb_const.py +353 -353
- fastplms/models/boltz/vb_layers_attention.py +103 -103
- fastplms/models/boltz/vb_layers_attentionv2.py +76 -76
- fastplms/models/boltz/vb_layers_confidence_utils.py +273 -273
- fastplms/models/boltz/vb_layers_dropout.py +43 -43
- fastplms/models/boltz/vb_layers_initialize.py +110 -110
- fastplms/models/boltz/vb_layers_outer_product_mean.py +105 -105
- fastplms/models/boltz/vb_layers_pair_averaging.py +112 -112
- fastplms/models/boltz/vb_layers_pairformer.py +350 -350
- fastplms/models/boltz/vb_layers_transition.py +79 -79
- fastplms/models/boltz/vb_layers_triangular_mult.py +132 -132
- fastplms/models/boltz/vb_loss_diffusionv2.py +186 -186
- fastplms/models/boltz/vb_modules_confidencev2.py +534 -534
- fastplms/models/boltz/vb_modules_diffusion_conditioning.py +147 -147
- fastplms/models/boltz/vb_modules_diffusionv2.py +675 -675
- fastplms/models/boltz/vb_modules_encodersv2.py +653 -653
- fastplms/models/boltz/vb_modules_transformersv2.py +276 -276
- fastplms/models/boltz/vb_modules_trunkv2.py +624 -624
- fastplms/models/boltz/vb_modules_utils.py +283 -283
- fastplms/models/boltz/vb_potentials_potentials.py +896 -896
- fastplms/models/boltz/vb_potentials_schedules.py +68 -68
- fastplms/models/boltz/vb_tri_attn_attention.py +151 -151
- fastplms/models/boltz/vb_tri_attn_primitives.py +266 -266
- fastplms/models/boltz/vb_tri_attn_utils.py +341 -341
- fastplms/registry.py +192 -2
- fastplms_bundle.py +0 -0
- modeling_fastplms.py +1 -1
- requirements.txt +18 -18
README.md
CHANGED
|
@@ -32,7 +32,7 @@ python -m pip install -r \
|
|
| 32 |
The FastPLMs implementation itself is embedded in the model repository.
|
| 33 |
Transformers loads it through `trust_remote_code=True`.
|
| 34 |
|
| 35 |
-
This model requires Python 3.
|
| 36 |
|
| 37 |
The artifact requirements include the structure dependencies.
|
| 38 |
|
|
@@ -74,7 +74,7 @@ materialize attention tensors. The configured backend does not change.
|
|
| 74 |
Install the training dependencies. Then attach LoRA to the loaded checkpoint:
|
| 75 |
|
| 76 |
```bash
|
| 77 |
-
python -m pip install "datasets>=
|
| 78 |
```
|
| 79 |
|
| 80 |
```python
|
|
|
|
| 32 |
The FastPLMs implementation itself is embedded in the model repository.
|
| 33 |
Transformers loads it through `trust_remote_code=True`.
|
| 34 |
|
| 35 |
+
This model requires Python 3.12-3.14, PyTorch 2.14, and Transformers 5.17.
|
| 36 |
|
| 37 |
The artifact requirements include the structure dependencies.
|
| 38 |
|
|
|
|
| 74 |
Install the training dependencies. Then attach LoRA to the loaded checkpoint:
|
| 75 |
|
| 76 |
```bash
|
| 77 |
+
python -m pip install "datasets>=5.0" "peft>=0.21"
|
| 78 |
```
|
| 79 |
|
| 80 |
```python
|
fastplms/atomic_files.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Replace a file in one step: readers see the old bytes or the new bytes, never a partial write."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import tempfile
|
| 7 |
+
|
| 8 |
+
from collections.abc import Iterator
|
| 9 |
+
from contextlib import contextmanager
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import IO, Any
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def write_bytes_atomically(path: Path, payload: bytes, *, create_parent: bool = False) -> None:
|
| 15 |
+
"""Write ``payload`` to a temporary file beside ``path``, flush it to disk, then rename it over ``path``.
|
| 16 |
+
|
| 17 |
+
A failed write removes the temporary file and leaves ``path`` untouched. The parent directory must
|
| 18 |
+
exist unless ``create_parent`` is true. The temporary file sits in the same directory so the rename
|
| 19 |
+
never crosses a file system.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
with _staged_handle(path, "wb", create_parent=create_parent) as handle:
|
| 23 |
+
handle.write(payload)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def write_text_atomically(
|
| 27 |
+
path: Path,
|
| 28 |
+
text: str,
|
| 29 |
+
*,
|
| 30 |
+
encoding: str = "utf-8",
|
| 31 |
+
newline: str | None = None,
|
| 32 |
+
create_parent: bool = False,
|
| 33 |
+
) -> None:
|
| 34 |
+
"""Write ``text`` as ``write_bytes_atomically`` does.
|
| 35 |
+
|
| 36 |
+
``newline`` is the argument of ``open``: ``None`` translates ``"\\n"`` to the platform separator and
|
| 37 |
+
``"\\n"`` writes it unchanged.
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
with _staged_handle(
|
| 41 |
+
path, "w", create_parent=create_parent, encoding=encoding, newline=newline
|
| 42 |
+
) as handle:
|
| 43 |
+
handle.write(text)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@contextmanager
|
| 47 |
+
def _staged_handle(
|
| 48 |
+
path: Path, mode: str, *, create_parent: bool, **open_arguments: Any
|
| 49 |
+
) -> Iterator[IO[Any]]:
|
| 50 |
+
"""Yield a handle to a temporary file; on a clean exit flush it and rename it over ``path``."""
|
| 51 |
+
|
| 52 |
+
if create_parent:
|
| 53 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 54 |
+
descriptor, temporary_name = tempfile.mkstemp(
|
| 55 |
+
prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
|
| 56 |
+
)
|
| 57 |
+
try:
|
| 58 |
+
with os.fdopen(descriptor, mode, **open_arguments) as handle:
|
| 59 |
+
yield handle
|
| 60 |
+
handle.flush()
|
| 61 |
+
os.fsync(handle.fileno())
|
| 62 |
+
os.replace(temporary_name, path)
|
| 63 |
+
except BaseException:
|
| 64 |
+
Path(temporary_name).unlink(missing_ok=True)
|
| 65 |
+
raise
|
fastplms/digests.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SHA-256 digests of files and JSON values, the identities FastPLMs records and compares.
|
| 2 |
+
|
| 3 |
+
This file exists twice, byte for byte: here and as ``features/digests.py``. ``features`` loads as a standalone
|
| 4 |
+
package (``foundry.embedding.private_store``) and imports nothing outside itself, so it cannot reach this
|
| 5 |
+
module. ``tests/tier1_unit/test_features_package_is_self_contained.py`` fails when the two differ.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import hashlib
|
| 11 |
+
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
from .json_files import compact_json
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
FILE_READ_BYTES = 1024 * 1024
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def file_sha256(path: str | Path) -> str:
|
| 22 |
+
"""Return the SHA-256 of a file's bytes, read in blocks so a checkpoint never sits in memory."""
|
| 23 |
+
|
| 24 |
+
digest = hashlib.sha256()
|
| 25 |
+
with Path(path).open("rb") as handle:
|
| 26 |
+
while block := handle.read(FILE_READ_BYTES):
|
| 27 |
+
digest.update(block)
|
| 28 |
+
return digest.hexdigest()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def json_sha256(value: Any, *, ensure_ascii: bool = True, allow_nan: bool = True) -> str:
|
| 32 |
+
"""Return the SHA-256 of ``value`` in its compact, key-sorted JSON form (``compact_json``)."""
|
| 33 |
+
|
| 34 |
+
encoded = compact_json(value, ensure_ascii=ensure_ascii, allow_nan=allow_nan).encode("utf-8")
|
| 35 |
+
return hashlib.sha256(encoded).hexdigest()
|
fastplms/json_files.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The two JSON text forms FastPLMs writes: compact for hashing, indented for files people read.
|
| 2 |
+
|
| 3 |
+
This file exists twice, byte for byte: here and as ``features/json_files.py``. ``features`` loads as a
|
| 4 |
+
standalone package (``foundry.embedding.private_store``) and imports nothing outside itself, so it cannot
|
| 5 |
+
reach this module. ``tests/tier1_unit/test_features_package_is_self_contained.py`` fails when the two differ.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def compact_json(value: Any, *, ensure_ascii: bool = True, allow_nan: bool = True) -> str:
|
| 16 |
+
"""Serialize with sorted keys and no whitespace, the form a digest or an identity is taken over."""
|
| 17 |
+
|
| 18 |
+
return json.dumps(
|
| 19 |
+
value,
|
| 20 |
+
sort_keys=True,
|
| 21 |
+
separators=(",", ":"),
|
| 22 |
+
ensure_ascii=ensure_ascii,
|
| 23 |
+
allow_nan=allow_nan,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def indented_json(
|
| 28 |
+
value: Any,
|
| 29 |
+
*,
|
| 30 |
+
ensure_ascii: bool = True,
|
| 31 |
+
allow_nan: bool = True,
|
| 32 |
+
sort_keys: bool = True,
|
| 33 |
+
) -> str:
|
| 34 |
+
"""Serialize with two-space indentation and one trailing newline, the form of a stored JSON file."""
|
| 35 |
+
|
| 36 |
+
return (
|
| 37 |
+
json.dumps(
|
| 38 |
+
value,
|
| 39 |
+
indent=2,
|
| 40 |
+
sort_keys=sort_keys,
|
| 41 |
+
ensure_ascii=ensure_ascii,
|
| 42 |
+
allow_nan=allow_nan,
|
| 43 |
+
)
|
| 44 |
+
+ "\n"
|
| 45 |
+
)
|
fastplms/models.toml
CHANGED
|
@@ -4,11 +4,58 @@ legal_files = [
|
|
| 4 |
"THIRD_PARTY_NOTICES.md=sha256:d35e506b728868b52290672d54a89c6aade09529af99dbc8a4db72d9e9ca3460",
|
| 5 |
]
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
[[attention_kernels]]
|
| 8 |
implementation = "flash_attention_2"
|
| 9 |
repository = "kernels-community/flash-attn2"
|
| 10 |
-
revision = "
|
| 11 |
-
version =
|
| 12 |
expected_variant = "flash_attn2"
|
| 13 |
dtypes = ["bfloat16"]
|
| 14 |
min_cuda_capability = [8, 0]
|
|
@@ -64,6 +111,9 @@ distribution_files = [
|
|
| 64 |
id = "biohub-transformers"
|
| 65 |
path = "vendor/upstream/biohub-transformers"
|
| 66 |
url = "https://github.com/Biohub/transformers.git"
|
|
|
|
|
|
|
|
|
|
| 67 |
revision = "3a8956fb4d4ea16b0ec8e71deef2c2909b6a5cbf"
|
| 68 |
license = "Apache-2.0"
|
| 69 |
license_files = ["LICENSE"]
|
|
@@ -177,7 +227,7 @@ conversion_provenance = "Input: the pinned official ESM2 state dictionary. Trans
|
|
| 177 |
representative = "esm2_8m"
|
| 178 |
documentation = "docs/models.md#esm2"
|
| 179 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 180 |
-
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_esm_rotary.py", "models/esm2", "models/ttt.py"]
|
| 181 |
auto_map = { AutoConfig = "fastplms.models.esm2.modeling_fastesm.FastEsmConfig", AutoModel = "fastplms.models.esm2.modeling_fastesm.FastEsmModel", AutoModelForMaskedLM = "fastplms.models.esm2.modeling_fastesm.FastEsmForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.esm2.modeling_fastesm.FastEsmForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esm2.modeling_fastesm.FastEsmForTokenClassification" }
|
| 182 |
|
| 183 |
[families.esm_plusplus]
|
|
@@ -204,7 +254,7 @@ conversion_provenance = "Input: the pinned Biohub ESMC checkpoint. Transformatio
|
|
| 204 |
representative = "esmc_small"
|
| 205 |
documentation = "docs/models.md#esm-and-esmc"
|
| 206 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 207 |
-
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/esm_plusplus", "models/ttt.py"]
|
| 208 |
auto_map = { AutoConfig = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusConfig", AutoModel = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusModel", AutoModelForMaskedLM = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusForTokenClassification" }
|
| 209 |
|
| 210 |
[families.esm3]
|
|
@@ -229,7 +279,7 @@ conversion_provenance = "Input: the pinned Biohub ESM3 checkpoint. Transformatio
|
|
| 229 |
representative = "esm3_small"
|
| 230 |
documentation = "docs/models.md#esm3"
|
| 231 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 232 |
-
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/esm3", "models/ttt.py"]
|
| 233 |
auto_map = { AutoConfig = "fastplms.models.esm3.modeling_esm3.FastESM3Config", AutoModel = "fastplms.models.esm3.modeling_esm3.FastESM3Model", AutoModelForSequenceClassification = "fastplms.models.esm3.modeling_esm3.FastESM3ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esm3.modeling_esm3.FastESM3ForTokenClassification" }
|
| 234 |
|
| 235 |
[families.e1]
|
|
@@ -256,7 +306,7 @@ conversion_provenance = "Input: the pinned Profluent-E1 checkpoint and tokenizer
|
|
| 256 |
representative = "e1_150m"
|
| 257 |
documentation = "docs/models.md#e1"
|
| 258 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 259 |
-
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/e1", "models/ttt.py"]
|
| 260 |
auto_map = { AutoConfig = "fastplms.models.e1.modeling_e1.E1Config", AutoModel = "fastplms.models.e1.modeling_e1.E1Model", AutoModelForMaskedLM = "fastplms.models.e1.modeling_e1.E1ForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.e1.modeling_e1.E1ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.e1.modeling_e1.E1ForTokenClassification" }
|
| 261 |
|
| 262 |
[families.dplm]
|
|
@@ -282,7 +332,7 @@ conversion_provenance = "Input: the pinned official DPLM1 checkpoint. Transforma
|
|
| 282 |
representative = "dplm_150m"
|
| 283 |
documentation = "docs/models.md#dplm"
|
| 284 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 285 |
-
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_diffusion_generation.py", "models/_esm_rotary.py", "models/dplm", "models/ttt.py"]
|
| 286 |
auto_map = { AutoConfig = "fastplms.models.dplm.modeling_dplm.DPLMConfig", AutoModel = "fastplms.models.dplm.modeling_dplm.DPLMModel", AutoModelForMaskedLM = "fastplms.models.dplm.modeling_dplm.DPLMForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.dplm.modeling_dplm.DPLMForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.dplm.modeling_dplm.DPLMForTokenClassification" }
|
| 287 |
|
| 288 |
[families.dplm2]
|
|
@@ -307,7 +357,7 @@ conversion_provenance = "Input: the pinned official DPLM2 checkpoint. Transforma
|
|
| 307 |
representative = "dplm2_150m"
|
| 308 |
documentation = "docs/models.md#dplm2"
|
| 309 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 310 |
-
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_diffusion_generation.py", "models/_esm_rotary.py", "models/dplm2", "models/ttt.py"]
|
| 311 |
auto_map = { AutoConfig = "fastplms.models.dplm2.modeling_dplm2.DPLM2Config", AutoModel = "fastplms.models.dplm2.modeling_dplm2.DPLM2Model", AutoModelForMaskedLM = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForTokenClassification" }
|
| 312 |
tokenizer_class = "fastplms.models.dplm2.tokenization_dplm2.DPLM2Tokenizer"
|
| 313 |
|
|
@@ -334,7 +384,7 @@ representative = "ankh_base"
|
|
| 334 |
documentation = "docs/models.md#ankh"
|
| 335 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 336 |
requires_complete_weight_publication = false
|
| 337 |
-
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/ankh", "models/ttt.py"]
|
| 338 |
auto_map = { AutoConfig = "fastplms.models.ankh.modeling_ankh.FastAnkhConfig", AutoModel = "fastplms.models.ankh.modeling_ankh.FastAnkhModel", AutoModelForMaskedLM = "fastplms.models.ankh.modeling_ankh.FastAnkhForMaskedLMExtension", AutoModelForSeq2SeqLM = "fastplms.models.ankh.modeling_ankh.FastAnkhForConditionalGeneration", AutoModelForSequenceClassification = "fastplms.models.ankh.modeling_ankh.FastAnkhForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.ankh.modeling_ankh.FastAnkhForTokenClassification" }
|
| 339 |
|
| 340 |
[families.boltz2]
|
|
@@ -358,7 +408,7 @@ conversion_provenance = "Input: the pinned official Boltz2 checkpoint. Transform
|
|
| 358 |
representative = "boltz2"
|
| 359 |
documentation = "docs/models.md#boltz2"
|
| 360 |
test_tiers = ["structure", "artifact", "benchmark"]
|
| 361 |
-
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "models/boltz"]
|
| 362 |
auto_map = { AutoConfig = "fastplms.models.boltz.modeling_boltz2.Boltz2Config", AutoModel = "fastplms.models.boltz.modeling_boltz2.Boltz2Model" }
|
| 363 |
|
| 364 |
[families.esmfold]
|
|
@@ -383,7 +433,7 @@ conversion_provenance = "Input: the pinned native Meta ESMFold checkpoint plus i
|
|
| 383 |
representative = "esmfold"
|
| 384 |
documentation = "docs/models.md#esmfold"
|
| 385 |
test_tiers = ["check", "compliance", "structure", "feature", "artifact", "benchmark"]
|
| 386 |
-
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_esm_rotary.py", "models/classification_probe.py", "models/esmfold"]
|
| 387 |
auto_map = { AutoConfig = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmFoldConfig", AutoModel = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmForProteinFolding", AutoModelForSequenceClassification = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmForTokenClassification" }
|
| 388 |
|
| 389 |
[families.esmfold2]
|
|
@@ -410,7 +460,7 @@ conversion_provenance = "Input: each pinned Biohub ESMFold2 checkpoint and its s
|
|
| 410 |
representative = "esmfold2"
|
| 411 |
documentation = "docs/esmfold2.md"
|
| 412 |
test_tiers = ["check", "compliance", "structure", "feature", "artifact", "benchmark"]
|
| 413 |
-
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/classification_probe.py", "models/_esm_rotary.py", "models/esmfold2", "models/esm_plusplus", "models/ttt.py"]
|
| 414 |
auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFold2Config", AutoModel = "fastplms.models.esmfold2.modeling_esmfold2.ESMFold2Model", AutoModelForSequenceClassification = "fastplms.models.esmfold2.modeling_esmfold2_classification.ESMFold2ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esmfold2.modeling_esmfold2_classification.ESMFold2ForTokenClassification" }
|
| 415 |
|
| 416 |
[[models]]
|
|
@@ -418,7 +468,7 @@ id = "esm2_8m"
|
|
| 418 |
family = "esm2"
|
| 419 |
size_category = "small"
|
| 420 |
generation_contract = "not_applicable"
|
| 421 |
-
official_golden = { metadata = "tests/goldens/esm2_8m.json=sha256:
|
| 422 |
fast_repo = "Synthyra/ESM2-8M"
|
| 423 |
fast_revision = "185ecbd45665d050a8dae326d91886d330c5f9d0"
|
| 424 |
fast_files = [
|
|
@@ -457,7 +507,7 @@ id = "esm2_35m"
|
|
| 457 |
family = "esm2"
|
| 458 |
size_category = "small"
|
| 459 |
generation_contract = "not_applicable"
|
| 460 |
-
official_golden = { metadata = "tests/goldens/esm2_35m.json=sha256:
|
| 461 |
fast_repo = "Synthyra/ESM2-35M"
|
| 462 |
fast_revision = "37ab9f56b41e365b3bd9e25d6fefe9150fd910f0"
|
| 463 |
fast_files = [
|
|
@@ -496,7 +546,7 @@ id = "esm2_150m"
|
|
| 496 |
family = "esm2"
|
| 497 |
size_category = "medium"
|
| 498 |
generation_contract = "not_applicable"
|
| 499 |
-
official_golden = { metadata = "tests/goldens/esm2_150m.json=sha256:
|
| 500 |
fast_repo = "Synthyra/ESM2-150M"
|
| 501 |
fast_revision = "979e0880dfc9e0c0080839b83d9d2dc05b92786a"
|
| 502 |
fast_files = [
|
|
@@ -535,7 +585,7 @@ id = "esm2_650m"
|
|
| 535 |
family = "esm2"
|
| 536 |
size_category = "large"
|
| 537 |
generation_contract = "not_applicable"
|
| 538 |
-
official_golden = { metadata = "tests/goldens/esm2_650m.json=sha256:
|
| 539 |
fast_repo = "Synthyra/ESM2-650M"
|
| 540 |
fast_revision = "ca0718a5d52b80d5c60dd76860e55e061a95fb0a"
|
| 541 |
fast_files = [
|
|
@@ -574,7 +624,7 @@ id = "esm2_3b"
|
|
| 574 |
family = "esm2"
|
| 575 |
size_category = "xlarge"
|
| 576 |
generation_contract = "not_applicable"
|
| 577 |
-
official_golden = { metadata = "tests/goldens/esm2_3b.json=sha256:
|
| 578 |
notes = "The pinned default SDPA BF16 path uses a checkpoint-specific numeric calibration: relative L2 target/hard limit 0.06/0.07, relative Q99.9 0.15/0.18, first-percentile residue cosine 0.994/0.992, and pooled cosine 0.998/0.997. Exact state identity and the global logits-distribution contract remain required."
|
| 579 |
fast_repo = "Synthyra/ESM2-3B"
|
| 580 |
fast_revision = "ff89d0180f414ab9c677219a25da79bf09185456"
|
|
@@ -617,7 +667,7 @@ id = "esmc_small"
|
|
| 617 |
family = "esm_plusplus"
|
| 618 |
size_category = "medium"
|
| 619 |
generation_contract = "not_applicable"
|
| 620 |
-
official_golden = { metadata = "tests/goldens/esmc_small.json=sha256:
|
| 621 |
notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
|
| 622 |
fast_repo = "Synthyra/ESMplusplus_small"
|
| 623 |
fast_revision = "46c5f7d562e47d4c14165b424c71ab7db008e6fb"
|
|
@@ -643,7 +693,7 @@ id = "esmc_large"
|
|
| 643 |
family = "esm_plusplus"
|
| 644 |
size_category = "large"
|
| 645 |
generation_contract = "not_applicable"
|
| 646 |
-
official_golden = { metadata = "tests/goldens/esmc_large.json=sha256:
|
| 647 |
notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
|
| 648 |
fast_repo = "Synthyra/ESMplusplus_large"
|
| 649 |
fast_revision = "f813401638b3fddab09748aec1ad2bf537aa4208"
|
|
@@ -669,7 +719,7 @@ id = "esmc_6b"
|
|
| 669 |
family = "esm_plusplus"
|
| 670 |
size_category = "xlarge"
|
| 671 |
generation_contract = "not_applicable"
|
| 672 |
-
official_golden = { metadata = "tests/goldens/esmc_6b.json=sha256:
|
| 673 |
notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
|
| 674 |
fast_repo = "Synthyra/ESMplusplus_6B"
|
| 675 |
fast_revision = "0d579cce3b0f09efa6b3baddf6cc3fd8c9b616c8"
|
|
@@ -681,6 +731,7 @@ fast_files = [
|
|
| 681 |
"model-00004-of-00006.safetensors=sha256:e46c6113c89c6f3e9b072c1bef02d763a625c37bcd8f9da2ed9363891c9a0758",
|
| 682 |
"model-00005-of-00006.safetensors=sha256:6d92cb2bf9791de644de2ae86f8523d802ac3b4aaabfff0716ab6c2b97f6fb14",
|
| 683 |
"model-00006-of-00006.safetensors=sha256:5fc1a8632490bb34162823c35d0d591337b9e4195b22cc0560741397a6e9d0b3",
|
|
|
|
| 684 |
"special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
|
| 685 |
"tokenizer.json=git-sha1:f49735e56cebab0e791aeaae777757b7fd114f71",
|
| 686 |
"tokenizer_config.json=git-sha1:2985ed2b8aa8ecfb1d12f53f47d2b8a44cc21756",
|
|
@@ -706,7 +757,7 @@ family = "esm3"
|
|
| 706 |
tokenizer_source = "esmc_small"
|
| 707 |
size_category = "large"
|
| 708 |
generation_contract = "not_applicable"
|
| 709 |
-
official_golden = { metadata = "tests/goldens/esm3_small.json=sha256:
|
| 710 |
fast_repo = "Synthyra/ESM3_small"
|
| 711 |
fast_revision = "7ddb5a740f9e5f93933eb6410c0ee8684bc63ec1"
|
| 712 |
fast_files = [
|
|
@@ -732,7 +783,7 @@ id = "e1_150m"
|
|
| 732 |
family = "e1"
|
| 733 |
size_category = "small"
|
| 734 |
generation_contract = "not_applicable"
|
| 735 |
-
official_golden = { metadata = "tests/goldens/e1_150m.json=sha256:
|
| 736 |
fast_repo = "Synthyra/Profluent-E1-150M"
|
| 737 |
fast_revision = "7c5f3bbf697226a2e0900db7a100f9201774a907"
|
| 738 |
fast_files = [
|
|
@@ -751,7 +802,7 @@ id = "e1_300m"
|
|
| 751 |
family = "e1"
|
| 752 |
size_category = "medium"
|
| 753 |
generation_contract = "not_applicable"
|
| 754 |
-
official_golden = { metadata = "tests/goldens/e1_300m.json=sha256:
|
| 755 |
fast_repo = "Synthyra/Profluent-E1-300M"
|
| 756 |
fast_revision = "5ef52c0ad2ae2578f40622696b763523810e8e26"
|
| 757 |
fast_files = [
|
|
@@ -770,7 +821,7 @@ id = "e1_600m"
|
|
| 770 |
family = "e1"
|
| 771 |
size_category = "large"
|
| 772 |
generation_contract = "not_applicable"
|
| 773 |
-
official_golden = { metadata = "tests/goldens/e1_600m.json=sha256:
|
| 774 |
fast_repo = "Synthyra/Profluent-E1-600M"
|
| 775 |
fast_revision = "6c8bf0ec83b0e0178677c528b101efffd0677742"
|
| 776 |
fast_files = [
|
|
@@ -789,7 +840,7 @@ id = "dplm_150m"
|
|
| 789 |
family = "dplm"
|
| 790 |
size_category = "small"
|
| 791 |
generation_contract = "required"
|
| 792 |
-
official_golden = { metadata = "tests/goldens/dplm_150m.json=sha256:
|
| 793 |
fast_repo = "Synthyra/DPLM-150M"
|
| 794 |
fast_revision = "90ba742754151a774f3b7ed580170d0a76b3e69d"
|
| 795 |
fast_files = [
|
|
@@ -814,7 +865,7 @@ id = "dplm_650m"
|
|
| 814 |
family = "dplm"
|
| 815 |
size_category = "large"
|
| 816 |
generation_contract = "required"
|
| 817 |
-
official_golden = { metadata = "tests/goldens/dplm_650m.json=sha256:
|
| 818 |
fast_repo = "Synthyra/DPLM-650M"
|
| 819 |
fast_revision = "05dc16d97c5c028aed924c9ed681cee4ab609760"
|
| 820 |
fast_files = [
|
|
@@ -839,7 +890,7 @@ id = "dplm_3b"
|
|
| 839 |
family = "dplm"
|
| 840 |
size_category = "xlarge"
|
| 841 |
generation_contract = "required"
|
| 842 |
-
official_golden = { metadata = "tests/goldens/dplm_3b.json=sha256:
|
| 843 |
fast_repo = "Synthyra/DPLM-3B"
|
| 844 |
fast_revision = "7d764dd3d70ecf1ac0e64693de64a0064aacac65"
|
| 845 |
fast_files = [
|
|
@@ -869,7 +920,7 @@ id = "dplm2_150m"
|
|
| 869 |
family = "dplm2"
|
| 870 |
size_category = "small"
|
| 871 |
generation_contract = "required"
|
| 872 |
-
official_golden = { metadata = "tests/goldens/dplm2_150m.json=sha256:
|
| 873 |
artifact_source = "official"
|
| 874 |
canonical_state_sha256 = "82e1751f59052b8de72b082517557db47947e8d9b4ac2f11278369e6c0cbf001"
|
| 875 |
fast_repo = "Synthyra/DPLM2-150M"
|
|
@@ -896,7 +947,7 @@ id = "dplm2_650m"
|
|
| 896 |
family = "dplm2"
|
| 897 |
size_category = "large"
|
| 898 |
generation_contract = "required"
|
| 899 |
-
official_golden = { metadata = "tests/goldens/dplm2_650m.json=sha256:
|
| 900 |
artifact_source = "official"
|
| 901 |
canonical_state_sha256 = "cba76b6602d2258de9fffff953b608d93cb8ef4a9e89b0bbd27e160c81e78bb4"
|
| 902 |
fast_repo = "Synthyra/DPLM2-650M"
|
|
@@ -925,7 +976,7 @@ size_category = "xlarge"
|
|
| 925 |
# The pinned public sampler fails before generation because cls_token_id is None.
|
| 926 |
# State, tokenizer, and inference parity remain required for this checkpoint.
|
| 927 |
generation_contract = "official_unavailable"
|
| 928 |
-
official_golden = { metadata = "tests/goldens/dplm2_3b.json=sha256:
|
| 929 |
notes = "The pinned official DPLM2-3B sampler fails before generation, so live generation equivalence cannot be established for this checkpoint. State, tokenizer, and inference parity remain required."
|
| 930 |
artifact_source = "official"
|
| 931 |
canonical_state_sha256 = "8c46ec09115dbe6cbfb91d94ab5e906369d57e27fe620a7741c6f8cb1b6ca890"
|
|
@@ -958,7 +1009,7 @@ id = "ankh_base"
|
|
| 958 |
family = "ankh"
|
| 959 |
size_category = "medium"
|
| 960 |
generation_contract = "required"
|
| 961 |
-
official_golden = { metadata = "tests/goldens/ankh_base.json=sha256:
|
| 962 |
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 963 |
artifact_source = "official"
|
| 964 |
canonical_state_sha256 = "cdd8d30d88e5bf41f44e1eef4470d8e46607aba5f7c7c805b06c035b89c8c16f"
|
|
@@ -987,7 +1038,7 @@ id = "ankh_large"
|
|
| 987 |
family = "ankh"
|
| 988 |
size_category = "large"
|
| 989 |
generation_contract = "required"
|
| 990 |
-
official_golden = { metadata = "tests/goldens/ankh_large.json=sha256:
|
| 991 |
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 992 |
artifact_source = "official"
|
| 993 |
canonical_state_sha256 = "e498a2e9aea76ef784cbe3e596c6b3f5e9a40e209ad837f7e3207099e4d74483"
|
|
@@ -1017,7 +1068,7 @@ id = "ankh2_large"
|
|
| 1017 |
family = "ankh"
|
| 1018 |
size_category = "large"
|
| 1019 |
generation_contract = "required"
|
| 1020 |
-
official_golden = { metadata = "tests/goldens/ankh2_large.json=sha256:
|
| 1021 |
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 1022 |
artifact_source = "official"
|
| 1023 |
canonical_state_sha256 = "597c4fe2fa8711f11a25317905f1d62fa92905e55fdd5c0a79614cd9c9d2bca3"
|
|
@@ -1049,7 +1100,7 @@ id = "ankh3_large"
|
|
| 1049 |
family = "ankh"
|
| 1050 |
size_category = "large"
|
| 1051 |
generation_contract = "required"
|
| 1052 |
-
official_golden = { metadata = "tests/goldens/ankh3_large.json=sha256:
|
| 1053 |
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 1054 |
artifact_source = "official"
|
| 1055 |
canonical_state_sha256 = "60acb7ef86e85dc0c51fc1edf4c8e69a0480049723b6b2c95e6e9faa720c112a"
|
|
@@ -1083,7 +1134,7 @@ id = "ankh3_xl"
|
|
| 1083 |
family = "ankh"
|
| 1084 |
size_category = "xlarge"
|
| 1085 |
generation_contract = "required"
|
| 1086 |
-
official_golden = { metadata = "tests/goldens/ankh3_xl.json=sha256:
|
| 1087 |
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head. The official PyTorch shard index is deliberately excluded: the builder verifies every declared source shard directly and writes a new canonical safetensors index."
|
| 1088 |
artifact_source = "official"
|
| 1089 |
canonical_state_sha256 = "dd2188e0d2ca65232135714eef6de394239734d843ddae4928c7398685d858e7"
|
|
@@ -1176,7 +1227,7 @@ family = "esmfold2"
|
|
| 1176 |
size_category = "structure"
|
| 1177 |
generation_contract = "not_applicable"
|
| 1178 |
msa_conditioning = true
|
| 1179 |
-
official_golden = { metadata = "tests/goldens/esmfold2.json=sha256:
|
| 1180 |
fast_repo = "Synthyra/ESMFold2"
|
| 1181 |
fast_revision = "cd5a0927cec585a778d983b99a8db23d2e9b281e"
|
| 1182 |
fast_files = [
|
|
@@ -1196,7 +1247,7 @@ family = "esmfold2"
|
|
| 1196 |
size_category = "structure"
|
| 1197 |
generation_contract = "not_applicable"
|
| 1198 |
msa_conditioning = false
|
| 1199 |
-
official_golden = { metadata = "tests/goldens/esmfold2_fast.json=sha256:
|
| 1200 |
fast_repo = "Synthyra/ESMFold2-Fast"
|
| 1201 |
fast_revision = "407875bfcaa42552bfcb25acd67ee1888b790170"
|
| 1202 |
fast_files = [
|
|
@@ -1216,7 +1267,7 @@ family = "esmfold2"
|
|
| 1216 |
size_category = "structure"
|
| 1217 |
generation_contract = "not_applicable"
|
| 1218 |
msa_conditioning = true
|
| 1219 |
-
official_golden = { metadata = "tests/goldens/esmfold2_experimental_cutoff2025.json=sha256:
|
| 1220 |
fast_repo = "Synthyra/ESMFold2-Experimental-Cutoff2025"
|
| 1221 |
fast_revision = "632ff4a9e68f1de78ee956a613267bdcdb5b354d"
|
| 1222 |
fast_files = [
|
|
@@ -1237,7 +1288,7 @@ family = "esmfold2"
|
|
| 1237 |
size_category = "structure"
|
| 1238 |
generation_contract = "not_applicable"
|
| 1239 |
msa_conditioning = false
|
| 1240 |
-
official_golden = { metadata = "tests/goldens/esmfold2_experimental_fast_cutoff2025.json=sha256:
|
| 1241 |
fast_repo = "Synthyra/ESMFold2-Experimental-Fast-Cutoff2025"
|
| 1242 |
fast_revision = "8f022c2514a6c32692aaca078a8391d6bc6c4bac"
|
| 1243 |
fast_files = [
|
|
@@ -1254,6 +1305,7 @@ auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFo
|
|
| 1254 |
|
| 1255 |
[[models]]
|
| 1256 |
id = "esmfold2_300"
|
|
|
|
| 1257 |
confidence_adaptation = { release = "v1", head_sha256 = "40fd7f3d82fcefe8ad20ab2b32a37a68a84b54a527a4bce6eb9437bad2b77e31", base_weight_sha256 = "44d6797c5efebf24753d502b40950e0874871c96ceea14f2d7f7e39cebac67fd", donor_repo = "biohub/ESMFold2-Experimental-Fast-Cutoff2025", donor_revision = "74b88548bf19688b8727432db0d698cb2e1d8783", donor_weight_sha256 = "4e903b740ad6ad704ec60881bfd593e0d6c874a630ffa0f0838276e0b665088f", training_url = "https://wandb.ai/lhallee/fastplms-confidence/runs/9558b6d23daf", evaluation_url = "https://huggingface.co/datasets/Synthyra/FastPLMs-artifacts/tree/309f353b07e0e46de4d77a5266d4eddd695538e3/confidence-v2/v2-reproduction-20260922/public/evaluation/esmfold2_300", evidence_path = "docs/evidence/confidence/esmfold2_300-v1.json", frozen_base = { repo = "Synthyra/ESMFold2-300", revision = "a38a62ae930d157484b331c2bf4241684573adba", files = ["config.json=git-sha1:47ec20cf8b234c3b41d6f3ae1bdfe95d4eb4849e", "model.safetensors=sha256:44d6797c5efebf24753d502b40950e0874871c96ceea14f2d7f7e39cebac67fd"] } }
|
| 1258 |
family = "esmfold2"
|
| 1259 |
size_category = "structure"
|
|
@@ -1273,6 +1325,7 @@ backbone = { repo = "biohub/ESMC-300M-1500000", revision = "56803b6378b82e16c3b2
|
|
| 1273 |
|
| 1274 |
[[models]]
|
| 1275 |
id = "esmfold2_600"
|
|
|
|
| 1276 |
confidence_adaptation = { release = "v1", head_sha256 = "e84726a050722e1b722712c87d17a5388bd3699e2520e4a59abb1d828dfb8de7", base_weight_sha256 = "11a53c1b4700b6c62a5a464fc3ec7076160c19e8584157f88e13275116cfd602", donor_repo = "biohub/ESMFold2-Experimental-Fast-Cutoff2025", donor_revision = "74b88548bf19688b8727432db0d698cb2e1d8783", donor_weight_sha256 = "4e903b740ad6ad704ec60881bfd593e0d6c874a630ffa0f0838276e0b665088f", training_url = "https://wandb.ai/lhallee/fastplms-confidence/runs/820d2cfa56c0", evaluation_url = "https://huggingface.co/datasets/Synthyra/FastPLMs-artifacts/tree/6e62186cd36b9047cc4691980076be9f76482192/confidence-v2/v2-reproduction-20260922/public/evaluation/esmfold2_600", evidence_path = "docs/evidence/confidence/esmfold2_600-v1.json", frozen_base = { repo = "Synthyra/ESMFold2-600", revision = "71c67d0b2b73dc245ea7c3cc0d0476439a882d08", files = ["config.json=git-sha1:8e271837cbdada96c4974c8e543f84065e0f06f1", "model.safetensors=sha256:11a53c1b4700b6c62a5a464fc3ec7076160c19e8584157f88e13275116cfd602"] } }
|
| 1277 |
family = "esmfold2"
|
| 1278 |
size_category = "structure"
|
|
@@ -1289,3 +1342,277 @@ notes = "Experimental Fast model with a frozen 600M ESM++ backbone, 24 folding b
|
|
| 1289 |
auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFold2Config", AutoModel = "fastplms.models.esmfold2.modeling_esmfold2_experimental.ESMFold2ExperimentalModel", AutoModelForSequenceClassification = "fastplms.models.esmfold2.modeling_esmfold2_classification.ESMFold2ExperimentalForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esmfold2.modeling_esmfold2_classification.ESMFold2ExperimentalForTokenClassification" }
|
| 1290 |
backbone_model = "esmc_large"
|
| 1291 |
backbone = { repo = "biohub/ESMC-600M-1500000", revision = "21af9cc429af76ebda6c48074fb624db4735aaaf", files = ["config.json=git-sha1:ec29f6009b21d710f64bf1c058f3a9710833d692", "model.safetensors=sha256:d6869f5ae0f11e5dc829b195e062e87cfcc2f851a08a5edbaf5d1083ae7f76cc", "tokenizer.json=git-sha1:81c797f56768b22dec0301fa771f018b7e43e98c", "tokenizer_config.json=git-sha1:f49f57b24a1c93bd544974811e8ecbd61b7fae89", "special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b"] }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"THIRD_PARTY_NOTICES.md=sha256:d35e506b728868b52290672d54a89c6aade09529af99dbc8a4db72d9e9ca3460",
|
| 5 |
]
|
| 6 |
|
| 7 |
+
# Released hidden-state SAEs used by the canonical suite. The training-backbone revision
|
| 8 |
+
# is not reported upstream; base_model names the selected inference base, not that history.
|
| 9 |
+
[[sparse_autoencoders]]
|
| 10 |
+
id = "esmc_small_sae_depth"
|
| 11 |
+
base_model = "esmc_small"
|
| 12 |
+
layer = 23
|
| 13 |
+
input_width = 960
|
| 14 |
+
k = 64
|
| 15 |
+
codebook_dim = 16384
|
| 16 |
+
input_kind = "hidden_state"
|
| 17 |
+
checkpoint_repo = "biohub/ESMC-300M-sae-layer23-k64-codebook16384"
|
| 18 |
+
checkpoint_revision = "71b054fc726a9f11153a601118f03657ac6e6e80"
|
| 19 |
+
checkpoint_files = [
|
| 20 |
+
"config.json=sha256:e2864c5f9756c052f343310bafeddafc3edc9514ad85981a51a1a3411186434d",
|
| 21 |
+
"layer_23.safetensors=sha256:825257f785600a7a9d462882d5cf3431ebbbba94761434e33b0c4814a7cc6270",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
[[sparse_autoencoders]]
|
| 25 |
+
id = "esmc_large_sae_depth"
|
| 26 |
+
base_model = "esmc_large"
|
| 27 |
+
layer = 27
|
| 28 |
+
input_width = 1152
|
| 29 |
+
k = 64
|
| 30 |
+
codebook_dim = 16384
|
| 31 |
+
input_kind = "hidden_state"
|
| 32 |
+
checkpoint_repo = "biohub/ESMC-600M-sae-layer27-k64-codebook16384"
|
| 33 |
+
checkpoint_revision = "b96480999aca49b7684e093f1adfcb09b55a1720"
|
| 34 |
+
checkpoint_files = [
|
| 35 |
+
"config.json=sha256:0e74c8af3ff4ac644f26be308e0fe6da6c75a4a52f0d3f05b0cbbdda27efc9ce",
|
| 36 |
+
"layer_27.safetensors=sha256:5e93c230aa200876cb300377c8e22a141a7e2e316c6a3b962b2a78c4518535db",
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
[[sparse_autoencoders]]
|
| 40 |
+
id = "esmc_6b_sae_depth"
|
| 41 |
+
base_model = "esmc_6b"
|
| 42 |
+
layer = 60
|
| 43 |
+
input_width = 2560
|
| 44 |
+
k = 64
|
| 45 |
+
codebook_dim = 16384
|
| 46 |
+
input_kind = "hidden_state"
|
| 47 |
+
checkpoint_repo = "biohub/ESMC-6B-sae-layer60-k64-codebook16384"
|
| 48 |
+
checkpoint_revision = "99752fe6e4d25fbb26f887db2d9225e7a577da73"
|
| 49 |
+
checkpoint_files = [
|
| 50 |
+
"config.json=sha256:42567d3f757bea5c6b618abf55ed905ce4304d1eed891a2c1feae8e1ef393fd0",
|
| 51 |
+
"layer_60.safetensors=sha256:ddc1417b42cffe2d7fc2ec31783a04ac88800e6a8f8c8542f1f92d8f4f16094c",
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
[[attention_kernels]]
|
| 55 |
implementation = "flash_attention_2"
|
| 56 |
repository = "kernels-community/flash-attn2"
|
| 57 |
+
revision = "81fb77c12b2ad5d69380669b46739d5868614502"
|
| 58 |
+
version = 3
|
| 59 |
expected_variant = "flash_attn2"
|
| 60 |
dtypes = ["bfloat16"]
|
| 61 |
min_cuda_capability = [8, 0]
|
|
|
|
| 111 |
id = "biohub-transformers"
|
| 112 |
path = "vendor/upstream/biohub-transformers"
|
| 113 |
url = "https://github.com/Biohub/transformers.git"
|
| 114 |
+
# Biohub/transformers left GitHub by 2026-10-08. Its pinned commit stays reachable through the
|
| 115 |
+
# fork network of huggingface/transformers, which serves the clone and the source archive.
|
| 116 |
+
fetch_url = "https://github.com/huggingface/transformers.git"
|
| 117 |
revision = "3a8956fb4d4ea16b0ec8e71deef2c2909b6a5cbf"
|
| 118 |
license = "Apache-2.0"
|
| 119 |
license_files = ["LICENSE"]
|
|
|
|
| 227 |
representative = "esm2_8m"
|
| 228 |
documentation = "docs/models.md#esm2"
|
| 229 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 230 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "atomic_files.py", "digests.py", "json_files.py", "models.toml", "models/__init__.py", "attention", "embeddings", "features","models/_esm_rotary.py", "models/esm2", "models/ttt.py"]
|
| 231 |
auto_map = { AutoConfig = "fastplms.models.esm2.modeling_fastesm.FastEsmConfig", AutoModel = "fastplms.models.esm2.modeling_fastesm.FastEsmModel", AutoModelForMaskedLM = "fastplms.models.esm2.modeling_fastesm.FastEsmForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.esm2.modeling_fastesm.FastEsmForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esm2.modeling_fastesm.FastEsmForTokenClassification" }
|
| 232 |
|
| 233 |
[families.esm_plusplus]
|
|
|
|
| 254 |
representative = "esmc_small"
|
| 255 |
documentation = "docs/models.md#esm-and-esmc"
|
| 256 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 257 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "atomic_files.py", "digests.py", "json_files.py", "models.toml", "models/__init__.py", "attention", "embeddings", "features","models/esm_plusplus", "models/ttt.py"]
|
| 258 |
auto_map = { AutoConfig = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusConfig", AutoModel = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusModel", AutoModelForMaskedLM = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusForTokenClassification" }
|
| 259 |
|
| 260 |
[families.esm3]
|
|
|
|
| 279 |
representative = "esm3_small"
|
| 280 |
documentation = "docs/models.md#esm3"
|
| 281 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 282 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "atomic_files.py", "digests.py", "json_files.py", "models.toml", "models/__init__.py", "attention", "embeddings", "features","models/esm3", "models/ttt.py"]
|
| 283 |
auto_map = { AutoConfig = "fastplms.models.esm3.modeling_esm3.FastESM3Config", AutoModel = "fastplms.models.esm3.modeling_esm3.FastESM3Model", AutoModelForSequenceClassification = "fastplms.models.esm3.modeling_esm3.FastESM3ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esm3.modeling_esm3.FastESM3ForTokenClassification" }
|
| 284 |
|
| 285 |
[families.e1]
|
|
|
|
| 306 |
representative = "e1_150m"
|
| 307 |
documentation = "docs/models.md#e1"
|
| 308 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 309 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "atomic_files.py", "digests.py", "json_files.py", "models.toml", "models/__init__.py", "attention", "embeddings", "features","models/e1", "models/ttt.py"]
|
| 310 |
auto_map = { AutoConfig = "fastplms.models.e1.modeling_e1.E1Config", AutoModel = "fastplms.models.e1.modeling_e1.E1Model", AutoModelForMaskedLM = "fastplms.models.e1.modeling_e1.E1ForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.e1.modeling_e1.E1ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.e1.modeling_e1.E1ForTokenClassification" }
|
| 311 |
|
| 312 |
[families.dplm]
|
|
|
|
| 332 |
representative = "dplm_150m"
|
| 333 |
documentation = "docs/models.md#dplm"
|
| 334 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 335 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "atomic_files.py", "digests.py", "json_files.py", "models.toml", "models/__init__.py", "attention", "embeddings", "features","models/_diffusion_generation.py", "models/_esm_rotary.py", "models/dplm", "models/ttt.py"]
|
| 336 |
auto_map = { AutoConfig = "fastplms.models.dplm.modeling_dplm.DPLMConfig", AutoModel = "fastplms.models.dplm.modeling_dplm.DPLMModel", AutoModelForMaskedLM = "fastplms.models.dplm.modeling_dplm.DPLMForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.dplm.modeling_dplm.DPLMForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.dplm.modeling_dplm.DPLMForTokenClassification" }
|
| 337 |
|
| 338 |
[families.dplm2]
|
|
|
|
| 357 |
representative = "dplm2_150m"
|
| 358 |
documentation = "docs/models.md#dplm2"
|
| 359 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 360 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "atomic_files.py", "digests.py", "json_files.py", "models.toml", "models/__init__.py", "attention", "embeddings", "features","models/_diffusion_generation.py", "models/_esm_rotary.py", "models/dplm2", "models/ttt.py"]
|
| 361 |
auto_map = { AutoConfig = "fastplms.models.dplm2.modeling_dplm2.DPLM2Config", AutoModel = "fastplms.models.dplm2.modeling_dplm2.DPLM2Model", AutoModelForMaskedLM = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForTokenClassification" }
|
| 362 |
tokenizer_class = "fastplms.models.dplm2.tokenization_dplm2.DPLM2Tokenizer"
|
| 363 |
|
|
|
|
| 384 |
documentation = "docs/models.md#ankh"
|
| 385 |
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 386 |
requires_complete_weight_publication = false
|
| 387 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "atomic_files.py", "digests.py", "json_files.py", "models.toml", "models/__init__.py", "attention", "embeddings", "features","models/ankh", "models/ttt.py"]
|
| 388 |
auto_map = { AutoConfig = "fastplms.models.ankh.modeling_ankh.FastAnkhConfig", AutoModel = "fastplms.models.ankh.modeling_ankh.FastAnkhModel", AutoModelForMaskedLM = "fastplms.models.ankh.modeling_ankh.FastAnkhForMaskedLMExtension", AutoModelForSeq2SeqLM = "fastplms.models.ankh.modeling_ankh.FastAnkhForConditionalGeneration", AutoModelForSequenceClassification = "fastplms.models.ankh.modeling_ankh.FastAnkhForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.ankh.modeling_ankh.FastAnkhForTokenClassification" }
|
| 389 |
|
| 390 |
[families.boltz2]
|
|
|
|
| 408 |
representative = "boltz2"
|
| 409 |
documentation = "docs/models.md#boltz2"
|
| 410 |
test_tiers = ["structure", "artifact", "benchmark"]
|
| 411 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "atomic_files.py", "digests.py", "json_files.py", "models.toml", "models/__init__.py", "models/boltz"]
|
| 412 |
auto_map = { AutoConfig = "fastplms.models.boltz.modeling_boltz2.Boltz2Config", AutoModel = "fastplms.models.boltz.modeling_boltz2.Boltz2Model" }
|
| 413 |
|
| 414 |
[families.esmfold]
|
|
|
|
| 433 |
representative = "esmfold"
|
| 434 |
documentation = "docs/models.md#esmfold"
|
| 435 |
test_tiers = ["check", "compliance", "structure", "feature", "artifact", "benchmark"]
|
| 436 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "atomic_files.py", "digests.py", "json_files.py", "models.toml", "models/__init__.py", "attention", "embeddings", "features","models/_esm_rotary.py", "models/classification_probe.py", "models/esmfold"]
|
| 437 |
auto_map = { AutoConfig = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmFoldConfig", AutoModel = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmForProteinFolding", AutoModelForSequenceClassification = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmForTokenClassification" }
|
| 438 |
|
| 439 |
[families.esmfold2]
|
|
|
|
| 460 |
representative = "esmfold2"
|
| 461 |
documentation = "docs/esmfold2.md"
|
| 462 |
test_tiers = ["check", "compliance", "structure", "feature", "artifact", "benchmark"]
|
| 463 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "atomic_files.py", "digests.py", "json_files.py", "models.toml", "models/__init__.py", "attention", "embeddings", "features","models/classification_probe.py", "models/_esm_rotary.py", "models/esmfold2", "models/esm_plusplus", "models/ttt.py"]
|
| 464 |
auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFold2Config", AutoModel = "fastplms.models.esmfold2.modeling_esmfold2.ESMFold2Model", AutoModelForSequenceClassification = "fastplms.models.esmfold2.modeling_esmfold2_classification.ESMFold2ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esmfold2.modeling_esmfold2_classification.ESMFold2ForTokenClassification" }
|
| 465 |
|
| 466 |
[[models]]
|
|
|
|
| 468 |
family = "esm2"
|
| 469 |
size_category = "small"
|
| 470 |
generation_contract = "not_applicable"
|
| 471 |
+
official_golden = { metadata = "tests/goldens/esm2_8m.json=sha256:06b3ccbf45a46a7aed3833b49503567f7534ffe60e6b3e49fcfcf17d04b7237e", tensors = "tests/goldens/esm2_8m.safetensors=sha256:d08a7572cbef20b8b19b545bcb0427b7e9ae19b986d015c559cd5a3a2cfc8aa4" }
|
| 472 |
fast_repo = "Synthyra/ESM2-8M"
|
| 473 |
fast_revision = "185ecbd45665d050a8dae326d91886d330c5f9d0"
|
| 474 |
fast_files = [
|
|
|
|
| 507 |
family = "esm2"
|
| 508 |
size_category = "small"
|
| 509 |
generation_contract = "not_applicable"
|
| 510 |
+
official_golden = { metadata = "tests/goldens/esm2_35m.json=sha256:61cd24fd91ef2dc49cbc0f97b14c0d5c97849eca6f36ed8c294295147909cb38", tensors = "tests/goldens/esm2_35m.safetensors=sha256:4f82d10286e16041c2f23365dfdd8508b911864633287dd76580425527c2d922" }
|
| 511 |
fast_repo = "Synthyra/ESM2-35M"
|
| 512 |
fast_revision = "37ab9f56b41e365b3bd9e25d6fefe9150fd910f0"
|
| 513 |
fast_files = [
|
|
|
|
| 546 |
family = "esm2"
|
| 547 |
size_category = "medium"
|
| 548 |
generation_contract = "not_applicable"
|
| 549 |
+
official_golden = { metadata = "tests/goldens/esm2_150m.json=sha256:864e1f9c1d8e939e8da050b8617b8a7d6983fbb11a5911275d1571d08e460a79", tensors = "tests/goldens/esm2_150m.safetensors=sha256:20ad986b8e6e09f36d0158f5939d1914e0809b84329695028d767aab93338621" }
|
| 550 |
fast_repo = "Synthyra/ESM2-150M"
|
| 551 |
fast_revision = "979e0880dfc9e0c0080839b83d9d2dc05b92786a"
|
| 552 |
fast_files = [
|
|
|
|
| 585 |
family = "esm2"
|
| 586 |
size_category = "large"
|
| 587 |
generation_contract = "not_applicable"
|
| 588 |
+
official_golden = { metadata = "tests/goldens/esm2_650m.json=sha256:b57f18c8803e68e6c621f479fd7a70596fbc416fc4a17d2ab91c639b480006c4", tensors = "tests/goldens/esm2_650m.safetensors=sha256:261a8b71c4b7c90b1f294031558ee0bf00d77a98352021eb78771200c8d40548" }
|
| 589 |
fast_repo = "Synthyra/ESM2-650M"
|
| 590 |
fast_revision = "ca0718a5d52b80d5c60dd76860e55e061a95fb0a"
|
| 591 |
fast_files = [
|
|
|
|
| 624 |
family = "esm2"
|
| 625 |
size_category = "xlarge"
|
| 626 |
generation_contract = "not_applicable"
|
| 627 |
+
official_golden = { metadata = "tests/goldens/esm2_3b.json=sha256:aa441786329889d43983811cac155cabd37b38cfd47bc90d415732a295c33c6d", tensors = "tests/goldens/esm2_3b.safetensors=sha256:61080e55e07db4a562a19e9b6b662e71a9fd3d8fee357aa3df00697d2fa58e33" }
|
| 628 |
notes = "The pinned default SDPA BF16 path uses a checkpoint-specific numeric calibration: relative L2 target/hard limit 0.06/0.07, relative Q99.9 0.15/0.18, first-percentile residue cosine 0.994/0.992, and pooled cosine 0.998/0.997. Exact state identity and the global logits-distribution contract remain required."
|
| 629 |
fast_repo = "Synthyra/ESM2-3B"
|
| 630 |
fast_revision = "ff89d0180f414ab9c677219a25da79bf09185456"
|
|
|
|
| 667 |
family = "esm_plusplus"
|
| 668 |
size_category = "medium"
|
| 669 |
generation_contract = "not_applicable"
|
| 670 |
+
official_golden = { metadata = "tests/goldens/esmc_small.json=sha256:15cbbf909a12b5b81ff47b5f142babb75ebf9b3d85673905f2320c2a119ae347", tensors = "tests/goldens/esmc_small.safetensors=sha256:98219356e2845cbd10a80715b19c95632956d40567a7a993409c426b56040b06" }
|
| 671 |
notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
|
| 672 |
fast_repo = "Synthyra/ESMplusplus_small"
|
| 673 |
fast_revision = "46c5f7d562e47d4c14165b424c71ab7db008e6fb"
|
|
|
|
| 693 |
family = "esm_plusplus"
|
| 694 |
size_category = "large"
|
| 695 |
generation_contract = "not_applicable"
|
| 696 |
+
official_golden = { metadata = "tests/goldens/esmc_large.json=sha256:86ffe179a9feafd067793f440d25e3606f184f0d32063aeaeb1842b14aa55d3b", tensors = "tests/goldens/esmc_large.safetensors=sha256:2b1ce3de5a7a27f171055f5c7aec4e7e50f0e809a1dc46508aaa0b472365015d" }
|
| 697 |
notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
|
| 698 |
fast_repo = "Synthyra/ESMplusplus_large"
|
| 699 |
fast_revision = "f813401638b3fddab09748aec1ad2bf537aa4208"
|
|
|
|
| 719 |
family = "esm_plusplus"
|
| 720 |
size_category = "xlarge"
|
| 721 |
generation_contract = "not_applicable"
|
| 722 |
+
official_golden = { metadata = "tests/goldens/esmc_6b.json=sha256:7e044e7e8d106d43169c4148521c54d0d09bb989624543d648247a110f1907d4", tensors = "tests/goldens/esmc_6b.safetensors=sha256:46139de6b28ab4f9e3244fb6ef3f259fc5e6518a3825f0fb4fd4a4fece6da8e6" }
|
| 723 |
notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
|
| 724 |
fast_repo = "Synthyra/ESMplusplus_6B"
|
| 725 |
fast_revision = "0d579cce3b0f09efa6b3baddf6cc3fd8c9b616c8"
|
|
|
|
| 731 |
"model-00004-of-00006.safetensors=sha256:e46c6113c89c6f3e9b072c1bef02d763a625c37bcd8f9da2ed9363891c9a0758",
|
| 732 |
"model-00005-of-00006.safetensors=sha256:6d92cb2bf9791de644de2ae86f8523d802ac3b4aaabfff0716ab6c2b97f6fb14",
|
| 733 |
"model-00006-of-00006.safetensors=sha256:5fc1a8632490bb34162823c35d0d591337b9e4195b22cc0560741397a6e9d0b3",
|
| 734 |
+
"model.safetensors.index.json=git-sha1:f30f0b6b35e11d09a516c078b0847ee023bd1817",
|
| 735 |
"special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
|
| 736 |
"tokenizer.json=git-sha1:f49735e56cebab0e791aeaae777757b7fd114f71",
|
| 737 |
"tokenizer_config.json=git-sha1:2985ed2b8aa8ecfb1d12f53f47d2b8a44cc21756",
|
|
|
|
| 757 |
tokenizer_source = "esmc_small"
|
| 758 |
size_category = "large"
|
| 759 |
generation_contract = "not_applicable"
|
| 760 |
+
official_golden = { metadata = "tests/goldens/esm3_small.json=sha256:e476bad7ccdfa2f908261a03e20116c0854eece292a553196f940b594088204d", tensors = "tests/goldens/esm3_small.safetensors=sha256:251e050926a1f2426401bac6b93b4cb00041d0b0a5c8a458d0ef0e6a5d0d87c3" }
|
| 761 |
fast_repo = "Synthyra/ESM3_small"
|
| 762 |
fast_revision = "7ddb5a740f9e5f93933eb6410c0ee8684bc63ec1"
|
| 763 |
fast_files = [
|
|
|
|
| 783 |
family = "e1"
|
| 784 |
size_category = "small"
|
| 785 |
generation_contract = "not_applicable"
|
| 786 |
+
official_golden = { metadata = "tests/goldens/e1_150m.json=sha256:e7520331135fd4c98f52f21c470dbba5cb5f4defb418494c63199fb000621e65", tensors = "tests/goldens/e1_150m.safetensors=sha256:03a2e93e7b3e54b12f99eea7678ff80828922f92c3683d4909346a05d74815bc" }
|
| 787 |
fast_repo = "Synthyra/Profluent-E1-150M"
|
| 788 |
fast_revision = "7c5f3bbf697226a2e0900db7a100f9201774a907"
|
| 789 |
fast_files = [
|
|
|
|
| 802 |
family = "e1"
|
| 803 |
size_category = "medium"
|
| 804 |
generation_contract = "not_applicable"
|
| 805 |
+
official_golden = { metadata = "tests/goldens/e1_300m.json=sha256:d97637bf789fed61b4ad575f904f37befcea42e9436e8d841e8f8f2cff1d1b17", tensors = "tests/goldens/e1_300m.safetensors=sha256:d125ea866899d2788330d6877d77c48c69febbf7f01d629f4107f9647cfa13ff" }
|
| 806 |
fast_repo = "Synthyra/Profluent-E1-300M"
|
| 807 |
fast_revision = "5ef52c0ad2ae2578f40622696b763523810e8e26"
|
| 808 |
fast_files = [
|
|
|
|
| 821 |
family = "e1"
|
| 822 |
size_category = "large"
|
| 823 |
generation_contract = "not_applicable"
|
| 824 |
+
official_golden = { metadata = "tests/goldens/e1_600m.json=sha256:0c9afc83b96ed8ad7f339a26df4ba5853e7994ad54dd3cd837deb9869c0cd2a9", tensors = "tests/goldens/e1_600m.safetensors=sha256:d470d6833f0b38bec070aa37e9f3c49717ea82d31c794af843550bb7e56253a6" }
|
| 825 |
fast_repo = "Synthyra/Profluent-E1-600M"
|
| 826 |
fast_revision = "6c8bf0ec83b0e0178677c528b101efffd0677742"
|
| 827 |
fast_files = [
|
|
|
|
| 840 |
family = "dplm"
|
| 841 |
size_category = "small"
|
| 842 |
generation_contract = "required"
|
| 843 |
+
official_golden = { metadata = "tests/goldens/dplm_150m.json=sha256:541167ffcd12b2d7e101046b2d17e800c70ba5db1b561212575301f2d1c407f8", tensors = "tests/goldens/dplm_150m.safetensors=sha256:f8e4ddd580d4708dad2d7da002735b2a01d69ece478c46c4932eb2f3933dc836" }
|
| 844 |
fast_repo = "Synthyra/DPLM-150M"
|
| 845 |
fast_revision = "90ba742754151a774f3b7ed580170d0a76b3e69d"
|
| 846 |
fast_files = [
|
|
|
|
| 865 |
family = "dplm"
|
| 866 |
size_category = "large"
|
| 867 |
generation_contract = "required"
|
| 868 |
+
official_golden = { metadata = "tests/goldens/dplm_650m.json=sha256:2bd482c94b1b6b3ac901b5ff9b06e2d189cda1a8fe2f8ff32e5d52b64c2c446b", tensors = "tests/goldens/dplm_650m.safetensors=sha256:70e2c46ed722942d3b628b556b92c9994688cb4853b278eb1e40a3dfbbd9dfc4" }
|
| 869 |
fast_repo = "Synthyra/DPLM-650M"
|
| 870 |
fast_revision = "05dc16d97c5c028aed924c9ed681cee4ab609760"
|
| 871 |
fast_files = [
|
|
|
|
| 890 |
family = "dplm"
|
| 891 |
size_category = "xlarge"
|
| 892 |
generation_contract = "required"
|
| 893 |
+
official_golden = { metadata = "tests/goldens/dplm_3b.json=sha256:0f8d8fb7df562c30abf730abe55db4a8c13d40877dd21772375fe1f34030159a", tensors = "tests/goldens/dplm_3b.safetensors=sha256:6942218079232a8185b1ae6e1578474c7784722e28f528b76ec89ca77777e2b9" }
|
| 894 |
fast_repo = "Synthyra/DPLM-3B"
|
| 895 |
fast_revision = "7d764dd3d70ecf1ac0e64693de64a0064aacac65"
|
| 896 |
fast_files = [
|
|
|
|
| 920 |
family = "dplm2"
|
| 921 |
size_category = "small"
|
| 922 |
generation_contract = "required"
|
| 923 |
+
official_golden = { metadata = "tests/goldens/dplm2_150m.json=sha256:5f2c496e4b557faf70bdde03b531f753fc7f20c5868bab79dcf378ee358781a1", tensors = "tests/goldens/dplm2_150m.safetensors=sha256:7390559bc54f27f08972c8f0adcf17954065450d937b3984303c1ae1ec003855" }
|
| 924 |
artifact_source = "official"
|
| 925 |
canonical_state_sha256 = "82e1751f59052b8de72b082517557db47947e8d9b4ac2f11278369e6c0cbf001"
|
| 926 |
fast_repo = "Synthyra/DPLM2-150M"
|
|
|
|
| 947 |
family = "dplm2"
|
| 948 |
size_category = "large"
|
| 949 |
generation_contract = "required"
|
| 950 |
+
official_golden = { metadata = "tests/goldens/dplm2_650m.json=sha256:af7b8c4fab48773e1ddd2cfd941cf3d518ea27ed8519f467ddf0da788a0aa6be", tensors = "tests/goldens/dplm2_650m.safetensors=sha256:c45f4ca267183b11dd06244ed3c3f39cf5b6139f778733fa63a6e2b5a46b0678" }
|
| 951 |
artifact_source = "official"
|
| 952 |
canonical_state_sha256 = "cba76b6602d2258de9fffff953b608d93cb8ef4a9e89b0bbd27e160c81e78bb4"
|
| 953 |
fast_repo = "Synthyra/DPLM2-650M"
|
|
|
|
| 976 |
# The pinned public sampler fails before generation because cls_token_id is None.
|
| 977 |
# State, tokenizer, and inference parity remain required for this checkpoint.
|
| 978 |
generation_contract = "official_unavailable"
|
| 979 |
+
official_golden = { metadata = "tests/goldens/dplm2_3b.json=sha256:ded7978cf84da1d56d8b4693418030ec3b0086bf6466be2242fb0ce424296b9f", tensors = "tests/goldens/dplm2_3b.safetensors=sha256:7490f771f8b8f8137b333f98d6b7dbd2cb48708c8649d4459279a6711b9e5870" }
|
| 980 |
notes = "The pinned official DPLM2-3B sampler fails before generation, so live generation equivalence cannot be established for this checkpoint. State, tokenizer, and inference parity remain required."
|
| 981 |
artifact_source = "official"
|
| 982 |
canonical_state_sha256 = "8c46ec09115dbe6cbfb91d94ab5e906369d57e27fe620a7741c6f8cb1b6ca890"
|
|
|
|
| 1009 |
family = "ankh"
|
| 1010 |
size_category = "medium"
|
| 1011 |
generation_contract = "required"
|
| 1012 |
+
official_golden = { metadata = "tests/goldens/ankh_base.json=sha256:21f129b7b71c026cbd3b3fdb712af06d5b6f256b487ef8b01fcc506d2bc6c7b6", tensors = "tests/goldens/ankh_base.safetensors=sha256:449db3638117d5b6027380fdf4b05698bb156024b2b3f7e0ceb295a86e5ec756" }
|
| 1013 |
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 1014 |
artifact_source = "official"
|
| 1015 |
canonical_state_sha256 = "cdd8d30d88e5bf41f44e1eef4470d8e46607aba5f7c7c805b06c035b89c8c16f"
|
|
|
|
| 1038 |
family = "ankh"
|
| 1039 |
size_category = "large"
|
| 1040 |
generation_contract = "required"
|
| 1041 |
+
official_golden = { metadata = "tests/goldens/ankh_large.json=sha256:8f269215ee29887661abb6c139139dd1782f5112b94eb63be968282a7f050e1b", tensors = "tests/goldens/ankh_large.safetensors=sha256:4a7aea66e91cf880b0b839495d7704831725b672fe169f8e7258b5643d77aff9" }
|
| 1042 |
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 1043 |
artifact_source = "official"
|
| 1044 |
canonical_state_sha256 = "e498a2e9aea76ef784cbe3e596c6b3f5e9a40e209ad837f7e3207099e4d74483"
|
|
|
|
| 1068 |
family = "ankh"
|
| 1069 |
size_category = "large"
|
| 1070 |
generation_contract = "required"
|
| 1071 |
+
official_golden = { metadata = "tests/goldens/ankh2_large.json=sha256:bdafcd179bc055ea223de43228801b1b6c35196741ac4cc4cca0571e3291e6dc", tensors = "tests/goldens/ankh2_large.safetensors=sha256:44d5fc5c74a0d166ceac54d12505a109f8338cb7090af2f37e39bd78545a3c52" }
|
| 1072 |
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 1073 |
artifact_source = "official"
|
| 1074 |
canonical_state_sha256 = "597c4fe2fa8711f11a25317905f1d62fa92905e55fdd5c0a79614cd9c9d2bca3"
|
|
|
|
| 1100 |
family = "ankh"
|
| 1101 |
size_category = "large"
|
| 1102 |
generation_contract = "required"
|
| 1103 |
+
official_golden = { metadata = "tests/goldens/ankh3_large.json=sha256:ab3a671260bdad481635d4a4be1b8072d5a2e02e3df1178202713c9a7e56af49", tensors = "tests/goldens/ankh3_large.safetensors=sha256:cc55274281e87d22cf7828117678df2e37a1ece543f5b401b65a0cd6f8b6fa27" }
|
| 1104 |
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 1105 |
artifact_source = "official"
|
| 1106 |
canonical_state_sha256 = "60acb7ef86e85dc0c51fc1edf4c8e69a0480049723b6b2c95e6e9faa720c112a"
|
|
|
|
| 1134 |
family = "ankh"
|
| 1135 |
size_category = "xlarge"
|
| 1136 |
generation_contract = "required"
|
| 1137 |
+
official_golden = { metadata = "tests/goldens/ankh3_xl.json=sha256:a2d302227ee616698af18502d03e2b6b136d589134058d82b018520f18498431", tensors = "tests/goldens/ankh3_xl.safetensors=sha256:d18c086a4849b220761019f219590fe0c1c6f17698c365ccb0a5a8804446286c" }
|
| 1138 |
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head. The official PyTorch shard index is deliberately excluded: the builder verifies every declared source shard directly and writes a new canonical safetensors index."
|
| 1139 |
artifact_source = "official"
|
| 1140 |
canonical_state_sha256 = "dd2188e0d2ca65232135714eef6de394239734d843ddae4928c7398685d858e7"
|
|
|
|
| 1227 |
size_category = "structure"
|
| 1228 |
generation_contract = "not_applicable"
|
| 1229 |
msa_conditioning = true
|
| 1230 |
+
official_golden = { metadata = "tests/goldens/esmfold2.json=sha256:60b9e8827615a73acd89ac23f13e3d0a412c17879fbf3308444546e6bbb4ff03", tensors = "tests/goldens/esmfold2.safetensors=sha256:a1a9f3b7a9f7e36ef6a7077d76fa1b1c8b5529ed42ac2cfa485797162ce866fb" }
|
| 1231 |
fast_repo = "Synthyra/ESMFold2"
|
| 1232 |
fast_revision = "cd5a0927cec585a778d983b99a8db23d2e9b281e"
|
| 1233 |
fast_files = [
|
|
|
|
| 1247 |
size_category = "structure"
|
| 1248 |
generation_contract = "not_applicable"
|
| 1249 |
msa_conditioning = false
|
| 1250 |
+
official_golden = { metadata = "tests/goldens/esmfold2_fast.json=sha256:659bb338be4584787faff619d1cbcb8766aece7b5facf8a84470e850299b662f", tensors = "tests/goldens/esmfold2_fast.safetensors=sha256:8da91024a2cc63984585267a18f595892ebe489b2a05c4f79a994c3ac5de2f40" }
|
| 1251 |
fast_repo = "Synthyra/ESMFold2-Fast"
|
| 1252 |
fast_revision = "407875bfcaa42552bfcb25acd67ee1888b790170"
|
| 1253 |
fast_files = [
|
|
|
|
| 1267 |
size_category = "structure"
|
| 1268 |
generation_contract = "not_applicable"
|
| 1269 |
msa_conditioning = true
|
| 1270 |
+
official_golden = { metadata = "tests/goldens/esmfold2_experimental_cutoff2025.json=sha256:2e42d958f7a99ca8edaf0d9fac8e1a58b78200df652116b7bc102ac799123633", tensors = "tests/goldens/esmfold2_experimental_cutoff2025.safetensors=sha256:8168f8692c4932e1160e733b15210836120223615f6190fb0ecfa2f940c2420f" }
|
| 1271 |
fast_repo = "Synthyra/ESMFold2-Experimental-Cutoff2025"
|
| 1272 |
fast_revision = "632ff4a9e68f1de78ee956a613267bdcdb5b354d"
|
| 1273 |
fast_files = [
|
|
|
|
| 1288 |
size_category = "structure"
|
| 1289 |
generation_contract = "not_applicable"
|
| 1290 |
msa_conditioning = false
|
| 1291 |
+
official_golden = { metadata = "tests/goldens/esmfold2_experimental_fast_cutoff2025.json=sha256:d41c54ca28270fc1c1b95485c5009990bd2c215d35ec5cc2c2fa611d096bad21", tensors = "tests/goldens/esmfold2_experimental_fast_cutoff2025.safetensors=sha256:97b58525aefd21c23ec6cb2defa3b3b41982c872dfe70d94306648edbe40a5b9" }
|
| 1292 |
fast_repo = "Synthyra/ESMFold2-Experimental-Fast-Cutoff2025"
|
| 1293 |
fast_revision = "8f022c2514a6c32692aaca078a8391d6bc6c4bac"
|
| 1294 |
fast_files = [
|
|
|
|
| 1305 |
|
| 1306 |
[[models]]
|
| 1307 |
id = "esmfold2_300"
|
| 1308 |
+
official_golden = { metadata = "tests/goldens/esmfold2_300.json=sha256:d4fd12f27352c53582bb40a8d85eab76d6d98f647ce7660a6f891a1dfe68c039", tensors = "tests/goldens/esmfold2_300.safetensors=sha256:40a954ff14edc7c2bff95a252241f08db4cae3821ca9b92ae216775cceed8e8b" }
|
| 1309 |
confidence_adaptation = { release = "v1", head_sha256 = "40fd7f3d82fcefe8ad20ab2b32a37a68a84b54a527a4bce6eb9437bad2b77e31", base_weight_sha256 = "44d6797c5efebf24753d502b40950e0874871c96ceea14f2d7f7e39cebac67fd", donor_repo = "biohub/ESMFold2-Experimental-Fast-Cutoff2025", donor_revision = "74b88548bf19688b8727432db0d698cb2e1d8783", donor_weight_sha256 = "4e903b740ad6ad704ec60881bfd593e0d6c874a630ffa0f0838276e0b665088f", training_url = "https://wandb.ai/lhallee/fastplms-confidence/runs/9558b6d23daf", evaluation_url = "https://huggingface.co/datasets/Synthyra/FastPLMs-artifacts/tree/309f353b07e0e46de4d77a5266d4eddd695538e3/confidence-v2/v2-reproduction-20260922/public/evaluation/esmfold2_300", evidence_path = "docs/evidence/confidence/esmfold2_300-v1.json", frozen_base = { repo = "Synthyra/ESMFold2-300", revision = "a38a62ae930d157484b331c2bf4241684573adba", files = ["config.json=git-sha1:47ec20cf8b234c3b41d6f3ae1bdfe95d4eb4849e", "model.safetensors=sha256:44d6797c5efebf24753d502b40950e0874871c96ceea14f2d7f7e39cebac67fd"] } }
|
| 1310 |
family = "esmfold2"
|
| 1311 |
size_category = "structure"
|
|
|
|
| 1325 |
|
| 1326 |
[[models]]
|
| 1327 |
id = "esmfold2_600"
|
| 1328 |
+
official_golden = { metadata = "tests/goldens/esmfold2_600.json=sha256:ea249ff118975f29979143ea081cfa1c0fb60cb1fa393dd99a12caf413524230", tensors = "tests/goldens/esmfold2_600.safetensors=sha256:69bbb8d53a816b29e979e4469728df0a6b662e4d0be81a76068b36974d2b909e" }
|
| 1329 |
confidence_adaptation = { release = "v1", head_sha256 = "e84726a050722e1b722712c87d17a5388bd3699e2520e4a59abb1d828dfb8de7", base_weight_sha256 = "11a53c1b4700b6c62a5a464fc3ec7076160c19e8584157f88e13275116cfd602", donor_repo = "biohub/ESMFold2-Experimental-Fast-Cutoff2025", donor_revision = "74b88548bf19688b8727432db0d698cb2e1d8783", donor_weight_sha256 = "4e903b740ad6ad704ec60881bfd593e0d6c874a630ffa0f0838276e0b665088f", training_url = "https://wandb.ai/lhallee/fastplms-confidence/runs/820d2cfa56c0", evaluation_url = "https://huggingface.co/datasets/Synthyra/FastPLMs-artifacts/tree/6e62186cd36b9047cc4691980076be9f76482192/confidence-v2/v2-reproduction-20260922/public/evaluation/esmfold2_600", evidence_path = "docs/evidence/confidence/esmfold2_600-v1.json", frozen_base = { repo = "Synthyra/ESMFold2-600", revision = "71c67d0b2b73dc245ea7c3cc0d0476439a882d08", files = ["config.json=git-sha1:8e271837cbdada96c4974c8e543f84065e0f06f1", "model.safetensors=sha256:11a53c1b4700b6c62a5a464fc3ec7076160c19e8584157f88e13275116cfd602"] } }
|
| 1330 |
family = "esmfold2"
|
| 1331 |
size_category = "structure"
|
|
|
|
| 1342 |
auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFold2Config", AutoModel = "fastplms.models.esmfold2.modeling_esmfold2_experimental.ESMFold2ExperimentalModel", AutoModelForSequenceClassification = "fastplms.models.esmfold2.modeling_esmfold2_classification.ESMFold2ExperimentalForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esmfold2.modeling_esmfold2_classification.ESMFold2ExperimentalForTokenClassification" }
|
| 1343 |
backbone_model = "esmc_large"
|
| 1344 |
backbone = { repo = "biohub/ESMC-600M-1500000", revision = "21af9cc429af76ebda6c48074fb624db4735aaaf", files = ["config.json=git-sha1:ec29f6009b21d710f64bf1c058f3a9710833d692", "model.safetensors=sha256:d6869f5ae0f11e5dc829b195e062e87cfcc2f851a08a5edbaf5d1083ae7f76cc", "tokenizer.json=git-sha1:81c797f56768b22dec0301fa771f018b7e43e98c", "tokenizer_config.json=git-sha1:f49f57b24a1c93bd544974811e8ecbd61b7fae89", "special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b"] }
|
| 1345 |
+
|
| 1346 |
+
# Parity goldens, uploaded 2026-09-22 so the reference tensors live somewhere durable
|
| 1347 |
+
# rather than only inside the GitHub repository. Each sha256 is the one already recorded
|
| 1348 |
+
# in that model's official_golden.tensors, so the existing integrity check is unchanged.
|
| 1349 |
+
|
| 1350 |
+
[[golden_artifacts]]
|
| 1351 |
+
id = "esm2_8m"
|
| 1352 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1353 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1354 |
+
path = "goldens/esm2_8m.safetensors"
|
| 1355 |
+
sha256 = "d08a7572cbef20b8b19b545bcb0427b7e9ae19b986d015c559cd5a3a2cfc8aa4"
|
| 1356 |
+
size = 537589
|
| 1357 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1358 |
+
|
| 1359 |
+
[[golden_artifacts]]
|
| 1360 |
+
id = "esm2_35m"
|
| 1361 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1362 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1363 |
+
path = "goldens/esm2_35m.safetensors"
|
| 1364 |
+
sha256 = "4f82d10286e16041c2f23365dfdd8508b911864633287dd76580425527c2d922"
|
| 1365 |
+
size = 779509
|
| 1366 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1367 |
+
|
| 1368 |
+
[[golden_artifacts]]
|
| 1369 |
+
id = "esm2_150m"
|
| 1370 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1371 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1372 |
+
path = "goldens/esm2_150m.safetensors"
|
| 1373 |
+
sha256 = "20ad986b8e6e09f36d0158f5939d1914e0809b84329695028d767aab93338621"
|
| 1374 |
+
size = 1021437
|
| 1375 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1376 |
+
|
| 1377 |
+
[[golden_artifacts]]
|
| 1378 |
+
id = "esm2_650m"
|
| 1379 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1380 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1381 |
+
path = "goldens/esm2_650m.safetensors"
|
| 1382 |
+
sha256 = "261a8b71c4b7c90b1f294031558ee0bf00d77a98352021eb78771200c8d40548"
|
| 1383 |
+
size = 1989117
|
| 1384 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1385 |
+
|
| 1386 |
+
[[golden_artifacts]]
|
| 1387 |
+
id = "esm2_3b"
|
| 1388 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1389 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1390 |
+
path = "goldens/esm2_3b.safetensors"
|
| 1391 |
+
sha256 = "61080e55e07db4a562a19e9b6b662e71a9fd3d8fee357aa3df00697d2fa58e33"
|
| 1392 |
+
size = 3924485
|
| 1393 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1394 |
+
|
| 1395 |
+
[[golden_artifacts]]
|
| 1396 |
+
id = "esmc_small"
|
| 1397 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1398 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1399 |
+
path = "goldens/esmc_small.safetensors"
|
| 1400 |
+
sha256 = "98219356e2845cbd10a80715b19c95632956d40567a7a993409c426b56040b06"
|
| 1401 |
+
size = 1165354
|
| 1402 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1403 |
+
|
| 1404 |
+
[[golden_artifacts]]
|
| 1405 |
+
id = "esmc_large"
|
| 1406 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1407 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1408 |
+
path = "goldens/esmc_large.safetensors"
|
| 1409 |
+
sha256 = "2b1ce3de5a7a27f171055f5c7aec4e7e50f0e809a1dc46508aaa0b472365015d"
|
| 1410 |
+
size = 1383082
|
| 1411 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1412 |
+
|
| 1413 |
+
[[golden_artifacts]]
|
| 1414 |
+
id = "esmc_6b"
|
| 1415 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1416 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1417 |
+
path = "goldens/esmc_6b.safetensors"
|
| 1418 |
+
sha256 = "46139de6b28ab4f9e3244fb6ef3f259fc5e6518a3825f0fb4fd4a4fece6da8e6"
|
| 1419 |
+
size = 2979762
|
| 1420 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1421 |
+
|
| 1422 |
+
[[golden_artifacts]]
|
| 1423 |
+
id = "esm3_small"
|
| 1424 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1425 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1426 |
+
path = "goldens/esm3_small.safetensors"
|
| 1427 |
+
sha256 = "251e050926a1f2426401bac6b93b4cb00041d0b0a5c8a458d0ef0e6a5d0d87c3"
|
| 1428 |
+
size = 2398877
|
| 1429 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1430 |
+
|
| 1431 |
+
[[golden_artifacts]]
|
| 1432 |
+
id = "e1_150m"
|
| 1433 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1434 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1435 |
+
path = "goldens/e1_150m.safetensors"
|
| 1436 |
+
sha256 = "03a2e93e7b3e54b12f99eea7678ff80828922f92c3683d4909346a05d74815bc"
|
| 1437 |
+
size = 958851
|
| 1438 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1439 |
+
|
| 1440 |
+
[[golden_artifacts]]
|
| 1441 |
+
id = "e1_300m"
|
| 1442 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1443 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1444 |
+
path = "goldens/e1_300m.safetensors"
|
| 1445 |
+
sha256 = "d125ea866899d2788330d6877d77c48c69febbf7f01d629f4107f9647cfa13ff"
|
| 1446 |
+
size = 1258379
|
| 1447 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1448 |
+
|
| 1449 |
+
[[golden_artifacts]]
|
| 1450 |
+
id = "e1_600m"
|
| 1451 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1452 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1453 |
+
path = "goldens/e1_600m.safetensors"
|
| 1454 |
+
sha256 = "d470d6833f0b38bec070aa37e9f3c49717ea82d31c794af843550bb7e56253a6"
|
| 1455 |
+
size = 1557899
|
| 1456 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1457 |
+
|
| 1458 |
+
[[golden_artifacts]]
|
| 1459 |
+
id = "dplm_150m"
|
| 1460 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1461 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1462 |
+
path = "goldens/dplm_150m.safetensors"
|
| 1463 |
+
sha256 = "f8e4ddd580d4708dad2d7da002735b2a01d69ece478c46c4932eb2f3933dc836"
|
| 1464 |
+
size = 1021437
|
| 1465 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1466 |
+
|
| 1467 |
+
[[golden_artifacts]]
|
| 1468 |
+
id = "dplm_650m"
|
| 1469 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1470 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1471 |
+
path = "goldens/dplm_650m.safetensors"
|
| 1472 |
+
sha256 = "70e2c46ed722942d3b628b556b92c9994688cb4853b278eb1e40a3dfbbd9dfc4"
|
| 1473 |
+
size = 1989117
|
| 1474 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1475 |
+
|
| 1476 |
+
[[golden_artifacts]]
|
| 1477 |
+
id = "dplm_3b"
|
| 1478 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1479 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1480 |
+
path = "goldens/dplm_3b.safetensors"
|
| 1481 |
+
sha256 = "6942218079232a8185b1ae6e1578474c7784722e28f528b76ec89ca77777e2b9"
|
| 1482 |
+
size = 3924485
|
| 1483 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1484 |
+
|
| 1485 |
+
[[golden_artifacts]]
|
| 1486 |
+
id = "dplm2_150m"
|
| 1487 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1488 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1489 |
+
path = "goldens/dplm2_150m.safetensors"
|
| 1490 |
+
sha256 = "7390559bc54f27f08972c8f0adcf17954065450d937b3984303c1ae1ec003855"
|
| 1491 |
+
size = 26826946
|
| 1492 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1493 |
+
|
| 1494 |
+
[[golden_artifacts]]
|
| 1495 |
+
id = "dplm2_650m"
|
| 1496 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1497 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1498 |
+
path = "goldens/dplm2_650m.safetensors"
|
| 1499 |
+
sha256 = "c45f4ca267183b11dd06244ed3c3f39cf5b6139f778733fa63a6e2b5a46b0678"
|
| 1500 |
+
size = 28762314
|
| 1501 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1502 |
+
|
| 1503 |
+
[[golden_artifacts]]
|
| 1504 |
+
id = "dplm2_3b"
|
| 1505 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1506 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1507 |
+
path = "goldens/dplm2_3b.safetensors"
|
| 1508 |
+
sha256 = "7490f771f8b8f8137b333f98d6b7dbd2cb48708c8649d4459279a6711b9e5870"
|
| 1509 |
+
size = 32633034
|
| 1510 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1511 |
+
|
| 1512 |
+
[[golden_artifacts]]
|
| 1513 |
+
id = "ankh_base"
|
| 1514 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1515 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1516 |
+
path = "goldens/ankh_base.safetensors"
|
| 1517 |
+
sha256 = "449db3638117d5b6027380fdf4b05698bb156024b2b3f7e0ceb295a86e5ec756"
|
| 1518 |
+
size = 860722
|
| 1519 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1520 |
+
|
| 1521 |
+
[[golden_artifacts]]
|
| 1522 |
+
id = "ankh_large"
|
| 1523 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1524 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1525 |
+
path = "goldens/ankh_large.safetensors"
|
| 1526 |
+
sha256 = "4a7aea66e91cf880b0b839495d7704831725b672fe169f8e7258b5643d77aff9"
|
| 1527 |
+
size = 1717818
|
| 1528 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1529 |
+
|
| 1530 |
+
[[golden_artifacts]]
|
| 1531 |
+
id = "ankh2_large"
|
| 1532 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1533 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1534 |
+
path = "goldens/ankh2_large.safetensors"
|
| 1535 |
+
sha256 = "44d5fc5c74a0d166ceac54d12505a109f8338cb7090af2f37e39bd78545a3c52"
|
| 1536 |
+
size = 1717818
|
| 1537 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1538 |
+
|
| 1539 |
+
[[golden_artifacts]]
|
| 1540 |
+
id = "ankh3_large"
|
| 1541 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1542 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1543 |
+
path = "goldens/ankh3_large.safetensors"
|
| 1544 |
+
sha256 = "cc55274281e87d22cf7828117678df2e37a1ece543f5b401b65a0cd6f8b6fa27"
|
| 1545 |
+
size = 1717818
|
| 1546 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1547 |
+
|
| 1548 |
+
[[golden_artifacts]]
|
| 1549 |
+
id = "ankh3_xl"
|
| 1550 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1551 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1552 |
+
path = "goldens/ankh3_xl.safetensors"
|
| 1553 |
+
sha256 = "d18c086a4849b220761019f219590fe0c1c6f17698c365ccb0a5a8804446286c"
|
| 1554 |
+
size = 2860602
|
| 1555 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1556 |
+
|
| 1557 |
+
[[golden_artifacts]]
|
| 1558 |
+
id = "esmfold"
|
| 1559 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1560 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1561 |
+
path = "goldens/esmfold.safetensors"
|
| 1562 |
+
sha256 = "873b1b325a43d8e0f35f355c8914a2a9fe611cc48763875e9e6a22e09ec9ebcb"
|
| 1563 |
+
size = 179144
|
| 1564 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1565 |
+
|
| 1566 |
+
[[golden_artifacts]]
|
| 1567 |
+
id = "esmfold2"
|
| 1568 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1569 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1570 |
+
path = "goldens/esmfold2.safetensors"
|
| 1571 |
+
sha256 = "a1a9f3b7a9f7e36ef6a7077d76fa1b1c8b5529ed42ac2cfa485797162ce866fb"
|
| 1572 |
+
size = 2573976
|
| 1573 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1574 |
+
|
| 1575 |
+
[[golden_artifacts]]
|
| 1576 |
+
id = "esmfold2_fast"
|
| 1577 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1578 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1579 |
+
path = "goldens/esmfold2_fast.safetensors"
|
| 1580 |
+
sha256 = "8da91024a2cc63984585267a18f595892ebe489b2a05c4f79a994c3ac5de2f40"
|
| 1581 |
+
size = 2573976
|
| 1582 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1583 |
+
|
| 1584 |
+
[[golden_artifacts]]
|
| 1585 |
+
id = "esmfold2_experimental_cutoff2025"
|
| 1586 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1587 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1588 |
+
path = "goldens/esmfold2_experimental_cutoff2025.safetensors"
|
| 1589 |
+
sha256 = "8168f8692c4932e1160e733b15210836120223615f6190fb0ecfa2f940c2420f"
|
| 1590 |
+
size = 1771064
|
| 1591 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1592 |
+
|
| 1593 |
+
[[golden_artifacts]]
|
| 1594 |
+
id = "esmfold2_experimental_fast_cutoff2025"
|
| 1595 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1596 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1597 |
+
path = "goldens/esmfold2_experimental_fast_cutoff2025.safetensors"
|
| 1598 |
+
sha256 = "97b58525aefd21c23ec6cb2defa3b3b41982c872dfe70d94306648edbe40a5b9"
|
| 1599 |
+
size = 1771064
|
| 1600 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1601 |
+
|
| 1602 |
+
[[golden_artifacts]]
|
| 1603 |
+
id = "esmfold2_300"
|
| 1604 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1605 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1606 |
+
path = "goldens/esmfold2_300.safetensors"
|
| 1607 |
+
sha256 = "40a954ff14edc7c2bff95a252241f08db4cae3821ca9b92ae216775cceed8e8b"
|
| 1608 |
+
size = 865376
|
| 1609 |
+
offline_behavior = "requires_cached_verified_file"
|
| 1610 |
+
|
| 1611 |
+
[[golden_artifacts]]
|
| 1612 |
+
id = "esmfold2_600"
|
| 1613 |
+
repository = "Synthyra/fastplms_parity_goldens"
|
| 1614 |
+
revision = "50b93a0fc2be21a36c2522118754c74c4a9631ad"
|
| 1615 |
+
path = "goldens/esmfold2_600.safetensors"
|
| 1616 |
+
sha256 = "69bbb8d53a816b29e979e4469728df0a6b662e4d0be81a76068b36974d2b909e"
|
| 1617 |
+
size = 865376
|
| 1618 |
+
offline_behavior = "requires_cached_verified_file"
|
fastplms/models/boltz/__init__.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
-
"""Transformers-compatible Boltz2 configuration, model, and structure outputs."""
|
| 2 |
-
|
| 3 |
-
from fastplms.models.boltz.modeling_boltz2 import (
|
| 4 |
-
Boltz2Config,
|
| 5 |
-
Boltz2Model,
|
| 6 |
-
Boltz2ModelOutput,
|
| 7 |
-
Boltz2StructureOutput,
|
| 8 |
-
)
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
__all__ = [
|
| 12 |
-
"Boltz2Config",
|
| 13 |
-
"Boltz2Model",
|
| 14 |
-
"Boltz2ModelOutput",
|
| 15 |
-
"Boltz2StructureOutput",
|
| 16 |
-
]
|
|
|
|
| 1 |
+
"""Transformers-compatible Boltz2 configuration, model, and structure outputs."""
|
| 2 |
+
|
| 3 |
+
from fastplms.models.boltz.modeling_boltz2 import (
|
| 4 |
+
Boltz2Config,
|
| 5 |
+
Boltz2Model,
|
| 6 |
+
Boltz2ModelOutput,
|
| 7 |
+
Boltz2StructureOutput,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"Boltz2Config",
|
| 13 |
+
"Boltz2Model",
|
| 14 |
+
"Boltz2ModelOutput",
|
| 15 |
+
"Boltz2StructureOutput",
|
| 16 |
+
]
|
fastplms/models/boltz/_pair_attention.py
CHANGED
|
@@ -1,51 +1,51 @@
|
|
| 1 |
-
"""Shared tensor operations for Boltz2 pair-biased attention layers."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import math
|
| 6 |
-
import torch
|
| 7 |
-
|
| 8 |
-
from torch import Tensor
|
| 9 |
-
|
| 10 |
-
|
| 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 |
-
# states: (b, l, d); num_heads: h
|
| 15 |
-
batch_size, sequence_length, width = states.shape # b, l, d
|
| 16 |
-
if width % num_heads:
|
| 17 |
-
raise ValueError(f"width {width} is not divisible by {num_heads} heads")
|
| 18 |
-
# states.view(...): (b, l, h, d_h)
|
| 19 |
-
return states.view(batch_size, sequence_length, num_heads, width // num_heads)
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def pair_biased_attention(
|
| 23 |
-
query_states: Tensor,
|
| 24 |
-
key_states: Tensor,
|
| 25 |
-
value_states: Tensor,
|
| 26 |
-
pair_bias: Tensor,
|
| 27 |
-
key_mask: Tensor,
|
| 28 |
-
mask_value: float,
|
| 29 |
-
) -> Tensor:
|
| 30 |
-
"""Return pair-biased attention values with shape ``(b, l_q, h, d_head)``."""
|
| 31 |
-
|
| 32 |
-
# query_states: (b, l_q, h, d_h); key_states, value_states: (b, l_k, h, d_h)
|
| 33 |
-
# pair_bias: (b, h, l_q, l_k); key_mask: (b, l_k)
|
| 34 |
-
head_dim = query_states.shape[-1] # d_h
|
| 35 |
-
with torch.autocast("cuda", enabled=False):
|
| 36 |
-
scores = torch.einsum(
|
| 37 |
-
"bihd,bjhd->bhij",
|
| 38 |
-
query_states.float(),
|
| 39 |
-
key_states.float(),
|
| 40 |
-
) # (b, h, l_q, l_k)
|
| 41 |
-
scores = scores / math.sqrt(head_dim) + pair_bias.float() # (b, h, l_q, l_k)
|
| 42 |
-
scores = (
|
| 43 |
-
scores + (1 - key_mask[:, None, None].float()) * -mask_value
|
| 44 |
-
) # (b, h, l_q, l_k)
|
| 45 |
-
probabilities = scores.softmax(dim=-1) # (b, h, l_q, l_k)
|
| 46 |
-
output = torch.einsum(
|
| 47 |
-
"bhij,bjhd->bihd",
|
| 48 |
-
probabilities,
|
| 49 |
-
value_states.float(),
|
| 50 |
-
) # (b, l_q, h, d_h)
|
| 51 |
-
return output.to(value_states.dtype) # (b, l_q, h, d_h)
|
|
|
|
| 1 |
+
"""Shared tensor operations for Boltz2 pair-biased attention layers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
|
| 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 |
+
# states: (b, l, d); num_heads: h
|
| 15 |
+
batch_size, sequence_length, width = states.shape # b, l, d
|
| 16 |
+
if width % num_heads:
|
| 17 |
+
raise ValueError(f"width {width} is not divisible by {num_heads} heads")
|
| 18 |
+
# states.view(...): (b, l, h, d_h)
|
| 19 |
+
return states.view(batch_size, sequence_length, num_heads, width // num_heads)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def pair_biased_attention(
|
| 23 |
+
query_states: Tensor,
|
| 24 |
+
key_states: Tensor,
|
| 25 |
+
value_states: Tensor,
|
| 26 |
+
pair_bias: Tensor,
|
| 27 |
+
key_mask: Tensor,
|
| 28 |
+
mask_value: float,
|
| 29 |
+
) -> Tensor:
|
| 30 |
+
"""Return pair-biased attention values with shape ``(b, l_q, h, d_head)``."""
|
| 31 |
+
|
| 32 |
+
# query_states: (b, l_q, h, d_h); key_states, value_states: (b, l_k, h, d_h)
|
| 33 |
+
# pair_bias: (b, h, l_q, l_k); key_mask: (b, l_k)
|
| 34 |
+
head_dim = query_states.shape[-1] # d_h
|
| 35 |
+
with torch.autocast("cuda", enabled=False):
|
| 36 |
+
scores = torch.einsum(
|
| 37 |
+
"bihd,bjhd->bhij",
|
| 38 |
+
query_states.float(),
|
| 39 |
+
key_states.float(),
|
| 40 |
+
) # (b, h, l_q, l_k)
|
| 41 |
+
scores = scores / math.sqrt(head_dim) + pair_bias.float() # (b, h, l_q, l_k)
|
| 42 |
+
scores = (
|
| 43 |
+
scores + (1 - key_mask[:, None, None].float()) * -mask_value
|
| 44 |
+
) # (b, h, l_q, l_k)
|
| 45 |
+
probabilities = scores.softmax(dim=-1) # (b, h, l_q, l_k)
|
| 46 |
+
output = torch.einsum(
|
| 47 |
+
"bhij,bjhd->bihd",
|
| 48 |
+
probabilities,
|
| 49 |
+
value_states.float(),
|
| 50 |
+
) # (b, l_q, h, d_h)
|
| 51 |
+
return output.to(value_states.dtype) # (b, l_q, h, d_h)
|
fastplms/models/boltz/cif_writer.py
CHANGED
|
@@ -1,150 +1,150 @@
|
|
| 1 |
-
"""Write Boltz2 coordinates and confidence values as an mmCIF atom table."""
|
| 2 |
-
|
| 3 |
-
import numpy as np
|
| 4 |
-
import torch
|
| 5 |
-
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
|
| 8 |
-
from .minimal_structures import ProteinStructureTemplate
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def _confidence_per_atom(
|
| 12 |
-
plddt: torch.Tensor | None,
|
| 13 |
-
atom_to_residue: list[int],
|
| 14 |
-
num_atoms: int,
|
| 15 |
-
sample_index: int,
|
| 16 |
-
) -> np.ndarray:
|
| 17 |
-
# plddt: (n_i,) or (n_s, n_i); atom_to_residue: length n_a; num_atoms: n_a
|
| 18 |
-
if plddt is None:
|
| 19 |
-
return np.ones((num_atoms,), dtype=np.float32) * 100.0 # (n_a,)
|
| 20 |
-
|
| 21 |
-
values = plddt.detach().cpu() # (n_i,) or (n_s, n_i)
|
| 22 |
-
if values.ndim == 1:
|
| 23 |
-
values = values.unsqueeze(0) # (1, n_i)
|
| 24 |
-
if values.ndim != 2:
|
| 25 |
-
raise ValueError("Expected pLDDT tensor S with shape (n_samples, n_items).")
|
| 26 |
-
# values: (n_s, n_i) beyond this point
|
| 27 |
-
if not 0 <= sample_index < values.shape[0]:
|
| 28 |
-
raise IndexError("sample_index out of range for pLDDT.")
|
| 29 |
-
|
| 30 |
-
selected = values[sample_index] # (n_i,)
|
| 31 |
-
if selected.shape[0] == num_atoms:
|
| 32 |
-
return (selected.numpy() * 100.0).astype(np.float32) # (n_a,)
|
| 33 |
-
|
| 34 |
-
num_residues = max(atom_to_residue) + 1 # n_r
|
| 35 |
-
if selected.shape[0] == num_residues:
|
| 36 |
-
expanded = np.zeros((num_atoms,), dtype=np.float32) # (n_a,)
|
| 37 |
-
selected_np = selected.numpy() # (n_r,)
|
| 38 |
-
for atom_idx, residue_idx in enumerate(atom_to_residue):
|
| 39 |
-
expanded[atom_idx] = selected_np[residue_idx] * 100.0 # () -> ()
|
| 40 |
-
return expanded # (n_a,)
|
| 41 |
-
|
| 42 |
-
raise ValueError(
|
| 43 |
-
"pLDDT item count must match either atoms or residues: "
|
| 44 |
-
f"received {selected.shape[0]}, expected {num_atoms} atoms or "
|
| 45 |
-
f"{num_residues} residues."
|
| 46 |
-
)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def write_cif(
|
| 50 |
-
structure_template: ProteinStructureTemplate,
|
| 51 |
-
atom_coords: torch.Tensor,
|
| 52 |
-
atom_mask: torch.Tensor,
|
| 53 |
-
output_path: str,
|
| 54 |
-
plddt: torch.Tensor | None = None,
|
| 55 |
-
sample_index: int = 0,
|
| 56 |
-
) -> str:
|
| 57 |
-
# atom_coords: (n_a, 3) or (n_s, n_a, 3)
|
| 58 |
-
# atom_mask: (n_a,) or (n_m, n_a); plddt: (n_i,) or (n_s, n_i)
|
| 59 |
-
coords = atom_coords.detach().cpu() # (n_a, 3) or (n_s, n_a, 3)
|
| 60 |
-
if coords.ndim == 2:
|
| 61 |
-
coords = coords.unsqueeze(0) # (1, n_a, 3)
|
| 62 |
-
if coords.ndim != 3 or coords.shape[-1] != 3:
|
| 63 |
-
raise ValueError(
|
| 64 |
-
"Expected coordinate tensor X with shape (n_samples, n_atoms, 3)."
|
| 65 |
-
)
|
| 66 |
-
# coords: (n_s, n_a, 3) beyond this point
|
| 67 |
-
if not 0 <= sample_index < coords.shape[0]:
|
| 68 |
-
raise IndexError("sample_index out of range.")
|
| 69 |
-
selected_coords_tensor = coords[sample_index] # (n_a, 3)
|
| 70 |
-
all_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor)) # (n_a, 3)
|
| 71 |
-
if torch.any(all_non_finite):
|
| 72 |
-
raise ValueError(
|
| 73 |
-
"CIF export received non-finite coordinates. "
|
| 74 |
-
f"Non-finite count: {int(all_non_finite.sum().item())}"
|
| 75 |
-
)
|
| 76 |
-
selected_coords = selected_coords_tensor.numpy() # (n_a, 3)
|
| 77 |
-
|
| 78 |
-
mask = atom_mask.detach().cpu() # (n_a,) or (n_m, n_a)
|
| 79 |
-
if mask.ndim == 2:
|
| 80 |
-
mask = mask[0] # (n_a,)
|
| 81 |
-
if mask.ndim != 1:
|
| 82 |
-
raise ValueError("Expected atom mask M with shape (n_atoms,).")
|
| 83 |
-
# mask: (n_a,) beyond this point
|
| 84 |
-
if mask.shape[0] != selected_coords.shape[0]:
|
| 85 |
-
raise ValueError("Atom mask/coord size mismatch.")
|
| 86 |
-
if not torch.any(mask > 0):
|
| 87 |
-
raise ValueError("Atom mask has no valid atoms for CIF export.")
|
| 88 |
-
valid_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor[mask > 0]))
|
| 89 |
-
# valid_non_finite: (n_v, 3), where n_v is the valid-atom count
|
| 90 |
-
if torch.any(valid_non_finite):
|
| 91 |
-
raise ValueError(
|
| 92 |
-
"CIF export has non-finite coordinates in unmasked atoms. "
|
| 93 |
-
f"Non-finite count: {int(valid_non_finite.sum().item())}"
|
| 94 |
-
)
|
| 95 |
-
|
| 96 |
-
b_iso = _confidence_per_atom(
|
| 97 |
-
plddt=plddt,
|
| 98 |
-
atom_to_residue=structure_template.atom_residue_index,
|
| 99 |
-
num_atoms=structure_template.num_atoms,
|
| 100 |
-
sample_index=sample_index,
|
| 101 |
-
) # (n_a,)
|
| 102 |
-
if b_iso.shape[0] != structure_template.num_atoms:
|
| 103 |
-
raise RuntimeError("CIF confidence values do not match the structure atom count.")
|
| 104 |
-
|
| 105 |
-
lines = [
|
| 106 |
-
"data_boltz2_prediction",
|
| 107 |
-
"#",
|
| 108 |
-
"loop_",
|
| 109 |
-
"_atom_site.group_PDB",
|
| 110 |
-
"_atom_site.id",
|
| 111 |
-
"_atom_site.type_symbol",
|
| 112 |
-
"_atom_site.label_atom_id",
|
| 113 |
-
"_atom_site.label_comp_id",
|
| 114 |
-
"_atom_site.label_asym_id",
|
| 115 |
-
"_atom_site.label_seq_id",
|
| 116 |
-
"_atom_site.Cartn_x",
|
| 117 |
-
"_atom_site.Cartn_y",
|
| 118 |
-
"_atom_site.Cartn_z",
|
| 119 |
-
"_atom_site.occupancy",
|
| 120 |
-
"_atom_site.B_iso_or_equiv",
|
| 121 |
-
"_atom_site.pdbx_PDB_model_num",
|
| 122 |
-
]
|
| 123 |
-
|
| 124 |
-
atom_id = 1
|
| 125 |
-
for idx in range(structure_template.num_atoms):
|
| 126 |
-
if mask[idx] <= 0:
|
| 127 |
-
continue
|
| 128 |
-
|
| 129 |
-
residue_idx = structure_template.atom_residue_index[idx]
|
| 130 |
-
residue_name = structure_template.residue_names[residue_idx]
|
| 131 |
-
atom_name = structure_template.atom_names[idx]
|
| 132 |
-
element = structure_template.atom_elements[idx]
|
| 133 |
-
chain_id = structure_template.atom_chain_id[idx]
|
| 134 |
-
x_val, y_val, z_val = selected_coords[idx].tolist() # (3,) -> three scalars
|
| 135 |
-
b_factor = float(b_iso[idx]) # () -> Python float
|
| 136 |
-
|
| 137 |
-
line = (
|
| 138 |
-
f"ATOM {atom_id} {element} {atom_name} {residue_name} {chain_id} "
|
| 139 |
-
f"{residue_idx + 1} {x_val:.3f} {y_val:.3f} {z_val:.3f} 1.00 {b_factor:.2f} 1"
|
| 140 |
-
)
|
| 141 |
-
lines.append(line)
|
| 142 |
-
atom_id += 1
|
| 143 |
-
|
| 144 |
-
lines.append("#")
|
| 145 |
-
text = "\n".join(lines) + "\n"
|
| 146 |
-
|
| 147 |
-
out_path = Path(output_path)
|
| 148 |
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 149 |
-
out_path.write_text(text, encoding="utf-8")
|
| 150 |
-
return str(out_path)
|
|
|
|
| 1 |
+
"""Write Boltz2 coordinates and confidence values as an mmCIF atom table."""
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from .minimal_structures import ProteinStructureTemplate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _confidence_per_atom(
|
| 12 |
+
plddt: torch.Tensor | None,
|
| 13 |
+
atom_to_residue: list[int],
|
| 14 |
+
num_atoms: int,
|
| 15 |
+
sample_index: int,
|
| 16 |
+
) -> np.ndarray:
|
| 17 |
+
# plddt: (n_i,) or (n_s, n_i); atom_to_residue: length n_a; num_atoms: n_a
|
| 18 |
+
if plddt is None:
|
| 19 |
+
return np.ones((num_atoms,), dtype=np.float32) * 100.0 # (n_a,)
|
| 20 |
+
|
| 21 |
+
values = plddt.detach().cpu() # (n_i,) or (n_s, n_i)
|
| 22 |
+
if values.ndim == 1:
|
| 23 |
+
values = values.unsqueeze(0) # (1, n_i)
|
| 24 |
+
if values.ndim != 2:
|
| 25 |
+
raise ValueError("Expected pLDDT tensor S with shape (n_samples, n_items).")
|
| 26 |
+
# values: (n_s, n_i) beyond this point
|
| 27 |
+
if not 0 <= sample_index < values.shape[0]:
|
| 28 |
+
raise IndexError("sample_index out of range for pLDDT.")
|
| 29 |
+
|
| 30 |
+
selected = values[sample_index] # (n_i,)
|
| 31 |
+
if selected.shape[0] == num_atoms:
|
| 32 |
+
return (selected.numpy() * 100.0).astype(np.float32) # (n_a,)
|
| 33 |
+
|
| 34 |
+
num_residues = max(atom_to_residue) + 1 # n_r
|
| 35 |
+
if selected.shape[0] == num_residues:
|
| 36 |
+
expanded = np.zeros((num_atoms,), dtype=np.float32) # (n_a,)
|
| 37 |
+
selected_np = selected.numpy() # (n_r,)
|
| 38 |
+
for atom_idx, residue_idx in enumerate(atom_to_residue):
|
| 39 |
+
expanded[atom_idx] = selected_np[residue_idx] * 100.0 # () -> ()
|
| 40 |
+
return expanded # (n_a,)
|
| 41 |
+
|
| 42 |
+
raise ValueError(
|
| 43 |
+
"pLDDT item count must match either atoms or residues: "
|
| 44 |
+
f"received {selected.shape[0]}, expected {num_atoms} atoms or "
|
| 45 |
+
f"{num_residues} residues."
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def write_cif(
|
| 50 |
+
structure_template: ProteinStructureTemplate,
|
| 51 |
+
atom_coords: torch.Tensor,
|
| 52 |
+
atom_mask: torch.Tensor,
|
| 53 |
+
output_path: str,
|
| 54 |
+
plddt: torch.Tensor | None = None,
|
| 55 |
+
sample_index: int = 0,
|
| 56 |
+
) -> str:
|
| 57 |
+
# atom_coords: (n_a, 3) or (n_s, n_a, 3)
|
| 58 |
+
# atom_mask: (n_a,) or (n_m, n_a); plddt: (n_i,) or (n_s, n_i)
|
| 59 |
+
coords = atom_coords.detach().cpu() # (n_a, 3) or (n_s, n_a, 3)
|
| 60 |
+
if coords.ndim == 2:
|
| 61 |
+
coords = coords.unsqueeze(0) # (1, n_a, 3)
|
| 62 |
+
if coords.ndim != 3 or coords.shape[-1] != 3:
|
| 63 |
+
raise ValueError(
|
| 64 |
+
"Expected coordinate tensor X with shape (n_samples, n_atoms, 3)."
|
| 65 |
+
)
|
| 66 |
+
# coords: (n_s, n_a, 3) beyond this point
|
| 67 |
+
if not 0 <= sample_index < coords.shape[0]:
|
| 68 |
+
raise IndexError("sample_index out of range.")
|
| 69 |
+
selected_coords_tensor = coords[sample_index] # (n_a, 3)
|
| 70 |
+
all_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor)) # (n_a, 3)
|
| 71 |
+
if torch.any(all_non_finite):
|
| 72 |
+
raise ValueError(
|
| 73 |
+
"CIF export received non-finite coordinates. "
|
| 74 |
+
f"Non-finite count: {int(all_non_finite.sum().item())}"
|
| 75 |
+
)
|
| 76 |
+
selected_coords = selected_coords_tensor.numpy() # (n_a, 3)
|
| 77 |
+
|
| 78 |
+
mask = atom_mask.detach().cpu() # (n_a,) or (n_m, n_a)
|
| 79 |
+
if mask.ndim == 2:
|
| 80 |
+
mask = mask[0] # (n_a,)
|
| 81 |
+
if mask.ndim != 1:
|
| 82 |
+
raise ValueError("Expected atom mask M with shape (n_atoms,).")
|
| 83 |
+
# mask: (n_a,) beyond this point
|
| 84 |
+
if mask.shape[0] != selected_coords.shape[0]:
|
| 85 |
+
raise ValueError("Atom mask/coord size mismatch.")
|
| 86 |
+
if not torch.any(mask > 0):
|
| 87 |
+
raise ValueError("Atom mask has no valid atoms for CIF export.")
|
| 88 |
+
valid_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor[mask > 0]))
|
| 89 |
+
# valid_non_finite: (n_v, 3), where n_v is the valid-atom count
|
| 90 |
+
if torch.any(valid_non_finite):
|
| 91 |
+
raise ValueError(
|
| 92 |
+
"CIF export has non-finite coordinates in unmasked atoms. "
|
| 93 |
+
f"Non-finite count: {int(valid_non_finite.sum().item())}"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
b_iso = _confidence_per_atom(
|
| 97 |
+
plddt=plddt,
|
| 98 |
+
atom_to_residue=structure_template.atom_residue_index,
|
| 99 |
+
num_atoms=structure_template.num_atoms,
|
| 100 |
+
sample_index=sample_index,
|
| 101 |
+
) # (n_a,)
|
| 102 |
+
if b_iso.shape[0] != structure_template.num_atoms:
|
| 103 |
+
raise RuntimeError("CIF confidence values do not match the structure atom count.")
|
| 104 |
+
|
| 105 |
+
lines = [
|
| 106 |
+
"data_boltz2_prediction",
|
| 107 |
+
"#",
|
| 108 |
+
"loop_",
|
| 109 |
+
"_atom_site.group_PDB",
|
| 110 |
+
"_atom_site.id",
|
| 111 |
+
"_atom_site.type_symbol",
|
| 112 |
+
"_atom_site.label_atom_id",
|
| 113 |
+
"_atom_site.label_comp_id",
|
| 114 |
+
"_atom_site.label_asym_id",
|
| 115 |
+
"_atom_site.label_seq_id",
|
| 116 |
+
"_atom_site.Cartn_x",
|
| 117 |
+
"_atom_site.Cartn_y",
|
| 118 |
+
"_atom_site.Cartn_z",
|
| 119 |
+
"_atom_site.occupancy",
|
| 120 |
+
"_atom_site.B_iso_or_equiv",
|
| 121 |
+
"_atom_site.pdbx_PDB_model_num",
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
atom_id = 1
|
| 125 |
+
for idx in range(structure_template.num_atoms):
|
| 126 |
+
if mask[idx] <= 0:
|
| 127 |
+
continue
|
| 128 |
+
|
| 129 |
+
residue_idx = structure_template.atom_residue_index[idx]
|
| 130 |
+
residue_name = structure_template.residue_names[residue_idx]
|
| 131 |
+
atom_name = structure_template.atom_names[idx]
|
| 132 |
+
element = structure_template.atom_elements[idx]
|
| 133 |
+
chain_id = structure_template.atom_chain_id[idx]
|
| 134 |
+
x_val, y_val, z_val = selected_coords[idx].tolist() # (3,) -> three scalars
|
| 135 |
+
b_factor = float(b_iso[idx]) # () -> Python float
|
| 136 |
+
|
| 137 |
+
line = (
|
| 138 |
+
f"ATOM {atom_id} {element} {atom_name} {residue_name} {chain_id} "
|
| 139 |
+
f"{residue_idx + 1} {x_val:.3f} {y_val:.3f} {z_val:.3f} 1.00 {b_factor:.2f} 1"
|
| 140 |
+
)
|
| 141 |
+
lines.append(line)
|
| 142 |
+
atom_id += 1
|
| 143 |
+
|
| 144 |
+
lines.append("#")
|
| 145 |
+
text = "\n".join(lines) + "\n"
|
| 146 |
+
|
| 147 |
+
out_path = Path(output_path)
|
| 148 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 149 |
+
out_path.write_text(text, encoding="utf-8")
|
| 150 |
+
return str(out_path)
|
fastplms/models/boltz/minimal_featurizer.py
CHANGED
|
@@ -1,853 +1,853 @@
|
|
| 1 |
-
"""Construct Boltz2 protein features from canonical atom tables and conformers."""
|
| 2 |
-
|
| 3 |
-
import math
|
| 4 |
-
import numpy as np
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from torch.nn.functional import one_hot
|
| 8 |
-
|
| 9 |
-
from . import vb_const as const
|
| 10 |
-
from .minimal_structures import ProteinStructureTemplate
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
_ELEMENT_TO_Z = {
|
| 14 |
-
"H": 1,
|
| 15 |
-
"C": 6,
|
| 16 |
-
"N": 7,
|
| 17 |
-
"O": 8,
|
| 18 |
-
"P": 15,
|
| 19 |
-
"S": 16,
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
# Canonical formal charges and tetrahedral chirality values extracted from the
|
| 23 |
-
# pinned Boltz2 molecule archive. Values use Boltz2's public feature encoding.
|
| 24 |
-
_FORMAL_CHARGES = {
|
| 25 |
-
("ARG", "NH1"): 1.0,
|
| 26 |
-
("HIS", "ND1"): 1.0,
|
| 27 |
-
("LYS", "NZ"): 1.0,
|
| 28 |
-
}
|
| 29 |
-
_CHIRAL_ATOMS = {
|
| 30 |
-
"ALA": frozenset({"CA"}),
|
| 31 |
-
"ARG": frozenset({"CA"}),
|
| 32 |
-
"ASN": frozenset({"CA"}),
|
| 33 |
-
"ASP": frozenset({"CA"}),
|
| 34 |
-
"CYS": frozenset({"CA"}),
|
| 35 |
-
"GLN": frozenset({"CA"}),
|
| 36 |
-
"GLU": frozenset({"CA"}),
|
| 37 |
-
"HIS": frozenset({"CA"}),
|
| 38 |
-
"ILE": frozenset({"CA", "CB"}),
|
| 39 |
-
"LEU": frozenset({"CA"}),
|
| 40 |
-
"LYS": frozenset({"CA"}),
|
| 41 |
-
"MET": frozenset({"CA"}),
|
| 42 |
-
"PHE": frozenset({"CA"}),
|
| 43 |
-
"PRO": frozenset({"CA"}),
|
| 44 |
-
"SER": frozenset({"CA"}),
|
| 45 |
-
"THR": frozenset({"CA", "CB"}),
|
| 46 |
-
"TRP": frozenset({"CA"}),
|
| 47 |
-
"TYR": frozenset({"CA"}),
|
| 48 |
-
"UNK": frozenset({"CA"}),
|
| 49 |
-
"VAL": frozenset({"CA"}),
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def _normalize_sequence(sequence: str) -> str:
|
| 54 |
-
if not isinstance(sequence, str):
|
| 55 |
-
raise TypeError("Amino acid sequence must be a string.")
|
| 56 |
-
seq = sequence.strip().upper()
|
| 57 |
-
if not seq:
|
| 58 |
-
raise ValueError("Amino acid sequence must be non-empty.")
|
| 59 |
-
for aa in seq:
|
| 60 |
-
if aa not in const.prot_letter_to_token:
|
| 61 |
-
raise ValueError(f"Unsupported residue code '{aa}'.")
|
| 62 |
-
return seq
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def _atom_name_to_element(atom_name: str) -> str:
|
| 66 |
-
name = atom_name.strip().upper()
|
| 67 |
-
if len(name) == 0:
|
| 68 |
-
return "C"
|
| 69 |
-
if name[0].isdigit():
|
| 70 |
-
name = name[1:]
|
| 71 |
-
if len(name) >= 2 and name[0:2] in ("CL", "BR", "FE", "MG", "ZN", "NA", "CA"):
|
| 72 |
-
return name[0]
|
| 73 |
-
return name[0]
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def _atom_name_to_codes(atom_name: str) -> torch.Tensor:
|
| 77 |
-
clipped = atom_name.strip()[:4]
|
| 78 |
-
vals = [ord(ch) - 32 for ch in clipped]
|
| 79 |
-
while len(vals) < 4:
|
| 80 |
-
vals.append(0)
|
| 81 |
-
out = torch.tensor(vals, dtype=torch.long) # (4,)
|
| 82 |
-
if not (torch.all(out >= 0) and torch.all(out < 64)):
|
| 83 |
-
raise ValueError(f"Invalid atom-name encoding for '{atom_name}'.")
|
| 84 |
-
return out
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
# Raw first-conformer coordinates extracted at full float32 precision from the
|
| 88 |
-
# hash-pinned Boltz2 molecule archive. The feature builder applies the official
|
| 89 |
-
# seeded centering, rotation, and translation policy below.
|
| 90 |
-
_RDKIT_CONFORMERS: dict[str, dict[str, list[float]]] = {
|
| 91 |
-
"ALA": {
|
| 92 |
-
"N": [-0.9241582155227661, 1.1821246147155762, 0.712748110294342],
|
| 93 |
-
"CA": [-0.2663755416870117, -0.08827890455722809, 0.4008508622646332],
|
| 94 |
-
"C": [1.1188693046569824, 0.1387452930212021, -0.14366498589515686],
|
| 95 |
-
"O": [1.2882297039031982, 0.8058497309684753, -1.2001261711120605],
|
| 96 |
-
"CB": [-1.1134333610534668, -0.8915302753448486, -0.5876986384391785],
|
| 97 |
-
},
|
| 98 |
-
"ARG": {
|
| 99 |
-
"N": [3.253326416015625, -1.699564814567566, -0.9852627515792847],
|
| 100 |
-
"CA": [2.191511392593384, -0.692007839679718, -0.9506462812423706],
|
| 101 |
-
"C": [2.80021333694458, 0.672307550907135, -1.1160222291946411],
|
| 102 |
-
"O": [2.465440034866333, 1.3987600803375244, -2.0899009704589844],
|
| 103 |
-
"CB": [1.3811039924621582, -0.7949594855308533, 0.36316436529159546],
|
| 104 |
-
"CG": [0.20344306528568268, 0.19287171959877014, 0.457282155752182],
|
| 105 |
-
"CD": [-0.902410626411438, -0.09953747689723969, -0.5671625137329102],
|
| 106 |
-
"NE": [-2.0867979526519775, 0.7146661281585693, -0.2911669909954071],
|
| 107 |
-
"CZ": [-3.018158197402954, 0.4804283082485199, 0.7831193804740906],
|
| 108 |
-
"NH1": [-4.080137729644775, 1.4154621362686157, 0.9938872456550598],
|
| 109 |
-
"NH2": [-2.9226150512695312, -0.5637124180793762, 1.5533623695373535],
|
| 110 |
-
},
|
| 111 |
-
"ASN": {
|
| 112 |
-
"N": [-1.5767980813980103, -1.7227835655212402, 0.2427234649658203],
|
| 113 |
-
"CA": [-0.7041534781455994, -0.5622008442878723, 0.42767149209976196],
|
| 114 |
-
"C": [-1.246688961982727, 0.6072975993156433, -0.3483567237854004],
|
| 115 |
-
"O": [-1.554918885231018, 0.47644123435020447, -1.5638835430145264],
|
| 116 |
-
"CB": [0.7263866662979126, -0.9032993316650391, -0.028588544577360153],
|
| 117 |
-
"CG": [1.6871562004089355, 0.20810098946094513, 0.28412675857543945],
|
| 118 |
-
"OD1": [2.19687557220459, 0.28916797041893005, 1.433493971824646],
|
| 119 |
-
"ND2": [1.996717095375061, 1.196706771850586, -0.6981719136238098],
|
| 120 |
-
},
|
| 121 |
-
"ASP": {
|
| 122 |
-
"N": [-0.11774874478578568, -1.6310220956802368, 0.374360054731369],
|
| 123 |
-
"CA": [-0.3847021758556366, -0.19290897250175476, 0.28121331334114075],
|
| 124 |
-
"C": [-1.806337594985962, 0.059408850967884064, -0.1472136378288269],
|
| 125 |
-
"O": [-2.2237648963928223, -0.36817988753318787, -1.257502555847168],
|
| 126 |
-
"CB": [0.5981687307357788, 0.4785478413105011, -0.6920451521873474],
|
| 127 |
-
"CG": [2.0088446140289307, 0.36018264293670654, -0.20304732024669647],
|
| 128 |
-
"OD1": [2.747267961502075, -0.5711832046508789, -0.6216948628425598],
|
| 129 |
-
"OD2": [2.49090313911438, 1.2577818632125854, 0.7443997859954834],
|
| 130 |
-
},
|
| 131 |
-
"CYS": {
|
| 132 |
-
"N": [-0.058702241629362106, 1.771048665046692, 0.2434082329273224],
|
| 133 |
-
"CA": [-0.06546340137720108, 0.47663936018943787, -0.4456723928451538],
|
| 134 |
-
"C": [-1.273919701576233, -0.3504071533679962, -0.08361759781837463],
|
| 135 |
-
"O": [-1.7324607372283936, -0.34313708543777466, 1.0909277200698853],
|
| 136 |
-
"CB": [1.2432398796081543, -0.28917407989501953, -0.19254730641841888],
|
| 137 |
-
"SG": [1.484184980392456, -0.7191365361213684, 1.5649693012237549],
|
| 138 |
-
},
|
| 139 |
-
"GLN": {
|
| 140 |
-
"N": [-1.8543237447738647, -1.0024770498275757, -1.6278940439224243],
|
| 141 |
-
"CA": [-1.292184591293335, -0.6786512732505798, -0.3153715133666992],
|
| 142 |
-
"C": [-2.2264492511749268, 0.25171151757240295, 0.4067918062210083],
|
| 143 |
-
"O": [-2.7255098819732666, -0.08834805339574814, 1.5129321813583374],
|
| 144 |
-
"CB": [0.11662524193525314, -0.06213820353150368, -0.45663803815841675],
|
| 145 |
-
"CG": [0.8140791058540344, 0.10461423546075821, 0.9002283811569214],
|
| 146 |
-
"CD": [2.195122003555298, 0.6569428443908691, 0.7154991030693054],
|
| 147 |
-
"OE1": [2.3870291709899902, 1.9003654718399048, 0.7825236916542053],
|
| 148 |
-
"NE2": [3.287371873855591, -0.21307705342769623, 0.41959717869758606],
|
| 149 |
-
},
|
| 150 |
-
"GLU": {
|
| 151 |
-
"N": [-1.3492857217788696, -1.114271640777588, -1.3739068508148193],
|
| 152 |
-
"CA": [-1.2765676975250244, -0.506334662437439, -0.04287439212203026],
|
| 153 |
-
"C": [-1.9195666313171387, 0.8547031879425049, -0.04214814677834511],
|
| 154 |
-
"O": [-1.9091347455978394, 1.5701956748962402, -1.080504298210144],
|
| 155 |
-
"CB": [0.18272103369235992, -0.43209096789360046, 0.45213374495506287],
|
| 156 |
-
"CG": [1.1058456897735596, 0.3659400939941406, -0.481784850358963],
|
| 157 |
-
"CD": [2.5090205669403076, 0.35135790705680847, 0.03646872192621231],
|
| 158 |
-
"OE1": [2.896414041519165, 1.2445733547210693, 0.8365193605422974],
|
| 159 |
-
"OE2": [3.3727896213531494, -0.6754258275032043, -0.33141785860061646],
|
| 160 |
-
},
|
| 161 |
-
"GLY": {
|
| 162 |
-
"N": [-1.291549801826477, 0.6080796122550964, -0.4228580892086029],
|
| 163 |
-
"CA": [-0.4895951449871063, -0.2882237136363983, 0.40191715955734253],
|
| 164 |
-
"C": [0.9350062608718872, -0.2543502449989319, -0.04786944016814232],
|
| 165 |
-
"O": [1.3473576307296753, -1.0836502313613892, -0.9022219777107239],
|
| 166 |
-
},
|
| 167 |
-
"HIS": {
|
| 168 |
-
"N": [1.0371263027191162, -1.5621215105056763, 0.4178937077522278],
|
| 169 |
-
"CA": [1.1980191469192505, -0.40241551399230957, -0.46312251687049866],
|
| 170 |
-
"C": [2.6561269760131836, -0.07123222947120667, -0.6407948136329651],
|
| 171 |
-
"O": [3.1795547008514404, -0.12371637672185898, -1.7859554290771484],
|
| 172 |
-
"CB": [0.44051393866539, 0.8116919994354248, 0.09850303083658218],
|
| 173 |
-
"CG": [-1.0356801748275757, 0.5519806742668152, 0.15254715085029602],
|
| 174 |
-
"ND1": [-1.9089158773422241, 0.6490958333015442, -0.968245267868042],
|
| 175 |
-
"CD2": [-1.7095766067504883, 0.1138235554099083, 1.2061808109283447],
|
| 176 |
-
"CE1": [-3.0751633644104004, 0.28129827976226807, -0.5511555075645447],
|
| 177 |
-
"NE2": [-3.0650453567504883, -0.07846951484680176, 0.8257263898849487],
|
| 178 |
-
},
|
| 179 |
-
"ILE": {
|
| 180 |
-
"N": [-1.2378733158111572, -1.8146690130233765, -0.16437499225139618],
|
| 181 |
-
"CA": [-1.2780015468597412, -0.3994033634662628, 0.22437156736850739],
|
| 182 |
-
"C": [-1.9879958629608154, 0.4497596025466919, -0.80372154712677],
|
| 183 |
-
"O": [-2.0291709899902344, 0.10288607329130173, -2.015406608581543],
|
| 184 |
-
"CB": [0.14225825667381287, 0.146738663315773, 0.5415176153182983],
|
| 185 |
-
"CG1": [1.1062071323394775, 0.04519597440958023, -0.6694174408912659],
|
| 186 |
-
"CG2": [0.7215384244918823, -0.5685611963272095, 1.7750537395477295],
|
| 187 |
-
"CD1": [2.371805191040039, 0.8852734565734863, -0.49045464396476746],
|
| 188 |
-
},
|
| 189 |
-
"LEU": {
|
| 190 |
-
"N": [1.6452248096466064, -1.017622470855713, -1.0626215934753418],
|
| 191 |
-
"CA": [1.3129304647445679, 0.08074887096881866, -0.15053576231002808],
|
| 192 |
-
"C": [2.431154727935791, 0.28310757875442505, 0.8357383608818054],
|
| 193 |
-
"O": [2.9231173992156982, -0.7013152837753296, 1.451209306716919],
|
| 194 |
-
"CB": [0.006050171796232462, -0.2141101360321045, 0.6198461651802063],
|
| 195 |
-
"CG": [-1.2534235715866089, -0.3772832751274109, -0.26637399196624756],
|
| 196 |
-
"CD1": [-2.455688714981079, -0.7604416012763977, 0.6070646643638611],
|
| 197 |
-
"CD2": [-1.5742294788360596, 0.8985711932182312, -1.059889554977417],
|
| 198 |
-
},
|
| 199 |
-
"LYS": {
|
| 200 |
-
"N": [-2.3938536643981934, -1.4751482009887695, -0.9336304068565369],
|
| 201 |
-
"CA": [-2.2094509601593018, -0.6203567981719971, 0.2421264797449112],
|
| 202 |
-
"C": [-3.4513463973999023, 0.1961250752210617, 0.4710204303264618],
|
| 203 |
-
"O": [-3.9688456058502197, 0.2503468096256256, 1.6188287734985352],
|
| 204 |
-
"CB": [-1.008726716041565, 0.3269270658493042, 0.0479682981967926],
|
| 205 |
-
"CG": [0.336457222700119, -0.41559070348739624, 0.047045741230249405],
|
| 206 |
-
"CD": [1.5088860988616943, 0.5716065168380737, -0.00632853340357542],
|
| 207 |
-
"CE": [2.853410005569458, -0.1678532361984253, -0.020898228511214256],
|
| 208 |
-
"NZ": [3.9690465927124023, 0.7765668630599976, -0.07007527351379395],
|
| 209 |
-
},
|
| 210 |
-
"MET": {
|
| 211 |
-
"N": [-1.743452548980713, -0.44039490818977356, 1.851780652999878],
|
| 212 |
-
"CA": [-1.2168668508529663, 0.4041755199432373, 0.7779996991157532],
|
| 213 |
-
"C": [-2.3118340969085693, 0.683431088924408, -0.2134547382593155],
|
| 214 |
-
"O": [-2.6660380363464355, 1.8705639839172363, -0.44476020336151123],
|
| 215 |
-
"CB": [-0.0005482881097123027, -0.262584388256073, 0.09933014959096909],
|
| 216 |
-
"CG": [0.6952047944068909, 0.6768104434013367, -0.8930339813232422],
|
| 217 |
-
"SD": [2.136805772781372, -0.14913176000118256, -1.6563159227371216],
|
| 218 |
-
"CE": [3.3404407501220703, 0.348065584897995, -0.38104408979415894],
|
| 219 |
-
},
|
| 220 |
-
"PHE": {
|
| 221 |
-
"N": [2.7976438999176025, -1.6209964752197266, -0.4779351055622101],
|
| 222 |
-
"CA": [1.5799994468688965, -0.8289614915847778, -0.6608061194419861],
|
| 223 |
-
"C": [1.9535551071166992, 0.5758142471313477, -1.0445665121078491],
|
| 224 |
-
"O": [1.5211102962493896, 1.072593331336975, -2.1190123558044434],
|
| 225 |
-
"CB": [0.7137221097946167, -0.8474348187446594, 0.6163292527198792],
|
| 226 |
-
"CG": [-0.6229045987129211, -0.18666480481624603, 0.3912639319896698],
|
| 227 |
-
"CD1": [-0.8259273767471313, 1.0978055000305176, 0.7381877303123474],
|
| 228 |
-
"CD2": [-1.7160141468048096, -0.9425657391548157, -0.2700555920600891],
|
| 229 |
-
"CE1": [-2.131312131881714, 1.741182565689087, 0.4800238013267517],
|
| 230 |
-
"CE2": [-2.8960046768188477, -0.3551671802997589, -0.505269467830658],
|
| 231 |
-
"CZ": [-3.11456298828125, 1.0511738061904907, -0.11009909957647324],
|
| 232 |
-
},
|
| 233 |
-
"PRO": {
|
| 234 |
-
"N": [-0.577339768409729, -0.5123927593231201, -1.150407314300537],
|
| 235 |
-
"CA": [0.5119893550872803, 0.23565764725208282, -0.5195383429527283],
|
| 236 |
-
"C": [1.8393173217773438, -0.46712079644203186, -0.6550371646881104],
|
| 237 |
-
"O": [2.0836586952209473, -1.166333556175232, -1.6751221418380737],
|
| 238 |
-
"CB": [0.1029478907585144, 0.45346882939338684, 0.9297074675559998],
|
| 239 |
-
"CG": [-1.4098080396652222, 0.5215330123901367, 0.8787456750869751],
|
| 240 |
-
"CD": [-1.7971025705337524, -0.06041542813181877, -0.47876954078674316],
|
| 241 |
-
},
|
| 242 |
-
"SER": {
|
| 243 |
-
"N": [0.8959873914718628, -1.423298716545105, -0.2692987024784088],
|
| 244 |
-
"CA": [-0.018787339329719543, -0.28751760721206665, -0.3863537311553955],
|
| 245 |
-
"C": [0.767615795135498, 0.9929561018943787, -0.42147380113601685],
|
| 246 |
-
"O": [0.5327092409133911, 1.8535151481628418, -1.3116248846054077],
|
| 247 |
-
"CB": [-0.9973848462104797, -0.2769756019115448, 0.7988247871398926],
|
| 248 |
-
"OG": [-1.8999865055084229, 0.7915746569633484, 0.691534698009491],
|
| 249 |
-
},
|
| 250 |
-
"THR": {
|
| 251 |
-
"N": [0.005045319441705942, 1.7655576467514038, 0.0639338344335556],
|
| 252 |
-
"CA": [0.4231603145599365, 0.3755212128162384, 0.2734847664833069],
|
| 253 |
-
"C": [1.7184523344039917, 0.11012311279773712, -0.4465082287788391],
|
| 254 |
-
"O": [1.9252793788909912, 0.5967074036598206, -1.5912789106369019],
|
| 255 |
-
"CB": [-0.6634359359741211, -0.615561306476593, -0.21173778176307678],
|
| 256 |
-
"OG1": [-1.0762907266616821, -0.29800114035606384, -1.5171915292739868],
|
| 257 |
-
"CG2": [-1.8869022130966187, -0.6176311373710632, 0.7084044218063354],
|
| 258 |
-
},
|
| 259 |
-
"TRP": {
|
| 260 |
-
"N": [-3.0439648628234863, 1.0628471374511719, 0.015692168846726418],
|
| 261 |
-
"CA": [-2.463679552078247, 0.024657616391777992, -0.8414031863212585],
|
| 262 |
-
"C": [-2.186711072921753, -1.2228965759277344, -0.04691409692168236],
|
| 263 |
-
"O": [-2.3364906311035156, -2.3535354137420654, -0.5830773115158081],
|
| 264 |
-
"CB": [-1.2062920331954956, 0.5195542573928833, -1.592403531074524],
|
| 265 |
-
"CG": [-0.06450498104095459, 0.884079098701477, -0.6835198402404785],
|
| 266 |
-
"CD1": [0.16109998524188995, 2.085163116455078, -0.14801783859729767],
|
| 267 |
-
"CD2": [0.9781660437583923, -0.005544683896005154, -0.17733941972255707],
|
| 268 |
-
"NE1": [1.3106837272644043, 2.0506503582000732, 0.698072075843811],
|
| 269 |
-
"CE2": [1.7528777122497559, 0.7044392228126526, 0.6196392774581909],
|
| 270 |
-
"CE3": [1.236093521118164, -1.4355946779251099, -0.4235689342021942],
|
| 271 |
-
"CZ2": [2.9070138931274414, 0.10196790844202042, 1.3070299625396729],
|
| 272 |
-
"CZ3": [2.2851555347442627, -2.007185697555542, 0.1949695348739624],
|
| 273 |
-
"CH2": [3.1585631370544434, -1.2034809589385986, 1.1002938747406006],
|
| 274 |
-
},
|
| 275 |
-
"TYR": {
|
| 276 |
-
"N": [-1.7950150966644287, 0.49119046330451965, -1.3951144218444824],
|
| 277 |
-
"CA": [-1.8436503410339355, -0.2694968581199646, -0.14338290691375732],
|
| 278 |
-
"C": [-3.240288019180298, -0.28215137124061584, 0.41974759101867676],
|
| 279 |
-
"O": [-3.8150112628936768, 0.7977103590965271, 0.7253130674362183],
|
| 280 |
-
"CB": [-0.8541494011878967, 0.30990728735923767, 0.8830214738845825],
|
| 281 |
-
"CG": [0.5672639012336731, 0.2125871181488037, 0.3916495442390442],
|
| 282 |
-
"CD1": [1.2694358825683594, -0.924761176109314, 0.5431669354438782],
|
| 283 |
-
"CD2": [1.1872504949569702, 1.3605931997299194, -0.31554359197616577],
|
| 284 |
-
"CE1": [2.6519992351531982, -1.0213464498519897, 0.029911600053310394],
|
| 285 |
-
"CE2": [2.4384164810180664, 1.2693941593170166, -0.7831515073776245],
|
| 286 |
-
"CZ": [3.211331367492676, 0.022552935406565666, -0.6021429300308228],
|
| 287 |
-
"OH": [4.513507843017578, -0.05260590463876724, -1.0929937362670898],
|
| 288 |
-
},
|
| 289 |
-
"VAL": {
|
| 290 |
-
"N": [0.9408224821090698, -1.2608877420425415, 0.652370810508728],
|
| 291 |
-
"CA": [0.7287879586219788, -0.3937721848487854, -0.5120788216590881],
|
| 292 |
-
"C": [1.7670493125915527, 0.6979415416717529, -0.5508439540863037],
|
| 293 |
-
"O": [2.2449657917022705, 1.1683598756790161, 0.5171348452568054],
|
| 294 |
-
"CB": [-0.7015937566757202, 0.21480272710323334, -0.5226467847824097],
|
| 295 |
-
"CG1": [-1.7662651538848877, -0.845522403717041, -0.8411717414855957],
|
| 296 |
-
"CG2": [-1.0500152111053467, 0.9374979138374329, 0.7892321944236755],
|
| 297 |
-
},
|
| 298 |
-
"UNK": {
|
| 299 |
-
"N": [1.6134154796600342, -1.304404377937317, -0.35241976380348206],
|
| 300 |
-
"CA": [0.6100443601608276, -0.23816797137260437, -0.3852081298828125],
|
| 301 |
-
"C": [1.0861350297927856, 0.9232651591300964, 0.4423142373561859],
|
| 302 |
-
"O": [1.374688982963562, 0.7629808187484741, 1.6591525077819824],
|
| 303 |
-
"CB": [-0.7607048153877258, -0.7552146315574646, 0.10266945511102676],
|
| 304 |
-
},
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
def _get_atom_position(res_name: str, atom_name: str, atom_idx: int) -> np.ndarray:
|
| 309 |
-
"""Get the canonical RDKit conformer position for an atom.
|
| 310 |
-
|
| 311 |
-
Uses pre-extracted positions from official Boltz2 mol files. Falls back
|
| 312 |
-
to a simple geometric placement for unknown residue/atom combinations.
|
| 313 |
-
"""
|
| 314 |
-
if res_name in _RDKIT_CONFORMERS and atom_name in _RDKIT_CONFORMERS[res_name]:
|
| 315 |
-
return np.array(_RDKIT_CONFORMERS[res_name][atom_name], dtype=np.float32) # (3,)
|
| 316 |
-
# Fallback for unknown atoms (should not happen for canonical AAs)
|
| 317 |
-
angle = (atom_idx + 1) * 0.7
|
| 318 |
-
radius = 1.4 + 0.03 * atom_idx
|
| 319 |
-
return np.array(
|
| 320 |
-
[
|
| 321 |
-
radius * math.cos(angle),
|
| 322 |
-
radius * math.sin(angle),
|
| 323 |
-
0.1 * ((atom_idx % 5) - 2),
|
| 324 |
-
],
|
| 325 |
-
dtype=np.float32,
|
| 326 |
-
) # (3,)
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
def _build_template(
|
| 330 |
-
sequence: str,
|
| 331 |
-
) -> tuple[
|
| 332 |
-
ProteinStructureTemplate,
|
| 333 |
-
list[str],
|
| 334 |
-
list[int],
|
| 335 |
-
list[int],
|
| 336 |
-
list[int],
|
| 337 |
-
list[np.ndarray],
|
| 338 |
-
list[int],
|
| 339 |
-
]:
|
| 340 |
-
residue_names: list[str] = []
|
| 341 |
-
residue_token_ids: list[int] = []
|
| 342 |
-
atom_names: list[str] = []
|
| 343 |
-
atom_elements: list[str] = []
|
| 344 |
-
atom_residue_index: list[int] = []
|
| 345 |
-
atom_chain_id: list[str] = []
|
| 346 |
-
atom_positions: list[np.ndarray] = [] # each: (3,)
|
| 347 |
-
residue_center_atom_idx: list[int] = []
|
| 348 |
-
residue_disto_atom_idx: list[int] = []
|
| 349 |
-
residue_frame_atom_idx: list[int] = []
|
| 350 |
-
|
| 351 |
-
global_atom_idx = 0
|
| 352 |
-
for res_idx, aa in enumerate(sequence):
|
| 353 |
-
token_name = const.prot_letter_to_token[aa]
|
| 354 |
-
residue_names.append(token_name)
|
| 355 |
-
residue_token_ids.append(const.token_ids[token_name])
|
| 356 |
-
|
| 357 |
-
residue_atoms = const.ref_atoms[token_name]
|
| 358 |
-
if not residue_atoms:
|
| 359 |
-
raise RuntimeError(f"No reference atoms for residue {token_name}.")
|
| 360 |
-
center_atom_name = const.res_to_center_atom[token_name]
|
| 361 |
-
disto_atom_name = const.res_to_disto_atom[token_name]
|
| 362 |
-
|
| 363 |
-
center_idx = -1
|
| 364 |
-
disto_idx = -1
|
| 365 |
-
n_idx = -1
|
| 366 |
-
ca_idx = -1
|
| 367 |
-
c_idx = -1
|
| 368 |
-
|
| 369 |
-
for local_idx, atom_name in enumerate(residue_atoms):
|
| 370 |
-
atom_names.append(atom_name)
|
| 371 |
-
element = _atom_name_to_element(atom_name)
|
| 372 |
-
atom_elements.append(element)
|
| 373 |
-
atom_residue_index.append(res_idx)
|
| 374 |
-
atom_chain_id.append("A")
|
| 375 |
-
|
| 376 |
-
atom_pos = _get_atom_position(token_name, atom_name, local_idx) # (3,)
|
| 377 |
-
atom_positions.append(atom_pos)
|
| 378 |
-
|
| 379 |
-
if atom_name == center_atom_name:
|
| 380 |
-
center_idx = global_atom_idx
|
| 381 |
-
if atom_name == disto_atom_name:
|
| 382 |
-
disto_idx = global_atom_idx
|
| 383 |
-
if atom_name == "N":
|
| 384 |
-
n_idx = global_atom_idx
|
| 385 |
-
if atom_name == "CA":
|
| 386 |
-
ca_idx = global_atom_idx
|
| 387 |
-
if atom_name == "C":
|
| 388 |
-
c_idx = global_atom_idx
|
| 389 |
-
global_atom_idx += 1
|
| 390 |
-
|
| 391 |
-
if center_idx == -1:
|
| 392 |
-
center_idx = global_atom_idx - len(residue_atoms)
|
| 393 |
-
if disto_idx == -1:
|
| 394 |
-
disto_idx = center_idx
|
| 395 |
-
if n_idx == -1:
|
| 396 |
-
n_idx = center_idx
|
| 397 |
-
if ca_idx == -1:
|
| 398 |
-
ca_idx = center_idx
|
| 399 |
-
if c_idx == -1:
|
| 400 |
-
c_idx = center_idx
|
| 401 |
-
|
| 402 |
-
residue_center_atom_idx.append(center_idx)
|
| 403 |
-
residue_disto_atom_idx.append(disto_idx)
|
| 404 |
-
residue_frame_atom_idx.extend([n_idx, ca_idx, c_idx])
|
| 405 |
-
|
| 406 |
-
template = ProteinStructureTemplate(
|
| 407 |
-
sequence=sequence,
|
| 408 |
-
residue_names=residue_names,
|
| 409 |
-
atom_names=atom_names,
|
| 410 |
-
atom_elements=atom_elements,
|
| 411 |
-
atom_residue_index=atom_residue_index,
|
| 412 |
-
atom_chain_id=atom_chain_id,
|
| 413 |
-
)
|
| 414 |
-
|
| 415 |
-
return (
|
| 416 |
-
template,
|
| 417 |
-
residue_names,
|
| 418 |
-
residue_token_ids,
|
| 419 |
-
residue_center_atom_idx,
|
| 420 |
-
residue_disto_atom_idx,
|
| 421 |
-
atom_positions,
|
| 422 |
-
residue_frame_atom_idx,
|
| 423 |
-
)
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
def _random_rotation_matrix() -> torch.Tensor:
|
| 427 |
-
"""Sample a uniform random 3x3 rotation matrix (Algorithm 19 from AF2/Boltz)."""
|
| 428 |
-
quaternion = torch.randn((1, 4), dtype=torch.float32) # (1, 4)
|
| 429 |
-
squared_norm = (quaternion * quaternion).sum(dim=1) # (1,)
|
| 430 |
-
norm = torch.sqrt(squared_norm) # (1,)
|
| 431 |
-
signed_norm = torch.where(quaternion[:, 0] < 0, -norm, norm) # (1,)
|
| 432 |
-
quaternion = quaternion / signed_norm[:, None] # (1, 4)
|
| 433 |
-
|
| 434 |
-
real, i, j, k = torch.unbind(quaternion, dim=-1) # each: (1,)
|
| 435 |
-
two_s = 2.0 / (quaternion * quaternion).sum(dim=-1) # (1,)
|
| 436 |
-
rotation = torch.stack(
|
| 437 |
-
(
|
| 438 |
-
1 - two_s * (j * j + k * k),
|
| 439 |
-
two_s * (i * j - k * real),
|
| 440 |
-
two_s * (i * k + j * real),
|
| 441 |
-
two_s * (i * j + k * real),
|
| 442 |
-
1 - two_s * (i * i + k * k),
|
| 443 |
-
two_s * (j * k - i * real),
|
| 444 |
-
two_s * (i * k - j * real),
|
| 445 |
-
two_s * (j * k + i * real),
|
| 446 |
-
1 - two_s * (i * i + j * j),
|
| 447 |
-
),
|
| 448 |
-
dim=-1,
|
| 449 |
-
) # (1, 9)
|
| 450 |
-
return rotation.reshape(1, 3, 3)[0] # (3, 3)
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
def _center_and_augment_atoms_per_residue(
|
| 454 |
-
atom_positions: torch.Tensor,
|
| 455 |
-
atom_residue_index: list[int],
|
| 456 |
-
num_residues: int,
|
| 457 |
-
) -> torch.Tensor:
|
| 458 |
-
"""Apply Boltz2's seeded conformer augmentation per residue.
|
| 459 |
-
|
| 460 |
-
The pinned implementation intentionally excludes the final residue because
|
| 461 |
-
it iterates to the maximum reference-space identifier rather than through it.
|
| 462 |
-
"""
|
| 463 |
-
# atom_positions: (a, 3), where a is the unpadded atom count.
|
| 464 |
-
result = atom_positions.clone() # (a, 3)
|
| 465 |
-
residue_index_tensor = torch.tensor(atom_residue_index, dtype=torch.long) # (a,)
|
| 466 |
-
for residue_idx in range(max(num_residues - 1, 0)):
|
| 467 |
-
residue_mask = residue_index_tensor == residue_idx # (a,)
|
| 468 |
-
if not torch.any(residue_mask):
|
| 469 |
-
raise RuntimeError(f"Residue index {residue_idx} has no atoms.")
|
| 470 |
-
residue_coords = result[residue_mask][None] # (1, a_r, 3)
|
| 471 |
-
resolved_mask = torch.ones(
|
| 472 |
-
residue_coords.shape[:2], dtype=torch.bool, device=residue_coords.device
|
| 473 |
-
) # (1, a_r)
|
| 474 |
-
residue_center = torch.sum(
|
| 475 |
-
residue_coords * resolved_mask[:, :, None], dim=1, keepdim=True
|
| 476 |
-
) / torch.sum(resolved_mask[:, :, None], dim=1, keepdim=True) # (1, 1, 3)
|
| 477 |
-
residue_coords = residue_coords - residue_center # (1, a_r, 3)
|
| 478 |
-
rotation = _random_rotation_matrix()[None] # (1, 3, 3)
|
| 479 |
-
residue_coords = torch.einsum(
|
| 480 |
-
"bmd,bds->bms", residue_coords, rotation
|
| 481 |
-
) # (1, a_r, 3)
|
| 482 |
-
residue_coords = (
|
| 483 |
-
residue_coords + torch.randn_like(residue_coords[:, 0:1, :])
|
| 484 |
-
) # (1, a_r, 3)
|
| 485 |
-
result[residue_mask] = residue_coords[0] # (a_r, 3)
|
| 486 |
-
return result # (a, 3)
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
def build_boltz2_features(
|
| 490 |
-
amino_acid_sequence: str,
|
| 491 |
-
num_bins: int = 64,
|
| 492 |
-
atoms_per_window_queries: int = 32,
|
| 493 |
-
) -> tuple[dict[str, torch.Tensor], ProteinStructureTemplate]:
|
| 494 |
-
sequence = _normalize_sequence(amino_acid_sequence)
|
| 495 |
-
(
|
| 496 |
-
template,
|
| 497 |
-
residue_names,
|
| 498 |
-
residue_token_ids,
|
| 499 |
-
residue_center_atom_idx,
|
| 500 |
-
residue_disto_atom_idx,
|
| 501 |
-
atom_positions_np,
|
| 502 |
-
residue_frame_atom_idx_flat,
|
| 503 |
-
) = _build_template(sequence)
|
| 504 |
-
|
| 505 |
-
num_tokens = len(residue_names)
|
| 506 |
-
num_atoms = len(atom_positions_np)
|
| 507 |
-
if num_tokens <= 0 or num_atoms <= 0:
|
| 508 |
-
raise RuntimeError("Boltz2 feature construction produced an empty protein template.")
|
| 509 |
-
|
| 510 |
-
# t is the token count, a is the unpadded atom count, and a_p is the padded atom count.
|
| 511 |
-
atom_positions = torch.tensor(
|
| 512 |
-
np.asarray(atom_positions_np), dtype=torch.float32
|
| 513 |
-
) # (a, 3)
|
| 514 |
-
atom_positions = _center_and_augment_atoms_per_residue(
|
| 515 |
-
atom_positions=atom_positions,
|
| 516 |
-
atom_residue_index=template.atom_residue_index,
|
| 517 |
-
num_residues=num_tokens,
|
| 518 |
-
) # (a, 3)
|
| 519 |
-
|
| 520 |
-
token_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0) # (1, t)
|
| 521 |
-
residue_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0) # (1, t)
|
| 522 |
-
asym_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 523 |
-
entity_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 524 |
-
sym_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 525 |
-
mol_type = torch.full(
|
| 526 |
-
(1, num_tokens),
|
| 527 |
-
fill_value=const.chain_type_ids["PROTEIN"],
|
| 528 |
-
dtype=torch.long,
|
| 529 |
-
) # (1, t)
|
| 530 |
-
|
| 531 |
-
res_type_ids = torch.tensor(residue_token_ids, dtype=torch.long) # (t,)
|
| 532 |
-
res_type = one_hot(
|
| 533 |
-
res_type_ids, num_classes=const.num_tokens
|
| 534 |
-
).unsqueeze(0) # (1, t, n_res_type)
|
| 535 |
-
|
| 536 |
-
# token_bonds encodes explicit covalent cross-links from structure bonds,
|
| 537 |
-
# NOT backbone peptide bonds (those are implicit via residue_index + asym_id).
|
| 538 |
-
# For a standard single-chain protein without cross-links, this is all zeros.
|
| 539 |
-
# This matches the official Boltz2 featurizer (featurizerv2.py lines 696-705).
|
| 540 |
-
token_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.float32) # (t, t)
|
| 541 |
-
type_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.long) # (t, t)
|
| 542 |
-
token_bonds = token_bonds.unsqueeze(0).unsqueeze(-1) # (1, t, t, 1)
|
| 543 |
-
type_bonds = type_bonds.unsqueeze(0) # (1, t, t)
|
| 544 |
-
|
| 545 |
-
token_pad_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 546 |
-
token_resolved_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 547 |
-
token_disto_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 548 |
-
|
| 549 |
-
num_contact_classes = len(const.contact_conditioning_info)
|
| 550 |
-
unspecified_id = const.contact_conditioning_info["UNSPECIFIED"]
|
| 551 |
-
contact_ids = torch.full(
|
| 552 |
-
(num_tokens, num_tokens),
|
| 553 |
-
fill_value=unspecified_id,
|
| 554 |
-
dtype=torch.long,
|
| 555 |
-
) # (t, t)
|
| 556 |
-
contact_conditioning = one_hot(
|
| 557 |
-
contact_ids,
|
| 558 |
-
num_classes=num_contact_classes,
|
| 559 |
-
).unsqueeze(0) # (1, t, t, n_contact)
|
| 560 |
-
contact_threshold = torch.zeros(
|
| 561 |
-
(1, num_tokens, num_tokens), dtype=torch.float32
|
| 562 |
-
) # (1, t, t)
|
| 563 |
-
|
| 564 |
-
if "x-ray diffraction" not in const.method_types_ids:
|
| 565 |
-
raise RuntimeError("Boltz2 method metadata omits x-ray diffraction.")
|
| 566 |
-
method_feature = torch.full(
|
| 567 |
-
(1, num_tokens),
|
| 568 |
-
fill_value=const.method_types_ids["x-ray diffraction"],
|
| 569 |
-
dtype=torch.long,
|
| 570 |
-
) # (1, t)
|
| 571 |
-
modified = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 572 |
-
cyclic_period = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 573 |
-
affinity_token_mask = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 574 |
-
|
| 575 |
-
ref_pos = atom_positions.unsqueeze(0) # (1, a, 3)
|
| 576 |
-
atom_pad_mask = torch.ones((1, num_atoms), dtype=torch.float32) # (1, a)
|
| 577 |
-
atom_resolved_mask = torch.ones((1, num_atoms), dtype=torch.bool) # (1, a)
|
| 578 |
-
|
| 579 |
-
atom_name_codes = torch.stack(
|
| 580 |
-
[_atom_name_to_codes(atom_name) for atom_name in template.atom_names],
|
| 581 |
-
dim=0,
|
| 582 |
-
) # (a, 4)
|
| 583 |
-
ref_atom_name_chars = one_hot(
|
| 584 |
-
atom_name_codes, num_classes=64
|
| 585 |
-
).unsqueeze(0) # (1, a, 4, 64)
|
| 586 |
-
|
| 587 |
-
atomic_numbers = []
|
| 588 |
-
for element in template.atom_elements:
|
| 589 |
-
z_value = _ELEMENT_TO_Z[element] if element in _ELEMENT_TO_Z else _ELEMENT_TO_Z["C"]
|
| 590 |
-
if z_value >= const.num_elements:
|
| 591 |
-
raise RuntimeError(
|
| 592 |
-
f"Atomic number {z_value} exceeds the Boltz2 element vocabulary."
|
| 593 |
-
)
|
| 594 |
-
atomic_numbers.append(z_value)
|
| 595 |
-
ref_element = one_hot(
|
| 596 |
-
torch.tensor(atomic_numbers, dtype=torch.long),
|
| 597 |
-
num_classes=const.num_elements,
|
| 598 |
-
).unsqueeze(0) # (1, a, n_element)
|
| 599 |
-
|
| 600 |
-
ref_charge_values = []
|
| 601 |
-
ref_chirality_values = []
|
| 602 |
-
for atom_name, residue_idx in zip(
|
| 603 |
-
template.atom_names,
|
| 604 |
-
template.atom_residue_index,
|
| 605 |
-
strict=True,
|
| 606 |
-
):
|
| 607 |
-
residue_name = residue_names[residue_idx]
|
| 608 |
-
ref_charge_values.append(_FORMAL_CHARGES.get((residue_name, atom_name), 0.0))
|
| 609 |
-
ref_chirality_values.append(
|
| 610 |
-
2 if atom_name in _CHIRAL_ATOMS.get(residue_name, frozenset()) else 0
|
| 611 |
-
)
|
| 612 |
-
ref_charge = torch.tensor(ref_charge_values, dtype=torch.float32).unsqueeze(0) # (1, a)
|
| 613 |
-
ref_chirality = torch.tensor(
|
| 614 |
-
ref_chirality_values, dtype=torch.long
|
| 615 |
-
).unsqueeze(0) # (1, a)
|
| 616 |
-
ref_space_uid = torch.tensor(
|
| 617 |
-
template.atom_residue_index, dtype=torch.long
|
| 618 |
-
).unsqueeze(0) # (1, a)
|
| 619 |
-
|
| 620 |
-
atom_to_token = one_hot(
|
| 621 |
-
torch.tensor(template.atom_residue_index, dtype=torch.long),
|
| 622 |
-
num_classes=num_tokens,
|
| 623 |
-
).unsqueeze(0) # (1, a, t)
|
| 624 |
-
token_to_rep_atom = one_hot(
|
| 625 |
-
torch.tensor(residue_disto_atom_idx, dtype=torch.long),
|
| 626 |
-
num_classes=num_atoms,
|
| 627 |
-
).unsqueeze(0) # (1, t, a)
|
| 628 |
-
token_to_center_atom = one_hot(
|
| 629 |
-
torch.tensor(residue_center_atom_idx, dtype=torch.long),
|
| 630 |
-
num_classes=num_atoms,
|
| 631 |
-
).unsqueeze(0) # (1, t, a)
|
| 632 |
-
r_set_to_rep_atom = token_to_center_atom.clone() # (1, t, a)
|
| 633 |
-
|
| 634 |
-
num_backbone_classes = (
|
| 635 |
-
1 + len(const.protein_backbone_atom_index) + len(const.nucleic_backbone_atom_index)
|
| 636 |
-
)
|
| 637 |
-
backbone_ids = []
|
| 638 |
-
for atom_name in template.atom_names:
|
| 639 |
-
if atom_name in const.protein_backbone_atom_index:
|
| 640 |
-
backbone_ids.append(const.protein_backbone_atom_index[atom_name] + 1)
|
| 641 |
-
else:
|
| 642 |
-
backbone_ids.append(0)
|
| 643 |
-
atom_backbone_feat = one_hot(
|
| 644 |
-
torch.tensor(backbone_ids, dtype=torch.long),
|
| 645 |
-
num_classes=num_backbone_classes,
|
| 646 |
-
).unsqueeze(0) # (1, a, n_backbone)
|
| 647 |
-
|
| 648 |
-
# X contains no observed coordinates for sequence-only inference.
|
| 649 |
-
coords = torch.zeros((1, 1, num_atoms, 3), dtype=torch.float32) # (1, 1, a, 3)
|
| 650 |
-
disto_coords_ensemble = torch.zeros(
|
| 651 |
-
(1, 1, num_tokens, 3),
|
| 652 |
-
dtype=torch.float32,
|
| 653 |
-
) # (1, 1, t, 3)
|
| 654 |
-
|
| 655 |
-
bfactor = torch.zeros((1, num_atoms), dtype=torch.float32) # (1, a)
|
| 656 |
-
atom_plddt = torch.ones((1, num_atoms), dtype=torch.float32) # (1, a)
|
| 657 |
-
|
| 658 |
-
if atoms_per_window_queries <= 0:
|
| 659 |
-
raise ValueError("atoms_per_window_queries must be positive.")
|
| 660 |
-
pad_atoms = (
|
| 661 |
-
(num_atoms - 1) // atoms_per_window_queries + 1
|
| 662 |
-
) * atoms_per_window_queries - num_atoms
|
| 663 |
-
if pad_atoms > 0:
|
| 664 |
-
ref_pos = torch.nn.functional.pad(
|
| 665 |
-
ref_pos, (0, 0, 0, pad_atoms), value=0.0
|
| 666 |
-
) # (1, a_p, 3)
|
| 667 |
-
atom_pad_mask = torch.nn.functional.pad(
|
| 668 |
-
atom_pad_mask, (0, pad_atoms), value=0.0
|
| 669 |
-
) # (1, a_p)
|
| 670 |
-
atom_resolved_mask = torch.nn.functional.pad(
|
| 671 |
-
atom_resolved_mask,
|
| 672 |
-
(0, pad_atoms),
|
| 673 |
-
value=0.0,
|
| 674 |
-
) # (1, a_p)
|
| 675 |
-
ref_atom_name_chars = torch.nn.functional.pad(
|
| 676 |
-
ref_atom_name_chars,
|
| 677 |
-
(0, 0, 0, 0, 0, pad_atoms),
|
| 678 |
-
value=0.0,
|
| 679 |
-
) # (1, a_p, 4, 64)
|
| 680 |
-
ref_element = torch.nn.functional.pad(
|
| 681 |
-
ref_element, (0, 0, 0, pad_atoms), value=0.0
|
| 682 |
-
) # (1, a_p, n_element)
|
| 683 |
-
ref_charge = torch.nn.functional.pad(
|
| 684 |
-
ref_charge, (0, pad_atoms), value=0.0
|
| 685 |
-
) # (1, a_p)
|
| 686 |
-
ref_chirality = torch.nn.functional.pad(
|
| 687 |
-
ref_chirality, (0, pad_atoms), value=0
|
| 688 |
-
) # (1, a_p)
|
| 689 |
-
atom_backbone_feat = torch.nn.functional.pad(
|
| 690 |
-
atom_backbone_feat,
|
| 691 |
-
(0, 0, 0, pad_atoms),
|
| 692 |
-
value=0.0,
|
| 693 |
-
) # (1, a_p, n_backbone)
|
| 694 |
-
ref_space_uid = torch.nn.functional.pad(
|
| 695 |
-
ref_space_uid, (0, pad_atoms), value=0
|
| 696 |
-
) # (1, a_p)
|
| 697 |
-
coords = torch.nn.functional.pad(
|
| 698 |
-
coords, (0, 0, 0, pad_atoms), value=0.0
|
| 699 |
-
) # (1, 1, a_p, 3)
|
| 700 |
-
atom_to_token = torch.nn.functional.pad(
|
| 701 |
-
atom_to_token, (0, 0, 0, pad_atoms), value=0.0
|
| 702 |
-
) # (1, a_p, t)
|
| 703 |
-
token_to_rep_atom = torch.nn.functional.pad(
|
| 704 |
-
token_to_rep_atom,
|
| 705 |
-
(0, pad_atoms),
|
| 706 |
-
value=0.0,
|
| 707 |
-
) # (1, t, a_p)
|
| 708 |
-
token_to_center_atom = torch.nn.functional.pad(
|
| 709 |
-
token_to_center_atom,
|
| 710 |
-
(0, pad_atoms),
|
| 711 |
-
value=0.0,
|
| 712 |
-
) # (1, t, a_p)
|
| 713 |
-
r_set_to_rep_atom = torch.nn.functional.pad(
|
| 714 |
-
r_set_to_rep_atom,
|
| 715 |
-
(0, pad_atoms),
|
| 716 |
-
value=0.0,
|
| 717 |
-
) # (1, t, a_p)
|
| 718 |
-
bfactor = torch.nn.functional.pad(
|
| 719 |
-
bfactor, (0, pad_atoms), value=0.0
|
| 720 |
-
) # (1, a_p)
|
| 721 |
-
atom_plddt = torch.nn.functional.pad(
|
| 722 |
-
atom_plddt, (0, pad_atoms), value=0.0
|
| 723 |
-
) # (1, a_p)
|
| 724 |
-
|
| 725 |
-
frames_idx = torch.tensor(
|
| 726 |
-
residue_frame_atom_idx_flat,
|
| 727 |
-
dtype=torch.long,
|
| 728 |
-
).reshape(num_tokens, 3) # (t, 3)
|
| 729 |
-
frames_idx = frames_idx.unsqueeze(0).unsqueeze(1) # (1, 1, t, 3)
|
| 730 |
-
frame_resolved_mask = torch.zeros(
|
| 731 |
-
(1, 1, num_tokens), dtype=torch.bool
|
| 732 |
-
) # (1, 1, t)
|
| 733 |
-
|
| 734 |
-
msa = (
|
| 735 |
-
torch.tensor(residue_token_ids, dtype=torch.long).unsqueeze(0).unsqueeze(0)
|
| 736 |
-
) # (1, 1, t)
|
| 737 |
-
msa_paired = torch.ones((1, 1, num_tokens), dtype=torch.float32) # (1, 1, t)
|
| 738 |
-
deletion_value = torch.zeros(
|
| 739 |
-
(1, 1, num_tokens), dtype=torch.float32
|
| 740 |
-
) # (1, 1, t)
|
| 741 |
-
has_deletion = torch.zeros((1, 1, num_tokens), dtype=torch.bool) # (1, 1, t)
|
| 742 |
-
msa_mask = torch.ones((1, 1, num_tokens), dtype=torch.long) # (1, 1, t)
|
| 743 |
-
deletion_mean = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 744 |
-
profile = (
|
| 745 |
-
one_hot(
|
| 746 |
-
torch.tensor(residue_token_ids, dtype=torch.long),
|
| 747 |
-
num_classes=const.num_tokens,
|
| 748 |
-
)
|
| 749 |
-
.float()
|
| 750 |
-
.unsqueeze(0)
|
| 751 |
-
) # (1, t, n_res_type)
|
| 752 |
-
|
| 753 |
-
template_restype = one_hot(
|
| 754 |
-
torch.zeros((1, 1, num_tokens), dtype=torch.long),
|
| 755 |
-
num_classes=const.num_tokens,
|
| 756 |
-
) # (1, 1, t, n_res_type)
|
| 757 |
-
template_frame_rot = torch.zeros(
|
| 758 |
-
(1, 1, num_tokens, 3, 3), dtype=torch.float32
|
| 759 |
-
) # (1, 1, t, 3, 3)
|
| 760 |
-
template_frame_t = torch.zeros(
|
| 761 |
-
(1, 1, num_tokens, 3), dtype=torch.float32
|
| 762 |
-
) # (1, 1, t, 3)
|
| 763 |
-
template_cb = torch.zeros(
|
| 764 |
-
(1, 1, num_tokens, 3), dtype=torch.float32
|
| 765 |
-
) # (1, 1, t, 3)
|
| 766 |
-
template_ca = torch.zeros(
|
| 767 |
-
(1, 1, num_tokens, 3), dtype=torch.float32
|
| 768 |
-
) # (1, 1, t, 3)
|
| 769 |
-
template_mask_cb = torch.zeros(
|
| 770 |
-
(1, 1, num_tokens), dtype=torch.float32
|
| 771 |
-
) # (1, 1, t)
|
| 772 |
-
template_mask_frame = torch.zeros(
|
| 773 |
-
(1, 1, num_tokens), dtype=torch.float32
|
| 774 |
-
) # (1, 1, t)
|
| 775 |
-
template_mask = torch.zeros(
|
| 776 |
-
(1, 1, num_tokens), dtype=torch.float32
|
| 777 |
-
) # (1, 1, t)
|
| 778 |
-
query_to_template = torch.zeros(
|
| 779 |
-
(1, 1, num_tokens), dtype=torch.long
|
| 780 |
-
) # (1, 1, t)
|
| 781 |
-
visibility_ids = torch.zeros(
|
| 782 |
-
(1, 1, num_tokens), dtype=torch.float32
|
| 783 |
-
) # (1, 1, t)
|
| 784 |
-
|
| 785 |
-
disto_target = torch.zeros(
|
| 786 |
-
(1, num_tokens, num_tokens, 1, num_bins),
|
| 787 |
-
dtype=torch.float32,
|
| 788 |
-
) # (1, t, t, 1, n_bin)
|
| 789 |
-
disto_target[..., 0] = 1.0 # (1, t, t, 1)
|
| 790 |
-
disto_center = torch.zeros(
|
| 791 |
-
(1, num_tokens, 3), dtype=torch.float32
|
| 792 |
-
) # (1, t, 3)
|
| 793 |
-
|
| 794 |
-
features: dict[str, torch.Tensor] = {
|
| 795 |
-
"token_index": token_index,
|
| 796 |
-
"residue_index": residue_index,
|
| 797 |
-
"asym_id": asym_id,
|
| 798 |
-
"entity_id": entity_id,
|
| 799 |
-
"sym_id": sym_id,
|
| 800 |
-
"mol_type": mol_type,
|
| 801 |
-
"res_type": res_type,
|
| 802 |
-
"disto_center": disto_center,
|
| 803 |
-
"token_bonds": token_bonds,
|
| 804 |
-
"type_bonds": type_bonds,
|
| 805 |
-
"token_pad_mask": token_pad_mask,
|
| 806 |
-
"token_resolved_mask": token_resolved_mask,
|
| 807 |
-
"token_disto_mask": token_disto_mask,
|
| 808 |
-
"contact_conditioning": contact_conditioning,
|
| 809 |
-
"contact_threshold": contact_threshold,
|
| 810 |
-
"method_feature": method_feature,
|
| 811 |
-
"modified": modified,
|
| 812 |
-
"cyclic_period": cyclic_period,
|
| 813 |
-
"affinity_token_mask": affinity_token_mask,
|
| 814 |
-
"ref_pos": ref_pos,
|
| 815 |
-
"atom_resolved_mask": atom_resolved_mask,
|
| 816 |
-
"ref_atom_name_chars": ref_atom_name_chars,
|
| 817 |
-
"ref_element": ref_element,
|
| 818 |
-
"ref_charge": ref_charge,
|
| 819 |
-
"ref_chirality": ref_chirality,
|
| 820 |
-
"atom_backbone_feat": atom_backbone_feat,
|
| 821 |
-
"ref_space_uid": ref_space_uid,
|
| 822 |
-
"coords": coords,
|
| 823 |
-
"atom_pad_mask": atom_pad_mask,
|
| 824 |
-
"atom_to_token": atom_to_token,
|
| 825 |
-
"token_to_rep_atom": token_to_rep_atom,
|
| 826 |
-
"r_set_to_rep_atom": r_set_to_rep_atom,
|
| 827 |
-
"token_to_center_atom": token_to_center_atom,
|
| 828 |
-
"disto_target": disto_target,
|
| 829 |
-
"disto_coords_ensemble": disto_coords_ensemble,
|
| 830 |
-
"bfactor": bfactor,
|
| 831 |
-
"plddt": atom_plddt,
|
| 832 |
-
"frames_idx": frames_idx,
|
| 833 |
-
"frame_resolved_mask": frame_resolved_mask,
|
| 834 |
-
"msa": msa,
|
| 835 |
-
"msa_paired": msa_paired,
|
| 836 |
-
"deletion_value": deletion_value,
|
| 837 |
-
"has_deletion": has_deletion,
|
| 838 |
-
"deletion_mean": deletion_mean,
|
| 839 |
-
"profile": profile,
|
| 840 |
-
"msa_mask": msa_mask,
|
| 841 |
-
"template_restype": template_restype,
|
| 842 |
-
"template_frame_rot": template_frame_rot,
|
| 843 |
-
"template_frame_t": template_frame_t,
|
| 844 |
-
"template_cb": template_cb,
|
| 845 |
-
"template_ca": template_ca,
|
| 846 |
-
"template_mask_cb": template_mask_cb,
|
| 847 |
-
"template_mask_frame": template_mask_frame,
|
| 848 |
-
"template_mask": template_mask,
|
| 849 |
-
"query_to_template": query_to_template,
|
| 850 |
-
"visibility_ids": visibility_ids,
|
| 851 |
-
}
|
| 852 |
-
|
| 853 |
-
return features, template # each feature shape is traced at its binding above
|
|
|
|
| 1 |
+
"""Construct Boltz2 protein features from canonical atom tables and conformers."""
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from torch.nn.functional import one_hot
|
| 8 |
+
|
| 9 |
+
from . import vb_const as const
|
| 10 |
+
from .minimal_structures import ProteinStructureTemplate
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
_ELEMENT_TO_Z = {
|
| 14 |
+
"H": 1,
|
| 15 |
+
"C": 6,
|
| 16 |
+
"N": 7,
|
| 17 |
+
"O": 8,
|
| 18 |
+
"P": 15,
|
| 19 |
+
"S": 16,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
# Canonical formal charges and tetrahedral chirality values extracted from the
|
| 23 |
+
# pinned Boltz2 molecule archive. Values use Boltz2's public feature encoding.
|
| 24 |
+
_FORMAL_CHARGES = {
|
| 25 |
+
("ARG", "NH1"): 1.0,
|
| 26 |
+
("HIS", "ND1"): 1.0,
|
| 27 |
+
("LYS", "NZ"): 1.0,
|
| 28 |
+
}
|
| 29 |
+
_CHIRAL_ATOMS = {
|
| 30 |
+
"ALA": frozenset({"CA"}),
|
| 31 |
+
"ARG": frozenset({"CA"}),
|
| 32 |
+
"ASN": frozenset({"CA"}),
|
| 33 |
+
"ASP": frozenset({"CA"}),
|
| 34 |
+
"CYS": frozenset({"CA"}),
|
| 35 |
+
"GLN": frozenset({"CA"}),
|
| 36 |
+
"GLU": frozenset({"CA"}),
|
| 37 |
+
"HIS": frozenset({"CA"}),
|
| 38 |
+
"ILE": frozenset({"CA", "CB"}),
|
| 39 |
+
"LEU": frozenset({"CA"}),
|
| 40 |
+
"LYS": frozenset({"CA"}),
|
| 41 |
+
"MET": frozenset({"CA"}),
|
| 42 |
+
"PHE": frozenset({"CA"}),
|
| 43 |
+
"PRO": frozenset({"CA"}),
|
| 44 |
+
"SER": frozenset({"CA"}),
|
| 45 |
+
"THR": frozenset({"CA", "CB"}),
|
| 46 |
+
"TRP": frozenset({"CA"}),
|
| 47 |
+
"TYR": frozenset({"CA"}),
|
| 48 |
+
"UNK": frozenset({"CA"}),
|
| 49 |
+
"VAL": frozenset({"CA"}),
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _normalize_sequence(sequence: str) -> str:
|
| 54 |
+
if not isinstance(sequence, str):
|
| 55 |
+
raise TypeError("Amino acid sequence must be a string.")
|
| 56 |
+
seq = sequence.strip().upper()
|
| 57 |
+
if not seq:
|
| 58 |
+
raise ValueError("Amino acid sequence must be non-empty.")
|
| 59 |
+
for aa in seq:
|
| 60 |
+
if aa not in const.prot_letter_to_token:
|
| 61 |
+
raise ValueError(f"Unsupported residue code '{aa}'.")
|
| 62 |
+
return seq
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _atom_name_to_element(atom_name: str) -> str:
|
| 66 |
+
name = atom_name.strip().upper()
|
| 67 |
+
if len(name) == 0:
|
| 68 |
+
return "C"
|
| 69 |
+
if name[0].isdigit():
|
| 70 |
+
name = name[1:]
|
| 71 |
+
if len(name) >= 2 and name[0:2] in ("CL", "BR", "FE", "MG", "ZN", "NA", "CA"):
|
| 72 |
+
return name[0]
|
| 73 |
+
return name[0]
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _atom_name_to_codes(atom_name: str) -> torch.Tensor:
|
| 77 |
+
clipped = atom_name.strip()[:4]
|
| 78 |
+
vals = [ord(ch) - 32 for ch in clipped]
|
| 79 |
+
while len(vals) < 4:
|
| 80 |
+
vals.append(0)
|
| 81 |
+
out = torch.tensor(vals, dtype=torch.long) # (4,)
|
| 82 |
+
if not (torch.all(out >= 0) and torch.all(out < 64)):
|
| 83 |
+
raise ValueError(f"Invalid atom-name encoding for '{atom_name}'.")
|
| 84 |
+
return out
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# Raw first-conformer coordinates extracted at full float32 precision from the
|
| 88 |
+
# hash-pinned Boltz2 molecule archive. The feature builder applies the official
|
| 89 |
+
# seeded centering, rotation, and translation policy below.
|
| 90 |
+
_RDKIT_CONFORMERS: dict[str, dict[str, list[float]]] = {
|
| 91 |
+
"ALA": {
|
| 92 |
+
"N": [-0.9241582155227661, 1.1821246147155762, 0.712748110294342],
|
| 93 |
+
"CA": [-0.2663755416870117, -0.08827890455722809, 0.4008508622646332],
|
| 94 |
+
"C": [1.1188693046569824, 0.1387452930212021, -0.14366498589515686],
|
| 95 |
+
"O": [1.2882297039031982, 0.8058497309684753, -1.2001261711120605],
|
| 96 |
+
"CB": [-1.1134333610534668, -0.8915302753448486, -0.5876986384391785],
|
| 97 |
+
},
|
| 98 |
+
"ARG": {
|
| 99 |
+
"N": [3.253326416015625, -1.699564814567566, -0.9852627515792847],
|
| 100 |
+
"CA": [2.191511392593384, -0.692007839679718, -0.9506462812423706],
|
| 101 |
+
"C": [2.80021333694458, 0.672307550907135, -1.1160222291946411],
|
| 102 |
+
"O": [2.465440034866333, 1.3987600803375244, -2.0899009704589844],
|
| 103 |
+
"CB": [1.3811039924621582, -0.7949594855308533, 0.36316436529159546],
|
| 104 |
+
"CG": [0.20344306528568268, 0.19287171959877014, 0.457282155752182],
|
| 105 |
+
"CD": [-0.902410626411438, -0.09953747689723969, -0.5671625137329102],
|
| 106 |
+
"NE": [-2.0867979526519775, 0.7146661281585693, -0.2911669909954071],
|
| 107 |
+
"CZ": [-3.018158197402954, 0.4804283082485199, 0.7831193804740906],
|
| 108 |
+
"NH1": [-4.080137729644775, 1.4154621362686157, 0.9938872456550598],
|
| 109 |
+
"NH2": [-2.9226150512695312, -0.5637124180793762, 1.5533623695373535],
|
| 110 |
+
},
|
| 111 |
+
"ASN": {
|
| 112 |
+
"N": [-1.5767980813980103, -1.7227835655212402, 0.2427234649658203],
|
| 113 |
+
"CA": [-0.7041534781455994, -0.5622008442878723, 0.42767149209976196],
|
| 114 |
+
"C": [-1.246688961982727, 0.6072975993156433, -0.3483567237854004],
|
| 115 |
+
"O": [-1.554918885231018, 0.47644123435020447, -1.5638835430145264],
|
| 116 |
+
"CB": [0.7263866662979126, -0.9032993316650391, -0.028588544577360153],
|
| 117 |
+
"CG": [1.6871562004089355, 0.20810098946094513, 0.28412675857543945],
|
| 118 |
+
"OD1": [2.19687557220459, 0.28916797041893005, 1.433493971824646],
|
| 119 |
+
"ND2": [1.996717095375061, 1.196706771850586, -0.6981719136238098],
|
| 120 |
+
},
|
| 121 |
+
"ASP": {
|
| 122 |
+
"N": [-0.11774874478578568, -1.6310220956802368, 0.374360054731369],
|
| 123 |
+
"CA": [-0.3847021758556366, -0.19290897250175476, 0.28121331334114075],
|
| 124 |
+
"C": [-1.806337594985962, 0.059408850967884064, -0.1472136378288269],
|
| 125 |
+
"O": [-2.2237648963928223, -0.36817988753318787, -1.257502555847168],
|
| 126 |
+
"CB": [0.5981687307357788, 0.4785478413105011, -0.6920451521873474],
|
| 127 |
+
"CG": [2.0088446140289307, 0.36018264293670654, -0.20304732024669647],
|
| 128 |
+
"OD1": [2.747267961502075, -0.5711832046508789, -0.6216948628425598],
|
| 129 |
+
"OD2": [2.49090313911438, 1.2577818632125854, 0.7443997859954834],
|
| 130 |
+
},
|
| 131 |
+
"CYS": {
|
| 132 |
+
"N": [-0.058702241629362106, 1.771048665046692, 0.2434082329273224],
|
| 133 |
+
"CA": [-0.06546340137720108, 0.47663936018943787, -0.4456723928451538],
|
| 134 |
+
"C": [-1.273919701576233, -0.3504071533679962, -0.08361759781837463],
|
| 135 |
+
"O": [-1.7324607372283936, -0.34313708543777466, 1.0909277200698853],
|
| 136 |
+
"CB": [1.2432398796081543, -0.28917407989501953, -0.19254730641841888],
|
| 137 |
+
"SG": [1.484184980392456, -0.7191365361213684, 1.5649693012237549],
|
| 138 |
+
},
|
| 139 |
+
"GLN": {
|
| 140 |
+
"N": [-1.8543237447738647, -1.0024770498275757, -1.6278940439224243],
|
| 141 |
+
"CA": [-1.292184591293335, -0.6786512732505798, -0.3153715133666992],
|
| 142 |
+
"C": [-2.2264492511749268, 0.25171151757240295, 0.4067918062210083],
|
| 143 |
+
"O": [-2.7255098819732666, -0.08834805339574814, 1.5129321813583374],
|
| 144 |
+
"CB": [0.11662524193525314, -0.06213820353150368, -0.45663803815841675],
|
| 145 |
+
"CG": [0.8140791058540344, 0.10461423546075821, 0.9002283811569214],
|
| 146 |
+
"CD": [2.195122003555298, 0.6569428443908691, 0.7154991030693054],
|
| 147 |
+
"OE1": [2.3870291709899902, 1.9003654718399048, 0.7825236916542053],
|
| 148 |
+
"NE2": [3.287371873855591, -0.21307705342769623, 0.41959717869758606],
|
| 149 |
+
},
|
| 150 |
+
"GLU": {
|
| 151 |
+
"N": [-1.3492857217788696, -1.114271640777588, -1.3739068508148193],
|
| 152 |
+
"CA": [-1.2765676975250244, -0.506334662437439, -0.04287439212203026],
|
| 153 |
+
"C": [-1.9195666313171387, 0.8547031879425049, -0.04214814677834511],
|
| 154 |
+
"O": [-1.9091347455978394, 1.5701956748962402, -1.080504298210144],
|
| 155 |
+
"CB": [0.18272103369235992, -0.43209096789360046, 0.45213374495506287],
|
| 156 |
+
"CG": [1.1058456897735596, 0.3659400939941406, -0.481784850358963],
|
| 157 |
+
"CD": [2.5090205669403076, 0.35135790705680847, 0.03646872192621231],
|
| 158 |
+
"OE1": [2.896414041519165, 1.2445733547210693, 0.8365193605422974],
|
| 159 |
+
"OE2": [3.3727896213531494, -0.6754258275032043, -0.33141785860061646],
|
| 160 |
+
},
|
| 161 |
+
"GLY": {
|
| 162 |
+
"N": [-1.291549801826477, 0.6080796122550964, -0.4228580892086029],
|
| 163 |
+
"CA": [-0.4895951449871063, -0.2882237136363983, 0.40191715955734253],
|
| 164 |
+
"C": [0.9350062608718872, -0.2543502449989319, -0.04786944016814232],
|
| 165 |
+
"O": [1.3473576307296753, -1.0836502313613892, -0.9022219777107239],
|
| 166 |
+
},
|
| 167 |
+
"HIS": {
|
| 168 |
+
"N": [1.0371263027191162, -1.5621215105056763, 0.4178937077522278],
|
| 169 |
+
"CA": [1.1980191469192505, -0.40241551399230957, -0.46312251687049866],
|
| 170 |
+
"C": [2.6561269760131836, -0.07123222947120667, -0.6407948136329651],
|
| 171 |
+
"O": [3.1795547008514404, -0.12371637672185898, -1.7859554290771484],
|
| 172 |
+
"CB": [0.44051393866539, 0.8116919994354248, 0.09850303083658218],
|
| 173 |
+
"CG": [-1.0356801748275757, 0.5519806742668152, 0.15254715085029602],
|
| 174 |
+
"ND1": [-1.9089158773422241, 0.6490958333015442, -0.968245267868042],
|
| 175 |
+
"CD2": [-1.7095766067504883, 0.1138235554099083, 1.2061808109283447],
|
| 176 |
+
"CE1": [-3.0751633644104004, 0.28129827976226807, -0.5511555075645447],
|
| 177 |
+
"NE2": [-3.0650453567504883, -0.07846951484680176, 0.8257263898849487],
|
| 178 |
+
},
|
| 179 |
+
"ILE": {
|
| 180 |
+
"N": [-1.2378733158111572, -1.8146690130233765, -0.16437499225139618],
|
| 181 |
+
"CA": [-1.2780015468597412, -0.3994033634662628, 0.22437156736850739],
|
| 182 |
+
"C": [-1.9879958629608154, 0.4497596025466919, -0.80372154712677],
|
| 183 |
+
"O": [-2.0291709899902344, 0.10288607329130173, -2.015406608581543],
|
| 184 |
+
"CB": [0.14225825667381287, 0.146738663315773, 0.5415176153182983],
|
| 185 |
+
"CG1": [1.1062071323394775, 0.04519597440958023, -0.6694174408912659],
|
| 186 |
+
"CG2": [0.7215384244918823, -0.5685611963272095, 1.7750537395477295],
|
| 187 |
+
"CD1": [2.371805191040039, 0.8852734565734863, -0.49045464396476746],
|
| 188 |
+
},
|
| 189 |
+
"LEU": {
|
| 190 |
+
"N": [1.6452248096466064, -1.017622470855713, -1.0626215934753418],
|
| 191 |
+
"CA": [1.3129304647445679, 0.08074887096881866, -0.15053576231002808],
|
| 192 |
+
"C": [2.431154727935791, 0.28310757875442505, 0.8357383608818054],
|
| 193 |
+
"O": [2.9231173992156982, -0.7013152837753296, 1.451209306716919],
|
| 194 |
+
"CB": [0.006050171796232462, -0.2141101360321045, 0.6198461651802063],
|
| 195 |
+
"CG": [-1.2534235715866089, -0.3772832751274109, -0.26637399196624756],
|
| 196 |
+
"CD1": [-2.455688714981079, -0.7604416012763977, 0.6070646643638611],
|
| 197 |
+
"CD2": [-1.5742294788360596, 0.8985711932182312, -1.059889554977417],
|
| 198 |
+
},
|
| 199 |
+
"LYS": {
|
| 200 |
+
"N": [-2.3938536643981934, -1.4751482009887695, -0.9336304068565369],
|
| 201 |
+
"CA": [-2.2094509601593018, -0.6203567981719971, 0.2421264797449112],
|
| 202 |
+
"C": [-3.4513463973999023, 0.1961250752210617, 0.4710204303264618],
|
| 203 |
+
"O": [-3.9688456058502197, 0.2503468096256256, 1.6188287734985352],
|
| 204 |
+
"CB": [-1.008726716041565, 0.3269270658493042, 0.0479682981967926],
|
| 205 |
+
"CG": [0.336457222700119, -0.41559070348739624, 0.047045741230249405],
|
| 206 |
+
"CD": [1.5088860988616943, 0.5716065168380737, -0.00632853340357542],
|
| 207 |
+
"CE": [2.853410005569458, -0.1678532361984253, -0.020898228511214256],
|
| 208 |
+
"NZ": [3.9690465927124023, 0.7765668630599976, -0.07007527351379395],
|
| 209 |
+
},
|
| 210 |
+
"MET": {
|
| 211 |
+
"N": [-1.743452548980713, -0.44039490818977356, 1.851780652999878],
|
| 212 |
+
"CA": [-1.2168668508529663, 0.4041755199432373, 0.7779996991157532],
|
| 213 |
+
"C": [-2.3118340969085693, 0.683431088924408, -0.2134547382593155],
|
| 214 |
+
"O": [-2.6660380363464355, 1.8705639839172363, -0.44476020336151123],
|
| 215 |
+
"CB": [-0.0005482881097123027, -0.262584388256073, 0.09933014959096909],
|
| 216 |
+
"CG": [0.6952047944068909, 0.6768104434013367, -0.8930339813232422],
|
| 217 |
+
"SD": [2.136805772781372, -0.14913176000118256, -1.6563159227371216],
|
| 218 |
+
"CE": [3.3404407501220703, 0.348065584897995, -0.38104408979415894],
|
| 219 |
+
},
|
| 220 |
+
"PHE": {
|
| 221 |
+
"N": [2.7976438999176025, -1.6209964752197266, -0.4779351055622101],
|
| 222 |
+
"CA": [1.5799994468688965, -0.8289614915847778, -0.6608061194419861],
|
| 223 |
+
"C": [1.9535551071166992, 0.5758142471313477, -1.0445665121078491],
|
| 224 |
+
"O": [1.5211102962493896, 1.072593331336975, -2.1190123558044434],
|
| 225 |
+
"CB": [0.7137221097946167, -0.8474348187446594, 0.6163292527198792],
|
| 226 |
+
"CG": [-0.6229045987129211, -0.18666480481624603, 0.3912639319896698],
|
| 227 |
+
"CD1": [-0.8259273767471313, 1.0978055000305176, 0.7381877303123474],
|
| 228 |
+
"CD2": [-1.7160141468048096, -0.9425657391548157, -0.2700555920600891],
|
| 229 |
+
"CE1": [-2.131312131881714, 1.741182565689087, 0.4800238013267517],
|
| 230 |
+
"CE2": [-2.8960046768188477, -0.3551671802997589, -0.505269467830658],
|
| 231 |
+
"CZ": [-3.11456298828125, 1.0511738061904907, -0.11009909957647324],
|
| 232 |
+
},
|
| 233 |
+
"PRO": {
|
| 234 |
+
"N": [-0.577339768409729, -0.5123927593231201, -1.150407314300537],
|
| 235 |
+
"CA": [0.5119893550872803, 0.23565764725208282, -0.5195383429527283],
|
| 236 |
+
"C": [1.8393173217773438, -0.46712079644203186, -0.6550371646881104],
|
| 237 |
+
"O": [2.0836586952209473, -1.166333556175232, -1.6751221418380737],
|
| 238 |
+
"CB": [0.1029478907585144, 0.45346882939338684, 0.9297074675559998],
|
| 239 |
+
"CG": [-1.4098080396652222, 0.5215330123901367, 0.8787456750869751],
|
| 240 |
+
"CD": [-1.7971025705337524, -0.06041542813181877, -0.47876954078674316],
|
| 241 |
+
},
|
| 242 |
+
"SER": {
|
| 243 |
+
"N": [0.8959873914718628, -1.423298716545105, -0.2692987024784088],
|
| 244 |
+
"CA": [-0.018787339329719543, -0.28751760721206665, -0.3863537311553955],
|
| 245 |
+
"C": [0.767615795135498, 0.9929561018943787, -0.42147380113601685],
|
| 246 |
+
"O": [0.5327092409133911, 1.8535151481628418, -1.3116248846054077],
|
| 247 |
+
"CB": [-0.9973848462104797, -0.2769756019115448, 0.7988247871398926],
|
| 248 |
+
"OG": [-1.8999865055084229, 0.7915746569633484, 0.691534698009491],
|
| 249 |
+
},
|
| 250 |
+
"THR": {
|
| 251 |
+
"N": [0.005045319441705942, 1.7655576467514038, 0.0639338344335556],
|
| 252 |
+
"CA": [0.4231603145599365, 0.3755212128162384, 0.2734847664833069],
|
| 253 |
+
"C": [1.7184523344039917, 0.11012311279773712, -0.4465082287788391],
|
| 254 |
+
"O": [1.9252793788909912, 0.5967074036598206, -1.5912789106369019],
|
| 255 |
+
"CB": [-0.6634359359741211, -0.615561306476593, -0.21173778176307678],
|
| 256 |
+
"OG1": [-1.0762907266616821, -0.29800114035606384, -1.5171915292739868],
|
| 257 |
+
"CG2": [-1.8869022130966187, -0.6176311373710632, 0.7084044218063354],
|
| 258 |
+
},
|
| 259 |
+
"TRP": {
|
| 260 |
+
"N": [-3.0439648628234863, 1.0628471374511719, 0.015692168846726418],
|
| 261 |
+
"CA": [-2.463679552078247, 0.024657616391777992, -0.8414031863212585],
|
| 262 |
+
"C": [-2.186711072921753, -1.2228965759277344, -0.04691409692168236],
|
| 263 |
+
"O": [-2.3364906311035156, -2.3535354137420654, -0.5830773115158081],
|
| 264 |
+
"CB": [-1.2062920331954956, 0.5195542573928833, -1.592403531074524],
|
| 265 |
+
"CG": [-0.06450498104095459, 0.884079098701477, -0.6835198402404785],
|
| 266 |
+
"CD1": [0.16109998524188995, 2.085163116455078, -0.14801783859729767],
|
| 267 |
+
"CD2": [0.9781660437583923, -0.005544683896005154, -0.17733941972255707],
|
| 268 |
+
"NE1": [1.3106837272644043, 2.0506503582000732, 0.698072075843811],
|
| 269 |
+
"CE2": [1.7528777122497559, 0.7044392228126526, 0.6196392774581909],
|
| 270 |
+
"CE3": [1.236093521118164, -1.4355946779251099, -0.4235689342021942],
|
| 271 |
+
"CZ2": [2.9070138931274414, 0.10196790844202042, 1.3070299625396729],
|
| 272 |
+
"CZ3": [2.2851555347442627, -2.007185697555542, 0.1949695348739624],
|
| 273 |
+
"CH2": [3.1585631370544434, -1.2034809589385986, 1.1002938747406006],
|
| 274 |
+
},
|
| 275 |
+
"TYR": {
|
| 276 |
+
"N": [-1.7950150966644287, 0.49119046330451965, -1.3951144218444824],
|
| 277 |
+
"CA": [-1.8436503410339355, -0.2694968581199646, -0.14338290691375732],
|
| 278 |
+
"C": [-3.240288019180298, -0.28215137124061584, 0.41974759101867676],
|
| 279 |
+
"O": [-3.8150112628936768, 0.7977103590965271, 0.7253130674362183],
|
| 280 |
+
"CB": [-0.8541494011878967, 0.30990728735923767, 0.8830214738845825],
|
| 281 |
+
"CG": [0.5672639012336731, 0.2125871181488037, 0.3916495442390442],
|
| 282 |
+
"CD1": [1.2694358825683594, -0.924761176109314, 0.5431669354438782],
|
| 283 |
+
"CD2": [1.1872504949569702, 1.3605931997299194, -0.31554359197616577],
|
| 284 |
+
"CE1": [2.6519992351531982, -1.0213464498519897, 0.029911600053310394],
|
| 285 |
+
"CE2": [2.4384164810180664, 1.2693941593170166, -0.7831515073776245],
|
| 286 |
+
"CZ": [3.211331367492676, 0.022552935406565666, -0.6021429300308228],
|
| 287 |
+
"OH": [4.513507843017578, -0.05260590463876724, -1.0929937362670898],
|
| 288 |
+
},
|
| 289 |
+
"VAL": {
|
| 290 |
+
"N": [0.9408224821090698, -1.2608877420425415, 0.652370810508728],
|
| 291 |
+
"CA": [0.7287879586219788, -0.3937721848487854, -0.5120788216590881],
|
| 292 |
+
"C": [1.7670493125915527, 0.6979415416717529, -0.5508439540863037],
|
| 293 |
+
"O": [2.2449657917022705, 1.1683598756790161, 0.5171348452568054],
|
| 294 |
+
"CB": [-0.7015937566757202, 0.21480272710323334, -0.5226467847824097],
|
| 295 |
+
"CG1": [-1.7662651538848877, -0.845522403717041, -0.8411717414855957],
|
| 296 |
+
"CG2": [-1.0500152111053467, 0.9374979138374329, 0.7892321944236755],
|
| 297 |
+
},
|
| 298 |
+
"UNK": {
|
| 299 |
+
"N": [1.6134154796600342, -1.304404377937317, -0.35241976380348206],
|
| 300 |
+
"CA": [0.6100443601608276, -0.23816797137260437, -0.3852081298828125],
|
| 301 |
+
"C": [1.0861350297927856, 0.9232651591300964, 0.4423142373561859],
|
| 302 |
+
"O": [1.374688982963562, 0.7629808187484741, 1.6591525077819824],
|
| 303 |
+
"CB": [-0.7607048153877258, -0.7552146315574646, 0.10266945511102676],
|
| 304 |
+
},
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
def _get_atom_position(res_name: str, atom_name: str, atom_idx: int) -> np.ndarray:
|
| 309 |
+
"""Get the canonical RDKit conformer position for an atom.
|
| 310 |
+
|
| 311 |
+
Uses pre-extracted positions from official Boltz2 mol files. Falls back
|
| 312 |
+
to a simple geometric placement for unknown residue/atom combinations.
|
| 313 |
+
"""
|
| 314 |
+
if res_name in _RDKIT_CONFORMERS and atom_name in _RDKIT_CONFORMERS[res_name]:
|
| 315 |
+
return np.array(_RDKIT_CONFORMERS[res_name][atom_name], dtype=np.float32) # (3,)
|
| 316 |
+
# Fallback for unknown atoms (should not happen for canonical AAs)
|
| 317 |
+
angle = (atom_idx + 1) * 0.7
|
| 318 |
+
radius = 1.4 + 0.03 * atom_idx
|
| 319 |
+
return np.array(
|
| 320 |
+
[
|
| 321 |
+
radius * math.cos(angle),
|
| 322 |
+
radius * math.sin(angle),
|
| 323 |
+
0.1 * ((atom_idx % 5) - 2),
|
| 324 |
+
],
|
| 325 |
+
dtype=np.float32,
|
| 326 |
+
) # (3,)
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def _build_template(
|
| 330 |
+
sequence: str,
|
| 331 |
+
) -> tuple[
|
| 332 |
+
ProteinStructureTemplate,
|
| 333 |
+
list[str],
|
| 334 |
+
list[int],
|
| 335 |
+
list[int],
|
| 336 |
+
list[int],
|
| 337 |
+
list[np.ndarray],
|
| 338 |
+
list[int],
|
| 339 |
+
]:
|
| 340 |
+
residue_names: list[str] = []
|
| 341 |
+
residue_token_ids: list[int] = []
|
| 342 |
+
atom_names: list[str] = []
|
| 343 |
+
atom_elements: list[str] = []
|
| 344 |
+
atom_residue_index: list[int] = []
|
| 345 |
+
atom_chain_id: list[str] = []
|
| 346 |
+
atom_positions: list[np.ndarray] = [] # each: (3,)
|
| 347 |
+
residue_center_atom_idx: list[int] = []
|
| 348 |
+
residue_disto_atom_idx: list[int] = []
|
| 349 |
+
residue_frame_atom_idx: list[int] = []
|
| 350 |
+
|
| 351 |
+
global_atom_idx = 0
|
| 352 |
+
for res_idx, aa in enumerate(sequence):
|
| 353 |
+
token_name = const.prot_letter_to_token[aa]
|
| 354 |
+
residue_names.append(token_name)
|
| 355 |
+
residue_token_ids.append(const.token_ids[token_name])
|
| 356 |
+
|
| 357 |
+
residue_atoms = const.ref_atoms[token_name]
|
| 358 |
+
if not residue_atoms:
|
| 359 |
+
raise RuntimeError(f"No reference atoms for residue {token_name}.")
|
| 360 |
+
center_atom_name = const.res_to_center_atom[token_name]
|
| 361 |
+
disto_atom_name = const.res_to_disto_atom[token_name]
|
| 362 |
+
|
| 363 |
+
center_idx = -1
|
| 364 |
+
disto_idx = -1
|
| 365 |
+
n_idx = -1
|
| 366 |
+
ca_idx = -1
|
| 367 |
+
c_idx = -1
|
| 368 |
+
|
| 369 |
+
for local_idx, atom_name in enumerate(residue_atoms):
|
| 370 |
+
atom_names.append(atom_name)
|
| 371 |
+
element = _atom_name_to_element(atom_name)
|
| 372 |
+
atom_elements.append(element)
|
| 373 |
+
atom_residue_index.append(res_idx)
|
| 374 |
+
atom_chain_id.append("A")
|
| 375 |
+
|
| 376 |
+
atom_pos = _get_atom_position(token_name, atom_name, local_idx) # (3,)
|
| 377 |
+
atom_positions.append(atom_pos)
|
| 378 |
+
|
| 379 |
+
if atom_name == center_atom_name:
|
| 380 |
+
center_idx = global_atom_idx
|
| 381 |
+
if atom_name == disto_atom_name:
|
| 382 |
+
disto_idx = global_atom_idx
|
| 383 |
+
if atom_name == "N":
|
| 384 |
+
n_idx = global_atom_idx
|
| 385 |
+
if atom_name == "CA":
|
| 386 |
+
ca_idx = global_atom_idx
|
| 387 |
+
if atom_name == "C":
|
| 388 |
+
c_idx = global_atom_idx
|
| 389 |
+
global_atom_idx += 1
|
| 390 |
+
|
| 391 |
+
if center_idx == -1:
|
| 392 |
+
center_idx = global_atom_idx - len(residue_atoms)
|
| 393 |
+
if disto_idx == -1:
|
| 394 |
+
disto_idx = center_idx
|
| 395 |
+
if n_idx == -1:
|
| 396 |
+
n_idx = center_idx
|
| 397 |
+
if ca_idx == -1:
|
| 398 |
+
ca_idx = center_idx
|
| 399 |
+
if c_idx == -1:
|
| 400 |
+
c_idx = center_idx
|
| 401 |
+
|
| 402 |
+
residue_center_atom_idx.append(center_idx)
|
| 403 |
+
residue_disto_atom_idx.append(disto_idx)
|
| 404 |
+
residue_frame_atom_idx.extend([n_idx, ca_idx, c_idx])
|
| 405 |
+
|
| 406 |
+
template = ProteinStructureTemplate(
|
| 407 |
+
sequence=sequence,
|
| 408 |
+
residue_names=residue_names,
|
| 409 |
+
atom_names=atom_names,
|
| 410 |
+
atom_elements=atom_elements,
|
| 411 |
+
atom_residue_index=atom_residue_index,
|
| 412 |
+
atom_chain_id=atom_chain_id,
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
return (
|
| 416 |
+
template,
|
| 417 |
+
residue_names,
|
| 418 |
+
residue_token_ids,
|
| 419 |
+
residue_center_atom_idx,
|
| 420 |
+
residue_disto_atom_idx,
|
| 421 |
+
atom_positions,
|
| 422 |
+
residue_frame_atom_idx,
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def _random_rotation_matrix() -> torch.Tensor:
|
| 427 |
+
"""Sample a uniform random 3x3 rotation matrix (Algorithm 19 from AF2/Boltz)."""
|
| 428 |
+
quaternion = torch.randn((1, 4), dtype=torch.float32) # (1, 4)
|
| 429 |
+
squared_norm = (quaternion * quaternion).sum(dim=1) # (1,)
|
| 430 |
+
norm = torch.sqrt(squared_norm) # (1,)
|
| 431 |
+
signed_norm = torch.where(quaternion[:, 0] < 0, -norm, norm) # (1,)
|
| 432 |
+
quaternion = quaternion / signed_norm[:, None] # (1, 4)
|
| 433 |
+
|
| 434 |
+
real, i, j, k = torch.unbind(quaternion, dim=-1) # each: (1,)
|
| 435 |
+
two_s = 2.0 / (quaternion * quaternion).sum(dim=-1) # (1,)
|
| 436 |
+
rotation = torch.stack(
|
| 437 |
+
(
|
| 438 |
+
1 - two_s * (j * j + k * k),
|
| 439 |
+
two_s * (i * j - k * real),
|
| 440 |
+
two_s * (i * k + j * real),
|
| 441 |
+
two_s * (i * j + k * real),
|
| 442 |
+
1 - two_s * (i * i + k * k),
|
| 443 |
+
two_s * (j * k - i * real),
|
| 444 |
+
two_s * (i * k - j * real),
|
| 445 |
+
two_s * (j * k + i * real),
|
| 446 |
+
1 - two_s * (i * i + j * j),
|
| 447 |
+
),
|
| 448 |
+
dim=-1,
|
| 449 |
+
) # (1, 9)
|
| 450 |
+
return rotation.reshape(1, 3, 3)[0] # (3, 3)
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def _center_and_augment_atoms_per_residue(
|
| 454 |
+
atom_positions: torch.Tensor,
|
| 455 |
+
atom_residue_index: list[int],
|
| 456 |
+
num_residues: int,
|
| 457 |
+
) -> torch.Tensor:
|
| 458 |
+
"""Apply Boltz2's seeded conformer augmentation per residue.
|
| 459 |
+
|
| 460 |
+
The pinned implementation intentionally excludes the final residue because
|
| 461 |
+
it iterates to the maximum reference-space identifier rather than through it.
|
| 462 |
+
"""
|
| 463 |
+
# atom_positions: (a, 3), where a is the unpadded atom count.
|
| 464 |
+
result = atom_positions.clone() # (a, 3)
|
| 465 |
+
residue_index_tensor = torch.tensor(atom_residue_index, dtype=torch.long) # (a,)
|
| 466 |
+
for residue_idx in range(max(num_residues - 1, 0)):
|
| 467 |
+
residue_mask = residue_index_tensor == residue_idx # (a,)
|
| 468 |
+
if not torch.any(residue_mask):
|
| 469 |
+
raise RuntimeError(f"Residue index {residue_idx} has no atoms.")
|
| 470 |
+
residue_coords = result[residue_mask][None] # (1, a_r, 3)
|
| 471 |
+
resolved_mask = torch.ones(
|
| 472 |
+
residue_coords.shape[:2], dtype=torch.bool, device=residue_coords.device
|
| 473 |
+
) # (1, a_r)
|
| 474 |
+
residue_center = torch.sum(
|
| 475 |
+
residue_coords * resolved_mask[:, :, None], dim=1, keepdim=True
|
| 476 |
+
) / torch.sum(resolved_mask[:, :, None], dim=1, keepdim=True) # (1, 1, 3)
|
| 477 |
+
residue_coords = residue_coords - residue_center # (1, a_r, 3)
|
| 478 |
+
rotation = _random_rotation_matrix()[None] # (1, 3, 3)
|
| 479 |
+
residue_coords = torch.einsum(
|
| 480 |
+
"bmd,bds->bms", residue_coords, rotation
|
| 481 |
+
) # (1, a_r, 3)
|
| 482 |
+
residue_coords = (
|
| 483 |
+
residue_coords + torch.randn_like(residue_coords[:, 0:1, :])
|
| 484 |
+
) # (1, a_r, 3)
|
| 485 |
+
result[residue_mask] = residue_coords[0] # (a_r, 3)
|
| 486 |
+
return result # (a, 3)
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def build_boltz2_features(
|
| 490 |
+
amino_acid_sequence: str,
|
| 491 |
+
num_bins: int = 64,
|
| 492 |
+
atoms_per_window_queries: int = 32,
|
| 493 |
+
) -> tuple[dict[str, torch.Tensor], ProteinStructureTemplate]:
|
| 494 |
+
sequence = _normalize_sequence(amino_acid_sequence)
|
| 495 |
+
(
|
| 496 |
+
template,
|
| 497 |
+
residue_names,
|
| 498 |
+
residue_token_ids,
|
| 499 |
+
residue_center_atom_idx,
|
| 500 |
+
residue_disto_atom_idx,
|
| 501 |
+
atom_positions_np,
|
| 502 |
+
residue_frame_atom_idx_flat,
|
| 503 |
+
) = _build_template(sequence)
|
| 504 |
+
|
| 505 |
+
num_tokens = len(residue_names)
|
| 506 |
+
num_atoms = len(atom_positions_np)
|
| 507 |
+
if num_tokens <= 0 or num_atoms <= 0:
|
| 508 |
+
raise RuntimeError("Boltz2 feature construction produced an empty protein template.")
|
| 509 |
+
|
| 510 |
+
# t is the token count, a is the unpadded atom count, and a_p is the padded atom count.
|
| 511 |
+
atom_positions = torch.tensor(
|
| 512 |
+
np.asarray(atom_positions_np), dtype=torch.float32
|
| 513 |
+
) # (a, 3)
|
| 514 |
+
atom_positions = _center_and_augment_atoms_per_residue(
|
| 515 |
+
atom_positions=atom_positions,
|
| 516 |
+
atom_residue_index=template.atom_residue_index,
|
| 517 |
+
num_residues=num_tokens,
|
| 518 |
+
) # (a, 3)
|
| 519 |
+
|
| 520 |
+
token_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0) # (1, t)
|
| 521 |
+
residue_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0) # (1, t)
|
| 522 |
+
asym_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 523 |
+
entity_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 524 |
+
sym_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 525 |
+
mol_type = torch.full(
|
| 526 |
+
(1, num_tokens),
|
| 527 |
+
fill_value=const.chain_type_ids["PROTEIN"],
|
| 528 |
+
dtype=torch.long,
|
| 529 |
+
) # (1, t)
|
| 530 |
+
|
| 531 |
+
res_type_ids = torch.tensor(residue_token_ids, dtype=torch.long) # (t,)
|
| 532 |
+
res_type = one_hot(
|
| 533 |
+
res_type_ids, num_classes=const.num_tokens
|
| 534 |
+
).unsqueeze(0) # (1, t, n_res_type)
|
| 535 |
+
|
| 536 |
+
# token_bonds encodes explicit covalent cross-links from structure bonds,
|
| 537 |
+
# NOT backbone peptide bonds (those are implicit via residue_index + asym_id).
|
| 538 |
+
# For a standard single-chain protein without cross-links, this is all zeros.
|
| 539 |
+
# This matches the official Boltz2 featurizer (featurizerv2.py lines 696-705).
|
| 540 |
+
token_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.float32) # (t, t)
|
| 541 |
+
type_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.long) # (t, t)
|
| 542 |
+
token_bonds = token_bonds.unsqueeze(0).unsqueeze(-1) # (1, t, t, 1)
|
| 543 |
+
type_bonds = type_bonds.unsqueeze(0) # (1, t, t)
|
| 544 |
+
|
| 545 |
+
token_pad_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 546 |
+
token_resolved_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 547 |
+
token_disto_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 548 |
+
|
| 549 |
+
num_contact_classes = len(const.contact_conditioning_info)
|
| 550 |
+
unspecified_id = const.contact_conditioning_info["UNSPECIFIED"]
|
| 551 |
+
contact_ids = torch.full(
|
| 552 |
+
(num_tokens, num_tokens),
|
| 553 |
+
fill_value=unspecified_id,
|
| 554 |
+
dtype=torch.long,
|
| 555 |
+
) # (t, t)
|
| 556 |
+
contact_conditioning = one_hot(
|
| 557 |
+
contact_ids,
|
| 558 |
+
num_classes=num_contact_classes,
|
| 559 |
+
).unsqueeze(0) # (1, t, t, n_contact)
|
| 560 |
+
contact_threshold = torch.zeros(
|
| 561 |
+
(1, num_tokens, num_tokens), dtype=torch.float32
|
| 562 |
+
) # (1, t, t)
|
| 563 |
+
|
| 564 |
+
if "x-ray diffraction" not in const.method_types_ids:
|
| 565 |
+
raise RuntimeError("Boltz2 method metadata omits x-ray diffraction.")
|
| 566 |
+
method_feature = torch.full(
|
| 567 |
+
(1, num_tokens),
|
| 568 |
+
fill_value=const.method_types_ids["x-ray diffraction"],
|
| 569 |
+
dtype=torch.long,
|
| 570 |
+
) # (1, t)
|
| 571 |
+
modified = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
|
| 572 |
+
cyclic_period = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 573 |
+
affinity_token_mask = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 574 |
+
|
| 575 |
+
ref_pos = atom_positions.unsqueeze(0) # (1, a, 3)
|
| 576 |
+
atom_pad_mask = torch.ones((1, num_atoms), dtype=torch.float32) # (1, a)
|
| 577 |
+
atom_resolved_mask = torch.ones((1, num_atoms), dtype=torch.bool) # (1, a)
|
| 578 |
+
|
| 579 |
+
atom_name_codes = torch.stack(
|
| 580 |
+
[_atom_name_to_codes(atom_name) for atom_name in template.atom_names],
|
| 581 |
+
dim=0,
|
| 582 |
+
) # (a, 4)
|
| 583 |
+
ref_atom_name_chars = one_hot(
|
| 584 |
+
atom_name_codes, num_classes=64
|
| 585 |
+
).unsqueeze(0) # (1, a, 4, 64)
|
| 586 |
+
|
| 587 |
+
atomic_numbers = []
|
| 588 |
+
for element in template.atom_elements:
|
| 589 |
+
z_value = _ELEMENT_TO_Z[element] if element in _ELEMENT_TO_Z else _ELEMENT_TO_Z["C"]
|
| 590 |
+
if z_value >= const.num_elements:
|
| 591 |
+
raise RuntimeError(
|
| 592 |
+
f"Atomic number {z_value} exceeds the Boltz2 element vocabulary."
|
| 593 |
+
)
|
| 594 |
+
atomic_numbers.append(z_value)
|
| 595 |
+
ref_element = one_hot(
|
| 596 |
+
torch.tensor(atomic_numbers, dtype=torch.long),
|
| 597 |
+
num_classes=const.num_elements,
|
| 598 |
+
).unsqueeze(0) # (1, a, n_element)
|
| 599 |
+
|
| 600 |
+
ref_charge_values = []
|
| 601 |
+
ref_chirality_values = []
|
| 602 |
+
for atom_name, residue_idx in zip(
|
| 603 |
+
template.atom_names,
|
| 604 |
+
template.atom_residue_index,
|
| 605 |
+
strict=True,
|
| 606 |
+
):
|
| 607 |
+
residue_name = residue_names[residue_idx]
|
| 608 |
+
ref_charge_values.append(_FORMAL_CHARGES.get((residue_name, atom_name), 0.0))
|
| 609 |
+
ref_chirality_values.append(
|
| 610 |
+
2 if atom_name in _CHIRAL_ATOMS.get(residue_name, frozenset()) else 0
|
| 611 |
+
)
|
| 612 |
+
ref_charge = torch.tensor(ref_charge_values, dtype=torch.float32).unsqueeze(0) # (1, a)
|
| 613 |
+
ref_chirality = torch.tensor(
|
| 614 |
+
ref_chirality_values, dtype=torch.long
|
| 615 |
+
).unsqueeze(0) # (1, a)
|
| 616 |
+
ref_space_uid = torch.tensor(
|
| 617 |
+
template.atom_residue_index, dtype=torch.long
|
| 618 |
+
).unsqueeze(0) # (1, a)
|
| 619 |
+
|
| 620 |
+
atom_to_token = one_hot(
|
| 621 |
+
torch.tensor(template.atom_residue_index, dtype=torch.long),
|
| 622 |
+
num_classes=num_tokens,
|
| 623 |
+
).unsqueeze(0) # (1, a, t)
|
| 624 |
+
token_to_rep_atom = one_hot(
|
| 625 |
+
torch.tensor(residue_disto_atom_idx, dtype=torch.long),
|
| 626 |
+
num_classes=num_atoms,
|
| 627 |
+
).unsqueeze(0) # (1, t, a)
|
| 628 |
+
token_to_center_atom = one_hot(
|
| 629 |
+
torch.tensor(residue_center_atom_idx, dtype=torch.long),
|
| 630 |
+
num_classes=num_atoms,
|
| 631 |
+
).unsqueeze(0) # (1, t, a)
|
| 632 |
+
r_set_to_rep_atom = token_to_center_atom.clone() # (1, t, a)
|
| 633 |
+
|
| 634 |
+
num_backbone_classes = (
|
| 635 |
+
1 + len(const.protein_backbone_atom_index) + len(const.nucleic_backbone_atom_index)
|
| 636 |
+
)
|
| 637 |
+
backbone_ids = []
|
| 638 |
+
for atom_name in template.atom_names:
|
| 639 |
+
if atom_name in const.protein_backbone_atom_index:
|
| 640 |
+
backbone_ids.append(const.protein_backbone_atom_index[atom_name] + 1)
|
| 641 |
+
else:
|
| 642 |
+
backbone_ids.append(0)
|
| 643 |
+
atom_backbone_feat = one_hot(
|
| 644 |
+
torch.tensor(backbone_ids, dtype=torch.long),
|
| 645 |
+
num_classes=num_backbone_classes,
|
| 646 |
+
).unsqueeze(0) # (1, a, n_backbone)
|
| 647 |
+
|
| 648 |
+
# X contains no observed coordinates for sequence-only inference.
|
| 649 |
+
coords = torch.zeros((1, 1, num_atoms, 3), dtype=torch.float32) # (1, 1, a, 3)
|
| 650 |
+
disto_coords_ensemble = torch.zeros(
|
| 651 |
+
(1, 1, num_tokens, 3),
|
| 652 |
+
dtype=torch.float32,
|
| 653 |
+
) # (1, 1, t, 3)
|
| 654 |
+
|
| 655 |
+
bfactor = torch.zeros((1, num_atoms), dtype=torch.float32) # (1, a)
|
| 656 |
+
atom_plddt = torch.ones((1, num_atoms), dtype=torch.float32) # (1, a)
|
| 657 |
+
|
| 658 |
+
if atoms_per_window_queries <= 0:
|
| 659 |
+
raise ValueError("atoms_per_window_queries must be positive.")
|
| 660 |
+
pad_atoms = (
|
| 661 |
+
(num_atoms - 1) // atoms_per_window_queries + 1
|
| 662 |
+
) * atoms_per_window_queries - num_atoms
|
| 663 |
+
if pad_atoms > 0:
|
| 664 |
+
ref_pos = torch.nn.functional.pad(
|
| 665 |
+
ref_pos, (0, 0, 0, pad_atoms), value=0.0
|
| 666 |
+
) # (1, a_p, 3)
|
| 667 |
+
atom_pad_mask = torch.nn.functional.pad(
|
| 668 |
+
atom_pad_mask, (0, pad_atoms), value=0.0
|
| 669 |
+
) # (1, a_p)
|
| 670 |
+
atom_resolved_mask = torch.nn.functional.pad(
|
| 671 |
+
atom_resolved_mask,
|
| 672 |
+
(0, pad_atoms),
|
| 673 |
+
value=0.0,
|
| 674 |
+
) # (1, a_p)
|
| 675 |
+
ref_atom_name_chars = torch.nn.functional.pad(
|
| 676 |
+
ref_atom_name_chars,
|
| 677 |
+
(0, 0, 0, 0, 0, pad_atoms),
|
| 678 |
+
value=0.0,
|
| 679 |
+
) # (1, a_p, 4, 64)
|
| 680 |
+
ref_element = torch.nn.functional.pad(
|
| 681 |
+
ref_element, (0, 0, 0, pad_atoms), value=0.0
|
| 682 |
+
) # (1, a_p, n_element)
|
| 683 |
+
ref_charge = torch.nn.functional.pad(
|
| 684 |
+
ref_charge, (0, pad_atoms), value=0.0
|
| 685 |
+
) # (1, a_p)
|
| 686 |
+
ref_chirality = torch.nn.functional.pad(
|
| 687 |
+
ref_chirality, (0, pad_atoms), value=0
|
| 688 |
+
) # (1, a_p)
|
| 689 |
+
atom_backbone_feat = torch.nn.functional.pad(
|
| 690 |
+
atom_backbone_feat,
|
| 691 |
+
(0, 0, 0, pad_atoms),
|
| 692 |
+
value=0.0,
|
| 693 |
+
) # (1, a_p, n_backbone)
|
| 694 |
+
ref_space_uid = torch.nn.functional.pad(
|
| 695 |
+
ref_space_uid, (0, pad_atoms), value=0
|
| 696 |
+
) # (1, a_p)
|
| 697 |
+
coords = torch.nn.functional.pad(
|
| 698 |
+
coords, (0, 0, 0, pad_atoms), value=0.0
|
| 699 |
+
) # (1, 1, a_p, 3)
|
| 700 |
+
atom_to_token = torch.nn.functional.pad(
|
| 701 |
+
atom_to_token, (0, 0, 0, pad_atoms), value=0.0
|
| 702 |
+
) # (1, a_p, t)
|
| 703 |
+
token_to_rep_atom = torch.nn.functional.pad(
|
| 704 |
+
token_to_rep_atom,
|
| 705 |
+
(0, pad_atoms),
|
| 706 |
+
value=0.0,
|
| 707 |
+
) # (1, t, a_p)
|
| 708 |
+
token_to_center_atom = torch.nn.functional.pad(
|
| 709 |
+
token_to_center_atom,
|
| 710 |
+
(0, pad_atoms),
|
| 711 |
+
value=0.0,
|
| 712 |
+
) # (1, t, a_p)
|
| 713 |
+
r_set_to_rep_atom = torch.nn.functional.pad(
|
| 714 |
+
r_set_to_rep_atom,
|
| 715 |
+
(0, pad_atoms),
|
| 716 |
+
value=0.0,
|
| 717 |
+
) # (1, t, a_p)
|
| 718 |
+
bfactor = torch.nn.functional.pad(
|
| 719 |
+
bfactor, (0, pad_atoms), value=0.0
|
| 720 |
+
) # (1, a_p)
|
| 721 |
+
atom_plddt = torch.nn.functional.pad(
|
| 722 |
+
atom_plddt, (0, pad_atoms), value=0.0
|
| 723 |
+
) # (1, a_p)
|
| 724 |
+
|
| 725 |
+
frames_idx = torch.tensor(
|
| 726 |
+
residue_frame_atom_idx_flat,
|
| 727 |
+
dtype=torch.long,
|
| 728 |
+
).reshape(num_tokens, 3) # (t, 3)
|
| 729 |
+
frames_idx = frames_idx.unsqueeze(0).unsqueeze(1) # (1, 1, t, 3)
|
| 730 |
+
frame_resolved_mask = torch.zeros(
|
| 731 |
+
(1, 1, num_tokens), dtype=torch.bool
|
| 732 |
+
) # (1, 1, t)
|
| 733 |
+
|
| 734 |
+
msa = (
|
| 735 |
+
torch.tensor(residue_token_ids, dtype=torch.long).unsqueeze(0).unsqueeze(0)
|
| 736 |
+
) # (1, 1, t)
|
| 737 |
+
msa_paired = torch.ones((1, 1, num_tokens), dtype=torch.float32) # (1, 1, t)
|
| 738 |
+
deletion_value = torch.zeros(
|
| 739 |
+
(1, 1, num_tokens), dtype=torch.float32
|
| 740 |
+
) # (1, 1, t)
|
| 741 |
+
has_deletion = torch.zeros((1, 1, num_tokens), dtype=torch.bool) # (1, 1, t)
|
| 742 |
+
msa_mask = torch.ones((1, 1, num_tokens), dtype=torch.long) # (1, 1, t)
|
| 743 |
+
deletion_mean = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
|
| 744 |
+
profile = (
|
| 745 |
+
one_hot(
|
| 746 |
+
torch.tensor(residue_token_ids, dtype=torch.long),
|
| 747 |
+
num_classes=const.num_tokens,
|
| 748 |
+
)
|
| 749 |
+
.float()
|
| 750 |
+
.unsqueeze(0)
|
| 751 |
+
) # (1, t, n_res_type)
|
| 752 |
+
|
| 753 |
+
template_restype = one_hot(
|
| 754 |
+
torch.zeros((1, 1, num_tokens), dtype=torch.long),
|
| 755 |
+
num_classes=const.num_tokens,
|
| 756 |
+
) # (1, 1, t, n_res_type)
|
| 757 |
+
template_frame_rot = torch.zeros(
|
| 758 |
+
(1, 1, num_tokens, 3, 3), dtype=torch.float32
|
| 759 |
+
) # (1, 1, t, 3, 3)
|
| 760 |
+
template_frame_t = torch.zeros(
|
| 761 |
+
(1, 1, num_tokens, 3), dtype=torch.float32
|
| 762 |
+
) # (1, 1, t, 3)
|
| 763 |
+
template_cb = torch.zeros(
|
| 764 |
+
(1, 1, num_tokens, 3), dtype=torch.float32
|
| 765 |
+
) # (1, 1, t, 3)
|
| 766 |
+
template_ca = torch.zeros(
|
| 767 |
+
(1, 1, num_tokens, 3), dtype=torch.float32
|
| 768 |
+
) # (1, 1, t, 3)
|
| 769 |
+
template_mask_cb = torch.zeros(
|
| 770 |
+
(1, 1, num_tokens), dtype=torch.float32
|
| 771 |
+
) # (1, 1, t)
|
| 772 |
+
template_mask_frame = torch.zeros(
|
| 773 |
+
(1, 1, num_tokens), dtype=torch.float32
|
| 774 |
+
) # (1, 1, t)
|
| 775 |
+
template_mask = torch.zeros(
|
| 776 |
+
(1, 1, num_tokens), dtype=torch.float32
|
| 777 |
+
) # (1, 1, t)
|
| 778 |
+
query_to_template = torch.zeros(
|
| 779 |
+
(1, 1, num_tokens), dtype=torch.long
|
| 780 |
+
) # (1, 1, t)
|
| 781 |
+
visibility_ids = torch.zeros(
|
| 782 |
+
(1, 1, num_tokens), dtype=torch.float32
|
| 783 |
+
) # (1, 1, t)
|
| 784 |
+
|
| 785 |
+
disto_target = torch.zeros(
|
| 786 |
+
(1, num_tokens, num_tokens, 1, num_bins),
|
| 787 |
+
dtype=torch.float32,
|
| 788 |
+
) # (1, t, t, 1, n_bin)
|
| 789 |
+
disto_target[..., 0] = 1.0 # (1, t, t, 1)
|
| 790 |
+
disto_center = torch.zeros(
|
| 791 |
+
(1, num_tokens, 3), dtype=torch.float32
|
| 792 |
+
) # (1, t, 3)
|
| 793 |
+
|
| 794 |
+
features: dict[str, torch.Tensor] = {
|
| 795 |
+
"token_index": token_index,
|
| 796 |
+
"residue_index": residue_index,
|
| 797 |
+
"asym_id": asym_id,
|
| 798 |
+
"entity_id": entity_id,
|
| 799 |
+
"sym_id": sym_id,
|
| 800 |
+
"mol_type": mol_type,
|
| 801 |
+
"res_type": res_type,
|
| 802 |
+
"disto_center": disto_center,
|
| 803 |
+
"token_bonds": token_bonds,
|
| 804 |
+
"type_bonds": type_bonds,
|
| 805 |
+
"token_pad_mask": token_pad_mask,
|
| 806 |
+
"token_resolved_mask": token_resolved_mask,
|
| 807 |
+
"token_disto_mask": token_disto_mask,
|
| 808 |
+
"contact_conditioning": contact_conditioning,
|
| 809 |
+
"contact_threshold": contact_threshold,
|
| 810 |
+
"method_feature": method_feature,
|
| 811 |
+
"modified": modified,
|
| 812 |
+
"cyclic_period": cyclic_period,
|
| 813 |
+
"affinity_token_mask": affinity_token_mask,
|
| 814 |
+
"ref_pos": ref_pos,
|
| 815 |
+
"atom_resolved_mask": atom_resolved_mask,
|
| 816 |
+
"ref_atom_name_chars": ref_atom_name_chars,
|
| 817 |
+
"ref_element": ref_element,
|
| 818 |
+
"ref_charge": ref_charge,
|
| 819 |
+
"ref_chirality": ref_chirality,
|
| 820 |
+
"atom_backbone_feat": atom_backbone_feat,
|
| 821 |
+
"ref_space_uid": ref_space_uid,
|
| 822 |
+
"coords": coords,
|
| 823 |
+
"atom_pad_mask": atom_pad_mask,
|
| 824 |
+
"atom_to_token": atom_to_token,
|
| 825 |
+
"token_to_rep_atom": token_to_rep_atom,
|
| 826 |
+
"r_set_to_rep_atom": r_set_to_rep_atom,
|
| 827 |
+
"token_to_center_atom": token_to_center_atom,
|
| 828 |
+
"disto_target": disto_target,
|
| 829 |
+
"disto_coords_ensemble": disto_coords_ensemble,
|
| 830 |
+
"bfactor": bfactor,
|
| 831 |
+
"plddt": atom_plddt,
|
| 832 |
+
"frames_idx": frames_idx,
|
| 833 |
+
"frame_resolved_mask": frame_resolved_mask,
|
| 834 |
+
"msa": msa,
|
| 835 |
+
"msa_paired": msa_paired,
|
| 836 |
+
"deletion_value": deletion_value,
|
| 837 |
+
"has_deletion": has_deletion,
|
| 838 |
+
"deletion_mean": deletion_mean,
|
| 839 |
+
"profile": profile,
|
| 840 |
+
"msa_mask": msa_mask,
|
| 841 |
+
"template_restype": template_restype,
|
| 842 |
+
"template_frame_rot": template_frame_rot,
|
| 843 |
+
"template_frame_t": template_frame_t,
|
| 844 |
+
"template_cb": template_cb,
|
| 845 |
+
"template_ca": template_ca,
|
| 846 |
+
"template_mask_cb": template_mask_cb,
|
| 847 |
+
"template_mask_frame": template_mask_frame,
|
| 848 |
+
"template_mask": template_mask,
|
| 849 |
+
"query_to_template": query_to_template,
|
| 850 |
+
"visibility_ids": visibility_ids,
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
return features, template # each feature shape is traced at its binding above
|
fastplms/models/boltz/minimal_structures.py
CHANGED
|
@@ -1,21 +1,21 @@
|
|
| 1 |
-
"""Atom and residue metadata for sequence-only Boltz2 structure exports."""
|
| 2 |
-
|
| 3 |
-
from dataclasses import dataclass
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
@dataclass
|
| 7 |
-
class ProteinStructureTemplate:
|
| 8 |
-
sequence: str
|
| 9 |
-
residue_names: list[str]
|
| 10 |
-
atom_names: list[str]
|
| 11 |
-
atom_elements: list[str]
|
| 12 |
-
atom_residue_index: list[int]
|
| 13 |
-
atom_chain_id: list[str]
|
| 14 |
-
|
| 15 |
-
@property
|
| 16 |
-
def num_atoms(self) -> int:
|
| 17 |
-
return len(self.atom_names)
|
| 18 |
-
|
| 19 |
-
@property
|
| 20 |
-
def num_residues(self) -> int:
|
| 21 |
-
return len(self.residue_names)
|
|
|
|
| 1 |
+
"""Atom and residue metadata for sequence-only Boltz2 structure exports."""
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@dataclass
|
| 7 |
+
class ProteinStructureTemplate:
|
| 8 |
+
sequence: str
|
| 9 |
+
residue_names: list[str]
|
| 10 |
+
atom_names: list[str]
|
| 11 |
+
atom_elements: list[str]
|
| 12 |
+
atom_residue_index: list[int]
|
| 13 |
+
atom_chain_id: list[str]
|
| 14 |
+
|
| 15 |
+
@property
|
| 16 |
+
def num_atoms(self) -> int:
|
| 17 |
+
return len(self.atom_names)
|
| 18 |
+
|
| 19 |
+
@property
|
| 20 |
+
def num_residues(self) -> int:
|
| 21 |
+
return len(self.residue_names)
|
fastplms/models/boltz/modeling_boltz2.py
CHANGED
|
@@ -1,1061 +1,1066 @@
|
|
| 1 |
-
"""Transformers interfaces and checkpoint conversion for the Boltz2 runtime."""
|
| 2 |
-
|
| 3 |
-
import copy
|
| 4 |
-
import inspect
|
| 5 |
-
import random
|
| 6 |
-
import numpy as np
|
| 7 |
-
import torch
|
| 8 |
-
import torch.nn as nn
|
| 9 |
-
|
| 10 |
-
from collections.abc import Iterator, Mapping, Sequence
|
| 11 |
-
from contextlib import contextmanager, nullcontext
|
| 12 |
-
from dataclasses import dataclass
|
| 13 |
-
from typing import Any, ClassVar
|
| 14 |
-
from torch import Tensor
|
| 15 |
-
from tqdm.auto import tqdm
|
| 16 |
-
from transformers import PretrainedConfig, PreTrainedModel
|
| 17 |
-
from transformers.modeling_outputs import ModelOutput
|
| 18 |
-
|
| 19 |
-
from . import vb_const as const
|
| 20 |
-
from . import vb_layers_initialize as init
|
| 21 |
-
from .cif_writer import write_cif
|
| 22 |
-
from .minimal_featurizer import build_boltz2_features
|
| 23 |
-
from .minimal_structures import ProteinStructureTemplate
|
| 24 |
-
from .vb_const import bond_types as _vb_const_bond_types # noqa: F401
|
| 25 |
-
from .vb_layers_attention import AttentionPairBias as _vb_layers_attention_marker # noqa: F401
|
| 26 |
-
from .vb_layers_attentionv2 import AttentionPairBias as _vb_layers_attentionv2_marker # noqa: F401
|
| 27 |
-
from .vb_layers_confidence_utils import (
|
| 28 |
-
compute_ptms as _vb_layers_confidence_utils_marker, # noqa: F401
|
| 29 |
-
)
|
| 30 |
-
from .vb_layers_dropout import get_dropout_mask as _vb_layers_dropout_marker # noqa: F401
|
| 31 |
-
from .vb_layers_initialize import gating_init_ as _vb_layers_initialize_marker # noqa: F401
|
| 32 |
-
from .vb_layers_outer_product_mean import (
|
| 33 |
-
OuterProductMean as _vb_layers_outer_product_mean_marker, # noqa: F401
|
| 34 |
-
)
|
| 35 |
-
from .vb_layers_pair_averaging import (
|
| 36 |
-
PairWeightedAveraging as _vb_layers_pair_averaging_marker, # noqa: F401
|
| 37 |
-
)
|
| 38 |
-
from .vb_layers_pairformer import PairformerModule
|
| 39 |
-
from .vb_layers_transition import Transition as _vb_layers_transition_marker # noqa: F401
|
| 40 |
-
from .vb_layers_triangular_mult import (
|
| 41 |
-
TriangleMultiplicationIncoming as _vb_layers_triangular_mult_marker, # noqa: F401
|
| 42 |
-
)
|
| 43 |
-
from .vb_loss_diffusionv2 import weighted_rigid_align as _vb_loss_diffusionv2_marker # noqa: F401
|
| 44 |
-
from .vb_modules_confidencev2 import ConfidenceModule
|
| 45 |
-
from .vb_modules_diffusion_conditioning import DiffusionConditioning
|
| 46 |
-
from .vb_modules_diffusionv2 import AtomDiffusion, DiffusionModule
|
| 47 |
-
from .vb_modules_encodersv2 import RelativePositionEncoder
|
| 48 |
-
from .vb_modules_transformersv2 import (
|
| 49 |
-
DiffusionTransformer as _vb_modules_transformersv2_marker, # noqa: F401
|
| 50 |
-
)
|
| 51 |
-
from .vb_modules_trunkv2 import (
|
| 52 |
-
ContactConditioning,
|
| 53 |
-
DistogramModule,
|
| 54 |
-
InputEmbedder,
|
| 55 |
-
MSAModule,
|
| 56 |
-
)
|
| 57 |
-
from .vb_modules_utils import LinearNoBias as _vb_modules_utils_marker # noqa: F401
|
| 58 |
-
from .vb_potentials_potentials import (
|
| 59 |
-
get_potentials as _vb_potentials_potentials_marker, # noqa: F401
|
| 60 |
-
)
|
| 61 |
-
from .vb_potentials_schedules import (
|
| 62 |
-
ParameterSchedule as _vb_potentials_schedules_marker, # noqa: F401
|
| 63 |
-
)
|
| 64 |
-
from .vb_tri_attn_attention import (
|
| 65 |
-
TriangleAttentionStartingNode as _vb_tri_attn_attention_marker, # noqa: F401
|
| 66 |
-
)
|
| 67 |
-
from .vb_tri_attn_primitives import Attention as _vb_tri_attn_primitives_marker # noqa: F401
|
| 68 |
-
from .vb_tri_attn_utils import permute_final_dims as _vb_tri_attn_utils_marker # noqa: F401
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def _default_steering_args() -> dict[str, Any]:
|
| 72 |
-
return {
|
| 73 |
-
"fk_steering": False,
|
| 74 |
-
"num_particles": 3,
|
| 75 |
-
"fk_lambda": 4.0,
|
| 76 |
-
"fk_resampling_interval": 3,
|
| 77 |
-
"physical_guidance_update": False,
|
| 78 |
-
"contact_guidance_update": False,
|
| 79 |
-
"num_gd_steps": 16,
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def _boltz2_reference_diffusion_overrides() -> dict[str, Any]:
|
| 84 |
-
# Match Boltz2 CLI inference defaults from boltz.main/Boltz2DiffusionParams.
|
| 85 |
-
return {
|
| 86 |
-
"gamma_0": 0.8,
|
| 87 |
-
"gamma_min": 1.0,
|
| 88 |
-
"noise_scale": 1.003,
|
| 89 |
-
"rho": 7,
|
| 90 |
-
"step_scale": 1.5,
|
| 91 |
-
"sigma_min": 0.0001,
|
| 92 |
-
"sigma_max": 160.0,
|
| 93 |
-
"sigma_data": 16.0,
|
| 94 |
-
"P_mean": -1.2,
|
| 95 |
-
"P_std": 1.5,
|
| 96 |
-
"coordinate_augmentation": True,
|
| 97 |
-
"alignment_reverse_diff": True,
|
| 98 |
-
"synchronize_sigmas": True,
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def _enforce_pairformer_v2(pairformer_args: Mapping[str, Any], context: str) -> dict[str, Any]:
|
| 103 |
-
if not isinstance(pairformer_args, Mapping):
|
| 104 |
-
raise TypeError(f"Expected {context} pairformer_args to be a dictionary.")
|
| 105 |
-
out = _to_plain_python(copy.deepcopy(pairformer_args))
|
| 106 |
-
if "v2" in out and not out["v2"]:
|
| 107 |
-
raise ValueError(f"{context} pairformer_args['v2'] must be True for Boltz2.")
|
| 108 |
-
out["v2"] = True
|
| 109 |
-
return out
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def _require_key(mapping: dict[str, Any], key: str) -> Any:
|
| 113 |
-
if key not in mapping:
|
| 114 |
-
raise KeyError(f"Missing required key '{key}' in checkpoint hyperparameters.")
|
| 115 |
-
return mapping[key]
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
def _state_dict_without_wrappers(state_dict: dict[str, Tensor]) -> dict[str, Tensor]:
|
| 119 |
-
# Every state tensor has its parameter-defined shape (...).
|
| 120 |
-
cleaned: dict[str, Tensor] = {}
|
| 121 |
-
for key, value in state_dict.items():
|
| 122 |
-
if key.startswith("ema."):
|
| 123 |
-
continue
|
| 124 |
-
new_key = key
|
| 125 |
-
if new_key.startswith("model."):
|
| 126 |
-
new_key = new_key[len("model.") :]
|
| 127 |
-
if new_key.startswith("module."):
|
| 128 |
-
new_key = new_key[len("module.") :]
|
| 129 |
-
cleaned[new_key] = value # (...)
|
| 130 |
-
return cleaned
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
def _to_cpu_detached(value: Any) -> Any:
|
| 134 |
-
if torch.is_tensor(value):
|
| 135 |
-
return value.detach().cpu() # same shape (...)
|
| 136 |
-
if isinstance(value, dict):
|
| 137 |
-
out: dict[Any, Any] = {}
|
| 138 |
-
for key, nested_value in value.items():
|
| 139 |
-
out[key] = _to_cpu_detached(nested_value)
|
| 140 |
-
return out
|
| 141 |
-
if isinstance(value, list):
|
| 142 |
-
return [_to_cpu_detached(item) for item in value]
|
| 143 |
-
if isinstance(value, tuple):
|
| 144 |
-
return tuple(_to_cpu_detached(item) for item in value)
|
| 145 |
-
return value
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
@dataclass(frozen=True)
|
| 149 |
-
class _RandomState:
|
| 150 |
-
python: object
|
| 151 |
-
numpy: tuple[Any, ...]
|
| 152 |
-
torch_cpu: Tensor # (n_cpu_rng_state,)
|
| 153 |
-
torch_cuda: list[Tensor] | None # each: (n_cuda_rng_state,)
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
@contextmanager
|
| 157 |
-
def _seed_context(seed: int | None) -> Iterator[None]:
|
| 158 |
-
"""Temporarily seed every RNG used by Boltz2 and restore caller state."""
|
| 159 |
-
|
| 160 |
-
if seed is None:
|
| 161 |
-
yield
|
| 162 |
-
return
|
| 163 |
-
if isinstance(seed, bool) or not isinstance(seed, int):
|
| 164 |
-
raise TypeError(
|
| 165 |
-
"Boltz2 seed must be an int or None; bool and coerced values are not accepted."
|
| 166 |
-
)
|
| 167 |
-
state = _RandomState(
|
| 168 |
-
python=random.getstate(),
|
| 169 |
-
numpy=np.random.get_state(),
|
| 170 |
-
torch_cpu=torch.random.get_rng_state(),
|
| 171 |
-
torch_cuda=torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None,
|
| 172 |
-
)
|
| 173 |
-
normalized_seed = seed % (2**32)
|
| 174 |
-
random.seed(normalized_seed)
|
| 175 |
-
np.random.seed(normalized_seed)
|
| 176 |
-
torch.manual_seed(normalized_seed)
|
| 177 |
-
if torch.cuda.is_available():
|
| 178 |
-
torch.cuda.manual_seed_all(normalized_seed)
|
| 179 |
-
try:
|
| 180 |
-
yield
|
| 181 |
-
finally:
|
| 182 |
-
random.setstate(state.python)
|
| 183 |
-
np.random.set_state(state.numpy)
|
| 184 |
-
torch.random.set_rng_state(state.torch_cpu)
|
| 185 |
-
if state.torch_cuda is not None:
|
| 186 |
-
torch.cuda.set_rng_state_all(state.torch_cuda)
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
def _to_plain_python(value: Any) -> Any:
|
| 190 |
-
if isinstance(value, Mapping):
|
| 191 |
-
out: dict[Any, Any] = {}
|
| 192 |
-
for key, nested_value in value.items():
|
| 193 |
-
out[key] = _to_plain_python(nested_value)
|
| 194 |
-
return out
|
| 195 |
-
if isinstance(value, list):
|
| 196 |
-
return [_to_plain_python(item) for item in value]
|
| 197 |
-
if isinstance(value, tuple):
|
| 198 |
-
return [_to_plain_python(item) for item in value]
|
| 199 |
-
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
| 200 |
-
return [_to_plain_python(item) for item in value]
|
| 201 |
-
return value
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
def _filtered_kwargs(target: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
|
| 205 |
-
signature = inspect.signature(target.__init__)
|
| 206 |
-
allowed = set(signature.parameters.keys())
|
| 207 |
-
allowed.discard("self")
|
| 208 |
-
filtered: dict[str, Any] = {}
|
| 209 |
-
for key, value in kwargs.items():
|
| 210 |
-
if key in allowed:
|
| 211 |
-
filtered[key] = value
|
| 212 |
-
return filtered
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
@dataclass
|
| 216 |
-
class Boltz2ModelOutput(ModelOutput):
|
| 217 |
-
"""Raw Boltz2 inference output with standard AutoModel controls."""
|
| 218 |
-
|
| 219 |
-
last_hidden_state: torch.Tensor | None = None # (b, t, d_s) or None
|
| 220 |
-
hidden_states: tuple[torch.Tensor, ...] | None = None # each shape traced at source
|
| 221 |
-
attentions: tuple[torch.Tensor, ...] | None = None # unsupported; always None
|
| 222 |
-
pdistogram: torch.Tensor | None = None # (b, t, t, n_distogram, n_bin) or None
|
| 223 |
-
s: torch.Tensor | None = None # (b, t, d_s) or None
|
| 224 |
-
z: torch.Tensor | None = None # (b, t, t, d_z) or None
|
| 225 |
-
sample_atom_coords: torch.Tensor | None = None # (b * m, a, 3) or None
|
| 226 |
-
diff_token_repr: torch.Tensor | None = None # (...) or None
|
| 227 |
-
s_conf: torch.Tensor | None = None # (...) or None
|
| 228 |
-
z_conf: torch.Tensor | None = None # (...) or None
|
| 229 |
-
pde_logits: torch.Tensor | None = None # (...) or None
|
| 230 |
-
plddt_logits: torch.Tensor | None = None # (...) or None
|
| 231 |
-
resolved_logits: torch.Tensor | None = None # (...) or None
|
| 232 |
-
pde: torch.Tensor | None = None # (...) or None
|
| 233 |
-
plddt: torch.Tensor | None = None # (...) or None
|
| 234 |
-
complex_plddt: torch.Tensor | None = None # (...) or None
|
| 235 |
-
complex_iplddt: torch.Tensor | None = None # (...) or None
|
| 236 |
-
complex_pde: torch.Tensor | None = None # (...) or None
|
| 237 |
-
complex_ipde: torch.Tensor | None = None # (...) or None
|
| 238 |
-
pae_logits: torch.Tensor | None = None # (...) or None
|
| 239 |
-
pae: torch.Tensor | None = None # (...) or None
|
| 240 |
-
ptm: torch.Tensor | None = None # (...) or None
|
| 241 |
-
iptm: torch.Tensor | None = None # (...) or None
|
| 242 |
-
ligand_iptm: torch.Tensor | None = None # (...) or None
|
| 243 |
-
protein_iptm: torch.Tensor | None = None # (...) or None
|
| 244 |
-
pair_chains_iptm: Any = None
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
@dataclass
|
| 248 |
-
class Boltz2StructureOutput(ModelOutput):
|
| 249 |
-
sample_atom_coords: torch.Tensor | None = None # (m, a_p, 3) or None
|
| 250 |
-
atom_pad_mask: torch.Tensor | None = None # (a_p,) or None
|
| 251 |
-
plddt: torch.Tensor | None = None # (m, t) or (m, a_p), depending on confidence resolution
|
| 252 |
-
confidence_score: torch.Tensor | None = None # (m,) or None
|
| 253 |
-
complex_plddt: torch.Tensor | None = None # (m,) or None
|
| 254 |
-
iptm: torch.Tensor | None = None # (m,) or None
|
| 255 |
-
ptm: torch.Tensor | None = None # (m,) or None
|
| 256 |
-
sequence: str | None = None
|
| 257 |
-
structure_template: ProteinStructureTemplate | None = None
|
| 258 |
-
raw_output: dict[str, torch.Tensor] | None = None # values keep model-output shapes
|
| 259 |
-
seed: int | None = None
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
class Boltz2Config(PretrainedConfig):
|
| 263 |
-
model_type = "boltz2_automodel"
|
| 264 |
-
|
| 265 |
-
def __init__(
|
| 266 |
-
self,
|
| 267 |
-
core_kwargs: dict[str, Any] | None = None,
|
| 268 |
-
num_bins: int = 64,
|
| 269 |
-
default_recycling_steps: int = 3,
|
| 270 |
-
default_sampling_steps: int = 200,
|
| 271 |
-
default_diffusion_samples: int = 1,
|
| 272 |
-
**kwargs,
|
| 273 |
-
) -> None:
|
| 274 |
-
super().__init__(**kwargs)
|
| 275 |
-
if core_kwargs is None:
|
| 276 |
-
core_kwargs = {}
|
| 277 |
-
self.core_kwargs = core_kwargs
|
| 278 |
-
self.num_bins = num_bins
|
| 279 |
-
self.default_recycling_steps = default_recycling_steps
|
| 280 |
-
self.default_sampling_steps = default_sampling_steps
|
| 281 |
-
self.default_diffusion_samples = default_diffusion_samples
|
| 282 |
-
|
| 283 |
-
@classmethod
|
| 284 |
-
def from_hyperparameters(
|
| 285 |
-
cls,
|
| 286 |
-
hparams: dict[str, Any],
|
| 287 |
-
use_kernels: bool = False,
|
| 288 |
-
default_recycling_steps: int | None = None,
|
| 289 |
-
default_sampling_steps: int | None = None,
|
| 290 |
-
default_diffusion_samples: int | None = None,
|
| 291 |
-
) -> "Boltz2Config":
|
| 292 |
-
if not isinstance(hparams, dict):
|
| 293 |
-
raise TypeError("Expected checkpoint hyperparameters as a dictionary.")
|
| 294 |
-
required = [
|
| 295 |
-
"atom_s",
|
| 296 |
-
"atom_z",
|
| 297 |
-
"token_s",
|
| 298 |
-
"token_z",
|
| 299 |
-
"num_bins",
|
| 300 |
-
"embedder_args",
|
| 301 |
-
"msa_args",
|
| 302 |
-
"pairformer_args",
|
| 303 |
-
"score_model_args",
|
| 304 |
-
"diffusion_process_args",
|
| 305 |
-
]
|
| 306 |
-
for key in required:
|
| 307 |
-
_require_key(hparams, key)
|
| 308 |
-
|
| 309 |
-
pairformer_args = _enforce_pairformer_v2(
|
| 310 |
-
hparams["pairformer_args"],
|
| 311 |
-
context="checkpoint",
|
| 312 |
-
)
|
| 313 |
-
diffusion_process_args = _to_plain_python(copy.deepcopy(hparams["diffusion_process_args"]))
|
| 314 |
-
diffusion_overrides = _boltz2_reference_diffusion_overrides()
|
| 315 |
-
for key in diffusion_overrides:
|
| 316 |
-
diffusion_process_args[key] = diffusion_overrides[key]
|
| 317 |
-
|
| 318 |
-
core_kwargs: dict[str, Any] = {
|
| 319 |
-
"atom_s": hparams["atom_s"],
|
| 320 |
-
"atom_z": hparams["atom_z"],
|
| 321 |
-
"token_s": hparams["token_s"],
|
| 322 |
-
"token_z": hparams["token_z"],
|
| 323 |
-
"num_bins": hparams["num_bins"],
|
| 324 |
-
"embedder_args": _to_plain_python(copy.deepcopy(hparams["embedder_args"])),
|
| 325 |
-
"msa_args": _to_plain_python(copy.deepcopy(hparams["msa_args"])),
|
| 326 |
-
"pairformer_args": pairformer_args,
|
| 327 |
-
"score_model_args": _to_plain_python(copy.deepcopy(hparams["score_model_args"])),
|
| 328 |
-
"diffusion_process_args": diffusion_process_args,
|
| 329 |
-
"use_kernels": use_kernels,
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
if "confidence_model_args" in hparams:
|
| 333 |
-
confidence_model_args = _to_plain_python(
|
| 334 |
-
copy.deepcopy(hparams["confidence_model_args"])
|
| 335 |
-
)
|
| 336 |
-
if "pairformer_args" in confidence_model_args:
|
| 337 |
-
confidence_model_args["pairformer_args"] = _enforce_pairformer_v2(
|
| 338 |
-
confidence_model_args["pairformer_args"],
|
| 339 |
-
context="confidence",
|
| 340 |
-
)
|
| 341 |
-
core_kwargs["confidence_model_args"] = confidence_model_args
|
| 342 |
-
else:
|
| 343 |
-
core_kwargs["confidence_model_args"] = None
|
| 344 |
-
|
| 345 |
-
core_kwargs["confidence_prediction"] = hparams.get("confidence_prediction", True)
|
| 346 |
-
core_kwargs["token_level_confidence"] = hparams.get("token_level_confidence", True)
|
| 347 |
-
core_kwargs["alpha_pae"] = hparams.get("alpha_pae", 0.0)
|
| 348 |
-
core_kwargs["atoms_per_window_queries"] = hparams.get("atoms_per_window_queries", 32)
|
| 349 |
-
core_kwargs["atoms_per_window_keys"] = hparams.get("atoms_per_window_keys", 128)
|
| 350 |
-
core_kwargs["atom_feature_dim"] = hparams.get("atom_feature_dim", 128)
|
| 351 |
-
core_kwargs["bond_type_feature"] = hparams.get("bond_type_feature", False)
|
| 352 |
-
core_kwargs["run_trunk_and_structure"] = hparams.get("run_trunk_and_structure", True)
|
| 353 |
-
core_kwargs["skip_run_structure"] = hparams.get("skip_run_structure", False)
|
| 354 |
-
core_kwargs["fix_sym_check"] = hparams.get("fix_sym_check", False)
|
| 355 |
-
core_kwargs["cyclic_pos_enc"] = hparams.get("cyclic_pos_enc", False)
|
| 356 |
-
core_kwargs["use_no_atom_char"] = hparams.get("use_no_atom_char", False)
|
| 357 |
-
core_kwargs["use_atom_backbone_feat"] = hparams.get("use_atom_backbone_feat", False)
|
| 358 |
-
core_kwargs["use_residue_feats_atoms"] = hparams.get("use_residue_feats_atoms", False)
|
| 359 |
-
core_kwargs["conditioning_cutoff_min"] = hparams.get("conditioning_cutoff_min", 4.0)
|
| 360 |
-
core_kwargs["conditioning_cutoff_max"] = hparams.get("conditioning_cutoff_max", 20.0)
|
| 361 |
-
|
| 362 |
-
if "steering_args" in hparams and hparams["steering_args"] is not None:
|
| 363 |
-
core_kwargs["steering_args"] = _to_plain_python(copy.deepcopy(hparams["steering_args"]))
|
| 364 |
-
else:
|
| 365 |
-
core_kwargs["steering_args"] = _default_steering_args()
|
| 366 |
-
|
| 367 |
-
if "validation_args" in hparams:
|
| 368 |
-
validation_args = hparams["validation_args"]
|
| 369 |
-
if not isinstance(validation_args, Mapping):
|
| 370 |
-
raise TypeError(
|
| 371 |
-
"Expected 'validation_args' in checkpoint hyperparameters to be a mapping."
|
| 372 |
-
)
|
| 373 |
-
if default_recycling_steps is None and "recycling_steps" in validation_args:
|
| 374 |
-
default_recycling_steps = validation_args["recycling_steps"]
|
| 375 |
-
if default_sampling_steps is None and "sampling_steps" in validation_args:
|
| 376 |
-
default_sampling_steps = validation_args["sampling_steps"]
|
| 377 |
-
if default_diffusion_samples is None and "diffusion_samples" in validation_args:
|
| 378 |
-
default_diffusion_samples = validation_args["diffusion_samples"]
|
| 379 |
-
|
| 380 |
-
if default_recycling_steps is None:
|
| 381 |
-
default_recycling_steps = 3
|
| 382 |
-
if default_sampling_steps is None:
|
| 383 |
-
default_sampling_steps = 200
|
| 384 |
-
if default_diffusion_samples is None:
|
| 385 |
-
default_diffusion_samples = 1
|
| 386 |
-
|
| 387 |
-
return cls(
|
| 388 |
-
core_kwargs=core_kwargs,
|
| 389 |
-
num_bins=hparams["num_bins"],
|
| 390 |
-
default_recycling_steps=default_recycling_steps,
|
| 391 |
-
default_sampling_steps=default_sampling_steps,
|
| 392 |
-
default_diffusion_samples=default_diffusion_samples,
|
| 393 |
-
)
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
class Boltz2InferenceCore(nn.Module):
|
| 397 |
-
def __init__(
|
| 398 |
-
self,
|
| 399 |
-
atom_s: int,
|
| 400 |
-
atom_z: int,
|
| 401 |
-
token_s: int,
|
| 402 |
-
token_z: int,
|
| 403 |
-
num_bins: int,
|
| 404 |
-
embedder_args: dict[str, Any],
|
| 405 |
-
msa_args: dict[str, Any],
|
| 406 |
-
pairformer_args: dict[str, Any],
|
| 407 |
-
score_model_args: dict[str, Any],
|
| 408 |
-
diffusion_process_args: dict[str, Any],
|
| 409 |
-
confidence_model_args: dict[str, Any] | None = None,
|
| 410 |
-
atom_feature_dim: int = 128,
|
| 411 |
-
confidence_prediction: bool = True,
|
| 412 |
-
token_level_confidence: bool = True,
|
| 413 |
-
alpha_pae: float = 0.0,
|
| 414 |
-
atoms_per_window_queries: int = 32,
|
| 415 |
-
atoms_per_window_keys: int = 128,
|
| 416 |
-
run_trunk_and_structure: bool = True,
|
| 417 |
-
skip_run_structure: bool = False,
|
| 418 |
-
bond_type_feature: bool = False,
|
| 419 |
-
fix_sym_check: bool = False,
|
| 420 |
-
cyclic_pos_enc: bool = False,
|
| 421 |
-
use_no_atom_char: bool = False,
|
| 422 |
-
use_atom_backbone_feat: bool = False,
|
| 423 |
-
use_residue_feats_atoms: bool = False,
|
| 424 |
-
conditioning_cutoff_min: float = 4.0,
|
| 425 |
-
conditioning_cutoff_max: float = 20.0,
|
| 426 |
-
use_kernels: bool = False,
|
| 427 |
-
steering_args: dict[str, Any] | None = None,
|
| 428 |
-
) -> None:
|
| 429 |
-
super().__init__()
|
| 430 |
-
self.use_kernels = use_kernels
|
| 431 |
-
self.confidence_prediction = confidence_prediction
|
| 432 |
-
self.token_level_confidence = token_level_confidence
|
| 433 |
-
self.alpha_pae = alpha_pae
|
| 434 |
-
self.run_trunk_and_structure = run_trunk_and_structure
|
| 435 |
-
self.skip_run_structure = skip_run_structure
|
| 436 |
-
self.bond_type_feature = bond_type_feature
|
| 437 |
-
self.steering_args = (
|
| 438 |
-
steering_args if steering_args is not None else _default_steering_args()
|
| 439 |
-
)
|
| 440 |
-
if "v2" not in pairformer_args:
|
| 441 |
-
raise ValueError("Boltz2 requires pairformer_args['v2'].")
|
| 442 |
-
if not pairformer_args["v2"]:
|
| 443 |
-
raise ValueError("Boltz2 requires pairformer_args['v2']=True.")
|
| 444 |
-
|
| 445 |
-
full_embedder_args = {
|
| 446 |
-
"atom_s": atom_s,
|
| 447 |
-
"atom_z": atom_z,
|
| 448 |
-
"token_s": token_s,
|
| 449 |
-
"token_z": token_z,
|
| 450 |
-
"atoms_per_window_queries": atoms_per_window_queries,
|
| 451 |
-
"atoms_per_window_keys": atoms_per_window_keys,
|
| 452 |
-
"atom_feature_dim": atom_feature_dim,
|
| 453 |
-
"use_no_atom_char": use_no_atom_char,
|
| 454 |
-
"use_atom_backbone_feat": use_atom_backbone_feat,
|
| 455 |
-
"use_residue_feats_atoms": use_residue_feats_atoms,
|
| 456 |
-
**embedder_args,
|
| 457 |
-
}
|
| 458 |
-
full_embedder_args = _filtered_kwargs(InputEmbedder, full_embedder_args)
|
| 459 |
-
self.input_embedder = InputEmbedder(**full_embedder_args)
|
| 460 |
-
|
| 461 |
-
self.s_init = nn.Linear(token_s, token_s, bias=False)
|
| 462 |
-
self.z_init_1 = nn.Linear(token_s, token_z, bias=False)
|
| 463 |
-
self.z_init_2 = nn.Linear(token_s, token_z, bias=False)
|
| 464 |
-
self.rel_pos = RelativePositionEncoder(
|
| 465 |
-
token_z,
|
| 466 |
-
fix_sym_check=fix_sym_check,
|
| 467 |
-
cyclic_pos_enc=cyclic_pos_enc,
|
| 468 |
-
)
|
| 469 |
-
self.token_bonds = nn.Linear(1, token_z, bias=False)
|
| 470 |
-
if self.bond_type_feature:
|
| 471 |
-
self.token_bonds_type = nn.Embedding(len(const.bond_types) + 1, token_z)
|
| 472 |
-
|
| 473 |
-
self.contact_conditioning = ContactConditioning(
|
| 474 |
-
token_z=token_z,
|
| 475 |
-
cutoff_min=conditioning_cutoff_min,
|
| 476 |
-
cutoff_max=conditioning_cutoff_max,
|
| 477 |
-
)
|
| 478 |
-
self.s_norm = nn.LayerNorm(token_s)
|
| 479 |
-
self.z_norm = nn.LayerNorm(token_z)
|
| 480 |
-
|
| 481 |
-
self.s_recycle = nn.Linear(token_s, token_s, bias=False)
|
| 482 |
-
self.z_recycle = nn.Linear(token_z, token_z, bias=False)
|
| 483 |
-
init.gating_init_(self.s_recycle.weight)
|
| 484 |
-
init.gating_init_(self.z_recycle.weight)
|
| 485 |
-
|
| 486 |
-
msa_kwargs = _filtered_kwargs(
|
| 487 |
-
MSAModule,
|
| 488 |
-
{"token_z": token_z, "token_s": token_s, **msa_args},
|
| 489 |
-
)
|
| 490 |
-
self.msa_module = MSAModule(**msa_kwargs)
|
| 491 |
-
|
| 492 |
-
pairformer_kwargs = _filtered_kwargs(
|
| 493 |
-
PairformerModule,
|
| 494 |
-
{"token_s": token_s, "token_z": token_z, **pairformer_args},
|
| 495 |
-
)
|
| 496 |
-
if "token_s" not in pairformer_kwargs or "token_z" not in pairformer_kwargs:
|
| 497 |
-
raise RuntimeError("Boltz2 pairformer construction lost its token dimensions.")
|
| 498 |
-
pairformer_token_s = pairformer_kwargs.pop("token_s")
|
| 499 |
-
pairformer_token_z = pairformer_kwargs.pop("token_z")
|
| 500 |
-
self.pairformer_module = PairformerModule(
|
| 501 |
-
pairformer_token_s,
|
| 502 |
-
pairformer_token_z,
|
| 503 |
-
**pairformer_kwargs,
|
| 504 |
-
)
|
| 505 |
-
|
| 506 |
-
diffusion_conditioning_kwargs = {
|
| 507 |
-
"token_s": token_s,
|
| 508 |
-
"token_z": token_z,
|
| 509 |
-
"atom_s": atom_s,
|
| 510 |
-
"atom_z": atom_z,
|
| 511 |
-
"atoms_per_window_queries": atoms_per_window_queries,
|
| 512 |
-
"atoms_per_window_keys": atoms_per_window_keys,
|
| 513 |
-
"atom_encoder_depth": score_model_args["atom_encoder_depth"],
|
| 514 |
-
"atom_encoder_heads": score_model_args["atom_encoder_heads"],
|
| 515 |
-
"token_transformer_depth": score_model_args["token_transformer_depth"],
|
| 516 |
-
"token_transformer_heads": score_model_args["token_transformer_heads"],
|
| 517 |
-
"atom_decoder_depth": score_model_args["atom_decoder_depth"],
|
| 518 |
-
"atom_decoder_heads": score_model_args["atom_decoder_heads"],
|
| 519 |
-
"atom_feature_dim": atom_feature_dim,
|
| 520 |
-
"conditioning_transition_layers": score_model_args["conditioning_transition_layers"],
|
| 521 |
-
"use_no_atom_char": use_no_atom_char,
|
| 522 |
-
"use_atom_backbone_feat": use_atom_backbone_feat,
|
| 523 |
-
"use_residue_feats_atoms": use_residue_feats_atoms,
|
| 524 |
-
}
|
| 525 |
-
diffusion_conditioning_kwargs = _filtered_kwargs(
|
| 526 |
-
DiffusionConditioning,
|
| 527 |
-
diffusion_conditioning_kwargs,
|
| 528 |
-
)
|
| 529 |
-
self.diffusion_conditioning = DiffusionConditioning(**diffusion_conditioning_kwargs)
|
| 530 |
-
|
| 531 |
-
structure_score_model_args = {
|
| 532 |
-
"token_s": token_s,
|
| 533 |
-
"atom_s": atom_s,
|
| 534 |
-
"atoms_per_window_queries": atoms_per_window_queries,
|
| 535 |
-
"atoms_per_window_keys": atoms_per_window_keys,
|
| 536 |
-
**score_model_args,
|
| 537 |
-
}
|
| 538 |
-
structure_score_model_args = _filtered_kwargs(
|
| 539 |
-
DiffusionModule,
|
| 540 |
-
structure_score_model_args,
|
| 541 |
-
)
|
| 542 |
-
structure_module_kwargs = {
|
| 543 |
-
"score_model_args": structure_score_model_args,
|
| 544 |
-
"compile_score": False,
|
| 545 |
-
**diffusion_process_args,
|
| 546 |
-
}
|
| 547 |
-
structure_module_kwargs = _filtered_kwargs(AtomDiffusion, structure_module_kwargs)
|
| 548 |
-
self.structure_module = AtomDiffusion(**structure_module_kwargs)
|
| 549 |
-
self.distogram_module = DistogramModule(token_z, num_bins)
|
| 550 |
-
|
| 551 |
-
if self.confidence_prediction:
|
| 552 |
-
if confidence_model_args is None:
|
| 553 |
-
raise ValueError(
|
| 554 |
-
"confidence_prediction=True requires confidence_model_args in config."
|
| 555 |
-
)
|
| 556 |
-
confidence_kwargs = {
|
| 557 |
-
"token_s": token_s,
|
| 558 |
-
"token_z": token_z,
|
| 559 |
-
"token_level_confidence": token_level_confidence,
|
| 560 |
-
"bond_type_feature": bond_type_feature,
|
| 561 |
-
"fix_sym_check": fix_sym_check,
|
| 562 |
-
"cyclic_pos_enc": cyclic_pos_enc,
|
| 563 |
-
"conditioning_cutoff_min": conditioning_cutoff_min,
|
| 564 |
-
"conditioning_cutoff_max": conditioning_cutoff_max,
|
| 565 |
-
**confidence_model_args,
|
| 566 |
-
}
|
| 567 |
-
confidence_kwargs = _filtered_kwargs(ConfidenceModule, confidence_kwargs)
|
| 568 |
-
self.confidence_module = ConfidenceModule(**confidence_kwargs)
|
| 569 |
-
|
| 570 |
-
def forward(
|
| 571 |
-
self,
|
| 572 |
-
feats: dict[str, Tensor],
|
| 573 |
-
recycling_steps: int = 3,
|
| 574 |
-
num_sampling_steps: int | None = None,
|
| 575 |
-
diffusion_samples: int = 1,
|
| 576 |
-
max_parallel_samples: int | None = None,
|
| 577 |
-
run_confidence_sequentially: bool = True,
|
| 578 |
-
detach_confidence: bool = True,
|
| 579 |
-
verbose: bool = False,
|
| 580 |
-
) -> dict[str, Tensor]:
|
| 581 |
-
# b is the batch size, t the token count, a the padded atom count,
|
| 582 |
-
# d_s the token width, d_z the pair width, and m the diffusion multiplicity.
|
| 583 |
-
with tqdm(
|
| 584 |
-
total=1,
|
| 585 |
-
desc="Boltz2: Input embeddings",
|
| 586 |
-
disable=not verbose,
|
| 587 |
-
dynamic_ncols=True,
|
| 588 |
-
leave=False,
|
| 589 |
-
) as progress:
|
| 590 |
-
s_inputs = self.input_embedder(feats) # (b, t, d_s)
|
| 591 |
-
progress.update()
|
| 592 |
-
s_init = self.s_init(s_inputs) # (b, t, d_s)
|
| 593 |
-
|
| 594 |
-
z_init = (
|
| 595 |
-
self.z_init_1(s_inputs)[:, :, None] + self.z_init_2(s_inputs)[:, None, :]
|
| 596 |
-
) # (b, t, t, d_z)
|
| 597 |
-
relative_position_encoding = self.rel_pos(feats) # (b, t, t, d_z)
|
| 598 |
-
z_init = z_init + relative_position_encoding # (b, t, t, d_z)
|
| 599 |
-
z_init = z_init + self.token_bonds(
|
| 600 |
-
feats["token_bonds"].float()
|
| 601 |
-
) # (b, t, t, d_z)
|
| 602 |
-
if self.bond_type_feature:
|
| 603 |
-
z_init = z_init + self.token_bonds_type(
|
| 604 |
-
feats["type_bonds"].long()
|
| 605 |
-
) # (b, t, t, d_z)
|
| 606 |
-
z_init = z_init + self.contact_conditioning(feats) # (b, t, t, d_z)
|
| 607 |
-
|
| 608 |
-
s = torch.zeros_like(s_init) # (b, t, d_s)
|
| 609 |
-
z = torch.zeros_like(z_init) # (b, t, t, d_z)
|
| 610 |
-
mask = feats["token_pad_mask"].float() # (b, t)
|
| 611 |
-
pair_mask = mask[:, :, None] * mask[:, None, :] # (b, t, t)
|
| 612 |
-
|
| 613 |
-
if self.run_trunk_and_structure:
|
| 614 |
-
for _ in tqdm(
|
| 615 |
-
range(recycling_steps + 1),
|
| 616 |
-
desc="Boltz2: Recycling",
|
| 617 |
-
unit="cycle",
|
| 618 |
-
disable=not verbose,
|
| 619 |
-
dynamic_ncols=True,
|
| 620 |
-
leave=False,
|
| 621 |
-
):
|
| 622 |
-
s = s_init + self.s_recycle(self.s_norm(s)) # (b, t, d_s)
|
| 623 |
-
z = z_init + self.z_recycle(self.z_norm(z)) # (b, t, t, d_z)
|
| 624 |
-
z = z + self.msa_module(
|
| 625 |
-
z,
|
| 626 |
-
s_inputs,
|
| 627 |
-
feats,
|
| 628 |
-
use_kernels=self.use_kernels,
|
| 629 |
-
) # (b, t, t, d_z)
|
| 630 |
-
s, z = self.pairformer_module(
|
| 631 |
-
s,
|
| 632 |
-
z,
|
| 633 |
-
mask=mask,
|
| 634 |
-
pair_mask=pair_mask,
|
| 635 |
-
use_kernels=self.use_kernels,
|
| 636 |
-
) # (b, t, d_s), (b, t, t, d_z)
|
| 637 |
-
|
| 638 |
-
pdistogram = self.distogram_module(z) # (b, t, t, n_distogram, n_bin)
|
| 639 |
-
output: dict[str, Tensor] = {
|
| 640 |
-
"pdistogram": pdistogram,
|
| 641 |
-
"s": s,
|
| 642 |
-
"z": z,
|
| 643 |
-
}
|
| 644 |
-
|
| 645 |
-
if self.run_trunk_and_structure and (not self.skip_run_structure):
|
| 646 |
-
q, c, to_keys, atom_enc_bias, atom_dec_bias, token_trans_bias = (
|
| 647 |
-
self.diffusion_conditioning(
|
| 648 |
-
s_trunk=s,
|
| 649 |
-
z_trunk=z,
|
| 650 |
-
relative_position_encoding=relative_position_encoding,
|
| 651 |
-
feats=feats,
|
| 652 |
-
)
|
| 653 |
-
) # q/c: (b, a, d_a); biases retain their attention-specific axes
|
| 654 |
-
diffusion_conditioning = {
|
| 655 |
-
"q": q,
|
| 656 |
-
"c": c,
|
| 657 |
-
"to_keys": to_keys,
|
| 658 |
-
"atom_enc_bias": atom_enc_bias,
|
| 659 |
-
"atom_dec_bias": atom_dec_bias,
|
| 660 |
-
"token_trans_bias": token_trans_bias,
|
| 661 |
-
}
|
| 662 |
-
with torch.autocast("cuda", enabled=False):
|
| 663 |
-
struct_out = self.structure_module.sample(
|
| 664 |
-
s_trunk=s.float(),
|
| 665 |
-
s_inputs=s_inputs.float(),
|
| 666 |
-
feats=feats,
|
| 667 |
-
num_sampling_steps=num_sampling_steps,
|
| 668 |
-
atom_mask=feats["atom_pad_mask"].float(),
|
| 669 |
-
multiplicity=diffusion_samples,
|
| 670 |
-
max_parallel_samples=max_parallel_samples,
|
| 671 |
-
steering_args=self.steering_args,
|
| 672 |
-
diffusion_conditioning=diffusion_conditioning,
|
| 673 |
-
verbose=verbose,
|
| 674 |
-
) # tensor values include sample_atom_coords: (b * m, a, 3)
|
| 675 |
-
output.update(struct_out)
|
| 676 |
-
|
| 677 |
-
if self.confidence_prediction:
|
| 678 |
-
if self.skip_run_structure:
|
| 679 |
-
x_pred = feats["coords"].repeat_interleave(
|
| 680 |
-
diffusion_samples, 0
|
| 681 |
-
) # (b * m, ..., a, 3)
|
| 682 |
-
else:
|
| 683 |
-
if "sample_atom_coords" not in output:
|
| 684 |
-
raise RuntimeError("Structure sampling did not produce sample_atom_coords.")
|
| 685 |
-
x_pred = output["sample_atom_coords"] # (b * m, a, 3)
|
| 686 |
-
|
| 687 |
-
if detach_confidence:
|
| 688 |
-
s_inputs_c = s_inputs.detach() # (b, t, d_s)
|
| 689 |
-
s_c = s.detach() # (b, t, d_s)
|
| 690 |
-
z_c = z.detach() # (b, t, t, d_z)
|
| 691 |
-
x_pred_c = x_pred.detach() # (b * m, ..., a, 3)
|
| 692 |
-
pdist_c = output["pdistogram"][:, :, :, 0].detach() # (b, t, t, n_bin)
|
| 693 |
-
else:
|
| 694 |
-
s_inputs_c = s_inputs # (b, t, d_s)
|
| 695 |
-
s_c = s # (b, t, d_s)
|
| 696 |
-
z_c = z # (b, t, t, d_z)
|
| 697 |
-
x_pred_c = x_pred # (b * m, ..., a, 3)
|
| 698 |
-
pdist_c = output["pdistogram"][:, :, :, 0] # (b, t, t, n_bin)
|
| 699 |
-
|
| 700 |
-
with tqdm(
|
| 701 |
-
total=1,
|
| 702 |
-
desc="Boltz2: Confidence",
|
| 703 |
-
disable=not verbose,
|
| 704 |
-
dynamic_ncols=True,
|
| 705 |
-
leave=False,
|
| 706 |
-
) as progress:
|
| 707 |
-
output.update(
|
| 708 |
-
self.confidence_module(
|
| 709 |
-
s_inputs=s_inputs_c,
|
| 710 |
-
s=s_c,
|
| 711 |
-
z=z_c,
|
| 712 |
-
x_pred=x_pred_c,
|
| 713 |
-
feats=feats,
|
| 714 |
-
pred_distogram_logits=pdist_c,
|
| 715 |
-
multiplicity=diffusion_samples,
|
| 716 |
-
run_sequentially=run_confidence_sequentially,
|
| 717 |
-
use_kernels=self.use_kernels,
|
| 718 |
-
)
|
| 719 |
-
)
|
| 720 |
-
progress.update()
|
| 721 |
-
|
| 722 |
-
return output # named tensors retain the shapes traced above
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
class Boltz2Model(PreTrainedModel):
|
| 726 |
-
config_class = Boltz2Config
|
| 727 |
-
base_model_prefix = "core"
|
| 728 |
-
all_tied_weights_keys: ClassVar[dict[str, str]] = {}
|
| 729 |
-
|
| 730 |
-
def __init__(self, config: Boltz2Config) -> None:
|
| 731 |
-
super().__init__(config)
|
| 732 |
-
if not isinstance(config.core_kwargs, dict):
|
| 733 |
-
raise TypeError("config.core_kwargs must be a dictionary.")
|
| 734 |
-
self.core = Boltz2InferenceCore(**config.core_kwargs)
|
| 735 |
-
|
| 736 |
-
def _init_weights(self, module: nn.Module) -> None:
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
#
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
if
|
| 849 |
-
raise RuntimeError(
|
| 850 |
-
"Model has unexpected
|
| 851 |
-
f"
|
| 852 |
-
)
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
if
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
diffusion_samples=
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Transformers interfaces and checkpoint conversion for the Boltz2 runtime."""
|
| 2 |
+
|
| 3 |
+
import copy
|
| 4 |
+
import inspect
|
| 5 |
+
import random
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
|
| 10 |
+
from collections.abc import Iterator, Mapping, Sequence
|
| 11 |
+
from contextlib import contextmanager, nullcontext
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from typing import Any, ClassVar
|
| 14 |
+
from torch import Tensor
|
| 15 |
+
from tqdm.auto import tqdm
|
| 16 |
+
from transformers import PretrainedConfig, PreTrainedModel
|
| 17 |
+
from transformers.modeling_outputs import ModelOutput
|
| 18 |
+
|
| 19 |
+
from . import vb_const as const
|
| 20 |
+
from . import vb_layers_initialize as init
|
| 21 |
+
from .cif_writer import write_cif
|
| 22 |
+
from .minimal_featurizer import build_boltz2_features
|
| 23 |
+
from .minimal_structures import ProteinStructureTemplate
|
| 24 |
+
from .vb_const import bond_types as _vb_const_bond_types # noqa: F401
|
| 25 |
+
from .vb_layers_attention import AttentionPairBias as _vb_layers_attention_marker # noqa: F401
|
| 26 |
+
from .vb_layers_attentionv2 import AttentionPairBias as _vb_layers_attentionv2_marker # noqa: F401
|
| 27 |
+
from .vb_layers_confidence_utils import (
|
| 28 |
+
compute_ptms as _vb_layers_confidence_utils_marker, # noqa: F401
|
| 29 |
+
)
|
| 30 |
+
from .vb_layers_dropout import get_dropout_mask as _vb_layers_dropout_marker # noqa: F401
|
| 31 |
+
from .vb_layers_initialize import gating_init_ as _vb_layers_initialize_marker # noqa: F401
|
| 32 |
+
from .vb_layers_outer_product_mean import (
|
| 33 |
+
OuterProductMean as _vb_layers_outer_product_mean_marker, # noqa: F401
|
| 34 |
+
)
|
| 35 |
+
from .vb_layers_pair_averaging import (
|
| 36 |
+
PairWeightedAveraging as _vb_layers_pair_averaging_marker, # noqa: F401
|
| 37 |
+
)
|
| 38 |
+
from .vb_layers_pairformer import PairformerModule
|
| 39 |
+
from .vb_layers_transition import Transition as _vb_layers_transition_marker # noqa: F401
|
| 40 |
+
from .vb_layers_triangular_mult import (
|
| 41 |
+
TriangleMultiplicationIncoming as _vb_layers_triangular_mult_marker, # noqa: F401
|
| 42 |
+
)
|
| 43 |
+
from .vb_loss_diffusionv2 import weighted_rigid_align as _vb_loss_diffusionv2_marker # noqa: F401
|
| 44 |
+
from .vb_modules_confidencev2 import ConfidenceModule
|
| 45 |
+
from .vb_modules_diffusion_conditioning import DiffusionConditioning
|
| 46 |
+
from .vb_modules_diffusionv2 import AtomDiffusion, DiffusionModule
|
| 47 |
+
from .vb_modules_encodersv2 import RelativePositionEncoder
|
| 48 |
+
from .vb_modules_transformersv2 import (
|
| 49 |
+
DiffusionTransformer as _vb_modules_transformersv2_marker, # noqa: F401
|
| 50 |
+
)
|
| 51 |
+
from .vb_modules_trunkv2 import (
|
| 52 |
+
ContactConditioning,
|
| 53 |
+
DistogramModule,
|
| 54 |
+
InputEmbedder,
|
| 55 |
+
MSAModule,
|
| 56 |
+
)
|
| 57 |
+
from .vb_modules_utils import LinearNoBias as _vb_modules_utils_marker # noqa: F401
|
| 58 |
+
from .vb_potentials_potentials import (
|
| 59 |
+
get_potentials as _vb_potentials_potentials_marker, # noqa: F401
|
| 60 |
+
)
|
| 61 |
+
from .vb_potentials_schedules import (
|
| 62 |
+
ParameterSchedule as _vb_potentials_schedules_marker, # noqa: F401
|
| 63 |
+
)
|
| 64 |
+
from .vb_tri_attn_attention import (
|
| 65 |
+
TriangleAttentionStartingNode as _vb_tri_attn_attention_marker, # noqa: F401
|
| 66 |
+
)
|
| 67 |
+
from .vb_tri_attn_primitives import Attention as _vb_tri_attn_primitives_marker # noqa: F401
|
| 68 |
+
from .vb_tri_attn_utils import permute_final_dims as _vb_tri_attn_utils_marker # noqa: F401
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _default_steering_args() -> dict[str, Any]:
|
| 72 |
+
return {
|
| 73 |
+
"fk_steering": False,
|
| 74 |
+
"num_particles": 3,
|
| 75 |
+
"fk_lambda": 4.0,
|
| 76 |
+
"fk_resampling_interval": 3,
|
| 77 |
+
"physical_guidance_update": False,
|
| 78 |
+
"contact_guidance_update": False,
|
| 79 |
+
"num_gd_steps": 16,
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _boltz2_reference_diffusion_overrides() -> dict[str, Any]:
|
| 84 |
+
# Match Boltz2 CLI inference defaults from boltz.main/Boltz2DiffusionParams.
|
| 85 |
+
return {
|
| 86 |
+
"gamma_0": 0.8,
|
| 87 |
+
"gamma_min": 1.0,
|
| 88 |
+
"noise_scale": 1.003,
|
| 89 |
+
"rho": 7,
|
| 90 |
+
"step_scale": 1.5,
|
| 91 |
+
"sigma_min": 0.0001,
|
| 92 |
+
"sigma_max": 160.0,
|
| 93 |
+
"sigma_data": 16.0,
|
| 94 |
+
"P_mean": -1.2,
|
| 95 |
+
"P_std": 1.5,
|
| 96 |
+
"coordinate_augmentation": True,
|
| 97 |
+
"alignment_reverse_diff": True,
|
| 98 |
+
"synchronize_sigmas": True,
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _enforce_pairformer_v2(pairformer_args: Mapping[str, Any], context: str) -> dict[str, Any]:
|
| 103 |
+
if not isinstance(pairformer_args, Mapping):
|
| 104 |
+
raise TypeError(f"Expected {context} pairformer_args to be a dictionary.")
|
| 105 |
+
out = _to_plain_python(copy.deepcopy(pairformer_args))
|
| 106 |
+
if "v2" in out and not out["v2"]:
|
| 107 |
+
raise ValueError(f"{context} pairformer_args['v2'] must be True for Boltz2.")
|
| 108 |
+
out["v2"] = True
|
| 109 |
+
return out
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _require_key(mapping: dict[str, Any], key: str) -> Any:
|
| 113 |
+
if key not in mapping:
|
| 114 |
+
raise KeyError(f"Missing required key '{key}' in checkpoint hyperparameters.")
|
| 115 |
+
return mapping[key]
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _state_dict_without_wrappers(state_dict: dict[str, Tensor]) -> dict[str, Tensor]:
|
| 119 |
+
# Every state tensor has its parameter-defined shape (...).
|
| 120 |
+
cleaned: dict[str, Tensor] = {}
|
| 121 |
+
for key, value in state_dict.items():
|
| 122 |
+
if key.startswith("ema."):
|
| 123 |
+
continue
|
| 124 |
+
new_key = key
|
| 125 |
+
if new_key.startswith("model."):
|
| 126 |
+
new_key = new_key[len("model.") :]
|
| 127 |
+
if new_key.startswith("module."):
|
| 128 |
+
new_key = new_key[len("module.") :]
|
| 129 |
+
cleaned[new_key] = value # (...)
|
| 130 |
+
return cleaned
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _to_cpu_detached(value: Any) -> Any:
|
| 134 |
+
if torch.is_tensor(value):
|
| 135 |
+
return value.detach().cpu() # same shape (...)
|
| 136 |
+
if isinstance(value, dict):
|
| 137 |
+
out: dict[Any, Any] = {}
|
| 138 |
+
for key, nested_value in value.items():
|
| 139 |
+
out[key] = _to_cpu_detached(nested_value)
|
| 140 |
+
return out
|
| 141 |
+
if isinstance(value, list):
|
| 142 |
+
return [_to_cpu_detached(item) for item in value]
|
| 143 |
+
if isinstance(value, tuple):
|
| 144 |
+
return tuple(_to_cpu_detached(item) for item in value)
|
| 145 |
+
return value
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@dataclass(frozen=True)
|
| 149 |
+
class _RandomState:
|
| 150 |
+
python: object
|
| 151 |
+
numpy: tuple[Any, ...]
|
| 152 |
+
torch_cpu: Tensor # (n_cpu_rng_state,)
|
| 153 |
+
torch_cuda: list[Tensor] | None # each: (n_cuda_rng_state,)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@contextmanager
|
| 157 |
+
def _seed_context(seed: int | None) -> Iterator[None]:
|
| 158 |
+
"""Temporarily seed every RNG used by Boltz2 and restore caller state."""
|
| 159 |
+
|
| 160 |
+
if seed is None:
|
| 161 |
+
yield
|
| 162 |
+
return
|
| 163 |
+
if isinstance(seed, bool) or not isinstance(seed, int):
|
| 164 |
+
raise TypeError(
|
| 165 |
+
"Boltz2 seed must be an int or None; bool and coerced values are not accepted."
|
| 166 |
+
)
|
| 167 |
+
state = _RandomState(
|
| 168 |
+
python=random.getstate(),
|
| 169 |
+
numpy=np.random.get_state(),
|
| 170 |
+
torch_cpu=torch.random.get_rng_state(),
|
| 171 |
+
torch_cuda=torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None,
|
| 172 |
+
)
|
| 173 |
+
normalized_seed = seed % (2**32)
|
| 174 |
+
random.seed(normalized_seed)
|
| 175 |
+
np.random.seed(normalized_seed)
|
| 176 |
+
torch.manual_seed(normalized_seed)
|
| 177 |
+
if torch.cuda.is_available():
|
| 178 |
+
torch.cuda.manual_seed_all(normalized_seed)
|
| 179 |
+
try:
|
| 180 |
+
yield
|
| 181 |
+
finally:
|
| 182 |
+
random.setstate(state.python)
|
| 183 |
+
np.random.set_state(state.numpy)
|
| 184 |
+
torch.random.set_rng_state(state.torch_cpu)
|
| 185 |
+
if state.torch_cuda is not None:
|
| 186 |
+
torch.cuda.set_rng_state_all(state.torch_cuda)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _to_plain_python(value: Any) -> Any:
|
| 190 |
+
if isinstance(value, Mapping):
|
| 191 |
+
out: dict[Any, Any] = {}
|
| 192 |
+
for key, nested_value in value.items():
|
| 193 |
+
out[key] = _to_plain_python(nested_value)
|
| 194 |
+
return out
|
| 195 |
+
if isinstance(value, list):
|
| 196 |
+
return [_to_plain_python(item) for item in value]
|
| 197 |
+
if isinstance(value, tuple):
|
| 198 |
+
return [_to_plain_python(item) for item in value]
|
| 199 |
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
| 200 |
+
return [_to_plain_python(item) for item in value]
|
| 201 |
+
return value
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def _filtered_kwargs(target: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
|
| 205 |
+
signature = inspect.signature(target.__init__)
|
| 206 |
+
allowed = set(signature.parameters.keys())
|
| 207 |
+
allowed.discard("self")
|
| 208 |
+
filtered: dict[str, Any] = {}
|
| 209 |
+
for key, value in kwargs.items():
|
| 210 |
+
if key in allowed:
|
| 211 |
+
filtered[key] = value
|
| 212 |
+
return filtered
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@dataclass
|
| 216 |
+
class Boltz2ModelOutput(ModelOutput):
|
| 217 |
+
"""Raw Boltz2 inference output with standard AutoModel controls."""
|
| 218 |
+
|
| 219 |
+
last_hidden_state: torch.Tensor | None = None # (b, t, d_s) or None
|
| 220 |
+
hidden_states: tuple[torch.Tensor, ...] | None = None # each shape traced at source
|
| 221 |
+
attentions: tuple[torch.Tensor, ...] | None = None # unsupported; always None
|
| 222 |
+
pdistogram: torch.Tensor | None = None # (b, t, t, n_distogram, n_bin) or None
|
| 223 |
+
s: torch.Tensor | None = None # (b, t, d_s) or None
|
| 224 |
+
z: torch.Tensor | None = None # (b, t, t, d_z) or None
|
| 225 |
+
sample_atom_coords: torch.Tensor | None = None # (b * m, a, 3) or None
|
| 226 |
+
diff_token_repr: torch.Tensor | None = None # (...) or None
|
| 227 |
+
s_conf: torch.Tensor | None = None # (...) or None
|
| 228 |
+
z_conf: torch.Tensor | None = None # (...) or None
|
| 229 |
+
pde_logits: torch.Tensor | None = None # (...) or None
|
| 230 |
+
plddt_logits: torch.Tensor | None = None # (...) or None
|
| 231 |
+
resolved_logits: torch.Tensor | None = None # (...) or None
|
| 232 |
+
pde: torch.Tensor | None = None # (...) or None
|
| 233 |
+
plddt: torch.Tensor | None = None # (...) or None
|
| 234 |
+
complex_plddt: torch.Tensor | None = None # (...) or None
|
| 235 |
+
complex_iplddt: torch.Tensor | None = None # (...) or None
|
| 236 |
+
complex_pde: torch.Tensor | None = None # (...) or None
|
| 237 |
+
complex_ipde: torch.Tensor | None = None # (...) or None
|
| 238 |
+
pae_logits: torch.Tensor | None = None # (...) or None
|
| 239 |
+
pae: torch.Tensor | None = None # (...) or None
|
| 240 |
+
ptm: torch.Tensor | None = None # (...) or None
|
| 241 |
+
iptm: torch.Tensor | None = None # (...) or None
|
| 242 |
+
ligand_iptm: torch.Tensor | None = None # (...) or None
|
| 243 |
+
protein_iptm: torch.Tensor | None = None # (...) or None
|
| 244 |
+
pair_chains_iptm: Any = None
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
@dataclass
|
| 248 |
+
class Boltz2StructureOutput(ModelOutput):
|
| 249 |
+
sample_atom_coords: torch.Tensor | None = None # (m, a_p, 3) or None
|
| 250 |
+
atom_pad_mask: torch.Tensor | None = None # (a_p,) or None
|
| 251 |
+
plddt: torch.Tensor | None = None # (m, t) or (m, a_p), depending on confidence resolution
|
| 252 |
+
confidence_score: torch.Tensor | None = None # (m,) or None
|
| 253 |
+
complex_plddt: torch.Tensor | None = None # (m,) or None
|
| 254 |
+
iptm: torch.Tensor | None = None # (m,) or None
|
| 255 |
+
ptm: torch.Tensor | None = None # (m,) or None
|
| 256 |
+
sequence: str | None = None
|
| 257 |
+
structure_template: ProteinStructureTemplate | None = None
|
| 258 |
+
raw_output: dict[str, torch.Tensor] | None = None # values keep model-output shapes
|
| 259 |
+
seed: int | None = None
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
class Boltz2Config(PretrainedConfig):
|
| 263 |
+
model_type = "boltz2_automodel"
|
| 264 |
+
|
| 265 |
+
def __init__(
|
| 266 |
+
self,
|
| 267 |
+
core_kwargs: dict[str, Any] | None = None,
|
| 268 |
+
num_bins: int = 64,
|
| 269 |
+
default_recycling_steps: int = 3,
|
| 270 |
+
default_sampling_steps: int = 200,
|
| 271 |
+
default_diffusion_samples: int = 1,
|
| 272 |
+
**kwargs,
|
| 273 |
+
) -> None:
|
| 274 |
+
super().__init__(**kwargs)
|
| 275 |
+
if core_kwargs is None:
|
| 276 |
+
core_kwargs = {}
|
| 277 |
+
self.core_kwargs = core_kwargs
|
| 278 |
+
self.num_bins = num_bins
|
| 279 |
+
self.default_recycling_steps = default_recycling_steps
|
| 280 |
+
self.default_sampling_steps = default_sampling_steps
|
| 281 |
+
self.default_diffusion_samples = default_diffusion_samples
|
| 282 |
+
|
| 283 |
+
@classmethod
|
| 284 |
+
def from_hyperparameters(
|
| 285 |
+
cls,
|
| 286 |
+
hparams: dict[str, Any],
|
| 287 |
+
use_kernels: bool = False,
|
| 288 |
+
default_recycling_steps: int | None = None,
|
| 289 |
+
default_sampling_steps: int | None = None,
|
| 290 |
+
default_diffusion_samples: int | None = None,
|
| 291 |
+
) -> "Boltz2Config":
|
| 292 |
+
if not isinstance(hparams, dict):
|
| 293 |
+
raise TypeError("Expected checkpoint hyperparameters as a dictionary.")
|
| 294 |
+
required = [
|
| 295 |
+
"atom_s",
|
| 296 |
+
"atom_z",
|
| 297 |
+
"token_s",
|
| 298 |
+
"token_z",
|
| 299 |
+
"num_bins",
|
| 300 |
+
"embedder_args",
|
| 301 |
+
"msa_args",
|
| 302 |
+
"pairformer_args",
|
| 303 |
+
"score_model_args",
|
| 304 |
+
"diffusion_process_args",
|
| 305 |
+
]
|
| 306 |
+
for key in required:
|
| 307 |
+
_require_key(hparams, key)
|
| 308 |
+
|
| 309 |
+
pairformer_args = _enforce_pairformer_v2(
|
| 310 |
+
hparams["pairformer_args"],
|
| 311 |
+
context="checkpoint",
|
| 312 |
+
)
|
| 313 |
+
diffusion_process_args = _to_plain_python(copy.deepcopy(hparams["diffusion_process_args"]))
|
| 314 |
+
diffusion_overrides = _boltz2_reference_diffusion_overrides()
|
| 315 |
+
for key in diffusion_overrides:
|
| 316 |
+
diffusion_process_args[key] = diffusion_overrides[key]
|
| 317 |
+
|
| 318 |
+
core_kwargs: dict[str, Any] = {
|
| 319 |
+
"atom_s": hparams["atom_s"],
|
| 320 |
+
"atom_z": hparams["atom_z"],
|
| 321 |
+
"token_s": hparams["token_s"],
|
| 322 |
+
"token_z": hparams["token_z"],
|
| 323 |
+
"num_bins": hparams["num_bins"],
|
| 324 |
+
"embedder_args": _to_plain_python(copy.deepcopy(hparams["embedder_args"])),
|
| 325 |
+
"msa_args": _to_plain_python(copy.deepcopy(hparams["msa_args"])),
|
| 326 |
+
"pairformer_args": pairformer_args,
|
| 327 |
+
"score_model_args": _to_plain_python(copy.deepcopy(hparams["score_model_args"])),
|
| 328 |
+
"diffusion_process_args": diffusion_process_args,
|
| 329 |
+
"use_kernels": use_kernels,
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
if "confidence_model_args" in hparams:
|
| 333 |
+
confidence_model_args = _to_plain_python(
|
| 334 |
+
copy.deepcopy(hparams["confidence_model_args"])
|
| 335 |
+
)
|
| 336 |
+
if "pairformer_args" in confidence_model_args:
|
| 337 |
+
confidence_model_args["pairformer_args"] = _enforce_pairformer_v2(
|
| 338 |
+
confidence_model_args["pairformer_args"],
|
| 339 |
+
context="confidence",
|
| 340 |
+
)
|
| 341 |
+
core_kwargs["confidence_model_args"] = confidence_model_args
|
| 342 |
+
else:
|
| 343 |
+
core_kwargs["confidence_model_args"] = None
|
| 344 |
+
|
| 345 |
+
core_kwargs["confidence_prediction"] = hparams.get("confidence_prediction", True)
|
| 346 |
+
core_kwargs["token_level_confidence"] = hparams.get("token_level_confidence", True)
|
| 347 |
+
core_kwargs["alpha_pae"] = hparams.get("alpha_pae", 0.0)
|
| 348 |
+
core_kwargs["atoms_per_window_queries"] = hparams.get("atoms_per_window_queries", 32)
|
| 349 |
+
core_kwargs["atoms_per_window_keys"] = hparams.get("atoms_per_window_keys", 128)
|
| 350 |
+
core_kwargs["atom_feature_dim"] = hparams.get("atom_feature_dim", 128)
|
| 351 |
+
core_kwargs["bond_type_feature"] = hparams.get("bond_type_feature", False)
|
| 352 |
+
core_kwargs["run_trunk_and_structure"] = hparams.get("run_trunk_and_structure", True)
|
| 353 |
+
core_kwargs["skip_run_structure"] = hparams.get("skip_run_structure", False)
|
| 354 |
+
core_kwargs["fix_sym_check"] = hparams.get("fix_sym_check", False)
|
| 355 |
+
core_kwargs["cyclic_pos_enc"] = hparams.get("cyclic_pos_enc", False)
|
| 356 |
+
core_kwargs["use_no_atom_char"] = hparams.get("use_no_atom_char", False)
|
| 357 |
+
core_kwargs["use_atom_backbone_feat"] = hparams.get("use_atom_backbone_feat", False)
|
| 358 |
+
core_kwargs["use_residue_feats_atoms"] = hparams.get("use_residue_feats_atoms", False)
|
| 359 |
+
core_kwargs["conditioning_cutoff_min"] = hparams.get("conditioning_cutoff_min", 4.0)
|
| 360 |
+
core_kwargs["conditioning_cutoff_max"] = hparams.get("conditioning_cutoff_max", 20.0)
|
| 361 |
+
|
| 362 |
+
if "steering_args" in hparams and hparams["steering_args"] is not None:
|
| 363 |
+
core_kwargs["steering_args"] = _to_plain_python(copy.deepcopy(hparams["steering_args"]))
|
| 364 |
+
else:
|
| 365 |
+
core_kwargs["steering_args"] = _default_steering_args()
|
| 366 |
+
|
| 367 |
+
if "validation_args" in hparams:
|
| 368 |
+
validation_args = hparams["validation_args"]
|
| 369 |
+
if not isinstance(validation_args, Mapping):
|
| 370 |
+
raise TypeError(
|
| 371 |
+
"Expected 'validation_args' in checkpoint hyperparameters to be a mapping."
|
| 372 |
+
)
|
| 373 |
+
if default_recycling_steps is None and "recycling_steps" in validation_args:
|
| 374 |
+
default_recycling_steps = validation_args["recycling_steps"]
|
| 375 |
+
if default_sampling_steps is None and "sampling_steps" in validation_args:
|
| 376 |
+
default_sampling_steps = validation_args["sampling_steps"]
|
| 377 |
+
if default_diffusion_samples is None and "diffusion_samples" in validation_args:
|
| 378 |
+
default_diffusion_samples = validation_args["diffusion_samples"]
|
| 379 |
+
|
| 380 |
+
if default_recycling_steps is None:
|
| 381 |
+
default_recycling_steps = 3
|
| 382 |
+
if default_sampling_steps is None:
|
| 383 |
+
default_sampling_steps = 200
|
| 384 |
+
if default_diffusion_samples is None:
|
| 385 |
+
default_diffusion_samples = 1
|
| 386 |
+
|
| 387 |
+
return cls(
|
| 388 |
+
core_kwargs=core_kwargs,
|
| 389 |
+
num_bins=hparams["num_bins"],
|
| 390 |
+
default_recycling_steps=default_recycling_steps,
|
| 391 |
+
default_sampling_steps=default_sampling_steps,
|
| 392 |
+
default_diffusion_samples=default_diffusion_samples,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
class Boltz2InferenceCore(nn.Module):
|
| 397 |
+
def __init__(
|
| 398 |
+
self,
|
| 399 |
+
atom_s: int,
|
| 400 |
+
atom_z: int,
|
| 401 |
+
token_s: int,
|
| 402 |
+
token_z: int,
|
| 403 |
+
num_bins: int,
|
| 404 |
+
embedder_args: dict[str, Any],
|
| 405 |
+
msa_args: dict[str, Any],
|
| 406 |
+
pairformer_args: dict[str, Any],
|
| 407 |
+
score_model_args: dict[str, Any],
|
| 408 |
+
diffusion_process_args: dict[str, Any],
|
| 409 |
+
confidence_model_args: dict[str, Any] | None = None,
|
| 410 |
+
atom_feature_dim: int = 128,
|
| 411 |
+
confidence_prediction: bool = True,
|
| 412 |
+
token_level_confidence: bool = True,
|
| 413 |
+
alpha_pae: float = 0.0,
|
| 414 |
+
atoms_per_window_queries: int = 32,
|
| 415 |
+
atoms_per_window_keys: int = 128,
|
| 416 |
+
run_trunk_and_structure: bool = True,
|
| 417 |
+
skip_run_structure: bool = False,
|
| 418 |
+
bond_type_feature: bool = False,
|
| 419 |
+
fix_sym_check: bool = False,
|
| 420 |
+
cyclic_pos_enc: bool = False,
|
| 421 |
+
use_no_atom_char: bool = False,
|
| 422 |
+
use_atom_backbone_feat: bool = False,
|
| 423 |
+
use_residue_feats_atoms: bool = False,
|
| 424 |
+
conditioning_cutoff_min: float = 4.0,
|
| 425 |
+
conditioning_cutoff_max: float = 20.0,
|
| 426 |
+
use_kernels: bool = False,
|
| 427 |
+
steering_args: dict[str, Any] | None = None,
|
| 428 |
+
) -> None:
|
| 429 |
+
super().__init__()
|
| 430 |
+
self.use_kernels = use_kernels
|
| 431 |
+
self.confidence_prediction = confidence_prediction
|
| 432 |
+
self.token_level_confidence = token_level_confidence
|
| 433 |
+
self.alpha_pae = alpha_pae
|
| 434 |
+
self.run_trunk_and_structure = run_trunk_and_structure
|
| 435 |
+
self.skip_run_structure = skip_run_structure
|
| 436 |
+
self.bond_type_feature = bond_type_feature
|
| 437 |
+
self.steering_args = (
|
| 438 |
+
steering_args if steering_args is not None else _default_steering_args()
|
| 439 |
+
)
|
| 440 |
+
if "v2" not in pairformer_args:
|
| 441 |
+
raise ValueError("Boltz2 requires pairformer_args['v2'].")
|
| 442 |
+
if not pairformer_args["v2"]:
|
| 443 |
+
raise ValueError("Boltz2 requires pairformer_args['v2']=True.")
|
| 444 |
+
|
| 445 |
+
full_embedder_args = {
|
| 446 |
+
"atom_s": atom_s,
|
| 447 |
+
"atom_z": atom_z,
|
| 448 |
+
"token_s": token_s,
|
| 449 |
+
"token_z": token_z,
|
| 450 |
+
"atoms_per_window_queries": atoms_per_window_queries,
|
| 451 |
+
"atoms_per_window_keys": atoms_per_window_keys,
|
| 452 |
+
"atom_feature_dim": atom_feature_dim,
|
| 453 |
+
"use_no_atom_char": use_no_atom_char,
|
| 454 |
+
"use_atom_backbone_feat": use_atom_backbone_feat,
|
| 455 |
+
"use_residue_feats_atoms": use_residue_feats_atoms,
|
| 456 |
+
**embedder_args,
|
| 457 |
+
}
|
| 458 |
+
full_embedder_args = _filtered_kwargs(InputEmbedder, full_embedder_args)
|
| 459 |
+
self.input_embedder = InputEmbedder(**full_embedder_args)
|
| 460 |
+
|
| 461 |
+
self.s_init = nn.Linear(token_s, token_s, bias=False)
|
| 462 |
+
self.z_init_1 = nn.Linear(token_s, token_z, bias=False)
|
| 463 |
+
self.z_init_2 = nn.Linear(token_s, token_z, bias=False)
|
| 464 |
+
self.rel_pos = RelativePositionEncoder(
|
| 465 |
+
token_z,
|
| 466 |
+
fix_sym_check=fix_sym_check,
|
| 467 |
+
cyclic_pos_enc=cyclic_pos_enc,
|
| 468 |
+
)
|
| 469 |
+
self.token_bonds = nn.Linear(1, token_z, bias=False)
|
| 470 |
+
if self.bond_type_feature:
|
| 471 |
+
self.token_bonds_type = nn.Embedding(len(const.bond_types) + 1, token_z)
|
| 472 |
+
|
| 473 |
+
self.contact_conditioning = ContactConditioning(
|
| 474 |
+
token_z=token_z,
|
| 475 |
+
cutoff_min=conditioning_cutoff_min,
|
| 476 |
+
cutoff_max=conditioning_cutoff_max,
|
| 477 |
+
)
|
| 478 |
+
self.s_norm = nn.LayerNorm(token_s)
|
| 479 |
+
self.z_norm = nn.LayerNorm(token_z)
|
| 480 |
+
|
| 481 |
+
self.s_recycle = nn.Linear(token_s, token_s, bias=False)
|
| 482 |
+
self.z_recycle = nn.Linear(token_z, token_z, bias=False)
|
| 483 |
+
init.gating_init_(self.s_recycle.weight)
|
| 484 |
+
init.gating_init_(self.z_recycle.weight)
|
| 485 |
+
|
| 486 |
+
msa_kwargs = _filtered_kwargs(
|
| 487 |
+
MSAModule,
|
| 488 |
+
{"token_z": token_z, "token_s": token_s, **msa_args},
|
| 489 |
+
)
|
| 490 |
+
self.msa_module = MSAModule(**msa_kwargs)
|
| 491 |
+
|
| 492 |
+
pairformer_kwargs = _filtered_kwargs(
|
| 493 |
+
PairformerModule,
|
| 494 |
+
{"token_s": token_s, "token_z": token_z, **pairformer_args},
|
| 495 |
+
)
|
| 496 |
+
if "token_s" not in pairformer_kwargs or "token_z" not in pairformer_kwargs:
|
| 497 |
+
raise RuntimeError("Boltz2 pairformer construction lost its token dimensions.")
|
| 498 |
+
pairformer_token_s = pairformer_kwargs.pop("token_s")
|
| 499 |
+
pairformer_token_z = pairformer_kwargs.pop("token_z")
|
| 500 |
+
self.pairformer_module = PairformerModule(
|
| 501 |
+
pairformer_token_s,
|
| 502 |
+
pairformer_token_z,
|
| 503 |
+
**pairformer_kwargs,
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
diffusion_conditioning_kwargs = {
|
| 507 |
+
"token_s": token_s,
|
| 508 |
+
"token_z": token_z,
|
| 509 |
+
"atom_s": atom_s,
|
| 510 |
+
"atom_z": atom_z,
|
| 511 |
+
"atoms_per_window_queries": atoms_per_window_queries,
|
| 512 |
+
"atoms_per_window_keys": atoms_per_window_keys,
|
| 513 |
+
"atom_encoder_depth": score_model_args["atom_encoder_depth"],
|
| 514 |
+
"atom_encoder_heads": score_model_args["atom_encoder_heads"],
|
| 515 |
+
"token_transformer_depth": score_model_args["token_transformer_depth"],
|
| 516 |
+
"token_transformer_heads": score_model_args["token_transformer_heads"],
|
| 517 |
+
"atom_decoder_depth": score_model_args["atom_decoder_depth"],
|
| 518 |
+
"atom_decoder_heads": score_model_args["atom_decoder_heads"],
|
| 519 |
+
"atom_feature_dim": atom_feature_dim,
|
| 520 |
+
"conditioning_transition_layers": score_model_args["conditioning_transition_layers"],
|
| 521 |
+
"use_no_atom_char": use_no_atom_char,
|
| 522 |
+
"use_atom_backbone_feat": use_atom_backbone_feat,
|
| 523 |
+
"use_residue_feats_atoms": use_residue_feats_atoms,
|
| 524 |
+
}
|
| 525 |
+
diffusion_conditioning_kwargs = _filtered_kwargs(
|
| 526 |
+
DiffusionConditioning,
|
| 527 |
+
diffusion_conditioning_kwargs,
|
| 528 |
+
)
|
| 529 |
+
self.diffusion_conditioning = DiffusionConditioning(**diffusion_conditioning_kwargs)
|
| 530 |
+
|
| 531 |
+
structure_score_model_args = {
|
| 532 |
+
"token_s": token_s,
|
| 533 |
+
"atom_s": atom_s,
|
| 534 |
+
"atoms_per_window_queries": atoms_per_window_queries,
|
| 535 |
+
"atoms_per_window_keys": atoms_per_window_keys,
|
| 536 |
+
**score_model_args,
|
| 537 |
+
}
|
| 538 |
+
structure_score_model_args = _filtered_kwargs(
|
| 539 |
+
DiffusionModule,
|
| 540 |
+
structure_score_model_args,
|
| 541 |
+
)
|
| 542 |
+
structure_module_kwargs = {
|
| 543 |
+
"score_model_args": structure_score_model_args,
|
| 544 |
+
"compile_score": False,
|
| 545 |
+
**diffusion_process_args,
|
| 546 |
+
}
|
| 547 |
+
structure_module_kwargs = _filtered_kwargs(AtomDiffusion, structure_module_kwargs)
|
| 548 |
+
self.structure_module = AtomDiffusion(**structure_module_kwargs)
|
| 549 |
+
self.distogram_module = DistogramModule(token_z, num_bins)
|
| 550 |
+
|
| 551 |
+
if self.confidence_prediction:
|
| 552 |
+
if confidence_model_args is None:
|
| 553 |
+
raise ValueError(
|
| 554 |
+
"confidence_prediction=True requires confidence_model_args in config."
|
| 555 |
+
)
|
| 556 |
+
confidence_kwargs = {
|
| 557 |
+
"token_s": token_s,
|
| 558 |
+
"token_z": token_z,
|
| 559 |
+
"token_level_confidence": token_level_confidence,
|
| 560 |
+
"bond_type_feature": bond_type_feature,
|
| 561 |
+
"fix_sym_check": fix_sym_check,
|
| 562 |
+
"cyclic_pos_enc": cyclic_pos_enc,
|
| 563 |
+
"conditioning_cutoff_min": conditioning_cutoff_min,
|
| 564 |
+
"conditioning_cutoff_max": conditioning_cutoff_max,
|
| 565 |
+
**confidence_model_args,
|
| 566 |
+
}
|
| 567 |
+
confidence_kwargs = _filtered_kwargs(ConfidenceModule, confidence_kwargs)
|
| 568 |
+
self.confidence_module = ConfidenceModule(**confidence_kwargs)
|
| 569 |
+
|
| 570 |
+
def forward(
|
| 571 |
+
self,
|
| 572 |
+
feats: dict[str, Tensor],
|
| 573 |
+
recycling_steps: int = 3,
|
| 574 |
+
num_sampling_steps: int | None = None,
|
| 575 |
+
diffusion_samples: int = 1,
|
| 576 |
+
max_parallel_samples: int | None = None,
|
| 577 |
+
run_confidence_sequentially: bool = True,
|
| 578 |
+
detach_confidence: bool = True,
|
| 579 |
+
verbose: bool = False,
|
| 580 |
+
) -> dict[str, Tensor]:
|
| 581 |
+
# b is the batch size, t the token count, a the padded atom count,
|
| 582 |
+
# d_s the token width, d_z the pair width, and m the diffusion multiplicity.
|
| 583 |
+
with tqdm(
|
| 584 |
+
total=1,
|
| 585 |
+
desc="Boltz2: Input embeddings",
|
| 586 |
+
disable=not verbose,
|
| 587 |
+
dynamic_ncols=True,
|
| 588 |
+
leave=False,
|
| 589 |
+
) as progress:
|
| 590 |
+
s_inputs = self.input_embedder(feats) # (b, t, d_s)
|
| 591 |
+
progress.update()
|
| 592 |
+
s_init = self.s_init(s_inputs) # (b, t, d_s)
|
| 593 |
+
|
| 594 |
+
z_init = (
|
| 595 |
+
self.z_init_1(s_inputs)[:, :, None] + self.z_init_2(s_inputs)[:, None, :]
|
| 596 |
+
) # (b, t, t, d_z)
|
| 597 |
+
relative_position_encoding = self.rel_pos(feats) # (b, t, t, d_z)
|
| 598 |
+
z_init = z_init + relative_position_encoding # (b, t, t, d_z)
|
| 599 |
+
z_init = z_init + self.token_bonds(
|
| 600 |
+
feats["token_bonds"].float()
|
| 601 |
+
) # (b, t, t, d_z)
|
| 602 |
+
if self.bond_type_feature:
|
| 603 |
+
z_init = z_init + self.token_bonds_type(
|
| 604 |
+
feats["type_bonds"].long()
|
| 605 |
+
) # (b, t, t, d_z)
|
| 606 |
+
z_init = z_init + self.contact_conditioning(feats) # (b, t, t, d_z)
|
| 607 |
+
|
| 608 |
+
s = torch.zeros_like(s_init) # (b, t, d_s)
|
| 609 |
+
z = torch.zeros_like(z_init) # (b, t, t, d_z)
|
| 610 |
+
mask = feats["token_pad_mask"].float() # (b, t)
|
| 611 |
+
pair_mask = mask[:, :, None] * mask[:, None, :] # (b, t, t)
|
| 612 |
+
|
| 613 |
+
if self.run_trunk_and_structure:
|
| 614 |
+
for _ in tqdm(
|
| 615 |
+
range(recycling_steps + 1),
|
| 616 |
+
desc="Boltz2: Recycling",
|
| 617 |
+
unit="cycle",
|
| 618 |
+
disable=not verbose,
|
| 619 |
+
dynamic_ncols=True,
|
| 620 |
+
leave=False,
|
| 621 |
+
):
|
| 622 |
+
s = s_init + self.s_recycle(self.s_norm(s)) # (b, t, d_s)
|
| 623 |
+
z = z_init + self.z_recycle(self.z_norm(z)) # (b, t, t, d_z)
|
| 624 |
+
z = z + self.msa_module(
|
| 625 |
+
z,
|
| 626 |
+
s_inputs,
|
| 627 |
+
feats,
|
| 628 |
+
use_kernels=self.use_kernels,
|
| 629 |
+
) # (b, t, t, d_z)
|
| 630 |
+
s, z = self.pairformer_module(
|
| 631 |
+
s,
|
| 632 |
+
z,
|
| 633 |
+
mask=mask,
|
| 634 |
+
pair_mask=pair_mask,
|
| 635 |
+
use_kernels=self.use_kernels,
|
| 636 |
+
) # (b, t, d_s), (b, t, t, d_z)
|
| 637 |
+
|
| 638 |
+
pdistogram = self.distogram_module(z) # (b, t, t, n_distogram, n_bin)
|
| 639 |
+
output: dict[str, Tensor] = {
|
| 640 |
+
"pdistogram": pdistogram,
|
| 641 |
+
"s": s,
|
| 642 |
+
"z": z,
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
if self.run_trunk_and_structure and (not self.skip_run_structure):
|
| 646 |
+
q, c, to_keys, atom_enc_bias, atom_dec_bias, token_trans_bias = (
|
| 647 |
+
self.diffusion_conditioning(
|
| 648 |
+
s_trunk=s,
|
| 649 |
+
z_trunk=z,
|
| 650 |
+
relative_position_encoding=relative_position_encoding,
|
| 651 |
+
feats=feats,
|
| 652 |
+
)
|
| 653 |
+
) # q/c: (b, a, d_a); biases retain their attention-specific axes
|
| 654 |
+
diffusion_conditioning = {
|
| 655 |
+
"q": q,
|
| 656 |
+
"c": c,
|
| 657 |
+
"to_keys": to_keys,
|
| 658 |
+
"atom_enc_bias": atom_enc_bias,
|
| 659 |
+
"atom_dec_bias": atom_dec_bias,
|
| 660 |
+
"token_trans_bias": token_trans_bias,
|
| 661 |
+
}
|
| 662 |
+
with torch.autocast("cuda", enabled=False):
|
| 663 |
+
struct_out = self.structure_module.sample(
|
| 664 |
+
s_trunk=s.float(),
|
| 665 |
+
s_inputs=s_inputs.float(),
|
| 666 |
+
feats=feats,
|
| 667 |
+
num_sampling_steps=num_sampling_steps,
|
| 668 |
+
atom_mask=feats["atom_pad_mask"].float(),
|
| 669 |
+
multiplicity=diffusion_samples,
|
| 670 |
+
max_parallel_samples=max_parallel_samples,
|
| 671 |
+
steering_args=self.steering_args,
|
| 672 |
+
diffusion_conditioning=diffusion_conditioning,
|
| 673 |
+
verbose=verbose,
|
| 674 |
+
) # tensor values include sample_atom_coords: (b * m, a, 3)
|
| 675 |
+
output.update(struct_out)
|
| 676 |
+
|
| 677 |
+
if self.confidence_prediction:
|
| 678 |
+
if self.skip_run_structure:
|
| 679 |
+
x_pred = feats["coords"].repeat_interleave(
|
| 680 |
+
diffusion_samples, 0
|
| 681 |
+
) # (b * m, ..., a, 3)
|
| 682 |
+
else:
|
| 683 |
+
if "sample_atom_coords" not in output:
|
| 684 |
+
raise RuntimeError("Structure sampling did not produce sample_atom_coords.")
|
| 685 |
+
x_pred = output["sample_atom_coords"] # (b * m, a, 3)
|
| 686 |
+
|
| 687 |
+
if detach_confidence:
|
| 688 |
+
s_inputs_c = s_inputs.detach() # (b, t, d_s)
|
| 689 |
+
s_c = s.detach() # (b, t, d_s)
|
| 690 |
+
z_c = z.detach() # (b, t, t, d_z)
|
| 691 |
+
x_pred_c = x_pred.detach() # (b * m, ..., a, 3)
|
| 692 |
+
pdist_c = output["pdistogram"][:, :, :, 0].detach() # (b, t, t, n_bin)
|
| 693 |
+
else:
|
| 694 |
+
s_inputs_c = s_inputs # (b, t, d_s)
|
| 695 |
+
s_c = s # (b, t, d_s)
|
| 696 |
+
z_c = z # (b, t, t, d_z)
|
| 697 |
+
x_pred_c = x_pred # (b * m, ..., a, 3)
|
| 698 |
+
pdist_c = output["pdistogram"][:, :, :, 0] # (b, t, t, n_bin)
|
| 699 |
+
|
| 700 |
+
with tqdm(
|
| 701 |
+
total=1,
|
| 702 |
+
desc="Boltz2: Confidence",
|
| 703 |
+
disable=not verbose,
|
| 704 |
+
dynamic_ncols=True,
|
| 705 |
+
leave=False,
|
| 706 |
+
) as progress:
|
| 707 |
+
output.update(
|
| 708 |
+
self.confidence_module(
|
| 709 |
+
s_inputs=s_inputs_c,
|
| 710 |
+
s=s_c,
|
| 711 |
+
z=z_c,
|
| 712 |
+
x_pred=x_pred_c,
|
| 713 |
+
feats=feats,
|
| 714 |
+
pred_distogram_logits=pdist_c,
|
| 715 |
+
multiplicity=diffusion_samples,
|
| 716 |
+
run_sequentially=run_confidence_sequentially,
|
| 717 |
+
use_kernels=self.use_kernels,
|
| 718 |
+
)
|
| 719 |
+
)
|
| 720 |
+
progress.update()
|
| 721 |
+
|
| 722 |
+
return output # named tensors retain the shapes traced above
|
| 723 |
+
|
| 724 |
+
|
| 725 |
+
class Boltz2Model(PreTrainedModel):
|
| 726 |
+
config_class = Boltz2Config
|
| 727 |
+
base_model_prefix = "core"
|
| 728 |
+
all_tied_weights_keys: ClassVar[dict[str, str]] = {}
|
| 729 |
+
|
| 730 |
+
def __init__(self, config: Boltz2Config) -> None:
|
| 731 |
+
super().__init__(config)
|
| 732 |
+
if not isinstance(config.core_kwargs, dict):
|
| 733 |
+
raise TypeError("config.core_kwargs must be a dictionary.")
|
| 734 |
+
self.core = Boltz2InferenceCore(**config.core_kwargs)
|
| 735 |
+
|
| 736 |
+
def _init_weights(self, module: nn.Module) -> None:
|
| 737 |
+
# Parameters keep their constructed or loaded values. Transformers 5 builds the
|
| 738 |
+
# model on the meta device and leaves a buffer no checkpoint holds uninitialized,
|
| 739 |
+
# so the diffusion module's non-persistent zero, which the loss breakdown
|
| 740 |
+
# reports when the smooth lDDT loss is off, is refilled here.
|
| 741 |
+
if isinstance(module, AtomDiffusion):
|
| 742 |
+
module.zero.zero_()
|
| 743 |
+
|
| 744 |
+
def _detied_state_dict(self) -> dict[str, Tensor]:
|
| 745 |
+
# Every state tensor has its parameter-defined shape (...).
|
| 746 |
+
raw_state = self.state_dict()
|
| 747 |
+
seen_ptrs: dict[int, str] = {}
|
| 748 |
+
out: dict[str, Tensor] = {}
|
| 749 |
+
for key, tensor in raw_state.items():
|
| 750 |
+
if torch.is_tensor(tensor):
|
| 751 |
+
ptr = tensor.untyped_storage().data_ptr()
|
| 752 |
+
if ptr in seen_ptrs:
|
| 753 |
+
out[key] = tensor.clone() # (...)
|
| 754 |
+
else:
|
| 755 |
+
seen_ptrs[ptr] = key
|
| 756 |
+
out[key] = tensor # (...)
|
| 757 |
+
else:
|
| 758 |
+
out[key] = tensor # (...)
|
| 759 |
+
return out # each tensor: (...)
|
| 760 |
+
|
| 761 |
+
def save_pretrained(self, save_directory: str, **kwargs: Any) -> None:
|
| 762 |
+
if "safe_serialization" not in kwargs:
|
| 763 |
+
kwargs["safe_serialization"] = True
|
| 764 |
+
if "state_dict" not in kwargs:
|
| 765 |
+
kwargs["state_dict"] = self._detied_state_dict()
|
| 766 |
+
super().save_pretrained(save_directory, **kwargs)
|
| 767 |
+
|
| 768 |
+
@property
|
| 769 |
+
def device(self) -> torch.device:
|
| 770 |
+
return next(self.parameters()).device
|
| 771 |
+
|
| 772 |
+
@classmethod
|
| 773 |
+
def from_boltz_checkpoint(
|
| 774 |
+
cls,
|
| 775 |
+
checkpoint_path: str,
|
| 776 |
+
map_location: str | torch.device = "cpu",
|
| 777 |
+
use_kernels: bool = False,
|
| 778 |
+
default_recycling_steps: int | None = None,
|
| 779 |
+
default_sampling_steps: int | None = None,
|
| 780 |
+
default_diffusion_samples: int | None = None,
|
| 781 |
+
*,
|
| 782 |
+
allow_unsafe_pickle: bool = False,
|
| 783 |
+
) -> "Boltz2Model":
|
| 784 |
+
"""Import an official Boltz Lightning checkpoint into the inference model.
|
| 785 |
+
|
| 786 |
+
Lightning checkpoints require Python pickle deserialization. The caller
|
| 787 |
+
must opt in with the literal value ``True`` after independently verifying
|
| 788 |
+
that the checkpoint comes from a trusted source and matches its expected
|
| 789 |
+
cryptographic hash.
|
| 790 |
+
"""
|
| 791 |
+
|
| 792 |
+
if allow_unsafe_pickle is not True:
|
| 793 |
+
raise ValueError(
|
| 794 |
+
"Boltz Lightning checkpoints contain Python pickle and cannot be loaded "
|
| 795 |
+
"safely. Pass allow_unsafe_pickle=True only for a trusted, hash-verified "
|
| 796 |
+
"checkpoint."
|
| 797 |
+
)
|
| 798 |
+
|
| 799 |
+
# The official Lightning checkpoints contain OmegaConf objects, so this explicit
|
| 800 |
+
# trusted-checkpoint boundary requires full Python unpickling.
|
| 801 |
+
checkpoint = torch.load(
|
| 802 |
+
checkpoint_path,
|
| 803 |
+
map_location=map_location,
|
| 804 |
+
weights_only=False,
|
| 805 |
+
) # mapping values include state tensors with parameter-defined shapes (...)
|
| 806 |
+
if not isinstance(checkpoint, dict):
|
| 807 |
+
raise TypeError("Checkpoint must deserialize to a dictionary.")
|
| 808 |
+
_require_key(checkpoint, "hyper_parameters")
|
| 809 |
+
_require_key(checkpoint, "state_dict")
|
| 810 |
+
|
| 811 |
+
hparams = checkpoint["hyper_parameters"]
|
| 812 |
+
if not isinstance(hparams, dict):
|
| 813 |
+
raise TypeError("Checkpoint hyper_parameters must be a dictionary.")
|
| 814 |
+
state_dict = checkpoint["state_dict"] # each tensor: (...)
|
| 815 |
+
if not isinstance(state_dict, dict):
|
| 816 |
+
raise TypeError("Checkpoint state_dict must be a dictionary.")
|
| 817 |
+
|
| 818 |
+
config = Boltz2Config.from_hyperparameters(
|
| 819 |
+
hparams,
|
| 820 |
+
use_kernels=use_kernels,
|
| 821 |
+
default_recycling_steps=default_recycling_steps,
|
| 822 |
+
default_sampling_steps=default_sampling_steps,
|
| 823 |
+
default_diffusion_samples=default_diffusion_samples,
|
| 824 |
+
)
|
| 825 |
+
model = cls(config)
|
| 826 |
+
cleaned = _state_dict_without_wrappers(state_dict) # each tensor: (...)
|
| 827 |
+
target_keys = set(model.core.state_dict().keys())
|
| 828 |
+
for key in target_keys:
|
| 829 |
+
if ".attention.norm_s." in key:
|
| 830 |
+
raise RuntimeError(
|
| 831 |
+
"Boltz2 inference core unexpectedly uses v1 attention parameters. "
|
| 832 |
+
"Expected pairformer v2 architecture."
|
| 833 |
+
)
|
| 834 |
+
filtered: dict[str, Tensor] = {}
|
| 835 |
+
for key, value in cleaned.items():
|
| 836 |
+
if key in target_keys:
|
| 837 |
+
filtered[key] = value # (...)
|
| 838 |
+
|
| 839 |
+
missing = sorted(target_keys.difference(filtered.keys()))
|
| 840 |
+
if missing:
|
| 841 |
+
raise RuntimeError(
|
| 842 |
+
"Checkpoint is missing required parameters for Boltz2 inference core. "
|
| 843 |
+
f"Missing keys (first 20): {missing[:20]}"
|
| 844 |
+
)
|
| 845 |
+
|
| 846 |
+
load_result = model.core.load_state_dict(filtered, strict=False)
|
| 847 |
+
loaded_missing = sorted(load_result.missing_keys)
|
| 848 |
+
if loaded_missing:
|
| 849 |
+
raise RuntimeError(
|
| 850 |
+
"Model has unexpected missing keys after load_state_dict. "
|
| 851 |
+
f"Missing keys (first 20): {loaded_missing[:20]}"
|
| 852 |
+
)
|
| 853 |
+
if load_result.unexpected_keys:
|
| 854 |
+
raise RuntimeError(
|
| 855 |
+
"Model has unexpected checkpoint keys after load_state_dict: "
|
| 856 |
+
f"{sorted(load_result.unexpected_keys)[:20]}"
|
| 857 |
+
)
|
| 858 |
+
model.eval()
|
| 859 |
+
return model
|
| 860 |
+
|
| 861 |
+
def forward(
|
| 862 |
+
self,
|
| 863 |
+
feats: dict[str, Tensor],
|
| 864 |
+
recycling_steps: int | None = None,
|
| 865 |
+
num_sampling_steps: int | None = None,
|
| 866 |
+
diffusion_samples: int | None = None,
|
| 867 |
+
max_parallel_samples: int | None = None,
|
| 868 |
+
run_confidence_sequentially: bool = True,
|
| 869 |
+
detach_confidence: bool = True,
|
| 870 |
+
output_attentions: bool | None = None,
|
| 871 |
+
output_hidden_states: bool | None = None,
|
| 872 |
+
return_dict: bool | None = None,
|
| 873 |
+
verbose: bool = False,
|
| 874 |
+
) -> Boltz2ModelOutput | tuple[Any, ...]:
|
| 875 |
+
# Feature shapes follow build_boltz2_features; b and t are batch and token counts.
|
| 876 |
+
output_attentions = (
|
| 877 |
+
self.config.output_attentions if output_attentions is None else output_attentions
|
| 878 |
+
)
|
| 879 |
+
if output_attentions:
|
| 880 |
+
raise NotImplementedError(
|
| 881 |
+
"Boltz2 does not expose normalized attention tensors from its structure "
|
| 882 |
+
"modules. output_attentions=True is unsupported."
|
| 883 |
+
)
|
| 884 |
+
output_hidden_states = (
|
| 885 |
+
self.config.output_hidden_states
|
| 886 |
+
if output_hidden_states is None
|
| 887 |
+
else output_hidden_states
|
| 888 |
+
)
|
| 889 |
+
return_dict = self.config.use_return_dict if return_dict is None else return_dict
|
| 890 |
+
if recycling_steps is None:
|
| 891 |
+
recycling_steps = self.config.default_recycling_steps
|
| 892 |
+
if num_sampling_steps is None:
|
| 893 |
+
num_sampling_steps = self.config.default_sampling_steps
|
| 894 |
+
if diffusion_samples is None:
|
| 895 |
+
diffusion_samples = self.config.default_diffusion_samples
|
| 896 |
+
raw_output = self.core(
|
| 897 |
+
feats=feats,
|
| 898 |
+
recycling_steps=recycling_steps,
|
| 899 |
+
num_sampling_steps=num_sampling_steps,
|
| 900 |
+
diffusion_samples=diffusion_samples,
|
| 901 |
+
max_parallel_samples=max_parallel_samples,
|
| 902 |
+
run_confidence_sequentially=run_confidence_sequentially,
|
| 903 |
+
detach_confidence=detach_confidence,
|
| 904 |
+
verbose=verbose,
|
| 905 |
+
) # named tensors use the Boltz2InferenceCore.forward shapes
|
| 906 |
+
token_state = raw_output.get("s") # (b, t, d_s) or None
|
| 907 |
+
pair_state = raw_output.get("z") # (b, t, t, d_z) or None
|
| 908 |
+
model_output = Boltz2ModelOutput(
|
| 909 |
+
last_hidden_state=token_state,
|
| 910 |
+
hidden_states=(token_state, pair_state)
|
| 911 |
+
if output_hidden_states and token_state is not None and pair_state is not None
|
| 912 |
+
else None,
|
| 913 |
+
**raw_output,
|
| 914 |
+
)
|
| 915 |
+
return model_output if return_dict else model_output.to_tuple()
|
| 916 |
+
|
| 917 |
+
def _to_model_device(
|
| 918 |
+
self,
|
| 919 |
+
feats: dict[str, Tensor],
|
| 920 |
+
float_dtype: torch.dtype = torch.float32,
|
| 921 |
+
) -> dict[str, Tensor]:
|
| 922 |
+
# Each feature tensor keeps its existing shape (...).
|
| 923 |
+
moved: dict[str, Tensor] = {}
|
| 924 |
+
for key, value in feats.items():
|
| 925 |
+
if torch.is_tensor(value):
|
| 926 |
+
if value.is_floating_point():
|
| 927 |
+
moved[key] = value.to(
|
| 928 |
+
device=self.device, dtype=float_dtype
|
| 929 |
+
) # same shape (...)
|
| 930 |
+
else:
|
| 931 |
+
moved[key] = value.to(device=self.device) # same shape (...)
|
| 932 |
+
else:
|
| 933 |
+
moved[key] = value # same shape (...)
|
| 934 |
+
return moved # each tensor: same shape (...)
|
| 935 |
+
|
| 936 |
+
def predict_structure(
|
| 937 |
+
self,
|
| 938 |
+
amino_acid_sequence: str,
|
| 939 |
+
recycling_steps: int | None = None,
|
| 940 |
+
num_sampling_steps: int | None = None,
|
| 941 |
+
diffusion_samples: int | None = None,
|
| 942 |
+
max_parallel_samples: int | None = None,
|
| 943 |
+
run_confidence_sequentially: bool = True,
|
| 944 |
+
float_dtype: torch.dtype = torch.float32,
|
| 945 |
+
seed: int | None = None,
|
| 946 |
+
verbose: bool = False,
|
| 947 |
+
) -> Boltz2StructureOutput:
|
| 948 |
+
if float_dtype != torch.float32:
|
| 949 |
+
raise ValueError(
|
| 950 |
+
"Boltz2 predict_structure requires FP32 features and parameter storage; "
|
| 951 |
+
"CUDA compute runs under the declared BF16 autocast policy."
|
| 952 |
+
)
|
| 953 |
+
parameter_dtypes = {
|
| 954 |
+
parameter.dtype for parameter in self.parameters() if parameter.is_floating_point()
|
| 955 |
+
}
|
| 956 |
+
if parameter_dtypes and parameter_dtypes != {torch.float32}:
|
| 957 |
+
raise ValueError(
|
| 958 |
+
"Boltz2 predict_structure requires FP32 parameter storage before CUDA "
|
| 959 |
+
f"BF16 autocast; found {sorted(map(str, parameter_dtypes))}."
|
| 960 |
+
)
|
| 961 |
+
|
| 962 |
+
with _seed_context(seed):
|
| 963 |
+
with tqdm(
|
| 964 |
+
total=1,
|
| 965 |
+
desc="Boltz2: Prepare features",
|
| 966 |
+
disable=not verbose,
|
| 967 |
+
dynamic_ncols=True,
|
| 968 |
+
leave=False,
|
| 969 |
+
) as progress:
|
| 970 |
+
feats, template = build_boltz2_features(
|
| 971 |
+
amino_acid_sequence=amino_acid_sequence,
|
| 972 |
+
num_bins=self.config.num_bins,
|
| 973 |
+
atoms_per_window_queries=(
|
| 974 |
+
self.core.input_embedder.atom_encoder.atoms_per_window_queries
|
| 975 |
+
),
|
| 976 |
+
) # feature shapes are traced in build_boltz2_features
|
| 977 |
+
feats = self._to_model_device(
|
| 978 |
+
feats, float_dtype=torch.float32
|
| 979 |
+
) # unchanged feature shapes
|
| 980 |
+
progress.update()
|
| 981 |
+
autocast_context = (
|
| 982 |
+
torch.autocast(device_type="cuda", dtype=torch.bfloat16)
|
| 983 |
+
if self.device.type == "cuda"
|
| 984 |
+
else nullcontext()
|
| 985 |
+
)
|
| 986 |
+
with torch.no_grad(), autocast_context:
|
| 987 |
+
output = self.forward(
|
| 988 |
+
feats=feats,
|
| 989 |
+
recycling_steps=recycling_steps,
|
| 990 |
+
num_sampling_steps=num_sampling_steps,
|
| 991 |
+
diffusion_samples=diffusion_samples,
|
| 992 |
+
max_parallel_samples=max_parallel_samples,
|
| 993 |
+
run_confidence_sequentially=run_confidence_sequentially,
|
| 994 |
+
return_dict=True,
|
| 995 |
+
verbose=verbose,
|
| 996 |
+
) # named tensors use Boltz2InferenceCore.forward shapes
|
| 997 |
+
|
| 998 |
+
sample_atom_coords_value = output.get(
|
| 999 |
+
"sample_atom_coords"
|
| 1000 |
+
) # (m, a_p, 3) or absent
|
| 1001 |
+
if not isinstance(sample_atom_coords_value, torch.Tensor):
|
| 1002 |
+
raise RuntimeError("Boltz2 structure sampling did not return coordinate tensors.")
|
| 1003 |
+
sample_atom_coords = sample_atom_coords_value.detach().cpu() # (m, a_p, 3)
|
| 1004 |
+
non_finite_mask = torch.logical_not(
|
| 1005 |
+
torch.isfinite(sample_atom_coords)
|
| 1006 |
+
) # (m, a_p, 3)
|
| 1007 |
+
if torch.any(non_finite_mask):
|
| 1008 |
+
raise RuntimeError(
|
| 1009 |
+
"sample_atom_coords contains non-finite values. "
|
| 1010 |
+
f"Non-finite count: {int(non_finite_mask.sum().item())}"
|
| 1011 |
+
)
|
| 1012 |
+
atom_pad_mask = feats["atom_pad_mask"][0].detach().cpu() # (a_p,)
|
| 1013 |
+
plddt = (
|
| 1014 |
+
output["plddt"].detach().cpu() if "plddt" in output else None
|
| 1015 |
+
) # (m, t) or (m, a_p), depending on confidence resolution
|
| 1016 |
+
complex_plddt = (
|
| 1017 |
+
output["complex_plddt"].detach().cpu() if "complex_plddt" in output else None
|
| 1018 |
+
) # (m,) or None
|
| 1019 |
+
iptm = (
|
| 1020 |
+
output["iptm"].detach().cpu() if "iptm" in output else None
|
| 1021 |
+
) # (m,) or None
|
| 1022 |
+
ptm = (
|
| 1023 |
+
output["ptm"].detach().cpu() if "ptm" in output else None
|
| 1024 |
+
) # (m,) or None
|
| 1025 |
+
|
| 1026 |
+
confidence_score = None # (m,) or None
|
| 1027 |
+
if (complex_plddt is not None) and (iptm is not None) and (ptm is not None):
|
| 1028 |
+
if torch.allclose(iptm, torch.zeros_like(iptm)):
|
| 1029 |
+
confidence_score = (4 * complex_plddt + ptm) / 5 # (m,)
|
| 1030 |
+
else:
|
| 1031 |
+
confidence_score = (4 * complex_plddt + iptm) / 5 # (m,)
|
| 1032 |
+
|
| 1033 |
+
return Boltz2StructureOutput(
|
| 1034 |
+
sample_atom_coords=sample_atom_coords,
|
| 1035 |
+
atom_pad_mask=atom_pad_mask,
|
| 1036 |
+
plddt=plddt,
|
| 1037 |
+
confidence_score=confidence_score,
|
| 1038 |
+
complex_plddt=complex_plddt,
|
| 1039 |
+
iptm=iptm,
|
| 1040 |
+
ptm=ptm,
|
| 1041 |
+
sequence=template.sequence,
|
| 1042 |
+
structure_template=template,
|
| 1043 |
+
raw_output={key: _to_cpu_detached(val) for key, val in output.items()},
|
| 1044 |
+
seed=seed,
|
| 1045 |
+
)
|
| 1046 |
+
|
| 1047 |
+
def save_as_cif(
|
| 1048 |
+
self,
|
| 1049 |
+
structure_output: Boltz2StructureOutput,
|
| 1050 |
+
output_path: str,
|
| 1051 |
+
sample_index: int = 0,
|
| 1052 |
+
) -> str:
|
| 1053 |
+
if structure_output.structure_template is None:
|
| 1054 |
+
raise ValueError("structure_output.structure_template is required for CIF export.")
|
| 1055 |
+
if structure_output.sample_atom_coords is None:
|
| 1056 |
+
raise ValueError("structure_output.sample_atom_coords is required for CIF export.")
|
| 1057 |
+
if structure_output.atom_pad_mask is None:
|
| 1058 |
+
raise ValueError("structure_output.atom_pad_mask is required for CIF export.")
|
| 1059 |
+
return write_cif(
|
| 1060 |
+
structure_template=structure_output.structure_template,
|
| 1061 |
+
atom_coords=structure_output.sample_atom_coords,
|
| 1062 |
+
atom_mask=structure_output.atom_pad_mask,
|
| 1063 |
+
output_path=output_path,
|
| 1064 |
+
plddt=structure_output.plddt,
|
| 1065 |
+
sample_index=sample_index,
|
| 1066 |
+
)
|
fastplms/models/boltz/vb_const.py
CHANGED
|
@@ -1,353 +1,353 @@
|
|
| 1 |
-
"""Minimal biological constants required by the local Boltz2 runtime.
|
| 2 |
-
|
| 3 |
-
The upstream project contains additional training-time curation tables. They
|
| 4 |
-
are intentionally excluded here because FastPLMs neither trains Boltz2 nor
|
| 5 |
-
uses upstream data-processing code. Keeping only runtime inputs makes this
|
| 6 |
-
module auditable and prevents parity-oracle data from becoming a package
|
| 7 |
-
dependency.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
def _index(values: list[str]) -> dict[str, int]:
|
| 14 |
-
return {value: index for index, value in enumerate(values)}
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
chain_types = ["PROTEIN", "DNA", "RNA", "NONPOLYMER"]
|
| 18 |
-
chain_type_ids = _index(chain_types)
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
canonical_tokens = [
|
| 22 |
-
"ALA",
|
| 23 |
-
"ARG",
|
| 24 |
-
"ASN",
|
| 25 |
-
"ASP",
|
| 26 |
-
"CYS",
|
| 27 |
-
"GLN",
|
| 28 |
-
"GLU",
|
| 29 |
-
"GLY",
|
| 30 |
-
"HIS",
|
| 31 |
-
"ILE",
|
| 32 |
-
"LEU",
|
| 33 |
-
"LYS",
|
| 34 |
-
"MET",
|
| 35 |
-
"PHE",
|
| 36 |
-
"PRO",
|
| 37 |
-
"SER",
|
| 38 |
-
"THR",
|
| 39 |
-
"TRP",
|
| 40 |
-
"TYR",
|
| 41 |
-
"VAL",
|
| 42 |
-
"UNK",
|
| 43 |
-
]
|
| 44 |
-
tokens = [
|
| 45 |
-
"<pad>",
|
| 46 |
-
"-",
|
| 47 |
-
*canonical_tokens,
|
| 48 |
-
"A",
|
| 49 |
-
"G",
|
| 50 |
-
"C",
|
| 51 |
-
"U",
|
| 52 |
-
"N",
|
| 53 |
-
"DA",
|
| 54 |
-
"DG",
|
| 55 |
-
"DC",
|
| 56 |
-
"DT",
|
| 57 |
-
"DN",
|
| 58 |
-
]
|
| 59 |
-
token_ids = _index(tokens)
|
| 60 |
-
num_tokens = len(tokens)
|
| 61 |
-
|
| 62 |
-
prot_letter_to_token = dict(
|
| 63 |
-
zip(
|
| 64 |
-
"ARNDCEQGHILKMFPSTWYV",
|
| 65 |
-
[
|
| 66 |
-
"ALA",
|
| 67 |
-
"ARG",
|
| 68 |
-
"ASN",
|
| 69 |
-
"ASP",
|
| 70 |
-
"CYS",
|
| 71 |
-
"GLU",
|
| 72 |
-
"GLN",
|
| 73 |
-
"GLY",
|
| 74 |
-
"HIS",
|
| 75 |
-
"ILE",
|
| 76 |
-
"LEU",
|
| 77 |
-
"LYS",
|
| 78 |
-
"MET",
|
| 79 |
-
"PHE",
|
| 80 |
-
"PRO",
|
| 81 |
-
"SER",
|
| 82 |
-
"THR",
|
| 83 |
-
"TRP",
|
| 84 |
-
"TYR",
|
| 85 |
-
"VAL",
|
| 86 |
-
],
|
| 87 |
-
strict=True,
|
| 88 |
-
)
|
| 89 |
-
)
|
| 90 |
-
prot_letter_to_token.update({letter: "UNK" for letter in "XJBZOU"} | {"-": "-"})
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
def _parse_atom_rows(rows: str) -> dict[str, list[str]]:
|
| 94 |
-
table: dict[str, list[str]] = {}
|
| 95 |
-
for row in rows.strip().splitlines():
|
| 96 |
-
residue, *atom_names = row.split()
|
| 97 |
-
table[residue] = atom_names
|
| 98 |
-
return table
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
ref_atoms = {"PAD": [], "-": []}
|
| 102 |
-
ref_atoms.update(
|
| 103 |
-
_parse_atom_rows(
|
| 104 |
-
"""
|
| 105 |
-
UNK N CA C O CB
|
| 106 |
-
ALA N CA C O CB
|
| 107 |
-
ARG N CA C O CB CG CD NE CZ NH1 NH2
|
| 108 |
-
ASN N CA C O CB CG OD1 ND2
|
| 109 |
-
ASP N CA C O CB CG OD1 OD2
|
| 110 |
-
CYS N CA C O CB SG
|
| 111 |
-
GLN N CA C O CB CG CD OE1 NE2
|
| 112 |
-
GLU N CA C O CB CG CD OE1 OE2
|
| 113 |
-
GLY N CA C O
|
| 114 |
-
HIS N CA C O CB CG ND1 CD2 CE1 NE2
|
| 115 |
-
ILE N CA C O CB CG1 CG2 CD1
|
| 116 |
-
LEU N CA C O CB CG CD1 CD2
|
| 117 |
-
LYS N CA C O CB CG CD CE NZ
|
| 118 |
-
MET N CA C O CB CG SD CE
|
| 119 |
-
PHE N CA C O CB CG CD1 CD2 CE1 CE2 CZ
|
| 120 |
-
PRO N CA C O CB CG CD
|
| 121 |
-
SER N CA C O CB OG
|
| 122 |
-
THR N CA C O CB OG1 CG2
|
| 123 |
-
TRP N CA C O CB CG CD1 CD2 NE1 CE2 CE3 CZ2 CZ3 CH2
|
| 124 |
-
TYR N CA C O CB CG CD1 CD2 CE1 CE2 CZ OH
|
| 125 |
-
VAL N CA C O CB CG1 CG2
|
| 126 |
-
"""
|
| 127 |
-
)
|
| 128 |
-
)
|
| 129 |
-
|
| 130 |
-
protein_backbone_atom_names = ["N", "CA", "C", "O"]
|
| 131 |
-
nucleic_backbone_atom_names = [
|
| 132 |
-
"P",
|
| 133 |
-
"OP1",
|
| 134 |
-
"OP2",
|
| 135 |
-
"O5'",
|
| 136 |
-
"C5'",
|
| 137 |
-
"C4'",
|
| 138 |
-
"O4'",
|
| 139 |
-
"C3'",
|
| 140 |
-
"O3'",
|
| 141 |
-
"C2'",
|
| 142 |
-
"O2'",
|
| 143 |
-
"C1'",
|
| 144 |
-
]
|
| 145 |
-
protein_backbone_atom_index = _index(protein_backbone_atom_names)
|
| 146 |
-
nucleic_backbone_atom_index = _index(nucleic_backbone_atom_names)
|
| 147 |
-
|
| 148 |
-
_rna_backbone = nucleic_backbone_atom_names
|
| 149 |
-
_dna_backbone = [atom for atom in _rna_backbone if atom != "O2'"]
|
| 150 |
-
ref_atoms.update(
|
| 151 |
-
{
|
| 152 |
-
"A": [*_rna_backbone, "N9", "C8", "N7", "C5", "C6", "N6", "N1", "C2", "N3", "C4"],
|
| 153 |
-
"G": [*_rna_backbone, "N9", "C8", "N7", "C5", "C6", "O6", "N1", "C2", "N2", "N3", "C4"],
|
| 154 |
-
"C": [*_rna_backbone, "N1", "C2", "O2", "N3", "C4", "N4", "C5", "C6"],
|
| 155 |
-
"U": [*_rna_backbone, "N1", "C2", "O2", "N3", "C4", "O4", "C5", "C6"],
|
| 156 |
-
"N": list(_rna_backbone),
|
| 157 |
-
"DA": [*_dna_backbone, "N9", "C8", "N7", "C5", "C6", "N6", "N1", "C2", "N3", "C4"],
|
| 158 |
-
"DG": [*_dna_backbone, "N9", "C8", "N7", "C5", "C6", "O6", "N1", "C2", "N2", "N3", "C4"],
|
| 159 |
-
"DC": [*_dna_backbone, "N1", "C2", "O2", "N3", "C4", "N4", "C5", "C6"],
|
| 160 |
-
"DT": [*_dna_backbone, "N1", "C2", "O2", "N3", "C4", "O4", "C5", "C7", "C6"],
|
| 161 |
-
"DN": list(_dna_backbone),
|
| 162 |
-
}
|
| 163 |
-
)
|
| 164 |
-
|
| 165 |
-
_protein_tokens = ["UNK", *canonical_tokens[:-1]]
|
| 166 |
-
_nucleic_tokens = ["A", "G", "C", "U", "N", "DA", "DG", "DC", "DT", "DN"]
|
| 167 |
-
res_to_center_atom = {
|
| 168 |
-
**dict.fromkeys(_protein_tokens, "CA"),
|
| 169 |
-
**dict.fromkeys(_nucleic_tokens, "C1'"),
|
| 170 |
-
}
|
| 171 |
-
res_to_disto_atom = {
|
| 172 |
-
**dict.fromkeys(_protein_tokens, "CB"),
|
| 173 |
-
"GLY": "CA",
|
| 174 |
-
"A": "C4",
|
| 175 |
-
"G": "C4",
|
| 176 |
-
"C": "C2",
|
| 177 |
-
"U": "C2",
|
| 178 |
-
"N": "C1'",
|
| 179 |
-
"DA": "C4",
|
| 180 |
-
"DG": "C4",
|
| 181 |
-
"DC": "C2",
|
| 182 |
-
"DT": "C2",
|
| 183 |
-
"DN": "C1'",
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
num_elements = 128
|
| 188 |
-
bond_types = ["OTHER", "SINGLE", "DOUBLE", "TRIPLE", "AROMATIC", "COVALENT"]
|
| 189 |
-
contact_conditioning_info = {
|
| 190 |
-
"UNSPECIFIED": 0,
|
| 191 |
-
"UNSELECTED": 1,
|
| 192 |
-
"POCKET>BINDER": 2,
|
| 193 |
-
"BINDER>POCKET": 3,
|
| 194 |
-
"CONTACT": 4,
|
| 195 |
-
}
|
| 196 |
-
chunk_size_threshold = 384
|
| 197 |
-
|
| 198 |
-
_method_groups = {
|
| 199 |
-
0: ("MD",),
|
| 200 |
-
1: ("X-RAY DIFFRACTION",),
|
| 201 |
-
2: ("ELECTRON MICROSCOPY",),
|
| 202 |
-
3: ("SOLUTION NMR",),
|
| 203 |
-
4: (
|
| 204 |
-
"SOLID-STATE NMR",
|
| 205 |
-
"NEUTRON DIFFRACTION",
|
| 206 |
-
"ELECTRON CRYSTALLOGRAPHY",
|
| 207 |
-
"FIBER DIFFRACTION",
|
| 208 |
-
"POWDER DIFFRACTION",
|
| 209 |
-
"INFRARED SPECTROSCOPY",
|
| 210 |
-
"FLUORESCENCE TRANSFER",
|
| 211 |
-
"EPR",
|
| 212 |
-
"THEORETICAL MODEL",
|
| 213 |
-
"SOLUTION SCATTERING",
|
| 214 |
-
"OTHER",
|
| 215 |
-
),
|
| 216 |
-
5: ("AFDB",),
|
| 217 |
-
6: ("BOLTZ-1",),
|
| 218 |
-
7: ("FUTURE1",),
|
| 219 |
-
8: ("FUTURE2",),
|
| 220 |
-
9: ("FUTURE3",),
|
| 221 |
-
10: ("FUTURE4",),
|
| 222 |
-
11: ("FUTURE5",),
|
| 223 |
-
}
|
| 224 |
-
method_types_ids = {
|
| 225 |
-
method.lower(): identifier
|
| 226 |
-
for identifier, methods in _method_groups.items()
|
| 227 |
-
for method in methods
|
| 228 |
-
}
|
| 229 |
-
num_method_types = len(_method_groups)
|
| 230 |
-
|
| 231 |
-
vdw_radii = [
|
| 232 |
-
float(radius)
|
| 233 |
-
for radius in [
|
| 234 |
-
"1.2",
|
| 235 |
-
"1.4",
|
| 236 |
-
"2.2",
|
| 237 |
-
"1.9",
|
| 238 |
-
"1.8",
|
| 239 |
-
"1.7",
|
| 240 |
-
"1.6",
|
| 241 |
-
"1.55",
|
| 242 |
-
"1.5",
|
| 243 |
-
"1.54",
|
| 244 |
-
"2.4",
|
| 245 |
-
"2.2",
|
| 246 |
-
"2.1",
|
| 247 |
-
"2.1",
|
| 248 |
-
"1.95",
|
| 249 |
-
"1.8",
|
| 250 |
-
"1.8",
|
| 251 |
-
"1.88",
|
| 252 |
-
"2.8",
|
| 253 |
-
"2.4",
|
| 254 |
-
"2.3",
|
| 255 |
-
"2.15",
|
| 256 |
-
"2.05",
|
| 257 |
-
"2.05",
|
| 258 |
-
"2.05",
|
| 259 |
-
"2.05",
|
| 260 |
-
"2.0",
|
| 261 |
-
"2.0",
|
| 262 |
-
"2.0",
|
| 263 |
-
"2.1",
|
| 264 |
-
"2.1",
|
| 265 |
-
"2.1",
|
| 266 |
-
"2.05",
|
| 267 |
-
"1.9",
|
| 268 |
-
"1.9",
|
| 269 |
-
"2.02",
|
| 270 |
-
"2.9",
|
| 271 |
-
"2.55",
|
| 272 |
-
"2.4",
|
| 273 |
-
"2.3",
|
| 274 |
-
"2.15",
|
| 275 |
-
"2.1",
|
| 276 |
-
"2.05",
|
| 277 |
-
"2.05",
|
| 278 |
-
"2.0",
|
| 279 |
-
"2.05",
|
| 280 |
-
"2.1",
|
| 281 |
-
"2.2",
|
| 282 |
-
"2.2",
|
| 283 |
-
"2.25",
|
| 284 |
-
"2.2",
|
| 285 |
-
"2.1",
|
| 286 |
-
"2.1",
|
| 287 |
-
"2.16",
|
| 288 |
-
"3.0",
|
| 289 |
-
"2.7",
|
| 290 |
-
"2.5",
|
| 291 |
-
"2.48",
|
| 292 |
-
"2.47",
|
| 293 |
-
"2.45",
|
| 294 |
-
"2.43",
|
| 295 |
-
"2.42",
|
| 296 |
-
"2.4",
|
| 297 |
-
"2.38",
|
| 298 |
-
"2.37",
|
| 299 |
-
"2.35",
|
| 300 |
-
"2.33",
|
| 301 |
-
"2.32",
|
| 302 |
-
"2.3",
|
| 303 |
-
"2.28",
|
| 304 |
-
"2.27",
|
| 305 |
-
"2.25",
|
| 306 |
-
"2.2",
|
| 307 |
-
"2.1",
|
| 308 |
-
"2.05",
|
| 309 |
-
"2.0",
|
| 310 |
-
"2.0",
|
| 311 |
-
"2.05",
|
| 312 |
-
"2.1",
|
| 313 |
-
"2.05",
|
| 314 |
-
"2.2",
|
| 315 |
-
"2.3",
|
| 316 |
-
"2.3",
|
| 317 |
-
"2.0",
|
| 318 |
-
"2.0",
|
| 319 |
-
"2.0",
|
| 320 |
-
"2.0",
|
| 321 |
-
"2.0",
|
| 322 |
-
"2.0",
|
| 323 |
-
"2.4",
|
| 324 |
-
"2.0",
|
| 325 |
-
"2.3",
|
| 326 |
-
"2.0",
|
| 327 |
-
"2.0",
|
| 328 |
-
"2.0",
|
| 329 |
-
"2.0",
|
| 330 |
-
"2.0",
|
| 331 |
-
"2.0",
|
| 332 |
-
"2.0",
|
| 333 |
-
"2.0",
|
| 334 |
-
"2.0",
|
| 335 |
-
"2.0",
|
| 336 |
-
"2.0",
|
| 337 |
-
"2.0",
|
| 338 |
-
"2.0",
|
| 339 |
-
"2.0",
|
| 340 |
-
"2.0",
|
| 341 |
-
"2.0",
|
| 342 |
-
"2.0",
|
| 343 |
-
"2.0",
|
| 344 |
-
"2.0",
|
| 345 |
-
"2.0",
|
| 346 |
-
"2.0",
|
| 347 |
-
"2.0",
|
| 348 |
-
"2.0",
|
| 349 |
-
"2.0",
|
| 350 |
-
"2.0",
|
| 351 |
-
"2.0",
|
| 352 |
-
]
|
| 353 |
-
]
|
|
|
|
| 1 |
+
"""Minimal biological constants required by the local Boltz2 runtime.
|
| 2 |
+
|
| 3 |
+
The upstream project contains additional training-time curation tables. They
|
| 4 |
+
are intentionally excluded here because FastPLMs neither trains Boltz2 nor
|
| 5 |
+
uses upstream data-processing code. Keeping only runtime inputs makes this
|
| 6 |
+
module auditable and prevents parity-oracle data from becoming a package
|
| 7 |
+
dependency.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _index(values: list[str]) -> dict[str, int]:
|
| 14 |
+
return {value: index for index, value in enumerate(values)}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
chain_types = ["PROTEIN", "DNA", "RNA", "NONPOLYMER"]
|
| 18 |
+
chain_type_ids = _index(chain_types)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
canonical_tokens = [
|
| 22 |
+
"ALA",
|
| 23 |
+
"ARG",
|
| 24 |
+
"ASN",
|
| 25 |
+
"ASP",
|
| 26 |
+
"CYS",
|
| 27 |
+
"GLN",
|
| 28 |
+
"GLU",
|
| 29 |
+
"GLY",
|
| 30 |
+
"HIS",
|
| 31 |
+
"ILE",
|
| 32 |
+
"LEU",
|
| 33 |
+
"LYS",
|
| 34 |
+
"MET",
|
| 35 |
+
"PHE",
|
| 36 |
+
"PRO",
|
| 37 |
+
"SER",
|
| 38 |
+
"THR",
|
| 39 |
+
"TRP",
|
| 40 |
+
"TYR",
|
| 41 |
+
"VAL",
|
| 42 |
+
"UNK",
|
| 43 |
+
]
|
| 44 |
+
tokens = [
|
| 45 |
+
"<pad>",
|
| 46 |
+
"-",
|
| 47 |
+
*canonical_tokens,
|
| 48 |
+
"A",
|
| 49 |
+
"G",
|
| 50 |
+
"C",
|
| 51 |
+
"U",
|
| 52 |
+
"N",
|
| 53 |
+
"DA",
|
| 54 |
+
"DG",
|
| 55 |
+
"DC",
|
| 56 |
+
"DT",
|
| 57 |
+
"DN",
|
| 58 |
+
]
|
| 59 |
+
token_ids = _index(tokens)
|
| 60 |
+
num_tokens = len(tokens)
|
| 61 |
+
|
| 62 |
+
prot_letter_to_token = dict(
|
| 63 |
+
zip(
|
| 64 |
+
"ARNDCEQGHILKMFPSTWYV",
|
| 65 |
+
[
|
| 66 |
+
"ALA",
|
| 67 |
+
"ARG",
|
| 68 |
+
"ASN",
|
| 69 |
+
"ASP",
|
| 70 |
+
"CYS",
|
| 71 |
+
"GLU",
|
| 72 |
+
"GLN",
|
| 73 |
+
"GLY",
|
| 74 |
+
"HIS",
|
| 75 |
+
"ILE",
|
| 76 |
+
"LEU",
|
| 77 |
+
"LYS",
|
| 78 |
+
"MET",
|
| 79 |
+
"PHE",
|
| 80 |
+
"PRO",
|
| 81 |
+
"SER",
|
| 82 |
+
"THR",
|
| 83 |
+
"TRP",
|
| 84 |
+
"TYR",
|
| 85 |
+
"VAL",
|
| 86 |
+
],
|
| 87 |
+
strict=True,
|
| 88 |
+
)
|
| 89 |
+
)
|
| 90 |
+
prot_letter_to_token.update({letter: "UNK" for letter in "XJBZOU"} | {"-": "-"})
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _parse_atom_rows(rows: str) -> dict[str, list[str]]:
|
| 94 |
+
table: dict[str, list[str]] = {}
|
| 95 |
+
for row in rows.strip().splitlines():
|
| 96 |
+
residue, *atom_names = row.split()
|
| 97 |
+
table[residue] = atom_names
|
| 98 |
+
return table
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
ref_atoms = {"PAD": [], "-": []}
|
| 102 |
+
ref_atoms.update(
|
| 103 |
+
_parse_atom_rows(
|
| 104 |
+
"""
|
| 105 |
+
UNK N CA C O CB
|
| 106 |
+
ALA N CA C O CB
|
| 107 |
+
ARG N CA C O CB CG CD NE CZ NH1 NH2
|
| 108 |
+
ASN N CA C O CB CG OD1 ND2
|
| 109 |
+
ASP N CA C O CB CG OD1 OD2
|
| 110 |
+
CYS N CA C O CB SG
|
| 111 |
+
GLN N CA C O CB CG CD OE1 NE2
|
| 112 |
+
GLU N CA C O CB CG CD OE1 OE2
|
| 113 |
+
GLY N CA C O
|
| 114 |
+
HIS N CA C O CB CG ND1 CD2 CE1 NE2
|
| 115 |
+
ILE N CA C O CB CG1 CG2 CD1
|
| 116 |
+
LEU N CA C O CB CG CD1 CD2
|
| 117 |
+
LYS N CA C O CB CG CD CE NZ
|
| 118 |
+
MET N CA C O CB CG SD CE
|
| 119 |
+
PHE N CA C O CB CG CD1 CD2 CE1 CE2 CZ
|
| 120 |
+
PRO N CA C O CB CG CD
|
| 121 |
+
SER N CA C O CB OG
|
| 122 |
+
THR N CA C O CB OG1 CG2
|
| 123 |
+
TRP N CA C O CB CG CD1 CD2 NE1 CE2 CE3 CZ2 CZ3 CH2
|
| 124 |
+
TYR N CA C O CB CG CD1 CD2 CE1 CE2 CZ OH
|
| 125 |
+
VAL N CA C O CB CG1 CG2
|
| 126 |
+
"""
|
| 127 |
+
)
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
protein_backbone_atom_names = ["N", "CA", "C", "O"]
|
| 131 |
+
nucleic_backbone_atom_names = [
|
| 132 |
+
"P",
|
| 133 |
+
"OP1",
|
| 134 |
+
"OP2",
|
| 135 |
+
"O5'",
|
| 136 |
+
"C5'",
|
| 137 |
+
"C4'",
|
| 138 |
+
"O4'",
|
| 139 |
+
"C3'",
|
| 140 |
+
"O3'",
|
| 141 |
+
"C2'",
|
| 142 |
+
"O2'",
|
| 143 |
+
"C1'",
|
| 144 |
+
]
|
| 145 |
+
protein_backbone_atom_index = _index(protein_backbone_atom_names)
|
| 146 |
+
nucleic_backbone_atom_index = _index(nucleic_backbone_atom_names)
|
| 147 |
+
|
| 148 |
+
_rna_backbone = nucleic_backbone_atom_names
|
| 149 |
+
_dna_backbone = [atom for atom in _rna_backbone if atom != "O2'"]
|
| 150 |
+
ref_atoms.update(
|
| 151 |
+
{
|
| 152 |
+
"A": [*_rna_backbone, "N9", "C8", "N7", "C5", "C6", "N6", "N1", "C2", "N3", "C4"],
|
| 153 |
+
"G": [*_rna_backbone, "N9", "C8", "N7", "C5", "C6", "O6", "N1", "C2", "N2", "N3", "C4"],
|
| 154 |
+
"C": [*_rna_backbone, "N1", "C2", "O2", "N3", "C4", "N4", "C5", "C6"],
|
| 155 |
+
"U": [*_rna_backbone, "N1", "C2", "O2", "N3", "C4", "O4", "C5", "C6"],
|
| 156 |
+
"N": list(_rna_backbone),
|
| 157 |
+
"DA": [*_dna_backbone, "N9", "C8", "N7", "C5", "C6", "N6", "N1", "C2", "N3", "C4"],
|
| 158 |
+
"DG": [*_dna_backbone, "N9", "C8", "N7", "C5", "C6", "O6", "N1", "C2", "N2", "N3", "C4"],
|
| 159 |
+
"DC": [*_dna_backbone, "N1", "C2", "O2", "N3", "C4", "N4", "C5", "C6"],
|
| 160 |
+
"DT": [*_dna_backbone, "N1", "C2", "O2", "N3", "C4", "O4", "C5", "C7", "C6"],
|
| 161 |
+
"DN": list(_dna_backbone),
|
| 162 |
+
}
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
_protein_tokens = ["UNK", *canonical_tokens[:-1]]
|
| 166 |
+
_nucleic_tokens = ["A", "G", "C", "U", "N", "DA", "DG", "DC", "DT", "DN"]
|
| 167 |
+
res_to_center_atom = {
|
| 168 |
+
**dict.fromkeys(_protein_tokens, "CA"),
|
| 169 |
+
**dict.fromkeys(_nucleic_tokens, "C1'"),
|
| 170 |
+
}
|
| 171 |
+
res_to_disto_atom = {
|
| 172 |
+
**dict.fromkeys(_protein_tokens, "CB"),
|
| 173 |
+
"GLY": "CA",
|
| 174 |
+
"A": "C4",
|
| 175 |
+
"G": "C4",
|
| 176 |
+
"C": "C2",
|
| 177 |
+
"U": "C2",
|
| 178 |
+
"N": "C1'",
|
| 179 |
+
"DA": "C4",
|
| 180 |
+
"DG": "C4",
|
| 181 |
+
"DC": "C2",
|
| 182 |
+
"DT": "C2",
|
| 183 |
+
"DN": "C1'",
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
num_elements = 128
|
| 188 |
+
bond_types = ["OTHER", "SINGLE", "DOUBLE", "TRIPLE", "AROMATIC", "COVALENT"]
|
| 189 |
+
contact_conditioning_info = {
|
| 190 |
+
"UNSPECIFIED": 0,
|
| 191 |
+
"UNSELECTED": 1,
|
| 192 |
+
"POCKET>BINDER": 2,
|
| 193 |
+
"BINDER>POCKET": 3,
|
| 194 |
+
"CONTACT": 4,
|
| 195 |
+
}
|
| 196 |
+
chunk_size_threshold = 384
|
| 197 |
+
|
| 198 |
+
_method_groups = {
|
| 199 |
+
0: ("MD",),
|
| 200 |
+
1: ("X-RAY DIFFRACTION",),
|
| 201 |
+
2: ("ELECTRON MICROSCOPY",),
|
| 202 |
+
3: ("SOLUTION NMR",),
|
| 203 |
+
4: (
|
| 204 |
+
"SOLID-STATE NMR",
|
| 205 |
+
"NEUTRON DIFFRACTION",
|
| 206 |
+
"ELECTRON CRYSTALLOGRAPHY",
|
| 207 |
+
"FIBER DIFFRACTION",
|
| 208 |
+
"POWDER DIFFRACTION",
|
| 209 |
+
"INFRARED SPECTROSCOPY",
|
| 210 |
+
"FLUORESCENCE TRANSFER",
|
| 211 |
+
"EPR",
|
| 212 |
+
"THEORETICAL MODEL",
|
| 213 |
+
"SOLUTION SCATTERING",
|
| 214 |
+
"OTHER",
|
| 215 |
+
),
|
| 216 |
+
5: ("AFDB",),
|
| 217 |
+
6: ("BOLTZ-1",),
|
| 218 |
+
7: ("FUTURE1",),
|
| 219 |
+
8: ("FUTURE2",),
|
| 220 |
+
9: ("FUTURE3",),
|
| 221 |
+
10: ("FUTURE4",),
|
| 222 |
+
11: ("FUTURE5",),
|
| 223 |
+
}
|
| 224 |
+
method_types_ids = {
|
| 225 |
+
method.lower(): identifier
|
| 226 |
+
for identifier, methods in _method_groups.items()
|
| 227 |
+
for method in methods
|
| 228 |
+
}
|
| 229 |
+
num_method_types = len(_method_groups)
|
| 230 |
+
|
| 231 |
+
vdw_radii = [
|
| 232 |
+
float(radius)
|
| 233 |
+
for radius in [
|
| 234 |
+
"1.2",
|
| 235 |
+
"1.4",
|
| 236 |
+
"2.2",
|
| 237 |
+
"1.9",
|
| 238 |
+
"1.8",
|
| 239 |
+
"1.7",
|
| 240 |
+
"1.6",
|
| 241 |
+
"1.55",
|
| 242 |
+
"1.5",
|
| 243 |
+
"1.54",
|
| 244 |
+
"2.4",
|
| 245 |
+
"2.2",
|
| 246 |
+
"2.1",
|
| 247 |
+
"2.1",
|
| 248 |
+
"1.95",
|
| 249 |
+
"1.8",
|
| 250 |
+
"1.8",
|
| 251 |
+
"1.88",
|
| 252 |
+
"2.8",
|
| 253 |
+
"2.4",
|
| 254 |
+
"2.3",
|
| 255 |
+
"2.15",
|
| 256 |
+
"2.05",
|
| 257 |
+
"2.05",
|
| 258 |
+
"2.05",
|
| 259 |
+
"2.05",
|
| 260 |
+
"2.0",
|
| 261 |
+
"2.0",
|
| 262 |
+
"2.0",
|
| 263 |
+
"2.1",
|
| 264 |
+
"2.1",
|
| 265 |
+
"2.1",
|
| 266 |
+
"2.05",
|
| 267 |
+
"1.9",
|
| 268 |
+
"1.9",
|
| 269 |
+
"2.02",
|
| 270 |
+
"2.9",
|
| 271 |
+
"2.55",
|
| 272 |
+
"2.4",
|
| 273 |
+
"2.3",
|
| 274 |
+
"2.15",
|
| 275 |
+
"2.1",
|
| 276 |
+
"2.05",
|
| 277 |
+
"2.05",
|
| 278 |
+
"2.0",
|
| 279 |
+
"2.05",
|
| 280 |
+
"2.1",
|
| 281 |
+
"2.2",
|
| 282 |
+
"2.2",
|
| 283 |
+
"2.25",
|
| 284 |
+
"2.2",
|
| 285 |
+
"2.1",
|
| 286 |
+
"2.1",
|
| 287 |
+
"2.16",
|
| 288 |
+
"3.0",
|
| 289 |
+
"2.7",
|
| 290 |
+
"2.5",
|
| 291 |
+
"2.48",
|
| 292 |
+
"2.47",
|
| 293 |
+
"2.45",
|
| 294 |
+
"2.43",
|
| 295 |
+
"2.42",
|
| 296 |
+
"2.4",
|
| 297 |
+
"2.38",
|
| 298 |
+
"2.37",
|
| 299 |
+
"2.35",
|
| 300 |
+
"2.33",
|
| 301 |
+
"2.32",
|
| 302 |
+
"2.3",
|
| 303 |
+
"2.28",
|
| 304 |
+
"2.27",
|
| 305 |
+
"2.25",
|
| 306 |
+
"2.2",
|
| 307 |
+
"2.1",
|
| 308 |
+
"2.05",
|
| 309 |
+
"2.0",
|
| 310 |
+
"2.0",
|
| 311 |
+
"2.05",
|
| 312 |
+
"2.1",
|
| 313 |
+
"2.05",
|
| 314 |
+
"2.2",
|
| 315 |
+
"2.3",
|
| 316 |
+
"2.3",
|
| 317 |
+
"2.0",
|
| 318 |
+
"2.0",
|
| 319 |
+
"2.0",
|
| 320 |
+
"2.0",
|
| 321 |
+
"2.0",
|
| 322 |
+
"2.0",
|
| 323 |
+
"2.4",
|
| 324 |
+
"2.0",
|
| 325 |
+
"2.3",
|
| 326 |
+
"2.0",
|
| 327 |
+
"2.0",
|
| 328 |
+
"2.0",
|
| 329 |
+
"2.0",
|
| 330 |
+
"2.0",
|
| 331 |
+
"2.0",
|
| 332 |
+
"2.0",
|
| 333 |
+
"2.0",
|
| 334 |
+
"2.0",
|
| 335 |
+
"2.0",
|
| 336 |
+
"2.0",
|
| 337 |
+
"2.0",
|
| 338 |
+
"2.0",
|
| 339 |
+
"2.0",
|
| 340 |
+
"2.0",
|
| 341 |
+
"2.0",
|
| 342 |
+
"2.0",
|
| 343 |
+
"2.0",
|
| 344 |
+
"2.0",
|
| 345 |
+
"2.0",
|
| 346 |
+
"2.0",
|
| 347 |
+
"2.0",
|
| 348 |
+
"2.0",
|
| 349 |
+
"2.0",
|
| 350 |
+
"2.0",
|
| 351 |
+
"2.0",
|
| 352 |
+
]
|
| 353 |
+
]
|
fastplms/models/boltz/vb_layers_attention.py
CHANGED
|
@@ -1,103 +1,103 @@
|
|
| 1 |
-
"""Pair-biased self-attention used in the Boltz2 diffusion stack."""
|
| 2 |
-
|
| 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 |
-
|
| 9 |
-
from . import vb_layers_initialize as init
|
| 10 |
-
from ._pair_attention import pair_biased_attention, reshape_heads
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class AttentionPairBias(nn.Module):
|
| 14 |
-
"""Attend over sequence states while adding a learned pair representation."""
|
| 15 |
-
|
| 16 |
-
def __init__(
|
| 17 |
-
self,
|
| 18 |
-
c_s: int,
|
| 19 |
-
c_z: int,
|
| 20 |
-
num_heads: int,
|
| 21 |
-
inf: float = 1e6,
|
| 22 |
-
initial_norm: bool = True,
|
| 23 |
-
) -> None:
|
| 24 |
-
super().__init__()
|
| 25 |
-
if c_s % num_heads:
|
| 26 |
-
raise ValueError(f"c_s={c_s} must be divisible by num_heads={num_heads}")
|
| 27 |
-
|
| 28 |
-
self.c_s = c_s
|
| 29 |
-
self.num_heads = num_heads
|
| 30 |
-
self.head_dim = c_s // num_heads
|
| 31 |
-
self.inf = inf
|
| 32 |
-
self.initial_norm = initial_norm
|
| 33 |
-
if initial_norm:
|
| 34 |
-
self.norm_s = nn.LayerNorm(c_s)
|
| 35 |
-
|
| 36 |
-
self.proj_q = nn.Linear(c_s, c_s)
|
| 37 |
-
self.proj_k = nn.Linear(c_s, c_s, bias=False)
|
| 38 |
-
self.proj_v = nn.Linear(c_s, c_s, bias=False)
|
| 39 |
-
self.proj_g = nn.Linear(c_s, c_s, bias=False)
|
| 40 |
-
self.proj_z = nn.Sequential(
|
| 41 |
-
nn.LayerNorm(c_z),
|
| 42 |
-
nn.Linear(c_z, num_heads, bias=False),
|
| 43 |
-
Rearrange("b ... h -> b h ..."),
|
| 44 |
-
)
|
| 45 |
-
self.proj_o = nn.Linear(c_s, c_s, bias=False)
|
| 46 |
-
init.final_init_(self.proj_o.weight)
|
| 47 |
-
|
| 48 |
-
def _resolve_pair_bias(
|
| 49 |
-
self,
|
| 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,
|
| 63 |
-
s: Tensor,
|
| 64 |
-
z: Tensor,
|
| 65 |
-
mask: Tensor,
|
| 66 |
-
k_in: Tensor | None = None,
|
| 67 |
-
multiplicity: int = 1,
|
| 68 |
-
to_keys: Callable[[Tensor], Tensor] | None = None,
|
| 69 |
-
model_cache: MutableMapping[str, Tensor] | None = None,
|
| 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,
|
| 96 |
-
value,
|
| 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)
|
|
|
|
| 1 |
+
"""Pair-biased self-attention used in the Boltz2 diffusion stack."""
|
| 2 |
+
|
| 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 |
+
|
| 9 |
+
from . import vb_layers_initialize as init
|
| 10 |
+
from ._pair_attention import pair_biased_attention, reshape_heads
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class AttentionPairBias(nn.Module):
|
| 14 |
+
"""Attend over sequence states while adding a learned pair representation."""
|
| 15 |
+
|
| 16 |
+
def __init__(
|
| 17 |
+
self,
|
| 18 |
+
c_s: int,
|
| 19 |
+
c_z: int,
|
| 20 |
+
num_heads: int,
|
| 21 |
+
inf: float = 1e6,
|
| 22 |
+
initial_norm: bool = True,
|
| 23 |
+
) -> None:
|
| 24 |
+
super().__init__()
|
| 25 |
+
if c_s % num_heads:
|
| 26 |
+
raise ValueError(f"c_s={c_s} must be divisible by num_heads={num_heads}")
|
| 27 |
+
|
| 28 |
+
self.c_s = c_s
|
| 29 |
+
self.num_heads = num_heads
|
| 30 |
+
self.head_dim = c_s // num_heads
|
| 31 |
+
self.inf = inf
|
| 32 |
+
self.initial_norm = initial_norm
|
| 33 |
+
if initial_norm:
|
| 34 |
+
self.norm_s = nn.LayerNorm(c_s)
|
| 35 |
+
|
| 36 |
+
self.proj_q = nn.Linear(c_s, c_s)
|
| 37 |
+
self.proj_k = nn.Linear(c_s, c_s, bias=False)
|
| 38 |
+
self.proj_v = nn.Linear(c_s, c_s, bias=False)
|
| 39 |
+
self.proj_g = nn.Linear(c_s, c_s, bias=False)
|
| 40 |
+
self.proj_z = nn.Sequential(
|
| 41 |
+
nn.LayerNorm(c_z),
|
| 42 |
+
nn.Linear(c_z, num_heads, bias=False),
|
| 43 |
+
Rearrange("b ... h -> b h ..."),
|
| 44 |
+
)
|
| 45 |
+
self.proj_o = nn.Linear(c_s, c_s, bias=False)
|
| 46 |
+
init.final_init_(self.proj_o.weight)
|
| 47 |
+
|
| 48 |
+
def _resolve_pair_bias(
|
| 49 |
+
self,
|
| 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,
|
| 63 |
+
s: Tensor,
|
| 64 |
+
z: Tensor,
|
| 65 |
+
mask: Tensor,
|
| 66 |
+
k_in: Tensor | None = None,
|
| 67 |
+
multiplicity: int = 1,
|
| 68 |
+
to_keys: Callable[[Tensor], Tensor] | None = None,
|
| 69 |
+
model_cache: MutableMapping[str, Tensor] | None = None,
|
| 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,
|
| 96 |
+
value,
|
| 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
|
@@ -1,76 +1,76 @@
|
|
| 1 |
-
"""Cross-attention variant of Boltz2 pair-biased attention."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from einops.layers.torch import Rearrange
|
| 6 |
-
from torch import Tensor, nn
|
| 7 |
-
|
| 8 |
-
from . import vb_layers_initialize as init
|
| 9 |
-
from ._pair_attention import pair_biased_attention, reshape_heads
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class AttentionPairBias(nn.Module):
|
| 13 |
-
"""Attend from S to K while adding either learned or precomputed pair bias."""
|
| 14 |
-
|
| 15 |
-
def __init__(
|
| 16 |
-
self,
|
| 17 |
-
c_s: int,
|
| 18 |
-
c_z: int | None = None,
|
| 19 |
-
num_heads: int | None = None,
|
| 20 |
-
inf: float = 1e6,
|
| 21 |
-
compute_pair_bias: bool = True,
|
| 22 |
-
) -> None:
|
| 23 |
-
super().__init__()
|
| 24 |
-
if num_heads is None or c_s % num_heads:
|
| 25 |
-
raise ValueError("num_heads must divide c_s")
|
| 26 |
-
if compute_pair_bias and c_z is None:
|
| 27 |
-
raise ValueError("c_z is required when pair bias is learned")
|
| 28 |
-
|
| 29 |
-
self.c_s = c_s
|
| 30 |
-
self.num_heads = num_heads
|
| 31 |
-
self.head_dim = c_s // num_heads
|
| 32 |
-
self.inf = inf
|
| 33 |
-
self.proj_q = nn.Linear(c_s, c_s)
|
| 34 |
-
self.proj_k = nn.Linear(c_s, c_s, bias=False)
|
| 35 |
-
self.proj_v = nn.Linear(c_s, c_s, bias=False)
|
| 36 |
-
self.proj_g = nn.Linear(c_s, c_s, bias=False)
|
| 37 |
-
self.compute_pair_bias = compute_pair_bias
|
| 38 |
-
if compute_pair_bias:
|
| 39 |
-
self.proj_z = nn.Sequential(
|
| 40 |
-
nn.LayerNorm(c_z),
|
| 41 |
-
nn.Linear(c_z, num_heads, bias=False),
|
| 42 |
-
Rearrange("b ... h -> b h ..."),
|
| 43 |
-
)
|
| 44 |
-
else:
|
| 45 |
-
self.proj_z = Rearrange("b ... h -> b h ...")
|
| 46 |
-
self.proj_o = nn.Linear(c_s, c_s, bias=False)
|
| 47 |
-
init.final_init_(self.proj_o.weight)
|
| 48 |
-
|
| 49 |
-
def forward(
|
| 50 |
-
self,
|
| 51 |
-
s: Tensor,
|
| 52 |
-
z: Tensor,
|
| 53 |
-
mask: Tensor,
|
| 54 |
-
k_in: Tensor,
|
| 55 |
-
multiplicity: int = 1,
|
| 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,
|
| 69 |
-
value,
|
| 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)
|
|
|
|
| 1 |
+
"""Cross-attention variant of Boltz2 pair-biased attention."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from einops.layers.torch import Rearrange
|
| 6 |
+
from torch import Tensor, nn
|
| 7 |
+
|
| 8 |
+
from . import vb_layers_initialize as init
|
| 9 |
+
from ._pair_attention import pair_biased_attention, reshape_heads
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class AttentionPairBias(nn.Module):
|
| 13 |
+
"""Attend from S to K while adding either learned or precomputed pair bias."""
|
| 14 |
+
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
c_s: int,
|
| 18 |
+
c_z: int | None = None,
|
| 19 |
+
num_heads: int | None = None,
|
| 20 |
+
inf: float = 1e6,
|
| 21 |
+
compute_pair_bias: bool = True,
|
| 22 |
+
) -> None:
|
| 23 |
+
super().__init__()
|
| 24 |
+
if num_heads is None or c_s % num_heads:
|
| 25 |
+
raise ValueError("num_heads must divide c_s")
|
| 26 |
+
if compute_pair_bias and c_z is None:
|
| 27 |
+
raise ValueError("c_z is required when pair bias is learned")
|
| 28 |
+
|
| 29 |
+
self.c_s = c_s
|
| 30 |
+
self.num_heads = num_heads
|
| 31 |
+
self.head_dim = c_s // num_heads
|
| 32 |
+
self.inf = inf
|
| 33 |
+
self.proj_q = nn.Linear(c_s, c_s)
|
| 34 |
+
self.proj_k = nn.Linear(c_s, c_s, bias=False)
|
| 35 |
+
self.proj_v = nn.Linear(c_s, c_s, bias=False)
|
| 36 |
+
self.proj_g = nn.Linear(c_s, c_s, bias=False)
|
| 37 |
+
self.compute_pair_bias = compute_pair_bias
|
| 38 |
+
if compute_pair_bias:
|
| 39 |
+
self.proj_z = nn.Sequential(
|
| 40 |
+
nn.LayerNorm(c_z),
|
| 41 |
+
nn.Linear(c_z, num_heads, bias=False),
|
| 42 |
+
Rearrange("b ... h -> b h ..."),
|
| 43 |
+
)
|
| 44 |
+
else:
|
| 45 |
+
self.proj_z = Rearrange("b ... h -> b h ...")
|
| 46 |
+
self.proj_o = nn.Linear(c_s, c_s, bias=False)
|
| 47 |
+
init.final_init_(self.proj_o.weight)
|
| 48 |
+
|
| 49 |
+
def forward(
|
| 50 |
+
self,
|
| 51 |
+
s: Tensor,
|
| 52 |
+
z: Tensor,
|
| 53 |
+
mask: Tensor,
|
| 54 |
+
k_in: Tensor,
|
| 55 |
+
multiplicity: int = 1,
|
| 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,
|
| 69 |
+
value,
|
| 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
|
@@ -1,273 +1,273 @@
|
|
| 1 |
-
"""Confidence geometry and aggregate metrics for Boltz2 outputs."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from torch import nn
|
| 8 |
-
|
| 9 |
-
from . import vb_const as const
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def compute_collinear_mask(v1: torch.Tensor, v2: torch.Tensor) -> torch.Tensor:
|
| 13 |
-
"""Mark nondegenerate vector pairs whose angle defines a stable frame."""
|
| 14 |
-
|
| 15 |
-
# v1/v2: (n, 3)
|
| 16 |
-
norm1 = torch.norm(v1, dim=1, keepdim=True) # (n, 1)
|
| 17 |
-
norm2 = torch.norm(v2, dim=1, keepdim=True) # (n, 1)
|
| 18 |
-
unit1 = v1 / (norm1 + 1e-6) # (n, 3)
|
| 19 |
-
unit2 = v2 / (norm2 + 1e-6) # (n, 3)
|
| 20 |
-
separated = torch.abs(torch.sum(unit1 * unit2, dim=1)) < 0.9063 # (n,)
|
| 21 |
-
return (
|
| 22 |
-
separated & (norm1.reshape(-1) > 1e-2) & (norm2.reshape(-1) > 1e-2)
|
| 23 |
-
) # (n,)
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 27 |
-
with torch.amp.autocast("cuda", enabled=False):
|
| 28 |
-
return torch.bmm(
|
| 29 |
-
feats["atom_to_token"].float(),
|
| 30 |
-
feats["asym_id"].unsqueeze(-1).float(),
|
| 31 |
-
).squeeze(-1) # (b, a)
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def _replace_nonpolymer_frames(
|
| 35 |
-
coordinates: torch.Tensor,
|
| 36 |
-
frame_indices: torch.Tensor,
|
| 37 |
-
feats: dict[str, torch.Tensor],
|
| 38 |
-
atom_chain_ids: torch.Tensor,
|
| 39 |
-
resolved_mask: torch.Tensor | None,
|
| 40 |
-
*,
|
| 41 |
-
inference: bool,
|
| 42 |
-
) -> None:
|
| 43 |
-
# coordinates: (b, m, a, 3); frame_indices: (b, m, t_or_a, 3).
|
| 44 |
-
token_chain_ids = feats["asym_id"] # (b, t)
|
| 45 |
-
for batch_index, sample_coordinates in enumerate(coordinates):
|
| 46 |
-
# sample_coordinates: (m, a, 3)
|
| 47 |
-
token_offset = 0
|
| 48 |
-
atom_offset = 0
|
| 49 |
-
for chain_id in torch.unique(token_chain_ids[batch_index]):
|
| 50 |
-
token_mask = (token_chain_ids[batch_index] == chain_id) * feats[
|
| 51 |
-
"token_pad_mask"
|
| 52 |
-
][batch_index] # (t,)
|
| 53 |
-
atom_mask = (atom_chain_ids[batch_index] == chain_id) * feats[
|
| 54 |
-
"atom_pad_mask"
|
| 55 |
-
][batch_index] # (a,)
|
| 56 |
-
token_count = int(token_mask.sum().item())
|
| 57 |
-
atom_count = int(atom_mask.sum().item())
|
| 58 |
-
is_nonpolymer = (
|
| 59 |
-
feats["mol_type"][batch_index, token_offset] == const.chain_type_ids["NONPOLYMER"]
|
| 60 |
-
)
|
| 61 |
-
if is_nonpolymer and atom_count >= 3:
|
| 62 |
-
chain_resolved = (
|
| 63 |
-
feats["atom_pad_mask"][batch_index]
|
| 64 |
-
if inference
|
| 65 |
-
else (
|
| 66 |
-
feats["atom_resolved_mask"][batch_index]
|
| 67 |
-
if resolved_mask is None
|
| 68 |
-
else resolved_mask[batch_index]
|
| 69 |
-
)
|
| 70 |
-
)
|
| 71 |
-
chain_atom_mask = atom_mask.bool() # (a,)
|
| 72 |
-
chain_coordinates = sample_coordinates[:, chain_atom_mask] # (m, a_c, 3)
|
| 73 |
-
differences = (
|
| 74 |
-
chain_coordinates[:, None, :, :] - chain_coordinates[:, :, None, :]
|
| 75 |
-
) # (m, a_c, a_c, 3)
|
| 76 |
-
distances = differences.square().sum(dim=-1) ** 0.5 # (m, a_c, a_c)
|
| 77 |
-
valid = chain_resolved[chain_atom_mask] # (a_c,)
|
| 78 |
-
invalid_pairs = 1 - (valid[None, :] * valid[:, None]).to(
|
| 79 |
-
torch.float32
|
| 80 |
-
) # (a_c, a_c)
|
| 81 |
-
invalid_pairs[invalid_pairs == 1] = torch.inf # (a_c, a_c)
|
| 82 |
-
nearest = torch.sort(
|
| 83 |
-
distances + invalid_pairs, dim=2
|
| 84 |
-
).indices # (m, a_c, a_c)
|
| 85 |
-
frames = (
|
| 86 |
-
torch.cat(
|
| 87 |
-
(nearest[:, :, 1:2], nearest[:, :, 0:1], nearest[:, :, 2:3]),
|
| 88 |
-
dim=2,
|
| 89 |
-
)
|
| 90 |
-
+ atom_offset
|
| 91 |
-
) # (m, a_c, 3)
|
| 92 |
-
frame_indices[
|
| 93 |
-
batch_index,
|
| 94 |
-
:,
|
| 95 |
-
token_offset : token_offset + atom_count,
|
| 96 |
-
:,
|
| 97 |
-
] = frames
|
| 98 |
-
token_offset += token_count
|
| 99 |
-
atom_offset += atom_count
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def compute_frame_pred(
|
| 103 |
-
pred_atom_coords: torch.Tensor,
|
| 104 |
-
frames_idx_true: torch.Tensor,
|
| 105 |
-
feats: dict[str, torch.Tensor],
|
| 106 |
-
multiplicity: int,
|
| 107 |
-
resolved_mask: torch.Tensor | None = None,
|
| 108 |
-
inference: bool = False,
|
| 109 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 110 |
-
"""Construct predicted local frames and their validity mask.
|
| 111 |
-
|
| 112 |
-
``pred_atom_coords`` has shape ``(b * multiplicity, a, 3)``. Polymer
|
| 113 |
-
frames retain the supplied atom indices; nonpolymer frames use each
|
| 114 |
-
atom's three nearest resolved neighbors.
|
| 115 |
-
"""
|
| 116 |
-
|
| 117 |
-
atom_chain_ids = _atom_chain_ids(feats) # (b, a)
|
| 118 |
-
expanded_batch, _, _ = pred_atom_coords.shape
|
| 119 |
-
base_batch = expanded_batch // multiplicity
|
| 120 |
-
coordinates = pred_atom_coords.reshape(
|
| 121 |
-
base_batch, multiplicity, -1, 3
|
| 122 |
-
) # (b, m, a, 3)
|
| 123 |
-
frame_indices = frames_idx_true.clone().repeat_interleave(
|
| 124 |
-
multiplicity, 0
|
| 125 |
-
) # (b * m, 1, t, 3) or (b * m, t, 3)
|
| 126 |
-
frame_indices = frame_indices.reshape(
|
| 127 |
-
base_batch, multiplicity, -1, 3
|
| 128 |
-
) # (b, m, t, 3)
|
| 129 |
-
_replace_nonpolymer_frames(
|
| 130 |
-
coordinates,
|
| 131 |
-
frame_indices,
|
| 132 |
-
feats,
|
| 133 |
-
atom_chain_ids,
|
| 134 |
-
resolved_mask,
|
| 135 |
-
inference=inference,
|
| 136 |
-
)
|
| 137 |
-
|
| 138 |
-
batch_indices = torch.arange(
|
| 139 |
-
base_batch, device=frame_indices.device
|
| 140 |
-
)[:, None, None, None] # (b, 1, 1, 1)
|
| 141 |
-
sample_indices = torch.arange(
|
| 142 |
-
multiplicity, device=frame_indices.device
|
| 143 |
-
)[None, :, None, None] # (1, m, 1, 1)
|
| 144 |
-
frame_coordinates = coordinates[
|
| 145 |
-
batch_indices,
|
| 146 |
-
sample_indices,
|
| 147 |
-
frame_indices,
|
| 148 |
-
].reshape(-1, 3, 3) # (b * m * t, 3, 3)
|
| 149 |
-
valid_frames = compute_collinear_mask(
|
| 150 |
-
frame_coordinates[:, 1] - frame_coordinates[:, 0],
|
| 151 |
-
frame_coordinates[:, 1] - frame_coordinates[:, 2],
|
| 152 |
-
).reshape(base_batch, multiplicity, -1) # (b, m, t)
|
| 153 |
-
return (
|
| 154 |
-
frame_indices,
|
| 155 |
-
valid_frames * feats["token_pad_mask"][:, None, :],
|
| 156 |
-
) # (b, m, t, 3), (b, m, t)
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
def compute_aggregated_metric(logits: torch.Tensor, end: float = 1.0) -> torch.Tensor:
|
| 160 |
-
"""Convert categorical confidence logits to bin-center expectations."""
|
| 161 |
-
|
| 162 |
-
num_bins = logits.shape[-1]
|
| 163 |
-
bin_width = end / num_bins
|
| 164 |
-
centers = torch.arange(
|
| 165 |
-
0.5 * bin_width,
|
| 166 |
-
end,
|
| 167 |
-
bin_width,
|
| 168 |
-
device=logits.device,
|
| 169 |
-
) # (n_bin,)
|
| 170 |
-
# logits: (..., n_bin)
|
| 171 |
-
probabilities = nn.functional.softmax(logits, dim=-1) # (..., n_bin)
|
| 172 |
-
broadcast_shape = (1,) * (probabilities.ndim - 1) + centers.shape
|
| 173 |
-
return torch.sum(
|
| 174 |
-
probabilities * centers.view(broadcast_shape), dim=-1
|
| 175 |
-
) # (...)
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
def tm_function(d: torch.Tensor, n_res: torch.Tensor) -> torch.Tensor:
|
| 179 |
-
"""Evaluate the TM-score distance kernel."""
|
| 180 |
-
|
| 181 |
-
# d and n_res broadcast to a shared shape (...).
|
| 182 |
-
distance_scale = 1.24 * (torch.clip(n_res, min=19) - 15) ** (1 / 3) - 1.8 # (...)
|
| 183 |
-
return 1 / (1 + (d / distance_scale) ** 2) # (...)
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
def _maximum_masked_tm(
|
| 187 |
-
expected_tm: torch.Tensor,
|
| 188 |
-
pair_mask: torch.Tensor,
|
| 189 |
-
) -> torch.Tensor:
|
| 190 |
-
# expected_tm/pair_mask: (b * m, t, t).
|
| 191 |
-
per_anchor = torch.sum(expected_tm * pair_mask, dim=-1) / (
|
| 192 |
-
torch.sum(pair_mask, dim=-1) + 1e-5
|
| 193 |
-
) # (b * m, t)
|
| 194 |
-
return torch.max(per_anchor, dim=1).values # (b * m,)
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
def compute_ptms(
|
| 198 |
-
logits: torch.Tensor,
|
| 199 |
-
x_preds: torch.Tensor,
|
| 200 |
-
feats: dict[str, torch.Tensor],
|
| 201 |
-
multiplicity: int,
|
| 202 |
-
) -> tuple[
|
| 203 |
-
torch.Tensor,
|
| 204 |
-
torch.Tensor,
|
| 205 |
-
torch.Tensor,
|
| 206 |
-
torch.Tensor,
|
| 207 |
-
dict[int, dict[int, torch.Tensor]],
|
| 208 |
-
]:
|
| 209 |
-
"""Compute pTM, ipTM, interface-type, and chain-pair confidence scores."""
|
| 210 |
-
|
| 211 |
-
# logits: (b * m, t, t, n_bin); x_preds: (b * m, a, 3).
|
| 212 |
-
_, frame_mask = compute_frame_pred(
|
| 213 |
-
x_preds,
|
| 214 |
-
feats["frames_idx"],
|
| 215 |
-
feats,
|
| 216 |
-
multiplicity,
|
| 217 |
-
inference=True,
|
| 218 |
-
) # frame_mask: (b, m, t)
|
| 219 |
-
token_mask = feats["token_pad_mask"].repeat_interleave(
|
| 220 |
-
multiplicity, 0
|
| 221 |
-
) # (b * m, t)
|
| 222 |
-
valid_anchor = frame_mask.reshape(-1, frame_mask.shape[-1]) # (b * m, t)
|
| 223 |
-
base_pair_mask = (
|
| 224 |
-
valid_anchor[:, :, None] * token_mask[:, None, :] * token_mask[:, :, None]
|
| 225 |
-
) # (b * m, t, t)
|
| 226 |
-
asym_id = feats["asym_id"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 227 |
-
interface_mask = base_pair_mask * (
|
| 228 |
-
asym_id[:, None, :] != asym_id[:, :, None]
|
| 229 |
-
) # (b * m, t, t)
|
| 230 |
-
|
| 231 |
-
num_bins = logits.shape[-1]
|
| 232 |
-
pae_centers = torch.arange(
|
| 233 |
-
0.5 * (32.0 / num_bins),
|
| 234 |
-
32.0,
|
| 235 |
-
32.0 / num_bins,
|
| 236 |
-
device=logits.device,
|
| 237 |
-
).unsqueeze(0) # (1, n_bin)
|
| 238 |
-
n_res = token_mask.sum(dim=-1, keepdim=True) # (b * m, 1)
|
| 239 |
-
tm_values = tm_function(pae_centers, n_res).unsqueeze(1).unsqueeze(
|
| 240 |
-
2
|
| 241 |
-
) # (b * m, 1, 1, n_bin)
|
| 242 |
-
expected_tm = torch.sum(
|
| 243 |
-
nn.functional.softmax(logits, dim=-1) * tm_values, dim=-1
|
| 244 |
-
) # (b * m, t, t)
|
| 245 |
-
|
| 246 |
-
ptm = _maximum_masked_tm(expected_tm, base_pair_mask) # (b * m,)
|
| 247 |
-
iptm = _maximum_masked_tm(expected_tm, interface_mask) # (b * m,)
|
| 248 |
-
|
| 249 |
-
token_type = feats["mol_type"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 250 |
-
ligand = (token_type == const.chain_type_ids["NONPOLYMER"]).float() # (b * m, t)
|
| 251 |
-
protein = (token_type == const.chain_type_ids["PROTEIN"]).float() # (b * m, t)
|
| 252 |
-
ligand_protein = (
|
| 253 |
-
ligand[:, :, None] * protein[:, None, :] + protein[:, :, None] * ligand[:, None, :]
|
| 254 |
-
) # (b * m, t, t)
|
| 255 |
-
protein_protein = protein[:, :, None] * protein[:, None, :] # (b * m, t, t)
|
| 256 |
-
ligand_iptm = _maximum_masked_tm(
|
| 257 |
-
expected_tm, interface_mask * ligand_protein
|
| 258 |
-
) # (b * m,)
|
| 259 |
-
protein_iptm = _maximum_masked_tm(
|
| 260 |
-
expected_tm, interface_mask * protein_protein
|
| 261 |
-
) # (b * m,)
|
| 262 |
-
|
| 263 |
-
chain_pair_iptm: dict[int, dict[int, torch.Tensor]] = {}
|
| 264 |
-
for first_chain in torch.unique(asym_id).tolist():
|
| 265 |
-
scores: dict[int, torch.Tensor] = {}
|
| 266 |
-
for second_chain in torch.unique(asym_id).tolist():
|
| 267 |
-
chain_mask = base_pair_mask
|
| 268 |
-
chain_mask = chain_mask * (asym_id[:, None, :] == first_chain)
|
| 269 |
-
chain_mask = chain_mask * (asym_id[:, :, None] == second_chain)
|
| 270 |
-
scores[second_chain] = _maximum_masked_tm(expected_tm, chain_mask)
|
| 271 |
-
chain_pair_iptm[first_chain] = scores
|
| 272 |
-
|
| 273 |
-
return ptm, iptm, ligand_iptm, protein_iptm, chain_pair_iptm
|
|
|
|
| 1 |
+
"""Confidence geometry and aggregate metrics for Boltz2 outputs."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from torch import nn
|
| 8 |
+
|
| 9 |
+
from . import vb_const as const
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def compute_collinear_mask(v1: torch.Tensor, v2: torch.Tensor) -> torch.Tensor:
|
| 13 |
+
"""Mark nondegenerate vector pairs whose angle defines a stable frame."""
|
| 14 |
+
|
| 15 |
+
# v1/v2: (n, 3)
|
| 16 |
+
norm1 = torch.norm(v1, dim=1, keepdim=True) # (n, 1)
|
| 17 |
+
norm2 = torch.norm(v2, dim=1, keepdim=True) # (n, 1)
|
| 18 |
+
unit1 = v1 / (norm1 + 1e-6) # (n, 3)
|
| 19 |
+
unit2 = v2 / (norm2 + 1e-6) # (n, 3)
|
| 20 |
+
separated = torch.abs(torch.sum(unit1 * unit2, dim=1)) < 0.9063 # (n,)
|
| 21 |
+
return (
|
| 22 |
+
separated & (norm1.reshape(-1) > 1e-2) & (norm2.reshape(-1) > 1e-2)
|
| 23 |
+
) # (n,)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 27 |
+
with torch.amp.autocast("cuda", enabled=False):
|
| 28 |
+
return torch.bmm(
|
| 29 |
+
feats["atom_to_token"].float(),
|
| 30 |
+
feats["asym_id"].unsqueeze(-1).float(),
|
| 31 |
+
).squeeze(-1) # (b, a)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _replace_nonpolymer_frames(
|
| 35 |
+
coordinates: torch.Tensor,
|
| 36 |
+
frame_indices: torch.Tensor,
|
| 37 |
+
feats: dict[str, torch.Tensor],
|
| 38 |
+
atom_chain_ids: torch.Tensor,
|
| 39 |
+
resolved_mask: torch.Tensor | None,
|
| 40 |
+
*,
|
| 41 |
+
inference: bool,
|
| 42 |
+
) -> None:
|
| 43 |
+
# coordinates: (b, m, a, 3); frame_indices: (b, m, t_or_a, 3).
|
| 44 |
+
token_chain_ids = feats["asym_id"] # (b, t)
|
| 45 |
+
for batch_index, sample_coordinates in enumerate(coordinates):
|
| 46 |
+
# sample_coordinates: (m, a, 3)
|
| 47 |
+
token_offset = 0
|
| 48 |
+
atom_offset = 0
|
| 49 |
+
for chain_id in torch.unique(token_chain_ids[batch_index]):
|
| 50 |
+
token_mask = (token_chain_ids[batch_index] == chain_id) * feats[
|
| 51 |
+
"token_pad_mask"
|
| 52 |
+
][batch_index] # (t,)
|
| 53 |
+
atom_mask = (atom_chain_ids[batch_index] == chain_id) * feats[
|
| 54 |
+
"atom_pad_mask"
|
| 55 |
+
][batch_index] # (a,)
|
| 56 |
+
token_count = int(token_mask.sum().item())
|
| 57 |
+
atom_count = int(atom_mask.sum().item())
|
| 58 |
+
is_nonpolymer = (
|
| 59 |
+
feats["mol_type"][batch_index, token_offset] == const.chain_type_ids["NONPOLYMER"]
|
| 60 |
+
)
|
| 61 |
+
if is_nonpolymer and atom_count >= 3:
|
| 62 |
+
chain_resolved = (
|
| 63 |
+
feats["atom_pad_mask"][batch_index]
|
| 64 |
+
if inference
|
| 65 |
+
else (
|
| 66 |
+
feats["atom_resolved_mask"][batch_index]
|
| 67 |
+
if resolved_mask is None
|
| 68 |
+
else resolved_mask[batch_index]
|
| 69 |
+
)
|
| 70 |
+
)
|
| 71 |
+
chain_atom_mask = atom_mask.bool() # (a,)
|
| 72 |
+
chain_coordinates = sample_coordinates[:, chain_atom_mask] # (m, a_c, 3)
|
| 73 |
+
differences = (
|
| 74 |
+
chain_coordinates[:, None, :, :] - chain_coordinates[:, :, None, :]
|
| 75 |
+
) # (m, a_c, a_c, 3)
|
| 76 |
+
distances = differences.square().sum(dim=-1) ** 0.5 # (m, a_c, a_c)
|
| 77 |
+
valid = chain_resolved[chain_atom_mask] # (a_c,)
|
| 78 |
+
invalid_pairs = 1 - (valid[None, :] * valid[:, None]).to(
|
| 79 |
+
torch.float32
|
| 80 |
+
) # (a_c, a_c)
|
| 81 |
+
invalid_pairs[invalid_pairs == 1] = torch.inf # (a_c, a_c)
|
| 82 |
+
nearest = torch.sort(
|
| 83 |
+
distances + invalid_pairs, dim=2
|
| 84 |
+
).indices # (m, a_c, a_c)
|
| 85 |
+
frames = (
|
| 86 |
+
torch.cat(
|
| 87 |
+
(nearest[:, :, 1:2], nearest[:, :, 0:1], nearest[:, :, 2:3]),
|
| 88 |
+
dim=2,
|
| 89 |
+
)
|
| 90 |
+
+ atom_offset
|
| 91 |
+
) # (m, a_c, 3)
|
| 92 |
+
frame_indices[
|
| 93 |
+
batch_index,
|
| 94 |
+
:,
|
| 95 |
+
token_offset : token_offset + atom_count,
|
| 96 |
+
:,
|
| 97 |
+
] = frames
|
| 98 |
+
token_offset += token_count
|
| 99 |
+
atom_offset += atom_count
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def compute_frame_pred(
|
| 103 |
+
pred_atom_coords: torch.Tensor,
|
| 104 |
+
frames_idx_true: torch.Tensor,
|
| 105 |
+
feats: dict[str, torch.Tensor],
|
| 106 |
+
multiplicity: int,
|
| 107 |
+
resolved_mask: torch.Tensor | None = None,
|
| 108 |
+
inference: bool = False,
|
| 109 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 110 |
+
"""Construct predicted local frames and their validity mask.
|
| 111 |
+
|
| 112 |
+
``pred_atom_coords`` has shape ``(b * multiplicity, a, 3)``. Polymer
|
| 113 |
+
frames retain the supplied atom indices; nonpolymer frames use each
|
| 114 |
+
atom's three nearest resolved neighbors.
|
| 115 |
+
"""
|
| 116 |
+
|
| 117 |
+
atom_chain_ids = _atom_chain_ids(feats) # (b, a)
|
| 118 |
+
expanded_batch, _, _ = pred_atom_coords.shape
|
| 119 |
+
base_batch = expanded_batch // multiplicity
|
| 120 |
+
coordinates = pred_atom_coords.reshape(
|
| 121 |
+
base_batch, multiplicity, -1, 3
|
| 122 |
+
) # (b, m, a, 3)
|
| 123 |
+
frame_indices = frames_idx_true.clone().repeat_interleave(
|
| 124 |
+
multiplicity, 0
|
| 125 |
+
) # (b * m, 1, t, 3) or (b * m, t, 3)
|
| 126 |
+
frame_indices = frame_indices.reshape(
|
| 127 |
+
base_batch, multiplicity, -1, 3
|
| 128 |
+
) # (b, m, t, 3)
|
| 129 |
+
_replace_nonpolymer_frames(
|
| 130 |
+
coordinates,
|
| 131 |
+
frame_indices,
|
| 132 |
+
feats,
|
| 133 |
+
atom_chain_ids,
|
| 134 |
+
resolved_mask,
|
| 135 |
+
inference=inference,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
batch_indices = torch.arange(
|
| 139 |
+
base_batch, device=frame_indices.device
|
| 140 |
+
)[:, None, None, None] # (b, 1, 1, 1)
|
| 141 |
+
sample_indices = torch.arange(
|
| 142 |
+
multiplicity, device=frame_indices.device
|
| 143 |
+
)[None, :, None, None] # (1, m, 1, 1)
|
| 144 |
+
frame_coordinates = coordinates[
|
| 145 |
+
batch_indices,
|
| 146 |
+
sample_indices,
|
| 147 |
+
frame_indices,
|
| 148 |
+
].reshape(-1, 3, 3) # (b * m * t, 3, 3)
|
| 149 |
+
valid_frames = compute_collinear_mask(
|
| 150 |
+
frame_coordinates[:, 1] - frame_coordinates[:, 0],
|
| 151 |
+
frame_coordinates[:, 1] - frame_coordinates[:, 2],
|
| 152 |
+
).reshape(base_batch, multiplicity, -1) # (b, m, t)
|
| 153 |
+
return (
|
| 154 |
+
frame_indices,
|
| 155 |
+
valid_frames * feats["token_pad_mask"][:, None, :],
|
| 156 |
+
) # (b, m, t, 3), (b, m, t)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def compute_aggregated_metric(logits: torch.Tensor, end: float = 1.0) -> torch.Tensor:
|
| 160 |
+
"""Convert categorical confidence logits to bin-center expectations."""
|
| 161 |
+
|
| 162 |
+
num_bins = logits.shape[-1]
|
| 163 |
+
bin_width = end / num_bins
|
| 164 |
+
centers = torch.arange(
|
| 165 |
+
0.5 * bin_width,
|
| 166 |
+
end,
|
| 167 |
+
bin_width,
|
| 168 |
+
device=logits.device,
|
| 169 |
+
) # (n_bin,)
|
| 170 |
+
# logits: (..., n_bin)
|
| 171 |
+
probabilities = nn.functional.softmax(logits, dim=-1) # (..., n_bin)
|
| 172 |
+
broadcast_shape = (1,) * (probabilities.ndim - 1) + centers.shape
|
| 173 |
+
return torch.sum(
|
| 174 |
+
probabilities * centers.view(broadcast_shape), dim=-1
|
| 175 |
+
) # (...)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def tm_function(d: torch.Tensor, n_res: torch.Tensor) -> torch.Tensor:
|
| 179 |
+
"""Evaluate the TM-score distance kernel."""
|
| 180 |
+
|
| 181 |
+
# d and n_res broadcast to a shared shape (...).
|
| 182 |
+
distance_scale = 1.24 * (torch.clip(n_res, min=19) - 15) ** (1 / 3) - 1.8 # (...)
|
| 183 |
+
return 1 / (1 + (d / distance_scale) ** 2) # (...)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _maximum_masked_tm(
|
| 187 |
+
expected_tm: torch.Tensor,
|
| 188 |
+
pair_mask: torch.Tensor,
|
| 189 |
+
) -> torch.Tensor:
|
| 190 |
+
# expected_tm/pair_mask: (b * m, t, t).
|
| 191 |
+
per_anchor = torch.sum(expected_tm * pair_mask, dim=-1) / (
|
| 192 |
+
torch.sum(pair_mask, dim=-1) + 1e-5
|
| 193 |
+
) # (b * m, t)
|
| 194 |
+
return torch.max(per_anchor, dim=1).values # (b * m,)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def compute_ptms(
|
| 198 |
+
logits: torch.Tensor,
|
| 199 |
+
x_preds: torch.Tensor,
|
| 200 |
+
feats: dict[str, torch.Tensor],
|
| 201 |
+
multiplicity: int,
|
| 202 |
+
) -> tuple[
|
| 203 |
+
torch.Tensor,
|
| 204 |
+
torch.Tensor,
|
| 205 |
+
torch.Tensor,
|
| 206 |
+
torch.Tensor,
|
| 207 |
+
dict[int, dict[int, torch.Tensor]],
|
| 208 |
+
]:
|
| 209 |
+
"""Compute pTM, ipTM, interface-type, and chain-pair confidence scores."""
|
| 210 |
+
|
| 211 |
+
# logits: (b * m, t, t, n_bin); x_preds: (b * m, a, 3).
|
| 212 |
+
_, frame_mask = compute_frame_pred(
|
| 213 |
+
x_preds,
|
| 214 |
+
feats["frames_idx"],
|
| 215 |
+
feats,
|
| 216 |
+
multiplicity,
|
| 217 |
+
inference=True,
|
| 218 |
+
) # frame_mask: (b, m, t)
|
| 219 |
+
token_mask = feats["token_pad_mask"].repeat_interleave(
|
| 220 |
+
multiplicity, 0
|
| 221 |
+
) # (b * m, t)
|
| 222 |
+
valid_anchor = frame_mask.reshape(-1, frame_mask.shape[-1]) # (b * m, t)
|
| 223 |
+
base_pair_mask = (
|
| 224 |
+
valid_anchor[:, :, None] * token_mask[:, None, :] * token_mask[:, :, None]
|
| 225 |
+
) # (b * m, t, t)
|
| 226 |
+
asym_id = feats["asym_id"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 227 |
+
interface_mask = base_pair_mask * (
|
| 228 |
+
asym_id[:, None, :] != asym_id[:, :, None]
|
| 229 |
+
) # (b * m, t, t)
|
| 230 |
+
|
| 231 |
+
num_bins = logits.shape[-1]
|
| 232 |
+
pae_centers = torch.arange(
|
| 233 |
+
0.5 * (32.0 / num_bins),
|
| 234 |
+
32.0,
|
| 235 |
+
32.0 / num_bins,
|
| 236 |
+
device=logits.device,
|
| 237 |
+
).unsqueeze(0) # (1, n_bin)
|
| 238 |
+
n_res = token_mask.sum(dim=-1, keepdim=True) # (b * m, 1)
|
| 239 |
+
tm_values = tm_function(pae_centers, n_res).unsqueeze(1).unsqueeze(
|
| 240 |
+
2
|
| 241 |
+
) # (b * m, 1, 1, n_bin)
|
| 242 |
+
expected_tm = torch.sum(
|
| 243 |
+
nn.functional.softmax(logits, dim=-1) * tm_values, dim=-1
|
| 244 |
+
) # (b * m, t, t)
|
| 245 |
+
|
| 246 |
+
ptm = _maximum_masked_tm(expected_tm, base_pair_mask) # (b * m,)
|
| 247 |
+
iptm = _maximum_masked_tm(expected_tm, interface_mask) # (b * m,)
|
| 248 |
+
|
| 249 |
+
token_type = feats["mol_type"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 250 |
+
ligand = (token_type == const.chain_type_ids["NONPOLYMER"]).float() # (b * m, t)
|
| 251 |
+
protein = (token_type == const.chain_type_ids["PROTEIN"]).float() # (b * m, t)
|
| 252 |
+
ligand_protein = (
|
| 253 |
+
ligand[:, :, None] * protein[:, None, :] + protein[:, :, None] * ligand[:, None, :]
|
| 254 |
+
) # (b * m, t, t)
|
| 255 |
+
protein_protein = protein[:, :, None] * protein[:, None, :] # (b * m, t, t)
|
| 256 |
+
ligand_iptm = _maximum_masked_tm(
|
| 257 |
+
expected_tm, interface_mask * ligand_protein
|
| 258 |
+
) # (b * m,)
|
| 259 |
+
protein_iptm = _maximum_masked_tm(
|
| 260 |
+
expected_tm, interface_mask * protein_protein
|
| 261 |
+
) # (b * m,)
|
| 262 |
+
|
| 263 |
+
chain_pair_iptm: dict[int, dict[int, torch.Tensor]] = {}
|
| 264 |
+
for first_chain in torch.unique(asym_id).tolist():
|
| 265 |
+
scores: dict[int, torch.Tensor] = {}
|
| 266 |
+
for second_chain in torch.unique(asym_id).tolist():
|
| 267 |
+
chain_mask = base_pair_mask
|
| 268 |
+
chain_mask = chain_mask * (asym_id[:, None, :] == first_chain)
|
| 269 |
+
chain_mask = chain_mask * (asym_id[:, :, None] == second_chain)
|
| 270 |
+
scores[second_chain] = _maximum_masked_tm(expected_tm, chain_mask)
|
| 271 |
+
chain_pair_iptm[first_chain] = scores
|
| 272 |
+
|
| 273 |
+
return ptm, iptm, ligand_iptm, protein_iptm, chain_pair_iptm
|
fastplms/models/boltz/vb_layers_dropout.py
CHANGED
|
@@ -1,43 +1,43 @@
|
|
| 1 |
-
"""Broadcast dropout masks used by the Boltz pair stack.
|
| 2 |
-
|
| 3 |
-
The mask is sampled in ``float32`` even when the pair tensor uses a lower
|
| 4 |
-
precision dtype. This matches the checkpoint implementation while keeping
|
| 5 |
-
the broadcast axis explicit.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import torch
|
| 11 |
-
|
| 12 |
-
from torch import Tensor
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def _broadcast_mask_shape(Z: Tensor, *, columnwise: bool) -> tuple[int, ...]:
|
| 16 |
-
"""Return the row-wise or column-wise pair-mask shape for ``Z``."""
|
| 17 |
-
|
| 18 |
-
if Z.ndim != 4:
|
| 19 |
-
raise ValueError(f"pair tensor must have four dimensions, got {Z.ndim}")
|
| 20 |
-
b, rows, columns, _ = Z.shape
|
| 21 |
-
return (b, 1, columns, 1) if columnwise else (b, rows, 1, 1)
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def get_dropout_mask(
|
| 25 |
-
dropout: float,
|
| 26 |
-
z: Tensor,
|
| 27 |
-
training: bool,
|
| 28 |
-
columnwise: bool = False,
|
| 29 |
-
) -> Tensor:
|
| 30 |
-
"""Sample an inverted-dropout mask that broadcasts over pair channels.
|
| 31 |
-
|
| 32 |
-
``Z`` is the pair tensor with shape ``(b, n, n, d)``. Row-wise masks
|
| 33 |
-
vary along the first residue axis; column-wise masks vary along the
|
| 34 |
-
second. Evaluation returns an all-one mask while retaining the same RNG
|
| 35 |
-
call pattern as training.
|
| 36 |
-
"""
|
| 37 |
-
|
| 38 |
-
probability = float(dropout) if training else 0.0
|
| 39 |
-
sample_shape = _broadcast_mask_shape(z, columnwise=columnwise)
|
| 40 |
-
keep = (
|
| 41 |
-
torch.rand(sample_shape, dtype=torch.float32, device=z.device) >= probability
|
| 42 |
-
) # (b, 1, n, 1) or (b, n, 1, 1)
|
| 43 |
-
return keep * (1.0 / (1.0 - probability)) # same broadcast-mask shape
|
|
|
|
| 1 |
+
"""Broadcast dropout masks used by the Boltz pair stack.
|
| 2 |
+
|
| 3 |
+
The mask is sampled in ``float32`` even when the pair tensor uses a lower
|
| 4 |
+
precision dtype. This matches the checkpoint implementation while keeping
|
| 5 |
+
the broadcast axis explicit.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from torch import Tensor
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _broadcast_mask_shape(Z: Tensor, *, columnwise: bool) -> tuple[int, ...]:
|
| 16 |
+
"""Return the row-wise or column-wise pair-mask shape for ``Z``."""
|
| 17 |
+
|
| 18 |
+
if Z.ndim != 4:
|
| 19 |
+
raise ValueError(f"pair tensor must have four dimensions, got {Z.ndim}")
|
| 20 |
+
b, rows, columns, _ = Z.shape
|
| 21 |
+
return (b, 1, columns, 1) if columnwise else (b, rows, 1, 1)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def get_dropout_mask(
|
| 25 |
+
dropout: float,
|
| 26 |
+
z: Tensor,
|
| 27 |
+
training: bool,
|
| 28 |
+
columnwise: bool = False,
|
| 29 |
+
) -> Tensor:
|
| 30 |
+
"""Sample an inverted-dropout mask that broadcasts over pair channels.
|
| 31 |
+
|
| 32 |
+
``Z`` is the pair tensor with shape ``(b, n, n, d)``. Row-wise masks
|
| 33 |
+
vary along the first residue axis; column-wise masks vary along the
|
| 34 |
+
second. Evaluation returns an all-one mask while retaining the same RNG
|
| 35 |
+
call pattern as training.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
probability = float(dropout) if training else 0.0
|
| 39 |
+
sample_shape = _broadcast_mask_shape(z, columnwise=columnwise)
|
| 40 |
+
keep = (
|
| 41 |
+
torch.rand(sample_shape, dtype=torch.float32, device=z.device) >= probability
|
| 42 |
+
) # (b, 1, n, 1) or (b, n, 1, 1)
|
| 43 |
+
return keep * (1.0 / (1.0 - probability)) # same broadcast-mask shape
|
fastplms/models/boltz/vb_layers_initialize.py
CHANGED
|
@@ -1,110 +1,110 @@
|
|
| 1 |
-
"""Torch-only parameter initialization for the Boltz2 runtime."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import math
|
| 6 |
-
import torch
|
| 7 |
-
|
| 8 |
-
from typing import Literal
|
| 9 |
-
from torch import Tensor
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
FanMode = Literal["fan_in", "fan_out", "fan_avg"]
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def _calculate_fan(shape: torch.Size | tuple[int, ...], fan: FanMode = "fan_in") -> float:
|
| 16 |
-
"""Resolve the selected fan for a two-dimensional linear weight tensor."""
|
| 17 |
-
|
| 18 |
-
if len(shape) != 2:
|
| 19 |
-
raise ValueError(f"linear weights must be two-dimensional, received {tuple(shape)}")
|
| 20 |
-
fan_out, fan_in = shape
|
| 21 |
-
if fan == "fan_in":
|
| 22 |
-
return float(fan_in)
|
| 23 |
-
if fan == "fan_out":
|
| 24 |
-
return float(fan_out)
|
| 25 |
-
if fan == "fan_avg":
|
| 26 |
-
return (fan_in + fan_out) / 2
|
| 27 |
-
raise ValueError(f"invalid fan mode: {fan!r}")
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def trunc_normal_init_(
|
| 31 |
-
weights: Tensor,
|
| 32 |
-
scale: float = 1.0,
|
| 33 |
-
fan: FanMode = "fan_in",
|
| 34 |
-
) -> None:
|
| 35 |
-
"""Fill W from a normal distribution truncated at two standard deviations."""
|
| 36 |
-
|
| 37 |
-
# weights: (d_out, d_in); initialization preserves this shape in place.
|
| 38 |
-
variance = scale / max(1.0, _calculate_fan(weights.shape, fan))
|
| 39 |
-
std = math.sqrt(variance)
|
| 40 |
-
with torch.no_grad():
|
| 41 |
-
torch.nn.init.trunc_normal_(weights, mean=0.0, std=std, a=-2 * std, b=2 * std)
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def lecun_normal_init_(weights: Tensor) -> None:
|
| 45 |
-
"""Initialize W using fan-in-scaled truncated normal values."""
|
| 46 |
-
|
| 47 |
-
# weights: (d_out, d_in), mutated in place.
|
| 48 |
-
trunc_normal_init_(weights)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def he_normal_init_(weights: Tensor) -> None:
|
| 52 |
-
"""Initialize W using twice the fan-in variance."""
|
| 53 |
-
|
| 54 |
-
# weights: (d_out, d_in), mutated in place.
|
| 55 |
-
trunc_normal_init_(weights, scale=2.0)
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def glorot_uniform_init_(weights: Tensor) -> None:
|
| 59 |
-
"""Initialize W with Xavier uniform values."""
|
| 60 |
-
|
| 61 |
-
# weights: (...), rank at least two; mutated in place.
|
| 62 |
-
torch.nn.init.xavier_uniform_(weights, gain=1.0)
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def _fill_(tensor: Tensor, value: float) -> None:
|
| 66 |
-
# tensor: (...), mutated in place without changing shape.
|
| 67 |
-
with torch.no_grad():
|
| 68 |
-
tensor.fill_(value)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def final_init_(weights: Tensor) -> None:
|
| 72 |
-
"""Zero the final projection W."""
|
| 73 |
-
|
| 74 |
-
# weights: (...), mutated in place.
|
| 75 |
-
_fill_(weights, 0.0)
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def gating_init_(weights: Tensor) -> None:
|
| 79 |
-
"""Zero the gating projection W."""
|
| 80 |
-
|
| 81 |
-
# weights: (...), mutated in place.
|
| 82 |
-
_fill_(weights, 0.0)
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
def bias_init_zero_(bias: Tensor) -> None:
|
| 86 |
-
"""Set the bias tensor to zero."""
|
| 87 |
-
|
| 88 |
-
# bias: (...), mutated in place.
|
| 89 |
-
_fill_(bias, 0.0)
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def bias_init_one_(bias: Tensor) -> None:
|
| 93 |
-
"""Set the bias tensor to one."""
|
| 94 |
-
|
| 95 |
-
# bias: (...), mutated in place.
|
| 96 |
-
_fill_(bias, 1.0)
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
def normal_init_(weights: Tensor) -> None:
|
| 100 |
-
"""Initialize W with linear Kaiming-normal values."""
|
| 101 |
-
|
| 102 |
-
# weights: (...), rank at least two; mutated in place.
|
| 103 |
-
torch.nn.init.kaiming_normal_(weights, nonlinearity="linear")
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def ipa_point_weights_init_(weights: Tensor) -> None:
|
| 107 |
-
"""Initialize W so applying softplus yields one."""
|
| 108 |
-
|
| 109 |
-
# weights: (...), mutated in place.
|
| 110 |
-
_fill_(weights, 0.541324854612918)
|
|
|
|
| 1 |
+
"""Torch-only parameter initialization for the Boltz2 runtime."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from typing import Literal
|
| 9 |
+
from torch import Tensor
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
FanMode = Literal["fan_in", "fan_out", "fan_avg"]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _calculate_fan(shape: torch.Size | tuple[int, ...], fan: FanMode = "fan_in") -> float:
|
| 16 |
+
"""Resolve the selected fan for a two-dimensional linear weight tensor."""
|
| 17 |
+
|
| 18 |
+
if len(shape) != 2:
|
| 19 |
+
raise ValueError(f"linear weights must be two-dimensional, received {tuple(shape)}")
|
| 20 |
+
fan_out, fan_in = shape
|
| 21 |
+
if fan == "fan_in":
|
| 22 |
+
return float(fan_in)
|
| 23 |
+
if fan == "fan_out":
|
| 24 |
+
return float(fan_out)
|
| 25 |
+
if fan == "fan_avg":
|
| 26 |
+
return (fan_in + fan_out) / 2
|
| 27 |
+
raise ValueError(f"invalid fan mode: {fan!r}")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def trunc_normal_init_(
|
| 31 |
+
weights: Tensor,
|
| 32 |
+
scale: float = 1.0,
|
| 33 |
+
fan: FanMode = "fan_in",
|
| 34 |
+
) -> None:
|
| 35 |
+
"""Fill W from a normal distribution truncated at two standard deviations."""
|
| 36 |
+
|
| 37 |
+
# weights: (d_out, d_in); initialization preserves this shape in place.
|
| 38 |
+
variance = scale / max(1.0, _calculate_fan(weights.shape, fan))
|
| 39 |
+
std = math.sqrt(variance)
|
| 40 |
+
with torch.no_grad():
|
| 41 |
+
torch.nn.init.trunc_normal_(weights, mean=0.0, std=std, a=-2 * std, b=2 * std)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def lecun_normal_init_(weights: Tensor) -> None:
|
| 45 |
+
"""Initialize W using fan-in-scaled truncated normal values."""
|
| 46 |
+
|
| 47 |
+
# weights: (d_out, d_in), mutated in place.
|
| 48 |
+
trunc_normal_init_(weights)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def he_normal_init_(weights: Tensor) -> None:
|
| 52 |
+
"""Initialize W using twice the fan-in variance."""
|
| 53 |
+
|
| 54 |
+
# weights: (d_out, d_in), mutated in place.
|
| 55 |
+
trunc_normal_init_(weights, scale=2.0)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def glorot_uniform_init_(weights: Tensor) -> None:
|
| 59 |
+
"""Initialize W with Xavier uniform values."""
|
| 60 |
+
|
| 61 |
+
# weights: (...), rank at least two; mutated in place.
|
| 62 |
+
torch.nn.init.xavier_uniform_(weights, gain=1.0)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _fill_(tensor: Tensor, value: float) -> None:
|
| 66 |
+
# tensor: (...), mutated in place without changing shape.
|
| 67 |
+
with torch.no_grad():
|
| 68 |
+
tensor.fill_(value)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def final_init_(weights: Tensor) -> None:
|
| 72 |
+
"""Zero the final projection W."""
|
| 73 |
+
|
| 74 |
+
# weights: (...), mutated in place.
|
| 75 |
+
_fill_(weights, 0.0)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def gating_init_(weights: Tensor) -> None:
|
| 79 |
+
"""Zero the gating projection W."""
|
| 80 |
+
|
| 81 |
+
# weights: (...), mutated in place.
|
| 82 |
+
_fill_(weights, 0.0)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def bias_init_zero_(bias: Tensor) -> None:
|
| 86 |
+
"""Set the bias tensor to zero."""
|
| 87 |
+
|
| 88 |
+
# bias: (...), mutated in place.
|
| 89 |
+
_fill_(bias, 0.0)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def bias_init_one_(bias: Tensor) -> None:
|
| 93 |
+
"""Set the bias tensor to one."""
|
| 94 |
+
|
| 95 |
+
# bias: (...), mutated in place.
|
| 96 |
+
_fill_(bias, 1.0)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def normal_init_(weights: Tensor) -> None:
|
| 100 |
+
"""Initialize W with linear Kaiming-normal values."""
|
| 101 |
+
|
| 102 |
+
# weights: (...), rank at least two; mutated in place.
|
| 103 |
+
torch.nn.init.kaiming_normal_(weights, nonlinearity="linear")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def ipa_point_weights_init_(weights: Tensor) -> None:
|
| 107 |
+
"""Initialize W so applying softplus yields one."""
|
| 108 |
+
|
| 109 |
+
# weights: (...), mutated in place.
|
| 110 |
+
_fill_(weights, 0.541324854612918)
|
fastplms/models/boltz/vb_layers_outer_product_mean.py
CHANGED
|
@@ -1,105 +1,105 @@
|
|
| 1 |
-
"""Outer-product aggregation from MSA states to pair states."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from torch import Tensor, nn
|
| 8 |
-
|
| 9 |
-
from . import vb_layers_initialize as init
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class OuterProductMean(nn.Module):
|
| 13 |
-
"""Aggregate M with shape ``(b, s, l, d_m)`` into pair tensor Z."""
|
| 14 |
-
|
| 15 |
-
def __init__(self, c_in: int, c_hidden: int, c_out: int) -> None:
|
| 16 |
-
super().__init__()
|
| 17 |
-
self.c_hidden = c_hidden
|
| 18 |
-
self.norm = nn.LayerNorm(c_in)
|
| 19 |
-
self.proj_a = nn.Linear(c_in, c_hidden, bias=False)
|
| 20 |
-
self.proj_b = nn.Linear(c_in, c_hidden, bias=False)
|
| 21 |
-
self.proj_o = nn.Linear(c_hidden * c_hidden, c_out)
|
| 22 |
-
init.final_init_(self.proj_o.weight)
|
| 23 |
-
init.final_init_(self.proj_o.bias)
|
| 24 |
-
|
| 25 |
-
@staticmethod
|
| 26 |
-
def _pair_counts(mask: Tensor) -> Tensor:
|
| 27 |
-
"""Count valid MSA observations for each residue pair."""
|
| 28 |
-
|
| 29 |
-
# mask: (b, s, l, 1)
|
| 30 |
-
counts: Tensor | None = None
|
| 31 |
-
for start in range(0, mask.shape[1], 64):
|
| 32 |
-
mask_slice = mask[:, start : start + 64] # (b, s_chunk, l, 1)
|
| 33 |
-
contribution = (
|
| 34 |
-
mask_slice[:, :, None, :] * mask_slice[:, :, :, None]
|
| 35 |
-
).sum(dim=1) # (b, l, l, 1)
|
| 36 |
-
counts = (
|
| 37 |
-
contribution if counts is None else counts + contribution
|
| 38 |
-
) # (b, l, l, 1)
|
| 39 |
-
if counts is None:
|
| 40 |
-
raise ValueError("MSA depth must be positive")
|
| 41 |
-
return counts.clamp(min=1) # (b, l, l, 1)
|
| 42 |
-
|
| 43 |
-
@staticmethod
|
| 44 |
-
def _outer_product(left: Tensor, right: Tensor, counts: Tensor) -> Tensor:
|
| 45 |
-
# left: (b, s, l, d_left); right: (b, s, l, d_right).
|
| 46 |
-
# counts: (b, l, l, 1); chunked callers may use d_left < d_right.
|
| 47 |
-
outer = torch.einsum("bsic,bsjd->bijcd", left, right) # (b, l, l, d_left, d_right)
|
| 48 |
-
return outer.reshape(*outer.shape[:3], -1) / counts # (b, l, l, d_left * d_right)
|
| 49 |
-
|
| 50 |
-
def _chunked_projection(
|
| 51 |
-
self,
|
| 52 |
-
left: Tensor,
|
| 53 |
-
right: Tensor,
|
| 54 |
-
counts: Tensor,
|
| 55 |
-
chunk_size: int,
|
| 56 |
-
target: Tensor,
|
| 57 |
-
) -> Tensor:
|
| 58 |
-
# left/right: (b, s, l, d_h); counts: (b, l, l, 1).
|
| 59 |
-
output: Tensor | None = None
|
| 60 |
-
for start in range(0, self.c_hidden, chunk_size):
|
| 61 |
-
stop = min(start + chunk_size, self.c_hidden)
|
| 62 |
-
outer = self._outer_product(
|
| 63 |
-
left[..., start:stop], right, counts
|
| 64 |
-
) # (b, l, l, d_chunk * d_h)
|
| 65 |
-
weight = self.proj_o.weight[
|
| 66 |
-
:,
|
| 67 |
-
start * self.c_hidden : stop * self.c_hidden,
|
| 68 |
-
] # (d_z, d_chunk * d_h)
|
| 69 |
-
contribution = outer.to(target) @ weight.T # (b, l, l, d_z)
|
| 70 |
-
output = (
|
| 71 |
-
contribution if output is None else output + contribution
|
| 72 |
-
) # (b, l, l, d_z)
|
| 73 |
-
if output is None:
|
| 74 |
-
raise ValueError("c_hidden must be positive")
|
| 75 |
-
return output + self.proj_o.bias # (b, l, l, d_z)
|
| 76 |
-
|
| 77 |
-
def forward(self, m: Tensor, mask: Tensor, chunk_size: int | None = None) -> Tensor:
|
| 78 |
-
"""Return pair tensor Z with shape ``(b, l, l, d_z)``."""
|
| 79 |
-
|
| 80 |
-
# m: (b, s, l, d_m); mask: (b, s, l).
|
| 81 |
-
expanded_mask = mask.unsqueeze(-1).to(m) # (b, s, l, 1)
|
| 82 |
-
normalized = self.norm(m) # (b, s, l, d_m)
|
| 83 |
-
left = self.proj_a(normalized) * expanded_mask # (b, s, l, d_h)
|
| 84 |
-
right = self.proj_b(normalized) * expanded_mask # (b, s, l, d_h)
|
| 85 |
-
|
| 86 |
-
if chunk_size is not None and not self.training:
|
| 87 |
-
if chunk_size <= 0:
|
| 88 |
-
raise ValueError("chunk_size must be positive")
|
| 89 |
-
counts = self._pair_counts(expanded_mask) # (b, l, l, 1)
|
| 90 |
-
return self._chunked_projection(
|
| 91 |
-
left,
|
| 92 |
-
right,
|
| 93 |
-
counts,
|
| 94 |
-
chunk_size,
|
| 95 |
-
normalized,
|
| 96 |
-
)
|
| 97 |
-
|
| 98 |
-
pair_mask = (
|
| 99 |
-
expanded_mask[:, :, None, :] * expanded_mask[:, :, :, None]
|
| 100 |
-
) # (b, s, l, l, 1)
|
| 101 |
-
counts = pair_mask.sum(dim=1).clamp(min=1) # (b, l, l, 1)
|
| 102 |
-
outer = self._outer_product(
|
| 103 |
-
left.float(), right.float(), counts
|
| 104 |
-
) # (b, l, l, d_h**2)
|
| 105 |
-
return self.proj_o(outer.to(normalized)) # (b, l, l, d_z)
|
|
|
|
| 1 |
+
"""Outer-product aggregation from MSA states to pair states."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from torch import Tensor, nn
|
| 8 |
+
|
| 9 |
+
from . import vb_layers_initialize as init
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class OuterProductMean(nn.Module):
|
| 13 |
+
"""Aggregate M with shape ``(b, s, l, d_m)`` into pair tensor Z."""
|
| 14 |
+
|
| 15 |
+
def __init__(self, c_in: int, c_hidden: int, c_out: int) -> None:
|
| 16 |
+
super().__init__()
|
| 17 |
+
self.c_hidden = c_hidden
|
| 18 |
+
self.norm = nn.LayerNorm(c_in)
|
| 19 |
+
self.proj_a = nn.Linear(c_in, c_hidden, bias=False)
|
| 20 |
+
self.proj_b = nn.Linear(c_in, c_hidden, bias=False)
|
| 21 |
+
self.proj_o = nn.Linear(c_hidden * c_hidden, c_out)
|
| 22 |
+
init.final_init_(self.proj_o.weight)
|
| 23 |
+
init.final_init_(self.proj_o.bias)
|
| 24 |
+
|
| 25 |
+
@staticmethod
|
| 26 |
+
def _pair_counts(mask: Tensor) -> Tensor:
|
| 27 |
+
"""Count valid MSA observations for each residue pair."""
|
| 28 |
+
|
| 29 |
+
# mask: (b, s, l, 1)
|
| 30 |
+
counts: Tensor | None = None
|
| 31 |
+
for start in range(0, mask.shape[1], 64):
|
| 32 |
+
mask_slice = mask[:, start : start + 64] # (b, s_chunk, l, 1)
|
| 33 |
+
contribution = (
|
| 34 |
+
mask_slice[:, :, None, :] * mask_slice[:, :, :, None]
|
| 35 |
+
).sum(dim=1) # (b, l, l, 1)
|
| 36 |
+
counts = (
|
| 37 |
+
contribution if counts is None else counts + contribution
|
| 38 |
+
) # (b, l, l, 1)
|
| 39 |
+
if counts is None:
|
| 40 |
+
raise ValueError("MSA depth must be positive")
|
| 41 |
+
return counts.clamp(min=1) # (b, l, l, 1)
|
| 42 |
+
|
| 43 |
+
@staticmethod
|
| 44 |
+
def _outer_product(left: Tensor, right: Tensor, counts: Tensor) -> Tensor:
|
| 45 |
+
# left: (b, s, l, d_left); right: (b, s, l, d_right).
|
| 46 |
+
# counts: (b, l, l, 1); chunked callers may use d_left < d_right.
|
| 47 |
+
outer = torch.einsum("bsic,bsjd->bijcd", left, right) # (b, l, l, d_left, d_right)
|
| 48 |
+
return outer.reshape(*outer.shape[:3], -1) / counts # (b, l, l, d_left * d_right)
|
| 49 |
+
|
| 50 |
+
def _chunked_projection(
|
| 51 |
+
self,
|
| 52 |
+
left: Tensor,
|
| 53 |
+
right: Tensor,
|
| 54 |
+
counts: Tensor,
|
| 55 |
+
chunk_size: int,
|
| 56 |
+
target: Tensor,
|
| 57 |
+
) -> Tensor:
|
| 58 |
+
# left/right: (b, s, l, d_h); counts: (b, l, l, 1).
|
| 59 |
+
output: Tensor | None = None
|
| 60 |
+
for start in range(0, self.c_hidden, chunk_size):
|
| 61 |
+
stop = min(start + chunk_size, self.c_hidden)
|
| 62 |
+
outer = self._outer_product(
|
| 63 |
+
left[..., start:stop], right, counts
|
| 64 |
+
) # (b, l, l, d_chunk * d_h)
|
| 65 |
+
weight = self.proj_o.weight[
|
| 66 |
+
:,
|
| 67 |
+
start * self.c_hidden : stop * self.c_hidden,
|
| 68 |
+
] # (d_z, d_chunk * d_h)
|
| 69 |
+
contribution = outer.to(target) @ weight.T # (b, l, l, d_z)
|
| 70 |
+
output = (
|
| 71 |
+
contribution if output is None else output + contribution
|
| 72 |
+
) # (b, l, l, d_z)
|
| 73 |
+
if output is None:
|
| 74 |
+
raise ValueError("c_hidden must be positive")
|
| 75 |
+
return output + self.proj_o.bias # (b, l, l, d_z)
|
| 76 |
+
|
| 77 |
+
def forward(self, m: Tensor, mask: Tensor, chunk_size: int | None = None) -> Tensor:
|
| 78 |
+
"""Return pair tensor Z with shape ``(b, l, l, d_z)``."""
|
| 79 |
+
|
| 80 |
+
# m: (b, s, l, d_m); mask: (b, s, l).
|
| 81 |
+
expanded_mask = mask.unsqueeze(-1).to(m) # (b, s, l, 1)
|
| 82 |
+
normalized = self.norm(m) # (b, s, l, d_m)
|
| 83 |
+
left = self.proj_a(normalized) * expanded_mask # (b, s, l, d_h)
|
| 84 |
+
right = self.proj_b(normalized) * expanded_mask # (b, s, l, d_h)
|
| 85 |
+
|
| 86 |
+
if chunk_size is not None and not self.training:
|
| 87 |
+
if chunk_size <= 0:
|
| 88 |
+
raise ValueError("chunk_size must be positive")
|
| 89 |
+
counts = self._pair_counts(expanded_mask) # (b, l, l, 1)
|
| 90 |
+
return self._chunked_projection(
|
| 91 |
+
left,
|
| 92 |
+
right,
|
| 93 |
+
counts,
|
| 94 |
+
chunk_size,
|
| 95 |
+
normalized,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
pair_mask = (
|
| 99 |
+
expanded_mask[:, :, None, :] * expanded_mask[:, :, :, None]
|
| 100 |
+
) # (b, s, l, l, 1)
|
| 101 |
+
counts = pair_mask.sum(dim=1).clamp(min=1) # (b, l, l, 1)
|
| 102 |
+
outer = self._outer_product(
|
| 103 |
+
left.float(), right.float(), counts
|
| 104 |
+
) # (b, l, l, d_h**2)
|
| 105 |
+
return self.proj_o(outer.to(normalized)) # (b, l, l, d_z)
|
fastplms/models/boltz/vb_layers_pair_averaging.py
CHANGED
|
@@ -1,112 +1,112 @@
|
|
| 1 |
-
"""Pair-weighted aggregation of Boltz2 MSA states."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from torch import Tensor, nn
|
| 8 |
-
|
| 9 |
-
from . import vb_layers_initialize as init
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class PairWeightedAveraging(nn.Module):
|
| 13 |
-
"""Update M with pair-derived attention weights while preserving its shape."""
|
| 14 |
-
|
| 15 |
-
def __init__(
|
| 16 |
-
self,
|
| 17 |
-
c_m: int,
|
| 18 |
-
c_z: int,
|
| 19 |
-
c_h: int,
|
| 20 |
-
num_heads: int,
|
| 21 |
-
inf: float = 1e6,
|
| 22 |
-
) -> None:
|
| 23 |
-
super().__init__()
|
| 24 |
-
self.c_m = c_m
|
| 25 |
-
self.c_z = c_z
|
| 26 |
-
self.c_h = c_h
|
| 27 |
-
self.num_heads = num_heads
|
| 28 |
-
self.inf = inf
|
| 29 |
-
self.norm_m = nn.LayerNorm(c_m)
|
| 30 |
-
self.norm_z = nn.LayerNorm(c_z)
|
| 31 |
-
self.proj_m = nn.Linear(c_m, c_h * num_heads, bias=False)
|
| 32 |
-
self.proj_g = nn.Linear(c_m, c_h * num_heads, bias=False)
|
| 33 |
-
self.proj_z = nn.Linear(c_z, num_heads, bias=False)
|
| 34 |
-
self.proj_o = nn.Linear(c_h * num_heads, c_m, bias=False)
|
| 35 |
-
init.final_init_(self.proj_o.weight)
|
| 36 |
-
|
| 37 |
-
def _attention_weights(self, pair_states: Tensor, mask: Tensor) -> Tensor:
|
| 38 |
-
# pair_states: (b, l, l, d_z); mask: (b, l, l).
|
| 39 |
-
logits = self.proj_z(pair_states).permute(0, 3, 1, 2) # (b, h, l, l)
|
| 40 |
-
logits = logits + (1 - mask[:, None]) * -self.inf # (b, h, l, l)
|
| 41 |
-
return torch.softmax(logits, dim=-1) # (b, h, l, l)
|
| 42 |
-
|
| 43 |
-
def _all_heads(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor) -> Tensor:
|
| 44 |
-
# msa_states: (b, s, l, d_m); pair_states: (b, l, l, d_z).
|
| 45 |
-
values = self.proj_m(msa_states).reshape(
|
| 46 |
-
*msa_states.shape[:3],
|
| 47 |
-
self.num_heads,
|
| 48 |
-
self.c_h,
|
| 49 |
-
) # (b, s, l, h, d_h)
|
| 50 |
-
values = values.permute(0, 3, 1, 2, 4) # (b, h, s, l, d_h)
|
| 51 |
-
weights = self._attention_weights(pair_states, mask) # (b, h, l, l)
|
| 52 |
-
gate = self.proj_g(msa_states).sigmoid() # (b, s, l, h * d_h)
|
| 53 |
-
attended = torch.einsum(
|
| 54 |
-
"bhij,bhsjd->bhsid", weights, values
|
| 55 |
-
) # (b, h, s, l, d_h)
|
| 56 |
-
attended = attended.permute(0, 2, 3, 1, 4) # (b, s, l, h, d_h)
|
| 57 |
-
attended = attended.reshape(
|
| 58 |
-
*attended.shape[:3], self.num_heads * self.c_h
|
| 59 |
-
) # (b, s, l, h * d_h)
|
| 60 |
-
return self.proj_o(gate * attended) # (b, s, l, d_m)
|
| 61 |
-
|
| 62 |
-
def _one_head(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor, head: int) -> Tensor:
|
| 63 |
-
# msa_states: (b, s, l, d_m); pair_states: (b, l, l, d_z).
|
| 64 |
-
start = head * self.c_h
|
| 65 |
-
stop = start + self.c_h
|
| 66 |
-
value = msa_states @ self.proj_m.weight[start:stop].T # (b, s, l, d_h)
|
| 67 |
-
value = value.reshape(*value.shape[:3], 1, self.c_h).permute(
|
| 68 |
-
0, 3, 1, 2, 4
|
| 69 |
-
) # (b, 1, s, l, d_h)
|
| 70 |
-
logits = pair_states @ self.proj_z.weight[head : head + 1].T # (b, l, l, 1)
|
| 71 |
-
logits = logits.permute(0, 3, 1, 2) # (b, 1, l, l)
|
| 72 |
-
weights = torch.softmax(
|
| 73 |
-
logits + (1 - mask[:, None]) * -self.inf, dim=-1
|
| 74 |
-
) # (b, 1, l, l)
|
| 75 |
-
gate = (
|
| 76 |
-
msa_states @ self.proj_g.weight[start:stop].T
|
| 77 |
-
).sigmoid() # (b, s, l, d_h)
|
| 78 |
-
attended = torch.einsum(
|
| 79 |
-
"bhij,bhsjd->bhsid", weights, value
|
| 80 |
-
) # (b, 1, s, l, d_h)
|
| 81 |
-
attended = attended.permute(0, 2, 3, 1, 4).reshape(
|
| 82 |
-
*msa_states.shape[:3],
|
| 83 |
-
self.c_h,
|
| 84 |
-
) # (b, s, l, d_h)
|
| 85 |
-
return (gate * attended) @ self.proj_o.weight[:, start:stop].T # (b, s, l, d_m)
|
| 86 |
-
|
| 87 |
-
def forward(
|
| 88 |
-
self,
|
| 89 |
-
m: Tensor,
|
| 90 |
-
z: Tensor,
|
| 91 |
-
mask: Tensor,
|
| 92 |
-
chunk_heads: bool = True,
|
| 93 |
-
) -> Tensor:
|
| 94 |
-
"""Return updated M with shape ``(b, s, l, d_m)``."""
|
| 95 |
-
|
| 96 |
-
# m: (b, s, l, d_m); z: (b, l, l, d_z); mask: (b, l, l).
|
| 97 |
-
msa_states = self.norm_m(m) # (b, s, l, d_m)
|
| 98 |
-
pair_states = self.norm_z(z) # (b, l, l, d_z)
|
| 99 |
-
if not chunk_heads or self.training:
|
| 100 |
-
return self._all_heads(msa_states, pair_states, mask)
|
| 101 |
-
|
| 102 |
-
output: Tensor | None = None
|
| 103 |
-
for head in range(self.num_heads):
|
| 104 |
-
contribution = self._one_head(
|
| 105 |
-
msa_states, pair_states, mask, head
|
| 106 |
-
) # (b, s, l, d_m)
|
| 107 |
-
output = (
|
| 108 |
-
contribution if output is None else output + contribution
|
| 109 |
-
) # (b, s, l, d_m)
|
| 110 |
-
if output is None:
|
| 111 |
-
raise ValueError("num_heads must be positive")
|
| 112 |
-
return output # (b, s, l, d_m)
|
|
|
|
| 1 |
+
"""Pair-weighted aggregation of Boltz2 MSA states."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from torch import Tensor, nn
|
| 8 |
+
|
| 9 |
+
from . import vb_layers_initialize as init
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class PairWeightedAveraging(nn.Module):
|
| 13 |
+
"""Update M with pair-derived attention weights while preserving its shape."""
|
| 14 |
+
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
c_m: int,
|
| 18 |
+
c_z: int,
|
| 19 |
+
c_h: int,
|
| 20 |
+
num_heads: int,
|
| 21 |
+
inf: float = 1e6,
|
| 22 |
+
) -> None:
|
| 23 |
+
super().__init__()
|
| 24 |
+
self.c_m = c_m
|
| 25 |
+
self.c_z = c_z
|
| 26 |
+
self.c_h = c_h
|
| 27 |
+
self.num_heads = num_heads
|
| 28 |
+
self.inf = inf
|
| 29 |
+
self.norm_m = nn.LayerNorm(c_m)
|
| 30 |
+
self.norm_z = nn.LayerNorm(c_z)
|
| 31 |
+
self.proj_m = nn.Linear(c_m, c_h * num_heads, bias=False)
|
| 32 |
+
self.proj_g = nn.Linear(c_m, c_h * num_heads, bias=False)
|
| 33 |
+
self.proj_z = nn.Linear(c_z, num_heads, bias=False)
|
| 34 |
+
self.proj_o = nn.Linear(c_h * num_heads, c_m, bias=False)
|
| 35 |
+
init.final_init_(self.proj_o.weight)
|
| 36 |
+
|
| 37 |
+
def _attention_weights(self, pair_states: Tensor, mask: Tensor) -> Tensor:
|
| 38 |
+
# pair_states: (b, l, l, d_z); mask: (b, l, l).
|
| 39 |
+
logits = self.proj_z(pair_states).permute(0, 3, 1, 2) # (b, h, l, l)
|
| 40 |
+
logits = logits + (1 - mask[:, None]) * -self.inf # (b, h, l, l)
|
| 41 |
+
return torch.softmax(logits, dim=-1) # (b, h, l, l)
|
| 42 |
+
|
| 43 |
+
def _all_heads(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor) -> Tensor:
|
| 44 |
+
# msa_states: (b, s, l, d_m); pair_states: (b, l, l, d_z).
|
| 45 |
+
values = self.proj_m(msa_states).reshape(
|
| 46 |
+
*msa_states.shape[:3],
|
| 47 |
+
self.num_heads,
|
| 48 |
+
self.c_h,
|
| 49 |
+
) # (b, s, l, h, d_h)
|
| 50 |
+
values = values.permute(0, 3, 1, 2, 4) # (b, h, s, l, d_h)
|
| 51 |
+
weights = self._attention_weights(pair_states, mask) # (b, h, l, l)
|
| 52 |
+
gate = self.proj_g(msa_states).sigmoid() # (b, s, l, h * d_h)
|
| 53 |
+
attended = torch.einsum(
|
| 54 |
+
"bhij,bhsjd->bhsid", weights, values
|
| 55 |
+
) # (b, h, s, l, d_h)
|
| 56 |
+
attended = attended.permute(0, 2, 3, 1, 4) # (b, s, l, h, d_h)
|
| 57 |
+
attended = attended.reshape(
|
| 58 |
+
*attended.shape[:3], self.num_heads * self.c_h
|
| 59 |
+
) # (b, s, l, h * d_h)
|
| 60 |
+
return self.proj_o(gate * attended) # (b, s, l, d_m)
|
| 61 |
+
|
| 62 |
+
def _one_head(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor, head: int) -> Tensor:
|
| 63 |
+
# msa_states: (b, s, l, d_m); pair_states: (b, l, l, d_z).
|
| 64 |
+
start = head * self.c_h
|
| 65 |
+
stop = start + self.c_h
|
| 66 |
+
value = msa_states @ self.proj_m.weight[start:stop].T # (b, s, l, d_h)
|
| 67 |
+
value = value.reshape(*value.shape[:3], 1, self.c_h).permute(
|
| 68 |
+
0, 3, 1, 2, 4
|
| 69 |
+
) # (b, 1, s, l, d_h)
|
| 70 |
+
logits = pair_states @ self.proj_z.weight[head : head + 1].T # (b, l, l, 1)
|
| 71 |
+
logits = logits.permute(0, 3, 1, 2) # (b, 1, l, l)
|
| 72 |
+
weights = torch.softmax(
|
| 73 |
+
logits + (1 - mask[:, None]) * -self.inf, dim=-1
|
| 74 |
+
) # (b, 1, l, l)
|
| 75 |
+
gate = (
|
| 76 |
+
msa_states @ self.proj_g.weight[start:stop].T
|
| 77 |
+
).sigmoid() # (b, s, l, d_h)
|
| 78 |
+
attended = torch.einsum(
|
| 79 |
+
"bhij,bhsjd->bhsid", weights, value
|
| 80 |
+
) # (b, 1, s, l, d_h)
|
| 81 |
+
attended = attended.permute(0, 2, 3, 1, 4).reshape(
|
| 82 |
+
*msa_states.shape[:3],
|
| 83 |
+
self.c_h,
|
| 84 |
+
) # (b, s, l, d_h)
|
| 85 |
+
return (gate * attended) @ self.proj_o.weight[:, start:stop].T # (b, s, l, d_m)
|
| 86 |
+
|
| 87 |
+
def forward(
|
| 88 |
+
self,
|
| 89 |
+
m: Tensor,
|
| 90 |
+
z: Tensor,
|
| 91 |
+
mask: Tensor,
|
| 92 |
+
chunk_heads: bool = True,
|
| 93 |
+
) -> Tensor:
|
| 94 |
+
"""Return updated M with shape ``(b, s, l, d_m)``."""
|
| 95 |
+
|
| 96 |
+
# m: (b, s, l, d_m); z: (b, l, l, d_z); mask: (b, l, l).
|
| 97 |
+
msa_states = self.norm_m(m) # (b, s, l, d_m)
|
| 98 |
+
pair_states = self.norm_z(z) # (b, l, l, d_z)
|
| 99 |
+
if not chunk_heads or self.training:
|
| 100 |
+
return self._all_heads(msa_states, pair_states, mask)
|
| 101 |
+
|
| 102 |
+
output: Tensor | None = None
|
| 103 |
+
for head in range(self.num_heads):
|
| 104 |
+
contribution = self._one_head(
|
| 105 |
+
msa_states, pair_states, mask, head
|
| 106 |
+
) # (b, s, l, d_m)
|
| 107 |
+
output = (
|
| 108 |
+
contribution if output is None else output + contribution
|
| 109 |
+
) # (b, s, l, d_m)
|
| 110 |
+
if output is None:
|
| 111 |
+
raise ValueError("num_heads must be positive")
|
| 112 |
+
return output # (b, s, l, d_m)
|
fastplms/models/boltz/vb_layers_pairformer.py
CHANGED
|
@@ -1,350 +1,350 @@
|
|
| 1 |
-
"""Pairformer layers for joint or pair-only Boltz2 representations."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from typing import Any, cast
|
| 8 |
-
from torch import Tensor, nn
|
| 9 |
-
from torch.utils.checkpoint import checkpoint
|
| 10 |
-
|
| 11 |
-
from . import vb_const as const
|
| 12 |
-
from .vb_layers_attention import AttentionPairBias
|
| 13 |
-
from .vb_layers_attentionv2 import AttentionPairBias as AttentionPairBiasV2
|
| 14 |
-
from .vb_layers_dropout import get_dropout_mask
|
| 15 |
-
from .vb_layers_transition import Transition
|
| 16 |
-
from .vb_layers_triangular_mult import (
|
| 17 |
-
TriangleMultiplicationIncoming,
|
| 18 |
-
TriangleMultiplicationOutgoing,
|
| 19 |
-
)
|
| 20 |
-
from .vb_tri_attn_attention import (
|
| 21 |
-
TriangleAttentionEndingNode,
|
| 22 |
-
TriangleAttentionStartingNode,
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def _pair_update(
|
| 27 |
-
modules: Any,
|
| 28 |
-
pair_states: Tensor,
|
| 29 |
-
pair_mask: Tensor,
|
| 30 |
-
chunk_size: int | None,
|
| 31 |
-
use_kernels: bool,
|
| 32 |
-
use_cuequiv_mul: bool,
|
| 33 |
-
use_cuequiv_attn: bool,
|
| 34 |
-
) -> Tensor:
|
| 35 |
-
"""Apply multiplicative, attention, and transition updates to pair tensor Z."""
|
| 36 |
-
|
| 37 |
-
# pair_states: (b, l, l, d_z); pair_mask: (b, l, l).
|
| 38 |
-
multiplication_kernel = use_kernels or use_cuequiv_mul
|
| 39 |
-
attention_kernel = use_kernels or use_cuequiv_attn
|
| 40 |
-
output = pair_states # (b, l, l, d_z)
|
| 41 |
-
for update in (modules.tri_mul_out, modules.tri_mul_in):
|
| 42 |
-
dropout = get_dropout_mask(
|
| 43 |
-
modules.dropout, output, modules.training
|
| 44 |
-
) # broadcastable to (b, l, l, d_z)
|
| 45 |
-
output = output + dropout * update(
|
| 46 |
-
output,
|
| 47 |
-
mask=pair_mask,
|
| 48 |
-
use_kernels=multiplication_kernel,
|
| 49 |
-
) # (b, l, l, d_z)
|
| 50 |
-
|
| 51 |
-
dropout = get_dropout_mask(
|
| 52 |
-
modules.dropout, output, modules.training
|
| 53 |
-
) # broadcastable to (b, l, l, d_z)
|
| 54 |
-
output = output + dropout * modules.tri_att_start(
|
| 55 |
-
output,
|
| 56 |
-
mask=pair_mask,
|
| 57 |
-
chunk_size=chunk_size,
|
| 58 |
-
use_kernels=attention_kernel,
|
| 59 |
-
) # (b, l, l, d_z)
|
| 60 |
-
dropout = get_dropout_mask(
|
| 61 |
-
modules.dropout,
|
| 62 |
-
output,
|
| 63 |
-
modules.training,
|
| 64 |
-
columnwise=True,
|
| 65 |
-
) # broadcastable to (b, l, l, d_z)
|
| 66 |
-
output = output + dropout * modules.tri_att_end(
|
| 67 |
-
output,
|
| 68 |
-
mask=pair_mask,
|
| 69 |
-
chunk_size=chunk_size,
|
| 70 |
-
use_kernels=attention_kernel,
|
| 71 |
-
) # (b, l, l, d_z)
|
| 72 |
-
return cast(Tensor, output + modules.transition_z(output)) # (b, l, l, d_z)
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def _triangle_chunk_size(pair_states: Tensor, training: bool) -> int | None:
|
| 76 |
-
if training:
|
| 77 |
-
return None
|
| 78 |
-
return 128 if pair_states.shape[1] > const.chunk_size_threshold else 512
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
class PairformerLayer(nn.Module):
|
| 82 |
-
"""Update sequence tensor S and pair tensor Z once."""
|
| 83 |
-
|
| 84 |
-
def __init__(
|
| 85 |
-
self,
|
| 86 |
-
token_s: int,
|
| 87 |
-
token_z: int,
|
| 88 |
-
num_heads: int = 16,
|
| 89 |
-
dropout: float = 0.25,
|
| 90 |
-
pairwise_head_width: int = 32,
|
| 91 |
-
pairwise_num_heads: int = 4,
|
| 92 |
-
post_layer_norm: bool = False,
|
| 93 |
-
v2: bool = False,
|
| 94 |
-
) -> None:
|
| 95 |
-
super().__init__()
|
| 96 |
-
self.token_z = token_z
|
| 97 |
-
self.dropout = dropout
|
| 98 |
-
self.num_heads = num_heads
|
| 99 |
-
self.post_layer_norm = post_layer_norm
|
| 100 |
-
self.pre_norm_s = nn.LayerNorm(token_s)
|
| 101 |
-
attention_class = AttentionPairBiasV2 if v2 else AttentionPairBias
|
| 102 |
-
self.attention = attention_class(token_s, token_z, num_heads)
|
| 103 |
-
self.tri_mul_out = TriangleMultiplicationOutgoing(token_z)
|
| 104 |
-
self.tri_mul_in = TriangleMultiplicationIncoming(token_z)
|
| 105 |
-
self.tri_att_start = TriangleAttentionStartingNode(
|
| 106 |
-
token_z,
|
| 107 |
-
pairwise_head_width,
|
| 108 |
-
pairwise_num_heads,
|
| 109 |
-
inf=1e9,
|
| 110 |
-
)
|
| 111 |
-
self.tri_att_end = TriangleAttentionEndingNode(
|
| 112 |
-
token_z,
|
| 113 |
-
pairwise_head_width,
|
| 114 |
-
pairwise_num_heads,
|
| 115 |
-
inf=1e9,
|
| 116 |
-
)
|
| 117 |
-
self.transition_s = Transition(token_s, token_s * 4)
|
| 118 |
-
self.transition_z = Transition(token_z, token_z * 4)
|
| 119 |
-
self.s_post_norm = nn.LayerNorm(token_s) if post_layer_norm else nn.Identity()
|
| 120 |
-
|
| 121 |
-
def forward(
|
| 122 |
-
self,
|
| 123 |
-
s: Tensor,
|
| 124 |
-
z: Tensor,
|
| 125 |
-
mask: Tensor,
|
| 126 |
-
pair_mask: Tensor,
|
| 127 |
-
chunk_size_tri_attn: int | None = None,
|
| 128 |
-
use_kernels: bool = False,
|
| 129 |
-
use_cuequiv_mul: bool = False,
|
| 130 |
-
use_cuequiv_attn: bool = False,
|
| 131 |
-
) -> tuple[Tensor, Tensor]:
|
| 132 |
-
"""Return updated ``S: (b, l, d_s)`` and ``Z: (b, l, l, d_z)``."""
|
| 133 |
-
|
| 134 |
-
pair_output = _pair_update(
|
| 135 |
-
self,
|
| 136 |
-
z,
|
| 137 |
-
pair_mask,
|
| 138 |
-
chunk_size_tri_attn,
|
| 139 |
-
use_kernels,
|
| 140 |
-
use_cuequiv_mul,
|
| 141 |
-
use_cuequiv_attn,
|
| 142 |
-
) # (b, l, l, d_z)
|
| 143 |
-
with torch.autocast("cuda", enabled=False):
|
| 144 |
-
normalized = self.pre_norm_s(s.float()) # (b, l, d_s)
|
| 145 |
-
sequence_output = s.float() + self.attention(
|
| 146 |
-
s=normalized,
|
| 147 |
-
z=pair_output.float(),
|
| 148 |
-
mask=mask.float(),
|
| 149 |
-
k_in=normalized,
|
| 150 |
-
) # (b, l, d_s)
|
| 151 |
-
sequence_output = (
|
| 152 |
-
sequence_output + self.transition_s(sequence_output)
|
| 153 |
-
) # (b, l, d_s)
|
| 154 |
-
sequence_output = cast(
|
| 155 |
-
Tensor, self.s_post_norm(sequence_output)
|
| 156 |
-
) # (b, l, d_s)
|
| 157 |
-
return sequence_output, pair_output # (b, l, d_s), (b, l, l, d_z)
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
class PairformerModule(nn.Module):
|
| 161 |
-
"""Run a stack of joint sequence and pair updates."""
|
| 162 |
-
|
| 163 |
-
def __init__(
|
| 164 |
-
self,
|
| 165 |
-
token_s: int,
|
| 166 |
-
token_z: int,
|
| 167 |
-
num_blocks: int,
|
| 168 |
-
num_heads: int = 16,
|
| 169 |
-
dropout: float = 0.25,
|
| 170 |
-
pairwise_head_width: int = 32,
|
| 171 |
-
pairwise_num_heads: int = 4,
|
| 172 |
-
post_layer_norm: bool = False,
|
| 173 |
-
activation_checkpointing: bool = False,
|
| 174 |
-
v2: bool = False,
|
| 175 |
-
**kwargs: Any,
|
| 176 |
-
) -> None:
|
| 177 |
-
del kwargs
|
| 178 |
-
super().__init__()
|
| 179 |
-
self.token_z = token_z
|
| 180 |
-
self.num_blocks = num_blocks
|
| 181 |
-
self.dropout = dropout
|
| 182 |
-
self.num_heads = num_heads
|
| 183 |
-
self.post_layer_norm = post_layer_norm
|
| 184 |
-
self.activation_checkpointing = activation_checkpointing
|
| 185 |
-
self.layers = nn.ModuleList(
|
| 186 |
-
[
|
| 187 |
-
PairformerLayer(
|
| 188 |
-
token_s,
|
| 189 |
-
token_z,
|
| 190 |
-
num_heads,
|
| 191 |
-
dropout,
|
| 192 |
-
pairwise_head_width,
|
| 193 |
-
pairwise_num_heads,
|
| 194 |
-
post_layer_norm,
|
| 195 |
-
v2,
|
| 196 |
-
)
|
| 197 |
-
for _ in range(num_blocks)
|
| 198 |
-
]
|
| 199 |
-
)
|
| 200 |
-
|
| 201 |
-
def forward(
|
| 202 |
-
self,
|
| 203 |
-
s: Tensor,
|
| 204 |
-
z: Tensor,
|
| 205 |
-
mask: Tensor,
|
| 206 |
-
pair_mask: Tensor,
|
| 207 |
-
use_kernels: bool = False,
|
| 208 |
-
) -> tuple[Tensor, Tensor]:
|
| 209 |
-
"""Return S and Z after every configured pairformer block."""
|
| 210 |
-
|
| 211 |
-
# s: (b, l, d_s); z: (b, l, l, d_z).
|
| 212 |
-
chunk_size = _triangle_chunk_size(z, self.training)
|
| 213 |
-
sequence_output, pair_output = s, z # (b, l, d_s), (b, l, l, d_z)
|
| 214 |
-
for layer in self.layers:
|
| 215 |
-
if self.activation_checkpointing:
|
| 216 |
-
sequence_output, pair_output = checkpoint(
|
| 217 |
-
layer,
|
| 218 |
-
sequence_output,
|
| 219 |
-
pair_output,
|
| 220 |
-
mask,
|
| 221 |
-
pair_mask,
|
| 222 |
-
chunk_size,
|
| 223 |
-
use_kernels,
|
| 224 |
-
use_reentrant=False,
|
| 225 |
-
) # (b, l, d_s), (b, l, l, d_z)
|
| 226 |
-
else:
|
| 227 |
-
sequence_output, pair_output = layer(
|
| 228 |
-
sequence_output,
|
| 229 |
-
pair_output,
|
| 230 |
-
mask,
|
| 231 |
-
pair_mask,
|
| 232 |
-
chunk_size,
|
| 233 |
-
use_kernels,
|
| 234 |
-
) # (b, l, d_s), (b, l, l, d_z)
|
| 235 |
-
return sequence_output, pair_output # (b, l, d_s), (b, l, l, d_z)
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
class PairformerNoSeqLayer(nn.Module):
|
| 239 |
-
"""Update pair tensor Z without a sequence track."""
|
| 240 |
-
|
| 241 |
-
def __init__(
|
| 242 |
-
self,
|
| 243 |
-
token_z: int,
|
| 244 |
-
dropout: float = 0.25,
|
| 245 |
-
pairwise_head_width: int = 32,
|
| 246 |
-
pairwise_num_heads: int = 4,
|
| 247 |
-
post_layer_norm: bool = False,
|
| 248 |
-
) -> None:
|
| 249 |
-
super().__init__()
|
| 250 |
-
self.token_z = token_z
|
| 251 |
-
self.dropout = dropout
|
| 252 |
-
self.post_layer_norm = post_layer_norm
|
| 253 |
-
self.tri_mul_out = TriangleMultiplicationOutgoing(token_z)
|
| 254 |
-
self.tri_mul_in = TriangleMultiplicationIncoming(token_z)
|
| 255 |
-
self.tri_att_start = TriangleAttentionStartingNode(
|
| 256 |
-
token_z,
|
| 257 |
-
pairwise_head_width,
|
| 258 |
-
pairwise_num_heads,
|
| 259 |
-
inf=1e9,
|
| 260 |
-
)
|
| 261 |
-
self.tri_att_end = TriangleAttentionEndingNode(
|
| 262 |
-
token_z,
|
| 263 |
-
pairwise_head_width,
|
| 264 |
-
pairwise_num_heads,
|
| 265 |
-
inf=1e9,
|
| 266 |
-
)
|
| 267 |
-
self.transition_z = Transition(token_z, token_z * 4)
|
| 268 |
-
|
| 269 |
-
def forward(
|
| 270 |
-
self,
|
| 271 |
-
z: Tensor,
|
| 272 |
-
pair_mask: Tensor,
|
| 273 |
-
chunk_size_tri_attn: int | None = None,
|
| 274 |
-
use_kernels: bool = False,
|
| 275 |
-
use_cuequiv_mul: bool = False,
|
| 276 |
-
use_cuequiv_attn: bool = False,
|
| 277 |
-
) -> Tensor:
|
| 278 |
-
"""Return updated pair tensor Z with shape ``(b, l, l, d_z)``."""
|
| 279 |
-
|
| 280 |
-
return _pair_update(
|
| 281 |
-
self,
|
| 282 |
-
z,
|
| 283 |
-
pair_mask,
|
| 284 |
-
chunk_size_tri_attn,
|
| 285 |
-
use_kernels,
|
| 286 |
-
use_cuequiv_mul,
|
| 287 |
-
use_cuequiv_attn,
|
| 288 |
-
)
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
class PairformerNoSeqModule(nn.Module):
|
| 292 |
-
"""Run a stack of pair-only updates."""
|
| 293 |
-
|
| 294 |
-
def __init__(
|
| 295 |
-
self,
|
| 296 |
-
token_z: int,
|
| 297 |
-
num_blocks: int,
|
| 298 |
-
dropout: float = 0.25,
|
| 299 |
-
pairwise_head_width: int = 32,
|
| 300 |
-
pairwise_num_heads: int = 4,
|
| 301 |
-
post_layer_norm: bool = False,
|
| 302 |
-
activation_checkpointing: bool = False,
|
| 303 |
-
**kwargs: Any,
|
| 304 |
-
) -> None:
|
| 305 |
-
del kwargs
|
| 306 |
-
super().__init__()
|
| 307 |
-
self.token_z = token_z
|
| 308 |
-
self.num_blocks = num_blocks
|
| 309 |
-
self.dropout = dropout
|
| 310 |
-
self.post_layer_norm = post_layer_norm
|
| 311 |
-
self.activation_checkpointing = activation_checkpointing
|
| 312 |
-
self.layers = nn.ModuleList(
|
| 313 |
-
[
|
| 314 |
-
PairformerNoSeqLayer(
|
| 315 |
-
token_z,
|
| 316 |
-
dropout,
|
| 317 |
-
pairwise_head_width,
|
| 318 |
-
pairwise_num_heads,
|
| 319 |
-
post_layer_norm,
|
| 320 |
-
)
|
| 321 |
-
for _ in range(num_blocks)
|
| 322 |
-
]
|
| 323 |
-
)
|
| 324 |
-
|
| 325 |
-
def forward(
|
| 326 |
-
self,
|
| 327 |
-
z: Tensor,
|
| 328 |
-
pair_mask: Tensor,
|
| 329 |
-
use_kernels: bool = False,
|
| 330 |
-
) -> Tensor:
|
| 331 |
-
"""Return Z after every configured pair-only block."""
|
| 332 |
-
|
| 333 |
-
# z: (b, l, l, d_z); pair_mask: (b, l, l).
|
| 334 |
-
chunk_size = _triangle_chunk_size(z, self.training)
|
| 335 |
-
output = z # (b, l, l, d_z)
|
| 336 |
-
for layer in self.layers:
|
| 337 |
-
if self.activation_checkpointing:
|
| 338 |
-
output = checkpoint(
|
| 339 |
-
layer,
|
| 340 |
-
output,
|
| 341 |
-
pair_mask,
|
| 342 |
-
chunk_size,
|
| 343 |
-
use_kernels,
|
| 344 |
-
use_reentrant=False,
|
| 345 |
-
) # (b, l, l, d_z)
|
| 346 |
-
else:
|
| 347 |
-
output = layer(
|
| 348 |
-
output, pair_mask, chunk_size, use_kernels
|
| 349 |
-
) # (b, l, l, d_z)
|
| 350 |
-
return output # (b, l, l, d_z)
|
|
|
|
| 1 |
+
"""Pairformer layers for joint or pair-only Boltz2 representations."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from typing import Any, cast
|
| 8 |
+
from torch import Tensor, nn
|
| 9 |
+
from torch.utils.checkpoint import checkpoint
|
| 10 |
+
|
| 11 |
+
from . import vb_const as const
|
| 12 |
+
from .vb_layers_attention import AttentionPairBias
|
| 13 |
+
from .vb_layers_attentionv2 import AttentionPairBias as AttentionPairBiasV2
|
| 14 |
+
from .vb_layers_dropout import get_dropout_mask
|
| 15 |
+
from .vb_layers_transition import Transition
|
| 16 |
+
from .vb_layers_triangular_mult import (
|
| 17 |
+
TriangleMultiplicationIncoming,
|
| 18 |
+
TriangleMultiplicationOutgoing,
|
| 19 |
+
)
|
| 20 |
+
from .vb_tri_attn_attention import (
|
| 21 |
+
TriangleAttentionEndingNode,
|
| 22 |
+
TriangleAttentionStartingNode,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _pair_update(
|
| 27 |
+
modules: Any,
|
| 28 |
+
pair_states: Tensor,
|
| 29 |
+
pair_mask: Tensor,
|
| 30 |
+
chunk_size: int | None,
|
| 31 |
+
use_kernels: bool,
|
| 32 |
+
use_cuequiv_mul: bool,
|
| 33 |
+
use_cuequiv_attn: bool,
|
| 34 |
+
) -> Tensor:
|
| 35 |
+
"""Apply multiplicative, attention, and transition updates to pair tensor Z."""
|
| 36 |
+
|
| 37 |
+
# pair_states: (b, l, l, d_z); pair_mask: (b, l, l).
|
| 38 |
+
multiplication_kernel = use_kernels or use_cuequiv_mul
|
| 39 |
+
attention_kernel = use_kernels or use_cuequiv_attn
|
| 40 |
+
output = pair_states # (b, l, l, d_z)
|
| 41 |
+
for update in (modules.tri_mul_out, modules.tri_mul_in):
|
| 42 |
+
dropout = get_dropout_mask(
|
| 43 |
+
modules.dropout, output, modules.training
|
| 44 |
+
) # broadcastable to (b, l, l, d_z)
|
| 45 |
+
output = output + dropout * update(
|
| 46 |
+
output,
|
| 47 |
+
mask=pair_mask,
|
| 48 |
+
use_kernels=multiplication_kernel,
|
| 49 |
+
) # (b, l, l, d_z)
|
| 50 |
+
|
| 51 |
+
dropout = get_dropout_mask(
|
| 52 |
+
modules.dropout, output, modules.training
|
| 53 |
+
) # broadcastable to (b, l, l, d_z)
|
| 54 |
+
output = output + dropout * modules.tri_att_start(
|
| 55 |
+
output,
|
| 56 |
+
mask=pair_mask,
|
| 57 |
+
chunk_size=chunk_size,
|
| 58 |
+
use_kernels=attention_kernel,
|
| 59 |
+
) # (b, l, l, d_z)
|
| 60 |
+
dropout = get_dropout_mask(
|
| 61 |
+
modules.dropout,
|
| 62 |
+
output,
|
| 63 |
+
modules.training,
|
| 64 |
+
columnwise=True,
|
| 65 |
+
) # broadcastable to (b, l, l, d_z)
|
| 66 |
+
output = output + dropout * modules.tri_att_end(
|
| 67 |
+
output,
|
| 68 |
+
mask=pair_mask,
|
| 69 |
+
chunk_size=chunk_size,
|
| 70 |
+
use_kernels=attention_kernel,
|
| 71 |
+
) # (b, l, l, d_z)
|
| 72 |
+
return cast(Tensor, output + modules.transition_z(output)) # (b, l, l, d_z)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _triangle_chunk_size(pair_states: Tensor, training: bool) -> int | None:
|
| 76 |
+
if training:
|
| 77 |
+
return None
|
| 78 |
+
return 128 if pair_states.shape[1] > const.chunk_size_threshold else 512
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class PairformerLayer(nn.Module):
|
| 82 |
+
"""Update sequence tensor S and pair tensor Z once."""
|
| 83 |
+
|
| 84 |
+
def __init__(
|
| 85 |
+
self,
|
| 86 |
+
token_s: int,
|
| 87 |
+
token_z: int,
|
| 88 |
+
num_heads: int = 16,
|
| 89 |
+
dropout: float = 0.25,
|
| 90 |
+
pairwise_head_width: int = 32,
|
| 91 |
+
pairwise_num_heads: int = 4,
|
| 92 |
+
post_layer_norm: bool = False,
|
| 93 |
+
v2: bool = False,
|
| 94 |
+
) -> None:
|
| 95 |
+
super().__init__()
|
| 96 |
+
self.token_z = token_z
|
| 97 |
+
self.dropout = dropout
|
| 98 |
+
self.num_heads = num_heads
|
| 99 |
+
self.post_layer_norm = post_layer_norm
|
| 100 |
+
self.pre_norm_s = nn.LayerNorm(token_s)
|
| 101 |
+
attention_class = AttentionPairBiasV2 if v2 else AttentionPairBias
|
| 102 |
+
self.attention = attention_class(token_s, token_z, num_heads)
|
| 103 |
+
self.tri_mul_out = TriangleMultiplicationOutgoing(token_z)
|
| 104 |
+
self.tri_mul_in = TriangleMultiplicationIncoming(token_z)
|
| 105 |
+
self.tri_att_start = TriangleAttentionStartingNode(
|
| 106 |
+
token_z,
|
| 107 |
+
pairwise_head_width,
|
| 108 |
+
pairwise_num_heads,
|
| 109 |
+
inf=1e9,
|
| 110 |
+
)
|
| 111 |
+
self.tri_att_end = TriangleAttentionEndingNode(
|
| 112 |
+
token_z,
|
| 113 |
+
pairwise_head_width,
|
| 114 |
+
pairwise_num_heads,
|
| 115 |
+
inf=1e9,
|
| 116 |
+
)
|
| 117 |
+
self.transition_s = Transition(token_s, token_s * 4)
|
| 118 |
+
self.transition_z = Transition(token_z, token_z * 4)
|
| 119 |
+
self.s_post_norm = nn.LayerNorm(token_s) if post_layer_norm else nn.Identity()
|
| 120 |
+
|
| 121 |
+
def forward(
|
| 122 |
+
self,
|
| 123 |
+
s: Tensor,
|
| 124 |
+
z: Tensor,
|
| 125 |
+
mask: Tensor,
|
| 126 |
+
pair_mask: Tensor,
|
| 127 |
+
chunk_size_tri_attn: int | None = None,
|
| 128 |
+
use_kernels: bool = False,
|
| 129 |
+
use_cuequiv_mul: bool = False,
|
| 130 |
+
use_cuequiv_attn: bool = False,
|
| 131 |
+
) -> tuple[Tensor, Tensor]:
|
| 132 |
+
"""Return updated ``S: (b, l, d_s)`` and ``Z: (b, l, l, d_z)``."""
|
| 133 |
+
|
| 134 |
+
pair_output = _pair_update(
|
| 135 |
+
self,
|
| 136 |
+
z,
|
| 137 |
+
pair_mask,
|
| 138 |
+
chunk_size_tri_attn,
|
| 139 |
+
use_kernels,
|
| 140 |
+
use_cuequiv_mul,
|
| 141 |
+
use_cuequiv_attn,
|
| 142 |
+
) # (b, l, l, d_z)
|
| 143 |
+
with torch.autocast("cuda", enabled=False):
|
| 144 |
+
normalized = self.pre_norm_s(s.float()) # (b, l, d_s)
|
| 145 |
+
sequence_output = s.float() + self.attention(
|
| 146 |
+
s=normalized,
|
| 147 |
+
z=pair_output.float(),
|
| 148 |
+
mask=mask.float(),
|
| 149 |
+
k_in=normalized,
|
| 150 |
+
) # (b, l, d_s)
|
| 151 |
+
sequence_output = (
|
| 152 |
+
sequence_output + self.transition_s(sequence_output)
|
| 153 |
+
) # (b, l, d_s)
|
| 154 |
+
sequence_output = cast(
|
| 155 |
+
Tensor, self.s_post_norm(sequence_output)
|
| 156 |
+
) # (b, l, d_s)
|
| 157 |
+
return sequence_output, pair_output # (b, l, d_s), (b, l, l, d_z)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class PairformerModule(nn.Module):
|
| 161 |
+
"""Run a stack of joint sequence and pair updates."""
|
| 162 |
+
|
| 163 |
+
def __init__(
|
| 164 |
+
self,
|
| 165 |
+
token_s: int,
|
| 166 |
+
token_z: int,
|
| 167 |
+
num_blocks: int,
|
| 168 |
+
num_heads: int = 16,
|
| 169 |
+
dropout: float = 0.25,
|
| 170 |
+
pairwise_head_width: int = 32,
|
| 171 |
+
pairwise_num_heads: int = 4,
|
| 172 |
+
post_layer_norm: bool = False,
|
| 173 |
+
activation_checkpointing: bool = False,
|
| 174 |
+
v2: bool = False,
|
| 175 |
+
**kwargs: Any,
|
| 176 |
+
) -> None:
|
| 177 |
+
del kwargs
|
| 178 |
+
super().__init__()
|
| 179 |
+
self.token_z = token_z
|
| 180 |
+
self.num_blocks = num_blocks
|
| 181 |
+
self.dropout = dropout
|
| 182 |
+
self.num_heads = num_heads
|
| 183 |
+
self.post_layer_norm = post_layer_norm
|
| 184 |
+
self.activation_checkpointing = activation_checkpointing
|
| 185 |
+
self.layers = nn.ModuleList(
|
| 186 |
+
[
|
| 187 |
+
PairformerLayer(
|
| 188 |
+
token_s,
|
| 189 |
+
token_z,
|
| 190 |
+
num_heads,
|
| 191 |
+
dropout,
|
| 192 |
+
pairwise_head_width,
|
| 193 |
+
pairwise_num_heads,
|
| 194 |
+
post_layer_norm,
|
| 195 |
+
v2,
|
| 196 |
+
)
|
| 197 |
+
for _ in range(num_blocks)
|
| 198 |
+
]
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
def forward(
|
| 202 |
+
self,
|
| 203 |
+
s: Tensor,
|
| 204 |
+
z: Tensor,
|
| 205 |
+
mask: Tensor,
|
| 206 |
+
pair_mask: Tensor,
|
| 207 |
+
use_kernels: bool = False,
|
| 208 |
+
) -> tuple[Tensor, Tensor]:
|
| 209 |
+
"""Return S and Z after every configured pairformer block."""
|
| 210 |
+
|
| 211 |
+
# s: (b, l, d_s); z: (b, l, l, d_z).
|
| 212 |
+
chunk_size = _triangle_chunk_size(z, self.training)
|
| 213 |
+
sequence_output, pair_output = s, z # (b, l, d_s), (b, l, l, d_z)
|
| 214 |
+
for layer in self.layers:
|
| 215 |
+
if self.activation_checkpointing:
|
| 216 |
+
sequence_output, pair_output = checkpoint(
|
| 217 |
+
layer,
|
| 218 |
+
sequence_output,
|
| 219 |
+
pair_output,
|
| 220 |
+
mask,
|
| 221 |
+
pair_mask,
|
| 222 |
+
chunk_size,
|
| 223 |
+
use_kernels,
|
| 224 |
+
use_reentrant=False,
|
| 225 |
+
) # (b, l, d_s), (b, l, l, d_z)
|
| 226 |
+
else:
|
| 227 |
+
sequence_output, pair_output = layer(
|
| 228 |
+
sequence_output,
|
| 229 |
+
pair_output,
|
| 230 |
+
mask,
|
| 231 |
+
pair_mask,
|
| 232 |
+
chunk_size,
|
| 233 |
+
use_kernels,
|
| 234 |
+
) # (b, l, d_s), (b, l, l, d_z)
|
| 235 |
+
return sequence_output, pair_output # (b, l, d_s), (b, l, l, d_z)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
class PairformerNoSeqLayer(nn.Module):
|
| 239 |
+
"""Update pair tensor Z without a sequence track."""
|
| 240 |
+
|
| 241 |
+
def __init__(
|
| 242 |
+
self,
|
| 243 |
+
token_z: int,
|
| 244 |
+
dropout: float = 0.25,
|
| 245 |
+
pairwise_head_width: int = 32,
|
| 246 |
+
pairwise_num_heads: int = 4,
|
| 247 |
+
post_layer_norm: bool = False,
|
| 248 |
+
) -> None:
|
| 249 |
+
super().__init__()
|
| 250 |
+
self.token_z = token_z
|
| 251 |
+
self.dropout = dropout
|
| 252 |
+
self.post_layer_norm = post_layer_norm
|
| 253 |
+
self.tri_mul_out = TriangleMultiplicationOutgoing(token_z)
|
| 254 |
+
self.tri_mul_in = TriangleMultiplicationIncoming(token_z)
|
| 255 |
+
self.tri_att_start = TriangleAttentionStartingNode(
|
| 256 |
+
token_z,
|
| 257 |
+
pairwise_head_width,
|
| 258 |
+
pairwise_num_heads,
|
| 259 |
+
inf=1e9,
|
| 260 |
+
)
|
| 261 |
+
self.tri_att_end = TriangleAttentionEndingNode(
|
| 262 |
+
token_z,
|
| 263 |
+
pairwise_head_width,
|
| 264 |
+
pairwise_num_heads,
|
| 265 |
+
inf=1e9,
|
| 266 |
+
)
|
| 267 |
+
self.transition_z = Transition(token_z, token_z * 4)
|
| 268 |
+
|
| 269 |
+
def forward(
|
| 270 |
+
self,
|
| 271 |
+
z: Tensor,
|
| 272 |
+
pair_mask: Tensor,
|
| 273 |
+
chunk_size_tri_attn: int | None = None,
|
| 274 |
+
use_kernels: bool = False,
|
| 275 |
+
use_cuequiv_mul: bool = False,
|
| 276 |
+
use_cuequiv_attn: bool = False,
|
| 277 |
+
) -> Tensor:
|
| 278 |
+
"""Return updated pair tensor Z with shape ``(b, l, l, d_z)``."""
|
| 279 |
+
|
| 280 |
+
return _pair_update(
|
| 281 |
+
self,
|
| 282 |
+
z,
|
| 283 |
+
pair_mask,
|
| 284 |
+
chunk_size_tri_attn,
|
| 285 |
+
use_kernels,
|
| 286 |
+
use_cuequiv_mul,
|
| 287 |
+
use_cuequiv_attn,
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
class PairformerNoSeqModule(nn.Module):
|
| 292 |
+
"""Run a stack of pair-only updates."""
|
| 293 |
+
|
| 294 |
+
def __init__(
|
| 295 |
+
self,
|
| 296 |
+
token_z: int,
|
| 297 |
+
num_blocks: int,
|
| 298 |
+
dropout: float = 0.25,
|
| 299 |
+
pairwise_head_width: int = 32,
|
| 300 |
+
pairwise_num_heads: int = 4,
|
| 301 |
+
post_layer_norm: bool = False,
|
| 302 |
+
activation_checkpointing: bool = False,
|
| 303 |
+
**kwargs: Any,
|
| 304 |
+
) -> None:
|
| 305 |
+
del kwargs
|
| 306 |
+
super().__init__()
|
| 307 |
+
self.token_z = token_z
|
| 308 |
+
self.num_blocks = num_blocks
|
| 309 |
+
self.dropout = dropout
|
| 310 |
+
self.post_layer_norm = post_layer_norm
|
| 311 |
+
self.activation_checkpointing = activation_checkpointing
|
| 312 |
+
self.layers = nn.ModuleList(
|
| 313 |
+
[
|
| 314 |
+
PairformerNoSeqLayer(
|
| 315 |
+
token_z,
|
| 316 |
+
dropout,
|
| 317 |
+
pairwise_head_width,
|
| 318 |
+
pairwise_num_heads,
|
| 319 |
+
post_layer_norm,
|
| 320 |
+
)
|
| 321 |
+
for _ in range(num_blocks)
|
| 322 |
+
]
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
def forward(
|
| 326 |
+
self,
|
| 327 |
+
z: Tensor,
|
| 328 |
+
pair_mask: Tensor,
|
| 329 |
+
use_kernels: bool = False,
|
| 330 |
+
) -> Tensor:
|
| 331 |
+
"""Return Z after every configured pair-only block."""
|
| 332 |
+
|
| 333 |
+
# z: (b, l, l, d_z); pair_mask: (b, l, l).
|
| 334 |
+
chunk_size = _triangle_chunk_size(z, self.training)
|
| 335 |
+
output = z # (b, l, l, d_z)
|
| 336 |
+
for layer in self.layers:
|
| 337 |
+
if self.activation_checkpointing:
|
| 338 |
+
output = checkpoint(
|
| 339 |
+
layer,
|
| 340 |
+
output,
|
| 341 |
+
pair_mask,
|
| 342 |
+
chunk_size,
|
| 343 |
+
use_kernels,
|
| 344 |
+
use_reentrant=False,
|
| 345 |
+
) # (b, l, l, d_z)
|
| 346 |
+
else:
|
| 347 |
+
output = layer(
|
| 348 |
+
output, pair_mask, chunk_size, use_kernels
|
| 349 |
+
) # (b, l, l, d_z)
|
| 350 |
+
return output # (b, l, l, d_z)
|
fastplms/models/boltz/vb_layers_transition.py
CHANGED
|
@@ -1,79 +1,79 @@
|
|
| 1 |
-
"""Gated feed-forward transition used by Boltz2 pair representations."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from torch import Tensor, nn
|
| 8 |
-
|
| 9 |
-
from . import vb_layers_initialize as init
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class Transition(nn.Module):
|
| 13 |
-
"""Apply a normalized SwiGLU-style transition.
|
| 14 |
-
|
| 15 |
-
The checkpoint-facing module names remain ``norm`` and ``fc1`` through
|
| 16 |
-
``fc3``. For an input tensor X with shape ``(..., d)``, the module returns
|
| 17 |
-
a tensor with shape ``(..., d_out)``.
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
def __init__(
|
| 21 |
-
self,
|
| 22 |
-
dim: int = 128,
|
| 23 |
-
hidden: int = 512,
|
| 24 |
-
out_dim: int | None = None,
|
| 25 |
-
) -> None:
|
| 26 |
-
super().__init__()
|
| 27 |
-
output_dim = dim if out_dim is None else out_dim
|
| 28 |
-
self.norm = nn.LayerNorm(dim, eps=1e-5)
|
| 29 |
-
self.fc1 = nn.Linear(dim, hidden, bias=False)
|
| 30 |
-
self.fc2 = nn.Linear(dim, hidden, bias=False)
|
| 31 |
-
self.fc3 = nn.Linear(hidden, output_dim, bias=False)
|
| 32 |
-
self.silu = nn.SiLU()
|
| 33 |
-
self.hidden = hidden
|
| 34 |
-
|
| 35 |
-
init.bias_init_one_(self.norm.weight)
|
| 36 |
-
init.bias_init_zero_(self.norm.bias)
|
| 37 |
-
init.lecun_normal_init_(self.fc1.weight)
|
| 38 |
-
init.lecun_normal_init_(self.fc2.weight)
|
| 39 |
-
init.final_init_(self.fc3.weight)
|
| 40 |
-
|
| 41 |
-
def _project_hidden_slice(self, normalized: Tensor, start: int, stop: int) -> Tensor:
|
| 42 |
-
"""Return one hidden-width contribution to the output projection."""
|
| 43 |
-
|
| 44 |
-
# normalized: (..., d); the selected hidden width is d_chunk.
|
| 45 |
-
gate = self.silu(
|
| 46 |
-
torch.matmul(normalized, self.fc1.weight[start:stop].T)
|
| 47 |
-
) # (..., d_chunk)
|
| 48 |
-
value = torch.matmul(
|
| 49 |
-
normalized, self.fc2.weight[start:stop].T
|
| 50 |
-
) # (..., d_chunk)
|
| 51 |
-
return torch.matmul(
|
| 52 |
-
gate * value, self.fc3.weight[:, start:stop].T
|
| 53 |
-
) # (..., d_out)
|
| 54 |
-
|
| 55 |
-
def forward(self, x: Tensor, chunk_size: int | None = None) -> Tensor:
|
| 56 |
-
"""Transform X, optionally accumulating the hidden dimension in chunks."""
|
| 57 |
-
|
| 58 |
-
# X is the normalized input tensor with shape (..., d).
|
| 59 |
-
normalized = self.norm(x) # (..., d)
|
| 60 |
-
if chunk_size is None or self.training:
|
| 61 |
-
# H is the gated hidden tensor with shape (..., d_hidden).
|
| 62 |
-
hidden_states = self.silu(self.fc1(normalized)) * self.fc2(normalized)
|
| 63 |
-
return self.fc3(hidden_states) # (..., d_out)
|
| 64 |
-
if chunk_size <= 0:
|
| 65 |
-
raise ValueError("chunk_size must be positive")
|
| 66 |
-
|
| 67 |
-
output: Tensor | None = None
|
| 68 |
-
for start in range(0, self.hidden, chunk_size):
|
| 69 |
-
contribution = self._project_hidden_slice(
|
| 70 |
-
normalized,
|
| 71 |
-
start,
|
| 72 |
-
min(start + chunk_size, self.hidden),
|
| 73 |
-
) # (..., d_out)
|
| 74 |
-
output = (
|
| 75 |
-
contribution if output is None else output + contribution
|
| 76 |
-
) # (..., d_out)
|
| 77 |
-
if output is None: # hidden is normally positive; keep malformed configs explicit.
|
| 78 |
-
raise ValueError("hidden must be positive")
|
| 79 |
-
return output # (..., d_out)
|
|
|
|
| 1 |
+
"""Gated feed-forward transition used by Boltz2 pair representations."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from torch import Tensor, nn
|
| 8 |
+
|
| 9 |
+
from . import vb_layers_initialize as init
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Transition(nn.Module):
|
| 13 |
+
"""Apply a normalized SwiGLU-style transition.
|
| 14 |
+
|
| 15 |
+
The checkpoint-facing module names remain ``norm`` and ``fc1`` through
|
| 16 |
+
``fc3``. For an input tensor X with shape ``(..., d)``, the module returns
|
| 17 |
+
a tensor with shape ``(..., d_out)``.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __init__(
|
| 21 |
+
self,
|
| 22 |
+
dim: int = 128,
|
| 23 |
+
hidden: int = 512,
|
| 24 |
+
out_dim: int | None = None,
|
| 25 |
+
) -> None:
|
| 26 |
+
super().__init__()
|
| 27 |
+
output_dim = dim if out_dim is None else out_dim
|
| 28 |
+
self.norm = nn.LayerNorm(dim, eps=1e-5)
|
| 29 |
+
self.fc1 = nn.Linear(dim, hidden, bias=False)
|
| 30 |
+
self.fc2 = nn.Linear(dim, hidden, bias=False)
|
| 31 |
+
self.fc3 = nn.Linear(hidden, output_dim, bias=False)
|
| 32 |
+
self.silu = nn.SiLU()
|
| 33 |
+
self.hidden = hidden
|
| 34 |
+
|
| 35 |
+
init.bias_init_one_(self.norm.weight)
|
| 36 |
+
init.bias_init_zero_(self.norm.bias)
|
| 37 |
+
init.lecun_normal_init_(self.fc1.weight)
|
| 38 |
+
init.lecun_normal_init_(self.fc2.weight)
|
| 39 |
+
init.final_init_(self.fc3.weight)
|
| 40 |
+
|
| 41 |
+
def _project_hidden_slice(self, normalized: Tensor, start: int, stop: int) -> Tensor:
|
| 42 |
+
"""Return one hidden-width contribution to the output projection."""
|
| 43 |
+
|
| 44 |
+
# normalized: (..., d); the selected hidden width is d_chunk.
|
| 45 |
+
gate = self.silu(
|
| 46 |
+
torch.matmul(normalized, self.fc1.weight[start:stop].T)
|
| 47 |
+
) # (..., d_chunk)
|
| 48 |
+
value = torch.matmul(
|
| 49 |
+
normalized, self.fc2.weight[start:stop].T
|
| 50 |
+
) # (..., d_chunk)
|
| 51 |
+
return torch.matmul(
|
| 52 |
+
gate * value, self.fc3.weight[:, start:stop].T
|
| 53 |
+
) # (..., d_out)
|
| 54 |
+
|
| 55 |
+
def forward(self, x: Tensor, chunk_size: int | None = None) -> Tensor:
|
| 56 |
+
"""Transform X, optionally accumulating the hidden dimension in chunks."""
|
| 57 |
+
|
| 58 |
+
# X is the normalized input tensor with shape (..., d).
|
| 59 |
+
normalized = self.norm(x) # (..., d)
|
| 60 |
+
if chunk_size is None or self.training:
|
| 61 |
+
# H is the gated hidden tensor with shape (..., d_hidden).
|
| 62 |
+
hidden_states = self.silu(self.fc1(normalized)) * self.fc2(normalized)
|
| 63 |
+
return self.fc3(hidden_states) # (..., d_out)
|
| 64 |
+
if chunk_size <= 0:
|
| 65 |
+
raise ValueError("chunk_size must be positive")
|
| 66 |
+
|
| 67 |
+
output: Tensor | None = None
|
| 68 |
+
for start in range(0, self.hidden, chunk_size):
|
| 69 |
+
contribution = self._project_hidden_slice(
|
| 70 |
+
normalized,
|
| 71 |
+
start,
|
| 72 |
+
min(start + chunk_size, self.hidden),
|
| 73 |
+
) # (..., d_out)
|
| 74 |
+
output = (
|
| 75 |
+
contribution if output is None else output + contribution
|
| 76 |
+
) # (..., d_out)
|
| 77 |
+
if output is None: # hidden is normally positive; keep malformed configs explicit.
|
| 78 |
+
raise ValueError("hidden must be positive")
|
| 79 |
+
return output # (..., d_out)
|
fastplms/models/boltz/vb_layers_triangular_mult.py
CHANGED
|
@@ -1,132 +1,132 @@
|
|
| 1 |
-
"""Incoming and outgoing triangular multiplicative pair updates."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import importlib
|
| 6 |
-
import torch
|
| 7 |
-
|
| 8 |
-
from importlib.util import find_spec
|
| 9 |
-
from typing import Literal
|
| 10 |
-
from torch import Tensor, nn
|
| 11 |
-
|
| 12 |
-
from . import vb_layers_initialize as init
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
TriangleDirection = Literal["incoming", "outgoing"]
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
@torch.compiler.disable
|
| 19 |
-
def kernel_triangular_mult(
|
| 20 |
-
x: Tensor,
|
| 21 |
-
direction: TriangleDirection,
|
| 22 |
-
mask: Tensor,
|
| 23 |
-
norm_in_weight: Tensor,
|
| 24 |
-
norm_in_bias: Tensor,
|
| 25 |
-
p_in_weight: Tensor,
|
| 26 |
-
g_in_weight: Tensor,
|
| 27 |
-
norm_out_weight: Tensor,
|
| 28 |
-
norm_out_bias: Tensor,
|
| 29 |
-
p_out_weight: Tensor,
|
| 30 |
-
g_out_weight: Tensor,
|
| 31 |
-
eps: float,
|
| 32 |
-
) -> Tensor:
|
| 33 |
-
"""Dispatch the optional cuEquivariance triangle primitive lazily."""
|
| 34 |
-
|
| 35 |
-
# x: (b, l, l, d); mask: (b, l, l); returned tensor: (b, l, l, d).
|
| 36 |
-
if (
|
| 37 |
-
find_spec("cuequivariance_torch") is None
|
| 38 |
-
or find_spec("cuequivariance_ops_torch") is None
|
| 39 |
-
):
|
| 40 |
-
raise RuntimeError(
|
| 41 |
-
"Boltz2 use_kernels=True requires cuequivariance_torch and the CUDA 13 "
|
| 42 |
-
"cuequivariance_ops_torch runtime from the 'structure,cueq' extras."
|
| 43 |
-
)
|
| 44 |
-
cueq = importlib.import_module("cuequivariance_torch")
|
| 45 |
-
return cueq.triangle_multiplicative_update(
|
| 46 |
-
x,
|
| 47 |
-
direction=direction,
|
| 48 |
-
mask=mask,
|
| 49 |
-
norm_in_weight=norm_in_weight,
|
| 50 |
-
norm_in_bias=norm_in_bias,
|
| 51 |
-
p_in_weight=p_in_weight,
|
| 52 |
-
g_in_weight=g_in_weight,
|
| 53 |
-
norm_out_weight=norm_out_weight,
|
| 54 |
-
norm_out_bias=norm_out_bias,
|
| 55 |
-
p_out_weight=p_out_weight,
|
| 56 |
-
g_out_weight=g_out_weight,
|
| 57 |
-
eps=eps,
|
| 58 |
-
)
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
class _TriangleMultiplication(nn.Module):
|
| 62 |
-
direction: TriangleDirection
|
| 63 |
-
equation: str
|
| 64 |
-
|
| 65 |
-
def __init__(self, dim: int, direction: TriangleDirection, equation: str) -> None:
|
| 66 |
-
super().__init__()
|
| 67 |
-
self.direction = direction
|
| 68 |
-
self.equation = equation
|
| 69 |
-
self.norm_in = nn.LayerNorm(dim, eps=1e-5)
|
| 70 |
-
self.p_in = nn.Linear(dim, 2 * dim, bias=False)
|
| 71 |
-
self.g_in = nn.Linear(dim, 2 * dim, bias=False)
|
| 72 |
-
self.norm_out = nn.LayerNorm(dim)
|
| 73 |
-
self.p_out = nn.Linear(dim, dim, bias=False)
|
| 74 |
-
self.g_out = nn.Linear(dim, dim, bias=False)
|
| 75 |
-
|
| 76 |
-
init.bias_init_one_(self.norm_in.weight)
|
| 77 |
-
init.bias_init_zero_(self.norm_in.bias)
|
| 78 |
-
init.lecun_normal_init_(self.p_in.weight)
|
| 79 |
-
init.gating_init_(self.g_in.weight)
|
| 80 |
-
init.bias_init_one_(self.norm_out.weight)
|
| 81 |
-
init.bias_init_zero_(self.norm_out.bias)
|
| 82 |
-
init.final_init_(self.p_out.weight)
|
| 83 |
-
init.gating_init_(self.g_out.weight)
|
| 84 |
-
|
| 85 |
-
def _kernel_forward(self, pair_states: Tensor, mask: Tensor) -> Tensor:
|
| 86 |
-
# pair_states: (b, l, l, d); mask: (b, l, l).
|
| 87 |
-
return kernel_triangular_mult(
|
| 88 |
-
pair_states,
|
| 89 |
-
direction=self.direction,
|
| 90 |
-
mask=mask,
|
| 91 |
-
norm_in_weight=self.norm_in.weight,
|
| 92 |
-
norm_in_bias=self.norm_in.bias,
|
| 93 |
-
p_in_weight=self.p_in.weight,
|
| 94 |
-
g_in_weight=self.g_in.weight,
|
| 95 |
-
norm_out_weight=self.norm_out.weight,
|
| 96 |
-
norm_out_bias=self.norm_out.bias,
|
| 97 |
-
p_out_weight=self.p_out.weight,
|
| 98 |
-
g_out_weight=self.g_out.weight,
|
| 99 |
-
eps=1e-5,
|
| 100 |
-
) # (b, l, l, d)
|
| 101 |
-
|
| 102 |
-
def forward(self, x: Tensor, mask: Tensor, use_kernels: bool = False) -> Tensor:
|
| 103 |
-
"""Transform pair tensor X with shape ``(b, l, l, d)``."""
|
| 104 |
-
|
| 105 |
-
if use_kernels:
|
| 106 |
-
return self._kernel_forward(x, mask)
|
| 107 |
-
|
| 108 |
-
# X_norm is the normalized pair tensor used by the output gate.
|
| 109 |
-
normalized = self.norm_in(x) # (b, l, l, d)
|
| 110 |
-
projected = (
|
| 111 |
-
self.p_in(normalized) * self.g_in(normalized).sigmoid()
|
| 112 |
-
) # (b, l, l, 2 * d)
|
| 113 |
-
projected = projected * mask.unsqueeze(-1) # (b, l, l, 2 * d)
|
| 114 |
-
left, right = torch.chunk(projected.float(), 2, dim=-1) # each: (b, l, l, d)
|
| 115 |
-
combined = torch.einsum(self.equation, left, right) # (b, l, l, d)
|
| 116 |
-
return (
|
| 117 |
-
self.p_out(self.norm_out(combined)) * self.g_out(normalized).sigmoid()
|
| 118 |
-
) # (b, l, l, d)
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
class TriangleMultiplicationOutgoing(_TriangleMultiplication):
|
| 122 |
-
"""Aggregate pair paths that share their destination index."""
|
| 123 |
-
|
| 124 |
-
def __init__(self, dim: int = 128) -> None:
|
| 125 |
-
super().__init__(dim, direction="outgoing", equation="bikd,bjkd->bijd")
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
class TriangleMultiplicationIncoming(_TriangleMultiplication):
|
| 129 |
-
"""Aggregate pair paths that share their source index."""
|
| 130 |
-
|
| 131 |
-
def __init__(self, dim: int = 128) -> None:
|
| 132 |
-
super().__init__(dim, direction="incoming", equation="bkid,bkjd->bijd")
|
|
|
|
| 1 |
+
"""Incoming and outgoing triangular multiplicative pair updates."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import importlib
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from importlib.util import find_spec
|
| 9 |
+
from typing import Literal
|
| 10 |
+
from torch import Tensor, nn
|
| 11 |
+
|
| 12 |
+
from . import vb_layers_initialize as init
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
TriangleDirection = Literal["incoming", "outgoing"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@torch.compiler.disable
|
| 19 |
+
def kernel_triangular_mult(
|
| 20 |
+
x: Tensor,
|
| 21 |
+
direction: TriangleDirection,
|
| 22 |
+
mask: Tensor,
|
| 23 |
+
norm_in_weight: Tensor,
|
| 24 |
+
norm_in_bias: Tensor,
|
| 25 |
+
p_in_weight: Tensor,
|
| 26 |
+
g_in_weight: Tensor,
|
| 27 |
+
norm_out_weight: Tensor,
|
| 28 |
+
norm_out_bias: Tensor,
|
| 29 |
+
p_out_weight: Tensor,
|
| 30 |
+
g_out_weight: Tensor,
|
| 31 |
+
eps: float,
|
| 32 |
+
) -> Tensor:
|
| 33 |
+
"""Dispatch the optional cuEquivariance triangle primitive lazily."""
|
| 34 |
+
|
| 35 |
+
# x: (b, l, l, d); mask: (b, l, l); returned tensor: (b, l, l, d).
|
| 36 |
+
if (
|
| 37 |
+
find_spec("cuequivariance_torch") is None
|
| 38 |
+
or find_spec("cuequivariance_ops_torch") is None
|
| 39 |
+
):
|
| 40 |
+
raise RuntimeError(
|
| 41 |
+
"Boltz2 use_kernels=True requires cuequivariance_torch and the CUDA 13 "
|
| 42 |
+
"cuequivariance_ops_torch runtime from the 'structure,cueq' extras."
|
| 43 |
+
)
|
| 44 |
+
cueq = importlib.import_module("cuequivariance_torch")
|
| 45 |
+
return cueq.triangle_multiplicative_update(
|
| 46 |
+
x,
|
| 47 |
+
direction=direction,
|
| 48 |
+
mask=mask,
|
| 49 |
+
norm_in_weight=norm_in_weight,
|
| 50 |
+
norm_in_bias=norm_in_bias,
|
| 51 |
+
p_in_weight=p_in_weight,
|
| 52 |
+
g_in_weight=g_in_weight,
|
| 53 |
+
norm_out_weight=norm_out_weight,
|
| 54 |
+
norm_out_bias=norm_out_bias,
|
| 55 |
+
p_out_weight=p_out_weight,
|
| 56 |
+
g_out_weight=g_out_weight,
|
| 57 |
+
eps=eps,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class _TriangleMultiplication(nn.Module):
|
| 62 |
+
direction: TriangleDirection
|
| 63 |
+
equation: str
|
| 64 |
+
|
| 65 |
+
def __init__(self, dim: int, direction: TriangleDirection, equation: str) -> None:
|
| 66 |
+
super().__init__()
|
| 67 |
+
self.direction = direction
|
| 68 |
+
self.equation = equation
|
| 69 |
+
self.norm_in = nn.LayerNorm(dim, eps=1e-5)
|
| 70 |
+
self.p_in = nn.Linear(dim, 2 * dim, bias=False)
|
| 71 |
+
self.g_in = nn.Linear(dim, 2 * dim, bias=False)
|
| 72 |
+
self.norm_out = nn.LayerNorm(dim)
|
| 73 |
+
self.p_out = nn.Linear(dim, dim, bias=False)
|
| 74 |
+
self.g_out = nn.Linear(dim, dim, bias=False)
|
| 75 |
+
|
| 76 |
+
init.bias_init_one_(self.norm_in.weight)
|
| 77 |
+
init.bias_init_zero_(self.norm_in.bias)
|
| 78 |
+
init.lecun_normal_init_(self.p_in.weight)
|
| 79 |
+
init.gating_init_(self.g_in.weight)
|
| 80 |
+
init.bias_init_one_(self.norm_out.weight)
|
| 81 |
+
init.bias_init_zero_(self.norm_out.bias)
|
| 82 |
+
init.final_init_(self.p_out.weight)
|
| 83 |
+
init.gating_init_(self.g_out.weight)
|
| 84 |
+
|
| 85 |
+
def _kernel_forward(self, pair_states: Tensor, mask: Tensor) -> Tensor:
|
| 86 |
+
# pair_states: (b, l, l, d); mask: (b, l, l).
|
| 87 |
+
return kernel_triangular_mult(
|
| 88 |
+
pair_states,
|
| 89 |
+
direction=self.direction,
|
| 90 |
+
mask=mask,
|
| 91 |
+
norm_in_weight=self.norm_in.weight,
|
| 92 |
+
norm_in_bias=self.norm_in.bias,
|
| 93 |
+
p_in_weight=self.p_in.weight,
|
| 94 |
+
g_in_weight=self.g_in.weight,
|
| 95 |
+
norm_out_weight=self.norm_out.weight,
|
| 96 |
+
norm_out_bias=self.norm_out.bias,
|
| 97 |
+
p_out_weight=self.p_out.weight,
|
| 98 |
+
g_out_weight=self.g_out.weight,
|
| 99 |
+
eps=1e-5,
|
| 100 |
+
) # (b, l, l, d)
|
| 101 |
+
|
| 102 |
+
def forward(self, x: Tensor, mask: Tensor, use_kernels: bool = False) -> Tensor:
|
| 103 |
+
"""Transform pair tensor X with shape ``(b, l, l, d)``."""
|
| 104 |
+
|
| 105 |
+
if use_kernels:
|
| 106 |
+
return self._kernel_forward(x, mask)
|
| 107 |
+
|
| 108 |
+
# X_norm is the normalized pair tensor used by the output gate.
|
| 109 |
+
normalized = self.norm_in(x) # (b, l, l, d)
|
| 110 |
+
projected = (
|
| 111 |
+
self.p_in(normalized) * self.g_in(normalized).sigmoid()
|
| 112 |
+
) # (b, l, l, 2 * d)
|
| 113 |
+
projected = projected * mask.unsqueeze(-1) # (b, l, l, 2 * d)
|
| 114 |
+
left, right = torch.chunk(projected.float(), 2, dim=-1) # each: (b, l, l, d)
|
| 115 |
+
combined = torch.einsum(self.equation, left, right) # (b, l, l, d)
|
| 116 |
+
return (
|
| 117 |
+
self.p_out(self.norm_out(combined)) * self.g_out(normalized).sigmoid()
|
| 118 |
+
) # (b, l, l, d)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class TriangleMultiplicationOutgoing(_TriangleMultiplication):
|
| 122 |
+
"""Aggregate pair paths that share their destination index."""
|
| 123 |
+
|
| 124 |
+
def __init__(self, dim: int = 128) -> None:
|
| 125 |
+
super().__init__(dim, direction="outgoing", equation="bikd,bjkd->bijd")
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class TriangleMultiplicationIncoming(_TriangleMultiplication):
|
| 129 |
+
"""Aggregate pair paths that share their source index."""
|
| 130 |
+
|
| 131 |
+
def __init__(self, dim: int = 128) -> None:
|
| 132 |
+
super().__init__(dim, direction="incoming", equation="bkid,bkjd->bijd")
|
fastplms/models/boltz/vb_loss_diffusionv2.py
CHANGED
|
@@ -1,186 +1,186 @@
|
|
| 1 |
-
"""Geometry objectives shared by Boltz diffusion and steering code.
|
| 2 |
-
|
| 3 |
-
The rigid-alignment mechanism is based on the Kabsch formulation used by
|
| 4 |
-
AlphaFold 3 implementations. The implementation is maintained locally and
|
| 5 |
-
does not import an upstream runtime package.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import warnings
|
| 11 |
-
import torch
|
| 12 |
-
import torch.nn.functional as functional
|
| 13 |
-
|
| 14 |
-
from einops import einsum
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def _weighted_centroid(
|
| 18 |
-
coordinates: torch.Tensor,
|
| 19 |
-
weights: torch.Tensor,
|
| 20 |
-
) -> torch.Tensor:
|
| 21 |
-
# coordinates: (..., n, 3); weights: (..., n, 1).
|
| 22 |
-
return (coordinates * weights).sum(dim=-2, keepdim=True) / weights.sum(
|
| 23 |
-
dim=-2,
|
| 24 |
-
keepdim=True,
|
| 25 |
-
) # (..., 1, 3)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _warn_if_alignment_is_ambiguous(
|
| 29 |
-
mask: torch.Tensor,
|
| 30 |
-
singular_values: torch.Tensor,
|
| 31 |
-
*,
|
| 32 |
-
num_points: int,
|
| 33 |
-
coordinate_dim: int,
|
| 34 |
-
) -> None:
|
| 35 |
-
if torch.any(mask.sum(dim=-1) < coordinate_dim + 1):
|
| 36 |
-
warnings.warn(
|
| 37 |
-
"The size of one of the point clouds is <= dim+1. "
|
| 38 |
-
"`WeightedRigidAlign` cannot return a unique rotation.",
|
| 39 |
-
RuntimeWarning,
|
| 40 |
-
stacklevel=3,
|
| 41 |
-
)
|
| 42 |
-
if (singular_values.abs() <= 1e-15).any() and num_points >= coordinate_dim + 1:
|
| 43 |
-
warnings.warn(
|
| 44 |
-
"Excessively low rank of cross-correlation between aligned "
|
| 45 |
-
"point clouds. `WeightedRigidAlign` cannot return a unique rotation.",
|
| 46 |
-
RuntimeWarning,
|
| 47 |
-
stacklevel=3,
|
| 48 |
-
)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def weighted_rigid_align(
|
| 52 |
-
true_coords: torch.Tensor,
|
| 53 |
-
pred_coords: torch.Tensor,
|
| 54 |
-
weights: torch.Tensor,
|
| 55 |
-
mask: torch.Tensor,
|
| 56 |
-
) -> torch.Tensor:
|
| 57 |
-
"""Align true coordinates to predicted coordinates with weighted Kabsch.
|
| 58 |
-
|
| 59 |
-
``true_coords`` and ``pred_coords`` have shape ``(..., n, 3)``. The
|
| 60 |
-
returned tensor is detached because alignment defines a fixed target for
|
| 61 |
-
the diffusion loss.
|
| 62 |
-
"""
|
| 63 |
-
|
| 64 |
-
output_shape = torch.broadcast_shapes(true_coords.shape, pred_coords.shape)
|
| 65 |
-
*batch_shape, num_points, coordinate_dim = output_shape
|
| 66 |
-
point_weights = (mask * weights).unsqueeze(-1) # (..., n, 1)
|
| 67 |
-
|
| 68 |
-
true_centroid = _weighted_centroid(true_coords, point_weights) # (..., 1, 3)
|
| 69 |
-
pred_centroid = _weighted_centroid(pred_coords, point_weights) # (..., 1, 3)
|
| 70 |
-
true_centered = true_coords - true_centroid # (..., n, 3)
|
| 71 |
-
pred_centered = pred_coords - pred_centroid # (..., n, 3)
|
| 72 |
-
|
| 73 |
-
covariance = einsum(
|
| 74 |
-
point_weights * pred_centered,
|
| 75 |
-
true_centered,
|
| 76 |
-
"... n i, ... n j -> ... i j",
|
| 77 |
-
) # (..., 3, 3)
|
| 78 |
-
original_dtype = covariance.dtype
|
| 79 |
-
covariance_fp32 = covariance.to(torch.float32) # (..., 3, 3)
|
| 80 |
-
left_vectors, singular_values, right_vectors_h = torch.linalg.svd(
|
| 81 |
-
covariance_fp32,
|
| 82 |
-
driver="gesvd" if covariance_fp32.is_cuda else None,
|
| 83 |
-
) # left/right: (..., 3, 3); singular_values: (..., 3)
|
| 84 |
-
right_vectors = right_vectors_h.mH # (..., 3, 3)
|
| 85 |
-
_warn_if_alignment_is_ambiguous(
|
| 86 |
-
mask,
|
| 87 |
-
singular_values,
|
| 88 |
-
num_points=num_points,
|
| 89 |
-
coordinate_dim=coordinate_dim,
|
| 90 |
-
)
|
| 91 |
-
|
| 92 |
-
preliminary_rotation = torch.einsum(
|
| 93 |
-
"... i j, ... k j -> ... i k",
|
| 94 |
-
left_vectors,
|
| 95 |
-
right_vectors,
|
| 96 |
-
).to(torch.float32) # (..., 3, 3)
|
| 97 |
-
orientation = torch.eye(
|
| 98 |
-
coordinate_dim,
|
| 99 |
-
dtype=covariance_fp32.dtype,
|
| 100 |
-
device=covariance.device,
|
| 101 |
-
)[None].repeat(*batch_shape, 1, 1) # (..., 3, 3)
|
| 102 |
-
orientation[..., -1, -1] = torch.det(preliminary_rotation) # (...)
|
| 103 |
-
rotation = einsum(
|
| 104 |
-
left_vectors,
|
| 105 |
-
orientation,
|
| 106 |
-
right_vectors,
|
| 107 |
-
"... i j, ... j k, ... l k -> ... i l",
|
| 108 |
-
).to(original_dtype) # (..., 3, 3)
|
| 109 |
-
|
| 110 |
-
aligned = (
|
| 111 |
-
einsum(true_centered, rotation, "... n i, ... j i -> ... n j") + pred_centroid
|
| 112 |
-
) # (..., n, 3)
|
| 113 |
-
aligned.detach_()
|
| 114 |
-
return aligned # (..., n, 3)
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
def _smooth_lddt_for_example(
|
| 118 |
-
pred_coords: torch.Tensor,
|
| 119 |
-
true_coords: torch.Tensor,
|
| 120 |
-
is_nucleotide: torch.Tensor,
|
| 121 |
-
coords_mask: torch.Tensor,
|
| 122 |
-
*,
|
| 123 |
-
nucleic_acid_cutoff: float,
|
| 124 |
-
other_cutoff: float,
|
| 125 |
-
) -> torch.Tensor:
|
| 126 |
-
# pred_coords/true_coords: (n, 3); is_nucleotide/coords_mask: (n,).
|
| 127 |
-
true_distances = torch.cdist(true_coords, true_coords) # (n, n)
|
| 128 |
-
nucleotide_rows = is_nucleotide.bool().unsqueeze(-1).expand_as(
|
| 129 |
-
true_distances
|
| 130 |
-
) # (n, n)
|
| 131 |
-
pair_mask = torch.where(
|
| 132 |
-
nucleotide_rows,
|
| 133 |
-
true_distances < nucleic_acid_cutoff,
|
| 134 |
-
true_distances < other_cutoff,
|
| 135 |
-
) # (n, n)
|
| 136 |
-
pair_mask &= ~torch.eye(
|
| 137 |
-
pred_coords.shape[0],
|
| 138 |
-
dtype=torch.bool,
|
| 139 |
-
device=pred_coords.device,
|
| 140 |
-
) # (n, n)
|
| 141 |
-
coordinate_rows = coords_mask.bool() # (n,)
|
| 142 |
-
pair_mask &= coordinate_rows.unsqueeze(-1) # (n, n)
|
| 143 |
-
pair_mask &= coordinate_rows.unsqueeze(-2) # (n, n)
|
| 144 |
-
|
| 145 |
-
pair_indices = pair_mask.nonzero() # (n_pair, 2)
|
| 146 |
-
true_pair_distances = true_distances[
|
| 147 |
-
pair_indices[:, 0], pair_indices[:, 1]
|
| 148 |
-
] # (n_pair,)
|
| 149 |
-
pred_pair_distances = functional.pairwise_distance(
|
| 150 |
-
pred_coords[pair_indices[:, 0]],
|
| 151 |
-
pred_coords[pair_indices[:, 1]],
|
| 152 |
-
) # (n_pair,)
|
| 153 |
-
distance_error = torch.abs(true_pair_distances - pred_pair_distances) # (n_pair,)
|
| 154 |
-
smooth_agreement = (
|
| 155 |
-
sum(torch.sigmoid(threshold - distance_error) for threshold in (0.5, 1.0, 2.0, 4.0)) / 4.0
|
| 156 |
-
) # (n_pair,)
|
| 157 |
-
return smooth_agreement.sum() / (pair_indices.shape[0] + 1e-5) # ()
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
def smooth_lddt_loss(
|
| 161 |
-
pred_coords: torch.Tensor,
|
| 162 |
-
true_coords: torch.Tensor,
|
| 163 |
-
is_nucleotide: torch.Tensor,
|
| 164 |
-
coords_mask: torch.Tensor,
|
| 165 |
-
nucleic_acid_cutoff: float = 30.0,
|
| 166 |
-
other_cutoff: float = 15.0,
|
| 167 |
-
multiplicity: int = 1,
|
| 168 |
-
) -> torch.Tensor:
|
| 169 |
-
"""Return one minus the smooth local-distance agreement.
|
| 170 |
-
|
| 171 |
-
Coordinate tensors have shape ``(b, n, 3)``. Sequence-level masks may
|
| 172 |
-
be shared across repeated diffusion samples through ``multiplicity``.
|
| 173 |
-
"""
|
| 174 |
-
|
| 175 |
-
agreements = [
|
| 176 |
-
_smooth_lddt_for_example(
|
| 177 |
-
pred_coords[index],
|
| 178 |
-
true_coords[index],
|
| 179 |
-
is_nucleotide[index // multiplicity],
|
| 180 |
-
coords_mask[index // multiplicity],
|
| 181 |
-
nucleic_acid_cutoff=nucleic_acid_cutoff,
|
| 182 |
-
other_cutoff=other_cutoff,
|
| 183 |
-
)
|
| 184 |
-
for index in range(true_coords.shape[0])
|
| 185 |
-
] # each: ()
|
| 186 |
-
return 1.0 - torch.stack(agreements).mean(dim=0) # ()
|
|
|
|
| 1 |
+
"""Geometry objectives shared by Boltz diffusion and steering code.
|
| 2 |
+
|
| 3 |
+
The rigid-alignment mechanism is based on the Kabsch formulation used by
|
| 4 |
+
AlphaFold 3 implementations. The implementation is maintained locally and
|
| 5 |
+
does not import an upstream runtime package.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import warnings
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn.functional as functional
|
| 13 |
+
|
| 14 |
+
from einops import einsum
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _weighted_centroid(
|
| 18 |
+
coordinates: torch.Tensor,
|
| 19 |
+
weights: torch.Tensor,
|
| 20 |
+
) -> torch.Tensor:
|
| 21 |
+
# coordinates: (..., n, 3); weights: (..., n, 1).
|
| 22 |
+
return (coordinates * weights).sum(dim=-2, keepdim=True) / weights.sum(
|
| 23 |
+
dim=-2,
|
| 24 |
+
keepdim=True,
|
| 25 |
+
) # (..., 1, 3)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _warn_if_alignment_is_ambiguous(
|
| 29 |
+
mask: torch.Tensor,
|
| 30 |
+
singular_values: torch.Tensor,
|
| 31 |
+
*,
|
| 32 |
+
num_points: int,
|
| 33 |
+
coordinate_dim: int,
|
| 34 |
+
) -> None:
|
| 35 |
+
if torch.any(mask.sum(dim=-1) < coordinate_dim + 1):
|
| 36 |
+
warnings.warn(
|
| 37 |
+
"The size of one of the point clouds is <= dim+1. "
|
| 38 |
+
"`WeightedRigidAlign` cannot return a unique rotation.",
|
| 39 |
+
RuntimeWarning,
|
| 40 |
+
stacklevel=3,
|
| 41 |
+
)
|
| 42 |
+
if (singular_values.abs() <= 1e-15).any() and num_points >= coordinate_dim + 1:
|
| 43 |
+
warnings.warn(
|
| 44 |
+
"Excessively low rank of cross-correlation between aligned "
|
| 45 |
+
"point clouds. `WeightedRigidAlign` cannot return a unique rotation.",
|
| 46 |
+
RuntimeWarning,
|
| 47 |
+
stacklevel=3,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def weighted_rigid_align(
|
| 52 |
+
true_coords: torch.Tensor,
|
| 53 |
+
pred_coords: torch.Tensor,
|
| 54 |
+
weights: torch.Tensor,
|
| 55 |
+
mask: torch.Tensor,
|
| 56 |
+
) -> torch.Tensor:
|
| 57 |
+
"""Align true coordinates to predicted coordinates with weighted Kabsch.
|
| 58 |
+
|
| 59 |
+
``true_coords`` and ``pred_coords`` have shape ``(..., n, 3)``. The
|
| 60 |
+
returned tensor is detached because alignment defines a fixed target for
|
| 61 |
+
the diffusion loss.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
output_shape = torch.broadcast_shapes(true_coords.shape, pred_coords.shape)
|
| 65 |
+
*batch_shape, num_points, coordinate_dim = output_shape
|
| 66 |
+
point_weights = (mask * weights).unsqueeze(-1) # (..., n, 1)
|
| 67 |
+
|
| 68 |
+
true_centroid = _weighted_centroid(true_coords, point_weights) # (..., 1, 3)
|
| 69 |
+
pred_centroid = _weighted_centroid(pred_coords, point_weights) # (..., 1, 3)
|
| 70 |
+
true_centered = true_coords - true_centroid # (..., n, 3)
|
| 71 |
+
pred_centered = pred_coords - pred_centroid # (..., n, 3)
|
| 72 |
+
|
| 73 |
+
covariance = einsum(
|
| 74 |
+
point_weights * pred_centered,
|
| 75 |
+
true_centered,
|
| 76 |
+
"... n i, ... n j -> ... i j",
|
| 77 |
+
) # (..., 3, 3)
|
| 78 |
+
original_dtype = covariance.dtype
|
| 79 |
+
covariance_fp32 = covariance.to(torch.float32) # (..., 3, 3)
|
| 80 |
+
left_vectors, singular_values, right_vectors_h = torch.linalg.svd(
|
| 81 |
+
covariance_fp32,
|
| 82 |
+
driver="gesvd" if covariance_fp32.is_cuda else None,
|
| 83 |
+
) # left/right: (..., 3, 3); singular_values: (..., 3)
|
| 84 |
+
right_vectors = right_vectors_h.mH # (..., 3, 3)
|
| 85 |
+
_warn_if_alignment_is_ambiguous(
|
| 86 |
+
mask,
|
| 87 |
+
singular_values,
|
| 88 |
+
num_points=num_points,
|
| 89 |
+
coordinate_dim=coordinate_dim,
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
preliminary_rotation = torch.einsum(
|
| 93 |
+
"... i j, ... k j -> ... i k",
|
| 94 |
+
left_vectors,
|
| 95 |
+
right_vectors,
|
| 96 |
+
).to(torch.float32) # (..., 3, 3)
|
| 97 |
+
orientation = torch.eye(
|
| 98 |
+
coordinate_dim,
|
| 99 |
+
dtype=covariance_fp32.dtype,
|
| 100 |
+
device=covariance.device,
|
| 101 |
+
)[None].repeat(*batch_shape, 1, 1) # (..., 3, 3)
|
| 102 |
+
orientation[..., -1, -1] = torch.det(preliminary_rotation) # (...)
|
| 103 |
+
rotation = einsum(
|
| 104 |
+
left_vectors,
|
| 105 |
+
orientation,
|
| 106 |
+
right_vectors,
|
| 107 |
+
"... i j, ... j k, ... l k -> ... i l",
|
| 108 |
+
).to(original_dtype) # (..., 3, 3)
|
| 109 |
+
|
| 110 |
+
aligned = (
|
| 111 |
+
einsum(true_centered, rotation, "... n i, ... j i -> ... n j") + pred_centroid
|
| 112 |
+
) # (..., n, 3)
|
| 113 |
+
aligned.detach_()
|
| 114 |
+
return aligned # (..., n, 3)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _smooth_lddt_for_example(
|
| 118 |
+
pred_coords: torch.Tensor,
|
| 119 |
+
true_coords: torch.Tensor,
|
| 120 |
+
is_nucleotide: torch.Tensor,
|
| 121 |
+
coords_mask: torch.Tensor,
|
| 122 |
+
*,
|
| 123 |
+
nucleic_acid_cutoff: float,
|
| 124 |
+
other_cutoff: float,
|
| 125 |
+
) -> torch.Tensor:
|
| 126 |
+
# pred_coords/true_coords: (n, 3); is_nucleotide/coords_mask: (n,).
|
| 127 |
+
true_distances = torch.cdist(true_coords, true_coords) # (n, n)
|
| 128 |
+
nucleotide_rows = is_nucleotide.bool().unsqueeze(-1).expand_as(
|
| 129 |
+
true_distances
|
| 130 |
+
) # (n, n)
|
| 131 |
+
pair_mask = torch.where(
|
| 132 |
+
nucleotide_rows,
|
| 133 |
+
true_distances < nucleic_acid_cutoff,
|
| 134 |
+
true_distances < other_cutoff,
|
| 135 |
+
) # (n, n)
|
| 136 |
+
pair_mask &= ~torch.eye(
|
| 137 |
+
pred_coords.shape[0],
|
| 138 |
+
dtype=torch.bool,
|
| 139 |
+
device=pred_coords.device,
|
| 140 |
+
) # (n, n)
|
| 141 |
+
coordinate_rows = coords_mask.bool() # (n,)
|
| 142 |
+
pair_mask &= coordinate_rows.unsqueeze(-1) # (n, n)
|
| 143 |
+
pair_mask &= coordinate_rows.unsqueeze(-2) # (n, n)
|
| 144 |
+
|
| 145 |
+
pair_indices = pair_mask.nonzero() # (n_pair, 2)
|
| 146 |
+
true_pair_distances = true_distances[
|
| 147 |
+
pair_indices[:, 0], pair_indices[:, 1]
|
| 148 |
+
] # (n_pair,)
|
| 149 |
+
pred_pair_distances = functional.pairwise_distance(
|
| 150 |
+
pred_coords[pair_indices[:, 0]],
|
| 151 |
+
pred_coords[pair_indices[:, 1]],
|
| 152 |
+
) # (n_pair,)
|
| 153 |
+
distance_error = torch.abs(true_pair_distances - pred_pair_distances) # (n_pair,)
|
| 154 |
+
smooth_agreement = (
|
| 155 |
+
sum(torch.sigmoid(threshold - distance_error) for threshold in (0.5, 1.0, 2.0, 4.0)) / 4.0
|
| 156 |
+
) # (n_pair,)
|
| 157 |
+
return smooth_agreement.sum() / (pair_indices.shape[0] + 1e-5) # ()
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def smooth_lddt_loss(
|
| 161 |
+
pred_coords: torch.Tensor,
|
| 162 |
+
true_coords: torch.Tensor,
|
| 163 |
+
is_nucleotide: torch.Tensor,
|
| 164 |
+
coords_mask: torch.Tensor,
|
| 165 |
+
nucleic_acid_cutoff: float = 30.0,
|
| 166 |
+
other_cutoff: float = 15.0,
|
| 167 |
+
multiplicity: int = 1,
|
| 168 |
+
) -> torch.Tensor:
|
| 169 |
+
"""Return one minus the smooth local-distance agreement.
|
| 170 |
+
|
| 171 |
+
Coordinate tensors have shape ``(b, n, 3)``. Sequence-level masks may
|
| 172 |
+
be shared across repeated diffusion samples through ``multiplicity``.
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
agreements = [
|
| 176 |
+
_smooth_lddt_for_example(
|
| 177 |
+
pred_coords[index],
|
| 178 |
+
true_coords[index],
|
| 179 |
+
is_nucleotide[index // multiplicity],
|
| 180 |
+
coords_mask[index // multiplicity],
|
| 181 |
+
nucleic_acid_cutoff=nucleic_acid_cutoff,
|
| 182 |
+
other_cutoff=other_cutoff,
|
| 183 |
+
)
|
| 184 |
+
for index in range(true_coords.shape[0])
|
| 185 |
+
] # each: ()
|
| 186 |
+
return 1.0 - torch.stack(agreements).mean(dim=0) # ()
|
fastplms/models/boltz/vb_modules_confidencev2.py
CHANGED
|
@@ -1,548 +1,548 @@
|
|
| 1 |
-
"""Predict atom and token confidence from Boltz2 sequence and pair states."""
|
| 2 |
-
|
| 3 |
-
import torch
|
| 4 |
-
|
| 5 |
-
from typing import Any
|
| 6 |
-
from torch import nn
|
| 7 |
-
|
| 8 |
-
from . import vb_const as const
|
| 9 |
-
from . import vb_layers_initialize as init
|
| 10 |
-
from .vb_layers_confidence_utils import (
|
| 11 |
-
compute_aggregated_metric,
|
| 12 |
-
compute_ptms,
|
| 13 |
-
)
|
| 14 |
-
from .vb_layers_pairformer import PairformerModule
|
| 15 |
-
from .vb_modules_encodersv2 import RelativePositionEncoder
|
| 16 |
-
from .vb_modules_trunkv2 import (
|
| 17 |
-
ContactConditioning,
|
| 18 |
-
)
|
| 19 |
-
from .vb_modules_utils import LinearNoBias
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def _token_slot_logits_to_atom_logits(
|
| 23 |
-
token_logits: torch.Tensor,
|
| 24 |
-
atom_to_token: torch.Tensor,
|
| 25 |
-
atom_pad_mask: torch.Tensor,
|
| 26 |
-
*,
|
| 27 |
-
multiplicity: int,
|
| 28 |
-
) -> torch.Tensor:
|
| 29 |
-
"""Gather per-token atom-slot logits onto each example's atom table.
|
| 30 |
-
|
| 31 |
-
``token_logits`` is ordered as ``(batch * multiplicity, token, slot,
|
| 32 |
-
channel)``. ``atom_to_token`` determines both the owning token and the
|
| 33 |
-
within-token slot of every atom, so heterogeneous atom counts do not leak
|
| 34 |
-
across batch rows.
|
| 35 |
-
"""
|
| 36 |
-
|
| 37 |
-
# b: batch; m: multiplicity; a: atoms; t: tokens; c: channels.
|
| 38 |
-
if token_logits.ndim != 4:
|
| 39 |
-
raise ValueError(
|
| 40 |
-
"token_logits must have shape (batch * multiplicity, token, slot, channel), "
|
| 41 |
-
f"got {tuple(token_logits.shape)}."
|
| 42 |
-
)
|
| 43 |
-
if atom_to_token.ndim != 3:
|
| 44 |
-
raise ValueError(
|
| 45 |
-
"atom_to_token must have shape (batch, atom, token), "
|
| 46 |
-
f"got {tuple(atom_to_token.shape)}."
|
| 47 |
-
)
|
| 48 |
-
if atom_pad_mask.shape != atom_to_token.shape[:2]:
|
| 49 |
-
raise ValueError(
|
| 50 |
-
"atom_pad_mask must match the batch and atom axes of atom_to_token; "
|
| 51 |
-
f"got {tuple(atom_pad_mask.shape)} and {tuple(atom_to_token.shape)}."
|
| 52 |
-
)
|
| 53 |
-
if multiplicity < 1:
|
| 54 |
-
raise ValueError(f"multiplicity must be positive, got {multiplicity}.")
|
| 55 |
-
|
| 56 |
-
batch_size, atom_count, token_count = atom_to_token.shape
|
| 57 |
-
if token_logits.shape[0] != batch_size * multiplicity:
|
| 58 |
-
raise ValueError(
|
| 59 |
-
"token_logits batch axis must equal batch * multiplicity; "
|
| 60 |
-
f"got {token_logits.shape[0]} and {batch_size} * {multiplicity}."
|
| 61 |
-
)
|
| 62 |
-
if token_logits.shape[1] != token_count:
|
| 63 |
-
raise ValueError(
|
| 64 |
-
"token_logits and atom_to_token disagree on token count; "
|
| 65 |
-
f"got {token_logits.shape[1]} and {token_count}."
|
| 66 |
-
)
|
| 67 |
-
|
| 68 |
-
valid_atoms = atom_pad_mask.bool() # (b, a)
|
| 69 |
-
assignments = atom_to_token.bool() & valid_atoms.unsqueeze(-1) # (b, a, t)
|
| 70 |
-
token_index = assignments.to(dtype=torch.int64).argmax(dim=-1) # (b, a)
|
| 71 |
-
# Cumulative one-hot counts give each atom its ordinal within its owning
|
| 72 |
-
# token without assuming that atoms from different tokens are contiguous.
|
| 73 |
-
cumulative_slots = assignments.to(dtype=torch.int64).cumsum(dim=1) - 1 # (b, a, t)
|
| 74 |
-
slot_index = (cumulative_slots * assignments).sum(dim=-1) # (b, a)
|
| 75 |
-
|
| 76 |
-
slots_per_token = token_logits.shape[2]
|
| 77 |
-
flattened_index = token_index * slots_per_token + slot_index # (b, a)
|
| 78 |
-
flattened_index = flattened_index.masked_fill(~valid_atoms, 0) # (b, a)
|
| 79 |
-
flattened_index = flattened_index.repeat_interleave(multiplicity, dim=0) # (b * m, a)
|
| 80 |
-
expanded_atom_mask = valid_atoms.repeat_interleave(multiplicity, dim=0) # (b * m, a)
|
| 81 |
-
|
| 82 |
-
flattened_logits = token_logits.flatten(1, 2) # (b * m, t * n_slot, c)
|
| 83 |
-
gather_index = flattened_index.unsqueeze(-1).expand(
|
| 84 |
-
-1,
|
| 85 |
-
atom_count,
|
| 86 |
-
flattened_logits.shape[-1],
|
| 87 |
-
) # (b * m, a, c)
|
| 88 |
-
atom_logits = torch.gather(flattened_logits, dim=1, index=gather_index) # (b * m, a, c)
|
| 89 |
-
return atom_logits * expanded_atom_mask.unsqueeze(-1).to(dtype=atom_logits.dtype) # (b * m, a, c)
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
class ConfidenceModule(nn.Module):
|
| 93 |
-
"""Update confidence features with predicted geometry and a pairformer stack."""
|
| 94 |
-
|
| 95 |
-
def __init__(
|
| 96 |
-
self,
|
| 97 |
-
token_s: int,
|
| 98 |
-
token_z: int,
|
| 99 |
-
pairformer_args: dict[str, Any],
|
| 100 |
-
num_dist_bins: int = 64,
|
| 101 |
-
token_level_confidence: bool = True,
|
| 102 |
-
max_dist: float = 22,
|
| 103 |
-
add_s_to_z_prod: bool = False,
|
| 104 |
-
add_s_input_to_s: bool = False,
|
| 105 |
-
add_z_input_to_z: bool = False,
|
| 106 |
-
maximum_bond_distance: int = 0,
|
| 107 |
-
bond_type_feature: bool = False,
|
| 108 |
-
confidence_args: dict[str, Any] | None = None,
|
| 109 |
-
compile_pairformer: bool = False,
|
| 110 |
-
fix_sym_check: bool = False,
|
| 111 |
-
cyclic_pos_enc: bool = False,
|
| 112 |
-
return_latent_feats: bool = False,
|
| 113 |
-
conditioning_cutoff_min: float | None = None,
|
| 114 |
-
conditioning_cutoff_max: float | None = None,
|
| 115 |
-
**kwargs: Any,
|
| 116 |
-
) -> None:
|
| 117 |
-
super().__init__()
|
| 118 |
-
self.max_num_atoms_per_token = 23
|
| 119 |
-
self.no_update_s = pairformer_args.get("no_update_s", False)
|
| 120 |
-
boundaries = torch.linspace(2, max_dist, num_dist_bins - 1) # (n_dist_bin - 1,)
|
| 121 |
-
self.register_buffer("boundaries", boundaries)
|
| 122 |
-
self.dist_bin_pairwise_embed = nn.Embedding(num_dist_bins, token_z)
|
| 123 |
-
init.gating_init_(self.dist_bin_pairwise_embed.weight)
|
| 124 |
-
self.token_level_confidence = token_level_confidence
|
| 125 |
-
|
| 126 |
-
self.s_to_z = LinearNoBias(token_s, token_z)
|
| 127 |
-
self.s_to_z_transpose = LinearNoBias(token_s, token_z)
|
| 128 |
-
init.gating_init_(self.s_to_z.weight)
|
| 129 |
-
init.gating_init_(self.s_to_z_transpose.weight)
|
| 130 |
-
|
| 131 |
-
self.add_s_to_z_prod = add_s_to_z_prod
|
| 132 |
-
if add_s_to_z_prod:
|
| 133 |
-
self.s_to_z_prod_in1 = LinearNoBias(token_s, token_z)
|
| 134 |
-
self.s_to_z_prod_in2 = LinearNoBias(token_s, token_z)
|
| 135 |
-
self.s_to_z_prod_out = LinearNoBias(token_z, token_z)
|
| 136 |
-
init.gating_init_(self.s_to_z_prod_out.weight)
|
| 137 |
-
|
| 138 |
-
self.s_inputs_norm = nn.LayerNorm(token_s)
|
| 139 |
-
if not self.no_update_s:
|
| 140 |
-
self.s_norm = nn.LayerNorm(token_s)
|
| 141 |
-
self.z_norm = nn.LayerNorm(token_z)
|
| 142 |
-
|
| 143 |
-
self.add_s_input_to_s = add_s_input_to_s
|
| 144 |
-
if add_s_input_to_s:
|
| 145 |
-
self.s_input_to_s = LinearNoBias(token_s, token_s)
|
| 146 |
-
init.gating_init_(self.s_input_to_s.weight)
|
| 147 |
-
|
| 148 |
-
self.add_z_input_to_z = add_z_input_to_z
|
| 149 |
-
if add_z_input_to_z:
|
| 150 |
-
self.rel_pos = RelativePositionEncoder(
|
| 151 |
-
token_z, fix_sym_check=fix_sym_check, cyclic_pos_enc=cyclic_pos_enc
|
| 152 |
-
)
|
| 153 |
-
self.token_bonds = nn.Linear(
|
| 154 |
-
1 if maximum_bond_distance == 0 else maximum_bond_distance + 2,
|
| 155 |
-
token_z,
|
| 156 |
-
bias=False,
|
| 157 |
-
)
|
| 158 |
-
self.bond_type_feature = bond_type_feature
|
| 159 |
-
if bond_type_feature:
|
| 160 |
-
self.token_bonds_type = nn.Embedding(len(const.bond_types) + 1, token_z)
|
| 161 |
-
|
| 162 |
-
self.contact_conditioning = ContactConditioning(
|
| 163 |
-
token_z=token_z,
|
| 164 |
-
cutoff_min=conditioning_cutoff_min,
|
| 165 |
-
cutoff_max=conditioning_cutoff_max,
|
| 166 |
-
)
|
| 167 |
-
pairformer_args["v2"] = True
|
| 168 |
-
self.pairformer_stack = PairformerModule(
|
| 169 |
-
token_s,
|
| 170 |
-
token_z,
|
| 171 |
-
**pairformer_args,
|
| 172 |
-
)
|
| 173 |
-
self.return_latent_feats = return_latent_feats
|
| 174 |
-
|
| 175 |
-
self.confidence_heads = ConfidenceHeads(
|
| 176 |
-
token_s,
|
| 177 |
-
token_z,
|
| 178 |
-
token_level_confidence=token_level_confidence,
|
| 179 |
-
**confidence_args,
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
def forward(
|
| 183 |
-
self,
|
| 184 |
-
s_inputs: torch.Tensor, # (b, t, d_s)
|
| 185 |
s: torch.Tensor, # (b, t, d_s)
|
| 186 |
z: torch.Tensor, # (b, t, t, d_z)
|
| 187 |
-
x_pred: torch.Tensor, # (b * m, a, 3) or (b, m, a, 3)
|
| 188 |
-
feats: dict[str, torch.Tensor],
|
| 189 |
pred_distogram_logits: torch.Tensor, # (b, t, t, n_bin)
|
| 190 |
-
multiplicity: int = 1,
|
| 191 |
-
run_sequentially: bool = False,
|
| 192 |
-
use_kernels: bool = False,
|
| 193 |
-
) -> dict[str, Any]:
|
| 194 |
-
# b: base batch; m: diffusion multiplicity; t: tokens; a: atoms.
|
| 195 |
-
if run_sequentially and multiplicity > 1:
|
| 196 |
-
batch_size = z.shape[0]
|
| 197 |
-
expected_shape = (batch_size, multiplicity)
|
| 198 |
-
if x_pred.ndim >= 4 and x_pred.shape[:2] == expected_shape:
|
| 199 |
-
sample_coordinates = x_pred # (b, m, a, 3)
|
| 200 |
-
elif x_pred.shape[0] == batch_size * multiplicity:
|
| 201 |
-
sample_coordinates = x_pred.reshape(
|
| 202 |
-
batch_size,
|
| 203 |
-
multiplicity,
|
| 204 |
-
*x_pred.shape[1:],
|
| 205 |
-
) # (b, m, a, 3)
|
| 206 |
-
else:
|
| 207 |
-
raise ValueError(
|
| 208 |
-
"Sequential confidence expected coordinates with leading shape "
|
| 209 |
-
f"{expected_shape} or {batch_size * multiplicity}, got "
|
| 210 |
-
f"{tuple(x_pred.shape)}."
|
| 211 |
-
)
|
| 212 |
-
out_dicts = []
|
| 213 |
-
for sample_idx in range(multiplicity):
|
| 214 |
-
out_dicts.append(
|
| 215 |
-
self.forward(
|
| 216 |
-
s_inputs,
|
| 217 |
-
s,
|
| 218 |
-
z,
|
| 219 |
-
sample_coordinates[:, sample_idx],
|
| 220 |
-
feats,
|
| 221 |
-
pred_distogram_logits,
|
| 222 |
-
multiplicity=1,
|
| 223 |
-
run_sequentially=False,
|
| 224 |
-
use_kernels=use_kernels,
|
| 225 |
-
)
|
| 226 |
-
)
|
| 227 |
-
|
| 228 |
-
out_dict = {}
|
| 229 |
-
for key in out_dicts[0]:
|
| 230 |
-
if key != "pair_chains_iptm":
|
| 231 |
-
values = [out[key] for out in out_dicts] # list of sample tensors, each (b, *feature_shape)
|
| 232 |
-
out_dict[key] = torch.stack(values, dim=1).flatten(0, 1) # (b * m, *feature_shape)
|
| 233 |
-
else:
|
| 234 |
-
pair_chains_iptm = {}
|
| 235 |
-
for chain_idx1 in out_dicts[0][key]:
|
| 236 |
-
chains_iptm = {}
|
| 237 |
-
for chain_idx2 in out_dicts[0][key][chain_idx1]:
|
| 238 |
-
values = [
|
| 239 |
-
out[key][chain_idx1][chain_idx2]
|
| 240 |
-
for out in out_dicts
|
| 241 |
] # list of sample chain-pair scores, each (b,)
|
| 242 |
-
chains_iptm[chain_idx2] = torch.stack(
|
| 243 |
-
values,
|
| 244 |
-
dim=1,
|
| 245 |
-
).flatten(0, 1) # (b * m,)
|
| 246 |
-
pair_chains_iptm[chain_idx1] = chains_iptm
|
| 247 |
-
out_dict[key] = pair_chains_iptm # mapping of chain-pair scores, each (b * m,)
|
| 248 |
-
return out_dict
|
| 249 |
-
|
| 250 |
-
s_inputs = self.s_inputs_norm(s_inputs) # (b, t, d_s)
|
| 251 |
-
if not self.no_update_s:
|
| 252 |
-
s = self.s_norm(s) # (b, t, d_s)
|
| 253 |
-
|
| 254 |
-
if self.add_s_input_to_s:
|
| 255 |
-
s = s + self.s_input_to_s(s_inputs) # (b, t, d_s)
|
| 256 |
-
|
| 257 |
-
z = self.z_norm(z) # (b, t, t, d_z)
|
| 258 |
-
|
| 259 |
-
if self.add_z_input_to_z:
|
| 260 |
-
relative_position_encoding = self.rel_pos(feats) # (b, t, t, d_z)
|
| 261 |
-
z = z + relative_position_encoding # (b, t, t, d_z)
|
| 262 |
-
z = z + self.token_bonds(feats["token_bonds"].float()) # (b, t, t, d_z)
|
| 263 |
-
if self.bond_type_feature:
|
| 264 |
-
z = z + self.token_bonds_type(feats["type_bonds"].long()) # (b, t, t, d_z)
|
| 265 |
-
z = z + self.contact_conditioning(feats) # (b, t, t, d_z)
|
| 266 |
-
|
| 267 |
-
s = s.repeat_interleave(multiplicity, 0) # (b * m, t, d_s)
|
| 268 |
-
|
| 269 |
-
z = (
|
| 270 |
-
z
|
| 271 |
-
+ self.s_to_z(s_inputs)[:, :, None, :]
|
| 272 |
-
+ self.s_to_z_transpose(s_inputs)[:, None, :, :]
|
| 273 |
-
) # (b, t, t, d_z)
|
| 274 |
-
if self.add_s_to_z_prod:
|
| 275 |
-
z = z + self.s_to_z_prod_out(
|
| 276 |
-
self.s_to_z_prod_in1(s_inputs)[:, :, None, :]
|
| 277 |
-
* self.s_to_z_prod_in2(s_inputs)[:, None, :, :]
|
| 278 |
-
) # (b, t, t, d_z)
|
| 279 |
-
|
| 280 |
-
z = z.repeat_interleave(multiplicity, 0) # (b * m, t, t, d_z)
|
| 281 |
-
s_inputs = s_inputs.repeat_interleave(multiplicity, 0) # (b * m, t, d_s)
|
| 282 |
-
|
| 283 |
token_to_rep_atom = feats["token_to_rep_atom"] # (b, t, a)
|
| 284 |
token_to_rep_atom = token_to_rep_atom.repeat_interleave(multiplicity, 0) # (b * m, t, a)
|
| 285 |
-
if len(x_pred.shape) == 4:
|
| 286 |
-
b, multiplicity, n, _ = x_pred.shape
|
| 287 |
-
x_pred = x_pred.reshape(b * multiplicity, n, -1) # (b * m, a, 3)
|
| 288 |
-
else:
|
| 289 |
-
_, n, _ = x_pred.shape
|
| 290 |
-
x_pred_repr = torch.bmm(token_to_rep_atom.float(), x_pred) # (b * m, t, 3)
|
| 291 |
-
d = torch.cdist(x_pred_repr, x_pred_repr) # (b * m, t, t)
|
| 292 |
-
distogram = (d.unsqueeze(-1) > self.boundaries).sum(dim=-1).long() # (b * m, t, t)
|
| 293 |
-
distogram = self.dist_bin_pairwise_embed(distogram) # (b * m, t, t, d_z)
|
| 294 |
z = z + distogram # (b * m, t, t, d_z)
|
| 295 |
-
|
| 296 |
-
mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 297 |
-
pair_mask = mask[:, :, None] * mask[:, None, :] # (b * m, t, t)
|
| 298 |
-
|
| 299 |
-
s_t, z_t = self.pairformer_stack(
|
| 300 |
-
s, z, mask=mask, pair_mask=pair_mask, use_kernels=use_kernels
|
| 301 |
-
) # (b * m, t, d_s), (b * m, t, t, d_z)
|
| 302 |
-
|
| 303 |
-
# Confidence heads consume the pairformer states without an additional residual.
|
| 304 |
-
s = s_t # (b * m, t, d_s)
|
| 305 |
-
z = z_t # (b * m, t, t, d_z)
|
| 306 |
-
|
| 307 |
-
out_dict = {}
|
| 308 |
-
|
| 309 |
-
if self.return_latent_feats:
|
| 310 |
-
out_dict["s_conf"] = s # (b * m, t, d_s)
|
| 311 |
-
out_dict["z_conf"] = z # (b * m, t, t, d_z)
|
| 312 |
-
|
| 313 |
-
out_dict.update(
|
| 314 |
-
self.confidence_heads(
|
| 315 |
-
s=s,
|
| 316 |
-
z=z,
|
| 317 |
-
x_pred=x_pred,
|
| 318 |
-
d=d,
|
| 319 |
-
feats=feats,
|
| 320 |
-
multiplicity=multiplicity,
|
| 321 |
-
pred_distogram_logits=pred_distogram_logits,
|
| 322 |
-
)
|
| 323 |
-
)
|
| 324 |
-
return out_dict
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
class ConfidenceHeads(nn.Module):
|
| 328 |
-
def __init__(
|
| 329 |
-
self,
|
| 330 |
-
token_s: int,
|
| 331 |
-
token_z: int,
|
| 332 |
-
num_plddt_bins: int = 50,
|
| 333 |
-
num_pde_bins: int = 64,
|
| 334 |
-
num_pae_bins: int = 64,
|
| 335 |
-
token_level_confidence: bool = True,
|
| 336 |
-
use_separate_heads: bool = False,
|
| 337 |
-
**kwargs: Any,
|
| 338 |
-
) -> None:
|
| 339 |
-
super().__init__()
|
| 340 |
-
self.max_num_atoms_per_token = 23
|
| 341 |
-
self.token_level_confidence = token_level_confidence
|
| 342 |
-
self.use_separate_heads = use_separate_heads
|
| 343 |
-
|
| 344 |
-
if self.use_separate_heads:
|
| 345 |
-
self.to_pae_intra_logits = LinearNoBias(token_z, num_pae_bins)
|
| 346 |
-
self.to_pae_inter_logits = LinearNoBias(token_z, num_pae_bins)
|
| 347 |
-
else:
|
| 348 |
-
self.to_pae_logits = LinearNoBias(token_z, num_pae_bins)
|
| 349 |
-
|
| 350 |
-
if self.use_separate_heads:
|
| 351 |
-
self.to_pde_intra_logits = LinearNoBias(token_z, num_pde_bins)
|
| 352 |
-
self.to_pde_inter_logits = LinearNoBias(token_z, num_pde_bins)
|
| 353 |
-
else:
|
| 354 |
-
self.to_pde_logits = LinearNoBias(token_z, num_pde_bins)
|
| 355 |
-
|
| 356 |
-
if self.token_level_confidence:
|
| 357 |
-
self.to_plddt_logits = LinearNoBias(token_s, num_plddt_bins)
|
| 358 |
-
self.to_resolved_logits = LinearNoBias(token_s, 2)
|
| 359 |
-
else:
|
| 360 |
-
self.to_plddt_logits = LinearNoBias(
|
| 361 |
-
token_s, num_plddt_bins * self.max_num_atoms_per_token
|
| 362 |
-
)
|
| 363 |
-
self.to_resolved_logits = LinearNoBias(token_s, 2 * self.max_num_atoms_per_token)
|
| 364 |
-
|
| 365 |
-
def forward(
|
| 366 |
-
self,
|
| 367 |
s: torch.Tensor, # (b * m, t, d_s)
|
| 368 |
z: torch.Tensor, # (b * m, t, t, d_z)
|
| 369 |
x_pred: torch.Tensor, # (b * m, a, 3)
|
| 370 |
d: torch.Tensor, # (b * m, t, t)
|
| 371 |
-
feats: dict[str, torch.Tensor],
|
| 372 |
pred_distogram_logits: torch.Tensor, # (b, t, t, n_bin)
|
| 373 |
-
multiplicity: int = 1,
|
| 374 |
-
) -> dict[str, Any]:
|
| 375 |
-
# b: base batch; m: diffusion multiplicity; t: tokens; a: atoms.
|
| 376 |
-
if self.use_separate_heads:
|
| 377 |
-
asym_id_token = feats["asym_id"] # (b, t)
|
| 378 |
-
is_same_chain = asym_id_token.unsqueeze(-1) == asym_id_token.unsqueeze(-2) # (b, t, t)
|
| 379 |
-
is_different_chain = ~is_same_chain # (b, t, t)
|
| 380 |
-
|
| 381 |
-
if self.use_separate_heads:
|
| 382 |
-
pae_intra_logits = self.to_pae_intra_logits(z) # (b * m, t, t, n_pae)
|
| 383 |
-
pae_intra_logits = pae_intra_logits * is_same_chain.float().unsqueeze(-1) # (b * m, t, t, n_pae)
|
| 384 |
-
|
| 385 |
-
pae_inter_logits = self.to_pae_inter_logits(z) # (b * m, t, t, n_pae)
|
| 386 |
-
pae_inter_logits = pae_inter_logits * is_different_chain.float().unsqueeze(-1) # (b * m, t, t, n_pae)
|
| 387 |
-
|
| 388 |
-
pae_logits = pae_inter_logits + pae_intra_logits # (b * m, t, t, n_pae)
|
| 389 |
-
else:
|
| 390 |
-
pae_logits = self.to_pae_logits(z) # (b * m, t, t, n_pae)
|
| 391 |
-
|
| 392 |
-
if self.use_separate_heads:
|
| 393 |
-
pde_intra_logits = self.to_pde_intra_logits(z + z.transpose(1, 2)) # (b * m, t, t, n_pde)
|
| 394 |
-
pde_intra_logits = pde_intra_logits * is_same_chain.float().unsqueeze(-1) # (b * m, t, t, n_pde)
|
| 395 |
-
|
| 396 |
-
pde_inter_logits = self.to_pde_inter_logits(z + z.transpose(1, 2)) # (b * m, t, t, n_pde)
|
| 397 |
-
pde_inter_logits = pde_inter_logits * is_different_chain.float().unsqueeze(-1) # (b * m, t, t, n_pde)
|
| 398 |
-
|
| 399 |
-
pde_logits = pde_inter_logits + pde_intra_logits # (b * m, t, t, n_pde)
|
| 400 |
-
else:
|
| 401 |
-
pde_logits = self.to_pde_logits(z + z.transpose(1, 2)) # (b * m, t, t, n_pde)
|
| 402 |
-
resolved_logits = self.to_resolved_logits(s) # (b * m, t, c_resolved); c_resolved is 2 or 2 * n_slot
|
| 403 |
-
plddt_logits = self.to_plddt_logits(s) # (b * m, t, c_plddt); c_plddt is n_plddt or n_slot * n_plddt
|
| 404 |
-
|
| 405 |
-
ligand_weight = 20
|
| 406 |
-
non_interface_weight = 1
|
| 407 |
-
interface_weight = 10
|
| 408 |
-
|
| 409 |
token_type = feats["mol_type"] # (b, t)
|
| 410 |
token_type = token_type.repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 411 |
-
is_ligand_token = (token_type == const.chain_type_ids["NONPOLYMER"]).float() # (b * m, t)
|
| 412 |
-
|
| 413 |
-
if self.token_level_confidence:
|
| 414 |
-
plddt = compute_aggregated_metric(plddt_logits) # (b * m, n_item); n_item is t for token confidence, a for atom confidence
|
| 415 |
-
token_pad_mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 416 |
-
complex_plddt = (plddt * token_pad_mask).sum(dim=-1) / token_pad_mask.sum(dim=-1) # (b * m,)
|
| 417 |
-
|
| 418 |
-
is_contact = (d < 8).float() # (b * m, t, t)
|
| 419 |
-
is_different_chain = (
|
| 420 |
-
feats["asym_id"].unsqueeze(-1) != feats["asym_id"].unsqueeze(-2)
|
| 421 |
-
).float() # (b, t, t)
|
| 422 |
-
is_different_chain = is_different_chain.repeat_interleave(multiplicity, 0) # (b * m, t, t)
|
| 423 |
-
token_interface_mask = torch.max(
|
| 424 |
-
is_contact * is_different_chain * (1 - is_ligand_token).unsqueeze(-1),
|
| 425 |
-
dim=-1,
|
| 426 |
-
).values # (b * m, t)
|
| 427 |
-
token_non_interface_mask = (1 - token_interface_mask) * (1 - is_ligand_token) # (b * m, t)
|
| 428 |
-
iplddt_weight = (
|
| 429 |
-
is_ligand_token * ligand_weight
|
| 430 |
-
+ token_interface_mask * interface_weight
|
| 431 |
-
+ token_non_interface_mask * non_interface_weight
|
| 432 |
-
) # (b * m, t)
|
| 433 |
-
complex_iplddt = (plddt * token_pad_mask * iplddt_weight).sum(dim=-1) / torch.sum(
|
| 434 |
-
token_pad_mask * iplddt_weight, dim=-1
|
| 435 |
-
) # (b * m,)
|
| 436 |
-
|
| 437 |
-
else:
|
| 438 |
-
# token to atom conversion for resolved logits
|
| 439 |
-
b, n, _ = resolved_logits.shape
|
| 440 |
-
resolved_logits = resolved_logits.reshape(b, n, self.max_num_atoms_per_token, 2) # (b * m, t, n_slot, 2)
|
| 441 |
-
resolved_logits = _token_slot_logits_to_atom_logits(
|
| 442 |
-
resolved_logits,
|
| 443 |
-
feats["atom_to_token"],
|
| 444 |
-
feats["atom_pad_mask"],
|
| 445 |
-
multiplicity=multiplicity,
|
| 446 |
-
) # (b * m, a, 2)
|
| 447 |
-
plddt_logits = plddt_logits.reshape(b, n, self.max_num_atoms_per_token, -1) # (b * m, t, n_slot, n_plddt)
|
| 448 |
-
plddt_logits = _token_slot_logits_to_atom_logits(
|
| 449 |
-
plddt_logits,
|
| 450 |
-
feats["atom_to_token"],
|
| 451 |
-
feats["atom_pad_mask"],
|
| 452 |
-
multiplicity=multiplicity,
|
| 453 |
-
) # (b * m, a, n_plddt)
|
| 454 |
-
atom_pad_mask = feats["atom_pad_mask"].repeat_interleave(multiplicity, 0) # (b * m, a)
|
| 455 |
-
plddt = compute_aggregated_metric(plddt_logits) # (b * m, n_item); n_item is t for token confidence, a for atom confidence
|
| 456 |
-
|
| 457 |
-
complex_plddt = (plddt * atom_pad_mask).sum(dim=-1) / atom_pad_mask.sum(dim=-1) # (b * m,)
|
| 458 |
-
atom_to_token = feats["atom_to_token"].float().repeat_interleave(
|
| 459 |
-
multiplicity,
|
| 460 |
-
0,
|
| 461 |
-
) # (b * m, a, t)
|
| 462 |
-
chain_id_token = feats["asym_id"].float().repeat_interleave(
|
| 463 |
-
multiplicity,
|
| 464 |
-
0,
|
| 465 |
-
) # (b * m, t)
|
| 466 |
-
atom_type = torch.bmm(
|
| 467 |
-
atom_to_token,
|
| 468 |
-
token_type.float().unsqueeze(-1),
|
| 469 |
-
).squeeze(-1) # (b * m, a)
|
| 470 |
-
is_ligand_atom = (atom_type == const.chain_type_ids["NONPOLYMER"]).float() # (b * m, a)
|
| 471 |
-
d_atom = torch.cdist(x_pred, x_pred) # (b * m, a, a)
|
| 472 |
-
is_contact = (d_atom < 8).float() # (b * m, a, a)
|
| 473 |
-
chain_id_atom = torch.bmm(atom_to_token, chain_id_token.unsqueeze(-1)).squeeze(-1) # (b * m, a)
|
| 474 |
-
is_different_chain = (
|
| 475 |
-
chain_id_atom.unsqueeze(-1) != chain_id_atom.unsqueeze(-2)
|
| 476 |
-
).float() # (b * m, a, a)
|
| 477 |
-
|
| 478 |
-
atom_interface_mask = torch.max(
|
| 479 |
-
is_contact * is_different_chain * (1 - is_ligand_atom).unsqueeze(-1),
|
| 480 |
-
dim=-1,
|
| 481 |
-
).values # (b * m, a)
|
| 482 |
-
atom_non_interface_mask = (1 - atom_interface_mask) * (1 - is_ligand_atom) # (b * m, a)
|
| 483 |
-
iplddt_weight = (
|
| 484 |
-
is_ligand_atom * ligand_weight
|
| 485 |
-
+ atom_interface_mask * interface_weight
|
| 486 |
-
+ atom_non_interface_mask * non_interface_weight
|
| 487 |
-
) # (b * m, a)
|
| 488 |
-
|
| 489 |
-
complex_iplddt = (plddt * atom_pad_mask * iplddt_weight).sum(dim=-1) / torch.sum(
|
| 490 |
-
atom_pad_mask * iplddt_weight,
|
| 491 |
-
dim=-1,
|
| 492 |
-
) # (b * m,)
|
| 493 |
-
|
| 494 |
-
# Compute the gPDE and giPDE
|
| 495 |
-
pde = compute_aggregated_metric(pde_logits, end=32) # (b * m, t, t)
|
| 496 |
-
pred_distogram_prob = nn.functional.softmax(
|
| 497 |
-
pred_distogram_logits, dim=-1
|
| 498 |
-
).repeat_interleave(multiplicity, 0) # (b * m, t, t, n_bin)
|
| 499 |
-
contacts = torch.zeros((1, 1, 1, 64), dtype=pred_distogram_prob.dtype).to(
|
| 500 |
-
pred_distogram_prob.device
|
| 501 |
-
) # (1, 1, 1, 64)
|
| 502 |
-
contacts[:, :, :, :20] = 1.0 # (1, 1, 1, 20) selected bins; contacts retains (1, 1, 1, 64)
|
| 503 |
-
prob_contact = (pred_distogram_prob * contacts).sum(-1) # (b * m, t, t)
|
| 504 |
-
token_pad_mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 505 |
-
token_pad_pair_mask = (
|
| 506 |
-
token_pad_mask.unsqueeze(-1)
|
| 507 |
-
* token_pad_mask.unsqueeze(-2)
|
| 508 |
-
* (1 - torch.eye(token_pad_mask.shape[1], device=token_pad_mask.device).unsqueeze(0))
|
| 509 |
-
) # (b * m, t, t)
|
| 510 |
-
token_pair_mask = token_pad_pair_mask * prob_contact # (b * m, t, t)
|
| 511 |
-
complex_pde_numerator = (pde * token_pair_mask).sum(dim=(1, 2)) # (b * m,)
|
| 512 |
-
complex_pde_denominator = token_pair_mask.sum(dim=(1, 2)) # (b * m,)
|
| 513 |
-
complex_pde = complex_pde_numerator / torch.where(
|
| 514 |
-
complex_pde_denominator > 0,
|
| 515 |
-
complex_pde_denominator,
|
| 516 |
-
torch.ones_like(complex_pde_denominator),
|
| 517 |
-
) # (b * m,)
|
| 518 |
-
asym_id = feats["asym_id"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 519 |
-
token_interface_pair_mask = token_pair_mask * (
|
| 520 |
-
asym_id.unsqueeze(-1) != asym_id.unsqueeze(-2)
|
| 521 |
-
) # (b * m, t, t)
|
| 522 |
-
complex_ipde = (pde * token_interface_pair_mask).sum(dim=(1, 2)) / (
|
| 523 |
-
token_interface_pair_mask.sum(dim=(1, 2)) + 1e-5
|
| 524 |
-
) # (b * m,)
|
| 525 |
-
out_dict = dict(
|
| 526 |
-
pde_logits=pde_logits,
|
| 527 |
-
plddt_logits=plddt_logits,
|
| 528 |
-
resolved_logits=resolved_logits,
|
| 529 |
-
pde=pde,
|
| 530 |
-
plddt=plddt,
|
| 531 |
-
complex_plddt=complex_plddt,
|
| 532 |
-
complex_iplddt=complex_iplddt,
|
| 533 |
-
complex_pde=complex_pde,
|
| 534 |
-
complex_ipde=complex_ipde,
|
| 535 |
-
)
|
| 536 |
-
out_dict["pae_logits"] = pae_logits # (b * m, t, t, n_pae)
|
| 537 |
-
out_dict["pae"] = compute_aggregated_metric(pae_logits, end=32) # (b * m, t, t)
|
| 538 |
-
|
| 539 |
-
ptm, iptm, ligand_iptm, protein_iptm, pair_chains_iptm = compute_ptms(
|
| 540 |
-
pae_logits, x_pred, feats, multiplicity
|
| 541 |
-
) # four (b * m,) scores and a mapping of chain-pair scores (b * m,)
|
| 542 |
-
out_dict["ptm"] = ptm # (b * m,)
|
| 543 |
-
out_dict["iptm"] = iptm # (b * m,)
|
| 544 |
-
out_dict["ligand_iptm"] = ligand_iptm # (b * m,)
|
| 545 |
-
out_dict["protein_iptm"] = protein_iptm # (b * m,)
|
| 546 |
-
out_dict["pair_chains_iptm"] = pair_chains_iptm # each chain-pair tensor: (b * m,)
|
| 547 |
-
|
| 548 |
-
return out_dict
|
|
|
|
| 1 |
+
"""Predict atom and token confidence from Boltz2 sequence and pair states."""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from typing import Any
|
| 6 |
+
from torch import nn
|
| 7 |
+
|
| 8 |
+
from . import vb_const as const
|
| 9 |
+
from . import vb_layers_initialize as init
|
| 10 |
+
from .vb_layers_confidence_utils import (
|
| 11 |
+
compute_aggregated_metric,
|
| 12 |
+
compute_ptms,
|
| 13 |
+
)
|
| 14 |
+
from .vb_layers_pairformer import PairformerModule
|
| 15 |
+
from .vb_modules_encodersv2 import RelativePositionEncoder
|
| 16 |
+
from .vb_modules_trunkv2 import (
|
| 17 |
+
ContactConditioning,
|
| 18 |
+
)
|
| 19 |
+
from .vb_modules_utils import LinearNoBias
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _token_slot_logits_to_atom_logits(
|
| 23 |
+
token_logits: torch.Tensor,
|
| 24 |
+
atom_to_token: torch.Tensor,
|
| 25 |
+
atom_pad_mask: torch.Tensor,
|
| 26 |
+
*,
|
| 27 |
+
multiplicity: int,
|
| 28 |
+
) -> torch.Tensor:
|
| 29 |
+
"""Gather per-token atom-slot logits onto each example's atom table.
|
| 30 |
+
|
| 31 |
+
``token_logits`` is ordered as ``(batch * multiplicity, token, slot,
|
| 32 |
+
channel)``. ``atom_to_token`` determines both the owning token and the
|
| 33 |
+
within-token slot of every atom, so heterogeneous atom counts do not leak
|
| 34 |
+
across batch rows.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
# b: batch; m: multiplicity; a: atoms; t: tokens; c: channels.
|
| 38 |
+
if token_logits.ndim != 4:
|
| 39 |
+
raise ValueError(
|
| 40 |
+
"token_logits must have shape (batch * multiplicity, token, slot, channel), "
|
| 41 |
+
f"got {tuple(token_logits.shape)}."
|
| 42 |
+
)
|
| 43 |
+
if atom_to_token.ndim != 3:
|
| 44 |
+
raise ValueError(
|
| 45 |
+
"atom_to_token must have shape (batch, atom, token), "
|
| 46 |
+
f"got {tuple(atom_to_token.shape)}."
|
| 47 |
+
)
|
| 48 |
+
if atom_pad_mask.shape != atom_to_token.shape[:2]:
|
| 49 |
+
raise ValueError(
|
| 50 |
+
"atom_pad_mask must match the batch and atom axes of atom_to_token; "
|
| 51 |
+
f"got {tuple(atom_pad_mask.shape)} and {tuple(atom_to_token.shape)}."
|
| 52 |
+
)
|
| 53 |
+
if multiplicity < 1:
|
| 54 |
+
raise ValueError(f"multiplicity must be positive, got {multiplicity}.")
|
| 55 |
+
|
| 56 |
+
batch_size, atom_count, token_count = atom_to_token.shape
|
| 57 |
+
if token_logits.shape[0] != batch_size * multiplicity:
|
| 58 |
+
raise ValueError(
|
| 59 |
+
"token_logits batch axis must equal batch * multiplicity; "
|
| 60 |
+
f"got {token_logits.shape[0]} and {batch_size} * {multiplicity}."
|
| 61 |
+
)
|
| 62 |
+
if token_logits.shape[1] != token_count:
|
| 63 |
+
raise ValueError(
|
| 64 |
+
"token_logits and atom_to_token disagree on token count; "
|
| 65 |
+
f"got {token_logits.shape[1]} and {token_count}."
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
valid_atoms = atom_pad_mask.bool() # (b, a)
|
| 69 |
+
assignments = atom_to_token.bool() & valid_atoms.unsqueeze(-1) # (b, a, t)
|
| 70 |
+
token_index = assignments.to(dtype=torch.int64).argmax(dim=-1) # (b, a)
|
| 71 |
+
# Cumulative one-hot counts give each atom its ordinal within its owning
|
| 72 |
+
# token without assuming that atoms from different tokens are contiguous.
|
| 73 |
+
cumulative_slots = assignments.to(dtype=torch.int64).cumsum(dim=1) - 1 # (b, a, t)
|
| 74 |
+
slot_index = (cumulative_slots * assignments).sum(dim=-1) # (b, a)
|
| 75 |
+
|
| 76 |
+
slots_per_token = token_logits.shape[2]
|
| 77 |
+
flattened_index = token_index * slots_per_token + slot_index # (b, a)
|
| 78 |
+
flattened_index = flattened_index.masked_fill(~valid_atoms, 0) # (b, a)
|
| 79 |
+
flattened_index = flattened_index.repeat_interleave(multiplicity, dim=0) # (b * m, a)
|
| 80 |
+
expanded_atom_mask = valid_atoms.repeat_interleave(multiplicity, dim=0) # (b * m, a)
|
| 81 |
+
|
| 82 |
+
flattened_logits = token_logits.flatten(1, 2) # (b * m, t * n_slot, c)
|
| 83 |
+
gather_index = flattened_index.unsqueeze(-1).expand(
|
| 84 |
+
-1,
|
| 85 |
+
atom_count,
|
| 86 |
+
flattened_logits.shape[-1],
|
| 87 |
+
) # (b * m, a, c)
|
| 88 |
+
atom_logits = torch.gather(flattened_logits, dim=1, index=gather_index) # (b * m, a, c)
|
| 89 |
+
return atom_logits * expanded_atom_mask.unsqueeze(-1).to(dtype=atom_logits.dtype) # (b * m, a, c)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class ConfidenceModule(nn.Module):
|
| 93 |
+
"""Update confidence features with predicted geometry and a pairformer stack."""
|
| 94 |
+
|
| 95 |
+
def __init__(
|
| 96 |
+
self,
|
| 97 |
+
token_s: int,
|
| 98 |
+
token_z: int,
|
| 99 |
+
pairformer_args: dict[str, Any],
|
| 100 |
+
num_dist_bins: int = 64,
|
| 101 |
+
token_level_confidence: bool = True,
|
| 102 |
+
max_dist: float = 22,
|
| 103 |
+
add_s_to_z_prod: bool = False,
|
| 104 |
+
add_s_input_to_s: bool = False,
|
| 105 |
+
add_z_input_to_z: bool = False,
|
| 106 |
+
maximum_bond_distance: int = 0,
|
| 107 |
+
bond_type_feature: bool = False,
|
| 108 |
+
confidence_args: dict[str, Any] | None = None,
|
| 109 |
+
compile_pairformer: bool = False,
|
| 110 |
+
fix_sym_check: bool = False,
|
| 111 |
+
cyclic_pos_enc: bool = False,
|
| 112 |
+
return_latent_feats: bool = False,
|
| 113 |
+
conditioning_cutoff_min: float | None = None,
|
| 114 |
+
conditioning_cutoff_max: float | None = None,
|
| 115 |
+
**kwargs: Any,
|
| 116 |
+
) -> None:
|
| 117 |
+
super().__init__()
|
| 118 |
+
self.max_num_atoms_per_token = 23
|
| 119 |
+
self.no_update_s = pairformer_args.get("no_update_s", False)
|
| 120 |
+
boundaries = torch.linspace(2, max_dist, num_dist_bins - 1) # (n_dist_bin - 1,)
|
| 121 |
+
self.register_buffer("boundaries", boundaries)
|
| 122 |
+
self.dist_bin_pairwise_embed = nn.Embedding(num_dist_bins, token_z)
|
| 123 |
+
init.gating_init_(self.dist_bin_pairwise_embed.weight)
|
| 124 |
+
self.token_level_confidence = token_level_confidence
|
| 125 |
+
|
| 126 |
+
self.s_to_z = LinearNoBias(token_s, token_z)
|
| 127 |
+
self.s_to_z_transpose = LinearNoBias(token_s, token_z)
|
| 128 |
+
init.gating_init_(self.s_to_z.weight)
|
| 129 |
+
init.gating_init_(self.s_to_z_transpose.weight)
|
| 130 |
+
|
| 131 |
+
self.add_s_to_z_prod = add_s_to_z_prod
|
| 132 |
+
if add_s_to_z_prod:
|
| 133 |
+
self.s_to_z_prod_in1 = LinearNoBias(token_s, token_z)
|
| 134 |
+
self.s_to_z_prod_in2 = LinearNoBias(token_s, token_z)
|
| 135 |
+
self.s_to_z_prod_out = LinearNoBias(token_z, token_z)
|
| 136 |
+
init.gating_init_(self.s_to_z_prod_out.weight)
|
| 137 |
+
|
| 138 |
+
self.s_inputs_norm = nn.LayerNorm(token_s)
|
| 139 |
+
if not self.no_update_s:
|
| 140 |
+
self.s_norm = nn.LayerNorm(token_s)
|
| 141 |
+
self.z_norm = nn.LayerNorm(token_z)
|
| 142 |
+
|
| 143 |
+
self.add_s_input_to_s = add_s_input_to_s
|
| 144 |
+
if add_s_input_to_s:
|
| 145 |
+
self.s_input_to_s = LinearNoBias(token_s, token_s)
|
| 146 |
+
init.gating_init_(self.s_input_to_s.weight)
|
| 147 |
+
|
| 148 |
+
self.add_z_input_to_z = add_z_input_to_z
|
| 149 |
+
if add_z_input_to_z:
|
| 150 |
+
self.rel_pos = RelativePositionEncoder(
|
| 151 |
+
token_z, fix_sym_check=fix_sym_check, cyclic_pos_enc=cyclic_pos_enc
|
| 152 |
+
)
|
| 153 |
+
self.token_bonds = nn.Linear(
|
| 154 |
+
1 if maximum_bond_distance == 0 else maximum_bond_distance + 2,
|
| 155 |
+
token_z,
|
| 156 |
+
bias=False,
|
| 157 |
+
)
|
| 158 |
+
self.bond_type_feature = bond_type_feature
|
| 159 |
+
if bond_type_feature:
|
| 160 |
+
self.token_bonds_type = nn.Embedding(len(const.bond_types) + 1, token_z)
|
| 161 |
+
|
| 162 |
+
self.contact_conditioning = ContactConditioning(
|
| 163 |
+
token_z=token_z,
|
| 164 |
+
cutoff_min=conditioning_cutoff_min,
|
| 165 |
+
cutoff_max=conditioning_cutoff_max,
|
| 166 |
+
)
|
| 167 |
+
pairformer_args["v2"] = True
|
| 168 |
+
self.pairformer_stack = PairformerModule(
|
| 169 |
+
token_s,
|
| 170 |
+
token_z,
|
| 171 |
+
**pairformer_args,
|
| 172 |
+
)
|
| 173 |
+
self.return_latent_feats = return_latent_feats
|
| 174 |
+
|
| 175 |
+
self.confidence_heads = ConfidenceHeads(
|
| 176 |
+
token_s,
|
| 177 |
+
token_z,
|
| 178 |
+
token_level_confidence=token_level_confidence,
|
| 179 |
+
**confidence_args,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
def forward(
|
| 183 |
+
self,
|
| 184 |
+
s_inputs: torch.Tensor, # (b, t, d_s)
|
| 185 |
s: torch.Tensor, # (b, t, d_s)
|
| 186 |
z: torch.Tensor, # (b, t, t, d_z)
|
| 187 |
+
x_pred: torch.Tensor, # (b * m, a, 3) or (b, m, a, 3)
|
| 188 |
+
feats: dict[str, torch.Tensor],
|
| 189 |
pred_distogram_logits: torch.Tensor, # (b, t, t, n_bin)
|
| 190 |
+
multiplicity: int = 1,
|
| 191 |
+
run_sequentially: bool = False,
|
| 192 |
+
use_kernels: bool = False,
|
| 193 |
+
) -> dict[str, Any]:
|
| 194 |
+
# b: base batch; m: diffusion multiplicity; t: tokens; a: atoms.
|
| 195 |
+
if run_sequentially and multiplicity > 1:
|
| 196 |
+
batch_size = z.shape[0]
|
| 197 |
+
expected_shape = (batch_size, multiplicity)
|
| 198 |
+
if x_pred.ndim >= 4 and x_pred.shape[:2] == expected_shape:
|
| 199 |
+
sample_coordinates = x_pred # (b, m, a, 3)
|
| 200 |
+
elif x_pred.shape[0] == batch_size * multiplicity:
|
| 201 |
+
sample_coordinates = x_pred.reshape(
|
| 202 |
+
batch_size,
|
| 203 |
+
multiplicity,
|
| 204 |
+
*x_pred.shape[1:],
|
| 205 |
+
) # (b, m, a, 3)
|
| 206 |
+
else:
|
| 207 |
+
raise ValueError(
|
| 208 |
+
"Sequential confidence expected coordinates with leading shape "
|
| 209 |
+
f"{expected_shape} or {batch_size * multiplicity}, got "
|
| 210 |
+
f"{tuple(x_pred.shape)}."
|
| 211 |
+
)
|
| 212 |
+
out_dicts = []
|
| 213 |
+
for sample_idx in range(multiplicity):
|
| 214 |
+
out_dicts.append(
|
| 215 |
+
self.forward(
|
| 216 |
+
s_inputs,
|
| 217 |
+
s,
|
| 218 |
+
z,
|
| 219 |
+
sample_coordinates[:, sample_idx],
|
| 220 |
+
feats,
|
| 221 |
+
pred_distogram_logits,
|
| 222 |
+
multiplicity=1,
|
| 223 |
+
run_sequentially=False,
|
| 224 |
+
use_kernels=use_kernels,
|
| 225 |
+
)
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
out_dict = {}
|
| 229 |
+
for key in out_dicts[0]:
|
| 230 |
+
if key != "pair_chains_iptm":
|
| 231 |
+
values = [out[key] for out in out_dicts] # list of sample tensors, each (b, *feature_shape)
|
| 232 |
+
out_dict[key] = torch.stack(values, dim=1).flatten(0, 1) # (b * m, *feature_shape)
|
| 233 |
+
else:
|
| 234 |
+
pair_chains_iptm = {}
|
| 235 |
+
for chain_idx1 in out_dicts[0][key]:
|
| 236 |
+
chains_iptm = {}
|
| 237 |
+
for chain_idx2 in out_dicts[0][key][chain_idx1]:
|
| 238 |
+
values = [
|
| 239 |
+
out[key][chain_idx1][chain_idx2]
|
| 240 |
+
for out in out_dicts
|
| 241 |
] # list of sample chain-pair scores, each (b,)
|
| 242 |
+
chains_iptm[chain_idx2] = torch.stack(
|
| 243 |
+
values,
|
| 244 |
+
dim=1,
|
| 245 |
+
).flatten(0, 1) # (b * m,)
|
| 246 |
+
pair_chains_iptm[chain_idx1] = chains_iptm
|
| 247 |
+
out_dict[key] = pair_chains_iptm # mapping of chain-pair scores, each (b * m,)
|
| 248 |
+
return out_dict
|
| 249 |
+
|
| 250 |
+
s_inputs = self.s_inputs_norm(s_inputs) # (b, t, d_s)
|
| 251 |
+
if not self.no_update_s:
|
| 252 |
+
s = self.s_norm(s) # (b, t, d_s)
|
| 253 |
+
|
| 254 |
+
if self.add_s_input_to_s:
|
| 255 |
+
s = s + self.s_input_to_s(s_inputs) # (b, t, d_s)
|
| 256 |
+
|
| 257 |
+
z = self.z_norm(z) # (b, t, t, d_z)
|
| 258 |
+
|
| 259 |
+
if self.add_z_input_to_z:
|
| 260 |
+
relative_position_encoding = self.rel_pos(feats) # (b, t, t, d_z)
|
| 261 |
+
z = z + relative_position_encoding # (b, t, t, d_z)
|
| 262 |
+
z = z + self.token_bonds(feats["token_bonds"].float()) # (b, t, t, d_z)
|
| 263 |
+
if self.bond_type_feature:
|
| 264 |
+
z = z + self.token_bonds_type(feats["type_bonds"].long()) # (b, t, t, d_z)
|
| 265 |
+
z = z + self.contact_conditioning(feats) # (b, t, t, d_z)
|
| 266 |
+
|
| 267 |
+
s = s.repeat_interleave(multiplicity, 0) # (b * m, t, d_s)
|
| 268 |
+
|
| 269 |
+
z = (
|
| 270 |
+
z
|
| 271 |
+
+ self.s_to_z(s_inputs)[:, :, None, :]
|
| 272 |
+
+ self.s_to_z_transpose(s_inputs)[:, None, :, :]
|
| 273 |
+
) # (b, t, t, d_z)
|
| 274 |
+
if self.add_s_to_z_prod:
|
| 275 |
+
z = z + self.s_to_z_prod_out(
|
| 276 |
+
self.s_to_z_prod_in1(s_inputs)[:, :, None, :]
|
| 277 |
+
* self.s_to_z_prod_in2(s_inputs)[:, None, :, :]
|
| 278 |
+
) # (b, t, t, d_z)
|
| 279 |
+
|
| 280 |
+
z = z.repeat_interleave(multiplicity, 0) # (b * m, t, t, d_z)
|
| 281 |
+
s_inputs = s_inputs.repeat_interleave(multiplicity, 0) # (b * m, t, d_s)
|
| 282 |
+
|
| 283 |
token_to_rep_atom = feats["token_to_rep_atom"] # (b, t, a)
|
| 284 |
token_to_rep_atom = token_to_rep_atom.repeat_interleave(multiplicity, 0) # (b * m, t, a)
|
| 285 |
+
if len(x_pred.shape) == 4:
|
| 286 |
+
b, multiplicity, n, _ = x_pred.shape
|
| 287 |
+
x_pred = x_pred.reshape(b * multiplicity, n, -1) # (b * m, a, 3)
|
| 288 |
+
else:
|
| 289 |
+
_, n, _ = x_pred.shape
|
| 290 |
+
x_pred_repr = torch.bmm(token_to_rep_atom.float(), x_pred) # (b * m, t, 3)
|
| 291 |
+
d = torch.cdist(x_pred_repr, x_pred_repr) # (b * m, t, t)
|
| 292 |
+
distogram = (d.unsqueeze(-1) > self.boundaries).sum(dim=-1).long() # (b * m, t, t)
|
| 293 |
+
distogram = self.dist_bin_pairwise_embed(distogram) # (b * m, t, t, d_z)
|
| 294 |
z = z + distogram # (b * m, t, t, d_z)
|
| 295 |
+
|
| 296 |
+
mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 297 |
+
pair_mask = mask[:, :, None] * mask[:, None, :] # (b * m, t, t)
|
| 298 |
+
|
| 299 |
+
s_t, z_t = self.pairformer_stack(
|
| 300 |
+
s, z, mask=mask, pair_mask=pair_mask, use_kernels=use_kernels
|
| 301 |
+
) # (b * m, t, d_s), (b * m, t, t, d_z)
|
| 302 |
+
|
| 303 |
+
# Confidence heads consume the pairformer states without an additional residual.
|
| 304 |
+
s = s_t # (b * m, t, d_s)
|
| 305 |
+
z = z_t # (b * m, t, t, d_z)
|
| 306 |
+
|
| 307 |
+
out_dict = {}
|
| 308 |
+
|
| 309 |
+
if self.return_latent_feats:
|
| 310 |
+
out_dict["s_conf"] = s # (b * m, t, d_s)
|
| 311 |
+
out_dict["z_conf"] = z # (b * m, t, t, d_z)
|
| 312 |
+
|
| 313 |
+
out_dict.update(
|
| 314 |
+
self.confidence_heads(
|
| 315 |
+
s=s,
|
| 316 |
+
z=z,
|
| 317 |
+
x_pred=x_pred,
|
| 318 |
+
d=d,
|
| 319 |
+
feats=feats,
|
| 320 |
+
multiplicity=multiplicity,
|
| 321 |
+
pred_distogram_logits=pred_distogram_logits,
|
| 322 |
+
)
|
| 323 |
+
)
|
| 324 |
+
return out_dict
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
class ConfidenceHeads(nn.Module):
|
| 328 |
+
def __init__(
|
| 329 |
+
self,
|
| 330 |
+
token_s: int,
|
| 331 |
+
token_z: int,
|
| 332 |
+
num_plddt_bins: int = 50,
|
| 333 |
+
num_pde_bins: int = 64,
|
| 334 |
+
num_pae_bins: int = 64,
|
| 335 |
+
token_level_confidence: bool = True,
|
| 336 |
+
use_separate_heads: bool = False,
|
| 337 |
+
**kwargs: Any,
|
| 338 |
+
) -> None:
|
| 339 |
+
super().__init__()
|
| 340 |
+
self.max_num_atoms_per_token = 23
|
| 341 |
+
self.token_level_confidence = token_level_confidence
|
| 342 |
+
self.use_separate_heads = use_separate_heads
|
| 343 |
+
|
| 344 |
+
if self.use_separate_heads:
|
| 345 |
+
self.to_pae_intra_logits = LinearNoBias(token_z, num_pae_bins)
|
| 346 |
+
self.to_pae_inter_logits = LinearNoBias(token_z, num_pae_bins)
|
| 347 |
+
else:
|
| 348 |
+
self.to_pae_logits = LinearNoBias(token_z, num_pae_bins)
|
| 349 |
+
|
| 350 |
+
if self.use_separate_heads:
|
| 351 |
+
self.to_pde_intra_logits = LinearNoBias(token_z, num_pde_bins)
|
| 352 |
+
self.to_pde_inter_logits = LinearNoBias(token_z, num_pde_bins)
|
| 353 |
+
else:
|
| 354 |
+
self.to_pde_logits = LinearNoBias(token_z, num_pde_bins)
|
| 355 |
+
|
| 356 |
+
if self.token_level_confidence:
|
| 357 |
+
self.to_plddt_logits = LinearNoBias(token_s, num_plddt_bins)
|
| 358 |
+
self.to_resolved_logits = LinearNoBias(token_s, 2)
|
| 359 |
+
else:
|
| 360 |
+
self.to_plddt_logits = LinearNoBias(
|
| 361 |
+
token_s, num_plddt_bins * self.max_num_atoms_per_token
|
| 362 |
+
)
|
| 363 |
+
self.to_resolved_logits = LinearNoBias(token_s, 2 * self.max_num_atoms_per_token)
|
| 364 |
+
|
| 365 |
+
def forward(
|
| 366 |
+
self,
|
| 367 |
s: torch.Tensor, # (b * m, t, d_s)
|
| 368 |
z: torch.Tensor, # (b * m, t, t, d_z)
|
| 369 |
x_pred: torch.Tensor, # (b * m, a, 3)
|
| 370 |
d: torch.Tensor, # (b * m, t, t)
|
| 371 |
+
feats: dict[str, torch.Tensor],
|
| 372 |
pred_distogram_logits: torch.Tensor, # (b, t, t, n_bin)
|
| 373 |
+
multiplicity: int = 1,
|
| 374 |
+
) -> dict[str, Any]:
|
| 375 |
+
# b: base batch; m: diffusion multiplicity; t: tokens; a: atoms.
|
| 376 |
+
if self.use_separate_heads:
|
| 377 |
+
asym_id_token = feats["asym_id"] # (b, t)
|
| 378 |
+
is_same_chain = asym_id_token.unsqueeze(-1) == asym_id_token.unsqueeze(-2) # (b, t, t)
|
| 379 |
+
is_different_chain = ~is_same_chain # (b, t, t)
|
| 380 |
+
|
| 381 |
+
if self.use_separate_heads:
|
| 382 |
+
pae_intra_logits = self.to_pae_intra_logits(z) # (b * m, t, t, n_pae)
|
| 383 |
+
pae_intra_logits = pae_intra_logits * is_same_chain.float().unsqueeze(-1) # (b * m, t, t, n_pae)
|
| 384 |
+
|
| 385 |
+
pae_inter_logits = self.to_pae_inter_logits(z) # (b * m, t, t, n_pae)
|
| 386 |
+
pae_inter_logits = pae_inter_logits * is_different_chain.float().unsqueeze(-1) # (b * m, t, t, n_pae)
|
| 387 |
+
|
| 388 |
+
pae_logits = pae_inter_logits + pae_intra_logits # (b * m, t, t, n_pae)
|
| 389 |
+
else:
|
| 390 |
+
pae_logits = self.to_pae_logits(z) # (b * m, t, t, n_pae)
|
| 391 |
+
|
| 392 |
+
if self.use_separate_heads:
|
| 393 |
+
pde_intra_logits = self.to_pde_intra_logits(z + z.transpose(1, 2)) # (b * m, t, t, n_pde)
|
| 394 |
+
pde_intra_logits = pde_intra_logits * is_same_chain.float().unsqueeze(-1) # (b * m, t, t, n_pde)
|
| 395 |
+
|
| 396 |
+
pde_inter_logits = self.to_pde_inter_logits(z + z.transpose(1, 2)) # (b * m, t, t, n_pde)
|
| 397 |
+
pde_inter_logits = pde_inter_logits * is_different_chain.float().unsqueeze(-1) # (b * m, t, t, n_pde)
|
| 398 |
+
|
| 399 |
+
pde_logits = pde_inter_logits + pde_intra_logits # (b * m, t, t, n_pde)
|
| 400 |
+
else:
|
| 401 |
+
pde_logits = self.to_pde_logits(z + z.transpose(1, 2)) # (b * m, t, t, n_pde)
|
| 402 |
+
resolved_logits = self.to_resolved_logits(s) # (b * m, t, c_resolved); c_resolved is 2 or 2 * n_slot
|
| 403 |
+
plddt_logits = self.to_plddt_logits(s) # (b * m, t, c_plddt); c_plddt is n_plddt or n_slot * n_plddt
|
| 404 |
+
|
| 405 |
+
ligand_weight = 20
|
| 406 |
+
non_interface_weight = 1
|
| 407 |
+
interface_weight = 10
|
| 408 |
+
|
| 409 |
token_type = feats["mol_type"] # (b, t)
|
| 410 |
token_type = token_type.repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 411 |
+
is_ligand_token = (token_type == const.chain_type_ids["NONPOLYMER"]).float() # (b * m, t)
|
| 412 |
+
|
| 413 |
+
if self.token_level_confidence:
|
| 414 |
+
plddt = compute_aggregated_metric(plddt_logits) # (b * m, n_item); n_item is t for token confidence, a for atom confidence
|
| 415 |
+
token_pad_mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 416 |
+
complex_plddt = (plddt * token_pad_mask).sum(dim=-1) / token_pad_mask.sum(dim=-1) # (b * m,)
|
| 417 |
+
|
| 418 |
+
is_contact = (d < 8).float() # (b * m, t, t)
|
| 419 |
+
is_different_chain = (
|
| 420 |
+
feats["asym_id"].unsqueeze(-1) != feats["asym_id"].unsqueeze(-2)
|
| 421 |
+
).float() # (b, t, t)
|
| 422 |
+
is_different_chain = is_different_chain.repeat_interleave(multiplicity, 0) # (b * m, t, t)
|
| 423 |
+
token_interface_mask = torch.max(
|
| 424 |
+
is_contact * is_different_chain * (1 - is_ligand_token).unsqueeze(-1),
|
| 425 |
+
dim=-1,
|
| 426 |
+
).values # (b * m, t)
|
| 427 |
+
token_non_interface_mask = (1 - token_interface_mask) * (1 - is_ligand_token) # (b * m, t)
|
| 428 |
+
iplddt_weight = (
|
| 429 |
+
is_ligand_token * ligand_weight
|
| 430 |
+
+ token_interface_mask * interface_weight
|
| 431 |
+
+ token_non_interface_mask * non_interface_weight
|
| 432 |
+
) # (b * m, t)
|
| 433 |
+
complex_iplddt = (plddt * token_pad_mask * iplddt_weight).sum(dim=-1) / torch.sum(
|
| 434 |
+
token_pad_mask * iplddt_weight, dim=-1
|
| 435 |
+
) # (b * m,)
|
| 436 |
+
|
| 437 |
+
else:
|
| 438 |
+
# token to atom conversion for resolved logits
|
| 439 |
+
b, n, _ = resolved_logits.shape
|
| 440 |
+
resolved_logits = resolved_logits.reshape(b, n, self.max_num_atoms_per_token, 2) # (b * m, t, n_slot, 2)
|
| 441 |
+
resolved_logits = _token_slot_logits_to_atom_logits(
|
| 442 |
+
resolved_logits,
|
| 443 |
+
feats["atom_to_token"],
|
| 444 |
+
feats["atom_pad_mask"],
|
| 445 |
+
multiplicity=multiplicity,
|
| 446 |
+
) # (b * m, a, 2)
|
| 447 |
+
plddt_logits = plddt_logits.reshape(b, n, self.max_num_atoms_per_token, -1) # (b * m, t, n_slot, n_plddt)
|
| 448 |
+
plddt_logits = _token_slot_logits_to_atom_logits(
|
| 449 |
+
plddt_logits,
|
| 450 |
+
feats["atom_to_token"],
|
| 451 |
+
feats["atom_pad_mask"],
|
| 452 |
+
multiplicity=multiplicity,
|
| 453 |
+
) # (b * m, a, n_plddt)
|
| 454 |
+
atom_pad_mask = feats["atom_pad_mask"].repeat_interleave(multiplicity, 0) # (b * m, a)
|
| 455 |
+
plddt = compute_aggregated_metric(plddt_logits) # (b * m, n_item); n_item is t for token confidence, a for atom confidence
|
| 456 |
+
|
| 457 |
+
complex_plddt = (plddt * atom_pad_mask).sum(dim=-1) / atom_pad_mask.sum(dim=-1) # (b * m,)
|
| 458 |
+
atom_to_token = feats["atom_to_token"].float().repeat_interleave(
|
| 459 |
+
multiplicity,
|
| 460 |
+
0,
|
| 461 |
+
) # (b * m, a, t)
|
| 462 |
+
chain_id_token = feats["asym_id"].float().repeat_interleave(
|
| 463 |
+
multiplicity,
|
| 464 |
+
0,
|
| 465 |
+
) # (b * m, t)
|
| 466 |
+
atom_type = torch.bmm(
|
| 467 |
+
atom_to_token,
|
| 468 |
+
token_type.float().unsqueeze(-1),
|
| 469 |
+
).squeeze(-1) # (b * m, a)
|
| 470 |
+
is_ligand_atom = (atom_type == const.chain_type_ids["NONPOLYMER"]).float() # (b * m, a)
|
| 471 |
+
d_atom = torch.cdist(x_pred, x_pred) # (b * m, a, a)
|
| 472 |
+
is_contact = (d_atom < 8).float() # (b * m, a, a)
|
| 473 |
+
chain_id_atom = torch.bmm(atom_to_token, chain_id_token.unsqueeze(-1)).squeeze(-1) # (b * m, a)
|
| 474 |
+
is_different_chain = (
|
| 475 |
+
chain_id_atom.unsqueeze(-1) != chain_id_atom.unsqueeze(-2)
|
| 476 |
+
).float() # (b * m, a, a)
|
| 477 |
+
|
| 478 |
+
atom_interface_mask = torch.max(
|
| 479 |
+
is_contact * is_different_chain * (1 - is_ligand_atom).unsqueeze(-1),
|
| 480 |
+
dim=-1,
|
| 481 |
+
).values # (b * m, a)
|
| 482 |
+
atom_non_interface_mask = (1 - atom_interface_mask) * (1 - is_ligand_atom) # (b * m, a)
|
| 483 |
+
iplddt_weight = (
|
| 484 |
+
is_ligand_atom * ligand_weight
|
| 485 |
+
+ atom_interface_mask * interface_weight
|
| 486 |
+
+ atom_non_interface_mask * non_interface_weight
|
| 487 |
+
) # (b * m, a)
|
| 488 |
+
|
| 489 |
+
complex_iplddt = (plddt * atom_pad_mask * iplddt_weight).sum(dim=-1) / torch.sum(
|
| 490 |
+
atom_pad_mask * iplddt_weight,
|
| 491 |
+
dim=-1,
|
| 492 |
+
) # (b * m,)
|
| 493 |
+
|
| 494 |
+
# Compute the gPDE and giPDE
|
| 495 |
+
pde = compute_aggregated_metric(pde_logits, end=32) # (b * m, t, t)
|
| 496 |
+
pred_distogram_prob = nn.functional.softmax(
|
| 497 |
+
pred_distogram_logits, dim=-1
|
| 498 |
+
).repeat_interleave(multiplicity, 0) # (b * m, t, t, n_bin)
|
| 499 |
+
contacts = torch.zeros((1, 1, 1, 64), dtype=pred_distogram_prob.dtype).to(
|
| 500 |
+
pred_distogram_prob.device
|
| 501 |
+
) # (1, 1, 1, 64)
|
| 502 |
+
contacts[:, :, :, :20] = 1.0 # (1, 1, 1, 20) selected bins; contacts retains (1, 1, 1, 64)
|
| 503 |
+
prob_contact = (pred_distogram_prob * contacts).sum(-1) # (b * m, t, t)
|
| 504 |
+
token_pad_mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 505 |
+
token_pad_pair_mask = (
|
| 506 |
+
token_pad_mask.unsqueeze(-1)
|
| 507 |
+
* token_pad_mask.unsqueeze(-2)
|
| 508 |
+
* (1 - torch.eye(token_pad_mask.shape[1], device=token_pad_mask.device).unsqueeze(0))
|
| 509 |
+
) # (b * m, t, t)
|
| 510 |
+
token_pair_mask = token_pad_pair_mask * prob_contact # (b * m, t, t)
|
| 511 |
+
complex_pde_numerator = (pde * token_pair_mask).sum(dim=(1, 2)) # (b * m,)
|
| 512 |
+
complex_pde_denominator = token_pair_mask.sum(dim=(1, 2)) # (b * m,)
|
| 513 |
+
complex_pde = complex_pde_numerator / torch.where(
|
| 514 |
+
complex_pde_denominator > 0,
|
| 515 |
+
complex_pde_denominator,
|
| 516 |
+
torch.ones_like(complex_pde_denominator),
|
| 517 |
+
) # (b * m,)
|
| 518 |
+
asym_id = feats["asym_id"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 519 |
+
token_interface_pair_mask = token_pair_mask * (
|
| 520 |
+
asym_id.unsqueeze(-1) != asym_id.unsqueeze(-2)
|
| 521 |
+
) # (b * m, t, t)
|
| 522 |
+
complex_ipde = (pde * token_interface_pair_mask).sum(dim=(1, 2)) / (
|
| 523 |
+
token_interface_pair_mask.sum(dim=(1, 2)) + 1e-5
|
| 524 |
+
) # (b * m,)
|
| 525 |
+
out_dict = dict(
|
| 526 |
+
pde_logits=pde_logits,
|
| 527 |
+
plddt_logits=plddt_logits,
|
| 528 |
+
resolved_logits=resolved_logits,
|
| 529 |
+
pde=pde,
|
| 530 |
+
plddt=plddt,
|
| 531 |
+
complex_plddt=complex_plddt,
|
| 532 |
+
complex_iplddt=complex_iplddt,
|
| 533 |
+
complex_pde=complex_pde,
|
| 534 |
+
complex_ipde=complex_ipde,
|
| 535 |
+
)
|
| 536 |
+
out_dict["pae_logits"] = pae_logits # (b * m, t, t, n_pae)
|
| 537 |
+
out_dict["pae"] = compute_aggregated_metric(pae_logits, end=32) # (b * m, t, t)
|
| 538 |
+
|
| 539 |
+
ptm, iptm, ligand_iptm, protein_iptm, pair_chains_iptm = compute_ptms(
|
| 540 |
+
pae_logits, x_pred, feats, multiplicity
|
| 541 |
+
) # four (b * m,) scores and a mapping of chain-pair scores (b * m,)
|
| 542 |
+
out_dict["ptm"] = ptm # (b * m,)
|
| 543 |
+
out_dict["iptm"] = iptm # (b * m,)
|
| 544 |
+
out_dict["ligand_iptm"] = ligand_iptm # (b * m,)
|
| 545 |
+
out_dict["protein_iptm"] = protein_iptm # (b * m,)
|
| 546 |
+
out_dict["pair_chains_iptm"] = pair_chains_iptm # each chain-pair tensor: (b * m,)
|
| 547 |
+
|
| 548 |
+
return out_dict
|
fastplms/models/boltz/vb_modules_diffusion_conditioning.py
CHANGED
|
@@ -1,147 +1,147 @@
|
|
| 1 |
-
"""Precompute pair and atom biases used by the Boltz2 diffusion stack."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from collections.abc import Callable
|
| 8 |
-
from torch import nn
|
| 9 |
-
|
| 10 |
-
from .vb_modules_encodersv2 import AtomEncoder, PairwiseConditioning
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
def _bias_projections(
|
| 14 |
-
depth: int,
|
| 15 |
-
input_dim: int,
|
| 16 |
-
num_heads: int,
|
| 17 |
-
) -> nn.ModuleList:
|
| 18 |
-
"""Build one normalized, bias-free projection per transformer block."""
|
| 19 |
-
|
| 20 |
-
return nn.ModuleList(
|
| 21 |
-
[
|
| 22 |
-
nn.Sequential(
|
| 23 |
-
nn.LayerNorm(input_dim),
|
| 24 |
-
nn.Linear(input_dim, num_heads, bias=False),
|
| 25 |
-
)
|
| 26 |
-
for _ in range(depth)
|
| 27 |
-
]
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def _concatenate_biases(
|
| 32 |
-
projections: nn.ModuleList,
|
| 33 |
-
pair_features: torch.Tensor,
|
| 34 |
-
) -> torch.Tensor:
|
| 35 |
-
# pair_features: (..., d_pair); each projection: (..., h).
|
| 36 |
-
return torch.cat(
|
| 37 |
-
[projection(pair_features) for projection in projections], dim=-1
|
| 38 |
-
) # (..., depth * h)
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
class DiffusionConditioning(nn.Module):
|
| 42 |
-
"""Prepare conditioned atom features and per-layer attention biases."""
|
| 43 |
-
|
| 44 |
-
def __init__(
|
| 45 |
-
self,
|
| 46 |
-
token_s: int,
|
| 47 |
-
token_z: int,
|
| 48 |
-
atom_s: int,
|
| 49 |
-
atom_z: int,
|
| 50 |
-
atoms_per_window_queries: int = 32,
|
| 51 |
-
atoms_per_window_keys: int = 128,
|
| 52 |
-
atom_encoder_depth: int = 3,
|
| 53 |
-
atom_encoder_heads: int = 4,
|
| 54 |
-
token_transformer_depth: int = 24,
|
| 55 |
-
token_transformer_heads: int = 8,
|
| 56 |
-
atom_decoder_depth: int = 3,
|
| 57 |
-
atom_decoder_heads: int = 4,
|
| 58 |
-
atom_feature_dim: int = 128,
|
| 59 |
-
conditioning_transition_layers: int = 2,
|
| 60 |
-
use_no_atom_char: bool = False,
|
| 61 |
-
use_atom_backbone_feat: bool = False,
|
| 62 |
-
use_residue_feats_atoms: bool = False,
|
| 63 |
-
) -> None:
|
| 64 |
-
super().__init__()
|
| 65 |
-
self.pairwise_conditioner = PairwiseConditioning(
|
| 66 |
-
token_z=token_z,
|
| 67 |
-
dim_token_rel_pos_feats=token_z,
|
| 68 |
-
num_transitions=conditioning_transition_layers,
|
| 69 |
-
)
|
| 70 |
-
self.atom_encoder = AtomEncoder(
|
| 71 |
-
atom_s=atom_s,
|
| 72 |
-
atom_z=atom_z,
|
| 73 |
-
token_s=token_s,
|
| 74 |
-
token_z=token_z,
|
| 75 |
-
atoms_per_window_queries=atoms_per_window_queries,
|
| 76 |
-
atoms_per_window_keys=atoms_per_window_keys,
|
| 77 |
-
atom_feature_dim=atom_feature_dim,
|
| 78 |
-
structure_prediction=True,
|
| 79 |
-
use_no_atom_char=use_no_atom_char,
|
| 80 |
-
use_atom_backbone_feat=use_atom_backbone_feat,
|
| 81 |
-
use_residue_feats_atoms=use_residue_feats_atoms,
|
| 82 |
-
)
|
| 83 |
-
self.atom_enc_proj_z = _bias_projections(
|
| 84 |
-
atom_encoder_depth,
|
| 85 |
-
atom_z,
|
| 86 |
-
atom_encoder_heads,
|
| 87 |
-
)
|
| 88 |
-
self.atom_dec_proj_z = _bias_projections(
|
| 89 |
-
atom_decoder_depth,
|
| 90 |
-
atom_z,
|
| 91 |
-
atom_decoder_heads,
|
| 92 |
-
)
|
| 93 |
-
self.token_trans_proj_z = _bias_projections(
|
| 94 |
-
token_transformer_depth,
|
| 95 |
-
token_z,
|
| 96 |
-
token_transformer_heads,
|
| 97 |
-
)
|
| 98 |
-
|
| 99 |
-
def forward(
|
| 100 |
-
self,
|
| 101 |
-
s_trunk: torch.Tensor,
|
| 102 |
-
z_trunk: torch.Tensor,
|
| 103 |
-
relative_position_encoding: torch.Tensor,
|
| 104 |
-
feats: dict[str, torch.Tensor],
|
| 105 |
-
) -> tuple[
|
| 106 |
-
torch.Tensor,
|
| 107 |
-
torch.Tensor,
|
| 108 |
-
Callable[[torch.Tensor], torch.Tensor],
|
| 109 |
-
torch.Tensor,
|
| 110 |
-
torch.Tensor,
|
| 111 |
-
torch.Tensor,
|
| 112 |
-
]:
|
| 113 |
-
"""Return conditioned atom tensors and concatenated layer biases.
|
| 114 |
-
|
| 115 |
-
``S`` has shape ``(b, n, d_s)`` and each ``Z`` tensor has shape
|
| 116 |
-
``(b, n, n, d_z)``. Biases are concatenated in transformer-block
|
| 117 |
-
order so downstream code can select them by layer.
|
| 118 |
-
"""
|
| 119 |
-
|
| 120 |
-
# b is batch size, t token count, a atom count, and k the atom-window count.
|
| 121 |
-
z_conditioned = self.pairwise_conditioner(
|
| 122 |
-
z_trunk,
|
| 123 |
-
relative_position_encoding,
|
| 124 |
-
) # (b, t, t, d_z)
|
| 125 |
-
q, c, p, to_keys = self.atom_encoder(
|
| 126 |
-
feats=feats,
|
| 127 |
-
s_trunk=s_trunk,
|
| 128 |
-
z=z_conditioned,
|
| 129 |
-
) # q/c: (b, a, d_a); p: (b, k, w, h_k, d_p); to_keys: callable
|
| 130 |
-
atom_encoder_bias = _concatenate_biases(
|
| 131 |
-
self.atom_enc_proj_z, p
|
| 132 |
-
) # (b, k, w, h_k, depth_enc * heads_enc)
|
| 133 |
-
atom_decoder_bias = _concatenate_biases(
|
| 134 |
-
self.atom_dec_proj_z, p
|
| 135 |
-
) # (b, k, w, h_k, depth_dec * heads_dec)
|
| 136 |
-
token_transformer_bias = _concatenate_biases(
|
| 137 |
-
self.token_trans_proj_z,
|
| 138 |
-
z_conditioned,
|
| 139 |
-
) # (b, t, t, depth_token * heads_token)
|
| 140 |
-
return (
|
| 141 |
-
q,
|
| 142 |
-
c,
|
| 143 |
-
to_keys,
|
| 144 |
-
atom_encoder_bias,
|
| 145 |
-
atom_decoder_bias,
|
| 146 |
-
token_transformer_bias,
|
| 147 |
-
) # tensor shapes are traced above; to_keys is the atom-key gatherer
|
|
|
|
| 1 |
+
"""Precompute pair and atom biases used by the Boltz2 diffusion stack."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from collections.abc import Callable
|
| 8 |
+
from torch import nn
|
| 9 |
+
|
| 10 |
+
from .vb_modules_encodersv2 import AtomEncoder, PairwiseConditioning
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _bias_projections(
|
| 14 |
+
depth: int,
|
| 15 |
+
input_dim: int,
|
| 16 |
+
num_heads: int,
|
| 17 |
+
) -> nn.ModuleList:
|
| 18 |
+
"""Build one normalized, bias-free projection per transformer block."""
|
| 19 |
+
|
| 20 |
+
return nn.ModuleList(
|
| 21 |
+
[
|
| 22 |
+
nn.Sequential(
|
| 23 |
+
nn.LayerNorm(input_dim),
|
| 24 |
+
nn.Linear(input_dim, num_heads, bias=False),
|
| 25 |
+
)
|
| 26 |
+
for _ in range(depth)
|
| 27 |
+
]
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _concatenate_biases(
|
| 32 |
+
projections: nn.ModuleList,
|
| 33 |
+
pair_features: torch.Tensor,
|
| 34 |
+
) -> torch.Tensor:
|
| 35 |
+
# pair_features: (..., d_pair); each projection: (..., h).
|
| 36 |
+
return torch.cat(
|
| 37 |
+
[projection(pair_features) for projection in projections], dim=-1
|
| 38 |
+
) # (..., depth * h)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class DiffusionConditioning(nn.Module):
|
| 42 |
+
"""Prepare conditioned atom features and per-layer attention biases."""
|
| 43 |
+
|
| 44 |
+
def __init__(
|
| 45 |
+
self,
|
| 46 |
+
token_s: int,
|
| 47 |
+
token_z: int,
|
| 48 |
+
atom_s: int,
|
| 49 |
+
atom_z: int,
|
| 50 |
+
atoms_per_window_queries: int = 32,
|
| 51 |
+
atoms_per_window_keys: int = 128,
|
| 52 |
+
atom_encoder_depth: int = 3,
|
| 53 |
+
atom_encoder_heads: int = 4,
|
| 54 |
+
token_transformer_depth: int = 24,
|
| 55 |
+
token_transformer_heads: int = 8,
|
| 56 |
+
atom_decoder_depth: int = 3,
|
| 57 |
+
atom_decoder_heads: int = 4,
|
| 58 |
+
atom_feature_dim: int = 128,
|
| 59 |
+
conditioning_transition_layers: int = 2,
|
| 60 |
+
use_no_atom_char: bool = False,
|
| 61 |
+
use_atom_backbone_feat: bool = False,
|
| 62 |
+
use_residue_feats_atoms: bool = False,
|
| 63 |
+
) -> None:
|
| 64 |
+
super().__init__()
|
| 65 |
+
self.pairwise_conditioner = PairwiseConditioning(
|
| 66 |
+
token_z=token_z,
|
| 67 |
+
dim_token_rel_pos_feats=token_z,
|
| 68 |
+
num_transitions=conditioning_transition_layers,
|
| 69 |
+
)
|
| 70 |
+
self.atom_encoder = AtomEncoder(
|
| 71 |
+
atom_s=atom_s,
|
| 72 |
+
atom_z=atom_z,
|
| 73 |
+
token_s=token_s,
|
| 74 |
+
token_z=token_z,
|
| 75 |
+
atoms_per_window_queries=atoms_per_window_queries,
|
| 76 |
+
atoms_per_window_keys=atoms_per_window_keys,
|
| 77 |
+
atom_feature_dim=atom_feature_dim,
|
| 78 |
+
structure_prediction=True,
|
| 79 |
+
use_no_atom_char=use_no_atom_char,
|
| 80 |
+
use_atom_backbone_feat=use_atom_backbone_feat,
|
| 81 |
+
use_residue_feats_atoms=use_residue_feats_atoms,
|
| 82 |
+
)
|
| 83 |
+
self.atom_enc_proj_z = _bias_projections(
|
| 84 |
+
atom_encoder_depth,
|
| 85 |
+
atom_z,
|
| 86 |
+
atom_encoder_heads,
|
| 87 |
+
)
|
| 88 |
+
self.atom_dec_proj_z = _bias_projections(
|
| 89 |
+
atom_decoder_depth,
|
| 90 |
+
atom_z,
|
| 91 |
+
atom_decoder_heads,
|
| 92 |
+
)
|
| 93 |
+
self.token_trans_proj_z = _bias_projections(
|
| 94 |
+
token_transformer_depth,
|
| 95 |
+
token_z,
|
| 96 |
+
token_transformer_heads,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
def forward(
|
| 100 |
+
self,
|
| 101 |
+
s_trunk: torch.Tensor,
|
| 102 |
+
z_trunk: torch.Tensor,
|
| 103 |
+
relative_position_encoding: torch.Tensor,
|
| 104 |
+
feats: dict[str, torch.Tensor],
|
| 105 |
+
) -> tuple[
|
| 106 |
+
torch.Tensor,
|
| 107 |
+
torch.Tensor,
|
| 108 |
+
Callable[[torch.Tensor], torch.Tensor],
|
| 109 |
+
torch.Tensor,
|
| 110 |
+
torch.Tensor,
|
| 111 |
+
torch.Tensor,
|
| 112 |
+
]:
|
| 113 |
+
"""Return conditioned atom tensors and concatenated layer biases.
|
| 114 |
+
|
| 115 |
+
``S`` has shape ``(b, n, d_s)`` and each ``Z`` tensor has shape
|
| 116 |
+
``(b, n, n, d_z)``. Biases are concatenated in transformer-block
|
| 117 |
+
order so downstream code can select them by layer.
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
# b is batch size, t token count, a atom count, and k the atom-window count.
|
| 121 |
+
z_conditioned = self.pairwise_conditioner(
|
| 122 |
+
z_trunk,
|
| 123 |
+
relative_position_encoding,
|
| 124 |
+
) # (b, t, t, d_z)
|
| 125 |
+
q, c, p, to_keys = self.atom_encoder(
|
| 126 |
+
feats=feats,
|
| 127 |
+
s_trunk=s_trunk,
|
| 128 |
+
z=z_conditioned,
|
| 129 |
+
) # q/c: (b, a, d_a); p: (b, k, w, h_k, d_p); to_keys: callable
|
| 130 |
+
atom_encoder_bias = _concatenate_biases(
|
| 131 |
+
self.atom_enc_proj_z, p
|
| 132 |
+
) # (b, k, w, h_k, depth_enc * heads_enc)
|
| 133 |
+
atom_decoder_bias = _concatenate_biases(
|
| 134 |
+
self.atom_dec_proj_z, p
|
| 135 |
+
) # (b, k, w, h_k, depth_dec * heads_dec)
|
| 136 |
+
token_transformer_bias = _concatenate_biases(
|
| 137 |
+
self.token_trans_proj_z,
|
| 138 |
+
z_conditioned,
|
| 139 |
+
) # (b, t, t, depth_token * heads_token)
|
| 140 |
+
return (
|
| 141 |
+
q,
|
| 142 |
+
c,
|
| 143 |
+
to_keys,
|
| 144 |
+
atom_encoder_bias,
|
| 145 |
+
atom_decoder_bias,
|
| 146 |
+
token_transformer_bias,
|
| 147 |
+
) # tensor shapes are traced above; to_keys is the atom-key gatherer
|
fastplms/models/boltz/vb_modules_diffusionv2.py
CHANGED
|
@@ -1,675 +1,675 @@
|
|
| 1 |
-
"""Denoise Boltz2 atom coordinates with optional structure-steering potentials."""
|
| 2 |
-
|
| 3 |
-
# Based on https://github.com/lucidrains/alphafold3-pytorch.
|
| 4 |
-
# MIT License, Copyright (c) 2024 Phil Wang.
|
| 5 |
-
|
| 6 |
-
from __future__ import annotations
|
| 7 |
-
|
| 8 |
-
import numpy as np
|
| 9 |
-
import torch
|
| 10 |
-
import torch.nn.functional as F
|
| 11 |
-
|
| 12 |
-
from math import sqrt
|
| 13 |
-
from typing import Any
|
| 14 |
-
from einops import rearrange
|
| 15 |
-
from torch import nn
|
| 16 |
-
from torch.nn import Module
|
| 17 |
-
from tqdm.auto import tqdm
|
| 18 |
-
|
| 19 |
-
from . import vb_const as const
|
| 20 |
-
from . import vb_layers_initialize as init
|
| 21 |
-
from .vb_loss_diffusionv2 import (
|
| 22 |
-
smooth_lddt_loss,
|
| 23 |
-
weighted_rigid_align,
|
| 24 |
-
)
|
| 25 |
-
from .vb_modules_encodersv2 import (
|
| 26 |
-
AtomAttentionDecoder,
|
| 27 |
-
AtomAttentionEncoder,
|
| 28 |
-
SingleConditioning,
|
| 29 |
-
)
|
| 30 |
-
from .vb_modules_transformersv2 import (
|
| 31 |
-
DiffusionTransformer,
|
| 32 |
-
)
|
| 33 |
-
from .vb_modules_utils import (
|
| 34 |
-
LinearNoBias,
|
| 35 |
-
center_random_augmentation,
|
| 36 |
-
compute_random_augmentation,
|
| 37 |
-
default,
|
| 38 |
-
log,
|
| 39 |
-
)
|
| 40 |
-
from .vb_potentials_potentials import get_potentials
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
class DiffusionModule(Module):
|
| 44 |
-
"""Predict coordinate updates from token and local atom attention."""
|
| 45 |
-
|
| 46 |
-
def __init__(
|
| 47 |
-
self,
|
| 48 |
-
token_s: int,
|
| 49 |
-
atom_s: int,
|
| 50 |
-
atoms_per_window_queries: int = 32,
|
| 51 |
-
atoms_per_window_keys: int = 128,
|
| 52 |
-
sigma_data: int = 16,
|
| 53 |
-
dim_fourier: int = 256,
|
| 54 |
-
atom_encoder_depth: int = 3,
|
| 55 |
-
atom_encoder_heads: int = 4,
|
| 56 |
-
token_transformer_depth: int = 24,
|
| 57 |
-
token_transformer_heads: int = 8,
|
| 58 |
-
atom_decoder_depth: int = 3,
|
| 59 |
-
atom_decoder_heads: int = 4,
|
| 60 |
-
conditioning_transition_layers: int = 2,
|
| 61 |
-
activation_checkpointing: bool = False,
|
| 62 |
-
transformer_post_ln: bool = False,
|
| 63 |
-
) -> None:
|
| 64 |
-
super().__init__()
|
| 65 |
-
|
| 66 |
-
self.atoms_per_window_queries = atoms_per_window_queries
|
| 67 |
-
self.atoms_per_window_keys = atoms_per_window_keys
|
| 68 |
-
self.sigma_data = sigma_data
|
| 69 |
-
self.activation_checkpointing = activation_checkpointing
|
| 70 |
-
|
| 71 |
-
# Token conditioning shares trunk features across repeated diffusion samples.
|
| 72 |
-
self.single_conditioner = SingleConditioning(
|
| 73 |
-
sigma_data=sigma_data,
|
| 74 |
-
token_s=token_s,
|
| 75 |
-
dim_fourier=dim_fourier,
|
| 76 |
-
num_transitions=conditioning_transition_layers,
|
| 77 |
-
)
|
| 78 |
-
|
| 79 |
-
self.atom_attention_encoder = AtomAttentionEncoder(
|
| 80 |
-
atom_s=atom_s,
|
| 81 |
-
token_s=token_s,
|
| 82 |
-
atoms_per_window_queries=atoms_per_window_queries,
|
| 83 |
-
atoms_per_window_keys=atoms_per_window_keys,
|
| 84 |
-
atom_encoder_depth=atom_encoder_depth,
|
| 85 |
-
atom_encoder_heads=atom_encoder_heads,
|
| 86 |
-
structure_prediction=True,
|
| 87 |
-
activation_checkpointing=activation_checkpointing,
|
| 88 |
-
transformer_post_layer_norm=transformer_post_ln,
|
| 89 |
-
)
|
| 90 |
-
|
| 91 |
-
self.s_to_a_linear = nn.Sequential(
|
| 92 |
-
nn.LayerNorm(2 * token_s), LinearNoBias(2 * token_s, 2 * token_s)
|
| 93 |
-
)
|
| 94 |
-
init.final_init_(self.s_to_a_linear[1].weight)
|
| 95 |
-
|
| 96 |
-
self.token_transformer = DiffusionTransformer(
|
| 97 |
-
dim=2 * token_s,
|
| 98 |
-
dim_single_cond=2 * token_s,
|
| 99 |
-
depth=token_transformer_depth,
|
| 100 |
-
heads=token_transformer_heads,
|
| 101 |
-
activation_checkpointing=activation_checkpointing,
|
| 102 |
-
)
|
| 103 |
-
|
| 104 |
-
self.a_norm = nn.LayerNorm(2 * token_s)
|
| 105 |
-
|
| 106 |
-
self.atom_attention_decoder = AtomAttentionDecoder(
|
| 107 |
-
atom_s=atom_s,
|
| 108 |
-
token_s=token_s,
|
| 109 |
-
attn_window_queries=atoms_per_window_queries,
|
| 110 |
-
attn_window_keys=atoms_per_window_keys,
|
| 111 |
-
atom_decoder_depth=atom_decoder_depth,
|
| 112 |
-
atom_decoder_heads=atom_decoder_heads,
|
| 113 |
-
activation_checkpointing=activation_checkpointing,
|
| 114 |
-
)
|
| 115 |
-
|
| 116 |
-
def forward(
|
| 117 |
-
self,
|
| 118 |
-
s_inputs: torch.Tensor, # (b, t, d_s)
|
| 119 |
-
s_trunk: torch.Tensor, # (b, t, d_s)
|
| 120 |
-
r_noisy: torch.Tensor, # (b * m, a, 3)
|
| 121 |
-
times: torch.Tensor, # (b * m,)
|
| 122 |
-
feats: dict[str, torch.Tensor],
|
| 123 |
-
diffusion_conditioning: dict[str, Any],
|
| 124 |
-
multiplicity: int = 1,
|
| 125 |
-
) -> torch.Tensor:
|
| 126 |
-
# b: base batch; m: multiplicity; t: tokens; a: atoms; d_s: token width.
|
| 127 |
-
if self.activation_checkpointing:
|
| 128 |
-
s, _normed_fourier = torch.utils.checkpoint.checkpoint(
|
| 129 |
-
self.single_conditioner,
|
| 130 |
-
times,
|
| 131 |
-
s_trunk.repeat_interleave(multiplicity, 0),
|
| 132 |
-
s_inputs.repeat_interleave(multiplicity, 0),
|
| 133 |
-
use_reentrant=False,
|
| 134 |
-
) # s: (b * m, t, 2 * d_s); Fourier features: (b * m, d_fourier)
|
| 135 |
-
else:
|
| 136 |
-
s, _normed_fourier = self.single_conditioner(
|
| 137 |
-
times,
|
| 138 |
-
s_trunk.repeat_interleave(multiplicity, 0),
|
| 139 |
-
s_inputs.repeat_interleave(multiplicity, 0),
|
| 140 |
-
) # s: (b * m, t, 2 * d_s); Fourier features: (b * m, d_fourier)
|
| 141 |
-
|
| 142 |
-
# Sequence-local Atom Attention and aggregation to coarse-grained tokens
|
| 143 |
-
a, q_skip, c_skip, to_keys = self.atom_attention_encoder(
|
| 144 |
-
feats=feats,
|
| 145 |
-
q=diffusion_conditioning["q"].float(),
|
| 146 |
-
c=diffusion_conditioning["c"].float(),
|
| 147 |
-
atom_enc_bias=diffusion_conditioning["atom_enc_bias"].float(),
|
| 148 |
-
to_keys=diffusion_conditioning["to_keys"],
|
| 149 |
-
r=r_noisy,
|
| 150 |
-
multiplicity=multiplicity,
|
| 151 |
-
) # a: (b * m, t, 2 * d_s); q_skip/c_skip: (b * m, n_atom, d_a)
|
| 152 |
-
|
| 153 |
-
# Full self-attention on token level
|
| 154 |
-
a = a + self.s_to_a_linear(s) # (b * m, t, 2 * d_s)
|
| 155 |
-
|
| 156 |
-
mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 157 |
-
a = self.token_transformer(
|
| 158 |
-
a,
|
| 159 |
-
mask=mask.float(),
|
| 160 |
-
s=s,
|
| 161 |
-
bias=diffusion_conditioning[
|
| 162 |
-
"token_trans_bias"
|
| 163 |
-
].float(), # note z is not expanded with multiplicity until after bias is computed
|
| 164 |
-
multiplicity=multiplicity,
|
| 165 |
-
) # (b * m, t, 2 * d_s)
|
| 166 |
-
a = self.a_norm(a) # (b * m, t, 2 * d_s)
|
| 167 |
-
|
| 168 |
-
# Broadcast token activations to atoms and run Sequence-local Atom Attention
|
| 169 |
-
r_update = self.atom_attention_decoder(
|
| 170 |
-
a=a,
|
| 171 |
-
q=q_skip,
|
| 172 |
-
c=c_skip,
|
| 173 |
-
atom_dec_bias=diffusion_conditioning["atom_dec_bias"].float(),
|
| 174 |
-
feats=feats,
|
| 175 |
-
multiplicity=multiplicity,
|
| 176 |
-
to_keys=to_keys,
|
| 177 |
-
) # (b * m, n_atom, 3)
|
| 178 |
-
|
| 179 |
-
return r_update # (b * m, n_atom, 3)
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
class AtomDiffusion(Module):
|
| 183 |
-
def __init__(
|
| 184 |
-
self,
|
| 185 |
-
score_model_args: dict[str, Any],
|
| 186 |
-
num_sampling_steps: int = 5, # number of sampling steps
|
| 187 |
-
sigma_min: float = 0.0004, # min noise level
|
| 188 |
-
sigma_max: float = 160.0, # max noise level
|
| 189 |
-
sigma_data: float = 16.0, # standard deviation of data distribution
|
| 190 |
-
rho: float = 7, # controls the sampling schedule
|
| 191 |
-
# Mean of the log-normal training-noise distribution.
|
| 192 |
-
P_mean: float = -1.2,
|
| 193 |
-
# Standard deviation of the log-normal training-noise distribution.
|
| 194 |
-
P_std: float = 1.5,
|
| 195 |
-
gamma_0: float = 0.8,
|
| 196 |
-
gamma_min: float = 1.0,
|
| 197 |
-
noise_scale: float = 1.003,
|
| 198 |
-
step_scale: float = 1.5,
|
| 199 |
-
step_scale_random: list[float] | None = None,
|
| 200 |
-
coordinate_augmentation: bool = True,
|
| 201 |
-
coordinate_augmentation_inference: bool | None = None,
|
| 202 |
-
compile_score: bool = False,
|
| 203 |
-
alignment_reverse_diff: bool = False,
|
| 204 |
-
synchronize_sigmas: bool = False,
|
| 205 |
-
) -> None:
|
| 206 |
-
super().__init__()
|
| 207 |
-
self.score_model = DiffusionModule(
|
| 208 |
-
**score_model_args,
|
| 209 |
-
)
|
| 210 |
-
if compile_score:
|
| 211 |
-
self.score_model = torch.compile(self.score_model, dynamic=False, fullgraph=False)
|
| 212 |
-
|
| 213 |
-
self.sigma_min = sigma_min
|
| 214 |
-
self.sigma_max = sigma_max
|
| 215 |
-
self.sigma_data = sigma_data
|
| 216 |
-
self.rho = rho
|
| 217 |
-
self.P_mean = P_mean
|
| 218 |
-
self.P_std = P_std
|
| 219 |
-
self.num_sampling_steps = num_sampling_steps
|
| 220 |
-
self.gamma_0 = gamma_0
|
| 221 |
-
self.gamma_min = gamma_min
|
| 222 |
-
self.noise_scale = noise_scale
|
| 223 |
-
self.step_scale = step_scale
|
| 224 |
-
self.step_scale_random = step_scale_random
|
| 225 |
-
self.coordinate_augmentation = coordinate_augmentation
|
| 226 |
-
self.coordinate_augmentation_inference = (
|
| 227 |
-
coordinate_augmentation_inference
|
| 228 |
-
if coordinate_augmentation_inference is not None
|
| 229 |
-
else coordinate_augmentation
|
| 230 |
-
)
|
| 231 |
-
self.alignment_reverse_diff = alignment_reverse_diff
|
| 232 |
-
self.synchronize_sigmas = synchronize_sigmas
|
| 233 |
-
|
| 234 |
-
self.token_s = score_model_args["token_s"]
|
| 235 |
-
self.register_buffer("zero", torch.tensor(0.0), persistent=False)
|
| 236 |
-
|
| 237 |
-
@property
|
| 238 |
-
def device(self) -> torch.device:
|
| 239 |
-
return next(self.score_model.parameters()).device
|
| 240 |
-
|
| 241 |
-
def c_skip(self, sigma: torch.Tensor) -> torch.Tensor:
|
| 242 |
-
# sigma: (...); coefficient preserves shape (...).
|
| 243 |
-
return (self.sigma_data**2) / (sigma**2 + self.sigma_data**2)
|
| 244 |
-
|
| 245 |
-
def c_out(self, sigma: torch.Tensor) -> torch.Tensor:
|
| 246 |
-
# sigma: (...); coefficient preserves shape (...).
|
| 247 |
-
return sigma * self.sigma_data / torch.sqrt(self.sigma_data**2 + sigma**2)
|
| 248 |
-
|
| 249 |
-
def c_in(self, sigma: torch.Tensor) -> torch.Tensor:
|
| 250 |
-
# sigma: (...); coefficient preserves shape (...).
|
| 251 |
-
return 1 / torch.sqrt(sigma**2 + self.sigma_data**2)
|
| 252 |
-
|
| 253 |
-
def c_noise(self, sigma: torch.Tensor) -> torch.Tensor:
|
| 254 |
-
# sigma: (...); log-noise condition preserves shape (...).
|
| 255 |
-
return log(sigma / self.sigma_data) * 0.25
|
| 256 |
-
|
| 257 |
-
def preconditioned_network_forward(
|
| 258 |
-
self,
|
| 259 |
-
noised_atom_coords: torch.Tensor, #: Float['b m 3'],
|
| 260 |
-
sigma: torch.Tensor | float, #: Float['b'] | Float[' '] | float,
|
| 261 |
-
network_condition_kwargs: dict[str, Any],
|
| 262 |
-
) -> torch.Tensor:
|
| 263 |
-
# noised_atom_coords: (s, a, 3); sigma: float or (s,).
|
| 264 |
-
batch, device = noised_atom_coords.shape[0], noised_atom_coords.device
|
| 265 |
-
|
| 266 |
-
if isinstance(sigma, float):
|
| 267 |
-
sigma = torch.full((batch,), sigma, device=device) # (s,) after a scalar input is expanded
|
| 268 |
-
|
| 269 |
-
padded_sigma = rearrange(sigma, "b -> b 1 1") # (s, 1, 1)
|
| 270 |
-
|
| 271 |
-
r_update = self.score_model(
|
| 272 |
-
r_noisy=self.c_in(padded_sigma) * noised_atom_coords,
|
| 273 |
-
times=self.c_noise(sigma),
|
| 274 |
-
**network_condition_kwargs,
|
| 275 |
-
) # (s, a, 3)
|
| 276 |
-
|
| 277 |
-
denoised_coords = (
|
| 278 |
-
self.c_skip(padded_sigma) * noised_atom_coords + self.c_out(padded_sigma) * r_update
|
| 279 |
-
) # (s, a, 3)
|
| 280 |
-
return denoised_coords
|
| 281 |
-
|
| 282 |
-
def sample_schedule(self, num_sampling_steps: int | None = None) -> torch.Tensor:
|
| 283 |
-
# n_step is the requested sampling-step count; the returned schedule has n_step + 1 values.
|
| 284 |
-
num_sampling_steps = default(num_sampling_steps, self.num_sampling_steps)
|
| 285 |
-
inv_rho = 1 / self.rho
|
| 286 |
-
|
| 287 |
-
steps = torch.arange(num_sampling_steps, device=self.device, dtype=torch.float32) # (n_step,)
|
| 288 |
-
sigmas = (
|
| 289 |
-
self.sigma_max**inv_rho
|
| 290 |
-
+ steps / (num_sampling_steps - 1) * (self.sigma_min**inv_rho - self.sigma_max**inv_rho)
|
| 291 |
-
) ** self.rho # (n_sigma,); schedule or batch axis is determined by the caller
|
| 292 |
-
|
| 293 |
-
sigmas = sigmas * self.sigma_data # (n_sigma,); schedule or batch axis is determined by the caller
|
| 294 |
-
|
| 295 |
-
sigmas = F.pad(sigmas, (0, 1), value=0.0) # last step is sigma value of 0.
|
| 296 |
-
return sigmas
|
| 297 |
-
|
| 298 |
-
def sample(
|
| 299 |
-
self,
|
| 300 |
-
atom_mask: torch.Tensor,
|
| 301 |
-
num_sampling_steps: int | None = None,
|
| 302 |
-
multiplicity: int = 1,
|
| 303 |
-
max_parallel_samples: int | None = None,
|
| 304 |
-
steering_args: dict[str, Any] | None = None,
|
| 305 |
-
verbose: bool = False,
|
| 306 |
-
**network_condition_kwargs: Any,
|
| 307 |
-
) -> dict[str, Any]:
|
| 308 |
-
# a is atom count; m is multiplicity, including steering particles.
|
| 309 |
-
# s is the current sample axis after batch repetition or particle resampling.
|
| 310 |
-
if steering_args is not None and (
|
| 311 |
-
steering_args["fk_steering"]
|
| 312 |
-
or steering_args["physical_guidance_update"]
|
| 313 |
-
or steering_args["contact_guidance_update"]
|
| 314 |
-
):
|
| 315 |
-
potentials = get_potentials(steering_args, boltz2=True)
|
| 316 |
-
|
| 317 |
-
if steering_args["fk_steering"]:
|
| 318 |
-
multiplicity = multiplicity * steering_args["num_particles"]
|
| 319 |
-
energy_traj = torch.empty((multiplicity, 0), device=self.device) # (m, n_evaluation)
|
| 320 |
-
resample_weights = torch.ones(multiplicity, device=self.device).reshape(
|
| 321 |
-
-1, steering_args["num_particles"]
|
| 322 |
-
) # (n_group, n_particle)
|
| 323 |
-
if steering_args["physical_guidance_update"] or steering_args["contact_guidance_update"]:
|
| 324 |
-
scaled_guidance_update = torch.zeros(
|
| 325 |
-
(multiplicity, *atom_mask.shape[1:], 3),
|
| 326 |
-
dtype=torch.float32,
|
| 327 |
-
device=self.device,
|
| 328 |
-
) # (s, a, 3)
|
| 329 |
-
if max_parallel_samples is None:
|
| 330 |
-
max_parallel_samples = multiplicity
|
| 331 |
-
|
| 332 |
-
num_sampling_steps = default(num_sampling_steps, self.num_sampling_steps)
|
| 333 |
-
atom_mask = atom_mask.repeat_interleave(multiplicity, 0) # (s, a); s expands on repetition and can shrink on resampling
|
| 334 |
-
|
| 335 |
-
shape = (*atom_mask.shape, 3)
|
| 336 |
-
|
| 337 |
-
# Pair each schedule value with the next sigma and gamma.
|
| 338 |
-
sigmas = self.sample_schedule(num_sampling_steps) # (n_sigma,); schedule or batch axis is determined by the caller
|
| 339 |
-
gammas = torch.where(sigmas > self.gamma_min, self.gamma_0, 0.0) # (n_step + 1,)
|
| 340 |
-
sigmas_and_gammas = list(zip(sigmas[:-1], sigmas[1:], gammas[1:], strict=True))
|
| 341 |
-
if self.training and self.step_scale_random is not None:
|
| 342 |
-
step_scale = np.random.choice(self.step_scale_random)
|
| 343 |
-
else:
|
| 344 |
-
step_scale = self.step_scale
|
| 345 |
-
|
| 346 |
-
# atom position is noise at the beginning
|
| 347 |
-
init_sigma = sigmas[0] # ()
|
| 348 |
-
atom_coords = init_sigma * torch.randn(shape, device=self.device) # (s, a, 3)
|
| 349 |
-
token_repr = None # None; the current sampler does not produce token features
|
| 350 |
-
atom_coords_denoised = None # (s, a, 3) or None before the first denoising step
|
| 351 |
-
|
| 352 |
-
for step_idx, (sigma_tm, sigma_t, gamma) in enumerate(
|
| 353 |
-
tqdm(
|
| 354 |
-
sigmas_and_gammas,
|
| 355 |
-
desc="Boltz2: Diffusion sampling",
|
| 356 |
-
unit="step",
|
| 357 |
-
disable=not verbose,
|
| 358 |
-
dynamic_ncols=True,
|
| 359 |
-
leave=False,
|
| 360 |
-
)
|
| 361 |
-
):
|
| 362 |
-
random_R, random_tr = compute_random_augmentation(
|
| 363 |
-
multiplicity, device=atom_coords.device, dtype=atom_coords.dtype
|
| 364 |
-
) # (m, 3, 3), (m, 1, 3)
|
| 365 |
-
atom_coords = atom_coords - atom_coords.mean(dim=-2, keepdims=True) # (s, a, 3)
|
| 366 |
-
atom_coords = torch.einsum("bmd,bds->bms", atom_coords, random_R) + random_tr # (s, a, 3)
|
| 367 |
-
if atom_coords_denoised is not None:
|
| 368 |
-
atom_coords_denoised -= atom_coords_denoised.mean(dim=-2, keepdims=True) # (s, a, 3) or None before the first denoising step
|
| 369 |
-
atom_coords_denoised = (
|
| 370 |
-
torch.einsum("bmd,bds->bms", atom_coords_denoised, random_R) + random_tr
|
| 371 |
-
) # (s, a, 3) or None before the first denoising step
|
| 372 |
-
if (
|
| 373 |
-
steering_args["physical_guidance_update"]
|
| 374 |
-
or steering_args["contact_guidance_update"]
|
| 375 |
-
) and scaled_guidance_update is not None:
|
| 376 |
-
scaled_guidance_update = torch.einsum(
|
| 377 |
-
"bmd,bds->bms", scaled_guidance_update, random_R
|
| 378 |
-
) # (s, a, 3)
|
| 379 |
-
|
| 380 |
-
sigma_tm, sigma_t, gamma = sigma_tm.item(), sigma_t.item(), gamma.item() # three scalar tensors () become Python floats
|
| 381 |
-
|
| 382 |
-
t_hat = sigma_tm * (1 + gamma)
|
| 383 |
-
steering_t = 1.0 - (step_idx / num_sampling_steps)
|
| 384 |
-
noise_var = self.noise_scale**2 * (t_hat**2 - sigma_tm**2)
|
| 385 |
-
eps = sqrt(noise_var) * torch.randn(shape, device=self.device) # (s, a, 3)
|
| 386 |
-
atom_coords_noisy = atom_coords + eps # (s, a, 3)
|
| 387 |
-
|
| 388 |
-
with torch.no_grad():
|
| 389 |
-
atom_coords_denoised = torch.zeros_like(atom_coords_noisy) # (s, a, 3) or None before the first denoising step
|
| 390 |
-
sample_ids = torch.arange(multiplicity).to(atom_coords_noisy.device) # (m,)
|
| 391 |
-
sample_ids_chunks = sample_ids.chunk(multiplicity % max_parallel_samples + 1) # tuple of index tensors, each (n_chunk,)
|
| 392 |
-
|
| 393 |
-
for sample_ids_chunk in sample_ids_chunks:
|
| 394 |
-
atom_coords_denoised_chunk = self.preconditioned_network_forward(
|
| 395 |
-
atom_coords_noisy[sample_ids_chunk],
|
| 396 |
-
t_hat,
|
| 397 |
-
network_condition_kwargs=dict(
|
| 398 |
-
multiplicity=sample_ids_chunk.numel(),
|
| 399 |
-
**network_condition_kwargs,
|
| 400 |
-
),
|
| 401 |
-
) # (n_chunk, a, 3)
|
| 402 |
-
atom_coords_denoised[sample_ids_chunk] = atom_coords_denoised_chunk # (n_chunk, a, 3)
|
| 403 |
-
|
| 404 |
-
if steering_args["fk_steering"] and (
|
| 405 |
-
(step_idx % steering_args["fk_resampling_interval"] == 0 and noise_var > 0)
|
| 406 |
-
or step_idx == num_sampling_steps - 1
|
| 407 |
-
):
|
| 408 |
-
# Compute energy of x_0 prediction
|
| 409 |
-
energy = torch.zeros(multiplicity, device=self.device) # (m,)
|
| 410 |
-
for potential in potentials:
|
| 411 |
-
parameters = potential.compute_parameters(steering_t)
|
| 412 |
-
if parameters["resampling_weight"] > 0:
|
| 413 |
-
component_energy = potential.compute(
|
| 414 |
-
atom_coords_denoised,
|
| 415 |
-
network_condition_kwargs["feats"],
|
| 416 |
-
parameters,
|
| 417 |
-
) # (m,)
|
| 418 |
-
energy += parameters["resampling_weight"] * component_energy # (m,)
|
| 419 |
-
energy_traj = torch.cat((energy_traj, energy.unsqueeze(1)), dim=1) # (m, n_evaluation)
|
| 420 |
-
|
| 421 |
-
# Compute log G values
|
| 422 |
-
if step_idx == 0:
|
| 423 |
-
log_G = -1 * energy # (m,)
|
| 424 |
-
else:
|
| 425 |
-
log_G = energy_traj[:, -2] - energy_traj[:, -1] # (m,)
|
| 426 |
-
|
| 427 |
-
# Compute ll difference between guided and unguided transition distribution
|
| 428 |
-
if (
|
| 429 |
-
steering_args["physical_guidance_update"]
|
| 430 |
-
or steering_args["contact_guidance_update"]
|
| 431 |
-
) and noise_var > 0:
|
| 432 |
-
ll_difference = (eps**2 - (eps + scaled_guidance_update) ** 2).sum(
|
| 433 |
-
dim=(-1, -2)
|
| 434 |
-
) / (2 * noise_var) # (m,)
|
| 435 |
-
else:
|
| 436 |
-
ll_difference = torch.zeros_like(energy) # (m,)
|
| 437 |
-
|
| 438 |
-
# Compute resampling weights
|
| 439 |
-
resample_weights = F.softmax(
|
| 440 |
-
(ll_difference + steering_args["fk_lambda"] * log_G).reshape(
|
| 441 |
-
-1, steering_args["num_particles"]
|
| 442 |
-
),
|
| 443 |
-
dim=1,
|
| 444 |
-
) # (n_group, n_particle)
|
| 445 |
-
|
| 446 |
-
# Compute guidance update to x_0 prediction
|
| 447 |
-
if (
|
| 448 |
-
steering_args["physical_guidance_update"]
|
| 449 |
-
or steering_args["contact_guidance_update"]
|
| 450 |
-
) and step_idx < num_sampling_steps - 1:
|
| 451 |
-
guidance_update = torch.zeros_like(atom_coords_denoised) # (s, a, 3)
|
| 452 |
-
for guidance_step in range(steering_args["num_gd_steps"]):
|
| 453 |
-
energy_gradient = torch.zeros_like(atom_coords_denoised) # (s, a, 3)
|
| 454 |
-
for potential in potentials:
|
| 455 |
-
parameters = potential.compute_parameters(steering_t)
|
| 456 |
-
if (
|
| 457 |
-
parameters["guidance_weight"] > 0
|
| 458 |
-
and (guidance_step) % parameters["guidance_interval"] == 0
|
| 459 |
-
):
|
| 460 |
-
energy_gradient += parameters[
|
| 461 |
-
"guidance_weight"
|
| 462 |
-
] * potential.compute_gradient(
|
| 463 |
-
atom_coords_denoised + guidance_update,
|
| 464 |
-
network_condition_kwargs["feats"],
|
| 465 |
-
parameters,
|
| 466 |
-
) # (s, a, 3)
|
| 467 |
-
guidance_update -= energy_gradient # (s, a, 3)
|
| 468 |
-
atom_coords_denoised += guidance_update # (s, a, 3) or None before the first denoising step
|
| 469 |
-
scaled_guidance_update = (
|
| 470 |
-
guidance_update * -1 * self.step_scale * (sigma_t - t_hat) / t_hat
|
| 471 |
-
) # (s, a, 3)
|
| 472 |
-
|
| 473 |
-
if steering_args["fk_steering"] and (
|
| 474 |
-
(step_idx % steering_args["fk_resampling_interval"] == 0 and noise_var > 0)
|
| 475 |
-
or step_idx == num_sampling_steps - 1
|
| 476 |
-
):
|
| 477 |
-
resample_indices = (
|
| 478 |
-
torch.multinomial(
|
| 479 |
-
resample_weights,
|
| 480 |
-
resample_weights.shape[1] if step_idx < num_sampling_steps - 1 else 1,
|
| 481 |
-
replacement=True,
|
| 482 |
-
)
|
| 483 |
-
+ resample_weights.shape[1]
|
| 484 |
-
* torch.arange(
|
| 485 |
-
resample_weights.shape[0], device=resample_weights.device
|
| 486 |
-
).unsqueeze(-1)
|
| 487 |
-
).flatten() # (s_next,); final resampling keeps one particle per group
|
| 488 |
-
|
| 489 |
-
atom_coords = atom_coords[resample_indices] # (s, a, 3)
|
| 490 |
-
atom_coords_noisy = atom_coords_noisy[resample_indices] # (s, a, 3)
|
| 491 |
-
atom_mask = atom_mask[resample_indices] # (s, a); s expands on repetition and can shrink on resampling
|
| 492 |
-
if atom_coords_denoised is not None:
|
| 493 |
-
atom_coords_denoised = atom_coords_denoised[resample_indices] # (s, a, 3) or None before the first denoising step
|
| 494 |
-
energy_traj = energy_traj[resample_indices] # (s_next, n_evaluation)
|
| 495 |
-
if (
|
| 496 |
-
steering_args["physical_guidance_update"]
|
| 497 |
-
or steering_args["contact_guidance_update"]
|
| 498 |
-
):
|
| 499 |
-
scaled_guidance_update = scaled_guidance_update[resample_indices] # (s, a, 3)
|
| 500 |
-
if token_repr is not None:
|
| 501 |
-
token_repr = token_repr[resample_indices] # (s_next, *feature_shape)
|
| 502 |
-
|
| 503 |
-
if self.alignment_reverse_diff:
|
| 504 |
-
with torch.autocast("cuda", enabled=False):
|
| 505 |
-
atom_coords_noisy = weighted_rigid_align(
|
| 506 |
-
atom_coords_noisy.float(),
|
| 507 |
-
atom_coords_denoised.float(),
|
| 508 |
-
atom_mask.float(),
|
| 509 |
-
atom_mask.float(),
|
| 510 |
-
) # (s, a, 3)
|
| 511 |
-
|
| 512 |
-
atom_coords_noisy = atom_coords_noisy.to(atom_coords_denoised) # (s, a, 3)
|
| 513 |
-
|
| 514 |
-
denoised_over_sigma = (atom_coords_noisy - atom_coords_denoised) / t_hat # (s, a, 3)
|
| 515 |
-
atom_coords_next = (
|
| 516 |
-
atom_coords_noisy + step_scale * (sigma_t - t_hat) * denoised_over_sigma
|
| 517 |
-
) # (s, a, 3)
|
| 518 |
-
|
| 519 |
-
atom_coords = atom_coords_next # (s, a, 3)
|
| 520 |
-
|
| 521 |
-
return dict(sample_atom_coords=atom_coords, diff_token_repr=token_repr)
|
| 522 |
-
|
| 523 |
-
def loss_weight(self, sigma: torch.Tensor) -> torch.Tensor:
|
| 524 |
-
# sigma: (...); weight preserves shape (...).
|
| 525 |
-
return (sigma**2 + self.sigma_data**2) / ((sigma * self.sigma_data) ** 2)
|
| 526 |
-
|
| 527 |
-
def noise_distribution(self, batch_size: int) -> torch.Tensor:
|
| 528 |
-
# Return one noise scale per example, shape (batch_size,).
|
| 529 |
-
return (
|
| 530 |
-
self.sigma_data
|
| 531 |
-
* (self.P_mean + self.P_std * torch.randn((batch_size,), device=self.device)).exp()
|
| 532 |
-
)
|
| 533 |
-
|
| 534 |
-
def forward(
|
| 535 |
-
self,
|
| 536 |
-
s_inputs: torch.Tensor,
|
| 537 |
-
s_trunk: torch.Tensor,
|
| 538 |
-
feats: dict[str, torch.Tensor],
|
| 539 |
-
diffusion_conditioning: dict[str, Any],
|
| 540 |
-
multiplicity: int = 1,
|
| 541 |
-
) -> dict[str, Any]:
|
| 542 |
-
# training diffusion step
|
| 543 |
-
batch_size = feats["coords"].shape[0] // multiplicity
|
| 544 |
-
|
| 545 |
-
if self.synchronize_sigmas:
|
| 546 |
-
sigmas = self.noise_distribution(batch_size).repeat_interleave(multiplicity, 0) # (n_sigma,); schedule or batch axis is determined by the caller
|
| 547 |
-
else:
|
| 548 |
-
sigmas = self.noise_distribution(batch_size * multiplicity) # (n_sigma,); schedule or batch axis is determined by the caller
|
| 549 |
-
padded_sigmas = rearrange(sigmas, "b -> b 1 1") # (b * m, 1, 1)
|
| 550 |
-
|
| 551 |
-
atom_coords = feats["coords"] # (s, a, 3)
|
| 552 |
-
|
| 553 |
-
atom_mask = feats["atom_pad_mask"] # (s, a); s expands on repetition and can shrink on resampling
|
| 554 |
-
atom_mask = atom_mask.repeat_interleave(multiplicity, 0) # (s, a); s expands on repetition and can shrink on resampling
|
| 555 |
-
|
| 556 |
-
atom_coords = center_random_augmentation(
|
| 557 |
-
atom_coords, atom_mask, augmentation=self.coordinate_augmentation
|
| 558 |
-
) # (s, a, 3)
|
| 559 |
-
|
| 560 |
-
noise = torch.randn_like(atom_coords) # (b * m, a, 3)
|
| 561 |
-
noised_atom_coords = atom_coords + padded_sigmas * noise # (b * m, a, 3)
|
| 562 |
-
|
| 563 |
-
denoised_atom_coords = self.preconditioned_network_forward(
|
| 564 |
-
noised_atom_coords,
|
| 565 |
-
sigmas,
|
| 566 |
-
network_condition_kwargs={
|
| 567 |
-
"s_inputs": s_inputs,
|
| 568 |
-
"s_trunk": s_trunk,
|
| 569 |
-
"feats": feats,
|
| 570 |
-
"multiplicity": multiplicity,
|
| 571 |
-
"diffusion_conditioning": diffusion_conditioning,
|
| 572 |
-
},
|
| 573 |
-
) # (b * m, a, 3)
|
| 574 |
-
|
| 575 |
-
return {
|
| 576 |
-
"denoised_atom_coords": denoised_atom_coords,
|
| 577 |
-
"sigmas": sigmas,
|
| 578 |
-
"aligned_true_atom_coords": atom_coords,
|
| 579 |
-
}
|
| 580 |
-
|
| 581 |
-
def compute_loss(
|
| 582 |
-
self,
|
| 583 |
-
feats: dict[str, torch.Tensor],
|
| 584 |
-
out_dict: dict[str, torch.Tensor],
|
| 585 |
-
add_smooth_lddt_loss: bool = True,
|
| 586 |
-
nucleotide_loss_weight: float = 5.0,
|
| 587 |
-
ligand_loss_weight: float = 10.0,
|
| 588 |
-
multiplicity: int = 1,
|
| 589 |
-
filter_by_plddt: float = 0.0,
|
| 590 |
-
) -> dict[str, Any]:
|
| 591 |
-
# Coordinates: (b * m, a, 3); base feature masks: (b, a).
|
| 592 |
-
with torch.autocast("cuda", enabled=False):
|
| 593 |
-
denoised_atom_coords = out_dict["denoised_atom_coords"].float() # (b * m, a, 3)
|
| 594 |
-
sigmas = out_dict["sigmas"].float() # (n_sigma,); schedule or batch axis is determined by the caller
|
| 595 |
-
|
| 596 |
-
resolved_atom_mask_uni = feats["atom_resolved_mask"].float() # (b, a)
|
| 597 |
-
|
| 598 |
-
if filter_by_plddt > 0:
|
| 599 |
-
plddt_mask = feats["plddt"] > filter_by_plddt # (b, a)
|
| 600 |
-
resolved_atom_mask_uni = resolved_atom_mask_uni * plddt_mask.float() # (b, a)
|
| 601 |
-
|
| 602 |
-
resolved_atom_mask = resolved_atom_mask_uni.repeat_interleave(multiplicity, 0) # (b * m, a)
|
| 603 |
-
|
| 604 |
-
align_weights = denoised_atom_coords.new_ones(denoised_atom_coords.shape[:2]) # (b * m, a)
|
| 605 |
-
atom_type = (
|
| 606 |
-
torch.bmm(
|
| 607 |
-
feats["atom_to_token"].float(),
|
| 608 |
-
feats["mol_type"].unsqueeze(-1).float(),
|
| 609 |
-
)
|
| 610 |
-
.squeeze(-1)
|
| 611 |
-
.long()
|
| 612 |
-
) # (b, a)
|
| 613 |
-
atom_type_mult = atom_type.repeat_interleave(multiplicity, 0) # (b * m, a)
|
| 614 |
-
|
| 615 |
-
align_weights = (
|
| 616 |
-
align_weights
|
| 617 |
-
* (
|
| 618 |
-
1
|
| 619 |
-
+ nucleotide_loss_weight
|
| 620 |
-
* (
|
| 621 |
-
torch.eq(atom_type_mult, const.chain_type_ids["DNA"]).float()
|
| 622 |
-
+ torch.eq(atom_type_mult, const.chain_type_ids["RNA"]).float()
|
| 623 |
-
)
|
| 624 |
-
+ ligand_loss_weight
|
| 625 |
-
* torch.eq(atom_type_mult, const.chain_type_ids["NONPOLYMER"]).float()
|
| 626 |
-
).float()
|
| 627 |
-
) # (b * m, a)
|
| 628 |
-
|
| 629 |
-
atom_coords = out_dict["aligned_true_atom_coords"].float() # (s, a, 3)
|
| 630 |
-
atom_coords_aligned_ground_truth = weighted_rigid_align(
|
| 631 |
-
atom_coords.detach(),
|
| 632 |
-
denoised_atom_coords.detach(),
|
| 633 |
-
align_weights.detach(),
|
| 634 |
-
mask=feats["atom_resolved_mask"]
|
| 635 |
-
.float()
|
| 636 |
-
.repeat_interleave(multiplicity, 0)
|
| 637 |
-
.detach(),
|
| 638 |
-
) # (b * m, a, 3)
|
| 639 |
-
|
| 640 |
-
atom_coords_aligned_ground_truth = atom_coords_aligned_ground_truth.to(
|
| 641 |
-
denoised_atom_coords
|
| 642 |
-
) # (b * m, a, 3)
|
| 643 |
-
|
| 644 |
-
# weighted MSE loss of denoised atom positions
|
| 645 |
-
mse_loss = ((denoised_atom_coords - atom_coords_aligned_ground_truth) ** 2).sum(dim=-1) # (b * m, a)
|
| 646 |
-
mse_loss = torch.sum(mse_loss * align_weights * resolved_atom_mask, dim=-1) / (
|
| 647 |
-
torch.sum(3 * align_weights * resolved_atom_mask, dim=-1) + 1e-5
|
| 648 |
-
) # (b * m,)
|
| 649 |
-
|
| 650 |
-
# weight by sigma factor
|
| 651 |
-
loss_weights = self.loss_weight(sigmas) # (b * m,)
|
| 652 |
-
mse_loss = (mse_loss * loss_weights).mean() # ()
|
| 653 |
-
|
| 654 |
-
total_loss = mse_loss # ()
|
| 655 |
-
|
| 656 |
-
# proposed auxiliary smooth lddt loss
|
| 657 |
-
lddt_loss = self.zero # ()
|
| 658 |
-
if add_smooth_lddt_loss:
|
| 659 |
-
lddt_loss = smooth_lddt_loss(
|
| 660 |
-
denoised_atom_coords,
|
| 661 |
-
feats["coords"],
|
| 662 |
-
torch.eq(atom_type, const.chain_type_ids["DNA"]).float()
|
| 663 |
-
+ torch.eq(atom_type, const.chain_type_ids["RNA"]).float(),
|
| 664 |
-
coords_mask=resolved_atom_mask_uni,
|
| 665 |
-
multiplicity=multiplicity,
|
| 666 |
-
) # ()
|
| 667 |
-
|
| 668 |
-
total_loss = total_loss + lddt_loss # ()
|
| 669 |
-
|
| 670 |
-
loss_breakdown = {
|
| 671 |
-
"mse_loss": mse_loss,
|
| 672 |
-
"smooth_lddt_loss": lddt_loss,
|
| 673 |
-
}
|
| 674 |
-
|
| 675 |
-
return {"loss": total_loss, "loss_breakdown": loss_breakdown}
|
|
|
|
| 1 |
+
"""Denoise Boltz2 atom coordinates with optional structure-steering potentials."""
|
| 2 |
+
|
| 3 |
+
# Based on https://github.com/lucidrains/alphafold3-pytorch.
|
| 4 |
+
# MIT License, Copyright (c) 2024 Phil Wang.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
|
| 12 |
+
from math import sqrt
|
| 13 |
+
from typing import Any
|
| 14 |
+
from einops import rearrange
|
| 15 |
+
from torch import nn
|
| 16 |
+
from torch.nn import Module
|
| 17 |
+
from tqdm.auto import tqdm
|
| 18 |
+
|
| 19 |
+
from . import vb_const as const
|
| 20 |
+
from . import vb_layers_initialize as init
|
| 21 |
+
from .vb_loss_diffusionv2 import (
|
| 22 |
+
smooth_lddt_loss,
|
| 23 |
+
weighted_rigid_align,
|
| 24 |
+
)
|
| 25 |
+
from .vb_modules_encodersv2 import (
|
| 26 |
+
AtomAttentionDecoder,
|
| 27 |
+
AtomAttentionEncoder,
|
| 28 |
+
SingleConditioning,
|
| 29 |
+
)
|
| 30 |
+
from .vb_modules_transformersv2 import (
|
| 31 |
+
DiffusionTransformer,
|
| 32 |
+
)
|
| 33 |
+
from .vb_modules_utils import (
|
| 34 |
+
LinearNoBias,
|
| 35 |
+
center_random_augmentation,
|
| 36 |
+
compute_random_augmentation,
|
| 37 |
+
default,
|
| 38 |
+
log,
|
| 39 |
+
)
|
| 40 |
+
from .vb_potentials_potentials import get_potentials
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class DiffusionModule(Module):
|
| 44 |
+
"""Predict coordinate updates from token and local atom attention."""
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
token_s: int,
|
| 49 |
+
atom_s: int,
|
| 50 |
+
atoms_per_window_queries: int = 32,
|
| 51 |
+
atoms_per_window_keys: int = 128,
|
| 52 |
+
sigma_data: int = 16,
|
| 53 |
+
dim_fourier: int = 256,
|
| 54 |
+
atom_encoder_depth: int = 3,
|
| 55 |
+
atom_encoder_heads: int = 4,
|
| 56 |
+
token_transformer_depth: int = 24,
|
| 57 |
+
token_transformer_heads: int = 8,
|
| 58 |
+
atom_decoder_depth: int = 3,
|
| 59 |
+
atom_decoder_heads: int = 4,
|
| 60 |
+
conditioning_transition_layers: int = 2,
|
| 61 |
+
activation_checkpointing: bool = False,
|
| 62 |
+
transformer_post_ln: bool = False,
|
| 63 |
+
) -> None:
|
| 64 |
+
super().__init__()
|
| 65 |
+
|
| 66 |
+
self.atoms_per_window_queries = atoms_per_window_queries
|
| 67 |
+
self.atoms_per_window_keys = atoms_per_window_keys
|
| 68 |
+
self.sigma_data = sigma_data
|
| 69 |
+
self.activation_checkpointing = activation_checkpointing
|
| 70 |
+
|
| 71 |
+
# Token conditioning shares trunk features across repeated diffusion samples.
|
| 72 |
+
self.single_conditioner = SingleConditioning(
|
| 73 |
+
sigma_data=sigma_data,
|
| 74 |
+
token_s=token_s,
|
| 75 |
+
dim_fourier=dim_fourier,
|
| 76 |
+
num_transitions=conditioning_transition_layers,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
self.atom_attention_encoder = AtomAttentionEncoder(
|
| 80 |
+
atom_s=atom_s,
|
| 81 |
+
token_s=token_s,
|
| 82 |
+
atoms_per_window_queries=atoms_per_window_queries,
|
| 83 |
+
atoms_per_window_keys=atoms_per_window_keys,
|
| 84 |
+
atom_encoder_depth=atom_encoder_depth,
|
| 85 |
+
atom_encoder_heads=atom_encoder_heads,
|
| 86 |
+
structure_prediction=True,
|
| 87 |
+
activation_checkpointing=activation_checkpointing,
|
| 88 |
+
transformer_post_layer_norm=transformer_post_ln,
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
self.s_to_a_linear = nn.Sequential(
|
| 92 |
+
nn.LayerNorm(2 * token_s), LinearNoBias(2 * token_s, 2 * token_s)
|
| 93 |
+
)
|
| 94 |
+
init.final_init_(self.s_to_a_linear[1].weight)
|
| 95 |
+
|
| 96 |
+
self.token_transformer = DiffusionTransformer(
|
| 97 |
+
dim=2 * token_s,
|
| 98 |
+
dim_single_cond=2 * token_s,
|
| 99 |
+
depth=token_transformer_depth,
|
| 100 |
+
heads=token_transformer_heads,
|
| 101 |
+
activation_checkpointing=activation_checkpointing,
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
self.a_norm = nn.LayerNorm(2 * token_s)
|
| 105 |
+
|
| 106 |
+
self.atom_attention_decoder = AtomAttentionDecoder(
|
| 107 |
+
atom_s=atom_s,
|
| 108 |
+
token_s=token_s,
|
| 109 |
+
attn_window_queries=atoms_per_window_queries,
|
| 110 |
+
attn_window_keys=atoms_per_window_keys,
|
| 111 |
+
atom_decoder_depth=atom_decoder_depth,
|
| 112 |
+
atom_decoder_heads=atom_decoder_heads,
|
| 113 |
+
activation_checkpointing=activation_checkpointing,
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
def forward(
|
| 117 |
+
self,
|
| 118 |
+
s_inputs: torch.Tensor, # (b, t, d_s)
|
| 119 |
+
s_trunk: torch.Tensor, # (b, t, d_s)
|
| 120 |
+
r_noisy: torch.Tensor, # (b * m, a, 3)
|
| 121 |
+
times: torch.Tensor, # (b * m,)
|
| 122 |
+
feats: dict[str, torch.Tensor],
|
| 123 |
+
diffusion_conditioning: dict[str, Any],
|
| 124 |
+
multiplicity: int = 1,
|
| 125 |
+
) -> torch.Tensor:
|
| 126 |
+
# b: base batch; m: multiplicity; t: tokens; a: atoms; d_s: token width.
|
| 127 |
+
if self.activation_checkpointing:
|
| 128 |
+
s, _normed_fourier = torch.utils.checkpoint.checkpoint(
|
| 129 |
+
self.single_conditioner,
|
| 130 |
+
times,
|
| 131 |
+
s_trunk.repeat_interleave(multiplicity, 0),
|
| 132 |
+
s_inputs.repeat_interleave(multiplicity, 0),
|
| 133 |
+
use_reentrant=False,
|
| 134 |
+
) # s: (b * m, t, 2 * d_s); Fourier features: (b * m, d_fourier)
|
| 135 |
+
else:
|
| 136 |
+
s, _normed_fourier = self.single_conditioner(
|
| 137 |
+
times,
|
| 138 |
+
s_trunk.repeat_interleave(multiplicity, 0),
|
| 139 |
+
s_inputs.repeat_interleave(multiplicity, 0),
|
| 140 |
+
) # s: (b * m, t, 2 * d_s); Fourier features: (b * m, d_fourier)
|
| 141 |
+
|
| 142 |
+
# Sequence-local Atom Attention and aggregation to coarse-grained tokens
|
| 143 |
+
a, q_skip, c_skip, to_keys = self.atom_attention_encoder(
|
| 144 |
+
feats=feats,
|
| 145 |
+
q=diffusion_conditioning["q"].float(),
|
| 146 |
+
c=diffusion_conditioning["c"].float(),
|
| 147 |
+
atom_enc_bias=diffusion_conditioning["atom_enc_bias"].float(),
|
| 148 |
+
to_keys=diffusion_conditioning["to_keys"],
|
| 149 |
+
r=r_noisy,
|
| 150 |
+
multiplicity=multiplicity,
|
| 151 |
+
) # a: (b * m, t, 2 * d_s); q_skip/c_skip: (b * m, n_atom, d_a)
|
| 152 |
+
|
| 153 |
+
# Full self-attention on token level
|
| 154 |
+
a = a + self.s_to_a_linear(s) # (b * m, t, 2 * d_s)
|
| 155 |
+
|
| 156 |
+
mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0) # (b * m, t)
|
| 157 |
+
a = self.token_transformer(
|
| 158 |
+
a,
|
| 159 |
+
mask=mask.float(),
|
| 160 |
+
s=s,
|
| 161 |
+
bias=diffusion_conditioning[
|
| 162 |
+
"token_trans_bias"
|
| 163 |
+
].float(), # note z is not expanded with multiplicity until after bias is computed
|
| 164 |
+
multiplicity=multiplicity,
|
| 165 |
+
) # (b * m, t, 2 * d_s)
|
| 166 |
+
a = self.a_norm(a) # (b * m, t, 2 * d_s)
|
| 167 |
+
|
| 168 |
+
# Broadcast token activations to atoms and run Sequence-local Atom Attention
|
| 169 |
+
r_update = self.atom_attention_decoder(
|
| 170 |
+
a=a,
|
| 171 |
+
q=q_skip,
|
| 172 |
+
c=c_skip,
|
| 173 |
+
atom_dec_bias=diffusion_conditioning["atom_dec_bias"].float(),
|
| 174 |
+
feats=feats,
|
| 175 |
+
multiplicity=multiplicity,
|
| 176 |
+
to_keys=to_keys,
|
| 177 |
+
) # (b * m, n_atom, 3)
|
| 178 |
+
|
| 179 |
+
return r_update # (b * m, n_atom, 3)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class AtomDiffusion(Module):
|
| 183 |
+
def __init__(
|
| 184 |
+
self,
|
| 185 |
+
score_model_args: dict[str, Any],
|
| 186 |
+
num_sampling_steps: int = 5, # number of sampling steps
|
| 187 |
+
sigma_min: float = 0.0004, # min noise level
|
| 188 |
+
sigma_max: float = 160.0, # max noise level
|
| 189 |
+
sigma_data: float = 16.0, # standard deviation of data distribution
|
| 190 |
+
rho: float = 7, # controls the sampling schedule
|
| 191 |
+
# Mean of the log-normal training-noise distribution.
|
| 192 |
+
P_mean: float = -1.2,
|
| 193 |
+
# Standard deviation of the log-normal training-noise distribution.
|
| 194 |
+
P_std: float = 1.5,
|
| 195 |
+
gamma_0: float = 0.8,
|
| 196 |
+
gamma_min: float = 1.0,
|
| 197 |
+
noise_scale: float = 1.003,
|
| 198 |
+
step_scale: float = 1.5,
|
| 199 |
+
step_scale_random: list[float] | None = None,
|
| 200 |
+
coordinate_augmentation: bool = True,
|
| 201 |
+
coordinate_augmentation_inference: bool | None = None,
|
| 202 |
+
compile_score: bool = False,
|
| 203 |
+
alignment_reverse_diff: bool = False,
|
| 204 |
+
synchronize_sigmas: bool = False,
|
| 205 |
+
) -> None:
|
| 206 |
+
super().__init__()
|
| 207 |
+
self.score_model = DiffusionModule(
|
| 208 |
+
**score_model_args,
|
| 209 |
+
)
|
| 210 |
+
if compile_score:
|
| 211 |
+
self.score_model = torch.compile(self.score_model, dynamic=False, fullgraph=False)
|
| 212 |
+
|
| 213 |
+
self.sigma_min = sigma_min
|
| 214 |
+
self.sigma_max = sigma_max
|
| 215 |
+
self.sigma_data = sigma_data
|
| 216 |
+
self.rho = rho
|
| 217 |
+
self.P_mean = P_mean
|
| 218 |
+
self.P_std = P_std
|
| 219 |
+
self.num_sampling_steps = num_sampling_steps
|
| 220 |
+
self.gamma_0 = gamma_0
|
| 221 |
+
self.gamma_min = gamma_min
|
| 222 |
+
self.noise_scale = noise_scale
|
| 223 |
+
self.step_scale = step_scale
|
| 224 |
+
self.step_scale_random = step_scale_random
|
| 225 |
+
self.coordinate_augmentation = coordinate_augmentation
|
| 226 |
+
self.coordinate_augmentation_inference = (
|
| 227 |
+
coordinate_augmentation_inference
|
| 228 |
+
if coordinate_augmentation_inference is not None
|
| 229 |
+
else coordinate_augmentation
|
| 230 |
+
)
|
| 231 |
+
self.alignment_reverse_diff = alignment_reverse_diff
|
| 232 |
+
self.synchronize_sigmas = synchronize_sigmas
|
| 233 |
+
|
| 234 |
+
self.token_s = score_model_args["token_s"]
|
| 235 |
+
self.register_buffer("zero", torch.tensor(0.0), persistent=False)
|
| 236 |
+
|
| 237 |
+
@property
|
| 238 |
+
def device(self) -> torch.device:
|
| 239 |
+
return next(self.score_model.parameters()).device
|
| 240 |
+
|
| 241 |
+
def c_skip(self, sigma: torch.Tensor) -> torch.Tensor:
|
| 242 |
+
# sigma: (...); coefficient preserves shape (...).
|
| 243 |
+
return (self.sigma_data**2) / (sigma**2 + self.sigma_data**2)
|
| 244 |
+
|
| 245 |
+
def c_out(self, sigma: torch.Tensor) -> torch.Tensor:
|
| 246 |
+
# sigma: (...); coefficient preserves shape (...).
|
| 247 |
+
return sigma * self.sigma_data / torch.sqrt(self.sigma_data**2 + sigma**2)
|
| 248 |
+
|
| 249 |
+
def c_in(self, sigma: torch.Tensor) -> torch.Tensor:
|
| 250 |
+
# sigma: (...); coefficient preserves shape (...).
|
| 251 |
+
return 1 / torch.sqrt(sigma**2 + self.sigma_data**2)
|
| 252 |
+
|
| 253 |
+
def c_noise(self, sigma: torch.Tensor) -> torch.Tensor:
|
| 254 |
+
# sigma: (...); log-noise condition preserves shape (...).
|
| 255 |
+
return log(sigma / self.sigma_data) * 0.25
|
| 256 |
+
|
| 257 |
+
def preconditioned_network_forward(
|
| 258 |
+
self,
|
| 259 |
+
noised_atom_coords: torch.Tensor, #: Float['b m 3'],
|
| 260 |
+
sigma: torch.Tensor | float, #: Float['b'] | Float[' '] | float,
|
| 261 |
+
network_condition_kwargs: dict[str, Any],
|
| 262 |
+
) -> torch.Tensor:
|
| 263 |
+
# noised_atom_coords: (s, a, 3); sigma: float or (s,).
|
| 264 |
+
batch, device = noised_atom_coords.shape[0], noised_atom_coords.device
|
| 265 |
+
|
| 266 |
+
if isinstance(sigma, float):
|
| 267 |
+
sigma = torch.full((batch,), sigma, device=device) # (s,) after a scalar input is expanded
|
| 268 |
+
|
| 269 |
+
padded_sigma = rearrange(sigma, "b -> b 1 1") # (s, 1, 1)
|
| 270 |
+
|
| 271 |
+
r_update = self.score_model(
|
| 272 |
+
r_noisy=self.c_in(padded_sigma) * noised_atom_coords,
|
| 273 |
+
times=self.c_noise(sigma),
|
| 274 |
+
**network_condition_kwargs,
|
| 275 |
+
) # (s, a, 3)
|
| 276 |
+
|
| 277 |
+
denoised_coords = (
|
| 278 |
+
self.c_skip(padded_sigma) * noised_atom_coords + self.c_out(padded_sigma) * r_update
|
| 279 |
+
) # (s, a, 3)
|
| 280 |
+
return denoised_coords
|
| 281 |
+
|
| 282 |
+
def sample_schedule(self, num_sampling_steps: int | None = None) -> torch.Tensor:
|
| 283 |
+
# n_step is the requested sampling-step count; the returned schedule has n_step + 1 values.
|
| 284 |
+
num_sampling_steps = default(num_sampling_steps, self.num_sampling_steps)
|
| 285 |
+
inv_rho = 1 / self.rho
|
| 286 |
+
|
| 287 |
+
steps = torch.arange(num_sampling_steps, device=self.device, dtype=torch.float32) # (n_step,)
|
| 288 |
+
sigmas = (
|
| 289 |
+
self.sigma_max**inv_rho
|
| 290 |
+
+ steps / (num_sampling_steps - 1) * (self.sigma_min**inv_rho - self.sigma_max**inv_rho)
|
| 291 |
+
) ** self.rho # (n_sigma,); schedule or batch axis is determined by the caller
|
| 292 |
+
|
| 293 |
+
sigmas = sigmas * self.sigma_data # (n_sigma,); schedule or batch axis is determined by the caller
|
| 294 |
+
|
| 295 |
+
sigmas = F.pad(sigmas, (0, 1), value=0.0) # last step is sigma value of 0.
|
| 296 |
+
return sigmas
|
| 297 |
+
|
| 298 |
+
def sample(
|
| 299 |
+
self,
|
| 300 |
+
atom_mask: torch.Tensor,
|
| 301 |
+
num_sampling_steps: int | None = None,
|
| 302 |
+
multiplicity: int = 1,
|
| 303 |
+
max_parallel_samples: int | None = None,
|
| 304 |
+
steering_args: dict[str, Any] | None = None,
|
| 305 |
+
verbose: bool = False,
|
| 306 |
+
**network_condition_kwargs: Any,
|
| 307 |
+
) -> dict[str, Any]:
|
| 308 |
+
# a is atom count; m is multiplicity, including steering particles.
|
| 309 |
+
# s is the current sample axis after batch repetition or particle resampling.
|
| 310 |
+
if steering_args is not None and (
|
| 311 |
+
steering_args["fk_steering"]
|
| 312 |
+
or steering_args["physical_guidance_update"]
|
| 313 |
+
or steering_args["contact_guidance_update"]
|
| 314 |
+
):
|
| 315 |
+
potentials = get_potentials(steering_args, boltz2=True)
|
| 316 |
+
|
| 317 |
+
if steering_args["fk_steering"]:
|
| 318 |
+
multiplicity = multiplicity * steering_args["num_particles"]
|
| 319 |
+
energy_traj = torch.empty((multiplicity, 0), device=self.device) # (m, n_evaluation)
|
| 320 |
+
resample_weights = torch.ones(multiplicity, device=self.device).reshape(
|
| 321 |
+
-1, steering_args["num_particles"]
|
| 322 |
+
) # (n_group, n_particle)
|
| 323 |
+
if steering_args["physical_guidance_update"] or steering_args["contact_guidance_update"]:
|
| 324 |
+
scaled_guidance_update = torch.zeros(
|
| 325 |
+
(multiplicity, *atom_mask.shape[1:], 3),
|
| 326 |
+
dtype=torch.float32,
|
| 327 |
+
device=self.device,
|
| 328 |
+
) # (s, a, 3)
|
| 329 |
+
if max_parallel_samples is None:
|
| 330 |
+
max_parallel_samples = multiplicity
|
| 331 |
+
|
| 332 |
+
num_sampling_steps = default(num_sampling_steps, self.num_sampling_steps)
|
| 333 |
+
atom_mask = atom_mask.repeat_interleave(multiplicity, 0) # (s, a); s expands on repetition and can shrink on resampling
|
| 334 |
+
|
| 335 |
+
shape = (*atom_mask.shape, 3)
|
| 336 |
+
|
| 337 |
+
# Pair each schedule value with the next sigma and gamma.
|
| 338 |
+
sigmas = self.sample_schedule(num_sampling_steps) # (n_sigma,); schedule or batch axis is determined by the caller
|
| 339 |
+
gammas = torch.where(sigmas > self.gamma_min, self.gamma_0, 0.0) # (n_step + 1,)
|
| 340 |
+
sigmas_and_gammas = list(zip(sigmas[:-1], sigmas[1:], gammas[1:], strict=True))
|
| 341 |
+
if self.training and self.step_scale_random is not None:
|
| 342 |
+
step_scale = np.random.choice(self.step_scale_random)
|
| 343 |
+
else:
|
| 344 |
+
step_scale = self.step_scale
|
| 345 |
+
|
| 346 |
+
# atom position is noise at the beginning
|
| 347 |
+
init_sigma = sigmas[0] # ()
|
| 348 |
+
atom_coords = init_sigma * torch.randn(shape, device=self.device) # (s, a, 3)
|
| 349 |
+
token_repr = None # None; the current sampler does not produce token features
|
| 350 |
+
atom_coords_denoised = None # (s, a, 3) or None before the first denoising step
|
| 351 |
+
|
| 352 |
+
for step_idx, (sigma_tm, sigma_t, gamma) in enumerate(
|
| 353 |
+
tqdm(
|
| 354 |
+
sigmas_and_gammas,
|
| 355 |
+
desc="Boltz2: Diffusion sampling",
|
| 356 |
+
unit="step",
|
| 357 |
+
disable=not verbose,
|
| 358 |
+
dynamic_ncols=True,
|
| 359 |
+
leave=False,
|
| 360 |
+
)
|
| 361 |
+
):
|
| 362 |
+
random_R, random_tr = compute_random_augmentation(
|
| 363 |
+
multiplicity, device=atom_coords.device, dtype=atom_coords.dtype
|
| 364 |
+
) # (m, 3, 3), (m, 1, 3)
|
| 365 |
+
atom_coords = atom_coords - atom_coords.mean(dim=-2, keepdims=True) # (s, a, 3)
|
| 366 |
+
atom_coords = torch.einsum("bmd,bds->bms", atom_coords, random_R) + random_tr # (s, a, 3)
|
| 367 |
+
if atom_coords_denoised is not None:
|
| 368 |
+
atom_coords_denoised -= atom_coords_denoised.mean(dim=-2, keepdims=True) # (s, a, 3) or None before the first denoising step
|
| 369 |
+
atom_coords_denoised = (
|
| 370 |
+
torch.einsum("bmd,bds->bms", atom_coords_denoised, random_R) + random_tr
|
| 371 |
+
) # (s, a, 3) or None before the first denoising step
|
| 372 |
+
if (
|
| 373 |
+
steering_args["physical_guidance_update"]
|
| 374 |
+
or steering_args["contact_guidance_update"]
|
| 375 |
+
) and scaled_guidance_update is not None:
|
| 376 |
+
scaled_guidance_update = torch.einsum(
|
| 377 |
+
"bmd,bds->bms", scaled_guidance_update, random_R
|
| 378 |
+
) # (s, a, 3)
|
| 379 |
+
|
| 380 |
+
sigma_tm, sigma_t, gamma = sigma_tm.item(), sigma_t.item(), gamma.item() # three scalar tensors () become Python floats
|
| 381 |
+
|
| 382 |
+
t_hat = sigma_tm * (1 + gamma)
|
| 383 |
+
steering_t = 1.0 - (step_idx / num_sampling_steps)
|
| 384 |
+
noise_var = self.noise_scale**2 * (t_hat**2 - sigma_tm**2)
|
| 385 |
+
eps = sqrt(noise_var) * torch.randn(shape, device=self.device) # (s, a, 3)
|
| 386 |
+
atom_coords_noisy = atom_coords + eps # (s, a, 3)
|
| 387 |
+
|
| 388 |
+
with torch.no_grad():
|
| 389 |
+
atom_coords_denoised = torch.zeros_like(atom_coords_noisy) # (s, a, 3) or None before the first denoising step
|
| 390 |
+
sample_ids = torch.arange(multiplicity).to(atom_coords_noisy.device) # (m,)
|
| 391 |
+
sample_ids_chunks = sample_ids.chunk(multiplicity % max_parallel_samples + 1) # tuple of index tensors, each (n_chunk,)
|
| 392 |
+
|
| 393 |
+
for sample_ids_chunk in sample_ids_chunks:
|
| 394 |
+
atom_coords_denoised_chunk = self.preconditioned_network_forward(
|
| 395 |
+
atom_coords_noisy[sample_ids_chunk],
|
| 396 |
+
t_hat,
|
| 397 |
+
network_condition_kwargs=dict(
|
| 398 |
+
multiplicity=sample_ids_chunk.numel(),
|
| 399 |
+
**network_condition_kwargs,
|
| 400 |
+
),
|
| 401 |
+
) # (n_chunk, a, 3)
|
| 402 |
+
atom_coords_denoised[sample_ids_chunk] = atom_coords_denoised_chunk # (n_chunk, a, 3)
|
| 403 |
+
|
| 404 |
+
if steering_args["fk_steering"] and (
|
| 405 |
+
(step_idx % steering_args["fk_resampling_interval"] == 0 and noise_var > 0)
|
| 406 |
+
or step_idx == num_sampling_steps - 1
|
| 407 |
+
):
|
| 408 |
+
# Compute energy of x_0 prediction
|
| 409 |
+
energy = torch.zeros(multiplicity, device=self.device) # (m,)
|
| 410 |
+
for potential in potentials:
|
| 411 |
+
parameters = potential.compute_parameters(steering_t)
|
| 412 |
+
if parameters["resampling_weight"] > 0:
|
| 413 |
+
component_energy = potential.compute(
|
| 414 |
+
atom_coords_denoised,
|
| 415 |
+
network_condition_kwargs["feats"],
|
| 416 |
+
parameters,
|
| 417 |
+
) # (m,)
|
| 418 |
+
energy += parameters["resampling_weight"] * component_energy # (m,)
|
| 419 |
+
energy_traj = torch.cat((energy_traj, energy.unsqueeze(1)), dim=1) # (m, n_evaluation)
|
| 420 |
+
|
| 421 |
+
# Compute log G values
|
| 422 |
+
if step_idx == 0:
|
| 423 |
+
log_G = -1 * energy # (m,)
|
| 424 |
+
else:
|
| 425 |
+
log_G = energy_traj[:, -2] - energy_traj[:, -1] # (m,)
|
| 426 |
+
|
| 427 |
+
# Compute ll difference between guided and unguided transition distribution
|
| 428 |
+
if (
|
| 429 |
+
steering_args["physical_guidance_update"]
|
| 430 |
+
or steering_args["contact_guidance_update"]
|
| 431 |
+
) and noise_var > 0:
|
| 432 |
+
ll_difference = (eps**2 - (eps + scaled_guidance_update) ** 2).sum(
|
| 433 |
+
dim=(-1, -2)
|
| 434 |
+
) / (2 * noise_var) # (m,)
|
| 435 |
+
else:
|
| 436 |
+
ll_difference = torch.zeros_like(energy) # (m,)
|
| 437 |
+
|
| 438 |
+
# Compute resampling weights
|
| 439 |
+
resample_weights = F.softmax(
|
| 440 |
+
(ll_difference + steering_args["fk_lambda"] * log_G).reshape(
|
| 441 |
+
-1, steering_args["num_particles"]
|
| 442 |
+
),
|
| 443 |
+
dim=1,
|
| 444 |
+
) # (n_group, n_particle)
|
| 445 |
+
|
| 446 |
+
# Compute guidance update to x_0 prediction
|
| 447 |
+
if (
|
| 448 |
+
steering_args["physical_guidance_update"]
|
| 449 |
+
or steering_args["contact_guidance_update"]
|
| 450 |
+
) and step_idx < num_sampling_steps - 1:
|
| 451 |
+
guidance_update = torch.zeros_like(atom_coords_denoised) # (s, a, 3)
|
| 452 |
+
for guidance_step in range(steering_args["num_gd_steps"]):
|
| 453 |
+
energy_gradient = torch.zeros_like(atom_coords_denoised) # (s, a, 3)
|
| 454 |
+
for potential in potentials:
|
| 455 |
+
parameters = potential.compute_parameters(steering_t)
|
| 456 |
+
if (
|
| 457 |
+
parameters["guidance_weight"] > 0
|
| 458 |
+
and (guidance_step) % parameters["guidance_interval"] == 0
|
| 459 |
+
):
|
| 460 |
+
energy_gradient += parameters[
|
| 461 |
+
"guidance_weight"
|
| 462 |
+
] * potential.compute_gradient(
|
| 463 |
+
atom_coords_denoised + guidance_update,
|
| 464 |
+
network_condition_kwargs["feats"],
|
| 465 |
+
parameters,
|
| 466 |
+
) # (s, a, 3)
|
| 467 |
+
guidance_update -= energy_gradient # (s, a, 3)
|
| 468 |
+
atom_coords_denoised += guidance_update # (s, a, 3) or None before the first denoising step
|
| 469 |
+
scaled_guidance_update = (
|
| 470 |
+
guidance_update * -1 * self.step_scale * (sigma_t - t_hat) / t_hat
|
| 471 |
+
) # (s, a, 3)
|
| 472 |
+
|
| 473 |
+
if steering_args["fk_steering"] and (
|
| 474 |
+
(step_idx % steering_args["fk_resampling_interval"] == 0 and noise_var > 0)
|
| 475 |
+
or step_idx == num_sampling_steps - 1
|
| 476 |
+
):
|
| 477 |
+
resample_indices = (
|
| 478 |
+
torch.multinomial(
|
| 479 |
+
resample_weights,
|
| 480 |
+
resample_weights.shape[1] if step_idx < num_sampling_steps - 1 else 1,
|
| 481 |
+
replacement=True,
|
| 482 |
+
)
|
| 483 |
+
+ resample_weights.shape[1]
|
| 484 |
+
* torch.arange(
|
| 485 |
+
resample_weights.shape[0], device=resample_weights.device
|
| 486 |
+
).unsqueeze(-1)
|
| 487 |
+
).flatten() # (s_next,); final resampling keeps one particle per group
|
| 488 |
+
|
| 489 |
+
atom_coords = atom_coords[resample_indices] # (s, a, 3)
|
| 490 |
+
atom_coords_noisy = atom_coords_noisy[resample_indices] # (s, a, 3)
|
| 491 |
+
atom_mask = atom_mask[resample_indices] # (s, a); s expands on repetition and can shrink on resampling
|
| 492 |
+
if atom_coords_denoised is not None:
|
| 493 |
+
atom_coords_denoised = atom_coords_denoised[resample_indices] # (s, a, 3) or None before the first denoising step
|
| 494 |
+
energy_traj = energy_traj[resample_indices] # (s_next, n_evaluation)
|
| 495 |
+
if (
|
| 496 |
+
steering_args["physical_guidance_update"]
|
| 497 |
+
or steering_args["contact_guidance_update"]
|
| 498 |
+
):
|
| 499 |
+
scaled_guidance_update = scaled_guidance_update[resample_indices] # (s, a, 3)
|
| 500 |
+
if token_repr is not None:
|
| 501 |
+
token_repr = token_repr[resample_indices] # (s_next, *feature_shape)
|
| 502 |
+
|
| 503 |
+
if self.alignment_reverse_diff:
|
| 504 |
+
with torch.autocast("cuda", enabled=False):
|
| 505 |
+
atom_coords_noisy = weighted_rigid_align(
|
| 506 |
+
atom_coords_noisy.float(),
|
| 507 |
+
atom_coords_denoised.float(),
|
| 508 |
+
atom_mask.float(),
|
| 509 |
+
atom_mask.float(),
|
| 510 |
+
) # (s, a, 3)
|
| 511 |
+
|
| 512 |
+
atom_coords_noisy = atom_coords_noisy.to(atom_coords_denoised) # (s, a, 3)
|
| 513 |
+
|
| 514 |
+
denoised_over_sigma = (atom_coords_noisy - atom_coords_denoised) / t_hat # (s, a, 3)
|
| 515 |
+
atom_coords_next = (
|
| 516 |
+
atom_coords_noisy + step_scale * (sigma_t - t_hat) * denoised_over_sigma
|
| 517 |
+
) # (s, a, 3)
|
| 518 |
+
|
| 519 |
+
atom_coords = atom_coords_next # (s, a, 3)
|
| 520 |
+
|
| 521 |
+
return dict(sample_atom_coords=atom_coords, diff_token_repr=token_repr)
|
| 522 |
+
|
| 523 |
+
def loss_weight(self, sigma: torch.Tensor) -> torch.Tensor:
|
| 524 |
+
# sigma: (...); weight preserves shape (...).
|
| 525 |
+
return (sigma**2 + self.sigma_data**2) / ((sigma * self.sigma_data) ** 2)
|
| 526 |
+
|
| 527 |
+
def noise_distribution(self, batch_size: int) -> torch.Tensor:
|
| 528 |
+
# Return one noise scale per example, shape (batch_size,).
|
| 529 |
+
return (
|
| 530 |
+
self.sigma_data
|
| 531 |
+
* (self.P_mean + self.P_std * torch.randn((batch_size,), device=self.device)).exp()
|
| 532 |
+
)
|
| 533 |
+
|
| 534 |
+
def forward(
|
| 535 |
+
self,
|
| 536 |
+
s_inputs: torch.Tensor,
|
| 537 |
+
s_trunk: torch.Tensor,
|
| 538 |
+
feats: dict[str, torch.Tensor],
|
| 539 |
+
diffusion_conditioning: dict[str, Any],
|
| 540 |
+
multiplicity: int = 1,
|
| 541 |
+
) -> dict[str, Any]:
|
| 542 |
+
# training diffusion step
|
| 543 |
+
batch_size = feats["coords"].shape[0] // multiplicity
|
| 544 |
+
|
| 545 |
+
if self.synchronize_sigmas:
|
| 546 |
+
sigmas = self.noise_distribution(batch_size).repeat_interleave(multiplicity, 0) # (n_sigma,); schedule or batch axis is determined by the caller
|
| 547 |
+
else:
|
| 548 |
+
sigmas = self.noise_distribution(batch_size * multiplicity) # (n_sigma,); schedule or batch axis is determined by the caller
|
| 549 |
+
padded_sigmas = rearrange(sigmas, "b -> b 1 1") # (b * m, 1, 1)
|
| 550 |
+
|
| 551 |
+
atom_coords = feats["coords"] # (s, a, 3)
|
| 552 |
+
|
| 553 |
+
atom_mask = feats["atom_pad_mask"] # (s, a); s expands on repetition and can shrink on resampling
|
| 554 |
+
atom_mask = atom_mask.repeat_interleave(multiplicity, 0) # (s, a); s expands on repetition and can shrink on resampling
|
| 555 |
+
|
| 556 |
+
atom_coords = center_random_augmentation(
|
| 557 |
+
atom_coords, atom_mask, augmentation=self.coordinate_augmentation
|
| 558 |
+
) # (s, a, 3)
|
| 559 |
+
|
| 560 |
+
noise = torch.randn_like(atom_coords) # (b * m, a, 3)
|
| 561 |
+
noised_atom_coords = atom_coords + padded_sigmas * noise # (b * m, a, 3)
|
| 562 |
+
|
| 563 |
+
denoised_atom_coords = self.preconditioned_network_forward(
|
| 564 |
+
noised_atom_coords,
|
| 565 |
+
sigmas,
|
| 566 |
+
network_condition_kwargs={
|
| 567 |
+
"s_inputs": s_inputs,
|
| 568 |
+
"s_trunk": s_trunk,
|
| 569 |
+
"feats": feats,
|
| 570 |
+
"multiplicity": multiplicity,
|
| 571 |
+
"diffusion_conditioning": diffusion_conditioning,
|
| 572 |
+
},
|
| 573 |
+
) # (b * m, a, 3)
|
| 574 |
+
|
| 575 |
+
return {
|
| 576 |
+
"denoised_atom_coords": denoised_atom_coords,
|
| 577 |
+
"sigmas": sigmas,
|
| 578 |
+
"aligned_true_atom_coords": atom_coords,
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
def compute_loss(
|
| 582 |
+
self,
|
| 583 |
+
feats: dict[str, torch.Tensor],
|
| 584 |
+
out_dict: dict[str, torch.Tensor],
|
| 585 |
+
add_smooth_lddt_loss: bool = True,
|
| 586 |
+
nucleotide_loss_weight: float = 5.0,
|
| 587 |
+
ligand_loss_weight: float = 10.0,
|
| 588 |
+
multiplicity: int = 1,
|
| 589 |
+
filter_by_plddt: float = 0.0,
|
| 590 |
+
) -> dict[str, Any]:
|
| 591 |
+
# Coordinates: (b * m, a, 3); base feature masks: (b, a).
|
| 592 |
+
with torch.autocast("cuda", enabled=False):
|
| 593 |
+
denoised_atom_coords = out_dict["denoised_atom_coords"].float() # (b * m, a, 3)
|
| 594 |
+
sigmas = out_dict["sigmas"].float() # (n_sigma,); schedule or batch axis is determined by the caller
|
| 595 |
+
|
| 596 |
+
resolved_atom_mask_uni = feats["atom_resolved_mask"].float() # (b, a)
|
| 597 |
+
|
| 598 |
+
if filter_by_plddt > 0:
|
| 599 |
+
plddt_mask = feats["plddt"] > filter_by_plddt # (b, a)
|
| 600 |
+
resolved_atom_mask_uni = resolved_atom_mask_uni * plddt_mask.float() # (b, a)
|
| 601 |
+
|
| 602 |
+
resolved_atom_mask = resolved_atom_mask_uni.repeat_interleave(multiplicity, 0) # (b * m, a)
|
| 603 |
+
|
| 604 |
+
align_weights = denoised_atom_coords.new_ones(denoised_atom_coords.shape[:2]) # (b * m, a)
|
| 605 |
+
atom_type = (
|
| 606 |
+
torch.bmm(
|
| 607 |
+
feats["atom_to_token"].float(),
|
| 608 |
+
feats["mol_type"].unsqueeze(-1).float(),
|
| 609 |
+
)
|
| 610 |
+
.squeeze(-1)
|
| 611 |
+
.long()
|
| 612 |
+
) # (b, a)
|
| 613 |
+
atom_type_mult = atom_type.repeat_interleave(multiplicity, 0) # (b * m, a)
|
| 614 |
+
|
| 615 |
+
align_weights = (
|
| 616 |
+
align_weights
|
| 617 |
+
* (
|
| 618 |
+
1
|
| 619 |
+
+ nucleotide_loss_weight
|
| 620 |
+
* (
|
| 621 |
+
torch.eq(atom_type_mult, const.chain_type_ids["DNA"]).float()
|
| 622 |
+
+ torch.eq(atom_type_mult, const.chain_type_ids["RNA"]).float()
|
| 623 |
+
)
|
| 624 |
+
+ ligand_loss_weight
|
| 625 |
+
* torch.eq(atom_type_mult, const.chain_type_ids["NONPOLYMER"]).float()
|
| 626 |
+
).float()
|
| 627 |
+
) # (b * m, a)
|
| 628 |
+
|
| 629 |
+
atom_coords = out_dict["aligned_true_atom_coords"].float() # (s, a, 3)
|
| 630 |
+
atom_coords_aligned_ground_truth = weighted_rigid_align(
|
| 631 |
+
atom_coords.detach(),
|
| 632 |
+
denoised_atom_coords.detach(),
|
| 633 |
+
align_weights.detach(),
|
| 634 |
+
mask=feats["atom_resolved_mask"]
|
| 635 |
+
.float()
|
| 636 |
+
.repeat_interleave(multiplicity, 0)
|
| 637 |
+
.detach(),
|
| 638 |
+
) # (b * m, a, 3)
|
| 639 |
+
|
| 640 |
+
atom_coords_aligned_ground_truth = atom_coords_aligned_ground_truth.to(
|
| 641 |
+
denoised_atom_coords
|
| 642 |
+
) # (b * m, a, 3)
|
| 643 |
+
|
| 644 |
+
# weighted MSE loss of denoised atom positions
|
| 645 |
+
mse_loss = ((denoised_atom_coords - atom_coords_aligned_ground_truth) ** 2).sum(dim=-1) # (b * m, a)
|
| 646 |
+
mse_loss = torch.sum(mse_loss * align_weights * resolved_atom_mask, dim=-1) / (
|
| 647 |
+
torch.sum(3 * align_weights * resolved_atom_mask, dim=-1) + 1e-5
|
| 648 |
+
) # (b * m,)
|
| 649 |
+
|
| 650 |
+
# weight by sigma factor
|
| 651 |
+
loss_weights = self.loss_weight(sigmas) # (b * m,)
|
| 652 |
+
mse_loss = (mse_loss * loss_weights).mean() # ()
|
| 653 |
+
|
| 654 |
+
total_loss = mse_loss # ()
|
| 655 |
+
|
| 656 |
+
# proposed auxiliary smooth lddt loss
|
| 657 |
+
lddt_loss = self.zero # ()
|
| 658 |
+
if add_smooth_lddt_loss:
|
| 659 |
+
lddt_loss = smooth_lddt_loss(
|
| 660 |
+
denoised_atom_coords,
|
| 661 |
+
feats["coords"],
|
| 662 |
+
torch.eq(atom_type, const.chain_type_ids["DNA"]).float()
|
| 663 |
+
+ torch.eq(atom_type, const.chain_type_ids["RNA"]).float(),
|
| 664 |
+
coords_mask=resolved_atom_mask_uni,
|
| 665 |
+
multiplicity=multiplicity,
|
| 666 |
+
) # ()
|
| 667 |
+
|
| 668 |
+
total_loss = total_loss + lddt_loss # ()
|
| 669 |
+
|
| 670 |
+
loss_breakdown = {
|
| 671 |
+
"mse_loss": mse_loss,
|
| 672 |
+
"smooth_lddt_loss": lddt_loss,
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
return {"loss": total_loss, "loss_breakdown": loss_breakdown}
|
fastplms/models/boltz/vb_modules_encodersv2.py
CHANGED
|
@@ -1,653 +1,653 @@
|
|
| 1 |
-
"""Token, pair, and atom encoders used by the local Boltz2 runtime.
|
| 2 |
-
|
| 3 |
-
The classes retain checkpoint-facing submodule names while separating feature
|
| 4 |
-
assembly, window indexing, conditioning, and atom-token aggregation into small
|
| 5 |
-
mechanism-specific units. No production import depends on the upstream Boltz
|
| 6 |
-
package.
|
| 7 |
-
|
| 8 |
-
The Fourier and atom-attention mechanisms derive from AlphaFold 3 community
|
| 9 |
-
implementations under MIT terms. See ``THIRD_PARTY_NOTICES.md``.
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
from __future__ import annotations
|
| 13 |
-
|
| 14 |
-
import torch
|
| 15 |
-
|
| 16 |
-
from collections.abc import Callable
|
| 17 |
-
from functools import partial
|
| 18 |
-
from math import pi
|
| 19 |
-
from torch import nn
|
| 20 |
-
from torch.nn.functional import one_hot
|
| 21 |
-
|
| 22 |
-
from . import vb_layers_initialize as init
|
| 23 |
-
from .vb_layers_transition import Transition
|
| 24 |
-
from .vb_modules_transformersv2 import AtomTransformer
|
| 25 |
-
from .vb_modules_utils import LinearNoBias
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _transition_stack(
|
| 29 |
-
count: int,
|
| 30 |
-
dim: int,
|
| 31 |
-
hidden_dim: int,
|
| 32 |
-
) -> nn.ModuleList:
|
| 33 |
-
return nn.ModuleList([Transition(dim=dim, hidden=hidden_dim) for _ in range(count)])
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
class FourierEmbedding(nn.Module):
|
| 37 |
-
"""Embed diffusion time with fixed random Fourier frequencies."""
|
| 38 |
-
|
| 39 |
-
def __init__(self, dim: int) -> None:
|
| 40 |
-
super().__init__()
|
| 41 |
-
self.proj = nn.Linear(1, dim)
|
| 42 |
-
nn.init.normal_(self.proj.weight, mean=0, std=1)
|
| 43 |
-
nn.init.normal_(self.proj.bias, mean=0, std=1)
|
| 44 |
-
self.proj.requires_grad_(False)
|
| 45 |
-
|
| 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)) # (b, d)
|
| 50 |
-
return torch.cos(2 * pi * random_phase) # (b, d)
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def _pairwise_difference(values: torch.Tensor) -> torch.Tensor:
|
| 54 |
-
# values: (b, n)
|
| 55 |
-
return values[:, :, None] - values[:, None, :] # (b, n, n)
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
class RelativePositionEncoder(nn.Module):
|
| 59 |
-
"""Encode residue, token, entity, and symmetry relationships."""
|
| 60 |
-
|
| 61 |
-
def __init__(
|
| 62 |
-
self,
|
| 63 |
-
token_z: int,
|
| 64 |
-
r_max: int = 32,
|
| 65 |
-
s_max: int = 2,
|
| 66 |
-
fix_sym_check: bool = False,
|
| 67 |
-
cyclic_pos_enc: bool = False,
|
| 68 |
-
) -> None:
|
| 69 |
-
super().__init__()
|
| 70 |
-
self.r_max = r_max
|
| 71 |
-
self.s_max = s_max
|
| 72 |
-
input_dim = 4 * (r_max + 1) + 2 * (s_max + 1) + 1
|
| 73 |
-
self.linear_layer = LinearNoBias(input_dim, token_z)
|
| 74 |
-
self.fix_sym_check = fix_sym_check
|
| 75 |
-
self.cyclic_pos_enc = cyclic_pos_enc
|
| 76 |
-
|
| 77 |
-
def forward(self, feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 78 |
-
"""Return relative pair features ``Z: (b, n, n, token_z)``."""
|
| 79 |
-
|
| 80 |
-
same_chain = (
|
| 81 |
-
feats["asym_id"][:, :, None] == feats["asym_id"][:, None, :]
|
| 82 |
-
) # (b, n, n)
|
| 83 |
-
same_residue = (
|
| 84 |
-
feats["residue_index"][:, :, None] == feats["residue_index"][:, None, :]
|
| 85 |
-
) # (b, n, n)
|
| 86 |
-
same_entity = (
|
| 87 |
-
feats["entity_id"][:, :, None] == feats["entity_id"][:, None, :]
|
| 88 |
-
) # (b, n, n)
|
| 89 |
-
|
| 90 |
-
residue_offset = _pairwise_difference(feats["residue_index"]) # (b, n, n)
|
| 91 |
-
if self.cyclic_pos_enc and torch.any(feats["cyclic_period"] > 0):
|
| 92 |
-
period = torch.where(
|
| 93 |
-
feats["cyclic_period"] > 0,
|
| 94 |
-
feats["cyclic_period"],
|
| 95 |
-
torch.zeros_like(feats["cyclic_period"]) + 10000,
|
| 96 |
-
) # (b, n)
|
| 97 |
-
residue_offset = (
|
| 98 |
-
residue_offset - period * torch.round(residue_offset / period)
|
| 99 |
-
).long() # (b, n, n)
|
| 100 |
-
residue_offset = torch.clip(
|
| 101 |
-
residue_offset + self.r_max,
|
| 102 |
-
0,
|
| 103 |
-
2 * self.r_max,
|
| 104 |
-
) # (b, n, n)
|
| 105 |
-
residue_offset = torch.where(
|
| 106 |
-
same_chain,
|
| 107 |
-
residue_offset,
|
| 108 |
-
torch.zeros_like(residue_offset) + 2 * self.r_max + 1,
|
| 109 |
-
) # (b, n, n)
|
| 110 |
-
residue_features = one_hot(
|
| 111 |
-
residue_offset, 2 * self.r_max + 2
|
| 112 |
-
) # (b, n, n, 2 * r_max + 2)
|
| 113 |
-
|
| 114 |
-
token_offset = torch.clip(
|
| 115 |
-
_pairwise_difference(feats["token_index"]) + self.r_max,
|
| 116 |
-
0,
|
| 117 |
-
2 * self.r_max,
|
| 118 |
-
) # (b, n, n)
|
| 119 |
-
token_offset = torch.where(
|
| 120 |
-
same_chain & same_residue,
|
| 121 |
-
token_offset,
|
| 122 |
-
torch.zeros_like(token_offset) + 2 * self.r_max + 1,
|
| 123 |
-
) # (b, n, n)
|
| 124 |
-
token_features = one_hot(
|
| 125 |
-
token_offset, 2 * self.r_max + 2
|
| 126 |
-
) # (b, n, n, 2 * r_max + 2)
|
| 127 |
-
|
| 128 |
-
symmetry_offset = torch.clip(
|
| 129 |
-
_pairwise_difference(feats["sym_id"]) + self.s_max,
|
| 130 |
-
0,
|
| 131 |
-
2 * self.s_max,
|
| 132 |
-
) # (b, n, n)
|
| 133 |
-
invalid_symmetry = ~same_entity if self.fix_sym_check else same_chain # (b, n, n)
|
| 134 |
-
symmetry_offset = torch.where(
|
| 135 |
-
invalid_symmetry,
|
| 136 |
-
torch.zeros_like(symmetry_offset) + 2 * self.s_max + 1,
|
| 137 |
-
symmetry_offset,
|
| 138 |
-
) # (b, n, n)
|
| 139 |
-
symmetry_features = one_hot(
|
| 140 |
-
symmetry_offset, 2 * self.s_max + 2
|
| 141 |
-
) # (b, n, n, 2 * s_max + 2)
|
| 142 |
-
|
| 143 |
-
pair_features = torch.cat(
|
| 144 |
-
(
|
| 145 |
-
residue_features.float(),
|
| 146 |
-
token_features.float(),
|
| 147 |
-
same_entity.unsqueeze(-1).float(),
|
| 148 |
-
symmetry_features.float(),
|
| 149 |
-
),
|
| 150 |
-
dim=-1,
|
| 151 |
-
) # (b, n, n, d_rel)
|
| 152 |
-
return self.linear_layer(pair_features) # (b, n, n, token_z)
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
class SingleConditioning(nn.Module):
|
| 156 |
-
"""Condition token features on trunk inputs and diffusion time."""
|
| 157 |
-
|
| 158 |
-
def __init__(
|
| 159 |
-
self,
|
| 160 |
-
sigma_data: float,
|
| 161 |
-
token_s: int = 384,
|
| 162 |
-
dim_fourier: int = 256,
|
| 163 |
-
num_transitions: int = 2,
|
| 164 |
-
transition_expansion_factor: int = 2,
|
| 165 |
-
eps: float = 1e-20,
|
| 166 |
-
disable_times: bool = False,
|
| 167 |
-
) -> None:
|
| 168 |
-
super().__init__()
|
| 169 |
-
self.eps = eps
|
| 170 |
-
self.sigma_data = sigma_data
|
| 171 |
-
self.disable_times = disable_times
|
| 172 |
-
conditioning_dim = 2 * token_s
|
| 173 |
-
self.norm_single = nn.LayerNorm(conditioning_dim)
|
| 174 |
-
self.single_embed = nn.Linear(conditioning_dim, conditioning_dim)
|
| 175 |
-
if not disable_times:
|
| 176 |
-
self.fourier_embed = FourierEmbedding(dim_fourier)
|
| 177 |
-
self.norm_fourier = nn.LayerNorm(dim_fourier)
|
| 178 |
-
self.fourier_to_single = LinearNoBias(dim_fourier, conditioning_dim)
|
| 179 |
-
self.transitions = _transition_stack(
|
| 180 |
-
num_transitions,
|
| 181 |
-
conditioning_dim,
|
| 182 |
-
transition_expansion_factor * conditioning_dim,
|
| 183 |
-
)
|
| 184 |
-
|
| 185 |
-
def forward(
|
| 186 |
-
self,
|
| 187 |
-
times: torch.Tensor,
|
| 188 |
-
s_trunk: torch.Tensor,
|
| 189 |
-
s_inputs: torch.Tensor,
|
| 190 |
-
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
| 191 |
-
"""Return conditioned token features ``S: (b, n, 2 * token_s)``."""
|
| 192 |
-
|
| 193 |
-
# times: (b,); s_trunk/s_inputs: (b, n, token_s).
|
| 194 |
-
single = torch.cat((s_trunk, s_inputs), dim=-1) # (b, n, 2 * token_s)
|
| 195 |
-
single = self.single_embed(self.norm_single(single)) # (b, n, 2 * token_s)
|
| 196 |
-
normalized_fourier = None # (b, d_fourier) or None
|
| 197 |
-
if not self.disable_times:
|
| 198 |
-
fourier = self.fourier_embed(times) # (b, d_fourier)
|
| 199 |
-
normalized_fourier = self.norm_fourier(fourier) # (b, d_fourier)
|
| 200 |
-
time_condition = self.fourier_to_single(
|
| 201 |
-
normalized_fourier
|
| 202 |
-
) # (b, 2 * token_s)
|
| 203 |
-
single = time_condition[:, None, :] + single # (b, n, 2 * token_s)
|
| 204 |
-
for transition in self.transitions:
|
| 205 |
-
single = transition(single) + single # (b, n, 2 * token_s)
|
| 206 |
-
return single, normalized_fourier # (b, n, 2 * token_s), (b, d_fourier) or None
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
class PairwiseConditioning(nn.Module):
|
| 210 |
-
"""Fuse trunk pair features with relative-position features."""
|
| 211 |
-
|
| 212 |
-
def __init__(
|
| 213 |
-
self,
|
| 214 |
-
token_z: int,
|
| 215 |
-
dim_token_rel_pos_feats: int,
|
| 216 |
-
num_transitions: int = 2,
|
| 217 |
-
transition_expansion_factor: int = 2,
|
| 218 |
-
) -> None:
|
| 219 |
-
super().__init__()
|
| 220 |
-
combined_dim = token_z + dim_token_rel_pos_feats
|
| 221 |
-
self.dim_pairwise_init_proj = nn.Sequential(
|
| 222 |
-
nn.LayerNorm(combined_dim),
|
| 223 |
-
LinearNoBias(combined_dim, token_z),
|
| 224 |
-
)
|
| 225 |
-
self.transitions = _transition_stack(
|
| 226 |
-
num_transitions,
|
| 227 |
-
token_z,
|
| 228 |
-
transition_expansion_factor * token_z,
|
| 229 |
-
)
|
| 230 |
-
|
| 231 |
-
def forward(
|
| 232 |
-
self,
|
| 233 |
-
z_trunk: torch.Tensor,
|
| 234 |
-
token_rel_pos_feats: torch.Tensor,
|
| 235 |
-
) -> torch.Tensor:
|
| 236 |
-
"""Return conditioned pair features ``Z: (b, n, n, token_z)``."""
|
| 237 |
-
|
| 238 |
-
pair = self.dim_pairwise_init_proj(
|
| 239 |
-
torch.cat((z_trunk, token_rel_pos_feats), dim=-1)
|
| 240 |
-
) # (b, n, n, token_z)
|
| 241 |
-
for transition in self.transitions:
|
| 242 |
-
pair = transition(pair) + pair # (b, n, n, token_z)
|
| 243 |
-
return pair # (b, n, n, token_z)
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
def get_indexing_matrix(
|
| 247 |
-
k: int,
|
| 248 |
-
w: int,
|
| 249 |
-
h: int,
|
| 250 |
-
device: torch.device,
|
| 251 |
-
) -> torch.Tensor:
|
| 252 |
-
"""Build the atom-window gather matrix used for local attention keys."""
|
| 253 |
-
|
| 254 |
-
for name, value in (("k", k), ("w", w), ("h", h)):
|
| 255 |
-
if isinstance(value, bool) or not isinstance(value, int):
|
| 256 |
-
raise TypeError(f"{name} must be an int, got {type(value).__name__}.")
|
| 257 |
-
if value <= 0:
|
| 258 |
-
raise ValueError(f"{name} must be positive, got {value}.")
|
| 259 |
-
if w % 2 != 0:
|
| 260 |
-
raise ValueError(f"w must be even, got {w}.")
|
| 261 |
-
half_window = w // 2
|
| 262 |
-
if h % half_window != 0:
|
| 263 |
-
raise ValueError(f"h must be divisible by w // 2 ({half_window}), got {h}.")
|
| 264 |
-
key_blocks = h // half_window
|
| 265 |
-
if key_blocks % 2 != 0:
|
| 266 |
-
raise ValueError(
|
| 267 |
-
f"h must contain an even number of half-window key blocks; received {key_blocks}."
|
| 268 |
-
)
|
| 269 |
-
|
| 270 |
-
positions = torch.arange(2 * k, device=device) # (2 * k,)
|
| 271 |
-
relative_blocks = (
|
| 272 |
-
(positions.unsqueeze(0) - positions.unsqueeze(1)) + key_blocks // 2
|
| 273 |
-
).clamp(min=0, max=key_blocks + 1) # (2 * k, 2 * k)
|
| 274 |
-
relative_blocks = relative_blocks.view(k, 2, 2 * k)[:, 0, :] # (k, 2 * k)
|
| 275 |
-
selectors = one_hot(relative_blocks, num_classes=key_blocks + 2)[
|
| 276 |
-
..., 1:-1
|
| 277 |
-
].transpose(1, 0) # (2 * k, k, key_blocks)
|
| 278 |
-
return selectors.reshape(
|
| 279 |
-
2 * k, key_blocks * k
|
| 280 |
-
).float() # (2 * k, key_blocks * k)
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
def single_to_keys(
|
| 284 |
-
single: torch.Tensor,
|
| 285 |
-
indexing_matrix: torch.Tensor,
|
| 286 |
-
w: int,
|
| 287 |
-
h: int,
|
| 288 |
-
) -> torch.Tensor:
|
| 289 |
-
"""Gather a sequence tensor into overlapping local key windows."""
|
| 290 |
-
|
| 291 |
-
b, n, d = single.shape
|
| 292 |
-
k = n // w
|
| 293 |
-
half_windows = single.view(b, 2 * k, w // 2, d) # (b, 2 * k, w / 2, d)
|
| 294 |
-
gathered = torch.einsum(
|
| 295 |
-
"b j i d, j k -> b k i d", half_windows, indexing_matrix
|
| 296 |
-
) # (b, key_blocks * k, w / 2, d)
|
| 297 |
-
return gathered.reshape(b, k, h, d) # (b, k, h, d)
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
class AtomEncoder(nn.Module):
|
| 301 |
-
"""Encode reference atoms and local atom-pair geometry."""
|
| 302 |
-
|
| 303 |
-
def __init__(
|
| 304 |
-
self,
|
| 305 |
-
atom_s: int,
|
| 306 |
-
atom_z: int,
|
| 307 |
-
token_s: int,
|
| 308 |
-
token_z: int,
|
| 309 |
-
atoms_per_window_queries: int,
|
| 310 |
-
atoms_per_window_keys: int,
|
| 311 |
-
atom_feature_dim: int,
|
| 312 |
-
structure_prediction: bool = True,
|
| 313 |
-
use_no_atom_char: bool = False,
|
| 314 |
-
use_atom_backbone_feat: bool = False,
|
| 315 |
-
use_residue_feats_atoms: bool = False,
|
| 316 |
-
) -> None:
|
| 317 |
-
super().__init__()
|
| 318 |
-
self.embed_atom_features = nn.Linear(atom_feature_dim, atom_s)
|
| 319 |
-
self.embed_atompair_ref_pos = LinearNoBias(3, atom_z)
|
| 320 |
-
self.embed_atompair_ref_dist = LinearNoBias(1, atom_z)
|
| 321 |
-
self.embed_atompair_mask = LinearNoBias(1, atom_z)
|
| 322 |
-
self.atoms_per_window_queries = atoms_per_window_queries
|
| 323 |
-
self.atoms_per_window_keys = atoms_per_window_keys
|
| 324 |
-
self.use_no_atom_char = use_no_atom_char
|
| 325 |
-
self.use_atom_backbone_feat = use_atom_backbone_feat
|
| 326 |
-
self.use_residue_feats_atoms = use_residue_feats_atoms
|
| 327 |
-
self.structure_prediction = structure_prediction
|
| 328 |
-
|
| 329 |
-
if structure_prediction:
|
| 330 |
-
self.s_to_c_trans = nn.Sequential(
|
| 331 |
-
nn.LayerNorm(token_s),
|
| 332 |
-
LinearNoBias(token_s, atom_s),
|
| 333 |
-
)
|
| 334 |
-
init.final_init_(self.s_to_c_trans[1].weight)
|
| 335 |
-
self.z_to_p_trans = nn.Sequential(
|
| 336 |
-
nn.LayerNorm(token_z),
|
| 337 |
-
LinearNoBias(token_z, atom_z),
|
| 338 |
-
)
|
| 339 |
-
init.final_init_(self.z_to_p_trans[1].weight)
|
| 340 |
-
|
| 341 |
-
self.c_to_p_trans_k = nn.Sequential(nn.ReLU(), LinearNoBias(atom_s, atom_z))
|
| 342 |
-
init.final_init_(self.c_to_p_trans_k[1].weight)
|
| 343 |
-
self.c_to_p_trans_q = nn.Sequential(nn.ReLU(), LinearNoBias(atom_s, atom_z))
|
| 344 |
-
init.final_init_(self.c_to_p_trans_q[1].weight)
|
| 345 |
-
self.p_mlp = nn.Sequential(
|
| 346 |
-
nn.ReLU(),
|
| 347 |
-
LinearNoBias(atom_z, atom_z),
|
| 348 |
-
nn.ReLU(),
|
| 349 |
-
LinearNoBias(atom_z, atom_z),
|
| 350 |
-
nn.ReLU(),
|
| 351 |
-
LinearNoBias(atom_z, atom_z),
|
| 352 |
-
)
|
| 353 |
-
init.final_init_(self.p_mlp[5].weight)
|
| 354 |
-
|
| 355 |
-
def _assemble_atom_features(
|
| 356 |
-
self,
|
| 357 |
-
feats: dict[str, torch.Tensor],
|
| 358 |
-
b: int,
|
| 359 |
-
n: int,
|
| 360 |
-
) -> torch.Tensor:
|
| 361 |
-
# Feature tensors share leading shape (b, n).
|
| 362 |
-
feature_parts = [
|
| 363 |
-
feats["ref_pos"],
|
| 364 |
-
feats["ref_charge"].unsqueeze(-1),
|
| 365 |
-
feats["ref_element"],
|
| 366 |
-
] # each: (b, n, d_feature)
|
| 367 |
-
if not self.use_no_atom_char:
|
| 368 |
-
feature_parts.append(feats["ref_atom_name_chars"].reshape(b, n, 4 * 64))
|
| 369 |
-
if self.use_atom_backbone_feat:
|
| 370 |
-
feature_parts.append(feats["atom_backbone_feat"])
|
| 371 |
-
if self.use_residue_feats_atoms:
|
| 372 |
-
residue_features = torch.cat(
|
| 373 |
-
(
|
| 374 |
-
feats["res_type"],
|
| 375 |
-
feats["modified"].unsqueeze(-1),
|
| 376 |
-
one_hot(feats["mol_type"], num_classes=4).float(),
|
| 377 |
-
),
|
| 378 |
-
dim=-1,
|
| 379 |
-
) # (b, t, d_residue)
|
| 380 |
-
feature_parts.append(
|
| 381 |
-
torch.bmm(feats["atom_to_token"].float(), residue_features)
|
| 382 |
-
) # (b, n, d_residue)
|
| 383 |
-
return torch.cat(feature_parts, dim=-1) # (b, n, atom_feature_dim)
|
| 384 |
-
|
| 385 |
-
def forward(
|
| 386 |
-
self,
|
| 387 |
-
feats: dict[str, torch.Tensor],
|
| 388 |
-
s_trunk: torch.Tensor | None = None,
|
| 389 |
-
z: torch.Tensor | None = None,
|
| 390 |
-
) -> tuple[
|
| 391 |
-
torch.Tensor,
|
| 392 |
-
torch.Tensor,
|
| 393 |
-
torch.Tensor,
|
| 394 |
-
Callable[[torch.Tensor], torch.Tensor],
|
| 395 |
-
]:
|
| 396 |
-
"""Return atom queries, conditioned singles, pairs, and key gatherer."""
|
| 397 |
-
|
| 398 |
-
with torch.autocast("cuda", enabled=False):
|
| 399 |
-
b, n, _ = feats["ref_pos"].shape
|
| 400 |
-
atom_mask = feats["atom_pad_mask"].bool() # (b, n)
|
| 401 |
-
atom_positions = feats["ref_pos"] # (b, n, 3)
|
| 402 |
-
atom_space = feats["ref_space_uid"] # (b, n)
|
| 403 |
-
atom_features = self._assemble_atom_features(
|
| 404 |
-
feats, b, n
|
| 405 |
-
) # (b, n, atom_feature_dim)
|
| 406 |
-
conditioned_single = self.embed_atom_features(
|
| 407 |
-
atom_features
|
| 408 |
-
) # (b, n, atom_s)
|
| 409 |
-
|
| 410 |
-
w = self.atoms_per_window_queries
|
| 411 |
-
h = self.atoms_per_window_keys
|
| 412 |
-
b, n = conditioned_single.shape[:2]
|
| 413 |
-
k = n // w
|
| 414 |
-
indexing = get_indexing_matrix(
|
| 415 |
-
k, w, h, conditioned_single.device
|
| 416 |
-
) # (2 * k, key_blocks * k)
|
| 417 |
-
to_keys = partial(single_to_keys, indexing_matrix=indexing, w=w, h=h)
|
| 418 |
-
|
| 419 |
-
position_queries = atom_positions.view(b, k, w, 1, 3) # (b, k, w, 1, 3)
|
| 420 |
-
position_keys = to_keys(atom_positions).view(
|
| 421 |
-
b, k, 1, h, 3
|
| 422 |
-
) # (b, k, 1, h, 3)
|
| 423 |
-
displacement = position_keys - position_queries # (b, k, w, h, 3)
|
| 424 |
-
inverse_squared_distance = 1 / (
|
| 425 |
-
1 + torch.sum(displacement * displacement, dim=-1, keepdim=True)
|
| 426 |
-
) # (b, k, w, h, 1)
|
| 427 |
-
|
| 428 |
-
mask_queries = atom_mask.view(b, k, w, 1) # (b, k, w, 1)
|
| 429 |
-
mask_keys = (
|
| 430 |
-
to_keys(atom_mask.unsqueeze(-1).float()).view(b, k, 1, h).bool()
|
| 431 |
-
) # (b, k, 1, h)
|
| 432 |
-
space_queries = atom_space.view(b, k, w, 1) # (b, k, w, 1)
|
| 433 |
-
space_keys = (
|
| 434 |
-
to_keys(atom_space.unsqueeze(-1).float()).view(b, k, 1, h).long()
|
| 435 |
-
) # (b, k, 1, h)
|
| 436 |
-
valid_pair = (
|
| 437 |
-
(mask_queries & mask_keys & (space_queries == space_keys)).float().unsqueeze(-1)
|
| 438 |
-
) # (b, k, w, h, 1)
|
| 439 |
-
|
| 440 |
-
pair = self.embed_atompair_ref_pos(displacement) * valid_pair # (b, k, w, h, atom_z)
|
| 441 |
-
pair = (
|
| 442 |
-
pair + self.embed_atompair_ref_dist(inverse_squared_distance) * valid_pair
|
| 443 |
-
) # (b, k, w, h, atom_z)
|
| 444 |
-
pair = pair + self.embed_atompair_mask(valid_pair) * valid_pair # (b, k, w, h, atom_z)
|
| 445 |
-
query = conditioned_single # (b, n, atom_s)
|
| 446 |
-
|
| 447 |
-
if self.structure_prediction:
|
| 448 |
-
if s_trunk is None or z is None:
|
| 449 |
-
raise ValueError("structure prediction requires S and Z trunk tensors")
|
| 450 |
-
atom_to_token = feats["atom_to_token"].float() # (b, n, t)
|
| 451 |
-
single_update = self.s_to_c_trans(s_trunk.float()) # (b, t, atom_s)
|
| 452 |
-
single_update = torch.bmm(
|
| 453 |
-
atom_to_token, single_update
|
| 454 |
-
) # (b, n, atom_s)
|
| 455 |
-
conditioned_single = conditioned_single + single_update.to(
|
| 456 |
-
conditioned_single
|
| 457 |
-
) # (b, n, atom_s)
|
| 458 |
-
|
| 459 |
-
token_queries = atom_to_token.view(
|
| 460 |
-
b, k, w, atom_to_token.shape[-1]
|
| 461 |
-
) # (b, k, w, t)
|
| 462 |
-
token_keys = to_keys(atom_to_token) # (b, k, h, t)
|
| 463 |
-
pair_update = self.z_to_p_trans(z.float()) # (b, t, t, atom_z)
|
| 464 |
-
pair_update = torch.einsum(
|
| 465 |
-
"bijd,bwki,bwlj->bwkld",
|
| 466 |
-
pair_update,
|
| 467 |
-
token_queries,
|
| 468 |
-
token_keys,
|
| 469 |
-
) # (b, k, w, h, atom_z)
|
| 470 |
-
pair = pair + pair_update.to(pair) # (b, k, w, h, atom_z)
|
| 471 |
-
|
| 472 |
-
pair = pair + self.c_to_p_trans_q(
|
| 473 |
-
conditioned_single.view(b, k, w, 1, conditioned_single.shape[-1])
|
| 474 |
-
) # (b, k, w, h, atom_z)
|
| 475 |
-
pair = pair + self.c_to_p_trans_k(
|
| 476 |
-
to_keys(conditioned_single).view(
|
| 477 |
-
b,
|
| 478 |
-
k,
|
| 479 |
-
1,
|
| 480 |
-
h,
|
| 481 |
-
conditioned_single.shape[-1],
|
| 482 |
-
)
|
| 483 |
-
) # (b, k, w, h, atom_z)
|
| 484 |
-
pair = pair + self.p_mlp(pair) # (b, k, w, h, atom_z)
|
| 485 |
-
return (
|
| 486 |
-
query,
|
| 487 |
-
conditioned_single,
|
| 488 |
-
pair,
|
| 489 |
-
to_keys,
|
| 490 |
-
) # (b, n, atom_s), (b, n, atom_s), (b, k, w, h, atom_z), callable
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
class AtomAttentionEncoder(nn.Module):
|
| 494 |
-
"""Run local atom attention and aggregate atoms to tokens."""
|
| 495 |
-
|
| 496 |
-
def __init__(
|
| 497 |
-
self,
|
| 498 |
-
atom_s: int,
|
| 499 |
-
token_s: int,
|
| 500 |
-
atoms_per_window_queries: int,
|
| 501 |
-
atoms_per_window_keys: int,
|
| 502 |
-
atom_encoder_depth: int = 3,
|
| 503 |
-
atom_encoder_heads: int = 4,
|
| 504 |
-
structure_prediction: bool = True,
|
| 505 |
-
activation_checkpointing: bool = False,
|
| 506 |
-
transformer_post_layer_norm: bool = False,
|
| 507 |
-
) -> None:
|
| 508 |
-
super().__init__()
|
| 509 |
-
self.structure_prediction = structure_prediction
|
| 510 |
-
if structure_prediction:
|
| 511 |
-
self.r_to_q_trans = LinearNoBias(3, atom_s)
|
| 512 |
-
init.final_init_(self.r_to_q_trans.weight)
|
| 513 |
-
self.atom_encoder = AtomTransformer(
|
| 514 |
-
dim=atom_s,
|
| 515 |
-
dim_single_cond=atom_s,
|
| 516 |
-
attn_window_queries=atoms_per_window_queries,
|
| 517 |
-
attn_window_keys=atoms_per_window_keys,
|
| 518 |
-
depth=atom_encoder_depth,
|
| 519 |
-
heads=atom_encoder_heads,
|
| 520 |
-
activation_checkpointing=activation_checkpointing,
|
| 521 |
-
post_layer_norm=transformer_post_layer_norm,
|
| 522 |
-
)
|
| 523 |
-
output_dim = 2 * token_s if structure_prediction else token_s
|
| 524 |
-
self.atom_to_token_trans = nn.Sequential(
|
| 525 |
-
LinearNoBias(atom_s, output_dim),
|
| 526 |
-
nn.ReLU(),
|
| 527 |
-
)
|
| 528 |
-
|
| 529 |
-
def forward(
|
| 530 |
-
self,
|
| 531 |
-
feats: dict[str, torch.Tensor],
|
| 532 |
-
q: torch.Tensor,
|
| 533 |
-
c: torch.Tensor,
|
| 534 |
-
atom_enc_bias: torch.Tensor,
|
| 535 |
-
to_keys: Callable[[torch.Tensor], torch.Tensor],
|
| 536 |
-
r: torch.Tensor | None = None,
|
| 537 |
-
multiplicity: int = 1,
|
| 538 |
-
) -> tuple[
|
| 539 |
-
torch.Tensor,
|
| 540 |
-
torch.Tensor,
|
| 541 |
-
torch.Tensor,
|
| 542 |
-
Callable[[torch.Tensor], torch.Tensor],
|
| 543 |
-
]:
|
| 544 |
-
b, n, _ = feats["ref_pos"].shape
|
| 545 |
-
del b, n
|
| 546 |
-
# q/c: (b, a, atom_s); atom_enc_bias uses local-window attention axes.
|
| 547 |
-
atom_mask = feats["atom_pad_mask"].bool() # (b, a)
|
| 548 |
-
if self.structure_prediction:
|
| 549 |
-
if r is None:
|
| 550 |
-
raise ValueError("structure prediction requires atom coordinates R")
|
| 551 |
-
q = q.repeat_interleave(multiplicity, 0) # (b * m, a, atom_s)
|
| 552 |
-
q = q + self.r_to_q_trans(r) # (b * m, a, atom_s)
|
| 553 |
-
c = c.repeat_interleave(multiplicity, 0) # (b * m, a, atom_s)
|
| 554 |
-
atom_mask = atom_mask.repeat_interleave(multiplicity, 0) # (b * m, a)
|
| 555 |
-
q = self.atom_encoder(
|
| 556 |
-
q=q,
|
| 557 |
-
mask=atom_mask,
|
| 558 |
-
c=c,
|
| 559 |
-
bias=atom_enc_bias,
|
| 560 |
-
multiplicity=multiplicity,
|
| 561 |
-
to_keys=to_keys,
|
| 562 |
-
) # (b * m, a, atom_s)
|
| 563 |
-
|
| 564 |
-
with torch.autocast("cuda", enabled=False):
|
| 565 |
-
atom_update = self.atom_to_token_trans(q).float() # (b * m, a, d_token)
|
| 566 |
-
atom_to_token = feats["atom_to_token"].float() # (b, a, t)
|
| 567 |
-
atom_to_token = atom_to_token.repeat_interleave(
|
| 568 |
-
multiplicity, 0
|
| 569 |
-
) # (b * m, a, t)
|
| 570 |
-
atom_to_token_mean = atom_to_token / (
|
| 571 |
-
atom_to_token.sum(dim=1, keepdim=True) + 1e-6
|
| 572 |
-
) # (b * m, a, t)
|
| 573 |
-
token_update = torch.bmm(
|
| 574 |
-
atom_to_token_mean.transpose(1, 2), atom_update
|
| 575 |
-
) # (b * m, t, d_token)
|
| 576 |
-
return (
|
| 577 |
-
token_update.to(q),
|
| 578 |
-
q,
|
| 579 |
-
c,
|
| 580 |
-
to_keys,
|
| 581 |
-
) # (b * m, t, d_token), two (b * m, a, atom_s), callable
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
class AtomAttentionDecoder(nn.Module):
|
| 585 |
-
"""Map token updates back to atoms and predict coordinate displacements."""
|
| 586 |
-
|
| 587 |
-
def __init__(
|
| 588 |
-
self,
|
| 589 |
-
atom_s: int,
|
| 590 |
-
token_s: int,
|
| 591 |
-
attn_window_queries: int,
|
| 592 |
-
attn_window_keys: int,
|
| 593 |
-
atom_decoder_depth: int = 3,
|
| 594 |
-
atom_decoder_heads: int = 4,
|
| 595 |
-
activation_checkpointing: bool = False,
|
| 596 |
-
transformer_post_layer_norm: bool = False,
|
| 597 |
-
) -> None:
|
| 598 |
-
super().__init__()
|
| 599 |
-
self.a_to_q_trans = LinearNoBias(2 * token_s, atom_s)
|
| 600 |
-
init.final_init_(self.a_to_q_trans.weight)
|
| 601 |
-
self.atom_decoder = AtomTransformer(
|
| 602 |
-
dim=atom_s,
|
| 603 |
-
dim_single_cond=atom_s,
|
| 604 |
-
attn_window_queries=attn_window_queries,
|
| 605 |
-
attn_window_keys=attn_window_keys,
|
| 606 |
-
depth=atom_decoder_depth,
|
| 607 |
-
heads=atom_decoder_heads,
|
| 608 |
-
activation_checkpointing=activation_checkpointing,
|
| 609 |
-
post_layer_norm=transformer_post_layer_norm,
|
| 610 |
-
)
|
| 611 |
-
if transformer_post_layer_norm:
|
| 612 |
-
self.atom_feat_to_atom_pos_update = LinearNoBias(atom_s, 3)
|
| 613 |
-
init.final_init_(self.atom_feat_to_atom_pos_update.weight)
|
| 614 |
-
else:
|
| 615 |
-
self.atom_feat_to_atom_pos_update = nn.Sequential(
|
| 616 |
-
nn.LayerNorm(atom_s),
|
| 617 |
-
LinearNoBias(atom_s, 3),
|
| 618 |
-
)
|
| 619 |
-
init.final_init_(self.atom_feat_to_atom_pos_update[1].weight)
|
| 620 |
-
|
| 621 |
-
def forward(
|
| 622 |
-
self,
|
| 623 |
-
a: torch.Tensor,
|
| 624 |
-
q: torch.Tensor,
|
| 625 |
-
c: torch.Tensor,
|
| 626 |
-
atom_dec_bias: torch.Tensor,
|
| 627 |
-
feats: dict[str, torch.Tensor],
|
| 628 |
-
to_keys: Callable[[torch.Tensor], torch.Tensor],
|
| 629 |
-
multiplicity: int = 1,
|
| 630 |
-
) -> torch.Tensor:
|
| 631 |
-
"""Return atom-coordinate updates ``R_update: (b, a, 3)``."""
|
| 632 |
-
|
| 633 |
-
with torch.autocast("cuda", enabled=False):
|
| 634 |
-
# a: (b * m, t, 2 * token_s); q/c: (b * m, a, atom_s).
|
| 635 |
-
atom_to_token = feats["atom_to_token"].float() # (b, a, t)
|
| 636 |
-
atom_to_token = atom_to_token.repeat_interleave(
|
| 637 |
-
multiplicity, 0
|
| 638 |
-
) # (b * m, a, t)
|
| 639 |
-
token_update = self.a_to_q_trans(a.float()) # (b * m, t, atom_s)
|
| 640 |
-
atom_update = torch.bmm(atom_to_token, token_update) # (b * m, a, atom_s)
|
| 641 |
-
q = q + atom_update.to(q) # (b * m, a, atom_s)
|
| 642 |
-
atom_mask = feats["atom_pad_mask"].repeat_interleave(
|
| 643 |
-
multiplicity, 0
|
| 644 |
-
) # (b * m, a)
|
| 645 |
-
q = self.atom_decoder(
|
| 646 |
-
q=q,
|
| 647 |
-
mask=atom_mask,
|
| 648 |
-
c=c,
|
| 649 |
-
bias=atom_dec_bias,
|
| 650 |
-
multiplicity=multiplicity,
|
| 651 |
-
to_keys=to_keys,
|
| 652 |
-
) # (b * m, a, atom_s)
|
| 653 |
-
return self.atom_feat_to_atom_pos_update(q) # (b * m, a, 3)
|
|
|
|
| 1 |
+
"""Token, pair, and atom encoders used by the local Boltz2 runtime.
|
| 2 |
+
|
| 3 |
+
The classes retain checkpoint-facing submodule names while separating feature
|
| 4 |
+
assembly, window indexing, conditioning, and atom-token aggregation into small
|
| 5 |
+
mechanism-specific units. No production import depends on the upstream Boltz
|
| 6 |
+
package.
|
| 7 |
+
|
| 8 |
+
The Fourier and atom-attention mechanisms derive from AlphaFold 3 community
|
| 9 |
+
implementations under MIT terms. See ``THIRD_PARTY_NOTICES.md``.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
from collections.abc import Callable
|
| 17 |
+
from functools import partial
|
| 18 |
+
from math import pi
|
| 19 |
+
from torch import nn
|
| 20 |
+
from torch.nn.functional import one_hot
|
| 21 |
+
|
| 22 |
+
from . import vb_layers_initialize as init
|
| 23 |
+
from .vb_layers_transition import Transition
|
| 24 |
+
from .vb_modules_transformersv2 import AtomTransformer
|
| 25 |
+
from .vb_modules_utils import LinearNoBias
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _transition_stack(
|
| 29 |
+
count: int,
|
| 30 |
+
dim: int,
|
| 31 |
+
hidden_dim: int,
|
| 32 |
+
) -> nn.ModuleList:
|
| 33 |
+
return nn.ModuleList([Transition(dim=dim, hidden=hidden_dim) for _ in range(count)])
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class FourierEmbedding(nn.Module):
|
| 37 |
+
"""Embed diffusion time with fixed random Fourier frequencies."""
|
| 38 |
+
|
| 39 |
+
def __init__(self, dim: int) -> None:
|
| 40 |
+
super().__init__()
|
| 41 |
+
self.proj = nn.Linear(1, dim)
|
| 42 |
+
nn.init.normal_(self.proj.weight, mean=0, std=1)
|
| 43 |
+
nn.init.normal_(self.proj.bias, mean=0, std=1)
|
| 44 |
+
self.proj.requires_grad_(False)
|
| 45 |
+
|
| 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)) # (b, d)
|
| 50 |
+
return torch.cos(2 * pi * random_phase) # (b, d)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _pairwise_difference(values: torch.Tensor) -> torch.Tensor:
|
| 54 |
+
# values: (b, n)
|
| 55 |
+
return values[:, :, None] - values[:, None, :] # (b, n, n)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class RelativePositionEncoder(nn.Module):
|
| 59 |
+
"""Encode residue, token, entity, and symmetry relationships."""
|
| 60 |
+
|
| 61 |
+
def __init__(
|
| 62 |
+
self,
|
| 63 |
+
token_z: int,
|
| 64 |
+
r_max: int = 32,
|
| 65 |
+
s_max: int = 2,
|
| 66 |
+
fix_sym_check: bool = False,
|
| 67 |
+
cyclic_pos_enc: bool = False,
|
| 68 |
+
) -> None:
|
| 69 |
+
super().__init__()
|
| 70 |
+
self.r_max = r_max
|
| 71 |
+
self.s_max = s_max
|
| 72 |
+
input_dim = 4 * (r_max + 1) + 2 * (s_max + 1) + 1
|
| 73 |
+
self.linear_layer = LinearNoBias(input_dim, token_z)
|
| 74 |
+
self.fix_sym_check = fix_sym_check
|
| 75 |
+
self.cyclic_pos_enc = cyclic_pos_enc
|
| 76 |
+
|
| 77 |
+
def forward(self, feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 78 |
+
"""Return relative pair features ``Z: (b, n, n, token_z)``."""
|
| 79 |
+
|
| 80 |
+
same_chain = (
|
| 81 |
+
feats["asym_id"][:, :, None] == feats["asym_id"][:, None, :]
|
| 82 |
+
) # (b, n, n)
|
| 83 |
+
same_residue = (
|
| 84 |
+
feats["residue_index"][:, :, None] == feats["residue_index"][:, None, :]
|
| 85 |
+
) # (b, n, n)
|
| 86 |
+
same_entity = (
|
| 87 |
+
feats["entity_id"][:, :, None] == feats["entity_id"][:, None, :]
|
| 88 |
+
) # (b, n, n)
|
| 89 |
+
|
| 90 |
+
residue_offset = _pairwise_difference(feats["residue_index"]) # (b, n, n)
|
| 91 |
+
if self.cyclic_pos_enc and torch.any(feats["cyclic_period"] > 0):
|
| 92 |
+
period = torch.where(
|
| 93 |
+
feats["cyclic_period"] > 0,
|
| 94 |
+
feats["cyclic_period"],
|
| 95 |
+
torch.zeros_like(feats["cyclic_period"]) + 10000,
|
| 96 |
+
) # (b, n)
|
| 97 |
+
residue_offset = (
|
| 98 |
+
residue_offset - period * torch.round(residue_offset / period)
|
| 99 |
+
).long() # (b, n, n)
|
| 100 |
+
residue_offset = torch.clip(
|
| 101 |
+
residue_offset + self.r_max,
|
| 102 |
+
0,
|
| 103 |
+
2 * self.r_max,
|
| 104 |
+
) # (b, n, n)
|
| 105 |
+
residue_offset = torch.where(
|
| 106 |
+
same_chain,
|
| 107 |
+
residue_offset,
|
| 108 |
+
torch.zeros_like(residue_offset) + 2 * self.r_max + 1,
|
| 109 |
+
) # (b, n, n)
|
| 110 |
+
residue_features = one_hot(
|
| 111 |
+
residue_offset, 2 * self.r_max + 2
|
| 112 |
+
) # (b, n, n, 2 * r_max + 2)
|
| 113 |
+
|
| 114 |
+
token_offset = torch.clip(
|
| 115 |
+
_pairwise_difference(feats["token_index"]) + self.r_max,
|
| 116 |
+
0,
|
| 117 |
+
2 * self.r_max,
|
| 118 |
+
) # (b, n, n)
|
| 119 |
+
token_offset = torch.where(
|
| 120 |
+
same_chain & same_residue,
|
| 121 |
+
token_offset,
|
| 122 |
+
torch.zeros_like(token_offset) + 2 * self.r_max + 1,
|
| 123 |
+
) # (b, n, n)
|
| 124 |
+
token_features = one_hot(
|
| 125 |
+
token_offset, 2 * self.r_max + 2
|
| 126 |
+
) # (b, n, n, 2 * r_max + 2)
|
| 127 |
+
|
| 128 |
+
symmetry_offset = torch.clip(
|
| 129 |
+
_pairwise_difference(feats["sym_id"]) + self.s_max,
|
| 130 |
+
0,
|
| 131 |
+
2 * self.s_max,
|
| 132 |
+
) # (b, n, n)
|
| 133 |
+
invalid_symmetry = ~same_entity if self.fix_sym_check else same_chain # (b, n, n)
|
| 134 |
+
symmetry_offset = torch.where(
|
| 135 |
+
invalid_symmetry,
|
| 136 |
+
torch.zeros_like(symmetry_offset) + 2 * self.s_max + 1,
|
| 137 |
+
symmetry_offset,
|
| 138 |
+
) # (b, n, n)
|
| 139 |
+
symmetry_features = one_hot(
|
| 140 |
+
symmetry_offset, 2 * self.s_max + 2
|
| 141 |
+
) # (b, n, n, 2 * s_max + 2)
|
| 142 |
+
|
| 143 |
+
pair_features = torch.cat(
|
| 144 |
+
(
|
| 145 |
+
residue_features.float(),
|
| 146 |
+
token_features.float(),
|
| 147 |
+
same_entity.unsqueeze(-1).float(),
|
| 148 |
+
symmetry_features.float(),
|
| 149 |
+
),
|
| 150 |
+
dim=-1,
|
| 151 |
+
) # (b, n, n, d_rel)
|
| 152 |
+
return self.linear_layer(pair_features) # (b, n, n, token_z)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class SingleConditioning(nn.Module):
|
| 156 |
+
"""Condition token features on trunk inputs and diffusion time."""
|
| 157 |
+
|
| 158 |
+
def __init__(
|
| 159 |
+
self,
|
| 160 |
+
sigma_data: float,
|
| 161 |
+
token_s: int = 384,
|
| 162 |
+
dim_fourier: int = 256,
|
| 163 |
+
num_transitions: int = 2,
|
| 164 |
+
transition_expansion_factor: int = 2,
|
| 165 |
+
eps: float = 1e-20,
|
| 166 |
+
disable_times: bool = False,
|
| 167 |
+
) -> None:
|
| 168 |
+
super().__init__()
|
| 169 |
+
self.eps = eps
|
| 170 |
+
self.sigma_data = sigma_data
|
| 171 |
+
self.disable_times = disable_times
|
| 172 |
+
conditioning_dim = 2 * token_s
|
| 173 |
+
self.norm_single = nn.LayerNorm(conditioning_dim)
|
| 174 |
+
self.single_embed = nn.Linear(conditioning_dim, conditioning_dim)
|
| 175 |
+
if not disable_times:
|
| 176 |
+
self.fourier_embed = FourierEmbedding(dim_fourier)
|
| 177 |
+
self.norm_fourier = nn.LayerNorm(dim_fourier)
|
| 178 |
+
self.fourier_to_single = LinearNoBias(dim_fourier, conditioning_dim)
|
| 179 |
+
self.transitions = _transition_stack(
|
| 180 |
+
num_transitions,
|
| 181 |
+
conditioning_dim,
|
| 182 |
+
transition_expansion_factor * conditioning_dim,
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
def forward(
|
| 186 |
+
self,
|
| 187 |
+
times: torch.Tensor,
|
| 188 |
+
s_trunk: torch.Tensor,
|
| 189 |
+
s_inputs: torch.Tensor,
|
| 190 |
+
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
| 191 |
+
"""Return conditioned token features ``S: (b, n, 2 * token_s)``."""
|
| 192 |
+
|
| 193 |
+
# times: (b,); s_trunk/s_inputs: (b, n, token_s).
|
| 194 |
+
single = torch.cat((s_trunk, s_inputs), dim=-1) # (b, n, 2 * token_s)
|
| 195 |
+
single = self.single_embed(self.norm_single(single)) # (b, n, 2 * token_s)
|
| 196 |
+
normalized_fourier = None # (b, d_fourier) or None
|
| 197 |
+
if not self.disable_times:
|
| 198 |
+
fourier = self.fourier_embed(times) # (b, d_fourier)
|
| 199 |
+
normalized_fourier = self.norm_fourier(fourier) # (b, d_fourier)
|
| 200 |
+
time_condition = self.fourier_to_single(
|
| 201 |
+
normalized_fourier
|
| 202 |
+
) # (b, 2 * token_s)
|
| 203 |
+
single = time_condition[:, None, :] + single # (b, n, 2 * token_s)
|
| 204 |
+
for transition in self.transitions:
|
| 205 |
+
single = transition(single) + single # (b, n, 2 * token_s)
|
| 206 |
+
return single, normalized_fourier # (b, n, 2 * token_s), (b, d_fourier) or None
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class PairwiseConditioning(nn.Module):
|
| 210 |
+
"""Fuse trunk pair features with relative-position features."""
|
| 211 |
+
|
| 212 |
+
def __init__(
|
| 213 |
+
self,
|
| 214 |
+
token_z: int,
|
| 215 |
+
dim_token_rel_pos_feats: int,
|
| 216 |
+
num_transitions: int = 2,
|
| 217 |
+
transition_expansion_factor: int = 2,
|
| 218 |
+
) -> None:
|
| 219 |
+
super().__init__()
|
| 220 |
+
combined_dim = token_z + dim_token_rel_pos_feats
|
| 221 |
+
self.dim_pairwise_init_proj = nn.Sequential(
|
| 222 |
+
nn.LayerNorm(combined_dim),
|
| 223 |
+
LinearNoBias(combined_dim, token_z),
|
| 224 |
+
)
|
| 225 |
+
self.transitions = _transition_stack(
|
| 226 |
+
num_transitions,
|
| 227 |
+
token_z,
|
| 228 |
+
transition_expansion_factor * token_z,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def forward(
|
| 232 |
+
self,
|
| 233 |
+
z_trunk: torch.Tensor,
|
| 234 |
+
token_rel_pos_feats: torch.Tensor,
|
| 235 |
+
) -> torch.Tensor:
|
| 236 |
+
"""Return conditioned pair features ``Z: (b, n, n, token_z)``."""
|
| 237 |
+
|
| 238 |
+
pair = self.dim_pairwise_init_proj(
|
| 239 |
+
torch.cat((z_trunk, token_rel_pos_feats), dim=-1)
|
| 240 |
+
) # (b, n, n, token_z)
|
| 241 |
+
for transition in self.transitions:
|
| 242 |
+
pair = transition(pair) + pair # (b, n, n, token_z)
|
| 243 |
+
return pair # (b, n, n, token_z)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def get_indexing_matrix(
|
| 247 |
+
k: int,
|
| 248 |
+
w: int,
|
| 249 |
+
h: int,
|
| 250 |
+
device: torch.device,
|
| 251 |
+
) -> torch.Tensor:
|
| 252 |
+
"""Build the atom-window gather matrix used for local attention keys."""
|
| 253 |
+
|
| 254 |
+
for name, value in (("k", k), ("w", w), ("h", h)):
|
| 255 |
+
if isinstance(value, bool) or not isinstance(value, int):
|
| 256 |
+
raise TypeError(f"{name} must be an int, got {type(value).__name__}.")
|
| 257 |
+
if value <= 0:
|
| 258 |
+
raise ValueError(f"{name} must be positive, got {value}.")
|
| 259 |
+
if w % 2 != 0:
|
| 260 |
+
raise ValueError(f"w must be even, got {w}.")
|
| 261 |
+
half_window = w // 2
|
| 262 |
+
if h % half_window != 0:
|
| 263 |
+
raise ValueError(f"h must be divisible by w // 2 ({half_window}), got {h}.")
|
| 264 |
+
key_blocks = h // half_window
|
| 265 |
+
if key_blocks % 2 != 0:
|
| 266 |
+
raise ValueError(
|
| 267 |
+
f"h must contain an even number of half-window key blocks; received {key_blocks}."
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
positions = torch.arange(2 * k, device=device) # (2 * k,)
|
| 271 |
+
relative_blocks = (
|
| 272 |
+
(positions.unsqueeze(0) - positions.unsqueeze(1)) + key_blocks // 2
|
| 273 |
+
).clamp(min=0, max=key_blocks + 1) # (2 * k, 2 * k)
|
| 274 |
+
relative_blocks = relative_blocks.view(k, 2, 2 * k)[:, 0, :] # (k, 2 * k)
|
| 275 |
+
selectors = one_hot(relative_blocks, num_classes=key_blocks + 2)[
|
| 276 |
+
..., 1:-1
|
| 277 |
+
].transpose(1, 0) # (2 * k, k, key_blocks)
|
| 278 |
+
return selectors.reshape(
|
| 279 |
+
2 * k, key_blocks * k
|
| 280 |
+
).float() # (2 * k, key_blocks * k)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def single_to_keys(
|
| 284 |
+
single: torch.Tensor,
|
| 285 |
+
indexing_matrix: torch.Tensor,
|
| 286 |
+
w: int,
|
| 287 |
+
h: int,
|
| 288 |
+
) -> torch.Tensor:
|
| 289 |
+
"""Gather a sequence tensor into overlapping local key windows."""
|
| 290 |
+
|
| 291 |
+
b, n, d = single.shape
|
| 292 |
+
k = n // w
|
| 293 |
+
half_windows = single.view(b, 2 * k, w // 2, d) # (b, 2 * k, w / 2, d)
|
| 294 |
+
gathered = torch.einsum(
|
| 295 |
+
"b j i d, j k -> b k i d", half_windows, indexing_matrix
|
| 296 |
+
) # (b, key_blocks * k, w / 2, d)
|
| 297 |
+
return gathered.reshape(b, k, h, d) # (b, k, h, d)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
class AtomEncoder(nn.Module):
|
| 301 |
+
"""Encode reference atoms and local atom-pair geometry."""
|
| 302 |
+
|
| 303 |
+
def __init__(
|
| 304 |
+
self,
|
| 305 |
+
atom_s: int,
|
| 306 |
+
atom_z: int,
|
| 307 |
+
token_s: int,
|
| 308 |
+
token_z: int,
|
| 309 |
+
atoms_per_window_queries: int,
|
| 310 |
+
atoms_per_window_keys: int,
|
| 311 |
+
atom_feature_dim: int,
|
| 312 |
+
structure_prediction: bool = True,
|
| 313 |
+
use_no_atom_char: bool = False,
|
| 314 |
+
use_atom_backbone_feat: bool = False,
|
| 315 |
+
use_residue_feats_atoms: bool = False,
|
| 316 |
+
) -> None:
|
| 317 |
+
super().__init__()
|
| 318 |
+
self.embed_atom_features = nn.Linear(atom_feature_dim, atom_s)
|
| 319 |
+
self.embed_atompair_ref_pos = LinearNoBias(3, atom_z)
|
| 320 |
+
self.embed_atompair_ref_dist = LinearNoBias(1, atom_z)
|
| 321 |
+
self.embed_atompair_mask = LinearNoBias(1, atom_z)
|
| 322 |
+
self.atoms_per_window_queries = atoms_per_window_queries
|
| 323 |
+
self.atoms_per_window_keys = atoms_per_window_keys
|
| 324 |
+
self.use_no_atom_char = use_no_atom_char
|
| 325 |
+
self.use_atom_backbone_feat = use_atom_backbone_feat
|
| 326 |
+
self.use_residue_feats_atoms = use_residue_feats_atoms
|
| 327 |
+
self.structure_prediction = structure_prediction
|
| 328 |
+
|
| 329 |
+
if structure_prediction:
|
| 330 |
+
self.s_to_c_trans = nn.Sequential(
|
| 331 |
+
nn.LayerNorm(token_s),
|
| 332 |
+
LinearNoBias(token_s, atom_s),
|
| 333 |
+
)
|
| 334 |
+
init.final_init_(self.s_to_c_trans[1].weight)
|
| 335 |
+
self.z_to_p_trans = nn.Sequential(
|
| 336 |
+
nn.LayerNorm(token_z),
|
| 337 |
+
LinearNoBias(token_z, atom_z),
|
| 338 |
+
)
|
| 339 |
+
init.final_init_(self.z_to_p_trans[1].weight)
|
| 340 |
+
|
| 341 |
+
self.c_to_p_trans_k = nn.Sequential(nn.ReLU(), LinearNoBias(atom_s, atom_z))
|
| 342 |
+
init.final_init_(self.c_to_p_trans_k[1].weight)
|
| 343 |
+
self.c_to_p_trans_q = nn.Sequential(nn.ReLU(), LinearNoBias(atom_s, atom_z))
|
| 344 |
+
init.final_init_(self.c_to_p_trans_q[1].weight)
|
| 345 |
+
self.p_mlp = nn.Sequential(
|
| 346 |
+
nn.ReLU(),
|
| 347 |
+
LinearNoBias(atom_z, atom_z),
|
| 348 |
+
nn.ReLU(),
|
| 349 |
+
LinearNoBias(atom_z, atom_z),
|
| 350 |
+
nn.ReLU(),
|
| 351 |
+
LinearNoBias(atom_z, atom_z),
|
| 352 |
+
)
|
| 353 |
+
init.final_init_(self.p_mlp[5].weight)
|
| 354 |
+
|
| 355 |
+
def _assemble_atom_features(
|
| 356 |
+
self,
|
| 357 |
+
feats: dict[str, torch.Tensor],
|
| 358 |
+
b: int,
|
| 359 |
+
n: int,
|
| 360 |
+
) -> torch.Tensor:
|
| 361 |
+
# Feature tensors share leading shape (b, n).
|
| 362 |
+
feature_parts = [
|
| 363 |
+
feats["ref_pos"],
|
| 364 |
+
feats["ref_charge"].unsqueeze(-1),
|
| 365 |
+
feats["ref_element"],
|
| 366 |
+
] # each: (b, n, d_feature)
|
| 367 |
+
if not self.use_no_atom_char:
|
| 368 |
+
feature_parts.append(feats["ref_atom_name_chars"].reshape(b, n, 4 * 64))
|
| 369 |
+
if self.use_atom_backbone_feat:
|
| 370 |
+
feature_parts.append(feats["atom_backbone_feat"])
|
| 371 |
+
if self.use_residue_feats_atoms:
|
| 372 |
+
residue_features = torch.cat(
|
| 373 |
+
(
|
| 374 |
+
feats["res_type"],
|
| 375 |
+
feats["modified"].unsqueeze(-1),
|
| 376 |
+
one_hot(feats["mol_type"], num_classes=4).float(),
|
| 377 |
+
),
|
| 378 |
+
dim=-1,
|
| 379 |
+
) # (b, t, d_residue)
|
| 380 |
+
feature_parts.append(
|
| 381 |
+
torch.bmm(feats["atom_to_token"].float(), residue_features)
|
| 382 |
+
) # (b, n, d_residue)
|
| 383 |
+
return torch.cat(feature_parts, dim=-1) # (b, n, atom_feature_dim)
|
| 384 |
+
|
| 385 |
+
def forward(
|
| 386 |
+
self,
|
| 387 |
+
feats: dict[str, torch.Tensor],
|
| 388 |
+
s_trunk: torch.Tensor | None = None,
|
| 389 |
+
z: torch.Tensor | None = None,
|
| 390 |
+
) -> tuple[
|
| 391 |
+
torch.Tensor,
|
| 392 |
+
torch.Tensor,
|
| 393 |
+
torch.Tensor,
|
| 394 |
+
Callable[[torch.Tensor], torch.Tensor],
|
| 395 |
+
]:
|
| 396 |
+
"""Return atom queries, conditioned singles, pairs, and key gatherer."""
|
| 397 |
+
|
| 398 |
+
with torch.autocast("cuda", enabled=False):
|
| 399 |
+
b, n, _ = feats["ref_pos"].shape
|
| 400 |
+
atom_mask = feats["atom_pad_mask"].bool() # (b, n)
|
| 401 |
+
atom_positions = feats["ref_pos"] # (b, n, 3)
|
| 402 |
+
atom_space = feats["ref_space_uid"] # (b, n)
|
| 403 |
+
atom_features = self._assemble_atom_features(
|
| 404 |
+
feats, b, n
|
| 405 |
+
) # (b, n, atom_feature_dim)
|
| 406 |
+
conditioned_single = self.embed_atom_features(
|
| 407 |
+
atom_features
|
| 408 |
+
) # (b, n, atom_s)
|
| 409 |
+
|
| 410 |
+
w = self.atoms_per_window_queries
|
| 411 |
+
h = self.atoms_per_window_keys
|
| 412 |
+
b, n = conditioned_single.shape[:2]
|
| 413 |
+
k = n // w
|
| 414 |
+
indexing = get_indexing_matrix(
|
| 415 |
+
k, w, h, conditioned_single.device
|
| 416 |
+
) # (2 * k, key_blocks * k)
|
| 417 |
+
to_keys = partial(single_to_keys, indexing_matrix=indexing, w=w, h=h)
|
| 418 |
+
|
| 419 |
+
position_queries = atom_positions.view(b, k, w, 1, 3) # (b, k, w, 1, 3)
|
| 420 |
+
position_keys = to_keys(atom_positions).view(
|
| 421 |
+
b, k, 1, h, 3
|
| 422 |
+
) # (b, k, 1, h, 3)
|
| 423 |
+
displacement = position_keys - position_queries # (b, k, w, h, 3)
|
| 424 |
+
inverse_squared_distance = 1 / (
|
| 425 |
+
1 + torch.sum(displacement * displacement, dim=-1, keepdim=True)
|
| 426 |
+
) # (b, k, w, h, 1)
|
| 427 |
+
|
| 428 |
+
mask_queries = atom_mask.view(b, k, w, 1) # (b, k, w, 1)
|
| 429 |
+
mask_keys = (
|
| 430 |
+
to_keys(atom_mask.unsqueeze(-1).float()).view(b, k, 1, h).bool()
|
| 431 |
+
) # (b, k, 1, h)
|
| 432 |
+
space_queries = atom_space.view(b, k, w, 1) # (b, k, w, 1)
|
| 433 |
+
space_keys = (
|
| 434 |
+
to_keys(atom_space.unsqueeze(-1).float()).view(b, k, 1, h).long()
|
| 435 |
+
) # (b, k, 1, h)
|
| 436 |
+
valid_pair = (
|
| 437 |
+
(mask_queries & mask_keys & (space_queries == space_keys)).float().unsqueeze(-1)
|
| 438 |
+
) # (b, k, w, h, 1)
|
| 439 |
+
|
| 440 |
+
pair = self.embed_atompair_ref_pos(displacement) * valid_pair # (b, k, w, h, atom_z)
|
| 441 |
+
pair = (
|
| 442 |
+
pair + self.embed_atompair_ref_dist(inverse_squared_distance) * valid_pair
|
| 443 |
+
) # (b, k, w, h, atom_z)
|
| 444 |
+
pair = pair + self.embed_atompair_mask(valid_pair) * valid_pair # (b, k, w, h, atom_z)
|
| 445 |
+
query = conditioned_single # (b, n, atom_s)
|
| 446 |
+
|
| 447 |
+
if self.structure_prediction:
|
| 448 |
+
if s_trunk is None or z is None:
|
| 449 |
+
raise ValueError("structure prediction requires S and Z trunk tensors")
|
| 450 |
+
atom_to_token = feats["atom_to_token"].float() # (b, n, t)
|
| 451 |
+
single_update = self.s_to_c_trans(s_trunk.float()) # (b, t, atom_s)
|
| 452 |
+
single_update = torch.bmm(
|
| 453 |
+
atom_to_token, single_update
|
| 454 |
+
) # (b, n, atom_s)
|
| 455 |
+
conditioned_single = conditioned_single + single_update.to(
|
| 456 |
+
conditioned_single
|
| 457 |
+
) # (b, n, atom_s)
|
| 458 |
+
|
| 459 |
+
token_queries = atom_to_token.view(
|
| 460 |
+
b, k, w, atom_to_token.shape[-1]
|
| 461 |
+
) # (b, k, w, t)
|
| 462 |
+
token_keys = to_keys(atom_to_token) # (b, k, h, t)
|
| 463 |
+
pair_update = self.z_to_p_trans(z.float()) # (b, t, t, atom_z)
|
| 464 |
+
pair_update = torch.einsum(
|
| 465 |
+
"bijd,bwki,bwlj->bwkld",
|
| 466 |
+
pair_update,
|
| 467 |
+
token_queries,
|
| 468 |
+
token_keys,
|
| 469 |
+
) # (b, k, w, h, atom_z)
|
| 470 |
+
pair = pair + pair_update.to(pair) # (b, k, w, h, atom_z)
|
| 471 |
+
|
| 472 |
+
pair = pair + self.c_to_p_trans_q(
|
| 473 |
+
conditioned_single.view(b, k, w, 1, conditioned_single.shape[-1])
|
| 474 |
+
) # (b, k, w, h, atom_z)
|
| 475 |
+
pair = pair + self.c_to_p_trans_k(
|
| 476 |
+
to_keys(conditioned_single).view(
|
| 477 |
+
b,
|
| 478 |
+
k,
|
| 479 |
+
1,
|
| 480 |
+
h,
|
| 481 |
+
conditioned_single.shape[-1],
|
| 482 |
+
)
|
| 483 |
+
) # (b, k, w, h, atom_z)
|
| 484 |
+
pair = pair + self.p_mlp(pair) # (b, k, w, h, atom_z)
|
| 485 |
+
return (
|
| 486 |
+
query,
|
| 487 |
+
conditioned_single,
|
| 488 |
+
pair,
|
| 489 |
+
to_keys,
|
| 490 |
+
) # (b, n, atom_s), (b, n, atom_s), (b, k, w, h, atom_z), callable
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
class AtomAttentionEncoder(nn.Module):
|
| 494 |
+
"""Run local atom attention and aggregate atoms to tokens."""
|
| 495 |
+
|
| 496 |
+
def __init__(
|
| 497 |
+
self,
|
| 498 |
+
atom_s: int,
|
| 499 |
+
token_s: int,
|
| 500 |
+
atoms_per_window_queries: int,
|
| 501 |
+
atoms_per_window_keys: int,
|
| 502 |
+
atom_encoder_depth: int = 3,
|
| 503 |
+
atom_encoder_heads: int = 4,
|
| 504 |
+
structure_prediction: bool = True,
|
| 505 |
+
activation_checkpointing: bool = False,
|
| 506 |
+
transformer_post_layer_norm: bool = False,
|
| 507 |
+
) -> None:
|
| 508 |
+
super().__init__()
|
| 509 |
+
self.structure_prediction = structure_prediction
|
| 510 |
+
if structure_prediction:
|
| 511 |
+
self.r_to_q_trans = LinearNoBias(3, atom_s)
|
| 512 |
+
init.final_init_(self.r_to_q_trans.weight)
|
| 513 |
+
self.atom_encoder = AtomTransformer(
|
| 514 |
+
dim=atom_s,
|
| 515 |
+
dim_single_cond=atom_s,
|
| 516 |
+
attn_window_queries=atoms_per_window_queries,
|
| 517 |
+
attn_window_keys=atoms_per_window_keys,
|
| 518 |
+
depth=atom_encoder_depth,
|
| 519 |
+
heads=atom_encoder_heads,
|
| 520 |
+
activation_checkpointing=activation_checkpointing,
|
| 521 |
+
post_layer_norm=transformer_post_layer_norm,
|
| 522 |
+
)
|
| 523 |
+
output_dim = 2 * token_s if structure_prediction else token_s
|
| 524 |
+
self.atom_to_token_trans = nn.Sequential(
|
| 525 |
+
LinearNoBias(atom_s, output_dim),
|
| 526 |
+
nn.ReLU(),
|
| 527 |
+
)
|
| 528 |
+
|
| 529 |
+
def forward(
|
| 530 |
+
self,
|
| 531 |
+
feats: dict[str, torch.Tensor],
|
| 532 |
+
q: torch.Tensor,
|
| 533 |
+
c: torch.Tensor,
|
| 534 |
+
atom_enc_bias: torch.Tensor,
|
| 535 |
+
to_keys: Callable[[torch.Tensor], torch.Tensor],
|
| 536 |
+
r: torch.Tensor | None = None,
|
| 537 |
+
multiplicity: int = 1,
|
| 538 |
+
) -> tuple[
|
| 539 |
+
torch.Tensor,
|
| 540 |
+
torch.Tensor,
|
| 541 |
+
torch.Tensor,
|
| 542 |
+
Callable[[torch.Tensor], torch.Tensor],
|
| 543 |
+
]:
|
| 544 |
+
b, n, _ = feats["ref_pos"].shape
|
| 545 |
+
del b, n
|
| 546 |
+
# q/c: (b, a, atom_s); atom_enc_bias uses local-window attention axes.
|
| 547 |
+
atom_mask = feats["atom_pad_mask"].bool() # (b, a)
|
| 548 |
+
if self.structure_prediction:
|
| 549 |
+
if r is None:
|
| 550 |
+
raise ValueError("structure prediction requires atom coordinates R")
|
| 551 |
+
q = q.repeat_interleave(multiplicity, 0) # (b * m, a, atom_s)
|
| 552 |
+
q = q + self.r_to_q_trans(r) # (b * m, a, atom_s)
|
| 553 |
+
c = c.repeat_interleave(multiplicity, 0) # (b * m, a, atom_s)
|
| 554 |
+
atom_mask = atom_mask.repeat_interleave(multiplicity, 0) # (b * m, a)
|
| 555 |
+
q = self.atom_encoder(
|
| 556 |
+
q=q,
|
| 557 |
+
mask=atom_mask,
|
| 558 |
+
c=c,
|
| 559 |
+
bias=atom_enc_bias,
|
| 560 |
+
multiplicity=multiplicity,
|
| 561 |
+
to_keys=to_keys,
|
| 562 |
+
) # (b * m, a, atom_s)
|
| 563 |
+
|
| 564 |
+
with torch.autocast("cuda", enabled=False):
|
| 565 |
+
atom_update = self.atom_to_token_trans(q).float() # (b * m, a, d_token)
|
| 566 |
+
atom_to_token = feats["atom_to_token"].float() # (b, a, t)
|
| 567 |
+
atom_to_token = atom_to_token.repeat_interleave(
|
| 568 |
+
multiplicity, 0
|
| 569 |
+
) # (b * m, a, t)
|
| 570 |
+
atom_to_token_mean = atom_to_token / (
|
| 571 |
+
atom_to_token.sum(dim=1, keepdim=True) + 1e-6
|
| 572 |
+
) # (b * m, a, t)
|
| 573 |
+
token_update = torch.bmm(
|
| 574 |
+
atom_to_token_mean.transpose(1, 2), atom_update
|
| 575 |
+
) # (b * m, t, d_token)
|
| 576 |
+
return (
|
| 577 |
+
token_update.to(q),
|
| 578 |
+
q,
|
| 579 |
+
c,
|
| 580 |
+
to_keys,
|
| 581 |
+
) # (b * m, t, d_token), two (b * m, a, atom_s), callable
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
class AtomAttentionDecoder(nn.Module):
|
| 585 |
+
"""Map token updates back to atoms and predict coordinate displacements."""
|
| 586 |
+
|
| 587 |
+
def __init__(
|
| 588 |
+
self,
|
| 589 |
+
atom_s: int,
|
| 590 |
+
token_s: int,
|
| 591 |
+
attn_window_queries: int,
|
| 592 |
+
attn_window_keys: int,
|
| 593 |
+
atom_decoder_depth: int = 3,
|
| 594 |
+
atom_decoder_heads: int = 4,
|
| 595 |
+
activation_checkpointing: bool = False,
|
| 596 |
+
transformer_post_layer_norm: bool = False,
|
| 597 |
+
) -> None:
|
| 598 |
+
super().__init__()
|
| 599 |
+
self.a_to_q_trans = LinearNoBias(2 * token_s, atom_s)
|
| 600 |
+
init.final_init_(self.a_to_q_trans.weight)
|
| 601 |
+
self.atom_decoder = AtomTransformer(
|
| 602 |
+
dim=atom_s,
|
| 603 |
+
dim_single_cond=atom_s,
|
| 604 |
+
attn_window_queries=attn_window_queries,
|
| 605 |
+
attn_window_keys=attn_window_keys,
|
| 606 |
+
depth=atom_decoder_depth,
|
| 607 |
+
heads=atom_decoder_heads,
|
| 608 |
+
activation_checkpointing=activation_checkpointing,
|
| 609 |
+
post_layer_norm=transformer_post_layer_norm,
|
| 610 |
+
)
|
| 611 |
+
if transformer_post_layer_norm:
|
| 612 |
+
self.atom_feat_to_atom_pos_update = LinearNoBias(atom_s, 3)
|
| 613 |
+
init.final_init_(self.atom_feat_to_atom_pos_update.weight)
|
| 614 |
+
else:
|
| 615 |
+
self.atom_feat_to_atom_pos_update = nn.Sequential(
|
| 616 |
+
nn.LayerNorm(atom_s),
|
| 617 |
+
LinearNoBias(atom_s, 3),
|
| 618 |
+
)
|
| 619 |
+
init.final_init_(self.atom_feat_to_atom_pos_update[1].weight)
|
| 620 |
+
|
| 621 |
+
def forward(
|
| 622 |
+
self,
|
| 623 |
+
a: torch.Tensor,
|
| 624 |
+
q: torch.Tensor,
|
| 625 |
+
c: torch.Tensor,
|
| 626 |
+
atom_dec_bias: torch.Tensor,
|
| 627 |
+
feats: dict[str, torch.Tensor],
|
| 628 |
+
to_keys: Callable[[torch.Tensor], torch.Tensor],
|
| 629 |
+
multiplicity: int = 1,
|
| 630 |
+
) -> torch.Tensor:
|
| 631 |
+
"""Return atom-coordinate updates ``R_update: (b, a, 3)``."""
|
| 632 |
+
|
| 633 |
+
with torch.autocast("cuda", enabled=False):
|
| 634 |
+
# a: (b * m, t, 2 * token_s); q/c: (b * m, a, atom_s).
|
| 635 |
+
atom_to_token = feats["atom_to_token"].float() # (b, a, t)
|
| 636 |
+
atom_to_token = atom_to_token.repeat_interleave(
|
| 637 |
+
multiplicity, 0
|
| 638 |
+
) # (b * m, a, t)
|
| 639 |
+
token_update = self.a_to_q_trans(a.float()) # (b * m, t, atom_s)
|
| 640 |
+
atom_update = torch.bmm(atom_to_token, token_update) # (b * m, a, atom_s)
|
| 641 |
+
q = q + atom_update.to(q) # (b * m, a, atom_s)
|
| 642 |
+
atom_mask = feats["atom_pad_mask"].repeat_interleave(
|
| 643 |
+
multiplicity, 0
|
| 644 |
+
) # (b * m, a)
|
| 645 |
+
q = self.atom_decoder(
|
| 646 |
+
q=q,
|
| 647 |
+
mask=atom_mask,
|
| 648 |
+
c=c,
|
| 649 |
+
bias=atom_dec_bias,
|
| 650 |
+
multiplicity=multiplicity,
|
| 651 |
+
to_keys=to_keys,
|
| 652 |
+
) # (b * m, a, atom_s)
|
| 653 |
+
return self.atom_feat_to_atom_pos_update(q) # (b * m, a, 3)
|
fastplms/models/boltz/vb_modules_transformersv2.py
CHANGED
|
@@ -1,276 +1,276 @@
|
|
| 1 |
-
"""Conditioned transformer blocks used by Boltz2 diffusion."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 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 |
-
|
| 10 |
-
from .vb_layers_attentionv2 import AttentionPairBias
|
| 11 |
-
from .vb_modules_utils import LinearNoBias, SwiGLU
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class AdaLN(nn.Module):
|
| 15 |
-
"""Normalize activations and apply scale and shift from conditioning S."""
|
| 16 |
-
|
| 17 |
-
def __init__(self, dim: int, dim_single_cond: int) -> None:
|
| 18 |
-
super().__init__()
|
| 19 |
-
self.a_norm = nn.LayerNorm(dim, elementwise_affine=False, bias=False)
|
| 20 |
-
self.s_norm = nn.LayerNorm(dim_single_cond, bias=False)
|
| 21 |
-
self.s_scale = nn.Linear(dim_single_cond, dim)
|
| 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):
|
| 34 |
-
"""Apply a conditioned gated transition without changing tensor shape."""
|
| 35 |
-
|
| 36 |
-
def __init__(
|
| 37 |
-
self,
|
| 38 |
-
dim_single: int,
|
| 39 |
-
dim_single_cond: int,
|
| 40 |
-
expansion_factor: float = 2,
|
| 41 |
-
) -> None:
|
| 42 |
-
super().__init__()
|
| 43 |
-
self.adaln = AdaLN(dim_single, dim_single_cond)
|
| 44 |
-
inner_dim = int(dim_single * expansion_factor)
|
| 45 |
-
self.swish_gate = nn.Sequential(
|
| 46 |
-
LinearNoBias(dim_single, inner_dim * 2),
|
| 47 |
-
SwiGLU(),
|
| 48 |
-
)
|
| 49 |
-
self.a_to_b = LinearNoBias(dim_single, inner_dim)
|
| 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 |
-
|
| 65 |
-
class DiffusionTransformer(nn.Module):
|
| 66 |
-
"""Run conditioned transformer layers over token or atom states."""
|
| 67 |
-
|
| 68 |
-
def __init__(
|
| 69 |
-
self,
|
| 70 |
-
depth: int,
|
| 71 |
-
heads: int,
|
| 72 |
-
dim: int = 384,
|
| 73 |
-
dim_single_cond: int | None = None,
|
| 74 |
-
pair_bias_attn: bool = True,
|
| 75 |
-
activation_checkpointing: bool = False,
|
| 76 |
-
post_layer_norm: bool = False,
|
| 77 |
-
) -> None:
|
| 78 |
-
super().__init__()
|
| 79 |
-
conditioning_dim = dim if dim_single_cond is None else dim_single_cond
|
| 80 |
-
self.activation_checkpointing = activation_checkpointing
|
| 81 |
-
self.pair_bias_attn = pair_bias_attn
|
| 82 |
-
self.layers = nn.ModuleList(
|
| 83 |
-
[
|
| 84 |
-
DiffusionTransformerLayer(
|
| 85 |
-
heads,
|
| 86 |
-
dim,
|
| 87 |
-
conditioning_dim,
|
| 88 |
-
post_layer_norm,
|
| 89 |
-
)
|
| 90 |
-
for _ in range(depth)
|
| 91 |
-
]
|
| 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(
|
| 108 |
-
self,
|
| 109 |
-
a: Tensor,
|
| 110 |
-
s: Tensor,
|
| 111 |
-
bias: Tensor | None = None,
|
| 112 |
-
mask: Tensor | None = None,
|
| 113 |
-
to_keys: Callable[[Tensor], Tensor] | None = None,
|
| 114 |
-
multiplicity: int = 1,
|
| 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,
|
| 131 |
-
layer_bias,
|
| 132 |
-
mask,
|
| 133 |
-
to_keys,
|
| 134 |
-
multiplicity,
|
| 135 |
-
use_reentrant=False,
|
| 136 |
-
)
|
| 137 |
-
else:
|
| 138 |
-
output = layer( # (b * m, l_q, d)
|
| 139 |
-
output,
|
| 140 |
-
s,
|
| 141 |
-
layer_bias,
|
| 142 |
-
mask,
|
| 143 |
-
to_keys,
|
| 144 |
-
multiplicity,
|
| 145 |
-
)
|
| 146 |
-
return output # (b * m, l_q, d)
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
class DiffusionTransformerLayer(nn.Module):
|
| 150 |
-
"""One adaptive-normalization attention and transition block."""
|
| 151 |
-
|
| 152 |
-
def __init__(
|
| 153 |
-
self,
|
| 154 |
-
heads: int,
|
| 155 |
-
dim: int = 384,
|
| 156 |
-
dim_single_cond: int | None = None,
|
| 157 |
-
post_layer_norm: bool = False,
|
| 158 |
-
) -> None:
|
| 159 |
-
super().__init__()
|
| 160 |
-
conditioning_dim = dim if dim_single_cond is None else dim_single_cond
|
| 161 |
-
self.adaln = AdaLN(dim, conditioning_dim)
|
| 162 |
-
self.pair_bias_attn = AttentionPairBias(
|
| 163 |
-
c_s=dim,
|
| 164 |
-
num_heads=heads,
|
| 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(),
|
| 173 |
-
)
|
| 174 |
-
self.transition = ConditionedTransitionBlock(dim, conditioning_dim)
|
| 175 |
-
self.post_lnorm = nn.LayerNorm(dim) if post_layer_norm else nn.Identity()
|
| 176 |
-
|
| 177 |
-
def forward(
|
| 178 |
-
self,
|
| 179 |
-
a: Tensor,
|
| 180 |
-
s: Tensor,
|
| 181 |
-
bias: Tensor | None = None,
|
| 182 |
-
mask: Tensor | None = None,
|
| 183 |
-
to_keys: Callable[[Tensor], Tensor] | None = None,
|
| 184 |
-
multiplicity: int = 1,
|
| 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):
|
| 213 |
-
"""Apply the diffusion transformer over fixed atom-query windows."""
|
| 214 |
-
|
| 215 |
-
def __init__(
|
| 216 |
-
self,
|
| 217 |
-
attn_window_queries: int,
|
| 218 |
-
attn_window_keys: int,
|
| 219 |
-
**diffusion_transformer_kwargs: Any,
|
| 220 |
-
) -> None:
|
| 221 |
-
super().__init__()
|
| 222 |
-
self.attn_window_queries = attn_window_queries
|
| 223 |
-
self.attn_window_keys = attn_window_keys
|
| 224 |
-
self.diffusion_transformer = DiffusionTransformer(
|
| 225 |
-
**diffusion_transformer_kwargs,
|
| 226 |
-
)
|
| 227 |
-
|
| 228 |
-
def forward(
|
| 229 |
-
self,
|
| 230 |
-
q: Tensor,
|
| 231 |
-
c: Tensor,
|
| 232 |
-
bias: Tensor,
|
| 233 |
-
to_keys: Callable[[Tensor], Tensor],
|
| 234 |
-
mask: Tensor,
|
| 235 |
-
multiplicity: int = 1,
|
| 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,
|
| 257 |
-
-1,
|
| 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,
|
| 271 |
-
mask=query_mask.float(),
|
| 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))
|
|
|
|
| 1 |
+
"""Conditioned transformer blocks used by Boltz2 diffusion."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 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 |
+
|
| 10 |
+
from .vb_layers_attentionv2 import AttentionPairBias
|
| 11 |
+
from .vb_modules_utils import LinearNoBias, SwiGLU
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AdaLN(nn.Module):
|
| 15 |
+
"""Normalize activations and apply scale and shift from conditioning S."""
|
| 16 |
+
|
| 17 |
+
def __init__(self, dim: int, dim_single_cond: int) -> None:
|
| 18 |
+
super().__init__()
|
| 19 |
+
self.a_norm = nn.LayerNorm(dim, elementwise_affine=False, bias=False)
|
| 20 |
+
self.s_norm = nn.LayerNorm(dim_single_cond, bias=False)
|
| 21 |
+
self.s_scale = nn.Linear(dim_single_cond, dim)
|
| 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):
|
| 34 |
+
"""Apply a conditioned gated transition without changing tensor shape."""
|
| 35 |
+
|
| 36 |
+
def __init__(
|
| 37 |
+
self,
|
| 38 |
+
dim_single: int,
|
| 39 |
+
dim_single_cond: int,
|
| 40 |
+
expansion_factor: float = 2,
|
| 41 |
+
) -> None:
|
| 42 |
+
super().__init__()
|
| 43 |
+
self.adaln = AdaLN(dim_single, dim_single_cond)
|
| 44 |
+
inner_dim = int(dim_single * expansion_factor)
|
| 45 |
+
self.swish_gate = nn.Sequential(
|
| 46 |
+
LinearNoBias(dim_single, inner_dim * 2),
|
| 47 |
+
SwiGLU(),
|
| 48 |
+
)
|
| 49 |
+
self.a_to_b = LinearNoBias(dim_single, inner_dim)
|
| 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 |
+
|
| 65 |
+
class DiffusionTransformer(nn.Module):
|
| 66 |
+
"""Run conditioned transformer layers over token or atom states."""
|
| 67 |
+
|
| 68 |
+
def __init__(
|
| 69 |
+
self,
|
| 70 |
+
depth: int,
|
| 71 |
+
heads: int,
|
| 72 |
+
dim: int = 384,
|
| 73 |
+
dim_single_cond: int | None = None,
|
| 74 |
+
pair_bias_attn: bool = True,
|
| 75 |
+
activation_checkpointing: bool = False,
|
| 76 |
+
post_layer_norm: bool = False,
|
| 77 |
+
) -> None:
|
| 78 |
+
super().__init__()
|
| 79 |
+
conditioning_dim = dim if dim_single_cond is None else dim_single_cond
|
| 80 |
+
self.activation_checkpointing = activation_checkpointing
|
| 81 |
+
self.pair_bias_attn = pair_bias_attn
|
| 82 |
+
self.layers = nn.ModuleList(
|
| 83 |
+
[
|
| 84 |
+
DiffusionTransformerLayer(
|
| 85 |
+
heads,
|
| 86 |
+
dim,
|
| 87 |
+
conditioning_dim,
|
| 88 |
+
post_layer_norm,
|
| 89 |
+
)
|
| 90 |
+
for _ in range(depth)
|
| 91 |
+
]
|
| 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(
|
| 108 |
+
self,
|
| 109 |
+
a: Tensor,
|
| 110 |
+
s: Tensor,
|
| 111 |
+
bias: Tensor | None = None,
|
| 112 |
+
mask: Tensor | None = None,
|
| 113 |
+
to_keys: Callable[[Tensor], Tensor] | None = None,
|
| 114 |
+
multiplicity: int = 1,
|
| 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,
|
| 131 |
+
layer_bias,
|
| 132 |
+
mask,
|
| 133 |
+
to_keys,
|
| 134 |
+
multiplicity,
|
| 135 |
+
use_reentrant=False,
|
| 136 |
+
)
|
| 137 |
+
else:
|
| 138 |
+
output = layer( # (b * m, l_q, d)
|
| 139 |
+
output,
|
| 140 |
+
s,
|
| 141 |
+
layer_bias,
|
| 142 |
+
mask,
|
| 143 |
+
to_keys,
|
| 144 |
+
multiplicity,
|
| 145 |
+
)
|
| 146 |
+
return output # (b * m, l_q, d)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class DiffusionTransformerLayer(nn.Module):
|
| 150 |
+
"""One adaptive-normalization attention and transition block."""
|
| 151 |
+
|
| 152 |
+
def __init__(
|
| 153 |
+
self,
|
| 154 |
+
heads: int,
|
| 155 |
+
dim: int = 384,
|
| 156 |
+
dim_single_cond: int | None = None,
|
| 157 |
+
post_layer_norm: bool = False,
|
| 158 |
+
) -> None:
|
| 159 |
+
super().__init__()
|
| 160 |
+
conditioning_dim = dim if dim_single_cond is None else dim_single_cond
|
| 161 |
+
self.adaln = AdaLN(dim, conditioning_dim)
|
| 162 |
+
self.pair_bias_attn = AttentionPairBias(
|
| 163 |
+
c_s=dim,
|
| 164 |
+
num_heads=heads,
|
| 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(),
|
| 173 |
+
)
|
| 174 |
+
self.transition = ConditionedTransitionBlock(dim, conditioning_dim)
|
| 175 |
+
self.post_lnorm = nn.LayerNorm(dim) if post_layer_norm else nn.Identity()
|
| 176 |
+
|
| 177 |
+
def forward(
|
| 178 |
+
self,
|
| 179 |
+
a: Tensor,
|
| 180 |
+
s: Tensor,
|
| 181 |
+
bias: Tensor | None = None,
|
| 182 |
+
mask: Tensor | None = None,
|
| 183 |
+
to_keys: Callable[[Tensor], Tensor] | None = None,
|
| 184 |
+
multiplicity: int = 1,
|
| 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):
|
| 213 |
+
"""Apply the diffusion transformer over fixed atom-query windows."""
|
| 214 |
+
|
| 215 |
+
def __init__(
|
| 216 |
+
self,
|
| 217 |
+
attn_window_queries: int,
|
| 218 |
+
attn_window_keys: int,
|
| 219 |
+
**diffusion_transformer_kwargs: Any,
|
| 220 |
+
) -> None:
|
| 221 |
+
super().__init__()
|
| 222 |
+
self.attn_window_queries = attn_window_queries
|
| 223 |
+
self.attn_window_keys = attn_window_keys
|
| 224 |
+
self.diffusion_transformer = DiffusionTransformer(
|
| 225 |
+
**diffusion_transformer_kwargs,
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
def forward(
|
| 229 |
+
self,
|
| 230 |
+
q: Tensor,
|
| 231 |
+
c: Tensor,
|
| 232 |
+
bias: Tensor,
|
| 233 |
+
to_keys: Callable[[Tensor], Tensor],
|
| 234 |
+
mask: Tensor,
|
| 235 |
+
multiplicity: int = 1,
|
| 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,
|
| 257 |
+
-1,
|
| 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,
|
| 271 |
+
mask=query_mask.float(),
|
| 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
|
@@ -1,624 +1,624 @@
|
|
| 1 |
-
"""Input, template, MSA, and prediction-head modules for the Boltz2 trunk."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from typing import Any, cast
|
| 8 |
-
from torch import Tensor, nn
|
| 9 |
-
from torch.nn import functional as F
|
| 10 |
-
from torch.utils.checkpoint import checkpoint
|
| 11 |
-
|
| 12 |
-
from . import vb_const as const
|
| 13 |
-
from .vb_layers_dropout import get_dropout_mask
|
| 14 |
-
from .vb_layers_outer_product_mean import OuterProductMean
|
| 15 |
-
from .vb_layers_pair_averaging import PairWeightedAveraging
|
| 16 |
-
from .vb_layers_pairformer import PairformerNoSeqLayer, PairformerNoSeqModule
|
| 17 |
-
from .vb_layers_transition import Transition
|
| 18 |
-
from .vb_modules_encodersv2 import (
|
| 19 |
-
AtomAttentionEncoder,
|
| 20 |
-
AtomEncoder,
|
| 21 |
-
FourierEmbedding,
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
class ContactConditioning(nn.Module):
|
| 26 |
-
"""Encode selected, unselected, and distance-threshold contact constraints."""
|
| 27 |
-
|
| 28 |
-
def __init__(self, token_z: int, cutoff_min: float, cutoff_max: float) -> None:
|
| 29 |
-
super().__init__()
|
| 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)) # (d_z,)
|
| 34 |
-
self.encoding_unselected = nn.Parameter(torch.zeros(token_z)) # (d_z,)
|
| 35 |
-
self.cutoff_min = cutoff_min
|
| 36 |
-
self.cutoff_max = cutoff_max
|
| 37 |
-
|
| 38 |
-
def forward(self, feats: dict[str, Tensor]) -> Tensor:
|
| 39 |
-
"""Return contact tensor C with shape ``(b, l, l, d_z)``."""
|
| 40 |
-
|
| 41 |
-
if const.contact_conditioning_info["UNSPECIFIED"] != 0:
|
| 42 |
-
raise ValueError("UNSPECIFIED contact conditioning must use channel zero")
|
| 43 |
-
if const.contact_conditioning_info["UNSELECTED"] != 1:
|
| 44 |
-
raise ValueError("UNSELECTED contact conditioning must use channel one")
|
| 45 |
-
|
| 46 |
-
# c_contact is the number of contact-conditioning categories.
|
| 47 |
-
categories = feats["contact_conditioning"] # (b, l, l, c_contact)
|
| 48 |
-
threshold = feats["contact_threshold"] # (b, l, l)
|
| 49 |
-
# (b, l, l)
|
| 50 |
-
normalized = (threshold - self.cutoff_min) / (self.cutoff_max - self.cutoff_min)
|
| 51 |
-
# (b, l, l, d_z)
|
| 52 |
-
fourier = self.fourier_embedding(normalized.flatten()).reshape((*normalized.shape, -1))
|
| 53 |
-
selected_features = torch.cat( # (b, l, l, c_contact - 1 + d_z)
|
| 54 |
-
[categories[..., 2:], normalized.unsqueeze(-1), fourier],
|
| 55 |
-
dim=-1,
|
| 56 |
-
)
|
| 57 |
-
selected = self.encoder(selected_features) # (b, l, l, d_z)
|
| 58 |
-
special = categories[..., :2] # (b, l, l, 2)
|
| 59 |
-
return cast(
|
| 60 |
-
Tensor,
|
| 61 |
-
selected * (1 - special.sum(dim=-1, keepdim=True))
|
| 62 |
-
+ self.encoding_unspecified * special[..., 0:1]
|
| 63 |
-
+ self.encoding_unselected * special[..., 1:2],
|
| 64 |
-
) # (b, l, l, d_z)
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
class InputEmbedder(nn.Module):
|
| 68 |
-
"""Combine atom, residue, profile, and optional experimental features."""
|
| 69 |
-
|
| 70 |
-
def __init__(
|
| 71 |
-
self,
|
| 72 |
-
atom_s: int,
|
| 73 |
-
atom_z: int,
|
| 74 |
-
token_s: int,
|
| 75 |
-
token_z: int,
|
| 76 |
-
atoms_per_window_queries: int,
|
| 77 |
-
atoms_per_window_keys: int,
|
| 78 |
-
atom_feature_dim: int,
|
| 79 |
-
atom_encoder_depth: int,
|
| 80 |
-
atom_encoder_heads: int,
|
| 81 |
-
activation_checkpointing: bool = False,
|
| 82 |
-
add_method_conditioning: bool = False,
|
| 83 |
-
add_modified_flag: bool = False,
|
| 84 |
-
add_cyclic_flag: bool = False,
|
| 85 |
-
add_mol_type_feat: bool = False,
|
| 86 |
-
use_no_atom_char: bool = False,
|
| 87 |
-
use_atom_backbone_feat: bool = False,
|
| 88 |
-
use_residue_feats_atoms: bool = False,
|
| 89 |
-
) -> None:
|
| 90 |
-
super().__init__()
|
| 91 |
-
self.token_s = token_s
|
| 92 |
-
self.add_method_conditioning = add_method_conditioning
|
| 93 |
-
self.add_modified_flag = add_modified_flag
|
| 94 |
-
self.add_cyclic_flag = add_cyclic_flag
|
| 95 |
-
self.add_mol_type_feat = add_mol_type_feat
|
| 96 |
-
self.atom_encoder = AtomEncoder(
|
| 97 |
-
atom_s=atom_s,
|
| 98 |
-
atom_z=atom_z,
|
| 99 |
-
token_s=token_s,
|
| 100 |
-
token_z=token_z,
|
| 101 |
-
atoms_per_window_queries=atoms_per_window_queries,
|
| 102 |
-
atoms_per_window_keys=atoms_per_window_keys,
|
| 103 |
-
atom_feature_dim=atom_feature_dim,
|
| 104 |
-
structure_prediction=False,
|
| 105 |
-
use_no_atom_char=use_no_atom_char,
|
| 106 |
-
use_atom_backbone_feat=use_atom_backbone_feat,
|
| 107 |
-
use_residue_feats_atoms=use_residue_feats_atoms,
|
| 108 |
-
)
|
| 109 |
-
self.atom_enc_proj_z = nn.Sequential(
|
| 110 |
-
nn.LayerNorm(atom_z),
|
| 111 |
-
nn.Linear(atom_z, atom_encoder_depth * atom_encoder_heads, bias=False),
|
| 112 |
-
)
|
| 113 |
-
self.atom_attention_encoder = AtomAttentionEncoder(
|
| 114 |
-
atom_s=atom_s,
|
| 115 |
-
token_s=token_s,
|
| 116 |
-
atoms_per_window_queries=atoms_per_window_queries,
|
| 117 |
-
atoms_per_window_keys=atoms_per_window_keys,
|
| 118 |
-
atom_encoder_depth=atom_encoder_depth,
|
| 119 |
-
atom_encoder_heads=atom_encoder_heads,
|
| 120 |
-
structure_prediction=False,
|
| 121 |
-
activation_checkpointing=activation_checkpointing,
|
| 122 |
-
)
|
| 123 |
-
self.res_type_encoding = nn.Linear(const.num_tokens, token_s, bias=False)
|
| 124 |
-
self.msa_profile_encoding = nn.Linear(const.num_tokens + 1, token_s, bias=False)
|
| 125 |
-
|
| 126 |
-
if add_method_conditioning:
|
| 127 |
-
self.method_conditioning_init = nn.Embedding(const.num_method_types, token_s)
|
| 128 |
-
self.method_conditioning_init.weight.data.fill_(0) # (n_method, d_s)
|
| 129 |
-
if add_modified_flag:
|
| 130 |
-
self.modified_conditioning_init = nn.Embedding(2, token_s)
|
| 131 |
-
self.modified_conditioning_init.weight.data.fill_(0) # (2, d_s)
|
| 132 |
-
if add_cyclic_flag:
|
| 133 |
-
self.cyclic_conditioning_init = nn.Linear(1, token_s, bias=False)
|
| 134 |
-
self.cyclic_conditioning_init.weight.data.fill_(0) # (d_s, 1)
|
| 135 |
-
if add_mol_type_feat:
|
| 136 |
-
self.mol_type_conditioning_init = nn.Embedding(
|
| 137 |
-
len(const.chain_type_ids),
|
| 138 |
-
token_s,
|
| 139 |
-
)
|
| 140 |
-
self.mol_type_conditioning_init.weight.data.fill_(0) # (n_mol_type, d_s)
|
| 141 |
-
|
| 142 |
-
def forward(self, feats: dict[str, Tensor], affinity: bool = False) -> Tensor:
|
| 143 |
-
"""Return embedded sequence tensor S with shape ``(b, l, d_s)``."""
|
| 144 |
-
|
| 145 |
-
# n_atom is the padded atom count; k is the number of atom windows.
|
| 146 |
-
residue_type = feats["res_type"].float() # (b, l, n_token_type)
|
| 147 |
-
suffix = "_affinity" if affinity else ""
|
| 148 |
-
profile = feats[f"profile{suffix}"] # (b, l, n_token_type)
|
| 149 |
-
deletion_mean = feats[f"deletion_mean{suffix}"].unsqueeze(-1) # (b, l, 1)
|
| 150 |
-
# (b, n_atom, d_a), (b, n_atom, d_a), (b, k, w_q, w_k, d_az), callable
|
| 151 |
-
atom_queries, atom_conditioning, atom_pairs, to_keys = self.atom_encoder(feats)
|
| 152 |
-
atom_bias = self.atom_enc_proj_z(atom_pairs) # (b, k, w_q, w_k, n_layer * h)
|
| 153 |
-
atom_output, _, _, _ = self.atom_attention_encoder(
|
| 154 |
-
feats=feats,
|
| 155 |
-
q=atom_queries,
|
| 156 |
-
c=atom_conditioning,
|
| 157 |
-
atom_enc_bias=atom_bias,
|
| 158 |
-
to_keys=to_keys,
|
| 159 |
-
) # (b, l, d_s), (b, n_atom, d_a), (b, n_atom, d_a), callable
|
| 160 |
-
output = ( # (b, l, d_s)
|
| 161 |
-
atom_output
|
| 162 |
-
+ self.res_type_encoding(residue_type)
|
| 163 |
-
+ self.msa_profile_encoding(torch.cat([profile, deletion_mean], dim=-1))
|
| 164 |
-
)
|
| 165 |
-
if self.add_method_conditioning:
|
| 166 |
-
# method_feature: (b, l); output: (b, l, d_s).
|
| 167 |
-
output = output + self.method_conditioning_init(feats["method_feature"])
|
| 168 |
-
if self.add_modified_flag:
|
| 169 |
-
# modified: (b, l); output: (b, l, d_s).
|
| 170 |
-
output = output + self.modified_conditioning_init(feats["modified"])
|
| 171 |
-
if self.add_cyclic_flag:
|
| 172 |
-
cyclic = feats["cyclic_period"].clamp(max=1.0).unsqueeze(-1) # (b, l, 1)
|
| 173 |
-
output = output + self.cyclic_conditioning_init(cyclic) # (b, l, d_s)
|
| 174 |
-
if self.add_mol_type_feat:
|
| 175 |
-
# mol_type: (b, l); output: (b, l, d_s).
|
| 176 |
-
output = output + self.mol_type_conditioning_init(feats["mol_type"])
|
| 177 |
-
return cast(Tensor, output) # (b, l, d_s)
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
class _TemplateBase(nn.Module):
|
| 181 |
-
def __init__(
|
| 182 |
-
self,
|
| 183 |
-
token_z: int,
|
| 184 |
-
template_dim: int,
|
| 185 |
-
template_blocks: int,
|
| 186 |
-
dropout: float,
|
| 187 |
-
pairwise_head_width: int,
|
| 188 |
-
pairwise_num_heads: int,
|
| 189 |
-
post_layer_norm: bool,
|
| 190 |
-
activation_checkpointing: bool,
|
| 191 |
-
min_dist: float,
|
| 192 |
-
max_dist: float,
|
| 193 |
-
num_bins: int,
|
| 194 |
-
) -> None:
|
| 195 |
-
super().__init__()
|
| 196 |
-
self.min_dist = min_dist
|
| 197 |
-
self.max_dist = max_dist
|
| 198 |
-
self.num_bins = num_bins
|
| 199 |
-
self.relu = nn.ReLU()
|
| 200 |
-
self.z_norm = nn.LayerNorm(token_z)
|
| 201 |
-
self.v_norm = nn.LayerNorm(template_dim)
|
| 202 |
-
self.z_proj = nn.Linear(token_z, template_dim, bias=False)
|
| 203 |
-
feature_width = const.num_tokens * 2 + num_bins + 5
|
| 204 |
-
self.a_proj = nn.Linear(feature_width, template_dim, bias=False)
|
| 205 |
-
self.u_proj = nn.Linear(template_dim, token_z, bias=False)
|
| 206 |
-
self.pairformer = PairformerNoSeqModule(
|
| 207 |
-
template_dim,
|
| 208 |
-
num_blocks=template_blocks,
|
| 209 |
-
dropout=dropout,
|
| 210 |
-
pairwise_head_width=pairwise_head_width,
|
| 211 |
-
pairwise_num_heads=pairwise_num_heads,
|
| 212 |
-
post_layer_norm=post_layer_norm,
|
| 213 |
-
activation_checkpointing=activation_checkpointing,
|
| 214 |
-
)
|
| 215 |
-
|
| 216 |
-
def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
|
| 217 |
-
raise NotImplementedError
|
| 218 |
-
|
| 219 |
-
def _template_features(
|
| 220 |
-
self,
|
| 221 |
-
feats: dict[str, Tensor],
|
| 222 |
-
template_pair_mask: Tensor,
|
| 223 |
-
) -> Tensor:
|
| 224 |
-
# residue_type: (b, t, l, n_token_type); template_pair_mask: (b, t, l, l).
|
| 225 |
-
residue_type = feats["template_restype"] # (b, t, l, n_token_type)
|
| 226 |
-
cb_mask = feats["template_mask_cb"] # (b, t, l)
|
| 227 |
-
frame_mask = feats["template_mask_frame"] # (b, t, l)
|
| 228 |
-
# (b, t, l, l, 1)
|
| 229 |
-
cb_pair_mask = (cb_mask[..., :, None] * cb_mask[..., None, :]).unsqueeze(-1)
|
| 230 |
-
# (b, t, l, l, 1)
|
| 231 |
-
frame_pair_mask = (frame_mask[..., :, None] * frame_mask[..., None, :]).unsqueeze(-1)
|
| 232 |
-
with torch.autocast(device_type="cuda", enabled=False):
|
| 233 |
-
# template_cb: (b, t, l, 3).
|
| 234 |
-
# (b, t, l, l)
|
| 235 |
-
cb_distances = torch.cdist(feats["template_cb"], feats["template_cb"])
|
| 236 |
-
boundaries = torch.linspace( # (n_bin - 1,)
|
| 237 |
-
self.min_dist,
|
| 238 |
-
self.max_dist,
|
| 239 |
-
self.num_bins - 1,
|
| 240 |
-
).to(cb_distances.device)
|
| 241 |
-
bins = (cb_distances[..., None] > boundaries).sum(dim=-1).long() # (b, t, l, l)
|
| 242 |
-
distogram = F.one_hot(bins, num_classes=self.num_bins) # (b, t, l, l, n_bin)
|
| 243 |
-
|
| 244 |
-
# (b, t, 1, l, 3, 3)
|
| 245 |
-
rotations = feats["template_frame_rot"].unsqueeze(2).transpose(-1, -2)
|
| 246 |
-
# (b, t, 1, l, 3, 1)
|
| 247 |
-
translations = feats["template_frame_t"].unsqueeze(2).unsqueeze(-1)
|
| 248 |
-
# (b, t, l, 1, 3, 1)
|
| 249 |
-
ca_coordinates = feats["template_ca"].unsqueeze(3).unsqueeze(-1)
|
| 250 |
-
# (b, t, l, l, 3, 1)
|
| 251 |
-
vectors = torch.matmul(rotations, ca_coordinates - translations)
|
| 252 |
-
norms = torch.norm(vectors, dim=-1, keepdim=True) # (b, t, l, l, 3, 1)
|
| 253 |
-
unit_vectors = torch.where( # (b, t, l, l, 3)
|
| 254 |
-
norms > 0,
|
| 255 |
-
vectors / norms,
|
| 256 |
-
torch.zeros_like(vectors),
|
| 257 |
-
).squeeze(-1)
|
| 258 |
-
pair_features = torch.cat( # (b, t, l, l, n_bin + 5)
|
| 259 |
-
[distogram, cb_pair_mask, unit_vectors, frame_pair_mask],
|
| 260 |
-
dim=-1,
|
| 261 |
-
)
|
| 262 |
-
# (b, t, l, l, n_bin + 5)
|
| 263 |
-
pair_features = pair_features * template_pair_mask.unsqueeze(-1)
|
| 264 |
-
residue_i = residue_type[:, :, :, None].expand( # (b, t, l, l, n_token_type)
|
| 265 |
-
-1,
|
| 266 |
-
-1,
|
| 267 |
-
-1,
|
| 268 |
-
residue_type.size(2),
|
| 269 |
-
-1,
|
| 270 |
-
)
|
| 271 |
-
residue_j = residue_type[:, :, None, :].expand( # (b, t, l, l, n_token_type)
|
| 272 |
-
-1,
|
| 273 |
-
-1,
|
| 274 |
-
residue_type.size(2),
|
| 275 |
-
-1,
|
| 276 |
-
-1,
|
| 277 |
-
)
|
| 278 |
-
return cast(
|
| 279 |
-
Tensor,
|
| 280 |
-
self.a_proj(torch.cat([pair_features, residue_i, residue_j], dim=-1)),
|
| 281 |
-
) # (b, t, l, l, d_t)
|
| 282 |
-
|
| 283 |
-
def forward(
|
| 284 |
-
self,
|
| 285 |
-
z: Tensor,
|
| 286 |
-
feats: dict[str, Tensor],
|
| 287 |
-
pair_mask: Tensor,
|
| 288 |
-
use_kernels: bool = False,
|
| 289 |
-
) -> Tensor:
|
| 290 |
-
"""Aggregate template pair tensor V into trunk update U."""
|
| 291 |
-
|
| 292 |
-
# z: (b, l, l, d_z); pair_mask: (b, l, l).
|
| 293 |
-
residue_type = feats["template_restype"] # (b, t, l, n_token_type)
|
| 294 |
-
batch_size, template_count = residue_type.shape[:2]
|
| 295 |
-
template_present = feats["template_mask"].any(dim=2).float() # (b, t)
|
| 296 |
-
present_count = template_present.sum(dim=1).clamp(min=1) # (b,)
|
| 297 |
-
features = self._template_features( # (b, t, l, l, d_t)
|
| 298 |
-
feats,
|
| 299 |
-
self._template_pair_mask(feats, template_count),
|
| 300 |
-
)
|
| 301 |
-
expanded_mask = pair_mask[:, None].expand(-1, template_count, -1, -1) # (b, t, l, l)
|
| 302 |
-
expanded_mask = expanded_mask.reshape( # (b * t, l, l)
|
| 303 |
-
batch_size * template_count,
|
| 304 |
-
*expanded_mask.shape[2:],
|
| 305 |
-
)
|
| 306 |
-
template_states = self.z_proj(self.z_norm(z[:, None])) + features # (b, t, l, l, d_t)
|
| 307 |
-
template_states = template_states.view( # (b * t, l, l, d_t)
|
| 308 |
-
batch_size * template_count,
|
| 309 |
-
*template_states.shape[2:],
|
| 310 |
-
)
|
| 311 |
-
template_states = template_states + self.pairformer( # (b * t, l, l, d_t)
|
| 312 |
-
template_states,
|
| 313 |
-
expanded_mask,
|
| 314 |
-
use_kernels=use_kernels,
|
| 315 |
-
)
|
| 316 |
-
template_states = self.v_norm(template_states).view( # (b, t, l, l, d_t)
|
| 317 |
-
batch_size,
|
| 318 |
-
template_count,
|
| 319 |
-
*template_states.shape[1:],
|
| 320 |
-
)
|
| 321 |
-
weights = template_present[:, :, None, None, None] # (b, t, 1, 1, 1)
|
| 322 |
-
aggregate = (template_states * weights).sum(dim=1) # (b, l, l, d_t)
|
| 323 |
-
# (b, l, l, d_t)
|
| 324 |
-
aggregate = aggregate / present_count[:, None, None, None].to(template_states)
|
| 325 |
-
return cast(Tensor, self.u_proj(self.relu(aggregate))) # (b, l, l, d_z)
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
class TemplateModule(_TemplateBase):
|
| 329 |
-
"""Aggregate templates while restricting features to the same chain."""
|
| 330 |
-
|
| 331 |
-
def __init__(
|
| 332 |
-
self,
|
| 333 |
-
token_z: int,
|
| 334 |
-
template_dim: int,
|
| 335 |
-
template_blocks: int,
|
| 336 |
-
dropout: float = 0.25,
|
| 337 |
-
pairwise_head_width: int = 32,
|
| 338 |
-
pairwise_num_heads: int = 4,
|
| 339 |
-
post_layer_norm: bool = False,
|
| 340 |
-
activation_checkpointing: bool = False,
|
| 341 |
-
min_dist: float = 3.25,
|
| 342 |
-
max_dist: float = 50.75,
|
| 343 |
-
num_bins: int = 38,
|
| 344 |
-
**kwargs: Any,
|
| 345 |
-
) -> None:
|
| 346 |
-
del kwargs
|
| 347 |
-
super().__init__(
|
| 348 |
-
token_z,
|
| 349 |
-
template_dim,
|
| 350 |
-
template_blocks,
|
| 351 |
-
dropout,
|
| 352 |
-
pairwise_head_width,
|
| 353 |
-
pairwise_num_heads,
|
| 354 |
-
post_layer_norm,
|
| 355 |
-
activation_checkpointing,
|
| 356 |
-
min_dist,
|
| 357 |
-
max_dist,
|
| 358 |
-
num_bins,
|
| 359 |
-
)
|
| 360 |
-
|
| 361 |
-
def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
|
| 362 |
-
asym_id = feats["asym_id"] # (b, l)
|
| 363 |
-
same_chain = (asym_id[:, :, None] == asym_id[:, None, :]).float() # (b, l, l)
|
| 364 |
-
return same_chain[:, None].expand(-1, count, -1, -1) # (b, t, l, l)
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
class TemplateV2Module(_TemplateBase):
|
| 368 |
-
"""Aggregate templates under per-template visibility groups."""
|
| 369 |
-
|
| 370 |
-
def __init__(
|
| 371 |
-
self,
|
| 372 |
-
token_z: int,
|
| 373 |
-
template_dim: int,
|
| 374 |
-
template_blocks: int,
|
| 375 |
-
dropout: float = 0.25,
|
| 376 |
-
pairwise_head_width: int = 32,
|
| 377 |
-
pairwise_num_heads: int = 4,
|
| 378 |
-
post_layer_norm: bool = False,
|
| 379 |
-
activation_checkpointing: bool = False,
|
| 380 |
-
min_dist: float = 3.25,
|
| 381 |
-
max_dist: float = 50.75,
|
| 382 |
-
num_bins: int = 38,
|
| 383 |
-
**kwargs: Any,
|
| 384 |
-
) -> None:
|
| 385 |
-
del kwargs
|
| 386 |
-
super().__init__(
|
| 387 |
-
token_z,
|
| 388 |
-
template_dim,
|
| 389 |
-
template_blocks,
|
| 390 |
-
dropout,
|
| 391 |
-
pairwise_head_width,
|
| 392 |
-
pairwise_num_heads,
|
| 393 |
-
post_layer_norm,
|
| 394 |
-
activation_checkpointing,
|
| 395 |
-
min_dist,
|
| 396 |
-
max_dist,
|
| 397 |
-
num_bins,
|
| 398 |
-
)
|
| 399 |
-
|
| 400 |
-
def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
|
| 401 |
-
del count
|
| 402 |
-
visibility = feats["visibility_ids"] # (b, t, l)
|
| 403 |
-
return (visibility[..., :, None] == visibility[..., None, :]).float() # (b, t, l, l)
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
class MSAModule(nn.Module):
|
| 407 |
-
"""Embed and update an MSA before returning its accumulated pair update."""
|
| 408 |
-
|
| 409 |
-
def __init__(
|
| 410 |
-
self,
|
| 411 |
-
msa_s: int,
|
| 412 |
-
token_z: int,
|
| 413 |
-
token_s: int,
|
| 414 |
-
msa_blocks: int,
|
| 415 |
-
msa_dropout: float,
|
| 416 |
-
z_dropout: float,
|
| 417 |
-
pairwise_head_width: int = 32,
|
| 418 |
-
pairwise_num_heads: int = 4,
|
| 419 |
-
activation_checkpointing: bool = False,
|
| 420 |
-
use_paired_feature: bool = True,
|
| 421 |
-
subsample_msa: bool = False,
|
| 422 |
-
num_subsampled_msa: int = 1024,
|
| 423 |
-
**kwargs: Any,
|
| 424 |
-
) -> None:
|
| 425 |
-
del kwargs
|
| 426 |
-
super().__init__()
|
| 427 |
-
self.msa_blocks = msa_blocks
|
| 428 |
-
self.msa_dropout = msa_dropout
|
| 429 |
-
self.z_dropout = z_dropout
|
| 430 |
-
self.use_paired_feature = use_paired_feature
|
| 431 |
-
self.activation_checkpointing = activation_checkpointing
|
| 432 |
-
self.subsample_msa = subsample_msa
|
| 433 |
-
self.num_subsampled_msa = num_subsampled_msa
|
| 434 |
-
self.s_proj = nn.Linear(token_s, msa_s, bias=False)
|
| 435 |
-
input_width = const.num_tokens + 2 + int(use_paired_feature)
|
| 436 |
-
self.msa_proj = nn.Linear(input_width, msa_s, bias=False)
|
| 437 |
-
self.layers = nn.ModuleList(
|
| 438 |
-
[
|
| 439 |
-
MSALayer(
|
| 440 |
-
msa_s,
|
| 441 |
-
token_z,
|
| 442 |
-
msa_dropout,
|
| 443 |
-
z_dropout,
|
| 444 |
-
pairwise_head_width,
|
| 445 |
-
pairwise_num_heads,
|
| 446 |
-
)
|
| 447 |
-
for _ in range(msa_blocks)
|
| 448 |
-
]
|
| 449 |
-
)
|
| 450 |
-
|
| 451 |
-
@staticmethod
|
| 452 |
-
def _chunk_configuration(
|
| 453 |
-
pair_states: Tensor,
|
| 454 |
-
training: bool,
|
| 455 |
-
) -> tuple[bool, int | None, int | None, int | None, int | None]:
|
| 456 |
-
# pair_states: (b, l, l, d_z).
|
| 457 |
-
if training:
|
| 458 |
-
return False, None, None, None, None
|
| 459 |
-
if pair_states.shape[1] > const.chunk_size_threshold:
|
| 460 |
-
return True, 64, 32, 4, 128
|
| 461 |
-
return False, None, None, None, 512
|
| 462 |
-
|
| 463 |
-
def forward(
|
| 464 |
-
self,
|
| 465 |
-
z: Tensor,
|
| 466 |
-
emb: Tensor,
|
| 467 |
-
feats: dict[str, Tensor],
|
| 468 |
-
use_kernels: bool = False,
|
| 469 |
-
) -> Tensor:
|
| 470 |
-
"""Return updated pair tensor Z after every MSA block."""
|
| 471 |
-
|
| 472 |
-
chunking = self._chunk_configuration(z, self.training)
|
| 473 |
-
# z: (b, l, l, d_z); emb: (b, l, d_s).
|
| 474 |
-
# s is MSA depth; n_token_type is the residue vocabulary size.
|
| 475 |
-
msa = feats["msa"] # (b, s, l) or (b, s, l, n_token_type)
|
| 476 |
-
if msa.dtype in (torch.long, torch.int32, torch.int64):
|
| 477 |
-
msa = F.one_hot(msa, num_classes=const.num_tokens).float() # (b, s, l, n_token_type)
|
| 478 |
-
msa_mask = feats["msa_mask"] # (b, s, l)
|
| 479 |
-
components = [ # (b, s, l, n_token_type), then two (b, s, l, 1) tensors
|
| 480 |
-
msa,
|
| 481 |
-
feats["has_deletion"].unsqueeze(-1), # (b, s, l, 1)
|
| 482 |
-
feats["deletion_value"].unsqueeze(-1), # (b, s, l, 1)
|
| 483 |
-
]
|
| 484 |
-
if self.use_paired_feature:
|
| 485 |
-
components.append(feats["msa_paired"].unsqueeze(-1)) # (b, s, l, 1)
|
| 486 |
-
msa_input = torch.cat(components, dim=-1) # (b, s, l, n_token_type + 2 or 3)
|
| 487 |
-
if self.subsample_msa:
|
| 488 |
-
indices = torch.randperm(msa.shape[1])[: self.num_subsampled_msa] # (s_sub,)
|
| 489 |
-
msa_input = msa_input[:, indices] # (b, s_sub, l, n_token_type + 2 or 3)
|
| 490 |
-
msa_mask = msa_mask[:, indices] # (b, s_sub, l)
|
| 491 |
-
|
| 492 |
-
msa_states = self.msa_proj(msa_input) + self.s_proj(emb).unsqueeze(1) # (b, s, l, d_m)
|
| 493 |
-
token_mask = feats["token_pad_mask"].float() # (b, l)
|
| 494 |
-
pair_mask = token_mask[:, :, None] * token_mask[:, None, :] # (b, l, l)
|
| 495 |
-
pair_states = z # (b, l, l, d_z)
|
| 496 |
-
for layer in self.layers:
|
| 497 |
-
# Tensor arguments: pair_states (b, l, l, d_z), msa_states (b, s, l, d_m),
|
| 498 |
-
# pair_mask (b, l, l), msa_mask (b, s, l).
|
| 499 |
-
arguments = (
|
| 500 |
-
pair_states,
|
| 501 |
-
msa_states,
|
| 502 |
-
pair_mask,
|
| 503 |
-
msa_mask,
|
| 504 |
-
*chunking,
|
| 505 |
-
use_kernels,
|
| 506 |
-
)
|
| 507 |
-
if self.activation_checkpointing and self.training:
|
| 508 |
-
pair_states, msa_states = checkpoint( # (b, l, l, d_z), (b, s, l, d_m)
|
| 509 |
-
layer,
|
| 510 |
-
*arguments,
|
| 511 |
-
)
|
| 512 |
-
else:
|
| 513 |
-
pair_states, msa_states = layer(*arguments) # (b, l, l, d_z), (b, s, l, d_m)
|
| 514 |
-
return pair_states # (b, l, l, d_z)
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
class MSALayer(nn.Module):
|
| 518 |
-
"""Exchange information between MSA tensor M and pair tensor Z."""
|
| 519 |
-
|
| 520 |
-
def __init__(
|
| 521 |
-
self,
|
| 522 |
-
msa_s: int,
|
| 523 |
-
token_z: int,
|
| 524 |
-
msa_dropout: float,
|
| 525 |
-
z_dropout: float,
|
| 526 |
-
pairwise_head_width: int = 32,
|
| 527 |
-
pairwise_num_heads: int = 4,
|
| 528 |
-
) -> None:
|
| 529 |
-
super().__init__()
|
| 530 |
-
self.msa_dropout = msa_dropout
|
| 531 |
-
self.msa_transition = Transition(msa_s, msa_s * 4)
|
| 532 |
-
self.pair_weighted_averaging = PairWeightedAveraging(
|
| 533 |
-
c_m=msa_s,
|
| 534 |
-
c_z=token_z,
|
| 535 |
-
c_h=32,
|
| 536 |
-
num_heads=8,
|
| 537 |
-
)
|
| 538 |
-
self.pairformer_layer = PairformerNoSeqLayer(
|
| 539 |
-
token_z=token_z,
|
| 540 |
-
dropout=z_dropout,
|
| 541 |
-
pairwise_head_width=pairwise_head_width,
|
| 542 |
-
pairwise_num_heads=pairwise_num_heads,
|
| 543 |
-
)
|
| 544 |
-
self.outer_product_mean = OuterProductMean(msa_s, 32, token_z)
|
| 545 |
-
|
| 546 |
-
def forward(
|
| 547 |
-
self,
|
| 548 |
-
z: Tensor,
|
| 549 |
-
m: Tensor,
|
| 550 |
-
token_mask: Tensor,
|
| 551 |
-
msa_mask: Tensor,
|
| 552 |
-
chunk_heads_pwa: bool = False,
|
| 553 |
-
chunk_size_transition_z: int | None = None,
|
| 554 |
-
chunk_size_transition_msa: int | None = None,
|
| 555 |
-
chunk_size_outer_product: int | None = None,
|
| 556 |
-
chunk_size_tri_attn: int | None = None,
|
| 557 |
-
use_kernels: bool = False,
|
| 558 |
-
) -> tuple[Tensor, Tensor]:
|
| 559 |
-
"""Return updated Z and M tensors."""
|
| 560 |
-
|
| 561 |
-
del chunk_size_transition_z
|
| 562 |
-
# z: (b, l, l, d_z); m: (b, s, l, d_m).
|
| 563 |
-
# token_mask: (b, l, l); msa_mask: (b, s, l).
|
| 564 |
-
dropout = get_dropout_mask(self.msa_dropout, m, self.training) # (b, s, 1, 1)
|
| 565 |
-
msa_states = m + dropout * self.pair_weighted_averaging( # (b, s, l, d_m)
|
| 566 |
-
m,
|
| 567 |
-
z,
|
| 568 |
-
token_mask,
|
| 569 |
-
chunk_heads_pwa,
|
| 570 |
-
)
|
| 571 |
-
msa_states = msa_states + self.msa_transition( # (b, s, l, d_m)
|
| 572 |
-
msa_states,
|
| 573 |
-
chunk_size_transition_msa,
|
| 574 |
-
)
|
| 575 |
-
pair_states = z + self.outer_product_mean( # (b, l, l, d_z)
|
| 576 |
-
msa_states,
|
| 577 |
-
msa_mask,
|
| 578 |
-
chunk_size_outer_product,
|
| 579 |
-
)
|
| 580 |
-
pair_states = self.pairformer_layer( # (b, l, l, d_z)
|
| 581 |
-
pair_states,
|
| 582 |
-
token_mask,
|
| 583 |
-
chunk_size_tri_attn,
|
| 584 |
-
use_kernels=use_kernels,
|
| 585 |
-
)
|
| 586 |
-
return pair_states, msa_states # (b, l, l, d_z), (b, s, l, d_m)
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
class BFactorModule(nn.Module):
|
| 590 |
-
"""Predict a per-token B-factor histogram."""
|
| 591 |
-
|
| 592 |
-
def __init__(self, token_s: int, num_bins: int) -> None:
|
| 593 |
-
super().__init__()
|
| 594 |
-
self.bfactor = nn.Linear(token_s, num_bins)
|
| 595 |
-
self.num_bins = num_bins
|
| 596 |
-
|
| 597 |
-
def forward(self, s: Tensor) -> Tensor:
|
| 598 |
-
# s: (..., d_s).
|
| 599 |
-
return cast(Tensor, self.bfactor(s)) # (..., n_bin)
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
class DistogramModule(nn.Module):
|
| 603 |
-
"""Predict symmetric residue-pair distance histograms."""
|
| 604 |
-
|
| 605 |
-
def __init__(self, token_z: int, num_bins: int, num_distograms: int = 1) -> None:
|
| 606 |
-
super().__init__()
|
| 607 |
-
self.distogram = nn.Linear(token_z, num_distograms * num_bins)
|
| 608 |
-
self.num_distograms = num_distograms
|
| 609 |
-
self.num_bins = num_bins
|
| 610 |
-
|
| 611 |
-
def forward(self, z: Tensor) -> Tensor:
|
| 612 |
-
# z: (b, l, l, d_z).
|
| 613 |
-
symmetric = z + z.transpose(1, 2) # (b, l, l, d_z)
|
| 614 |
-
logits = self.distogram(symmetric) # (b, l, l, n_distogram * n_bin)
|
| 615 |
-
return cast(
|
| 616 |
-
Tensor,
|
| 617 |
-
logits.reshape(
|
| 618 |
-
symmetric.shape[0],
|
| 619 |
-
symmetric.shape[1],
|
| 620 |
-
symmetric.shape[2],
|
| 621 |
-
self.num_distograms,
|
| 622 |
-
self.num_bins,
|
| 623 |
-
),
|
| 624 |
-
) # (b, l, l, n_distogram, n_bin)
|
|
|
|
| 1 |
+
"""Input, template, MSA, and prediction-head modules for the Boltz2 trunk."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from typing import Any, cast
|
| 8 |
+
from torch import Tensor, nn
|
| 9 |
+
from torch.nn import functional as F
|
| 10 |
+
from torch.utils.checkpoint import checkpoint
|
| 11 |
+
|
| 12 |
+
from . import vb_const as const
|
| 13 |
+
from .vb_layers_dropout import get_dropout_mask
|
| 14 |
+
from .vb_layers_outer_product_mean import OuterProductMean
|
| 15 |
+
from .vb_layers_pair_averaging import PairWeightedAveraging
|
| 16 |
+
from .vb_layers_pairformer import PairformerNoSeqLayer, PairformerNoSeqModule
|
| 17 |
+
from .vb_layers_transition import Transition
|
| 18 |
+
from .vb_modules_encodersv2 import (
|
| 19 |
+
AtomAttentionEncoder,
|
| 20 |
+
AtomEncoder,
|
| 21 |
+
FourierEmbedding,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ContactConditioning(nn.Module):
|
| 26 |
+
"""Encode selected, unselected, and distance-threshold contact constraints."""
|
| 27 |
+
|
| 28 |
+
def __init__(self, token_z: int, cutoff_min: float, cutoff_max: float) -> None:
|
| 29 |
+
super().__init__()
|
| 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)) # (d_z,)
|
| 34 |
+
self.encoding_unselected = nn.Parameter(torch.zeros(token_z)) # (d_z,)
|
| 35 |
+
self.cutoff_min = cutoff_min
|
| 36 |
+
self.cutoff_max = cutoff_max
|
| 37 |
+
|
| 38 |
+
def forward(self, feats: dict[str, Tensor]) -> Tensor:
|
| 39 |
+
"""Return contact tensor C with shape ``(b, l, l, d_z)``."""
|
| 40 |
+
|
| 41 |
+
if const.contact_conditioning_info["UNSPECIFIED"] != 0:
|
| 42 |
+
raise ValueError("UNSPECIFIED contact conditioning must use channel zero")
|
| 43 |
+
if const.contact_conditioning_info["UNSELECTED"] != 1:
|
| 44 |
+
raise ValueError("UNSELECTED contact conditioning must use channel one")
|
| 45 |
+
|
| 46 |
+
# c_contact is the number of contact-conditioning categories.
|
| 47 |
+
categories = feats["contact_conditioning"] # (b, l, l, c_contact)
|
| 48 |
+
threshold = feats["contact_threshold"] # (b, l, l)
|
| 49 |
+
# (b, l, l)
|
| 50 |
+
normalized = (threshold - self.cutoff_min) / (self.cutoff_max - self.cutoff_min)
|
| 51 |
+
# (b, l, l, d_z)
|
| 52 |
+
fourier = self.fourier_embedding(normalized.flatten()).reshape((*normalized.shape, -1))
|
| 53 |
+
selected_features = torch.cat( # (b, l, l, c_contact - 1 + d_z)
|
| 54 |
+
[categories[..., 2:], normalized.unsqueeze(-1), fourier],
|
| 55 |
+
dim=-1,
|
| 56 |
+
)
|
| 57 |
+
selected = self.encoder(selected_features) # (b, l, l, d_z)
|
| 58 |
+
special = categories[..., :2] # (b, l, l, 2)
|
| 59 |
+
return cast(
|
| 60 |
+
Tensor,
|
| 61 |
+
selected * (1 - special.sum(dim=-1, keepdim=True))
|
| 62 |
+
+ self.encoding_unspecified * special[..., 0:1]
|
| 63 |
+
+ self.encoding_unselected * special[..., 1:2],
|
| 64 |
+
) # (b, l, l, d_z)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class InputEmbedder(nn.Module):
|
| 68 |
+
"""Combine atom, residue, profile, and optional experimental features."""
|
| 69 |
+
|
| 70 |
+
def __init__(
|
| 71 |
+
self,
|
| 72 |
+
atom_s: int,
|
| 73 |
+
atom_z: int,
|
| 74 |
+
token_s: int,
|
| 75 |
+
token_z: int,
|
| 76 |
+
atoms_per_window_queries: int,
|
| 77 |
+
atoms_per_window_keys: int,
|
| 78 |
+
atom_feature_dim: int,
|
| 79 |
+
atom_encoder_depth: int,
|
| 80 |
+
atom_encoder_heads: int,
|
| 81 |
+
activation_checkpointing: bool = False,
|
| 82 |
+
add_method_conditioning: bool = False,
|
| 83 |
+
add_modified_flag: bool = False,
|
| 84 |
+
add_cyclic_flag: bool = False,
|
| 85 |
+
add_mol_type_feat: bool = False,
|
| 86 |
+
use_no_atom_char: bool = False,
|
| 87 |
+
use_atom_backbone_feat: bool = False,
|
| 88 |
+
use_residue_feats_atoms: bool = False,
|
| 89 |
+
) -> None:
|
| 90 |
+
super().__init__()
|
| 91 |
+
self.token_s = token_s
|
| 92 |
+
self.add_method_conditioning = add_method_conditioning
|
| 93 |
+
self.add_modified_flag = add_modified_flag
|
| 94 |
+
self.add_cyclic_flag = add_cyclic_flag
|
| 95 |
+
self.add_mol_type_feat = add_mol_type_feat
|
| 96 |
+
self.atom_encoder = AtomEncoder(
|
| 97 |
+
atom_s=atom_s,
|
| 98 |
+
atom_z=atom_z,
|
| 99 |
+
token_s=token_s,
|
| 100 |
+
token_z=token_z,
|
| 101 |
+
atoms_per_window_queries=atoms_per_window_queries,
|
| 102 |
+
atoms_per_window_keys=atoms_per_window_keys,
|
| 103 |
+
atom_feature_dim=atom_feature_dim,
|
| 104 |
+
structure_prediction=False,
|
| 105 |
+
use_no_atom_char=use_no_atom_char,
|
| 106 |
+
use_atom_backbone_feat=use_atom_backbone_feat,
|
| 107 |
+
use_residue_feats_atoms=use_residue_feats_atoms,
|
| 108 |
+
)
|
| 109 |
+
self.atom_enc_proj_z = nn.Sequential(
|
| 110 |
+
nn.LayerNorm(atom_z),
|
| 111 |
+
nn.Linear(atom_z, atom_encoder_depth * atom_encoder_heads, bias=False),
|
| 112 |
+
)
|
| 113 |
+
self.atom_attention_encoder = AtomAttentionEncoder(
|
| 114 |
+
atom_s=atom_s,
|
| 115 |
+
token_s=token_s,
|
| 116 |
+
atoms_per_window_queries=atoms_per_window_queries,
|
| 117 |
+
atoms_per_window_keys=atoms_per_window_keys,
|
| 118 |
+
atom_encoder_depth=atom_encoder_depth,
|
| 119 |
+
atom_encoder_heads=atom_encoder_heads,
|
| 120 |
+
structure_prediction=False,
|
| 121 |
+
activation_checkpointing=activation_checkpointing,
|
| 122 |
+
)
|
| 123 |
+
self.res_type_encoding = nn.Linear(const.num_tokens, token_s, bias=False)
|
| 124 |
+
self.msa_profile_encoding = nn.Linear(const.num_tokens + 1, token_s, bias=False)
|
| 125 |
+
|
| 126 |
+
if add_method_conditioning:
|
| 127 |
+
self.method_conditioning_init = nn.Embedding(const.num_method_types, token_s)
|
| 128 |
+
self.method_conditioning_init.weight.data.fill_(0) # (n_method, d_s)
|
| 129 |
+
if add_modified_flag:
|
| 130 |
+
self.modified_conditioning_init = nn.Embedding(2, token_s)
|
| 131 |
+
self.modified_conditioning_init.weight.data.fill_(0) # (2, d_s)
|
| 132 |
+
if add_cyclic_flag:
|
| 133 |
+
self.cyclic_conditioning_init = nn.Linear(1, token_s, bias=False)
|
| 134 |
+
self.cyclic_conditioning_init.weight.data.fill_(0) # (d_s, 1)
|
| 135 |
+
if add_mol_type_feat:
|
| 136 |
+
self.mol_type_conditioning_init = nn.Embedding(
|
| 137 |
+
len(const.chain_type_ids),
|
| 138 |
+
token_s,
|
| 139 |
+
)
|
| 140 |
+
self.mol_type_conditioning_init.weight.data.fill_(0) # (n_mol_type, d_s)
|
| 141 |
+
|
| 142 |
+
def forward(self, feats: dict[str, Tensor], affinity: bool = False) -> Tensor:
|
| 143 |
+
"""Return embedded sequence tensor S with shape ``(b, l, d_s)``."""
|
| 144 |
+
|
| 145 |
+
# n_atom is the padded atom count; k is the number of atom windows.
|
| 146 |
+
residue_type = feats["res_type"].float() # (b, l, n_token_type)
|
| 147 |
+
suffix = "_affinity" if affinity else ""
|
| 148 |
+
profile = feats[f"profile{suffix}"] # (b, l, n_token_type)
|
| 149 |
+
deletion_mean = feats[f"deletion_mean{suffix}"].unsqueeze(-1) # (b, l, 1)
|
| 150 |
+
# (b, n_atom, d_a), (b, n_atom, d_a), (b, k, w_q, w_k, d_az), callable
|
| 151 |
+
atom_queries, atom_conditioning, atom_pairs, to_keys = self.atom_encoder(feats)
|
| 152 |
+
atom_bias = self.atom_enc_proj_z(atom_pairs) # (b, k, w_q, w_k, n_layer * h)
|
| 153 |
+
atom_output, _, _, _ = self.atom_attention_encoder(
|
| 154 |
+
feats=feats,
|
| 155 |
+
q=atom_queries,
|
| 156 |
+
c=atom_conditioning,
|
| 157 |
+
atom_enc_bias=atom_bias,
|
| 158 |
+
to_keys=to_keys,
|
| 159 |
+
) # (b, l, d_s), (b, n_atom, d_a), (b, n_atom, d_a), callable
|
| 160 |
+
output = ( # (b, l, d_s)
|
| 161 |
+
atom_output
|
| 162 |
+
+ self.res_type_encoding(residue_type)
|
| 163 |
+
+ self.msa_profile_encoding(torch.cat([profile, deletion_mean], dim=-1))
|
| 164 |
+
)
|
| 165 |
+
if self.add_method_conditioning:
|
| 166 |
+
# method_feature: (b, l); output: (b, l, d_s).
|
| 167 |
+
output = output + self.method_conditioning_init(feats["method_feature"])
|
| 168 |
+
if self.add_modified_flag:
|
| 169 |
+
# modified: (b, l); output: (b, l, d_s).
|
| 170 |
+
output = output + self.modified_conditioning_init(feats["modified"])
|
| 171 |
+
if self.add_cyclic_flag:
|
| 172 |
+
cyclic = feats["cyclic_period"].clamp(max=1.0).unsqueeze(-1) # (b, l, 1)
|
| 173 |
+
output = output + self.cyclic_conditioning_init(cyclic) # (b, l, d_s)
|
| 174 |
+
if self.add_mol_type_feat:
|
| 175 |
+
# mol_type: (b, l); output: (b, l, d_s).
|
| 176 |
+
output = output + self.mol_type_conditioning_init(feats["mol_type"])
|
| 177 |
+
return cast(Tensor, output) # (b, l, d_s)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class _TemplateBase(nn.Module):
|
| 181 |
+
def __init__(
|
| 182 |
+
self,
|
| 183 |
+
token_z: int,
|
| 184 |
+
template_dim: int,
|
| 185 |
+
template_blocks: int,
|
| 186 |
+
dropout: float,
|
| 187 |
+
pairwise_head_width: int,
|
| 188 |
+
pairwise_num_heads: int,
|
| 189 |
+
post_layer_norm: bool,
|
| 190 |
+
activation_checkpointing: bool,
|
| 191 |
+
min_dist: float,
|
| 192 |
+
max_dist: float,
|
| 193 |
+
num_bins: int,
|
| 194 |
+
) -> None:
|
| 195 |
+
super().__init__()
|
| 196 |
+
self.min_dist = min_dist
|
| 197 |
+
self.max_dist = max_dist
|
| 198 |
+
self.num_bins = num_bins
|
| 199 |
+
self.relu = nn.ReLU()
|
| 200 |
+
self.z_norm = nn.LayerNorm(token_z)
|
| 201 |
+
self.v_norm = nn.LayerNorm(template_dim)
|
| 202 |
+
self.z_proj = nn.Linear(token_z, template_dim, bias=False)
|
| 203 |
+
feature_width = const.num_tokens * 2 + num_bins + 5
|
| 204 |
+
self.a_proj = nn.Linear(feature_width, template_dim, bias=False)
|
| 205 |
+
self.u_proj = nn.Linear(template_dim, token_z, bias=False)
|
| 206 |
+
self.pairformer = PairformerNoSeqModule(
|
| 207 |
+
template_dim,
|
| 208 |
+
num_blocks=template_blocks,
|
| 209 |
+
dropout=dropout,
|
| 210 |
+
pairwise_head_width=pairwise_head_width,
|
| 211 |
+
pairwise_num_heads=pairwise_num_heads,
|
| 212 |
+
post_layer_norm=post_layer_norm,
|
| 213 |
+
activation_checkpointing=activation_checkpointing,
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
|
| 217 |
+
raise NotImplementedError
|
| 218 |
+
|
| 219 |
+
def _template_features(
|
| 220 |
+
self,
|
| 221 |
+
feats: dict[str, Tensor],
|
| 222 |
+
template_pair_mask: Tensor,
|
| 223 |
+
) -> Tensor:
|
| 224 |
+
# residue_type: (b, t, l, n_token_type); template_pair_mask: (b, t, l, l).
|
| 225 |
+
residue_type = feats["template_restype"] # (b, t, l, n_token_type)
|
| 226 |
+
cb_mask = feats["template_mask_cb"] # (b, t, l)
|
| 227 |
+
frame_mask = feats["template_mask_frame"] # (b, t, l)
|
| 228 |
+
# (b, t, l, l, 1)
|
| 229 |
+
cb_pair_mask = (cb_mask[..., :, None] * cb_mask[..., None, :]).unsqueeze(-1)
|
| 230 |
+
# (b, t, l, l, 1)
|
| 231 |
+
frame_pair_mask = (frame_mask[..., :, None] * frame_mask[..., None, :]).unsqueeze(-1)
|
| 232 |
+
with torch.autocast(device_type="cuda", enabled=False):
|
| 233 |
+
# template_cb: (b, t, l, 3).
|
| 234 |
+
# (b, t, l, l)
|
| 235 |
+
cb_distances = torch.cdist(feats["template_cb"], feats["template_cb"])
|
| 236 |
+
boundaries = torch.linspace( # (n_bin - 1,)
|
| 237 |
+
self.min_dist,
|
| 238 |
+
self.max_dist,
|
| 239 |
+
self.num_bins - 1,
|
| 240 |
+
).to(cb_distances.device)
|
| 241 |
+
bins = (cb_distances[..., None] > boundaries).sum(dim=-1).long() # (b, t, l, l)
|
| 242 |
+
distogram = F.one_hot(bins, num_classes=self.num_bins) # (b, t, l, l, n_bin)
|
| 243 |
+
|
| 244 |
+
# (b, t, 1, l, 3, 3)
|
| 245 |
+
rotations = feats["template_frame_rot"].unsqueeze(2).transpose(-1, -2)
|
| 246 |
+
# (b, t, 1, l, 3, 1)
|
| 247 |
+
translations = feats["template_frame_t"].unsqueeze(2).unsqueeze(-1)
|
| 248 |
+
# (b, t, l, 1, 3, 1)
|
| 249 |
+
ca_coordinates = feats["template_ca"].unsqueeze(3).unsqueeze(-1)
|
| 250 |
+
# (b, t, l, l, 3, 1)
|
| 251 |
+
vectors = torch.matmul(rotations, ca_coordinates - translations)
|
| 252 |
+
norms = torch.norm(vectors, dim=-1, keepdim=True) # (b, t, l, l, 3, 1)
|
| 253 |
+
unit_vectors = torch.where( # (b, t, l, l, 3)
|
| 254 |
+
norms > 0,
|
| 255 |
+
vectors / norms,
|
| 256 |
+
torch.zeros_like(vectors),
|
| 257 |
+
).squeeze(-1)
|
| 258 |
+
pair_features = torch.cat( # (b, t, l, l, n_bin + 5)
|
| 259 |
+
[distogram, cb_pair_mask, unit_vectors, frame_pair_mask],
|
| 260 |
+
dim=-1,
|
| 261 |
+
)
|
| 262 |
+
# (b, t, l, l, n_bin + 5)
|
| 263 |
+
pair_features = pair_features * template_pair_mask.unsqueeze(-1)
|
| 264 |
+
residue_i = residue_type[:, :, :, None].expand( # (b, t, l, l, n_token_type)
|
| 265 |
+
-1,
|
| 266 |
+
-1,
|
| 267 |
+
-1,
|
| 268 |
+
residue_type.size(2),
|
| 269 |
+
-1,
|
| 270 |
+
)
|
| 271 |
+
residue_j = residue_type[:, :, None, :].expand( # (b, t, l, l, n_token_type)
|
| 272 |
+
-1,
|
| 273 |
+
-1,
|
| 274 |
+
residue_type.size(2),
|
| 275 |
+
-1,
|
| 276 |
+
-1,
|
| 277 |
+
)
|
| 278 |
+
return cast(
|
| 279 |
+
Tensor,
|
| 280 |
+
self.a_proj(torch.cat([pair_features, residue_i, residue_j], dim=-1)),
|
| 281 |
+
) # (b, t, l, l, d_t)
|
| 282 |
+
|
| 283 |
+
def forward(
|
| 284 |
+
self,
|
| 285 |
+
z: Tensor,
|
| 286 |
+
feats: dict[str, Tensor],
|
| 287 |
+
pair_mask: Tensor,
|
| 288 |
+
use_kernels: bool = False,
|
| 289 |
+
) -> Tensor:
|
| 290 |
+
"""Aggregate template pair tensor V into trunk update U."""
|
| 291 |
+
|
| 292 |
+
# z: (b, l, l, d_z); pair_mask: (b, l, l).
|
| 293 |
+
residue_type = feats["template_restype"] # (b, t, l, n_token_type)
|
| 294 |
+
batch_size, template_count = residue_type.shape[:2]
|
| 295 |
+
template_present = feats["template_mask"].any(dim=2).float() # (b, t)
|
| 296 |
+
present_count = template_present.sum(dim=1).clamp(min=1) # (b,)
|
| 297 |
+
features = self._template_features( # (b, t, l, l, d_t)
|
| 298 |
+
feats,
|
| 299 |
+
self._template_pair_mask(feats, template_count),
|
| 300 |
+
)
|
| 301 |
+
expanded_mask = pair_mask[:, None].expand(-1, template_count, -1, -1) # (b, t, l, l)
|
| 302 |
+
expanded_mask = expanded_mask.reshape( # (b * t, l, l)
|
| 303 |
+
batch_size * template_count,
|
| 304 |
+
*expanded_mask.shape[2:],
|
| 305 |
+
)
|
| 306 |
+
template_states = self.z_proj(self.z_norm(z[:, None])) + features # (b, t, l, l, d_t)
|
| 307 |
+
template_states = template_states.view( # (b * t, l, l, d_t)
|
| 308 |
+
batch_size * template_count,
|
| 309 |
+
*template_states.shape[2:],
|
| 310 |
+
)
|
| 311 |
+
template_states = template_states + self.pairformer( # (b * t, l, l, d_t)
|
| 312 |
+
template_states,
|
| 313 |
+
expanded_mask,
|
| 314 |
+
use_kernels=use_kernels,
|
| 315 |
+
)
|
| 316 |
+
template_states = self.v_norm(template_states).view( # (b, t, l, l, d_t)
|
| 317 |
+
batch_size,
|
| 318 |
+
template_count,
|
| 319 |
+
*template_states.shape[1:],
|
| 320 |
+
)
|
| 321 |
+
weights = template_present[:, :, None, None, None] # (b, t, 1, 1, 1)
|
| 322 |
+
aggregate = (template_states * weights).sum(dim=1) # (b, l, l, d_t)
|
| 323 |
+
# (b, l, l, d_t)
|
| 324 |
+
aggregate = aggregate / present_count[:, None, None, None].to(template_states)
|
| 325 |
+
return cast(Tensor, self.u_proj(self.relu(aggregate))) # (b, l, l, d_z)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
class TemplateModule(_TemplateBase):
|
| 329 |
+
"""Aggregate templates while restricting features to the same chain."""
|
| 330 |
+
|
| 331 |
+
def __init__(
|
| 332 |
+
self,
|
| 333 |
+
token_z: int,
|
| 334 |
+
template_dim: int,
|
| 335 |
+
template_blocks: int,
|
| 336 |
+
dropout: float = 0.25,
|
| 337 |
+
pairwise_head_width: int = 32,
|
| 338 |
+
pairwise_num_heads: int = 4,
|
| 339 |
+
post_layer_norm: bool = False,
|
| 340 |
+
activation_checkpointing: bool = False,
|
| 341 |
+
min_dist: float = 3.25,
|
| 342 |
+
max_dist: float = 50.75,
|
| 343 |
+
num_bins: int = 38,
|
| 344 |
+
**kwargs: Any,
|
| 345 |
+
) -> None:
|
| 346 |
+
del kwargs
|
| 347 |
+
super().__init__(
|
| 348 |
+
token_z,
|
| 349 |
+
template_dim,
|
| 350 |
+
template_blocks,
|
| 351 |
+
dropout,
|
| 352 |
+
pairwise_head_width,
|
| 353 |
+
pairwise_num_heads,
|
| 354 |
+
post_layer_norm,
|
| 355 |
+
activation_checkpointing,
|
| 356 |
+
min_dist,
|
| 357 |
+
max_dist,
|
| 358 |
+
num_bins,
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
|
| 362 |
+
asym_id = feats["asym_id"] # (b, l)
|
| 363 |
+
same_chain = (asym_id[:, :, None] == asym_id[:, None, :]).float() # (b, l, l)
|
| 364 |
+
return same_chain[:, None].expand(-1, count, -1, -1) # (b, t, l, l)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
class TemplateV2Module(_TemplateBase):
|
| 368 |
+
"""Aggregate templates under per-template visibility groups."""
|
| 369 |
+
|
| 370 |
+
def __init__(
|
| 371 |
+
self,
|
| 372 |
+
token_z: int,
|
| 373 |
+
template_dim: int,
|
| 374 |
+
template_blocks: int,
|
| 375 |
+
dropout: float = 0.25,
|
| 376 |
+
pairwise_head_width: int = 32,
|
| 377 |
+
pairwise_num_heads: int = 4,
|
| 378 |
+
post_layer_norm: bool = False,
|
| 379 |
+
activation_checkpointing: bool = False,
|
| 380 |
+
min_dist: float = 3.25,
|
| 381 |
+
max_dist: float = 50.75,
|
| 382 |
+
num_bins: int = 38,
|
| 383 |
+
**kwargs: Any,
|
| 384 |
+
) -> None:
|
| 385 |
+
del kwargs
|
| 386 |
+
super().__init__(
|
| 387 |
+
token_z,
|
| 388 |
+
template_dim,
|
| 389 |
+
template_blocks,
|
| 390 |
+
dropout,
|
| 391 |
+
pairwise_head_width,
|
| 392 |
+
pairwise_num_heads,
|
| 393 |
+
post_layer_norm,
|
| 394 |
+
activation_checkpointing,
|
| 395 |
+
min_dist,
|
| 396 |
+
max_dist,
|
| 397 |
+
num_bins,
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
|
| 401 |
+
del count
|
| 402 |
+
visibility = feats["visibility_ids"] # (b, t, l)
|
| 403 |
+
return (visibility[..., :, None] == visibility[..., None, :]).float() # (b, t, l, l)
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
class MSAModule(nn.Module):
|
| 407 |
+
"""Embed and update an MSA before returning its accumulated pair update."""
|
| 408 |
+
|
| 409 |
+
def __init__(
|
| 410 |
+
self,
|
| 411 |
+
msa_s: int,
|
| 412 |
+
token_z: int,
|
| 413 |
+
token_s: int,
|
| 414 |
+
msa_blocks: int,
|
| 415 |
+
msa_dropout: float,
|
| 416 |
+
z_dropout: float,
|
| 417 |
+
pairwise_head_width: int = 32,
|
| 418 |
+
pairwise_num_heads: int = 4,
|
| 419 |
+
activation_checkpointing: bool = False,
|
| 420 |
+
use_paired_feature: bool = True,
|
| 421 |
+
subsample_msa: bool = False,
|
| 422 |
+
num_subsampled_msa: int = 1024,
|
| 423 |
+
**kwargs: Any,
|
| 424 |
+
) -> None:
|
| 425 |
+
del kwargs
|
| 426 |
+
super().__init__()
|
| 427 |
+
self.msa_blocks = msa_blocks
|
| 428 |
+
self.msa_dropout = msa_dropout
|
| 429 |
+
self.z_dropout = z_dropout
|
| 430 |
+
self.use_paired_feature = use_paired_feature
|
| 431 |
+
self.activation_checkpointing = activation_checkpointing
|
| 432 |
+
self.subsample_msa = subsample_msa
|
| 433 |
+
self.num_subsampled_msa = num_subsampled_msa
|
| 434 |
+
self.s_proj = nn.Linear(token_s, msa_s, bias=False)
|
| 435 |
+
input_width = const.num_tokens + 2 + int(use_paired_feature)
|
| 436 |
+
self.msa_proj = nn.Linear(input_width, msa_s, bias=False)
|
| 437 |
+
self.layers = nn.ModuleList(
|
| 438 |
+
[
|
| 439 |
+
MSALayer(
|
| 440 |
+
msa_s,
|
| 441 |
+
token_z,
|
| 442 |
+
msa_dropout,
|
| 443 |
+
z_dropout,
|
| 444 |
+
pairwise_head_width,
|
| 445 |
+
pairwise_num_heads,
|
| 446 |
+
)
|
| 447 |
+
for _ in range(msa_blocks)
|
| 448 |
+
]
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
@staticmethod
|
| 452 |
+
def _chunk_configuration(
|
| 453 |
+
pair_states: Tensor,
|
| 454 |
+
training: bool,
|
| 455 |
+
) -> tuple[bool, int | None, int | None, int | None, int | None]:
|
| 456 |
+
# pair_states: (b, l, l, d_z).
|
| 457 |
+
if training:
|
| 458 |
+
return False, None, None, None, None
|
| 459 |
+
if pair_states.shape[1] > const.chunk_size_threshold:
|
| 460 |
+
return True, 64, 32, 4, 128
|
| 461 |
+
return False, None, None, None, 512
|
| 462 |
+
|
| 463 |
+
def forward(
|
| 464 |
+
self,
|
| 465 |
+
z: Tensor,
|
| 466 |
+
emb: Tensor,
|
| 467 |
+
feats: dict[str, Tensor],
|
| 468 |
+
use_kernels: bool = False,
|
| 469 |
+
) -> Tensor:
|
| 470 |
+
"""Return updated pair tensor Z after every MSA block."""
|
| 471 |
+
|
| 472 |
+
chunking = self._chunk_configuration(z, self.training)
|
| 473 |
+
# z: (b, l, l, d_z); emb: (b, l, d_s).
|
| 474 |
+
# s is MSA depth; n_token_type is the residue vocabulary size.
|
| 475 |
+
msa = feats["msa"] # (b, s, l) or (b, s, l, n_token_type)
|
| 476 |
+
if msa.dtype in (torch.long, torch.int32, torch.int64):
|
| 477 |
+
msa = F.one_hot(msa, num_classes=const.num_tokens).float() # (b, s, l, n_token_type)
|
| 478 |
+
msa_mask = feats["msa_mask"] # (b, s, l)
|
| 479 |
+
components = [ # (b, s, l, n_token_type), then two (b, s, l, 1) tensors
|
| 480 |
+
msa,
|
| 481 |
+
feats["has_deletion"].unsqueeze(-1), # (b, s, l, 1)
|
| 482 |
+
feats["deletion_value"].unsqueeze(-1), # (b, s, l, 1)
|
| 483 |
+
]
|
| 484 |
+
if self.use_paired_feature:
|
| 485 |
+
components.append(feats["msa_paired"].unsqueeze(-1)) # (b, s, l, 1)
|
| 486 |
+
msa_input = torch.cat(components, dim=-1) # (b, s, l, n_token_type + 2 or 3)
|
| 487 |
+
if self.subsample_msa:
|
| 488 |
+
indices = torch.randperm(msa.shape[1])[: self.num_subsampled_msa] # (s_sub,)
|
| 489 |
+
msa_input = msa_input[:, indices] # (b, s_sub, l, n_token_type + 2 or 3)
|
| 490 |
+
msa_mask = msa_mask[:, indices] # (b, s_sub, l)
|
| 491 |
+
|
| 492 |
+
msa_states = self.msa_proj(msa_input) + self.s_proj(emb).unsqueeze(1) # (b, s, l, d_m)
|
| 493 |
+
token_mask = feats["token_pad_mask"].float() # (b, l)
|
| 494 |
+
pair_mask = token_mask[:, :, None] * token_mask[:, None, :] # (b, l, l)
|
| 495 |
+
pair_states = z # (b, l, l, d_z)
|
| 496 |
+
for layer in self.layers:
|
| 497 |
+
# Tensor arguments: pair_states (b, l, l, d_z), msa_states (b, s, l, d_m),
|
| 498 |
+
# pair_mask (b, l, l), msa_mask (b, s, l).
|
| 499 |
+
arguments = (
|
| 500 |
+
pair_states,
|
| 501 |
+
msa_states,
|
| 502 |
+
pair_mask,
|
| 503 |
+
msa_mask,
|
| 504 |
+
*chunking,
|
| 505 |
+
use_kernels,
|
| 506 |
+
)
|
| 507 |
+
if self.activation_checkpointing and self.training:
|
| 508 |
+
pair_states, msa_states = checkpoint( # (b, l, l, d_z), (b, s, l, d_m)
|
| 509 |
+
layer,
|
| 510 |
+
*arguments,
|
| 511 |
+
)
|
| 512 |
+
else:
|
| 513 |
+
pair_states, msa_states = layer(*arguments) # (b, l, l, d_z), (b, s, l, d_m)
|
| 514 |
+
return pair_states # (b, l, l, d_z)
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
class MSALayer(nn.Module):
|
| 518 |
+
"""Exchange information between MSA tensor M and pair tensor Z."""
|
| 519 |
+
|
| 520 |
+
def __init__(
|
| 521 |
+
self,
|
| 522 |
+
msa_s: int,
|
| 523 |
+
token_z: int,
|
| 524 |
+
msa_dropout: float,
|
| 525 |
+
z_dropout: float,
|
| 526 |
+
pairwise_head_width: int = 32,
|
| 527 |
+
pairwise_num_heads: int = 4,
|
| 528 |
+
) -> None:
|
| 529 |
+
super().__init__()
|
| 530 |
+
self.msa_dropout = msa_dropout
|
| 531 |
+
self.msa_transition = Transition(msa_s, msa_s * 4)
|
| 532 |
+
self.pair_weighted_averaging = PairWeightedAveraging(
|
| 533 |
+
c_m=msa_s,
|
| 534 |
+
c_z=token_z,
|
| 535 |
+
c_h=32,
|
| 536 |
+
num_heads=8,
|
| 537 |
+
)
|
| 538 |
+
self.pairformer_layer = PairformerNoSeqLayer(
|
| 539 |
+
token_z=token_z,
|
| 540 |
+
dropout=z_dropout,
|
| 541 |
+
pairwise_head_width=pairwise_head_width,
|
| 542 |
+
pairwise_num_heads=pairwise_num_heads,
|
| 543 |
+
)
|
| 544 |
+
self.outer_product_mean = OuterProductMean(msa_s, 32, token_z)
|
| 545 |
+
|
| 546 |
+
def forward(
|
| 547 |
+
self,
|
| 548 |
+
z: Tensor,
|
| 549 |
+
m: Tensor,
|
| 550 |
+
token_mask: Tensor,
|
| 551 |
+
msa_mask: Tensor,
|
| 552 |
+
chunk_heads_pwa: bool = False,
|
| 553 |
+
chunk_size_transition_z: int | None = None,
|
| 554 |
+
chunk_size_transition_msa: int | None = None,
|
| 555 |
+
chunk_size_outer_product: int | None = None,
|
| 556 |
+
chunk_size_tri_attn: int | None = None,
|
| 557 |
+
use_kernels: bool = False,
|
| 558 |
+
) -> tuple[Tensor, Tensor]:
|
| 559 |
+
"""Return updated Z and M tensors."""
|
| 560 |
+
|
| 561 |
+
del chunk_size_transition_z
|
| 562 |
+
# z: (b, l, l, d_z); m: (b, s, l, d_m).
|
| 563 |
+
# token_mask: (b, l, l); msa_mask: (b, s, l).
|
| 564 |
+
dropout = get_dropout_mask(self.msa_dropout, m, self.training) # (b, s, 1, 1)
|
| 565 |
+
msa_states = m + dropout * self.pair_weighted_averaging( # (b, s, l, d_m)
|
| 566 |
+
m,
|
| 567 |
+
z,
|
| 568 |
+
token_mask,
|
| 569 |
+
chunk_heads_pwa,
|
| 570 |
+
)
|
| 571 |
+
msa_states = msa_states + self.msa_transition( # (b, s, l, d_m)
|
| 572 |
+
msa_states,
|
| 573 |
+
chunk_size_transition_msa,
|
| 574 |
+
)
|
| 575 |
+
pair_states = z + self.outer_product_mean( # (b, l, l, d_z)
|
| 576 |
+
msa_states,
|
| 577 |
+
msa_mask,
|
| 578 |
+
chunk_size_outer_product,
|
| 579 |
+
)
|
| 580 |
+
pair_states = self.pairformer_layer( # (b, l, l, d_z)
|
| 581 |
+
pair_states,
|
| 582 |
+
token_mask,
|
| 583 |
+
chunk_size_tri_attn,
|
| 584 |
+
use_kernels=use_kernels,
|
| 585 |
+
)
|
| 586 |
+
return pair_states, msa_states # (b, l, l, d_z), (b, s, l, d_m)
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
class BFactorModule(nn.Module):
|
| 590 |
+
"""Predict a per-token B-factor histogram."""
|
| 591 |
+
|
| 592 |
+
def __init__(self, token_s: int, num_bins: int) -> None:
|
| 593 |
+
super().__init__()
|
| 594 |
+
self.bfactor = nn.Linear(token_s, num_bins)
|
| 595 |
+
self.num_bins = num_bins
|
| 596 |
+
|
| 597 |
+
def forward(self, s: Tensor) -> Tensor:
|
| 598 |
+
# s: (..., d_s).
|
| 599 |
+
return cast(Tensor, self.bfactor(s)) # (..., n_bin)
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
class DistogramModule(nn.Module):
|
| 603 |
+
"""Predict symmetric residue-pair distance histograms."""
|
| 604 |
+
|
| 605 |
+
def __init__(self, token_z: int, num_bins: int, num_distograms: int = 1) -> None:
|
| 606 |
+
super().__init__()
|
| 607 |
+
self.distogram = nn.Linear(token_z, num_distograms * num_bins)
|
| 608 |
+
self.num_distograms = num_distograms
|
| 609 |
+
self.num_bins = num_bins
|
| 610 |
+
|
| 611 |
+
def forward(self, z: Tensor) -> Tensor:
|
| 612 |
+
# z: (b, l, l, d_z).
|
| 613 |
+
symmetric = z + z.transpose(1, 2) # (b, l, l, d_z)
|
| 614 |
+
logits = self.distogram(symmetric) # (b, l, l, n_distogram * n_bin)
|
| 615 |
+
return cast(
|
| 616 |
+
Tensor,
|
| 617 |
+
logits.reshape(
|
| 618 |
+
symmetric.shape[0],
|
| 619 |
+
symmetric.shape[1],
|
| 620 |
+
symmetric.shape[2],
|
| 621 |
+
self.num_distograms,
|
| 622 |
+
self.num_bins,
|
| 623 |
+
),
|
| 624 |
+
) # (b, l, l, n_distogram, n_bin)
|
fastplms/models/boltz/vb_modules_utils.py
CHANGED
|
@@ -1,283 +1,283 @@
|
|
| 1 |
-
"""Small geometry, augmentation, activation, and EMA utilities for Boltz2.
|
| 2 |
-
|
| 3 |
-
The quaternion behavior follows PyTorch3D's public rotation convention. License
|
| 4 |
-
and attribution records are retained in ``THIRD_PARTY_NOTICES.md``.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
|
| 9 |
-
import torch
|
| 10 |
-
|
| 11 |
-
from collections.abc import Iterable, Mapping, Sequence
|
| 12 |
-
from functools import partial
|
| 13 |
-
from typing import Any
|
| 14 |
-
from torch import Tensor, nn
|
| 15 |
-
from torch.nn import functional as F
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
LinearNoBias = partial(nn.Linear, bias=False)
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def exists(value: object) -> bool:
|
| 22 |
-
"""Return whether a value is not ``None``."""
|
| 23 |
-
|
| 24 |
-
return value is not None
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def default(value: Any, fallback: Any) -> Any:
|
| 28 |
-
"""Return ``value`` unless it is ``None``."""
|
| 29 |
-
|
| 30 |
-
return fallback if value is None else value
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def log(tensor: Tensor, eps: float = 1e-20) -> Tensor:
|
| 34 |
-
"""Compute a finite logarithm by applying a scalar lower bound."""
|
| 35 |
-
|
| 36 |
-
# tensor: (...).
|
| 37 |
-
return torch.log(tensor.clamp(min=eps)) # (...)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
class SwiGLU(nn.Module):
|
| 41 |
-
"""Split X in half and apply a SiLU-gated linear unit."""
|
| 42 |
-
|
| 43 |
-
def forward(self, x: Tensor) -> Tensor:
|
| 44 |
-
# x: (..., 2 * d).
|
| 45 |
-
values, gates = x.chunk(2, dim=-1) # each: (..., d)
|
| 46 |
-
return F.silu(gates) * values # (..., d)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def _masked_center(coordinates: Tensor, mask: Tensor) -> Tensor:
|
| 50 |
-
# coordinates: (b, n_atom, 3); mask: (b, n_atom).
|
| 51 |
-
weights = mask[:, :, None] # (b, n_atom, 1)
|
| 52 |
-
# (b, 1, 3)
|
| 53 |
-
return (coordinates * weights).sum(dim=1, keepdim=True) / weights.sum(
|
| 54 |
-
dim=1,
|
| 55 |
-
keepdim=True,
|
| 56 |
-
)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def center(atom_coords: Tensor, atom_mask: Tensor) -> Tensor:
|
| 60 |
-
"""Center coordinate tensor X with shape ``(b, n_atoms, 3)``."""
|
| 61 |
-
|
| 62 |
-
# atom_coords: (b, n_atom, 3); atom_mask: (b, n_atom).
|
| 63 |
-
return atom_coords - _masked_center(atom_coords, atom_mask) # (b, n_atom, 3)
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def _copysign(magnitudes: Tensor, signs: Tensor) -> Tensor:
|
| 67 |
-
"""Apply the elementwise sign of S to magnitude tensor M."""
|
| 68 |
-
|
| 69 |
-
# magnitudes, signs: broadcast-compatible (...).
|
| 70 |
-
return torch.where((magnitudes < 0) != (signs < 0), -magnitudes, magnitudes) # (...)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def quaternion_to_matrix(quaternions: Tensor) -> Tensor:
|
| 74 |
-
"""Convert real-first quaternion tensor Q from ``(..., 4)`` to ``(..., 3, 3)``."""
|
| 75 |
-
|
| 76 |
-
# quaternions: (..., 4).
|
| 77 |
-
real, i_axis, j_axis, k_axis = torch.unbind(quaternions, dim=-1) # each: (...)
|
| 78 |
-
scale = 2.0 / (quaternions * quaternions).sum(dim=-1) # (...)
|
| 79 |
-
entries = ( # nine tensors, each (...)
|
| 80 |
-
1 - scale * (j_axis * j_axis + k_axis * k_axis),
|
| 81 |
-
scale * (i_axis * j_axis - k_axis * real),
|
| 82 |
-
scale * (i_axis * k_axis + j_axis * real),
|
| 83 |
-
scale * (i_axis * j_axis + k_axis * real),
|
| 84 |
-
1 - scale * (i_axis * i_axis + k_axis * k_axis),
|
| 85 |
-
scale * (j_axis * k_axis - i_axis * real),
|
| 86 |
-
scale * (i_axis * k_axis - j_axis * real),
|
| 87 |
-
scale * (j_axis * k_axis + i_axis * real),
|
| 88 |
-
1 - scale * (i_axis * i_axis + j_axis * j_axis),
|
| 89 |
-
)
|
| 90 |
-
# (..., 3, 3)
|
| 91 |
-
return torch.stack(entries, dim=-1).reshape((*quaternions.shape[:-1], 3, 3))
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def random_quaternions(
|
| 95 |
-
n: int,
|
| 96 |
-
dtype: torch.dtype | None = None,
|
| 97 |
-
device: torch.device | str | None = None,
|
| 98 |
-
) -> Tensor:
|
| 99 |
-
"""Draw ``n`` uniformly distributed unit quaternions with nonnegative real part."""
|
| 100 |
-
|
| 101 |
-
resolved_device = torch.device(device) if isinstance(device, str) else device
|
| 102 |
-
samples = torch.randn((n, 4), dtype=dtype, device=resolved_device) # (n, 4)
|
| 103 |
-
squared_norm = (samples * samples).sum(dim=1) # (n,)
|
| 104 |
-
signed_norm = _copysign(torch.sqrt(squared_norm), samples[:, 0]) # (n,)
|
| 105 |
-
return samples / signed_norm[:, None] # (n, 4)
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def random_rotations(
|
| 109 |
-
n: int,
|
| 110 |
-
dtype: torch.dtype | None = None,
|
| 111 |
-
device: torch.device | str | None = None,
|
| 112 |
-
) -> Tensor:
|
| 113 |
-
"""Draw rotation tensor R with shape ``(n, 3, 3)``."""
|
| 114 |
-
|
| 115 |
-
# (n, 3, 3)
|
| 116 |
-
return quaternion_to_matrix(random_quaternions(n, dtype=dtype, device=device))
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def compute_random_augmentation(
|
| 120 |
-
multiplicity: int,
|
| 121 |
-
s_trans: float = 1.0,
|
| 122 |
-
device: torch.device | str | None = None,
|
| 123 |
-
dtype: torch.dtype = torch.float32,
|
| 124 |
-
) -> tuple[Tensor, Tensor]:
|
| 125 |
-
"""Draw independent rotations R and translations T for a replicated batch."""
|
| 126 |
-
|
| 127 |
-
rotations = random_rotations(multiplicity, dtype=dtype, device=device) # (m, 3, 3)
|
| 128 |
-
translations = ( # (m, 1, 3)
|
| 129 |
-
torch.randn(
|
| 130 |
-
(multiplicity, 1, 3),
|
| 131 |
-
dtype=dtype,
|
| 132 |
-
device=device,
|
| 133 |
-
)
|
| 134 |
-
* s_trans
|
| 135 |
-
)
|
| 136 |
-
return rotations, translations # (m, 3, 3), (m, 1, 3)
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
def randomly_rotate(
|
| 140 |
-
coords: Tensor,
|
| 141 |
-
return_second_coords: bool = False,
|
| 142 |
-
second_coords: Tensor | None = None,
|
| 143 |
-
) -> Tensor | tuple[Tensor, Tensor | None]:
|
| 144 |
-
"""Rotate X and optionally Y using the same sampled rotation tensor R."""
|
| 145 |
-
|
| 146 |
-
# coords: (b, n, 3); second_coords: (b, n_second, 3) or None.
|
| 147 |
-
rotations = random_rotations(len(coords), coords.dtype, coords.device) # (b, 3, 3)
|
| 148 |
-
rotated = torch.einsum("bmd,bds->bms", coords, rotations) # (b, n, 3)
|
| 149 |
-
if not return_second_coords:
|
| 150 |
-
return rotated # (b, n, 3)
|
| 151 |
-
rotated_second = ( # (b, n_second, 3) or None
|
| 152 |
-
None if second_coords is None else torch.einsum("bmd,bds->bms", second_coords, rotations)
|
| 153 |
-
)
|
| 154 |
-
return rotated, rotated_second # (b, n, 3), (b, n_second, 3) or None
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
def center_random_augmentation(
|
| 158 |
-
atom_coords: Tensor,
|
| 159 |
-
atom_mask: Tensor,
|
| 160 |
-
s_trans: float = 1.0,
|
| 161 |
-
augmentation: bool = True,
|
| 162 |
-
centering: bool = True,
|
| 163 |
-
return_second_coords: bool = False,
|
| 164 |
-
second_coords: Tensor | None = None,
|
| 165 |
-
) -> Tensor | tuple[Tensor, Tensor | None]:
|
| 166 |
-
"""Center and rigidly augment coordinate tensors X and optional Y."""
|
| 167 |
-
|
| 168 |
-
# atom_coords: (b, n_atom, 3); atom_mask: (b, n_atom).
|
| 169 |
-
# second_coords: (b, n_second, 3) or None.
|
| 170 |
-
primary = atom_coords # (b, n_atom, 3)
|
| 171 |
-
secondary = second_coords # (b, n_second, 3) or None
|
| 172 |
-
if centering:
|
| 173 |
-
centroid = _masked_center(primary, atom_mask) # (b, 1, 3)
|
| 174 |
-
primary = primary - centroid # (b, n_atom, 3)
|
| 175 |
-
if secondary is not None:
|
| 176 |
-
secondary = secondary - centroid # (b, n_second, 3)
|
| 177 |
-
|
| 178 |
-
if augmentation:
|
| 179 |
-
primary, secondary = randomly_rotate( # (b, n_atom, 3), (b, n_second, 3) or None
|
| 180 |
-
primary,
|
| 181 |
-
return_second_coords=True,
|
| 182 |
-
second_coords=secondary,
|
| 183 |
-
)
|
| 184 |
-
translation = torch.randn_like(primary[:, 0:1, :]) * s_trans # (b, 1, 3)
|
| 185 |
-
primary = primary + translation # (b, n_atom, 3)
|
| 186 |
-
if secondary is not None:
|
| 187 |
-
secondary = secondary + translation # (b, n_second, 3)
|
| 188 |
-
|
| 189 |
-
# (b, n_atom, 3), optionally paired with (b, n_second, 3) or None.
|
| 190 |
-
return (primary, secondary) if return_second_coords else primary
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
class ExponentialMovingAverage:
|
| 194 |
-
"""Maintain detached exponential moving averages of trainable parameters."""
|
| 195 |
-
|
| 196 |
-
def __init__(
|
| 197 |
-
self,
|
| 198 |
-
parameters: Iterable[nn.Parameter],
|
| 199 |
-
decay: float,
|
| 200 |
-
use_num_updates: bool = True,
|
| 201 |
-
) -> None:
|
| 202 |
-
# parameters: one independently shaped tensor (...) per trainable parameter.
|
| 203 |
-
if not 0.0 <= decay <= 1.0:
|
| 204 |
-
raise ValueError("decay must lie in [0, 1]")
|
| 205 |
-
self.decay = decay
|
| 206 |
-
self.num_updates: int | None = 0 if use_num_updates else None
|
| 207 |
-
self.shadow_params = [ # each: same shape as one trainable parameter, (...)
|
| 208 |
-
parameter.clone().detach() for parameter in parameters if parameter.requires_grad
|
| 209 |
-
]
|
| 210 |
-
self.collected_params: list[Tensor] = []
|
| 211 |
-
|
| 212 |
-
@staticmethod
|
| 213 |
-
def _trainable(parameters: Iterable[nn.Parameter]) -> list[nn.Parameter]:
|
| 214 |
-
# Each item is one trainable parameter with an independently varying shape (...).
|
| 215 |
-
return [parameter for parameter in parameters if parameter.requires_grad]
|
| 216 |
-
|
| 217 |
-
def update(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 218 |
-
# parameters: one independently shaped tensor (...) per trainable parameter.
|
| 219 |
-
trainable = self._trainable(parameters) # each: one trainable parameter, (...)
|
| 220 |
-
if len(trainable) != len(self.shadow_params):
|
| 221 |
-
raise ValueError("EMA parameter count changed")
|
| 222 |
-
decay = self.decay
|
| 223 |
-
if self.num_updates is not None:
|
| 224 |
-
self.num_updates += 1
|
| 225 |
-
decay = min(decay, (1 + self.num_updates) / (10 + self.num_updates))
|
| 226 |
-
update_weight = 1.0 - decay
|
| 227 |
-
with torch.no_grad():
|
| 228 |
-
for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
|
| 229 |
-
shadow.sub_(update_weight * (shadow - parameter)) # (...) unchanged in place
|
| 230 |
-
|
| 231 |
-
def compatible(self, parameters: Sequence[Tensor]) -> bool:
|
| 232 |
-
"""Return whether parameter count and tensor shapes match the EMA state."""
|
| 233 |
-
|
| 234 |
-
# Each shadow and corresponding parameter has an independently varying shape (...).
|
| 235 |
-
return len(self.shadow_params) == len(parameters) and all(
|
| 236 |
-
shadow.shape == parameter.shape
|
| 237 |
-
for shadow, parameter in zip(self.shadow_params, parameters, strict=True)
|
| 238 |
-
)
|
| 239 |
-
|
| 240 |
-
def copy_to(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 241 |
-
# parameters: one independently shaped tensor (...) per trainable parameter.
|
| 242 |
-
trainable = self._trainable(parameters) # each: one trainable parameter, (...)
|
| 243 |
-
if len(trainable) != len(self.shadow_params):
|
| 244 |
-
raise ValueError("EMA parameter count changed")
|
| 245 |
-
for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
|
| 246 |
-
parameter.data.copy_(shadow.data) # (...) unchanged in place
|
| 247 |
-
|
| 248 |
-
def store(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 249 |
-
# parameters: one independently shaped tensor (...) per parameter.
|
| 250 |
-
# Each clone has the same shape (...) as its corresponding parameter.
|
| 251 |
-
self.collected_params = [parameter.clone() for parameter in parameters]
|
| 252 |
-
|
| 253 |
-
def restore(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 254 |
-
# parameters: one independently shaped tensor (...) per parameter.
|
| 255 |
-
current = list(parameters) # each: one parameter, (...)
|
| 256 |
-
if len(current) != len(self.collected_params):
|
| 257 |
-
raise ValueError("stored EMA parameter count changed")
|
| 258 |
-
for collected, parameter in zip(self.collected_params, current, strict=True):
|
| 259 |
-
parameter.data.copy_(collected.data) # (...) unchanged in place
|
| 260 |
-
|
| 261 |
-
def state_dict(self) -> dict[str, Any]:
|
| 262 |
-
# shadow_params contains independently shaped tensors (...).
|
| 263 |
-
return {
|
| 264 |
-
"decay": self.decay,
|
| 265 |
-
"num_updates": self.num_updates,
|
| 266 |
-
"shadow_params": self.shadow_params,
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
def load_state_dict(
|
| 270 |
-
self,
|
| 271 |
-
state_dict: Mapping[str, Any],
|
| 272 |
-
device: torch.device | str,
|
| 273 |
-
) -> None:
|
| 274 |
-
# state_dict["shadow_params"] contains independently shaped tensors (...).
|
| 275 |
-
self.decay = float(state_dict["decay"])
|
| 276 |
-
updates = state_dict["num_updates"]
|
| 277 |
-
self.num_updates = None if updates is None else int(updates)
|
| 278 |
-
# Each tensor retains its shape (...).
|
| 279 |
-
self.shadow_params = [tensor.to(device) for tensor in state_dict["shadow_params"]]
|
| 280 |
-
|
| 281 |
-
def to(self, device: torch.device | str) -> None:
|
| 282 |
-
# Each shadow parameter retains its independently varying shape (...).
|
| 283 |
-
self.shadow_params = [tensor.to(device) for tensor in self.shadow_params]
|
|
|
|
| 1 |
+
"""Small geometry, augmentation, activation, and EMA utilities for Boltz2.
|
| 2 |
+
|
| 3 |
+
The quaternion behavior follows PyTorch3D's public rotation convention. License
|
| 4 |
+
and attribution records are retained in ``THIRD_PARTY_NOTICES.md``.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
from collections.abc import Iterable, Mapping, Sequence
|
| 12 |
+
from functools import partial
|
| 13 |
+
from typing import Any
|
| 14 |
+
from torch import Tensor, nn
|
| 15 |
+
from torch.nn import functional as F
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
LinearNoBias = partial(nn.Linear, bias=False)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def exists(value: object) -> bool:
|
| 22 |
+
"""Return whether a value is not ``None``."""
|
| 23 |
+
|
| 24 |
+
return value is not None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def default(value: Any, fallback: Any) -> Any:
|
| 28 |
+
"""Return ``value`` unless it is ``None``."""
|
| 29 |
+
|
| 30 |
+
return fallback if value is None else value
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def log(tensor: Tensor, eps: float = 1e-20) -> Tensor:
|
| 34 |
+
"""Compute a finite logarithm by applying a scalar lower bound."""
|
| 35 |
+
|
| 36 |
+
# tensor: (...).
|
| 37 |
+
return torch.log(tensor.clamp(min=eps)) # (...)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class SwiGLU(nn.Module):
|
| 41 |
+
"""Split X in half and apply a SiLU-gated linear unit."""
|
| 42 |
+
|
| 43 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 44 |
+
# x: (..., 2 * d).
|
| 45 |
+
values, gates = x.chunk(2, dim=-1) # each: (..., d)
|
| 46 |
+
return F.silu(gates) * values # (..., d)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _masked_center(coordinates: Tensor, mask: Tensor) -> Tensor:
|
| 50 |
+
# coordinates: (b, n_atom, 3); mask: (b, n_atom).
|
| 51 |
+
weights = mask[:, :, None] # (b, n_atom, 1)
|
| 52 |
+
# (b, 1, 3)
|
| 53 |
+
return (coordinates * weights).sum(dim=1, keepdim=True) / weights.sum(
|
| 54 |
+
dim=1,
|
| 55 |
+
keepdim=True,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def center(atom_coords: Tensor, atom_mask: Tensor) -> Tensor:
|
| 60 |
+
"""Center coordinate tensor X with shape ``(b, n_atoms, 3)``."""
|
| 61 |
+
|
| 62 |
+
# atom_coords: (b, n_atom, 3); atom_mask: (b, n_atom).
|
| 63 |
+
return atom_coords - _masked_center(atom_coords, atom_mask) # (b, n_atom, 3)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _copysign(magnitudes: Tensor, signs: Tensor) -> Tensor:
|
| 67 |
+
"""Apply the elementwise sign of S to magnitude tensor M."""
|
| 68 |
+
|
| 69 |
+
# magnitudes, signs: broadcast-compatible (...).
|
| 70 |
+
return torch.where((magnitudes < 0) != (signs < 0), -magnitudes, magnitudes) # (...)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def quaternion_to_matrix(quaternions: Tensor) -> Tensor:
|
| 74 |
+
"""Convert real-first quaternion tensor Q from ``(..., 4)`` to ``(..., 3, 3)``."""
|
| 75 |
+
|
| 76 |
+
# quaternions: (..., 4).
|
| 77 |
+
real, i_axis, j_axis, k_axis = torch.unbind(quaternions, dim=-1) # each: (...)
|
| 78 |
+
scale = 2.0 / (quaternions * quaternions).sum(dim=-1) # (...)
|
| 79 |
+
entries = ( # nine tensors, each (...)
|
| 80 |
+
1 - scale * (j_axis * j_axis + k_axis * k_axis),
|
| 81 |
+
scale * (i_axis * j_axis - k_axis * real),
|
| 82 |
+
scale * (i_axis * k_axis + j_axis * real),
|
| 83 |
+
scale * (i_axis * j_axis + k_axis * real),
|
| 84 |
+
1 - scale * (i_axis * i_axis + k_axis * k_axis),
|
| 85 |
+
scale * (j_axis * k_axis - i_axis * real),
|
| 86 |
+
scale * (i_axis * k_axis - j_axis * real),
|
| 87 |
+
scale * (j_axis * k_axis + i_axis * real),
|
| 88 |
+
1 - scale * (i_axis * i_axis + j_axis * j_axis),
|
| 89 |
+
)
|
| 90 |
+
# (..., 3, 3)
|
| 91 |
+
return torch.stack(entries, dim=-1).reshape((*quaternions.shape[:-1], 3, 3))
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def random_quaternions(
|
| 95 |
+
n: int,
|
| 96 |
+
dtype: torch.dtype | None = None,
|
| 97 |
+
device: torch.device | str | None = None,
|
| 98 |
+
) -> Tensor:
|
| 99 |
+
"""Draw ``n`` uniformly distributed unit quaternions with nonnegative real part."""
|
| 100 |
+
|
| 101 |
+
resolved_device = torch.device(device) if isinstance(device, str) else device
|
| 102 |
+
samples = torch.randn((n, 4), dtype=dtype, device=resolved_device) # (n, 4)
|
| 103 |
+
squared_norm = (samples * samples).sum(dim=1) # (n,)
|
| 104 |
+
signed_norm = _copysign(torch.sqrt(squared_norm), samples[:, 0]) # (n,)
|
| 105 |
+
return samples / signed_norm[:, None] # (n, 4)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def random_rotations(
|
| 109 |
+
n: int,
|
| 110 |
+
dtype: torch.dtype | None = None,
|
| 111 |
+
device: torch.device | str | None = None,
|
| 112 |
+
) -> Tensor:
|
| 113 |
+
"""Draw rotation tensor R with shape ``(n, 3, 3)``."""
|
| 114 |
+
|
| 115 |
+
# (n, 3, 3)
|
| 116 |
+
return quaternion_to_matrix(random_quaternions(n, dtype=dtype, device=device))
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def compute_random_augmentation(
|
| 120 |
+
multiplicity: int,
|
| 121 |
+
s_trans: float = 1.0,
|
| 122 |
+
device: torch.device | str | None = None,
|
| 123 |
+
dtype: torch.dtype = torch.float32,
|
| 124 |
+
) -> tuple[Tensor, Tensor]:
|
| 125 |
+
"""Draw independent rotations R and translations T for a replicated batch."""
|
| 126 |
+
|
| 127 |
+
rotations = random_rotations(multiplicity, dtype=dtype, device=device) # (m, 3, 3)
|
| 128 |
+
translations = ( # (m, 1, 3)
|
| 129 |
+
torch.randn(
|
| 130 |
+
(multiplicity, 1, 3),
|
| 131 |
+
dtype=dtype,
|
| 132 |
+
device=device,
|
| 133 |
+
)
|
| 134 |
+
* s_trans
|
| 135 |
+
)
|
| 136 |
+
return rotations, translations # (m, 3, 3), (m, 1, 3)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def randomly_rotate(
|
| 140 |
+
coords: Tensor,
|
| 141 |
+
return_second_coords: bool = False,
|
| 142 |
+
second_coords: Tensor | None = None,
|
| 143 |
+
) -> Tensor | tuple[Tensor, Tensor | None]:
|
| 144 |
+
"""Rotate X and optionally Y using the same sampled rotation tensor R."""
|
| 145 |
+
|
| 146 |
+
# coords: (b, n, 3); second_coords: (b, n_second, 3) or None.
|
| 147 |
+
rotations = random_rotations(len(coords), coords.dtype, coords.device) # (b, 3, 3)
|
| 148 |
+
rotated = torch.einsum("bmd,bds->bms", coords, rotations) # (b, n, 3)
|
| 149 |
+
if not return_second_coords:
|
| 150 |
+
return rotated # (b, n, 3)
|
| 151 |
+
rotated_second = ( # (b, n_second, 3) or None
|
| 152 |
+
None if second_coords is None else torch.einsum("bmd,bds->bms", second_coords, rotations)
|
| 153 |
+
)
|
| 154 |
+
return rotated, rotated_second # (b, n, 3), (b, n_second, 3) or None
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def center_random_augmentation(
|
| 158 |
+
atom_coords: Tensor,
|
| 159 |
+
atom_mask: Tensor,
|
| 160 |
+
s_trans: float = 1.0,
|
| 161 |
+
augmentation: bool = True,
|
| 162 |
+
centering: bool = True,
|
| 163 |
+
return_second_coords: bool = False,
|
| 164 |
+
second_coords: Tensor | None = None,
|
| 165 |
+
) -> Tensor | tuple[Tensor, Tensor | None]:
|
| 166 |
+
"""Center and rigidly augment coordinate tensors X and optional Y."""
|
| 167 |
+
|
| 168 |
+
# atom_coords: (b, n_atom, 3); atom_mask: (b, n_atom).
|
| 169 |
+
# second_coords: (b, n_second, 3) or None.
|
| 170 |
+
primary = atom_coords # (b, n_atom, 3)
|
| 171 |
+
secondary = second_coords # (b, n_second, 3) or None
|
| 172 |
+
if centering:
|
| 173 |
+
centroid = _masked_center(primary, atom_mask) # (b, 1, 3)
|
| 174 |
+
primary = primary - centroid # (b, n_atom, 3)
|
| 175 |
+
if secondary is not None:
|
| 176 |
+
secondary = secondary - centroid # (b, n_second, 3)
|
| 177 |
+
|
| 178 |
+
if augmentation:
|
| 179 |
+
primary, secondary = randomly_rotate( # (b, n_atom, 3), (b, n_second, 3) or None
|
| 180 |
+
primary,
|
| 181 |
+
return_second_coords=True,
|
| 182 |
+
second_coords=secondary,
|
| 183 |
+
)
|
| 184 |
+
translation = torch.randn_like(primary[:, 0:1, :]) * s_trans # (b, 1, 3)
|
| 185 |
+
primary = primary + translation # (b, n_atom, 3)
|
| 186 |
+
if secondary is not None:
|
| 187 |
+
secondary = secondary + translation # (b, n_second, 3)
|
| 188 |
+
|
| 189 |
+
# (b, n_atom, 3), optionally paired with (b, n_second, 3) or None.
|
| 190 |
+
return (primary, secondary) if return_second_coords else primary
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
class ExponentialMovingAverage:
|
| 194 |
+
"""Maintain detached exponential moving averages of trainable parameters."""
|
| 195 |
+
|
| 196 |
+
def __init__(
|
| 197 |
+
self,
|
| 198 |
+
parameters: Iterable[nn.Parameter],
|
| 199 |
+
decay: float,
|
| 200 |
+
use_num_updates: bool = True,
|
| 201 |
+
) -> None:
|
| 202 |
+
# parameters: one independently shaped tensor (...) per trainable parameter.
|
| 203 |
+
if not 0.0 <= decay <= 1.0:
|
| 204 |
+
raise ValueError("decay must lie in [0, 1]")
|
| 205 |
+
self.decay = decay
|
| 206 |
+
self.num_updates: int | None = 0 if use_num_updates else None
|
| 207 |
+
self.shadow_params = [ # each: same shape as one trainable parameter, (...)
|
| 208 |
+
parameter.clone().detach() for parameter in parameters if parameter.requires_grad
|
| 209 |
+
]
|
| 210 |
+
self.collected_params: list[Tensor] = []
|
| 211 |
+
|
| 212 |
+
@staticmethod
|
| 213 |
+
def _trainable(parameters: Iterable[nn.Parameter]) -> list[nn.Parameter]:
|
| 214 |
+
# Each item is one trainable parameter with an independently varying shape (...).
|
| 215 |
+
return [parameter for parameter in parameters if parameter.requires_grad]
|
| 216 |
+
|
| 217 |
+
def update(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 218 |
+
# parameters: one independently shaped tensor (...) per trainable parameter.
|
| 219 |
+
trainable = self._trainable(parameters) # each: one trainable parameter, (...)
|
| 220 |
+
if len(trainable) != len(self.shadow_params):
|
| 221 |
+
raise ValueError("EMA parameter count changed")
|
| 222 |
+
decay = self.decay
|
| 223 |
+
if self.num_updates is not None:
|
| 224 |
+
self.num_updates += 1
|
| 225 |
+
decay = min(decay, (1 + self.num_updates) / (10 + self.num_updates))
|
| 226 |
+
update_weight = 1.0 - decay
|
| 227 |
+
with torch.no_grad():
|
| 228 |
+
for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
|
| 229 |
+
shadow.sub_(update_weight * (shadow - parameter)) # (...) unchanged in place
|
| 230 |
+
|
| 231 |
+
def compatible(self, parameters: Sequence[Tensor]) -> bool:
|
| 232 |
+
"""Return whether parameter count and tensor shapes match the EMA state."""
|
| 233 |
+
|
| 234 |
+
# Each shadow and corresponding parameter has an independently varying shape (...).
|
| 235 |
+
return len(self.shadow_params) == len(parameters) and all(
|
| 236 |
+
shadow.shape == parameter.shape
|
| 237 |
+
for shadow, parameter in zip(self.shadow_params, parameters, strict=True)
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
def copy_to(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 241 |
+
# parameters: one independently shaped tensor (...) per trainable parameter.
|
| 242 |
+
trainable = self._trainable(parameters) # each: one trainable parameter, (...)
|
| 243 |
+
if len(trainable) != len(self.shadow_params):
|
| 244 |
+
raise ValueError("EMA parameter count changed")
|
| 245 |
+
for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
|
| 246 |
+
parameter.data.copy_(shadow.data) # (...) unchanged in place
|
| 247 |
+
|
| 248 |
+
def store(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 249 |
+
# parameters: one independently shaped tensor (...) per parameter.
|
| 250 |
+
# Each clone has the same shape (...) as its corresponding parameter.
|
| 251 |
+
self.collected_params = [parameter.clone() for parameter in parameters]
|
| 252 |
+
|
| 253 |
+
def restore(self, parameters: Iterable[nn.Parameter]) -> None:
|
| 254 |
+
# parameters: one independently shaped tensor (...) per parameter.
|
| 255 |
+
current = list(parameters) # each: one parameter, (...)
|
| 256 |
+
if len(current) != len(self.collected_params):
|
| 257 |
+
raise ValueError("stored EMA parameter count changed")
|
| 258 |
+
for collected, parameter in zip(self.collected_params, current, strict=True):
|
| 259 |
+
parameter.data.copy_(collected.data) # (...) unchanged in place
|
| 260 |
+
|
| 261 |
+
def state_dict(self) -> dict[str, Any]:
|
| 262 |
+
# shadow_params contains independently shaped tensors (...).
|
| 263 |
+
return {
|
| 264 |
+
"decay": self.decay,
|
| 265 |
+
"num_updates": self.num_updates,
|
| 266 |
+
"shadow_params": self.shadow_params,
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
def load_state_dict(
|
| 270 |
+
self,
|
| 271 |
+
state_dict: Mapping[str, Any],
|
| 272 |
+
device: torch.device | str,
|
| 273 |
+
) -> None:
|
| 274 |
+
# state_dict["shadow_params"] contains independently shaped tensors (...).
|
| 275 |
+
self.decay = float(state_dict["decay"])
|
| 276 |
+
updates = state_dict["num_updates"]
|
| 277 |
+
self.num_updates = None if updates is None else int(updates)
|
| 278 |
+
# Each tensor retains its shape (...).
|
| 279 |
+
self.shadow_params = [tensor.to(device) for tensor in state_dict["shadow_params"]]
|
| 280 |
+
|
| 281 |
+
def to(self, device: torch.device | str) -> None:
|
| 282 |
+
# Each shadow parameter retains its independently varying shape (...).
|
| 283 |
+
self.shadow_params = [tensor.to(device) for tensor in self.shadow_params]
|
fastplms/models/boltz/vb_potentials_potentials.py
CHANGED
|
@@ -1,896 +1,896 @@
|
|
| 1 |
-
"""Differentiable structure-steering potentials for Boltz2 diffusion.
|
| 2 |
-
|
| 3 |
-
Each potential follows the same mechanism: derive constraint indices and
|
| 4 |
-
bounds from features, evaluate a geometric variable, map that variable to an
|
| 5 |
-
energy, and optionally scatter its analytic derivative back to atom
|
| 6 |
-
coordinates. This module is maintained independently from the pinned parity
|
| 7 |
-
oracle while preserving the public class names used by converted checkpoints.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import torch
|
| 13 |
-
|
| 14 |
-
from abc import ABC, abstractmethod
|
| 15 |
-
from dataclasses import dataclass
|
| 16 |
-
from typing import Any
|
| 17 |
-
|
| 18 |
-
from . import vb_const as const
|
| 19 |
-
from .vb_loss_diffusionv2 import weighted_rigid_align
|
| 20 |
-
from .vb_potentials_schedules import (
|
| 21 |
-
ExponentialInterpolation,
|
| 22 |
-
ParameterSchedule,
|
| 23 |
-
PiecewiseStepFunction,
|
| 24 |
-
)
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
Parameter = ParameterSchedule | float | int | bool
|
| 28 |
-
ParameterMap = dict[str, Parameter]
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
@dataclass(frozen=True, slots=True)
|
| 32 |
-
class _PreparedPotential:
|
| 33 |
-
coordinates: torch.Tensor # (..., a, 3)
|
| 34 |
-
index: torch.Tensor # (q, n)
|
| 35 |
-
function_args: tuple[Any, ...]
|
| 36 |
-
com_index: torch.Tensor | None # (a_input,)
|
| 37 |
-
atom_pad_mask: torch.Tensor | None # (a_input,)
|
| 38 |
-
ref_coords: torch.Tensor | None # (n_ref, n, 3)
|
| 39 |
-
ref_mask: torch.Tensor | None # (n_ref, n)
|
| 40 |
-
ref_token_index: torch.Tensor | None # (a_input,)
|
| 41 |
-
negation_mask: torch.Tensor | None # (n,)
|
| 42 |
-
union_index: torch.Tensor | None # (n,)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def _reduce_atoms_to_centers(
|
| 46 |
-
coordinates: torch.Tensor,
|
| 47 |
-
com_index: torch.Tensor,
|
| 48 |
-
atom_pad_mask: torch.Tensor,
|
| 49 |
-
) -> torch.Tensor:
|
| 50 |
-
# coordinates: (..., a, 3); com_index/atom_pad_mask: (a,).
|
| 51 |
-
unpadded_index = com_index[atom_pad_mask] # (a_valid,)
|
| 52 |
-
unpadded_coordinates = coordinates[..., atom_pad_mask, :] # (..., a_valid, 3)
|
| 53 |
-
center_shape = (*unpadded_coordinates.shape[:-2], unpadded_index.max() + 1, 3)
|
| 54 |
-
return torch.zeros(center_shape, device=coordinates.device).scatter_reduce(
|
| 55 |
-
-2,
|
| 56 |
-
unpadded_index.unsqueeze(-1).expand_as(unpadded_coordinates),
|
| 57 |
-
unpadded_coordinates,
|
| 58 |
-
"mean",
|
| 59 |
-
) # (..., n_center, 3)
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def _union_weights(
|
| 63 |
-
energy: torch.Tensor,
|
| 64 |
-
union_index: torch.Tensor,
|
| 65 |
-
union_lambda: float,
|
| 66 |
-
) -> torch.Tensor:
|
| 67 |
-
# energy: (..., n); union_index: (n,).
|
| 68 |
-
unnormalized = torch.exp(-union_lambda * energy) # (..., n)
|
| 69 |
-
partition = torch.zeros(
|
| 70 |
-
(*energy.shape[:-1], union_index.max() + 1),
|
| 71 |
-
device=union_index.device,
|
| 72 |
-
).scatter_reduce(
|
| 73 |
-
-1,
|
| 74 |
-
union_index.expand_as(unnormalized),
|
| 75 |
-
unnormalized,
|
| 76 |
-
"sum",
|
| 77 |
-
) # (..., n_union)
|
| 78 |
-
weights = unnormalized / partition[..., union_index] # (..., n)
|
| 79 |
-
weights[partition[..., union_index] == 0] = 0 # (..., n)
|
| 80 |
-
return weights # (..., n)
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def _scatter_constraint_gradients(
|
| 84 |
-
coefficients: torch.Tensor,
|
| 85 |
-
variable_gradient: torch.Tensor,
|
| 86 |
-
index: torch.Tensor,
|
| 87 |
-
coordinates: torch.Tensor,
|
| 88 |
-
) -> torch.Tensor:
|
| 89 |
-
# coefficients: (..., n); variable_gradient: (..., q, n, 3).
|
| 90 |
-
# index: (q, n); coordinates: (b, a, 3).
|
| 91 |
-
product = coefficients.tile(variable_gradient.shape[-3]).unsqueeze(
|
| 92 |
-
-1
|
| 93 |
-
) * variable_gradient.flatten(start_dim=-3, end_dim=-2) # (..., q * n, 3)
|
| 94 |
-
if product.dim() > 3:
|
| 95 |
-
product = product.sum(dim=list(range(1, product.dim() - 2))) # (b, q * n, 3)
|
| 96 |
-
scatter_index = (
|
| 97 |
-
index.flatten(start_dim=0, end_dim=1).unsqueeze(-1).expand((*coordinates.shape[:-2], -1, 3))
|
| 98 |
-
) # (b, q * n, 3)
|
| 99 |
-
return torch.zeros_like(coordinates).scatter_reduce(
|
| 100 |
-
-2,
|
| 101 |
-
scatter_index,
|
| 102 |
-
product,
|
| 103 |
-
"sum",
|
| 104 |
-
) # (b, a, 3)
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
class Potential(ABC):
|
| 108 |
-
"""Base contract for energy and analytic-gradient steering potentials."""
|
| 109 |
-
|
| 110 |
-
def __init__(self, parameters: ParameterMap | None = None) -> None:
|
| 111 |
-
self.parameters = parameters
|
| 112 |
-
|
| 113 |
-
def _prepare(
|
| 114 |
-
self,
|
| 115 |
-
coordinates: torch.Tensor,
|
| 116 |
-
feats: dict[str, torch.Tensor],
|
| 117 |
-
parameters: dict[str, Any],
|
| 118 |
-
computed_args: tuple[Any, ...] | None = None,
|
| 119 |
-
) -> _PreparedPotential:
|
| 120 |
-
# coordinates: (..., a_input, 3).
|
| 121 |
-
if computed_args is None:
|
| 122 |
-
computed_args = self.compute_args(feats, parameters)
|
| 123 |
-
index, args, com_args, ref_args, operator_args = computed_args # index: (q, n)
|
| 124 |
-
com_index = atom_pad_mask = None
|
| 125 |
-
if com_args is not None:
|
| 126 |
-
com_index, atom_pad_mask = com_args # each (a_input,)
|
| 127 |
-
coordinates = _reduce_atoms_to_centers(
|
| 128 |
-
coordinates,
|
| 129 |
-
com_index,
|
| 130 |
-
atom_pad_mask,
|
| 131 |
-
) # (..., n_center, 3)
|
| 132 |
-
|
| 133 |
-
ref_coords = ref_mask = ref_token_index = None
|
| 134 |
-
if ref_args is not None:
|
| 135 |
-
# ref_coords: (n_ref, n, 3); ref_mask: (n_ref, n).
|
| 136 |
-
# ref_atom_index: (n,); ref_token_index: (a_input,).
|
| 137 |
-
ref_coords, ref_mask, ref_atom_index, ref_token_index = ref_args
|
| 138 |
-
coordinates = coordinates[..., ref_atom_index, :] # (..., n, 3)
|
| 139 |
-
|
| 140 |
-
negation_mask = union_index = None
|
| 141 |
-
if operator_args is not None:
|
| 142 |
-
negation_mask, union_index = operator_args # each (n,)
|
| 143 |
-
return _PreparedPotential(
|
| 144 |
-
coordinates=coordinates,
|
| 145 |
-
index=index,
|
| 146 |
-
function_args=args,
|
| 147 |
-
com_index=com_index,
|
| 148 |
-
atom_pad_mask=atom_pad_mask,
|
| 149 |
-
ref_coords=ref_coords,
|
| 150 |
-
ref_mask=ref_mask,
|
| 151 |
-
ref_token_index=ref_token_index,
|
| 152 |
-
negation_mask=negation_mask,
|
| 153 |
-
union_index=union_index,
|
| 154 |
-
)
|
| 155 |
-
|
| 156 |
-
def compute(
|
| 157 |
-
self,
|
| 158 |
-
coords: torch.Tensor,
|
| 159 |
-
feats: dict[str, torch.Tensor],
|
| 160 |
-
parameters: dict[str, Any],
|
| 161 |
-
) -> torch.Tensor:
|
| 162 |
-
"""Evaluate one energy per coordinate sample."""
|
| 163 |
-
|
| 164 |
-
# coords: (..., a, 3).
|
| 165 |
-
computed_args = self.compute_args(feats, parameters)
|
| 166 |
-
if computed_args[0].shape[1] == 0:
|
| 167 |
-
return torch.zeros(coords.shape[:-2], device=coords.device) # (...)
|
| 168 |
-
prepared = self._prepare(coords, feats, parameters, computed_args)
|
| 169 |
-
value = self.compute_variable(
|
| 170 |
-
prepared.coordinates,
|
| 171 |
-
prepared.index,
|
| 172 |
-
ref_coords=prepared.ref_coords,
|
| 173 |
-
ref_mask=prepared.ref_mask,
|
| 174 |
-
compute_gradient=False,
|
| 175 |
-
) # (..., n)
|
| 176 |
-
energy = self.compute_function(
|
| 177 |
-
value,
|
| 178 |
-
*prepared.function_args,
|
| 179 |
-
negation_mask=prepared.negation_mask,
|
| 180 |
-
compute_derivative=False,
|
| 181 |
-
) # (..., n)
|
| 182 |
-
if prepared.union_index is not None:
|
| 183 |
-
weights = _union_weights(
|
| 184 |
-
energy,
|
| 185 |
-
prepared.union_index,
|
| 186 |
-
parameters["union_lambda"],
|
| 187 |
-
) # (..., n)
|
| 188 |
-
return (energy * weights).sum(dim=-1) # (...)
|
| 189 |
-
return energy.sum(dim=tuple(range(1, energy.dim()))) # (b,)
|
| 190 |
-
|
| 191 |
-
def compute_gradient(
|
| 192 |
-
self,
|
| 193 |
-
coords: torch.Tensor,
|
| 194 |
-
feats: dict[str, torch.Tensor],
|
| 195 |
-
parameters: dict[str, Any],
|
| 196 |
-
) -> torch.Tensor:
|
| 197 |
-
"""Return the analytic coordinate gradient of the potential energy."""
|
| 198 |
-
|
| 199 |
-
# coords: (b, a, 3).
|
| 200 |
-
computed_args = self.compute_args(feats, parameters)
|
| 201 |
-
if computed_args[0].shape[1] == 0:
|
| 202 |
-
return torch.zeros_like(coords) # (b, a, 3)
|
| 203 |
-
prepared = self._prepare(coords, feats, parameters, computed_args)
|
| 204 |
-
value, variable_gradient = self.compute_variable(
|
| 205 |
-
prepared.coordinates,
|
| 206 |
-
prepared.index,
|
| 207 |
-
ref_coords=prepared.ref_coords,
|
| 208 |
-
ref_mask=prepared.ref_mask,
|
| 209 |
-
compute_gradient=True,
|
| 210 |
-
) # value: (..., n); variable_gradient: (..., q, n, 3)
|
| 211 |
-
energy, energy_derivative = self.compute_function(
|
| 212 |
-
value,
|
| 213 |
-
*prepared.function_args,
|
| 214 |
-
negation_mask=prepared.negation_mask,
|
| 215 |
-
compute_derivative=True,
|
| 216 |
-
) # each (..., n)
|
| 217 |
-
if prepared.union_index is not None:
|
| 218 |
-
weights = _union_weights(
|
| 219 |
-
energy,
|
| 220 |
-
prepared.union_index,
|
| 221 |
-
parameters["union_lambda"],
|
| 222 |
-
) # (..., n)
|
| 223 |
-
union_energy = torch.zeros(
|
| 224 |
-
(*energy.shape[:-1], prepared.union_index.max() + 1),
|
| 225 |
-
device=prepared.union_index.device,
|
| 226 |
-
).scatter_reduce(
|
| 227 |
-
-1,
|
| 228 |
-
prepared.union_index.expand_as(energy),
|
| 229 |
-
energy * weights,
|
| 230 |
-
"sum",
|
| 231 |
-
) # (..., n_union)
|
| 232 |
-
coefficients = (
|
| 233 |
-
energy_derivative
|
| 234 |
-
* weights
|
| 235 |
-
* (
|
| 236 |
-
1
|
| 237 |
-
+ parameters["union_lambda"]
|
| 238 |
-
* (energy - union_energy[..., prepared.union_index])
|
| 239 |
-
)
|
| 240 |
-
) # (..., n)
|
| 241 |
-
else:
|
| 242 |
-
coefficients = energy_derivative # (..., n)
|
| 243 |
-
|
| 244 |
-
atom_gradient = _scatter_constraint_gradients(
|
| 245 |
-
coefficients,
|
| 246 |
-
variable_gradient,
|
| 247 |
-
prepared.index,
|
| 248 |
-
prepared.coordinates,
|
| 249 |
-
) # (b, a_prepared, 3)
|
| 250 |
-
if prepared.com_index is not None:
|
| 251 |
-
atom_gradient = atom_gradient[..., prepared.com_index, :] # (b, a, 3)
|
| 252 |
-
elif prepared.ref_token_index is not None:
|
| 253 |
-
atom_gradient = atom_gradient[..., prepared.ref_token_index, :] # (b, a, 3)
|
| 254 |
-
return atom_gradient # (b, a, 3)
|
| 255 |
-
|
| 256 |
-
def compute_parameters(self, t: float) -> dict[str, Any] | None:
|
| 257 |
-
"""Resolve scheduled parameters at diffusion time ``t``."""
|
| 258 |
-
|
| 259 |
-
if self.parameters is None:
|
| 260 |
-
return None
|
| 261 |
-
return {
|
| 262 |
-
name: parameter.compute(t) if isinstance(parameter, ParameterSchedule) else parameter
|
| 263 |
-
for name, parameter in self.parameters.items()
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
@abstractmethod
|
| 267 |
-
def compute_function(
|
| 268 |
-
self,
|
| 269 |
-
value: torch.Tensor,
|
| 270 |
-
*args: Any,
|
| 271 |
-
negation_mask: torch.Tensor | None = None,
|
| 272 |
-
compute_derivative: bool = False,
|
| 273 |
-
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 274 |
-
# value: (..., n); negation_mask: (n,).
|
| 275 |
-
raise NotImplementedError
|
| 276 |
-
|
| 277 |
-
@abstractmethod
|
| 278 |
-
def compute_variable(
|
| 279 |
-
self,
|
| 280 |
-
coords: torch.Tensor,
|
| 281 |
-
index: torch.Tensor,
|
| 282 |
-
ref_coords: torch.Tensor | None = None,
|
| 283 |
-
ref_mask: torch.Tensor | None = None,
|
| 284 |
-
compute_gradient: bool = False,
|
| 285 |
-
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 286 |
-
# coords: (..., a, 3); index: (q, n).
|
| 287 |
-
raise NotImplementedError
|
| 288 |
-
|
| 289 |
-
@abstractmethod
|
| 290 |
-
def compute_args(
|
| 291 |
-
self,
|
| 292 |
-
feats: dict[str, torch.Tensor],
|
| 293 |
-
parameters: dict[str, Any],
|
| 294 |
-
) -> tuple[Any, ...]:
|
| 295 |
-
raise NotImplementedError
|
| 296 |
-
|
| 297 |
-
def get_reference_coords(
|
| 298 |
-
self,
|
| 299 |
-
feats: dict[str, torch.Tensor],
|
| 300 |
-
parameters: dict[str, Any],
|
| 301 |
-
) -> tuple[None, None]:
|
| 302 |
-
del feats, parameters
|
| 303 |
-
return None, None
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
class FlatBottomPotential(Potential):
|
| 307 |
-
"""Linear penalty outside an allowed lower-to-upper interval."""
|
| 308 |
-
|
| 309 |
-
def compute_function(
|
| 310 |
-
self,
|
| 311 |
-
value: torch.Tensor,
|
| 312 |
-
k: torch.Tensor,
|
| 313 |
-
lower_bounds: torch.Tensor | None,
|
| 314 |
-
upper_bounds: torch.Tensor | None,
|
| 315 |
-
negation_mask: torch.Tensor | None = None,
|
| 316 |
-
compute_derivative: bool = False,
|
| 317 |
-
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 318 |
-
# value: (..., n); k/lower_bounds/upper_bounds: (n,) or (..., n).
|
| 319 |
-
# negation_mask: (n,) or a shape broadcastable to (..., n).
|
| 320 |
-
lower = (
|
| 321 |
-
(torch.full_like(value, float("-inf")) if lower_bounds is None else lower_bounds)
|
| 322 |
-
.expand_as(value)
|
| 323 |
-
.clone()
|
| 324 |
-
) # (..., n)
|
| 325 |
-
upper = (
|
| 326 |
-
(torch.full_like(value, float("inf")) if upper_bounds is None else upper_bounds)
|
| 327 |
-
.expand_as(value)
|
| 328 |
-
.clone()
|
| 329 |
-
) # (..., n)
|
| 330 |
-
|
| 331 |
-
if negation_mask is not None:
|
| 332 |
-
if not torch.is_tensor(negation_mask):
|
| 333 |
-
raise TypeError(
|
| 334 |
-
f"negation_mask must be a boolean tensor, got {type(negation_mask).__name__}."
|
| 335 |
-
)
|
| 336 |
-
if negation_mask.dtype != torch.bool:
|
| 337 |
-
raise TypeError(
|
| 338 |
-
f"negation_mask must be a boolean tensor, got {negation_mask.dtype}."
|
| 339 |
-
)
|
| 340 |
-
if negation_mask.device != value.device:
|
| 341 |
-
raise ValueError(
|
| 342 |
-
"negation_mask must be on the same device as value; "
|
| 343 |
-
f"got {negation_mask.device} and {value.device}."
|
| 344 |
-
)
|
| 345 |
-
try:
|
| 346 |
-
expanded_negation_mask = negation_mask.expand_as(value) # (..., n)
|
| 347 |
-
except RuntimeError as error:
|
| 348 |
-
raise ValueError(
|
| 349 |
-
"negation_mask must be broadcastable to value shape "
|
| 350 |
-
f"{tuple(value.shape)}, got {tuple(negation_mask.shape)}."
|
| 351 |
-
) from error
|
| 352 |
-
unbounded_below = torch.isneginf(lower) # (..., n)
|
| 353 |
-
unbounded_above = torch.isposinf(upper) # (..., n)
|
| 354 |
-
# (..., n)
|
| 355 |
-
valid_negation = unbounded_below | unbounded_above | expanded_negation_mask
|
| 356 |
-
if not bool(torch.all(valid_negation).item()):
|
| 357 |
-
raise ValueError(
|
| 358 |
-
"negation_mask may be false only where at least one bound is infinite."
|
| 359 |
-
)
|
| 360 |
-
select_upper = ~unbounded_above & ~expanded_negation_mask # (..., n)
|
| 361 |
-
lower[select_upper] = upper[select_upper] # (..., n)
|
| 362 |
-
upper[select_upper] = float("inf") # (..., n)
|
| 363 |
-
select_lower = ~unbounded_below & ~expanded_negation_mask # (..., n)
|
| 364 |
-
upper[select_lower] = lower[select_lower] # (..., n)
|
| 365 |
-
lower[select_lower] = float("-inf") # (..., n)
|
| 366 |
-
|
| 367 |
-
below = value < lower # (..., n)
|
| 368 |
-
above = value > upper # (..., n)
|
| 369 |
-
energy = torch.zeros_like(value) # (..., n)
|
| 370 |
-
energy[below] = (k * (lower - value))[below] # (..., n)
|
| 371 |
-
energy[above] = (k * (value - upper))[above] # (..., n)
|
| 372 |
-
if not compute_derivative:
|
| 373 |
-
return energy # (..., n)
|
| 374 |
-
derivative = torch.zeros_like(value) # (..., n)
|
| 375 |
-
derivative[below] = -k.expand_as(below)[below] # (..., n)
|
| 376 |
-
derivative[above] = k.expand_as(above)[above] # (..., n)
|
| 377 |
-
return energy, derivative # each (..., n)
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
class ReferencePotential(Potential):
|
| 381 |
-
"""Measure atom displacement after weighted rigid alignment."""
|
| 382 |
-
|
| 383 |
-
def compute_variable(
|
| 384 |
-
self,
|
| 385 |
-
coords: torch.Tensor,
|
| 386 |
-
index: torch.Tensor,
|
| 387 |
-
ref_coords: torch.Tensor,
|
| 388 |
-
ref_mask: torch.Tensor,
|
| 389 |
-
compute_gradient: bool = False,
|
| 390 |
-
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 391 |
-
# coords: (b, a, 3); index: (1, n).
|
| 392 |
-
# ref_coords: (n_ref, n, 3); ref_mask: (n_ref, n).
|
| 393 |
-
aligned_reference = weighted_rigid_align(
|
| 394 |
-
ref_coords.float(),
|
| 395 |
-
coords[:, index].float(),
|
| 396 |
-
ref_mask,
|
| 397 |
-
ref_mask,
|
| 398 |
-
) # (b, n_ref, n, 3)
|
| 399 |
-
displacement = coords[:, index] - aligned_reference # (b, n_ref, n, 3)
|
| 400 |
-
distance = torch.linalg.norm(displacement, dim=-1) # (b, n_ref, n)
|
| 401 |
-
if not compute_gradient:
|
| 402 |
-
return distance # (b, n_ref, n)
|
| 403 |
-
unit_displacement = displacement / distance.unsqueeze(-1) # (b, n_ref, n, 3)
|
| 404 |
-
# (b, 1, n_ref, n, 3)
|
| 405 |
-
gradient = (unit_displacement * ref_mask.unsqueeze(-1)).unsqueeze(1)
|
| 406 |
-
return distance, gradient # (b, n_ref, n); (b, 1, n_ref, n, 3)
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
class DistancePotential(Potential):
|
| 410 |
-
"""Measure Euclidean distances for indexed atom pairs."""
|
| 411 |
-
|
| 412 |
-
def compute_variable(
|
| 413 |
-
self,
|
| 414 |
-
coords: torch.Tensor,
|
| 415 |
-
index: torch.Tensor,
|
| 416 |
-
ref_coords: torch.Tensor | None = None,
|
| 417 |
-
ref_mask: torch.Tensor | None = None,
|
| 418 |
-
compute_gradient: bool = False,
|
| 419 |
-
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 420 |
-
# coords: (..., a, 3); index: (2, n).
|
| 421 |
-
del ref_coords, ref_mask
|
| 422 |
-
displacement = coords.index_select(-2, index[0]) - coords.index_select(
|
| 423 |
-
-2,
|
| 424 |
-
index[1],
|
| 425 |
-
) # (..., n, 3)
|
| 426 |
-
distance = torch.linalg.norm(displacement, dim=-1) # (..., n)
|
| 427 |
-
if not compute_gradient:
|
| 428 |
-
return distance # (..., n)
|
| 429 |
-
unit_displacement = displacement / distance.unsqueeze(-1) # (..., n, 3)
|
| 430 |
-
# Returns (..., n); (..., 2, n, 3).
|
| 431 |
-
return distance, torch.stack((unit_displacement, -unit_displacement), dim=1)
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
class DihedralPotential(Potential):
|
| 435 |
-
"""Measure signed torsion angles for indexed atom quartets."""
|
| 436 |
-
|
| 437 |
-
def compute_variable(
|
| 438 |
-
self,
|
| 439 |
-
coords: torch.Tensor,
|
| 440 |
-
index: torch.Tensor,
|
| 441 |
-
ref_coords: torch.Tensor | None = None,
|
| 442 |
-
ref_mask: torch.Tensor | None = None,
|
| 443 |
-
compute_gradient: bool = False,
|
| 444 |
-
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 445 |
-
# coords: (..., a, 3); index: (4, n).
|
| 446 |
-
del ref_coords, ref_mask
|
| 447 |
-
# Each displacement has shape (..., n, 3).
|
| 448 |
-
r_ij = coords.index_select(-2, index[0]) - coords.index_select(-2, index[1])
|
| 449 |
-
r_kj = coords.index_select(-2, index[2]) - coords.index_select(-2, index[1])
|
| 450 |
-
r_kl = coords.index_select(-2, index[2]) - coords.index_select(-2, index[3])
|
| 451 |
-
n_ijk = torch.cross(r_ij, r_kj, dim=-1) # (..., n, 3)
|
| 452 |
-
n_jkl = torch.cross(r_kj, r_kl, dim=-1) # (..., n, 3)
|
| 453 |
-
r_kj_norm = torch.linalg.norm(r_kj, dim=-1) # (..., n)
|
| 454 |
-
n_ijk_norm = torch.linalg.norm(n_ijk, dim=-1) # (..., n)
|
| 455 |
-
n_jkl_norm = torch.linalg.norm(n_jkl, dim=-1) # (..., n)
|
| 456 |
-
|
| 457 |
-
orientation = torch.sign(
|
| 458 |
-
r_kj.unsqueeze(-2) @ torch.cross(n_ijk, n_jkl, dim=-1).unsqueeze(-1)
|
| 459 |
-
).squeeze(-1, -2) # (..., n)
|
| 460 |
-
cosine = (n_ijk.unsqueeze(-2) @ n_jkl.unsqueeze(-1)).squeeze(-1, -2) / (
|
| 461 |
-
n_ijk_norm * n_jkl_norm
|
| 462 |
-
) # (..., n)
|
| 463 |
-
# (..., n)
|
| 464 |
-
angle = orientation * torch.arccos(torch.clamp(cosine, -1 + 1e-8, 1 - 1e-8))
|
| 465 |
-
if not compute_gradient:
|
| 466 |
-
return angle # (..., n)
|
| 467 |
-
|
| 468 |
-
projection_i = (
|
| 469 |
-
(r_ij.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
|
| 470 |
-
).unsqueeze(-1) # (..., n, 1)
|
| 471 |
-
projection_l = (
|
| 472 |
-
(r_kl.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
|
| 473 |
-
).unsqueeze(-1) # (..., n, 1)
|
| 474 |
-
grad_i = n_ijk * (r_kj_norm / n_ijk_norm**2).unsqueeze(-1) # (..., n, 3)
|
| 475 |
-
grad_l = -n_jkl * (r_kj_norm / n_jkl_norm**2).unsqueeze(-1) # (..., n, 3)
|
| 476 |
-
grad_j = (projection_i - 1) * grad_i - projection_l * grad_l # (..., n, 3)
|
| 477 |
-
grad_k = (projection_l - 1) * grad_l - projection_i * grad_i # (..., n, 3)
|
| 478 |
-
# Returns (..., n); (..., 4, n, 3).
|
| 479 |
-
return angle, torch.stack((grad_i, grad_j, grad_k, grad_l), dim=1)
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
class AbsDihedralPotential(DihedralPotential):
|
| 483 |
-
"""Measure the unsigned magnitude of indexed torsion angles."""
|
| 484 |
-
|
| 485 |
-
def compute_variable(
|
| 486 |
-
self,
|
| 487 |
-
coords: torch.Tensor,
|
| 488 |
-
index: torch.Tensor,
|
| 489 |
-
ref_coords: torch.Tensor | None = None,
|
| 490 |
-
ref_mask: torch.Tensor | None = None,
|
| 491 |
-
compute_gradient: bool = False,
|
| 492 |
-
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 493 |
-
# coords: (..., a, 3); index: (4, n).
|
| 494 |
-
del ref_coords, ref_mask
|
| 495 |
-
if not compute_gradient:
|
| 496 |
-
return torch.abs(super().compute_variable(coords, index)) # (..., n)
|
| 497 |
-
angle, gradient = super().compute_variable(
|
| 498 |
-
coords,
|
| 499 |
-
index,
|
| 500 |
-
compute_gradient=True,
|
| 501 |
-
) # (..., n); (..., 4, n, 3)
|
| 502 |
-
gradient[(angle < 0)[..., None, :, None].expand_as(gradient)] *= -1 # same
|
| 503 |
-
return torch.abs(angle), gradient # (..., n); (..., 4, n, 3)
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
def _element_radii(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 507 |
-
# feats["ref_element"]: (1, a, n_element).
|
| 508 |
-
element_radii = torch.zeros(
|
| 509 |
-
const.num_elements,
|
| 510 |
-
dtype=torch.float32,
|
| 511 |
-
device=feats["ref_element"].device,
|
| 512 |
-
) # (n_element,)
|
| 513 |
-
element_radii[1:119] = torch.tensor(
|
| 514 |
-
const.vdw_radii,
|
| 515 |
-
dtype=torch.float32,
|
| 516 |
-
device=element_radii.device,
|
| 517 |
-
) # (n_element,)
|
| 518 |
-
# (a,)
|
| 519 |
-
return (feats["ref_element"].float() @ element_radii.unsqueeze(-1)).squeeze(-1)[0]
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 523 |
-
# atom_to_token: (1, a, l); asym_id: (1, l).
|
| 524 |
-
return (
|
| 525 |
-
torch.bmm(
|
| 526 |
-
feats["atom_to_token"].float(),
|
| 527 |
-
feats["asym_id"].unsqueeze(-1).float(),
|
| 528 |
-
)
|
| 529 |
-
.squeeze(-1)
|
| 530 |
-
.long()
|
| 531 |
-
)[0] # (a,)
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
class PoseBustersPotential(FlatBottomPotential, DistancePotential):
|
| 535 |
-
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 536 |
-
# rdkit_bounds_index: (1, 2, n); bound and mask features: (1, n).
|
| 537 |
-
pair_index = feats["rdkit_bounds_index"][0] # (2, n)
|
| 538 |
-
lower = feats["rdkit_lower_bounds"][0].clone() # (n,)
|
| 539 |
-
upper = feats["rdkit_upper_bounds"][0].clone() # (n,)
|
| 540 |
-
bond = feats["rdkit_bounds_bond_mask"][0] # (n,)
|
| 541 |
-
angle = feats["rdkit_bounds_angle_mask"][0] # (n,)
|
| 542 |
-
lower[bond * ~angle] *= 1.0 - parameters["bond_buffer"] # (n,)
|
| 543 |
-
upper[bond * ~angle] *= 1.0 + parameters["bond_buffer"] # (n,)
|
| 544 |
-
lower[~bond * angle] *= 1.0 - parameters["angle_buffer"] # (n,)
|
| 545 |
-
upper[~bond * angle] *= 1.0 + parameters["angle_buffer"] # (n,)
|
| 546 |
-
shared_buffer = min(parameters["bond_buffer"], parameters["angle_buffer"])
|
| 547 |
-
lower[bond * angle] *= 1.0 - shared_buffer # (n,)
|
| 548 |
-
upper[bond * angle] *= 1.0 + shared_buffer # (n,)
|
| 549 |
-
lower[~bond * ~angle] *= 1.0 - parameters["clash_buffer"] # (n,)
|
| 550 |
-
upper[~bond * ~angle] = float("inf") # (n,)
|
| 551 |
-
|
| 552 |
-
atom_radii = _element_radii(feats) # (a,)
|
| 553 |
-
bond_cutoff = 0.35 + atom_radii[pair_index].mean(dim=0) # (n,)
|
| 554 |
-
lower[~bond] = torch.max(lower[~bond], bond_cutoff[~bond]) # (n,)
|
| 555 |
-
upper[bond] = torch.min(upper[bond], bond_cutoff[bond]) # (n,)
|
| 556 |
-
# Returns index (2, n) and three function arguments of shape (n,).
|
| 557 |
-
return pair_index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
class ConnectionsPotential(FlatBottomPotential, DistancePotential):
|
| 561 |
-
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 562 |
-
# connected_atom_index: (1, 2, n).
|
| 563 |
-
pair_index = feats["connected_atom_index"][0] # (2, n)
|
| 564 |
-
upper = torch.full(
|
| 565 |
-
(pair_index.shape[1],),
|
| 566 |
-
parameters["buffer"],
|
| 567 |
-
device=pair_index.device,
|
| 568 |
-
) # (n,)
|
| 569 |
-
# Returns index (2, n), k (n,), and upper bound (n,).
|
| 570 |
-
return pair_index, (torch.ones_like(upper), None, upper), None, None, None
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
class VDWOverlapPotential(FlatBottomPotential, DistancePotential):
|
| 574 |
-
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 575 |
-
atom_chain_id = _atom_chain_ids(feats) # (a,)
|
| 576 |
-
atom_pad_mask = feats["atom_pad_mask"][0].bool() # (a,)
|
| 577 |
-
chain_sizes = torch.bincount(atom_chain_id[atom_pad_mask]) # (n_chain,)
|
| 578 |
-
nonion_atom = (chain_sizes > 1)[atom_chain_id] # (a,)
|
| 579 |
-
atom_radii = _element_radii(feats) # (a,)
|
| 580 |
-
pair_index = torch.triu_indices(
|
| 581 |
-
atom_chain_id.shape[0],
|
| 582 |
-
atom_chain_id.shape[0],
|
| 583 |
-
1,
|
| 584 |
-
device=atom_chain_id.device,
|
| 585 |
-
) # (2, n_pair)
|
| 586 |
-
pair_pad_mask = atom_pad_mask[pair_index].all(dim=0) # (n_pair,)
|
| 587 |
-
# (n_pair,)
|
| 588 |
-
pair_ion_mask = nonion_atom[pair_index[0]] * nonion_atom[pair_index[1]]
|
| 589 |
-
|
| 590 |
-
num_chains = atom_chain_id.max() + 1 # ()
|
| 591 |
-
connected = feats["connected_chain_index"][0] # (2, n_connection)
|
| 592 |
-
connected_matrix = torch.eye(
|
| 593 |
-
num_chains,
|
| 594 |
-
device=atom_chain_id.device,
|
| 595 |
-
dtype=torch.bool,
|
| 596 |
-
) # (n_chain, n_chain)
|
| 597 |
-
connected_matrix[connected[0], connected[1]] = True # (n_chain, n_chain)
|
| 598 |
-
connected_matrix[connected[1], connected[0]] = True # (n_chain, n_chain)
|
| 599 |
-
connected_pair = connected_matrix[
|
| 600 |
-
atom_chain_id[pair_index[0]],
|
| 601 |
-
atom_chain_id[pair_index[1]],
|
| 602 |
-
] # (n_pair,)
|
| 603 |
-
# (2, n)
|
| 604 |
-
pair_index = pair_index[:, pair_pad_mask * pair_ion_mask * ~connected_pair]
|
| 605 |
-
# (n,)
|
| 606 |
-
lower = atom_radii[pair_index].sum(dim=0) * (1.0 - parameters["buffer"])
|
| 607 |
-
# Returns index (2, n), k (n,), and lower bound (n,).
|
| 608 |
-
return pair_index, (torch.ones_like(lower), lower, None), None, None, None
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
class SymmetricChainCOMPotential(FlatBottomPotential, DistancePotential):
|
| 612 |
-
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 613 |
-
atom_chain_id = _atom_chain_ids(feats) # (a,)
|
| 614 |
-
atom_pad_mask = feats["atom_pad_mask"][0].bool() # (a,)
|
| 615 |
-
nonion_chain = torch.bincount(atom_chain_id[atom_pad_mask]) > 1 # (n_chain,)
|
| 616 |
-
pair_index = feats["symmetric_chain_index"][0] # (2, n_candidate)
|
| 617 |
-
pair_index = pair_index[
|
| 618 |
-
:,
|
| 619 |
-
nonion_chain[pair_index[0]] * nonion_chain[pair_index[1]],
|
| 620 |
-
] # (2, n)
|
| 621 |
-
lower = torch.full(
|
| 622 |
-
(pair_index.shape[1],),
|
| 623 |
-
parameters["buffer"],
|
| 624 |
-
dtype=torch.float32,
|
| 625 |
-
device=pair_index.device,
|
| 626 |
-
) # (n,)
|
| 627 |
-
# Returns center index (2, n), k/lower (n,), and atom-to-center maps (a,).
|
| 628 |
-
return (
|
| 629 |
-
pair_index,
|
| 630 |
-
(torch.ones_like(lower), lower, None),
|
| 631 |
-
(atom_chain_id, atom_pad_mask),
|
| 632 |
-
None,
|
| 633 |
-
None,
|
| 634 |
-
)
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
def _oriented_bounds(
|
| 638 |
-
orientations: torch.Tensor,
|
| 639 |
-
positive_lower: float,
|
| 640 |
-
negative_upper: float,
|
| 641 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 642 |
-
# orientations: (n,).
|
| 643 |
-
lower = torch.zeros(orientations.shape, device=orientations.device) # (n,)
|
| 644 |
-
upper = torch.zeros(orientations.shape, device=orientations.device) # (n,)
|
| 645 |
-
lower[orientations] = positive_lower # (n,)
|
| 646 |
-
upper[orientations] = float("inf") # (n,)
|
| 647 |
-
lower[~orientations] = float("-inf") # (n,)
|
| 648 |
-
upper[~orientations] = negative_upper # (n,)
|
| 649 |
-
return lower, upper # each (n,)
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
class StereoBondPotential(FlatBottomPotential, AbsDihedralPotential):
|
| 653 |
-
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 654 |
-
# stereo_bond_index: (1, 4, n); orientations: (1, n).
|
| 655 |
-
index = feats["stereo_bond_index"][0] # (4, n)
|
| 656 |
-
orientation = feats["stereo_bond_orientations"][0].bool() # (n,)
|
| 657 |
-
lower, upper = _oriented_bounds(
|
| 658 |
-
orientation,
|
| 659 |
-
torch.pi - parameters["buffer"],
|
| 660 |
-
parameters["buffer"],
|
| 661 |
-
) # each (n,)
|
| 662 |
-
# Returns index (4, n) and three function arguments of shape (n,).
|
| 663 |
-
return index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
class ChiralAtomPotential(FlatBottomPotential, DihedralPotential):
|
| 667 |
-
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 668 |
-
# chiral_atom_index: (1, 4, n); orientations: (1, n).
|
| 669 |
-
index = feats["chiral_atom_index"][0] # (4, n)
|
| 670 |
-
orientation = feats["chiral_atom_orientations"][0].bool() # (n,)
|
| 671 |
-
lower, upper = _oriented_bounds(
|
| 672 |
-
orientation,
|
| 673 |
-
parameters["buffer"],
|
| 674 |
-
-parameters["buffer"],
|
| 675 |
-
) # each (n,)
|
| 676 |
-
# Returns index (4, n) and three function arguments of shape (n,).
|
| 677 |
-
return index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
class PlanarBondPotential(FlatBottomPotential, AbsDihedralPotential):
|
| 681 |
-
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 682 |
-
# The feature stores two atom-index rows for each six-entry bond pattern.
|
| 683 |
-
bond_index = feats["planar_bond_index"][0].T # (2, n_bond_entry)
|
| 684 |
-
improper_pattern = torch.tensor(
|
| 685 |
-
[[1, 2, 3, 0], [4, 5, 0, 3]],
|
| 686 |
-
device=bond_index.device,
|
| 687 |
-
).T # (4, 2)
|
| 688 |
-
# (4, n_improper)
|
| 689 |
-
improper_index = bond_index[:, improper_pattern].swapaxes(0, 1).flatten(start_dim=1)
|
| 690 |
-
upper = torch.full(
|
| 691 |
-
(improper_index.shape[1],),
|
| 692 |
-
parameters["buffer"],
|
| 693 |
-
device=improper_index.device,
|
| 694 |
-
) # (n_improper,)
|
| 695 |
-
# Returns index (4, n_improper), k (n_improper,), and upper (n_improper,).
|
| 696 |
-
return (
|
| 697 |
-
improper_index,
|
| 698 |
-
(torch.ones_like(upper), None, upper),
|
| 699 |
-
None,
|
| 700 |
-
None,
|
| 701 |
-
None,
|
| 702 |
-
)
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
class TemplateReferencePotential(FlatBottomPotential, ReferencePotential):
|
| 706 |
-
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 707 |
-
del parameters
|
| 708 |
-
if "template_mask_cb" not in feats or "template_force" not in feats:
|
| 709 |
-
# Empty sentinel index: (1, 0).
|
| 710 |
-
return torch.empty((1, 0)), None, None, None, None
|
| 711 |
-
# template_mask_cb/template_force: (b, n_template, n)/(b, n_template).
|
| 712 |
-
template_mask = feats["template_mask_cb"][feats["template_force"]] # (n_ref, n)
|
| 713 |
-
if template_mask.shape[0] == 0:
|
| 714 |
-
# Empty sentinel index: (1, 0).
|
| 715 |
-
return torch.empty((1, 0)), None, None, None, None
|
| 716 |
-
|
| 717 |
-
ref_coords = feats["template_cb"][feats["template_force"]].clone() # (n_ref, n, 3)
|
| 718 |
-
ref_mask = feats["template_mask_cb"][feats["template_force"]].clone() # (n_ref, n)
|
| 719 |
-
atom_indices = torch.arange(
|
| 720 |
-
feats["atom_pad_mask"].shape[1],
|
| 721 |
-
device=feats["atom_pad_mask"].device,
|
| 722 |
-
dtype=torch.float32,
|
| 723 |
-
)[None, :, None] # (1, a, 1)
|
| 724 |
-
ref_atom_index = (
|
| 725 |
-
torch.bmm(
|
| 726 |
-
feats["token_to_rep_atom"].float(),
|
| 727 |
-
atom_indices,
|
| 728 |
-
)
|
| 729 |
-
.squeeze(-1)
|
| 730 |
-
.long()[0] # (n,)
|
| 731 |
-
)
|
| 732 |
-
ref_token_index = (
|
| 733 |
-
torch.bmm(
|
| 734 |
-
feats["atom_to_token"].float(),
|
| 735 |
-
feats["token_index"].unsqueeze(-1).float(),
|
| 736 |
-
)
|
| 737 |
-
.squeeze(-1)
|
| 738 |
-
.long()[0] # (a,)
|
| 739 |
-
)
|
| 740 |
-
|
| 741 |
-
index = torch.arange(
|
| 742 |
-
template_mask.shape[-1],
|
| 743 |
-
dtype=torch.long,
|
| 744 |
-
device=template_mask.device,
|
| 745 |
-
)[None] # (1, n)
|
| 746 |
-
upper = torch.full(
|
| 747 |
-
template_mask.shape,
|
| 748 |
-
float("inf"),
|
| 749 |
-
device=index.device,
|
| 750 |
-
dtype=torch.float32,
|
| 751 |
-
) # (n_ref, n)
|
| 752 |
-
reference_indices = torch.argwhere(template_mask).T # (2, n_active)
|
| 753 |
-
upper[reference_indices.unbind()] = feats["template_force_threshold"][
|
| 754 |
-
feats["template_force"]
|
| 755 |
-
][reference_indices[0]] # (n_ref, n)
|
| 756 |
-
# Returns index (1, n), function args (n_ref, n), and reference mappings.
|
| 757 |
-
return (
|
| 758 |
-
index,
|
| 759 |
-
(torch.ones_like(upper), None, upper),
|
| 760 |
-
None,
|
| 761 |
-
(ref_coords, ref_mask, ref_atom_index, ref_token_index),
|
| 762 |
-
None,
|
| 763 |
-
)
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
class ContactPotentital(FlatBottomPotential, DistancePotential):
|
| 767 |
-
"""Contact-union potential retaining the historical checkpoint name."""
|
| 768 |
-
|
| 769 |
-
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 770 |
-
del parameters
|
| 771 |
-
# pair index: (1, 2, n); threshold/operator features: (1, n).
|
| 772 |
-
index = feats["contact_pair_index"][0] # (2, n)
|
| 773 |
-
upper = feats["contact_thresholds"][0].clone() # (n,)
|
| 774 |
-
# Returns index (2, n), k/upper (n,), and two operator tensors (n,).
|
| 775 |
-
return (
|
| 776 |
-
index,
|
| 777 |
-
(torch.ones_like(upper), None, upper),
|
| 778 |
-
None,
|
| 779 |
-
None,
|
| 780 |
-
(
|
| 781 |
-
feats["contact_negation_mask"][0],
|
| 782 |
-
feats["contact_union_index"][0],
|
| 783 |
-
),
|
| 784 |
-
)
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
ContactPotential = ContactPotentital
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
def get_potentials(
|
| 791 |
-
steering_args: dict[str, bool],
|
| 792 |
-
boltz2: bool = False,
|
| 793 |
-
) -> list[Potential]:
|
| 794 |
-
"""Build the ordered potential stack for requested steering modes."""
|
| 795 |
-
|
| 796 |
-
use_fk = steering_args["fk_steering"]
|
| 797 |
-
use_physical = steering_args["physical_guidance_update"]
|
| 798 |
-
use_contacts = steering_args.get("contact_guidance_update", False)
|
| 799 |
-
potentials: list[Potential] = []
|
| 800 |
-
if use_fk or use_physical:
|
| 801 |
-
potentials.extend(
|
| 802 |
-
(
|
| 803 |
-
SymmetricChainCOMPotential(
|
| 804 |
-
{
|
| 805 |
-
"guidance_interval": 4,
|
| 806 |
-
"guidance_weight": 0.5 if use_physical else 0.0,
|
| 807 |
-
"resampling_weight": 0.5,
|
| 808 |
-
"buffer": ExponentialInterpolation(1.0, 5.0, -2.0),
|
| 809 |
-
}
|
| 810 |
-
),
|
| 811 |
-
VDWOverlapPotential(
|
| 812 |
-
{
|
| 813 |
-
"guidance_interval": 5,
|
| 814 |
-
"guidance_weight": PiecewiseStepFunction(
|
| 815 |
-
[0.4],
|
| 816 |
-
[0.125, 0.0],
|
| 817 |
-
)
|
| 818 |
-
if use_physical
|
| 819 |
-
else 0.0,
|
| 820 |
-
"resampling_weight": PiecewiseStepFunction(
|
| 821 |
-
[0.6],
|
| 822 |
-
[0.01, 0.0],
|
| 823 |
-
),
|
| 824 |
-
"buffer": 0.225,
|
| 825 |
-
}
|
| 826 |
-
),
|
| 827 |
-
ConnectionsPotential(
|
| 828 |
-
{
|
| 829 |
-
"guidance_interval": 1,
|
| 830 |
-
"guidance_weight": 0.15 if use_physical else 0.0,
|
| 831 |
-
"resampling_weight": 1.0,
|
| 832 |
-
"buffer": 2.0,
|
| 833 |
-
}
|
| 834 |
-
),
|
| 835 |
-
PoseBustersPotential(
|
| 836 |
-
{
|
| 837 |
-
"guidance_interval": 1,
|
| 838 |
-
"guidance_weight": 0.01 if use_physical else 0.0,
|
| 839 |
-
"resampling_weight": 0.1,
|
| 840 |
-
"bond_buffer": 0.125,
|
| 841 |
-
"angle_buffer": 0.125,
|
| 842 |
-
"clash_buffer": 0.10,
|
| 843 |
-
}
|
| 844 |
-
),
|
| 845 |
-
ChiralAtomPotential(
|
| 846 |
-
{
|
| 847 |
-
"guidance_interval": 1,
|
| 848 |
-
"guidance_weight": 0.1 if use_physical else 0.0,
|
| 849 |
-
"resampling_weight": 1.0,
|
| 850 |
-
"buffer": 0.52360,
|
| 851 |
-
}
|
| 852 |
-
),
|
| 853 |
-
StereoBondPotential(
|
| 854 |
-
{
|
| 855 |
-
"guidance_interval": 1,
|
| 856 |
-
"guidance_weight": 0.05 if use_physical else 0.0,
|
| 857 |
-
"resampling_weight": 1.0,
|
| 858 |
-
"buffer": 0.52360,
|
| 859 |
-
}
|
| 860 |
-
),
|
| 861 |
-
PlanarBondPotential(
|
| 862 |
-
{
|
| 863 |
-
"guidance_interval": 1,
|
| 864 |
-
"guidance_weight": 0.05 if use_physical else 0.0,
|
| 865 |
-
"resampling_weight": 1.0,
|
| 866 |
-
"buffer": 0.26180,
|
| 867 |
-
}
|
| 868 |
-
),
|
| 869 |
-
)
|
| 870 |
-
)
|
| 871 |
-
if boltz2 and (use_fk or use_contacts):
|
| 872 |
-
potentials.extend(
|
| 873 |
-
(
|
| 874 |
-
ContactPotentital(
|
| 875 |
-
{
|
| 876 |
-
"guidance_interval": 4,
|
| 877 |
-
"guidance_weight": PiecewiseStepFunction(
|
| 878 |
-
[0.25, 0.75],
|
| 879 |
-
[0.0, 0.5, 1.0],
|
| 880 |
-
)
|
| 881 |
-
if use_contacts
|
| 882 |
-
else 0.0,
|
| 883 |
-
"resampling_weight": 1.0,
|
| 884 |
-
"union_lambda": ExponentialInterpolation(8.0, 0.0, -2.0),
|
| 885 |
-
}
|
| 886 |
-
),
|
| 887 |
-
TemplateReferencePotential(
|
| 888 |
-
{
|
| 889 |
-
"guidance_interval": 2,
|
| 890 |
-
"guidance_weight": 0.1 if use_contacts else 0.0,
|
| 891 |
-
"resampling_weight": 1.0,
|
| 892 |
-
}
|
| 893 |
-
),
|
| 894 |
-
)
|
| 895 |
-
)
|
| 896 |
-
return potentials
|
|
|
|
| 1 |
+
"""Differentiable structure-steering potentials for Boltz2 diffusion.
|
| 2 |
+
|
| 3 |
+
Each potential follows the same mechanism: derive constraint indices and
|
| 4 |
+
bounds from features, evaluate a geometric variable, map that variable to an
|
| 5 |
+
energy, and optionally scatter its analytic derivative back to atom
|
| 6 |
+
coordinates. This module is maintained independently from the pinned parity
|
| 7 |
+
oracle while preserving the public class names used by converted checkpoints.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
from abc import ABC, abstractmethod
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from typing import Any
|
| 17 |
+
|
| 18 |
+
from . import vb_const as const
|
| 19 |
+
from .vb_loss_diffusionv2 import weighted_rigid_align
|
| 20 |
+
from .vb_potentials_schedules import (
|
| 21 |
+
ExponentialInterpolation,
|
| 22 |
+
ParameterSchedule,
|
| 23 |
+
PiecewiseStepFunction,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
Parameter = ParameterSchedule | float | int | bool
|
| 28 |
+
ParameterMap = dict[str, Parameter]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass(frozen=True, slots=True)
|
| 32 |
+
class _PreparedPotential:
|
| 33 |
+
coordinates: torch.Tensor # (..., a, 3)
|
| 34 |
+
index: torch.Tensor # (q, n)
|
| 35 |
+
function_args: tuple[Any, ...]
|
| 36 |
+
com_index: torch.Tensor | None # (a_input,)
|
| 37 |
+
atom_pad_mask: torch.Tensor | None # (a_input,)
|
| 38 |
+
ref_coords: torch.Tensor | None # (n_ref, n, 3)
|
| 39 |
+
ref_mask: torch.Tensor | None # (n_ref, n)
|
| 40 |
+
ref_token_index: torch.Tensor | None # (a_input,)
|
| 41 |
+
negation_mask: torch.Tensor | None # (n,)
|
| 42 |
+
union_index: torch.Tensor | None # (n,)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _reduce_atoms_to_centers(
|
| 46 |
+
coordinates: torch.Tensor,
|
| 47 |
+
com_index: torch.Tensor,
|
| 48 |
+
atom_pad_mask: torch.Tensor,
|
| 49 |
+
) -> torch.Tensor:
|
| 50 |
+
# coordinates: (..., a, 3); com_index/atom_pad_mask: (a,).
|
| 51 |
+
unpadded_index = com_index[atom_pad_mask] # (a_valid,)
|
| 52 |
+
unpadded_coordinates = coordinates[..., atom_pad_mask, :] # (..., a_valid, 3)
|
| 53 |
+
center_shape = (*unpadded_coordinates.shape[:-2], unpadded_index.max() + 1, 3)
|
| 54 |
+
return torch.zeros(center_shape, device=coordinates.device).scatter_reduce(
|
| 55 |
+
-2,
|
| 56 |
+
unpadded_index.unsqueeze(-1).expand_as(unpadded_coordinates),
|
| 57 |
+
unpadded_coordinates,
|
| 58 |
+
"mean",
|
| 59 |
+
) # (..., n_center, 3)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _union_weights(
|
| 63 |
+
energy: torch.Tensor,
|
| 64 |
+
union_index: torch.Tensor,
|
| 65 |
+
union_lambda: float,
|
| 66 |
+
) -> torch.Tensor:
|
| 67 |
+
# energy: (..., n); union_index: (n,).
|
| 68 |
+
unnormalized = torch.exp(-union_lambda * energy) # (..., n)
|
| 69 |
+
partition = torch.zeros(
|
| 70 |
+
(*energy.shape[:-1], union_index.max() + 1),
|
| 71 |
+
device=union_index.device,
|
| 72 |
+
).scatter_reduce(
|
| 73 |
+
-1,
|
| 74 |
+
union_index.expand_as(unnormalized),
|
| 75 |
+
unnormalized,
|
| 76 |
+
"sum",
|
| 77 |
+
) # (..., n_union)
|
| 78 |
+
weights = unnormalized / partition[..., union_index] # (..., n)
|
| 79 |
+
weights[partition[..., union_index] == 0] = 0 # (..., n)
|
| 80 |
+
return weights # (..., n)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _scatter_constraint_gradients(
|
| 84 |
+
coefficients: torch.Tensor,
|
| 85 |
+
variable_gradient: torch.Tensor,
|
| 86 |
+
index: torch.Tensor,
|
| 87 |
+
coordinates: torch.Tensor,
|
| 88 |
+
) -> torch.Tensor:
|
| 89 |
+
# coefficients: (..., n); variable_gradient: (..., q, n, 3).
|
| 90 |
+
# index: (q, n); coordinates: (b, a, 3).
|
| 91 |
+
product = coefficients.tile(variable_gradient.shape[-3]).unsqueeze(
|
| 92 |
+
-1
|
| 93 |
+
) * variable_gradient.flatten(start_dim=-3, end_dim=-2) # (..., q * n, 3)
|
| 94 |
+
if product.dim() > 3:
|
| 95 |
+
product = product.sum(dim=list(range(1, product.dim() - 2))) # (b, q * n, 3)
|
| 96 |
+
scatter_index = (
|
| 97 |
+
index.flatten(start_dim=0, end_dim=1).unsqueeze(-1).expand((*coordinates.shape[:-2], -1, 3))
|
| 98 |
+
) # (b, q * n, 3)
|
| 99 |
+
return torch.zeros_like(coordinates).scatter_reduce(
|
| 100 |
+
-2,
|
| 101 |
+
scatter_index,
|
| 102 |
+
product,
|
| 103 |
+
"sum",
|
| 104 |
+
) # (b, a, 3)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class Potential(ABC):
|
| 108 |
+
"""Base contract for energy and analytic-gradient steering potentials."""
|
| 109 |
+
|
| 110 |
+
def __init__(self, parameters: ParameterMap | None = None) -> None:
|
| 111 |
+
self.parameters = parameters
|
| 112 |
+
|
| 113 |
+
def _prepare(
|
| 114 |
+
self,
|
| 115 |
+
coordinates: torch.Tensor,
|
| 116 |
+
feats: dict[str, torch.Tensor],
|
| 117 |
+
parameters: dict[str, Any],
|
| 118 |
+
computed_args: tuple[Any, ...] | None = None,
|
| 119 |
+
) -> _PreparedPotential:
|
| 120 |
+
# coordinates: (..., a_input, 3).
|
| 121 |
+
if computed_args is None:
|
| 122 |
+
computed_args = self.compute_args(feats, parameters)
|
| 123 |
+
index, args, com_args, ref_args, operator_args = computed_args # index: (q, n)
|
| 124 |
+
com_index = atom_pad_mask = None
|
| 125 |
+
if com_args is not None:
|
| 126 |
+
com_index, atom_pad_mask = com_args # each (a_input,)
|
| 127 |
+
coordinates = _reduce_atoms_to_centers(
|
| 128 |
+
coordinates,
|
| 129 |
+
com_index,
|
| 130 |
+
atom_pad_mask,
|
| 131 |
+
) # (..., n_center, 3)
|
| 132 |
+
|
| 133 |
+
ref_coords = ref_mask = ref_token_index = None
|
| 134 |
+
if ref_args is not None:
|
| 135 |
+
# ref_coords: (n_ref, n, 3); ref_mask: (n_ref, n).
|
| 136 |
+
# ref_atom_index: (n,); ref_token_index: (a_input,).
|
| 137 |
+
ref_coords, ref_mask, ref_atom_index, ref_token_index = ref_args
|
| 138 |
+
coordinates = coordinates[..., ref_atom_index, :] # (..., n, 3)
|
| 139 |
+
|
| 140 |
+
negation_mask = union_index = None
|
| 141 |
+
if operator_args is not None:
|
| 142 |
+
negation_mask, union_index = operator_args # each (n,)
|
| 143 |
+
return _PreparedPotential(
|
| 144 |
+
coordinates=coordinates,
|
| 145 |
+
index=index,
|
| 146 |
+
function_args=args,
|
| 147 |
+
com_index=com_index,
|
| 148 |
+
atom_pad_mask=atom_pad_mask,
|
| 149 |
+
ref_coords=ref_coords,
|
| 150 |
+
ref_mask=ref_mask,
|
| 151 |
+
ref_token_index=ref_token_index,
|
| 152 |
+
negation_mask=negation_mask,
|
| 153 |
+
union_index=union_index,
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
def compute(
|
| 157 |
+
self,
|
| 158 |
+
coords: torch.Tensor,
|
| 159 |
+
feats: dict[str, torch.Tensor],
|
| 160 |
+
parameters: dict[str, Any],
|
| 161 |
+
) -> torch.Tensor:
|
| 162 |
+
"""Evaluate one energy per coordinate sample."""
|
| 163 |
+
|
| 164 |
+
# coords: (..., a, 3).
|
| 165 |
+
computed_args = self.compute_args(feats, parameters)
|
| 166 |
+
if computed_args[0].shape[1] == 0:
|
| 167 |
+
return torch.zeros(coords.shape[:-2], device=coords.device) # (...)
|
| 168 |
+
prepared = self._prepare(coords, feats, parameters, computed_args)
|
| 169 |
+
value = self.compute_variable(
|
| 170 |
+
prepared.coordinates,
|
| 171 |
+
prepared.index,
|
| 172 |
+
ref_coords=prepared.ref_coords,
|
| 173 |
+
ref_mask=prepared.ref_mask,
|
| 174 |
+
compute_gradient=False,
|
| 175 |
+
) # (..., n)
|
| 176 |
+
energy = self.compute_function(
|
| 177 |
+
value,
|
| 178 |
+
*prepared.function_args,
|
| 179 |
+
negation_mask=prepared.negation_mask,
|
| 180 |
+
compute_derivative=False,
|
| 181 |
+
) # (..., n)
|
| 182 |
+
if prepared.union_index is not None:
|
| 183 |
+
weights = _union_weights(
|
| 184 |
+
energy,
|
| 185 |
+
prepared.union_index,
|
| 186 |
+
parameters["union_lambda"],
|
| 187 |
+
) # (..., n)
|
| 188 |
+
return (energy * weights).sum(dim=-1) # (...)
|
| 189 |
+
return energy.sum(dim=tuple(range(1, energy.dim()))) # (b,)
|
| 190 |
+
|
| 191 |
+
def compute_gradient(
|
| 192 |
+
self,
|
| 193 |
+
coords: torch.Tensor,
|
| 194 |
+
feats: dict[str, torch.Tensor],
|
| 195 |
+
parameters: dict[str, Any],
|
| 196 |
+
) -> torch.Tensor:
|
| 197 |
+
"""Return the analytic coordinate gradient of the potential energy."""
|
| 198 |
+
|
| 199 |
+
# coords: (b, a, 3).
|
| 200 |
+
computed_args = self.compute_args(feats, parameters)
|
| 201 |
+
if computed_args[0].shape[1] == 0:
|
| 202 |
+
return torch.zeros_like(coords) # (b, a, 3)
|
| 203 |
+
prepared = self._prepare(coords, feats, parameters, computed_args)
|
| 204 |
+
value, variable_gradient = self.compute_variable(
|
| 205 |
+
prepared.coordinates,
|
| 206 |
+
prepared.index,
|
| 207 |
+
ref_coords=prepared.ref_coords,
|
| 208 |
+
ref_mask=prepared.ref_mask,
|
| 209 |
+
compute_gradient=True,
|
| 210 |
+
) # value: (..., n); variable_gradient: (..., q, n, 3)
|
| 211 |
+
energy, energy_derivative = self.compute_function(
|
| 212 |
+
value,
|
| 213 |
+
*prepared.function_args,
|
| 214 |
+
negation_mask=prepared.negation_mask,
|
| 215 |
+
compute_derivative=True,
|
| 216 |
+
) # each (..., n)
|
| 217 |
+
if prepared.union_index is not None:
|
| 218 |
+
weights = _union_weights(
|
| 219 |
+
energy,
|
| 220 |
+
prepared.union_index,
|
| 221 |
+
parameters["union_lambda"],
|
| 222 |
+
) # (..., n)
|
| 223 |
+
union_energy = torch.zeros(
|
| 224 |
+
(*energy.shape[:-1], prepared.union_index.max() + 1),
|
| 225 |
+
device=prepared.union_index.device,
|
| 226 |
+
).scatter_reduce(
|
| 227 |
+
-1,
|
| 228 |
+
prepared.union_index.expand_as(energy),
|
| 229 |
+
energy * weights,
|
| 230 |
+
"sum",
|
| 231 |
+
) # (..., n_union)
|
| 232 |
+
coefficients = (
|
| 233 |
+
energy_derivative
|
| 234 |
+
* weights
|
| 235 |
+
* (
|
| 236 |
+
1
|
| 237 |
+
+ parameters["union_lambda"]
|
| 238 |
+
* (energy - union_energy[..., prepared.union_index])
|
| 239 |
+
)
|
| 240 |
+
) # (..., n)
|
| 241 |
+
else:
|
| 242 |
+
coefficients = energy_derivative # (..., n)
|
| 243 |
+
|
| 244 |
+
atom_gradient = _scatter_constraint_gradients(
|
| 245 |
+
coefficients,
|
| 246 |
+
variable_gradient,
|
| 247 |
+
prepared.index,
|
| 248 |
+
prepared.coordinates,
|
| 249 |
+
) # (b, a_prepared, 3)
|
| 250 |
+
if prepared.com_index is not None:
|
| 251 |
+
atom_gradient = atom_gradient[..., prepared.com_index, :] # (b, a, 3)
|
| 252 |
+
elif prepared.ref_token_index is not None:
|
| 253 |
+
atom_gradient = atom_gradient[..., prepared.ref_token_index, :] # (b, a, 3)
|
| 254 |
+
return atom_gradient # (b, a, 3)
|
| 255 |
+
|
| 256 |
+
def compute_parameters(self, t: float) -> dict[str, Any] | None:
|
| 257 |
+
"""Resolve scheduled parameters at diffusion time ``t``."""
|
| 258 |
+
|
| 259 |
+
if self.parameters is None:
|
| 260 |
+
return None
|
| 261 |
+
return {
|
| 262 |
+
name: parameter.compute(t) if isinstance(parameter, ParameterSchedule) else parameter
|
| 263 |
+
for name, parameter in self.parameters.items()
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
@abstractmethod
|
| 267 |
+
def compute_function(
|
| 268 |
+
self,
|
| 269 |
+
value: torch.Tensor,
|
| 270 |
+
*args: Any,
|
| 271 |
+
negation_mask: torch.Tensor | None = None,
|
| 272 |
+
compute_derivative: bool = False,
|
| 273 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 274 |
+
# value: (..., n); negation_mask: (n,).
|
| 275 |
+
raise NotImplementedError
|
| 276 |
+
|
| 277 |
+
@abstractmethod
|
| 278 |
+
def compute_variable(
|
| 279 |
+
self,
|
| 280 |
+
coords: torch.Tensor,
|
| 281 |
+
index: torch.Tensor,
|
| 282 |
+
ref_coords: torch.Tensor | None = None,
|
| 283 |
+
ref_mask: torch.Tensor | None = None,
|
| 284 |
+
compute_gradient: bool = False,
|
| 285 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 286 |
+
# coords: (..., a, 3); index: (q, n).
|
| 287 |
+
raise NotImplementedError
|
| 288 |
+
|
| 289 |
+
@abstractmethod
|
| 290 |
+
def compute_args(
|
| 291 |
+
self,
|
| 292 |
+
feats: dict[str, torch.Tensor],
|
| 293 |
+
parameters: dict[str, Any],
|
| 294 |
+
) -> tuple[Any, ...]:
|
| 295 |
+
raise NotImplementedError
|
| 296 |
+
|
| 297 |
+
def get_reference_coords(
|
| 298 |
+
self,
|
| 299 |
+
feats: dict[str, torch.Tensor],
|
| 300 |
+
parameters: dict[str, Any],
|
| 301 |
+
) -> tuple[None, None]:
|
| 302 |
+
del feats, parameters
|
| 303 |
+
return None, None
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
class FlatBottomPotential(Potential):
|
| 307 |
+
"""Linear penalty outside an allowed lower-to-upper interval."""
|
| 308 |
+
|
| 309 |
+
def compute_function(
|
| 310 |
+
self,
|
| 311 |
+
value: torch.Tensor,
|
| 312 |
+
k: torch.Tensor,
|
| 313 |
+
lower_bounds: torch.Tensor | None,
|
| 314 |
+
upper_bounds: torch.Tensor | None,
|
| 315 |
+
negation_mask: torch.Tensor | None = None,
|
| 316 |
+
compute_derivative: bool = False,
|
| 317 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 318 |
+
# value: (..., n); k/lower_bounds/upper_bounds: (n,) or (..., n).
|
| 319 |
+
# negation_mask: (n,) or a shape broadcastable to (..., n).
|
| 320 |
+
lower = (
|
| 321 |
+
(torch.full_like(value, float("-inf")) if lower_bounds is None else lower_bounds)
|
| 322 |
+
.expand_as(value)
|
| 323 |
+
.clone()
|
| 324 |
+
) # (..., n)
|
| 325 |
+
upper = (
|
| 326 |
+
(torch.full_like(value, float("inf")) if upper_bounds is None else upper_bounds)
|
| 327 |
+
.expand_as(value)
|
| 328 |
+
.clone()
|
| 329 |
+
) # (..., n)
|
| 330 |
+
|
| 331 |
+
if negation_mask is not None:
|
| 332 |
+
if not torch.is_tensor(negation_mask):
|
| 333 |
+
raise TypeError(
|
| 334 |
+
f"negation_mask must be a boolean tensor, got {type(negation_mask).__name__}."
|
| 335 |
+
)
|
| 336 |
+
if negation_mask.dtype != torch.bool:
|
| 337 |
+
raise TypeError(
|
| 338 |
+
f"negation_mask must be a boolean tensor, got {negation_mask.dtype}."
|
| 339 |
+
)
|
| 340 |
+
if negation_mask.device != value.device:
|
| 341 |
+
raise ValueError(
|
| 342 |
+
"negation_mask must be on the same device as value; "
|
| 343 |
+
f"got {negation_mask.device} and {value.device}."
|
| 344 |
+
)
|
| 345 |
+
try:
|
| 346 |
+
expanded_negation_mask = negation_mask.expand_as(value) # (..., n)
|
| 347 |
+
except RuntimeError as error:
|
| 348 |
+
raise ValueError(
|
| 349 |
+
"negation_mask must be broadcastable to value shape "
|
| 350 |
+
f"{tuple(value.shape)}, got {tuple(negation_mask.shape)}."
|
| 351 |
+
) from error
|
| 352 |
+
unbounded_below = torch.isneginf(lower) # (..., n)
|
| 353 |
+
unbounded_above = torch.isposinf(upper) # (..., n)
|
| 354 |
+
# (..., n)
|
| 355 |
+
valid_negation = unbounded_below | unbounded_above | expanded_negation_mask
|
| 356 |
+
if not bool(torch.all(valid_negation).item()):
|
| 357 |
+
raise ValueError(
|
| 358 |
+
"negation_mask may be false only where at least one bound is infinite."
|
| 359 |
+
)
|
| 360 |
+
select_upper = ~unbounded_above & ~expanded_negation_mask # (..., n)
|
| 361 |
+
lower[select_upper] = upper[select_upper] # (..., n)
|
| 362 |
+
upper[select_upper] = float("inf") # (..., n)
|
| 363 |
+
select_lower = ~unbounded_below & ~expanded_negation_mask # (..., n)
|
| 364 |
+
upper[select_lower] = lower[select_lower] # (..., n)
|
| 365 |
+
lower[select_lower] = float("-inf") # (..., n)
|
| 366 |
+
|
| 367 |
+
below = value < lower # (..., n)
|
| 368 |
+
above = value > upper # (..., n)
|
| 369 |
+
energy = torch.zeros_like(value) # (..., n)
|
| 370 |
+
energy[below] = (k * (lower - value))[below] # (..., n)
|
| 371 |
+
energy[above] = (k * (value - upper))[above] # (..., n)
|
| 372 |
+
if not compute_derivative:
|
| 373 |
+
return energy # (..., n)
|
| 374 |
+
derivative = torch.zeros_like(value) # (..., n)
|
| 375 |
+
derivative[below] = -k.expand_as(below)[below] # (..., n)
|
| 376 |
+
derivative[above] = k.expand_as(above)[above] # (..., n)
|
| 377 |
+
return energy, derivative # each (..., n)
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
class ReferencePotential(Potential):
|
| 381 |
+
"""Measure atom displacement after weighted rigid alignment."""
|
| 382 |
+
|
| 383 |
+
def compute_variable(
|
| 384 |
+
self,
|
| 385 |
+
coords: torch.Tensor,
|
| 386 |
+
index: torch.Tensor,
|
| 387 |
+
ref_coords: torch.Tensor,
|
| 388 |
+
ref_mask: torch.Tensor,
|
| 389 |
+
compute_gradient: bool = False,
|
| 390 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 391 |
+
# coords: (b, a, 3); index: (1, n).
|
| 392 |
+
# ref_coords: (n_ref, n, 3); ref_mask: (n_ref, n).
|
| 393 |
+
aligned_reference = weighted_rigid_align(
|
| 394 |
+
ref_coords.float(),
|
| 395 |
+
coords[:, index].float(),
|
| 396 |
+
ref_mask,
|
| 397 |
+
ref_mask,
|
| 398 |
+
) # (b, n_ref, n, 3)
|
| 399 |
+
displacement = coords[:, index] - aligned_reference # (b, n_ref, n, 3)
|
| 400 |
+
distance = torch.linalg.norm(displacement, dim=-1) # (b, n_ref, n)
|
| 401 |
+
if not compute_gradient:
|
| 402 |
+
return distance # (b, n_ref, n)
|
| 403 |
+
unit_displacement = displacement / distance.unsqueeze(-1) # (b, n_ref, n, 3)
|
| 404 |
+
# (b, 1, n_ref, n, 3)
|
| 405 |
+
gradient = (unit_displacement * ref_mask.unsqueeze(-1)).unsqueeze(1)
|
| 406 |
+
return distance, gradient # (b, n_ref, n); (b, 1, n_ref, n, 3)
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
class DistancePotential(Potential):
|
| 410 |
+
"""Measure Euclidean distances for indexed atom pairs."""
|
| 411 |
+
|
| 412 |
+
def compute_variable(
|
| 413 |
+
self,
|
| 414 |
+
coords: torch.Tensor,
|
| 415 |
+
index: torch.Tensor,
|
| 416 |
+
ref_coords: torch.Tensor | None = None,
|
| 417 |
+
ref_mask: torch.Tensor | None = None,
|
| 418 |
+
compute_gradient: bool = False,
|
| 419 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 420 |
+
# coords: (..., a, 3); index: (2, n).
|
| 421 |
+
del ref_coords, ref_mask
|
| 422 |
+
displacement = coords.index_select(-2, index[0]) - coords.index_select(
|
| 423 |
+
-2,
|
| 424 |
+
index[1],
|
| 425 |
+
) # (..., n, 3)
|
| 426 |
+
distance = torch.linalg.norm(displacement, dim=-1) # (..., n)
|
| 427 |
+
if not compute_gradient:
|
| 428 |
+
return distance # (..., n)
|
| 429 |
+
unit_displacement = displacement / distance.unsqueeze(-1) # (..., n, 3)
|
| 430 |
+
# Returns (..., n); (..., 2, n, 3).
|
| 431 |
+
return distance, torch.stack((unit_displacement, -unit_displacement), dim=1)
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
class DihedralPotential(Potential):
|
| 435 |
+
"""Measure signed torsion angles for indexed atom quartets."""
|
| 436 |
+
|
| 437 |
+
def compute_variable(
|
| 438 |
+
self,
|
| 439 |
+
coords: torch.Tensor,
|
| 440 |
+
index: torch.Tensor,
|
| 441 |
+
ref_coords: torch.Tensor | None = None,
|
| 442 |
+
ref_mask: torch.Tensor | None = None,
|
| 443 |
+
compute_gradient: bool = False,
|
| 444 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 445 |
+
# coords: (..., a, 3); index: (4, n).
|
| 446 |
+
del ref_coords, ref_mask
|
| 447 |
+
# Each displacement has shape (..., n, 3).
|
| 448 |
+
r_ij = coords.index_select(-2, index[0]) - coords.index_select(-2, index[1])
|
| 449 |
+
r_kj = coords.index_select(-2, index[2]) - coords.index_select(-2, index[1])
|
| 450 |
+
r_kl = coords.index_select(-2, index[2]) - coords.index_select(-2, index[3])
|
| 451 |
+
n_ijk = torch.cross(r_ij, r_kj, dim=-1) # (..., n, 3)
|
| 452 |
+
n_jkl = torch.cross(r_kj, r_kl, dim=-1) # (..., n, 3)
|
| 453 |
+
r_kj_norm = torch.linalg.norm(r_kj, dim=-1) # (..., n)
|
| 454 |
+
n_ijk_norm = torch.linalg.norm(n_ijk, dim=-1) # (..., n)
|
| 455 |
+
n_jkl_norm = torch.linalg.norm(n_jkl, dim=-1) # (..., n)
|
| 456 |
+
|
| 457 |
+
orientation = torch.sign(
|
| 458 |
+
r_kj.unsqueeze(-2) @ torch.cross(n_ijk, n_jkl, dim=-1).unsqueeze(-1)
|
| 459 |
+
).squeeze(-1, -2) # (..., n)
|
| 460 |
+
cosine = (n_ijk.unsqueeze(-2) @ n_jkl.unsqueeze(-1)).squeeze(-1, -2) / (
|
| 461 |
+
n_ijk_norm * n_jkl_norm
|
| 462 |
+
) # (..., n)
|
| 463 |
+
# (..., n)
|
| 464 |
+
angle = orientation * torch.arccos(torch.clamp(cosine, -1 + 1e-8, 1 - 1e-8))
|
| 465 |
+
if not compute_gradient:
|
| 466 |
+
return angle # (..., n)
|
| 467 |
+
|
| 468 |
+
projection_i = (
|
| 469 |
+
(r_ij.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
|
| 470 |
+
).unsqueeze(-1) # (..., n, 1)
|
| 471 |
+
projection_l = (
|
| 472 |
+
(r_kl.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
|
| 473 |
+
).unsqueeze(-1) # (..., n, 1)
|
| 474 |
+
grad_i = n_ijk * (r_kj_norm / n_ijk_norm**2).unsqueeze(-1) # (..., n, 3)
|
| 475 |
+
grad_l = -n_jkl * (r_kj_norm / n_jkl_norm**2).unsqueeze(-1) # (..., n, 3)
|
| 476 |
+
grad_j = (projection_i - 1) * grad_i - projection_l * grad_l # (..., n, 3)
|
| 477 |
+
grad_k = (projection_l - 1) * grad_l - projection_i * grad_i # (..., n, 3)
|
| 478 |
+
# Returns (..., n); (..., 4, n, 3).
|
| 479 |
+
return angle, torch.stack((grad_i, grad_j, grad_k, grad_l), dim=1)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
class AbsDihedralPotential(DihedralPotential):
|
| 483 |
+
"""Measure the unsigned magnitude of indexed torsion angles."""
|
| 484 |
+
|
| 485 |
+
def compute_variable(
|
| 486 |
+
self,
|
| 487 |
+
coords: torch.Tensor,
|
| 488 |
+
index: torch.Tensor,
|
| 489 |
+
ref_coords: torch.Tensor | None = None,
|
| 490 |
+
ref_mask: torch.Tensor | None = None,
|
| 491 |
+
compute_gradient: bool = False,
|
| 492 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 493 |
+
# coords: (..., a, 3); index: (4, n).
|
| 494 |
+
del ref_coords, ref_mask
|
| 495 |
+
if not compute_gradient:
|
| 496 |
+
return torch.abs(super().compute_variable(coords, index)) # (..., n)
|
| 497 |
+
angle, gradient = super().compute_variable(
|
| 498 |
+
coords,
|
| 499 |
+
index,
|
| 500 |
+
compute_gradient=True,
|
| 501 |
+
) # (..., n); (..., 4, n, 3)
|
| 502 |
+
gradient[(angle < 0)[..., None, :, None].expand_as(gradient)] *= -1 # same
|
| 503 |
+
return torch.abs(angle), gradient # (..., n); (..., 4, n, 3)
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
def _element_radii(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 507 |
+
# feats["ref_element"]: (1, a, n_element).
|
| 508 |
+
element_radii = torch.zeros(
|
| 509 |
+
const.num_elements,
|
| 510 |
+
dtype=torch.float32,
|
| 511 |
+
device=feats["ref_element"].device,
|
| 512 |
+
) # (n_element,)
|
| 513 |
+
element_radii[1:119] = torch.tensor(
|
| 514 |
+
const.vdw_radii,
|
| 515 |
+
dtype=torch.float32,
|
| 516 |
+
device=element_radii.device,
|
| 517 |
+
) # (n_element,)
|
| 518 |
+
# (a,)
|
| 519 |
+
return (feats["ref_element"].float() @ element_radii.unsqueeze(-1)).squeeze(-1)[0]
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 523 |
+
# atom_to_token: (1, a, l); asym_id: (1, l).
|
| 524 |
+
return (
|
| 525 |
+
torch.bmm(
|
| 526 |
+
feats["atom_to_token"].float(),
|
| 527 |
+
feats["asym_id"].unsqueeze(-1).float(),
|
| 528 |
+
)
|
| 529 |
+
.squeeze(-1)
|
| 530 |
+
.long()
|
| 531 |
+
)[0] # (a,)
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
class PoseBustersPotential(FlatBottomPotential, DistancePotential):
|
| 535 |
+
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 536 |
+
# rdkit_bounds_index: (1, 2, n); bound and mask features: (1, n).
|
| 537 |
+
pair_index = feats["rdkit_bounds_index"][0] # (2, n)
|
| 538 |
+
lower = feats["rdkit_lower_bounds"][0].clone() # (n,)
|
| 539 |
+
upper = feats["rdkit_upper_bounds"][0].clone() # (n,)
|
| 540 |
+
bond = feats["rdkit_bounds_bond_mask"][0] # (n,)
|
| 541 |
+
angle = feats["rdkit_bounds_angle_mask"][0] # (n,)
|
| 542 |
+
lower[bond * ~angle] *= 1.0 - parameters["bond_buffer"] # (n,)
|
| 543 |
+
upper[bond * ~angle] *= 1.0 + parameters["bond_buffer"] # (n,)
|
| 544 |
+
lower[~bond * angle] *= 1.0 - parameters["angle_buffer"] # (n,)
|
| 545 |
+
upper[~bond * angle] *= 1.0 + parameters["angle_buffer"] # (n,)
|
| 546 |
+
shared_buffer = min(parameters["bond_buffer"], parameters["angle_buffer"])
|
| 547 |
+
lower[bond * angle] *= 1.0 - shared_buffer # (n,)
|
| 548 |
+
upper[bond * angle] *= 1.0 + shared_buffer # (n,)
|
| 549 |
+
lower[~bond * ~angle] *= 1.0 - parameters["clash_buffer"] # (n,)
|
| 550 |
+
upper[~bond * ~angle] = float("inf") # (n,)
|
| 551 |
+
|
| 552 |
+
atom_radii = _element_radii(feats) # (a,)
|
| 553 |
+
bond_cutoff = 0.35 + atom_radii[pair_index].mean(dim=0) # (n,)
|
| 554 |
+
lower[~bond] = torch.max(lower[~bond], bond_cutoff[~bond]) # (n,)
|
| 555 |
+
upper[bond] = torch.min(upper[bond], bond_cutoff[bond]) # (n,)
|
| 556 |
+
# Returns index (2, n) and three function arguments of shape (n,).
|
| 557 |
+
return pair_index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
class ConnectionsPotential(FlatBottomPotential, DistancePotential):
|
| 561 |
+
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 562 |
+
# connected_atom_index: (1, 2, n).
|
| 563 |
+
pair_index = feats["connected_atom_index"][0] # (2, n)
|
| 564 |
+
upper = torch.full(
|
| 565 |
+
(pair_index.shape[1],),
|
| 566 |
+
parameters["buffer"],
|
| 567 |
+
device=pair_index.device,
|
| 568 |
+
) # (n,)
|
| 569 |
+
# Returns index (2, n), k (n,), and upper bound (n,).
|
| 570 |
+
return pair_index, (torch.ones_like(upper), None, upper), None, None, None
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
class VDWOverlapPotential(FlatBottomPotential, DistancePotential):
|
| 574 |
+
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 575 |
+
atom_chain_id = _atom_chain_ids(feats) # (a,)
|
| 576 |
+
atom_pad_mask = feats["atom_pad_mask"][0].bool() # (a,)
|
| 577 |
+
chain_sizes = torch.bincount(atom_chain_id[atom_pad_mask]) # (n_chain,)
|
| 578 |
+
nonion_atom = (chain_sizes > 1)[atom_chain_id] # (a,)
|
| 579 |
+
atom_radii = _element_radii(feats) # (a,)
|
| 580 |
+
pair_index = torch.triu_indices(
|
| 581 |
+
atom_chain_id.shape[0],
|
| 582 |
+
atom_chain_id.shape[0],
|
| 583 |
+
1,
|
| 584 |
+
device=atom_chain_id.device,
|
| 585 |
+
) # (2, n_pair)
|
| 586 |
+
pair_pad_mask = atom_pad_mask[pair_index].all(dim=0) # (n_pair,)
|
| 587 |
+
# (n_pair,)
|
| 588 |
+
pair_ion_mask = nonion_atom[pair_index[0]] * nonion_atom[pair_index[1]]
|
| 589 |
+
|
| 590 |
+
num_chains = atom_chain_id.max() + 1 # ()
|
| 591 |
+
connected = feats["connected_chain_index"][0] # (2, n_connection)
|
| 592 |
+
connected_matrix = torch.eye(
|
| 593 |
+
num_chains,
|
| 594 |
+
device=atom_chain_id.device,
|
| 595 |
+
dtype=torch.bool,
|
| 596 |
+
) # (n_chain, n_chain)
|
| 597 |
+
connected_matrix[connected[0], connected[1]] = True # (n_chain, n_chain)
|
| 598 |
+
connected_matrix[connected[1], connected[0]] = True # (n_chain, n_chain)
|
| 599 |
+
connected_pair = connected_matrix[
|
| 600 |
+
atom_chain_id[pair_index[0]],
|
| 601 |
+
atom_chain_id[pair_index[1]],
|
| 602 |
+
] # (n_pair,)
|
| 603 |
+
# (2, n)
|
| 604 |
+
pair_index = pair_index[:, pair_pad_mask * pair_ion_mask * ~connected_pair]
|
| 605 |
+
# (n,)
|
| 606 |
+
lower = atom_radii[pair_index].sum(dim=0) * (1.0 - parameters["buffer"])
|
| 607 |
+
# Returns index (2, n), k (n,), and lower bound (n,).
|
| 608 |
+
return pair_index, (torch.ones_like(lower), lower, None), None, None, None
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
class SymmetricChainCOMPotential(FlatBottomPotential, DistancePotential):
|
| 612 |
+
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 613 |
+
atom_chain_id = _atom_chain_ids(feats) # (a,)
|
| 614 |
+
atom_pad_mask = feats["atom_pad_mask"][0].bool() # (a,)
|
| 615 |
+
nonion_chain = torch.bincount(atom_chain_id[atom_pad_mask]) > 1 # (n_chain,)
|
| 616 |
+
pair_index = feats["symmetric_chain_index"][0] # (2, n_candidate)
|
| 617 |
+
pair_index = pair_index[
|
| 618 |
+
:,
|
| 619 |
+
nonion_chain[pair_index[0]] * nonion_chain[pair_index[1]],
|
| 620 |
+
] # (2, n)
|
| 621 |
+
lower = torch.full(
|
| 622 |
+
(pair_index.shape[1],),
|
| 623 |
+
parameters["buffer"],
|
| 624 |
+
dtype=torch.float32,
|
| 625 |
+
device=pair_index.device,
|
| 626 |
+
) # (n,)
|
| 627 |
+
# Returns center index (2, n), k/lower (n,), and atom-to-center maps (a,).
|
| 628 |
+
return (
|
| 629 |
+
pair_index,
|
| 630 |
+
(torch.ones_like(lower), lower, None),
|
| 631 |
+
(atom_chain_id, atom_pad_mask),
|
| 632 |
+
None,
|
| 633 |
+
None,
|
| 634 |
+
)
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
def _oriented_bounds(
|
| 638 |
+
orientations: torch.Tensor,
|
| 639 |
+
positive_lower: float,
|
| 640 |
+
negative_upper: float,
|
| 641 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 642 |
+
# orientations: (n,).
|
| 643 |
+
lower = torch.zeros(orientations.shape, device=orientations.device) # (n,)
|
| 644 |
+
upper = torch.zeros(orientations.shape, device=orientations.device) # (n,)
|
| 645 |
+
lower[orientations] = positive_lower # (n,)
|
| 646 |
+
upper[orientations] = float("inf") # (n,)
|
| 647 |
+
lower[~orientations] = float("-inf") # (n,)
|
| 648 |
+
upper[~orientations] = negative_upper # (n,)
|
| 649 |
+
return lower, upper # each (n,)
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
class StereoBondPotential(FlatBottomPotential, AbsDihedralPotential):
|
| 653 |
+
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 654 |
+
# stereo_bond_index: (1, 4, n); orientations: (1, n).
|
| 655 |
+
index = feats["stereo_bond_index"][0] # (4, n)
|
| 656 |
+
orientation = feats["stereo_bond_orientations"][0].bool() # (n,)
|
| 657 |
+
lower, upper = _oriented_bounds(
|
| 658 |
+
orientation,
|
| 659 |
+
torch.pi - parameters["buffer"],
|
| 660 |
+
parameters["buffer"],
|
| 661 |
+
) # each (n,)
|
| 662 |
+
# Returns index (4, n) and three function arguments of shape (n,).
|
| 663 |
+
return index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
class ChiralAtomPotential(FlatBottomPotential, DihedralPotential):
|
| 667 |
+
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 668 |
+
# chiral_atom_index: (1, 4, n); orientations: (1, n).
|
| 669 |
+
index = feats["chiral_atom_index"][0] # (4, n)
|
| 670 |
+
orientation = feats["chiral_atom_orientations"][0].bool() # (n,)
|
| 671 |
+
lower, upper = _oriented_bounds(
|
| 672 |
+
orientation,
|
| 673 |
+
parameters["buffer"],
|
| 674 |
+
-parameters["buffer"],
|
| 675 |
+
) # each (n,)
|
| 676 |
+
# Returns index (4, n) and three function arguments of shape (n,).
|
| 677 |
+
return index, (torch.ones_like(lower), lower, upper), None, None, None
|
| 678 |
+
|
| 679 |
+
|
| 680 |
+
class PlanarBondPotential(FlatBottomPotential, AbsDihedralPotential):
|
| 681 |
+
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 682 |
+
# The feature stores two atom-index rows for each six-entry bond pattern.
|
| 683 |
+
bond_index = feats["planar_bond_index"][0].T # (2, n_bond_entry)
|
| 684 |
+
improper_pattern = torch.tensor(
|
| 685 |
+
[[1, 2, 3, 0], [4, 5, 0, 3]],
|
| 686 |
+
device=bond_index.device,
|
| 687 |
+
).T # (4, 2)
|
| 688 |
+
# (4, n_improper)
|
| 689 |
+
improper_index = bond_index[:, improper_pattern].swapaxes(0, 1).flatten(start_dim=1)
|
| 690 |
+
upper = torch.full(
|
| 691 |
+
(improper_index.shape[1],),
|
| 692 |
+
parameters["buffer"],
|
| 693 |
+
device=improper_index.device,
|
| 694 |
+
) # (n_improper,)
|
| 695 |
+
# Returns index (4, n_improper), k (n_improper,), and upper (n_improper,).
|
| 696 |
+
return (
|
| 697 |
+
improper_index,
|
| 698 |
+
(torch.ones_like(upper), None, upper),
|
| 699 |
+
None,
|
| 700 |
+
None,
|
| 701 |
+
None,
|
| 702 |
+
)
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
class TemplateReferencePotential(FlatBottomPotential, ReferencePotential):
|
| 706 |
+
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 707 |
+
del parameters
|
| 708 |
+
if "template_mask_cb" not in feats or "template_force" not in feats:
|
| 709 |
+
# Empty sentinel index: (1, 0).
|
| 710 |
+
return torch.empty((1, 0)), None, None, None, None
|
| 711 |
+
# template_mask_cb/template_force: (b, n_template, n)/(b, n_template).
|
| 712 |
+
template_mask = feats["template_mask_cb"][feats["template_force"]] # (n_ref, n)
|
| 713 |
+
if template_mask.shape[0] == 0:
|
| 714 |
+
# Empty sentinel index: (1, 0).
|
| 715 |
+
return torch.empty((1, 0)), None, None, None, None
|
| 716 |
+
|
| 717 |
+
ref_coords = feats["template_cb"][feats["template_force"]].clone() # (n_ref, n, 3)
|
| 718 |
+
ref_mask = feats["template_mask_cb"][feats["template_force"]].clone() # (n_ref, n)
|
| 719 |
+
atom_indices = torch.arange(
|
| 720 |
+
feats["atom_pad_mask"].shape[1],
|
| 721 |
+
device=feats["atom_pad_mask"].device,
|
| 722 |
+
dtype=torch.float32,
|
| 723 |
+
)[None, :, None] # (1, a, 1)
|
| 724 |
+
ref_atom_index = (
|
| 725 |
+
torch.bmm(
|
| 726 |
+
feats["token_to_rep_atom"].float(),
|
| 727 |
+
atom_indices,
|
| 728 |
+
)
|
| 729 |
+
.squeeze(-1)
|
| 730 |
+
.long()[0] # (n,)
|
| 731 |
+
)
|
| 732 |
+
ref_token_index = (
|
| 733 |
+
torch.bmm(
|
| 734 |
+
feats["atom_to_token"].float(),
|
| 735 |
+
feats["token_index"].unsqueeze(-1).float(),
|
| 736 |
+
)
|
| 737 |
+
.squeeze(-1)
|
| 738 |
+
.long()[0] # (a,)
|
| 739 |
+
)
|
| 740 |
+
|
| 741 |
+
index = torch.arange(
|
| 742 |
+
template_mask.shape[-1],
|
| 743 |
+
dtype=torch.long,
|
| 744 |
+
device=template_mask.device,
|
| 745 |
+
)[None] # (1, n)
|
| 746 |
+
upper = torch.full(
|
| 747 |
+
template_mask.shape,
|
| 748 |
+
float("inf"),
|
| 749 |
+
device=index.device,
|
| 750 |
+
dtype=torch.float32,
|
| 751 |
+
) # (n_ref, n)
|
| 752 |
+
reference_indices = torch.argwhere(template_mask).T # (2, n_active)
|
| 753 |
+
upper[reference_indices.unbind()] = feats["template_force_threshold"][
|
| 754 |
+
feats["template_force"]
|
| 755 |
+
][reference_indices[0]] # (n_ref, n)
|
| 756 |
+
# Returns index (1, n), function args (n_ref, n), and reference mappings.
|
| 757 |
+
return (
|
| 758 |
+
index,
|
| 759 |
+
(torch.ones_like(upper), None, upper),
|
| 760 |
+
None,
|
| 761 |
+
(ref_coords, ref_mask, ref_atom_index, ref_token_index),
|
| 762 |
+
None,
|
| 763 |
+
)
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
class ContactPotentital(FlatBottomPotential, DistancePotential):
|
| 767 |
+
"""Contact-union potential retaining the historical checkpoint name."""
|
| 768 |
+
|
| 769 |
+
def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]) -> tuple[Any, ...]:
|
| 770 |
+
del parameters
|
| 771 |
+
# pair index: (1, 2, n); threshold/operator features: (1, n).
|
| 772 |
+
index = feats["contact_pair_index"][0] # (2, n)
|
| 773 |
+
upper = feats["contact_thresholds"][0].clone() # (n,)
|
| 774 |
+
# Returns index (2, n), k/upper (n,), and two operator tensors (n,).
|
| 775 |
+
return (
|
| 776 |
+
index,
|
| 777 |
+
(torch.ones_like(upper), None, upper),
|
| 778 |
+
None,
|
| 779 |
+
None,
|
| 780 |
+
(
|
| 781 |
+
feats["contact_negation_mask"][0],
|
| 782 |
+
feats["contact_union_index"][0],
|
| 783 |
+
),
|
| 784 |
+
)
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
ContactPotential = ContactPotentital
|
| 788 |
+
|
| 789 |
+
|
| 790 |
+
def get_potentials(
|
| 791 |
+
steering_args: dict[str, bool],
|
| 792 |
+
boltz2: bool = False,
|
| 793 |
+
) -> list[Potential]:
|
| 794 |
+
"""Build the ordered potential stack for requested steering modes."""
|
| 795 |
+
|
| 796 |
+
use_fk = steering_args["fk_steering"]
|
| 797 |
+
use_physical = steering_args["physical_guidance_update"]
|
| 798 |
+
use_contacts = steering_args.get("contact_guidance_update", False)
|
| 799 |
+
potentials: list[Potential] = []
|
| 800 |
+
if use_fk or use_physical:
|
| 801 |
+
potentials.extend(
|
| 802 |
+
(
|
| 803 |
+
SymmetricChainCOMPotential(
|
| 804 |
+
{
|
| 805 |
+
"guidance_interval": 4,
|
| 806 |
+
"guidance_weight": 0.5 if use_physical else 0.0,
|
| 807 |
+
"resampling_weight": 0.5,
|
| 808 |
+
"buffer": ExponentialInterpolation(1.0, 5.0, -2.0),
|
| 809 |
+
}
|
| 810 |
+
),
|
| 811 |
+
VDWOverlapPotential(
|
| 812 |
+
{
|
| 813 |
+
"guidance_interval": 5,
|
| 814 |
+
"guidance_weight": PiecewiseStepFunction(
|
| 815 |
+
[0.4],
|
| 816 |
+
[0.125, 0.0],
|
| 817 |
+
)
|
| 818 |
+
if use_physical
|
| 819 |
+
else 0.0,
|
| 820 |
+
"resampling_weight": PiecewiseStepFunction(
|
| 821 |
+
[0.6],
|
| 822 |
+
[0.01, 0.0],
|
| 823 |
+
),
|
| 824 |
+
"buffer": 0.225,
|
| 825 |
+
}
|
| 826 |
+
),
|
| 827 |
+
ConnectionsPotential(
|
| 828 |
+
{
|
| 829 |
+
"guidance_interval": 1,
|
| 830 |
+
"guidance_weight": 0.15 if use_physical else 0.0,
|
| 831 |
+
"resampling_weight": 1.0,
|
| 832 |
+
"buffer": 2.0,
|
| 833 |
+
}
|
| 834 |
+
),
|
| 835 |
+
PoseBustersPotential(
|
| 836 |
+
{
|
| 837 |
+
"guidance_interval": 1,
|
| 838 |
+
"guidance_weight": 0.01 if use_physical else 0.0,
|
| 839 |
+
"resampling_weight": 0.1,
|
| 840 |
+
"bond_buffer": 0.125,
|
| 841 |
+
"angle_buffer": 0.125,
|
| 842 |
+
"clash_buffer": 0.10,
|
| 843 |
+
}
|
| 844 |
+
),
|
| 845 |
+
ChiralAtomPotential(
|
| 846 |
+
{
|
| 847 |
+
"guidance_interval": 1,
|
| 848 |
+
"guidance_weight": 0.1 if use_physical else 0.0,
|
| 849 |
+
"resampling_weight": 1.0,
|
| 850 |
+
"buffer": 0.52360,
|
| 851 |
+
}
|
| 852 |
+
),
|
| 853 |
+
StereoBondPotential(
|
| 854 |
+
{
|
| 855 |
+
"guidance_interval": 1,
|
| 856 |
+
"guidance_weight": 0.05 if use_physical else 0.0,
|
| 857 |
+
"resampling_weight": 1.0,
|
| 858 |
+
"buffer": 0.52360,
|
| 859 |
+
}
|
| 860 |
+
),
|
| 861 |
+
PlanarBondPotential(
|
| 862 |
+
{
|
| 863 |
+
"guidance_interval": 1,
|
| 864 |
+
"guidance_weight": 0.05 if use_physical else 0.0,
|
| 865 |
+
"resampling_weight": 1.0,
|
| 866 |
+
"buffer": 0.26180,
|
| 867 |
+
}
|
| 868 |
+
),
|
| 869 |
+
)
|
| 870 |
+
)
|
| 871 |
+
if boltz2 and (use_fk or use_contacts):
|
| 872 |
+
potentials.extend(
|
| 873 |
+
(
|
| 874 |
+
ContactPotentital(
|
| 875 |
+
{
|
| 876 |
+
"guidance_interval": 4,
|
| 877 |
+
"guidance_weight": PiecewiseStepFunction(
|
| 878 |
+
[0.25, 0.75],
|
| 879 |
+
[0.0, 0.5, 1.0],
|
| 880 |
+
)
|
| 881 |
+
if use_contacts
|
| 882 |
+
else 0.0,
|
| 883 |
+
"resampling_weight": 1.0,
|
| 884 |
+
"union_lambda": ExponentialInterpolation(8.0, 0.0, -2.0),
|
| 885 |
+
}
|
| 886 |
+
),
|
| 887 |
+
TemplateReferencePotential(
|
| 888 |
+
{
|
| 889 |
+
"guidance_interval": 2,
|
| 890 |
+
"guidance_weight": 0.1 if use_contacts else 0.0,
|
| 891 |
+
"resampling_weight": 1.0,
|
| 892 |
+
}
|
| 893 |
+
),
|
| 894 |
+
)
|
| 895 |
+
)
|
| 896 |
+
return potentials
|
fastplms/models/boltz/vb_potentials_schedules.py
CHANGED
|
@@ -1,68 +1,68 @@
|
|
| 1 |
-
"""Scalar schedules for structure-steering potentials."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import math
|
| 6 |
-
|
| 7 |
-
from abc import ABC, abstractmethod
|
| 8 |
-
from collections.abc import Sequence
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
class ParameterSchedule(ABC):
|
| 12 |
-
"""Map normalized diffusion time ``t`` to a potential parameter."""
|
| 13 |
-
|
| 14 |
-
@abstractmethod
|
| 15 |
-
def compute(self, t: float) -> float:
|
| 16 |
-
"""Evaluate the schedule at ``t``."""
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class ExponentialInterpolation(ParameterSchedule):
|
| 20 |
-
"""Interpolate from ``start`` to ``end`` with exponential curvature."""
|
| 21 |
-
|
| 22 |
-
def __init__(self, start: float, end: float, alpha: float) -> None:
|
| 23 |
-
self.start = start
|
| 24 |
-
self.end = end
|
| 25 |
-
self.alpha = alpha
|
| 26 |
-
|
| 27 |
-
def compute(self, t: float) -> float:
|
| 28 |
-
span = self.end - self.start
|
| 29 |
-
if self.alpha == 0:
|
| 30 |
-
return self.start + span * t
|
| 31 |
-
numerator = math.exp(self.alpha * t) - 1
|
| 32 |
-
denominator = math.exp(self.alpha) - 1
|
| 33 |
-
return self.start + span * numerator / denominator
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
class PiecewiseStepFunction(ParameterSchedule):
|
| 37 |
-
"""Select values separated by strict upper thresholds.
|
| 38 |
-
|
| 39 |
-
A time exactly equal to a threshold remains in the lower interval. This
|
| 40 |
-
boundary convention is part of the steering-input contract.
|
| 41 |
-
"""
|
| 42 |
-
|
| 43 |
-
def __init__(
|
| 44 |
-
self,
|
| 45 |
-
thresholds: Sequence[float],
|
| 46 |
-
values: Sequence[float],
|
| 47 |
-
) -> None:
|
| 48 |
-
self.thresholds = tuple(thresholds)
|
| 49 |
-
self.values = tuple(values)
|
| 50 |
-
if not self.thresholds:
|
| 51 |
-
raise ValueError("PiecewiseStepFunction requires at least one threshold.")
|
| 52 |
-
if len(self.values) != len(self.thresholds) + 1:
|
| 53 |
-
raise ValueError(
|
| 54 |
-
"PiecewiseStepFunction requires exactly one more value than threshold; "
|
| 55 |
-
f"received {len(self.values)} values and {len(self.thresholds)} thresholds."
|
| 56 |
-
)
|
| 57 |
-
if any(
|
| 58 |
-
current >= following
|
| 59 |
-
for current, following in zip(self.thresholds, self.thresholds[1:], strict=False)
|
| 60 |
-
):
|
| 61 |
-
raise ValueError("PiecewiseStepFunction thresholds must be strictly increasing.")
|
| 62 |
-
|
| 63 |
-
def compute(self, t: float) -> float:
|
| 64 |
-
interval = next(
|
| 65 |
-
(index for index, threshold in enumerate(self.thresholds) if t <= threshold),
|
| 66 |
-
len(self.thresholds),
|
| 67 |
-
)
|
| 68 |
-
return self.values[interval]
|
|
|
|
| 1 |
+
"""Scalar schedules for structure-steering potentials."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
|
| 7 |
+
from abc import ABC, abstractmethod
|
| 8 |
+
from collections.abc import Sequence
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class ParameterSchedule(ABC):
|
| 12 |
+
"""Map normalized diffusion time ``t`` to a potential parameter."""
|
| 13 |
+
|
| 14 |
+
@abstractmethod
|
| 15 |
+
def compute(self, t: float) -> float:
|
| 16 |
+
"""Evaluate the schedule at ``t``."""
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class ExponentialInterpolation(ParameterSchedule):
|
| 20 |
+
"""Interpolate from ``start`` to ``end`` with exponential curvature."""
|
| 21 |
+
|
| 22 |
+
def __init__(self, start: float, end: float, alpha: float) -> None:
|
| 23 |
+
self.start = start
|
| 24 |
+
self.end = end
|
| 25 |
+
self.alpha = alpha
|
| 26 |
+
|
| 27 |
+
def compute(self, t: float) -> float:
|
| 28 |
+
span = self.end - self.start
|
| 29 |
+
if self.alpha == 0:
|
| 30 |
+
return self.start + span * t
|
| 31 |
+
numerator = math.exp(self.alpha * t) - 1
|
| 32 |
+
denominator = math.exp(self.alpha) - 1
|
| 33 |
+
return self.start + span * numerator / denominator
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class PiecewiseStepFunction(ParameterSchedule):
|
| 37 |
+
"""Select values separated by strict upper thresholds.
|
| 38 |
+
|
| 39 |
+
A time exactly equal to a threshold remains in the lower interval. This
|
| 40 |
+
boundary convention is part of the steering-input contract.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(
|
| 44 |
+
self,
|
| 45 |
+
thresholds: Sequence[float],
|
| 46 |
+
values: Sequence[float],
|
| 47 |
+
) -> None:
|
| 48 |
+
self.thresholds = tuple(thresholds)
|
| 49 |
+
self.values = tuple(values)
|
| 50 |
+
if not self.thresholds:
|
| 51 |
+
raise ValueError("PiecewiseStepFunction requires at least one threshold.")
|
| 52 |
+
if len(self.values) != len(self.thresholds) + 1:
|
| 53 |
+
raise ValueError(
|
| 54 |
+
"PiecewiseStepFunction requires exactly one more value than threshold; "
|
| 55 |
+
f"received {len(self.values)} values and {len(self.thresholds)} thresholds."
|
| 56 |
+
)
|
| 57 |
+
if any(
|
| 58 |
+
current >= following
|
| 59 |
+
for current, following in zip(self.thresholds, self.thresholds[1:], strict=False)
|
| 60 |
+
):
|
| 61 |
+
raise ValueError("PiecewiseStepFunction thresholds must be strictly increasing.")
|
| 62 |
+
|
| 63 |
+
def compute(self, t: float) -> float:
|
| 64 |
+
interval = next(
|
| 65 |
+
(index for index, threshold in enumerate(self.thresholds) if t <= threshold),
|
| 66 |
+
len(self.thresholds),
|
| 67 |
+
)
|
| 68 |
+
return self.values[interval]
|
fastplms/models/boltz/vb_tri_attn_attention.py
CHANGED
|
@@ -1,151 +1,151 @@
|
|
| 1 |
-
"""Triangular row and column attention for Boltz2 pair states."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from typing import cast
|
| 8 |
-
from torch import Tensor, nn
|
| 9 |
-
|
| 10 |
-
from .vb_tri_attn_primitives import Attention, LayerNorm, Linear
|
| 11 |
-
from .vb_tri_attn_utils import chunk_layer, permute_final_dims
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class TriangleAttention(nn.Module):
|
| 15 |
-
"""Apply pair-biased attention along one axis of pair tensor X."""
|
| 16 |
-
|
| 17 |
-
def __init__(
|
| 18 |
-
self,
|
| 19 |
-
c_in: int,
|
| 20 |
-
c_hidden: int,
|
| 21 |
-
no_heads: int,
|
| 22 |
-
starting: bool = True,
|
| 23 |
-
inf: float = 1e9,
|
| 24 |
-
) -> None:
|
| 25 |
-
super().__init__()
|
| 26 |
-
self.c_in = c_in
|
| 27 |
-
self.c_hidden = c_hidden
|
| 28 |
-
self.no_heads = no_heads
|
| 29 |
-
self.starting = starting
|
| 30 |
-
self.inf = inf
|
| 31 |
-
self.layer_norm = LayerNorm(c_in)
|
| 32 |
-
self.linear = Linear(c_in, no_heads, bias=False, init="normal")
|
| 33 |
-
self.mha = Attention(c_in, c_in, c_in, c_hidden, no_heads)
|
| 34 |
-
|
| 35 |
-
@torch.jit.ignore # type: ignore[untyped-decorator]
|
| 36 |
-
def _chunk(
|
| 37 |
-
self,
|
| 38 |
-
x: Tensor,
|
| 39 |
-
tri_bias: Tensor,
|
| 40 |
-
mask_bias: Tensor,
|
| 41 |
-
mask: Tensor,
|
| 42 |
-
chunk_size: int,
|
| 43 |
-
use_kernels: bool = False,
|
| 44 |
-
) -> Tensor:
|
| 45 |
-
"""Evaluate attention in slices of the batch-like leading axes."""
|
| 46 |
-
|
| 47 |
-
# x: (..., l, l, d); biases/mask retain broadcastable triangular axes.
|
| 48 |
-
def attention_call(**inputs: Tensor) -> Tensor:
|
| 49 |
-
return cast(Tensor, self.mha(**inputs, use_kernels=use_kernels))
|
| 50 |
-
|
| 51 |
-
inputs = {
|
| 52 |
-
"q_x": x,
|
| 53 |
-
"kv_x": x,
|
| 54 |
-
"tri_bias": tri_bias,
|
| 55 |
-
"mask_bias": mask_bias,
|
| 56 |
-
"mask": mask,
|
| 57 |
-
} # tensor values retain the input shapes above
|
| 58 |
-
return cast(
|
| 59 |
-
Tensor,
|
| 60 |
-
chunk_layer(
|
| 61 |
-
attention_call,
|
| 62 |
-
inputs,
|
| 63 |
-
chunk_size=chunk_size,
|
| 64 |
-
no_batch_dims=len(x.shape[:-2]),
|
| 65 |
-
_out=None,
|
| 66 |
-
),
|
| 67 |
-
)
|
| 68 |
-
|
| 69 |
-
def _run_attention(
|
| 70 |
-
self,
|
| 71 |
-
states: Tensor,
|
| 72 |
-
triangle_bias: Tensor,
|
| 73 |
-
mask_bias: Tensor,
|
| 74 |
-
expanded_mask: Tensor,
|
| 75 |
-
chunk_size: int | None,
|
| 76 |
-
use_kernels: bool,
|
| 77 |
-
) -> Tensor:
|
| 78 |
-
# states: (..., l, l, d); returned tensor has the same shape.
|
| 79 |
-
if chunk_size is not None and not use_kernels:
|
| 80 |
-
return cast(
|
| 81 |
-
Tensor,
|
| 82 |
-
self._chunk(
|
| 83 |
-
states,
|
| 84 |
-
triangle_bias,
|
| 85 |
-
mask_bias,
|
| 86 |
-
expanded_mask,
|
| 87 |
-
chunk_size,
|
| 88 |
-
use_kernels=False,
|
| 89 |
-
),
|
| 90 |
-
)
|
| 91 |
-
return cast(
|
| 92 |
-
Tensor,
|
| 93 |
-
self.mha(
|
| 94 |
-
states,
|
| 95 |
-
states,
|
| 96 |
-
triangle_bias,
|
| 97 |
-
mask_bias,
|
| 98 |
-
expanded_mask,
|
| 99 |
-
use_kernels=use_kernels,
|
| 100 |
-
),
|
| 101 |
-
)
|
| 102 |
-
|
| 103 |
-
def forward(
|
| 104 |
-
self,
|
| 105 |
-
x: Tensor,
|
| 106 |
-
mask: Tensor | None = None,
|
| 107 |
-
chunk_size: int | None = None,
|
| 108 |
-
use_kernels: bool = False,
|
| 109 |
-
) -> Tensor:
|
| 110 |
-
"""Transform X with shape ``(..., l, l, d)`` under mask M."""
|
| 111 |
-
|
| 112 |
-
pair_mask = x.new_ones(x.shape[:-1]) if mask is None else mask # (..., l, l)
|
| 113 |
-
pair_states = x # (..., l, l, d)
|
| 114 |
-
if not self.starting:
|
| 115 |
-
pair_states = pair_states.transpose(-2, -3) # (..., l, l, d)
|
| 116 |
-
pair_mask = pair_mask.transpose(-1, -2) # (..., l, l)
|
| 117 |
-
|
| 118 |
-
normalized = self.layer_norm(pair_states) # (..., l, l, d)
|
| 119 |
-
expanded_mask = pair_mask[..., :, None, None, :] # (..., l, 1, 1, l)
|
| 120 |
-
mask_bias = self.inf * (expanded_mask - 1) # (..., l, 1, 1, l)
|
| 121 |
-
triangle_bias = permute_final_dims(
|
| 122 |
-
self.linear(normalized), (2, 0, 1)
|
| 123 |
-
) # (..., h, l, l)
|
| 124 |
-
triangle_bias = triangle_bias.unsqueeze(-4) # (..., 1, h, l, l)
|
| 125 |
-
output = self._run_attention(
|
| 126 |
-
normalized,
|
| 127 |
-
triangle_bias,
|
| 128 |
-
mask_bias,
|
| 129 |
-
expanded_mask,
|
| 130 |
-
chunk_size,
|
| 131 |
-
use_kernels,
|
| 132 |
-
) # (..., l, l, d)
|
| 133 |
-
return (
|
| 134 |
-
output if self.starting else output.transpose(-2, -3)
|
| 135 |
-
) # (..., l, l, d)
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
TriangleAttentionStartingNode = TriangleAttention
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
class TriangleAttentionEndingNode(TriangleAttention):
|
| 142 |
-
"""Apply triangular attention around each pair's ending node."""
|
| 143 |
-
|
| 144 |
-
def __init__(
|
| 145 |
-
self,
|
| 146 |
-
c_in: int,
|
| 147 |
-
c_hidden: int,
|
| 148 |
-
no_heads: int,
|
| 149 |
-
inf: float = 1e9,
|
| 150 |
-
) -> None:
|
| 151 |
-
super().__init__(c_in, c_hidden, no_heads, starting=False, inf=inf)
|
|
|
|
| 1 |
+
"""Triangular row and column attention for Boltz2 pair states."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from typing import cast
|
| 8 |
+
from torch import Tensor, nn
|
| 9 |
+
|
| 10 |
+
from .vb_tri_attn_primitives import Attention, LayerNorm, Linear
|
| 11 |
+
from .vb_tri_attn_utils import chunk_layer, permute_final_dims
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TriangleAttention(nn.Module):
|
| 15 |
+
"""Apply pair-biased attention along one axis of pair tensor X."""
|
| 16 |
+
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
c_in: int,
|
| 20 |
+
c_hidden: int,
|
| 21 |
+
no_heads: int,
|
| 22 |
+
starting: bool = True,
|
| 23 |
+
inf: float = 1e9,
|
| 24 |
+
) -> None:
|
| 25 |
+
super().__init__()
|
| 26 |
+
self.c_in = c_in
|
| 27 |
+
self.c_hidden = c_hidden
|
| 28 |
+
self.no_heads = no_heads
|
| 29 |
+
self.starting = starting
|
| 30 |
+
self.inf = inf
|
| 31 |
+
self.layer_norm = LayerNorm(c_in)
|
| 32 |
+
self.linear = Linear(c_in, no_heads, bias=False, init="normal")
|
| 33 |
+
self.mha = Attention(c_in, c_in, c_in, c_hidden, no_heads)
|
| 34 |
+
|
| 35 |
+
@torch.jit.ignore # type: ignore[untyped-decorator]
|
| 36 |
+
def _chunk(
|
| 37 |
+
self,
|
| 38 |
+
x: Tensor,
|
| 39 |
+
tri_bias: Tensor,
|
| 40 |
+
mask_bias: Tensor,
|
| 41 |
+
mask: Tensor,
|
| 42 |
+
chunk_size: int,
|
| 43 |
+
use_kernels: bool = False,
|
| 44 |
+
) -> Tensor:
|
| 45 |
+
"""Evaluate attention in slices of the batch-like leading axes."""
|
| 46 |
+
|
| 47 |
+
# x: (..., l, l, d); biases/mask retain broadcastable triangular axes.
|
| 48 |
+
def attention_call(**inputs: Tensor) -> Tensor:
|
| 49 |
+
return cast(Tensor, self.mha(**inputs, use_kernels=use_kernels))
|
| 50 |
+
|
| 51 |
+
inputs = {
|
| 52 |
+
"q_x": x,
|
| 53 |
+
"kv_x": x,
|
| 54 |
+
"tri_bias": tri_bias,
|
| 55 |
+
"mask_bias": mask_bias,
|
| 56 |
+
"mask": mask,
|
| 57 |
+
} # tensor values retain the input shapes above
|
| 58 |
+
return cast(
|
| 59 |
+
Tensor,
|
| 60 |
+
chunk_layer(
|
| 61 |
+
attention_call,
|
| 62 |
+
inputs,
|
| 63 |
+
chunk_size=chunk_size,
|
| 64 |
+
no_batch_dims=len(x.shape[:-2]),
|
| 65 |
+
_out=None,
|
| 66 |
+
),
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
def _run_attention(
|
| 70 |
+
self,
|
| 71 |
+
states: Tensor,
|
| 72 |
+
triangle_bias: Tensor,
|
| 73 |
+
mask_bias: Tensor,
|
| 74 |
+
expanded_mask: Tensor,
|
| 75 |
+
chunk_size: int | None,
|
| 76 |
+
use_kernels: bool,
|
| 77 |
+
) -> Tensor:
|
| 78 |
+
# states: (..., l, l, d); returned tensor has the same shape.
|
| 79 |
+
if chunk_size is not None and not use_kernels:
|
| 80 |
+
return cast(
|
| 81 |
+
Tensor,
|
| 82 |
+
self._chunk(
|
| 83 |
+
states,
|
| 84 |
+
triangle_bias,
|
| 85 |
+
mask_bias,
|
| 86 |
+
expanded_mask,
|
| 87 |
+
chunk_size,
|
| 88 |
+
use_kernels=False,
|
| 89 |
+
),
|
| 90 |
+
)
|
| 91 |
+
return cast(
|
| 92 |
+
Tensor,
|
| 93 |
+
self.mha(
|
| 94 |
+
states,
|
| 95 |
+
states,
|
| 96 |
+
triangle_bias,
|
| 97 |
+
mask_bias,
|
| 98 |
+
expanded_mask,
|
| 99 |
+
use_kernels=use_kernels,
|
| 100 |
+
),
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
def forward(
|
| 104 |
+
self,
|
| 105 |
+
x: Tensor,
|
| 106 |
+
mask: Tensor | None = None,
|
| 107 |
+
chunk_size: int | None = None,
|
| 108 |
+
use_kernels: bool = False,
|
| 109 |
+
) -> Tensor:
|
| 110 |
+
"""Transform X with shape ``(..., l, l, d)`` under mask M."""
|
| 111 |
+
|
| 112 |
+
pair_mask = x.new_ones(x.shape[:-1]) if mask is None else mask # (..., l, l)
|
| 113 |
+
pair_states = x # (..., l, l, d)
|
| 114 |
+
if not self.starting:
|
| 115 |
+
pair_states = pair_states.transpose(-2, -3) # (..., l, l, d)
|
| 116 |
+
pair_mask = pair_mask.transpose(-1, -2) # (..., l, l)
|
| 117 |
+
|
| 118 |
+
normalized = self.layer_norm(pair_states) # (..., l, l, d)
|
| 119 |
+
expanded_mask = pair_mask[..., :, None, None, :] # (..., l, 1, 1, l)
|
| 120 |
+
mask_bias = self.inf * (expanded_mask - 1) # (..., l, 1, 1, l)
|
| 121 |
+
triangle_bias = permute_final_dims(
|
| 122 |
+
self.linear(normalized), (2, 0, 1)
|
| 123 |
+
) # (..., h, l, l)
|
| 124 |
+
triangle_bias = triangle_bias.unsqueeze(-4) # (..., 1, h, l, l)
|
| 125 |
+
output = self._run_attention(
|
| 126 |
+
normalized,
|
| 127 |
+
triangle_bias,
|
| 128 |
+
mask_bias,
|
| 129 |
+
expanded_mask,
|
| 130 |
+
chunk_size,
|
| 131 |
+
use_kernels,
|
| 132 |
+
) # (..., l, l, d)
|
| 133 |
+
return (
|
| 134 |
+
output if self.starting else output.transpose(-2, -3)
|
| 135 |
+
) # (..., l, l, d)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
TriangleAttentionStartingNode = TriangleAttention
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class TriangleAttentionEndingNode(TriangleAttention):
|
| 142 |
+
"""Apply triangular attention around each pair's ending node."""
|
| 143 |
+
|
| 144 |
+
def __init__(
|
| 145 |
+
self,
|
| 146 |
+
c_in: int,
|
| 147 |
+
c_hidden: int,
|
| 148 |
+
no_heads: int,
|
| 149 |
+
inf: float = 1e9,
|
| 150 |
+
) -> None:
|
| 151 |
+
super().__init__(c_in, c_hidden, no_heads, starting=False, inf=inf)
|
fastplms/models/boltz/vb_tri_attn_primitives.py
CHANGED
|
@@ -1,266 +1,266 @@
|
|
| 1 |
-
"""Attention primitives for the Boltz pair stack.
|
| 2 |
-
|
| 3 |
-
The module retains the converted checkpoint names while providing a compact
|
| 4 |
-
implementation around PyTorch operations. Optional triangle kernels are
|
| 5 |
-
loaded only at the call site, so importing FastPLMs does not initialize CUDA.
|
| 6 |
-
|
| 7 |
-
Portions of the numerical contract follow OpenFold and DeepMind AlphaFold
|
| 8 |
-
under Apache-2.0. See ``THIRD_PARTY_NOTICES.md`` for provenance.
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
from __future__ import annotations
|
| 12 |
-
|
| 13 |
-
import importlib
|
| 14 |
-
import math
|
| 15 |
-
import torch
|
| 16 |
-
|
| 17 |
-
from collections.abc import Callable, Sequence
|
| 18 |
-
from importlib.util import find_spec
|
| 19 |
-
from torch import nn
|
| 20 |
-
|
| 21 |
-
from . import vb_layers_initialize as initialize
|
| 22 |
-
from .vb_tri_attn_utils import flatten_final_dims, permute_final_dims
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def _initialize_weight(
|
| 26 |
-
weight: torch.Tensor,
|
| 27 |
-
bias: torch.Tensor | None,
|
| 28 |
-
method: str,
|
| 29 |
-
) -> None:
|
| 30 |
-
# weight: (d_out, d_in); bias: (d_out,) or None; both retain shape in place.
|
| 31 |
-
initializers = {
|
| 32 |
-
"default": initialize.lecun_normal_init_,
|
| 33 |
-
"relu": initialize.he_normal_init_,
|
| 34 |
-
"glorot": initialize.glorot_uniform_init_,
|
| 35 |
-
"normal": initialize.normal_init_,
|
| 36 |
-
"final": initialize.final_init_,
|
| 37 |
-
"gating": initialize.gating_init_,
|
| 38 |
-
}
|
| 39 |
-
try:
|
| 40 |
-
initializers[method](weight)
|
| 41 |
-
except KeyError as error:
|
| 42 |
-
raise ValueError(f"unknown linear initialization {method!r}") from error
|
| 43 |
-
if method == "gating" and bias is not None:
|
| 44 |
-
bias.fill_(1.0)
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
class Linear(nn.Linear):
|
| 48 |
-
"""Linear projection with the initializers used by converted checkpoints."""
|
| 49 |
-
|
| 50 |
-
def __init__(
|
| 51 |
-
self,
|
| 52 |
-
in_dim: int,
|
| 53 |
-
out_dim: int,
|
| 54 |
-
bias: bool = True,
|
| 55 |
-
init: str = "default",
|
| 56 |
-
init_fn: Callable[[torch.Tensor, torch.Tensor | None], None] | None = None,
|
| 57 |
-
precision: torch.dtype | None = None,
|
| 58 |
-
) -> None:
|
| 59 |
-
super().__init__(in_dim, out_dim, bias=bias)
|
| 60 |
-
with torch.no_grad():
|
| 61 |
-
if self.bias is not None:
|
| 62 |
-
self.bias.zero_()
|
| 63 |
-
if init_fn is None:
|
| 64 |
-
_initialize_weight(self.weight, self.bias, init)
|
| 65 |
-
else:
|
| 66 |
-
init_fn(self.weight, self.bias)
|
| 67 |
-
self.precision = precision
|
| 68 |
-
|
| 69 |
-
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
| 70 |
-
"""Project ``input`` while preserving explicit precision policies."""
|
| 71 |
-
|
| 72 |
-
# input: (..., d_in); every branch returns (..., d_out).
|
| 73 |
-
input_dtype = input.dtype
|
| 74 |
-
if self.precision is not None:
|
| 75 |
-
with torch.autocast("cuda", enabled=False):
|
| 76 |
-
bias = (
|
| 77 |
-
None if self.bias is None else self.bias.to(self.precision)
|
| 78 |
-
) # (d_out,) or None
|
| 79 |
-
projected = nn.functional.linear(
|
| 80 |
-
input.to(self.precision),
|
| 81 |
-
self.weight.to(self.precision),
|
| 82 |
-
bias,
|
| 83 |
-
) # (..., d_out)
|
| 84 |
-
return projected.to(input_dtype) # (..., d_out)
|
| 85 |
-
if input_dtype is torch.bfloat16:
|
| 86 |
-
with torch.autocast("cuda", enabled=False):
|
| 87 |
-
bias = (
|
| 88 |
-
None if self.bias is None else self.bias.to(input_dtype)
|
| 89 |
-
) # (d_out,) or None
|
| 90 |
-
return nn.functional.linear(
|
| 91 |
-
input, self.weight.to(input_dtype), bias
|
| 92 |
-
) # (..., d_out)
|
| 93 |
-
return nn.functional.linear(input, self.weight, self.bias) # (..., d_out)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
class LayerNorm(nn.Module):
|
| 97 |
-
"""Layer normalization that avoids an autocast upcast for BF16 inputs."""
|
| 98 |
-
|
| 99 |
-
def __init__(self, c_in: int, eps: float = 1e-5) -> None:
|
| 100 |
-
super().__init__()
|
| 101 |
-
self.c_in = (c_in,)
|
| 102 |
-
self.eps = eps
|
| 103 |
-
self.weight = nn.Parameter(torch.ones(c_in)) # (c_in,)
|
| 104 |
-
self.bias = nn.Parameter(torch.zeros(c_in)) # (c_in,)
|
| 105 |
-
|
| 106 |
-
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 107 |
-
# x: (..., c_in); normalization preserves shape.
|
| 108 |
-
if x.dtype is torch.bfloat16:
|
| 109 |
-
with torch.autocast("cuda", enabled=False):
|
| 110 |
-
return nn.functional.layer_norm(
|
| 111 |
-
x,
|
| 112 |
-
self.c_in,
|
| 113 |
-
self.weight.to(x.dtype),
|
| 114 |
-
self.bias.to(x.dtype),
|
| 115 |
-
self.eps,
|
| 116 |
-
)
|
| 117 |
-
return nn.functional.layer_norm(x, self.c_in, self.weight, self.bias, self.eps)
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
@torch.jit.ignore
|
| 121 |
-
def softmax_no_cast(tensor: torch.Tensor, dim: int = -1) -> torch.Tensor:
|
| 122 |
-
"""Apply softmax without promoting BF16 inputs under CUDA autocast."""
|
| 123 |
-
|
| 124 |
-
# tensor: (...); softmax preserves shape.
|
| 125 |
-
if tensor.dtype is torch.bfloat16:
|
| 126 |
-
with torch.autocast("cuda", enabled=False):
|
| 127 |
-
return nn.functional.softmax(tensor, dim=dim)
|
| 128 |
-
return nn.functional.softmax(tensor, dim=dim)
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
def _attention(
|
| 132 |
-
query: torch.Tensor,
|
| 133 |
-
key: torch.Tensor,
|
| 134 |
-
value: torch.Tensor,
|
| 135 |
-
biases: Sequence[torch.Tensor],
|
| 136 |
-
) -> torch.Tensor:
|
| 137 |
-
"""Compute biased scaled attention after query scaling."""
|
| 138 |
-
|
| 139 |
-
# query/key/value: (..., h, n_q_or_k, d_h).
|
| 140 |
-
scores = torch.matmul(
|
| 141 |
-
query, permute_final_dims(key, (1, 0))
|
| 142 |
-
) # (..., h, n_q, n_k)
|
| 143 |
-
for bias in biases:
|
| 144 |
-
scores += bias # (..., h, n_q, n_k)
|
| 145 |
-
probabilities = softmax_no_cast(scores, dim=-1) # (..., h, n_q, n_k)
|
| 146 |
-
return torch.matmul(probabilities, value) # (..., h, n_q, d_h)
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
@torch.compiler.disable
|
| 150 |
-
def kernel_triangular_attn(
|
| 151 |
-
q: torch.Tensor,
|
| 152 |
-
k: torch.Tensor,
|
| 153 |
-
v: torch.Tensor,
|
| 154 |
-
tri_bias: torch.Tensor,
|
| 155 |
-
mask: torch.Tensor,
|
| 156 |
-
scale: float,
|
| 157 |
-
) -> torch.Tensor:
|
| 158 |
-
"""Call the optional cuEquivariance triangle-attention primitive."""
|
| 159 |
-
|
| 160 |
-
# q/k/v: (..., h, n, d_h); returned tensor uses the kernel's matching layout.
|
| 161 |
-
if (
|
| 162 |
-
find_spec("cuequivariance_torch") is None
|
| 163 |
-
or find_spec("cuequivariance_ops_torch") is None
|
| 164 |
-
):
|
| 165 |
-
raise RuntimeError(
|
| 166 |
-
"Boltz2 use_kernels=True requires cuequivariance_torch and the CUDA 13 "
|
| 167 |
-
"cuequivariance_ops_torch runtime from the 'structure,cueq' extras."
|
| 168 |
-
)
|
| 169 |
-
cueq = importlib.import_module("cuequivariance_torch")
|
| 170 |
-
return cueq.triangle_attention(q, k, v, tri_bias, mask=mask, scale=scale)
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
class Attention(nn.Module):
|
| 174 |
-
"""Multi-head pair attention with optional query-dependent output gates."""
|
| 175 |
-
|
| 176 |
-
def __init__(
|
| 177 |
-
self,
|
| 178 |
-
c_q: int,
|
| 179 |
-
c_k: int,
|
| 180 |
-
c_v: int,
|
| 181 |
-
c_hidden: int,
|
| 182 |
-
no_heads: int,
|
| 183 |
-
gating: bool = True,
|
| 184 |
-
) -> None:
|
| 185 |
-
super().__init__()
|
| 186 |
-
self.c_q = c_q
|
| 187 |
-
self.c_k = c_k
|
| 188 |
-
self.c_v = c_v
|
| 189 |
-
self.c_hidden = c_hidden
|
| 190 |
-
self.no_heads = no_heads
|
| 191 |
-
self.gating = gating
|
| 192 |
-
|
| 193 |
-
projected_dim = c_hidden * no_heads
|
| 194 |
-
self.linear_q = Linear(c_q, projected_dim, bias=False, init="glorot")
|
| 195 |
-
self.linear_k = Linear(c_k, projected_dim, bias=False, init="glorot")
|
| 196 |
-
self.linear_v = Linear(c_v, projected_dim, bias=False, init="glorot")
|
| 197 |
-
self.linear_o = Linear(projected_dim, c_q, bias=False, init="final")
|
| 198 |
-
self.linear_g = Linear(c_q, projected_dim, bias=False, init="gating") if gating else None
|
| 199 |
-
self.sigmoid = nn.Sigmoid()
|
| 200 |
-
|
| 201 |
-
def _prep_qkv(
|
| 202 |
-
self,
|
| 203 |
-
q_x: torch.Tensor,
|
| 204 |
-
kv_x: torch.Tensor,
|
| 205 |
-
apply_scale: bool = True,
|
| 206 |
-
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 207 |
-
"""Project Q, K, and V to ``(..., h, n, d)`` tensors."""
|
| 208 |
-
|
| 209 |
-
def split_heads(X: torch.Tensor) -> torch.Tensor:
|
| 210 |
-
# X: (..., n, h * d_h)
|
| 211 |
-
return X.view(*X.shape[:-1], self.no_heads, -1).transpose(
|
| 212 |
-
-2, -3
|
| 213 |
-
) # (..., h, n, d_h)
|
| 214 |
-
|
| 215 |
-
# q_x: (..., n_q, c_q); kv_x: (..., n_k, c_k).
|
| 216 |
-
q = split_heads(self.linear_q(q_x)) # (..., h, n_q, d_h)
|
| 217 |
-
k = split_heads(self.linear_k(kv_x)) # (..., h, n_k, d_h)
|
| 218 |
-
v = split_heads(self.linear_v(kv_x)) # (..., h, n_k, d_h)
|
| 219 |
-
if apply_scale:
|
| 220 |
-
q /= math.sqrt(self.c_hidden) # (..., h, n_q, d_h)
|
| 221 |
-
return q, k, v # (..., h, n_q, d_h), two (..., h, n_k, d_h)
|
| 222 |
-
|
| 223 |
-
def _wrap_up(self, output: torch.Tensor, q_x: torch.Tensor) -> torch.Tensor:
|
| 224 |
-
"""Apply the optional gate, merge heads, and project the update."""
|
| 225 |
-
|
| 226 |
-
# output: (..., n_q, h, d_h); q_x: (..., n_q, c_q).
|
| 227 |
-
if self.linear_g is not None:
|
| 228 |
-
gate = self.sigmoid(self.linear_g(q_x)) # (..., n_q, h * d_h)
|
| 229 |
-
gate = gate.view(
|
| 230 |
-
*gate.shape[:-1], self.no_heads, -1
|
| 231 |
-
) # (..., n_q, h, d_h)
|
| 232 |
-
output = output * gate # (..., n_q, h, d_h)
|
| 233 |
-
return self.linear_o(
|
| 234 |
-
flatten_final_dims(output, 2)
|
| 235 |
-
) # (..., n_q, c_q)
|
| 236 |
-
|
| 237 |
-
def forward(
|
| 238 |
-
self,
|
| 239 |
-
q_x: torch.Tensor,
|
| 240 |
-
kv_x: torch.Tensor,
|
| 241 |
-
tri_bias: torch.Tensor,
|
| 242 |
-
mask_bias: torch.Tensor,
|
| 243 |
-
mask: torch.Tensor,
|
| 244 |
-
use_kernels: bool = False,
|
| 245 |
-
) -> torch.Tensor:
|
| 246 |
-
"""Return a gated pair update with shape ``(..., n, c_q)``."""
|
| 247 |
-
|
| 248 |
-
q, k, v = self._prep_qkv(
|
| 249 |
-
q_x, kv_x, apply_scale=not use_kernels
|
| 250 |
-
) # q: (..., h, n_q, d_h); k/v: (..., h, n_k, d_h)
|
| 251 |
-
if use_kernels:
|
| 252 |
-
output = kernel_triangular_attn(
|
| 253 |
-
q,
|
| 254 |
-
k,
|
| 255 |
-
v,
|
| 256 |
-
tri_bias=tri_bias,
|
| 257 |
-
mask=mask.bool(),
|
| 258 |
-
scale=1.0 / math.sqrt(self.c_hidden),
|
| 259 |
-
) # (..., h, n_q, d_h)
|
| 260 |
-
else:
|
| 261 |
-
output = _attention(
|
| 262 |
-
q, k, v, (mask_bias, tri_bias)
|
| 263 |
-
) # (..., h, n_q, d_h)
|
| 264 |
-
return self._wrap_up(
|
| 265 |
-
output.transpose(-2, -3), q_x
|
| 266 |
-
) # (..., n_q, c_q)
|
|
|
|
| 1 |
+
"""Attention primitives for the Boltz pair stack.
|
| 2 |
+
|
| 3 |
+
The module retains the converted checkpoint names while providing a compact
|
| 4 |
+
implementation around PyTorch operations. Optional triangle kernels are
|
| 5 |
+
loaded only at the call site, so importing FastPLMs does not initialize CUDA.
|
| 6 |
+
|
| 7 |
+
Portions of the numerical contract follow OpenFold and DeepMind AlphaFold
|
| 8 |
+
under Apache-2.0. See ``THIRD_PARTY_NOTICES.md`` for provenance.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import importlib
|
| 14 |
+
import math
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from collections.abc import Callable, Sequence
|
| 18 |
+
from importlib.util import find_spec
|
| 19 |
+
from torch import nn
|
| 20 |
+
|
| 21 |
+
from . import vb_layers_initialize as initialize
|
| 22 |
+
from .vb_tri_attn_utils import flatten_final_dims, permute_final_dims
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _initialize_weight(
|
| 26 |
+
weight: torch.Tensor,
|
| 27 |
+
bias: torch.Tensor | None,
|
| 28 |
+
method: str,
|
| 29 |
+
) -> None:
|
| 30 |
+
# weight: (d_out, d_in); bias: (d_out,) or None; both retain shape in place.
|
| 31 |
+
initializers = {
|
| 32 |
+
"default": initialize.lecun_normal_init_,
|
| 33 |
+
"relu": initialize.he_normal_init_,
|
| 34 |
+
"glorot": initialize.glorot_uniform_init_,
|
| 35 |
+
"normal": initialize.normal_init_,
|
| 36 |
+
"final": initialize.final_init_,
|
| 37 |
+
"gating": initialize.gating_init_,
|
| 38 |
+
}
|
| 39 |
+
try:
|
| 40 |
+
initializers[method](weight)
|
| 41 |
+
except KeyError as error:
|
| 42 |
+
raise ValueError(f"unknown linear initialization {method!r}") from error
|
| 43 |
+
if method == "gating" and bias is not None:
|
| 44 |
+
bias.fill_(1.0)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class Linear(nn.Linear):
|
| 48 |
+
"""Linear projection with the initializers used by converted checkpoints."""
|
| 49 |
+
|
| 50 |
+
def __init__(
|
| 51 |
+
self,
|
| 52 |
+
in_dim: int,
|
| 53 |
+
out_dim: int,
|
| 54 |
+
bias: bool = True,
|
| 55 |
+
init: str = "default",
|
| 56 |
+
init_fn: Callable[[torch.Tensor, torch.Tensor | None], None] | None = None,
|
| 57 |
+
precision: torch.dtype | None = None,
|
| 58 |
+
) -> None:
|
| 59 |
+
super().__init__(in_dim, out_dim, bias=bias)
|
| 60 |
+
with torch.no_grad():
|
| 61 |
+
if self.bias is not None:
|
| 62 |
+
self.bias.zero_()
|
| 63 |
+
if init_fn is None:
|
| 64 |
+
_initialize_weight(self.weight, self.bias, init)
|
| 65 |
+
else:
|
| 66 |
+
init_fn(self.weight, self.bias)
|
| 67 |
+
self.precision = precision
|
| 68 |
+
|
| 69 |
+
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
| 70 |
+
"""Project ``input`` while preserving explicit precision policies."""
|
| 71 |
+
|
| 72 |
+
# input: (..., d_in); every branch returns (..., d_out).
|
| 73 |
+
input_dtype = input.dtype
|
| 74 |
+
if self.precision is not None:
|
| 75 |
+
with torch.autocast("cuda", enabled=False):
|
| 76 |
+
bias = (
|
| 77 |
+
None if self.bias is None else self.bias.to(self.precision)
|
| 78 |
+
) # (d_out,) or None
|
| 79 |
+
projected = nn.functional.linear(
|
| 80 |
+
input.to(self.precision),
|
| 81 |
+
self.weight.to(self.precision),
|
| 82 |
+
bias,
|
| 83 |
+
) # (..., d_out)
|
| 84 |
+
return projected.to(input_dtype) # (..., d_out)
|
| 85 |
+
if input_dtype is torch.bfloat16:
|
| 86 |
+
with torch.autocast("cuda", enabled=False):
|
| 87 |
+
bias = (
|
| 88 |
+
None if self.bias is None else self.bias.to(input_dtype)
|
| 89 |
+
) # (d_out,) or None
|
| 90 |
+
return nn.functional.linear(
|
| 91 |
+
input, self.weight.to(input_dtype), bias
|
| 92 |
+
) # (..., d_out)
|
| 93 |
+
return nn.functional.linear(input, self.weight, self.bias) # (..., d_out)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class LayerNorm(nn.Module):
|
| 97 |
+
"""Layer normalization that avoids an autocast upcast for BF16 inputs."""
|
| 98 |
+
|
| 99 |
+
def __init__(self, c_in: int, eps: float = 1e-5) -> None:
|
| 100 |
+
super().__init__()
|
| 101 |
+
self.c_in = (c_in,)
|
| 102 |
+
self.eps = eps
|
| 103 |
+
self.weight = nn.Parameter(torch.ones(c_in)) # (c_in,)
|
| 104 |
+
self.bias = nn.Parameter(torch.zeros(c_in)) # (c_in,)
|
| 105 |
+
|
| 106 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 107 |
+
# x: (..., c_in); normalization preserves shape.
|
| 108 |
+
if x.dtype is torch.bfloat16:
|
| 109 |
+
with torch.autocast("cuda", enabled=False):
|
| 110 |
+
return nn.functional.layer_norm(
|
| 111 |
+
x,
|
| 112 |
+
self.c_in,
|
| 113 |
+
self.weight.to(x.dtype),
|
| 114 |
+
self.bias.to(x.dtype),
|
| 115 |
+
self.eps,
|
| 116 |
+
)
|
| 117 |
+
return nn.functional.layer_norm(x, self.c_in, self.weight, self.bias, self.eps)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@torch.jit.ignore
|
| 121 |
+
def softmax_no_cast(tensor: torch.Tensor, dim: int = -1) -> torch.Tensor:
|
| 122 |
+
"""Apply softmax without promoting BF16 inputs under CUDA autocast."""
|
| 123 |
+
|
| 124 |
+
# tensor: (...); softmax preserves shape.
|
| 125 |
+
if tensor.dtype is torch.bfloat16:
|
| 126 |
+
with torch.autocast("cuda", enabled=False):
|
| 127 |
+
return nn.functional.softmax(tensor, dim=dim)
|
| 128 |
+
return nn.functional.softmax(tensor, dim=dim)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _attention(
|
| 132 |
+
query: torch.Tensor,
|
| 133 |
+
key: torch.Tensor,
|
| 134 |
+
value: torch.Tensor,
|
| 135 |
+
biases: Sequence[torch.Tensor],
|
| 136 |
+
) -> torch.Tensor:
|
| 137 |
+
"""Compute biased scaled attention after query scaling."""
|
| 138 |
+
|
| 139 |
+
# query/key/value: (..., h, n_q_or_k, d_h).
|
| 140 |
+
scores = torch.matmul(
|
| 141 |
+
query, permute_final_dims(key, (1, 0))
|
| 142 |
+
) # (..., h, n_q, n_k)
|
| 143 |
+
for bias in biases:
|
| 144 |
+
scores += bias # (..., h, n_q, n_k)
|
| 145 |
+
probabilities = softmax_no_cast(scores, dim=-1) # (..., h, n_q, n_k)
|
| 146 |
+
return torch.matmul(probabilities, value) # (..., h, n_q, d_h)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@torch.compiler.disable
|
| 150 |
+
def kernel_triangular_attn(
|
| 151 |
+
q: torch.Tensor,
|
| 152 |
+
k: torch.Tensor,
|
| 153 |
+
v: torch.Tensor,
|
| 154 |
+
tri_bias: torch.Tensor,
|
| 155 |
+
mask: torch.Tensor,
|
| 156 |
+
scale: float,
|
| 157 |
+
) -> torch.Tensor:
|
| 158 |
+
"""Call the optional cuEquivariance triangle-attention primitive."""
|
| 159 |
+
|
| 160 |
+
# q/k/v: (..., h, n, d_h); returned tensor uses the kernel's matching layout.
|
| 161 |
+
if (
|
| 162 |
+
find_spec("cuequivariance_torch") is None
|
| 163 |
+
or find_spec("cuequivariance_ops_torch") is None
|
| 164 |
+
):
|
| 165 |
+
raise RuntimeError(
|
| 166 |
+
"Boltz2 use_kernels=True requires cuequivariance_torch and the CUDA 13 "
|
| 167 |
+
"cuequivariance_ops_torch runtime from the 'structure,cueq' extras."
|
| 168 |
+
)
|
| 169 |
+
cueq = importlib.import_module("cuequivariance_torch")
|
| 170 |
+
return cueq.triangle_attention(q, k, v, tri_bias, mask=mask, scale=scale)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class Attention(nn.Module):
|
| 174 |
+
"""Multi-head pair attention with optional query-dependent output gates."""
|
| 175 |
+
|
| 176 |
+
def __init__(
|
| 177 |
+
self,
|
| 178 |
+
c_q: int,
|
| 179 |
+
c_k: int,
|
| 180 |
+
c_v: int,
|
| 181 |
+
c_hidden: int,
|
| 182 |
+
no_heads: int,
|
| 183 |
+
gating: bool = True,
|
| 184 |
+
) -> None:
|
| 185 |
+
super().__init__()
|
| 186 |
+
self.c_q = c_q
|
| 187 |
+
self.c_k = c_k
|
| 188 |
+
self.c_v = c_v
|
| 189 |
+
self.c_hidden = c_hidden
|
| 190 |
+
self.no_heads = no_heads
|
| 191 |
+
self.gating = gating
|
| 192 |
+
|
| 193 |
+
projected_dim = c_hidden * no_heads
|
| 194 |
+
self.linear_q = Linear(c_q, projected_dim, bias=False, init="glorot")
|
| 195 |
+
self.linear_k = Linear(c_k, projected_dim, bias=False, init="glorot")
|
| 196 |
+
self.linear_v = Linear(c_v, projected_dim, bias=False, init="glorot")
|
| 197 |
+
self.linear_o = Linear(projected_dim, c_q, bias=False, init="final")
|
| 198 |
+
self.linear_g = Linear(c_q, projected_dim, bias=False, init="gating") if gating else None
|
| 199 |
+
self.sigmoid = nn.Sigmoid()
|
| 200 |
+
|
| 201 |
+
def _prep_qkv(
|
| 202 |
+
self,
|
| 203 |
+
q_x: torch.Tensor,
|
| 204 |
+
kv_x: torch.Tensor,
|
| 205 |
+
apply_scale: bool = True,
|
| 206 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 207 |
+
"""Project Q, K, and V to ``(..., h, n, d)`` tensors."""
|
| 208 |
+
|
| 209 |
+
def split_heads(X: torch.Tensor) -> torch.Tensor:
|
| 210 |
+
# X: (..., n, h * d_h)
|
| 211 |
+
return X.view(*X.shape[:-1], self.no_heads, -1).transpose(
|
| 212 |
+
-2, -3
|
| 213 |
+
) # (..., h, n, d_h)
|
| 214 |
+
|
| 215 |
+
# q_x: (..., n_q, c_q); kv_x: (..., n_k, c_k).
|
| 216 |
+
q = split_heads(self.linear_q(q_x)) # (..., h, n_q, d_h)
|
| 217 |
+
k = split_heads(self.linear_k(kv_x)) # (..., h, n_k, d_h)
|
| 218 |
+
v = split_heads(self.linear_v(kv_x)) # (..., h, n_k, d_h)
|
| 219 |
+
if apply_scale:
|
| 220 |
+
q /= math.sqrt(self.c_hidden) # (..., h, n_q, d_h)
|
| 221 |
+
return q, k, v # (..., h, n_q, d_h), two (..., h, n_k, d_h)
|
| 222 |
+
|
| 223 |
+
def _wrap_up(self, output: torch.Tensor, q_x: torch.Tensor) -> torch.Tensor:
|
| 224 |
+
"""Apply the optional gate, merge heads, and project the update."""
|
| 225 |
+
|
| 226 |
+
# output: (..., n_q, h, d_h); q_x: (..., n_q, c_q).
|
| 227 |
+
if self.linear_g is not None:
|
| 228 |
+
gate = self.sigmoid(self.linear_g(q_x)) # (..., n_q, h * d_h)
|
| 229 |
+
gate = gate.view(
|
| 230 |
+
*gate.shape[:-1], self.no_heads, -1
|
| 231 |
+
) # (..., n_q, h, d_h)
|
| 232 |
+
output = output * gate # (..., n_q, h, d_h)
|
| 233 |
+
return self.linear_o(
|
| 234 |
+
flatten_final_dims(output, 2)
|
| 235 |
+
) # (..., n_q, c_q)
|
| 236 |
+
|
| 237 |
+
def forward(
|
| 238 |
+
self,
|
| 239 |
+
q_x: torch.Tensor,
|
| 240 |
+
kv_x: torch.Tensor,
|
| 241 |
+
tri_bias: torch.Tensor,
|
| 242 |
+
mask_bias: torch.Tensor,
|
| 243 |
+
mask: torch.Tensor,
|
| 244 |
+
use_kernels: bool = False,
|
| 245 |
+
) -> torch.Tensor:
|
| 246 |
+
"""Return a gated pair update with shape ``(..., n, c_q)``."""
|
| 247 |
+
|
| 248 |
+
q, k, v = self._prep_qkv(
|
| 249 |
+
q_x, kv_x, apply_scale=not use_kernels
|
| 250 |
+
) # q: (..., h, n_q, d_h); k/v: (..., h, n_k, d_h)
|
| 251 |
+
if use_kernels:
|
| 252 |
+
output = kernel_triangular_attn(
|
| 253 |
+
q,
|
| 254 |
+
k,
|
| 255 |
+
v,
|
| 256 |
+
tri_bias=tri_bias,
|
| 257 |
+
mask=mask.bool(),
|
| 258 |
+
scale=1.0 / math.sqrt(self.c_hidden),
|
| 259 |
+
) # (..., h, n_q, d_h)
|
| 260 |
+
else:
|
| 261 |
+
output = _attention(
|
| 262 |
+
q, k, v, (mask_bias, tri_bias)
|
| 263 |
+
) # (..., h, n_q, d_h)
|
| 264 |
+
return self._wrap_up(
|
| 265 |
+
output.transpose(-2, -3), q_x
|
| 266 |
+
) # (..., n_q, c_q)
|
fastplms/models/boltz/vb_tri_attn_utils.py
CHANGED
|
@@ -1,341 +1,341 @@
|
|
| 1 |
-
"""Tensor-tree and memory-bounded execution helpers for pair attention.
|
| 2 |
-
|
| 3 |
-
These utilities implement the small subset of OpenFold-style chunking needed
|
| 4 |
-
by the local Boltz pair stack. The implementation is independent, but keeps
|
| 5 |
-
the public function names expected by converted checkpoints.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import torch
|
| 11 |
-
|
| 12 |
-
from collections.abc import Callable, Sequence
|
| 13 |
-
from functools import partial
|
| 14 |
-
from math import prod
|
| 15 |
-
from typing import Any
|
| 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 |
-
# left/right: broadcast-compatible shapes; result has their broadcast shape.
|
| 22 |
-
if inplace:
|
| 23 |
-
left += right # same shape as left
|
| 24 |
-
return left # same shape as left
|
| 25 |
-
return left + right # broadcast shape
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def permute_final_dims(tensor: torch.Tensor, inds: Sequence[int]) -> torch.Tensor:
|
| 29 |
-
"""Permute only the final ``len(inds)`` axes of ``tensor``."""
|
| 30 |
-
|
| 31 |
-
# tensor: (..., d_0, ..., d_n); inds reorders the named final axes.
|
| 32 |
-
final_count = len(inds)
|
| 33 |
-
leading = list(range(tensor.ndim - final_count))
|
| 34 |
-
final = [tensor.ndim - final_count + index for index in inds]
|
| 35 |
-
return tensor.permute(leading + final) # (..., d_0, ..., d_n)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def is_fp16_enabled() -> bool:
|
| 39 |
-
"""Return whether CUDA autocast currently targets ``float16``."""
|
| 40 |
-
|
| 41 |
-
return torch.is_autocast_enabled() and torch.get_autocast_dtype("cuda") == torch.float16
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def dict_map(
|
| 45 |
-
fn: Callable[[Any], Any],
|
| 46 |
-
dic: dict[Any, Any],
|
| 47 |
-
leaf_type: type | tuple[type, ...],
|
| 48 |
-
) -> dict[Any, Any]:
|
| 49 |
-
"""Apply ``fn`` to leaves in a nested dictionary tree."""
|
| 50 |
-
|
| 51 |
-
return {key: tree_map(fn, value, leaf_type) for key, value in dic.items()}
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def tree_map(
|
| 55 |
-
fn: Callable[[Any], Any],
|
| 56 |
-
tree: Any,
|
| 57 |
-
leaf_type: type | tuple[type, ...],
|
| 58 |
-
) -> Any:
|
| 59 |
-
"""Map a function over dict, list, and tuple containers."""
|
| 60 |
-
|
| 61 |
-
if isinstance(tree, leaf_type):
|
| 62 |
-
return fn(tree)
|
| 63 |
-
if isinstance(tree, dict):
|
| 64 |
-
return dict_map(fn, tree, leaf_type)
|
| 65 |
-
if isinstance(tree, list):
|
| 66 |
-
return [tree_map(fn, item, leaf_type) for item in tree]
|
| 67 |
-
if isinstance(tree, tuple):
|
| 68 |
-
return tuple(tree_map(fn, item, leaf_type) for item in tree)
|
| 69 |
-
raise ValueError(f"tree type {type(tree)!r} is not supported")
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
tensor_tree_map = partial(tree_map, leaf_type=torch.Tensor)
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def flatten_final_dims(tensor: torch.Tensor, no_dims: int) -> torch.Tensor:
|
| 76 |
-
"""Collapse the final ``no_dims`` axes into one axis."""
|
| 77 |
-
|
| 78 |
-
# tensor: (..., d_0, ..., d_n)
|
| 79 |
-
return tensor.reshape(*tensor.shape[:-no_dims], -1) # (..., prod(final dims))
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def _fetch_dims(tree: Any) -> list[torch.Size]:
|
| 83 |
-
"""Collect tensor shapes from a supported tree."""
|
| 84 |
-
|
| 85 |
-
if isinstance(tree, torch.Tensor):
|
| 86 |
-
return [tree.shape]
|
| 87 |
-
if isinstance(tree, dict):
|
| 88 |
-
children = tree.values()
|
| 89 |
-
elif isinstance(tree, (list, tuple)):
|
| 90 |
-
children = tree
|
| 91 |
-
else:
|
| 92 |
-
raise ValueError(f"tree type {type(tree)!r} is not supported")
|
| 93 |
-
|
| 94 |
-
shapes: list[torch.Size] = []
|
| 95 |
-
for child in children:
|
| 96 |
-
shapes.extend(_fetch_dims(child))
|
| 97 |
-
return shapes
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
@torch.jit.ignore
|
| 101 |
-
def _flat_idx_to_idx(flat_idx: int, dims: tuple[int, ...]) -> tuple[int, ...]:
|
| 102 |
-
"""Convert a row-major flat index into an index tuple."""
|
| 103 |
-
|
| 104 |
-
coordinates = [0] * len(dims)
|
| 105 |
-
remainder = flat_idx
|
| 106 |
-
for axis in range(len(dims) - 1, -1, -1):
|
| 107 |
-
remainder, coordinates[axis] = divmod(remainder, dims[axis])
|
| 108 |
-
return tuple(coordinates)
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def _ravel_index(index: Sequence[int], dims: Sequence[int]) -> int:
|
| 112 |
-
"""Convert a row-major index tuple to a flat index."""
|
| 113 |
-
|
| 114 |
-
flat = 0
|
| 115 |
-
for coordinate, size in zip(index, dims, strict=True):
|
| 116 |
-
flat = flat * size + coordinate
|
| 117 |
-
return flat
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def _cover_flat_interval(
|
| 121 |
-
start: int,
|
| 122 |
-
stop: int,
|
| 123 |
-
dims: tuple[int, ...],
|
| 124 |
-
) -> list[tuple[slice, ...]]:
|
| 125 |
-
"""Cover ``[start, stop)`` with ordered contiguous tensor slices."""
|
| 126 |
-
|
| 127 |
-
if not dims:
|
| 128 |
-
return [tuple()]
|
| 129 |
-
if len(dims) == 1:
|
| 130 |
-
return [(slice(start, stop),)]
|
| 131 |
-
|
| 132 |
-
child_size = prod(dims[1:])
|
| 133 |
-
first_child = start // child_size
|
| 134 |
-
last_child = (stop - 1) // child_size
|
| 135 |
-
if first_child == last_child:
|
| 136 |
-
tail = _cover_flat_interval(
|
| 137 |
-
start % child_size,
|
| 138 |
-
(stop - 1) % child_size + 1,
|
| 139 |
-
dims[1:],
|
| 140 |
-
)
|
| 141 |
-
prefix = slice(first_child, first_child + 1)
|
| 142 |
-
return [(prefix, *item) for item in tail]
|
| 143 |
-
|
| 144 |
-
slices: list[tuple[slice, ...]] = []
|
| 145 |
-
start_offset = start % child_size
|
| 146 |
-
first_full_child = first_child
|
| 147 |
-
if start_offset:
|
| 148 |
-
prefix = slice(first_child, first_child + 1)
|
| 149 |
-
slices.extend(
|
| 150 |
-
(prefix, *item) for item in _cover_flat_interval(start_offset, child_size, dims[1:])
|
| 151 |
-
)
|
| 152 |
-
first_full_child += 1
|
| 153 |
-
|
| 154 |
-
stop_offset = stop % child_size
|
| 155 |
-
full_stop = last_child if stop_offset else last_child + 1
|
| 156 |
-
if first_full_child < full_stop:
|
| 157 |
-
slices.append((slice(first_full_child, full_stop),))
|
| 158 |
-
|
| 159 |
-
if stop_offset:
|
| 160 |
-
prefix = slice(last_child, last_child + 1)
|
| 161 |
-
slices.extend((prefix, *item) for item in _cover_flat_interval(0, stop_offset, dims[1:]))
|
| 162 |
-
return slices
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
@torch.jit.ignore
|
| 166 |
-
def _get_minimal_slice_set(
|
| 167 |
-
start: Sequence[int],
|
| 168 |
-
end: Sequence[int],
|
| 169 |
-
dims: Sequence[int],
|
| 170 |
-
start_edges: Sequence[bool] | None = None,
|
| 171 |
-
end_edges: Sequence[bool] | None = None,
|
| 172 |
-
) -> list[tuple[slice, ...]]:
|
| 173 |
-
"""Return ordered slices covering the inclusive row-major interval.
|
| 174 |
-
|
| 175 |
-
``start_edges`` and ``end_edges`` remain accepted for compatibility. The
|
| 176 |
-
interval decomposition derives the same information directly.
|
| 177 |
-
"""
|
| 178 |
-
|
| 179 |
-
del start_edges, end_edges
|
| 180 |
-
shape = tuple(dims)
|
| 181 |
-
if not shape:
|
| 182 |
-
return [tuple()]
|
| 183 |
-
flat_start = _ravel_index(start, shape)
|
| 184 |
-
flat_stop = _ravel_index(end, shape) + 1
|
| 185 |
-
return _cover_flat_interval(flat_start, flat_stop, shape)
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
@torch.jit.ignore
|
| 189 |
-
def _chunk_slice(
|
| 190 |
-
tensor: torch.Tensor,
|
| 191 |
-
flat_start: int,
|
| 192 |
-
flat_end: int,
|
| 193 |
-
no_batch_dims: int,
|
| 194 |
-
) -> torch.Tensor:
|
| 195 |
-
"""Slice a flattened batch interval without flattening the full tensor."""
|
| 196 |
-
|
| 197 |
-
# tensor: (*batch_shape, *feature_shape).
|
| 198 |
-
batch_shape = tuple(tensor.shape[:no_batch_dims])
|
| 199 |
-
start = _flat_idx_to_idx(flat_start, batch_shape)
|
| 200 |
-
end = _flat_idx_to_idx(flat_end - 1, batch_shape)
|
| 201 |
-
pieces = [
|
| 202 |
-
tensor[item] for item in _get_minimal_slice_set(start, end, batch_shape)
|
| 203 |
-
] # each: (*covered_batch_shape, *feature_shape)
|
| 204 |
-
feature_shape = tuple(tensor.shape[no_batch_dims:])
|
| 205 |
-
return torch.cat(
|
| 206 |
-
[piece.reshape(-1, *feature_shape) for piece in pieces]
|
| 207 |
-
) # (flat_end - flat_start, *feature_shape)
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
def _prepare_input(
|
| 211 |
-
tensor: torch.Tensor,
|
| 212 |
-
batch_shape: tuple[int, ...],
|
| 213 |
-
no_batch_dims: int,
|
| 214 |
-
*,
|
| 215 |
-
low_mem: bool,
|
| 216 |
-
) -> torch.Tensor:
|
| 217 |
-
# tensor: (*input_batch_shape, *feature_shape)
|
| 218 |
-
feature_shape = tuple(tensor.shape[no_batch_dims:])
|
| 219 |
-
if low_mem:
|
| 220 |
-
return tensor.expand(batch_shape + feature_shape) # (*batch_shape, *feature_shape)
|
| 221 |
-
if any(size != 1 for size in tensor.shape[:no_batch_dims]):
|
| 222 |
-
tensor = tensor.expand(
|
| 223 |
-
batch_shape + feature_shape
|
| 224 |
-
) # (*batch_shape, *feature_shape)
|
| 225 |
-
return tensor.reshape(-1, *feature_shape) # (flat_batch, *feature_shape)
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
def _write_chunk(
|
| 229 |
-
destination: Any,
|
| 230 |
-
source: Any,
|
| 231 |
-
start: int,
|
| 232 |
-
stop: int,
|
| 233 |
-
*,
|
| 234 |
-
add_into_out: bool,
|
| 235 |
-
) -> None:
|
| 236 |
-
if isinstance(source, dict):
|
| 237 |
-
for key, value in source.items():
|
| 238 |
-
_write_chunk(
|
| 239 |
-
destination[key],
|
| 240 |
-
value,
|
| 241 |
-
start,
|
| 242 |
-
stop,
|
| 243 |
-
add_into_out=add_into_out,
|
| 244 |
-
)
|
| 245 |
-
return
|
| 246 |
-
if isinstance(source, (tuple, list)):
|
| 247 |
-
for destination_item, source_item in zip(destination, source, strict=True):
|
| 248 |
-
_write_chunk(
|
| 249 |
-
destination_item,
|
| 250 |
-
source_item,
|
| 251 |
-
start,
|
| 252 |
-
stop,
|
| 253 |
-
add_into_out=add_into_out,
|
| 254 |
-
)
|
| 255 |
-
return
|
| 256 |
-
if not isinstance(source, torch.Tensor):
|
| 257 |
-
raise ValueError(f"output type {type(source)!r} is not supported")
|
| 258 |
-
if add_into_out:
|
| 259 |
-
destination[start:stop] += source # (stop - start, *feature_shape)
|
| 260 |
-
else:
|
| 261 |
-
destination[start:stop] = source # (stop - start, *feature_shape)
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
def chunk_layer(
|
| 265 |
-
layer: Callable[..., Any],
|
| 266 |
-
inputs: dict[str, Any],
|
| 267 |
-
chunk_size: int,
|
| 268 |
-
no_batch_dims: int,
|
| 269 |
-
low_mem: bool = False,
|
| 270 |
-
_out: Any = None,
|
| 271 |
-
_add_into_out: bool = False,
|
| 272 |
-
) -> Any:
|
| 273 |
-
"""Apply ``layer`` to flattened batch chunks and reassemble its output."""
|
| 274 |
-
|
| 275 |
-
if not inputs:
|
| 276 |
-
raise ValueError("at least one input is required")
|
| 277 |
-
if chunk_size <= 0:
|
| 278 |
-
raise ValueError("chunk_size must be positive")
|
| 279 |
-
|
| 280 |
-
leading_shapes = [shape[:no_batch_dims] for shape in _fetch_dims(inputs)]
|
| 281 |
-
batch_shape = tuple(max(sizes) for sizes in zip(*leading_shapes, strict=True))
|
| 282 |
-
prepare = partial(
|
| 283 |
-
_prepare_input,
|
| 284 |
-
batch_shape=batch_shape,
|
| 285 |
-
no_batch_dims=no_batch_dims,
|
| 286 |
-
low_mem=low_mem,
|
| 287 |
-
)
|
| 288 |
-
prepared_inputs = tensor_tree_map(
|
| 289 |
-
prepare, inputs
|
| 290 |
-
) # each tensor: (flat_batch, *feature_shape), or broadcast batch in low-memory mode
|
| 291 |
-
|
| 292 |
-
output = None
|
| 293 |
-
if _out is not None:
|
| 294 |
-
output = tensor_tree_map(
|
| 295 |
-
lambda tensor: tensor.reshape(-1, *tensor.shape[no_batch_dims:]),
|
| 296 |
-
_out,
|
| 297 |
-
) # each tensor: (flat_batch, *feature_shape)
|
| 298 |
-
|
| 299 |
-
flat_batch_size = prod(batch_shape)
|
| 300 |
-
for start in range(0, flat_batch_size, chunk_size):
|
| 301 |
-
stop = min(flat_batch_size, start + chunk_size)
|
| 302 |
-
if low_mem:
|
| 303 |
-
select = partial(
|
| 304 |
-
_chunk_slice,
|
| 305 |
-
flat_start=start,
|
| 306 |
-
flat_end=stop,
|
| 307 |
-
no_batch_dims=len(batch_shape),
|
| 308 |
-
)
|
| 309 |
-
else:
|
| 310 |
-
|
| 311 |
-
def select(
|
| 312 |
-
tensor: torch.Tensor,
|
| 313 |
-
start: int = start,
|
| 314 |
-
stop: int = stop,
|
| 315 |
-
) -> torch.Tensor:
|
| 316 |
-
return tensor if tensor.shape[0] == 1 else tensor[start:stop]
|
| 317 |
-
|
| 318 |
-
input_chunk = tensor_tree_map(
|
| 319 |
-
select, prepared_inputs
|
| 320 |
-
) # each tensor: (chunk, *feature_shape)
|
| 321 |
-
output_chunk = layer(
|
| 322 |
-
**input_chunk
|
| 323 |
-
) # each output tensor: (chunk, *output_feature_shape)
|
| 324 |
-
|
| 325 |
-
if output is None:
|
| 326 |
-
output = tensor_tree_map(
|
| 327 |
-
lambda tensor: tensor.new_zeros((flat_batch_size, *tensor.shape[1:])),
|
| 328 |
-
output_chunk,
|
| 329 |
-
) # each tensor: (flat_batch, *output_feature_shape)
|
| 330 |
-
_write_chunk(
|
| 331 |
-
output,
|
| 332 |
-
output_chunk,
|
| 333 |
-
start,
|
| 334 |
-
stop,
|
| 335 |
-
add_into_out=_add_into_out,
|
| 336 |
-
)
|
| 337 |
-
|
| 338 |
-
return tensor_tree_map(
|
| 339 |
-
lambda tensor: tensor.reshape(batch_shape + tuple(tensor.shape[1:])),
|
| 340 |
-
output,
|
| 341 |
-
) # each tensor: (*batch_shape, *output_feature_shape)
|
|
|
|
| 1 |
+
"""Tensor-tree and memory-bounded execution helpers for pair attention.
|
| 2 |
+
|
| 3 |
+
These utilities implement the small subset of OpenFold-style chunking needed
|
| 4 |
+
by the local Boltz pair stack. The implementation is independent, but keeps
|
| 5 |
+
the public function names expected by converted checkpoints.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from collections.abc import Callable, Sequence
|
| 13 |
+
from functools import partial
|
| 14 |
+
from math import prod
|
| 15 |
+
from typing import Any
|
| 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 |
+
# left/right: broadcast-compatible shapes; result has their broadcast shape.
|
| 22 |
+
if inplace:
|
| 23 |
+
left += right # same shape as left
|
| 24 |
+
return left # same shape as left
|
| 25 |
+
return left + right # broadcast shape
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def permute_final_dims(tensor: torch.Tensor, inds: Sequence[int]) -> torch.Tensor:
|
| 29 |
+
"""Permute only the final ``len(inds)`` axes of ``tensor``."""
|
| 30 |
+
|
| 31 |
+
# tensor: (..., d_0, ..., d_n); inds reorders the named final axes.
|
| 32 |
+
final_count = len(inds)
|
| 33 |
+
leading = list(range(tensor.ndim - final_count))
|
| 34 |
+
final = [tensor.ndim - final_count + index for index in inds]
|
| 35 |
+
return tensor.permute(leading + final) # (..., d_0, ..., d_n)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def is_fp16_enabled() -> bool:
|
| 39 |
+
"""Return whether CUDA autocast currently targets ``float16``."""
|
| 40 |
+
|
| 41 |
+
return torch.is_autocast_enabled() and torch.get_autocast_dtype("cuda") == torch.float16
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def dict_map(
|
| 45 |
+
fn: Callable[[Any], Any],
|
| 46 |
+
dic: dict[Any, Any],
|
| 47 |
+
leaf_type: type | tuple[type, ...],
|
| 48 |
+
) -> dict[Any, Any]:
|
| 49 |
+
"""Apply ``fn`` to leaves in a nested dictionary tree."""
|
| 50 |
+
|
| 51 |
+
return {key: tree_map(fn, value, leaf_type) for key, value in dic.items()}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def tree_map(
|
| 55 |
+
fn: Callable[[Any], Any],
|
| 56 |
+
tree: Any,
|
| 57 |
+
leaf_type: type | tuple[type, ...],
|
| 58 |
+
) -> Any:
|
| 59 |
+
"""Map a function over dict, list, and tuple containers."""
|
| 60 |
+
|
| 61 |
+
if isinstance(tree, leaf_type):
|
| 62 |
+
return fn(tree)
|
| 63 |
+
if isinstance(tree, dict):
|
| 64 |
+
return dict_map(fn, tree, leaf_type)
|
| 65 |
+
if isinstance(tree, list):
|
| 66 |
+
return [tree_map(fn, item, leaf_type) for item in tree]
|
| 67 |
+
if isinstance(tree, tuple):
|
| 68 |
+
return tuple(tree_map(fn, item, leaf_type) for item in tree)
|
| 69 |
+
raise ValueError(f"tree type {type(tree)!r} is not supported")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
tensor_tree_map = partial(tree_map, leaf_type=torch.Tensor)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def flatten_final_dims(tensor: torch.Tensor, no_dims: int) -> torch.Tensor:
|
| 76 |
+
"""Collapse the final ``no_dims`` axes into one axis."""
|
| 77 |
+
|
| 78 |
+
# tensor: (..., d_0, ..., d_n)
|
| 79 |
+
return tensor.reshape(*tensor.shape[:-no_dims], -1) # (..., prod(final dims))
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _fetch_dims(tree: Any) -> list[torch.Size]:
|
| 83 |
+
"""Collect tensor shapes from a supported tree."""
|
| 84 |
+
|
| 85 |
+
if isinstance(tree, torch.Tensor):
|
| 86 |
+
return [tree.shape]
|
| 87 |
+
if isinstance(tree, dict):
|
| 88 |
+
children = tree.values()
|
| 89 |
+
elif isinstance(tree, (list, tuple)):
|
| 90 |
+
children = tree
|
| 91 |
+
else:
|
| 92 |
+
raise ValueError(f"tree type {type(tree)!r} is not supported")
|
| 93 |
+
|
| 94 |
+
shapes: list[torch.Size] = []
|
| 95 |
+
for child in children:
|
| 96 |
+
shapes.extend(_fetch_dims(child))
|
| 97 |
+
return shapes
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@torch.jit.ignore
|
| 101 |
+
def _flat_idx_to_idx(flat_idx: int, dims: tuple[int, ...]) -> tuple[int, ...]:
|
| 102 |
+
"""Convert a row-major flat index into an index tuple."""
|
| 103 |
+
|
| 104 |
+
coordinates = [0] * len(dims)
|
| 105 |
+
remainder = flat_idx
|
| 106 |
+
for axis in range(len(dims) - 1, -1, -1):
|
| 107 |
+
remainder, coordinates[axis] = divmod(remainder, dims[axis])
|
| 108 |
+
return tuple(coordinates)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _ravel_index(index: Sequence[int], dims: Sequence[int]) -> int:
|
| 112 |
+
"""Convert a row-major index tuple to a flat index."""
|
| 113 |
+
|
| 114 |
+
flat = 0
|
| 115 |
+
for coordinate, size in zip(index, dims, strict=True):
|
| 116 |
+
flat = flat * size + coordinate
|
| 117 |
+
return flat
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _cover_flat_interval(
|
| 121 |
+
start: int,
|
| 122 |
+
stop: int,
|
| 123 |
+
dims: tuple[int, ...],
|
| 124 |
+
) -> list[tuple[slice, ...]]:
|
| 125 |
+
"""Cover ``[start, stop)`` with ordered contiguous tensor slices."""
|
| 126 |
+
|
| 127 |
+
if not dims:
|
| 128 |
+
return [tuple()]
|
| 129 |
+
if len(dims) == 1:
|
| 130 |
+
return [(slice(start, stop),)]
|
| 131 |
+
|
| 132 |
+
child_size = prod(dims[1:])
|
| 133 |
+
first_child = start // child_size
|
| 134 |
+
last_child = (stop - 1) // child_size
|
| 135 |
+
if first_child == last_child:
|
| 136 |
+
tail = _cover_flat_interval(
|
| 137 |
+
start % child_size,
|
| 138 |
+
(stop - 1) % child_size + 1,
|
| 139 |
+
dims[1:],
|
| 140 |
+
)
|
| 141 |
+
prefix = slice(first_child, first_child + 1)
|
| 142 |
+
return [(prefix, *item) for item in tail]
|
| 143 |
+
|
| 144 |
+
slices: list[tuple[slice, ...]] = []
|
| 145 |
+
start_offset = start % child_size
|
| 146 |
+
first_full_child = first_child
|
| 147 |
+
if start_offset:
|
| 148 |
+
prefix = slice(first_child, first_child + 1)
|
| 149 |
+
slices.extend(
|
| 150 |
+
(prefix, *item) for item in _cover_flat_interval(start_offset, child_size, dims[1:])
|
| 151 |
+
)
|
| 152 |
+
first_full_child += 1
|
| 153 |
+
|
| 154 |
+
stop_offset = stop % child_size
|
| 155 |
+
full_stop = last_child if stop_offset else last_child + 1
|
| 156 |
+
if first_full_child < full_stop:
|
| 157 |
+
slices.append((slice(first_full_child, full_stop),))
|
| 158 |
+
|
| 159 |
+
if stop_offset:
|
| 160 |
+
prefix = slice(last_child, last_child + 1)
|
| 161 |
+
slices.extend((prefix, *item) for item in _cover_flat_interval(0, stop_offset, dims[1:]))
|
| 162 |
+
return slices
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@torch.jit.ignore
|
| 166 |
+
def _get_minimal_slice_set(
|
| 167 |
+
start: Sequence[int],
|
| 168 |
+
end: Sequence[int],
|
| 169 |
+
dims: Sequence[int],
|
| 170 |
+
start_edges: Sequence[bool] | None = None,
|
| 171 |
+
end_edges: Sequence[bool] | None = None,
|
| 172 |
+
) -> list[tuple[slice, ...]]:
|
| 173 |
+
"""Return ordered slices covering the inclusive row-major interval.
|
| 174 |
+
|
| 175 |
+
``start_edges`` and ``end_edges`` remain accepted for compatibility. The
|
| 176 |
+
interval decomposition derives the same information directly.
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
del start_edges, end_edges
|
| 180 |
+
shape = tuple(dims)
|
| 181 |
+
if not shape:
|
| 182 |
+
return [tuple()]
|
| 183 |
+
flat_start = _ravel_index(start, shape)
|
| 184 |
+
flat_stop = _ravel_index(end, shape) + 1
|
| 185 |
+
return _cover_flat_interval(flat_start, flat_stop, shape)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@torch.jit.ignore
|
| 189 |
+
def _chunk_slice(
|
| 190 |
+
tensor: torch.Tensor,
|
| 191 |
+
flat_start: int,
|
| 192 |
+
flat_end: int,
|
| 193 |
+
no_batch_dims: int,
|
| 194 |
+
) -> torch.Tensor:
|
| 195 |
+
"""Slice a flattened batch interval without flattening the full tensor."""
|
| 196 |
+
|
| 197 |
+
# tensor: (*batch_shape, *feature_shape).
|
| 198 |
+
batch_shape = tuple(tensor.shape[:no_batch_dims])
|
| 199 |
+
start = _flat_idx_to_idx(flat_start, batch_shape)
|
| 200 |
+
end = _flat_idx_to_idx(flat_end - 1, batch_shape)
|
| 201 |
+
pieces = [
|
| 202 |
+
tensor[item] for item in _get_minimal_slice_set(start, end, batch_shape)
|
| 203 |
+
] # each: (*covered_batch_shape, *feature_shape)
|
| 204 |
+
feature_shape = tuple(tensor.shape[no_batch_dims:])
|
| 205 |
+
return torch.cat(
|
| 206 |
+
[piece.reshape(-1, *feature_shape) for piece in pieces]
|
| 207 |
+
) # (flat_end - flat_start, *feature_shape)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def _prepare_input(
|
| 211 |
+
tensor: torch.Tensor,
|
| 212 |
+
batch_shape: tuple[int, ...],
|
| 213 |
+
no_batch_dims: int,
|
| 214 |
+
*,
|
| 215 |
+
low_mem: bool,
|
| 216 |
+
) -> torch.Tensor:
|
| 217 |
+
# tensor: (*input_batch_shape, *feature_shape)
|
| 218 |
+
feature_shape = tuple(tensor.shape[no_batch_dims:])
|
| 219 |
+
if low_mem:
|
| 220 |
+
return tensor.expand(batch_shape + feature_shape) # (*batch_shape, *feature_shape)
|
| 221 |
+
if any(size != 1 for size in tensor.shape[:no_batch_dims]):
|
| 222 |
+
tensor = tensor.expand(
|
| 223 |
+
batch_shape + feature_shape
|
| 224 |
+
) # (*batch_shape, *feature_shape)
|
| 225 |
+
return tensor.reshape(-1, *feature_shape) # (flat_batch, *feature_shape)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def _write_chunk(
|
| 229 |
+
destination: Any,
|
| 230 |
+
source: Any,
|
| 231 |
+
start: int,
|
| 232 |
+
stop: int,
|
| 233 |
+
*,
|
| 234 |
+
add_into_out: bool,
|
| 235 |
+
) -> None:
|
| 236 |
+
if isinstance(source, dict):
|
| 237 |
+
for key, value in source.items():
|
| 238 |
+
_write_chunk(
|
| 239 |
+
destination[key],
|
| 240 |
+
value,
|
| 241 |
+
start,
|
| 242 |
+
stop,
|
| 243 |
+
add_into_out=add_into_out,
|
| 244 |
+
)
|
| 245 |
+
return
|
| 246 |
+
if isinstance(source, (tuple, list)):
|
| 247 |
+
for destination_item, source_item in zip(destination, source, strict=True):
|
| 248 |
+
_write_chunk(
|
| 249 |
+
destination_item,
|
| 250 |
+
source_item,
|
| 251 |
+
start,
|
| 252 |
+
stop,
|
| 253 |
+
add_into_out=add_into_out,
|
| 254 |
+
)
|
| 255 |
+
return
|
| 256 |
+
if not isinstance(source, torch.Tensor):
|
| 257 |
+
raise ValueError(f"output type {type(source)!r} is not supported")
|
| 258 |
+
if add_into_out:
|
| 259 |
+
destination[start:stop] += source # (stop - start, *feature_shape)
|
| 260 |
+
else:
|
| 261 |
+
destination[start:stop] = source # (stop - start, *feature_shape)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def chunk_layer(
|
| 265 |
+
layer: Callable[..., Any],
|
| 266 |
+
inputs: dict[str, Any],
|
| 267 |
+
chunk_size: int,
|
| 268 |
+
no_batch_dims: int,
|
| 269 |
+
low_mem: bool = False,
|
| 270 |
+
_out: Any = None,
|
| 271 |
+
_add_into_out: bool = False,
|
| 272 |
+
) -> Any:
|
| 273 |
+
"""Apply ``layer`` to flattened batch chunks and reassemble its output."""
|
| 274 |
+
|
| 275 |
+
if not inputs:
|
| 276 |
+
raise ValueError("at least one input is required")
|
| 277 |
+
if chunk_size <= 0:
|
| 278 |
+
raise ValueError("chunk_size must be positive")
|
| 279 |
+
|
| 280 |
+
leading_shapes = [shape[:no_batch_dims] for shape in _fetch_dims(inputs)]
|
| 281 |
+
batch_shape = tuple(max(sizes) for sizes in zip(*leading_shapes, strict=True))
|
| 282 |
+
prepare = partial(
|
| 283 |
+
_prepare_input,
|
| 284 |
+
batch_shape=batch_shape,
|
| 285 |
+
no_batch_dims=no_batch_dims,
|
| 286 |
+
low_mem=low_mem,
|
| 287 |
+
)
|
| 288 |
+
prepared_inputs = tensor_tree_map(
|
| 289 |
+
prepare, inputs
|
| 290 |
+
) # each tensor: (flat_batch, *feature_shape), or broadcast batch in low-memory mode
|
| 291 |
+
|
| 292 |
+
output = None
|
| 293 |
+
if _out is not None:
|
| 294 |
+
output = tensor_tree_map(
|
| 295 |
+
lambda tensor: tensor.reshape(-1, *tensor.shape[no_batch_dims:]),
|
| 296 |
+
_out,
|
| 297 |
+
) # each tensor: (flat_batch, *feature_shape)
|
| 298 |
+
|
| 299 |
+
flat_batch_size = prod(batch_shape)
|
| 300 |
+
for start in range(0, flat_batch_size, chunk_size):
|
| 301 |
+
stop = min(flat_batch_size, start + chunk_size)
|
| 302 |
+
if low_mem:
|
| 303 |
+
select = partial(
|
| 304 |
+
_chunk_slice,
|
| 305 |
+
flat_start=start,
|
| 306 |
+
flat_end=stop,
|
| 307 |
+
no_batch_dims=len(batch_shape),
|
| 308 |
+
)
|
| 309 |
+
else:
|
| 310 |
+
|
| 311 |
+
def select(
|
| 312 |
+
tensor: torch.Tensor,
|
| 313 |
+
start: int = start,
|
| 314 |
+
stop: int = stop,
|
| 315 |
+
) -> torch.Tensor:
|
| 316 |
+
return tensor if tensor.shape[0] == 1 else tensor[start:stop]
|
| 317 |
+
|
| 318 |
+
input_chunk = tensor_tree_map(
|
| 319 |
+
select, prepared_inputs
|
| 320 |
+
) # each tensor: (chunk, *feature_shape)
|
| 321 |
+
output_chunk = layer(
|
| 322 |
+
**input_chunk
|
| 323 |
+
) # each output tensor: (chunk, *output_feature_shape)
|
| 324 |
+
|
| 325 |
+
if output is None:
|
| 326 |
+
output = tensor_tree_map(
|
| 327 |
+
lambda tensor: tensor.new_zeros((flat_batch_size, *tensor.shape[1:])),
|
| 328 |
+
output_chunk,
|
| 329 |
+
) # each tensor: (flat_batch, *output_feature_shape)
|
| 330 |
+
_write_chunk(
|
| 331 |
+
output,
|
| 332 |
+
output_chunk,
|
| 333 |
+
start,
|
| 334 |
+
stop,
|
| 335 |
+
add_into_out=_add_into_out,
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
return tensor_tree_map(
|
| 339 |
+
lambda tensor: tensor.reshape(batch_shape + tuple(tensor.shape[1:])),
|
| 340 |
+
output,
|
| 341 |
+
) # each tensor: (*batch_shape, *output_feature_shape)
|
fastplms/registry.py
CHANGED
|
@@ -87,6 +87,8 @@ _ROOT_FIELDS = frozenset(
|
|
| 87 |
"families",
|
| 88 |
"models",
|
| 89 |
"runtime_assets",
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
)
|
| 92 |
_UPSTREAM_FIELDS = frozenset(
|
|
@@ -94,6 +96,7 @@ _UPSTREAM_FIELDS = frozenset(
|
|
| 94 |
"id",
|
| 95 |
"path",
|
| 96 |
"url",
|
|
|
|
| 97 |
"revision",
|
| 98 |
"license",
|
| 99 |
"license_files",
|
|
@@ -177,6 +180,17 @@ _RUNTIME_ASSET_FIELDS = frozenset(
|
|
| 177 |
"offline_behavior",
|
| 178 |
}
|
| 179 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
|
| 182 |
class RegistryError(ValueError):
|
|
@@ -262,6 +276,20 @@ class CheckpointSource:
|
|
| 262 |
return MappingProxyType({item.path: item for item in self.files})
|
| 263 |
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
@dataclass(frozen=True, slots=True)
|
| 266 |
class OracleAsset:
|
| 267 |
"""Hash-pinned external file required by a native parity oracle."""
|
|
@@ -297,6 +325,19 @@ class OfficialGolden:
|
|
| 297 |
tensors: FileDigest
|
| 298 |
|
| 299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
@dataclass(frozen=True, slots=True)
|
| 301 |
class UpstreamSource:
|
| 302 |
"""Pinned official implementation used as a parity oracle."""
|
|
@@ -309,6 +350,13 @@ class UpstreamSource:
|
|
| 309 |
license_files: tuple[str, ...]
|
| 310 |
license_digests: tuple[FileDigest, ...] = ()
|
| 311 |
distribution_files: tuple[FileDigest, ...] = ()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
|
| 314 |
@dataclass(frozen=True, slots=True)
|
|
@@ -478,6 +526,8 @@ class ModelRegistry(Mapping[str, ModelSpec]):
|
|
| 478 |
runtime_assets: Mapping[str, RuntimeAsset] = MappingProxyType({}),
|
| 479 |
attention_kernels: Mapping[str, AttentionKernelSpec] = MappingProxyType({}),
|
| 480 |
legal_files: tuple[FileDigest, ...] = (),
|
|
|
|
|
|
|
| 481 |
) -> None:
|
| 482 |
self.schema_version = schema_version
|
| 483 |
self.upstreams = MappingProxyType(dict(upstreams))
|
|
@@ -486,6 +536,8 @@ class ModelRegistry(Mapping[str, ModelSpec]):
|
|
| 486 |
self._models = MappingProxyType(dict(models))
|
| 487 |
self.runtime_assets = MappingProxyType(dict(runtime_assets))
|
| 488 |
self.legal_files = legal_files
|
|
|
|
|
|
|
| 489 |
|
| 490 |
def __getitem__(self, key: str) -> ModelSpec:
|
| 491 |
return self._models[key]
|
|
@@ -501,6 +553,22 @@ class ModelRegistry(Mapping[str, ModelSpec]):
|
|
| 501 |
raise KeyError(family_id)
|
| 502 |
return tuple(model for model in self._models.values() if model.family.id == family_id)
|
| 503 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
def supported_attention_dtypes(
|
| 505 |
self,
|
| 506 |
family_id: str,
|
|
@@ -988,8 +1056,11 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
|
|
| 988 |
raise RegistryError(f"Duplicate upstream path: {path!r}")
|
| 989 |
paths.add(path)
|
| 990 |
url = _require_str(value, "url", context)
|
| 991 |
-
|
| 992 |
-
|
|
|
|
|
|
|
|
|
|
| 993 |
license_files = _require_str_list(value, "license_files", context)
|
| 994 |
license_digests = _require_digest_list(value, "license_digests", context)
|
| 995 |
if tuple(item.path for item in license_digests) != license_files:
|
|
@@ -1026,6 +1097,7 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
|
|
| 1026 |
license_files=license_files,
|
| 1027 |
license_digests=license_digests,
|
| 1028 |
distribution_files=distribution_files,
|
|
|
|
| 1029 |
)
|
| 1030 |
return upstreams
|
| 1031 |
|
|
@@ -1288,6 +1360,67 @@ def _parse_runtime_assets(
|
|
| 1288 |
return runtime_assets
|
| 1289 |
|
| 1290 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1291 |
def _parse_confidence_adaptation(
|
| 1292 |
table: Mapping[str, Any], context: str, model_id: str | None = None
|
| 1293 |
) -> ConfidenceAdaptation | None:
|
|
@@ -1667,6 +1800,57 @@ def _validate_registry(
|
|
| 1667 |
)
|
| 1668 |
|
| 1669 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1670 |
def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
|
| 1671 |
try:
|
| 1672 |
manifest = tomllib.loads(raw_bytes.decode("utf-8"))
|
|
@@ -1685,6 +1869,8 @@ def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
|
|
| 1685 |
runtime_assets = _parse_runtime_assets(manifest.get("runtime_assets"), families)
|
| 1686 |
models = _parse_models(manifest.get("models"), families)
|
| 1687 |
_validate_registry(upstreams, attention_kernels, families, models)
|
|
|
|
|
|
|
| 1688 |
return ModelRegistry(
|
| 1689 |
schema_version=1,
|
| 1690 |
upstreams=upstreams,
|
|
@@ -1693,6 +1879,8 @@ def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
|
|
| 1693 |
models=models,
|
| 1694 |
runtime_assets=runtime_assets,
|
| 1695 |
legal_files=legal_files,
|
|
|
|
|
|
|
| 1696 |
)
|
| 1697 |
|
| 1698 |
|
|
@@ -1729,6 +1917,7 @@ __all__ = [
|
|
| 1729 |
"CheckpointSource",
|
| 1730 |
"FileDigest",
|
| 1731 |
"GenerationContract",
|
|
|
|
| 1732 |
"ModelFamily",
|
| 1733 |
"ModelRegistry",
|
| 1734 |
"ModelSpec",
|
|
@@ -1737,6 +1926,7 @@ __all__ = [
|
|
| 1737 |
"RuntimeAsset",
|
| 1738 |
"RuntimeAssetTrustKind",
|
| 1739 |
"RuntimeExtra",
|
|
|
|
| 1740 |
"TestTier",
|
| 1741 |
"UpstreamSource",
|
| 1742 |
"VramTier",
|
|
|
|
| 87 |
"families",
|
| 88 |
"models",
|
| 89 |
"runtime_assets",
|
| 90 |
+
"golden_artifacts",
|
| 91 |
+
"sparse_autoencoders",
|
| 92 |
}
|
| 93 |
)
|
| 94 |
_UPSTREAM_FIELDS = frozenset(
|
|
|
|
| 96 |
"id",
|
| 97 |
"path",
|
| 98 |
"url",
|
| 99 |
+
"fetch_url",
|
| 100 |
"revision",
|
| 101 |
"license",
|
| 102 |
"license_files",
|
|
|
|
| 180 |
"offline_behavior",
|
| 181 |
}
|
| 182 |
)
|
| 183 |
+
_GOLDEN_ARTIFACT_FIELDS = frozenset(
|
| 184 |
+
{
|
| 185 |
+
"id",
|
| 186 |
+
"repository",
|
| 187 |
+
"revision",
|
| 188 |
+
"path",
|
| 189 |
+
"sha256",
|
| 190 |
+
"size",
|
| 191 |
+
"offline_behavior",
|
| 192 |
+
}
|
| 193 |
+
)
|
| 194 |
|
| 195 |
|
| 196 |
class RegistryError(ValueError):
|
|
|
|
| 276 |
return MappingProxyType({item.path: item for item in self.files})
|
| 277 |
|
| 278 |
|
| 279 |
+
@dataclass(frozen=True, slots=True)
|
| 280 |
+
class SparseAutoencoderSpec:
|
| 281 |
+
"""Pinned released SAE and its input contract, not its unknown training revision."""
|
| 282 |
+
|
| 283 |
+
id: str
|
| 284 |
+
base_model: str
|
| 285 |
+
checkpoint: CheckpointSource
|
| 286 |
+
layer: int
|
| 287 |
+
input_width: int
|
| 288 |
+
k: int
|
| 289 |
+
codebook_dim: int
|
| 290 |
+
input_kind: Literal["hidden_state"] = "hidden_state"
|
| 291 |
+
|
| 292 |
+
|
| 293 |
@dataclass(frozen=True, slots=True)
|
| 294 |
class OracleAsset:
|
| 295 |
"""Hash-pinned external file required by a native parity oracle."""
|
|
|
|
| 325 |
tensors: FileDigest
|
| 326 |
|
| 327 |
|
| 328 |
+
@dataclass(frozen=True, slots=True)
|
| 329 |
+
class GoldenArtifact:
|
| 330 |
+
"""Durable Hub copy of one model's official golden tensors at an immutable revision."""
|
| 331 |
+
|
| 332 |
+
model_id: str
|
| 333 |
+
repository: str
|
| 334 |
+
revision: str
|
| 335 |
+
path: str
|
| 336 |
+
sha256: str
|
| 337 |
+
size: int
|
| 338 |
+
offline_behavior: str
|
| 339 |
+
|
| 340 |
+
|
| 341 |
@dataclass(frozen=True, slots=True)
|
| 342 |
class UpstreamSource:
|
| 343 |
"""Pinned official implementation used as a parity oracle."""
|
|
|
|
| 350 |
license_files: tuple[str, ...]
|
| 351 |
license_digests: tuple[FileDigest, ...] = ()
|
| 352 |
distribution_files: tuple[FileDigest, ...] = ()
|
| 353 |
+
# Where the pinned revision is fetched when `url`, the source of record, no longer serves it.
|
| 354 |
+
fetch_url: str = ""
|
| 355 |
+
|
| 356 |
+
@property
|
| 357 |
+
def clone_url(self) -> str:
|
| 358 |
+
"""The URL that `.gitmodules` and source archives use."""
|
| 359 |
+
return self.fetch_url or self.url
|
| 360 |
|
| 361 |
|
| 362 |
@dataclass(frozen=True, slots=True)
|
|
|
|
| 526 |
runtime_assets: Mapping[str, RuntimeAsset] = MappingProxyType({}),
|
| 527 |
attention_kernels: Mapping[str, AttentionKernelSpec] = MappingProxyType({}),
|
| 528 |
legal_files: tuple[FileDigest, ...] = (),
|
| 529 |
+
golden_artifacts: Mapping[str, GoldenArtifact] = MappingProxyType({}),
|
| 530 |
+
sparse_autoencoders: Mapping[str, SparseAutoencoderSpec] = MappingProxyType({}),
|
| 531 |
) -> None:
|
| 532 |
self.schema_version = schema_version
|
| 533 |
self.upstreams = MappingProxyType(dict(upstreams))
|
|
|
|
| 536 |
self._models = MappingProxyType(dict(models))
|
| 537 |
self.runtime_assets = MappingProxyType(dict(runtime_assets))
|
| 538 |
self.legal_files = legal_files
|
| 539 |
+
self.golden_artifacts = MappingProxyType(dict(golden_artifacts))
|
| 540 |
+
self.sparse_autoencoders = MappingProxyType(dict(sparse_autoencoders))
|
| 541 |
|
| 542 |
def __getitem__(self, key: str) -> ModelSpec:
|
| 543 |
return self._models[key]
|
|
|
|
| 553 |
raise KeyError(family_id)
|
| 554 |
return tuple(model for model in self._models.values() if model.family.id == family_id)
|
| 555 |
|
| 556 |
+
def sae_for_base(
|
| 557 |
+
self, base_model: str, *, layer: int, k: int, codebook_dim: int
|
| 558 |
+
) -> SparseAutoencoderSpec:
|
| 559 |
+
"""Resolve an exact registered SAE selection without a Hub lookup or fallback."""
|
| 560 |
+
matches = tuple(
|
| 561 |
+
spec for spec in self.sparse_autoencoders.values()
|
| 562 |
+
if (spec.base_model, spec.layer, spec.k, spec.codebook_dim)
|
| 563 |
+
== (base_model, layer, k, codebook_dim)
|
| 564 |
+
)
|
| 565 |
+
if len(matches) != 1:
|
| 566 |
+
raise RegistryError(
|
| 567 |
+
f"Expected one pinned SAE for {base_model}, layer={layer}, k={k}, "
|
| 568 |
+
f"codebook_dim={codebook_dim}; found {len(matches)}."
|
| 569 |
+
)
|
| 570 |
+
return matches[0]
|
| 571 |
+
|
| 572 |
def supported_attention_dtypes(
|
| 573 |
self,
|
| 574 |
family_id: str,
|
|
|
|
| 1056 |
raise RegistryError(f"Duplicate upstream path: {path!r}")
|
| 1057 |
paths.add(path)
|
| 1058 |
url = _require_str(value, "url", context)
|
| 1059 |
+
fetch_url = _optional_str(value, "fetch_url", context) or ""
|
| 1060 |
+
for field, candidate in (("url", url), ("fetch_url", fetch_url)):
|
| 1061 |
+
is_github = candidate.startswith("https://github.com/")
|
| 1062 |
+
if candidate and not (is_github and candidate.endswith(".git")):
|
| 1063 |
+
raise RegistryError(f"{context}.{field} must be an HTTPS GitHub clone URL.")
|
| 1064 |
license_files = _require_str_list(value, "license_files", context)
|
| 1065 |
license_digests = _require_digest_list(value, "license_digests", context)
|
| 1066 |
if tuple(item.path for item in license_digests) != license_files:
|
|
|
|
| 1097 |
license_files=license_files,
|
| 1098 |
license_digests=license_digests,
|
| 1099 |
distribution_files=distribution_files,
|
| 1100 |
+
fetch_url=fetch_url,
|
| 1101 |
)
|
| 1102 |
return upstreams
|
| 1103 |
|
|
|
|
| 1360 |
return runtime_assets
|
| 1361 |
|
| 1362 |
|
| 1363 |
+
def _parse_golden_artifacts(
|
| 1364 |
+
raw: object,
|
| 1365 |
+
models: Mapping[str, ModelSpec],
|
| 1366 |
+
) -> dict[str, GoldenArtifact]:
|
| 1367 |
+
"""Parse the optional Hub copies of official golden tensors, keyed by model ID.
|
| 1368 |
+
|
| 1369 |
+
A copy must be byte-identical to the tensors its model's official_golden pins, so its
|
| 1370 |
+
SHA-256 must equal that digest and the existing integrity check covers both files.
|
| 1371 |
+
"""
|
| 1372 |
+
if raw is None:
|
| 1373 |
+
return {}
|
| 1374 |
+
if not isinstance(raw, list):
|
| 1375 |
+
raise RegistryError("golden_artifacts must be an array of tables.")
|
| 1376 |
+
golden_artifacts: dict[str, GoldenArtifact] = {}
|
| 1377 |
+
for index, value in enumerate(raw):
|
| 1378 |
+
context = f"golden_artifacts[{index}]"
|
| 1379 |
+
if not isinstance(value, dict):
|
| 1380 |
+
raise RegistryError(f"{context} must be a table.")
|
| 1381 |
+
_reject_unknown_fields(value, _GOLDEN_ARTIFACT_FIELDS, context)
|
| 1382 |
+
model_id = _require_str(value, "id", context)
|
| 1383 |
+
model = models.get(model_id)
|
| 1384 |
+
if model is None:
|
| 1385 |
+
raise RegistryError(f"{context}.id references unknown model {model_id!r}.")
|
| 1386 |
+
if model.official_golden is None:
|
| 1387 |
+
raise RegistryError(f"{context}.id names {model_id!r}, which has no official_golden.")
|
| 1388 |
+
if model_id in golden_artifacts:
|
| 1389 |
+
raise RegistryError(f"Duplicate golden artifact for model {model_id!r}.")
|
| 1390 |
+
repository = _require_str(value, "repository", context)
|
| 1391 |
+
if _REPOSITORY_ID_RE.fullmatch(repository) is None:
|
| 1392 |
+
raise RegistryError(f"{context}.repository must be a Hugging Face repository ID.")
|
| 1393 |
+
revision = _require_str(value, "revision", context)
|
| 1394 |
+
_validate_revision(revision, f"{context}.revision")
|
| 1395 |
+
path = _require_str(value, "path", context)
|
| 1396 |
+
expected_path = f"goldens/{model_id}.safetensors"
|
| 1397 |
+
if path != expected_path:
|
| 1398 |
+
raise RegistryError(f"{context}.path must be {expected_path!r}.")
|
| 1399 |
+
sha256 = _require_str(value, "sha256", context)
|
| 1400 |
+
if sha256 != model.official_golden.tensors.digest:
|
| 1401 |
+
raise RegistryError(
|
| 1402 |
+
f"{context}.sha256 must equal the official_golden tensors digest of {model_id!r}."
|
| 1403 |
+
)
|
| 1404 |
+
size = value.get("size")
|
| 1405 |
+
if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
|
| 1406 |
+
raise RegistryError(f"{context}.size must be a positive byte count.")
|
| 1407 |
+
offline_behavior = _require_str(value, "offline_behavior", context)
|
| 1408 |
+
if offline_behavior not in _ALLOWED_RUNTIME_ASSET_OFFLINE_BEHAVIORS:
|
| 1409 |
+
raise RegistryError(
|
| 1410 |
+
f"{context}.offline_behavior is unsupported: {offline_behavior!r}."
|
| 1411 |
+
)
|
| 1412 |
+
golden_artifacts[model_id] = GoldenArtifact(
|
| 1413 |
+
model_id=model_id,
|
| 1414 |
+
repository=repository,
|
| 1415 |
+
revision=revision,
|
| 1416 |
+
path=path,
|
| 1417 |
+
sha256=sha256,
|
| 1418 |
+
size=size,
|
| 1419 |
+
offline_behavior=offline_behavior,
|
| 1420 |
+
)
|
| 1421 |
+
return golden_artifacts
|
| 1422 |
+
|
| 1423 |
+
|
| 1424 |
def _parse_confidence_adaptation(
|
| 1425 |
table: Mapping[str, Any], context: str, model_id: str | None = None
|
| 1426 |
) -> ConfidenceAdaptation | None:
|
|
|
|
| 1800 |
)
|
| 1801 |
|
| 1802 |
|
| 1803 |
+
def _parse_sparse_autoencoders(
|
| 1804 |
+
raw: object, models: Mapping[str, ModelSpec]
|
| 1805 |
+
) -> dict[str, SparseAutoencoderSpec]:
|
| 1806 |
+
"""Validate optional SAE records independently of the base-model mapping."""
|
| 1807 |
+
if raw is None:
|
| 1808 |
+
return {}
|
| 1809 |
+
if not isinstance(raw, list):
|
| 1810 |
+
raise RegistryError("sparse_autoencoders must be an array of tables.")
|
| 1811 |
+
allowed = frozenset({
|
| 1812 |
+
"id", "base_model", "layer", "input_width", "k", "codebook_dim", "input_kind",
|
| 1813 |
+
"checkpoint_repo", "checkpoint_revision", "checkpoint_files",
|
| 1814 |
+
})
|
| 1815 |
+
records: dict[str, SparseAutoencoderSpec] = {}
|
| 1816 |
+
selections: set[tuple[str, int, int, int]] = set()
|
| 1817 |
+
for index, table in enumerate(raw):
|
| 1818 |
+
context = f"sparse_autoencoders[{index}]"
|
| 1819 |
+
if not isinstance(table, dict):
|
| 1820 |
+
raise RegistryError(f"{context} must be a table.")
|
| 1821 |
+
_reject_unknown_fields(table, allowed, context)
|
| 1822 |
+
identifier = _require_str(table, "id", context)
|
| 1823 |
+
if _IDENTIFIER_RE.fullmatch(identifier) is None or identifier in records:
|
| 1824 |
+
raise RegistryError(f"{context} has an invalid or duplicate SAE id: {identifier!r}.")
|
| 1825 |
+
base = _require_str(table, "base_model", context)
|
| 1826 |
+
if base not in models or models[base].family.id != "esm_plusplus":
|
| 1827 |
+
raise RegistryError(f"{context}.base_model must name a registered ESMC base.")
|
| 1828 |
+
numbers: dict[str, int] = {}
|
| 1829 |
+
for field in ("layer", "input_width", "k", "codebook_dim"):
|
| 1830 |
+
value = table.get(field)
|
| 1831 |
+
if type(value) is not int or value < (0 if field == "layer" else 1):
|
| 1832 |
+
raise RegistryError(f"{context}.{field} must be a valid integer dimension.")
|
| 1833 |
+
numbers[field] = value
|
| 1834 |
+
if numbers["k"] > numbers["codebook_dim"]:
|
| 1835 |
+
raise RegistryError(f"{context}.k exceeds codebook_dim.")
|
| 1836 |
+
if _require_str(table, "input_kind", context) != "hidden_state":
|
| 1837 |
+
raise RegistryError(f"{context} requires hidden_state input, not residual updates.")
|
| 1838 |
+
checkpoint = _parse_checkpoint(table, "checkpoint", context)
|
| 1839 |
+
expected_files = {"config.json", f"layer_{numbers['layer']}.safetensors"}
|
| 1840 |
+
if set(checkpoint.file_map) != expected_files or any(
|
| 1841 |
+
item.algorithm != "sha256" for item in checkpoint.files
|
| 1842 |
+
):
|
| 1843 |
+
raise RegistryError(f"{context} must SHA-256 pin exactly {sorted(expected_files)}.")
|
| 1844 |
+
selection = (base, numbers["layer"], numbers["k"], numbers["codebook_dim"])
|
| 1845 |
+
if selection in selections:
|
| 1846 |
+
raise RegistryError(f"{context} duplicates an SAE selection: {selection}.")
|
| 1847 |
+
selections.add(selection)
|
| 1848 |
+
records[identifier] = SparseAutoencoderSpec(
|
| 1849 |
+
id=identifier, base_model=base, checkpoint=checkpoint, **numbers
|
| 1850 |
+
)
|
| 1851 |
+
return records
|
| 1852 |
+
|
| 1853 |
+
|
| 1854 |
def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
|
| 1855 |
try:
|
| 1856 |
manifest = tomllib.loads(raw_bytes.decode("utf-8"))
|
|
|
|
| 1869 |
runtime_assets = _parse_runtime_assets(manifest.get("runtime_assets"), families)
|
| 1870 |
models = _parse_models(manifest.get("models"), families)
|
| 1871 |
_validate_registry(upstreams, attention_kernels, families, models)
|
| 1872 |
+
golden_artifacts = _parse_golden_artifacts(manifest.get("golden_artifacts"), models)
|
| 1873 |
+
sparse_autoencoders = _parse_sparse_autoencoders(manifest.get("sparse_autoencoders"), models)
|
| 1874 |
return ModelRegistry(
|
| 1875 |
schema_version=1,
|
| 1876 |
upstreams=upstreams,
|
|
|
|
| 1879 |
models=models,
|
| 1880 |
runtime_assets=runtime_assets,
|
| 1881 |
legal_files=legal_files,
|
| 1882 |
+
golden_artifacts=golden_artifacts,
|
| 1883 |
+
sparse_autoencoders=sparse_autoencoders,
|
| 1884 |
)
|
| 1885 |
|
| 1886 |
|
|
|
|
| 1917 |
"CheckpointSource",
|
| 1918 |
"FileDigest",
|
| 1919 |
"GenerationContract",
|
| 1920 |
+
"GoldenArtifact",
|
| 1921 |
"ModelFamily",
|
| 1922 |
"ModelRegistry",
|
| 1923 |
"ModelSpec",
|
|
|
|
| 1926 |
"RuntimeAsset",
|
| 1927 |
"RuntimeAssetTrustKind",
|
| 1928 |
"RuntimeExtra",
|
| 1929 |
+
"SparseAutoencoderSpec",
|
| 1930 |
"TestTier",
|
| 1931 |
"UpstreamSource",
|
| 1932 |
"VramTier",
|
fastplms_bundle.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
modeling_fastplms.py
CHANGED
|
@@ -13,7 +13,7 @@ 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 = []
|
|
|
|
| 13 |
|
| 14 |
from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
|
| 15 |
|
| 16 |
+
if RUNTIME_HASH != "bc88b7fe9bf0dd2e7b5b3337e94c4d316bf38106418cdccdd1e27882f91a1659":
|
| 17 |
raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
|
| 18 |
|
| 19 |
_RUNTIME_TEMPORARIES = []
|
requirements.txt
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
# Direct runtime dependencies for Synthyra/Boltz2.
|
| 2 |
# FastPLMs source is embedded in this model repository.
|
| 3 |
-
torch>=2.
|
| 4 |
-
transformers>=5.
|
| 5 |
-
huggingface-hub>=
|
| 6 |
-
tokenizers>=0.
|
| 7 |
-
safetensors>=0.
|
| 8 |
-
numpy>=
|
| 9 |
-
einops>=0.8
|
| 10 |
-
tqdm>=4.
|
| 11 |
-
accelerate>=1.
|
| 12 |
-
biopython>=1.
|
| 13 |
-
biotite>=1.
|
| 14 |
-
brotli>=1.
|
| 15 |
-
msgpack>=1.
|
| 16 |
-
msgpack-numpy>=0.4.8
|
| 17 |
-
omegaconf>=2.3
|
| 18 |
-
rdkit>=
|
| 19 |
-
scipy>=1.
|
| 20 |
-
zstandard>=0.
|
|
|
|
| 1 |
# Direct runtime dependencies for Synthyra/Boltz2.
|
| 2 |
# FastPLMs source is embedded in this model repository.
|
| 3 |
+
torch>=2.14
|
| 4 |
+
transformers>=5.17
|
| 5 |
+
huggingface-hub>=1.32
|
| 6 |
+
tokenizers>=0.23
|
| 7 |
+
safetensors>=0.8
|
| 8 |
+
numpy>=2.5
|
| 9 |
+
einops>=0.8
|
| 10 |
+
tqdm>=4.70
|
| 11 |
+
accelerate>=1.15
|
| 12 |
+
biopython>=1.88
|
| 13 |
+
biotite>=1.7
|
| 14 |
+
brotli>=1.2
|
| 15 |
+
msgpack>=1.2
|
| 16 |
+
msgpack-numpy>=0.4.8
|
| 17 |
+
omegaconf>=2.3
|
| 18 |
+
rdkit>=2026.3
|
| 19 |
+
scipy>=1.18
|
| 20 |
+
zstandard>=0.25
|