lhallee commited on
Commit
4c8d0e0
·
verified ·
1 Parent(s): 8ba6e2b

Update FastPLMs runtime and model cards

Browse files

Add-only FastPLMs files-only publication. Checkpoint weights and complete-artifact attestations are unchanged.

Files changed (35) hide show
  1. README.md +70 -29
  2. config.json +5 -5
  3. fastplms/__init__.py +1 -0
  4. fastplms/models/__init__.py +1 -0
  5. fastplms/models/boltz/__init__.py +1 -0
  6. fastplms/models/boltz/_pair_attention.py +14 -10
  7. fastplms/models/boltz/cif_writer.py +28 -22
  8. fastplms/models/boltz/minimal_featurizer.py +175 -111
  9. fastplms/models/boltz/modeling_boltz2.py +131 -104
  10. fastplms/models/boltz/vb_layers_attention.py +22 -17
  11. fastplms/models/boltz/vb_layers_attentionv2.py +11 -8
  12. fastplms/models/boltz/vb_layers_confidence_utils.py +104 -58
  13. fastplms/models/boltz/vb_layers_dropout.py +4 -2
  14. fastplms/models/boltz/vb_layers_initialize.py +13 -2
  15. fastplms/models/boltz/vb_layers_outer_product_mean.py +36 -20
  16. fastplms/models/boltz/vb_layers_pair_averaging.py +45 -25
  17. fastplms/models/boltz/vb_layers_pairformer.py +36 -24
  18. fastplms/models/boltz/vb_layers_transition.py +17 -8
  19. fastplms/models/boltz/vb_layers_triangular_mult.py +15 -9
  20. fastplms/models/boltz/vb_loss_diffusionv2.py +40 -33
  21. fastplms/models/boltz/vb_modules_diffusion_conditioning.py +15 -7
  22. fastplms/models/boltz/vb_modules_encodersv2.py +174 -104
  23. fastplms/models/boltz/vb_modules_transformersv2.py +54 -32
  24. fastplms/models/boltz/vb_modules_trunkv2.py +110 -82
  25. fastplms/models/boltz/vb_modules_utils.py +64 -39
  26. fastplms/models/boltz/vb_potentials_potentials.py +215 -165
  27. fastplms/models/boltz/vb_tri_attn_attention.py +20 -15
  28. fastplms/models/boltz/vb_tri_attn_primitives.py +57 -29
  29. fastplms/models/boltz/vb_tri_attn_utils.py +36 -20
  30. fastplms/registry.py +45 -44
  31. fastplms/runtime.py +1 -0
  32. fastplms_bundle.py +0 -0
  33. modeling_fastplms.py +26 -70
  34. requirements.txt +20 -0
  35. runtime-attestation.json +39 -38
README.md CHANGED
@@ -16,16 +16,35 @@ Accepted inputs are raw amino-acid sequences through the convenience API, or
16
  prepared model features.
17
  Supported Transformers entry points are `AutoConfig`, `AutoModel`.
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  ## Install and platform requirements
20
 
21
- Install the current FastPLMs package:
22
 
23
  ```bash
24
- python -m pip install \
25
- "fastplms[structure] @ git+https://github.com/Synthyra/FastPLMs.git"
26
  ```
27
 
28
- Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. Structure inference requires the `structure` extra and a CUDA device for the published execution contract. The current validated release target is the exact NVIDIA GH200 on Linux aarch64; Linux x86-64, CPU-only, Windows, and macOS structure runs are not current release evidence. The Hub quick start below requires network
 
 
 
29
  access on first download. For an air-gapped run, first build the manifest-pinned
30
  local artifact and use the offline form shown in the example.
31
 
@@ -38,22 +57,50 @@ model_id = "Synthyra/Boltz2"
38
  model = AutoModel.from_pretrained(
39
  model_id,
40
  trust_remote_code=True,
 
41
  ).eval()
42
  ```
43
 
44
- This example uses the published Hub repository. For offline validation, build
45
- the manifest-pinned artifact and replace `model_id` with its local
46
- `dist/hub/Boltz2` path, then pass `local_files_only=True`.
 
 
 
 
 
 
47
 
48
- Leave attention unspecified for the Transformers default. Supported explicit
49
- choices are `eager`.
50
- Pass the selected name through `attn_implementation`.
51
- When an optimized backend cannot return full attention tensors,
52
- `output_attentions=True` emits one explicit runtime warning and uses a correctly
53
- masked eager implementation for that call only. The warning identifies the
54
- configured backend, effective backend, and reason. Configuration and later
55
- calls are unchanged.
56
- For BF16 execution, this family uses FP32 parameters with CUDA BF16 autocast.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  ## Protein structure prediction
59
 
@@ -100,7 +147,7 @@ continues independently of the ESM++ and ESMFold2 release gates.
100
  - Precision policies: `default`
101
  - BF16 execution: `fp32_parameters_autocast`
102
  - Generation contract: `not_applicable`
103
- - Optional dependency group: `structure`
104
  - Weight publication allowed: `true`
105
  - Weight license status: `resolved`
106
  - Redistributable: `true`
@@ -111,28 +158,22 @@ continues independently of the ESM++ and ESMFold2 release gates.
111
  - FastPLMs weights: `Synthyra/Boltz2`
112
  - Runtime revision: recorded separately in the built artifact and published commit
113
  - Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
114
- - Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
115
  - Official checkpoint: `boltz-community/boltz-2`
116
  - Artifact source: `fast`
117
  - State transform: `boltz2_inference_core_v1`
118
- - BF16 execution: `fp32_parameters_autocast`
119
  - Pinned upstreams: `boltz`
120
- - Reference container: `reference-boltz2`
121
  - Release tiers: `structure`, `artifact`, `benchmark`
122
  - Unresolved required file identities: `0`
123
 
124
- The local artifact records exact file identities, conversion details, source
125
- revisions, and legal texts in `provenance.json`. A nonzero unresolved count is a
126
- release blocker.
127
 
128
  ## Validation boundary
129
 
130
- For tiers declared by the manifest, the release contract compares applicable
131
- semantic configuration, tokenizer behavior, state keys, shapes, dtypes,
132
- values, aliases, and representative inference with the pinned official
133
- implementation. This metadata does not by itself claim that a particular build
134
- passed, that one backend is faster, or that an output has biological or
135
- therapeutic validity.
136
 
137
  ## License
138
 
 
16
  prepared model features.
17
  Supported Transformers entry points are `AutoConfig`, `AutoModel`.
18
 
19
+ ## Capabilities
20
+
21
+ | Feature | Status |
22
+ | --- | --- |
23
+ | Sequence classification | Unavailable: no advertised AutoClass |
24
+ | Token classification | Unavailable: no advertised AutoClass |
25
+ | PEFT fine-tuning | Supported pattern: attach LoRA to the pretrained model |
26
+ | Embeddings | Unavailable for this structure-only checkpoint |
27
+ | Test-time training | Unavailable for this inference-only checkpoint |
28
+ | Attention variants | Supported: `eager` |
29
+ | Compliance | Unavailable: this provisional family has no compliance tier |
30
+
31
+ A supported interface is not a pretrained downstream predictor. Classification
32
+ heads start untrained, and declared compliance metadata is not a claim that an
33
+ arbitrary local build passed its release gate.
34
+
35
  ## Install and platform requirements
36
 
37
+ Install the direct dependencies published with this model:
38
 
39
  ```bash
40
+ python -m pip install -r \
41
+ "https://huggingface.co/Synthyra/Boltz2/resolve/main/requirements.txt"
42
  ```
43
 
44
+ The FastPLMs implementation itself is embedded in the model repository and loaded
45
+ by Transformers through `trust_remote_code=True`.
46
+
47
+ Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. The artifact requirements include the direct structure dependencies. The published execution contract requires a CUDA device. The current validated release target is the exact NVIDIA GH200 on Linux aarch64; Linux x86-64, CPU-only, Windows, and macOS structure runs are not current release evidence. The Hub quick start below requires network
48
  access on first download. For an air-gapped run, first build the manifest-pinned
49
  local artifact and use the offline form shown in the example.
50
 
 
57
  model = AutoModel.from_pretrained(
58
  model_id,
59
  trust_remote_code=True,
60
+ attn_implementation="eager",
61
  ).eval()
62
  ```
63
 
64
+ For offline validation, replace `model_id` with the manifest-built
65
+ `dist/hub/Boltz2` path and pass `local_files_only=True`.
66
+
67
+ ## Attention and compliance
68
+
69
+ The quick start selects `eager` explicitly. Declared variants are `eager`. An unavailable requested backend raises instead
70
+ of silently switching implementations.
71
+ `output_attentions=True` may use the documented, one-call eager fallback solely
72
+ to materialize attention tensors; the configured backend remains unchanged.
73
 
74
+ This family does not declare the `compliance` tier. Boltz2 remains provisional
75
+ and its structure checks must not be broadened into parity claims.
76
+
77
+ ## PEFT fine-tuning
78
+
79
+ Install the direct training dependencies, then attach LoRA to the loaded checkpoint:
80
+
81
+ ```bash
82
+ python -m pip install "datasets>=4.8,<5" "peft>=0.19,<0.20"
83
+ ```
84
+
85
+ ```python
86
+ from peft import LoraConfig, get_peft_model
87
+
88
+ peft_model = get_peft_model(
89
+ model,
90
+ LoraConfig(
91
+ r=8,
92
+ lora_alpha=16,
93
+ target_modules="all-linear",
94
+ ),
95
+ )
96
+ ```
97
+
98
+ This checkpoint has no advertised classifier. Supply the task-specific
99
+ objective and preserve any new head through `modules_to_save`.
100
+ All FastPLMs checkpoints follow the Transformers `PreTrainedModel` contract and
101
+ can be adapted with PEFT. The ESM2-specific shipped CLI is an example, not a
102
+ support boundary. Record the target modules, base revision, data identity, and
103
+ trainable parameter scope.
104
 
105
  ## Protein structure prediction
106
 
 
147
  - Precision policies: `default`
148
  - BF16 execution: `fp32_parameters_autocast`
149
  - Generation contract: `not_applicable`
150
+ - Artifact dependency set: `core + structure`
151
  - Weight publication allowed: `true`
152
  - Weight license status: `resolved`
153
  - Redistributable: `true`
 
158
  - FastPLMs weights: `Synthyra/Boltz2`
159
  - Runtime revision: recorded separately in the built artifact and published commit
160
  - Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
 
161
  - Official checkpoint: `boltz-community/boltz-2`
162
  - Artifact source: `fast`
163
  - State transform: `boltz2_inference_core_v1`
 
164
  - Pinned upstreams: `boltz`
 
165
  - Release tiers: `structure`, `artifact`, `benchmark`
166
  - Unresolved required file identities: `0`
167
 
168
+ `provenance.json` records exact file identities, conversion, source revisions,
169
+ legal texts, schema, and attestations. A nonzero unresolved count blocks release.
 
170
 
171
  ## Validation boundary
172
 
173
+ Declared tiers compare applicable configuration, tokenizer behavior, state,
174
+ and representative inference with the pinned reference. Metadata alone does
175
+ not claim a build passed, a backend is faster, or an output is biologically
176
+ valid.
 
 
177
 
178
  ## License
179
 
config.json CHANGED
@@ -137,11 +137,11 @@
137
  "fastplms_checkpoint_repo_id": "Synthyra/Boltz2",
138
  "fastplms_checkpoint_revision": "3b148fc5efea109c065ec82ba8683d024de7134e",
139
  "fastplms_model_id": "boltz2",
140
- "fastplms_release_tool_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
141
- "fastplms_release_tool_sha256": "19b4247aba04a74de5069397518374ab2c3b9e0ce18ba9f36de34f0f4c5d1ceb",
142
- "fastplms_runtime_bundle_sha256": "e6e0f8c8632238f40a5112f22397a9a211eb02d3415c1c93d43305aef51f7728",
143
- "fastplms_runtime_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
144
- "fastplms_source_tree_sha256": "78061b1bd37ba2dda853641fba681fdf943b859503e4fb21b1a7508909c369bf",
145
  "fastplms_weights_revision": "3b148fc5efea109c065ec82ba8683d024de7134e",
146
  "model_type": "boltz2_automodel",
147
  "num_bins": 64,
 
137
  "fastplms_checkpoint_repo_id": "Synthyra/Boltz2",
138
  "fastplms_checkpoint_revision": "3b148fc5efea109c065ec82ba8683d024de7134e",
139
  "fastplms_model_id": "boltz2",
140
+ "fastplms_release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
141
+ "fastplms_release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
142
+ "fastplms_runtime_bundle_sha256": "3aebb85d11edb01329d2aafb4c8260bd67339785f0f252582246ae7c33206143",
143
+ "fastplms_runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
144
+ "fastplms_source_tree_sha256": "9d2fc9586cfbd1d2d2cc49d745311acb81d4e3740e59ead09e633917589806a8",
145
  "fastplms_weights_revision": "3b148fc5efea109c065ec82ba8683d024de7134e",
146
  "model_type": "boltz2_automodel",
147
  "num_bins": 64,
fastplms/__init__.py CHANGED
@@ -9,6 +9,7 @@ from __future__ import annotations
9
  from importlib import import_module
10
  from typing import Any
11
 
 
12
  __version__ = "1.0.0"
13
 
14
  _LAZY_EXPORTS = {
 
9
  from importlib import import_module
10
  from typing import Any
11
 
12
+
13
  __version__ = "1.0.0"
14
 
15
  _LAZY_EXPORTS = {
fastplms/models/__init__.py CHANGED
@@ -7,4 +7,5 @@ tokenizers, compile kernels, or initialize an accelerator runtime.
7
 
8
  from __future__ import annotations
9
 
 
10
  __all__: tuple[str, ...] = ()
 
7
 
8
  from __future__ import annotations
9
 
10
+
11
  __all__: tuple[str, ...] = ()
fastplms/models/boltz/__init__.py CHANGED
@@ -5,6 +5,7 @@ from fastplms.models.boltz.modeling_boltz2 import (
5
  Boltz2StructureOutput,
6
  )
7
 
 
8
  __all__ = [
9
  "Boltz2Config",
10
  "Boltz2Model",
 
5
  Boltz2StructureOutput,
6
  )
7
 
8
+
9
  __all__ = [
10
  "Boltz2Config",
11
  "Boltz2Model",
fastplms/models/boltz/_pair_attention.py CHANGED
@@ -3,7 +3,6 @@
3
  from __future__ import annotations
4
 
5
  import math
6
-
7
  import torch
8
  from torch import Tensor
9
 
@@ -11,9 +10,11 @@ from torch import Tensor
11
  def reshape_heads(states: Tensor, num_heads: int) -> Tensor:
12
  """Reshape X from ``(b, l, d)`` to ``(b, l, h, d_head)``."""
13
 
14
- batch_size, sequence_length, width = states.shape
 
15
  if width % num_heads:
16
  raise ValueError(f"width {width} is not divisible by {num_heads} heads")
 
17
  return states.view(batch_size, sequence_length, num_heads, width // num_heads)
18
 
19
 
@@ -27,20 +28,23 @@ def pair_biased_attention(
27
  ) -> Tensor:
28
  """Return pair-biased attention values with shape ``(b, l_q, h, d_head)``."""
29
 
30
- head_dim = query_states.shape[-1]
 
 
31
  with torch.autocast("cuda", enabled=False):
32
- # S is the FP32 attention-score tensor with shape (b, h, l_q, l_k).
33
  scores = torch.einsum(
34
  "bihd,bjhd->bhij",
35
  query_states.float(),
36
  key_states.float(),
37
- )
38
- scores = scores / math.sqrt(head_dim) + pair_bias.float()
39
- scores = scores + (1 - key_mask[:, None, None].float()) * -mask_value
40
- probabilities = scores.softmax(dim=-1)
 
 
41
  output = torch.einsum(
42
  "bhij,bjhd->bihd",
43
  probabilities,
44
  value_states.float(),
45
- )
46
- return output.to(value_states.dtype)
 
3
  from __future__ import annotations
4
 
5
  import math
 
6
  import torch
7
  from torch import Tensor
8
 
 
10
  def reshape_heads(states: Tensor, num_heads: int) -> Tensor:
11
  """Reshape X from ``(b, l, d)`` to ``(b, l, h, d_head)``."""
12
 
13
+ # states: (b, l, d); num_heads: h
14
+ batch_size, sequence_length, width = states.shape # b, l, d
15
  if width % num_heads:
16
  raise ValueError(f"width {width} is not divisible by {num_heads} heads")
17
+ # states.view(...): (b, l, h, d_h)
18
  return states.view(batch_size, sequence_length, num_heads, width // num_heads)
19
 
20
 
 
28
  ) -> Tensor:
29
  """Return pair-biased attention values with shape ``(b, l_q, h, d_head)``."""
30
 
31
+ # query_states: (b, l_q, h, d_h); key_states, value_states: (b, l_k, h, d_h)
32
+ # pair_bias: (b, h, l_q, l_k); key_mask: (b, l_k)
33
+ head_dim = query_states.shape[-1] # d_h
34
  with torch.autocast("cuda", enabled=False):
 
35
  scores = torch.einsum(
36
  "bihd,bjhd->bhij",
37
  query_states.float(),
38
  key_states.float(),
39
+ ) # (b, h, l_q, l_k)
40
+ scores = scores / math.sqrt(head_dim) + pair_bias.float() # (b, h, l_q, l_k)
41
+ scores = (
42
+ scores + (1 - key_mask[:, None, None].float()) * -mask_value
43
+ ) # (b, h, l_q, l_k)
44
+ probabilities = scores.softmax(dim=-1) # (b, h, l_q, l_k)
45
  output = torch.einsum(
46
  "bhij,bjhd->bihd",
47
  probabilities,
48
  value_states.float(),
49
+ ) # (b, l_q, h, d_h)
50
+ return output.to(value_states.dtype) # (b, l_q, h, d_h)
fastplms/models/boltz/cif_writer.py CHANGED
@@ -1,7 +1,6 @@
1
- from pathlib import Path
2
-
3
  import numpy as np
4
  import torch
 
5
 
6
  from .minimal_structures import ProteinStructureTemplate
7
 
@@ -12,28 +11,30 @@ def _confidence_per_atom(
12
  num_atoms: int,
13
  sample_index: int,
14
  ) -> np.ndarray:
 
15
  if plddt is None:
16
- return np.ones((num_atoms,), dtype=np.float32) * 100.0
17
 
18
- values = plddt.detach().cpu()
19
  if values.ndim == 1:
20
- values = values.unsqueeze(0)
21
  if values.ndim != 2:
22
  raise ValueError("Expected pLDDT tensor S with shape (n_samples, n_items).")
 
23
  if not 0 <= sample_index < values.shape[0]:
24
  raise IndexError("sample_index out of range for pLDDT.")
25
 
26
- selected = values[sample_index]
27
  if selected.shape[0] == num_atoms:
28
- return (selected.numpy() * 100.0).astype(np.float32)
29
 
30
- num_residues = max(atom_to_residue) + 1
31
  if selected.shape[0] == num_residues:
32
- expanded = np.zeros((num_atoms,), dtype=np.float32)
33
- selected_np = selected.numpy()
34
  for atom_idx, residue_idx in enumerate(atom_to_residue):
35
- expanded[atom_idx] = selected_np[residue_idx] * 100.0
36
- return expanded
37
 
38
  raise ValueError(
39
  "pLDDT item count must match either atoms or residues: "
@@ -50,34 +51,39 @@ def write_cif(
50
  plddt: torch.Tensor | None = None,
51
  sample_index: int = 0,
52
  ) -> str:
53
- coords = atom_coords.detach().cpu()
 
 
54
  if coords.ndim == 2:
55
- coords = coords.unsqueeze(0)
56
  if coords.ndim != 3 or coords.shape[-1] != 3:
57
  raise ValueError(
58
  "Expected coordinate tensor X with shape (n_samples, n_atoms, 3)."
59
  )
 
60
  if not 0 <= sample_index < coords.shape[0]:
61
  raise IndexError("sample_index out of range.")
62
- selected_coords_tensor = coords[sample_index]
63
- all_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor))
64
  if torch.any(all_non_finite):
65
  raise ValueError(
66
  "CIF export received non-finite coordinates. "
67
  f"Non-finite count: {int(all_non_finite.sum().item())}"
68
  )
69
- selected_coords = selected_coords_tensor.numpy()
70
 
71
- mask = atom_mask.detach().cpu()
72
  if mask.ndim == 2:
73
- mask = mask[0]
74
  if mask.ndim != 1:
75
  raise ValueError("Expected atom mask M with shape (n_atoms,).")
 
76
  if mask.shape[0] != selected_coords.shape[0]:
77
  raise ValueError("Atom mask/coord size mismatch.")
78
  if not torch.any(mask > 0):
79
  raise ValueError("Atom mask has no valid atoms for CIF export.")
80
  valid_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor[mask > 0]))
 
81
  if torch.any(valid_non_finite):
82
  raise ValueError(
83
  "CIF export has non-finite coordinates in unmasked atoms. "
@@ -89,7 +95,7 @@ def write_cif(
89
  atom_to_residue=structure_template.atom_residue_index,
90
  num_atoms=structure_template.num_atoms,
91
  sample_index=sample_index,
92
- )
93
  if b_iso.shape[0] != structure_template.num_atoms:
94
  raise RuntimeError("CIF confidence values do not match the structure atom count.")
95
 
@@ -122,8 +128,8 @@ def write_cif(
122
  atom_name = structure_template.atom_names[idx]
123
  element = structure_template.atom_elements[idx]
124
  chain_id = structure_template.atom_chain_id[idx]
125
- x_val, y_val, z_val = selected_coords[idx].tolist()
126
- b_factor = float(b_iso[idx])
127
 
128
  line = (
129
  f"ATOM {atom_id} {element} {atom_name} {residue_name} {chain_id} "
 
 
 
1
  import numpy as np
2
  import torch
3
+ from pathlib import Path
4
 
5
  from .minimal_structures import ProteinStructureTemplate
6
 
 
11
  num_atoms: int,
12
  sample_index: int,
13
  ) -> np.ndarray:
14
+ # plddt: (n_i,) or (n_s, n_i); atom_to_residue: length n_a; num_atoms: n_a
15
  if plddt is None:
16
+ return np.ones((num_atoms,), dtype=np.float32) * 100.0 # (n_a,)
17
 
18
+ values = plddt.detach().cpu() # (n_i,) or (n_s, n_i)
19
  if values.ndim == 1:
20
+ values = values.unsqueeze(0) # (1, n_i)
21
  if values.ndim != 2:
22
  raise ValueError("Expected pLDDT tensor S with shape (n_samples, n_items).")
23
+ # values: (n_s, n_i) beyond this point
24
  if not 0 <= sample_index < values.shape[0]:
25
  raise IndexError("sample_index out of range for pLDDT.")
26
 
27
+ selected = values[sample_index] # (n_i,)
28
  if selected.shape[0] == num_atoms:
29
+ return (selected.numpy() * 100.0).astype(np.float32) # (n_a,)
30
 
31
+ num_residues = max(atom_to_residue) + 1 # n_r
32
  if selected.shape[0] == num_residues:
33
+ expanded = np.zeros((num_atoms,), dtype=np.float32) # (n_a,)
34
+ selected_np = selected.numpy() # (n_r,)
35
  for atom_idx, residue_idx in enumerate(atom_to_residue):
36
+ expanded[atom_idx] = selected_np[residue_idx] * 100.0 # () -> ()
37
+ return expanded # (n_a,)
38
 
39
  raise ValueError(
40
  "pLDDT item count must match either atoms or residues: "
 
51
  plddt: torch.Tensor | None = None,
52
  sample_index: int = 0,
53
  ) -> str:
54
+ # atom_coords: (n_a, 3) or (n_s, n_a, 3)
55
+ # atom_mask: (n_a,) or (n_m, n_a); plddt: (n_i,) or (n_s, n_i)
56
+ coords = atom_coords.detach().cpu() # (n_a, 3) or (n_s, n_a, 3)
57
  if coords.ndim == 2:
58
+ coords = coords.unsqueeze(0) # (1, n_a, 3)
59
  if coords.ndim != 3 or coords.shape[-1] != 3:
60
  raise ValueError(
61
  "Expected coordinate tensor X with shape (n_samples, n_atoms, 3)."
62
  )
63
+ # coords: (n_s, n_a, 3) beyond this point
64
  if not 0 <= sample_index < coords.shape[0]:
65
  raise IndexError("sample_index out of range.")
66
+ selected_coords_tensor = coords[sample_index] # (n_a, 3)
67
+ all_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor)) # (n_a, 3)
68
  if torch.any(all_non_finite):
69
  raise ValueError(
70
  "CIF export received non-finite coordinates. "
71
  f"Non-finite count: {int(all_non_finite.sum().item())}"
72
  )
73
+ selected_coords = selected_coords_tensor.numpy() # (n_a, 3)
74
 
75
+ mask = atom_mask.detach().cpu() # (n_a,) or (n_m, n_a)
76
  if mask.ndim == 2:
77
+ mask = mask[0] # (n_a,)
78
  if mask.ndim != 1:
79
  raise ValueError("Expected atom mask M with shape (n_atoms,).")
80
+ # mask: (n_a,) beyond this point
81
  if mask.shape[0] != selected_coords.shape[0]:
82
  raise ValueError("Atom mask/coord size mismatch.")
83
  if not torch.any(mask > 0):
84
  raise ValueError("Atom mask has no valid atoms for CIF export.")
85
  valid_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor[mask > 0]))
86
+ # valid_non_finite: (n_v, 3), where n_v is the valid-atom count
87
  if torch.any(valid_non_finite):
88
  raise ValueError(
89
  "CIF export has non-finite coordinates in unmasked atoms. "
 
95
  atom_to_residue=structure_template.atom_residue_index,
96
  num_atoms=structure_template.num_atoms,
97
  sample_index=sample_index,
98
+ ) # (n_a,)
99
  if b_iso.shape[0] != structure_template.num_atoms:
100
  raise RuntimeError("CIF confidence values do not match the structure atom count.")
101
 
 
128
  atom_name = structure_template.atom_names[idx]
129
  element = structure_template.atom_elements[idx]
130
  chain_id = structure_template.atom_chain_id[idx]
131
+ x_val, y_val, z_val = selected_coords[idx].tolist() # (3,) -> three scalars
132
+ b_factor = float(b_iso[idx]) # () -> Python float
133
 
134
  line = (
135
  f"ATOM {atom_id} {element} {atom_name} {residue_name} {chain_id} "
fastplms/models/boltz/minimal_featurizer.py CHANGED
@@ -1,5 +1,4 @@
1
  import math
2
-
3
  import numpy as np
4
  import torch
5
  from torch.nn.functional import one_hot
@@ -7,6 +6,7 @@ from torch.nn.functional import one_hot
7
  from . import vb_const as const
8
  from .minimal_structures import ProteinStructureTemplate
9
 
 
10
  _ELEMENT_TO_Z = {
11
  "H": 1,
12
  "C": 6,
@@ -75,7 +75,7 @@ def _atom_name_to_codes(atom_name: str) -> torch.Tensor:
75
  vals = [ord(ch) - 32 for ch in clipped]
76
  while len(vals) < 4:
77
  vals.append(0)
78
- out = torch.tensor(vals, dtype=torch.long)
79
  if not (torch.all(out >= 0) and torch.all(out < 64)):
80
  raise ValueError(f"Invalid atom-name encoding for '{atom_name}'.")
81
  return out
@@ -309,7 +309,7 @@ def _get_atom_position(res_name: str, atom_name: str, atom_idx: int) -> np.ndarr
309
  to a simple geometric placement for unknown residue/atom combinations.
310
  """
311
  if res_name in _RDKIT_CONFORMERS and atom_name in _RDKIT_CONFORMERS[res_name]:
312
- return np.array(_RDKIT_CONFORMERS[res_name][atom_name], dtype=np.float32)
313
  # Fallback for unknown atoms (should not happen for canonical AAs)
314
  angle = (atom_idx + 1) * 0.7
315
  radius = 1.4 + 0.03 * atom_idx
@@ -320,7 +320,7 @@ def _get_atom_position(res_name: str, atom_name: str, atom_idx: int) -> np.ndarr
320
  0.1 * ((atom_idx % 5) - 2),
321
  ],
322
  dtype=np.float32,
323
- )
324
 
325
 
326
  def _build_template(
@@ -340,7 +340,7 @@ def _build_template(
340
  atom_elements: list[str] = []
341
  atom_residue_index: list[int] = []
342
  atom_chain_id: list[str] = []
343
- atom_positions: list[np.ndarray] = []
344
  residue_center_atom_idx: list[int] = []
345
  residue_disto_atom_idx: list[int] = []
346
  residue_frame_atom_idx: list[int] = []
@@ -370,7 +370,7 @@ def _build_template(
370
  atom_residue_index.append(res_idx)
371
  atom_chain_id.append("A")
372
 
373
- atom_pos = _get_atom_position(token_name, atom_name, local_idx)
374
  atom_positions.append(atom_pos)
375
 
376
  if atom_name == center_atom_name:
@@ -422,14 +422,14 @@ def _build_template(
422
 
423
  def _random_rotation_matrix() -> torch.Tensor:
424
  """Sample a uniform random 3x3 rotation matrix (Algorithm 19 from AF2/Boltz)."""
425
- quaternion = torch.randn((1, 4), dtype=torch.float32)
426
- squared_norm = (quaternion * quaternion).sum(dim=1)
427
- norm = torch.sqrt(squared_norm)
428
- signed_norm = torch.where(quaternion[:, 0] < 0, -norm, norm)
429
- quaternion = quaternion / signed_norm[:, None]
430
-
431
- real, i, j, k = torch.unbind(quaternion, dim=-1)
432
- two_s = 2.0 / (quaternion * quaternion).sum(dim=-1)
433
  rotation = torch.stack(
434
  (
435
  1 - two_s * (j * j + k * k),
@@ -443,8 +443,8 @@ def _random_rotation_matrix() -> torch.Tensor:
443
  1 - two_s * (i * i + j * j),
444
  ),
445
  dim=-1,
446
- )
447
- return rotation.reshape(1, 3, 3)[0]
448
 
449
 
450
  def _center_and_augment_atoms_per_residue(
@@ -457,25 +457,30 @@ def _center_and_augment_atoms_per_residue(
457
  The pinned implementation intentionally excludes the final residue because
458
  it iterates to the maximum reference-space identifier rather than through it.
459
  """
460
- result = atom_positions.clone()
461
- residue_index_tensor = torch.tensor(atom_residue_index, dtype=torch.long)
 
462
  for residue_idx in range(max(num_residues - 1, 0)):
463
- residue_mask = residue_index_tensor == residue_idx
464
  if not torch.any(residue_mask):
465
  raise RuntimeError(f"Residue index {residue_idx} has no atoms.")
466
- residue_coords = result[residue_mask][None]
467
  resolved_mask = torch.ones(
468
  residue_coords.shape[:2], dtype=torch.bool, device=residue_coords.device
469
- )
470
  residue_center = torch.sum(
471
  residue_coords * resolved_mask[:, :, None], dim=1, keepdim=True
472
- ) / torch.sum(resolved_mask[:, :, None], dim=1, keepdim=True)
473
- residue_coords = residue_coords - residue_center
474
- rotation = _random_rotation_matrix()[None]
475
- residue_coords = torch.einsum("bmd,bds->bms", residue_coords, rotation)
476
- residue_coords = residue_coords + torch.randn_like(residue_coords[:, 0:1, :])
477
- result[residue_mask] = residue_coords[0]
478
- return result
 
 
 
 
479
 
480
 
481
  def build_boltz2_features(
@@ -499,39 +504,44 @@ def build_boltz2_features(
499
  if num_tokens <= 0 or num_atoms <= 0:
500
  raise RuntimeError("Boltz2 feature construction produced an empty protein template.")
501
 
502
- atom_positions = torch.tensor(np.asarray(atom_positions_np), dtype=torch.float32)
 
 
 
503
  atom_positions = _center_and_augment_atoms_per_residue(
504
  atom_positions=atom_positions,
505
  atom_residue_index=template.atom_residue_index,
506
  num_residues=num_tokens,
507
- )
508
 
509
- token_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0)
510
- residue_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0)
511
- asym_id = torch.zeros((1, num_tokens), dtype=torch.long)
512
- entity_id = torch.zeros((1, num_tokens), dtype=torch.long)
513
- sym_id = torch.zeros((1, num_tokens), dtype=torch.long)
514
  mol_type = torch.full(
515
  (1, num_tokens),
516
  fill_value=const.chain_type_ids["PROTEIN"],
517
  dtype=torch.long,
518
- )
519
 
520
- res_type_ids = torch.tensor(residue_token_ids, dtype=torch.long)
521
- res_type = one_hot(res_type_ids, num_classes=const.num_tokens).unsqueeze(0)
 
 
522
 
523
  # token_bonds encodes explicit covalent cross-links from structure bonds,
524
  # NOT backbone peptide bonds (those are implicit via residue_index + asym_id).
525
  # For a standard single-chain protein without cross-links, this is all zeros.
526
  # This matches the official Boltz2 featurizer (featurizerv2.py lines 696-705).
527
- token_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.float32)
528
- type_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.long)
529
- token_bonds = token_bonds.unsqueeze(0).unsqueeze(-1)
530
- type_bonds = type_bonds.unsqueeze(0)
531
 
532
- token_pad_mask = torch.ones((1, num_tokens), dtype=torch.float32)
533
- token_resolved_mask = torch.ones((1, num_tokens), dtype=torch.float32)
534
- token_disto_mask = torch.ones((1, num_tokens), dtype=torch.float32)
535
 
536
  num_contact_classes = len(const.contact_conditioning_info)
537
  unspecified_id = const.contact_conditioning_info["UNSPECIFIED"]
@@ -539,12 +549,14 @@ def build_boltz2_features(
539
  (num_tokens, num_tokens),
540
  fill_value=unspecified_id,
541
  dtype=torch.long,
542
- )
543
  contact_conditioning = one_hot(
544
  contact_ids,
545
  num_classes=num_contact_classes,
546
- ).unsqueeze(0)
547
- contact_threshold = torch.zeros((1, num_tokens, num_tokens), dtype=torch.float32)
 
 
548
 
549
  if "x-ray diffraction" not in const.method_types_ids:
550
  raise RuntimeError("Boltz2 method metadata omits x-ray diffraction.")
@@ -552,20 +564,22 @@ def build_boltz2_features(
552
  (1, num_tokens),
553
  fill_value=const.method_types_ids["x-ray diffraction"],
554
  dtype=torch.long,
555
- )
556
- modified = torch.zeros((1, num_tokens), dtype=torch.long)
557
- cyclic_period = torch.zeros((1, num_tokens), dtype=torch.float32)
558
- affinity_token_mask = torch.zeros((1, num_tokens), dtype=torch.float32)
559
 
560
- ref_pos = atom_positions.unsqueeze(0)
561
- atom_pad_mask = torch.ones((1, num_atoms), dtype=torch.float32)
562
- atom_resolved_mask = torch.ones((1, num_atoms), dtype=torch.bool)
563
 
564
  atom_name_codes = torch.stack(
565
  [_atom_name_to_codes(atom_name) for atom_name in template.atom_names],
566
  dim=0,
567
- )
568
- ref_atom_name_chars = one_hot(atom_name_codes, num_classes=64).unsqueeze(0)
 
 
569
 
570
  atomic_numbers = []
571
  for element in template.atom_elements:
@@ -578,7 +592,7 @@ def build_boltz2_features(
578
  ref_element = one_hot(
579
  torch.tensor(atomic_numbers, dtype=torch.long),
580
  num_classes=const.num_elements,
581
- ).unsqueeze(0)
582
 
583
  ref_charge_values = []
584
  ref_chirality_values = []
@@ -592,23 +606,27 @@ def build_boltz2_features(
592
  ref_chirality_values.append(
593
  2 if atom_name in _CHIRAL_ATOMS.get(residue_name, frozenset()) else 0
594
  )
595
- ref_charge = torch.tensor(ref_charge_values, dtype=torch.float32).unsqueeze(0)
596
- ref_chirality = torch.tensor(ref_chirality_values, dtype=torch.long).unsqueeze(0)
597
- ref_space_uid = torch.tensor(template.atom_residue_index, dtype=torch.long).unsqueeze(0)
 
 
 
 
598
 
599
  atom_to_token = one_hot(
600
  torch.tensor(template.atom_residue_index, dtype=torch.long),
601
  num_classes=num_tokens,
602
- ).unsqueeze(0)
603
  token_to_rep_atom = one_hot(
604
  torch.tensor(residue_disto_atom_idx, dtype=torch.long),
605
  num_classes=num_atoms,
606
- ).unsqueeze(0)
607
  token_to_center_atom = one_hot(
608
  torch.tensor(residue_center_atom_idx, dtype=torch.long),
609
  num_classes=num_atoms,
610
- ).unsqueeze(0)
611
- r_set_to_rep_atom = token_to_center_atom.clone()
612
 
613
  num_backbone_classes = (
614
  1 + len(const.protein_backbone_atom_index) + len(const.nucleic_backbone_atom_index)
@@ -622,17 +640,17 @@ def build_boltz2_features(
622
  atom_backbone_feat = one_hot(
623
  torch.tensor(backbone_ids, dtype=torch.long),
624
  num_classes=num_backbone_classes,
625
- ).unsqueeze(0)
626
 
627
  # X contains no observed coordinates for sequence-only inference.
628
- coords = torch.zeros((1, 1, num_atoms, 3), dtype=torch.float32)
629
  disto_coords_ensemble = torch.zeros(
630
  (1, 1, num_tokens, 3),
631
  dtype=torch.float32,
632
- )
633
 
634
- bfactor = torch.zeros((1, num_atoms), dtype=torch.float32)
635
- atom_plddt = torch.ones((1, num_atoms), dtype=torch.float32)
636
 
637
  if atoms_per_window_queries <= 0:
638
  raise ValueError("atoms_per_window_queries must be positive.")
@@ -640,60 +658,86 @@ def build_boltz2_features(
640
  (num_atoms - 1) // atoms_per_window_queries + 1
641
  ) * atoms_per_window_queries - num_atoms
642
  if pad_atoms > 0:
643
- ref_pos = torch.nn.functional.pad(ref_pos, (0, 0, 0, pad_atoms), value=0.0)
644
- atom_pad_mask = torch.nn.functional.pad(atom_pad_mask, (0, pad_atoms), value=0.0)
 
 
 
 
645
  atom_resolved_mask = torch.nn.functional.pad(
646
  atom_resolved_mask,
647
  (0, pad_atoms),
648
  value=0.0,
649
- )
650
  ref_atom_name_chars = torch.nn.functional.pad(
651
  ref_atom_name_chars,
652
  (0, 0, 0, 0, 0, pad_atoms),
653
  value=0.0,
654
- )
655
- ref_element = torch.nn.functional.pad(ref_element, (0, 0, 0, pad_atoms), value=0.0)
656
- ref_charge = torch.nn.functional.pad(ref_charge, (0, pad_atoms), value=0.0)
657
- ref_chirality = torch.nn.functional.pad(ref_chirality, (0, pad_atoms), value=0)
 
 
 
 
 
 
658
  atom_backbone_feat = torch.nn.functional.pad(
659
  atom_backbone_feat,
660
  (0, 0, 0, pad_atoms),
661
  value=0.0,
662
- )
663
- ref_space_uid = torch.nn.functional.pad(ref_space_uid, (0, pad_atoms), value=0)
664
- coords = torch.nn.functional.pad(coords, (0, 0, 0, pad_atoms), value=0.0)
665
- atom_to_token = torch.nn.functional.pad(atom_to_token, (0, 0, 0, pad_atoms), value=0.0)
 
 
 
 
 
 
666
  token_to_rep_atom = torch.nn.functional.pad(
667
  token_to_rep_atom,
668
  (0, pad_atoms),
669
  value=0.0,
670
- )
671
  token_to_center_atom = torch.nn.functional.pad(
672
  token_to_center_atom,
673
  (0, pad_atoms),
674
  value=0.0,
675
- )
676
  r_set_to_rep_atom = torch.nn.functional.pad(
677
  r_set_to_rep_atom,
678
  (0, pad_atoms),
679
  value=0.0,
680
- )
681
- bfactor = torch.nn.functional.pad(bfactor, (0, pad_atoms), value=0.0)
682
- atom_plddt = torch.nn.functional.pad(atom_plddt, (0, pad_atoms), value=0.0)
 
 
 
 
683
 
684
  frames_idx = torch.tensor(
685
  residue_frame_atom_idx_flat,
686
  dtype=torch.long,
687
- ).reshape(num_tokens, 3)
688
- frames_idx = frames_idx.unsqueeze(0).unsqueeze(1)
689
- frame_resolved_mask = torch.zeros((1, 1, num_tokens), dtype=torch.bool)
690
-
691
- msa = torch.tensor(residue_token_ids, dtype=torch.long).unsqueeze(0).unsqueeze(0)
692
- msa_paired = torch.ones((1, 1, num_tokens), dtype=torch.float32)
693
- deletion_value = torch.zeros((1, 1, num_tokens), dtype=torch.float32)
694
- has_deletion = torch.zeros((1, 1, num_tokens), dtype=torch.bool)
695
- msa_mask = torch.ones((1, 1, num_tokens), dtype=torch.long)
696
- deletion_mean = torch.zeros((1, num_tokens), dtype=torch.float32)
 
 
 
 
 
 
697
  profile = (
698
  one_hot(
699
  torch.tensor(residue_token_ids, dtype=torch.long),
@@ -701,28 +745,48 @@ def build_boltz2_features(
701
  )
702
  .float()
703
  .unsqueeze(0)
704
- )
705
 
706
  template_restype = one_hot(
707
  torch.zeros((1, 1, num_tokens), dtype=torch.long),
708
  num_classes=const.num_tokens,
709
- )
710
- template_frame_rot = torch.zeros((1, 1, num_tokens, 3, 3), dtype=torch.float32)
711
- template_frame_t = torch.zeros((1, 1, num_tokens, 3), dtype=torch.float32)
712
- template_cb = torch.zeros((1, 1, num_tokens, 3), dtype=torch.float32)
713
- template_ca = torch.zeros((1, 1, num_tokens, 3), dtype=torch.float32)
714
- template_mask_cb = torch.zeros((1, 1, num_tokens), dtype=torch.float32)
715
- template_mask_frame = torch.zeros((1, 1, num_tokens), dtype=torch.float32)
716
- template_mask = torch.zeros((1, 1, num_tokens), dtype=torch.float32)
717
- query_to_template = torch.zeros((1, 1, num_tokens), dtype=torch.long)
718
- visibility_ids = torch.zeros((1, 1, num_tokens), dtype=torch.float32)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719
 
720
  disto_target = torch.zeros(
721
  (1, num_tokens, num_tokens, 1, num_bins),
722
  dtype=torch.float32,
723
- )
724
- disto_target[..., 0] = 1.0
725
- disto_center = torch.zeros((1, num_tokens, 3), dtype=torch.float32)
 
 
726
 
727
  features: dict[str, torch.Tensor] = {
728
  "token_index": token_index,
@@ -783,4 +847,4 @@ def build_boltz2_features(
783
  "visibility_ids": visibility_ids,
784
  }
785
 
786
- return features, template
 
1
  import math
 
2
  import numpy as np
3
  import torch
4
  from torch.nn.functional import one_hot
 
6
  from . import vb_const as const
7
  from .minimal_structures import ProteinStructureTemplate
8
 
9
+
10
  _ELEMENT_TO_Z = {
11
  "H": 1,
12
  "C": 6,
 
75
  vals = [ord(ch) - 32 for ch in clipped]
76
  while len(vals) < 4:
77
  vals.append(0)
78
+ out = torch.tensor(vals, dtype=torch.long) # (4,)
79
  if not (torch.all(out >= 0) and torch.all(out < 64)):
80
  raise ValueError(f"Invalid atom-name encoding for '{atom_name}'.")
81
  return out
 
309
  to a simple geometric placement for unknown residue/atom combinations.
310
  """
311
  if res_name in _RDKIT_CONFORMERS and atom_name in _RDKIT_CONFORMERS[res_name]:
312
+ return np.array(_RDKIT_CONFORMERS[res_name][atom_name], dtype=np.float32) # (3,)
313
  # Fallback for unknown atoms (should not happen for canonical AAs)
314
  angle = (atom_idx + 1) * 0.7
315
  radius = 1.4 + 0.03 * atom_idx
 
320
  0.1 * ((atom_idx % 5) - 2),
321
  ],
322
  dtype=np.float32,
323
+ ) # (3,)
324
 
325
 
326
  def _build_template(
 
340
  atom_elements: list[str] = []
341
  atom_residue_index: list[int] = []
342
  atom_chain_id: list[str] = []
343
+ atom_positions: list[np.ndarray] = [] # each: (3,)
344
  residue_center_atom_idx: list[int] = []
345
  residue_disto_atom_idx: list[int] = []
346
  residue_frame_atom_idx: list[int] = []
 
370
  atom_residue_index.append(res_idx)
371
  atom_chain_id.append("A")
372
 
373
+ atom_pos = _get_atom_position(token_name, atom_name, local_idx) # (3,)
374
  atom_positions.append(atom_pos)
375
 
376
  if atom_name == center_atom_name:
 
422
 
423
  def _random_rotation_matrix() -> torch.Tensor:
424
  """Sample a uniform random 3x3 rotation matrix (Algorithm 19 from AF2/Boltz)."""
425
+ quaternion = torch.randn((1, 4), dtype=torch.float32) # (1, 4)
426
+ squared_norm = (quaternion * quaternion).sum(dim=1) # (1,)
427
+ norm = torch.sqrt(squared_norm) # (1,)
428
+ signed_norm = torch.where(quaternion[:, 0] < 0, -norm, norm) # (1,)
429
+ quaternion = quaternion / signed_norm[:, None] # (1, 4)
430
+
431
+ real, i, j, k = torch.unbind(quaternion, dim=-1) # each: (1,)
432
+ two_s = 2.0 / (quaternion * quaternion).sum(dim=-1) # (1,)
433
  rotation = torch.stack(
434
  (
435
  1 - two_s * (j * j + k * k),
 
443
  1 - two_s * (i * i + j * j),
444
  ),
445
  dim=-1,
446
+ ) # (1, 9)
447
+ return rotation.reshape(1, 3, 3)[0] # (3, 3)
448
 
449
 
450
  def _center_and_augment_atoms_per_residue(
 
457
  The pinned implementation intentionally excludes the final residue because
458
  it iterates to the maximum reference-space identifier rather than through it.
459
  """
460
+ # atom_positions: (a, 3), where a is the unpadded atom count.
461
+ result = atom_positions.clone() # (a, 3)
462
+ residue_index_tensor = torch.tensor(atom_residue_index, dtype=torch.long) # (a,)
463
  for residue_idx in range(max(num_residues - 1, 0)):
464
+ residue_mask = residue_index_tensor == residue_idx # (a,)
465
  if not torch.any(residue_mask):
466
  raise RuntimeError(f"Residue index {residue_idx} has no atoms.")
467
+ residue_coords = result[residue_mask][None] # (1, a_r, 3)
468
  resolved_mask = torch.ones(
469
  residue_coords.shape[:2], dtype=torch.bool, device=residue_coords.device
470
+ ) # (1, a_r)
471
  residue_center = torch.sum(
472
  residue_coords * resolved_mask[:, :, None], dim=1, keepdim=True
473
+ ) / torch.sum(resolved_mask[:, :, None], dim=1, keepdim=True) # (1, 1, 3)
474
+ residue_coords = residue_coords - residue_center # (1, a_r, 3)
475
+ rotation = _random_rotation_matrix()[None] # (1, 3, 3)
476
+ residue_coords = torch.einsum(
477
+ "bmd,bds->bms", residue_coords, rotation
478
+ ) # (1, a_r, 3)
479
+ residue_coords = (
480
+ residue_coords + torch.randn_like(residue_coords[:, 0:1, :])
481
+ ) # (1, a_r, 3)
482
+ result[residue_mask] = residue_coords[0] # (a_r, 3)
483
+ return result # (a, 3)
484
 
485
 
486
  def build_boltz2_features(
 
504
  if num_tokens <= 0 or num_atoms <= 0:
505
  raise RuntimeError("Boltz2 feature construction produced an empty protein template.")
506
 
507
+ # t is the token count, a is the unpadded atom count, and a_p is the padded atom count.
508
+ atom_positions = torch.tensor(
509
+ np.asarray(atom_positions_np), dtype=torch.float32
510
+ ) # (a, 3)
511
  atom_positions = _center_and_augment_atoms_per_residue(
512
  atom_positions=atom_positions,
513
  atom_residue_index=template.atom_residue_index,
514
  num_residues=num_tokens,
515
+ ) # (a, 3)
516
 
517
+ token_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0) # (1, t)
518
+ residue_index = torch.arange(num_tokens, dtype=torch.long).unsqueeze(0) # (1, t)
519
+ asym_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
520
+ entity_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
521
+ sym_id = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
522
  mol_type = torch.full(
523
  (1, num_tokens),
524
  fill_value=const.chain_type_ids["PROTEIN"],
525
  dtype=torch.long,
526
+ ) # (1, t)
527
 
528
+ res_type_ids = torch.tensor(residue_token_ids, dtype=torch.long) # (t,)
529
+ res_type = one_hot(
530
+ res_type_ids, num_classes=const.num_tokens
531
+ ).unsqueeze(0) # (1, t, n_res_type)
532
 
533
  # token_bonds encodes explicit covalent cross-links from structure bonds,
534
  # NOT backbone peptide bonds (those are implicit via residue_index + asym_id).
535
  # For a standard single-chain protein without cross-links, this is all zeros.
536
  # This matches the official Boltz2 featurizer (featurizerv2.py lines 696-705).
537
+ token_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.float32) # (t, t)
538
+ type_bonds = torch.zeros((num_tokens, num_tokens), dtype=torch.long) # (t, t)
539
+ token_bonds = token_bonds.unsqueeze(0).unsqueeze(-1) # (1, t, t, 1)
540
+ type_bonds = type_bonds.unsqueeze(0) # (1, t, t)
541
 
542
+ token_pad_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
543
+ token_resolved_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
544
+ token_disto_mask = torch.ones((1, num_tokens), dtype=torch.float32) # (1, t)
545
 
546
  num_contact_classes = len(const.contact_conditioning_info)
547
  unspecified_id = const.contact_conditioning_info["UNSPECIFIED"]
 
549
  (num_tokens, num_tokens),
550
  fill_value=unspecified_id,
551
  dtype=torch.long,
552
+ ) # (t, t)
553
  contact_conditioning = one_hot(
554
  contact_ids,
555
  num_classes=num_contact_classes,
556
+ ).unsqueeze(0) # (1, t, t, n_contact)
557
+ contact_threshold = torch.zeros(
558
+ (1, num_tokens, num_tokens), dtype=torch.float32
559
+ ) # (1, t, t)
560
 
561
  if "x-ray diffraction" not in const.method_types_ids:
562
  raise RuntimeError("Boltz2 method metadata omits x-ray diffraction.")
 
564
  (1, num_tokens),
565
  fill_value=const.method_types_ids["x-ray diffraction"],
566
  dtype=torch.long,
567
+ ) # (1, t)
568
+ modified = torch.zeros((1, num_tokens), dtype=torch.long) # (1, t)
569
+ cyclic_period = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
570
+ affinity_token_mask = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
571
 
572
+ ref_pos = atom_positions.unsqueeze(0) # (1, a, 3)
573
+ atom_pad_mask = torch.ones((1, num_atoms), dtype=torch.float32) # (1, a)
574
+ atom_resolved_mask = torch.ones((1, num_atoms), dtype=torch.bool) # (1, a)
575
 
576
  atom_name_codes = torch.stack(
577
  [_atom_name_to_codes(atom_name) for atom_name in template.atom_names],
578
  dim=0,
579
+ ) # (a, 4)
580
+ ref_atom_name_chars = one_hot(
581
+ atom_name_codes, num_classes=64
582
+ ).unsqueeze(0) # (1, a, 4, 64)
583
 
584
  atomic_numbers = []
585
  for element in template.atom_elements:
 
592
  ref_element = one_hot(
593
  torch.tensor(atomic_numbers, dtype=torch.long),
594
  num_classes=const.num_elements,
595
+ ).unsqueeze(0) # (1, a, n_element)
596
 
597
  ref_charge_values = []
598
  ref_chirality_values = []
 
606
  ref_chirality_values.append(
607
  2 if atom_name in _CHIRAL_ATOMS.get(residue_name, frozenset()) else 0
608
  )
609
+ ref_charge = torch.tensor(ref_charge_values, dtype=torch.float32).unsqueeze(0) # (1, a)
610
+ ref_chirality = torch.tensor(
611
+ ref_chirality_values, dtype=torch.long
612
+ ).unsqueeze(0) # (1, a)
613
+ ref_space_uid = torch.tensor(
614
+ template.atom_residue_index, dtype=torch.long
615
+ ).unsqueeze(0) # (1, a)
616
 
617
  atom_to_token = one_hot(
618
  torch.tensor(template.atom_residue_index, dtype=torch.long),
619
  num_classes=num_tokens,
620
+ ).unsqueeze(0) # (1, a, t)
621
  token_to_rep_atom = one_hot(
622
  torch.tensor(residue_disto_atom_idx, dtype=torch.long),
623
  num_classes=num_atoms,
624
+ ).unsqueeze(0) # (1, t, a)
625
  token_to_center_atom = one_hot(
626
  torch.tensor(residue_center_atom_idx, dtype=torch.long),
627
  num_classes=num_atoms,
628
+ ).unsqueeze(0) # (1, t, a)
629
+ r_set_to_rep_atom = token_to_center_atom.clone() # (1, t, a)
630
 
631
  num_backbone_classes = (
632
  1 + len(const.protein_backbone_atom_index) + len(const.nucleic_backbone_atom_index)
 
640
  atom_backbone_feat = one_hot(
641
  torch.tensor(backbone_ids, dtype=torch.long),
642
  num_classes=num_backbone_classes,
643
+ ).unsqueeze(0) # (1, a, n_backbone)
644
 
645
  # X contains no observed coordinates for sequence-only inference.
646
+ coords = torch.zeros((1, 1, num_atoms, 3), dtype=torch.float32) # (1, 1, a, 3)
647
  disto_coords_ensemble = torch.zeros(
648
  (1, 1, num_tokens, 3),
649
  dtype=torch.float32,
650
+ ) # (1, 1, t, 3)
651
 
652
+ bfactor = torch.zeros((1, num_atoms), dtype=torch.float32) # (1, a)
653
+ atom_plddt = torch.ones((1, num_atoms), dtype=torch.float32) # (1, a)
654
 
655
  if atoms_per_window_queries <= 0:
656
  raise ValueError("atoms_per_window_queries must be positive.")
 
658
  (num_atoms - 1) // atoms_per_window_queries + 1
659
  ) * atoms_per_window_queries - num_atoms
660
  if pad_atoms > 0:
661
+ ref_pos = torch.nn.functional.pad(
662
+ ref_pos, (0, 0, 0, pad_atoms), value=0.0
663
+ ) # (1, a_p, 3)
664
+ atom_pad_mask = torch.nn.functional.pad(
665
+ atom_pad_mask, (0, pad_atoms), value=0.0
666
+ ) # (1, a_p)
667
  atom_resolved_mask = torch.nn.functional.pad(
668
  atom_resolved_mask,
669
  (0, pad_atoms),
670
  value=0.0,
671
+ ) # (1, a_p)
672
  ref_atom_name_chars = torch.nn.functional.pad(
673
  ref_atom_name_chars,
674
  (0, 0, 0, 0, 0, pad_atoms),
675
  value=0.0,
676
+ ) # (1, a_p, 4, 64)
677
+ ref_element = torch.nn.functional.pad(
678
+ ref_element, (0, 0, 0, pad_atoms), value=0.0
679
+ ) # (1, a_p, n_element)
680
+ ref_charge = torch.nn.functional.pad(
681
+ ref_charge, (0, pad_atoms), value=0.0
682
+ ) # (1, a_p)
683
+ ref_chirality = torch.nn.functional.pad(
684
+ ref_chirality, (0, pad_atoms), value=0
685
+ ) # (1, a_p)
686
  atom_backbone_feat = torch.nn.functional.pad(
687
  atom_backbone_feat,
688
  (0, 0, 0, pad_atoms),
689
  value=0.0,
690
+ ) # (1, a_p, n_backbone)
691
+ ref_space_uid = torch.nn.functional.pad(
692
+ ref_space_uid, (0, pad_atoms), value=0
693
+ ) # (1, a_p)
694
+ coords = torch.nn.functional.pad(
695
+ coords, (0, 0, 0, pad_atoms), value=0.0
696
+ ) # (1, 1, a_p, 3)
697
+ atom_to_token = torch.nn.functional.pad(
698
+ atom_to_token, (0, 0, 0, pad_atoms), value=0.0
699
+ ) # (1, a_p, t)
700
  token_to_rep_atom = torch.nn.functional.pad(
701
  token_to_rep_atom,
702
  (0, pad_atoms),
703
  value=0.0,
704
+ ) # (1, t, a_p)
705
  token_to_center_atom = torch.nn.functional.pad(
706
  token_to_center_atom,
707
  (0, pad_atoms),
708
  value=0.0,
709
+ ) # (1, t, a_p)
710
  r_set_to_rep_atom = torch.nn.functional.pad(
711
  r_set_to_rep_atom,
712
  (0, pad_atoms),
713
  value=0.0,
714
+ ) # (1, t, a_p)
715
+ bfactor = torch.nn.functional.pad(
716
+ bfactor, (0, pad_atoms), value=0.0
717
+ ) # (1, a_p)
718
+ atom_plddt = torch.nn.functional.pad(
719
+ atom_plddt, (0, pad_atoms), value=0.0
720
+ ) # (1, a_p)
721
 
722
  frames_idx = torch.tensor(
723
  residue_frame_atom_idx_flat,
724
  dtype=torch.long,
725
+ ).reshape(num_tokens, 3) # (t, 3)
726
+ frames_idx = frames_idx.unsqueeze(0).unsqueeze(1) # (1, 1, t, 3)
727
+ frame_resolved_mask = torch.zeros(
728
+ (1, 1, num_tokens), dtype=torch.bool
729
+ ) # (1, 1, t)
730
+
731
+ msa = (
732
+ torch.tensor(residue_token_ids, dtype=torch.long).unsqueeze(0).unsqueeze(0)
733
+ ) # (1, 1, t)
734
+ msa_paired = torch.ones((1, 1, num_tokens), dtype=torch.float32) # (1, 1, t)
735
+ deletion_value = torch.zeros(
736
+ (1, 1, num_tokens), dtype=torch.float32
737
+ ) # (1, 1, t)
738
+ has_deletion = torch.zeros((1, 1, num_tokens), dtype=torch.bool) # (1, 1, t)
739
+ msa_mask = torch.ones((1, 1, num_tokens), dtype=torch.long) # (1, 1, t)
740
+ deletion_mean = torch.zeros((1, num_tokens), dtype=torch.float32) # (1, t)
741
  profile = (
742
  one_hot(
743
  torch.tensor(residue_token_ids, dtype=torch.long),
 
745
  )
746
  .float()
747
  .unsqueeze(0)
748
+ ) # (1, t, n_res_type)
749
 
750
  template_restype = one_hot(
751
  torch.zeros((1, 1, num_tokens), dtype=torch.long),
752
  num_classes=const.num_tokens,
753
+ ) # (1, 1, t, n_res_type)
754
+ template_frame_rot = torch.zeros(
755
+ (1, 1, num_tokens, 3, 3), dtype=torch.float32
756
+ ) # (1, 1, t, 3, 3)
757
+ template_frame_t = torch.zeros(
758
+ (1, 1, num_tokens, 3), dtype=torch.float32
759
+ ) # (1, 1, t, 3)
760
+ template_cb = torch.zeros(
761
+ (1, 1, num_tokens, 3), dtype=torch.float32
762
+ ) # (1, 1, t, 3)
763
+ template_ca = torch.zeros(
764
+ (1, 1, num_tokens, 3), dtype=torch.float32
765
+ ) # (1, 1, t, 3)
766
+ template_mask_cb = torch.zeros(
767
+ (1, 1, num_tokens), dtype=torch.float32
768
+ ) # (1, 1, t)
769
+ template_mask_frame = torch.zeros(
770
+ (1, 1, num_tokens), dtype=torch.float32
771
+ ) # (1, 1, t)
772
+ template_mask = torch.zeros(
773
+ (1, 1, num_tokens), dtype=torch.float32
774
+ ) # (1, 1, t)
775
+ query_to_template = torch.zeros(
776
+ (1, 1, num_tokens), dtype=torch.long
777
+ ) # (1, 1, t)
778
+ visibility_ids = torch.zeros(
779
+ (1, 1, num_tokens), dtype=torch.float32
780
+ ) # (1, 1, t)
781
 
782
  disto_target = torch.zeros(
783
  (1, num_tokens, num_tokens, 1, num_bins),
784
  dtype=torch.float32,
785
+ ) # (1, t, t, 1, n_bin)
786
+ disto_target[..., 0] = 1.0 # (1, t, t, 1)
787
+ disto_center = torch.zeros(
788
+ (1, num_tokens, 3), dtype=torch.float32
789
+ ) # (1, t, 3)
790
 
791
  features: dict[str, torch.Tensor] = {
792
  "token_index": token_index,
 
847
  "visibility_ids": visibility_ids,
848
  }
849
 
850
+ return features, template # each feature shape is traced at its binding above
fastplms/models/boltz/modeling_boltz2.py CHANGED
@@ -1,14 +1,13 @@
1
  import copy
2
  import inspect
3
  import random
 
 
 
4
  from collections.abc import Iterator, Mapping, Sequence
5
  from contextlib import contextmanager, nullcontext
6
  from dataclasses import dataclass
7
  from typing import Any, ClassVar
8
-
9
- import numpy as np
10
- import torch
11
- import torch.nn as nn
12
  from torch import Tensor
13
  from transformers import PretrainedConfig, PreTrainedModel
14
  from transformers.modeling_outputs import ModelOutput
@@ -113,6 +112,7 @@ def _require_key(mapping: dict[str, Any], key: str) -> Any:
113
 
114
 
115
  def _state_dict_without_wrappers(state_dict: dict[str, Tensor]) -> dict[str, Tensor]:
 
116
  cleaned: dict[str, Tensor] = {}
117
  for key, value in state_dict.items():
118
  if key.startswith("ema."):
@@ -122,13 +122,13 @@ def _state_dict_without_wrappers(state_dict: dict[str, Tensor]) -> dict[str, Ten
122
  new_key = new_key[len("model.") :]
123
  if new_key.startswith("module."):
124
  new_key = new_key[len("module.") :]
125
- cleaned[new_key] = value
126
  return cleaned
127
 
128
 
129
  def _to_cpu_detached(value: Any) -> Any:
130
  if torch.is_tensor(value):
131
- return value.detach().cpu()
132
  if isinstance(value, dict):
133
  out: dict[Any, Any] = {}
134
  for key, nested_value in value.items():
@@ -145,8 +145,8 @@ def _to_cpu_detached(value: Any) -> Any:
145
  class _RandomState:
146
  python: object
147
  numpy: tuple[Any, ...]
148
- torch_cpu: Tensor
149
- torch_cuda: list[Tensor] | None
150
 
151
 
152
  @contextmanager
@@ -212,46 +212,46 @@ def _filtered_kwargs(target: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
212
  class Boltz2ModelOutput(ModelOutput):
213
  """Raw Boltz2 inference output with standard AutoModel controls."""
214
 
215
- last_hidden_state: torch.Tensor | None = None
216
- hidden_states: tuple[torch.Tensor, ...] | None = None
217
- attentions: tuple[torch.Tensor, ...] | None = None
218
- pdistogram: torch.Tensor | None = None
219
- s: torch.Tensor | None = None
220
- z: torch.Tensor | None = None
221
- sample_atom_coords: torch.Tensor | None = None
222
- diff_token_repr: torch.Tensor | None = None
223
- s_conf: torch.Tensor | None = None
224
- z_conf: torch.Tensor | None = None
225
- pde_logits: torch.Tensor | None = None
226
- plddt_logits: torch.Tensor | None = None
227
- resolved_logits: torch.Tensor | None = None
228
- pde: torch.Tensor | None = None
229
- plddt: torch.Tensor | None = None
230
- complex_plddt: torch.Tensor | None = None
231
- complex_iplddt: torch.Tensor | None = None
232
- complex_pde: torch.Tensor | None = None
233
- complex_ipde: torch.Tensor | None = None
234
- pae_logits: torch.Tensor | None = None
235
- pae: torch.Tensor | None = None
236
- ptm: torch.Tensor | None = None
237
- iptm: torch.Tensor | None = None
238
- ligand_iptm: torch.Tensor | None = None
239
- protein_iptm: torch.Tensor | None = None
240
  pair_chains_iptm: Any = None
241
 
242
 
243
  @dataclass
244
  class Boltz2StructureOutput(ModelOutput):
245
- sample_atom_coords: torch.Tensor | None = None
246
- atom_pad_mask: torch.Tensor | None = None
247
- plddt: torch.Tensor | None = None
248
- confidence_score: torch.Tensor | None = None
249
- complex_plddt: torch.Tensor | None = None
250
- iptm: torch.Tensor | None = None
251
- ptm: torch.Tensor | None = None
252
  sequence: str | None = None
253
  structure_template: ProteinStructureTemplate | None = None
254
- raw_output: dict[str, torch.Tensor] | None = None
255
  seed: int | None = None
256
 
257
 
@@ -573,41 +573,49 @@ class Boltz2InferenceCore(nn.Module):
573
  run_confidence_sequentially: bool = True,
574
  detach_confidence: bool = True,
575
  ) -> dict[str, Tensor]:
576
- s_inputs = self.input_embedder(feats)
577
- s_init = self.s_init(s_inputs)
578
-
579
- z_init = self.z_init_1(s_inputs)[:, :, None] + self.z_init_2(s_inputs)[:, None, :]
580
- relative_position_encoding = self.rel_pos(feats)
581
- z_init = z_init + relative_position_encoding
582
- z_init = z_init + self.token_bonds(feats["token_bonds"].float())
 
 
 
 
 
 
583
  if self.bond_type_feature:
584
- z_init = z_init + self.token_bonds_type(feats["type_bonds"].long())
585
- z_init = z_init + self.contact_conditioning(feats)
 
 
586
 
587
- s = torch.zeros_like(s_init)
588
- z = torch.zeros_like(z_init)
589
- mask = feats["token_pad_mask"].float()
590
- pair_mask = mask[:, :, None] * mask[:, None, :]
591
 
592
  if self.run_trunk_and_structure:
593
  for _ in range(recycling_steps + 1):
594
- s = s_init + self.s_recycle(self.s_norm(s))
595
- z = z_init + self.z_recycle(self.z_norm(z))
596
  z = z + self.msa_module(
597
  z,
598
  s_inputs,
599
  feats,
600
  use_kernels=self.use_kernels,
601
- )
602
  s, z = self.pairformer_module(
603
  s,
604
  z,
605
  mask=mask,
606
  pair_mask=pair_mask,
607
  use_kernels=self.use_kernels,
608
- )
609
 
610
- pdistogram = self.distogram_module(z)
611
  output: dict[str, Tensor] = {
612
  "pdistogram": pdistogram,
613
  "s": s,
@@ -622,7 +630,7 @@ class Boltz2InferenceCore(nn.Module):
622
  relative_position_encoding=relative_position_encoding,
623
  feats=feats,
624
  )
625
- )
626
  diffusion_conditioning = {
627
  "q": q,
628
  "c": c,
@@ -642,29 +650,31 @@ class Boltz2InferenceCore(nn.Module):
642
  max_parallel_samples=max_parallel_samples,
643
  steering_args=self.steering_args,
644
  diffusion_conditioning=diffusion_conditioning,
645
- )
646
  output.update(struct_out)
647
 
648
  if self.confidence_prediction:
649
  if self.skip_run_structure:
650
- x_pred = feats["coords"].repeat_interleave(diffusion_samples, 0)
 
 
651
  else:
652
  if "sample_atom_coords" not in output:
653
  raise RuntimeError("Structure sampling did not produce sample_atom_coords.")
654
- x_pred = output["sample_atom_coords"]
655
 
656
  if detach_confidence:
657
- s_inputs_c = s_inputs.detach()
658
- s_c = s.detach()
659
- z_c = z.detach()
660
- x_pred_c = x_pred.detach()
661
- pdist_c = output["pdistogram"][:, :, :, 0].detach()
662
  else:
663
- s_inputs_c = s_inputs
664
- s_c = s
665
- z_c = z
666
- x_pred_c = x_pred
667
- pdist_c = output["pdistogram"][:, :, :, 0]
668
 
669
  output.update(
670
  self.confidence_module(
@@ -680,7 +690,7 @@ class Boltz2InferenceCore(nn.Module):
680
  )
681
  )
682
 
683
- return output
684
 
685
 
686
  class Boltz2Model(PreTrainedModel):
@@ -698,6 +708,7 @@ class Boltz2Model(PreTrainedModel):
698
  return
699
 
700
  def _detied_state_dict(self) -> dict[str, Tensor]:
 
701
  raw_state = self.state_dict()
702
  seen_ptrs: dict[int, str] = {}
703
  out: dict[str, Tensor] = {}
@@ -705,13 +716,13 @@ class Boltz2Model(PreTrainedModel):
705
  if torch.is_tensor(tensor):
706
  ptr = tensor.untyped_storage().data_ptr()
707
  if ptr in seen_ptrs:
708
- out[key] = tensor.clone()
709
  else:
710
  seen_ptrs[ptr] = key
711
- out[key] = tensor
712
  else:
713
- out[key] = tensor
714
- return out
715
 
716
  def save_pretrained(self, save_directory: str, **kwargs: Any) -> None:
717
  if "safe_serialization" not in kwargs:
@@ -757,7 +768,7 @@ class Boltz2Model(PreTrainedModel):
757
  checkpoint_path,
758
  map_location=map_location,
759
  weights_only=False,
760
- )
761
  if not isinstance(checkpoint, dict):
762
  raise TypeError("Checkpoint must deserialize to a dictionary.")
763
  _require_key(checkpoint, "hyper_parameters")
@@ -766,7 +777,7 @@ class Boltz2Model(PreTrainedModel):
766
  hparams = checkpoint["hyper_parameters"]
767
  if not isinstance(hparams, dict):
768
  raise TypeError("Checkpoint hyper_parameters must be a dictionary.")
769
- state_dict = checkpoint["state_dict"]
770
  if not isinstance(state_dict, dict):
771
  raise TypeError("Checkpoint state_dict must be a dictionary.")
772
 
@@ -778,7 +789,7 @@ class Boltz2Model(PreTrainedModel):
778
  default_diffusion_samples=default_diffusion_samples,
779
  )
780
  model = cls(config)
781
- cleaned = _state_dict_without_wrappers(state_dict)
782
  target_keys = set(model.core.state_dict().keys())
783
  for key in target_keys:
784
  if ".attention.norm_s." in key:
@@ -789,7 +800,7 @@ class Boltz2Model(PreTrainedModel):
789
  filtered: dict[str, Tensor] = {}
790
  for key, value in cleaned.items():
791
  if key in target_keys:
792
- filtered[key] = value
793
 
794
  missing = sorted(target_keys.difference(filtered.keys()))
795
  if missing:
@@ -826,6 +837,7 @@ class Boltz2Model(PreTrainedModel):
826
  output_hidden_states: bool | None = None,
827
  return_dict: bool | None = None,
828
  ) -> Boltz2ModelOutput | tuple[Any, ...]:
 
829
  output_attentions = (
830
  self.config.output_attentions if output_attentions is None else output_attentions
831
  )
@@ -854,9 +866,9 @@ class Boltz2Model(PreTrainedModel):
854
  max_parallel_samples=max_parallel_samples,
855
  run_confidence_sequentially=run_confidence_sequentially,
856
  detach_confidence=detach_confidence,
857
- )
858
- token_state = raw_output.get("s")
859
- pair_state = raw_output.get("z")
860
  model_output = Boltz2ModelOutput(
861
  last_hidden_state=token_state,
862
  hidden_states=(token_state, pair_state)
@@ -871,16 +883,19 @@ class Boltz2Model(PreTrainedModel):
871
  feats: dict[str, Tensor],
872
  float_dtype: torch.dtype = torch.float32,
873
  ) -> dict[str, Tensor]:
 
874
  moved: dict[str, Tensor] = {}
875
  for key, value in feats.items():
876
  if torch.is_tensor(value):
877
  if value.is_floating_point():
878
- moved[key] = value.to(device=self.device, dtype=float_dtype)
 
 
879
  else:
880
- moved[key] = value.to(device=self.device)
881
  else:
882
- moved[key] = value
883
- return moved
884
 
885
  def predict_structure(
886
  self,
@@ -914,8 +929,10 @@ class Boltz2Model(PreTrainedModel):
914
  atoms_per_window_queries=(
915
  self.core.input_embedder.atom_encoder.atoms_per_window_queries
916
  ),
917
- )
918
- feats = self._to_model_device(feats, float_dtype=torch.float32)
 
 
919
  autocast_context = (
920
  torch.autocast(device_type="cuda", dtype=torch.bfloat16)
921
  if self.device.type == "cuda"
@@ -930,32 +947,42 @@ class Boltz2Model(PreTrainedModel):
930
  max_parallel_samples=max_parallel_samples,
931
  run_confidence_sequentially=run_confidence_sequentially,
932
  return_dict=True,
933
- )
934
 
935
- sample_atom_coords_value = output.get("sample_atom_coords")
 
 
936
  if not isinstance(sample_atom_coords_value, torch.Tensor):
937
  raise RuntimeError("Boltz2 structure sampling did not return coordinate tensors.")
938
- sample_atom_coords = sample_atom_coords_value.detach().cpu()
939
- non_finite_mask = torch.logical_not(torch.isfinite(sample_atom_coords))
 
 
940
  if torch.any(non_finite_mask):
941
  raise RuntimeError(
942
  "sample_atom_coords contains non-finite values. "
943
  f"Non-finite count: {int(non_finite_mask.sum().item())}"
944
  )
945
- atom_pad_mask = feats["atom_pad_mask"][0].detach().cpu()
946
- plddt = output["plddt"].detach().cpu() if "plddt" in output else None
 
 
947
  complex_plddt = (
948
  output["complex_plddt"].detach().cpu() if "complex_plddt" in output else None
949
- )
950
- iptm = output["iptm"].detach().cpu() if "iptm" in output else None
951
- ptm = output["ptm"].detach().cpu() if "ptm" in output else None
952
-
953
- confidence_score = None
 
 
 
 
954
  if (complex_plddt is not None) and (iptm is not None) and (ptm is not None):
955
  if torch.allclose(iptm, torch.zeros_like(iptm)):
956
- confidence_score = (4 * complex_plddt + ptm) / 5
957
  else:
958
- confidence_score = (4 * complex_plddt + iptm) / 5
959
 
960
  return Boltz2StructureOutput(
961
  sample_atom_coords=sample_atom_coords,
 
1
  import copy
2
  import inspect
3
  import random
4
+ import numpy as np
5
+ import torch
6
+ import torch.nn as nn
7
  from collections.abc import Iterator, Mapping, Sequence
8
  from contextlib import contextmanager, nullcontext
9
  from dataclasses import dataclass
10
  from typing import Any, ClassVar
 
 
 
 
11
  from torch import Tensor
12
  from transformers import PretrainedConfig, PreTrainedModel
13
  from transformers.modeling_outputs import ModelOutput
 
112
 
113
 
114
  def _state_dict_without_wrappers(state_dict: dict[str, Tensor]) -> dict[str, Tensor]:
115
+ # Every state tensor has its parameter-defined shape (...).
116
  cleaned: dict[str, Tensor] = {}
117
  for key, value in state_dict.items():
118
  if key.startswith("ema."):
 
122
  new_key = new_key[len("model.") :]
123
  if new_key.startswith("module."):
124
  new_key = new_key[len("module.") :]
125
+ cleaned[new_key] = value # (...)
126
  return cleaned
127
 
128
 
129
  def _to_cpu_detached(value: Any) -> Any:
130
  if torch.is_tensor(value):
131
+ return value.detach().cpu() # same shape (...)
132
  if isinstance(value, dict):
133
  out: dict[Any, Any] = {}
134
  for key, nested_value in value.items():
 
145
  class _RandomState:
146
  python: object
147
  numpy: tuple[Any, ...]
148
+ torch_cpu: Tensor # (n_cpu_rng_state,)
149
+ torch_cuda: list[Tensor] | None # each: (n_cuda_rng_state,)
150
 
151
 
152
  @contextmanager
 
212
  class Boltz2ModelOutput(ModelOutput):
213
  """Raw Boltz2 inference output with standard AutoModel controls."""
214
 
215
+ last_hidden_state: torch.Tensor | None = None # (b, t, d_s) or None
216
+ hidden_states: tuple[torch.Tensor, ...] | None = None # each shape traced at source
217
+ attentions: tuple[torch.Tensor, ...] | None = None # unsupported; always None
218
+ pdistogram: torch.Tensor | None = None # (b, t, t, n_distogram, n_bin) or None
219
+ s: torch.Tensor | None = None # (b, t, d_s) or None
220
+ z: torch.Tensor | None = None # (b, t, t, d_z) or None
221
+ sample_atom_coords: torch.Tensor | None = None # (b * m, a, 3) or None
222
+ diff_token_repr: torch.Tensor | None = None # (...) or None
223
+ s_conf: torch.Tensor | None = None # (...) or None
224
+ z_conf: torch.Tensor | None = None # (...) or None
225
+ pde_logits: torch.Tensor | None = None # (...) or None
226
+ plddt_logits: torch.Tensor | None = None # (...) or None
227
+ resolved_logits: torch.Tensor | None = None # (...) or None
228
+ pde: torch.Tensor | None = None # (...) or None
229
+ plddt: torch.Tensor | None = None # (...) or None
230
+ complex_plddt: torch.Tensor | None = None # (...) or None
231
+ complex_iplddt: torch.Tensor | None = None # (...) or None
232
+ complex_pde: torch.Tensor | None = None # (...) or None
233
+ complex_ipde: torch.Tensor | None = None # (...) or None
234
+ pae_logits: torch.Tensor | None = None # (...) or None
235
+ pae: torch.Tensor | None = None # (...) or None
236
+ ptm: torch.Tensor | None = None # (...) or None
237
+ iptm: torch.Tensor | None = None # (...) or None
238
+ ligand_iptm: torch.Tensor | None = None # (...) or None
239
+ protein_iptm: torch.Tensor | None = None # (...) or None
240
  pair_chains_iptm: Any = None
241
 
242
 
243
  @dataclass
244
  class Boltz2StructureOutput(ModelOutput):
245
+ sample_atom_coords: torch.Tensor | None = None # (m, a_p, 3) or None
246
+ atom_pad_mask: torch.Tensor | None = None # (a_p,) or None
247
+ plddt: torch.Tensor | None = None # (m, a_p) or None
248
+ confidence_score: torch.Tensor | None = None # (m,) or None
249
+ complex_plddt: torch.Tensor | None = None # (m,) or None
250
+ iptm: torch.Tensor | None = None # (m,) or None
251
+ ptm: torch.Tensor | None = None # (m,) or None
252
  sequence: str | None = None
253
  structure_template: ProteinStructureTemplate | None = None
254
+ raw_output: dict[str, torch.Tensor] | None = None # values keep model-output shapes
255
  seed: int | None = None
256
 
257
 
 
573
  run_confidence_sequentially: bool = True,
574
  detach_confidence: bool = True,
575
  ) -> dict[str, Tensor]:
576
+ # b is the batch size, t the token count, a the padded atom count,
577
+ # d_s the token width, d_z the pair width, and m the diffusion multiplicity.
578
+ s_inputs = self.input_embedder(feats) # (b, t, d_s)
579
+ s_init = self.s_init(s_inputs) # (b, t, d_s)
580
+
581
+ z_init = (
582
+ self.z_init_1(s_inputs)[:, :, None] + self.z_init_2(s_inputs)[:, None, :]
583
+ ) # (b, t, t, d_z)
584
+ relative_position_encoding = self.rel_pos(feats) # (b, t, t, d_z)
585
+ z_init = z_init + relative_position_encoding # (b, t, t, d_z)
586
+ z_init = z_init + self.token_bonds(
587
+ feats["token_bonds"].float()
588
+ ) # (b, t, t, d_z)
589
  if self.bond_type_feature:
590
+ z_init = z_init + self.token_bonds_type(
591
+ feats["type_bonds"].long()
592
+ ) # (b, t, t, d_z)
593
+ z_init = z_init + self.contact_conditioning(feats) # (b, t, t, d_z)
594
 
595
+ s = torch.zeros_like(s_init) # (b, t, d_s)
596
+ z = torch.zeros_like(z_init) # (b, t, t, d_z)
597
+ mask = feats["token_pad_mask"].float() # (b, t)
598
+ pair_mask = mask[:, :, None] * mask[:, None, :] # (b, t, t)
599
 
600
  if self.run_trunk_and_structure:
601
  for _ in range(recycling_steps + 1):
602
+ s = s_init + self.s_recycle(self.s_norm(s)) # (b, t, d_s)
603
+ z = z_init + self.z_recycle(self.z_norm(z)) # (b, t, t, d_z)
604
  z = z + self.msa_module(
605
  z,
606
  s_inputs,
607
  feats,
608
  use_kernels=self.use_kernels,
609
+ ) # (b, t, t, d_z)
610
  s, z = self.pairformer_module(
611
  s,
612
  z,
613
  mask=mask,
614
  pair_mask=pair_mask,
615
  use_kernels=self.use_kernels,
616
+ ) # (b, t, d_s), (b, t, t, d_z)
617
 
618
+ pdistogram = self.distogram_module(z) # (b, t, t, n_distogram, n_bin)
619
  output: dict[str, Tensor] = {
620
  "pdistogram": pdistogram,
621
  "s": s,
 
630
  relative_position_encoding=relative_position_encoding,
631
  feats=feats,
632
  )
633
+ ) # q/c: (b, a, d_a); biases retain their attention-specific axes
634
  diffusion_conditioning = {
635
  "q": q,
636
  "c": c,
 
650
  max_parallel_samples=max_parallel_samples,
651
  steering_args=self.steering_args,
652
  diffusion_conditioning=diffusion_conditioning,
653
+ ) # tensor values include sample_atom_coords: (b * m, a, 3)
654
  output.update(struct_out)
655
 
656
  if self.confidence_prediction:
657
  if self.skip_run_structure:
658
+ x_pred = feats["coords"].repeat_interleave(
659
+ diffusion_samples, 0
660
+ ) # (b * m, ..., a, 3)
661
  else:
662
  if "sample_atom_coords" not in output:
663
  raise RuntimeError("Structure sampling did not produce sample_atom_coords.")
664
+ x_pred = output["sample_atom_coords"] # (b * m, a, 3)
665
 
666
  if detach_confidence:
667
+ s_inputs_c = s_inputs.detach() # (b, t, d_s)
668
+ s_c = s.detach() # (b, t, d_s)
669
+ z_c = z.detach() # (b, t, t, d_z)
670
+ x_pred_c = x_pred.detach() # (b * m, ..., a, 3)
671
+ pdist_c = output["pdistogram"][:, :, :, 0].detach() # (b, t, t, n_bin)
672
  else:
673
+ s_inputs_c = s_inputs # (b, t, d_s)
674
+ s_c = s # (b, t, d_s)
675
+ z_c = z # (b, t, t, d_z)
676
+ x_pred_c = x_pred # (b * m, ..., a, 3)
677
+ pdist_c = output["pdistogram"][:, :, :, 0] # (b, t, t, n_bin)
678
 
679
  output.update(
680
  self.confidence_module(
 
690
  )
691
  )
692
 
693
+ return output # named tensors retain the shapes traced above
694
 
695
 
696
  class Boltz2Model(PreTrainedModel):
 
708
  return
709
 
710
  def _detied_state_dict(self) -> dict[str, Tensor]:
711
+ # Every state tensor has its parameter-defined shape (...).
712
  raw_state = self.state_dict()
713
  seen_ptrs: dict[int, str] = {}
714
  out: dict[str, Tensor] = {}
 
716
  if torch.is_tensor(tensor):
717
  ptr = tensor.untyped_storage().data_ptr()
718
  if ptr in seen_ptrs:
719
+ out[key] = tensor.clone() # (...)
720
  else:
721
  seen_ptrs[ptr] = key
722
+ out[key] = tensor # (...)
723
  else:
724
+ out[key] = tensor # (...)
725
+ return out # each tensor: (...)
726
 
727
  def save_pretrained(self, save_directory: str, **kwargs: Any) -> None:
728
  if "safe_serialization" not in kwargs:
 
768
  checkpoint_path,
769
  map_location=map_location,
770
  weights_only=False,
771
+ ) # mapping values include state tensors with parameter-defined shapes (...)
772
  if not isinstance(checkpoint, dict):
773
  raise TypeError("Checkpoint must deserialize to a dictionary.")
774
  _require_key(checkpoint, "hyper_parameters")
 
777
  hparams = checkpoint["hyper_parameters"]
778
  if not isinstance(hparams, dict):
779
  raise TypeError("Checkpoint hyper_parameters must be a dictionary.")
780
+ state_dict = checkpoint["state_dict"] # each tensor: (...)
781
  if not isinstance(state_dict, dict):
782
  raise TypeError("Checkpoint state_dict must be a dictionary.")
783
 
 
789
  default_diffusion_samples=default_diffusion_samples,
790
  )
791
  model = cls(config)
792
+ cleaned = _state_dict_without_wrappers(state_dict) # each tensor: (...)
793
  target_keys = set(model.core.state_dict().keys())
794
  for key in target_keys:
795
  if ".attention.norm_s." in key:
 
800
  filtered: dict[str, Tensor] = {}
801
  for key, value in cleaned.items():
802
  if key in target_keys:
803
+ filtered[key] = value # (...)
804
 
805
  missing = sorted(target_keys.difference(filtered.keys()))
806
  if missing:
 
837
  output_hidden_states: bool | None = None,
838
  return_dict: bool | None = None,
839
  ) -> Boltz2ModelOutput | tuple[Any, ...]:
840
+ # Feature shapes follow build_boltz2_features; b and t are batch and token counts.
841
  output_attentions = (
842
  self.config.output_attentions if output_attentions is None else output_attentions
843
  )
 
866
  max_parallel_samples=max_parallel_samples,
867
  run_confidence_sequentially=run_confidence_sequentially,
868
  detach_confidence=detach_confidence,
869
+ ) # named tensors use the Boltz2InferenceCore.forward shapes
870
+ token_state = raw_output.get("s") # (b, t, d_s) or None
871
+ pair_state = raw_output.get("z") # (b, t, t, d_z) or None
872
  model_output = Boltz2ModelOutput(
873
  last_hidden_state=token_state,
874
  hidden_states=(token_state, pair_state)
 
883
  feats: dict[str, Tensor],
884
  float_dtype: torch.dtype = torch.float32,
885
  ) -> dict[str, Tensor]:
886
+ # Each feature tensor keeps its existing shape (...).
887
  moved: dict[str, Tensor] = {}
888
  for key, value in feats.items():
889
  if torch.is_tensor(value):
890
  if value.is_floating_point():
891
+ moved[key] = value.to(
892
+ device=self.device, dtype=float_dtype
893
+ ) # same shape (...)
894
  else:
895
+ moved[key] = value.to(device=self.device) # same shape (...)
896
  else:
897
+ moved[key] = value # same shape (...)
898
+ return moved # each tensor: same shape (...)
899
 
900
  def predict_structure(
901
  self,
 
929
  atoms_per_window_queries=(
930
  self.core.input_embedder.atom_encoder.atoms_per_window_queries
931
  ),
932
+ ) # feature shapes are traced in build_boltz2_features
933
+ feats = self._to_model_device(
934
+ feats, float_dtype=torch.float32
935
+ ) # unchanged feature shapes
936
  autocast_context = (
937
  torch.autocast(device_type="cuda", dtype=torch.bfloat16)
938
  if self.device.type == "cuda"
 
947
  max_parallel_samples=max_parallel_samples,
948
  run_confidence_sequentially=run_confidence_sequentially,
949
  return_dict=True,
950
+ ) # named tensors use Boltz2InferenceCore.forward shapes
951
 
952
+ sample_atom_coords_value = output.get(
953
+ "sample_atom_coords"
954
+ ) # (m, a_p, 3) or absent
955
  if not isinstance(sample_atom_coords_value, torch.Tensor):
956
  raise RuntimeError("Boltz2 structure sampling did not return coordinate tensors.")
957
+ sample_atom_coords = sample_atom_coords_value.detach().cpu() # (m, a_p, 3)
958
+ non_finite_mask = torch.logical_not(
959
+ torch.isfinite(sample_atom_coords)
960
+ ) # (m, a_p, 3)
961
  if torch.any(non_finite_mask):
962
  raise RuntimeError(
963
  "sample_atom_coords contains non-finite values. "
964
  f"Non-finite count: {int(non_finite_mask.sum().item())}"
965
  )
966
+ atom_pad_mask = feats["atom_pad_mask"][0].detach().cpu() # (a_p,)
967
+ plddt = (
968
+ output["plddt"].detach().cpu() if "plddt" in output else None
969
+ ) # (m, a_p) or None
970
  complex_plddt = (
971
  output["complex_plddt"].detach().cpu() if "complex_plddt" in output else None
972
+ ) # (m,) or None
973
+ iptm = (
974
+ output["iptm"].detach().cpu() if "iptm" in output else None
975
+ ) # (m,) or None
976
+ ptm = (
977
+ output["ptm"].detach().cpu() if "ptm" in output else None
978
+ ) # (m,) or None
979
+
980
+ confidence_score = None # (m,) or None
981
  if (complex_plddt is not None) and (iptm is not None) and (ptm is not None):
982
  if torch.allclose(iptm, torch.zeros_like(iptm)):
983
+ confidence_score = (4 * complex_plddt + ptm) / 5 # (m,)
984
  else:
985
+ confidence_score = (4 * complex_plddt + iptm) / 5 # (m,)
986
 
987
  return Boltz2StructureOutput(
988
  sample_atom_coords=sample_atom_coords,
fastplms/models/boltz/vb_layers_attention.py CHANGED
@@ -3,7 +3,6 @@
3
  from __future__ import annotations
4
 
5
  from collections.abc import Callable, MutableMapping
6
-
7
  from einops.layers.torch import Rearrange
8
  from torch import Tensor, nn
9
 
@@ -51,12 +50,13 @@ class AttentionPairBias(nn.Module):
51
  pair_states: Tensor,
52
  model_cache: MutableMapping[str, Tensor] | None,
53
  ) -> Tensor:
 
54
  if model_cache is not None and "z" in model_cache:
55
- return model_cache["z"]
56
- pair_bias = self.proj_z(pair_states)
57
  if model_cache is not None:
58
  model_cache["z"] = pair_bias
59
- return pair_bias
60
 
61
  def forward(
62
  self,
@@ -70,21 +70,26 @@ class AttentionPairBias(nn.Module):
70
  ) -> Tensor:
71
  """Transform S with shapes ``S: (b, l_q, d)`` and ``Z: (b, l, l, d_z)``."""
72
 
73
- sequence_states = self.norm_s(s) if self.initial_norm else s
 
74
  if to_keys is not None:
75
- key_states = to_keys(sequence_states)
76
- key_mask = to_keys(mask.unsqueeze(-1)).squeeze(-1)
77
  else:
78
- key_states = sequence_states if k_in is None else k_in
79
- key_mask = mask
80
 
81
- query = reshape_heads(self.proj_q(sequence_states), self.num_heads)
82
- key = reshape_heads(self.proj_k(key_states), self.num_heads)
83
- value = reshape_heads(self.proj_v(key_states), self.num_heads)
 
 
 
 
84
  pair_bias = self._resolve_pair_bias(z, model_cache).repeat_interleave(
85
  multiplicity,
86
  dim=0,
87
- )
88
  attended = pair_biased_attention(
89
  query,
90
  key,
@@ -92,7 +97,7 @@ class AttentionPairBias(nn.Module):
92
  pair_bias,
93
  key_mask,
94
  self.inf,
95
- )
96
- attended = attended.reshape(s.shape[0], -1, self.c_s)
97
- gate = self.proj_g(sequence_states).sigmoid()
98
- return self.proj_o(gate * attended)
 
3
  from __future__ import annotations
4
 
5
  from collections.abc import Callable, MutableMapping
 
6
  from einops.layers.torch import Rearrange
7
  from torch import Tensor, nn
8
 
 
50
  pair_states: Tensor,
51
  model_cache: MutableMapping[str, Tensor] | None,
52
  ) -> Tensor:
53
+ # pair_states: (b, l_q, l_k, d_z)
54
  if model_cache is not None and "z" in model_cache:
55
+ return model_cache["z"] # (b, h, l_q, l_k)
56
+ pair_bias = self.proj_z(pair_states) # (b, h, l_q, l_k)
57
  if model_cache is not None:
58
  model_cache["z"] = pair_bias
59
+ return pair_bias # (b, h, l_q, l_k)
60
 
61
  def forward(
62
  self,
 
70
  ) -> Tensor:
71
  """Transform S with shapes ``S: (b, l_q, d)`` and ``Z: (b, l, l, d_z)``."""
72
 
73
+ # s: (b, l_q, d); z: (b_z, l_q, l_k, d_z); mask: (b_k, l_k).
74
+ sequence_states = self.norm_s(s) if self.initial_norm else s # (b, l_q, d)
75
  if to_keys is not None:
76
+ key_states = to_keys(sequence_states) # (b, l_k, d)
77
+ key_mask = to_keys(mask.unsqueeze(-1)).squeeze(-1) # (b_k, l_k)
78
  else:
79
+ key_states = sequence_states if k_in is None else k_in # (b, l_k, d)
80
+ key_mask = mask # (b_k, l_k)
81
 
82
+ query = reshape_heads(
83
+ self.proj_q(sequence_states), self.num_heads
84
+ ) # (b, l_q, h, d_h)
85
+ key = reshape_heads(self.proj_k(key_states), self.num_heads) # (b, l_k, h, d_h)
86
+ value = reshape_heads(
87
+ self.proj_v(key_states), self.num_heads
88
+ ) # (b, l_k, h, d_h)
89
  pair_bias = self._resolve_pair_bias(z, model_cache).repeat_interleave(
90
  multiplicity,
91
  dim=0,
92
+ ) # (b, h, l_q, l_k)
93
  attended = pair_biased_attention(
94
  query,
95
  key,
 
97
  pair_bias,
98
  key_mask,
99
  self.inf,
100
+ ) # (b, l_q, h, d_h)
101
+ attended = attended.reshape(s.shape[0], -1, self.c_s) # (b, l_q, d)
102
+ gate = self.proj_g(sequence_states).sigmoid() # (b, l_q, d)
103
+ return self.proj_o(gate * attended) # (b, l_q, d)
fastplms/models/boltz/vb_layers_attentionv2.py CHANGED
@@ -56,10 +56,13 @@ class AttentionPairBias(nn.Module):
56
  ) -> Tensor:
57
  """Transform S with K and pair input Z, returning shape ``(b, l_q, d)``."""
58
 
59
- query = reshape_heads(self.proj_q(s), self.num_heads)
60
- key = reshape_heads(self.proj_k(k_in), self.num_heads)
61
- value = reshape_heads(self.proj_v(k_in), self.num_heads)
62
- pair_bias = self.proj_z(z).repeat_interleave(multiplicity, dim=0)
 
 
 
63
  attended = pair_biased_attention(
64
  query,
65
  key,
@@ -67,7 +70,7 @@ class AttentionPairBias(nn.Module):
67
  pair_bias,
68
  mask,
69
  self.inf,
70
- )
71
- attended = attended.reshape(s.shape[0], -1, self.c_s)
72
- gate = self.proj_g(s).sigmoid()
73
- return self.proj_o(gate * attended)
 
56
  ) -> Tensor:
57
  """Transform S with K and pair input Z, returning shape ``(b, l_q, d)``."""
58
 
59
+ # s: (b, l_q, d); k_in: (b, l_k, d); z: (b_z, l_q, l_k, d_z or h).
60
+ query = reshape_heads(self.proj_q(s), self.num_heads) # (b, l_q, h, d_h)
61
+ key = reshape_heads(self.proj_k(k_in), self.num_heads) # (b, l_k, h, d_h)
62
+ value = reshape_heads(self.proj_v(k_in), self.num_heads) # (b, l_k, h, d_h)
63
+ pair_bias = self.proj_z(z).repeat_interleave(
64
+ multiplicity, dim=0
65
+ ) # (b, h, l_q, l_k)
66
  attended = pair_biased_attention(
67
  query,
68
  key,
 
70
  pair_bias,
71
  mask,
72
  self.inf,
73
+ ) # (b, l_q, h, d_h)
74
+ attended = attended.reshape(s.shape[0], -1, self.c_s) # (b, l_q, d)
75
+ gate = self.proj_g(s).sigmoid() # (b, l_q, d)
76
+ return self.proj_o(gate * attended) # (b, l_q, d)
fastplms/models/boltz/vb_layers_confidence_utils.py CHANGED
@@ -11,12 +11,15 @@ from . import vb_const as const
11
  def compute_collinear_mask(v1: torch.Tensor, v2: torch.Tensor) -> torch.Tensor:
12
  """Mark nondegenerate vector pairs whose angle defines a stable frame."""
13
 
14
- norm1 = torch.norm(v1, dim=1, keepdim=True)
15
- norm2 = torch.norm(v2, dim=1, keepdim=True)
16
- unit1 = v1 / (norm1 + 1e-6)
17
- unit2 = v2 / (norm2 + 1e-6)
18
- separated = torch.abs(torch.sum(unit1 * unit2, dim=1)) < 0.9063
19
- return separated & (norm1.reshape(-1) > 1e-2) & (norm2.reshape(-1) > 1e-2)
 
 
 
20
 
21
 
22
  def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
@@ -24,7 +27,7 @@ def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
24
  return torch.bmm(
25
  feats["atom_to_token"].float(),
26
  feats["asym_id"].unsqueeze(-1).float(),
27
- ).squeeze(-1)
28
 
29
 
30
  def _replace_nonpolymer_frames(
@@ -36,17 +39,19 @@ def _replace_nonpolymer_frames(
36
  *,
37
  inference: bool,
38
  ) -> None:
39
- token_chain_ids = feats["asym_id"]
 
40
  for batch_index, sample_coordinates in enumerate(coordinates):
 
41
  token_offset = 0
42
  atom_offset = 0
43
  for chain_id in torch.unique(token_chain_ids[batch_index]):
44
- token_mask = (token_chain_ids[batch_index] == chain_id) * feats["token_pad_mask"][
45
- batch_index
46
- ]
47
- atom_mask = (atom_chain_ids[batch_index] == chain_id) * feats["atom_pad_mask"][
48
- batch_index
49
- ]
50
  token_count = int(token_mask.sum().item())
51
  atom_count = int(atom_mask.sum().item())
52
  is_nonpolymer = (
@@ -62,21 +67,27 @@ def _replace_nonpolymer_frames(
62
  else resolved_mask[batch_index]
63
  )
64
  )
65
- chain_atom_mask = atom_mask.bool()
66
- chain_coordinates = sample_coordinates[:, chain_atom_mask]
67
- differences = chain_coordinates[:, None, :, :] - chain_coordinates[:, :, None, :]
68
- distances = differences.square().sum(dim=-1) ** 0.5
69
- valid = chain_resolved[chain_atom_mask]
70
- invalid_pairs = 1 - (valid[None, :] * valid[:, None]).to(torch.float32)
71
- invalid_pairs[invalid_pairs == 1] = torch.inf
72
- nearest = torch.sort(distances + invalid_pairs, dim=2).indices
 
 
 
 
 
 
73
  frames = (
74
  torch.cat(
75
  (nearest[:, :, 1:2], nearest[:, :, 0:1], nearest[:, :, 2:3]),
76
  dim=2,
77
  )
78
  + atom_offset
79
- )
80
  frame_indices[
81
  batch_index,
82
  :,
@@ -102,12 +113,18 @@ def compute_frame_pred(
102
  atom's three nearest resolved neighbors.
103
  """
104
 
105
- atom_chain_ids = _atom_chain_ids(feats)
106
  expanded_batch, _, _ = pred_atom_coords.shape
107
  base_batch = expanded_batch // multiplicity
108
- coordinates = pred_atom_coords.reshape(base_batch, multiplicity, -1, 3)
109
- frame_indices = frames_idx_true.clone().repeat_interleave(multiplicity, 0)
110
- frame_indices = frame_indices.reshape(base_batch, multiplicity, -1, 3)
 
 
 
 
 
 
111
  _replace_nonpolymer_frames(
112
  coordinates,
113
  frame_indices,
@@ -117,18 +134,25 @@ def compute_frame_pred(
117
  inference=inference,
118
  )
119
 
120
- batch_indices = torch.arange(base_batch, device=frame_indices.device)[:, None, None, None]
121
- sample_indices = torch.arange(multiplicity, device=frame_indices.device)[None, :, None, None]
 
 
 
 
122
  frame_coordinates = coordinates[
123
  batch_indices,
124
  sample_indices,
125
  frame_indices,
126
- ].reshape(-1, 3, 3)
127
  valid_frames = compute_collinear_mask(
128
  frame_coordinates[:, 1] - frame_coordinates[:, 0],
129
  frame_coordinates[:, 1] - frame_coordinates[:, 2],
130
- ).reshape(base_batch, multiplicity, -1)
131
- return frame_indices, valid_frames * feats["token_pad_mask"][:, None, :]
 
 
 
132
 
133
 
134
  def compute_aggregated_metric(logits: torch.Tensor, end: float = 1.0) -> torch.Tensor:
@@ -141,25 +165,32 @@ def compute_aggregated_metric(logits: torch.Tensor, end: float = 1.0) -> torch.T
141
  end,
142
  bin_width,
143
  device=logits.device,
144
- )
145
- probabilities = nn.functional.softmax(logits, dim=-1)
 
146
  broadcast_shape = (1,) * (probabilities.ndim - 1) + centers.shape
147
- return torch.sum(probabilities * centers.view(broadcast_shape), dim=-1)
 
 
148
 
149
 
150
  def tm_function(d: torch.Tensor, n_res: torch.Tensor) -> torch.Tensor:
151
  """Evaluate the TM-score distance kernel."""
152
 
153
- distance_scale = 1.24 * (torch.clip(n_res, min=19) - 15) ** (1 / 3) - 1.8
154
- return 1 / (1 + (d / distance_scale) ** 2)
 
155
 
156
 
157
  def _maximum_masked_tm(
158
  expected_tm: torch.Tensor,
159
  pair_mask: torch.Tensor,
160
  ) -> torch.Tensor:
161
- per_anchor = torch.sum(expected_tm * pair_mask, dim=-1) / (torch.sum(pair_mask, dim=-1) + 1e-5)
162
- return torch.max(per_anchor, dim=1).values
 
 
 
163
 
164
 
165
  def compute_ptms(
@@ -176,18 +207,25 @@ def compute_ptms(
176
  ]:
177
  """Compute pTM, ipTM, interface-type, and chain-pair confidence scores."""
178
 
 
179
  _, frame_mask = compute_frame_pred(
180
  x_preds,
181
  feats["frames_idx"],
182
  feats,
183
  multiplicity,
184
  inference=True,
185
- )
186
- token_mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0)
187
- valid_anchor = frame_mask.reshape(-1, frame_mask.shape[-1])
188
- base_pair_mask = valid_anchor[:, :, None] * token_mask[:, None, :] * token_mask[:, :, None]
189
- asym_id = feats["asym_id"].repeat_interleave(multiplicity, 0)
190
- interface_mask = base_pair_mask * (asym_id[:, None, :] != asym_id[:, :, None])
 
 
 
 
 
 
191
 
192
  num_bins = logits.shape[-1]
193
  pae_centers = torch.arange(
@@ -195,23 +233,31 @@ def compute_ptms(
195
  32.0,
196
  32.0 / num_bins,
197
  device=logits.device,
198
- ).unsqueeze(0)
199
- n_res = token_mask.sum(dim=-1, keepdim=True)
200
- tm_values = tm_function(pae_centers, n_res).unsqueeze(1).unsqueeze(2)
201
- expected_tm = torch.sum(nn.functional.softmax(logits, dim=-1) * tm_values, dim=-1)
 
 
 
 
202
 
203
- ptm = _maximum_masked_tm(expected_tm, base_pair_mask)
204
- iptm = _maximum_masked_tm(expected_tm, interface_mask)
205
 
206
- token_type = feats["mol_type"].repeat_interleave(multiplicity, 0)
207
- ligand = (token_type == const.chain_type_ids["NONPOLYMER"]).float()
208
- protein = (token_type == const.chain_type_ids["PROTEIN"]).float()
209
  ligand_protein = (
210
  ligand[:, :, None] * protein[:, None, :] + protein[:, :, None] * ligand[:, None, :]
211
- )
212
- protein_protein = protein[:, :, None] * protein[:, None, :]
213
- ligand_iptm = _maximum_masked_tm(expected_tm, interface_mask * ligand_protein)
214
- protein_iptm = _maximum_masked_tm(expected_tm, interface_mask * protein_protein)
 
 
 
 
215
 
216
  chain_pair_iptm: dict[int, dict[int, torch.Tensor]] = {}
217
  for first_chain in torch.unique(asym_id).tolist():
 
11
  def compute_collinear_mask(v1: torch.Tensor, v2: torch.Tensor) -> torch.Tensor:
12
  """Mark nondegenerate vector pairs whose angle defines a stable frame."""
13
 
14
+ # v1/v2: (n, 3)
15
+ norm1 = torch.norm(v1, dim=1, keepdim=True) # (n, 1)
16
+ norm2 = torch.norm(v2, dim=1, keepdim=True) # (n, 1)
17
+ unit1 = v1 / (norm1 + 1e-6) # (n, 3)
18
+ unit2 = v2 / (norm2 + 1e-6) # (n, 3)
19
+ separated = torch.abs(torch.sum(unit1 * unit2, dim=1)) < 0.9063 # (n,)
20
+ return (
21
+ separated & (norm1.reshape(-1) > 1e-2) & (norm2.reshape(-1) > 1e-2)
22
+ ) # (n,)
23
 
24
 
25
  def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
 
27
  return torch.bmm(
28
  feats["atom_to_token"].float(),
29
  feats["asym_id"].unsqueeze(-1).float(),
30
+ ).squeeze(-1) # (b, a)
31
 
32
 
33
  def _replace_nonpolymer_frames(
 
39
  *,
40
  inference: bool,
41
  ) -> None:
42
+ # coordinates: (b, m, a, 3); frame_indices: (b, m, t_or_a, 3).
43
+ token_chain_ids = feats["asym_id"] # (b, t)
44
  for batch_index, sample_coordinates in enumerate(coordinates):
45
+ # sample_coordinates: (m, a, 3)
46
  token_offset = 0
47
  atom_offset = 0
48
  for chain_id in torch.unique(token_chain_ids[batch_index]):
49
+ token_mask = (token_chain_ids[batch_index] == chain_id) * feats[
50
+ "token_pad_mask"
51
+ ][batch_index] # (t,)
52
+ atom_mask = (atom_chain_ids[batch_index] == chain_id) * feats[
53
+ "atom_pad_mask"
54
+ ][batch_index] # (a,)
55
  token_count = int(token_mask.sum().item())
56
  atom_count = int(atom_mask.sum().item())
57
  is_nonpolymer = (
 
67
  else resolved_mask[batch_index]
68
  )
69
  )
70
+ chain_atom_mask = atom_mask.bool() # (a,)
71
+ chain_coordinates = sample_coordinates[:, chain_atom_mask] # (m, a_c, 3)
72
+ differences = (
73
+ chain_coordinates[:, None, :, :] - chain_coordinates[:, :, None, :]
74
+ ) # (m, a_c, a_c, 3)
75
+ distances = differences.square().sum(dim=-1) ** 0.5 # (m, a_c, a_c)
76
+ valid = chain_resolved[chain_atom_mask] # (a_c,)
77
+ invalid_pairs = 1 - (valid[None, :] * valid[:, None]).to(
78
+ torch.float32
79
+ ) # (a_c, a_c)
80
+ invalid_pairs[invalid_pairs == 1] = torch.inf # (a_c, a_c)
81
+ nearest = torch.sort(
82
+ distances + invalid_pairs, dim=2
83
+ ).indices # (m, a_c, a_c)
84
  frames = (
85
  torch.cat(
86
  (nearest[:, :, 1:2], nearest[:, :, 0:1], nearest[:, :, 2:3]),
87
  dim=2,
88
  )
89
  + atom_offset
90
+ ) # (m, a_c, 3)
91
  frame_indices[
92
  batch_index,
93
  :,
 
113
  atom's three nearest resolved neighbors.
114
  """
115
 
116
+ atom_chain_ids = _atom_chain_ids(feats) # (b, a)
117
  expanded_batch, _, _ = pred_atom_coords.shape
118
  base_batch = expanded_batch // multiplicity
119
+ coordinates = pred_atom_coords.reshape(
120
+ base_batch, multiplicity, -1, 3
121
+ ) # (b, m, a, 3)
122
+ frame_indices = frames_idx_true.clone().repeat_interleave(
123
+ multiplicity, 0
124
+ ) # (b * m, 1, t, 3) or (b * m, t, 3)
125
+ frame_indices = frame_indices.reshape(
126
+ base_batch, multiplicity, -1, 3
127
+ ) # (b, m, t, 3)
128
  _replace_nonpolymer_frames(
129
  coordinates,
130
  frame_indices,
 
134
  inference=inference,
135
  )
136
 
137
+ batch_indices = torch.arange(
138
+ base_batch, device=frame_indices.device
139
+ )[:, None, None, None] # (b, 1, 1, 1)
140
+ sample_indices = torch.arange(
141
+ multiplicity, device=frame_indices.device
142
+ )[None, :, None, None] # (1, m, 1, 1)
143
  frame_coordinates = coordinates[
144
  batch_indices,
145
  sample_indices,
146
  frame_indices,
147
+ ].reshape(-1, 3, 3) # (b * m * t, 3, 3)
148
  valid_frames = compute_collinear_mask(
149
  frame_coordinates[:, 1] - frame_coordinates[:, 0],
150
  frame_coordinates[:, 1] - frame_coordinates[:, 2],
151
+ ).reshape(base_batch, multiplicity, -1) # (b, m, t)
152
+ return (
153
+ frame_indices,
154
+ valid_frames * feats["token_pad_mask"][:, None, :],
155
+ ) # (b, m, t, 3), (b, m, t)
156
 
157
 
158
  def compute_aggregated_metric(logits: torch.Tensor, end: float = 1.0) -> torch.Tensor:
 
165
  end,
166
  bin_width,
167
  device=logits.device,
168
+ ) # (n_bin,)
169
+ # logits: (..., n_bin)
170
+ probabilities = nn.functional.softmax(logits, dim=-1) # (..., n_bin)
171
  broadcast_shape = (1,) * (probabilities.ndim - 1) + centers.shape
172
+ return torch.sum(
173
+ probabilities * centers.view(broadcast_shape), dim=-1
174
+ ) # (...)
175
 
176
 
177
  def tm_function(d: torch.Tensor, n_res: torch.Tensor) -> torch.Tensor:
178
  """Evaluate the TM-score distance kernel."""
179
 
180
+ # d and n_res broadcast to a shared shape (...).
181
+ distance_scale = 1.24 * (torch.clip(n_res, min=19) - 15) ** (1 / 3) - 1.8 # (...)
182
+ return 1 / (1 + (d / distance_scale) ** 2) # (...)
183
 
184
 
185
  def _maximum_masked_tm(
186
  expected_tm: torch.Tensor,
187
  pair_mask: torch.Tensor,
188
  ) -> torch.Tensor:
189
+ # expected_tm/pair_mask: (b * m, t, t).
190
+ per_anchor = torch.sum(expected_tm * pair_mask, dim=-1) / (
191
+ torch.sum(pair_mask, dim=-1) + 1e-5
192
+ ) # (b * m, t)
193
+ return torch.max(per_anchor, dim=1).values # (b * m,)
194
 
195
 
196
  def compute_ptms(
 
207
  ]:
208
  """Compute pTM, ipTM, interface-type, and chain-pair confidence scores."""
209
 
210
+ # logits: (b * m, t, t, n_bin); x_preds: (b * m, a, 3).
211
  _, frame_mask = compute_frame_pred(
212
  x_preds,
213
  feats["frames_idx"],
214
  feats,
215
  multiplicity,
216
  inference=True,
217
+ ) # frame_mask: (b, m, t)
218
+ token_mask = feats["token_pad_mask"].repeat_interleave(
219
+ multiplicity, 0
220
+ ) # (b * m, t)
221
+ valid_anchor = frame_mask.reshape(-1, frame_mask.shape[-1]) # (b * m, t)
222
+ base_pair_mask = (
223
+ valid_anchor[:, :, None] * token_mask[:, None, :] * token_mask[:, :, None]
224
+ ) # (b * m, t, t)
225
+ asym_id = feats["asym_id"].repeat_interleave(multiplicity, 0) # (b * m, t)
226
+ interface_mask = base_pair_mask * (
227
+ asym_id[:, None, :] != asym_id[:, :, None]
228
+ ) # (b * m, t, t)
229
 
230
  num_bins = logits.shape[-1]
231
  pae_centers = torch.arange(
 
233
  32.0,
234
  32.0 / num_bins,
235
  device=logits.device,
236
+ ).unsqueeze(0) # (1, n_bin)
237
+ n_res = token_mask.sum(dim=-1, keepdim=True) # (b * m, 1)
238
+ tm_values = tm_function(pae_centers, n_res).unsqueeze(1).unsqueeze(
239
+ 2
240
+ ) # (b * m, 1, 1, n_bin)
241
+ expected_tm = torch.sum(
242
+ nn.functional.softmax(logits, dim=-1) * tm_values, dim=-1
243
+ ) # (b * m, t, t)
244
 
245
+ ptm = _maximum_masked_tm(expected_tm, base_pair_mask) # (b * m,)
246
+ iptm = _maximum_masked_tm(expected_tm, interface_mask) # (b * m,)
247
 
248
+ token_type = feats["mol_type"].repeat_interleave(multiplicity, 0) # (b * m, t)
249
+ ligand = (token_type == const.chain_type_ids["NONPOLYMER"]).float() # (b * m, t)
250
+ protein = (token_type == const.chain_type_ids["PROTEIN"]).float() # (b * m, t)
251
  ligand_protein = (
252
  ligand[:, :, None] * protein[:, None, :] + protein[:, :, None] * ligand[:, None, :]
253
+ ) # (b * m, t, t)
254
+ protein_protein = protein[:, :, None] * protein[:, None, :] # (b * m, t, t)
255
+ ligand_iptm = _maximum_masked_tm(
256
+ expected_tm, interface_mask * ligand_protein
257
+ ) # (b * m,)
258
+ protein_iptm = _maximum_masked_tm(
259
+ expected_tm, interface_mask * protein_protein
260
+ ) # (b * m,)
261
 
262
  chain_pair_iptm: dict[int, dict[int, torch.Tensor]] = {}
263
  for first_chain in torch.unique(asym_id).tolist():
fastplms/models/boltz/vb_layers_dropout.py CHANGED
@@ -36,5 +36,7 @@ def get_dropout_mask(
36
 
37
  probability = float(dropout) if training else 0.0
38
  sample_shape = _broadcast_mask_shape(z, columnwise=columnwise)
39
- keep = torch.rand(sample_shape, dtype=torch.float32, device=z.device) >= probability
40
- return keep * (1.0 / (1.0 - probability))
 
 
 
36
 
37
  probability = float(dropout) if training else 0.0
38
  sample_shape = _broadcast_mask_shape(z, columnwise=columnwise)
39
+ keep = (
40
+ torch.rand(sample_shape, dtype=torch.float32, device=z.device) >= probability
41
+ ) # (b, 1, n, 1) or (b, n, 1, 1)
42
+ return keep * (1.0 / (1.0 - probability)) # same broadcast-mask shape
fastplms/models/boltz/vb_layers_initialize.py CHANGED
@@ -3,11 +3,11 @@
3
  from __future__ import annotations
4
 
5
  import math
6
- from typing import Literal
7
-
8
  import torch
 
9
  from torch import Tensor
10
 
 
11
  FanMode = Literal["fan_in", "fan_out", "fan_avg"]
12
 
13
 
@@ -33,6 +33,7 @@ def trunc_normal_init_(
33
  ) -> None:
34
  """Fill W from a normal distribution truncated at two standard deviations."""
35
 
 
36
  variance = scale / max(1.0, _calculate_fan(weights.shape, fan))
37
  std = math.sqrt(variance)
38
  with torch.no_grad():
@@ -42,6 +43,7 @@ def trunc_normal_init_(
42
  def lecun_normal_init_(weights: Tensor) -> None:
43
  """Initialize W using fan-in-scaled truncated normal values."""
44
 
 
45
  trunc_normal_init_(weights)
46
 
47
 
@@ -58,6 +60,7 @@ def glorot_uniform_init_(weights: Tensor) -> None:
58
 
59
 
60
  def _fill_(tensor: Tensor, value: float) -> None:
 
61
  with torch.no_grad():
62
  tensor.fill_(value)
63
 
@@ -96,3 +99,11 @@ def ipa_point_weights_init_(weights: Tensor) -> None:
96
  """Initialize W so applying softplus yields one."""
97
 
98
  _fill_(weights, 0.541324854612918)
 
 
 
 
 
 
 
 
 
3
  from __future__ import annotations
4
 
5
  import math
 
 
6
  import torch
7
+ from typing import Literal
8
  from torch import Tensor
9
 
10
+
11
  FanMode = Literal["fan_in", "fan_out", "fan_avg"]
12
 
13
 
 
33
  ) -> None:
34
  """Fill W from a normal distribution truncated at two standard deviations."""
35
 
36
+ # weights: (d_out, d_in); initialization preserves this shape in place.
37
  variance = scale / max(1.0, _calculate_fan(weights.shape, fan))
38
  std = math.sqrt(variance)
39
  with torch.no_grad():
 
43
  def lecun_normal_init_(weights: Tensor) -> None:
44
  """Initialize W using fan-in-scaled truncated normal values."""
45
 
46
+ # weights: (d_out, d_in), mutated in place.
47
  trunc_normal_init_(weights)
48
 
49
 
 
60
 
61
 
62
  def _fill_(tensor: Tensor, value: float) -> None:
63
+ # tensor: (...), mutated in place without changing shape.
64
  with torch.no_grad():
65
  tensor.fill_(value)
66
 
 
99
  """Initialize W so applying softplus yields one."""
100
 
101
  _fill_(weights, 0.541324854612918)
102
+ # weights: (d_out, d_in), mutated in place.
103
+ # weights: (d_out, d_in), mutated in place.
104
+ # weights: (...), mutated in place.
105
+ # weights: (...), mutated in place.
106
+ # bias: (...), mutated in place.
107
+ # bias: (...), mutated in place.
108
+ # weights: (d_out, d_in), mutated in place.
109
+ # weights: (...), mutated in place.
fastplms/models/boltz/vb_layers_outer_product_mean.py CHANGED
@@ -25,19 +25,25 @@ class OuterProductMean(nn.Module):
25
  def _pair_counts(mask: Tensor) -> Tensor:
26
  """Count valid MSA observations for each residue pair."""
27
 
 
28
  counts: Tensor | None = None
29
  for start in range(0, mask.shape[1], 64):
30
- mask_slice = mask[:, start : start + 64]
31
- contribution = (mask_slice[:, :, None, :] * mask_slice[:, :, :, None]).sum(dim=1)
32
- counts = contribution if counts is None else counts + contribution
 
 
 
 
33
  if counts is None:
34
  raise ValueError("MSA depth must be positive")
35
- return counts.clamp(min=1)
36
 
37
  @staticmethod
38
  def _outer_product(left: Tensor, right: Tensor, counts: Tensor) -> Tensor:
39
- outer = torch.einsum("bsic,bsjd->bijcd", left, right)
40
- return outer.reshape(*outer.shape[:3], -1) / counts
 
41
 
42
  def _chunked_projection(
43
  self,
@@ -47,32 +53,38 @@ class OuterProductMean(nn.Module):
47
  chunk_size: int,
48
  target: Tensor,
49
  ) -> Tensor:
 
50
  output: Tensor | None = None
51
  for start in range(0, self.c_hidden, chunk_size):
52
  stop = min(start + chunk_size, self.c_hidden)
53
- outer = self._outer_product(left[..., start:stop], right, counts)
 
 
54
  weight = self.proj_o.weight[
55
  :,
56
  start * self.c_hidden : stop * self.c_hidden,
57
- ]
58
- contribution = outer.to(target) @ weight.T
59
- output = contribution if output is None else output + contribution
 
 
60
  if output is None:
61
  raise ValueError("c_hidden must be positive")
62
- return output + self.proj_o.bias
63
 
64
  def forward(self, m: Tensor, mask: Tensor, chunk_size: int | None = None) -> Tensor:
65
  """Return pair tensor Z with shape ``(b, l, l, d_z)``."""
66
 
67
- expanded_mask = mask.unsqueeze(-1).to(m)
68
- normalized = self.norm(m)
69
- left = self.proj_a(normalized) * expanded_mask
70
- right = self.proj_b(normalized) * expanded_mask
 
71
 
72
  if chunk_size is not None and not self.training:
73
  if chunk_size <= 0:
74
  raise ValueError("chunk_size must be positive")
75
- counts = self._pair_counts(expanded_mask)
76
  return self._chunked_projection(
77
  left,
78
  right,
@@ -81,7 +93,11 @@ class OuterProductMean(nn.Module):
81
  normalized,
82
  )
83
 
84
- pair_mask = expanded_mask[:, :, None, :] * expanded_mask[:, :, :, None]
85
- counts = pair_mask.sum(dim=1).clamp(min=1)
86
- outer = self._outer_product(left.float(), right.float(), counts)
87
- return self.proj_o(outer.to(normalized))
 
 
 
 
 
25
  def _pair_counts(mask: Tensor) -> Tensor:
26
  """Count valid MSA observations for each residue pair."""
27
 
28
+ # mask: (b, s, l, 1)
29
  counts: Tensor | None = None
30
  for start in range(0, mask.shape[1], 64):
31
+ mask_slice = mask[:, start : start + 64] # (b, s_chunk, l, 1)
32
+ contribution = (
33
+ mask_slice[:, :, None, :] * mask_slice[:, :, :, None]
34
+ ).sum(dim=1) # (b, l, l, 1)
35
+ counts = (
36
+ contribution if counts is None else counts + contribution
37
+ ) # (b, l, l, 1)
38
  if counts is None:
39
  raise ValueError("MSA depth must be positive")
40
+ return counts.clamp(min=1) # (b, l, l, 1)
41
 
42
  @staticmethod
43
  def _outer_product(left: Tensor, right: Tensor, counts: Tensor) -> Tensor:
44
+ # left/right: (b, s, l, d_h); counts: (b, l, l, 1).
45
+ outer = torch.einsum("bsic,bsjd->bijcd", left, right) # (b, l, l, d_h, d_h)
46
+ return outer.reshape(*outer.shape[:3], -1) / counts # (b, l, l, d_h**2)
47
 
48
  def _chunked_projection(
49
  self,
 
53
  chunk_size: int,
54
  target: Tensor,
55
  ) -> Tensor:
56
+ # left/right: (b, s, l, d_h); counts: (b, l, l, 1).
57
  output: Tensor | None = None
58
  for start in range(0, self.c_hidden, chunk_size):
59
  stop = min(start + chunk_size, self.c_hidden)
60
+ outer = self._outer_product(
61
+ left[..., start:stop], right, counts
62
+ ) # (b, l, l, d_chunk * d_h)
63
  weight = self.proj_o.weight[
64
  :,
65
  start * self.c_hidden : stop * self.c_hidden,
66
+ ] # (d_z, d_chunk * d_h)
67
+ contribution = outer.to(target) @ weight.T # (b, l, l, d_z)
68
+ output = (
69
+ contribution if output is None else output + contribution
70
+ ) # (b, l, l, d_z)
71
  if output is None:
72
  raise ValueError("c_hidden must be positive")
73
+ return output + self.proj_o.bias # (b, l, l, d_z)
74
 
75
  def forward(self, m: Tensor, mask: Tensor, chunk_size: int | None = None) -> Tensor:
76
  """Return pair tensor Z with shape ``(b, l, l, d_z)``."""
77
 
78
+ # m: (b, s, l, d_m); mask: (b, s, l).
79
+ expanded_mask = mask.unsqueeze(-1).to(m) # (b, s, l, 1)
80
+ normalized = self.norm(m) # (b, s, l, d_m)
81
+ left = self.proj_a(normalized) * expanded_mask # (b, s, l, d_h)
82
+ right = self.proj_b(normalized) * expanded_mask # (b, s, l, d_h)
83
 
84
  if chunk_size is not None and not self.training:
85
  if chunk_size <= 0:
86
  raise ValueError("chunk_size must be positive")
87
+ counts = self._pair_counts(expanded_mask) # (b, l, l, 1)
88
  return self._chunked_projection(
89
  left,
90
  right,
 
93
  normalized,
94
  )
95
 
96
+ pair_mask = (
97
+ expanded_mask[:, :, None, :] * expanded_mask[:, :, :, None]
98
+ ) # (b, s, l, l, 1)
99
+ counts = pair_mask.sum(dim=1).clamp(min=1) # (b, l, l, 1)
100
+ outer = self._outer_product(
101
+ left.float(), right.float(), counts
102
+ ) # (b, l, l, d_h**2)
103
+ return self.proj_o(outer.to(normalized)) # (b, l, l, d_z)
fastplms/models/boltz/vb_layers_pair_averaging.py CHANGED
@@ -34,39 +34,54 @@ class PairWeightedAveraging(nn.Module):
34
  init.final_init_(self.proj_o.weight)
35
 
36
  def _attention_weights(self, pair_states: Tensor, mask: Tensor) -> Tensor:
37
- logits = self.proj_z(pair_states).permute(0, 3, 1, 2)
38
- logits = logits + (1 - mask[:, None]) * -self.inf
39
- return torch.softmax(logits, dim=-1)
 
40
 
41
  def _all_heads(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor) -> Tensor:
 
42
  values = self.proj_m(msa_states).reshape(
43
  *msa_states.shape[:3],
44
  self.num_heads,
45
  self.c_h,
46
- )
47
- values = values.permute(0, 3, 1, 2, 4)
48
- weights = self._attention_weights(pair_states, mask)
49
- gate = self.proj_g(msa_states).sigmoid()
50
- attended = torch.einsum("bhij,bhsjd->bhsid", weights, values)
51
- attended = attended.permute(0, 2, 3, 1, 4)
52
- attended = attended.reshape(*attended.shape[:3], self.num_heads * self.c_h)
53
- return self.proj_o(gate * attended)
 
 
 
 
54
 
55
  def _one_head(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor, head: int) -> Tensor:
 
56
  start = head * self.c_h
57
  stop = start + self.c_h
58
- value = msa_states @ self.proj_m.weight[start:stop].T
59
- value = value.reshape(*value.shape[:3], 1, self.c_h).permute(0, 3, 1, 2, 4)
60
- logits = pair_states @ self.proj_z.weight[head : head + 1].T
61
- logits = logits.permute(0, 3, 1, 2)
62
- weights = torch.softmax(logits + (1 - mask[:, None]) * -self.inf, dim=-1)
63
- gate = (msa_states @ self.proj_g.weight[start:stop].T).sigmoid()
64
- attended = torch.einsum("bhij,bhsjd->bhsid", weights, value)
 
 
 
 
 
 
 
 
65
  attended = attended.permute(0, 2, 3, 1, 4).reshape(
66
  *msa_states.shape[:3],
67
  self.c_h,
68
- )
69
- return (gate * attended) @ self.proj_o.weight[:, start:stop].T
70
 
71
  def forward(
72
  self,
@@ -77,15 +92,20 @@ class PairWeightedAveraging(nn.Module):
77
  ) -> Tensor:
78
  """Return updated M with shape ``(b, s, l, d_m)``."""
79
 
80
- msa_states = self.norm_m(m)
81
- pair_states = self.norm_z(z)
 
82
  if not chunk_heads or self.training:
83
  return self._all_heads(msa_states, pair_states, mask)
84
 
85
  output: Tensor | None = None
86
  for head in range(self.num_heads):
87
- contribution = self._one_head(msa_states, pair_states, mask, head)
88
- output = contribution if output is None else output + contribution
 
 
 
 
89
  if output is None:
90
  raise ValueError("num_heads must be positive")
91
- return output
 
34
  init.final_init_(self.proj_o.weight)
35
 
36
  def _attention_weights(self, pair_states: Tensor, mask: Tensor) -> Tensor:
37
+ # pair_states: (b, l, l, d_z); mask: (b, l).
38
+ logits = self.proj_z(pair_states).permute(0, 3, 1, 2) # (b, h, l, l)
39
+ logits = logits + (1 - mask[:, None]) * -self.inf # (b, h, l, l)
40
+ return torch.softmax(logits, dim=-1) # (b, h, l, l)
41
 
42
  def _all_heads(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor) -> Tensor:
43
+ # msa_states: (b, s, l, d_m); pair_states: (b, l, l, d_z).
44
  values = self.proj_m(msa_states).reshape(
45
  *msa_states.shape[:3],
46
  self.num_heads,
47
  self.c_h,
48
+ ) # (b, s, l, h, d_h)
49
+ values = values.permute(0, 3, 1, 2, 4) # (b, h, s, l, d_h)
50
+ weights = self._attention_weights(pair_states, mask) # (b, h, l, l)
51
+ gate = self.proj_g(msa_states).sigmoid() # (b, s, l, h * d_h)
52
+ attended = torch.einsum(
53
+ "bhij,bhsjd->bhsid", weights, values
54
+ ) # (b, h, s, l, d_h)
55
+ attended = attended.permute(0, 2, 3, 1, 4) # (b, s, l, h, d_h)
56
+ attended = attended.reshape(
57
+ *attended.shape[:3], self.num_heads * self.c_h
58
+ ) # (b, s, l, h * d_h)
59
+ return self.proj_o(gate * attended) # (b, s, l, d_m)
60
 
61
  def _one_head(self, msa_states: Tensor, pair_states: Tensor, mask: Tensor, head: int) -> Tensor:
62
+ # msa_states: (b, s, l, d_m); pair_states: (b, l, l, d_z).
63
  start = head * self.c_h
64
  stop = start + self.c_h
65
+ value = msa_states @ self.proj_m.weight[start:stop].T # (b, s, l, d_h)
66
+ value = value.reshape(*value.shape[:3], 1, self.c_h).permute(
67
+ 0, 3, 1, 2, 4
68
+ ) # (b, 1, s, l, d_h)
69
+ logits = pair_states @ self.proj_z.weight[head : head + 1].T # (b, l, l, 1)
70
+ logits = logits.permute(0, 3, 1, 2) # (b, 1, l, l)
71
+ weights = torch.softmax(
72
+ logits + (1 - mask[:, None]) * -self.inf, dim=-1
73
+ ) # (b, 1, l, l)
74
+ gate = (
75
+ msa_states @ self.proj_g.weight[start:stop].T
76
+ ).sigmoid() # (b, s, l, d_h)
77
+ attended = torch.einsum(
78
+ "bhij,bhsjd->bhsid", weights, value
79
+ ) # (b, 1, s, l, d_h)
80
  attended = attended.permute(0, 2, 3, 1, 4).reshape(
81
  *msa_states.shape[:3],
82
  self.c_h,
83
+ ) # (b, s, l, d_h)
84
+ return (gate * attended) @ self.proj_o.weight[:, start:stop].T # (b, s, l, d_m)
85
 
86
  def forward(
87
  self,
 
92
  ) -> Tensor:
93
  """Return updated M with shape ``(b, s, l, d_m)``."""
94
 
95
+ # m: (b, s, l, d_m); z: (b, l, l, d_z); mask: (b, l).
96
+ msa_states = self.norm_m(m) # (b, s, l, d_m)
97
+ pair_states = self.norm_z(z) # (b, l, l, d_z)
98
  if not chunk_heads or self.training:
99
  return self._all_heads(msa_states, pair_states, mask)
100
 
101
  output: Tensor | None = None
102
  for head in range(self.num_heads):
103
+ contribution = self._one_head(
104
+ msa_states, pair_states, mask, head
105
+ ) # (b, s, l, d_m)
106
+ output = (
107
+ contribution if output is None else output + contribution
108
+ ) # (b, s, l, d_m)
109
  if output is None:
110
  raise ValueError("num_heads must be positive")
111
+ return output # (b, s, l, d_m)
fastplms/models/boltz/vb_layers_pairformer.py CHANGED
@@ -2,9 +2,8 @@
2
 
3
  from __future__ import annotations
4
 
5
- from typing import Any, cast
6
-
7
  import torch
 
8
  from torch import Tensor, nn
9
  from torch.utils.checkpoint import checkpoint
10
 
@@ -34,37 +33,42 @@ def _pair_update(
34
  ) -> Tensor:
35
  """Apply multiplicative, attention, and transition updates to pair tensor Z."""
36
 
 
37
  multiplication_kernel = use_kernels or use_cuequiv_mul
38
  attention_kernel = use_kernels or use_cuequiv_attn
39
- output = pair_states
40
  for update in (modules.tri_mul_out, modules.tri_mul_in):
41
- dropout = get_dropout_mask(modules.dropout, output, modules.training)
 
 
42
  output = output + dropout * update(
43
  output,
44
  mask=pair_mask,
45
  use_kernels=multiplication_kernel,
46
- )
47
 
48
- dropout = get_dropout_mask(modules.dropout, output, modules.training)
 
 
49
  output = output + dropout * modules.tri_att_start(
50
  output,
51
  mask=pair_mask,
52
  chunk_size=chunk_size,
53
  use_kernels=attention_kernel,
54
- )
55
  dropout = get_dropout_mask(
56
  modules.dropout,
57
  output,
58
  modules.training,
59
  columnwise=True,
60
- )
61
  output = output + dropout * modules.tri_att_end(
62
  output,
63
  mask=pair_mask,
64
  chunk_size=chunk_size,
65
  use_kernels=attention_kernel,
66
- )
67
- return cast(Tensor, output + modules.transition_z(output))
68
 
69
 
70
  def _triangle_chunk_size(pair_states: Tensor, training: bool) -> int | None:
@@ -134,18 +138,22 @@ class PairformerLayer(nn.Module):
134
  use_kernels,
135
  use_cuequiv_mul,
136
  use_cuequiv_attn,
137
- )
138
  with torch.autocast("cuda", enabled=False):
139
- normalized = self.pre_norm_s(s.float())
140
  sequence_output = s.float() + self.attention(
141
  s=normalized,
142
  z=pair_output.float(),
143
  mask=mask.float(),
144
  k_in=normalized,
145
- )
146
- sequence_output = sequence_output + self.transition_s(sequence_output)
147
- sequence_output = cast(Tensor, self.s_post_norm(sequence_output))
148
- return sequence_output, pair_output
 
 
 
 
149
 
150
 
151
  class PairformerModule(nn.Module):
@@ -199,8 +207,9 @@ class PairformerModule(nn.Module):
199
  ) -> tuple[Tensor, Tensor]:
200
  """Return S and Z after every configured pairformer block."""
201
 
 
202
  chunk_size = _triangle_chunk_size(z, self.training)
203
- sequence_output, pair_output = s, z
204
  for layer in self.layers:
205
  if self.activation_checkpointing:
206
  sequence_output, pair_output = checkpoint(
@@ -212,7 +221,7 @@ class PairformerModule(nn.Module):
212
  chunk_size,
213
  use_kernels,
214
  use_reentrant=False,
215
- )
216
  else:
217
  sequence_output, pair_output = layer(
218
  sequence_output,
@@ -221,8 +230,8 @@ class PairformerModule(nn.Module):
221
  pair_mask,
222
  chunk_size,
223
  use_kernels,
224
- )
225
- return sequence_output, pair_output
226
 
227
 
228
  class PairformerNoSeqLayer(nn.Module):
@@ -320,8 +329,9 @@ class PairformerNoSeqModule(nn.Module):
320
  ) -> Tensor:
321
  """Return Z after every configured pair-only block."""
322
 
 
323
  chunk_size = _triangle_chunk_size(z, self.training)
324
- output = z
325
  for layer in self.layers:
326
  if self.activation_checkpointing:
327
  output = checkpoint(
@@ -331,7 +341,9 @@ class PairformerNoSeqModule(nn.Module):
331
  chunk_size,
332
  use_kernels,
333
  use_reentrant=False,
334
- )
335
  else:
336
- output = layer(output, pair_mask, chunk_size, use_kernels)
337
- return output
 
 
 
2
 
3
  from __future__ import annotations
4
 
 
 
5
  import torch
6
+ from typing import Any, cast
7
  from torch import Tensor, nn
8
  from torch.utils.checkpoint import checkpoint
9
 
 
33
  ) -> Tensor:
34
  """Apply multiplicative, attention, and transition updates to pair tensor Z."""
35
 
36
+ # pair_states: (b, l, l, d_z); pair_mask: (b, l, l).
37
  multiplication_kernel = use_kernels or use_cuequiv_mul
38
  attention_kernel = use_kernels or use_cuequiv_attn
39
+ output = pair_states # (b, l, l, d_z)
40
  for update in (modules.tri_mul_out, modules.tri_mul_in):
41
+ dropout = get_dropout_mask(
42
+ modules.dropout, output, modules.training
43
+ ) # broadcastable to (b, l, l, d_z)
44
  output = output + dropout * update(
45
  output,
46
  mask=pair_mask,
47
  use_kernels=multiplication_kernel,
48
+ ) # (b, l, l, d_z)
49
 
50
+ dropout = get_dropout_mask(
51
+ modules.dropout, output, modules.training
52
+ ) # broadcastable to (b, l, l, d_z)
53
  output = output + dropout * modules.tri_att_start(
54
  output,
55
  mask=pair_mask,
56
  chunk_size=chunk_size,
57
  use_kernels=attention_kernel,
58
+ ) # (b, l, l, d_z)
59
  dropout = get_dropout_mask(
60
  modules.dropout,
61
  output,
62
  modules.training,
63
  columnwise=True,
64
+ ) # broadcastable to (b, l, l, d_z)
65
  output = output + dropout * modules.tri_att_end(
66
  output,
67
  mask=pair_mask,
68
  chunk_size=chunk_size,
69
  use_kernels=attention_kernel,
70
+ ) # (b, l, l, d_z)
71
+ return cast(Tensor, output + modules.transition_z(output)) # (b, l, l, d_z)
72
 
73
 
74
  def _triangle_chunk_size(pair_states: Tensor, training: bool) -> int | None:
 
138
  use_kernels,
139
  use_cuequiv_mul,
140
  use_cuequiv_attn,
141
+ ) # (b, l, l, d_z)
142
  with torch.autocast("cuda", enabled=False):
143
+ normalized = self.pre_norm_s(s.float()) # (b, l, d_s)
144
  sequence_output = s.float() + self.attention(
145
  s=normalized,
146
  z=pair_output.float(),
147
  mask=mask.float(),
148
  k_in=normalized,
149
+ ) # (b, l, d_s)
150
+ sequence_output = (
151
+ sequence_output + self.transition_s(sequence_output)
152
+ ) # (b, l, d_s)
153
+ sequence_output = cast(
154
+ Tensor, self.s_post_norm(sequence_output)
155
+ ) # (b, l, d_s)
156
+ return sequence_output, pair_output # (b, l, d_s), (b, l, l, d_z)
157
 
158
 
159
  class PairformerModule(nn.Module):
 
207
  ) -> tuple[Tensor, Tensor]:
208
  """Return S and Z after every configured pairformer block."""
209
 
210
+ # s: (b, l, d_s); z: (b, l, l, d_z).
211
  chunk_size = _triangle_chunk_size(z, self.training)
212
+ sequence_output, pair_output = s, z # (b, l, d_s), (b, l, l, d_z)
213
  for layer in self.layers:
214
  if self.activation_checkpointing:
215
  sequence_output, pair_output = checkpoint(
 
221
  chunk_size,
222
  use_kernels,
223
  use_reentrant=False,
224
+ ) # (b, l, d_s), (b, l, l, d_z)
225
  else:
226
  sequence_output, pair_output = layer(
227
  sequence_output,
 
230
  pair_mask,
231
  chunk_size,
232
  use_kernels,
233
+ ) # (b, l, d_s), (b, l, l, d_z)
234
+ return sequence_output, pair_output # (b, l, d_s), (b, l, l, d_z)
235
 
236
 
237
  class PairformerNoSeqLayer(nn.Module):
 
329
  ) -> Tensor:
330
  """Return Z after every configured pair-only block."""
331
 
332
+ # z: (b, l, l, d_z); pair_mask: (b, l, l).
333
  chunk_size = _triangle_chunk_size(z, self.training)
334
+ output = z # (b, l, l, d_z)
335
  for layer in self.layers:
336
  if self.activation_checkpointing:
337
  output = checkpoint(
 
341
  chunk_size,
342
  use_kernels,
343
  use_reentrant=False,
344
+ ) # (b, l, l, d_z)
345
  else:
346
+ output = layer(
347
+ output, pair_mask, chunk_size, use_kernels
348
+ ) # (b, l, l, d_z)
349
+ return output # (b, l, l, d_z)
fastplms/models/boltz/vb_layers_transition.py CHANGED
@@ -40,19 +40,26 @@ class Transition(nn.Module):
40
  def _project_hidden_slice(self, normalized: Tensor, start: int, stop: int) -> Tensor:
41
  """Return one hidden-width contribution to the output projection."""
42
 
43
- gate = self.silu(torch.matmul(normalized, self.fc1.weight[start:stop].T))
44
- value = torch.matmul(normalized, self.fc2.weight[start:stop].T)
45
- return torch.matmul(gate * value, self.fc3.weight[:, start:stop].T)
 
 
 
 
 
 
 
46
 
47
  def forward(self, x: Tensor, chunk_size: int | None = None) -> Tensor:
48
  """Transform X, optionally accumulating the hidden dimension in chunks."""
49
 
50
  # X is the normalized input tensor with shape (..., d).
51
- normalized = self.norm(x)
52
  if chunk_size is None or self.training:
53
  # H is the gated hidden tensor with shape (..., d_hidden).
54
  hidden_states = self.silu(self.fc1(normalized)) * self.fc2(normalized)
55
- return self.fc3(hidden_states)
56
  if chunk_size <= 0:
57
  raise ValueError("chunk_size must be positive")
58
 
@@ -62,8 +69,10 @@ class Transition(nn.Module):
62
  normalized,
63
  start,
64
  min(start + chunk_size, self.hidden),
65
- )
66
- output = contribution if output is None else output + contribution
 
 
67
  if output is None: # hidden is normally positive; keep malformed configs explicit.
68
  raise ValueError("hidden must be positive")
69
- return output
 
40
  def _project_hidden_slice(self, normalized: Tensor, start: int, stop: int) -> Tensor:
41
  """Return one hidden-width contribution to the output projection."""
42
 
43
+ # normalized: (..., d); the selected hidden width is d_chunk.
44
+ gate = self.silu(
45
+ torch.matmul(normalized, self.fc1.weight[start:stop].T)
46
+ ) # (..., d_chunk)
47
+ value = torch.matmul(
48
+ normalized, self.fc2.weight[start:stop].T
49
+ ) # (..., d_chunk)
50
+ return torch.matmul(
51
+ gate * value, self.fc3.weight[:, start:stop].T
52
+ ) # (..., d_out)
53
 
54
  def forward(self, x: Tensor, chunk_size: int | None = None) -> Tensor:
55
  """Transform X, optionally accumulating the hidden dimension in chunks."""
56
 
57
  # X is the normalized input tensor with shape (..., d).
58
+ normalized = self.norm(x) # (..., d)
59
  if chunk_size is None or self.training:
60
  # H is the gated hidden tensor with shape (..., d_hidden).
61
  hidden_states = self.silu(self.fc1(normalized)) * self.fc2(normalized)
62
+ return self.fc3(hidden_states) # (..., d_out)
63
  if chunk_size <= 0:
64
  raise ValueError("chunk_size must be positive")
65
 
 
69
  normalized,
70
  start,
71
  min(start + chunk_size, self.hidden),
72
+ ) # (..., d_out)
73
+ output = (
74
+ contribution if output is None else output + contribution
75
+ ) # (..., d_out)
76
  if output is None: # hidden is normally positive; keep malformed configs explicit.
77
  raise ValueError("hidden must be positive")
78
+ return output # (..., d_out)
fastplms/models/boltz/vb_layers_triangular_mult.py CHANGED
@@ -3,14 +3,14 @@
3
  from __future__ import annotations
4
 
5
  import importlib
 
6
  from importlib.util import find_spec
7
  from typing import Literal
8
-
9
- import torch
10
  from torch import Tensor, nn
11
 
12
  from . import vb_layers_initialize as init
13
 
 
14
  TriangleDirection = Literal["incoming", "outgoing"]
15
 
16
 
@@ -31,6 +31,7 @@ def kernel_triangular_mult(
31
  ) -> Tensor:
32
  """Dispatch the optional cuEquivariance triangle primitive lazily."""
33
 
 
34
  if (
35
  find_spec("cuequivariance_torch") is None
36
  or find_spec("cuequivariance_ops_torch") is None
@@ -81,6 +82,7 @@ class _TriangleMultiplication(nn.Module):
81
  init.gating_init_(self.g_out.weight)
82
 
83
  def _kernel_forward(self, pair_states: Tensor, mask: Tensor) -> Tensor:
 
84
  return kernel_triangular_mult(
85
  pair_states,
86
  direction=self.direction,
@@ -94,7 +96,7 @@ class _TriangleMultiplication(nn.Module):
94
  p_out_weight=self.p_out.weight,
95
  g_out_weight=self.g_out.weight,
96
  eps=1e-5,
97
- )
98
 
99
  def forward(self, x: Tensor, mask: Tensor, use_kernels: bool = False) -> Tensor:
100
  """Transform pair tensor X with shape ``(b, l, l, d)``."""
@@ -103,12 +105,16 @@ class _TriangleMultiplication(nn.Module):
103
  return self._kernel_forward(x, mask)
104
 
105
  # X_norm is the normalized pair tensor used by the output gate.
106
- normalized = self.norm_in(x)
107
- projected = self.p_in(normalized) * self.g_in(normalized).sigmoid()
108
- projected = projected * mask.unsqueeze(-1)
109
- left, right = torch.chunk(projected.float(), 2, dim=-1)
110
- combined = torch.einsum(self.equation, left, right)
111
- return self.p_out(self.norm_out(combined)) * self.g_out(normalized).sigmoid()
 
 
 
 
112
 
113
 
114
  class TriangleMultiplicationOutgoing(_TriangleMultiplication):
 
3
  from __future__ import annotations
4
 
5
  import importlib
6
+ import torch
7
  from importlib.util import find_spec
8
  from typing import Literal
 
 
9
  from torch import Tensor, nn
10
 
11
  from . import vb_layers_initialize as init
12
 
13
+
14
  TriangleDirection = Literal["incoming", "outgoing"]
15
 
16
 
 
31
  ) -> Tensor:
32
  """Dispatch the optional cuEquivariance triangle primitive lazily."""
33
 
34
+ # x: (b, l, l, d); mask: (b, l, l); returned tensor: (b, l, l, d).
35
  if (
36
  find_spec("cuequivariance_torch") is None
37
  or find_spec("cuequivariance_ops_torch") is None
 
82
  init.gating_init_(self.g_out.weight)
83
 
84
  def _kernel_forward(self, pair_states: Tensor, mask: Tensor) -> Tensor:
85
+ # pair_states: (b, l, l, d); mask: (b, l, l).
86
  return kernel_triangular_mult(
87
  pair_states,
88
  direction=self.direction,
 
96
  p_out_weight=self.p_out.weight,
97
  g_out_weight=self.g_out.weight,
98
  eps=1e-5,
99
+ ) # (b, l, l, d)
100
 
101
  def forward(self, x: Tensor, mask: Tensor, use_kernels: bool = False) -> Tensor:
102
  """Transform pair tensor X with shape ``(b, l, l, d)``."""
 
105
  return self._kernel_forward(x, mask)
106
 
107
  # X_norm is the normalized pair tensor used by the output gate.
108
+ normalized = self.norm_in(x) # (b, l, l, d)
109
+ projected = (
110
+ self.p_in(normalized) * self.g_in(normalized).sigmoid()
111
+ ) # (b, l, l, 2 * d)
112
+ projected = projected * mask.unsqueeze(-1) # (b, l, l, 2 * d)
113
+ left, right = torch.chunk(projected.float(), 2, dim=-1) # each: (b, l, l, d)
114
+ combined = torch.einsum(self.equation, left, right) # (b, l, l, d)
115
+ return (
116
+ self.p_out(self.norm_out(combined)) * self.g_out(normalized).sigmoid()
117
+ ) # (b, l, l, d)
118
 
119
 
120
  class TriangleMultiplicationOutgoing(_TriangleMultiplication):
fastplms/models/boltz/vb_loss_diffusionv2.py CHANGED
@@ -8,7 +8,6 @@ does not import an upstream runtime package.
8
  from __future__ import annotations
9
 
10
  import warnings
11
-
12
  import torch
13
  import torch.nn.functional as functional
14
  from einops import einsum
@@ -18,10 +17,11 @@ def _weighted_centroid(
18
  coordinates: torch.Tensor,
19
  weights: torch.Tensor,
20
  ) -> torch.Tensor:
 
21
  return (coordinates * weights).sum(dim=-2, keepdim=True) / weights.sum(
22
  dim=-2,
23
  keepdim=True,
24
- )
25
 
26
 
27
  def _warn_if_alignment_is_ambiguous(
@@ -62,25 +62,25 @@ def weighted_rigid_align(
62
 
63
  output_shape = torch.broadcast_shapes(true_coords.shape, pred_coords.shape)
64
  *batch_shape, num_points, coordinate_dim = output_shape
65
- point_weights = (mask * weights).unsqueeze(-1)
66
 
67
- true_centroid = _weighted_centroid(true_coords, point_weights)
68
- pred_centroid = _weighted_centroid(pred_coords, point_weights)
69
- true_centered = true_coords - true_centroid
70
- pred_centered = pred_coords - pred_centroid
71
 
72
  covariance = einsum(
73
  point_weights * pred_centered,
74
  true_centered,
75
  "... n i, ... n j -> ... i j",
76
- )
77
  original_dtype = covariance.dtype
78
- covariance_fp32 = covariance.to(torch.float32)
79
  left_vectors, singular_values, right_vectors_h = torch.linalg.svd(
80
  covariance_fp32,
81
  driver="gesvd" if covariance_fp32.is_cuda else None,
82
- )
83
- right_vectors = right_vectors_h.mH
84
  _warn_if_alignment_is_ambiguous(
85
  mask,
86
  singular_values,
@@ -92,23 +92,25 @@ def weighted_rigid_align(
92
  "... i j, ... k j -> ... i k",
93
  left_vectors,
94
  right_vectors,
95
- ).to(torch.float32)
96
  orientation = torch.eye(
97
  coordinate_dim,
98
  dtype=covariance_fp32.dtype,
99
  device=covariance.device,
100
- )[None].repeat(*batch_shape, 1, 1)
101
- orientation[..., -1, -1] = torch.det(preliminary_rotation)
102
  rotation = einsum(
103
  left_vectors,
104
  orientation,
105
  right_vectors,
106
  "... i j, ... j k, ... l k -> ... i l",
107
- ).to(original_dtype)
108
 
109
- aligned = einsum(true_centered, rotation, "... n i, ... j i -> ... n j") + pred_centroid
 
 
110
  aligned.detach_()
111
- return aligned
112
 
113
 
114
  def _smooth_lddt_for_example(
@@ -120,33 +122,38 @@ def _smooth_lddt_for_example(
120
  nucleic_acid_cutoff: float,
121
  other_cutoff: float,
122
  ) -> torch.Tensor:
123
- true_distances = torch.cdist(true_coords, true_coords)
124
- nucleotide_rows = is_nucleotide.bool().unsqueeze(-1).expand_as(true_distances)
 
 
 
125
  pair_mask = torch.where(
126
  nucleotide_rows,
127
  true_distances < nucleic_acid_cutoff,
128
  true_distances < other_cutoff,
129
- )
130
  pair_mask &= ~torch.eye(
131
  pred_coords.shape[0],
132
  dtype=torch.bool,
133
  device=pred_coords.device,
134
- )
135
- coordinate_rows = coords_mask.bool()
136
- pair_mask &= coordinate_rows.unsqueeze(-1)
137
- pair_mask &= coordinate_rows.unsqueeze(-2)
138
-
139
- pair_indices = pair_mask.nonzero()
140
- true_pair_distances = true_distances[pair_indices[:, 0], pair_indices[:, 1]]
 
 
141
  pred_pair_distances = functional.pairwise_distance(
142
  pred_coords[pair_indices[:, 0]],
143
  pred_coords[pair_indices[:, 1]],
144
- )
145
- distance_error = torch.abs(true_pair_distances - pred_pair_distances)
146
  smooth_agreement = (
147
  sum(torch.sigmoid(threshold - distance_error) for threshold in (0.5, 1.0, 2.0, 4.0)) / 4.0
148
- )
149
- return smooth_agreement.sum() / (pair_indices.shape[0] + 1e-5)
150
 
151
 
152
  def smooth_lddt_loss(
@@ -174,5 +181,5 @@ def smooth_lddt_loss(
174
  other_cutoff=other_cutoff,
175
  )
176
  for index in range(true_coords.shape[0])
177
- ]
178
- return 1.0 - torch.stack(agreements).mean(dim=0)
 
8
  from __future__ import annotations
9
 
10
  import warnings
 
11
  import torch
12
  import torch.nn.functional as functional
13
  from einops import einsum
 
17
  coordinates: torch.Tensor,
18
  weights: torch.Tensor,
19
  ) -> torch.Tensor:
20
+ # coordinates: (..., n, 3); weights: (..., n, 1).
21
  return (coordinates * weights).sum(dim=-2, keepdim=True) / weights.sum(
22
  dim=-2,
23
  keepdim=True,
24
+ ) # (..., 1, 3)
25
 
26
 
27
  def _warn_if_alignment_is_ambiguous(
 
62
 
63
  output_shape = torch.broadcast_shapes(true_coords.shape, pred_coords.shape)
64
  *batch_shape, num_points, coordinate_dim = output_shape
65
+ point_weights = (mask * weights).unsqueeze(-1) # (..., n, 1)
66
 
67
+ true_centroid = _weighted_centroid(true_coords, point_weights) # (..., 1, 3)
68
+ pred_centroid = _weighted_centroid(pred_coords, point_weights) # (..., 1, 3)
69
+ true_centered = true_coords - true_centroid # (..., n, 3)
70
+ pred_centered = pred_coords - pred_centroid # (..., n, 3)
71
 
72
  covariance = einsum(
73
  point_weights * pred_centered,
74
  true_centered,
75
  "... n i, ... n j -> ... i j",
76
+ ) # (..., 3, 3)
77
  original_dtype = covariance.dtype
78
+ covariance_fp32 = covariance.to(torch.float32) # (..., 3, 3)
79
  left_vectors, singular_values, right_vectors_h = torch.linalg.svd(
80
  covariance_fp32,
81
  driver="gesvd" if covariance_fp32.is_cuda else None,
82
+ ) # left/right: (..., 3, 3); singular_values: (..., 3)
83
+ right_vectors = right_vectors_h.mH # (..., 3, 3)
84
  _warn_if_alignment_is_ambiguous(
85
  mask,
86
  singular_values,
 
92
  "... i j, ... k j -> ... i k",
93
  left_vectors,
94
  right_vectors,
95
+ ).to(torch.float32) # (..., 3, 3)
96
  orientation = torch.eye(
97
  coordinate_dim,
98
  dtype=covariance_fp32.dtype,
99
  device=covariance.device,
100
+ )[None].repeat(*batch_shape, 1, 1) # (..., 3, 3)
101
+ orientation[..., -1, -1] = torch.det(preliminary_rotation) # (...)
102
  rotation = einsum(
103
  left_vectors,
104
  orientation,
105
  right_vectors,
106
  "... i j, ... j k, ... l k -> ... i l",
107
+ ).to(original_dtype) # (..., 3, 3)
108
 
109
+ aligned = (
110
+ einsum(true_centered, rotation, "... n i, ... j i -> ... n j") + pred_centroid
111
+ ) # (..., n, 3)
112
  aligned.detach_()
113
+ return aligned # (..., n, 3)
114
 
115
 
116
  def _smooth_lddt_for_example(
 
122
  nucleic_acid_cutoff: float,
123
  other_cutoff: float,
124
  ) -> torch.Tensor:
125
+ # pred_coords/true_coords: (n, 3); is_nucleotide/coords_mask: (n,).
126
+ true_distances = torch.cdist(true_coords, true_coords) # (n, n)
127
+ nucleotide_rows = is_nucleotide.bool().unsqueeze(-1).expand_as(
128
+ true_distances
129
+ ) # (n, n)
130
  pair_mask = torch.where(
131
  nucleotide_rows,
132
  true_distances < nucleic_acid_cutoff,
133
  true_distances < other_cutoff,
134
+ ) # (n, n)
135
  pair_mask &= ~torch.eye(
136
  pred_coords.shape[0],
137
  dtype=torch.bool,
138
  device=pred_coords.device,
139
+ ) # (n, n)
140
+ coordinate_rows = coords_mask.bool() # (n,)
141
+ pair_mask &= coordinate_rows.unsqueeze(-1) # (n, n)
142
+ pair_mask &= coordinate_rows.unsqueeze(-2) # (n, n)
143
+
144
+ pair_indices = pair_mask.nonzero() # (n_pair, 2)
145
+ true_pair_distances = true_distances[
146
+ pair_indices[:, 0], pair_indices[:, 1]
147
+ ] # (n_pair,)
148
  pred_pair_distances = functional.pairwise_distance(
149
  pred_coords[pair_indices[:, 0]],
150
  pred_coords[pair_indices[:, 1]],
151
+ ) # (n_pair,)
152
+ distance_error = torch.abs(true_pair_distances - pred_pair_distances) # (n_pair,)
153
  smooth_agreement = (
154
  sum(torch.sigmoid(threshold - distance_error) for threshold in (0.5, 1.0, 2.0, 4.0)) / 4.0
155
+ ) # (n_pair,)
156
+ return smooth_agreement.sum() / (pair_indices.shape[0] + 1e-5) # ()
157
 
158
 
159
  def smooth_lddt_loss(
 
181
  other_cutoff=other_cutoff,
182
  )
183
  for index in range(true_coords.shape[0])
184
+ ] # each: ()
185
+ return 1.0 - torch.stack(agreements).mean(dim=0) # ()
fastplms/models/boltz/vb_modules_diffusion_conditioning.py CHANGED
@@ -30,7 +30,10 @@ def _concatenate_biases(
30
  projections: nn.ModuleList,
31
  pair_features: torch.Tensor,
32
  ) -> torch.Tensor:
33
- return torch.cat([projection(pair_features) for projection in projections], dim=-1)
 
 
 
34
 
35
 
36
  class DiffusionConditioning(nn.Module):
@@ -112,21 +115,26 @@ class DiffusionConditioning(nn.Module):
112
  order so downstream code can select them by layer.
113
  """
114
 
 
115
  z_conditioned = self.pairwise_conditioner(
116
  z_trunk,
117
  relative_position_encoding,
118
- )
119
  q, c, p, to_keys = self.atom_encoder(
120
  feats=feats,
121
  s_trunk=s_trunk,
122
  z=z_conditioned,
123
- )
124
- atom_encoder_bias = _concatenate_biases(self.atom_enc_proj_z, p)
125
- atom_decoder_bias = _concatenate_biases(self.atom_dec_proj_z, p)
 
 
 
 
126
  token_transformer_bias = _concatenate_biases(
127
  self.token_trans_proj_z,
128
  z_conditioned,
129
- )
130
  return (
131
  q,
132
  c,
@@ -134,4 +142,4 @@ class DiffusionConditioning(nn.Module):
134
  atom_encoder_bias,
135
  atom_decoder_bias,
136
  token_transformer_bias,
137
- )
 
30
  projections: nn.ModuleList,
31
  pair_features: torch.Tensor,
32
  ) -> torch.Tensor:
33
+ # pair_features: (..., d_pair); each projection: (..., h).
34
+ return torch.cat(
35
+ [projection(pair_features) for projection in projections], dim=-1
36
+ ) # (..., depth * h)
37
 
38
 
39
  class DiffusionConditioning(nn.Module):
 
115
  order so downstream code can select them by layer.
116
  """
117
 
118
+ # b is batch size, t token count, a atom count, and k the atom-window count.
119
  z_conditioned = self.pairwise_conditioner(
120
  z_trunk,
121
  relative_position_encoding,
122
+ ) # (b, t, t, d_z)
123
  q, c, p, to_keys = self.atom_encoder(
124
  feats=feats,
125
  s_trunk=s_trunk,
126
  z=z_conditioned,
127
+ ) # q/c: (b, a, d_a); p: (b, k, w, h_k, d_p); to_keys: callable
128
+ atom_encoder_bias = _concatenate_biases(
129
+ self.atom_enc_proj_z, p
130
+ ) # (b, k, w, h_k, depth_enc * heads_enc)
131
+ atom_decoder_bias = _concatenate_biases(
132
+ self.atom_dec_proj_z, p
133
+ ) # (b, k, w, h_k, depth_dec * heads_dec)
134
  token_transformer_bias = _concatenate_biases(
135
  self.token_trans_proj_z,
136
  z_conditioned,
137
+ ) # (b, t, t, depth_token * heads_token)
138
  return (
139
  q,
140
  c,
 
142
  atom_encoder_bias,
143
  atom_decoder_bias,
144
  token_transformer_bias,
145
+ ) # tensor shapes are traced above; to_keys is the atom-key gatherer
fastplms/models/boltz/vb_modules_encodersv2.py CHANGED
@@ -11,11 +11,10 @@ implementations under MIT terms. See ``THIRD_PARTY_NOTICES.md``.
11
 
12
  from __future__ import annotations
13
 
 
14
  from collections.abc import Callable
15
  from functools import partial
16
  from math import pi
17
-
18
- import torch
19
  from torch import nn
20
  from torch.nn.functional import one_hot
21
 
@@ -46,12 +45,13 @@ class FourierEmbedding(nn.Module):
46
  def forward(self, times: torch.Tensor) -> torch.Tensor:
47
  """Map times with shape ``(b,)`` to an embedding ``H: (b, d)``."""
48
 
49
- random_phase = self.proj(times.reshape(-1, 1))
50
- return torch.cos(2 * pi * random_phase)
51
 
52
 
53
  def _pairwise_difference(values: torch.Tensor) -> torch.Tensor:
54
- return values[:, :, None] - values[:, None, :]
 
55
 
56
 
57
  class RelativePositionEncoder(nn.Module):
@@ -76,54 +76,68 @@ class RelativePositionEncoder(nn.Module):
76
  def forward(self, feats: dict[str, torch.Tensor]) -> torch.Tensor:
77
  """Return relative pair features ``Z: (b, n, n, token_z)``."""
78
 
79
- same_chain = feats["asym_id"][:, :, None] == feats["asym_id"][:, None, :]
80
- same_residue = feats["residue_index"][:, :, None] == feats["residue_index"][:, None, :]
81
- same_entity = feats["entity_id"][:, :, None] == feats["entity_id"][:, None, :]
82
-
83
- residue_offset = _pairwise_difference(feats["residue_index"])
 
 
 
 
 
 
84
  if self.cyclic_pos_enc and torch.any(feats["cyclic_period"] > 0):
85
  period = torch.where(
86
  feats["cyclic_period"] > 0,
87
  feats["cyclic_period"],
88
  torch.zeros_like(feats["cyclic_period"]) + 10000,
89
- )
90
- residue_offset = (residue_offset - period * torch.round(residue_offset / period)).long()
 
 
91
  residue_offset = torch.clip(
92
  residue_offset + self.r_max,
93
  0,
94
  2 * self.r_max,
95
- )
96
  residue_offset = torch.where(
97
  same_chain,
98
  residue_offset,
99
  torch.zeros_like(residue_offset) + 2 * self.r_max + 1,
100
- )
101
- residue_features = one_hot(residue_offset, 2 * self.r_max + 2)
 
 
102
 
103
  token_offset = torch.clip(
104
  _pairwise_difference(feats["token_index"]) + self.r_max,
105
  0,
106
  2 * self.r_max,
107
- )
108
  token_offset = torch.where(
109
  same_chain & same_residue,
110
  token_offset,
111
  torch.zeros_like(token_offset) + 2 * self.r_max + 1,
112
- )
113
- token_features = one_hot(token_offset, 2 * self.r_max + 2)
 
 
114
 
115
  symmetry_offset = torch.clip(
116
  _pairwise_difference(feats["sym_id"]) + self.s_max,
117
  0,
118
  2 * self.s_max,
119
- )
120
- invalid_symmetry = ~same_entity if self.fix_sym_check else same_chain
121
  symmetry_offset = torch.where(
122
  invalid_symmetry,
123
  torch.zeros_like(symmetry_offset) + 2 * self.s_max + 1,
124
  symmetry_offset,
125
- )
126
- symmetry_features = one_hot(symmetry_offset, 2 * self.s_max + 2)
 
 
127
 
128
  pair_features = torch.cat(
129
  (
@@ -133,8 +147,8 @@ class RelativePositionEncoder(nn.Module):
133
  symmetry_features.float(),
134
  ),
135
  dim=-1,
136
- )
137
- return self.linear_layer(pair_features)
138
 
139
 
140
  class SingleConditioning(nn.Module):
@@ -175,17 +189,20 @@ class SingleConditioning(nn.Module):
175
  ) -> tuple[torch.Tensor, torch.Tensor | None]:
176
  """Return conditioned token features ``S: (b, n, 2 * token_s)``."""
177
 
178
- single = torch.cat((s_trunk, s_inputs), dim=-1)
179
- single = self.single_embed(self.norm_single(single))
180
- normalized_fourier = None
 
181
  if not self.disable_times:
182
- fourier = self.fourier_embed(times)
183
- normalized_fourier = self.norm_fourier(fourier)
184
- time_condition = self.fourier_to_single(normalized_fourier)
185
- single = time_condition[:, None, :] + single
 
 
186
  for transition in self.transitions:
187
- single = transition(single) + single
188
- return single, normalized_fourier
189
 
190
 
191
  class PairwiseConditioning(nn.Module):
@@ -217,10 +234,12 @@ class PairwiseConditioning(nn.Module):
217
  ) -> torch.Tensor:
218
  """Return conditioned pair features ``Z: (b, n, n, token_z)``."""
219
 
220
- pair = self.dim_pairwise_init_proj(torch.cat((z_trunk, token_rel_pos_feats), dim=-1))
 
 
221
  for transition in self.transitions:
222
- pair = transition(pair) + pair
223
- return pair
224
 
225
 
226
  def get_indexing_matrix(
@@ -247,13 +266,17 @@ def get_indexing_matrix(
247
  f"h must contain an even number of half-window key blocks; received {key_blocks}."
248
  )
249
 
250
- positions = torch.arange(2 * k, device=device)
251
- relative_blocks = ((positions.unsqueeze(0) - positions.unsqueeze(1)) + key_blocks // 2).clamp(
252
- min=0, max=key_blocks + 1
253
- )
254
- relative_blocks = relative_blocks.view(k, 2, 2 * k)[:, 0, :]
255
- selectors = one_hot(relative_blocks, num_classes=key_blocks + 2)[..., 1:-1].transpose(1, 0)
256
- return selectors.reshape(2 * k, key_blocks * k).float()
 
 
 
 
257
 
258
 
259
  def single_to_keys(
@@ -266,9 +289,11 @@ def single_to_keys(
266
 
267
  b, n, d = single.shape
268
  k = n // w
269
- half_windows = single.view(b, 2 * k, w // 2, d)
270
- gathered = torch.einsum("b j i d, j k -> b k i d", half_windows, indexing_matrix)
271
- return gathered.reshape(b, k, h, d)
 
 
272
 
273
 
274
  class AtomEncoder(nn.Module):
@@ -332,11 +357,12 @@ class AtomEncoder(nn.Module):
332
  b: int,
333
  n: int,
334
  ) -> torch.Tensor:
 
335
  feature_parts = [
336
  feats["ref_pos"],
337
  feats["ref_charge"].unsqueeze(-1),
338
  feats["ref_element"],
339
- ]
340
  if not self.use_no_atom_char:
341
  feature_parts.append(feats["ref_atom_name_chars"].reshape(b, n, 4 * 64))
342
  if self.use_atom_backbone_feat:
@@ -349,9 +375,11 @@ class AtomEncoder(nn.Module):
349
  one_hot(feats["mol_type"], num_classes=4).float(),
350
  ),
351
  dim=-1,
352
- )
353
- feature_parts.append(torch.bmm(feats["atom_to_token"].float(), residue_features))
354
- return torch.cat(feature_parts, dim=-1)
 
 
355
 
356
  def forward(
357
  self,
@@ -368,61 +396,81 @@ class AtomEncoder(nn.Module):
368
 
369
  with torch.autocast("cuda", enabled=False):
370
  b, n, _ = feats["ref_pos"].shape
371
- atom_mask = feats["atom_pad_mask"].bool()
372
- atom_positions = feats["ref_pos"]
373
- atom_space = feats["ref_space_uid"]
374
- atom_features = self._assemble_atom_features(feats, b, n)
375
- conditioned_single = self.embed_atom_features(atom_features)
 
 
 
 
376
 
377
  w = self.atoms_per_window_queries
378
  h = self.atoms_per_window_keys
379
  b, n = conditioned_single.shape[:2]
380
  k = n // w
381
- indexing = get_indexing_matrix(k, w, h, conditioned_single.device)
 
 
382
  to_keys = partial(single_to_keys, indexing_matrix=indexing, w=w, h=h)
383
 
384
- position_queries = atom_positions.view(b, k, w, 1, 3)
385
- position_keys = to_keys(atom_positions).view(b, k, 1, h, 3)
386
- displacement = position_keys - position_queries
 
 
387
  inverse_squared_distance = 1 / (
388
  1 + torch.sum(displacement * displacement, dim=-1, keepdim=True)
389
- )
390
-
391
- mask_queries = atom_mask.view(b, k, w, 1)
392
- mask_keys = to_keys(atom_mask.unsqueeze(-1).float()).view(b, k, 1, h).bool()
393
- space_queries = atom_space.view(b, k, w, 1)
394
- space_keys = to_keys(atom_space.unsqueeze(-1).float()).view(b, k, 1, h).long()
 
 
 
 
395
  valid_pair = (
396
  (mask_queries & mask_keys & (space_queries == space_keys)).float().unsqueeze(-1)
397
- )
398
 
399
- pair = self.embed_atompair_ref_pos(displacement) * valid_pair
400
- pair = pair + self.embed_atompair_ref_dist(inverse_squared_distance) * valid_pair
401
- pair = pair + self.embed_atompair_mask(valid_pair) * valid_pair
402
- query = conditioned_single
 
 
403
 
404
  if self.structure_prediction:
405
  if s_trunk is None or z is None:
406
  raise ValueError("structure prediction requires S and Z trunk tensors")
407
- atom_to_token = feats["atom_to_token"].float()
408
- single_update = self.s_to_c_trans(s_trunk.float())
409
- single_update = torch.bmm(atom_to_token, single_update)
410
- conditioned_single = conditioned_single + single_update.to(conditioned_single)
411
-
412
- token_queries = atom_to_token.view(b, k, w, atom_to_token.shape[-1])
413
- token_keys = to_keys(atom_to_token)
414
- pair_update = self.z_to_p_trans(z.float())
 
 
 
 
 
 
415
  pair_update = torch.einsum(
416
  "bijd,bwki,bwlj->bwkld",
417
  pair_update,
418
  token_queries,
419
  token_keys,
420
- )
421
- pair = pair + pair_update.to(pair)
422
 
423
  pair = pair + self.c_to_p_trans_q(
424
  conditioned_single.view(b, k, w, 1, conditioned_single.shape[-1])
425
- )
426
  pair = pair + self.c_to_p_trans_k(
427
  to_keys(conditioned_single).view(
428
  b,
@@ -431,9 +479,14 @@ class AtomEncoder(nn.Module):
431
  h,
432
  conditioned_single.shape[-1],
433
  )
434
- )
435
- pair = pair + self.p_mlp(pair)
436
- return query, conditioned_single, pair, to_keys
 
 
 
 
 
437
 
438
 
439
  class AtomAttentionEncoder(nn.Module):
@@ -484,14 +537,15 @@ class AtomAttentionEncoder(nn.Module):
484
  ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, Callable]:
485
  b, n, _ = feats["ref_pos"].shape
486
  del b, n
487
- atom_mask = feats["atom_pad_mask"].bool()
 
488
  if self.structure_prediction:
489
  if r is None:
490
  raise ValueError("structure prediction requires atom coordinates R")
491
- q = q.repeat_interleave(multiplicity, 0)
492
- q = q + self.r_to_q_trans(r)
493
- c = c.repeat_interleave(multiplicity, 0)
494
- atom_mask = atom_mask.repeat_interleave(multiplicity, 0)
495
  q = self.atom_encoder(
496
  q=q,
497
  mask=atom_mask,
@@ -499,15 +553,26 @@ class AtomAttentionEncoder(nn.Module):
499
  bias=atom_enc_bias,
500
  multiplicity=multiplicity,
501
  to_keys=to_keys,
502
- )
503
 
504
  with torch.autocast("cuda", enabled=False):
505
- atom_update = self.atom_to_token_trans(q).float()
506
- atom_to_token = feats["atom_to_token"].float()
507
- atom_to_token = atom_to_token.repeat_interleave(multiplicity, 0)
508
- atom_to_token_mean = atom_to_token / (atom_to_token.sum(dim=1, keepdim=True) + 1e-6)
509
- token_update = torch.bmm(atom_to_token_mean.transpose(1, 2), atom_update)
510
- return token_update.to(q), q, c, to_keys
 
 
 
 
 
 
 
 
 
 
 
511
 
512
 
513
  class AtomAttentionDecoder(nn.Module):
@@ -560,12 +625,17 @@ class AtomAttentionDecoder(nn.Module):
560
  """Return atom-coordinate updates ``R_update: (b, a, 3)``."""
561
 
562
  with torch.autocast("cuda", enabled=False):
563
- atom_to_token = feats["atom_to_token"].float()
564
- atom_to_token = atom_to_token.repeat_interleave(multiplicity, 0)
565
- token_update = self.a_to_q_trans(a.float())
566
- atom_update = torch.bmm(atom_to_token, token_update)
567
- q = q + atom_update.to(q)
568
- atom_mask = feats["atom_pad_mask"].repeat_interleave(multiplicity, 0)
 
 
 
 
 
569
  q = self.atom_decoder(
570
  q=q,
571
  mask=atom_mask,
@@ -573,5 +643,5 @@ class AtomAttentionDecoder(nn.Module):
573
  bias=atom_dec_bias,
574
  multiplicity=multiplicity,
575
  to_keys=to_keys,
576
- )
577
- return self.atom_feat_to_atom_pos_update(q)
 
11
 
12
  from __future__ import annotations
13
 
14
+ import torch
15
  from collections.abc import Callable
16
  from functools import partial
17
  from math import pi
 
 
18
  from torch import nn
19
  from torch.nn.functional import one_hot
20
 
 
45
  def forward(self, times: torch.Tensor) -> torch.Tensor:
46
  """Map times with shape ``(b,)`` to an embedding ``H: (b, d)``."""
47
 
48
+ random_phase = self.proj(times.reshape(-1, 1)) # (b, d)
49
+ return torch.cos(2 * pi * random_phase) # (b, d)
50
 
51
 
52
  def _pairwise_difference(values: torch.Tensor) -> torch.Tensor:
53
+ # values: (b, n)
54
+ return values[:, :, None] - values[:, None, :] # (b, n, n)
55
 
56
 
57
  class RelativePositionEncoder(nn.Module):
 
76
  def forward(self, feats: dict[str, torch.Tensor]) -> torch.Tensor:
77
  """Return relative pair features ``Z: (b, n, n, token_z)``."""
78
 
79
+ same_chain = (
80
+ feats["asym_id"][:, :, None] == feats["asym_id"][:, None, :]
81
+ ) # (b, n, n)
82
+ same_residue = (
83
+ feats["residue_index"][:, :, None] == feats["residue_index"][:, None, :]
84
+ ) # (b, n, n)
85
+ same_entity = (
86
+ feats["entity_id"][:, :, None] == feats["entity_id"][:, None, :]
87
+ ) # (b, n, n)
88
+
89
+ residue_offset = _pairwise_difference(feats["residue_index"]) # (b, n, n)
90
  if self.cyclic_pos_enc and torch.any(feats["cyclic_period"] > 0):
91
  period = torch.where(
92
  feats["cyclic_period"] > 0,
93
  feats["cyclic_period"],
94
  torch.zeros_like(feats["cyclic_period"]) + 10000,
95
+ ) # (b, n)
96
+ residue_offset = (
97
+ residue_offset - period * torch.round(residue_offset / period)
98
+ ).long() # (b, n, n)
99
  residue_offset = torch.clip(
100
  residue_offset + self.r_max,
101
  0,
102
  2 * self.r_max,
103
+ ) # (b, n, n)
104
  residue_offset = torch.where(
105
  same_chain,
106
  residue_offset,
107
  torch.zeros_like(residue_offset) + 2 * self.r_max + 1,
108
+ ) # (b, n, n)
109
+ residue_features = one_hot(
110
+ residue_offset, 2 * self.r_max + 2
111
+ ) # (b, n, n, 2 * r_max + 2)
112
 
113
  token_offset = torch.clip(
114
  _pairwise_difference(feats["token_index"]) + self.r_max,
115
  0,
116
  2 * self.r_max,
117
+ ) # (b, n, n)
118
  token_offset = torch.where(
119
  same_chain & same_residue,
120
  token_offset,
121
  torch.zeros_like(token_offset) + 2 * self.r_max + 1,
122
+ ) # (b, n, n)
123
+ token_features = one_hot(
124
+ token_offset, 2 * self.r_max + 2
125
+ ) # (b, n, n, 2 * r_max + 2)
126
 
127
  symmetry_offset = torch.clip(
128
  _pairwise_difference(feats["sym_id"]) + self.s_max,
129
  0,
130
  2 * self.s_max,
131
+ ) # (b, n, n)
132
+ invalid_symmetry = ~same_entity if self.fix_sym_check else same_chain # (b, n, n)
133
  symmetry_offset = torch.where(
134
  invalid_symmetry,
135
  torch.zeros_like(symmetry_offset) + 2 * self.s_max + 1,
136
  symmetry_offset,
137
+ ) # (b, n, n)
138
+ symmetry_features = one_hot(
139
+ symmetry_offset, 2 * self.s_max + 2
140
+ ) # (b, n, n, 2 * s_max + 2)
141
 
142
  pair_features = torch.cat(
143
  (
 
147
  symmetry_features.float(),
148
  ),
149
  dim=-1,
150
+ ) # (b, n, n, d_rel)
151
+ return self.linear_layer(pair_features) # (b, n, n, token_z)
152
 
153
 
154
  class SingleConditioning(nn.Module):
 
189
  ) -> tuple[torch.Tensor, torch.Tensor | None]:
190
  """Return conditioned token features ``S: (b, n, 2 * token_s)``."""
191
 
192
+ # times: (b,); s_trunk/s_inputs: (b, n, token_s).
193
+ single = torch.cat((s_trunk, s_inputs), dim=-1) # (b, n, 2 * token_s)
194
+ single = self.single_embed(self.norm_single(single)) # (b, n, 2 * token_s)
195
+ normalized_fourier = None # (b, d_fourier) or None
196
  if not self.disable_times:
197
+ fourier = self.fourier_embed(times) # (b, d_fourier)
198
+ normalized_fourier = self.norm_fourier(fourier) # (b, d_fourier)
199
+ time_condition = self.fourier_to_single(
200
+ normalized_fourier
201
+ ) # (b, 2 * token_s)
202
+ single = time_condition[:, None, :] + single # (b, n, 2 * token_s)
203
  for transition in self.transitions:
204
+ single = transition(single) + single # (b, n, 2 * token_s)
205
+ return single, normalized_fourier # (b, n, 2 * token_s), (b, d_fourier) or None
206
 
207
 
208
  class PairwiseConditioning(nn.Module):
 
234
  ) -> torch.Tensor:
235
  """Return conditioned pair features ``Z: (b, n, n, token_z)``."""
236
 
237
+ pair = self.dim_pairwise_init_proj(
238
+ torch.cat((z_trunk, token_rel_pos_feats), dim=-1)
239
+ ) # (b, n, n, token_z)
240
  for transition in self.transitions:
241
+ pair = transition(pair) + pair # (b, n, n, token_z)
242
+ return pair # (b, n, n, token_z)
243
 
244
 
245
  def get_indexing_matrix(
 
266
  f"h must contain an even number of half-window key blocks; received {key_blocks}."
267
  )
268
 
269
+ positions = torch.arange(2 * k, device=device) # (2 * k,)
270
+ relative_blocks = (
271
+ (positions.unsqueeze(0) - positions.unsqueeze(1)) + key_blocks // 2
272
+ ).clamp(min=0, max=key_blocks + 1) # (2 * k, 2 * k)
273
+ relative_blocks = relative_blocks.view(k, 2, 2 * k)[:, 0, :] # (k, 2 * k)
274
+ selectors = one_hot(relative_blocks, num_classes=key_blocks + 2)[
275
+ ..., 1:-1
276
+ ].transpose(1, 0) # (2 * k, k, key_blocks)
277
+ return selectors.reshape(
278
+ 2 * k, key_blocks * k
279
+ ).float() # (2 * k, key_blocks * k)
280
 
281
 
282
  def single_to_keys(
 
289
 
290
  b, n, d = single.shape
291
  k = n // w
292
+ half_windows = single.view(b, 2 * k, w // 2, d) # (b, 2 * k, w / 2, d)
293
+ gathered = torch.einsum(
294
+ "b j i d, j k -> b k i d", half_windows, indexing_matrix
295
+ ) # (b, key_blocks * k, w / 2, d)
296
+ return gathered.reshape(b, k, h, d) # (b, k, h, d)
297
 
298
 
299
  class AtomEncoder(nn.Module):
 
357
  b: int,
358
  n: int,
359
  ) -> torch.Tensor:
360
+ # Feature tensors share leading shape (b, n).
361
  feature_parts = [
362
  feats["ref_pos"],
363
  feats["ref_charge"].unsqueeze(-1),
364
  feats["ref_element"],
365
+ ] # each: (b, n, d_feature)
366
  if not self.use_no_atom_char:
367
  feature_parts.append(feats["ref_atom_name_chars"].reshape(b, n, 4 * 64))
368
  if self.use_atom_backbone_feat:
 
375
  one_hot(feats["mol_type"], num_classes=4).float(),
376
  ),
377
  dim=-1,
378
+ ) # (b, t, d_residue)
379
+ feature_parts.append(
380
+ torch.bmm(feats["atom_to_token"].float(), residue_features)
381
+ ) # (b, n, d_residue)
382
+ return torch.cat(feature_parts, dim=-1) # (b, n, atom_feature_dim)
383
 
384
  def forward(
385
  self,
 
396
 
397
  with torch.autocast("cuda", enabled=False):
398
  b, n, _ = feats["ref_pos"].shape
399
+ atom_mask = feats["atom_pad_mask"].bool() # (b, n)
400
+ atom_positions = feats["ref_pos"] # (b, n, 3)
401
+ atom_space = feats["ref_space_uid"] # (b, n)
402
+ atom_features = self._assemble_atom_features(
403
+ feats, b, n
404
+ ) # (b, n, atom_feature_dim)
405
+ conditioned_single = self.embed_atom_features(
406
+ atom_features
407
+ ) # (b, n, atom_s)
408
 
409
  w = self.atoms_per_window_queries
410
  h = self.atoms_per_window_keys
411
  b, n = conditioned_single.shape[:2]
412
  k = n // w
413
+ indexing = get_indexing_matrix(
414
+ k, w, h, conditioned_single.device
415
+ ) # (2 * k, key_blocks * k)
416
  to_keys = partial(single_to_keys, indexing_matrix=indexing, w=w, h=h)
417
 
418
+ position_queries = atom_positions.view(b, k, w, 1, 3) # (b, k, w, 1, 3)
419
+ position_keys = to_keys(atom_positions).view(
420
+ b, k, 1, h, 3
421
+ ) # (b, k, 1, h, 3)
422
+ displacement = position_keys - position_queries # (b, k, w, h, 3)
423
  inverse_squared_distance = 1 / (
424
  1 + torch.sum(displacement * displacement, dim=-1, keepdim=True)
425
+ ) # (b, k, w, h, 1)
426
+
427
+ mask_queries = atom_mask.view(b, k, w, 1) # (b, k, w, 1)
428
+ mask_keys = (
429
+ to_keys(atom_mask.unsqueeze(-1).float()).view(b, k, 1, h).bool()
430
+ ) # (b, k, 1, h)
431
+ space_queries = atom_space.view(b, k, w, 1) # (b, k, w, 1)
432
+ space_keys = (
433
+ to_keys(atom_space.unsqueeze(-1).float()).view(b, k, 1, h).long()
434
+ ) # (b, k, 1, h)
435
  valid_pair = (
436
  (mask_queries & mask_keys & (space_queries == space_keys)).float().unsqueeze(-1)
437
+ ) # (b, k, w, h, 1)
438
 
439
+ pair = self.embed_atompair_ref_pos(displacement) * valid_pair # (b, k, w, h, atom_z)
440
+ pair = (
441
+ pair + self.embed_atompair_ref_dist(inverse_squared_distance) * valid_pair
442
+ ) # (b, k, w, h, atom_z)
443
+ pair = pair + self.embed_atompair_mask(valid_pair) * valid_pair # (b, k, w, h, atom_z)
444
+ query = conditioned_single # (b, n, atom_s)
445
 
446
  if self.structure_prediction:
447
  if s_trunk is None or z is None:
448
  raise ValueError("structure prediction requires S and Z trunk tensors")
449
+ atom_to_token = feats["atom_to_token"].float() # (b, n, t)
450
+ single_update = self.s_to_c_trans(s_trunk.float()) # (b, t, atom_s)
451
+ single_update = torch.bmm(
452
+ atom_to_token, single_update
453
+ ) # (b, n, atom_s)
454
+ conditioned_single = conditioned_single + single_update.to(
455
+ conditioned_single
456
+ ) # (b, n, atom_s)
457
+
458
+ token_queries = atom_to_token.view(
459
+ b, k, w, atom_to_token.shape[-1]
460
+ ) # (b, k, w, t)
461
+ token_keys = to_keys(atom_to_token) # (b, k, h, t)
462
+ pair_update = self.z_to_p_trans(z.float()) # (b, t, t, atom_z)
463
  pair_update = torch.einsum(
464
  "bijd,bwki,bwlj->bwkld",
465
  pair_update,
466
  token_queries,
467
  token_keys,
468
+ ) # (b, k, w, h, atom_z)
469
+ pair = pair + pair_update.to(pair) # (b, k, w, h, atom_z)
470
 
471
  pair = pair + self.c_to_p_trans_q(
472
  conditioned_single.view(b, k, w, 1, conditioned_single.shape[-1])
473
+ ) # (b, k, w, h, atom_z)
474
  pair = pair + self.c_to_p_trans_k(
475
  to_keys(conditioned_single).view(
476
  b,
 
479
  h,
480
  conditioned_single.shape[-1],
481
  )
482
+ ) # (b, k, w, h, atom_z)
483
+ pair = pair + self.p_mlp(pair) # (b, k, w, h, atom_z)
484
+ return (
485
+ query,
486
+ conditioned_single,
487
+ pair,
488
+ to_keys,
489
+ ) # (b, n, atom_s), (b, n, atom_s), (b, k, w, h, atom_z), callable
490
 
491
 
492
  class AtomAttentionEncoder(nn.Module):
 
537
  ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, Callable]:
538
  b, n, _ = feats["ref_pos"].shape
539
  del b, n
540
+ # q/c: (b, a, atom_s); atom_enc_bias uses local-window attention axes.
541
+ atom_mask = feats["atom_pad_mask"].bool() # (b, a)
542
  if self.structure_prediction:
543
  if r is None:
544
  raise ValueError("structure prediction requires atom coordinates R")
545
+ q = q.repeat_interleave(multiplicity, 0) # (b * m, a, atom_s)
546
+ q = q + self.r_to_q_trans(r) # (b * m, a, atom_s)
547
+ c = c.repeat_interleave(multiplicity, 0) # (b * m, a, atom_s)
548
+ atom_mask = atom_mask.repeat_interleave(multiplicity, 0) # (b * m, a)
549
  q = self.atom_encoder(
550
  q=q,
551
  mask=atom_mask,
 
553
  bias=atom_enc_bias,
554
  multiplicity=multiplicity,
555
  to_keys=to_keys,
556
+ ) # (b * m, a, atom_s)
557
 
558
  with torch.autocast("cuda", enabled=False):
559
+ atom_update = self.atom_to_token_trans(q).float() # (b * m, a, d_token)
560
+ atom_to_token = feats["atom_to_token"].float() # (b, a, t)
561
+ atom_to_token = atom_to_token.repeat_interleave(
562
+ multiplicity, 0
563
+ ) # (b * m, a, t)
564
+ atom_to_token_mean = atom_to_token / (
565
+ atom_to_token.sum(dim=1, keepdim=True) + 1e-6
566
+ ) # (b * m, a, t)
567
+ token_update = torch.bmm(
568
+ atom_to_token_mean.transpose(1, 2), atom_update
569
+ ) # (b * m, t, d_token)
570
+ return (
571
+ token_update.to(q),
572
+ q,
573
+ c,
574
+ to_keys,
575
+ ) # (b * m, t, d_token), two (b * m, a, atom_s), callable
576
 
577
 
578
  class AtomAttentionDecoder(nn.Module):
 
625
  """Return atom-coordinate updates ``R_update: (b, a, 3)``."""
626
 
627
  with torch.autocast("cuda", enabled=False):
628
+ # a: (b * m, t, 2 * token_s); q/c: (b * m, a, atom_s).
629
+ atom_to_token = feats["atom_to_token"].float() # (b, a, t)
630
+ atom_to_token = atom_to_token.repeat_interleave(
631
+ multiplicity, 0
632
+ ) # (b * m, a, t)
633
+ token_update = self.a_to_q_trans(a.float()) # (b * m, t, atom_s)
634
+ atom_update = torch.bmm(atom_to_token, token_update) # (b * m, a, atom_s)
635
+ q = q + atom_update.to(q) # (b * m, a, atom_s)
636
+ atom_mask = feats["atom_pad_mask"].repeat_interleave(
637
+ multiplicity, 0
638
+ ) # (b * m, a)
639
  q = self.atom_decoder(
640
  q=q,
641
  mask=atom_mask,
 
643
  bias=atom_dec_bias,
644
  multiplicity=multiplicity,
645
  to_keys=to_keys,
646
+ ) # (b * m, a, atom_s)
647
+ return self.atom_feat_to_atom_pos_update(q) # (b * m, a, 3)
fastplms/models/boltz/vb_modules_transformersv2.py CHANGED
@@ -4,7 +4,6 @@ from __future__ import annotations
4
 
5
  from collections.abc import Callable
6
  from typing import Any, cast
7
-
8
  from torch import Tensor, nn
9
  from torch.utils.checkpoint import checkpoint
10
 
@@ -23,10 +22,12 @@ class AdaLN(nn.Module):
23
  self.s_bias = LinearNoBias(dim_single_cond, dim)
24
 
25
  def forward(self, a: Tensor, s: Tensor) -> Tensor:
26
- normalized = self.a_norm(a)
27
- conditioning = self.s_norm(s)
 
 
28
  output = self.s_scale(conditioning).sigmoid() * normalized + self.s_bias(conditioning)
29
- return cast(Tensor, output)
30
 
31
 
32
  class ConditionedTransitionBlock(nn.Module):
@@ -49,13 +50,15 @@ class ConditionedTransitionBlock(nn.Module):
49
  self.b_to_a = LinearNoBias(inner_dim, dim_single)
50
 
51
  projection = nn.Linear(dim_single_cond, dim_single)
52
- nn.init.zeros_(projection.weight)
53
- nn.init.constant_(projection.bias, -2.0)
54
  self.output_projection = nn.Sequential(projection, nn.Sigmoid())
55
 
56
  def forward(self, a: Tensor, s: Tensor) -> Tensor:
57
- normalized = self.adaln(a, s)
58
- hidden = self.swish_gate(normalized) * self.a_to_b(normalized)
 
 
59
  return cast(Tensor, self.output_projection(s) * self.b_to_a(hidden))
60
 
61
 
@@ -89,14 +92,16 @@ class DiffusionTransformer(nn.Module):
89
  )
90
 
91
  def _split_pair_bias(self, bias: Tensor | None) -> Tensor | None:
 
92
  if not self.pair_bias_attn:
93
  return None
94
  if bias is None:
95
  raise ValueError("pair bias is required when pair_bias_attn=True")
96
- batch_size, query_length, key_length, width = bias.shape
97
- depth = len(self.layers)
98
  if depth == 0 or width % depth:
99
  raise ValueError("pair-bias width must be divisible by transformer depth")
 
100
  return bias.view(batch_size, query_length, key_length, depth, width // depth)
101
 
102
  def forward(
@@ -110,12 +115,16 @@ class DiffusionTransformer(nn.Module):
110
  ) -> Tensor:
111
  """Transform A and preserve shape ``(b * m, l, d)``."""
112
 
113
- layer_biases = self._split_pair_bias(bias)
114
- output = a
 
 
 
115
  for index, layer in enumerate(self.layers):
 
116
  layer_bias = None if layer_biases is None else layer_biases[..., index, :]
117
  if self.activation_checkpointing:
118
- output = checkpoint(
119
  layer,
120
  output,
121
  s,
@@ -126,7 +135,7 @@ class DiffusionTransformer(nn.Module):
126
  use_reentrant=False,
127
  )
128
  else:
129
- output = layer(
130
  output,
131
  s,
132
  layer_bias,
@@ -134,7 +143,7 @@ class DiffusionTransformer(nn.Module):
134
  to_keys,
135
  multiplicity,
136
  )
137
- return output
138
 
139
 
140
  class DiffusionTransformerLayer(nn.Module):
@@ -156,8 +165,8 @@ class DiffusionTransformerLayer(nn.Module):
156
  compute_pair_bias=False,
157
  )
158
  self.output_projection_linear = nn.Linear(conditioning_dim, dim)
159
- nn.init.zeros_(self.output_projection_linear.weight)
160
- nn.init.constant_(self.output_projection_linear.bias, -2.0)
161
  self.output_projection = nn.Sequential(
162
  self.output_projection_linear,
163
  nn.Sigmoid(),
@@ -176,25 +185,28 @@ class DiffusionTransformerLayer(nn.Module):
176
  ) -> Tensor:
177
  """Update activation tensor A with conditioning S and pair bias."""
178
 
 
 
 
179
  if bias is None or mask is None:
180
  raise ValueError("diffusion attention requires pair bias and a key mask")
181
- normalized = self.adaln(a, s)
182
- key_states = normalized
183
- key_mask = mask
184
  if to_keys is not None:
185
- key_states = to_keys(normalized)
186
- key_mask = to_keys(mask.unsqueeze(-1)).squeeze(-1)
187
 
188
- attended = self.pair_bias_attn(
189
  s=normalized,
190
  z=bias,
191
  mask=key_mask,
192
  multiplicity=multiplicity,
193
  k_in=key_states,
194
  )
195
- output = a + self.output_projection(s) * attended
196
- output = output + self.transition(output, s)
197
- return cast(Tensor, self.post_lnorm(output))
198
 
199
 
200
  class AtomTransformer(nn.Module):
@@ -224,15 +236,21 @@ class AtomTransformer(nn.Module):
224
  ) -> Tensor:
225
  """Transform atom tensor Q with shape ``(b, n_atoms, d)``."""
226
 
227
- query_window = self.attn_window_queries
228
- key_window = self.attn_window_keys
229
- batch_size, atom_count, width = q.shape
230
- window_count = atom_count // query_window
 
 
 
231
  query_states = q.view(batch_size * window_count, query_window, -1)
 
232
  conditioning = c.view(batch_size * window_count, query_window, -1)
 
233
  query_mask = mask.view(batch_size * window_count, query_window)
 
234
  pair_bias = bias.repeat_interleave(multiplicity, dim=0)
235
- pair_bias = pair_bias.view(
236
  pair_bias.shape[0] * window_count,
237
  query_window,
238
  key_window,
@@ -240,10 +258,13 @@ class AtomTransformer(nn.Module):
240
  )
241
 
242
  def windowed_keys(states: Tensor) -> Tensor:
 
 
243
  merged = states.view(batch_size, window_count * query_window, -1)
 
244
  return to_keys(merged).view(batch_size * window_count, key_window, -1)
245
 
246
- output = self.diffusion_transformer(
247
  a=query_states,
248
  s=conditioning,
249
  bias=pair_bias,
@@ -251,4 +272,5 @@ class AtomTransformer(nn.Module):
251
  multiplicity=1,
252
  to_keys=windowed_keys,
253
  )
 
254
  return cast(Tensor, output.view(batch_size, window_count * query_window, width))
 
4
 
5
  from collections.abc import Callable
6
  from typing import Any, cast
 
7
  from torch import Tensor, nn
8
  from torch.utils.checkpoint import checkpoint
9
 
 
22
  self.s_bias = LinearNoBias(dim_single_cond, dim)
23
 
24
  def forward(self, a: Tensor, s: Tensor) -> Tensor:
25
+ # a: (..., d); s: (..., d_c).
26
+ normalized = self.a_norm(a) # (..., d)
27
+ conditioning = self.s_norm(s) # (..., d_c)
28
+ # (..., d)
29
  output = self.s_scale(conditioning).sigmoid() * normalized + self.s_bias(conditioning)
30
+ return cast(Tensor, output) # (..., d)
31
 
32
 
33
  class ConditionedTransitionBlock(nn.Module):
 
50
  self.b_to_a = LinearNoBias(inner_dim, dim_single)
51
 
52
  projection = nn.Linear(dim_single_cond, dim_single)
53
+ nn.init.zeros_(projection.weight) # (d, d_c)
54
+ nn.init.constant_(projection.bias, -2.0) # (d,)
55
  self.output_projection = nn.Sequential(projection, nn.Sigmoid())
56
 
57
  def forward(self, a: Tensor, s: Tensor) -> Tensor:
58
+ # a: (..., d); s: (..., d_c).
59
+ normalized = self.adaln(a, s) # (..., d)
60
+ hidden = self.swish_gate(normalized) * self.a_to_b(normalized) # (..., d_inner)
61
+ # (..., d)
62
  return cast(Tensor, self.output_projection(s) * self.b_to_a(hidden))
63
 
64
 
 
92
  )
93
 
94
  def _split_pair_bias(self, bias: Tensor | None) -> Tensor | None:
95
+ # bias: (b_z, l_q, l_k, n_layer * h) or None.
96
  if not self.pair_bias_attn:
97
  return None
98
  if bias is None:
99
  raise ValueError("pair bias is required when pair_bias_attn=True")
100
+ batch_size, query_length, key_length, width = bias.shape # b_z, l_q, l_k, n_layer * h
101
+ depth = len(self.layers) # n_layer
102
  if depth == 0 or width % depth:
103
  raise ValueError("pair-bias width must be divisible by transformer depth")
104
+ # (b_z, l_q, l_k, n_layer, h)
105
  return bias.view(batch_size, query_length, key_length, depth, width // depth)
106
 
107
  def forward(
 
115
  ) -> Tensor:
116
  """Transform A and preserve shape ``(b * m, l, d)``."""
117
 
118
+ # a: (b * m, l_q, d); s: (b * m, l_q, d_c).
119
+ # bias: (b, l_q, l_k, n_layer * h) or None.
120
+ # mask: (b * m, l_q) before an optional to_keys mapping; to_keys preserves d.
121
+ layer_biases = self._split_pair_bias(bias) # (b, l_q, l_k, n_layer, h) or None
122
+ output = a # (b * m, l_q, d)
123
  for index, layer in enumerate(self.layers):
124
+ # (b, l_q, l_k, h) or None
125
  layer_bias = None if layer_biases is None else layer_biases[..., index, :]
126
  if self.activation_checkpointing:
127
+ output = checkpoint( # (b * m, l_q, d)
128
  layer,
129
  output,
130
  s,
 
135
  use_reentrant=False,
136
  )
137
  else:
138
+ output = layer( # (b * m, l_q, d)
139
  output,
140
  s,
141
  layer_bias,
 
143
  to_keys,
144
  multiplicity,
145
  )
146
+ return output # (b * m, l_q, d)
147
 
148
 
149
  class DiffusionTransformerLayer(nn.Module):
 
165
  compute_pair_bias=False,
166
  )
167
  self.output_projection_linear = nn.Linear(conditioning_dim, dim)
168
+ nn.init.zeros_(self.output_projection_linear.weight) # (d, d_c)
169
+ nn.init.constant_(self.output_projection_linear.bias, -2.0) # (d,)
170
  self.output_projection = nn.Sequential(
171
  self.output_projection_linear,
172
  nn.Sigmoid(),
 
185
  ) -> Tensor:
186
  """Update activation tensor A with conditioning S and pair bias."""
187
 
188
+ # a: (b * m, l_q, d); s: (b * m, l_q, d_c).
189
+ # bias: (b, l_q, l_k, h) or None.
190
+ # mask: (b * m, l_q) or None; to_keys maps l_q to l_k.
191
  if bias is None or mask is None:
192
  raise ValueError("diffusion attention requires pair bias and a key mask")
193
+ normalized = self.adaln(a, s) # (b * m, l_q, d)
194
+ key_states = normalized # (b * m, l_q, d)
195
+ key_mask = mask # (b * m, l_q)
196
  if to_keys is not None:
197
+ key_states = to_keys(normalized) # (b * m, l_k, d)
198
+ key_mask = to_keys(mask.unsqueeze(-1)).squeeze(-1) # (b * m, l_k)
199
 
200
+ attended = self.pair_bias_attn( # (b * m, l_q, d)
201
  s=normalized,
202
  z=bias,
203
  mask=key_mask,
204
  multiplicity=multiplicity,
205
  k_in=key_states,
206
  )
207
+ output = a + self.output_projection(s) * attended # (b * m, l_q, d)
208
+ output = output + self.transition(output, s) # (b * m, l_q, d)
209
+ return cast(Tensor, self.post_lnorm(output)) # (b * m, l_q, d)
210
 
211
 
212
  class AtomTransformer(nn.Module):
 
236
  ) -> Tensor:
237
  """Transform atom tensor Q with shape ``(b, n_atoms, d)``."""
238
 
239
+ query_window = self.attn_window_queries # w_q
240
+ key_window = self.attn_window_keys # w_k
241
+ batch_size, atom_count, width = q.shape # b * m, n_atom, d
242
+ window_count = atom_count // query_window # k
243
+ # q, c: (b * m, n_atom, d); bias: (b, k, w_q, w_k, n_layer * h).
244
+ # mask: (b * m, n_atom); k = n_atom // w_q.
245
+ # (b * m * k, w_q, d)
246
  query_states = q.view(batch_size * window_count, query_window, -1)
247
+ # (b * m * k, w_q, d_c)
248
  conditioning = c.view(batch_size * window_count, query_window, -1)
249
+ # (b * m * k, w_q)
250
  query_mask = mask.view(batch_size * window_count, query_window)
251
+ # (b * m, k, w_q, w_k, n_layer * h)
252
  pair_bias = bias.repeat_interleave(multiplicity, dim=0)
253
+ pair_bias = pair_bias.view( # (b * m * k, w_q, w_k, n_layer * h)
254
  pair_bias.shape[0] * window_count,
255
  query_window,
256
  key_window,
 
258
  )
259
 
260
  def windowed_keys(states: Tensor) -> Tensor:
261
+ # states: (b * m * k, w_q, d_x).
262
+ # (b * m, n_atom, d_x)
263
  merged = states.view(batch_size, window_count * query_window, -1)
264
+ # (b * m * k, w_k, d_x)
265
  return to_keys(merged).view(batch_size * window_count, key_window, -1)
266
 
267
+ output = self.diffusion_transformer( # (b * m * k, w_q, d)
268
  a=query_states,
269
  s=conditioning,
270
  bias=pair_bias,
 
272
  multiplicity=1,
273
  to_keys=windowed_keys,
274
  )
275
+ # (b * m, n_atom, d)
276
  return cast(Tensor, output.view(batch_size, window_count * query_window, width))
fastplms/models/boltz/vb_modules_trunkv2.py CHANGED
@@ -2,9 +2,8 @@
2
 
3
  from __future__ import annotations
4
 
5
- from typing import Any, cast
6
-
7
  import torch
 
8
  from torch import Tensor, nn
9
  from torch.nn import functional as F
10
  from torch.utils.checkpoint import checkpoint
@@ -30,8 +29,8 @@ class ContactConditioning(nn.Module):
30
  self.fourier_embedding = FourierEmbedding(token_z)
31
  input_width = token_z + len(const.contact_conditioning_info) - 1
32
  self.encoder = nn.Linear(input_width, token_z)
33
- self.encoding_unspecified = nn.Parameter(torch.zeros(token_z))
34
- self.encoding_unselected = nn.Parameter(torch.zeros(token_z))
35
  self.cutoff_min = cutoff_min
36
  self.cutoff_max = cutoff_max
37
 
@@ -43,22 +42,25 @@ class ContactConditioning(nn.Module):
43
  if const.contact_conditioning_info["UNSELECTED"] != 1:
44
  raise ValueError("UNSELECTED contact conditioning must use channel one")
45
 
46
- categories = feats["contact_conditioning"]
47
- threshold = feats["contact_threshold"]
 
 
48
  normalized = (threshold - self.cutoff_min) / (self.cutoff_max - self.cutoff_min)
 
49
  fourier = self.fourier_embedding(normalized.flatten()).reshape((*normalized.shape, -1))
50
- selected_features = torch.cat(
51
  [categories[..., 2:], normalized.unsqueeze(-1), fourier],
52
  dim=-1,
53
  )
54
- selected = self.encoder(selected_features)
55
- special = categories[..., :2]
56
  return cast(
57
  Tensor,
58
  selected * (1 - special.sum(dim=-1, keepdim=True))
59
  + self.encoding_unspecified * special[..., 0:1]
60
  + self.encoding_unselected * special[..., 1:2],
61
- )
62
 
63
 
64
  class InputEmbedder(nn.Module):
@@ -122,51 +124,56 @@ class InputEmbedder(nn.Module):
122
 
123
  if add_method_conditioning:
124
  self.method_conditioning_init = nn.Embedding(const.num_method_types, token_s)
125
- self.method_conditioning_init.weight.data.fill_(0)
126
  if add_modified_flag:
127
  self.modified_conditioning_init = nn.Embedding(2, token_s)
128
- self.modified_conditioning_init.weight.data.fill_(0)
129
  if add_cyclic_flag:
130
  self.cyclic_conditioning_init = nn.Linear(1, token_s, bias=False)
131
- self.cyclic_conditioning_init.weight.data.fill_(0)
132
  if add_mol_type_feat:
133
  self.mol_type_conditioning_init = nn.Embedding(
134
  len(const.chain_type_ids),
135
  token_s,
136
  )
137
- self.mol_type_conditioning_init.weight.data.fill_(0)
138
 
139
  def forward(self, feats: dict[str, Tensor], affinity: bool = False) -> Tensor:
140
  """Return embedded sequence tensor S with shape ``(b, l, d_s)``."""
141
 
142
- residue_type = feats["res_type"].float()
 
143
  suffix = "_affinity" if affinity else ""
144
- profile = feats[f"profile{suffix}"]
145
- deletion_mean = feats[f"deletion_mean{suffix}"].unsqueeze(-1)
 
146
  atom_queries, atom_conditioning, atom_pairs, to_keys = self.atom_encoder(feats)
147
- atom_bias = self.atom_enc_proj_z(atom_pairs)
148
  atom_output, _, _, _ = self.atom_attention_encoder(
149
  feats=feats,
150
  q=atom_queries,
151
  c=atom_conditioning,
152
  atom_enc_bias=atom_bias,
153
  to_keys=to_keys,
154
- )
155
- output = (
156
  atom_output
157
  + self.res_type_encoding(residue_type)
158
  + self.msa_profile_encoding(torch.cat([profile, deletion_mean], dim=-1))
159
  )
160
  if self.add_method_conditioning:
 
161
  output = output + self.method_conditioning_init(feats["method_feature"])
162
  if self.add_modified_flag:
 
163
  output = output + self.modified_conditioning_init(feats["modified"])
164
  if self.add_cyclic_flag:
165
- cyclic = feats["cyclic_period"].clamp(max=1.0).unsqueeze(-1)
166
- output = output + self.cyclic_conditioning_init(cyclic)
167
  if self.add_mol_type_feat:
 
168
  output = output + self.mol_type_conditioning_init(feats["mol_type"])
169
- return cast(Tensor, output)
170
 
171
 
172
  class _TemplateBase(nn.Module):
@@ -213,44 +220,54 @@ class _TemplateBase(nn.Module):
213
  feats: dict[str, Tensor],
214
  template_pair_mask: Tensor,
215
  ) -> Tensor:
216
- residue_type = feats["template_restype"]
217
- cb_mask = feats["template_mask_cb"]
218
- frame_mask = feats["template_mask_frame"]
 
 
219
  cb_pair_mask = (cb_mask[..., :, None] * cb_mask[..., None, :]).unsqueeze(-1)
 
220
  frame_pair_mask = (frame_mask[..., :, None] * frame_mask[..., None, :]).unsqueeze(-1)
221
  with torch.autocast(device_type="cuda", enabled=False):
 
 
222
  cb_distances = torch.cdist(feats["template_cb"], feats["template_cb"])
223
- boundaries = torch.linspace(
224
  self.min_dist,
225
  self.max_dist,
226
  self.num_bins - 1,
227
  ).to(cb_distances.device)
228
- bins = (cb_distances[..., None] > boundaries).sum(dim=-1).long()
229
- distogram = F.one_hot(bins, num_classes=self.num_bins)
230
 
 
231
  rotations = feats["template_frame_rot"].unsqueeze(2).transpose(-1, -2)
 
232
  translations = feats["template_frame_t"].unsqueeze(2).unsqueeze(-1)
 
233
  ca_coordinates = feats["template_ca"].unsqueeze(3).unsqueeze(-1)
 
234
  vectors = torch.matmul(rotations, ca_coordinates - translations)
235
- norms = torch.norm(vectors, dim=-1, keepdim=True)
236
- unit_vectors = torch.where(
237
  norms > 0,
238
  vectors / norms,
239
  torch.zeros_like(vectors),
240
  ).squeeze(-1)
241
- pair_features = torch.cat(
242
  [distogram, cb_pair_mask, unit_vectors, frame_pair_mask],
243
  dim=-1,
244
  )
 
245
  pair_features = pair_features * template_pair_mask.unsqueeze(-1)
246
- residue_i = residue_type[:, :, :, None].expand(
247
  -1,
248
  -1,
249
  -1,
250
  residue_type.size(2),
251
  -1,
252
  )
253
- residue_j = residue_type[:, :, None, :].expand(
254
  -1,
255
  -1,
256
  residue_type.size(2),
@@ -260,7 +277,7 @@ class _TemplateBase(nn.Module):
260
  return cast(
261
  Tensor,
262
  self.a_proj(torch.cat([pair_features, residue_i, residue_j], dim=-1)),
263
- )
264
 
265
  def forward(
266
  self,
@@ -271,38 +288,40 @@ class _TemplateBase(nn.Module):
271
  ) -> Tensor:
272
  """Aggregate template pair tensor V into trunk update U."""
273
 
274
- residue_type = feats["template_restype"]
 
275
  batch_size, template_count = residue_type.shape[:2]
276
- template_present = feats["template_mask"].any(dim=2).float()
277
- present_count = template_present.sum(dim=1).clamp(min=1)
278
- features = self._template_features(
279
  feats,
280
  self._template_pair_mask(feats, template_count),
281
  )
282
- expanded_mask = pair_mask[:, None].expand(-1, template_count, -1, -1)
283
- expanded_mask = expanded_mask.reshape(
284
  batch_size * template_count,
285
  *expanded_mask.shape[2:],
286
  )
287
- template_states = self.z_proj(self.z_norm(z[:, None])) + features
288
- template_states = template_states.view(
289
  batch_size * template_count,
290
  *template_states.shape[2:],
291
  )
292
- template_states = template_states + self.pairformer(
293
  template_states,
294
  expanded_mask,
295
  use_kernels=use_kernels,
296
  )
297
- template_states = self.v_norm(template_states).view(
298
  batch_size,
299
  template_count,
300
  *template_states.shape[1:],
301
  )
302
- weights = template_present[:, :, None, None, None]
303
- aggregate = (template_states * weights).sum(dim=1)
 
304
  aggregate = aggregate / present_count[:, None, None, None].to(template_states)
305
- return cast(Tensor, self.u_proj(self.relu(aggregate)))
306
 
307
 
308
  class TemplateModule(_TemplateBase):
@@ -339,9 +358,9 @@ class TemplateModule(_TemplateBase):
339
  )
340
 
341
  def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
342
- asym_id = feats["asym_id"]
343
- same_chain = (asym_id[:, :, None] == asym_id[:, None, :]).float()
344
- return same_chain[:, None].expand(-1, count, -1, -1)
345
 
346
 
347
  class TemplateV2Module(_TemplateBase):
@@ -379,8 +398,8 @@ class TemplateV2Module(_TemplateBase):
379
 
380
  def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
381
  del count
382
- visibility = feats["visibility_ids"]
383
- return (visibility[..., :, None] == visibility[..., None, :]).float()
384
 
385
 
386
  class MSAModule(nn.Module):
@@ -433,6 +452,7 @@ class MSAModule(nn.Module):
433
  pair_states: Tensor,
434
  training: bool,
435
  ) -> tuple[bool, int | None, int | None, int | None, int | None]:
 
436
  if training:
437
  return False, None, None, None, None
438
  if pair_states.shape[1] > const.chunk_size_threshold:
@@ -449,28 +469,32 @@ class MSAModule(nn.Module):
449
  """Return updated pair tensor Z after every MSA block."""
450
 
451
  chunking = self._chunk_configuration(z, self.training)
452
- msa = feats["msa"]
 
 
453
  if msa.dtype in (torch.long, torch.int32, torch.int64):
454
- msa = F.one_hot(msa, num_classes=const.num_tokens).float()
455
- msa_mask = feats["msa_mask"]
456
- components = [
457
  msa,
458
- feats["has_deletion"].unsqueeze(-1),
459
- feats["deletion_value"].unsqueeze(-1),
460
  ]
461
  if self.use_paired_feature:
462
- components.append(feats["msa_paired"].unsqueeze(-1))
463
- msa_input = torch.cat(components, dim=-1)
464
  if self.subsample_msa:
465
- indices = torch.randperm(msa.shape[1])[: self.num_subsampled_msa]
466
- msa_input = msa_input[:, indices]
467
- msa_mask = msa_mask[:, indices]
468
-
469
- msa_states = self.msa_proj(msa_input) + self.s_proj(emb).unsqueeze(1)
470
- token_mask = feats["token_pad_mask"].float()
471
- pair_mask = token_mask[:, :, None] * token_mask[:, None, :]
472
- pair_states = z
473
  for layer in self.layers:
 
 
474
  arguments = (
475
  pair_states,
476
  msa_states,
@@ -480,13 +504,13 @@ class MSAModule(nn.Module):
480
  use_kernels,
481
  )
482
  if self.activation_checkpointing and self.training:
483
- pair_states, msa_states = checkpoint(
484
  layer,
485
  *arguments,
486
  )
487
  else:
488
- pair_states, msa_states = layer(*arguments)
489
- return pair_states
490
 
491
 
492
  class MSALayer(nn.Module):
@@ -534,29 +558,31 @@ class MSALayer(nn.Module):
534
  """Return updated Z and M tensors."""
535
 
536
  del chunk_size_transition_z
537
- dropout = get_dropout_mask(self.msa_dropout, m, self.training)
538
- msa_states = m + dropout * self.pair_weighted_averaging(
 
 
539
  m,
540
  z,
541
  token_mask,
542
  chunk_heads_pwa,
543
  )
544
- msa_states = msa_states + self.msa_transition(
545
  msa_states,
546
  chunk_size_transition_msa,
547
  )
548
- pair_states = z + self.outer_product_mean(
549
  msa_states,
550
  msa_mask,
551
  chunk_size_outer_product,
552
  )
553
- pair_states = self.pairformer_layer(
554
  pair_states,
555
  token_mask,
556
  chunk_size_tri_attn,
557
  use_kernels=use_kernels,
558
  )
559
- return pair_states, msa_states
560
 
561
 
562
  class BFactorModule(nn.Module):
@@ -568,7 +594,8 @@ class BFactorModule(nn.Module):
568
  self.num_bins = num_bins
569
 
570
  def forward(self, s: Tensor) -> Tensor:
571
- return cast(Tensor, self.bfactor(s))
 
572
 
573
 
574
  class DistogramModule(nn.Module):
@@ -581,8 +608,9 @@ class DistogramModule(nn.Module):
581
  self.num_bins = num_bins
582
 
583
  def forward(self, z: Tensor) -> Tensor:
584
- symmetric = z + z.transpose(1, 2)
585
- logits = self.distogram(symmetric)
 
586
  return cast(
587
  Tensor,
588
  logits.reshape(
@@ -592,4 +620,4 @@ class DistogramModule(nn.Module):
592
  self.num_distograms,
593
  self.num_bins,
594
  ),
595
- )
 
2
 
3
  from __future__ import annotations
4
 
 
 
5
  import torch
6
+ from typing import Any, cast
7
  from torch import Tensor, nn
8
  from torch.nn import functional as F
9
  from torch.utils.checkpoint import checkpoint
 
29
  self.fourier_embedding = FourierEmbedding(token_z)
30
  input_width = token_z + len(const.contact_conditioning_info) - 1
31
  self.encoder = nn.Linear(input_width, token_z)
32
+ self.encoding_unspecified = nn.Parameter(torch.zeros(token_z)) # (d_z,)
33
+ self.encoding_unselected = nn.Parameter(torch.zeros(token_z)) # (d_z,)
34
  self.cutoff_min = cutoff_min
35
  self.cutoff_max = cutoff_max
36
 
 
42
  if const.contact_conditioning_info["UNSELECTED"] != 1:
43
  raise ValueError("UNSELECTED contact conditioning must use channel one")
44
 
45
+ # c_contact is the number of contact-conditioning categories.
46
+ categories = feats["contact_conditioning"] # (b, l, l, c_contact)
47
+ threshold = feats["contact_threshold"] # (b, l, l)
48
+ # (b, l, l)
49
  normalized = (threshold - self.cutoff_min) / (self.cutoff_max - self.cutoff_min)
50
+ # (b, l, l, d_z)
51
  fourier = self.fourier_embedding(normalized.flatten()).reshape((*normalized.shape, -1))
52
+ selected_features = torch.cat( # (b, l, l, c_contact - 1 + d_z)
53
  [categories[..., 2:], normalized.unsqueeze(-1), fourier],
54
  dim=-1,
55
  )
56
+ selected = self.encoder(selected_features) # (b, l, l, d_z)
57
+ special = categories[..., :2] # (b, l, l, 2)
58
  return cast(
59
  Tensor,
60
  selected * (1 - special.sum(dim=-1, keepdim=True))
61
  + self.encoding_unspecified * special[..., 0:1]
62
  + self.encoding_unselected * special[..., 1:2],
63
+ ) # (b, l, l, d_z)
64
 
65
 
66
  class InputEmbedder(nn.Module):
 
124
 
125
  if add_method_conditioning:
126
  self.method_conditioning_init = nn.Embedding(const.num_method_types, token_s)
127
+ self.method_conditioning_init.weight.data.fill_(0) # (n_method, d_s)
128
  if add_modified_flag:
129
  self.modified_conditioning_init = nn.Embedding(2, token_s)
130
+ self.modified_conditioning_init.weight.data.fill_(0) # (2, d_s)
131
  if add_cyclic_flag:
132
  self.cyclic_conditioning_init = nn.Linear(1, token_s, bias=False)
133
+ self.cyclic_conditioning_init.weight.data.fill_(0) # (d_s, 1)
134
  if add_mol_type_feat:
135
  self.mol_type_conditioning_init = nn.Embedding(
136
  len(const.chain_type_ids),
137
  token_s,
138
  )
139
+ self.mol_type_conditioning_init.weight.data.fill_(0) # (n_mol_type, d_s)
140
 
141
  def forward(self, feats: dict[str, Tensor], affinity: bool = False) -> Tensor:
142
  """Return embedded sequence tensor S with shape ``(b, l, d_s)``."""
143
 
144
+ # n_atom is the padded atom count; k is the number of atom windows.
145
+ residue_type = feats["res_type"].float() # (b, l, n_token_type)
146
  suffix = "_affinity" if affinity else ""
147
+ profile = feats[f"profile{suffix}"] # (b, l, n_token_type)
148
+ deletion_mean = feats[f"deletion_mean{suffix}"].unsqueeze(-1) # (b, l, 1)
149
+ # (b, n_atom, d_a), (b, n_atom, d_a), (b, k, w_q, w_k, d_az), callable
150
  atom_queries, atom_conditioning, atom_pairs, to_keys = self.atom_encoder(feats)
151
+ atom_bias = self.atom_enc_proj_z(atom_pairs) # (b, k, w_q, w_k, n_layer * h)
152
  atom_output, _, _, _ = self.atom_attention_encoder(
153
  feats=feats,
154
  q=atom_queries,
155
  c=atom_conditioning,
156
  atom_enc_bias=atom_bias,
157
  to_keys=to_keys,
158
+ ) # (b, l, d_s), (b, n_atom, d_a), (b, n_atom, d_a), callable
159
+ output = ( # (b, l, d_s)
160
  atom_output
161
  + self.res_type_encoding(residue_type)
162
  + self.msa_profile_encoding(torch.cat([profile, deletion_mean], dim=-1))
163
  )
164
  if self.add_method_conditioning:
165
+ # method_feature: (b, l); output: (b, l, d_s).
166
  output = output + self.method_conditioning_init(feats["method_feature"])
167
  if self.add_modified_flag:
168
+ # modified: (b, l); output: (b, l, d_s).
169
  output = output + self.modified_conditioning_init(feats["modified"])
170
  if self.add_cyclic_flag:
171
+ cyclic = feats["cyclic_period"].clamp(max=1.0).unsqueeze(-1) # (b, l, 1)
172
+ output = output + self.cyclic_conditioning_init(cyclic) # (b, l, d_s)
173
  if self.add_mol_type_feat:
174
+ # mol_type: (b, l); output: (b, l, d_s).
175
  output = output + self.mol_type_conditioning_init(feats["mol_type"])
176
+ return cast(Tensor, output) # (b, l, d_s)
177
 
178
 
179
  class _TemplateBase(nn.Module):
 
220
  feats: dict[str, Tensor],
221
  template_pair_mask: Tensor,
222
  ) -> Tensor:
223
+ # residue_type: (b, t, l, n_token_type); template_pair_mask: (b, t, l, l).
224
+ residue_type = feats["template_restype"] # (b, t, l, n_token_type)
225
+ cb_mask = feats["template_mask_cb"] # (b, t, l)
226
+ frame_mask = feats["template_mask_frame"] # (b, t, l)
227
+ # (b, t, l, l, 1)
228
  cb_pair_mask = (cb_mask[..., :, None] * cb_mask[..., None, :]).unsqueeze(-1)
229
+ # (b, t, l, l, 1)
230
  frame_pair_mask = (frame_mask[..., :, None] * frame_mask[..., None, :]).unsqueeze(-1)
231
  with torch.autocast(device_type="cuda", enabled=False):
232
+ # template_cb: (b, t, l, 3).
233
+ # (b, t, l, l)
234
  cb_distances = torch.cdist(feats["template_cb"], feats["template_cb"])
235
+ boundaries = torch.linspace( # (n_bin - 1,)
236
  self.min_dist,
237
  self.max_dist,
238
  self.num_bins - 1,
239
  ).to(cb_distances.device)
240
+ bins = (cb_distances[..., None] > boundaries).sum(dim=-1).long() # (b, t, l, l)
241
+ distogram = F.one_hot(bins, num_classes=self.num_bins) # (b, t, l, l, n_bin)
242
 
243
+ # (b, t, 1, l, 3, 3)
244
  rotations = feats["template_frame_rot"].unsqueeze(2).transpose(-1, -2)
245
+ # (b, t, 1, l, 3, 1)
246
  translations = feats["template_frame_t"].unsqueeze(2).unsqueeze(-1)
247
+ # (b, t, l, 1, 3, 1)
248
  ca_coordinates = feats["template_ca"].unsqueeze(3).unsqueeze(-1)
249
+ # (b, t, l, l, 3, 1)
250
  vectors = torch.matmul(rotations, ca_coordinates - translations)
251
+ norms = torch.norm(vectors, dim=-1, keepdim=True) # (b, t, l, l, 3, 1)
252
+ unit_vectors = torch.where( # (b, t, l, l, 3)
253
  norms > 0,
254
  vectors / norms,
255
  torch.zeros_like(vectors),
256
  ).squeeze(-1)
257
+ pair_features = torch.cat( # (b, t, l, l, n_bin + 5)
258
  [distogram, cb_pair_mask, unit_vectors, frame_pair_mask],
259
  dim=-1,
260
  )
261
+ # (b, t, l, l, n_bin + 5)
262
  pair_features = pair_features * template_pair_mask.unsqueeze(-1)
263
+ residue_i = residue_type[:, :, :, None].expand( # (b, t, l, l, n_token_type)
264
  -1,
265
  -1,
266
  -1,
267
  residue_type.size(2),
268
  -1,
269
  )
270
+ residue_j = residue_type[:, :, None, :].expand( # (b, t, l, l, n_token_type)
271
  -1,
272
  -1,
273
  residue_type.size(2),
 
277
  return cast(
278
  Tensor,
279
  self.a_proj(torch.cat([pair_features, residue_i, residue_j], dim=-1)),
280
+ ) # (b, t, l, l, d_t)
281
 
282
  def forward(
283
  self,
 
288
  ) -> Tensor:
289
  """Aggregate template pair tensor V into trunk update U."""
290
 
291
+ # z: (b, l, l, d_z); pair_mask: (b, l, l).
292
+ residue_type = feats["template_restype"] # (b, t, l, n_token_type)
293
  batch_size, template_count = residue_type.shape[:2]
294
+ template_present = feats["template_mask"].any(dim=2).float() # (b, t)
295
+ present_count = template_present.sum(dim=1).clamp(min=1) # (b,)
296
+ features = self._template_features( # (b, t, l, l, d_t)
297
  feats,
298
  self._template_pair_mask(feats, template_count),
299
  )
300
+ expanded_mask = pair_mask[:, None].expand(-1, template_count, -1, -1) # (b, t, l, l)
301
+ expanded_mask = expanded_mask.reshape( # (b * t, l, l)
302
  batch_size * template_count,
303
  *expanded_mask.shape[2:],
304
  )
305
+ template_states = self.z_proj(self.z_norm(z[:, None])) + features # (b, t, l, l, d_t)
306
+ template_states = template_states.view( # (b * t, l, l, d_t)
307
  batch_size * template_count,
308
  *template_states.shape[2:],
309
  )
310
+ template_states = template_states + self.pairformer( # (b * t, l, l, d_t)
311
  template_states,
312
  expanded_mask,
313
  use_kernels=use_kernels,
314
  )
315
+ template_states = self.v_norm(template_states).view( # (b, t, l, l, d_t)
316
  batch_size,
317
  template_count,
318
  *template_states.shape[1:],
319
  )
320
+ weights = template_present[:, :, None, None, None] # (b, t, 1, 1, 1)
321
+ aggregate = (template_states * weights).sum(dim=1) # (b, l, l, d_t)
322
+ # (b, l, l, d_t)
323
  aggregate = aggregate / present_count[:, None, None, None].to(template_states)
324
+ return cast(Tensor, self.u_proj(self.relu(aggregate))) # (b, l, l, d_z)
325
 
326
 
327
  class TemplateModule(_TemplateBase):
 
358
  )
359
 
360
  def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
361
+ asym_id = feats["asym_id"] # (b, l)
362
+ same_chain = (asym_id[:, :, None] == asym_id[:, None, :]).float() # (b, l, l)
363
+ return same_chain[:, None].expand(-1, count, -1, -1) # (b, t, l, l)
364
 
365
 
366
  class TemplateV2Module(_TemplateBase):
 
398
 
399
  def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
400
  del count
401
+ visibility = feats["visibility_ids"] # (b, t, l)
402
+ return (visibility[..., :, None] == visibility[..., None, :]).float() # (b, t, l, l)
403
 
404
 
405
  class MSAModule(nn.Module):
 
452
  pair_states: Tensor,
453
  training: bool,
454
  ) -> tuple[bool, int | None, int | None, int | None, int | None]:
455
+ # pair_states: (b, l, l, d_z).
456
  if training:
457
  return False, None, None, None, None
458
  if pair_states.shape[1] > const.chunk_size_threshold:
 
469
  """Return updated pair tensor Z after every MSA block."""
470
 
471
  chunking = self._chunk_configuration(z, self.training)
472
+ # z: (b, l, l, d_z); emb: (b, l, d_s).
473
+ # s is MSA depth; n_token_type is the residue vocabulary size.
474
+ msa = feats["msa"] # (b, s, l) or (b, s, l, n_token_type)
475
  if msa.dtype in (torch.long, torch.int32, torch.int64):
476
+ msa = F.one_hot(msa, num_classes=const.num_tokens).float() # (b, s, l, n_token_type)
477
+ msa_mask = feats["msa_mask"] # (b, s, l)
478
+ components = [ # (b, s, l, n_token_type), then two (b, s, l, 1) tensors
479
  msa,
480
+ feats["has_deletion"].unsqueeze(-1), # (b, s, l, 1)
481
+ feats["deletion_value"].unsqueeze(-1), # (b, s, l, 1)
482
  ]
483
  if self.use_paired_feature:
484
+ components.append(feats["msa_paired"].unsqueeze(-1)) # (b, s, l, 1)
485
+ msa_input = torch.cat(components, dim=-1) # (b, s, l, n_token_type + 2 or 3)
486
  if self.subsample_msa:
487
+ indices = torch.randperm(msa.shape[1])[: self.num_subsampled_msa] # (s_sub,)
488
+ msa_input = msa_input[:, indices] # (b, s_sub, l, n_token_type + 2 or 3)
489
+ msa_mask = msa_mask[:, indices] # (b, s_sub, l)
490
+
491
+ msa_states = self.msa_proj(msa_input) + self.s_proj(emb).unsqueeze(1) # (b, s, l, d_m)
492
+ token_mask = feats["token_pad_mask"].float() # (b, l)
493
+ pair_mask = token_mask[:, :, None] * token_mask[:, None, :] # (b, l, l)
494
+ pair_states = z # (b, l, l, d_z)
495
  for layer in self.layers:
496
+ # Tensor arguments: pair_states (b, l, l, d_z), msa_states (b, s, l, d_m),
497
+ # pair_mask (b, l, l), msa_mask (b, s, l).
498
  arguments = (
499
  pair_states,
500
  msa_states,
 
504
  use_kernels,
505
  )
506
  if self.activation_checkpointing and self.training:
507
+ pair_states, msa_states = checkpoint( # (b, l, l, d_z), (b, s, l, d_m)
508
  layer,
509
  *arguments,
510
  )
511
  else:
512
+ pair_states, msa_states = layer(*arguments) # (b, l, l, d_z), (b, s, l, d_m)
513
+ return pair_states # (b, l, l, d_z)
514
 
515
 
516
  class MSALayer(nn.Module):
 
558
  """Return updated Z and M tensors."""
559
 
560
  del chunk_size_transition_z
561
+ # z: (b, l, l, d_z); m: (b, s, l, d_m).
562
+ # token_mask: (b, l, l); msa_mask: (b, s, l).
563
+ dropout = get_dropout_mask(self.msa_dropout, m, self.training) # (b, s, 1, 1)
564
+ msa_states = m + dropout * self.pair_weighted_averaging( # (b, s, l, d_m)
565
  m,
566
  z,
567
  token_mask,
568
  chunk_heads_pwa,
569
  )
570
+ msa_states = msa_states + self.msa_transition( # (b, s, l, d_m)
571
  msa_states,
572
  chunk_size_transition_msa,
573
  )
574
+ pair_states = z + self.outer_product_mean( # (b, l, l, d_z)
575
  msa_states,
576
  msa_mask,
577
  chunk_size_outer_product,
578
  )
579
+ pair_states = self.pairformer_layer( # (b, l, l, d_z)
580
  pair_states,
581
  token_mask,
582
  chunk_size_tri_attn,
583
  use_kernels=use_kernels,
584
  )
585
+ return pair_states, msa_states # (b, l, l, d_z), (b, s, l, d_m)
586
 
587
 
588
  class BFactorModule(nn.Module):
 
594
  self.num_bins = num_bins
595
 
596
  def forward(self, s: Tensor) -> Tensor:
597
+ # s: (..., d_s).
598
+ return cast(Tensor, self.bfactor(s)) # (..., n_bin)
599
 
600
 
601
  class DistogramModule(nn.Module):
 
608
  self.num_bins = num_bins
609
 
610
  def forward(self, z: Tensor) -> Tensor:
611
+ # z: (b, l, l, d_z).
612
+ symmetric = z + z.transpose(1, 2) # (b, l, l, d_z)
613
+ logits = self.distogram(symmetric) # (b, l, l, n_distogram * n_bin)
614
  return cast(
615
  Tensor,
616
  logits.reshape(
 
620
  self.num_distograms,
621
  self.num_bins,
622
  ),
623
+ ) # (b, l, l, n_distogram, n_bin)
fastplms/models/boltz/vb_modules_utils.py CHANGED
@@ -6,14 +6,14 @@ and attribution records are retained in ``THIRD_PARTY_NOTICES.md``.
6
 
7
  from __future__ import annotations
8
 
 
9
  from collections.abc import Iterable, Mapping, Sequence
10
  from functools import partial
11
  from typing import Any
12
-
13
- import torch
14
  from torch import Tensor, nn
15
  from torch.nn import functional as F
16
 
 
17
  LinearNoBias = partial(nn.Linear, bias=False)
18
 
19
 
@@ -32,19 +32,23 @@ def default(value: Any, fallback: Any) -> Any:
32
  def log(tensor: Tensor, eps: float = 1e-20) -> Tensor:
33
  """Compute a finite logarithm by applying a scalar lower bound."""
34
 
35
- return torch.log(tensor.clamp(min=eps))
 
36
 
37
 
38
  class SwiGLU(nn.Module):
39
  """Split X in half and apply a SiLU-gated linear unit."""
40
 
41
  def forward(self, x: Tensor) -> Tensor:
42
- values, gates = x.chunk(2, dim=-1)
43
- return F.silu(gates) * values
 
44
 
45
 
46
  def _masked_center(coordinates: Tensor, mask: Tensor) -> Tensor:
47
- weights = mask[:, :, None]
 
 
48
  return (coordinates * weights).sum(dim=1, keepdim=True) / weights.sum(
49
  dim=1,
50
  keepdim=True,
@@ -54,21 +58,24 @@ def _masked_center(coordinates: Tensor, mask: Tensor) -> Tensor:
54
  def center(atom_coords: Tensor, atom_mask: Tensor) -> Tensor:
55
  """Center coordinate tensor X with shape ``(b, n_atoms, 3)``."""
56
 
57
- return atom_coords - _masked_center(atom_coords, atom_mask)
 
58
 
59
 
60
  def _copysign(magnitudes: Tensor, signs: Tensor) -> Tensor:
61
  """Apply the elementwise sign of S to magnitude tensor M."""
62
 
63
- return torch.where((magnitudes < 0) != (signs < 0), -magnitudes, magnitudes)
 
64
 
65
 
66
  def quaternion_to_matrix(quaternions: Tensor) -> Tensor:
67
  """Convert real-first quaternion tensor Q from ``(..., 4)`` to ``(..., 3, 3)``."""
68
 
69
- real, i_axis, j_axis, k_axis = torch.unbind(quaternions, dim=-1)
70
- scale = 2.0 / (quaternions * quaternions).sum(dim=-1)
71
- entries = (
 
72
  1 - scale * (j_axis * j_axis + k_axis * k_axis),
73
  scale * (i_axis * j_axis - k_axis * real),
74
  scale * (i_axis * k_axis + j_axis * real),
@@ -79,6 +86,7 @@ def quaternion_to_matrix(quaternions: Tensor) -> Tensor:
79
  scale * (j_axis * k_axis + i_axis * real),
80
  1 - scale * (i_axis * i_axis + j_axis * j_axis),
81
  )
 
82
  return torch.stack(entries, dim=-1).reshape((*quaternions.shape[:-1], 3, 3))
83
 
84
 
@@ -90,10 +98,10 @@ def random_quaternions(
90
  """Draw ``n`` uniformly distributed unit quaternions with nonnegative real part."""
91
 
92
  resolved_device = torch.device(device) if isinstance(device, str) else device
93
- samples = torch.randn((n, 4), dtype=dtype, device=resolved_device)
94
- squared_norm = (samples * samples).sum(dim=1)
95
- signed_norm = _copysign(torch.sqrt(squared_norm), samples[:, 0])
96
- return samples / signed_norm[:, None]
97
 
98
 
99
  def random_rotations(
@@ -103,6 +111,7 @@ def random_rotations(
103
  ) -> Tensor:
104
  """Draw rotation tensor R with shape ``(n, 3, 3)``."""
105
 
 
106
  return quaternion_to_matrix(random_quaternions(n, dtype=dtype, device=device))
107
 
108
 
@@ -114,8 +123,8 @@ def compute_random_augmentation(
114
  ) -> tuple[Tensor, Tensor]:
115
  """Draw independent rotations R and translations T for a replicated batch."""
116
 
117
- rotations = random_rotations(multiplicity, dtype=dtype, device=device)
118
- translations = (
119
  torch.randn(
120
  (multiplicity, 1, 3),
121
  dtype=dtype,
@@ -123,7 +132,7 @@ def compute_random_augmentation(
123
  )
124
  * s_trans
125
  )
126
- return rotations, translations
127
 
128
 
129
  def randomly_rotate(
@@ -133,14 +142,15 @@ def randomly_rotate(
133
  ) -> Tensor | tuple[Tensor, Tensor | None]:
134
  """Rotate X and optionally Y using the same sampled rotation tensor R."""
135
 
136
- rotations = random_rotations(len(coords), coords.dtype, coords.device)
137
- rotated = torch.einsum("bmd,bds->bms", coords, rotations)
 
138
  if not return_second_coords:
139
- return rotated
140
- rotated_second = (
141
  None if second_coords is None else torch.einsum("bmd,bds->bms", second_coords, rotations)
142
  )
143
- return rotated, rotated_second
144
 
145
 
146
  def center_random_augmentation(
@@ -154,25 +164,28 @@ def center_random_augmentation(
154
  ) -> Tensor | tuple[Tensor, Tensor | None]:
155
  """Center and rigidly augment coordinate tensors X and optional Y."""
156
 
157
- primary = atom_coords
158
- secondary = second_coords
 
 
159
  if centering:
160
- centroid = _masked_center(primary, atom_mask)
161
- primary = primary - centroid
162
  if secondary is not None:
163
- secondary = secondary - centroid
164
 
165
  if augmentation:
166
- primary, secondary = randomly_rotate(
167
  primary,
168
  return_second_coords=True,
169
  second_coords=secondary,
170
  )
171
- translation = torch.randn_like(primary[:, 0:1, :]) * s_trans
172
- primary = primary + translation
173
  if secondary is not None:
174
- secondary = secondary + translation
175
 
 
176
  return (primary, secondary) if return_second_coords else primary
177
 
178
 
@@ -185,21 +198,24 @@ class ExponentialMovingAverage:
185
  decay: float,
186
  use_num_updates: bool = True,
187
  ) -> None:
 
188
  if not 0.0 <= decay <= 1.0:
189
  raise ValueError("decay must lie in [0, 1]")
190
  self.decay = decay
191
  self.num_updates: int | None = 0 if use_num_updates else None
192
- self.shadow_params = [
193
  parameter.clone().detach() for parameter in parameters if parameter.requires_grad
194
  ]
195
  self.collected_params: list[Tensor] = []
196
 
197
  @staticmethod
198
  def _trainable(parameters: Iterable[nn.Parameter]) -> list[nn.Parameter]:
 
199
  return [parameter for parameter in parameters if parameter.requires_grad]
200
 
201
  def update(self, parameters: Iterable[nn.Parameter]) -> None:
202
- trainable = self._trainable(parameters)
 
203
  if len(trainable) != len(self.shadow_params):
204
  raise ValueError("EMA parameter count changed")
205
  decay = self.decay
@@ -209,34 +225,40 @@ class ExponentialMovingAverage:
209
  update_weight = 1.0 - decay
210
  with torch.no_grad():
211
  for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
212
- shadow.sub_(update_weight * (shadow - parameter))
213
 
214
  def compatible(self, parameters: Sequence[Tensor]) -> bool:
215
  """Return whether parameter count and tensor shapes match the EMA state."""
216
 
 
217
  return len(self.shadow_params) == len(parameters) and all(
218
  shadow.shape == parameter.shape
219
  for shadow, parameter in zip(self.shadow_params, parameters, strict=True)
220
  )
221
 
222
  def copy_to(self, parameters: Iterable[nn.Parameter]) -> None:
223
- trainable = self._trainable(parameters)
 
224
  if len(trainable) != len(self.shadow_params):
225
  raise ValueError("EMA parameter count changed")
226
  for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
227
- parameter.data.copy_(shadow.data)
228
 
229
  def store(self, parameters: Iterable[nn.Parameter]) -> None:
 
 
230
  self.collected_params = [parameter.clone() for parameter in parameters]
231
 
232
  def restore(self, parameters: Iterable[nn.Parameter]) -> None:
233
- current = list(parameters)
 
234
  if len(current) != len(self.collected_params):
235
  raise ValueError("stored EMA parameter count changed")
236
  for collected, parameter in zip(self.collected_params, current, strict=True):
237
- parameter.data.copy_(collected.data)
238
 
239
  def state_dict(self) -> dict[str, Any]:
 
240
  return {
241
  "decay": self.decay,
242
  "num_updates": self.num_updates,
@@ -248,10 +270,13 @@ class ExponentialMovingAverage:
248
  state_dict: Mapping[str, Any],
249
  device: torch.device | str,
250
  ) -> None:
 
251
  self.decay = float(state_dict["decay"])
252
  updates = state_dict["num_updates"]
253
  self.num_updates = None if updates is None else int(updates)
 
254
  self.shadow_params = [tensor.to(device) for tensor in state_dict["shadow_params"]]
255
 
256
  def to(self, device: torch.device | str) -> None:
 
257
  self.shadow_params = [tensor.to(device) for tensor in self.shadow_params]
 
6
 
7
  from __future__ import annotations
8
 
9
+ import torch
10
  from collections.abc import Iterable, Mapping, Sequence
11
  from functools import partial
12
  from typing import Any
 
 
13
  from torch import Tensor, nn
14
  from torch.nn import functional as F
15
 
16
+
17
  LinearNoBias = partial(nn.Linear, bias=False)
18
 
19
 
 
32
  def log(tensor: Tensor, eps: float = 1e-20) -> Tensor:
33
  """Compute a finite logarithm by applying a scalar lower bound."""
34
 
35
+ # tensor: (...).
36
+ return torch.log(tensor.clamp(min=eps)) # (...)
37
 
38
 
39
  class SwiGLU(nn.Module):
40
  """Split X in half and apply a SiLU-gated linear unit."""
41
 
42
  def forward(self, x: Tensor) -> Tensor:
43
+ # x: (..., 2 * d).
44
+ values, gates = x.chunk(2, dim=-1) # each: (..., d)
45
+ return F.silu(gates) * values # (..., d)
46
 
47
 
48
  def _masked_center(coordinates: Tensor, mask: Tensor) -> Tensor:
49
+ # coordinates: (b, n_atom, 3); mask: (b, n_atom).
50
+ weights = mask[:, :, None] # (b, n_atom, 1)
51
+ # (b, 1, 3)
52
  return (coordinates * weights).sum(dim=1, keepdim=True) / weights.sum(
53
  dim=1,
54
  keepdim=True,
 
58
  def center(atom_coords: Tensor, atom_mask: Tensor) -> Tensor:
59
  """Center coordinate tensor X with shape ``(b, n_atoms, 3)``."""
60
 
61
+ # atom_coords: (b, n_atom, 3); atom_mask: (b, n_atom).
62
+ return atom_coords - _masked_center(atom_coords, atom_mask) # (b, n_atom, 3)
63
 
64
 
65
  def _copysign(magnitudes: Tensor, signs: Tensor) -> Tensor:
66
  """Apply the elementwise sign of S to magnitude tensor M."""
67
 
68
+ # magnitudes, signs: broadcast-compatible (...).
69
+ return torch.where((magnitudes < 0) != (signs < 0), -magnitudes, magnitudes) # (...)
70
 
71
 
72
  def quaternion_to_matrix(quaternions: Tensor) -> Tensor:
73
  """Convert real-first quaternion tensor Q from ``(..., 4)`` to ``(..., 3, 3)``."""
74
 
75
+ # quaternions: (..., 4).
76
+ real, i_axis, j_axis, k_axis = torch.unbind(quaternions, dim=-1) # each: (...)
77
+ scale = 2.0 / (quaternions * quaternions).sum(dim=-1) # (...)
78
+ entries = ( # nine tensors, each (...)
79
  1 - scale * (j_axis * j_axis + k_axis * k_axis),
80
  scale * (i_axis * j_axis - k_axis * real),
81
  scale * (i_axis * k_axis + j_axis * real),
 
86
  scale * (j_axis * k_axis + i_axis * real),
87
  1 - scale * (i_axis * i_axis + j_axis * j_axis),
88
  )
89
+ # (..., 3, 3)
90
  return torch.stack(entries, dim=-1).reshape((*quaternions.shape[:-1], 3, 3))
91
 
92
 
 
98
  """Draw ``n`` uniformly distributed unit quaternions with nonnegative real part."""
99
 
100
  resolved_device = torch.device(device) if isinstance(device, str) else device
101
+ samples = torch.randn((n, 4), dtype=dtype, device=resolved_device) # (n, 4)
102
+ squared_norm = (samples * samples).sum(dim=1) # (n,)
103
+ signed_norm = _copysign(torch.sqrt(squared_norm), samples[:, 0]) # (n,)
104
+ return samples / signed_norm[:, None] # (n, 4)
105
 
106
 
107
  def random_rotations(
 
111
  ) -> Tensor:
112
  """Draw rotation tensor R with shape ``(n, 3, 3)``."""
113
 
114
+ # (n, 3, 3)
115
  return quaternion_to_matrix(random_quaternions(n, dtype=dtype, device=device))
116
 
117
 
 
123
  ) -> tuple[Tensor, Tensor]:
124
  """Draw independent rotations R and translations T for a replicated batch."""
125
 
126
+ rotations = random_rotations(multiplicity, dtype=dtype, device=device) # (m, 3, 3)
127
+ translations = ( # (m, 1, 3)
128
  torch.randn(
129
  (multiplicity, 1, 3),
130
  dtype=dtype,
 
132
  )
133
  * s_trans
134
  )
135
+ return rotations, translations # (m, 3, 3), (m, 1, 3)
136
 
137
 
138
  def randomly_rotate(
 
142
  ) -> Tensor | tuple[Tensor, Tensor | None]:
143
  """Rotate X and optionally Y using the same sampled rotation tensor R."""
144
 
145
+ # coords: (b, n, 3); second_coords: (b, n_second, 3) or None.
146
+ rotations = random_rotations(len(coords), coords.dtype, coords.device) # (b, 3, 3)
147
+ rotated = torch.einsum("bmd,bds->bms", coords, rotations) # (b, n, 3)
148
  if not return_second_coords:
149
+ return rotated # (b, n, 3)
150
+ rotated_second = ( # (b, n_second, 3) or None
151
  None if second_coords is None else torch.einsum("bmd,bds->bms", second_coords, rotations)
152
  )
153
+ return rotated, rotated_second # (b, n, 3), (b, n_second, 3) or None
154
 
155
 
156
  def center_random_augmentation(
 
164
  ) -> Tensor | tuple[Tensor, Tensor | None]:
165
  """Center and rigidly augment coordinate tensors X and optional Y."""
166
 
167
+ # atom_coords: (b, n_atom, 3); atom_mask: (b, n_atom).
168
+ # second_coords: (b, n_second, 3) or None.
169
+ primary = atom_coords # (b, n_atom, 3)
170
+ secondary = second_coords # (b, n_second, 3) or None
171
  if centering:
172
+ centroid = _masked_center(primary, atom_mask) # (b, 1, 3)
173
+ primary = primary - centroid # (b, n_atom, 3)
174
  if secondary is not None:
175
+ secondary = secondary - centroid # (b, n_second, 3)
176
 
177
  if augmentation:
178
+ primary, secondary = randomly_rotate( # (b, n_atom, 3), (b, n_second, 3) or None
179
  primary,
180
  return_second_coords=True,
181
  second_coords=secondary,
182
  )
183
+ translation = torch.randn_like(primary[:, 0:1, :]) * s_trans # (b, 1, 3)
184
+ primary = primary + translation # (b, n_atom, 3)
185
  if secondary is not None:
186
+ secondary = secondary + translation # (b, n_second, 3)
187
 
188
+ # (b, n_atom, 3), optionally paired with (b, n_second, 3) or None.
189
  return (primary, secondary) if return_second_coords else primary
190
 
191
 
 
198
  decay: float,
199
  use_num_updates: bool = True,
200
  ) -> None:
201
+ # parameters: one independently shaped tensor (...) per trainable parameter.
202
  if not 0.0 <= decay <= 1.0:
203
  raise ValueError("decay must lie in [0, 1]")
204
  self.decay = decay
205
  self.num_updates: int | None = 0 if use_num_updates else None
206
+ self.shadow_params = [ # each: same shape as one trainable parameter, (...)
207
  parameter.clone().detach() for parameter in parameters if parameter.requires_grad
208
  ]
209
  self.collected_params: list[Tensor] = []
210
 
211
  @staticmethod
212
  def _trainable(parameters: Iterable[nn.Parameter]) -> list[nn.Parameter]:
213
+ # Each item is one trainable parameter with an independently varying shape (...).
214
  return [parameter for parameter in parameters if parameter.requires_grad]
215
 
216
  def update(self, parameters: Iterable[nn.Parameter]) -> None:
217
+ # parameters: one independently shaped tensor (...) per trainable parameter.
218
+ trainable = self._trainable(parameters) # each: one trainable parameter, (...)
219
  if len(trainable) != len(self.shadow_params):
220
  raise ValueError("EMA parameter count changed")
221
  decay = self.decay
 
225
  update_weight = 1.0 - decay
226
  with torch.no_grad():
227
  for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
228
+ shadow.sub_(update_weight * (shadow - parameter)) # (...) unchanged in place
229
 
230
  def compatible(self, parameters: Sequence[Tensor]) -> bool:
231
  """Return whether parameter count and tensor shapes match the EMA state."""
232
 
233
+ # Each shadow and corresponding parameter has an independently varying shape (...).
234
  return len(self.shadow_params) == len(parameters) and all(
235
  shadow.shape == parameter.shape
236
  for shadow, parameter in zip(self.shadow_params, parameters, strict=True)
237
  )
238
 
239
  def copy_to(self, parameters: Iterable[nn.Parameter]) -> None:
240
+ # parameters: one independently shaped tensor (...) per trainable parameter.
241
+ trainable = self._trainable(parameters) # each: one trainable parameter, (...)
242
  if len(trainable) != len(self.shadow_params):
243
  raise ValueError("EMA parameter count changed")
244
  for shadow, parameter in zip(self.shadow_params, trainable, strict=True):
245
+ parameter.data.copy_(shadow.data) # (...) unchanged in place
246
 
247
  def store(self, parameters: Iterable[nn.Parameter]) -> None:
248
+ # parameters: one independently shaped tensor (...) per parameter.
249
+ # Each clone has the same shape (...) as its corresponding parameter.
250
  self.collected_params = [parameter.clone() for parameter in parameters]
251
 
252
  def restore(self, parameters: Iterable[nn.Parameter]) -> None:
253
+ # parameters: one independently shaped tensor (...) per parameter.
254
+ current = list(parameters) # each: one parameter, (...)
255
  if len(current) != len(self.collected_params):
256
  raise ValueError("stored EMA parameter count changed")
257
  for collected, parameter in zip(self.collected_params, current, strict=True):
258
+ parameter.data.copy_(collected.data) # (...) unchanged in place
259
 
260
  def state_dict(self) -> dict[str, Any]:
261
+ # shadow_params contains independently shaped tensors (...).
262
  return {
263
  "decay": self.decay,
264
  "num_updates": self.num_updates,
 
270
  state_dict: Mapping[str, Any],
271
  device: torch.device | str,
272
  ) -> None:
273
+ # state_dict["shadow_params"] contains independently shaped tensors (...).
274
  self.decay = float(state_dict["decay"])
275
  updates = state_dict["num_updates"]
276
  self.num_updates = None if updates is None else int(updates)
277
+ # Each tensor retains its shape (...).
278
  self.shadow_params = [tensor.to(device) for tensor in state_dict["shadow_params"]]
279
 
280
  def to(self, device: torch.device | str) -> None:
281
+ # Each shadow parameter retains its independently varying shape (...).
282
  self.shadow_params = [tensor.to(device) for tensor in self.shadow_params]
fastplms/models/boltz/vb_potentials_potentials.py CHANGED
@@ -9,12 +9,11 @@ oracle while preserving the public class names used by converted checkpoints.
9
 
10
  from __future__ import annotations
11
 
 
12
  from abc import ABC, abstractmethod
13
  from dataclasses import dataclass
14
  from typing import Any
15
 
16
- import torch
17
-
18
  from . import vb_const as const
19
  from .vb_loss_diffusionv2 import weighted_rigid_align
20
  from .vb_potentials_schedules import (
@@ -23,22 +22,23 @@ from .vb_potentials_schedules import (
23
  PiecewiseStepFunction,
24
  )
25
 
 
26
  Parameter = ParameterSchedule | float | int | bool
27
  ParameterMap = dict[str, Parameter]
28
 
29
 
30
  @dataclass(frozen=True, slots=True)
31
  class _PreparedPotential:
32
- coordinates: torch.Tensor
33
- index: torch.Tensor
34
  function_args: tuple[Any, ...]
35
- com_index: torch.Tensor | None
36
- atom_pad_mask: torch.Tensor | None
37
- ref_coords: torch.Tensor | None
38
- ref_mask: torch.Tensor | None
39
- ref_token_index: torch.Tensor | None
40
- negation_mask: torch.Tensor | None
41
- union_index: torch.Tensor | None
42
 
43
 
44
  def _reduce_atoms_to_centers(
@@ -46,15 +46,16 @@ def _reduce_atoms_to_centers(
46
  com_index: torch.Tensor,
47
  atom_pad_mask: torch.Tensor,
48
  ) -> torch.Tensor:
49
- unpadded_index = com_index[atom_pad_mask]
50
- unpadded_coordinates = coordinates[..., atom_pad_mask, :]
 
51
  center_shape = (*unpadded_coordinates.shape[:-2], unpadded_index.max() + 1, 3)
52
  return torch.zeros(center_shape, device=coordinates.device).scatter_reduce(
53
  -2,
54
  unpadded_index.unsqueeze(-1).expand_as(unpadded_coordinates),
55
  unpadded_coordinates,
56
  "mean",
57
- )
58
 
59
 
60
  def _union_weights(
@@ -62,7 +63,8 @@ def _union_weights(
62
  union_index: torch.Tensor,
63
  union_lambda: float,
64
  ) -> torch.Tensor:
65
- unnormalized = torch.exp(-union_lambda * energy)
 
66
  partition = torch.zeros(
67
  (*energy.shape[:-1], union_index.max() + 1),
68
  device=union_index.device,
@@ -71,10 +73,10 @@ def _union_weights(
71
  union_index.expand_as(unnormalized),
72
  unnormalized,
73
  "sum",
74
- )
75
- weights = unnormalized / partition[..., union_index]
76
- weights[partition[..., union_index] == 0] = 0
77
- return weights
78
 
79
 
80
  def _scatter_constraint_gradients(
@@ -83,20 +85,22 @@ def _scatter_constraint_gradients(
83
  index: torch.Tensor,
84
  coordinates: torch.Tensor,
85
  ) -> torch.Tensor:
 
 
86
  product = coefficients.tile(variable_gradient.shape[-3]).unsqueeze(
87
  -1
88
- ) * variable_gradient.flatten(start_dim=-3, end_dim=-2)
89
  if product.dim() > 3:
90
- product = product.sum(dim=list(range(1, product.dim() - 2)))
91
  scatter_index = (
92
  index.flatten(start_dim=0, end_dim=1).unsqueeze(-1).expand((*coordinates.shape[:-2], -1, 3))
93
- )
94
  return torch.zeros_like(coordinates).scatter_reduce(
95
  -2,
96
  scatter_index,
97
  product,
98
  "sum",
99
- )
100
 
101
 
102
  class Potential(ABC):
@@ -112,26 +116,29 @@ class Potential(ABC):
112
  parameters: dict[str, Any],
113
  computed_args: tuple[Any, ...] | None = None,
114
  ) -> _PreparedPotential:
 
115
  if computed_args is None:
116
  computed_args = self.compute_args(feats, parameters)
117
- index, args, com_args, ref_args, operator_args = computed_args
118
  com_index = atom_pad_mask = None
119
  if com_args is not None:
120
- com_index, atom_pad_mask = com_args
121
  coordinates = _reduce_atoms_to_centers(
122
  coordinates,
123
  com_index,
124
  atom_pad_mask,
125
- )
126
 
127
  ref_coords = ref_mask = ref_token_index = None
128
  if ref_args is not None:
 
 
129
  ref_coords, ref_mask, ref_atom_index, ref_token_index = ref_args
130
- coordinates = coordinates[..., ref_atom_index, :]
131
 
132
  negation_mask = union_index = None
133
  if operator_args is not None:
134
- negation_mask, union_index = operator_args
135
  return _PreparedPotential(
136
  coordinates=coordinates,
137
  index=index,
@@ -153,9 +160,10 @@ class Potential(ABC):
153
  ) -> torch.Tensor:
154
  """Evaluate one energy per coordinate sample."""
155
 
 
156
  computed_args = self.compute_args(feats, parameters)
157
  if computed_args[0].shape[1] == 0:
158
- return torch.zeros(coords.shape[:-2], device=coords.device)
159
  prepared = self._prepare(coords, feats, parameters, computed_args)
160
  value = self.compute_variable(
161
  prepared.coordinates,
@@ -163,21 +171,21 @@ class Potential(ABC):
163
  ref_coords=prepared.ref_coords,
164
  ref_mask=prepared.ref_mask,
165
  compute_gradient=False,
166
- )
167
  energy = self.compute_function(
168
  value,
169
  *prepared.function_args,
170
  negation_mask=prepared.negation_mask,
171
  compute_derivative=False,
172
- )
173
  if prepared.union_index is not None:
174
  weights = _union_weights(
175
  energy,
176
  prepared.union_index,
177
  parameters["union_lambda"],
178
- )
179
- return (energy * weights).sum(dim=-1)
180
- return energy.sum(dim=tuple(range(1, energy.dim())))
181
 
182
  def compute_gradient(
183
  self,
@@ -187,9 +195,10 @@ class Potential(ABC):
187
  ) -> torch.Tensor:
188
  """Return the analytic coordinate gradient of the potential energy."""
189
 
 
190
  computed_args = self.compute_args(feats, parameters)
191
  if computed_args[0].shape[1] == 0:
192
- return torch.zeros_like(coords)
193
  prepared = self._prepare(coords, feats, parameters, computed_args)
194
  value, variable_gradient = self.compute_variable(
195
  prepared.coordinates,
@@ -197,19 +206,19 @@ class Potential(ABC):
197
  ref_coords=prepared.ref_coords,
198
  ref_mask=prepared.ref_mask,
199
  compute_gradient=True,
200
- )
201
  energy, energy_derivative = self.compute_function(
202
  value,
203
  *prepared.function_args,
204
  negation_mask=prepared.negation_mask,
205
  compute_derivative=True,
206
- )
207
  if prepared.union_index is not None:
208
  weights = _union_weights(
209
  energy,
210
  prepared.union_index,
211
  parameters["union_lambda"],
212
- )
213
  union_energy = torch.zeros(
214
  (*energy.shape[:-1], prepared.union_index.max() + 1),
215
  device=prepared.union_index.device,
@@ -218,7 +227,7 @@ class Potential(ABC):
218
  prepared.union_index.expand_as(energy),
219
  energy * weights,
220
  "sum",
221
- )
222
  coefficients = (
223
  energy_derivative
224
  * weights
@@ -227,21 +236,21 @@ class Potential(ABC):
227
  + parameters["union_lambda"]
228
  * (energy - union_energy[..., prepared.union_index])
229
  )
230
- )
231
  else:
232
- coefficients = energy_derivative
233
 
234
  atom_gradient = _scatter_constraint_gradients(
235
  coefficients,
236
  variable_gradient,
237
  prepared.index,
238
  prepared.coordinates,
239
- )
240
  if prepared.com_index is not None:
241
- atom_gradient = atom_gradient[..., prepared.com_index, :]
242
  elif prepared.ref_token_index is not None:
243
- atom_gradient = atom_gradient[..., prepared.ref_token_index, :]
244
- return atom_gradient
245
 
246
  def compute_parameters(self, t: float) -> dict[str, Any] | None:
247
  """Resolve scheduled parameters at diffusion time ``t``."""
@@ -261,6 +270,7 @@ class Potential(ABC):
261
  negation_mask: torch.Tensor | None = None,
262
  compute_derivative: bool = False,
263
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
 
264
  raise NotImplementedError
265
 
266
  @abstractmethod
@@ -272,6 +282,7 @@ class Potential(ABC):
272
  ref_mask: torch.Tensor | None = None,
273
  compute_gradient: bool = False,
274
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
 
275
  raise NotImplementedError
276
 
277
  @abstractmethod
@@ -303,16 +314,18 @@ class FlatBottomPotential(Potential):
303
  negation_mask: torch.Tensor | None = None,
304
  compute_derivative: bool = False,
305
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
 
 
306
  lower = (
307
  (torch.full_like(value, float("-inf")) if lower_bounds is None else lower_bounds)
308
  .expand_as(value)
309
  .clone()
310
- )
311
  upper = (
312
  (torch.full_like(value, float("inf")) if upper_bounds is None else upper_bounds)
313
  .expand_as(value)
314
  .clone()
315
- )
316
 
317
  if negation_mask is not None:
318
  if not torch.is_tensor(negation_mask):
@@ -329,37 +342,38 @@ class FlatBottomPotential(Potential):
329
  f"got {negation_mask.device} and {value.device}."
330
  )
331
  try:
332
- expanded_negation_mask = negation_mask.expand_as(value)
333
  except RuntimeError as error:
334
  raise ValueError(
335
  "negation_mask must be broadcastable to value shape "
336
  f"{tuple(value.shape)}, got {tuple(negation_mask.shape)}."
337
  ) from error
338
- unbounded_below = torch.isneginf(lower)
339
- unbounded_above = torch.isposinf(upper)
 
340
  valid_negation = unbounded_below | unbounded_above | expanded_negation_mask
341
  if not bool(torch.all(valid_negation).item()):
342
  raise ValueError(
343
  "negation_mask may be false only where at least one bound is infinite."
344
  )
345
- select_upper = ~unbounded_above & ~expanded_negation_mask
346
- lower[select_upper] = upper[select_upper]
347
- upper[select_upper] = float("inf")
348
- select_lower = ~unbounded_below & ~expanded_negation_mask
349
- upper[select_lower] = lower[select_lower]
350
- lower[select_lower] = float("-inf")
351
-
352
- below = value < lower
353
- above = value > upper
354
- energy = torch.zeros_like(value)
355
- energy[below] = (k * (lower - value))[below]
356
- energy[above] = (k * (value - upper))[above]
357
  if not compute_derivative:
358
- return energy
359
- derivative = torch.zeros_like(value)
360
- derivative[below] = -k.expand_as(below)[below]
361
- derivative[above] = k.expand_as(above)[above]
362
- return energy, derivative
363
 
364
 
365
  class ReferencePotential(Potential):
@@ -373,19 +387,22 @@ class ReferencePotential(Potential):
373
  ref_mask: torch.Tensor,
374
  compute_gradient: bool = False,
375
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
 
 
376
  aligned_reference = weighted_rigid_align(
377
  ref_coords.float(),
378
  coords[:, index].float(),
379
  ref_mask,
380
  ref_mask,
381
- )
382
- displacement = coords[:, index] - aligned_reference
383
- distance = torch.linalg.norm(displacement, dim=-1)
384
  if not compute_gradient:
385
- return distance
386
- unit_displacement = displacement / distance.unsqueeze(-1)
 
387
  gradient = (unit_displacement * ref_mask.unsqueeze(-1)).unsqueeze(1)
388
- return distance, gradient
389
 
390
 
391
  class DistancePotential(Potential):
@@ -399,15 +416,17 @@ class DistancePotential(Potential):
399
  ref_mask: torch.Tensor | None = None,
400
  compute_gradient: bool = False,
401
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
 
402
  del ref_coords, ref_mask
403
  displacement = coords.index_select(-2, index[0]) - coords.index_select(
404
  -2,
405
  index[1],
406
- )
407
- distance = torch.linalg.norm(displacement, dim=-1)
408
  if not compute_gradient:
409
- return distance
410
- unit_displacement = displacement / distance.unsqueeze(-1)
 
411
  return distance, torch.stack((unit_displacement, -unit_displacement), dim=1)
412
 
413
 
@@ -422,36 +441,40 @@ class DihedralPotential(Potential):
422
  ref_mask: torch.Tensor | None = None,
423
  compute_gradient: bool = False,
424
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
 
425
  del ref_coords, ref_mask
 
426
  r_ij = coords.index_select(-2, index[0]) - coords.index_select(-2, index[1])
427
  r_kj = coords.index_select(-2, index[2]) - coords.index_select(-2, index[1])
428
  r_kl = coords.index_select(-2, index[2]) - coords.index_select(-2, index[3])
429
- n_ijk = torch.cross(r_ij, r_kj, dim=-1)
430
- n_jkl = torch.cross(r_kj, r_kl, dim=-1)
431
- r_kj_norm = torch.linalg.norm(r_kj, dim=-1)
432
- n_ijk_norm = torch.linalg.norm(n_ijk, dim=-1)
433
- n_jkl_norm = torch.linalg.norm(n_jkl, dim=-1)
434
 
435
  orientation = torch.sign(
436
  r_kj.unsqueeze(-2) @ torch.cross(n_ijk, n_jkl, dim=-1).unsqueeze(-1)
437
- ).squeeze(-1, -2)
438
  cosine = (n_ijk.unsqueeze(-2) @ n_jkl.unsqueeze(-1)).squeeze(-1, -2) / (
439
  n_ijk_norm * n_jkl_norm
440
- )
 
441
  angle = orientation * torch.arccos(torch.clamp(cosine, -1 + 1e-8, 1 - 1e-8))
442
  if not compute_gradient:
443
- return angle
444
 
445
  projection_i = (
446
  (r_ij.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
447
- ).unsqueeze(-1)
448
  projection_l = (
449
  (r_kl.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
450
- ).unsqueeze(-1)
451
- grad_i = n_ijk * (r_kj_norm / n_ijk_norm**2).unsqueeze(-1)
452
- grad_l = -n_jkl * (r_kj_norm / n_jkl_norm**2).unsqueeze(-1)
453
- grad_j = (projection_i - 1) * grad_i - projection_l * grad_l
454
- grad_k = (projection_l - 1) * grad_l - projection_i * grad_i
 
455
  return angle, torch.stack((grad_i, grad_j, grad_k, grad_l), dim=1)
456
 
457
 
@@ -466,33 +489,37 @@ class AbsDihedralPotential(DihedralPotential):
466
  ref_mask: torch.Tensor | None = None,
467
  compute_gradient: bool = False,
468
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
 
469
  del ref_coords, ref_mask
470
  if not compute_gradient:
471
- return torch.abs(super().compute_variable(coords, index))
472
  angle, gradient = super().compute_variable(
473
  coords,
474
  index,
475
  compute_gradient=True,
476
- )
477
- gradient[(angle < 0)[..., None, :, None].expand_as(gradient)] *= -1
478
- return torch.abs(angle), gradient
479
 
480
 
481
  def _element_radii(feats: dict[str, torch.Tensor]) -> torch.Tensor:
 
482
  element_radii = torch.zeros(
483
  const.num_elements,
484
  dtype=torch.float32,
485
  device=feats["ref_element"].device,
486
- )
487
  element_radii[1:119] = torch.tensor(
488
  const.vdw_radii,
489
  dtype=torch.float32,
490
  device=element_radii.device,
491
- )
 
492
  return (feats["ref_element"].float() @ element_radii.unsqueeze(-1)).squeeze(-1)[0]
493
 
494
 
495
  def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
 
496
  return (
497
  torch.bmm(
498
  feats["atom_to_token"].float(),
@@ -500,94 +527,103 @@ def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
500
  )
501
  .squeeze(-1)
502
  .long()
503
- )[0]
504
 
505
 
506
  class PoseBustersPotential(FlatBottomPotential, DistancePotential):
507
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
508
- pair_index = feats["rdkit_bounds_index"][0]
509
- lower = feats["rdkit_lower_bounds"][0].clone()
510
- upper = feats["rdkit_upper_bounds"][0].clone()
511
- bond = feats["rdkit_bounds_bond_mask"][0]
512
- angle = feats["rdkit_bounds_angle_mask"][0]
513
- lower[bond * ~angle] *= 1.0 - parameters["bond_buffer"]
514
- upper[bond * ~angle] *= 1.0 + parameters["bond_buffer"]
515
- lower[~bond * angle] *= 1.0 - parameters["angle_buffer"]
516
- upper[~bond * angle] *= 1.0 + parameters["angle_buffer"]
 
517
  shared_buffer = min(parameters["bond_buffer"], parameters["angle_buffer"])
518
- lower[bond * angle] *= 1.0 - shared_buffer
519
- upper[bond * angle] *= 1.0 + shared_buffer
520
- lower[~bond * ~angle] *= 1.0 - parameters["clash_buffer"]
521
- upper[~bond * ~angle] = float("inf")
522
-
523
- atom_radii = _element_radii(feats)
524
- bond_cutoff = 0.35 + atom_radii[pair_index].mean(dim=0)
525
- lower[~bond] = torch.max(lower[~bond], bond_cutoff[~bond])
526
- upper[bond] = torch.min(upper[bond], bond_cutoff[bond])
 
527
  return pair_index, (torch.ones_like(lower), lower, upper), None, None, None
528
 
529
 
530
  class ConnectionsPotential(FlatBottomPotential, DistancePotential):
531
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
532
- pair_index = feats["connected_atom_index"][0]
 
533
  upper = torch.full(
534
  (pair_index.shape[1],),
535
  parameters["buffer"],
536
  device=pair_index.device,
537
- )
 
538
  return pair_index, (torch.ones_like(upper), None, upper), None, None, None
539
 
540
 
541
  class VDWOverlapPotential(FlatBottomPotential, DistancePotential):
542
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
543
- atom_chain_id = _atom_chain_ids(feats)
544
- atom_pad_mask = feats["atom_pad_mask"][0].bool()
545
- chain_sizes = torch.bincount(atom_chain_id[atom_pad_mask])
546
- nonion_atom = (chain_sizes > 1)[atom_chain_id]
547
- atom_radii = _element_radii(feats)
548
  pair_index = torch.triu_indices(
549
  atom_chain_id.shape[0],
550
  atom_chain_id.shape[0],
551
  1,
552
  device=atom_chain_id.device,
553
- )
554
- pair_pad_mask = atom_pad_mask[pair_index].all(dim=0)
 
555
  pair_ion_mask = nonion_atom[pair_index[0]] * nonion_atom[pair_index[1]]
556
 
557
- num_chains = atom_chain_id.max() + 1
558
- connected = feats["connected_chain_index"][0]
559
  connected_matrix = torch.eye(
560
  num_chains,
561
  device=atom_chain_id.device,
562
  dtype=torch.bool,
563
- )
564
- connected_matrix[connected[0], connected[1]] = True
565
- connected_matrix[connected[1], connected[0]] = True
566
  connected_pair = connected_matrix[
567
  atom_chain_id[pair_index[0]],
568
  atom_chain_id[pair_index[1]],
569
- ]
 
570
  pair_index = pair_index[:, pair_pad_mask * pair_ion_mask * ~connected_pair]
 
571
  lower = atom_radii[pair_index].sum(dim=0) * (1.0 - parameters["buffer"])
 
572
  return pair_index, (torch.ones_like(lower), lower, None), None, None, None
573
 
574
 
575
  class SymmetricChainCOMPotential(FlatBottomPotential, DistancePotential):
576
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
577
- atom_chain_id = _atom_chain_ids(feats)
578
- atom_pad_mask = feats["atom_pad_mask"][0].bool()
579
- nonion_chain = torch.bincount(atom_chain_id[atom_pad_mask]) > 1
580
- pair_index = feats["symmetric_chain_index"][0]
581
  pair_index = pair_index[
582
  :,
583
  nonion_chain[pair_index[0]] * nonion_chain[pair_index[1]],
584
- ]
585
  lower = torch.full(
586
  (pair_index.shape[1],),
587
  parameters["buffer"],
588
  dtype=torch.float32,
589
  device=pair_index.device,
590
- )
 
591
  return (
592
  pair_index,
593
  (torch.ones_like(lower), lower, None),
@@ -602,52 +638,60 @@ def _oriented_bounds(
602
  positive_lower: float,
603
  negative_upper: float,
604
  ) -> tuple[torch.Tensor, torch.Tensor]:
605
- lower = torch.zeros(orientations.shape, device=orientations.device)
606
- upper = torch.zeros(orientations.shape, device=orientations.device)
607
- lower[orientations] = positive_lower
608
- upper[orientations] = float("inf")
609
- lower[~orientations] = float("-inf")
610
- upper[~orientations] = negative_upper
611
- return lower, upper
 
612
 
613
 
614
  class StereoBondPotential(FlatBottomPotential, AbsDihedralPotential):
615
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
616
- index = feats["stereo_bond_index"][0]
617
- orientation = feats["stereo_bond_orientations"][0].bool()
 
618
  lower, upper = _oriented_bounds(
619
  orientation,
620
  torch.pi - parameters["buffer"],
621
  parameters["buffer"],
622
- )
 
623
  return index, (torch.ones_like(lower), lower, upper), None, None, None
624
 
625
 
626
  class ChiralAtomPotential(FlatBottomPotential, DihedralPotential):
627
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
628
- index = feats["chiral_atom_index"][0]
629
- orientation = feats["chiral_atom_orientations"][0].bool()
 
630
  lower, upper = _oriented_bounds(
631
  orientation,
632
  parameters["buffer"],
633
  -parameters["buffer"],
634
- )
 
635
  return index, (torch.ones_like(lower), lower, upper), None, None, None
636
 
637
 
638
  class PlanarBondPotential(FlatBottomPotential, AbsDihedralPotential):
639
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
640
- bond_index = feats["planar_bond_index"][0].T
 
641
  improper_pattern = torch.tensor(
642
  [[1, 2, 3, 0], [4, 5, 0, 3]],
643
  device=bond_index.device,
644
- ).T
 
645
  improper_index = bond_index[:, improper_pattern].swapaxes(0, 1).flatten(start_dim=1)
646
  upper = torch.full(
647
  (improper_index.shape[1],),
648
  parameters["buffer"],
649
  device=improper_index.device,
650
- )
 
651
  return (
652
  improper_index,
653
  (torch.ones_like(upper), None, upper),
@@ -661,25 +705,28 @@ class TemplateReferencePotential(FlatBottomPotential, ReferencePotential):
661
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
662
  del parameters
663
  if "template_mask_cb" not in feats or "template_force" not in feats:
 
664
  return torch.empty((1, 0)), None, None, None, None
665
- template_mask = feats["template_mask_cb"][feats["template_force"]]
 
666
  if template_mask.shape[0] == 0:
 
667
  return torch.empty((1, 0)), None, None, None, None
668
 
669
- ref_coords = feats["template_cb"][feats["template_force"]].clone()
670
- ref_mask = feats["template_mask_cb"][feats["template_force"]].clone()
671
  atom_indices = torch.arange(
672
  feats["atom_pad_mask"].shape[1],
673
  device=feats["atom_pad_mask"].device,
674
  dtype=torch.float32,
675
- )[None, :, None]
676
  ref_atom_index = (
677
  torch.bmm(
678
  feats["token_to_rep_atom"].float(),
679
  atom_indices,
680
  )
681
  .squeeze(-1)
682
- .long()[0]
683
  )
684
  ref_token_index = (
685
  torch.bmm(
@@ -687,24 +734,25 @@ class TemplateReferencePotential(FlatBottomPotential, ReferencePotential):
687
  feats["token_index"].unsqueeze(-1).float(),
688
  )
689
  .squeeze(-1)
690
- .long()[0]
691
  )
692
 
693
  index = torch.arange(
694
  template_mask.shape[-1],
695
  dtype=torch.long,
696
  device=template_mask.device,
697
- )[None]
698
  upper = torch.full(
699
  template_mask.shape,
700
  float("inf"),
701
  device=index.device,
702
  dtype=torch.float32,
703
- )
704
- reference_indices = torch.argwhere(template_mask).T
705
  upper[reference_indices.unbind()] = feats["template_force_threshold"][
706
  feats["template_force"]
707
- ][reference_indices[0]]
 
708
  return (
709
  index,
710
  (torch.ones_like(upper), None, upper),
@@ -719,8 +767,10 @@ class ContactPotentital(FlatBottomPotential, DistancePotential):
719
 
720
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
721
  del parameters
722
- index = feats["contact_pair_index"][0]
723
- upper = feats["contact_thresholds"][0].clone()
 
 
724
  return (
725
  index,
726
  (torch.ones_like(upper), None, upper),
 
9
 
10
  from __future__ import annotations
11
 
12
+ import torch
13
  from abc import ABC, abstractmethod
14
  from dataclasses import dataclass
15
  from typing import Any
16
 
 
 
17
  from . import vb_const as const
18
  from .vb_loss_diffusionv2 import weighted_rigid_align
19
  from .vb_potentials_schedules import (
 
22
  PiecewiseStepFunction,
23
  )
24
 
25
+
26
  Parameter = ParameterSchedule | float | int | bool
27
  ParameterMap = dict[str, Parameter]
28
 
29
 
30
  @dataclass(frozen=True, slots=True)
31
  class _PreparedPotential:
32
+ coordinates: torch.Tensor # (..., a, 3)
33
+ index: torch.Tensor # (q, n)
34
  function_args: tuple[Any, ...]
35
+ com_index: torch.Tensor | None # (a_input,)
36
+ atom_pad_mask: torch.Tensor | None # (a_input,)
37
+ ref_coords: torch.Tensor | None # (n_ref, n, 3)
38
+ ref_mask: torch.Tensor | None # (n_ref, n)
39
+ ref_token_index: torch.Tensor | None # (a_input,)
40
+ negation_mask: torch.Tensor | None # (n,)
41
+ union_index: torch.Tensor | None # (n,)
42
 
43
 
44
  def _reduce_atoms_to_centers(
 
46
  com_index: torch.Tensor,
47
  atom_pad_mask: torch.Tensor,
48
  ) -> torch.Tensor:
49
+ # coordinates: (..., a, 3); com_index/atom_pad_mask: (a,).
50
+ unpadded_index = com_index[atom_pad_mask] # (a_valid,)
51
+ unpadded_coordinates = coordinates[..., atom_pad_mask, :] # (..., a_valid, 3)
52
  center_shape = (*unpadded_coordinates.shape[:-2], unpadded_index.max() + 1, 3)
53
  return torch.zeros(center_shape, device=coordinates.device).scatter_reduce(
54
  -2,
55
  unpadded_index.unsqueeze(-1).expand_as(unpadded_coordinates),
56
  unpadded_coordinates,
57
  "mean",
58
+ ) # (..., n_center, 3)
59
 
60
 
61
  def _union_weights(
 
63
  union_index: torch.Tensor,
64
  union_lambda: float,
65
  ) -> torch.Tensor:
66
+ # energy: (..., n); union_index: (n,).
67
+ unnormalized = torch.exp(-union_lambda * energy) # (..., n)
68
  partition = torch.zeros(
69
  (*energy.shape[:-1], union_index.max() + 1),
70
  device=union_index.device,
 
73
  union_index.expand_as(unnormalized),
74
  unnormalized,
75
  "sum",
76
+ ) # (..., n_union)
77
+ weights = unnormalized / partition[..., union_index] # (..., n)
78
+ weights[partition[..., union_index] == 0] = 0 # (..., n)
79
+ return weights # (..., n)
80
 
81
 
82
  def _scatter_constraint_gradients(
 
85
  index: torch.Tensor,
86
  coordinates: torch.Tensor,
87
  ) -> torch.Tensor:
88
+ # coefficients: (..., n); variable_gradient: (..., q, n, 3).
89
+ # index: (q, n); coordinates: (b, a, 3).
90
  product = coefficients.tile(variable_gradient.shape[-3]).unsqueeze(
91
  -1
92
+ ) * variable_gradient.flatten(start_dim=-3, end_dim=-2) # (..., q * n, 3)
93
  if product.dim() > 3:
94
+ product = product.sum(dim=list(range(1, product.dim() - 2))) # (b, q * n, 3)
95
  scatter_index = (
96
  index.flatten(start_dim=0, end_dim=1).unsqueeze(-1).expand((*coordinates.shape[:-2], -1, 3))
97
+ ) # (b, q * n, 3)
98
  return torch.zeros_like(coordinates).scatter_reduce(
99
  -2,
100
  scatter_index,
101
  product,
102
  "sum",
103
+ ) # (b, a, 3)
104
 
105
 
106
  class Potential(ABC):
 
116
  parameters: dict[str, Any],
117
  computed_args: tuple[Any, ...] | None = None,
118
  ) -> _PreparedPotential:
119
+ # coordinates: (..., a_input, 3).
120
  if computed_args is None:
121
  computed_args = self.compute_args(feats, parameters)
122
+ index, args, com_args, ref_args, operator_args = computed_args # index: (q, n)
123
  com_index = atom_pad_mask = None
124
  if com_args is not None:
125
+ com_index, atom_pad_mask = com_args # each (a_input,)
126
  coordinates = _reduce_atoms_to_centers(
127
  coordinates,
128
  com_index,
129
  atom_pad_mask,
130
+ ) # (..., n_center, 3)
131
 
132
  ref_coords = ref_mask = ref_token_index = None
133
  if ref_args is not None:
134
+ # ref_coords: (n_ref, n, 3); ref_mask: (n_ref, n).
135
+ # ref_atom_index: (n,); ref_token_index: (a_input,).
136
  ref_coords, ref_mask, ref_atom_index, ref_token_index = ref_args
137
+ coordinates = coordinates[..., ref_atom_index, :] # (..., n, 3)
138
 
139
  negation_mask = union_index = None
140
  if operator_args is not None:
141
+ negation_mask, union_index = operator_args # each (n,)
142
  return _PreparedPotential(
143
  coordinates=coordinates,
144
  index=index,
 
160
  ) -> torch.Tensor:
161
  """Evaluate one energy per coordinate sample."""
162
 
163
+ # coords: (..., a, 3).
164
  computed_args = self.compute_args(feats, parameters)
165
  if computed_args[0].shape[1] == 0:
166
+ return torch.zeros(coords.shape[:-2], device=coords.device) # (...)
167
  prepared = self._prepare(coords, feats, parameters, computed_args)
168
  value = self.compute_variable(
169
  prepared.coordinates,
 
171
  ref_coords=prepared.ref_coords,
172
  ref_mask=prepared.ref_mask,
173
  compute_gradient=False,
174
+ ) # (..., n)
175
  energy = self.compute_function(
176
  value,
177
  *prepared.function_args,
178
  negation_mask=prepared.negation_mask,
179
  compute_derivative=False,
180
+ ) # (..., n)
181
  if prepared.union_index is not None:
182
  weights = _union_weights(
183
  energy,
184
  prepared.union_index,
185
  parameters["union_lambda"],
186
+ ) # (..., n)
187
+ return (energy * weights).sum(dim=-1) # (...)
188
+ return energy.sum(dim=tuple(range(1, energy.dim()))) # (b,)
189
 
190
  def compute_gradient(
191
  self,
 
195
  ) -> torch.Tensor:
196
  """Return the analytic coordinate gradient of the potential energy."""
197
 
198
+ # coords: (b, a, 3).
199
  computed_args = self.compute_args(feats, parameters)
200
  if computed_args[0].shape[1] == 0:
201
+ return torch.zeros_like(coords) # (b, a, 3)
202
  prepared = self._prepare(coords, feats, parameters, computed_args)
203
  value, variable_gradient = self.compute_variable(
204
  prepared.coordinates,
 
206
  ref_coords=prepared.ref_coords,
207
  ref_mask=prepared.ref_mask,
208
  compute_gradient=True,
209
+ ) # value: (..., n); variable_gradient: (..., q, n, 3)
210
  energy, energy_derivative = self.compute_function(
211
  value,
212
  *prepared.function_args,
213
  negation_mask=prepared.negation_mask,
214
  compute_derivative=True,
215
+ ) # each (..., n)
216
  if prepared.union_index is not None:
217
  weights = _union_weights(
218
  energy,
219
  prepared.union_index,
220
  parameters["union_lambda"],
221
+ ) # (..., n)
222
  union_energy = torch.zeros(
223
  (*energy.shape[:-1], prepared.union_index.max() + 1),
224
  device=prepared.union_index.device,
 
227
  prepared.union_index.expand_as(energy),
228
  energy * weights,
229
  "sum",
230
+ ) # (..., n_union)
231
  coefficients = (
232
  energy_derivative
233
  * weights
 
236
  + parameters["union_lambda"]
237
  * (energy - union_energy[..., prepared.union_index])
238
  )
239
+ ) # (..., n)
240
  else:
241
+ coefficients = energy_derivative # (..., n)
242
 
243
  atom_gradient = _scatter_constraint_gradients(
244
  coefficients,
245
  variable_gradient,
246
  prepared.index,
247
  prepared.coordinates,
248
+ ) # (b, a_prepared, 3)
249
  if prepared.com_index is not None:
250
+ atom_gradient = atom_gradient[..., prepared.com_index, :] # (b, a, 3)
251
  elif prepared.ref_token_index is not None:
252
+ atom_gradient = atom_gradient[..., prepared.ref_token_index, :] # (b, a, 3)
253
+ return atom_gradient # (b, a, 3)
254
 
255
  def compute_parameters(self, t: float) -> dict[str, Any] | None:
256
  """Resolve scheduled parameters at diffusion time ``t``."""
 
270
  negation_mask: torch.Tensor | None = None,
271
  compute_derivative: bool = False,
272
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
273
+ # value: (..., n); negation_mask: (n,).
274
  raise NotImplementedError
275
 
276
  @abstractmethod
 
282
  ref_mask: torch.Tensor | None = None,
283
  compute_gradient: bool = False,
284
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
285
+ # coords: (..., a, 3); index: (q, n).
286
  raise NotImplementedError
287
 
288
  @abstractmethod
 
314
  negation_mask: torch.Tensor | None = None,
315
  compute_derivative: bool = False,
316
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
317
+ # value: (..., n); k/lower_bounds/upper_bounds: (n,) or (..., n).
318
+ # negation_mask: (n,) or a shape broadcastable to (..., n).
319
  lower = (
320
  (torch.full_like(value, float("-inf")) if lower_bounds is None else lower_bounds)
321
  .expand_as(value)
322
  .clone()
323
+ ) # (..., n)
324
  upper = (
325
  (torch.full_like(value, float("inf")) if upper_bounds is None else upper_bounds)
326
  .expand_as(value)
327
  .clone()
328
+ ) # (..., n)
329
 
330
  if negation_mask is not None:
331
  if not torch.is_tensor(negation_mask):
 
342
  f"got {negation_mask.device} and {value.device}."
343
  )
344
  try:
345
+ expanded_negation_mask = negation_mask.expand_as(value) # (..., n)
346
  except RuntimeError as error:
347
  raise ValueError(
348
  "negation_mask must be broadcastable to value shape "
349
  f"{tuple(value.shape)}, got {tuple(negation_mask.shape)}."
350
  ) from error
351
+ unbounded_below = torch.isneginf(lower) # (..., n)
352
+ unbounded_above = torch.isposinf(upper) # (..., n)
353
+ # (..., n)
354
  valid_negation = unbounded_below | unbounded_above | expanded_negation_mask
355
  if not bool(torch.all(valid_negation).item()):
356
  raise ValueError(
357
  "negation_mask may be false only where at least one bound is infinite."
358
  )
359
+ select_upper = ~unbounded_above & ~expanded_negation_mask # (..., n)
360
+ lower[select_upper] = upper[select_upper] # (..., n)
361
+ upper[select_upper] = float("inf") # (..., n)
362
+ select_lower = ~unbounded_below & ~expanded_negation_mask # (..., n)
363
+ upper[select_lower] = lower[select_lower] # (..., n)
364
+ lower[select_lower] = float("-inf") # (..., n)
365
+
366
+ below = value < lower # (..., n)
367
+ above = value > upper # (..., n)
368
+ energy = torch.zeros_like(value) # (..., n)
369
+ energy[below] = (k * (lower - value))[below] # (..., n)
370
+ energy[above] = (k * (value - upper))[above] # (..., n)
371
  if not compute_derivative:
372
+ return energy # (..., n)
373
+ derivative = torch.zeros_like(value) # (..., n)
374
+ derivative[below] = -k.expand_as(below)[below] # (..., n)
375
+ derivative[above] = k.expand_as(above)[above] # (..., n)
376
+ return energy, derivative # each (..., n)
377
 
378
 
379
  class ReferencePotential(Potential):
 
387
  ref_mask: torch.Tensor,
388
  compute_gradient: bool = False,
389
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
390
+ # coords: (b, a, 3); index: (1, n).
391
+ # ref_coords: (n_ref, n, 3); ref_mask: (n_ref, n).
392
  aligned_reference = weighted_rigid_align(
393
  ref_coords.float(),
394
  coords[:, index].float(),
395
  ref_mask,
396
  ref_mask,
397
+ ) # (b, n_ref, n, 3)
398
+ displacement = coords[:, index] - aligned_reference # (b, n_ref, n, 3)
399
+ distance = torch.linalg.norm(displacement, dim=-1) # (b, n_ref, n)
400
  if not compute_gradient:
401
+ return distance # (b, n_ref, n)
402
+ unit_displacement = displacement / distance.unsqueeze(-1) # (b, n_ref, n, 3)
403
+ # (b, 1, n_ref, n, 3)
404
  gradient = (unit_displacement * ref_mask.unsqueeze(-1)).unsqueeze(1)
405
+ return distance, gradient # (b, n_ref, n); (b, 1, n_ref, n, 3)
406
 
407
 
408
  class DistancePotential(Potential):
 
416
  ref_mask: torch.Tensor | None = None,
417
  compute_gradient: bool = False,
418
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
419
+ # coords: (..., a, 3); index: (2, n).
420
  del ref_coords, ref_mask
421
  displacement = coords.index_select(-2, index[0]) - coords.index_select(
422
  -2,
423
  index[1],
424
+ ) # (..., n, 3)
425
+ distance = torch.linalg.norm(displacement, dim=-1) # (..., n)
426
  if not compute_gradient:
427
+ return distance # (..., n)
428
+ unit_displacement = displacement / distance.unsqueeze(-1) # (..., n, 3)
429
+ # Returns (..., n); (..., 2, n, 3).
430
  return distance, torch.stack((unit_displacement, -unit_displacement), dim=1)
431
 
432
 
 
441
  ref_mask: torch.Tensor | None = None,
442
  compute_gradient: bool = False,
443
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
444
+ # coords: (..., a, 3); index: (4, n).
445
  del ref_coords, ref_mask
446
+ # Each displacement has shape (..., n, 3).
447
  r_ij = coords.index_select(-2, index[0]) - coords.index_select(-2, index[1])
448
  r_kj = coords.index_select(-2, index[2]) - coords.index_select(-2, index[1])
449
  r_kl = coords.index_select(-2, index[2]) - coords.index_select(-2, index[3])
450
+ n_ijk = torch.cross(r_ij, r_kj, dim=-1) # (..., n, 3)
451
+ n_jkl = torch.cross(r_kj, r_kl, dim=-1) # (..., n, 3)
452
+ r_kj_norm = torch.linalg.norm(r_kj, dim=-1) # (..., n)
453
+ n_ijk_norm = torch.linalg.norm(n_ijk, dim=-1) # (..., n)
454
+ n_jkl_norm = torch.linalg.norm(n_jkl, dim=-1) # (..., n)
455
 
456
  orientation = torch.sign(
457
  r_kj.unsqueeze(-2) @ torch.cross(n_ijk, n_jkl, dim=-1).unsqueeze(-1)
458
+ ).squeeze(-1, -2) # (..., n)
459
  cosine = (n_ijk.unsqueeze(-2) @ n_jkl.unsqueeze(-1)).squeeze(-1, -2) / (
460
  n_ijk_norm * n_jkl_norm
461
+ ) # (..., n)
462
+ # (..., n)
463
  angle = orientation * torch.arccos(torch.clamp(cosine, -1 + 1e-8, 1 - 1e-8))
464
  if not compute_gradient:
465
+ return angle # (..., n)
466
 
467
  projection_i = (
468
  (r_ij.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
469
+ ).unsqueeze(-1) # (..., n, 1)
470
  projection_l = (
471
  (r_kl.unsqueeze(-2) @ r_kj.unsqueeze(-1)).squeeze(-1, -2) / (r_kj_norm**2)
472
+ ).unsqueeze(-1) # (..., n, 1)
473
+ grad_i = n_ijk * (r_kj_norm / n_ijk_norm**2).unsqueeze(-1) # (..., n, 3)
474
+ grad_l = -n_jkl * (r_kj_norm / n_jkl_norm**2).unsqueeze(-1) # (..., n, 3)
475
+ grad_j = (projection_i - 1) * grad_i - projection_l * grad_l # (..., n, 3)
476
+ grad_k = (projection_l - 1) * grad_l - projection_i * grad_i # (..., n, 3)
477
+ # Returns (..., n); (..., 4, n, 3).
478
  return angle, torch.stack((grad_i, grad_j, grad_k, grad_l), dim=1)
479
 
480
 
 
489
  ref_mask: torch.Tensor | None = None,
490
  compute_gradient: bool = False,
491
  ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
492
+ # coords: (..., a, 3); index: (4, n).
493
  del ref_coords, ref_mask
494
  if not compute_gradient:
495
+ return torch.abs(super().compute_variable(coords, index)) # (..., n)
496
  angle, gradient = super().compute_variable(
497
  coords,
498
  index,
499
  compute_gradient=True,
500
+ ) # (..., n); (..., 4, n, 3)
501
+ gradient[(angle < 0)[..., None, :, None].expand_as(gradient)] *= -1 # same
502
+ return torch.abs(angle), gradient # (..., n); (..., 4, n, 3)
503
 
504
 
505
  def _element_radii(feats: dict[str, torch.Tensor]) -> torch.Tensor:
506
+ # feats["ref_element"]: (1, a, n_element).
507
  element_radii = torch.zeros(
508
  const.num_elements,
509
  dtype=torch.float32,
510
  device=feats["ref_element"].device,
511
+ ) # (n_element,)
512
  element_radii[1:119] = torch.tensor(
513
  const.vdw_radii,
514
  dtype=torch.float32,
515
  device=element_radii.device,
516
+ ) # (n_element,)
517
+ # (a,)
518
  return (feats["ref_element"].float() @ element_radii.unsqueeze(-1)).squeeze(-1)[0]
519
 
520
 
521
  def _atom_chain_ids(feats: dict[str, torch.Tensor]) -> torch.Tensor:
522
+ # atom_to_token: (1, a, l); asym_id: (1, l).
523
  return (
524
  torch.bmm(
525
  feats["atom_to_token"].float(),
 
527
  )
528
  .squeeze(-1)
529
  .long()
530
+ )[0] # (a,)
531
 
532
 
533
  class PoseBustersPotential(FlatBottomPotential, DistancePotential):
534
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
535
+ # rdkit_bounds_index: (1, 2, n); bound and mask features: (1, n).
536
+ pair_index = feats["rdkit_bounds_index"][0] # (2, n)
537
+ lower = feats["rdkit_lower_bounds"][0].clone() # (n,)
538
+ upper = feats["rdkit_upper_bounds"][0].clone() # (n,)
539
+ bond = feats["rdkit_bounds_bond_mask"][0] # (n,)
540
+ angle = feats["rdkit_bounds_angle_mask"][0] # (n,)
541
+ lower[bond * ~angle] *= 1.0 - parameters["bond_buffer"] # (n,)
542
+ upper[bond * ~angle] *= 1.0 + parameters["bond_buffer"] # (n,)
543
+ lower[~bond * angle] *= 1.0 - parameters["angle_buffer"] # (n,)
544
+ upper[~bond * angle] *= 1.0 + parameters["angle_buffer"] # (n,)
545
  shared_buffer = min(parameters["bond_buffer"], parameters["angle_buffer"])
546
+ lower[bond * angle] *= 1.0 - shared_buffer # (n,)
547
+ upper[bond * angle] *= 1.0 + shared_buffer # (n,)
548
+ lower[~bond * ~angle] *= 1.0 - parameters["clash_buffer"] # (n,)
549
+ upper[~bond * ~angle] = float("inf") # (n,)
550
+
551
+ atom_radii = _element_radii(feats) # (a,)
552
+ bond_cutoff = 0.35 + atom_radii[pair_index].mean(dim=0) # (n,)
553
+ lower[~bond] = torch.max(lower[~bond], bond_cutoff[~bond]) # (n,)
554
+ upper[bond] = torch.min(upper[bond], bond_cutoff[bond]) # (n,)
555
+ # Returns index (2, n) and three function arguments of shape (n,).
556
  return pair_index, (torch.ones_like(lower), lower, upper), None, None, None
557
 
558
 
559
  class ConnectionsPotential(FlatBottomPotential, DistancePotential):
560
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
561
+ # connected_atom_index: (1, 2, n).
562
+ pair_index = feats["connected_atom_index"][0] # (2, n)
563
  upper = torch.full(
564
  (pair_index.shape[1],),
565
  parameters["buffer"],
566
  device=pair_index.device,
567
+ ) # (n,)
568
+ # Returns index (2, n), k (n,), and upper bound (n,).
569
  return pair_index, (torch.ones_like(upper), None, upper), None, None, None
570
 
571
 
572
  class VDWOverlapPotential(FlatBottomPotential, DistancePotential):
573
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
574
+ atom_chain_id = _atom_chain_ids(feats) # (a,)
575
+ atom_pad_mask = feats["atom_pad_mask"][0].bool() # (a,)
576
+ chain_sizes = torch.bincount(atom_chain_id[atom_pad_mask]) # (n_chain,)
577
+ nonion_atom = (chain_sizes > 1)[atom_chain_id] # (a,)
578
+ atom_radii = _element_radii(feats) # (a,)
579
  pair_index = torch.triu_indices(
580
  atom_chain_id.shape[0],
581
  atom_chain_id.shape[0],
582
  1,
583
  device=atom_chain_id.device,
584
+ ) # (2, n_pair)
585
+ pair_pad_mask = atom_pad_mask[pair_index].all(dim=0) # (n_pair,)
586
+ # (n_pair,)
587
  pair_ion_mask = nonion_atom[pair_index[0]] * nonion_atom[pair_index[1]]
588
 
589
+ num_chains = atom_chain_id.max() + 1 # ()
590
+ connected = feats["connected_chain_index"][0] # (2, n_connection)
591
  connected_matrix = torch.eye(
592
  num_chains,
593
  device=atom_chain_id.device,
594
  dtype=torch.bool,
595
+ ) # (n_chain, n_chain)
596
+ connected_matrix[connected[0], connected[1]] = True # (n_chain, n_chain)
597
+ connected_matrix[connected[1], connected[0]] = True # (n_chain, n_chain)
598
  connected_pair = connected_matrix[
599
  atom_chain_id[pair_index[0]],
600
  atom_chain_id[pair_index[1]],
601
+ ] # (n_pair,)
602
+ # (2, n)
603
  pair_index = pair_index[:, pair_pad_mask * pair_ion_mask * ~connected_pair]
604
+ # (n,)
605
  lower = atom_radii[pair_index].sum(dim=0) * (1.0 - parameters["buffer"])
606
+ # Returns index (2, n), k (n,), and lower bound (n,).
607
  return pair_index, (torch.ones_like(lower), lower, None), None, None, None
608
 
609
 
610
  class SymmetricChainCOMPotential(FlatBottomPotential, DistancePotential):
611
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
612
+ atom_chain_id = _atom_chain_ids(feats) # (a,)
613
+ atom_pad_mask = feats["atom_pad_mask"][0].bool() # (a,)
614
+ nonion_chain = torch.bincount(atom_chain_id[atom_pad_mask]) > 1 # (n_chain,)
615
+ pair_index = feats["symmetric_chain_index"][0] # (2, n_candidate)
616
  pair_index = pair_index[
617
  :,
618
  nonion_chain[pair_index[0]] * nonion_chain[pair_index[1]],
619
+ ] # (2, n)
620
  lower = torch.full(
621
  (pair_index.shape[1],),
622
  parameters["buffer"],
623
  dtype=torch.float32,
624
  device=pair_index.device,
625
+ ) # (n,)
626
+ # Returns center index (2, n), k/lower (n,), and atom-to-center maps (a,).
627
  return (
628
  pair_index,
629
  (torch.ones_like(lower), lower, None),
 
638
  positive_lower: float,
639
  negative_upper: float,
640
  ) -> tuple[torch.Tensor, torch.Tensor]:
641
+ # orientations: (n,).
642
+ lower = torch.zeros(orientations.shape, device=orientations.device) # (n,)
643
+ upper = torch.zeros(orientations.shape, device=orientations.device) # (n,)
644
+ lower[orientations] = positive_lower # (n,)
645
+ upper[orientations] = float("inf") # (n,)
646
+ lower[~orientations] = float("-inf") # (n,)
647
+ upper[~orientations] = negative_upper # (n,)
648
+ return lower, upper # each (n,)
649
 
650
 
651
  class StereoBondPotential(FlatBottomPotential, AbsDihedralPotential):
652
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
653
+ # stereo_bond_index: (1, 4, n); orientations: (1, n).
654
+ index = feats["stereo_bond_index"][0] # (4, n)
655
+ orientation = feats["stereo_bond_orientations"][0].bool() # (n,)
656
  lower, upper = _oriented_bounds(
657
  orientation,
658
  torch.pi - parameters["buffer"],
659
  parameters["buffer"],
660
+ ) # each (n,)
661
+ # Returns index (4, n) and three function arguments of shape (n,).
662
  return index, (torch.ones_like(lower), lower, upper), None, None, None
663
 
664
 
665
  class ChiralAtomPotential(FlatBottomPotential, DihedralPotential):
666
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
667
+ # chiral_atom_index: (1, 4, n); orientations: (1, n).
668
+ index = feats["chiral_atom_index"][0] # (4, n)
669
+ orientation = feats["chiral_atom_orientations"][0].bool() # (n,)
670
  lower, upper = _oriented_bounds(
671
  orientation,
672
  parameters["buffer"],
673
  -parameters["buffer"],
674
+ ) # each (n,)
675
+ # Returns index (4, n) and three function arguments of shape (n,).
676
  return index, (torch.ones_like(lower), lower, upper), None, None, None
677
 
678
 
679
  class PlanarBondPotential(FlatBottomPotential, AbsDihedralPotential):
680
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
681
+ # The feature stores two atom-index rows for each six-entry bond pattern.
682
+ bond_index = feats["planar_bond_index"][0].T # (2, n_bond_entry)
683
  improper_pattern = torch.tensor(
684
  [[1, 2, 3, 0], [4, 5, 0, 3]],
685
  device=bond_index.device,
686
+ ).T # (4, 2)
687
+ # (4, n_improper)
688
  improper_index = bond_index[:, improper_pattern].swapaxes(0, 1).flatten(start_dim=1)
689
  upper = torch.full(
690
  (improper_index.shape[1],),
691
  parameters["buffer"],
692
  device=improper_index.device,
693
+ ) # (n_improper,)
694
+ # Returns index (4, n_improper), k (n_improper,), and upper (n_improper,).
695
  return (
696
  improper_index,
697
  (torch.ones_like(upper), None, upper),
 
705
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
706
  del parameters
707
  if "template_mask_cb" not in feats or "template_force" not in feats:
708
+ # Empty sentinel index: (1, 0).
709
  return torch.empty((1, 0)), None, None, None, None
710
+ # template_mask_cb/template_force: (b, n_template, n)/(b, n_template).
711
+ template_mask = feats["template_mask_cb"][feats["template_force"]] # (n_ref, n)
712
  if template_mask.shape[0] == 0:
713
+ # Empty sentinel index: (1, 0).
714
  return torch.empty((1, 0)), None, None, None, None
715
 
716
+ ref_coords = feats["template_cb"][feats["template_force"]].clone() # (n_ref, n, 3)
717
+ ref_mask = feats["template_mask_cb"][feats["template_force"]].clone() # (n_ref, n)
718
  atom_indices = torch.arange(
719
  feats["atom_pad_mask"].shape[1],
720
  device=feats["atom_pad_mask"].device,
721
  dtype=torch.float32,
722
+ )[None, :, None] # (1, a, 1)
723
  ref_atom_index = (
724
  torch.bmm(
725
  feats["token_to_rep_atom"].float(),
726
  atom_indices,
727
  )
728
  .squeeze(-1)
729
+ .long()[0] # (n,)
730
  )
731
  ref_token_index = (
732
  torch.bmm(
 
734
  feats["token_index"].unsqueeze(-1).float(),
735
  )
736
  .squeeze(-1)
737
+ .long()[0] # (a,)
738
  )
739
 
740
  index = torch.arange(
741
  template_mask.shape[-1],
742
  dtype=torch.long,
743
  device=template_mask.device,
744
+ )[None] # (1, n)
745
  upper = torch.full(
746
  template_mask.shape,
747
  float("inf"),
748
  device=index.device,
749
  dtype=torch.float32,
750
+ ) # (n_ref, n)
751
+ reference_indices = torch.argwhere(template_mask).T # (2, n_active)
752
  upper[reference_indices.unbind()] = feats["template_force_threshold"][
753
  feats["template_force"]
754
+ ][reference_indices[0]] # (n_ref, n)
755
+ # Returns index (1, n), function args (n_ref, n), and reference mappings.
756
  return (
757
  index,
758
  (torch.ones_like(upper), None, upper),
 
767
 
768
  def compute_args(self, feats: dict[str, torch.Tensor], parameters: dict[str, Any]):
769
  del parameters
770
+ # pair index: (1, 2, n); threshold/operator features: (1, n).
771
+ index = feats["contact_pair_index"][0] # (2, n)
772
+ upper = feats["contact_thresholds"][0].clone() # (n,)
773
+ # Returns index (2, n), k/upper (n,), and two operator tensors (n,).
774
  return (
775
  index,
776
  (torch.ones_like(upper), None, upper),
fastplms/models/boltz/vb_tri_attn_attention.py CHANGED
@@ -2,9 +2,8 @@
2
 
3
  from __future__ import annotations
4
 
5
- from typing import cast
6
-
7
  import torch
 
8
  from torch import Tensor, nn
9
 
10
  from .vb_tri_attn_primitives import Attention, LayerNorm, Linear
@@ -44,6 +43,7 @@ class TriangleAttention(nn.Module):
44
  ) -> Tensor:
45
  """Evaluate attention in slices of the batch-like leading axes."""
46
 
 
47
  def attention_call(**inputs: Tensor) -> Tensor:
48
  return cast(Tensor, self.mha(**inputs, use_kernels=use_kernels))
49
 
@@ -53,7 +53,7 @@ class TriangleAttention(nn.Module):
53
  "tri_bias": tri_bias,
54
  "mask_bias": mask_bias,
55
  "mask": mask,
56
- }
57
  return cast(
58
  Tensor,
59
  chunk_layer(
@@ -74,6 +74,7 @@ class TriangleAttention(nn.Module):
74
  chunk_size: int | None,
75
  use_kernels: bool,
76
  ) -> Tensor:
 
77
  if chunk_size is not None and not use_kernels:
78
  return cast(
79
  Tensor,
@@ -107,17 +108,19 @@ class TriangleAttention(nn.Module):
107
  ) -> Tensor:
108
  """Transform X with shape ``(..., l, l, d)`` under mask M."""
109
 
110
- pair_mask = x.new_ones(x.shape[:-1]) if mask is None else mask
111
- pair_states = x
112
  if not self.starting:
113
- pair_states = pair_states.transpose(-2, -3)
114
- pair_mask = pair_mask.transpose(-1, -2)
115
-
116
- normalized = self.layer_norm(pair_states)
117
- expanded_mask = pair_mask[..., :, None, None, :]
118
- mask_bias = self.inf * (expanded_mask - 1)
119
- triangle_bias = permute_final_dims(self.linear(normalized), (2, 0, 1))
120
- triangle_bias = triangle_bias.unsqueeze(-4)
 
 
121
  output = self._run_attention(
122
  normalized,
123
  triangle_bias,
@@ -125,8 +128,10 @@ class TriangleAttention(nn.Module):
125
  expanded_mask,
126
  chunk_size,
127
  use_kernels,
128
- )
129
- return output if self.starting else output.transpose(-2, -3)
 
 
130
 
131
 
132
  TriangleAttentionStartingNode = TriangleAttention
 
2
 
3
  from __future__ import annotations
4
 
 
 
5
  import torch
6
+ from typing import cast
7
  from torch import Tensor, nn
8
 
9
  from .vb_tri_attn_primitives import Attention, LayerNorm, Linear
 
43
  ) -> Tensor:
44
  """Evaluate attention in slices of the batch-like leading axes."""
45
 
46
+ # x: (..., l, l, d); biases/mask retain broadcastable triangular axes.
47
  def attention_call(**inputs: Tensor) -> Tensor:
48
  return cast(Tensor, self.mha(**inputs, use_kernels=use_kernels))
49
 
 
53
  "tri_bias": tri_bias,
54
  "mask_bias": mask_bias,
55
  "mask": mask,
56
+ } # tensor values retain the input shapes above
57
  return cast(
58
  Tensor,
59
  chunk_layer(
 
74
  chunk_size: int | None,
75
  use_kernels: bool,
76
  ) -> Tensor:
77
+ # states: (..., l, l, d); returned tensor has the same shape.
78
  if chunk_size is not None and not use_kernels:
79
  return cast(
80
  Tensor,
 
108
  ) -> Tensor:
109
  """Transform X with shape ``(..., l, l, d)`` under mask M."""
110
 
111
+ pair_mask = x.new_ones(x.shape[:-1]) if mask is None else mask # (..., l, l)
112
+ pair_states = x # (..., l, l, d)
113
  if not self.starting:
114
+ pair_states = pair_states.transpose(-2, -3) # (..., l, l, d)
115
+ pair_mask = pair_mask.transpose(-1, -2) # (..., l, l)
116
+
117
+ normalized = self.layer_norm(pair_states) # (..., l, l, d)
118
+ expanded_mask = pair_mask[..., :, None, None, :] # (..., l, 1, 1, l)
119
+ mask_bias = self.inf * (expanded_mask - 1) # (..., l, 1, 1, l)
120
+ triangle_bias = permute_final_dims(
121
+ self.linear(normalized), (2, 0, 1)
122
+ ) # (..., h, l, l)
123
+ triangle_bias = triangle_bias.unsqueeze(-4) # (..., 1, h, l, l)
124
  output = self._run_attention(
125
  normalized,
126
  triangle_bias,
 
128
  expanded_mask,
129
  chunk_size,
130
  use_kernels,
131
+ ) # (..., l, l, d)
132
+ return (
133
+ output if self.starting else output.transpose(-2, -3)
134
+ ) # (..., l, l, d)
135
 
136
 
137
  TriangleAttentionStartingNode = TriangleAttention
fastplms/models/boltz/vb_tri_attn_primitives.py CHANGED
@@ -12,10 +12,9 @@ from __future__ import annotations
12
 
13
  import importlib
14
  import math
 
15
  from collections.abc import Callable, Sequence
16
  from importlib.util import find_spec
17
-
18
- import torch
19
  from torch import nn
20
 
21
  from . import vb_layers_initialize as initialize
@@ -27,6 +26,7 @@ def _initialize_weight(
27
  bias: torch.Tensor | None,
28
  method: str,
29
  ) -> None:
 
30
  initializers = {
31
  "default": initialize.lecun_normal_init_,
32
  "relu": initialize.he_normal_init_,
@@ -68,21 +68,28 @@ class Linear(nn.Linear):
68
  def forward(self, input: torch.Tensor) -> torch.Tensor:
69
  """Project ``input`` while preserving explicit precision policies."""
70
 
 
71
  input_dtype = input.dtype
72
  if self.precision is not None:
73
  with torch.autocast("cuda", enabled=False):
74
- bias = None if self.bias is None else self.bias.to(self.precision)
 
 
75
  projected = nn.functional.linear(
76
  input.to(self.precision),
77
  self.weight.to(self.precision),
78
  bias,
79
- )
80
- return projected.to(input_dtype)
81
  if input_dtype is torch.bfloat16:
82
  with torch.autocast("cuda", enabled=False):
83
- bias = None if self.bias is None else self.bias.to(input_dtype)
84
- return nn.functional.linear(input, self.weight.to(input_dtype), bias)
85
- return nn.functional.linear(input, self.weight, self.bias)
 
 
 
 
86
 
87
 
88
  class LayerNorm(nn.Module):
@@ -92,10 +99,11 @@ class LayerNorm(nn.Module):
92
  super().__init__()
93
  self.c_in = (c_in,)
94
  self.eps = eps
95
- self.weight = nn.Parameter(torch.ones(c_in))
96
- self.bias = nn.Parameter(torch.zeros(c_in))
97
 
98
  def forward(self, x: torch.Tensor) -> torch.Tensor:
 
99
  if x.dtype is torch.bfloat16:
100
  with torch.autocast("cuda", enabled=False):
101
  return nn.functional.layer_norm(
@@ -126,11 +134,14 @@ def _attention(
126
  ) -> torch.Tensor:
127
  """Compute biased scaled attention after query scaling."""
128
 
129
- scores = torch.matmul(query, permute_final_dims(key, (1, 0)))
 
 
 
130
  for bias in biases:
131
- scores += bias
132
- probabilities = softmax_no_cast(scores, dim=-1)
133
- return torch.matmul(probabilities, value)
134
 
135
 
136
  @torch.compiler.disable
@@ -144,6 +155,7 @@ def kernel_triangular_attn(
144
  ) -> torch.Tensor:
145
  """Call the optional cuEquivariance triangle-attention primitive."""
146
 
 
147
  if (
148
  find_spec("cuequivariance_torch") is None
149
  or find_spec("cuequivariance_ops_torch") is None
@@ -193,23 +205,32 @@ class Attention(nn.Module):
193
  """Project Q, K, and V to ``(..., h, n, d)`` tensors."""
194
 
195
  def split_heads(X: torch.Tensor) -> torch.Tensor:
196
- return X.view(*X.shape[:-1], self.no_heads, -1).transpose(-2, -3)
197
-
198
- q = split_heads(self.linear_q(q_x))
199
- k = split_heads(self.linear_k(kv_x))
200
- v = split_heads(self.linear_v(kv_x))
 
 
 
 
201
  if apply_scale:
202
- q /= math.sqrt(self.c_hidden)
203
- return q, k, v
204
 
205
  def _wrap_up(self, output: torch.Tensor, q_x: torch.Tensor) -> torch.Tensor:
206
  """Apply the optional gate, merge heads, and project the update."""
207
 
 
208
  if self.linear_g is not None:
209
- gate = self.sigmoid(self.linear_g(q_x))
210
- gate = gate.view(*gate.shape[:-1], self.no_heads, -1)
211
- output = output * gate
212
- return self.linear_o(flatten_final_dims(output, 2))
 
 
 
 
213
 
214
  def forward(
215
  self,
@@ -222,7 +243,9 @@ class Attention(nn.Module):
222
  ) -> torch.Tensor:
223
  """Return a gated pair update with shape ``(..., n, c_q)``."""
224
 
225
- q, k, v = self._prep_qkv(q_x, kv_x, apply_scale=not use_kernels)
 
 
226
  if use_kernels:
227
  output = kernel_triangular_attn(
228
  q,
@@ -231,7 +254,12 @@ class Attention(nn.Module):
231
  tri_bias=tri_bias,
232
  mask=mask.bool(),
233
  scale=1.0 / math.sqrt(self.c_hidden),
234
- )
235
  else:
236
- output = _attention(q, k, v, (mask_bias, tri_bias))
237
- return self._wrap_up(output.transpose(-2, -3), q_x)
 
 
 
 
 
 
12
 
13
  import importlib
14
  import math
15
+ import torch
16
  from collections.abc import Callable, Sequence
17
  from importlib.util import find_spec
 
 
18
  from torch import nn
19
 
20
  from . import vb_layers_initialize as initialize
 
26
  bias: torch.Tensor | None,
27
  method: str,
28
  ) -> None:
29
+ # weight: (d_out, d_in); bias: (d_out,) or None; both retain shape in place.
30
  initializers = {
31
  "default": initialize.lecun_normal_init_,
32
  "relu": initialize.he_normal_init_,
 
68
  def forward(self, input: torch.Tensor) -> torch.Tensor:
69
  """Project ``input`` while preserving explicit precision policies."""
70
 
71
+ # input: (..., d_in); every branch returns (..., d_out).
72
  input_dtype = input.dtype
73
  if self.precision is not None:
74
  with torch.autocast("cuda", enabled=False):
75
+ bias = (
76
+ None if self.bias is None else self.bias.to(self.precision)
77
+ ) # (d_out,) or None
78
  projected = nn.functional.linear(
79
  input.to(self.precision),
80
  self.weight.to(self.precision),
81
  bias,
82
+ ) # (..., d_out)
83
+ return projected.to(input_dtype) # (..., d_out)
84
  if input_dtype is torch.bfloat16:
85
  with torch.autocast("cuda", enabled=False):
86
+ bias = (
87
+ None if self.bias is None else self.bias.to(input_dtype)
88
+ ) # (d_out,) or None
89
+ return nn.functional.linear(
90
+ input, self.weight.to(input_dtype), bias
91
+ ) # (..., d_out)
92
+ return nn.functional.linear(input, self.weight, self.bias) # (..., d_out)
93
 
94
 
95
  class LayerNorm(nn.Module):
 
99
  super().__init__()
100
  self.c_in = (c_in,)
101
  self.eps = eps
102
+ self.weight = nn.Parameter(torch.ones(c_in)) # (c_in,)
103
+ self.bias = nn.Parameter(torch.zeros(c_in)) # (c_in,)
104
 
105
  def forward(self, x: torch.Tensor) -> torch.Tensor:
106
+ # x: (..., c_in); normalization preserves shape.
107
  if x.dtype is torch.bfloat16:
108
  with torch.autocast("cuda", enabled=False):
109
  return nn.functional.layer_norm(
 
134
  ) -> torch.Tensor:
135
  """Compute biased scaled attention after query scaling."""
136
 
137
+ # query/key/value: (..., h, n_q_or_k, d_h).
138
+ scores = torch.matmul(
139
+ query, permute_final_dims(key, (1, 0))
140
+ ) # (..., h, n_q, n_k)
141
  for bias in biases:
142
+ scores += bias # (..., h, n_q, n_k)
143
+ probabilities = softmax_no_cast(scores, dim=-1) # (..., h, n_q, n_k)
144
+ return torch.matmul(probabilities, value) # (..., h, n_q, d_h)
145
 
146
 
147
  @torch.compiler.disable
 
155
  ) -> torch.Tensor:
156
  """Call the optional cuEquivariance triangle-attention primitive."""
157
 
158
+ # q/k/v: (..., h, n, d_h); returned tensor uses the kernel's matching layout.
159
  if (
160
  find_spec("cuequivariance_torch") is None
161
  or find_spec("cuequivariance_ops_torch") is None
 
205
  """Project Q, K, and V to ``(..., h, n, d)`` tensors."""
206
 
207
  def split_heads(X: torch.Tensor) -> torch.Tensor:
208
+ # X: (..., n, h * d_h)
209
+ return X.view(*X.shape[:-1], self.no_heads, -1).transpose(
210
+ -2, -3
211
+ ) # (..., h, n, d_h)
212
+
213
+ # q_x: (..., n_q, c_q); kv_x: (..., n_k, c_k).
214
+ q = split_heads(self.linear_q(q_x)) # (..., h, n_q, d_h)
215
+ k = split_heads(self.linear_k(kv_x)) # (..., h, n_k, d_h)
216
+ v = split_heads(self.linear_v(kv_x)) # (..., h, n_k, d_h)
217
  if apply_scale:
218
+ q /= math.sqrt(self.c_hidden) # (..., h, n_q, d_h)
219
+ return q, k, v # (..., h, n_q, d_h), two (..., h, n_k, d_h)
220
 
221
  def _wrap_up(self, output: torch.Tensor, q_x: torch.Tensor) -> torch.Tensor:
222
  """Apply the optional gate, merge heads, and project the update."""
223
 
224
+ # output: (..., n_q, h, d_h); q_x: (..., n_q, c_q).
225
  if self.linear_g is not None:
226
+ gate = self.sigmoid(self.linear_g(q_x)) # (..., n_q, h * d_h)
227
+ gate = gate.view(
228
+ *gate.shape[:-1], self.no_heads, -1
229
+ ) # (..., n_q, h, d_h)
230
+ output = output * gate # (..., n_q, h, d_h)
231
+ return self.linear_o(
232
+ flatten_final_dims(output, 2)
233
+ ) # (..., n_q, c_q)
234
 
235
  def forward(
236
  self,
 
243
  ) -> torch.Tensor:
244
  """Return a gated pair update with shape ``(..., n, c_q)``."""
245
 
246
+ q, k, v = self._prep_qkv(
247
+ q_x, kv_x, apply_scale=not use_kernels
248
+ ) # q: (..., h, n_q, d_h); k/v: (..., h, n_k, d_h)
249
  if use_kernels:
250
  output = kernel_triangular_attn(
251
  q,
 
254
  tri_bias=tri_bias,
255
  mask=mask.bool(),
256
  scale=1.0 / math.sqrt(self.c_hidden),
257
+ ) # (..., h, n_q, d_h)
258
  else:
259
+ output = _attention(
260
+ q, k, v, (mask_bias, tri_bias)
261
+ ) # (..., h, n_q, d_h)
262
+ return self._wrap_up(
263
+ output.transpose(-2, -3), q_x
264
+ ) # (..., n_q, c_q)
265
+ # tensor: (...); softmax preserves shape.
fastplms/models/boltz/vb_tri_attn_utils.py CHANGED
@@ -7,21 +7,21 @@ the public function names expected by converted checkpoints.
7
 
8
  from __future__ import annotations
9
 
 
10
  from collections.abc import Callable, Sequence
11
  from functools import partial
12
  from math import prod
13
  from typing import Any
14
 
15
- import torch
16
-
17
 
18
  def add(left: torch.Tensor, right: torch.Tensor, inplace: bool) -> torch.Tensor:
19
  """Add ``right`` to ``left``, optionally reusing ``left`` storage."""
20
 
 
21
  if inplace:
22
- left += right
23
- return left
24
- return left + right
25
 
26
 
27
  def permute_final_dims(tensor: torch.Tensor, inds: Sequence[int]) -> torch.Tensor:
@@ -30,7 +30,7 @@ def permute_final_dims(tensor: torch.Tensor, inds: Sequence[int]) -> torch.Tenso
30
  final_count = len(inds)
31
  leading = list(range(tensor.ndim - final_count))
32
  final = [tensor.ndim - final_count + index for index in inds]
33
- return tensor.permute(leading + final)
34
 
35
 
36
  def is_fp16_enabled() -> bool:
@@ -73,7 +73,8 @@ tensor_tree_map = partial(tree_map, leaf_type=torch.Tensor)
73
  def flatten_final_dims(tensor: torch.Tensor, no_dims: int) -> torch.Tensor:
74
  """Collapse the final ``no_dims`` axes into one axis."""
75
 
76
- return tensor.reshape(*tensor.shape[:-no_dims], -1)
 
77
 
78
 
79
  def _fetch_dims(tree: Any) -> list[torch.Size]:
@@ -194,9 +195,13 @@ def _chunk_slice(
194
  batch_shape = tuple(tensor.shape[:no_batch_dims])
195
  start = _flat_idx_to_idx(flat_start, batch_shape)
196
  end = _flat_idx_to_idx(flat_end - 1, batch_shape)
197
- pieces = [tensor[item] for item in _get_minimal_slice_set(start, end, batch_shape)]
 
 
198
  feature_shape = tuple(tensor.shape[no_batch_dims:])
199
- return torch.cat([piece.reshape(-1, *feature_shape) for piece in pieces])
 
 
200
 
201
 
202
  def _prepare_input(
@@ -206,12 +211,15 @@ def _prepare_input(
206
  *,
207
  low_mem: bool,
208
  ) -> torch.Tensor:
 
209
  feature_shape = tuple(tensor.shape[no_batch_dims:])
210
  if low_mem:
211
- return tensor.expand(batch_shape + feature_shape)
212
  if any(size != 1 for size in tensor.shape[:no_batch_dims]):
213
- tensor = tensor.expand(batch_shape + feature_shape)
214
- return tensor.reshape(-1, *feature_shape)
 
 
215
 
216
 
217
  def _write_chunk(
@@ -245,9 +253,9 @@ def _write_chunk(
245
  if not isinstance(source, torch.Tensor):
246
  raise ValueError(f"output type {type(source)!r} is not supported")
247
  if add_into_out:
248
- destination[start:stop] += source
249
  else:
250
- destination[start:stop] = source
251
 
252
 
253
  def chunk_layer(
@@ -274,14 +282,16 @@ def chunk_layer(
274
  no_batch_dims=no_batch_dims,
275
  low_mem=low_mem,
276
  )
277
- prepared_inputs = tensor_tree_map(prepare, inputs)
 
 
278
 
279
  output = None
280
  if _out is not None:
281
  output = tensor_tree_map(
282
  lambda tensor: tensor.reshape(-1, *tensor.shape[no_batch_dims:]),
283
  _out,
284
- )
285
 
286
  flat_batch_size = prod(batch_shape)
287
  for start in range(0, flat_batch_size, chunk_size):
@@ -302,14 +312,18 @@ def chunk_layer(
302
  ) -> torch.Tensor:
303
  return tensor if tensor.shape[0] == 1 else tensor[start:stop]
304
 
305
- input_chunk = tensor_tree_map(select, prepared_inputs)
306
- output_chunk = layer(**input_chunk)
 
 
 
 
307
 
308
  if output is None:
309
  output = tensor_tree_map(
310
  lambda tensor: tensor.new_zeros((flat_batch_size, *tensor.shape[1:])),
311
  output_chunk,
312
- )
313
  _write_chunk(
314
  output,
315
  output_chunk,
@@ -321,4 +335,6 @@ def chunk_layer(
321
  return tensor_tree_map(
322
  lambda tensor: tensor.reshape(batch_shape + tuple(tensor.shape[1:])),
323
  output,
324
- )
 
 
 
7
 
8
  from __future__ import annotations
9
 
10
+ import torch
11
  from collections.abc import Callable, Sequence
12
  from functools import partial
13
  from math import prod
14
  from typing import Any
15
 
 
 
16
 
17
  def add(left: torch.Tensor, right: torch.Tensor, inplace: bool) -> torch.Tensor:
18
  """Add ``right`` to ``left``, optionally reusing ``left`` storage."""
19
 
20
+ # left/right: broadcast-compatible shapes; result has their broadcast shape.
21
  if inplace:
22
+ left += right # same shape as left
23
+ return left # same shape as left
24
+ return left + right # broadcast shape
25
 
26
 
27
  def permute_final_dims(tensor: torch.Tensor, inds: Sequence[int]) -> torch.Tensor:
 
30
  final_count = len(inds)
31
  leading = list(range(tensor.ndim - final_count))
32
  final = [tensor.ndim - final_count + index for index in inds]
33
+ return tensor.permute(leading + final) # (..., d_0, ..., d_n)
34
 
35
 
36
  def is_fp16_enabled() -> bool:
 
73
  def flatten_final_dims(tensor: torch.Tensor, no_dims: int) -> torch.Tensor:
74
  """Collapse the final ``no_dims`` axes into one axis."""
75
 
76
+ # tensor: (..., d_0, ..., d_n)
77
+ return tensor.reshape(*tensor.shape[:-no_dims], -1) # (..., prod(final dims))
78
 
79
 
80
  def _fetch_dims(tree: Any) -> list[torch.Size]:
 
195
  batch_shape = tuple(tensor.shape[:no_batch_dims])
196
  start = _flat_idx_to_idx(flat_start, batch_shape)
197
  end = _flat_idx_to_idx(flat_end - 1, batch_shape)
198
+ pieces = [
199
+ tensor[item] for item in _get_minimal_slice_set(start, end, batch_shape)
200
+ ] # each: (*covered_batch_shape, *feature_shape)
201
  feature_shape = tuple(tensor.shape[no_batch_dims:])
202
+ return torch.cat(
203
+ [piece.reshape(-1, *feature_shape) for piece in pieces]
204
+ ) # (flat_end - flat_start, *feature_shape)
205
 
206
 
207
  def _prepare_input(
 
211
  *,
212
  low_mem: bool,
213
  ) -> torch.Tensor:
214
+ # tensor: (*input_batch_shape, *feature_shape)
215
  feature_shape = tuple(tensor.shape[no_batch_dims:])
216
  if low_mem:
217
+ return tensor.expand(batch_shape + feature_shape) # (*batch_shape, *feature_shape)
218
  if any(size != 1 for size in tensor.shape[:no_batch_dims]):
219
+ tensor = tensor.expand(
220
+ batch_shape + feature_shape
221
+ ) # (*batch_shape, *feature_shape)
222
+ return tensor.reshape(-1, *feature_shape) # (flat_batch, *feature_shape)
223
 
224
 
225
  def _write_chunk(
 
253
  if not isinstance(source, torch.Tensor):
254
  raise ValueError(f"output type {type(source)!r} is not supported")
255
  if add_into_out:
256
+ destination[start:stop] += source # (stop - start, *feature_shape)
257
  else:
258
+ destination[start:stop] = source # (stop - start, *feature_shape)
259
 
260
 
261
  def chunk_layer(
 
282
  no_batch_dims=no_batch_dims,
283
  low_mem=low_mem,
284
  )
285
+ prepared_inputs = tensor_tree_map(
286
+ prepare, inputs
287
+ ) # each tensor: (flat_batch, *feature_shape), or broadcast batch in low-memory mode
288
 
289
  output = None
290
  if _out is not None:
291
  output = tensor_tree_map(
292
  lambda tensor: tensor.reshape(-1, *tensor.shape[no_batch_dims:]),
293
  _out,
294
+ ) # each tensor: (flat_batch, *feature_shape)
295
 
296
  flat_batch_size = prod(batch_shape)
297
  for start in range(0, flat_batch_size, chunk_size):
 
312
  ) -> torch.Tensor:
313
  return tensor if tensor.shape[0] == 1 else tensor[start:stop]
314
 
315
+ input_chunk = tensor_tree_map(
316
+ select, prepared_inputs
317
+ ) # each tensor: (chunk, *feature_shape)
318
+ output_chunk = layer(
319
+ **input_chunk
320
+ ) # each output tensor: (chunk, *output_feature_shape)
321
 
322
  if output is None:
323
  output = tensor_tree_map(
324
  lambda tensor: tensor.new_zeros((flat_batch_size, *tensor.shape[1:])),
325
  output_chunk,
326
+ ) # each tensor: (flat_batch, *output_feature_shape)
327
  _write_chunk(
328
  output,
329
  output_chunk,
 
335
  return tensor_tree_map(
336
  lambda tensor: tensor.reshape(batch_shape + tuple(tensor.shape[1:])),
337
  output,
338
+ ) # each tensor: (*batch_shape, *output_feature_shape)
339
+ # tensor: (..., d_0, ..., d_n); only the named final dimensions are reordered.
340
+ # tensor: (*batch_shape, *feature_shape)
fastplms/registry.py CHANGED
@@ -18,6 +18,7 @@ from types import MappingProxyType
18
  from typing import Any, Literal, cast
19
  from urllib.parse import urlparse
20
 
 
21
  _HEX_RE = re.compile(r"^[0-9a-f]+$")
22
  _IDENTIFIER_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
23
  _HUB_LICENSE_NAME_RE = re.compile(r"[^a-z0-9.]+")
@@ -559,20 +560,20 @@ def _require_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple
559
  value = table.get(key)
560
  if not isinstance(value, list) or not value or any(not isinstance(item, str) for item in value):
561
  raise RegistryError(f"{context}.{key} must be a non-empty string array.")
562
- result = tuple(value)
563
- if len(set(result)) != len(result):
564
  raise RegistryError(f"{context}.{key} contains duplicate values.")
565
- return result
566
 
567
 
568
  def _optional_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
569
  value = table.get(key, [])
570
  if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
571
  raise RegistryError(f"{context}.{key} must be a string array.")
572
- result = tuple(value)
573
- if len(set(result)) != len(result):
574
  raise RegistryError(f"{context}.{key} contains duplicate values.")
575
- return result
576
 
577
 
578
  def _optional_str(table: Mapping[str, Any], key: str, context: str) -> str | None:
@@ -657,11 +658,11 @@ def _require_digest_list(
657
  table: Mapping[str, Any], key: str, context: str
658
  ) -> tuple[FileDigest, ...]:
659
  encoded = _require_str_list(table, key, context)
660
- result = tuple(FileDigest.parse(value) for value in encoded)
661
- paths = [item.path for item in result]
662
  if len(paths) != len(set(paths)):
663
  raise RegistryError(f"{context}.{key} contains duplicate paths.")
664
- return result
665
 
666
 
667
  def _validate_revision(revision: str, context: str) -> None:
@@ -701,7 +702,7 @@ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[Oracle
701
  raw = table.get("oracle_assets", [])
702
  if not isinstance(raw, list):
703
  raise RegistryError(f"{context}.oracle_assets must be an array of tables.")
704
- result: list[OracleAsset] = []
705
  for index, value in enumerate(raw):
706
  asset_context = f"{context}.oracle_assets[{index}]"
707
  if not isinstance(value, dict):
@@ -736,7 +737,7 @@ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[Oracle
736
  size = value.get("size")
737
  if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
738
  raise RegistryError(f"{asset_context}.size must be a positive byte count.")
739
- result.append(
740
  OracleAsset(
741
  role=role,
742
  path=path,
@@ -745,16 +746,16 @@ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[Oracle
745
  size=size,
746
  )
747
  )
748
- roles = [asset.role for asset in result]
749
- paths = [asset.path for asset in result]
750
- urls = [asset.url for asset in result]
751
  if (
752
  len(roles) != len(set(roles))
753
  or len(paths) != len(set(paths))
754
  or len(urls) != len(set(urls))
755
  ):
756
  raise RegistryError(f"{context}.oracle_assets contains duplicate identities.")
757
- return tuple(result)
758
 
759
 
760
  def _parse_official_golden(
@@ -789,7 +790,7 @@ def _parse_official_golden(
789
  def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
790
  if not isinstance(raw, list) or not raw:
791
  raise RegistryError("The manifest must contain [[attention_kernels]] entries.")
792
- result: dict[str, AttentionKernelSpec] = {}
793
  expected_variants = {
794
  "flash_attention_2": "flash_attn2",
795
  "flash_attention_3": "flash_attn3",
@@ -812,7 +813,7 @@ def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
812
  implementation = _require_str(value, "implementation", context)
813
  if implementation not in expected_variants:
814
  raise RegistryError(f"Unsupported attention kernel {implementation!r}.")
815
- if implementation in result:
816
  raise RegistryError(f"Duplicate attention kernel {implementation!r}.")
817
  repository = _require_str(value, "repository", context)
818
  if _REPOSITORY_ID_RE.fullmatch(repository) is None:
@@ -834,7 +835,7 @@ def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
834
  dtypes = _require_str_list(value, "dtypes", context)
835
  if not set(dtypes).issubset(_ALLOWED_DTYPES):
836
  raise RegistryError(f"{context}.dtypes contains unsupported dtypes.")
837
- result[implementation] = AttentionKernelSpec(
838
  implementation=implementation,
839
  repository=repository,
840
  revision=revision,
@@ -842,15 +843,15 @@ def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
842
  expected_variant=expected_variant,
843
  dtypes=cast(tuple[DtypeName, ...], dtypes),
844
  )
845
- if set(result) != set(expected_variants):
846
  raise RegistryError("The manifest must pin both FlashAttention kernel versions.")
847
- return result
848
 
849
 
850
  def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
851
  if not isinstance(raw, list) or not raw:
852
  raise RegistryError("The manifest must contain at least one [[upstreams]] entry.")
853
- result: dict[str, UpstreamSource] = {}
854
  paths: set[str] = set()
855
  for index, value in enumerate(raw):
856
  context = f"upstreams[{index}]"
@@ -860,7 +861,7 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
860
  source_id = _require_str(value, "id", context)
861
  if _IDENTIFIER_RE.fullmatch(source_id) is None:
862
  raise RegistryError(f"Invalid upstream ID: {source_id!r}")
863
- if source_id in result:
864
  raise RegistryError(f"Duplicate upstream ID: {source_id!r}")
865
  revision = _require_str(value, "revision", context)
866
  _validate_revision(revision, f"{context}.revision")
@@ -913,7 +914,7 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
913
  missing_e1 = sorted(required_e1.difference(distribution_map))
914
  if missing_e1:
915
  raise RegistryError(f"{context} is missing E1 legal files: {missing_e1}")
916
- result[source_id] = UpstreamSource(
917
  id=source_id,
918
  path=path,
919
  url=url,
@@ -923,7 +924,7 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
923
  license_digests=license_digests,
924
  distribution_files=distribution_files,
925
  )
926
- return result
927
 
928
 
929
  def _parse_families(
@@ -932,7 +933,7 @@ def _parse_families(
932
  ) -> dict[str, ModelFamily]:
933
  if not isinstance(raw, dict) or not raw:
934
  raise RegistryError("The manifest must contain [families.<id>] tables.")
935
- result: dict[str, ModelFamily] = {}
936
  for family_id, value in raw.items():
937
  context = f"families.{family_id}"
938
  if _IDENTIFIER_RE.fullmatch(family_id) is None or not isinstance(value, dict):
@@ -1068,7 +1069,7 @@ def _parse_families(
1068
  f"{context}.conversion_provenance must identify {state_transform!r} and "
1069
  f"contain mechanism-first sections; missing {missing_sections}."
1070
  )
1071
- result[family_id] = ModelFamily(
1072
  id=family_id,
1073
  architecture=_require_str(value, "architecture", context),
1074
  upstreams=source_ids,
@@ -1099,7 +1100,7 @@ def _parse_families(
1099
  conversion_provenance=conversion_provenance,
1100
  backbone_model=backbone_model,
1101
  )
1102
- return result
1103
 
1104
 
1105
  def _parse_runtime_assets(
@@ -1108,7 +1109,7 @@ def _parse_runtime_assets(
1108
  ) -> dict[str, RuntimeAsset]:
1109
  if not isinstance(raw, list) or not raw:
1110
  raise RegistryError("The manifest must contain at least one [[runtime_assets]] entry.")
1111
- result: dict[str, RuntimeAsset] = {}
1112
  identities: set[tuple[str, str, str]] = set()
1113
  for index, value in enumerate(raw):
1114
  context = f"runtime_assets[{index}]"
@@ -1118,7 +1119,7 @@ def _parse_runtime_assets(
1118
  asset_id = _require_str(value, "id", context)
1119
  if _IDENTIFIER_RE.fullmatch(asset_id) is None:
1120
  raise RegistryError(f"Invalid runtime asset ID: {asset_id!r}")
1121
- if asset_id in result:
1122
  raise RegistryError(f"Duplicate runtime asset ID: {asset_id!r}")
1123
  repository = _require_str(value, "repository", context)
1124
  if _REPOSITORY_ID_RE.fullmatch(repository) is None:
@@ -1164,7 +1165,7 @@ def _parse_runtime_assets(
1164
  if identity in identities:
1165
  raise RegistryError(f"Duplicate runtime asset identity: {identity!r}")
1166
  identities.add(identity)
1167
- result[asset_id] = RuntimeAsset(
1168
  id=asset_id,
1169
  repository=repository,
1170
  revision=revision,
@@ -1176,7 +1177,7 @@ def _parse_runtime_assets(
1176
  license_expression=license_expression,
1177
  offline_behavior=offline_behavior,
1178
  )
1179
- return result
1180
 
1181
 
1182
  def _parse_models(
@@ -1185,7 +1186,7 @@ def _parse_models(
1185
  ) -> dict[str, ModelSpec]:
1186
  if not isinstance(raw, list) or not raw:
1187
  raise RegistryError("The manifest must contain at least one [[models]] entry.")
1188
- result: dict[str, ModelSpec] = {}
1189
  fast_repositories: set[str] = set()
1190
  for index, value in enumerate(raw):
1191
  context = f"models[{index}]"
@@ -1195,7 +1196,7 @@ def _parse_models(
1195
  model_id = _require_str(value, "id", context)
1196
  if _IDENTIFIER_RE.fullmatch(model_id) is None:
1197
  raise RegistryError(f"Invalid model ID: {model_id!r}")
1198
- if model_id in result:
1199
  raise RegistryError(f"Duplicate model ID: {model_id!r}")
1200
  family_id = _require_str(value, "family", context)
1201
  if family_id not in families:
@@ -1278,7 +1279,7 @@ def _parse_models(
1278
  if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
1279
  raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
1280
  auto_map.append((auto_class, class_path))
1281
- result[model_id] = ModelSpec(
1282
  id=model_id,
1283
  family=family,
1284
  fast=fast,
@@ -1294,7 +1295,7 @@ def _parse_models(
1294
  notes=notes,
1295
  msa_conditioning=msa_conditioning,
1296
  )
1297
- return result
1298
 
1299
 
1300
  def _validate_registry(
@@ -1409,21 +1410,21 @@ def _validate_registry(
1409
 
1410
  def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
1411
  try:
1412
- data = tomllib.loads(raw_bytes.decode("utf-8"))
1413
  except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
1414
  raise RegistryError(f"Unable to parse model manifest: {error}") from error
1415
- _reject_unknown_fields(data, _ROOT_FIELDS, "manifest")
1416
- if data.get("schema_version") != 1:
1417
  raise RegistryError("Unsupported model manifest schema_version; expected 1.")
1418
- legal_files = _require_digest_list(data, "legal_files", "manifest")
1419
  required_legal_paths = {"LICENSE", "THIRD_PARTY_NOTICES.md"}
1420
  if {item.path for item in legal_files} != required_legal_paths:
1421
  raise RegistryError("manifest.legal_files must contain LICENSE and THIRD_PARTY_NOTICES.md.")
1422
- attention_kernels = _parse_attention_kernels(data.get("attention_kernels"))
1423
- upstreams = _parse_upstreams(data.get("upstreams"))
1424
- families = _parse_families(data.get("families"), upstreams)
1425
- runtime_assets = _parse_runtime_assets(data.get("runtime_assets"), families)
1426
- models = _parse_models(data.get("models"), families)
1427
  _validate_registry(upstreams, attention_kernels, families, models)
1428
  return ModelRegistry(
1429
  schema_version=1,
 
18
  from typing import Any, Literal, cast
19
  from urllib.parse import urlparse
20
 
21
+
22
  _HEX_RE = re.compile(r"^[0-9a-f]+$")
23
  _IDENTIFIER_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
24
  _HUB_LICENSE_NAME_RE = re.compile(r"[^a-z0-9.]+")
 
560
  value = table.get(key)
561
  if not isinstance(value, list) or not value or any(not isinstance(item, str) for item in value):
562
  raise RegistryError(f"{context}.{key} must be a non-empty string array.")
563
+ strings = tuple(value)
564
+ if len(set(strings)) != len(strings):
565
  raise RegistryError(f"{context}.{key} contains duplicate values.")
566
+ return strings
567
 
568
 
569
  def _optional_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
570
  value = table.get(key, [])
571
  if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
572
  raise RegistryError(f"{context}.{key} must be a string array.")
573
+ strings = tuple(value)
574
+ if len(set(strings)) != len(strings):
575
  raise RegistryError(f"{context}.{key} contains duplicate values.")
576
+ return strings
577
 
578
 
579
  def _optional_str(table: Mapping[str, Any], key: str, context: str) -> str | None:
 
658
  table: Mapping[str, Any], key: str, context: str
659
  ) -> tuple[FileDigest, ...]:
660
  encoded = _require_str_list(table, key, context)
661
+ digests = tuple(FileDigest.parse(value) for value in encoded)
662
+ paths = [item.path for item in digests]
663
  if len(paths) != len(set(paths)):
664
  raise RegistryError(f"{context}.{key} contains duplicate paths.")
665
+ return digests
666
 
667
 
668
  def _validate_revision(revision: str, context: str) -> None:
 
702
  raw = table.get("oracle_assets", [])
703
  if not isinstance(raw, list):
704
  raise RegistryError(f"{context}.oracle_assets must be an array of tables.")
705
+ assets: list[OracleAsset] = []
706
  for index, value in enumerate(raw):
707
  asset_context = f"{context}.oracle_assets[{index}]"
708
  if not isinstance(value, dict):
 
737
  size = value.get("size")
738
  if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
739
  raise RegistryError(f"{asset_context}.size must be a positive byte count.")
740
+ assets.append(
741
  OracleAsset(
742
  role=role,
743
  path=path,
 
746
  size=size,
747
  )
748
  )
749
+ roles = [asset.role for asset in assets]
750
+ paths = [asset.path for asset in assets]
751
+ urls = [asset.url for asset in assets]
752
  if (
753
  len(roles) != len(set(roles))
754
  or len(paths) != len(set(paths))
755
  or len(urls) != len(set(urls))
756
  ):
757
  raise RegistryError(f"{context}.oracle_assets contains duplicate identities.")
758
+ return tuple(assets)
759
 
760
 
761
  def _parse_official_golden(
 
790
  def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
791
  if not isinstance(raw, list) or not raw:
792
  raise RegistryError("The manifest must contain [[attention_kernels]] entries.")
793
+ kernels: dict[str, AttentionKernelSpec] = {}
794
  expected_variants = {
795
  "flash_attention_2": "flash_attn2",
796
  "flash_attention_3": "flash_attn3",
 
813
  implementation = _require_str(value, "implementation", context)
814
  if implementation not in expected_variants:
815
  raise RegistryError(f"Unsupported attention kernel {implementation!r}.")
816
+ if implementation in kernels:
817
  raise RegistryError(f"Duplicate attention kernel {implementation!r}.")
818
  repository = _require_str(value, "repository", context)
819
  if _REPOSITORY_ID_RE.fullmatch(repository) is None:
 
835
  dtypes = _require_str_list(value, "dtypes", context)
836
  if not set(dtypes).issubset(_ALLOWED_DTYPES):
837
  raise RegistryError(f"{context}.dtypes contains unsupported dtypes.")
838
+ kernels[implementation] = AttentionKernelSpec(
839
  implementation=implementation,
840
  repository=repository,
841
  revision=revision,
 
843
  expected_variant=expected_variant,
844
  dtypes=cast(tuple[DtypeName, ...], dtypes),
845
  )
846
+ if set(kernels) != set(expected_variants):
847
  raise RegistryError("The manifest must pin both FlashAttention kernel versions.")
848
+ return kernels
849
 
850
 
851
  def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
852
  if not isinstance(raw, list) or not raw:
853
  raise RegistryError("The manifest must contain at least one [[upstreams]] entry.")
854
+ upstreams: dict[str, UpstreamSource] = {}
855
  paths: set[str] = set()
856
  for index, value in enumerate(raw):
857
  context = f"upstreams[{index}]"
 
861
  source_id = _require_str(value, "id", context)
862
  if _IDENTIFIER_RE.fullmatch(source_id) is None:
863
  raise RegistryError(f"Invalid upstream ID: {source_id!r}")
864
+ if source_id in upstreams:
865
  raise RegistryError(f"Duplicate upstream ID: {source_id!r}")
866
  revision = _require_str(value, "revision", context)
867
  _validate_revision(revision, f"{context}.revision")
 
914
  missing_e1 = sorted(required_e1.difference(distribution_map))
915
  if missing_e1:
916
  raise RegistryError(f"{context} is missing E1 legal files: {missing_e1}")
917
+ upstreams[source_id] = UpstreamSource(
918
  id=source_id,
919
  path=path,
920
  url=url,
 
924
  license_digests=license_digests,
925
  distribution_files=distribution_files,
926
  )
927
+ return upstreams
928
 
929
 
930
  def _parse_families(
 
933
  ) -> dict[str, ModelFamily]:
934
  if not isinstance(raw, dict) or not raw:
935
  raise RegistryError("The manifest must contain [families.<id>] tables.")
936
+ families: dict[str, ModelFamily] = {}
937
  for family_id, value in raw.items():
938
  context = f"families.{family_id}"
939
  if _IDENTIFIER_RE.fullmatch(family_id) is None or not isinstance(value, dict):
 
1069
  f"{context}.conversion_provenance must identify {state_transform!r} and "
1070
  f"contain mechanism-first sections; missing {missing_sections}."
1071
  )
1072
+ families[family_id] = ModelFamily(
1073
  id=family_id,
1074
  architecture=_require_str(value, "architecture", context),
1075
  upstreams=source_ids,
 
1100
  conversion_provenance=conversion_provenance,
1101
  backbone_model=backbone_model,
1102
  )
1103
+ return families
1104
 
1105
 
1106
  def _parse_runtime_assets(
 
1109
  ) -> dict[str, RuntimeAsset]:
1110
  if not isinstance(raw, list) or not raw:
1111
  raise RegistryError("The manifest must contain at least one [[runtime_assets]] entry.")
1112
+ runtime_assets: dict[str, RuntimeAsset] = {}
1113
  identities: set[tuple[str, str, str]] = set()
1114
  for index, value in enumerate(raw):
1115
  context = f"runtime_assets[{index}]"
 
1119
  asset_id = _require_str(value, "id", context)
1120
  if _IDENTIFIER_RE.fullmatch(asset_id) is None:
1121
  raise RegistryError(f"Invalid runtime asset ID: {asset_id!r}")
1122
+ if asset_id in runtime_assets:
1123
  raise RegistryError(f"Duplicate runtime asset ID: {asset_id!r}")
1124
  repository = _require_str(value, "repository", context)
1125
  if _REPOSITORY_ID_RE.fullmatch(repository) is None:
 
1165
  if identity in identities:
1166
  raise RegistryError(f"Duplicate runtime asset identity: {identity!r}")
1167
  identities.add(identity)
1168
+ runtime_assets[asset_id] = RuntimeAsset(
1169
  id=asset_id,
1170
  repository=repository,
1171
  revision=revision,
 
1177
  license_expression=license_expression,
1178
  offline_behavior=offline_behavior,
1179
  )
1180
+ return runtime_assets
1181
 
1182
 
1183
  def _parse_models(
 
1186
  ) -> dict[str, ModelSpec]:
1187
  if not isinstance(raw, list) or not raw:
1188
  raise RegistryError("The manifest must contain at least one [[models]] entry.")
1189
+ models: dict[str, ModelSpec] = {}
1190
  fast_repositories: set[str] = set()
1191
  for index, value in enumerate(raw):
1192
  context = f"models[{index}]"
 
1196
  model_id = _require_str(value, "id", context)
1197
  if _IDENTIFIER_RE.fullmatch(model_id) is None:
1198
  raise RegistryError(f"Invalid model ID: {model_id!r}")
1199
+ if model_id in models:
1200
  raise RegistryError(f"Duplicate model ID: {model_id!r}")
1201
  family_id = _require_str(value, "family", context)
1202
  if family_id not in families:
 
1279
  if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
1280
  raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
1281
  auto_map.append((auto_class, class_path))
1282
+ models[model_id] = ModelSpec(
1283
  id=model_id,
1284
  family=family,
1285
  fast=fast,
 
1295
  notes=notes,
1296
  msa_conditioning=msa_conditioning,
1297
  )
1298
+ return models
1299
 
1300
 
1301
  def _validate_registry(
 
1410
 
1411
  def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
1412
  try:
1413
+ manifest = tomllib.loads(raw_bytes.decode("utf-8"))
1414
  except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
1415
  raise RegistryError(f"Unable to parse model manifest: {error}") from error
1416
+ _reject_unknown_fields(manifest, _ROOT_FIELDS, "manifest")
1417
+ if manifest.get("schema_version") != 1:
1418
  raise RegistryError("Unsupported model manifest schema_version; expected 1.")
1419
+ legal_files = _require_digest_list(manifest, "legal_files", "manifest")
1420
  required_legal_paths = {"LICENSE", "THIRD_PARTY_NOTICES.md"}
1421
  if {item.path for item in legal_files} != required_legal_paths:
1422
  raise RegistryError("manifest.legal_files must contain LICENSE and THIRD_PARTY_NOTICES.md.")
1423
+ attention_kernels = _parse_attention_kernels(manifest.get("attention_kernels"))
1424
+ upstreams = _parse_upstreams(manifest.get("upstreams"))
1425
+ families = _parse_families(manifest.get("families"), upstreams)
1426
+ runtime_assets = _parse_runtime_assets(manifest.get("runtime_assets"), families)
1427
+ models = _parse_models(manifest.get("models"), families)
1428
  _validate_registry(upstreams, attention_kernels, families, models)
1429
  return ModelRegistry(
1430
  schema_version=1,
fastplms/runtime.py CHANGED
@@ -11,6 +11,7 @@ from contextlib import contextmanager
11
  from dataclasses import dataclass
12
  from typing import TYPE_CHECKING, Literal
13
 
 
14
  if TYPE_CHECKING:
15
  from collections.abc import Iterator
16
 
 
11
  from dataclasses import dataclass
12
  from typing import TYPE_CHECKING, Literal
13
 
14
+
15
  if TYPE_CHECKING:
16
  from collections.abc import Iterator
17
 
fastplms_bundle.py CHANGED
The diff for this file is too large to render. See raw diff
 
modeling_fastplms.py CHANGED
@@ -1,4 +1,4 @@
1
- """Generated bridge to the unchanged FastPLMs package sources."""
2
 
3
  import base64
4
  import hashlib
@@ -6,14 +6,13 @@ import importlib
6
  import importlib.util
7
  import sys
8
  import tempfile
9
- from importlib.metadata import PackageNotFoundError, distribution
10
  from io import BytesIO
11
  from pathlib import Path
12
  from zipfile import ZIP_DEFLATED, ZipFile
13
 
14
  from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
15
 
16
- if RUNTIME_HASH != "e6e0f8c8632238f40a5112f22397a9a211eb02d3415c1c93d43305aef51f7728":
17
  raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
18
 
19
  _RUNTIME_TEMPORARIES = []
@@ -83,24 +82,6 @@ def _runtime_file_hashes(package_root):
83
  result[relative.as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
84
  return result
85
 
86
- def _installed_runtime_digest(installed_root, relative):
87
- candidate = installed_root / relative
88
- if candidate.is_file():
89
- return hashlib.sha256(candidate.read_bytes()).hexdigest()
90
- if relative != "kernels.lock":
91
- return None
92
- try:
93
- installed_distribution = distribution("fastplms")
94
- except PackageNotFoundError:
95
- return None
96
- for entry in installed_distribution.files or ():
97
- normalized = str(entry).replace("\\", "/")
98
- if normalized.endswith(".dist-info/kernels.lock"):
99
- lock_path = Path(installed_distribution.locate_file(entry))
100
- if lock_path.is_file():
101
- return hashlib.sha256(lock_path.read_bytes()).hexdigest()
102
- return None
103
-
104
  def _extend_loaded_package_paths(package_root):
105
  for name, module in list(sys.modules.items()):
106
  if name != "fastplms" and not name.startswith("fastplms."):
@@ -114,29 +95,14 @@ def _extend_loaded_package_paths(package_root):
114
  if candidate.is_dir() and candidate_text not in paths:
115
  paths.append(candidate_text)
116
 
117
- def _merge_runtime(installed, package_root):
118
  incoming = _runtime_file_hashes(package_root)
119
- known = dict(getattr(installed, "__fastplms_artifact_runtime_files__", {}))
120
- installed_root_text = getattr(
121
- installed, "__fastplms_artifact_installed_root__", None
122
- )
123
- if not known:
124
- installed_file = getattr(installed, "__file__", None)
125
- if installed_file is None:
126
- raise RuntimeError(
127
- "The loaded fastplms package has no source path and cannot be verified "
128
- "against the embedded artifact runtime."
129
- )
130
- installed_root = Path(installed_file).resolve().parent
131
- for relative, digest in incoming.items():
132
- if _installed_runtime_digest(installed_root, relative) != digest:
133
- raise RuntimeError(
134
- "The installed FastPLMs runtime differs from this artifact at "
135
- f"{relative!r}. Install the artifact's matching FastPLMs release "
136
- "or use a separate Python process."
137
- )
138
- installed_root_text = str(installed_root)
139
- installed.__fastplms_artifact_installed_root__ = installed_root_text
140
  conflicts = sorted(
141
  relative
142
  for relative, digest in incoming.items()
@@ -148,35 +114,25 @@ def _merge_runtime(installed, package_root):
148
  + ", ".join(repr(path) for path in conflicts[:5])
149
  + ". Load incompatible releases in separate Python processes."
150
  )
151
- if installed_root_text is not None:
152
- installed_root = Path(installed_root_text)
153
- for relative, digest in incoming.items():
154
- if relative in known:
155
- continue
156
- if _installed_runtime_digest(installed_root, relative) != digest:
157
- raise RuntimeError(
158
- "The installed FastPLMs runtime differs from this artifact at "
159
- f"{relative!r}. Install the artifact's matching FastPLMs release "
160
- "or use a separate Python process."
161
- )
162
  known.update(incoming)
163
- installed.__fastplms_artifact_runtime_files__ = known
164
- roots = list(getattr(installed, "__fastplms_artifact_runtime_roots__", ()))
165
  if str(package_root) not in roots:
166
  roots.append(str(package_root))
167
- installed.__fastplms_artifact_runtime_roots__ = tuple(roots)
168
  temporaries = list(
169
- getattr(installed, "__fastplms_artifact_runtime_temporaries__", ())
170
  )
171
  for temporary in _RUNTIME_TEMPORARIES:
172
  if temporary not in temporaries:
173
  temporaries.append(temporary)
174
- installed.__fastplms_artifact_runtime_temporaries__ = tuple(temporaries)
175
- hashes = set(getattr(installed, "__fastplms_artifact_runtime_hashes__", ()))
176
  hashes.add(RUNTIME_HASH)
177
- installed.__fastplms_artifact_runtime_hashes__ = frozenset(hashes)
178
  _extend_loaded_package_paths(package_root)
179
- return installed
180
 
181
  def _import_without_bytecode(module_name):
182
  previous = sys.dont_write_bytecode
@@ -187,13 +143,13 @@ def _import_without_bytecode(module_name):
187
  sys.dont_write_bytecode = previous
188
 
189
  def _install_runtime():
190
- installed = sys.modules.get("fastplms")
191
- hashes = getattr(installed, "__fastplms_artifact_runtime_hashes__", ())
192
  if RUNTIME_HASH in hashes:
193
- return installed
194
  package_root = _ensure_runtime()
195
- if installed is not None:
196
- return _merge_runtime(installed, package_root)
197
  spec = importlib.util.spec_from_file_location(
198
  "fastplms",
199
  package_root / "__init__.py",
@@ -223,8 +179,8 @@ def _install_runtime():
223
  return package
224
 
225
  _install_runtime()
226
- _module_225 = _import_without_bytecode("fastplms.models.boltz.modeling_boltz2")
227
- Boltz2Config = _module_225.Boltz2Config
228
  Boltz2Config.__module__ = __name__
229
- Boltz2Model = _module_225.Boltz2Model
230
  Boltz2Model.__module__ = __name__
 
1
+ """Generated bridge to the embedded FastPLMs runtime sources."""
2
 
3
  import base64
4
  import hashlib
 
6
  import importlib.util
7
  import sys
8
  import tempfile
 
9
  from io import BytesIO
10
  from pathlib import Path
11
  from zipfile import ZIP_DEFLATED, ZipFile
12
 
13
  from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
14
 
15
+ if RUNTIME_HASH != "3aebb85d11edb01329d2aafb4c8260bd67339785f0f252582246ae7c33206143":
16
  raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
17
 
18
  _RUNTIME_TEMPORARIES = []
 
82
  result[relative.as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
83
  return result
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def _extend_loaded_package_paths(package_root):
86
  for name, module in list(sys.modules.items()):
87
  if name != "fastplms" and not name.startswith("fastplms."):
 
95
  if candidate.is_dir() and candidate_text not in paths:
96
  paths.append(candidate_text)
97
 
98
+ def _merge_runtime(package, package_root):
99
  incoming = _runtime_file_hashes(package_root)
100
+ known = getattr(package, "__fastplms_artifact_runtime_files__", None)
101
+ if not isinstance(known, dict):
102
+ raise RuntimeError(
103
+ "A non-artifact fastplms module is already loaded. Load the Hub artifact "
104
+ "in a separate Python process."
105
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  conflicts = sorted(
107
  relative
108
  for relative, digest in incoming.items()
 
114
  + ", ".join(repr(path) for path in conflicts[:5])
115
  + ". Load incompatible releases in separate Python processes."
116
  )
117
+ known = dict(known)
 
 
 
 
 
 
 
 
 
 
118
  known.update(incoming)
119
+ package.__fastplms_artifact_runtime_files__ = known
120
+ roots = list(getattr(package, "__fastplms_artifact_runtime_roots__", ()))
121
  if str(package_root) not in roots:
122
  roots.append(str(package_root))
123
+ package.__fastplms_artifact_runtime_roots__ = tuple(roots)
124
  temporaries = list(
125
+ getattr(package, "__fastplms_artifact_runtime_temporaries__", ())
126
  )
127
  for temporary in _RUNTIME_TEMPORARIES:
128
  if temporary not in temporaries:
129
  temporaries.append(temporary)
130
+ package.__fastplms_artifact_runtime_temporaries__ = tuple(temporaries)
131
+ hashes = set(getattr(package, "__fastplms_artifact_runtime_hashes__", ()))
132
  hashes.add(RUNTIME_HASH)
133
+ package.__fastplms_artifact_runtime_hashes__ = frozenset(hashes)
134
  _extend_loaded_package_paths(package_root)
135
+ return package
136
 
137
  def _import_without_bytecode(module_name):
138
  previous = sys.dont_write_bytecode
 
143
  sys.dont_write_bytecode = previous
144
 
145
  def _install_runtime():
146
+ package = sys.modules.get("fastplms")
147
+ hashes = getattr(package, "__fastplms_artifact_runtime_hashes__", ())
148
  if RUNTIME_HASH in hashes:
149
+ return package
150
  package_root = _ensure_runtime()
151
+ if package is not None:
152
+ return _merge_runtime(package, package_root)
153
  spec = importlib.util.spec_from_file_location(
154
  "fastplms",
155
  package_root / "__init__.py",
 
179
  return package
180
 
181
  _install_runtime()
182
+ _module_181 = _import_without_bytecode("fastplms.models.boltz.modeling_boltz2")
183
+ Boltz2Config = _module_181.Boltz2Config
184
  Boltz2Config.__module__ = __name__
185
+ Boltz2Model = _module_181.Boltz2Model
186
  Boltz2Model.__module__ = __name__
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Direct runtime dependencies for Synthyra/Boltz2.
2
+ # FastPLMs source is embedded in this model repository.
3
+ torch>=2.13,<2.14
4
+ transformers>=5.13,<5.14
5
+ huggingface-hub>=0.34,<2
6
+ tokenizers>=0.22,<0.23
7
+ safetensors>=0.5,<1
8
+ numpy>=1.26,<3
9
+ einops>=0.8,<1
10
+ tqdm>=4.67,<5
11
+ accelerate>=1.10,<2
12
+ biopython>=1.85,<2
13
+ biotite>=1.4,<2
14
+ brotli>=1.1,<2
15
+ msgpack>=1.1,<2
16
+ msgpack-numpy>=0.4.8,<1
17
+ omegaconf>=2.3,<3
18
+ rdkit>=2025.9,<2027
19
+ scipy>=1.15,<2
20
+ zstandard>=0.23,<1
runtime-attestation.json CHANGED
@@ -2,56 +2,57 @@
2
  "files": {
3
  "LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
4
  "LICENSES/boltz/LICENSE": "sha256:f0667fd5e66c51e1ba8ddaa0249c6d7225b30037e02c45782d8f2c2943ac2617",
5
- "README.md": "sha256:f12be28984a29a0257a997ac948faa35766e2914613af35c653d9772325e737c",
6
  "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
7
- "config.json": "sha256:7f24cc83dab019d998f24fdeffe46526c01f52b041527c9ae17bf66570f7040d",
8
- "fastplms/__init__.py": "sha256:4fb3196022ca8ec699d59d09bdbc5f0184195552b773698ab9b061fe3cd7df12",
9
  "fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
10
- "fastplms/models/__init__.py": "sha256:5e48c2cb3877aa6f42f3b5411d53b16bba2e32827bbde634f47f174c5cb36f86",
11
- "fastplms/models/boltz/__init__.py": "sha256:54f7ad44ddbb3fd160382a50bb5d5a8a5b86cb66978b7d72445194fc21caa673",
12
- "fastplms/models/boltz/_pair_attention.py": "sha256:13d16831664ac7d0bfc859e2d4b97d523f1c2ae0fe4a0b11f65b9a3197dffccd",
13
- "fastplms/models/boltz/cif_writer.py": "sha256:095eeab50ee8b97fe0160b4c1f9c6df2be1412a1de13a1d8ad00f9847032a7ab",
14
- "fastplms/models/boltz/minimal_featurizer.py": "sha256:35e5f4f85d24d5ba8d952ee64f1ddb98e9ce5245f364d231da185ae8a3b28ce1",
15
  "fastplms/models/boltz/minimal_structures.py": "sha256:df7f1ec765ddbb5a730edb9f304fe312a5d3c431ae564ca9b97e6537e0ebf476",
16
- "fastplms/models/boltz/modeling_boltz2.py": "sha256:c0d00a63a23762853257a4a4c718f7a4ded358c900562ca260ed3596a113d934",
17
  "fastplms/models/boltz/vb_const.py": "sha256:67d3cfdb7f0facdb217fe9220404d598ac7924a672f1a2720f7d2d82b23270df",
18
- "fastplms/models/boltz/vb_layers_attention.py": "sha256:ad09126f467b9c73f764b6258ae3f2bce9f9752f641c0347052f12548abdfe4b",
19
- "fastplms/models/boltz/vb_layers_attentionv2.py": "sha256:407b1ce628d53c3e0b6f801d989b0ae59bd87d9f9e5d1155e61c1e1ec5a78e0f",
20
- "fastplms/models/boltz/vb_layers_confidence_utils.py": "sha256:61b024671ca2eaaa08eb6ddfd75f374f8db97ef6ef75ffcfc8c10347f4fd239a",
21
- "fastplms/models/boltz/vb_layers_dropout.py": "sha256:61db64dc4e6be29a41bad2705f73cc489dcf3227cc6e8914c1ea9e6f45ba57a5",
22
- "fastplms/models/boltz/vb_layers_initialize.py": "sha256:c1f5a5d56843438bd11639778eed06352e3d4995d2d12c967865d1221208048c",
23
- "fastplms/models/boltz/vb_layers_outer_product_mean.py": "sha256:60478bea7d9d6d7f518ab1461c37b91e42d9fa0ab8cfcd777be88c1ddbfff07e",
24
- "fastplms/models/boltz/vb_layers_pair_averaging.py": "sha256:e626502b549348ecf26077d0cb1f88f37970b6b9987a6780b4599c5a0b1c0ada",
25
- "fastplms/models/boltz/vb_layers_pairformer.py": "sha256:fb056b76231366a1a93373ff2effddde887768dcfca1c9ebc46a567ba39eacb2",
26
- "fastplms/models/boltz/vb_layers_transition.py": "sha256:62d306f33a600d08f8bb60a1a8cb9a00a253f81c05ef8c2af3f4fb7936ea8c18",
27
- "fastplms/models/boltz/vb_layers_triangular_mult.py": "sha256:ace40becff882828ae3aa2b4f77e1a3d854d2adf7180216013996e38dd0e6fdd",
28
- "fastplms/models/boltz/vb_loss_diffusionv2.py": "sha256:3778aea2ac541a3c173d9cc89b0d1b2cff63555571e0b3776b98c746daa0720c",
29
  "fastplms/models/boltz/vb_modules_confidencev2.py": "sha256:a80a6f711e9ea021c45d89967fd37f7e668eea9ab209f9c70f2562d695a15041",
30
- "fastplms/models/boltz/vb_modules_diffusion_conditioning.py": "sha256:a3e94f4493c056e54691b8289cbc35de3408b41bcbc7a75a8786eb1c273c1c31",
31
  "fastplms/models/boltz/vb_modules_diffusionv2.py": "sha256:0077b40a72d9bf19904e0ab04fdb78b2c642ec6169b688732a1d221aa20a3637",
32
- "fastplms/models/boltz/vb_modules_encodersv2.py": "sha256:9e78b92db67911795eb7691c7538edd12153ba30f4918ff8abf9874a99d02943",
33
- "fastplms/models/boltz/vb_modules_transformersv2.py": "sha256:3eb5790030c333452beddeb3fe3583e65529a70f0e443d9d44304f7447de48f6",
34
- "fastplms/models/boltz/vb_modules_trunkv2.py": "sha256:355cf735d57393167273b3be1d802ef495f69f8d20900e4cd2ce9b2df2147c25",
35
- "fastplms/models/boltz/vb_modules_utils.py": "sha256:7a8f348cb8c703c6fef98312bea1beb179ceae5ae00eadc6962780a722171d05",
36
- "fastplms/models/boltz/vb_potentials_potentials.py": "sha256:f332824228c5fabca51dd4695334e4a2822c6c36cca99dfc0c7f659eaf6062a3",
37
  "fastplms/models/boltz/vb_potentials_schedules.py": "sha256:91997ee4b7a2a5ba1f40475c9e4fe6a58927f5ef7b51012743615b0f05e278d1",
38
- "fastplms/models/boltz/vb_tri_attn_attention.py": "sha256:3c599f47575bfa762afd1b2fa70d6135ad66af72bfff8b703803d1f34f861080",
39
- "fastplms/models/boltz/vb_tri_attn_primitives.py": "sha256:99b149ed2775b2d58baf4d5b4be414cfb2183234a0f46567ac6a37d9327909e7",
40
- "fastplms/models/boltz/vb_tri_attn_utils.py": "sha256:df831665ee4bf485c5f95b3bc2a04dcb37aa002b03c42417036fc866e87622f1",
41
- "fastplms/registry.py": "sha256:afca271911b651a882345b74a58366494a1d784e4a48c8683a87f5508f4ba16e",
42
- "fastplms/runtime.py": "sha256:110018646d6f248cedab140a030c3065e1b062b61f6aff659c231e538614bc01",
43
- "fastplms_bundle.py": "sha256:f55c37e173eca40cfcd7e38ba7cdc2d0b15f6b846e9b478d7eff146ac15d3da7",
44
- "modeling_fastplms.py": "sha256:f5defd6c4657ff805c1e835e51b2ef4becca9a21e255df767bd210ef34f808e5"
 
45
  },
46
  "model_id": "boltz2",
47
  "redistributable": true,
48
- "release_tool_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
49
- "release_tool_sha256": "19b4247aba04a74de5069397518374ab2c3b9e0ce18ba9f36de34f0f4c5d1ceb",
50
- "runtime_bundle_sha256": "e6e0f8c8632238f40a5112f22397a9a211eb02d3415c1c93d43305aef51f7728",
51
- "runtime_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
52
  "schema_version": 2,
53
  "scope": "runtime-only",
54
- "source_tree_sha256": "78061b1bd37ba2dda853641fba681fdf943b859503e4fb21b1a7508909c369bf",
55
  "weights": {
56
  "repo_id": "Synthyra/Boltz2",
57
  "revision": "3b148fc5efea109c065ec82ba8683d024de7134e"
 
2
  "files": {
3
  "LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
4
  "LICENSES/boltz/LICENSE": "sha256:f0667fd5e66c51e1ba8ddaa0249c6d7225b30037e02c45782d8f2c2943ac2617",
5
+ "README.md": "sha256:4d8e31396f39d7b2e1f5b131a4d688c391091531477e896005b2b28f418ef8e5",
6
  "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
7
+ "config.json": "sha256:cefe82f26d2a25cbff6958749f3e8727efdb42e8a5eae8c8677498482f45933a",
8
+ "fastplms/__init__.py": "sha256:8503e02debf24abc6d33c1913ff3eb9f245e63f7e62017e220d072c7393fdf2c",
9
  "fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
10
+ "fastplms/models/__init__.py": "sha256:d3dd084f5e8fafcf2d0fe4a26e4ab9284d3f9414b1f9a5bd86e308a406447c43",
11
+ "fastplms/models/boltz/__init__.py": "sha256:ef2bad434c2cb2dd171051ce13765ac364651839748670ad2affb5ac816256e9",
12
+ "fastplms/models/boltz/_pair_attention.py": "sha256:da466a398fd30427758c937d49c570dc5319f792bf954091e51ded1c0b51e0ff",
13
+ "fastplms/models/boltz/cif_writer.py": "sha256:f15329f21a0dc070b564dfd95114f145ef638ecd93024d62f342b3b1b26b0334",
14
+ "fastplms/models/boltz/minimal_featurizer.py": "sha256:e7647e2cb62e7608fdfce986d773609ae3a749c63818039bb1c4f77edb5f8415",
15
  "fastplms/models/boltz/minimal_structures.py": "sha256:df7f1ec765ddbb5a730edb9f304fe312a5d3c431ae564ca9b97e6537e0ebf476",
16
+ "fastplms/models/boltz/modeling_boltz2.py": "sha256:98fa1c9d307b9595ccd53021ac678293c7fecaf7ea12e851b107fc369dc172b5",
17
  "fastplms/models/boltz/vb_const.py": "sha256:67d3cfdb7f0facdb217fe9220404d598ac7924a672f1a2720f7d2d82b23270df",
18
+ "fastplms/models/boltz/vb_layers_attention.py": "sha256:c0fc4b0b5b9a38d56f18af85d0ed506e0c45826d8d4c797da4a281dc35801390",
19
+ "fastplms/models/boltz/vb_layers_attentionv2.py": "sha256:f0509a5be8ee19c3d01797317bdcb6caaf1d37bde695bd6e6be33d06e19e46d7",
20
+ "fastplms/models/boltz/vb_layers_confidence_utils.py": "sha256:ee802fa8c364a38b17dfd0c141bc0b87ac03c8731e8029129756511aa7194e5b",
21
+ "fastplms/models/boltz/vb_layers_dropout.py": "sha256:e962d7fd699c27808f0348ccfc57b719e801ac443328c2fcd799a9cd44753241",
22
+ "fastplms/models/boltz/vb_layers_initialize.py": "sha256:e05108acaf92497f02d6a5567862f4bfe53265ac8f0117d7263dfa0e396ddac3",
23
+ "fastplms/models/boltz/vb_layers_outer_product_mean.py": "sha256:e435b827c110ae54bde63b0a184b65fe3b87cebdd50f3cea6d39e4818ce324da",
24
+ "fastplms/models/boltz/vb_layers_pair_averaging.py": "sha256:73b93eaa790f77d03bf68200ed505d3b2a40afd0f2d806709ecc2c277fe58b13",
25
+ "fastplms/models/boltz/vb_layers_pairformer.py": "sha256:932566fbffd07ab817af1cd5d2b71de98f8ab2bf9c0f6b270e21bf6adca2bb99",
26
+ "fastplms/models/boltz/vb_layers_transition.py": "sha256:e3aad7419fbe86b833e5f2991a779d12fc99d329f797a3b33ce836d8a4abb2e7",
27
+ "fastplms/models/boltz/vb_layers_triangular_mult.py": "sha256:13f55152ad8478ca6fc1dcc5c56bb9028ba8f4a030e5402001399e9359cb643e",
28
+ "fastplms/models/boltz/vb_loss_diffusionv2.py": "sha256:77137ab5da4393af127c3022c43ca4721906cba0c3cdc1d2b6b73fd70352c573",
29
  "fastplms/models/boltz/vb_modules_confidencev2.py": "sha256:a80a6f711e9ea021c45d89967fd37f7e668eea9ab209f9c70f2562d695a15041",
30
+ "fastplms/models/boltz/vb_modules_diffusion_conditioning.py": "sha256:dcbabd880c2682e910973512b875fc43e08ddf1a98ee13ab0b5ada01ca5a71d4",
31
  "fastplms/models/boltz/vb_modules_diffusionv2.py": "sha256:0077b40a72d9bf19904e0ab04fdb78b2c642ec6169b688732a1d221aa20a3637",
32
+ "fastplms/models/boltz/vb_modules_encodersv2.py": "sha256:135a20ce20c7fda10b92f2770464cd81be4899a3960ae4f5ef0a96dc6f6df105",
33
+ "fastplms/models/boltz/vb_modules_transformersv2.py": "sha256:22a77bdb8af6ee1235369cbe0f0c987eba74ae5cbf10b2fdfcfc5ad1b081aae1",
34
+ "fastplms/models/boltz/vb_modules_trunkv2.py": "sha256:cbd222f1c939f7d1728c5de97535ab1b940205dbdc23bb1895e71d405568d192",
35
+ "fastplms/models/boltz/vb_modules_utils.py": "sha256:10a69588abb92424007971a5c5bb91294a07d3a0850932f7145276d132a73041",
36
+ "fastplms/models/boltz/vb_potentials_potentials.py": "sha256:ca2aacf0c2e5b8de13c5c6456967fb4d9b1cd72f74e13584e9248bbfc8b1a918",
37
  "fastplms/models/boltz/vb_potentials_schedules.py": "sha256:91997ee4b7a2a5ba1f40475c9e4fe6a58927f5ef7b51012743615b0f05e278d1",
38
+ "fastplms/models/boltz/vb_tri_attn_attention.py": "sha256:70afafa17bb4c6fa1151a188dfe8a635b1224a6d814bf16b0a4d2949edbdd29a",
39
+ "fastplms/models/boltz/vb_tri_attn_primitives.py": "sha256:3d4beefb52ee69ee98422c3d9058f8e394c2125fc029e79eb959eee9d0eea4ce",
40
+ "fastplms/models/boltz/vb_tri_attn_utils.py": "sha256:bd0566f3c6d5e36d7e6c1a3f79a95fa2e06c1d18d554d4f9331a7a69ac7ab2e1",
41
+ "fastplms/registry.py": "sha256:cb37799dae5a4c0c780089ab2cca4cc56300c1c5f515b275892aa4e1c4b97eb6",
42
+ "fastplms/runtime.py": "sha256:9521c37fcd168bf9a4137277fb73672403ccf30d70d4845aa879fb22f42ddeb6",
43
+ "fastplms_bundle.py": "sha256:e23521271a92290a4da0a66314fba4a3c499826b802817c16340922f85e02421",
44
+ "modeling_fastplms.py": "sha256:39540ac66a63879fac53c2cf365f4d378bc9f81814f7ea6c4261c41a75e789e0",
45
+ "requirements.txt": "sha256:bf8969fb385af9448d39a9574a024d20e1e65609d60c28b5b3e58e6bf55bc351"
46
  },
47
  "model_id": "boltz2",
48
  "redistributable": true,
49
+ "release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
50
+ "release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
51
+ "runtime_bundle_sha256": "3aebb85d11edb01329d2aafb4c8260bd67339785f0f252582246ae7c33206143",
52
+ "runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
53
  "schema_version": 2,
54
  "scope": "runtime-only",
55
+ "source_tree_sha256": "9d2fc9586cfbd1d2d2cc49d745311acb81d4e3740e59ead09e633917589806a8",
56
  "weights": {
57
  "repo_id": "Synthyra/Boltz2",
58
  "revision": "3b148fc5efea109c065ec82ba8683d024de7134e"