lhallee commited on
Commit
c65e212
·
verified ·
1 Parent(s): 3b148fc

Update FastPLMs runtime files

Browse files

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

Files changed (43) hide show
  1. LICENSES/FastPLMs-Apache-2.0.txt +203 -0
  2. LICENSES/boltz/LICENSE +21 -0
  3. README.md +143 -164
  4. THIRD_PARTY_NOTICES.md +99 -0
  5. config.json +12 -2
  6. fastplms/__init__.py +48 -0
  7. fastplms/models.toml +1223 -0
  8. fastplms/models/__init__.py +10 -0
  9. fastplms/models/boltz/__init__.py +13 -0
  10. fastplms/models/boltz/_pair_attention.py +46 -0
  11. fastplms/models/boltz/cif_writer.py +141 -0
  12. fastplms/models/boltz/minimal_featurizer.py +786 -0
  13. fastplms/models/boltz/minimal_structures.py +19 -0
  14. fastplms/models/boltz/modeling_boltz2.py +993 -0
  15. fastplms/models/boltz/vb_const.py +353 -0
  16. fastplms/models/boltz/vb_layers_attention.py +98 -0
  17. fastplms/models/boltz/vb_layers_attentionv2.py +73 -0
  18. fastplms/models/boltz/vb_layers_confidence_utils.py +226 -0
  19. fastplms/models/boltz/vb_layers_dropout.py +40 -0
  20. fastplms/models/boltz/vb_layers_initialize.py +98 -0
  21. fastplms/models/boltz/vb_layers_outer_product_mean.py +87 -0
  22. fastplms/models/boltz/vb_layers_pair_averaging.py +91 -0
  23. fastplms/models/boltz/vb_layers_pairformer.py +337 -0
  24. fastplms/models/boltz/vb_layers_transition.py +69 -0
  25. fastplms/models/boltz/vb_layers_triangular_mult.py +125 -0
  26. fastplms/models/boltz/vb_loss_diffusionv2.py +178 -0
  27. fastplms/models/boltz/vb_modules_confidencev2.py +542 -0
  28. fastplms/models/boltz/vb_modules_diffusion_conditioning.py +137 -0
  29. fastplms/models/boltz/vb_modules_diffusionv2.py +654 -0
  30. fastplms/models/boltz/vb_modules_encodersv2.py +577 -0
  31. fastplms/models/boltz/vb_modules_transformersv2.py +254 -0
  32. fastplms/models/boltz/vb_modules_trunkv2.py +595 -0
  33. fastplms/models/boltz/vb_modules_utils.py +257 -0
  34. fastplms/models/boltz/vb_potentials_potentials.py +845 -0
  35. fastplms/models/boltz/vb_potentials_schedules.py +67 -0
  36. fastplms/models/boltz/vb_tri_attn_attention.py +145 -0
  37. fastplms/models/boltz/vb_tri_attn_primitives.py +237 -0
  38. fastplms/models/boltz/vb_tri_attn_utils.py +324 -0
  39. fastplms/registry.py +1486 -0
  40. fastplms/runtime.py +68 -0
  41. fastplms_bundle.py +0 -0
  42. modeling_fastplms.py +230 -0
  43. runtime-attestation.json +60 -0
LICENSES/FastPLMs-Apache-2.0.txt ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PLEASE NOTE THE APACHE LICENSE ONLY APPLIES TO THE CODE IN THE FastPLMs GITHUB AND ASSOCIATED HUGGINGFACE REPOSITORIES, NOT NECESSARILY THE MODEL WEIGHTS. THOSE LICENSES CAN BE FOUND HERE https://github.com/Synthyra/FastPLMs/tree/main/LICENSES
2
+
3
+ Apache License
4
+ Version 2.0, January 2004
5
+ http://www.apache.org/licenses/
6
+
7
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8
+
9
+ 1. Definitions.
10
+
11
+ "License" shall mean the terms and conditions for use, reproduction,
12
+ and distribution as defined by Sections 1 through 9 of this document.
13
+
14
+ "Licensor" shall mean the copyright owner or entity authorized by
15
+ the copyright owner that is granting the License.
16
+
17
+ "Legal Entity" shall mean the union of the acting entity and all
18
+ other entities that control, are controlled by, or are under common
19
+ control with that entity. For the purposes of this definition,
20
+ "control" means (i) the power, direct or indirect, to cause the
21
+ direction or management of such entity, whether by contract or
22
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
23
+ outstanding shares, or (iii) beneficial ownership of such entity.
24
+
25
+ "You" (or "Your") shall mean an individual or Legal Entity
26
+ exercising permissions granted by this License.
27
+
28
+ "Source" form shall mean the preferred form for making modifications,
29
+ including but not limited to software source code, documentation
30
+ source, and configuration files.
31
+
32
+ "Object" form shall mean any form resulting from mechanical
33
+ transformation or translation of a Source form, including but
34
+ not limited to compiled object code, generated documentation,
35
+ and conversions to other media types.
36
+
37
+ "Work" shall mean the work of authorship, whether in Source or
38
+ Object form, made available under the License, as indicated by a
39
+ copyright notice that is included in or attached to the work
40
+ (an example is provided in the Appendix below).
41
+
42
+ "Derivative Works" shall mean any work, whether in Source or Object
43
+ form, that is based on (or derived from) the Work and for which the
44
+ editorial revisions, annotations, elaborations, or other modifications
45
+ represent, as a whole, an original work of authorship. For the purposes
46
+ of this License, Derivative Works shall not include works that remain
47
+ separable from, or merely link (or bind by name) to the interfaces of,
48
+ the Work and Derivative Works thereof.
49
+
50
+ "Contribution" shall mean any work of authorship, including
51
+ the original version of the Work and any modifications or additions
52
+ to that Work or Derivative Works thereof, that is intentionally
53
+ submitted to Licensor for inclusion in the Work by the copyright owner
54
+ or by an individual or Legal Entity authorized to submit on behalf of
55
+ the copyright owner. For the purposes of this definition, "submitted"
56
+ means any form of electronic, verbal, or written communication sent
57
+ to the Licensor or its representatives, including but not limited to
58
+ communication on electronic mailing lists, source code control systems,
59
+ and issue tracking systems that are managed by, or on behalf of, the
60
+ Licensor for the purpose of discussing and improving the Work, but
61
+ excluding communication that is conspicuously marked or otherwise
62
+ designated in writing by the copyright owner as "Not a Contribution."
63
+
64
+ "Contributor" shall mean Licensor and any individual or Legal Entity
65
+ on behalf of whom a Contribution has been received by Licensor and
66
+ subsequently incorporated within the Work.
67
+
68
+ 2. Grant of Copyright License. Subject to the terms and conditions of
69
+ this License, each Contributor hereby grants to You a perpetual,
70
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
71
+ copyright license to reproduce, prepare Derivative Works of,
72
+ publicly display, publicly perform, sublicense, and distribute the
73
+ Work and such Derivative Works in Source or Object form.
74
+
75
+ 3. Grant of Patent License. Subject to the terms and conditions of
76
+ this License, each Contributor hereby grants to You a perpetual,
77
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
78
+ (except as stated in this section) patent license to make, have made,
79
+ use, offer to sell, sell, import, and otherwise transfer the Work,
80
+ where such license applies only to those patent claims licensable
81
+ by such Contributor that are necessarily infringed by their
82
+ Contribution(s) alone or by combination of their Contribution(s)
83
+ with the Work to which such Contribution(s) was submitted. If You
84
+ institute patent litigation against any entity (including a
85
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
86
+ or a Contribution incorporated within the Work constitutes direct
87
+ or contributory patent infringement, then any patent licenses
88
+ granted to You under this License for that Work shall terminate
89
+ as of the date such litigation is filed.
90
+
91
+ 4. Redistribution. You may reproduce and distribute copies of the
92
+ Work or Derivative Works thereof in any medium, with or without
93
+ modifications, and in Source or Object form, provided that You
94
+ meet the following conditions:
95
+
96
+ (a) You must give any other recipients of the Work or
97
+ Derivative Works a copy of this License; and
98
+
99
+ (b) You must cause any modified files to carry prominent notices
100
+ stating that You changed the files; and
101
+
102
+ (c) You must retain, in the Source form of any Derivative Works
103
+ that You distribute, all copyright, patent, trademark, and
104
+ attribution notices from the Source form of the Work,
105
+ excluding those notices that do not pertain to any part of
106
+ the Derivative Works; and
107
+
108
+ (d) If the Work includes a "NOTICE" text file as part of its
109
+ distribution, then any Derivative Works that You distribute must
110
+ include a readable copy of the attribution notices contained
111
+ within such NOTICE file, excluding those notices that do not
112
+ pertain to any part of the Derivative Works, in at least one
113
+ of the following places: within a NOTICE text file distributed
114
+ as part of the Derivative Works; within the Source form or
115
+ documentation, if provided along with the Derivative Works; or,
116
+ within a display generated by the Derivative Works, if and
117
+ wherever such third-party notices normally appear. The contents
118
+ of the NOTICE file are for informational purposes only and
119
+ do not modify the License. You may add Your own attribution
120
+ notices within Derivative Works that You distribute, alongside
121
+ or as an addendum to the NOTICE text from the Work, provided
122
+ that such additional attribution notices cannot be construed
123
+ as modifying the License.
124
+
125
+ You may add Your own copyright statement to Your modifications and
126
+ may provide additional or different license terms and conditions
127
+ for use, reproduction, or distribution of Your modifications, or
128
+ for any such Derivative Works as a whole, provided Your use,
129
+ reproduction, and distribution of the Work otherwise complies with
130
+ the conditions stated in this License.
131
+
132
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
133
+ any Contribution intentionally submitted for inclusion in the Work
134
+ by You to the Licensor shall be under the terms and conditions of
135
+ this License, without any additional terms or conditions.
136
+ Notwithstanding the above, nothing herein shall supersede or modify
137
+ the terms of any separate license agreement you may have executed
138
+ with Licensor regarding such Contributions.
139
+
140
+ 6. Trademarks. This License does not grant permission to use the trade
141
+ names, trademarks, service marks, or product names of the Licensor,
142
+ except as required for reasonable and customary use in describing the
143
+ origin of the Work and reproducing the content of the NOTICE file.
144
+
145
+ 7. Disclaimer of Warranty. Unless required by applicable law or
146
+ agreed to in writing, Licensor provides the Work (and each
147
+ Contributor provides its Contributions) on an "AS IS" BASIS,
148
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
149
+ implied, including, without limitation, any warranties or conditions
150
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
151
+ PARTICULAR PURPOSE. You are solely responsible for determining the
152
+ appropriateness of using or redistributing the Work and assume any
153
+ risks associated with Your exercise of permissions under this License.
154
+
155
+ 8. Limitation of Liability. In no event and under no legal theory,
156
+ whether in tort (including negligence), contract, or otherwise,
157
+ unless required by applicable law (such as deliberate and grossly
158
+ negligent acts) or agreed to in writing, shall any Contributor be
159
+ liable to You for damages, including any direct, indirect, special,
160
+ incidental, or consequential damages of any character arising as a
161
+ result of this License or out of the use or inability to use the
162
+ Work (including but not limited to damages for loss of goodwill,
163
+ work stoppage, computer failure or malfunction, or any and all
164
+ other commercial damages or losses), even if such Contributor
165
+ has been advised of the possibility of such damages.
166
+
167
+ 9. Accepting Warranty or Additional Liability. While redistributing
168
+ the Work or Derivative Works thereof, You may choose to offer,
169
+ and charge a fee for, acceptance of support, warranty, indemnity,
170
+ or other liability obligations and/or rights consistent with this
171
+ License. However, in accepting such obligations, You may act only
172
+ on Your own behalf and on Your sole responsibility, not on behalf
173
+ of any other Contributor, and only if You agree to indemnify,
174
+ defend, and hold each Contributor harmless for any liability
175
+ incurred by, or claims asserted against, such Contributor by reason
176
+ of your accepting any such warranty or additional liability.
177
+
178
+ END OF TERMS AND CONDITIONS
179
+
180
+ APPENDIX: How to apply the Apache License to your work.
181
+
182
+ To apply the Apache License to your work, attach the following
183
+ boilerplate notice, with the fields enclosed by brackets "[]"
184
+ replaced with your own identifying information. (Don't include
185
+ the brackets!) The text should be enclosed in the appropriate
186
+ comment syntax for the file format. We also recommend that a
187
+ file or class name and description of purpose be included on the
188
+ same "printed page" as the copyright notice for easier
189
+ identification within third-party archives.
190
+
191
+ Copyright [yyyy] [name of copyright owner]
192
+
193
+ Licensed under the Apache License, Version 2.0 (the "License");
194
+ you may not use this file except in compliance with the License.
195
+ You may obtain a copy of the License at
196
+
197
+ http://www.apache.org/licenses/LICENSE-2.0
198
+
199
+ Unless required by applicable law or agreed to in writing, software
200
+ distributed under the License is distributed on an "AS IS" BASIS,
201
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
202
+ See the License for the specific language governing permissions and
203
+ limitations under the License.
LICENSES/boltz/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jeremy Wohlwend, Gabriele Corso, Saro Passaro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,164 +1,143 @@
1
- ---
2
- library_name: transformers
3
- tags: []
4
- ---
5
-
6
- # NOTE
7
- The GitHub with the implementation and requirements can be found [here](https://github.com/Synthyra/FastPLMs.git).
8
-
9
- # Boltz2 AutoModel (Inference-only)
10
- This is a barebones Huggingface `AutoModel` compatible implementation of Boltz2 focused on fast inference workflows.
11
-
12
- The implementation is located in `fastplms/boltz/` and exposes:
13
- - `Boltz2Config`
14
- - `Boltz2Model`
15
- - `predict_structure(amino_acid_sequence, ...)`
16
- - `save_as_cif(structure_output, output_path, ...)`
17
-
18
- ## Design goals
19
- - Inference-only (no training hooks, no Lightning trainer usage).
20
- - Lightweight runtime around `torch` + `transformers` (plus `numpy`).
21
- - AutoModel remote-code compatibility via `trust_remote_code=True`.
22
- - Confidence outputs included in prediction outputs (`plddt`, `ptm`, `iptm`, and derived confidence score when available).
23
-
24
- ## Runtime note
25
- This implementation is self-contained inside `fastplms/boltz/` and does not require
26
- the original cloned `boltz` package at runtime.
27
-
28
- ## Use with transformers
29
-
30
- ### Load from an exported directory
31
- ```python
32
- import torch
33
- from transformers import AutoModel
34
-
35
- model = AutoModel.from_pretrained(
36
- "Synthyra/Boltz2",
37
- trust_remote_code=True,
38
- dtype=torch.float32,
39
- ).eval()
40
- ```
41
-
42
- ### Predict structure from sequence
43
- ```python
44
- out = model.predict_structure(
45
- amino_acid_sequence="MSTNPKPQRKTKRNTNRRPQDVKFPGG",
46
- recycling_steps=3,
47
- num_sampling_steps=200,
48
- diffusion_samples=1,
49
- )
50
-
51
- print(out.sample_atom_coords.shape)
52
- print(None if out.plddt is None else out.plddt.shape)
53
- ```
54
-
55
- ### Save CIF
56
- ```python
57
- model.save_as_cif(out, "prediction.cif")
58
- ```
59
-
60
- ## Convert Boltz checkpoint to HF export
61
- Use:
62
-
63
- ```bash
64
- py -m fastplms.boltz.get_weights --checkpoint_path fastplms/boltz/weights/boltz2_conf.ckpt --output_dir boltz2_automodel_export
65
- ```
66
-
67
- The export directory contains:
68
- - `config.json`
69
- - `pytorch_model.bin`
70
- - `modeling_boltz2.py`
71
- - `minimal_featurizer.py`
72
- - `minimal_structures.py`
73
- - `cif_writer.py`
74
- - `vb_*.py` (self-contained vendored Boltz2 inference modules/constants)
75
-
76
- ## Output object fields
77
- `predict_structure(...)` returns `Boltz2StructureOutput` with:
78
- - `sample_atom_coords`
79
- - `atom_pad_mask`
80
- - `plddt`
81
- - `complex_plddt`
82
- - `ptm`
83
- - `iptm`
84
- - `confidence_score` (derived when available)
85
- - `raw_output`
86
-
87
- ## Limitations
88
- - Current featurization path is protein-only and minimal.
89
- - This implementation is meant for practical inference and export workflows, not full Boltz training parity.
90
- - Test-time training is not supported for Boltz2 in FastPLMs. TTT is currently limited to sequence PLMs plus ESMFold and ESMFold2 PLM backbones.
91
-
92
- ## Docker-first compliance testing
93
-
94
- Build the container at repo root:
95
-
96
- ```bash
97
- docker build -t fastplms-test -f Dockerfile .
98
- ```
99
-
100
- Launch a test shell:
101
-
102
- ```bash
103
- docker run --rm --gpus all -it -v ${PWD}:/workspace fastplms-test bash
104
- ```
105
-
106
- Inside the container, run Boltz2 compliance against pip `boltz`:
107
-
108
- ```bash
109
- python -m testing.run_boltz2_compliance --device cuda --dtype float32 --seed 42 --num-sequences 3 --recycling-steps 3 --num-sampling-steps 200 --diffusion-samples 1 --pass-coord-metric aligned --write-cif-artifacts
110
- ```
111
-
112
- Artifacts are written to `testing/results/<timestamp>/boltz2_compliance/` by default:
113
- - `metrics.json`
114
- - `metrics.csv`
115
- - `summary.txt`
116
- - `structures/seq_<idx>/ours_seq<idx>.cif`
117
- - `structures/seq_<idx>/ref_seq<idx>.cif`
118
-
119
- Coordinate metrics now include both raw and rigid-aligned variants:
120
- - `coord_mae`, `coord_rmse`, `coord_max_abs` (raw frame-dependent deltas)
121
- - `coord_mae_aligned`, `coord_rmse_aligned`, `coord_max_abs_aligned` (Kabsch aligned)
122
- - `pairwise_dist_mae` (frame-invariant pairwise-distance delta)
123
-
124
- Pass/fail uses `--pass-coord-metric aligned` by default. Set `--pass-coord-metric raw` to use the raw coordinate thresholds.
125
-
126
- ## Citations
127
-
128
- ```bibtex
129
- @misc{FastPLMs,
130
- author={Hallee, Logan and Bichara, David and Gleghorn, Jason P.},
131
- title={FastPLMs: Fast, efficient, protein language model inference from Huggingface AutoModel.},
132
- year={2024},
133
- url={https://huggingface.co/Synthyra/ESMplusplus_small},
134
- DOI={10.57967/hf/3726},
135
- publisher={Hugging Face}
136
- }
137
- ```
138
-
139
- ```bibtex
140
- @article{passaro2025boltz2,
141
- title={Boltz-2: Exploring the Frontiers of Biomolecular Prediction},
142
- author={Passaro, Saro and Corso, Gabriele and Wohlwend, Jeremy and Reveiz, Mateo and Bordes, Florian and Wicky, Basile and Dayan, Peter and Jing, Bowen},
143
- journal={bioRxiv},
144
- year={2025}
145
- }
146
- ```
147
-
148
- ```bibtex
149
- @article{wohlwend2024boltz1,
150
- title={Boltz-1: Democratizing Biomolecular Interaction Modeling},
151
- author={Wohlwend, Jeremy and Corso, Gabriele and Passaro, Saro and Reveiz, Mateo and Leidal, Ken and Swanson, Wojtek and Kher, Gilmer and Lember, Tommi and Jaakkola, Tommi},
152
- journal={bioRxiv},
153
- year={2024}
154
- }
155
- ```
156
-
157
- ```bibtex
158
- @inproceedings{paszke2019pytorch,
159
- title={PyTorch: An Imperative Style, High-Performance Deep Learning Library},
160
- author={Paszke, Adam and Gross, Sam and Massa, Francisco and Lerer, Adam and Bradbury, James and Chanan, Gregory and Killeen, Trevor and Lin, Zeming and Gimelshein, Natalia and Antiga, Luca and Desmaison, Alban and K{\"o}pf, Andreas and Yang, Edward and DeVito, Zach and Raison, Martin and Tejani, Alykhan and Chilamkurthy, Sasank and Steiner, Benoit and Fang, Lu and Bai, Junjie and Chintala, Soumith},
161
- booktitle={Advances in Neural Information Processing Systems 32},
162
- year={2019}
163
- }
164
- ```
 
1
+ ---
2
+ library_name: transformers
3
+ license: "mit"
4
+ tags:
5
+ - protein-language-model
6
+ - fastplms
7
+ ---
8
+
9
+ <!-- Generated from src/fastplms/models.toml. Do not edit. -->
10
+
11
+ # Synthyra/Boltz2
12
+
13
+ This checkpoint packages the FastPLMs `Boltz2` implementation.
14
+
15
+ 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 FastPLMs from the exact source revision paired with this model card:
22
+
23
+ ```bash
24
+ python -m pip install \
25
+ "fastplms[structure] @ git+https://github.com/Synthyra/FastPLMs.git@1b9ce023f1e06571cf3e6324be0610ffa53e0a4a"
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
+
32
+ ## Quick start
33
+
34
+ ```python
35
+ from transformers import AutoModel
36
+
37
+ 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
+
60
+ The high-level helper prepares a protein-only input, runs the declared Boltz2
61
+ inference core, and returns coordinates and confidence fields:
62
+
63
+ ```python
64
+ import torch
65
+
66
+ model = model.cuda().eval()
67
+ output = model.predict_structure(
68
+ amino_acid_sequence="MSTNPKPQRKTKRNTNRRPQDVKFPGG",
69
+ recycling_steps=3,
70
+ num_sampling_steps=50,
71
+ diffusion_samples=1,
72
+ seed=7,
73
+ )
74
+ model.save_as_cif(output, "prediction.cif")
75
+
76
+ print(output.sample_atom_coords.shape)
77
+ print(output.plddt, output.ptm, output.iptm)
78
+ ```
79
+
80
+ The validation boundary below describes the currently supported inference
81
+ subset and its provisional status. The helper scopes and restores Python,
82
+ NumPy, CPU Torch, and CUDA RNG state. Parameters and prepared features remain
83
+ FP32; supported CUDA inference executes inside BF16 autocast.
84
+
85
+ ## Notes and limitations
86
+
87
+ Boltz2 is provisional in FastPLMs 1.0. Exact configuration, the declared
88
+ inference-core state, feature preparation, and seeded execution remain tested,
89
+ but native-environment BF16 end-to-end inference currently exceeds the fixed
90
+ numerical-equivalence limits. FastPLMs therefore does not claim official
91
+ inference equivalence for this checkpoint yet. Work on that numerical gap
92
+ continues independently of the ESM++ and ESMFold2 release gates.
93
+
94
+ ## Runtime contract
95
+
96
+ - Public input: Raw amino-acid sequences through the convenience API, or prepared model features
97
+ - Advertised AutoClasses: `AutoConfig`, `AutoModel`
98
+ - AutoClass weight status: `AutoConfig` = `FastPLMs extension`, `AutoModel` = `pretrained`
99
+ - Attention implementations: `eager`
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`
107
+ - Complete weight publication required: `false`
108
+
109
+ ## Provenance
110
+
111
+ - FastPLMs weights: `Synthyra/Boltz2@3b148fc5efea109c065ec82ba8683d024de7134e`
112
+ - Runtime revision: `1b9ce023f1e06571cf3e6324be0610ffa53e0a4a`
113
+ - Runtime source-tree SHA-256: `21579947908f914e1b38ff1e69f9df9d22b7798899090355a53691a937b024e5`
114
+ - Runtime bundle SHA-256: `297af2aeeb817b6e3a343bf73dbc2eb3b3e29a2ca88ccba8af1aeb2e12243ffd`
115
+ - Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
116
+ - Official checkpoint: `boltz-community/boltz-2@6fdef46d763fee7fbb83ca5501ccceff43b85607`
117
+ - Artifact source: `fast`
118
+ - State transform: `boltz2_inference_core_v1`
119
+ - BF16 execution: `fp32_parameters_autocast`
120
+ - Pinned upstreams: `boltz`
121
+ - Reference container: `reference-boltz2`
122
+ - Release tiers: `structure`, `artifact`, `benchmark`
123
+ - Unresolved required file identities: `0`
124
+
125
+ The local artifact records exact file identities, conversion provenance, source
126
+ revisions, and legal texts in `provenance.json`. A nonzero unresolved count is a
127
+ release blocker.
128
+
129
+ ## Validation boundary
130
+
131
+ For tiers declared by the manifest, the release contract compares applicable
132
+ semantic configuration, tokenizer behavior, state keys, shapes, dtypes,
133
+ values, aliases, and representative inference with the pinned official
134
+ implementation. This metadata does not by itself claim that a particular build
135
+ passed, that one backend is faster, or that an output has biological or
136
+ therapeutic validity.
137
+
138
+ ## License
139
+
140
+ Checkpoint terms: MIT. The Hub model-card identifier is
141
+ `mit`. Applicable source licenses, notices, attribution,
142
+ and conversion records are distributed with the local artifact. Review them
143
+ before use.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
THIRD_PARTY_NOTICES.md ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Third-party notices
2
+
3
+ FastPLMs implements interfaces and checkpoint mappings for independently
4
+ released protein models. The pinned repositories under `vendor/upstream/` are
5
+ parity oracles. Production code does not import them, and runtime images do not
6
+ contain them.
7
+
8
+ This notice is informational and is not legal advice. A checkpoint license can
9
+ differ from the license covering its source implementation. The typed inventory
10
+ in `src/fastplms/models.toml` and the verbatim files under `LICENSES/` are the
11
+ distribution record.
12
+
13
+ ## ANKH
14
+
15
+ The pinned ANKH implementation and the mirrored ANKH checkpoints are identified
16
+ as CC BY-NC-SA 4.0. FastPLMs displays those terms but does not enforce them in
17
+ software. Users are responsible for determining whether their use and
18
+ redistribution comply. The complete text is in `LICENSES/ankh/LICENSE.md`.
19
+
20
+ ## Profluent-E1
21
+
22
+ Profluent identifies its E1 model code as Apache-2.0. The E1 weights and full
23
+ release are subject to the Profluent-E1 Clickthrough License Agreement and the
24
+ incorporated attribution requirements. Any E1 distribution must retain all of
25
+ the following files:
26
+
27
+ - `LICENSES/e1/LICENSE`, the Profluent-E1 agreement
28
+ - `LICENSES/e1/ATTRIBUTION`, the attribution guidelines
29
+ - `LICENSES/e1/NOTICE`, the required notice
30
+ - `LICENSES/e1/Apache-2.0.txt`, the code license
31
+ - `LICENSES/e1/BSD-3-Clause.txt`, covering the FlashAttention-derived padding
32
+ utility identified by the official E1 source
33
+ - `LICENSES/e1/MODIFICATIONS.md`, the FastPLMs modified-file notice
34
+
35
+ The exact text `Profluent-E1` must remain prominently displayed in E1
36
+ documentation and at each launch of an executable E1 workflow, as required by
37
+ the upstream attribution guidelines. Certain commercial outputs, including
38
+ specified pharmaceutical and target-related outputs, can require the separate
39
+ `Built with Profluent-E1` statement described in `ATTRIBUTION`.
40
+
41
+ ## DPLM
42
+
43
+ The pinned ByteDance DPLM repository is Apache-2.0. Its
44
+ [README](https://github.com/bytedance/dplm/blob/8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d/README.md#overview)
45
+ explicitly defines the repository release as including pretrained DPLM1 and
46
+ DPLM2 weights, and the same revision carries the complete
47
+ [Apache-2.0 license](https://github.com/bytedance/dplm/blob/8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d/LICENSE).
48
+ FastPLMs records both checkpoint families as Apache-2.0 and distributes the
49
+ verbatim license plus `LICENSES/dplm/PROVENANCE.md`. Converted weights retain
50
+ those terms and remain subject to the ordinary artifact and publication gates.
51
+
52
+ ## Biohub
53
+
54
+ The pinned Biohub ESM implementation is MIT and includes a separate
55
+ `THIRD_PARTY_NOTICE.md`; both files are distributed under
56
+ `LICENSES/biohub-esm/`. The pinned Biohub Transformers fork is Apache-2.0, with
57
+ its complete text under `LICENSES/biohub-transformers/`.
58
+
59
+ ## Boltz
60
+
61
+ The pinned Boltz source is MIT. The verbatim notice is in
62
+ `LICENSES/boltz/LICENSE`.
63
+
64
+ ## Meta ESM and OpenFold
65
+
66
+ The pinned Meta ESM source is MIT. The pinned OpenFold source is Apache-2.0.
67
+ Their verbatim texts and revision-specific provenance notices are under
68
+ `LICENSES/fair-esm/` and `LICENSES/openfold/`.
69
+
70
+ The native H100 ESMFold reference image applies the tracked
71
+ `docker/constraints/openfold-sm90.patch` to the copied OpenFold `setup.py`.
72
+ This build-only change restricts the CUDA extension to `sm90` and selects the
73
+ C++17 standard required by the reference PyTorch version. It leaves the pinned
74
+ submodule, extension source, model classes, checkpoint data, and public API
75
+ unchanged. The complete modified-file record is in
76
+ `LICENSES/openfold/MODIFICATIONS.md`.
77
+
78
+ The isolated reference image also includes Apache-2.0 PyTorch Lightning,
79
+ TorchMetrics, Lightning Utilities, and NVIDIA DLLogger. Their exact versions or
80
+ revision are pinned in `docker/constraints/esmfold.txt`; OpenFold imports them
81
+ eagerly, and FastPLMs production code does not depend on them. DLLogger's exact
82
+ source identity and installed-license handling are recorded in
83
+ `LICENSES/dllogger/PROVENANCE.md`.
84
+
85
+ ## ProteinTTT
86
+
87
+ The optional test-time training workflow is validated against the pinned
88
+ ProteinTTT repository under its MIT license. Its verbatim license and
89
+ revision-specific provenance are under `LICENSES/protein-ttt/`.
90
+
91
+ ## Conversion and packaging record
92
+
93
+ For every supported family, `src/fastplms/models.toml` records an immutable
94
+ official checkpoint revision, an immutable FastPLMs checkpoint revision, file
95
+ digests, a named state transformation, and a mechanism-level conversion record.
96
+ Generated artifacts reproduce that record in `provenance.json`. A release or
97
+ artifact build must fail when a required file identity, legal text, attribution
98
+ notice, modified-file notice, upstream revision, or conversion record is absent
99
+ or differs from its manifest digest.
config.json CHANGED
@@ -3,8 +3,8 @@
3
  "Boltz2Model"
4
  ],
5
  "auto_map": {
6
- "AutoConfig": "modeling_boltz2.Boltz2Config",
7
- "AutoModel": "modeling_boltz2.Boltz2Model"
8
  },
9
  "core_kwargs": {
10
  "alpha_pae": 1,
@@ -133,6 +133,16 @@
133
  "default_recycling_steps": 3,
134
  "default_sampling_steps": 200,
135
  "dtype": "float32",
 
 
 
 
 
 
 
 
 
 
136
  "model_type": "boltz2_automodel",
137
  "num_bins": 64,
138
  "transformers_version": "4.57.6"
 
3
  "Boltz2Model"
4
  ],
5
  "auto_map": {
6
+ "AutoConfig": "modeling_fastplms.Boltz2Config",
7
+ "AutoModel": "modeling_fastplms.Boltz2Model"
8
  },
9
  "core_kwargs": {
10
  "alpha_pae": 1,
 
133
  "default_recycling_steps": 3,
134
  "default_sampling_steps": 200,
135
  "dtype": "float32",
136
+ "fastplms_checkpoint_hash": "19fdd55cb04969d3e63f010a4655b73e52024bc88616f34bd0b5b38d74d303ca",
137
+ "fastplms_checkpoint_repo_id": "Synthyra/Boltz2",
138
+ "fastplms_checkpoint_revision": "3b148fc5efea109c065ec82ba8683d024de7134e",
139
+ "fastplms_model_id": "boltz2",
140
+ "fastplms_release_tool_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
141
+ "fastplms_release_tool_sha256": "1459b5d7d13d9b07bd97b3eee764f2ce73623e15e32d07ddf6825c2a9509afb9",
142
+ "fastplms_runtime_bundle_sha256": "297af2aeeb817b6e3a343bf73dbc2eb3b3e29a2ca88ccba8af1aeb2e12243ffd",
143
+ "fastplms_runtime_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
144
+ "fastplms_source_tree_sha256": "21579947908f914e1b38ff1e69f9df9d22b7798899090355a53691a937b024e5",
145
+ "fastplms_weights_revision": "3b148fc5efea109c065ec82ba8683d024de7134e",
146
  "model_type": "boltz2_automodel",
147
  "num_bins": 64,
148
  "transformers_version": "4.57.6"
fastplms/__init__.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastPLMs public package interface.
2
+
3
+ The module uses lazy exports so importing :mod:`fastplms` does not initialize
4
+ Torch, download checkpoints, construct tokenizers, or compile kernels.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib import import_module
10
+ from typing import Any
11
+
12
+ __version__ = "1.0.0"
13
+
14
+ _LAZY_EXPORTS = {
15
+ "CheckpointSource": ("fastplms.registry", "CheckpointSource"),
16
+ "EmbeddingInput": ("fastplms.embeddings", "EmbeddingInput"),
17
+ "EmbeddingRecord": ("fastplms.embeddings", "EmbeddingRecord"),
18
+ "EmbeddingResult": ("fastplms.embeddings", "EmbeddingResult"),
19
+ "FileDigest": ("fastplms.registry", "FileDigest"),
20
+ "ModelFamily": ("fastplms.registry", "ModelFamily"),
21
+ "ModelRegistry": ("fastplms.registry", "ModelRegistry"),
22
+ "ModelSpec": ("fastplms.registry", "ModelSpec"),
23
+ "OracleAsset": ("fastplms.registry", "OracleAsset"),
24
+ "RegistryError": ("fastplms.registry", "RegistryError"),
25
+ "RuntimeProfile": ("fastplms.runtime", "RuntimeProfile"),
26
+ "UpstreamSource": ("fastplms.registry", "UpstreamSource"),
27
+ "embed_dataset": ("fastplms.embeddings", "embed_dataset"),
28
+ "get_model_registry": ("fastplms.registry", "get_model_registry"),
29
+ "get_model_spec": ("fastplms.registry", "get_model_spec"),
30
+ "load_model_registry": ("fastplms.registry", "load_model_registry"),
31
+ "runtime_profile": ("fastplms.runtime", "runtime_profile"),
32
+ }
33
+
34
+ __all__ = ["__version__", *_LAZY_EXPORTS]
35
+
36
+
37
+ def __getattr__(name: str) -> Any:
38
+ try:
39
+ module_name, attribute_name = _LAZY_EXPORTS[name]
40
+ except KeyError as error:
41
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from error
42
+ value = getattr(import_module(module_name), attribute_name)
43
+ globals()[name] = value
44
+ return value
45
+
46
+
47
+ def __dir__() -> list[str]:
48
+ return sorted(set(globals()).union(__all__))
fastplms/models.toml ADDED
@@ -0,0 +1,1223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ schema_version = 1
2
+ legal_files = [
3
+ "LICENSE=sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
4
+ "THIRD_PARTY_NOTICES.md=sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
5
+ ]
6
+
7
+ [[attention_kernels]]
8
+ implementation = "flash_attention_2"
9
+ repository = "kernels-community/flash-attn2"
10
+ revision = "db6b51744f0cd7061386442c09df890fc6d9f47e"
11
+ version = 2
12
+ expected_variant = "flash_attn2"
13
+ dtypes = ["bfloat16"]
14
+
15
+ [[attention_kernels]]
16
+ implementation = "flash_attention_3"
17
+ repository = "kernels-community/flash-attn3"
18
+ revision = "43f0bd269777115d94ff826e0d113ce9c1c9087b"
19
+ version = 1
20
+ expected_variant = "flash_attn3"
21
+ dtypes = ["bfloat16"]
22
+
23
+ [[runtime_assets]]
24
+ id = "esmfold2_ccd"
25
+ repository = "biohub/ESMFold2"
26
+ revision = "1ebf0e3481a5184eb6171d40615c79e384b48796"
27
+ path = "ccd.pkl"
28
+ sha256 = "9ff44b1927c6b9198e38ffe0928706827a09a350c15530beeeabebfa88038fc5"
29
+ size = 417306584
30
+ consumer_family = "esmfold2"
31
+ trust_kind = "hash_pinned_pickle"
32
+ license = "MIT"
33
+ offline_behavior = "requires_cached_verified_file"
34
+
35
+ [[upstreams]]
36
+ id = "ankh"
37
+ path = "vendor/upstream/ankh"
38
+ url = "https://github.com/agemagician/Ankh.git"
39
+ revision = "02b4e25ce5389b9e771c9df6e546c62af1216f8e"
40
+ license = "CC-BY-NC-SA-4.0"
41
+ license_files = ["LICENSE.md"]
42
+ license_digests = ["LICENSE.md=sha256:cd041d7f9f52936e8824ac3f754e9c67410763205fc8a7020ba74fc8b6edc088"]
43
+ distribution_files = ["LICENSE.md=sha256:cd041d7f9f52936e8824ac3f754e9c67410763205fc8a7020ba74fc8b6edc088"]
44
+
45
+ [[upstreams]]
46
+ id = "biohub-esm"
47
+ path = "vendor/upstream/biohub-esm"
48
+ url = "https://github.com/Biohub/esm.git"
49
+ revision = "82ee35553d39169d678f784c8d3f8712ffd7d2c4"
50
+ license = "MIT"
51
+ license_files = ["LICENSE.md", "THIRD_PARTY_NOTICE.md"]
52
+ license_digests = [
53
+ "LICENSE.md=sha256:b63df9ca1dd96b3b21eec226b51b236d0bd152ac20eafc43aad46bf832b48d8a",
54
+ "THIRD_PARTY_NOTICE.md=sha256:5bff8515ba4e0f53abdc43714c180b79c5b606160497d98de741a369cb9b6a23",
55
+ ]
56
+ distribution_files = [
57
+ "LICENSE.md=sha256:b63df9ca1dd96b3b21eec226b51b236d0bd152ac20eafc43aad46bf832b48d8a",
58
+ "THIRD_PARTY_NOTICE.md=sha256:5bff8515ba4e0f53abdc43714c180b79c5b606160497d98de741a369cb9b6a23",
59
+ ]
60
+
61
+ [[upstreams]]
62
+ id = "biohub-transformers"
63
+ path = "vendor/upstream/biohub-transformers"
64
+ url = "https://github.com/Biohub/transformers.git"
65
+ revision = "3a8956fb4d4ea16b0ec8e71deef2c2909b6a5cbf"
66
+ license = "Apache-2.0"
67
+ license_files = ["LICENSE"]
68
+ license_digests = ["LICENSE=sha256:77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049"]
69
+ distribution_files = ["LICENSE=sha256:77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049"]
70
+
71
+ [[upstreams]]
72
+ id = "boltz"
73
+ path = "vendor/upstream/boltz"
74
+ url = "https://github.com/jwohlwend/boltz.git"
75
+ revision = "b1ebfc46ecf57f5414e0d1a6f9027bbb122c53bc"
76
+ license = "MIT"
77
+ license_files = ["LICENSE"]
78
+ license_digests = ["LICENSE=sha256:f0667fd5e66c51e1ba8ddaa0249c6d7225b30037e02c45782d8f2c2943ac2617"]
79
+ distribution_files = ["LICENSE=sha256:f0667fd5e66c51e1ba8ddaa0249c6d7225b30037e02c45782d8f2c2943ac2617"]
80
+
81
+ [[upstreams]]
82
+ id = "dplm"
83
+ path = "vendor/upstream/dplm"
84
+ url = "https://github.com/bytedance/dplm.git"
85
+ revision = "8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d"
86
+ license = "Apache-2.0"
87
+ license_files = ["LICENSE"]
88
+ license_digests = ["LICENSE=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"]
89
+ distribution_files = [
90
+ "LICENSE=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
91
+ "PROVENANCE.md=sha256:a659f74be9073cf1ad2d2f7071531ca56959b421f111152cf4c41184ace5970e",
92
+ ]
93
+
94
+ [[upstreams]]
95
+ id = "e1"
96
+ path = "vendor/upstream/e1"
97
+ url = "https://github.com/Profluent-AI/E1.git"
98
+ revision = "bfd2620a602248499f3d2583d85a7ecddf0b6e02"
99
+ license = "Apache-2.0 AND Profluent-E1-Agreement"
100
+ license_files = ["LICENSE", "ATTRIBUTION", "NOTICE"]
101
+ license_digests = [
102
+ "LICENSE=sha256:8ef1dd556091544db3044164a8015424a3dcb3450fb3765a81b88463551bbe81",
103
+ "ATTRIBUTION=sha256:deb22b250f6491b649eda5c63e080dd56486b8d2736cea6a52ef875436214367",
104
+ "NOTICE=sha256:6de9db0320b4ee82f665c0951d8fd4cd53701a659c9dbce9bc3e3ea6afc4c6b3",
105
+ ]
106
+ distribution_files = [
107
+ "LICENSE=sha256:8ef1dd556091544db3044164a8015424a3dcb3450fb3765a81b88463551bbe81",
108
+ "ATTRIBUTION=sha256:deb22b250f6491b649eda5c63e080dd56486b8d2736cea6a52ef875436214367",
109
+ "NOTICE=sha256:6de9db0320b4ee82f665c0951d8fd4cd53701a659c9dbce9bc3e3ea6afc4c6b3",
110
+ "Apache-2.0.txt=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
111
+ "BSD-3-Clause.txt=sha256:36e1987f2f17db7f8ad36cd7a37dbb7aeaaf0ab68b97ab4b9d3556f3a7a76ae8",
112
+ "MODIFICATIONS.md=sha256:2506f47c0f5475af8e8ff2cff13eb8b79e8e25a08a054cdd617bf336536750ca",
113
+ ]
114
+
115
+ [[upstreams]]
116
+ id = "fair-esm"
117
+ path = "vendor/upstream/fair-esm"
118
+ url = "https://github.com/facebookresearch/esm.git"
119
+ revision = "2b369911bb5b4b0dda914521b9475cad1656b2ac"
120
+ license = "MIT"
121
+ license_files = ["LICENSE"]
122
+ license_digests = ["LICENSE=sha256:da6d3703ed11cbe42bd212c725957c98da23cbff1998c05fa4b3d976d1a58e93"]
123
+ distribution_files = [
124
+ "LICENSE=sha256:da6d3703ed11cbe42bd212c725957c98da23cbff1998c05fa4b3d976d1a58e93",
125
+ "PROVENANCE.md=sha256:950adb94daf15e646ddf226dacfe2a8e77801aa0793e439a9a3490a48eb666e7",
126
+ ]
127
+
128
+ [[upstreams]]
129
+ id = "openfold"
130
+ path = "vendor/upstream/openfold"
131
+ url = "https://github.com/aqlaboratory/openfold.git"
132
+ revision = "4b41059694619831a7db195b7e0988fc4ff3a307"
133
+ license = "Apache-2.0"
134
+ license_files = ["LICENSE"]
135
+ license_digests = ["LICENSE=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"]
136
+ distribution_files = [
137
+ "LICENSE=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
138
+ "MODIFICATIONS.md=sha256:fd6f0aa1086a0c996cf967b326d18e965660cda0ad5c7f36a3474a8490720da3",
139
+ "PROVENANCE.md=sha256:48c903db43a217a3126afaefbac60b7ddac7efda2dfcc0cbff0bffc7d6c30081",
140
+ ]
141
+
142
+ [[upstreams]]
143
+ id = "protein-ttt"
144
+ path = "vendor/upstream/protein-ttt"
145
+ url = "https://github.com/anton-bushuiev/ProteinTTT.git"
146
+ revision = "fde2817cd84b936167cc76ccabf31e5c0fe49962"
147
+ license = "MIT"
148
+ license_files = ["LICENSE"]
149
+ license_digests = ["LICENSE=sha256:bb01e7d5554f9e2e117172e56551452f68a7818df7bc8e71cd7a776a1d4ba3df"]
150
+ distribution_files = [
151
+ "LICENSE=sha256:bb01e7d5554f9e2e117172e56551452f68a7818df7bc8e71cd7a776a1d4ba3df",
152
+ "PROVENANCE.md=sha256:dc641c37353c2efd50ccbdb316ca4aae495ec02c1563e0e15bac92f75fc482e5",
153
+ ]
154
+
155
+ [families.esm2]
156
+ architecture = "ESM2"
157
+ upstreams = ["fair-esm"]
158
+ tokenizer_mode = "tokenizer"
159
+ public_input = "Amino-acid sequences tokenized to residue IDs"
160
+ extra = "core"
161
+ reference_container = "reference-esm2"
162
+ reference_adapter = "tests.parity.support.reference_adapters.esm2"
163
+ attention = ["eager", "sdpa", "flex_attention", "flash_attention_2", "flash_attention_3"]
164
+ dtypes = ["float32", "bfloat16"]
165
+ bf16_execution = "fp32_parameters_autocast"
166
+ precisions = ["default"]
167
+ vram_tier = "sequence"
168
+ checkpoint_license = "MIT"
169
+ hub_license = "mit"
170
+ weights_publication_allowed = true
171
+ state_transform = "esm2_hf_to_fastplms_v1"
172
+ conversion_provenance = "Input: the pinned official ESM2 state dictionary. Transformation: apply the deterministic esm2_hf_to_fastplms_v1 key map while preserving tensor values and materializing the tied input/output embedding values as independent tensors. Output: the pinned Synthyra FastPLMs checkpoint. Validation: release parity compares exact keys and values after the declared non-aliasing transform, tokenizer behavior, and inference. Limitation: any numerical rewrite requires a new transform identifier and exact conversion test."
173
+ representative = "esm2_8m"
174
+ documentation = "docs/models.md#esm2"
175
+ test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
176
+ runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_esm_rotary.py", "models/esm2", "models/ttt.py"]
177
+ auto_map = { AutoConfig = "fastplms.models.esm2.modeling_fastesm.FastEsmConfig", AutoModel = "fastplms.models.esm2.modeling_fastesm.FastEsmModel", AutoModelForMaskedLM = "fastplms.models.esm2.modeling_fastesm.FastEsmForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.esm2.modeling_fastesm.FastEsmForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esm2.modeling_fastesm.FastEsmForTokenClassification" }
178
+
179
+ [families.esm_plusplus]
180
+ architecture = "ESMC"
181
+ upstreams = ["biohub-esm", "biohub-transformers"]
182
+ tokenizer_mode = "tokenizer"
183
+ public_input = "Amino-acid sequences tokenized to residue IDs"
184
+ extra = "core"
185
+ reference_container = "reference-biohub-esm"
186
+ reference_adapter = "tests.parity.support.reference_adapters.esm_plusplus"
187
+ attention = ["eager", "sdpa", "flex_attention", "flash_attention_2", "flash_attention_3"]
188
+ dtypes = ["float32", "bfloat16"]
189
+ bf16_execution = "static_parameters"
190
+ precisions = ["default"]
191
+ vram_tier = "sequence"
192
+ checkpoint_license = "MIT"
193
+ hub_license = "mit"
194
+ weights_publication_allowed = true
195
+ state_transform = "esmc_to_fastplms_v1"
196
+ conversion_provenance = "Input: the pinned Biohub ESMC checkpoint. Transformation: apply the deterministic esmc_to_fastplms_v1 parameter map into the FastPLMs ESMC modules. Output: the pinned Synthyra ESMplusplus checkpoint. Validation: release parity compares keys, shapes, dtypes, values, aliases, and live inference. Limitation: runtime attention and precision selection are not serialized weight transforms."
197
+ representative = "esmc_small"
198
+ documentation = "docs/models.md#esm-and-esmc"
199
+ test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
200
+ runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/esm_plusplus", "models/ttt.py"]
201
+ auto_map = { AutoConfig = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusConfig", AutoModel = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusModel", AutoModelForMaskedLM = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusForMaskedLM" }
202
+
203
+ [families.esm3]
204
+ architecture = "ESM3"
205
+ upstreams = ["biohub-esm", "biohub-transformers"]
206
+ tokenizer_mode = "tokenizer"
207
+ public_input = "Sequence, structure, and function tracks prepared through the multimodal helpers"
208
+ extra = "core"
209
+ reference_container = "reference-biohub-esm"
210
+ reference_adapter = "tests.parity.support.reference_adapters.esm3"
211
+ attention = ["eager", "sdpa", "flex_attention"]
212
+ dtypes = ["float32", "bfloat16"]
213
+ bf16_execution = "fp32_parameters_autocast"
214
+ precisions = ["default"]
215
+ vram_tier = "large-sequence"
216
+ checkpoint_license = "MIT"
217
+ hub_license = "mit"
218
+ weights_publication_allowed = true
219
+ state_transform = "esm3_to_fastplms_v1"
220
+ conversion_provenance = "Input: the pinned Biohub ESM3 checkpoint. Transformation: apply the deterministic esm3_to_fastplms_v1 parameter map for the supported sequence and multimodal modules and expand BF16 checkpoint tensors to FP32 storage. Output: the pinned Synthyra ESM3 checkpoint. Validation: release parity compares exact state identity after the declared map and live feature behavior. Limitation: unsupported upstream modalities may not be inferred from this record."
221
+ representative = "esm3_small"
222
+ documentation = "docs/models.md#esm3"
223
+ test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
224
+ runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/esm3", "models/ttt.py"]
225
+ auto_map = { AutoConfig = "fastplms.models.esm3.modeling_esm3.FastESM3Config", AutoModel = "fastplms.models.esm3.modeling_esm3.FastESM3Model" }
226
+
227
+ [families.e1]
228
+ architecture = "E1"
229
+ upstreams = ["e1"]
230
+ tokenizer_mode = "sequence"
231
+ public_input = "Raw amino-acid sequences prepared by the native E1 adapter"
232
+ extra = "core"
233
+ reference_container = "reference-e1"
234
+ reference_adapter = "tests.parity.support.reference_adapters.e1"
235
+ attention = ["sdpa", "flex_attention"]
236
+ dtypes = ["float32", "bfloat16"]
237
+ bf16_execution = "static_parameters"
238
+ precisions = ["default"]
239
+ vram_tier = "sequence"
240
+ checkpoint_license = "Profluent-E1-Agreement"
241
+ hub_license = "other"
242
+ hub_license_name = "Profluent-E1 Clickthrough License Agreement"
243
+ hub_license_link = "https://github.com/Profluent-AI/E1/blob/bfd2620a602248499f3d2583d85a7ecddf0b6e02/LICENSE"
244
+ weights_publication_allowed = true
245
+ state_transform = "e1_to_fastplms_v1"
246
+ conversion_provenance = "Input: the pinned Profluent-E1 checkpoint and tokenizer-free sequence contract. Transformation: apply e1_to_fastplms_v1 to the FastPLMs encoder and official task heads, storing floating tensors in BF16. Output: the pinned Synthyra Profluent-E1 checkpoint. Validation: release parity covers state identity after the declared cast, sequence and RAG preparation, aliases, and inference. Limitation: the FastPLMs scoring extension is not represented as an official E1 head."
247
+ representative = "e1_150m"
248
+ documentation = "docs/models.md#e1"
249
+ test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
250
+ runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/e1", "models/ttt.py"]
251
+ auto_map = { AutoConfig = "fastplms.models.e1.modeling_e1.E1Config", AutoModel = "fastplms.models.e1.modeling_e1.E1Model", AutoModelForMaskedLM = "fastplms.models.e1.modeling_e1.E1ForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.e1.modeling_e1.E1ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.e1.modeling_e1.E1ForTokenClassification" }
252
+
253
+ [families.dplm]
254
+ architecture = "DPLM"
255
+ upstreams = ["dplm"]
256
+ tokenizer_mode = "tokenizer"
257
+ public_input = "Amino-acid sequences tokenized to masked or partially masked residue IDs"
258
+ extra = "core"
259
+ reference_container = "reference-dplm"
260
+ reference_adapter = "tests.parity.support.reference_adapters.dplm"
261
+ attention = ["eager", "sdpa", "flex_attention", "flash_attention_3"]
262
+ dtypes = ["float32", "bfloat16"]
263
+ bf16_execution = "fp32_parameters_autocast"
264
+ precisions = ["default"]
265
+ vram_tier = "sequence"
266
+ checkpoint_license = "Apache-2.0"
267
+ hub_license = "apache-2.0"
268
+ weights_publication_allowed = true
269
+ state_transform = "dplm_to_fastplms_v1"
270
+ conversion_provenance = "Input: the pinned official DPLM1 checkpoint. Transformation: apply dplm_to_fastplms_v1, omitting the unused absolute-position table for rotary checkpoints and materializing the tied input/output embedding values as independent tensors. Output: the pinned Synthyra DPLM checkpoint. Validation: release parity compares exact state identity after the declared transform, tokenizer behavior, generation, and inference. License basis: the pinned ByteDance DPLM Apache-2.0 LICENSE and README explicitly scope the repository release to the pretrained DPLM1 and DPLM2 weights; immutable evidence is recorded in LICENSES/dplm/PROVENANCE.md. Limitation: redistribution remains subject to Apache-2.0 and the pinned provenance record; no broader rights are inferred."
271
+ representative = "dplm_150m"
272
+ documentation = "docs/models.md#dplm"
273
+ test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
274
+ runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_diffusion_generation.py", "models/_esm_rotary.py", "models/dplm", "models/ttt.py"]
275
+ auto_map = { AutoConfig = "fastplms.models.dplm.modeling_dplm.DPLMConfig", AutoModel = "fastplms.models.dplm.modeling_dplm.DPLMModel", AutoModelForMaskedLM = "fastplms.models.dplm.modeling_dplm.DPLMForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.dplm.modeling_dplm.DPLMForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.dplm.modeling_dplm.DPLMForTokenClassification" }
276
+
277
+ [families.dplm2]
278
+ architecture = "DPLM2"
279
+ upstreams = ["dplm"]
280
+ tokenizer_mode = "tokenizer"
281
+ public_input = "Tokenized amino-acid and structure tracks with explicit modality boundaries"
282
+ extra = "core"
283
+ reference_container = "reference-dplm"
284
+ reference_adapter = "tests.parity.support.reference_adapters.dplm2"
285
+ attention = ["sdpa"]
286
+ dtypes = ["float32", "bfloat16"]
287
+ bf16_execution = "fp32_parameters_autocast"
288
+ precisions = ["default"]
289
+ vram_tier = "sequence"
290
+ checkpoint_license = "Apache-2.0"
291
+ hub_license = "apache-2.0"
292
+ weights_publication_allowed = true
293
+ state_transform = "dplm2_to_fastplms_v1"
294
+ conversion_provenance = "Input: the pinned official DPLM2 checkpoint. Transformation: apply dplm2_to_fastplms_v1, retaining the independent language-model head and trained encoder contact head while omitting the unused absolute-position table for rotary checkpoints. Output: the pinned Synthyra DPLM2 checkpoint. Validation: release parity compares exact keys and values after the declared omission, non-aliasing, tokenizer behavior, generation, and inference. License basis: the pinned ByteDance DPLM Apache-2.0 LICENSE and README explicitly scope the repository release to the pretrained DPLM1 and DPLM2 weights; immutable evidence is recorded in LICENSES/dplm/PROVENANCE.md. Limitation: no head exception is permitted by this record, and redistribution remains subject to Apache-2.0."
295
+ representative = "dplm2_150m"
296
+ documentation = "docs/models.md#dplm2"
297
+ test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
298
+ runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_diffusion_generation.py", "models/_esm_rotary.py", "models/dplm2", "models/ttt.py"]
299
+ auto_map = { AutoConfig = "fastplms.models.dplm2.modeling_dplm2.DPLM2Config", AutoModel = "fastplms.models.dplm2.modeling_dplm2.DPLM2Model", AutoModelForMaskedLM = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForTokenClassification" }
300
+ tokenizer_class = "fastplms.models.dplm2.tokenization_dplm2.DPLM2Tokenizer"
301
+
302
+ [families.ankh]
303
+ architecture = "ANKH"
304
+ upstreams = ["ankh"]
305
+ tokenizer_mode = "tokenizer"
306
+ public_input = "Amino-acid sequences tokenized for encoder or sequence-to-sequence use"
307
+ extra = "core"
308
+ reference_container = "reference-ankh"
309
+ reference_adapter = "tests.parity.support.reference_adapters.ankh"
310
+ attention = ["eager", "sdpa"]
311
+ dtypes = ["float32", "bfloat16"]
312
+ bf16_execution = "static_parameters"
313
+ precisions = ["default"]
314
+ vram_tier = "large-sequence"
315
+ checkpoint_license = "CC-BY-NC-SA-4.0"
316
+ hub_license = "cc-by-nc-sa-4.0"
317
+ weights_publication_allowed = true
318
+ state_transform = "ankh_t5_to_fastplms_v1"
319
+ conversion_provenance = "Input: the pinned official ANKH T5 checkpoint. Transformation: apply ankh_t5_to_fastplms_v1 to the official encoder and sequence-to-sequence heads. Output: the pinned Synthyra ANKH checkpoint. Validation: release parity compares exact mapped state, tokenizer behavior, official heads, and inference. Limitation: the separately named FastPLMs masked-language-model extension is not an official ANKH head."
320
+ representative = "ankh_base"
321
+ documentation = "docs/models.md#ankh"
322
+ test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
323
+ requires_complete_weight_publication = true
324
+ runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/ankh", "models/ttt.py"]
325
+ auto_map = { AutoConfig = "fastplms.models.ankh.modeling_ankh.FastAnkhConfig", AutoModel = "fastplms.models.ankh.modeling_ankh.FastAnkhModel", AutoModelForMaskedLM = "fastplms.models.ankh.modeling_ankh.FastAnkhForMaskedLMExtension", AutoModelForSeq2SeqLM = "fastplms.models.ankh.modeling_ankh.FastAnkhForConditionalGeneration", AutoModelForSequenceClassification = "fastplms.models.ankh.modeling_ankh.FastAnkhForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.ankh.modeling_ankh.FastAnkhForTokenClassification" }
326
+
327
+ [families.boltz2]
328
+ architecture = "Boltz2"
329
+ upstreams = ["boltz"]
330
+ tokenizer_mode = "structure"
331
+ public_input = "Raw amino-acid sequences through the convenience API, or prepared model features"
332
+ extra = "structure"
333
+ reference_container = "reference-boltz2"
334
+ reference_adapter = "tests.parity.support.reference_adapters.boltz"
335
+ attention = ["eager"]
336
+ dtypes = ["float32", "bfloat16"]
337
+ bf16_execution = "fp32_parameters_autocast"
338
+ precisions = ["default"]
339
+ vram_tier = "structure"
340
+ checkpoint_license = "MIT"
341
+ hub_license = "mit"
342
+ weights_publication_allowed = true
343
+ state_transform = "boltz2_inference_core_v1"
344
+ conversion_provenance = "Input: the pinned official Boltz2 checkpoint. Transformation: select and map the supported Boltz2 inference-core parameters with boltz2_inference_core_v1. Output: the pinned Synthyra Boltz2 checkpoint. Validation: release parity covers state identity for the declared subset, feature preparation, seeded inference, and structure outputs. Limitation: this record does not claim support for undeclared upstream training components."
345
+ representative = "boltz2"
346
+ documentation = "docs/models.md#boltz2"
347
+ test_tiers = ["structure", "artifact", "benchmark"]
348
+ runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "models/boltz"]
349
+ auto_map = { AutoConfig = "fastplms.models.boltz.modeling_boltz2.Boltz2Config", AutoModel = "fastplms.models.boltz.modeling_boltz2.Boltz2Model" }
350
+
351
+ [families.esmfold]
352
+ architecture = "ESMFold"
353
+ upstreams = ["fair-esm", "openfold"]
354
+ tokenizer_mode = "structure"
355
+ public_input = "Raw amino-acid sequences through folding helpers, or prepared residue tensors"
356
+ extra = "structure"
357
+ reference_container = "reference-esmfold"
358
+ reference_adapter = "tests.parity.support.reference_adapters.esmfold"
359
+ attention = ["eager", "sdpa", "flex_attention"]
360
+ dtypes = ["float32", "bfloat16"]
361
+ bf16_execution = "fp32_parameters_autocast"
362
+ precisions = ["default"]
363
+ vram_tier = "structure"
364
+ checkpoint_license = "MIT"
365
+ hub_license = "mit"
366
+ weights_publication_allowed = true
367
+ state_transform = "esmfold_meta_to_fastplms_v1"
368
+ conversion_provenance = "Input: the pinned native Meta ESMFold checkpoint plus its pinned ESM2 backbone. Transformation: apply esmfold_meta_to_fastplms_v1 to map native ESM2 names into the structure-only FastPLMs backbone, retain folding tensors, omit five deterministically reconstructed geometry buffers, omit the folding-unused ESM2 masked-LM and contact-regression heads, and remove the obsolete random FastPLMs TTT head from earlier mirrors. Output: canonical FP32 FastPLMs ESMFold state with an explicit CUDA BF16-autocast execution path. Validation: release parity compares exact mapped keys, shapes, dtypes, values, aliases, semantic configuration, FP32 and BF16-compute seeded inference, and structure metrics with pLDDT normalized to (0, 1). Limitation: ESMFold TTT is rejected because the official checkpoint contains no trained masked-language-model head."
369
+ representative = "esmfold"
370
+ documentation = "docs/models.md#esmfold"
371
+ test_tiers = ["check", "compliance", "structure", "feature", "artifact", "benchmark"]
372
+ runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_esm_rotary.py", "models/esmfold"]
373
+ auto_map = { AutoConfig = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmFoldConfig", AutoModel = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmForProteinFolding" }
374
+
375
+ [families.esmfold2]
376
+ architecture = "ESMFold2"
377
+ upstreams = ["biohub-esm", "biohub-transformers", "protein-ttt"]
378
+ backbone_model = "esmc_6b"
379
+ tokenizer_mode = "structure"
380
+ public_input = "Raw amino-acid sequences or typed molecular-complex specifications; low-level forward accepts prepared feature tensors"
381
+ extra = "structure"
382
+ reference_container = "reference-esmfold2"
383
+ reference_adapter = "tests.parity.support.reference_adapters.esmfold2"
384
+ attention = ["eager", "sdpa", "flex_attention"]
385
+ dtypes = ["float32", "bfloat16"]
386
+ bf16_execution = "fp32_parameters_autocast"
387
+ precisions = ["auto", "fp32", "bf16", "fp8"]
388
+ experimental_precisions = ["fp8"]
389
+ vram_tier = "structure-6b"
390
+ checkpoint_license = "MIT"
391
+ hub_license = "mit"
392
+ weights_publication_allowed = true
393
+ state_transform = "identity"
394
+ conversion_provenance = "Input: each pinned Biohub ESMFold2 checkpoint and its separately pinned ESMC checkpoint. Transformation: apply identity to preserve the folding checkpoint exactly, load its parameters in FP32 for CUDA BF16-autocast execution, retain canonical BF16 ESMC weights, and optionally rebuild exactly 80 ESMC attention output projections as transient Transformer Engine linears. Output: the corresponding pinned Synthyra ESMFold2 checkpoint plus its declared ESMC precision policy. Validation: release parity covers exact canonical state, learned projection, prepared features, and seeded BF16 folding; experimental FP8 validation covers strict unavailable-device behavior, all four variants, and three BF16-to-FP8 reload cycles on the standard variant. Limitation: only the four manifest-listed ESMFold2 variants are supported; FP8 is experimental, applies only to inference-time ESMC execution, and requires direct CUDA loading with Transformer Engine availability."
395
+ representative = "esmfold2"
396
+ documentation = "docs/esmfold2.md"
397
+ test_tiers = ["check", "compliance", "structure", "feature", "artifact", "benchmark"]
398
+ runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/esmfold2", "models/esm_plusplus", "models/ttt.py"]
399
+ auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFold2Config", AutoModel = "fastplms.models.esmfold2.modeling_esmfold2.ESMFold2Model" }
400
+
401
+ [[models]]
402
+ id = "esm2_8m"
403
+ family = "esm2"
404
+ size_category = "small"
405
+ generation_contract = "not_applicable"
406
+ official_golden = { metadata = "tests/goldens/esm2_8m.json=sha256:6975e86d1d8f27488bf2a676551feaa48cc19254c9d24b6acb09198122745609", tensors = "tests/goldens/esm2_8m.safetensors=sha256:b40217566c33c71988d28869de353be54a3b3ebfc21fdfd29056e88cf7e99f4c" }
407
+ fast_repo = "Synthyra/ESM2-8M"
408
+ fast_revision = "185ecbd45665d050a8dae326d91886d330c5f9d0"
409
+ fast_files = [
410
+ "config.json=git-sha1:46d0a7b517f59123c6ebc6d1011585731cbab259",
411
+ "model.safetensors=sha256:c824e6ded5fb71c72bc5ac05300699947819023cb26cdaf6897665e6b2645e1b",
412
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
413
+ "tokenizer_config.json=git-sha1:3cfc5db0c6790859a3bc2a4dc053a813acd65295",
414
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
415
+ ]
416
+ official_repo = "facebook/esm2_t6_8M_UR50D"
417
+ official_revision = "c731040fcd8d73dceaa04b0a8e6329b345b0f5df"
418
+ official_files = [
419
+ "config.json=git-sha1:c2c6e65a87d9d20d47699ae236d605b80c741dd3",
420
+ "model.safetensors=sha256:24c5fa474c48f3b754b86efe752d5f189d2bcd88190fa2270fc92b2ef3034189",
421
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
422
+ "tokenizer_config.json=git-sha1:3f0d47e841e1cb75257aeaf76d156802899a217e",
423
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
424
+ ]
425
+
426
+ [[models.oracle_assets]]
427
+ role = "weights"
428
+ path = "models/esm2_t6_8M_UR50D.pt"
429
+ url = "https://dl.fbaipublicfiles.com/fair-esm/models/esm2_t6_8M_UR50D.pt"
430
+ sha256 = "46f002a9870c9bdecd0ea887acb1f9a38a6b561e8f8bf8a6990b679b9d31b928"
431
+ size = 30099493
432
+
433
+ [[models.oracle_assets]]
434
+ role = "contact_regression"
435
+ path = "regression/esm2_t6_8M_UR50D-contact-regression.pt"
436
+ url = "https://dl.fbaipublicfiles.com/fair-esm/regression/esm2_t6_8M_UR50D-contact-regression.pt"
437
+ sha256 = "8f7a4557d57713b97ba0e484303007efb7230d25299c0ac47a0a1b12a87bbb9d"
438
+ size = 1511
439
+
440
+ [[models]]
441
+ id = "esm2_35m"
442
+ family = "esm2"
443
+ size_category = "small"
444
+ generation_contract = "not_applicable"
445
+ official_golden = { metadata = "tests/goldens/esm2_35m.json=sha256:e919d3ce6d20b6a942d27d92323814ae7594a0129dc9c4de27c5053e96675bcd", tensors = "tests/goldens/esm2_35m.safetensors=sha256:c9b8bb616cf884fb7744521a2fcc6eed23586342d11241e6c9ef16454ec31e17" }
446
+ fast_repo = "Synthyra/ESM2-35M"
447
+ fast_revision = "37ab9f56b41e365b3bd9e25d6fefe9150fd910f0"
448
+ fast_files = [
449
+ "config.json=git-sha1:4d428c9934572f39e2a00db162249971f37c88e4",
450
+ "model.safetensors=sha256:21d95ab6bb9aa91bfec87eff11da61a657b732f2df279cbddbae6a7f1f0bba9c",
451
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
452
+ "tokenizer_config.json=git-sha1:3cfc5db0c6790859a3bc2a4dc053a813acd65295",
453
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
454
+ ]
455
+ official_repo = "facebook/esm2_t12_35M_UR50D"
456
+ official_revision = "6fbf070e65b0b7291e7bbcd451118c216cff79d8"
457
+ official_files = [
458
+ "config.json=git-sha1:3f64131bb610ed1ce482c4b5421fc358c785278f",
459
+ "model.safetensors=sha256:e35647818e0e064351d4531ed480d225a002567b4b2b93ad3a9246d753150fc0",
460
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
461
+ "tokenizer_config.json=git-sha1:3f0d47e841e1cb75257aeaf76d156802899a217e",
462
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
463
+ ]
464
+
465
+ [[models.oracle_assets]]
466
+ role = "weights"
467
+ path = "models/esm2_t12_35M_UR50D.pt"
468
+ url = "https://dl.fbaipublicfiles.com/fair-esm/models/esm2_t12_35M_UR50D.pt"
469
+ sha256 = "7f21e80e61d16a71735163ef555d3009afb0c98da74c48e29df08606973cc55e"
470
+ size = 134095705
471
+
472
+ [[models.oracle_assets]]
473
+ role = "contact_regression"
474
+ path = "regression/esm2_t12_35M_UR50D-contact-regression.pt"
475
+ url = "https://dl.fbaipublicfiles.com/fair-esm/regression/esm2_t12_35M_UR50D-contact-regression.pt"
476
+ sha256 = "16641e05d830d0ce863dd152dbb8c2f3ddfa3c3ec2a66080152c8abad01d8585"
477
+ size = 1959
478
+
479
+ [[models]]
480
+ id = "esm2_150m"
481
+ family = "esm2"
482
+ size_category = "medium"
483
+ generation_contract = "not_applicable"
484
+ official_golden = { metadata = "tests/goldens/esm2_150m.json=sha256:c04c93486024ba0fa1c81fbfbe92ee79d1d4c7f1cfcc2c9886728522f752feab", tensors = "tests/goldens/esm2_150m.safetensors=sha256:c03fe9916dba137b452a6bbe944c7dc414db4019a6f0921e87b92d4bb6a8a42f" }
485
+ fast_repo = "Synthyra/ESM2-150M"
486
+ fast_revision = "979e0880dfc9e0c0080839b83d9d2dc05b92786a"
487
+ fast_files = [
488
+ "config.json=git-sha1:efeae2af182b7d34dc35740a45f157661e7acdf4",
489
+ "model.safetensors=sha256:d1f7c60f98c31af328381519a750972b6a31b13b97aa7cca2e71b5ae1b3f8f53",
490
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
491
+ "tokenizer_config.json=git-sha1:3cfc5db0c6790859a3bc2a4dc053a813acd65295",
492
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
493
+ ]
494
+ official_repo = "facebook/esm2_t30_150M_UR50D"
495
+ official_revision = "a695f6045e2e32885fa60af20c13cb35398ce30c"
496
+ official_files = [
497
+ "config.json=git-sha1:52e04179e6fbad6663a94ea5cc44f09d764c5cd4",
498
+ "model.safetensors=sha256:c3f1da8aea53bddd32c246c86168c23b9fd72341fb9db9a94436f855f5053566",
499
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
500
+ "tokenizer_config.json=git-sha1:3f0d47e841e1cb75257aeaf76d156802899a217e",
501
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
502
+ ]
503
+
504
+ [[models.oracle_assets]]
505
+ role = "weights"
506
+ path = "models/esm2_t30_150M_UR50D.pt"
507
+ url = "https://dl.fbaipublicfiles.com/fair-esm/models/esm2_t30_150M_UR50D.pt"
508
+ sha256 = "881c7176cf198ef8dec26a3c375d40eb58d0c33df95c22562ca6cc6d3f812c62"
509
+ size = 592774773
510
+
511
+ [[models.oracle_assets]]
512
+ role = "contact_regression"
513
+ path = "regression/esm2_t30_150M_UR50D-contact-regression.pt"
514
+ url = "https://dl.fbaipublicfiles.com/fair-esm/regression/esm2_t30_150M_UR50D-contact-regression.pt"
515
+ sha256 = "6a604b96722ed052eef8a094ad90b275ba2e987d406315dbed0bdc6b3c4238a7"
516
+ size = 3431
517
+
518
+ [[models]]
519
+ id = "esm2_650m"
520
+ family = "esm2"
521
+ size_category = "large"
522
+ generation_contract = "not_applicable"
523
+ official_golden = { metadata = "tests/goldens/esm2_650m.json=sha256:f18332172fcb3abf5dd2485fd55f5b0d193ad3b93a44cc744e0d02817c927477", tensors = "tests/goldens/esm2_650m.safetensors=sha256:c3a66b75add03628e62e238cb63da6a9e4d321f8160e84bdf2a131c096977f86" }
524
+ fast_repo = "Synthyra/ESM2-650M"
525
+ fast_revision = "ca0718a5d52b80d5c60dd76860e55e061a95fb0a"
526
+ fast_files = [
527
+ "config.json=git-sha1:88f6bd240680b29c3244df8292246048401f5caf",
528
+ "model.safetensors=sha256:a15142e94ecf36f0edde9b37796f591e609ebe1694ca411e93640f0ee384994a",
529
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
530
+ "tokenizer_config.json=git-sha1:3cfc5db0c6790859a3bc2a4dc053a813acd65295",
531
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
532
+ ]
533
+ official_repo = "facebook/esm2_t33_650M_UR50D"
534
+ official_revision = "08e4846e537177426273712802403f7ba8261b6c"
535
+ official_files = [
536
+ "config.json=git-sha1:a956a25d277f30bd870d3760b9a116f19ead885e",
537
+ "model.safetensors=sha256:a08adabb949fa67ad3c14b509d04fd60368b35007b0095e3358f81200c4f4db0",
538
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
539
+ "tokenizer_config.json=git-sha1:3f0d47e841e1cb75257aeaf76d156802899a217e",
540
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
541
+ ]
542
+
543
+ [[models.oracle_assets]]
544
+ role = "weights"
545
+ path = "models/esm2_t33_650M_UR50D.pt"
546
+ url = "https://dl.fbaipublicfiles.com/fair-esm/models/esm2_t33_650M_UR50D.pt"
547
+ sha256 = "ea9d0522b335a8778dea6535a65301f10208dece28cd5865482b0b1fc446168c"
548
+ size = 2604537549
549
+
550
+ [[models.oracle_assets]]
551
+ role = "contact_regression"
552
+ path = "regression/esm2_t33_650M_UR50D-contact-regression.pt"
553
+ url = "https://dl.fbaipublicfiles.com/fair-esm/regression/esm2_t33_650M_UR50D-contact-regression.pt"
554
+ sha256 = "8ffe6edbd4173dc8d45c2cd5cb27d43aad77ec26b4c768200c58ae1f96693575"
555
+ size = 3687
556
+
557
+ [[models]]
558
+ id = "esm2_3b"
559
+ family = "esm2"
560
+ size_category = "xlarge"
561
+ generation_contract = "not_applicable"
562
+ official_golden = { metadata = "tests/goldens/esm2_3b.json=sha256:5043b2333c57a34d54fac53916722d1acb4b6fd50395b9abafa805435b184a48", tensors = "tests/goldens/esm2_3b.safetensors=sha256:dfd5a8cb05d3e814a080185c4808c8e7ec2277f070f395562fcfbe4376789e4e" }
563
+ notes = "The pinned default SDPA BF16 path uses a checkpoint-specific numeric calibration: relative L2 target/hard limit 0.06/0.07, relative Q99.9 0.15/0.18, first-percentile residue cosine 0.994/0.992, and pooled cosine 0.998/0.997. Exact state identity and the global logits-distribution contract remain required."
564
+ fast_repo = "Synthyra/ESM2-3B"
565
+ fast_revision = "ff89d0180f414ab9c677219a25da79bf09185456"
566
+ fast_files = [
567
+ "config.json=git-sha1:94944ad6cabaa40a3ce1cbe6699cf464fdc1b2c0",
568
+ "model-00001-of-00003.safetensors=sha256:04b57854545c23779b562ee2ae22f10021ba0f4d586ba0ad482ee6eda187d562",
569
+ "model-00002-of-00003.safetensors=sha256:34954aaa05bc91635776ba6672946da5822626753d80db97b38c0538e9525102",
570
+ "model-00003-of-00003.safetensors=sha256:a6b3a55b9e3b2e1778de34c665c3dd17bdfdf6da9d6d5c97730c57168709ccae",
571
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
572
+ "tokenizer_config.json=git-sha1:3cfc5db0c6790859a3bc2a4dc053a813acd65295",
573
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
574
+ ]
575
+ official_repo = "facebook/esm2_t36_3B_UR50D"
576
+ official_revision = "476b639933c8baad5ad09a60ac1a87f987b656fc"
577
+ official_files = [
578
+ "config.json=git-sha1:69e7563923f87d2d7439bfb83e5a19b44b46d71b",
579
+ "pytorch_model-00001-of-00002.bin=sha256:0f971f11c449d21422aa982b791619c10351972992c735f4c3cd43fe09790412",
580
+ "pytorch_model-00002-of-00002.bin=sha256:7560b46fc383c691fb74b915b7d4bcef40d3df181447f16ba4b298845e308d0c",
581
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
582
+ "tokenizer_config.json=git-sha1:3f0d47e841e1cb75257aeaf76d156802899a217e",
583
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
584
+ ]
585
+
586
+ [[models.oracle_assets]]
587
+ role = "weights"
588
+ path = "models/esm2_t36_3B_UR50D.pt"
589
+ url = "https://dl.fbaipublicfiles.com/fair-esm/models/esm2_t36_3B_UR50D.pt"
590
+ sha256 = "7de8b4082ba15891959ab368b77ce3886697af1efb16d3c9e9e7b0c5d3f07500"
591
+ size = 5678116398
592
+
593
+ [[models.oracle_assets]]
594
+ role = "contact_regression"
595
+ path = "regression/esm2_t36_3B_UR50D-contact-regression.pt"
596
+ url = "https://dl.fbaipublicfiles.com/fair-esm/regression/esm2_t36_3B_UR50D-contact-regression.pt"
597
+ sha256 = "4da500eab246481dc9c8c95bc7b1d02f2803d761c380b0e95186d4a07d0fc84e"
598
+ size = 6759
599
+
600
+ [[models]]
601
+ id = "esmc_small"
602
+ family = "esm_plusplus"
603
+ size_category = "medium"
604
+ generation_contract = "not_applicable"
605
+ official_golden = { metadata = "tests/goldens/esmc_small.json=sha256:bb02652cf3cc484756b98ffa4ba55ed4c55870d2cea3342adb1d920ba9dfe10a", tensors = "tests/goldens/esmc_small.safetensors=sha256:03378d0f0fdd8161178ebb2c1f0da1b9776a726c8e8d3a10c009808a24de5654" }
606
+ notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
607
+ fast_repo = "Synthyra/ESMplusplus_small"
608
+ fast_revision = "46c5f7d562e47d4c14165b424c71ab7db008e6fb"
609
+ fast_files = [
610
+ "config.json=git-sha1:df2f44187157b0cc371c48c887b77b1783679201",
611
+ "model.safetensors=sha256:d099223765bc4f1ae8d6c7e18561ce41df1d54073fdc5327ef0a229235a8f52a",
612
+ "special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
613
+ "tokenizer.json=git-sha1:f49735e56cebab0e791aeaae777757b7fd114f71",
614
+ "tokenizer_config.json=git-sha1:2985ed2b8aa8ecfb1d12f53f47d2b8a44cc21756",
615
+ ]
616
+ official_repo = "biohub/ESMC-300M"
617
+ official_revision = "a59b831785f907e96e6a246b1d142bfb76df31ee"
618
+ official_files = [
619
+ "config.json=git-sha1:9a49eacf4e65c39f74381f0f0d240e3b89ef43d7",
620
+ "model.safetensors=sha256:0772d8fe64bb25e14fe6f23b80e3c9a7d215d0da3c6cba5bd356d7c0e0bb22cc",
621
+ "special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
622
+ "tokenizer.json=git-sha1:81c797f56768b22dec0301fa771f018b7e43e98c",
623
+ "tokenizer_config.json=git-sha1:2238856624f8d39f03af53a2576c2d9b18c82f61",
624
+ ]
625
+
626
+ [[models]]
627
+ id = "esmc_large"
628
+ family = "esm_plusplus"
629
+ size_category = "large"
630
+ generation_contract = "not_applicable"
631
+ official_golden = { metadata = "tests/goldens/esmc_large.json=sha256:7a4d614f67b6fde417f3fd89f61e7ec442ae284769734b2b73e14945a816a8fd", tensors = "tests/goldens/esmc_large.safetensors=sha256:e13302df4cf7e8381552f1043a8fd0f31f3e0d50b2ab6009fb86b7940ae8ff79" }
632
+ notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
633
+ fast_repo = "Synthyra/ESMplusplus_large"
634
+ fast_revision = "f813401638b3fddab09748aec1ad2bf537aa4208"
635
+ fast_files = [
636
+ "config.json=git-sha1:5736371902fe5d04e2859be30ac7dbd31b271b25",
637
+ "model.safetensors=sha256:4aff3f8c5de68c4d3e3824eb2c478e4a47355d3f849f3c745e5c8a5ee6cff851",
638
+ "special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
639
+ "tokenizer.json=git-sha1:f49735e56cebab0e791aeaae777757b7fd114f71",
640
+ "tokenizer_config.json=git-sha1:2985ed2b8aa8ecfb1d12f53f47d2b8a44cc21756",
641
+ ]
642
+ official_repo = "biohub/ESMC-600M"
643
+ official_revision = "a7e82012c83126b9eedb055fea9fa84b6c02f094"
644
+ official_files = [
645
+ "config.json=git-sha1:71c8241dc28a5fb636248267a0927c0242b264c1",
646
+ "model.safetensors=sha256:e4232c30fd35fe2f57051ec88a703996ac94520580b4b836894207a3d45d9ff8",
647
+ "special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
648
+ "tokenizer.json=git-sha1:81c797f56768b22dec0301fa771f018b7e43e98c",
649
+ "tokenizer_config.json=git-sha1:2238856624f8d39f03af53a2576c2d9b18c82f61",
650
+ ]
651
+
652
+ [[models]]
653
+ id = "esmc_6b"
654
+ family = "esm_plusplus"
655
+ size_category = "xlarge"
656
+ generation_contract = "not_applicable"
657
+ official_golden = { metadata = "tests/goldens/esmc_6b.json=sha256:e229d938719782f280fab22dfc4c43e86109fdb0cc523631168c5a491afaace3", tensors = "tests/goldens/esmc_6b.safetensors=sha256:a948945e985c7deaca7be8b7eed09c0a9521a2af3f2b10fc2ec7a7d2a0f99ada" }
658
+ notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
659
+ fast_repo = "Synthyra/ESMplusplus_6B"
660
+ fast_revision = "0d579cce3b0f09efa6b3baddf6cc3fd8c9b616c8"
661
+ fast_files = [
662
+ "config.json=git-sha1:e740cbcf211f2511c70c25a1ff6017a757ba7a69",
663
+ "model-00001-of-00006.safetensors=sha256:d30d18703453019f2d2d050866309888720c28eebc9a10307d1ddf3799e85a65",
664
+ "model-00002-of-00006.safetensors=sha256:b3d85378ab5023f4160a96e9c8cbd4cc6f78a771a83c856e88d48112f555bc13",
665
+ "model-00003-of-00006.safetensors=sha256:52595519b59349c5c6e373e6f5ca4a3d48ea6dde345f7e61e24766df5fab0e5b",
666
+ "model-00004-of-00006.safetensors=sha256:e46c6113c89c6f3e9b072c1bef02d763a625c37bcd8f9da2ed9363891c9a0758",
667
+ "model-00005-of-00006.safetensors=sha256:6d92cb2bf9791de644de2ae86f8523d802ac3b4aaabfff0716ab6c2b97f6fb14",
668
+ "model-00006-of-00006.safetensors=sha256:5fc1a8632490bb34162823c35d0d591337b9e4195b22cc0560741397a6e9d0b3",
669
+ "special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
670
+ "tokenizer.json=git-sha1:f49735e56cebab0e791aeaae777757b7fd114f71",
671
+ "tokenizer_config.json=git-sha1:2985ed2b8aa8ecfb1d12f53f47d2b8a44cc21756",
672
+ ]
673
+ official_repo = "biohub/ESMC-6B"
674
+ official_revision = "45b0fa5d7fb06faefbd5e3b89bdcef35d564e79a"
675
+ official_files = [
676
+ "config.json=git-sha1:19f5fb09e4f630fb5b748a497183c22a87ec5102",
677
+ "model-00001-of-00006.safetensors=sha256:bd90149ff223e6ac1a0cac6147a5ae0df20d3a21df4f65356a1f19cd14f4aa8a",
678
+ "model-00002-of-00006.safetensors=sha256:f75e2144d8269fe2eb4b3e0823fb089b94f176d8024153e85b8fb573a42294fa",
679
+ "model-00003-of-00006.safetensors=sha256:f699f01ecc9691d9c6470492765fe54b8b5d2e9f277c139e89427433ffdfe0b2",
680
+ "model-00004-of-00006.safetensors=sha256:46add1b7be098bbfdc3073884851ba3057f1b33ea23a158b650a37007dabd13d",
681
+ "model-00005-of-00006.safetensors=sha256:1e1cb62f060a34e18f54a31a76683ef888b8cec59e73315f5b31d25d45a1f88c",
682
+ "model-00006-of-00006.safetensors=sha256:56c73e13ae96e777ce65eee99364056069ef93b646470f352f83c5f1037b1b18",
683
+ "special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
684
+ "tokenizer.json=git-sha1:81c797f56768b22dec0301fa771f018b7e43e98c",
685
+ "tokenizer_config.json=git-sha1:2238856624f8d39f03af53a2576c2d9b18c82f61",
686
+ ]
687
+
688
+ [[models]]
689
+ id = "esm3_small"
690
+ family = "esm3"
691
+ tokenizer_source = "esmc_small"
692
+ size_category = "large"
693
+ generation_contract = "not_applicable"
694
+ official_golden = { metadata = "tests/goldens/esm3_small.json=sha256:5470e8596cbba0e2882647eccbc53c36d8b48b0f3947d1fe0bcea68da1078c32", tensors = "tests/goldens/esm3_small.safetensors=sha256:d957922f810c9ab4c557d80d5aaaf6a3aab79a5a45e4638012a634a4134803b1" }
695
+ fast_repo = "Synthyra/ESM3_small"
696
+ fast_revision = "7ddb5a740f9e5f93933eb6410c0ee8684bc63ec1"
697
+ fast_files = [
698
+ "config.json=git-sha1:60526e2fdd8af9d4fba17f323775458ef5a1a1f9",
699
+ "model-00001-of-00002.safetensors=sha256:a4c9b736c4c59d51180e966005a164859b47d5cd36e1f8ecdea619fbd34a0e92",
700
+ "model-00002-of-00002.safetensors=sha256:bea60e4e91b03bb00b6cedd29b07606b8543f0869fb74454af7b26e216d80d2b",
701
+ "special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
702
+ "tokenizer.json=git-sha1:f49735e56cebab0e791aeaae777757b7fd114f71",
703
+ "tokenizer_config.json=git-sha1:2985ed2b8aa8ecfb1d12f53f47d2b8a44cc21756",
704
+ ]
705
+ official_repo = "biohub/esm3-sm-open-v1"
706
+ official_revision = "47f0545b2b6daf26a93439a3cd610f4f7f3d5478"
707
+ official_files = [
708
+ "config.json=git-sha1:0967ef424bce6791893e9a57bb952f80fd536e93",
709
+ "data/weights/esm3_function_decoder_v0.pth=sha256:f76d074efcaccfe21365a4fa96f212dadd66798e1e49d809ab7ffbe025d227c9",
710
+ "data/weights/esm3_sm_open_v1.pth=sha256:5ead5a135c658068db6a4f1b933e72d6110992c4668822e1c0e2dcc53e38acd9",
711
+ "data/weights/esm3_structure_decoder_v0.pth=sha256:3b726258a44274792b40ce7ea307e10c5da09936368a4ffa2970264d909da65b",
712
+ "data/weights/esm3_structure_encoder_v0.pth=sha256:467acbaee703ba3ccde6e75241a912a316952e5ff071355f85c1d33c68704f40",
713
+ ]
714
+
715
+ [[models]]
716
+ id = "e1_150m"
717
+ family = "e1"
718
+ size_category = "small"
719
+ generation_contract = "not_applicable"
720
+ official_golden = { metadata = "tests/goldens/e1_150m.json=sha256:701a64a6ab1a2fec5a427555b6af96232526c15cb3d5b4dc7fb253ac8f20b922", tensors = "tests/goldens/e1_150m.safetensors=sha256:6558bc8f1a7b20629eaaaa6f72601d0c2cdb859a5dc13595549b1773b6e2de41" }
721
+ fast_repo = "Synthyra/Profluent-E1-150M"
722
+ fast_revision = "7c5f3bbf697226a2e0900db7a100f9201774a907"
723
+ fast_files = [
724
+ "config.json=git-sha1:562ef21e722ca708064fc3d54d25b731d4ac8171",
725
+ "model.safetensors=sha256:d779ed3a4e23799aafc932dc09c9963428d10aa7075999b5f8851b39c76b67f6",
726
+ ]
727
+ official_repo = "Profluent-Bio/E1-150m"
728
+ official_revision = "c4dbfe827e4aa6ed7f95eaef50dc1e084f4d77dc"
729
+ official_files = [
730
+ "config.json=git-sha1:485e649199b46fe6ee7456bebf7aae9b3d4baeab",
731
+ "model.safetensors=sha256:ba2656339005e6598642836acfdafde480fecc7e145ce0058eb54adf572c3484",
732
+ ]
733
+
734
+ [[models]]
735
+ id = "e1_300m"
736
+ family = "e1"
737
+ size_category = "medium"
738
+ generation_contract = "not_applicable"
739
+ official_golden = { metadata = "tests/goldens/e1_300m.json=sha256:d3478f3f5957a0e0377864074dde0107de890019f96cb63548ee17ffb8f3ec3a", tensors = "tests/goldens/e1_300m.safetensors=sha256:92778b9ef95a803ddc84b3e3ca764c59e045872a94bcff0eb0cd47647732c188" }
740
+ fast_repo = "Synthyra/Profluent-E1-300M"
741
+ fast_revision = "5ef52c0ad2ae2578f40622696b763523810e8e26"
742
+ fast_files = [
743
+ "config.json=git-sha1:f5c91498b76a3e3282a0d716d87738abb1a1b6c1",
744
+ "model.safetensors=sha256:9271c4176a8a2e0905a0bb769570ba1c2978fb999a87da92db4cf2b041224864",
745
+ ]
746
+ official_repo = "Profluent-Bio/E1-300m"
747
+ official_revision = "5a2871c587eadbcc9237bc686ea45e5b4d28dfb3"
748
+ official_files = [
749
+ "config.json=git-sha1:918cb09e6e96d4719ed85951f38c693360f9cdb8",
750
+ "model.safetensors=sha256:31e09a2542f45b04e6ce4adafb3b657f21e2d56d12bf68fd2266b1576a80bc9b",
751
+ ]
752
+
753
+ [[models]]
754
+ id = "e1_600m"
755
+ family = "e1"
756
+ size_category = "large"
757
+ generation_contract = "not_applicable"
758
+ official_golden = { metadata = "tests/goldens/e1_600m.json=sha256:914be191c28141c1f84535cdb69ead0588a2057bb19d46c5bc7f3891a3d6739e", tensors = "tests/goldens/e1_600m.safetensors=sha256:22ed8417a4651ded255099f6d15c63c2c40552e700d2b0470d1adfde3a39c513" }
759
+ fast_repo = "Synthyra/Profluent-E1-600M"
760
+ fast_revision = "6c8bf0ec83b0e0178677c528b101efffd0677742"
761
+ fast_files = [
762
+ "config.json=git-sha1:1d35c0b35b473259875fd29ee80167487a0d6afe",
763
+ "model.safetensors=sha256:793483b1b3411eab73fe5214b94d1424ca0545992dfac6889cfc0186af472363",
764
+ ]
765
+ official_repo = "Profluent-Bio/E1-600m"
766
+ official_revision = "52d959fb87a609d15cf223a485127b29ed5c382a"
767
+ official_files = [
768
+ "config.json=git-sha1:8a0a439ed4201462bc01189c9f8b43523b257b5c",
769
+ "model.safetensors=sha256:cfc108d4b98baaa62932331b40be265eae39dc382595bc3cde4a5ab55db1bf7a",
770
+ ]
771
+
772
+ [[models]]
773
+ id = "dplm_150m"
774
+ family = "dplm"
775
+ size_category = "small"
776
+ generation_contract = "required"
777
+ official_golden = { metadata = "tests/goldens/dplm_150m.json=sha256:3228551fe3bed951db9ec97347143ec4462ce7c221ac240b7ce7730948c1dc1f", tensors = "tests/goldens/dplm_150m.safetensors=sha256:392992235195beed97ab8359b90a2e11e52f4326606f99a471447bed81d146bd" }
778
+ fast_repo = "Synthyra/DPLM-150M"
779
+ fast_revision = "90ba742754151a774f3b7ed580170d0a76b3e69d"
780
+ fast_files = [
781
+ "config.json=git-sha1:117ac2c1222152ef378abaad1f605e18c4a18ab0",
782
+ "model.safetensors=sha256:8bac5ac767ceb8deb511b272d32883f811768d56cb25e920cea94ba9b979ca14",
783
+ "special_tokens_map.json=git-sha1:ef5f0f7d7baf4947564eafcf79972d272cd80a15",
784
+ "tokenizer_config.json=git-sha1:80100348e3f2b8ab05b59f3352ea7631685083cd",
785
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
786
+ ]
787
+ official_repo = "airkingbd/dplm_150m"
788
+ official_revision = "49b7125a5d28c6418fcc2f3c4fe799352ac1488b"
789
+ official_files = [
790
+ "config.json=git-sha1:4910cb02f1840e9ac577026f601829604af58c74",
791
+ "pytorch_model.bin=sha256:ea4eaa99536b60ed76f945f71a1a5e604f08447ec3def5104a93ca6001a59961",
792
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
793
+ "tokenizer_config.json=git-sha1:dbcdd9fb2e742627ee310713615e0d7aeed0c34e",
794
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
795
+ ]
796
+
797
+ [[models]]
798
+ id = "dplm_650m"
799
+ family = "dplm"
800
+ size_category = "large"
801
+ generation_contract = "required"
802
+ official_golden = { metadata = "tests/goldens/dplm_650m.json=sha256:bf58d0ce73aaac7e6fb1923ef3d9adad67122df2a3dd414c3229488ef9587a6d", tensors = "tests/goldens/dplm_650m.safetensors=sha256:073f0a6abea7e48f28c2d921ff8329a28e22627f01979277cb324908a01b3378" }
803
+ fast_repo = "Synthyra/DPLM-650M"
804
+ fast_revision = "05dc16d97c5c028aed924c9ed681cee4ab609760"
805
+ fast_files = [
806
+ "config.json=git-sha1:3537150eb87b213a676d5840548625e220b60e8b",
807
+ "model.safetensors=sha256:e27a47b8ec1c078b3fccb36542210e20f0380c88828db2ca9acf3d8a25048bd8",
808
+ "special_tokens_map.json=git-sha1:ef5f0f7d7baf4947564eafcf79972d272cd80a15",
809
+ "tokenizer_config.json=git-sha1:80100348e3f2b8ab05b59f3352ea7631685083cd",
810
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
811
+ ]
812
+ official_repo = "airkingbd/dplm_650m"
813
+ official_revision = "7a7e651baa667d094aba05e9dc1cf52a3332110a"
814
+ official_files = [
815
+ "config.json=git-sha1:625574d625a4178ca6966e9545fee56026c0b634",
816
+ "pytorch_model.bin=sha256:db4e54343a89e7600f41c3aacbc593db1b0caee82ec28cab25ff2ae090eba39c",
817
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
818
+ "tokenizer_config.json=git-sha1:dbcdd9fb2e742627ee310713615e0d7aeed0c34e",
819
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
820
+ ]
821
+
822
+ [[models]]
823
+ id = "dplm_3b"
824
+ family = "dplm"
825
+ size_category = "xlarge"
826
+ generation_contract = "required"
827
+ official_golden = { metadata = "tests/goldens/dplm_3b.json=sha256:a5b6df8b9c7b371976892ec1d6c45581a32ad3a6325c6c0a0b3267012848c8ed", tensors = "tests/goldens/dplm_3b.safetensors=sha256:75b0a0854fc391133920b0feaaeb8f69ab7568a88b3759627aca1556c4338c1e" }
828
+ fast_repo = "Synthyra/DPLM-3B"
829
+ fast_revision = "7d764dd3d70ecf1ac0e64693de64a0064aacac65"
830
+ fast_files = [
831
+ "config.json=git-sha1:7f5baf9426be06760c86882948b0f4af2e681e22",
832
+ "model-00001-of-00003.safetensors=sha256:37b54855d087ef3e7d883464ae9d5ea3127ec15a16c6323d91ad16a6b98305c9",
833
+ "model-00002-of-00003.safetensors=sha256:042604fefb05ea8c360a48416ce7ba662a4f90b176b4baf646c5c1814c35e6e8",
834
+ "model-00003-of-00003.safetensors=sha256:b9ae04012665163c3fc9781dd04fcd69738ac20c07e615e98fc4483fd2c4de45",
835
+ "special_tokens_map.json=git-sha1:ef5f0f7d7baf4947564eafcf79972d272cd80a15",
836
+ "tokenizer_config.json=git-sha1:80100348e3f2b8ab05b59f3352ea7631685083cd",
837
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
838
+ ]
839
+ official_repo = "airkingbd/dplm_3b"
840
+ official_revision = "53849d4a7fe944ae0b9cf2bbc0d2cc0054795b51"
841
+ official_files = [
842
+ "config.json=git-sha1:f6206456e8c2f22ebe1d37fce3b5d50fd8073e68",
843
+ "pytorch_model-00001-of-00004.bin=sha256:0bcb86a115fe744ed686756db143f78851304e855e2f83cec58681c6080ced5f",
844
+ "pytorch_model-00002-of-00004.bin=sha256:daf3324f3be949e7dd1c3c84b28da7fec5151b1890cb0904e73427266856a06f",
845
+ "pytorch_model-00003-of-00004.bin=sha256:dbbeb7924a21059854f994931e23590b054aa000b10370a71c052c4aa36e9246",
846
+ "pytorch_model-00004-of-00004.bin=sha256:21c01740d091487db43446489d8a893dea1fcc6f2e1c1991ece13945f7ab4e07",
847
+ "special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
848
+ "tokenizer_config.json=git-sha1:dbcdd9fb2e742627ee310713615e0d7aeed0c34e",
849
+ "vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
850
+ ]
851
+
852
+ [[models]]
853
+ id = "dplm2_150m"
854
+ family = "dplm2"
855
+ size_category = "small"
856
+ generation_contract = "required"
857
+ official_golden = { metadata = "tests/goldens/dplm2_150m.json=sha256:d269de779ea1503de72c77e7b2e6224afc9797bd945b40c571ff6faec782e4aa", tensors = "tests/goldens/dplm2_150m.safetensors=sha256:17fc26600938ba5364b8ecb96750786d33e9f92bcd4ea4df3e12a389340748eb" }
858
+ artifact_source = "official"
859
+ canonical_state_sha256 = "82e1751f59052b8de72b082517557db47947e8d9b4ac2f11278369e6c0cbf001"
860
+ fast_repo = "Synthyra/DPLM2-150M"
861
+ fast_revision = "182745b8dc5661f898481a4fa60a7af9d53385c4"
862
+ fast_files = [
863
+ "config.json=git-sha1:07905a2e4327d27d073cd0390f140aec2976125a",
864
+ "model.safetensors=sha256:0a7751b3113027b1d9c966a5bda2d6ab831855de7aaa047b911731665a7c3cc6",
865
+ "special_tokens_map.json=git-sha1:e6378d20e897b8806734e65fd3ef9cf42a17631b",
866
+ "tokenizer_config.json=git-sha1:f2090783e3368b7323aa877e2b740e09f0862259",
867
+ "vocab.txt=git-sha1:9706a4277a5c39dc9b4ec7b283e8eb130ceaa7f2",
868
+ ]
869
+ official_repo = "airkingbd/dplm2_150m"
870
+ official_revision = "3451d984d06497f835ed49634bd68c9dfb54d730"
871
+ official_files = [
872
+ "config.json=git-sha1:20f1e55c64fdc4d1d30f7b1df64b6167fa23dc7c",
873
+ "pytorch_model.bin=sha256:be7f5cf9e421f59fcc437e63ce1c7391099a314a4e9a4f10b8688785fa581238",
874
+ "special_tokens_map.json=git-sha1:eb760e9f49a55145bbe0c64922d4ec2d3de1692a",
875
+ "tokenizer_config.json=git-sha1:fc8c21760dcff173955afb106859e5f015d4f757",
876
+ "vocab.txt=git-sha1:e133a3abd4350ddc3fc62548e162c8df7e62cf37",
877
+ ]
878
+
879
+ [[models]]
880
+ id = "dplm2_650m"
881
+ family = "dplm2"
882
+ size_category = "large"
883
+ generation_contract = "required"
884
+ official_golden = { metadata = "tests/goldens/dplm2_650m.json=sha256:d9a7548f9af657a72d441ca70f27379863724fcce8ddd3da4f672104b7bfb772", tensors = "tests/goldens/dplm2_650m.safetensors=sha256:c4e0e467c252c3ac813363d2d4b17a5e3bd99e75fad315e76d97689b4655ddac" }
885
+ artifact_source = "official"
886
+ canonical_state_sha256 = "cba76b6602d2258de9fffff953b608d93cb8ef4a9e89b0bbd27e160c81e78bb4"
887
+ fast_repo = "Synthyra/DPLM2-650M"
888
+ fast_revision = "b9d8527a9473a54954fa2764f590b9ea1b435bb2"
889
+ fast_files = [
890
+ "config.json=git-sha1:3e079579b214d48a09db57f2c60be6a1acea5baf",
891
+ "model.safetensors=sha256:92db08c7dbfd6c5e03fbfeaea3f36b09640ee794dcf5ea8d550527869a9f1d63",
892
+ "special_tokens_map.json=git-sha1:e6378d20e897b8806734e65fd3ef9cf42a17631b",
893
+ "tokenizer_config.json=git-sha1:f2090783e3368b7323aa877e2b740e09f0862259",
894
+ "vocab.txt=git-sha1:9706a4277a5c39dc9b4ec7b283e8eb130ceaa7f2",
895
+ ]
896
+ official_repo = "airkingbd/dplm2_650m"
897
+ official_revision = "0bc69b644976c6680ab7e26669854d1979e8876e"
898
+ official_files = [
899
+ "config.json=git-sha1:4cce8d9dc212cdace0e20e89169790bcf199c158",
900
+ "pytorch_model.bin=sha256:8d6e08cc05e4858064a714013c74cc88c9caa2cc8b12c34605a3c24bcd877cfb",
901
+ "special_tokens_map.json=git-sha1:eb760e9f49a55145bbe0c64922d4ec2d3de1692a",
902
+ "tokenizer_config.json=git-sha1:fc8c21760dcff173955afb106859e5f015d4f757",
903
+ "vocab.txt=git-sha1:e133a3abd4350ddc3fc62548e162c8df7e62cf37",
904
+ ]
905
+
906
+ [[models]]
907
+ id = "dplm2_3b"
908
+ family = "dplm2"
909
+ size_category = "xlarge"
910
+ # The pinned public sampler fails before generation because cls_token_id is None.
911
+ # State, tokenizer, and inference parity remain required for this checkpoint.
912
+ generation_contract = "official_unavailable"
913
+ official_golden = { metadata = "tests/goldens/dplm2_3b.json=sha256:d6e0e02af53b13cb129192f06e264758aa21c9ebf4ee82411cf67037082d2329", tensors = "tests/goldens/dplm2_3b.safetensors=sha256:838b11824d08f83bcb0c0b3268e579f3a87dbfb965370cfe5c3f8793b96b1964" }
914
+ notes = "The pinned official DPLM2-3B sampler fails before generation, so live generation equivalence cannot be established for this checkpoint. State, tokenizer, and inference parity remain required."
915
+ artifact_source = "official"
916
+ canonical_state_sha256 = "8c46ec09115dbe6cbfb91d94ab5e906369d57e27fe620a7741c6f8cb1b6ca890"
917
+ fast_repo = "Synthyra/DPLM2-3B"
918
+ fast_revision = "2a63babe8848abf5233d31bd55891dff8285fc50"
919
+ fast_files = [
920
+ "config.json=git-sha1:5932b1d501fed28b84614e0d2c1ecc4e89f10d6e",
921
+ "model-00001-of-00003.safetensors=sha256:2ff393f6e8df1568ce075d50de69ff4e5e9d9886e5ec47e43d6c24df23459be3",
922
+ "model-00002-of-00003.safetensors=sha256:feb3cea852c2aa849cc30783a984a97f0d076990ade6606cda5e38bf2a5a9621",
923
+ "model-00003-of-00003.safetensors=sha256:9be363ddb98436af20901981ffbed2f1097377424987f6c1baad27d512b62e71",
924
+ "special_tokens_map.json=git-sha1:e6378d20e897b8806734e65fd3ef9cf42a17631b",
925
+ "tokenizer_config.json=git-sha1:f2090783e3368b7323aa877e2b740e09f0862259",
926
+ "vocab.txt=git-sha1:9706a4277a5c39dc9b4ec7b283e8eb130ceaa7f2",
927
+ ]
928
+ official_repo = "airkingbd/dplm2_3b"
929
+ official_revision = "9e77567926f98d1b997ea9131a8eeb035b9bf827"
930
+ official_files = [
931
+ "config.json=git-sha1:22d51ce44cd6da8d819e0d00566987bb51d74753",
932
+ "pytorch_model-00001-of-00004.bin=sha256:d8c641eae6bf891581ec64d543169891b093e296f5679ac75c695bcf596b4211",
933
+ "pytorch_model-00002-of-00004.bin=sha256:6478ad86ec5fef3d1d26580493af2d8666009d3ff884f3f88548080c8bbf94b5",
934
+ "pytorch_model-00003-of-00004.bin=sha256:dde8f88dac4a6355488c2fb433ee12cd69f1169950566624fba43684d4d99dc6",
935
+ "pytorch_model-00004-of-00004.bin=sha256:17ec0145152bc10e4dd3b4c2edff337979f6b99ee7c7bfd6cf4e6dbd7262d079",
936
+ "special_tokens_map.json=git-sha1:eb760e9f49a55145bbe0c64922d4ec2d3de1692a",
937
+ "tokenizer_config.json=git-sha1:fc8c21760dcff173955afb106859e5f015d4f757",
938
+ "vocab.txt=git-sha1:e133a3abd4350ddc3fc62548e162c8df7e62cf37",
939
+ ]
940
+
941
+ [[models]]
942
+ id = "ankh_base"
943
+ family = "ankh"
944
+ size_category = "medium"
945
+ generation_contract = "required"
946
+ official_golden = { metadata = "tests/goldens/ankh_base.json=sha256:ebce8d7de821827ee995789c9b38d79252d3b2f76888130b0a8a7eedafaefe2b", tensors = "tests/goldens/ankh_base.safetensors=sha256:f0e78aa15d11749e0c64ff57f9e88c51cec6538a0adf8951f839df70cc708b65" }
947
+ notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
948
+ artifact_source = "official"
949
+ canonical_state_sha256 = "cdd8d30d88e5bf41f44e1eef4470d8e46607aba5f7c7c805b06c035b89c8c16f"
950
+ fast_repo = "Synthyra/ANKH_base"
951
+ fast_revision = "7ec329aae8e3e174bf22a1eb9e0e9fcc12b53092"
952
+ fast_files = [
953
+ "config.json=git-sha1:7e1cbce6d08f9bb64eee4410899b1c6b4054f418",
954
+ "model.safetensors=sha256:b0d3473cac1bda90e39cde54f2abe86da1fc84f872c833ca3415672776dccb95",
955
+ "special_tokens_map.json=git-sha1:a2d8d626c31389a935e197fb94072e2414a6e7d1",
956
+ "tokenizer.json=git-sha1:0734d752d12d0f46ac96467fbceb1c4bfbeee0be",
957
+ "tokenizer_config.json=git-sha1:db0b80de72d3b16242b9eda74ed4663e39c65bcf",
958
+ ]
959
+ official_repo = "ElnaggarLab/ankh-base"
960
+ official_revision = "d99cb6b966530dfc2ae96bc69d9255c2a07308b0"
961
+ official_files = [
962
+ "config.json=git-sha1:abd44a36b5469e9a7cb019e4059b5ac1392d8422",
963
+ "pytorch_model.bin=sha256:9b2a886374f0ff4a893f4e7a989deed76bb2458c8998bd5202ea8e97d92ddcc3",
964
+ "special_tokens_map.json=git-sha1:55b145827029ae9672e50d4bb368540daacce791",
965
+ "tokenizer.json=git-sha1:212c5ef08819fa2463c6289ba4ef7db30e715c0a",
966
+ "tokenizer_config.json=git-sha1:a8a872ae3441e7cc85ce19210dff1e4c5d2d7bd0",
967
+ ]
968
+
969
+ [[models]]
970
+ id = "ankh_large"
971
+ family = "ankh"
972
+ size_category = "large"
973
+ generation_contract = "required"
974
+ official_golden = { metadata = "tests/goldens/ankh_large.json=sha256:59492518b021de5cfaea87d672c9448c8558e99a3443ba2cc7ab544963196ecb", tensors = "tests/goldens/ankh_large.safetensors=sha256:3fb8d3ac27716d15a9ea92aeef6acf2b977bcc887d9b535000539e523673459b" }
975
+ notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
976
+ artifact_source = "official"
977
+ canonical_state_sha256 = "e498a2e9aea76ef784cbe3e596c6b3f5e9a40e209ad837f7e3207099e4d74483"
978
+ fast_repo = "Synthyra/ANKH_large"
979
+ fast_revision = "3be3df34140f49dc4e65bd1f247e3ce819e7fc59"
980
+ fast_files = [
981
+ "config.json=git-sha1:272509deedb527e5c2c95b0c269194a44148fdcc",
982
+ "model.safetensors=sha256:e70b8f9755ac6bfe95d18359060ae9fe38fac63b12a89a886c83349d1adbaa53",
983
+ "special_tokens_map.json=git-sha1:a2d8d626c31389a935e197fb94072e2414a6e7d1",
984
+ "tokenizer.json=git-sha1:0734d752d12d0f46ac96467fbceb1c4bfbeee0be",
985
+ "tokenizer_config.json=git-sha1:2bcaff2567826f5f51188b00600d2c6e7bcea56e",
986
+ ]
987
+ official_repo = "ElnaggarLab/ankh-large"
988
+ official_revision = "74b371dbfa3ee0a05d32ae74df0c2e0b82d6b9a6"
989
+ official_files = [
990
+ "config.json=git-sha1:1abf33e52ee3d6be67d780ec57d32ac2b27b5306",
991
+ "pytorch_model.bin=sha256:517b6e8b279dedcb477af240b35c46bd6eb3307723eb281e60d4b2c8a87b889b",
992
+ "special_tokens_map.json=git-sha1:55b145827029ae9672e50d4bb368540daacce791",
993
+ "tokenizer.json=git-sha1:212c5ef08819fa2463c6289ba4ef7db30e715c0a",
994
+ "tokenizer_config.json=git-sha1:d7fe02ba6f2b18d9ccfa19ac129c9fdc9ec24d09",
995
+ ]
996
+
997
+ [[models]]
998
+ id = "ankh2_large"
999
+ family = "ankh"
1000
+ size_category = "large"
1001
+ generation_contract = "required"
1002
+ official_golden = { metadata = "tests/goldens/ankh2_large.json=sha256:e8df38994ca1a1e0c598ace34a0b257b264937e4fdbb01bc41544985116b02a4", tensors = "tests/goldens/ankh2_large.safetensors=sha256:25fe1569f55c635fab8fa49c1d62a889a35a2a738bad921f5764a85b58fd4b5d" }
1003
+ notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
1004
+ artifact_source = "official"
1005
+ canonical_state_sha256 = "597c4fe2fa8711f11a25317905f1d62fa92905e55fdd5c0a79614cd9c9d2bca3"
1006
+ fast_repo = "Synthyra/ANKH2_large"
1007
+ fast_revision = "392de5ed52bbfd73b45f545e378aaebcff096d0e"
1008
+ fast_files = [
1009
+ "config.json=git-sha1:66b6adc7215743a98a3229958bbd1c9c42b6108b",
1010
+ "model.safetensors=sha256:be8e6242388d93b51cd9719a0e32cfc17a2e804786570c795ba332197eccb915",
1011
+ "special_tokens_map.json=git-sha1:a2d8d626c31389a935e197fb94072e2414a6e7d1",
1012
+ "tokenizer.json=git-sha1:0734d752d12d0f46ac96467fbceb1c4bfbeee0be",
1013
+ "tokenizer_config.json=git-sha1:db0b80de72d3b16242b9eda74ed4663e39c65bcf",
1014
+ ]
1015
+ official_repo = "ElnaggarLab/ankh2-ext2"
1016
+ official_revision = "aa9b9fa72288c47d9f618ce80c011e24b54e17a8"
1017
+ official_files = [
1018
+ "config.json=git-sha1:9286bed4ecbc4f7113024919d16ec9719b0c0748",
1019
+ "generation_config.json=git-sha1:91f792e452403d46e170e206f9e50be5ddef9b9a",
1020
+ "pytorch_model.bin=sha256:2df583f28f111276ee22a7b76007f4297e9a69766d60bccd9c8d7169c06ac606",
1021
+ "special_tokens_map.json=git-sha1:55b145827029ae9672e50d4bb368540daacce791",
1022
+ "tokenizer.json=git-sha1:212c5ef08819fa2463c6289ba4ef7db30e715c0a",
1023
+ "tokenizer_config.json=git-sha1:854e5db75dae8b1e9dd39c5bae80dae5508b3e25",
1024
+ ]
1025
+
1026
+ [[models]]
1027
+ id = "ankh3_large"
1028
+ family = "ankh"
1029
+ size_category = "large"
1030
+ generation_contract = "required"
1031
+ official_golden = { metadata = "tests/goldens/ankh3_large.json=sha256:2e5bb05b3baa5baa78f61fef7d2a2c669b0da5dbfaf6b50b12abd3e17253a961", tensors = "tests/goldens/ankh3_large.safetensors=sha256:e5c494ac418e0a2fe7bdad1376676d48960d58ec9e044d19bfffccb8c3288513" }
1032
+ notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
1033
+ artifact_source = "official"
1034
+ canonical_state_sha256 = "60acb7ef86e85dc0c51fc1edf4c8e69a0480049723b6b2c95e6e9faa720c112a"
1035
+ fast_repo = "Synthyra/ANKH3_large"
1036
+ fast_revision = "53600f175f328f986f43e55ca8ceb14935d337a4"
1037
+ fast_files = [
1038
+ "config.json=git-sha1:432b09625d44a2eeab679fddb7495d42b560b7f9",
1039
+ "model.safetensors=sha256:9f50f58cf5b3a537a0a41aa918695c3a26d7985dd0b2266642d6f86324c9e7a1",
1040
+ "special_tokens_map.json=git-sha1:1fc3a4d6d4282e5201cd7c30d5c0a6a8bfa04f82",
1041
+ "tokenizer.json=git-sha1:3d14291df2d6db3a183c5c4fe133afb330cc44cf",
1042
+ "tokenizer_config.json=git-sha1:2005fec00a7ae9a49e248a1ecefbbd81c56674d6",
1043
+ ]
1044
+ official_repo = "ElnaggarLab/ankh3-large"
1045
+ official_revision = "2be091622e8a393f0ef21735070084123c874b6e"
1046
+ official_files = [
1047
+ "config.json=git-sha1:f5278f77d158cdd8a173df888e3ed365e84a80a3",
1048
+ "generation_config.json=git-sha1:5767cc0cacebfd06884eb27ae1c796d3ca829fd2",
1049
+ "pytorch_model.bin=sha256:26321a345e07a25b21c6c41b651c4db91b420892e52c0dcbc55bd7a8f510f95b",
1050
+ "special_tokens_map.json=git-sha1:d596919b7fa2a197edd441ec3ec4685ecacd2de4",
1051
+ "spiece.model=sha256:f2b5e1bbd110b71ca9b2878e1fcd3265610076ecc97bd696e8a745c9bacc54e0",
1052
+ "tokenizer.json=git-sha1:90f0c94b43c81496b3ca81e3ec1c092ef2dd7fca",
1053
+ "tokenizer_config.json=git-sha1:0e699eebfa778698473b4faf1e66ef363b93fb21",
1054
+ ]
1055
+
1056
+ [[models]]
1057
+ id = "ankh3_xl"
1058
+ family = "ankh"
1059
+ size_category = "xlarge"
1060
+ generation_contract = "required"
1061
+ official_golden = { metadata = "tests/goldens/ankh3_xl.json=sha256:66bb12e033e4163be225d636108a479393228a4f5061015c8af114e766c3c486", tensors = "tests/goldens/ankh3_xl.safetensors=sha256:72d34567d0228cb6f1ee701c578ed4039fead4346e3f161a52e0e74df28dc8ae" }
1062
+ notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head. The official PyTorch shard index is deliberately excluded: the builder verifies every declared source shard directly and writes a new canonical safetensors index."
1063
+ artifact_source = "official"
1064
+ canonical_state_sha256 = "dd2188e0d2ca65232135714eef6de394239734d843ddae4928c7398685d858e7"
1065
+ fast_repo = "Synthyra/ANKH3_xl"
1066
+ fast_revision = "3cbf2c22c4f7d67bf0bfcbdcd500f41723e91d29"
1067
+ fast_files = [
1068
+ "config.json=git-sha1:23f6d78ddcb3a031b88f876eaaf04c2fafaea46f",
1069
+ "model-00001-of-00003.safetensors=sha256:39bd8f75cf98a67cf04055399f9fc401198f6fc2896b112aba9fd9ec9df52ab9",
1070
+ "model-00002-of-00003.safetensors=sha256:9ff73233b39d2c200abb78e66b320c014ec61431bd6e1af36fb188a3cfa24c34",
1071
+ "model-00003-of-00003.safetensors=sha256:c13125c02dbcd7f07bd412e9e085f2bca6624d2f1f45fedc95fb777f53161cbe",
1072
+ "special_tokens_map.json=git-sha1:1fc3a4d6d4282e5201cd7c30d5c0a6a8bfa04f82",
1073
+ "tokenizer.json=git-sha1:3d14291df2d6db3a183c5c4fe133afb330cc44cf",
1074
+ "tokenizer_config.json=git-sha1:2005fec00a7ae9a49e248a1ecefbbd81c56674d6",
1075
+ ]
1076
+ official_repo = "ElnaggarLab/ankh3-xl"
1077
+ official_revision = "e00113df5c95ef71df7ea3f5a73d56bd00e473a4"
1078
+ official_files = [
1079
+ "config.json=git-sha1:f8997040e8913df75fd2eebe71a2a8eb750ed0d0",
1080
+ "generation_config.json=git-sha1:91f792e452403d46e170e206f9e50be5ddef9b9a",
1081
+ "pytorch_model-00001-of-00003.bin=sha256:2c9793cbee16697cd4149debe07d3a27143e280f6e970fa46042aae820fea981",
1082
+ "pytorch_model-00002-of-00003.bin=sha256:31c5a860e414513c829ae52affb0970d7cef2c0545df2d6e1338b6806ab7174b",
1083
+ "pytorch_model-00003-of-00003.bin=sha256:055a853bdd3623db95a637935aa299427e837cd8ea69fc04708b0262508bec75",
1084
+ "special_tokens_map.json=git-sha1:d596919b7fa2a197edd441ec3ec4685ecacd2de4",
1085
+ "spiece.model=sha256:f2b5e1bbd110b71ca9b2878e1fcd3265610076ecc97bd696e8a745c9bacc54e0",
1086
+ "tokenizer.json=git-sha1:90f0c94b43c81496b3ca81e3ec1c092ef2dd7fca",
1087
+ "tokenizer_config.json=git-sha1:0e699eebfa778698473b4faf1e66ef363b93fb21",
1088
+ ]
1089
+
1090
+ [[models]]
1091
+ id = "boltz2"
1092
+ family = "boltz2"
1093
+ size_category = "structure"
1094
+ generation_contract = "not_applicable"
1095
+ notes = "Boltz2 is provisional in FastPLMs 1.0. Exact configuration, the declared inference-core state, feature preparation, and seeded execution remain tested, but native-environment BF16 end-to-end inference currently exceeds the fixed numerical-equivalence limits. FastPLMs therefore does not claim official inference equivalence for this checkpoint yet. Work on that numerical gap continues independently of the ESM++ and ESMFold2 release gates."
1096
+ fast_repo = "Synthyra/Boltz2"
1097
+ fast_revision = "3b148fc5efea109c065ec82ba8683d024de7134e"
1098
+ fast_files = [
1099
+ "config.json=git-sha1:8682ccb12e177e73bc7a351ff7e3af484bfb6fac",
1100
+ "model.safetensors=sha256:5c863fd200a1613a0e311071e2ad73ab350635e3fd336e6822cf45c52cb960e5",
1101
+ ]
1102
+ official_repo = "boltz-community/boltz-2"
1103
+ official_revision = "6fdef46d763fee7fbb83ca5501ccceff43b85607"
1104
+ official_files = [
1105
+ "boltz2_conf.ckpt=sha256:090e82ac8c92f5e943fa1b39e7410a44027bea7243c0bbb3caa67a77fc1428e1",
1106
+ "mols.tar=sha256:39e076d96dbec6b4e86982bbda16f3a53a2a60c9bdc17828d88f6f9a0c7d1fd7",
1107
+ ]
1108
+
1109
+ [[models]]
1110
+ id = "esmfold"
1111
+ family = "esmfold"
1112
+ size_category = "structure"
1113
+ generation_contract = "not_applicable"
1114
+ official_golden = { metadata = "tests/goldens/esmfold.json=sha256:380b9a96168410717d1f698feaabb826b1606444cbdeec86c2ea06d9ffe8f186", tensors = "tests/goldens/esmfold.safetensors=sha256:873b1b325a43d8e0f35f355c8914a2a9fe611cc48763875e9e6a22e09ec9ebcb" }
1115
+ fast_repo = "Synthyra/FastESMFold"
1116
+ fast_revision = "b88c8cb50d19b2cf7ab4fee4b0a61f5e02da7823"
1117
+ fast_files = [
1118
+ "config.json=git-sha1:18e0091dcbf6140bf68924d53c4c8917b9cd90b1",
1119
+ "model-00001-of-00003.safetensors=sha256:36fab9e5c96d409b2a34a8b4f1273acac8c07f119c32c4fcfa7d47bbcd55b83c",
1120
+ "model-00002-of-00003.safetensors=sha256:34954aaa05bc91635776ba6672946da5822626753d80db97b38c0538e9525102",
1121
+ "model-00003-of-00003.safetensors=sha256:2f1178cda0e6cff3b1e158e1acc59c83e3f4fc46e246388a5127bc56b8d9c4f2",
1122
+ "special_tokens_map.json=git-sha1:53cd95604a28eb7e23da763c8da23f5006ab2179",
1123
+ "tokenizer_config.json=git-sha1:10213f69b51b4b38876a29271b8f908e853a5800",
1124
+ "vocab.txt=git-sha1:eee0a1fc93c82568f78f086550fbd7c591cf423a",
1125
+ ]
1126
+ official_repo = "facebook/esmfold_v1"
1127
+ official_revision = "75a3841ee059df2bf4d56688166c8fb459ddd97a"
1128
+ official_files = [
1129
+ "config.json=git-sha1:1232d0aee4be551021d8e70e66ed2b062df917bf",
1130
+ "pytorch_model.bin=sha256:2ee07356b125d1e3e57503c204111fd7323347fc4735d41d3caac57c2a78e116",
1131
+ "special_tokens_map.json=git-sha1:121c8d54f8ea66cdf678f48b3cb37c05b4de5c0d",
1132
+ "tokenizer_config.json=git-sha1:aad24fba9f1bad2d74ed79d414ddcd60e6b0f812",
1133
+ "vocab.txt=git-sha1:9abfdf5472c0ed970648b683b86ab131256b3e42",
1134
+ ]
1135
+
1136
+ [[models.oracle_assets]]
1137
+ role = "weights"
1138
+ path = "models/esmfold_3B_v1.pt"
1139
+ url = "https://dl.fbaipublicfiles.com/fair-esm/models/esmfold_3B_v1.pt"
1140
+ sha256 = "e9a52579027e77d2d2e0a18218e755821f395730e86624cab9413dc117f5ca62"
1141
+ size = 2771653574
1142
+
1143
+ [[models]]
1144
+ id = "esmfold2"
1145
+ family = "esmfold2"
1146
+ size_category = "structure"
1147
+ generation_contract = "not_applicable"
1148
+ msa_conditioning = true
1149
+ official_golden = { metadata = "tests/goldens/esmfold2.json=sha256:f6e0ed1ec400b9a0fcc817db51774be968dc454b7a32645a07c479e42423ab20", tensors = "tests/goldens/esmfold2.safetensors=sha256:e4d6be4344c528e26b13f79a9303549e3de7e582da195c0078db3ce957fad420" }
1150
+ fast_repo = "Synthyra/ESMFold2"
1151
+ fast_revision = "cd5a0927cec585a778d983b99a8db23d2e9b281e"
1152
+ fast_files = [
1153
+ "config.json=git-sha1:67e81ff571f393f0b630cd5a22398bd84979c030",
1154
+ "model.safetensors=sha256:138fd4350d6892b81ce6be7ff9bf5a93ae9d4d3751f46a27438a3f9f0dcefa0e",
1155
+ ]
1156
+ official_repo = "biohub/ESMFold2"
1157
+ official_revision = "1ebf0e3481a5184eb6171d40615c79e384b48796"
1158
+ official_files = [
1159
+ "config.json=git-sha1:0300c084b990b2bd600efd9f538aa5de27109fea",
1160
+ "model.safetensors=sha256:138fd4350d6892b81ce6be7ff9bf5a93ae9d4d3751f46a27438a3f9f0dcefa0e",
1161
+ ]
1162
+
1163
+ [[models]]
1164
+ id = "esmfold2_fast"
1165
+ family = "esmfold2"
1166
+ size_category = "structure"
1167
+ generation_contract = "not_applicable"
1168
+ msa_conditioning = false
1169
+ official_golden = { metadata = "tests/goldens/esmfold2_fast.json=sha256:091b004c0b330217b59c12acd6da3d6edaf91e48d95f6d5f40fc20399cef9478", tensors = "tests/goldens/esmfold2_fast.safetensors=sha256:6e2e1cd07401538b4d9df994f82abe7a5b38a01e8d1ee26681e1216d44a81990" }
1170
+ fast_repo = "Synthyra/ESMFold2-Fast"
1171
+ fast_revision = "407875bfcaa42552bfcb25acd67ee1888b790170"
1172
+ fast_files = [
1173
+ "config.json=git-sha1:62ccca15a416a5dcbd02cd6ce161f432c7b4de58",
1174
+ "model.safetensors=sha256:60ca19f2898188beba92944365f7b909efd9c99212f5018af75cc47cd9a6184a",
1175
+ ]
1176
+ official_repo = "biohub/ESMFold2-Fast"
1177
+ official_revision = "b28d8ace5e05e61e5bec1e6820cfd3e221819d12"
1178
+ official_files = [
1179
+ "config.json=git-sha1:c0ca526090fa7f8342ee4666d56e7fe3a4b8cbb2",
1180
+ "model.safetensors=sha256:60ca19f2898188beba92944365f7b909efd9c99212f5018af75cc47cd9a6184a",
1181
+ ]
1182
+
1183
+ [[models]]
1184
+ id = "esmfold2_experimental_cutoff2025"
1185
+ family = "esmfold2"
1186
+ size_category = "structure"
1187
+ generation_contract = "not_applicable"
1188
+ msa_conditioning = true
1189
+ official_golden = { metadata = "tests/goldens/esmfold2_experimental_cutoff2025.json=sha256:cfd0e35b2bc468a0dc4f614d3acfa2fce004f96e9ae2433256ed095b829d55cc", tensors = "tests/goldens/esmfold2_experimental_cutoff2025.safetensors=sha256:9347466bbe803b6f5dc82e3356ca6cbbf2c2edd8765f9fd273385bda255019f6" }
1190
+ fast_repo = "Synthyra/ESMFold2-Experimental-Cutoff2025"
1191
+ fast_revision = "632ff4a9e68f1de78ee956a613267bdcdb5b354d"
1192
+ fast_files = [
1193
+ "config.json=git-sha1:41119745d38bc5503a0212ad923e75211dec565f",
1194
+ "model.safetensors=sha256:01358c317428d38535e3db513cab177336fc0f7fab0d84002e64b7741d5181b3",
1195
+ ]
1196
+ official_repo = "biohub/ESMFold2-Experimental-Cutoff2025"
1197
+ official_revision = "56f94f5c1069ecde17512c96928850518340d287"
1198
+ official_files = [
1199
+ "config.json=git-sha1:79ed0dc0f867b8f09bfa004d6f77397c2ab9b38d",
1200
+ "model.safetensors=sha256:01358c317428d38535e3db513cab177336fc0f7fab0d84002e64b7741d5181b3",
1201
+ ]
1202
+ auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFold2Config", AutoModel = "fastplms.models.esmfold2.modeling_esmfold2_experimental.ESMFold2ExperimentalModel" }
1203
+
1204
+ [[models]]
1205
+ id = "esmfold2_experimental_fast_cutoff2025"
1206
+ family = "esmfold2"
1207
+ size_category = "structure"
1208
+ generation_contract = "not_applicable"
1209
+ msa_conditioning = false
1210
+ official_golden = { metadata = "tests/goldens/esmfold2_experimental_fast_cutoff2025.json=sha256:1d0b2da4f1579243f37ae04bd4b834b747005cd8e8e7665e00d088123c43afd9", tensors = "tests/goldens/esmfold2_experimental_fast_cutoff2025.safetensors=sha256:516e216d05d7e6bee59e77126d3e595e2bb7821929433f00c259c5d5241964bb" }
1211
+ fast_repo = "Synthyra/ESMFold2-Experimental-Fast-Cutoff2025"
1212
+ fast_revision = "8f022c2514a6c32692aaca078a8391d6bc6c4bac"
1213
+ fast_files = [
1214
+ "config.json=git-sha1:b9d39e941050179ca51faaed58cbbd77778c1143",
1215
+ "model.safetensors=sha256:4e903b740ad6ad704ec60881bfd593e0d6c874a630ffa0f0838276e0b665088f",
1216
+ ]
1217
+ official_repo = "biohub/ESMFold2-Experimental-Fast-Cutoff2025"
1218
+ official_revision = "74b88548bf19688b8727432db0d698cb2e1d8783"
1219
+ official_files = [
1220
+ "config.json=git-sha1:0333d68ddb12ed2f066741dcb801142f466c0a2c",
1221
+ "model.safetensors=sha256:4e903b740ad6ad704ec60881bfd593e0d6c874a630ffa0f0838276e0b665088f",
1222
+ ]
1223
+ auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFold2Config", AutoModel = "fastplms.models.esmfold2.modeling_esmfold2_experimental.ESMFold2ExperimentalModel" }
fastplms/models/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lazy model-family namespace for FastPLMs.
2
+
3
+ Model classes are resolved through Transformers AutoClasses and the typed
4
+ registry. Importing this package therefore does not load checkpoints, create
5
+ tokenizers, compile kernels, or initialize an accelerator runtime.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __all__: tuple[str, ...] = ()
fastplms/models/boltz/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastplms.models.boltz.modeling_boltz2 import (
2
+ Boltz2Config,
3
+ Boltz2Model,
4
+ Boltz2ModelOutput,
5
+ Boltz2StructureOutput,
6
+ )
7
+
8
+ __all__ = [
9
+ "Boltz2Config",
10
+ "Boltz2Model",
11
+ "Boltz2ModelOutput",
12
+ "Boltz2StructureOutput",
13
+ ]
fastplms/models/boltz/_pair_attention.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared tensor operations for Boltz2 pair-biased attention layers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ import torch
8
+ from torch import Tensor
9
+
10
+
11
+ def reshape_heads(states: Tensor, num_heads: int) -> Tensor:
12
+ """Reshape X from ``(b, l, d)`` to ``(b, l, h, d_head)``."""
13
+
14
+ 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
+
20
+ def pair_biased_attention(
21
+ query_states: Tensor,
22
+ key_states: Tensor,
23
+ value_states: Tensor,
24
+ pair_bias: Tensor,
25
+ key_mask: Tensor,
26
+ mask_value: float,
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)
fastplms/models/boltz/cif_writer.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import numpy as np
4
+ import torch
5
+
6
+ from .minimal_structures import ProteinStructureTemplate
7
+
8
+
9
+ def _confidence_per_atom(
10
+ plddt: torch.Tensor | None,
11
+ atom_to_residue: list[int],
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: "
40
+ f"received {selected.shape[0]}, expected {num_atoms} atoms or "
41
+ f"{num_residues} residues."
42
+ )
43
+
44
+
45
+ def write_cif(
46
+ structure_template: ProteinStructureTemplate,
47
+ atom_coords: torch.Tensor,
48
+ atom_mask: torch.Tensor,
49
+ output_path: str,
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. "
84
+ f"Non-finite count: {int(valid_non_finite.sum().item())}"
85
+ )
86
+
87
+ b_iso = _confidence_per_atom(
88
+ plddt=plddt,
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
+
96
+ lines = [
97
+ "data_boltz2_prediction",
98
+ "#",
99
+ "loop_",
100
+ "_atom_site.group_PDB",
101
+ "_atom_site.id",
102
+ "_atom_site.type_symbol",
103
+ "_atom_site.label_atom_id",
104
+ "_atom_site.label_comp_id",
105
+ "_atom_site.label_asym_id",
106
+ "_atom_site.label_seq_id",
107
+ "_atom_site.Cartn_x",
108
+ "_atom_site.Cartn_y",
109
+ "_atom_site.Cartn_z",
110
+ "_atom_site.occupancy",
111
+ "_atom_site.B_iso_or_equiv",
112
+ "_atom_site.pdbx_PDB_model_num",
113
+ ]
114
+
115
+ atom_id = 1
116
+ for idx in range(structure_template.num_atoms):
117
+ if mask[idx] <= 0:
118
+ continue
119
+
120
+ residue_idx = structure_template.atom_residue_index[idx]
121
+ residue_name = structure_template.residue_names[residue_idx]
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} "
130
+ f"{residue_idx + 1} {x_val:.3f} {y_val:.3f} {z_val:.3f} 1.00 {b_factor:.2f} 1"
131
+ )
132
+ lines.append(line)
133
+ atom_id += 1
134
+
135
+ lines.append("#")
136
+ text = "\n".join(lines) + "\n"
137
+
138
+ out_path = Path(output_path)
139
+ out_path.parent.mkdir(parents=True, exist_ok=True)
140
+ out_path.write_text(text, encoding="utf-8")
141
+ return str(out_path)
fastplms/models/boltz/minimal_featurizer.py ADDED
@@ -0,0 +1,786 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import numpy as np
4
+ import torch
5
+ from torch.nn.functional import one_hot
6
+
7
+ from . import vb_const as const
8
+ from .minimal_structures import ProteinStructureTemplate
9
+
10
+ _ELEMENT_TO_Z = {
11
+ "H": 1,
12
+ "C": 6,
13
+ "N": 7,
14
+ "O": 8,
15
+ "P": 15,
16
+ "S": 16,
17
+ }
18
+
19
+ # Canonical formal charges and tetrahedral chirality values extracted from the
20
+ # pinned Boltz2 molecule archive. Values use Boltz2's public feature encoding.
21
+ _FORMAL_CHARGES = {
22
+ ("ARG", "NH1"): 1.0,
23
+ ("HIS", "ND1"): 1.0,
24
+ ("LYS", "NZ"): 1.0,
25
+ }
26
+ _CHIRAL_ATOMS = {
27
+ "ALA": frozenset({"CA"}),
28
+ "ARG": frozenset({"CA"}),
29
+ "ASN": frozenset({"CA"}),
30
+ "ASP": frozenset({"CA"}),
31
+ "CYS": frozenset({"CA"}),
32
+ "GLN": frozenset({"CA"}),
33
+ "GLU": frozenset({"CA"}),
34
+ "HIS": frozenset({"CA"}),
35
+ "ILE": frozenset({"CA", "CB"}),
36
+ "LEU": frozenset({"CA"}),
37
+ "LYS": frozenset({"CA"}),
38
+ "MET": frozenset({"CA"}),
39
+ "PHE": frozenset({"CA"}),
40
+ "PRO": frozenset({"CA"}),
41
+ "SER": frozenset({"CA"}),
42
+ "THR": frozenset({"CA", "CB"}),
43
+ "TRP": frozenset({"CA"}),
44
+ "TYR": frozenset({"CA"}),
45
+ "UNK": frozenset({"CA"}),
46
+ "VAL": frozenset({"CA"}),
47
+ }
48
+
49
+
50
+ def _normalize_sequence(sequence: str) -> str:
51
+ if not isinstance(sequence, str):
52
+ raise TypeError("Amino acid sequence must be a string.")
53
+ seq = sequence.strip().upper()
54
+ if not seq:
55
+ raise ValueError("Amino acid sequence must be non-empty.")
56
+ for aa in seq:
57
+ if aa not in const.prot_letter_to_token:
58
+ raise ValueError(f"Unsupported residue code '{aa}'.")
59
+ return seq
60
+
61
+
62
+ def _atom_name_to_element(atom_name: str) -> str:
63
+ name = atom_name.strip().upper()
64
+ if len(name) == 0:
65
+ return "C"
66
+ if name[0].isdigit():
67
+ name = name[1:]
68
+ if len(name) >= 2 and name[0:2] in ("CL", "BR", "FE", "MG", "ZN", "NA", "CA"):
69
+ return name[0]
70
+ return name[0]
71
+
72
+
73
+ def _atom_name_to_codes(atom_name: str) -> torch.Tensor:
74
+ clipped = atom_name.strip()[:4]
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
82
+
83
+
84
+ # Raw first-conformer coordinates extracted at full float32 precision from the
85
+ # hash-pinned Boltz2 molecule archive. The feature builder applies the official
86
+ # seeded centering, rotation, and translation policy below.
87
+ _RDKIT_CONFORMERS: dict[str, dict[str, list[float]]] = {
88
+ "ALA": {
89
+ "N": [-0.9241582155227661, 1.1821246147155762, 0.712748110294342],
90
+ "CA": [-0.2663755416870117, -0.08827890455722809, 0.4008508622646332],
91
+ "C": [1.1188693046569824, 0.1387452930212021, -0.14366498589515686],
92
+ "O": [1.2882297039031982, 0.8058497309684753, -1.2001261711120605],
93
+ "CB": [-1.1134333610534668, -0.8915302753448486, -0.5876986384391785],
94
+ },
95
+ "ARG": {
96
+ "N": [3.253326416015625, -1.699564814567566, -0.9852627515792847],
97
+ "CA": [2.191511392593384, -0.692007839679718, -0.9506462812423706],
98
+ "C": [2.80021333694458, 0.672307550907135, -1.1160222291946411],
99
+ "O": [2.465440034866333, 1.3987600803375244, -2.0899009704589844],
100
+ "CB": [1.3811039924621582, -0.7949594855308533, 0.36316436529159546],
101
+ "CG": [0.20344306528568268, 0.19287171959877014, 0.457282155752182],
102
+ "CD": [-0.902410626411438, -0.09953747689723969, -0.5671625137329102],
103
+ "NE": [-2.0867979526519775, 0.7146661281585693, -0.2911669909954071],
104
+ "CZ": [-3.018158197402954, 0.4804283082485199, 0.7831193804740906],
105
+ "NH1": [-4.080137729644775, 1.4154621362686157, 0.9938872456550598],
106
+ "NH2": [-2.9226150512695312, -0.5637124180793762, 1.5533623695373535],
107
+ },
108
+ "ASN": {
109
+ "N": [-1.5767980813980103, -1.7227835655212402, 0.2427234649658203],
110
+ "CA": [-0.7041534781455994, -0.5622008442878723, 0.42767149209976196],
111
+ "C": [-1.246688961982727, 0.6072975993156433, -0.3483567237854004],
112
+ "O": [-1.554918885231018, 0.47644123435020447, -1.5638835430145264],
113
+ "CB": [0.7263866662979126, -0.9032993316650391, -0.028588544577360153],
114
+ "CG": [1.6871562004089355, 0.20810098946094513, 0.28412675857543945],
115
+ "OD1": [2.19687557220459, 0.28916797041893005, 1.433493971824646],
116
+ "ND2": [1.996717095375061, 1.196706771850586, -0.6981719136238098],
117
+ },
118
+ "ASP": {
119
+ "N": [-0.11774874478578568, -1.6310220956802368, 0.374360054731369],
120
+ "CA": [-0.3847021758556366, -0.19290897250175476, 0.28121331334114075],
121
+ "C": [-1.806337594985962, 0.059408850967884064, -0.1472136378288269],
122
+ "O": [-2.2237648963928223, -0.36817988753318787, -1.257502555847168],
123
+ "CB": [0.5981687307357788, 0.4785478413105011, -0.6920451521873474],
124
+ "CG": [2.0088446140289307, 0.36018264293670654, -0.20304732024669647],
125
+ "OD1": [2.747267961502075, -0.5711832046508789, -0.6216948628425598],
126
+ "OD2": [2.49090313911438, 1.2577818632125854, 0.7443997859954834],
127
+ },
128
+ "CYS": {
129
+ "N": [-0.058702241629362106, 1.771048665046692, 0.2434082329273224],
130
+ "CA": [-0.06546340137720108, 0.47663936018943787, -0.4456723928451538],
131
+ "C": [-1.273919701576233, -0.3504071533679962, -0.08361759781837463],
132
+ "O": [-1.7324607372283936, -0.34313708543777466, 1.0909277200698853],
133
+ "CB": [1.2432398796081543, -0.28917407989501953, -0.19254730641841888],
134
+ "SG": [1.484184980392456, -0.7191365361213684, 1.5649693012237549],
135
+ },
136
+ "GLN": {
137
+ "N": [-1.8543237447738647, -1.0024770498275757, -1.6278940439224243],
138
+ "CA": [-1.292184591293335, -0.6786512732505798, -0.3153715133666992],
139
+ "C": [-2.2264492511749268, 0.25171151757240295, 0.4067918062210083],
140
+ "O": [-2.7255098819732666, -0.08834805339574814, 1.5129321813583374],
141
+ "CB": [0.11662524193525314, -0.06213820353150368, -0.45663803815841675],
142
+ "CG": [0.8140791058540344, 0.10461423546075821, 0.9002283811569214],
143
+ "CD": [2.195122003555298, 0.6569428443908691, 0.7154991030693054],
144
+ "OE1": [2.3870291709899902, 1.9003654718399048, 0.7825236916542053],
145
+ "NE2": [3.287371873855591, -0.21307705342769623, 0.41959717869758606],
146
+ },
147
+ "GLU": {
148
+ "N": [-1.3492857217788696, -1.114271640777588, -1.3739068508148193],
149
+ "CA": [-1.2765676975250244, -0.506334662437439, -0.04287439212203026],
150
+ "C": [-1.9195666313171387, 0.8547031879425049, -0.04214814677834511],
151
+ "O": [-1.9091347455978394, 1.5701956748962402, -1.080504298210144],
152
+ "CB": [0.18272103369235992, -0.43209096789360046, 0.45213374495506287],
153
+ "CG": [1.1058456897735596, 0.3659400939941406, -0.481784850358963],
154
+ "CD": [2.5090205669403076, 0.35135790705680847, 0.03646872192621231],
155
+ "OE1": [2.896414041519165, 1.2445733547210693, 0.8365193605422974],
156
+ "OE2": [3.3727896213531494, -0.6754258275032043, -0.33141785860061646],
157
+ },
158
+ "GLY": {
159
+ "N": [-1.291549801826477, 0.6080796122550964, -0.4228580892086029],
160
+ "CA": [-0.4895951449871063, -0.2882237136363983, 0.40191715955734253],
161
+ "C": [0.9350062608718872, -0.2543502449989319, -0.04786944016814232],
162
+ "O": [1.3473576307296753, -1.0836502313613892, -0.9022219777107239],
163
+ },
164
+ "HIS": {
165
+ "N": [1.0371263027191162, -1.5621215105056763, 0.4178937077522278],
166
+ "CA": [1.1980191469192505, -0.40241551399230957, -0.46312251687049866],
167
+ "C": [2.6561269760131836, -0.07123222947120667, -0.6407948136329651],
168
+ "O": [3.1795547008514404, -0.12371637672185898, -1.7859554290771484],
169
+ "CB": [0.44051393866539, 0.8116919994354248, 0.09850303083658218],
170
+ "CG": [-1.0356801748275757, 0.5519806742668152, 0.15254715085029602],
171
+ "ND1": [-1.9089158773422241, 0.6490958333015442, -0.968245267868042],
172
+ "CD2": [-1.7095766067504883, 0.1138235554099083, 1.2061808109283447],
173
+ "CE1": [-3.0751633644104004, 0.28129827976226807, -0.5511555075645447],
174
+ "NE2": [-3.0650453567504883, -0.07846951484680176, 0.8257263898849487],
175
+ },
176
+ "ILE": {
177
+ "N": [-1.2378733158111572, -1.8146690130233765, -0.16437499225139618],
178
+ "CA": [-1.2780015468597412, -0.3994033634662628, 0.22437156736850739],
179
+ "C": [-1.9879958629608154, 0.4497596025466919, -0.80372154712677],
180
+ "O": [-2.0291709899902344, 0.10288607329130173, -2.015406608581543],
181
+ "CB": [0.14225825667381287, 0.146738663315773, 0.5415176153182983],
182
+ "CG1": [1.1062071323394775, 0.04519597440958023, -0.6694174408912659],
183
+ "CG2": [0.7215384244918823, -0.5685611963272095, 1.7750537395477295],
184
+ "CD1": [2.371805191040039, 0.8852734565734863, -0.49045464396476746],
185
+ },
186
+ "LEU": {
187
+ "N": [1.6452248096466064, -1.017622470855713, -1.0626215934753418],
188
+ "CA": [1.3129304647445679, 0.08074887096881866, -0.15053576231002808],
189
+ "C": [2.431154727935791, 0.28310757875442505, 0.8357383608818054],
190
+ "O": [2.9231173992156982, -0.7013152837753296, 1.451209306716919],
191
+ "CB": [0.006050171796232462, -0.2141101360321045, 0.6198461651802063],
192
+ "CG": [-1.2534235715866089, -0.3772832751274109, -0.26637399196624756],
193
+ "CD1": [-2.455688714981079, -0.7604416012763977, 0.6070646643638611],
194
+ "CD2": [-1.5742294788360596, 0.8985711932182312, -1.059889554977417],
195
+ },
196
+ "LYS": {
197
+ "N": [-2.3938536643981934, -1.4751482009887695, -0.9336304068565369],
198
+ "CA": [-2.2094509601593018, -0.6203567981719971, 0.2421264797449112],
199
+ "C": [-3.4513463973999023, 0.1961250752210617, 0.4710204303264618],
200
+ "O": [-3.9688456058502197, 0.2503468096256256, 1.6188287734985352],
201
+ "CB": [-1.008726716041565, 0.3269270658493042, 0.0479682981967926],
202
+ "CG": [0.336457222700119, -0.41559070348739624, 0.047045741230249405],
203
+ "CD": [1.5088860988616943, 0.5716065168380737, -0.00632853340357542],
204
+ "CE": [2.853410005569458, -0.1678532361984253, -0.020898228511214256],
205
+ "NZ": [3.9690465927124023, 0.7765668630599976, -0.07007527351379395],
206
+ },
207
+ "MET": {
208
+ "N": [-1.743452548980713, -0.44039490818977356, 1.851780652999878],
209
+ "CA": [-1.2168668508529663, 0.4041755199432373, 0.7779996991157532],
210
+ "C": [-2.3118340969085693, 0.683431088924408, -0.2134547382593155],
211
+ "O": [-2.6660380363464355, 1.8705639839172363, -0.44476020336151123],
212
+ "CB": [-0.0005482881097123027, -0.262584388256073, 0.09933014959096909],
213
+ "CG": [0.6952047944068909, 0.6768104434013367, -0.8930339813232422],
214
+ "SD": [2.136805772781372, -0.14913176000118256, -1.6563159227371216],
215
+ "CE": [3.3404407501220703, 0.348065584897995, -0.38104408979415894],
216
+ },
217
+ "PHE": {
218
+ "N": [2.7976438999176025, -1.6209964752197266, -0.4779351055622101],
219
+ "CA": [1.5799994468688965, -0.8289614915847778, -0.6608061194419861],
220
+ "C": [1.9535551071166992, 0.5758142471313477, -1.0445665121078491],
221
+ "O": [1.5211102962493896, 1.072593331336975, -2.1190123558044434],
222
+ "CB": [0.7137221097946167, -0.8474348187446594, 0.6163292527198792],
223
+ "CG": [-0.6229045987129211, -0.18666480481624603, 0.3912639319896698],
224
+ "CD1": [-0.8259273767471313, 1.0978055000305176, 0.7381877303123474],
225
+ "CD2": [-1.7160141468048096, -0.9425657391548157, -0.2700555920600891],
226
+ "CE1": [-2.131312131881714, 1.741182565689087, 0.4800238013267517],
227
+ "CE2": [-2.8960046768188477, -0.3551671802997589, -0.505269467830658],
228
+ "CZ": [-3.11456298828125, 1.0511738061904907, -0.11009909957647324],
229
+ },
230
+ "PRO": {
231
+ "N": [-0.577339768409729, -0.5123927593231201, -1.150407314300537],
232
+ "CA": [0.5119893550872803, 0.23565764725208282, -0.5195383429527283],
233
+ "C": [1.8393173217773438, -0.46712079644203186, -0.6550371646881104],
234
+ "O": [2.0836586952209473, -1.166333556175232, -1.6751221418380737],
235
+ "CB": [0.1029478907585144, 0.45346882939338684, 0.9297074675559998],
236
+ "CG": [-1.4098080396652222, 0.5215330123901367, 0.8787456750869751],
237
+ "CD": [-1.7971025705337524, -0.06041542813181877, -0.47876954078674316],
238
+ },
239
+ "SER": {
240
+ "N": [0.8959873914718628, -1.423298716545105, -0.2692987024784088],
241
+ "CA": [-0.018787339329719543, -0.28751760721206665, -0.3863537311553955],
242
+ "C": [0.767615795135498, 0.9929561018943787, -0.42147380113601685],
243
+ "O": [0.5327092409133911, 1.8535151481628418, -1.3116248846054077],
244
+ "CB": [-0.9973848462104797, -0.2769756019115448, 0.7988247871398926],
245
+ "OG": [-1.8999865055084229, 0.7915746569633484, 0.691534698009491],
246
+ },
247
+ "THR": {
248
+ "N": [0.005045319441705942, 1.7655576467514038, 0.0639338344335556],
249
+ "CA": [0.4231603145599365, 0.3755212128162384, 0.2734847664833069],
250
+ "C": [1.7184523344039917, 0.11012311279773712, -0.4465082287788391],
251
+ "O": [1.9252793788909912, 0.5967074036598206, -1.5912789106369019],
252
+ "CB": [-0.6634359359741211, -0.615561306476593, -0.21173778176307678],
253
+ "OG1": [-1.0762907266616821, -0.29800114035606384, -1.5171915292739868],
254
+ "CG2": [-1.8869022130966187, -0.6176311373710632, 0.7084044218063354],
255
+ },
256
+ "TRP": {
257
+ "N": [-3.0439648628234863, 1.0628471374511719, 0.015692168846726418],
258
+ "CA": [-2.463679552078247, 0.024657616391777992, -0.8414031863212585],
259
+ "C": [-2.186711072921753, -1.2228965759277344, -0.04691409692168236],
260
+ "O": [-2.3364906311035156, -2.3535354137420654, -0.5830773115158081],
261
+ "CB": [-1.2062920331954956, 0.5195542573928833, -1.592403531074524],
262
+ "CG": [-0.06450498104095459, 0.884079098701477, -0.6835198402404785],
263
+ "CD1": [0.16109998524188995, 2.085163116455078, -0.14801783859729767],
264
+ "CD2": [0.9781660437583923, -0.005544683896005154, -0.17733941972255707],
265
+ "NE1": [1.3106837272644043, 2.0506503582000732, 0.698072075843811],
266
+ "CE2": [1.7528777122497559, 0.7044392228126526, 0.6196392774581909],
267
+ "CE3": [1.236093521118164, -1.4355946779251099, -0.4235689342021942],
268
+ "CZ2": [2.9070138931274414, 0.10196790844202042, 1.3070299625396729],
269
+ "CZ3": [2.2851555347442627, -2.007185697555542, 0.1949695348739624],
270
+ "CH2": [3.1585631370544434, -1.2034809589385986, 1.1002938747406006],
271
+ },
272
+ "TYR": {
273
+ "N": [-1.7950150966644287, 0.49119046330451965, -1.3951144218444824],
274
+ "CA": [-1.8436503410339355, -0.2694968581199646, -0.14338290691375732],
275
+ "C": [-3.240288019180298, -0.28215137124061584, 0.41974759101867676],
276
+ "O": [-3.8150112628936768, 0.7977103590965271, 0.7253130674362183],
277
+ "CB": [-0.8541494011878967, 0.30990728735923767, 0.8830214738845825],
278
+ "CG": [0.5672639012336731, 0.2125871181488037, 0.3916495442390442],
279
+ "CD1": [1.2694358825683594, -0.924761176109314, 0.5431669354438782],
280
+ "CD2": [1.1872504949569702, 1.3605931997299194, -0.31554359197616577],
281
+ "CE1": [2.6519992351531982, -1.0213464498519897, 0.029911600053310394],
282
+ "CE2": [2.4384164810180664, 1.2693941593170166, -0.7831515073776245],
283
+ "CZ": [3.211331367492676, 0.022552935406565666, -0.6021429300308228],
284
+ "OH": [4.513507843017578, -0.05260590463876724, -1.0929937362670898],
285
+ },
286
+ "VAL": {
287
+ "N": [0.9408224821090698, -1.2608877420425415, 0.652370810508728],
288
+ "CA": [0.7287879586219788, -0.3937721848487854, -0.5120788216590881],
289
+ "C": [1.7670493125915527, 0.6979415416717529, -0.5508439540863037],
290
+ "O": [2.2449657917022705, 1.1683598756790161, 0.5171348452568054],
291
+ "CB": [-0.7015937566757202, 0.21480272710323334, -0.5226467847824097],
292
+ "CG1": [-1.7662651538848877, -0.845522403717041, -0.8411717414855957],
293
+ "CG2": [-1.0500152111053467, 0.9374979138374329, 0.7892321944236755],
294
+ },
295
+ "UNK": {
296
+ "N": [1.6134154796600342, -1.304404377937317, -0.35241976380348206],
297
+ "CA": [0.6100443601608276, -0.23816797137260437, -0.3852081298828125],
298
+ "C": [1.0861350297927856, 0.9232651591300964, 0.4423142373561859],
299
+ "O": [1.374688982963562, 0.7629808187484741, 1.6591525077819824],
300
+ "CB": [-0.7607048153877258, -0.7552146315574646, 0.10266945511102676],
301
+ },
302
+ }
303
+
304
+
305
+ def _get_atom_position(res_name: str, atom_name: str, atom_idx: int) -> np.ndarray:
306
+ """Get the canonical RDKit conformer position for an atom.
307
+
308
+ Uses pre-extracted positions from official Boltz2 mol files. Falls back
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
316
+ return np.array(
317
+ [
318
+ radius * math.cos(angle),
319
+ radius * math.sin(angle),
320
+ 0.1 * ((atom_idx % 5) - 2),
321
+ ],
322
+ dtype=np.float32,
323
+ )
324
+
325
+
326
+ def _build_template(
327
+ sequence: str,
328
+ ) -> tuple[
329
+ ProteinStructureTemplate,
330
+ list[str],
331
+ list[int],
332
+ list[int],
333
+ list[int],
334
+ list[np.ndarray],
335
+ list[int],
336
+ ]:
337
+ residue_names: list[str] = []
338
+ residue_token_ids: list[int] = []
339
+ atom_names: list[str] = []
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] = []
347
+
348
+ global_atom_idx = 0
349
+ for res_idx, aa in enumerate(sequence):
350
+ token_name = const.prot_letter_to_token[aa]
351
+ residue_names.append(token_name)
352
+ residue_token_ids.append(const.token_ids[token_name])
353
+
354
+ residue_atoms = const.ref_atoms[token_name]
355
+ if not residue_atoms:
356
+ raise RuntimeError(f"No reference atoms for residue {token_name}.")
357
+ center_atom_name = const.res_to_center_atom[token_name]
358
+ disto_atom_name = const.res_to_disto_atom[token_name]
359
+
360
+ center_idx = -1
361
+ disto_idx = -1
362
+ n_idx = -1
363
+ ca_idx = -1
364
+ c_idx = -1
365
+
366
+ for local_idx, atom_name in enumerate(residue_atoms):
367
+ atom_names.append(atom_name)
368
+ element = _atom_name_to_element(atom_name)
369
+ atom_elements.append(element)
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:
377
+ center_idx = global_atom_idx
378
+ if atom_name == disto_atom_name:
379
+ disto_idx = global_atom_idx
380
+ if atom_name == "N":
381
+ n_idx = global_atom_idx
382
+ if atom_name == "CA":
383
+ ca_idx = global_atom_idx
384
+ if atom_name == "C":
385
+ c_idx = global_atom_idx
386
+ global_atom_idx += 1
387
+
388
+ if center_idx == -1:
389
+ center_idx = global_atom_idx - len(residue_atoms)
390
+ if disto_idx == -1:
391
+ disto_idx = center_idx
392
+ if n_idx == -1:
393
+ n_idx = center_idx
394
+ if ca_idx == -1:
395
+ ca_idx = center_idx
396
+ if c_idx == -1:
397
+ c_idx = center_idx
398
+
399
+ residue_center_atom_idx.append(center_idx)
400
+ residue_disto_atom_idx.append(disto_idx)
401
+ residue_frame_atom_idx.extend([n_idx, ca_idx, c_idx])
402
+
403
+ template = ProteinStructureTemplate(
404
+ sequence=sequence,
405
+ residue_names=residue_names,
406
+ atom_names=atom_names,
407
+ atom_elements=atom_elements,
408
+ atom_residue_index=atom_residue_index,
409
+ atom_chain_id=atom_chain_id,
410
+ )
411
+
412
+ return (
413
+ template,
414
+ residue_names,
415
+ residue_token_ids,
416
+ residue_center_atom_idx,
417
+ residue_disto_atom_idx,
418
+ atom_positions,
419
+ residue_frame_atom_idx,
420
+ )
421
+
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),
436
+ two_s * (i * j - k * real),
437
+ two_s * (i * k + j * real),
438
+ two_s * (i * j + k * real),
439
+ 1 - two_s * (i * i + k * k),
440
+ two_s * (j * k - i * real),
441
+ two_s * (i * k - j * real),
442
+ two_s * (j * k + i * real),
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(
451
+ atom_positions: torch.Tensor,
452
+ atom_residue_index: list[int],
453
+ num_residues: int,
454
+ ) -> torch.Tensor:
455
+ """Apply Boltz2's seeded conformer augmentation per residue.
456
+
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(
482
+ amino_acid_sequence: str,
483
+ num_bins: int = 64,
484
+ atoms_per_window_queries: int = 32,
485
+ ) -> tuple[dict[str, torch.Tensor], ProteinStructureTemplate]:
486
+ sequence = _normalize_sequence(amino_acid_sequence)
487
+ (
488
+ template,
489
+ residue_names,
490
+ residue_token_ids,
491
+ residue_center_atom_idx,
492
+ residue_disto_atom_idx,
493
+ atom_positions_np,
494
+ residue_frame_atom_idx_flat,
495
+ ) = _build_template(sequence)
496
+
497
+ num_tokens = len(residue_names)
498
+ num_atoms = len(atom_positions_np)
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"]
538
+ contact_ids = torch.full(
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.")
551
+ method_feature = torch.full(
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:
572
+ z_value = _ELEMENT_TO_Z[element] if element in _ELEMENT_TO_Z else _ELEMENT_TO_Z["C"]
573
+ if z_value >= const.num_elements:
574
+ raise RuntimeError(
575
+ f"Atomic number {z_value} exceeds the Boltz2 element vocabulary."
576
+ )
577
+ atomic_numbers.append(z_value)
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 = []
585
+ for atom_name, residue_idx in zip(
586
+ template.atom_names,
587
+ template.atom_residue_index,
588
+ strict=True,
589
+ ):
590
+ residue_name = residue_names[residue_idx]
591
+ ref_charge_values.append(_FORMAL_CHARGES.get((residue_name, atom_name), 0.0))
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)
615
+ )
616
+ backbone_ids = []
617
+ for atom_name in template.atom_names:
618
+ if atom_name in const.protein_backbone_atom_index:
619
+ backbone_ids.append(const.protein_backbone_atom_index[atom_name] + 1)
620
+ else:
621
+ backbone_ids.append(0)
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.")
639
+ pad_atoms = (
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),
700
+ num_classes=const.num_tokens,
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,
729
+ "residue_index": residue_index,
730
+ "asym_id": asym_id,
731
+ "entity_id": entity_id,
732
+ "sym_id": sym_id,
733
+ "mol_type": mol_type,
734
+ "res_type": res_type,
735
+ "disto_center": disto_center,
736
+ "token_bonds": token_bonds,
737
+ "type_bonds": type_bonds,
738
+ "token_pad_mask": token_pad_mask,
739
+ "token_resolved_mask": token_resolved_mask,
740
+ "token_disto_mask": token_disto_mask,
741
+ "contact_conditioning": contact_conditioning,
742
+ "contact_threshold": contact_threshold,
743
+ "method_feature": method_feature,
744
+ "modified": modified,
745
+ "cyclic_period": cyclic_period,
746
+ "affinity_token_mask": affinity_token_mask,
747
+ "ref_pos": ref_pos,
748
+ "atom_resolved_mask": atom_resolved_mask,
749
+ "ref_atom_name_chars": ref_atom_name_chars,
750
+ "ref_element": ref_element,
751
+ "ref_charge": ref_charge,
752
+ "ref_chirality": ref_chirality,
753
+ "atom_backbone_feat": atom_backbone_feat,
754
+ "ref_space_uid": ref_space_uid,
755
+ "coords": coords,
756
+ "atom_pad_mask": atom_pad_mask,
757
+ "atom_to_token": atom_to_token,
758
+ "token_to_rep_atom": token_to_rep_atom,
759
+ "r_set_to_rep_atom": r_set_to_rep_atom,
760
+ "token_to_center_atom": token_to_center_atom,
761
+ "disto_target": disto_target,
762
+ "disto_coords_ensemble": disto_coords_ensemble,
763
+ "bfactor": bfactor,
764
+ "plddt": atom_plddt,
765
+ "frames_idx": frames_idx,
766
+ "frame_resolved_mask": frame_resolved_mask,
767
+ "msa": msa,
768
+ "msa_paired": msa_paired,
769
+ "deletion_value": deletion_value,
770
+ "has_deletion": has_deletion,
771
+ "deletion_mean": deletion_mean,
772
+ "profile": profile,
773
+ "msa_mask": msa_mask,
774
+ "template_restype": template_restype,
775
+ "template_frame_rot": template_frame_rot,
776
+ "template_frame_t": template_frame_t,
777
+ "template_cb": template_cb,
778
+ "template_ca": template_ca,
779
+ "template_mask_cb": template_mask_cb,
780
+ "template_mask_frame": template_mask_frame,
781
+ "template_mask": template_mask,
782
+ "query_to_template": query_to_template,
783
+ "visibility_ids": visibility_ids,
784
+ }
785
+
786
+ return features, template
fastplms/models/boltz/minimal_structures.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class ProteinStructureTemplate:
6
+ sequence: str
7
+ residue_names: list[str]
8
+ atom_names: list[str]
9
+ atom_elements: list[str]
10
+ atom_residue_index: list[int]
11
+ atom_chain_id: list[str]
12
+
13
+ @property
14
+ def num_atoms(self) -> int:
15
+ return len(self.atom_names)
16
+
17
+ @property
18
+ def num_residues(self) -> int:
19
+ return len(self.residue_names)
fastplms/models/boltz/modeling_boltz2.py ADDED
@@ -0,0 +1,993 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
15
+
16
+ from . import vb_const as const
17
+ from . import vb_layers_initialize as init
18
+ from .cif_writer import write_cif
19
+ from .minimal_featurizer import build_boltz2_features
20
+ from .minimal_structures import ProteinStructureTemplate
21
+ from .vb_const import bond_types as _vb_const_bond_types # noqa: F401
22
+ from .vb_layers_attention import AttentionPairBias as _vb_layers_attention_marker # noqa: F401
23
+ from .vb_layers_attentionv2 import AttentionPairBias as _vb_layers_attentionv2_marker # noqa: F401
24
+ from .vb_layers_confidence_utils import (
25
+ compute_ptms as _vb_layers_confidence_utils_marker, # noqa: F401
26
+ )
27
+ from .vb_layers_dropout import get_dropout_mask as _vb_layers_dropout_marker # noqa: F401
28
+ from .vb_layers_initialize import gating_init_ as _vb_layers_initialize_marker # noqa: F401
29
+ from .vb_layers_outer_product_mean import (
30
+ OuterProductMean as _vb_layers_outer_product_mean_marker, # noqa: F401
31
+ )
32
+ from .vb_layers_pair_averaging import (
33
+ PairWeightedAveraging as _vb_layers_pair_averaging_marker, # noqa: F401
34
+ )
35
+ from .vb_layers_pairformer import PairformerModule
36
+ from .vb_layers_transition import Transition as _vb_layers_transition_marker # noqa: F401
37
+ from .vb_layers_triangular_mult import (
38
+ TriangleMultiplicationIncoming as _vb_layers_triangular_mult_marker, # noqa: F401
39
+ )
40
+ from .vb_loss_diffusionv2 import weighted_rigid_align as _vb_loss_diffusionv2_marker # noqa: F401
41
+ from .vb_modules_confidencev2 import ConfidenceModule
42
+ from .vb_modules_diffusion_conditioning import DiffusionConditioning
43
+ from .vb_modules_diffusionv2 import AtomDiffusion, DiffusionModule
44
+ from .vb_modules_encodersv2 import RelativePositionEncoder
45
+ from .vb_modules_transformersv2 import (
46
+ DiffusionTransformer as _vb_modules_transformersv2_marker, # noqa: F401
47
+ )
48
+ from .vb_modules_trunkv2 import (
49
+ ContactConditioning,
50
+ DistogramModule,
51
+ InputEmbedder,
52
+ MSAModule,
53
+ )
54
+ from .vb_modules_utils import LinearNoBias as _vb_modules_utils_marker # noqa: F401
55
+ from .vb_potentials_potentials import (
56
+ get_potentials as _vb_potentials_potentials_marker, # noqa: F401
57
+ )
58
+ from .vb_potentials_schedules import (
59
+ ParameterSchedule as _vb_potentials_schedules_marker, # noqa: F401
60
+ )
61
+ from .vb_tri_attn_attention import (
62
+ TriangleAttentionStartingNode as _vb_tri_attn_attention_marker, # noqa: F401
63
+ )
64
+ from .vb_tri_attn_primitives import Attention as _vb_tri_attn_primitives_marker # noqa: F401
65
+ from .vb_tri_attn_utils import permute_final_dims as _vb_tri_attn_utils_marker # noqa: F401
66
+
67
+
68
+ def _default_steering_args() -> dict[str, Any]:
69
+ return {
70
+ "fk_steering": False,
71
+ "num_particles": 3,
72
+ "fk_lambda": 4.0,
73
+ "fk_resampling_interval": 3,
74
+ "physical_guidance_update": False,
75
+ "contact_guidance_update": False,
76
+ "num_gd_steps": 16,
77
+ }
78
+
79
+
80
+ def _boltz2_reference_diffusion_overrides() -> dict[str, Any]:
81
+ # Match Boltz2 CLI inference defaults from boltz.main/Boltz2DiffusionParams.
82
+ return {
83
+ "gamma_0": 0.8,
84
+ "gamma_min": 1.0,
85
+ "noise_scale": 1.003,
86
+ "rho": 7,
87
+ "step_scale": 1.5,
88
+ "sigma_min": 0.0001,
89
+ "sigma_max": 160.0,
90
+ "sigma_data": 16.0,
91
+ "P_mean": -1.2,
92
+ "P_std": 1.5,
93
+ "coordinate_augmentation": True,
94
+ "alignment_reverse_diff": True,
95
+ "synchronize_sigmas": True,
96
+ }
97
+
98
+
99
+ def _enforce_pairformer_v2(pairformer_args: Mapping[str, Any], context: str) -> dict[str, Any]:
100
+ if not isinstance(pairformer_args, Mapping):
101
+ raise TypeError(f"Expected {context} pairformer_args to be a dictionary.")
102
+ out = _to_plain_python(copy.deepcopy(pairformer_args))
103
+ if "v2" in out and not out["v2"]:
104
+ raise ValueError(f"{context} pairformer_args['v2'] must be True for Boltz2.")
105
+ out["v2"] = True
106
+ return out
107
+
108
+
109
+ def _require_key(mapping: dict[str, Any], key: str) -> Any:
110
+ if key not in mapping:
111
+ raise KeyError(f"Missing required key '{key}' in checkpoint hyperparameters.")
112
+ return mapping[key]
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."):
119
+ continue
120
+ new_key = key
121
+ if new_key.startswith("model."):
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():
135
+ out[key] = _to_cpu_detached(nested_value)
136
+ return out
137
+ if isinstance(value, list):
138
+ return [_to_cpu_detached(item) for item in value]
139
+ if isinstance(value, tuple):
140
+ return tuple(_to_cpu_detached(item) for item in value)
141
+ return value
142
+
143
+
144
+ @dataclass(frozen=True)
145
+ class _RandomState:
146
+ python: object
147
+ numpy: tuple[Any, ...]
148
+ torch_cpu: Tensor
149
+ torch_cuda: list[Tensor] | None
150
+
151
+
152
+ @contextmanager
153
+ def _seed_context(seed: int | None) -> Iterator[None]:
154
+ """Temporarily seed every RNG used by Boltz2 and restore caller state."""
155
+
156
+ if seed is None:
157
+ yield
158
+ return
159
+ if isinstance(seed, bool) or not isinstance(seed, int):
160
+ raise TypeError(
161
+ "Boltz2 seed must be an int or None; bool and coerced values are not accepted."
162
+ )
163
+ state = _RandomState(
164
+ python=random.getstate(),
165
+ numpy=np.random.get_state(),
166
+ torch_cpu=torch.random.get_rng_state(),
167
+ torch_cuda=torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None,
168
+ )
169
+ normalized_seed = seed % (2**32)
170
+ random.seed(normalized_seed)
171
+ np.random.seed(normalized_seed)
172
+ torch.manual_seed(normalized_seed)
173
+ if torch.cuda.is_available():
174
+ torch.cuda.manual_seed_all(normalized_seed)
175
+ try:
176
+ yield
177
+ finally:
178
+ random.setstate(state.python)
179
+ np.random.set_state(state.numpy)
180
+ torch.random.set_rng_state(state.torch_cpu)
181
+ if state.torch_cuda is not None:
182
+ torch.cuda.set_rng_state_all(state.torch_cuda)
183
+
184
+
185
+ def _to_plain_python(value: Any) -> Any:
186
+ if isinstance(value, Mapping):
187
+ out: dict[Any, Any] = {}
188
+ for key, nested_value in value.items():
189
+ out[key] = _to_plain_python(nested_value)
190
+ return out
191
+ if isinstance(value, list):
192
+ return [_to_plain_python(item) for item in value]
193
+ if isinstance(value, tuple):
194
+ return [_to_plain_python(item) for item in value]
195
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
196
+ return [_to_plain_python(item) for item in value]
197
+ return value
198
+
199
+
200
+ def _filtered_kwargs(target: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
201
+ signature = inspect.signature(target.__init__)
202
+ allowed = set(signature.parameters.keys())
203
+ allowed.discard("self")
204
+ filtered: dict[str, Any] = {}
205
+ for key, value in kwargs.items():
206
+ if key in allowed:
207
+ filtered[key] = value
208
+ return filtered
209
+
210
+
211
+ @dataclass
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
+
258
+ class Boltz2Config(PretrainedConfig):
259
+ model_type = "boltz2_automodel"
260
+
261
+ def __init__(
262
+ self,
263
+ core_kwargs: dict[str, Any] | None = None,
264
+ num_bins: int = 64,
265
+ default_recycling_steps: int = 3,
266
+ default_sampling_steps: int = 200,
267
+ default_diffusion_samples: int = 1,
268
+ **kwargs,
269
+ ) -> None:
270
+ super().__init__(**kwargs)
271
+ if core_kwargs is None:
272
+ core_kwargs = {}
273
+ self.core_kwargs = core_kwargs
274
+ self.num_bins = num_bins
275
+ self.default_recycling_steps = default_recycling_steps
276
+ self.default_sampling_steps = default_sampling_steps
277
+ self.default_diffusion_samples = default_diffusion_samples
278
+
279
+ @classmethod
280
+ def from_hyperparameters(
281
+ cls,
282
+ hparams: dict[str, Any],
283
+ use_kernels: bool = False,
284
+ default_recycling_steps: int | None = None,
285
+ default_sampling_steps: int | None = None,
286
+ default_diffusion_samples: int | None = None,
287
+ ) -> "Boltz2Config":
288
+ if not isinstance(hparams, dict):
289
+ raise TypeError("Expected checkpoint hyperparameters as a dictionary.")
290
+ required = [
291
+ "atom_s",
292
+ "atom_z",
293
+ "token_s",
294
+ "token_z",
295
+ "num_bins",
296
+ "embedder_args",
297
+ "msa_args",
298
+ "pairformer_args",
299
+ "score_model_args",
300
+ "diffusion_process_args",
301
+ ]
302
+ for key in required:
303
+ _require_key(hparams, key)
304
+
305
+ pairformer_args = _enforce_pairformer_v2(
306
+ hparams["pairformer_args"],
307
+ context="checkpoint",
308
+ )
309
+ diffusion_process_args = _to_plain_python(copy.deepcopy(hparams["diffusion_process_args"]))
310
+ diffusion_overrides = _boltz2_reference_diffusion_overrides()
311
+ for key in diffusion_overrides:
312
+ diffusion_process_args[key] = diffusion_overrides[key]
313
+
314
+ core_kwargs: dict[str, Any] = {
315
+ "atom_s": hparams["atom_s"],
316
+ "atom_z": hparams["atom_z"],
317
+ "token_s": hparams["token_s"],
318
+ "token_z": hparams["token_z"],
319
+ "num_bins": hparams["num_bins"],
320
+ "embedder_args": _to_plain_python(copy.deepcopy(hparams["embedder_args"])),
321
+ "msa_args": _to_plain_python(copy.deepcopy(hparams["msa_args"])),
322
+ "pairformer_args": pairformer_args,
323
+ "score_model_args": _to_plain_python(copy.deepcopy(hparams["score_model_args"])),
324
+ "diffusion_process_args": diffusion_process_args,
325
+ "use_kernels": use_kernels,
326
+ }
327
+
328
+ if "confidence_model_args" in hparams:
329
+ confidence_model_args = _to_plain_python(
330
+ copy.deepcopy(hparams["confidence_model_args"])
331
+ )
332
+ if "pairformer_args" in confidence_model_args:
333
+ confidence_model_args["pairformer_args"] = _enforce_pairformer_v2(
334
+ confidence_model_args["pairformer_args"],
335
+ context="confidence",
336
+ )
337
+ core_kwargs["confidence_model_args"] = confidence_model_args
338
+ else:
339
+ core_kwargs["confidence_model_args"] = None
340
+
341
+ core_kwargs["confidence_prediction"] = hparams.get("confidence_prediction", True)
342
+ core_kwargs["token_level_confidence"] = hparams.get("token_level_confidence", True)
343
+ core_kwargs["alpha_pae"] = hparams.get("alpha_pae", 0.0)
344
+ core_kwargs["atoms_per_window_queries"] = hparams.get("atoms_per_window_queries", 32)
345
+ core_kwargs["atoms_per_window_keys"] = hparams.get("atoms_per_window_keys", 128)
346
+ core_kwargs["atom_feature_dim"] = hparams.get("atom_feature_dim", 128)
347
+ core_kwargs["bond_type_feature"] = hparams.get("bond_type_feature", False)
348
+ core_kwargs["run_trunk_and_structure"] = hparams.get("run_trunk_and_structure", True)
349
+ core_kwargs["skip_run_structure"] = hparams.get("skip_run_structure", False)
350
+ core_kwargs["fix_sym_check"] = hparams.get("fix_sym_check", False)
351
+ core_kwargs["cyclic_pos_enc"] = hparams.get("cyclic_pos_enc", False)
352
+ core_kwargs["use_no_atom_char"] = hparams.get("use_no_atom_char", False)
353
+ core_kwargs["use_atom_backbone_feat"] = hparams.get("use_atom_backbone_feat", False)
354
+ core_kwargs["use_residue_feats_atoms"] = hparams.get("use_residue_feats_atoms", False)
355
+ core_kwargs["conditioning_cutoff_min"] = hparams.get("conditioning_cutoff_min", 4.0)
356
+ core_kwargs["conditioning_cutoff_max"] = hparams.get("conditioning_cutoff_max", 20.0)
357
+
358
+ if "steering_args" in hparams and hparams["steering_args"] is not None:
359
+ core_kwargs["steering_args"] = _to_plain_python(copy.deepcopy(hparams["steering_args"]))
360
+ else:
361
+ core_kwargs["steering_args"] = _default_steering_args()
362
+
363
+ if "validation_args" in hparams:
364
+ validation_args = hparams["validation_args"]
365
+ if not isinstance(validation_args, Mapping):
366
+ raise TypeError(
367
+ "Expected 'validation_args' in checkpoint hyperparameters to be a mapping."
368
+ )
369
+ if default_recycling_steps is None and "recycling_steps" in validation_args:
370
+ default_recycling_steps = validation_args["recycling_steps"]
371
+ if default_sampling_steps is None and "sampling_steps" in validation_args:
372
+ default_sampling_steps = validation_args["sampling_steps"]
373
+ if default_diffusion_samples is None and "diffusion_samples" in validation_args:
374
+ default_diffusion_samples = validation_args["diffusion_samples"]
375
+
376
+ if default_recycling_steps is None:
377
+ default_recycling_steps = 3
378
+ if default_sampling_steps is None:
379
+ default_sampling_steps = 200
380
+ if default_diffusion_samples is None:
381
+ default_diffusion_samples = 1
382
+
383
+ return cls(
384
+ core_kwargs=core_kwargs,
385
+ num_bins=hparams["num_bins"],
386
+ default_recycling_steps=default_recycling_steps,
387
+ default_sampling_steps=default_sampling_steps,
388
+ default_diffusion_samples=default_diffusion_samples,
389
+ )
390
+
391
+
392
+ class Boltz2InferenceCore(nn.Module):
393
+ def __init__(
394
+ self,
395
+ atom_s: int,
396
+ atom_z: int,
397
+ token_s: int,
398
+ token_z: int,
399
+ num_bins: int,
400
+ embedder_args: dict[str, Any],
401
+ msa_args: dict[str, Any],
402
+ pairformer_args: dict[str, Any],
403
+ score_model_args: dict[str, Any],
404
+ diffusion_process_args: dict[str, Any],
405
+ confidence_model_args: dict[str, Any] | None = None,
406
+ atom_feature_dim: int = 128,
407
+ confidence_prediction: bool = True,
408
+ token_level_confidence: bool = True,
409
+ alpha_pae: float = 0.0,
410
+ atoms_per_window_queries: int = 32,
411
+ atoms_per_window_keys: int = 128,
412
+ run_trunk_and_structure: bool = True,
413
+ skip_run_structure: bool = False,
414
+ bond_type_feature: bool = False,
415
+ fix_sym_check: bool = False,
416
+ cyclic_pos_enc: bool = False,
417
+ use_no_atom_char: bool = False,
418
+ use_atom_backbone_feat: bool = False,
419
+ use_residue_feats_atoms: bool = False,
420
+ conditioning_cutoff_min: float = 4.0,
421
+ conditioning_cutoff_max: float = 20.0,
422
+ use_kernels: bool = False,
423
+ steering_args: dict[str, Any] | None = None,
424
+ ) -> None:
425
+ super().__init__()
426
+ self.use_kernels = use_kernels
427
+ self.confidence_prediction = confidence_prediction
428
+ self.token_level_confidence = token_level_confidence
429
+ self.alpha_pae = alpha_pae
430
+ self.run_trunk_and_structure = run_trunk_and_structure
431
+ self.skip_run_structure = skip_run_structure
432
+ self.bond_type_feature = bond_type_feature
433
+ self.steering_args = (
434
+ steering_args if steering_args is not None else _default_steering_args()
435
+ )
436
+ if "v2" not in pairformer_args:
437
+ raise ValueError("Boltz2 requires pairformer_args['v2'].")
438
+ if not pairformer_args["v2"]:
439
+ raise ValueError("Boltz2 requires pairformer_args['v2']=True.")
440
+
441
+ full_embedder_args = {
442
+ "atom_s": atom_s,
443
+ "atom_z": atom_z,
444
+ "token_s": token_s,
445
+ "token_z": token_z,
446
+ "atoms_per_window_queries": atoms_per_window_queries,
447
+ "atoms_per_window_keys": atoms_per_window_keys,
448
+ "atom_feature_dim": atom_feature_dim,
449
+ "use_no_atom_char": use_no_atom_char,
450
+ "use_atom_backbone_feat": use_atom_backbone_feat,
451
+ "use_residue_feats_atoms": use_residue_feats_atoms,
452
+ **embedder_args,
453
+ }
454
+ full_embedder_args = _filtered_kwargs(InputEmbedder, full_embedder_args)
455
+ self.input_embedder = InputEmbedder(**full_embedder_args)
456
+
457
+ self.s_init = nn.Linear(token_s, token_s, bias=False)
458
+ self.z_init_1 = nn.Linear(token_s, token_z, bias=False)
459
+ self.z_init_2 = nn.Linear(token_s, token_z, bias=False)
460
+ self.rel_pos = RelativePositionEncoder(
461
+ token_z,
462
+ fix_sym_check=fix_sym_check,
463
+ cyclic_pos_enc=cyclic_pos_enc,
464
+ )
465
+ self.token_bonds = nn.Linear(1, token_z, bias=False)
466
+ if self.bond_type_feature:
467
+ self.token_bonds_type = nn.Embedding(len(const.bond_types) + 1, token_z)
468
+
469
+ self.contact_conditioning = ContactConditioning(
470
+ token_z=token_z,
471
+ cutoff_min=conditioning_cutoff_min,
472
+ cutoff_max=conditioning_cutoff_max,
473
+ )
474
+ self.s_norm = nn.LayerNorm(token_s)
475
+ self.z_norm = nn.LayerNorm(token_z)
476
+
477
+ self.s_recycle = nn.Linear(token_s, token_s, bias=False)
478
+ self.z_recycle = nn.Linear(token_z, token_z, bias=False)
479
+ init.gating_init_(self.s_recycle.weight)
480
+ init.gating_init_(self.z_recycle.weight)
481
+
482
+ msa_kwargs = _filtered_kwargs(
483
+ MSAModule,
484
+ {"token_z": token_z, "token_s": token_s, **msa_args},
485
+ )
486
+ self.msa_module = MSAModule(**msa_kwargs)
487
+
488
+ pairformer_kwargs = _filtered_kwargs(
489
+ PairformerModule,
490
+ {"token_s": token_s, "token_z": token_z, **pairformer_args},
491
+ )
492
+ if "token_s" not in pairformer_kwargs or "token_z" not in pairformer_kwargs:
493
+ raise RuntimeError("Boltz2 pairformer construction lost its token dimensions.")
494
+ pairformer_token_s = pairformer_kwargs.pop("token_s")
495
+ pairformer_token_z = pairformer_kwargs.pop("token_z")
496
+ self.pairformer_module = PairformerModule(
497
+ pairformer_token_s,
498
+ pairformer_token_z,
499
+ **pairformer_kwargs,
500
+ )
501
+
502
+ diffusion_conditioning_kwargs = {
503
+ "token_s": token_s,
504
+ "token_z": token_z,
505
+ "atom_s": atom_s,
506
+ "atom_z": atom_z,
507
+ "atoms_per_window_queries": atoms_per_window_queries,
508
+ "atoms_per_window_keys": atoms_per_window_keys,
509
+ "atom_encoder_depth": score_model_args["atom_encoder_depth"],
510
+ "atom_encoder_heads": score_model_args["atom_encoder_heads"],
511
+ "token_transformer_depth": score_model_args["token_transformer_depth"],
512
+ "token_transformer_heads": score_model_args["token_transformer_heads"],
513
+ "atom_decoder_depth": score_model_args["atom_decoder_depth"],
514
+ "atom_decoder_heads": score_model_args["atom_decoder_heads"],
515
+ "atom_feature_dim": atom_feature_dim,
516
+ "conditioning_transition_layers": score_model_args["conditioning_transition_layers"],
517
+ "use_no_atom_char": use_no_atom_char,
518
+ "use_atom_backbone_feat": use_atom_backbone_feat,
519
+ "use_residue_feats_atoms": use_residue_feats_atoms,
520
+ }
521
+ diffusion_conditioning_kwargs = _filtered_kwargs(
522
+ DiffusionConditioning,
523
+ diffusion_conditioning_kwargs,
524
+ )
525
+ self.diffusion_conditioning = DiffusionConditioning(**diffusion_conditioning_kwargs)
526
+
527
+ structure_score_model_args = {
528
+ "token_s": token_s,
529
+ "atom_s": atom_s,
530
+ "atoms_per_window_queries": atoms_per_window_queries,
531
+ "atoms_per_window_keys": atoms_per_window_keys,
532
+ **score_model_args,
533
+ }
534
+ structure_score_model_args = _filtered_kwargs(
535
+ DiffusionModule,
536
+ structure_score_model_args,
537
+ )
538
+ structure_module_kwargs = {
539
+ "score_model_args": structure_score_model_args,
540
+ "compile_score": False,
541
+ **diffusion_process_args,
542
+ }
543
+ structure_module_kwargs = _filtered_kwargs(AtomDiffusion, structure_module_kwargs)
544
+ self.structure_module = AtomDiffusion(**structure_module_kwargs)
545
+ self.distogram_module = DistogramModule(token_z, num_bins)
546
+
547
+ if self.confidence_prediction:
548
+ if confidence_model_args is None:
549
+ raise ValueError(
550
+ "confidence_prediction=True requires confidence_model_args in config."
551
+ )
552
+ confidence_kwargs = {
553
+ "token_s": token_s,
554
+ "token_z": token_z,
555
+ "token_level_confidence": token_level_confidence,
556
+ "bond_type_feature": bond_type_feature,
557
+ "fix_sym_check": fix_sym_check,
558
+ "cyclic_pos_enc": cyclic_pos_enc,
559
+ "conditioning_cutoff_min": conditioning_cutoff_min,
560
+ "conditioning_cutoff_max": conditioning_cutoff_max,
561
+ **confidence_model_args,
562
+ }
563
+ confidence_kwargs = _filtered_kwargs(ConfidenceModule, confidence_kwargs)
564
+ self.confidence_module = ConfidenceModule(**confidence_kwargs)
565
+
566
+ def forward(
567
+ self,
568
+ feats: dict[str, Tensor],
569
+ recycling_steps: int = 3,
570
+ num_sampling_steps: int | None = None,
571
+ diffusion_samples: int = 1,
572
+ max_parallel_samples: int | None = None,
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,
614
+ "z": z,
615
+ }
616
+
617
+ if self.run_trunk_and_structure and (not self.skip_run_structure):
618
+ q, c, to_keys, atom_enc_bias, atom_dec_bias, token_trans_bias = (
619
+ self.diffusion_conditioning(
620
+ s_trunk=s,
621
+ z_trunk=z,
622
+ relative_position_encoding=relative_position_encoding,
623
+ feats=feats,
624
+ )
625
+ )
626
+ diffusion_conditioning = {
627
+ "q": q,
628
+ "c": c,
629
+ "to_keys": to_keys,
630
+ "atom_enc_bias": atom_enc_bias,
631
+ "atom_dec_bias": atom_dec_bias,
632
+ "token_trans_bias": token_trans_bias,
633
+ }
634
+ with torch.autocast("cuda", enabled=False):
635
+ struct_out = self.structure_module.sample(
636
+ s_trunk=s.float(),
637
+ s_inputs=s_inputs.float(),
638
+ feats=feats,
639
+ num_sampling_steps=num_sampling_steps,
640
+ atom_mask=feats["atom_pad_mask"].float(),
641
+ multiplicity=diffusion_samples,
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(
671
+ s_inputs=s_inputs_c,
672
+ s=s_c,
673
+ z=z_c,
674
+ x_pred=x_pred_c,
675
+ feats=feats,
676
+ pred_distogram_logits=pdist_c,
677
+ multiplicity=diffusion_samples,
678
+ run_sequentially=run_confidence_sequentially,
679
+ use_kernels=self.use_kernels,
680
+ )
681
+ )
682
+
683
+ return output
684
+
685
+
686
+ class Boltz2Model(PreTrainedModel):
687
+ config_class = Boltz2Config
688
+ base_model_prefix = "core"
689
+ all_tied_weights_keys: ClassVar[dict[str, str]] = {}
690
+
691
+ def __init__(self, config: Boltz2Config) -> None:
692
+ super().__init__(config)
693
+ if not isinstance(config.core_kwargs, dict):
694
+ raise TypeError("config.core_kwargs must be a dictionary.")
695
+ self.core = Boltz2InferenceCore(**config.core_kwargs)
696
+
697
+ def _init_weights(self, module: nn.Module) -> None:
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] = {}
704
+ for key, tensor in raw_state.items():
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:
718
+ kwargs["safe_serialization"] = True
719
+ if "state_dict" not in kwargs:
720
+ kwargs["state_dict"] = self._detied_state_dict()
721
+ super().save_pretrained(save_directory, **kwargs)
722
+
723
+ @property
724
+ def device(self) -> torch.device:
725
+ return next(self.parameters()).device
726
+
727
+ @classmethod
728
+ def from_boltz_checkpoint(
729
+ cls,
730
+ checkpoint_path: str,
731
+ map_location: str | torch.device = "cpu",
732
+ use_kernels: bool = False,
733
+ default_recycling_steps: int | None = None,
734
+ default_sampling_steps: int | None = None,
735
+ default_diffusion_samples: int | None = None,
736
+ *,
737
+ allow_unsafe_pickle: bool = False,
738
+ ) -> "Boltz2Model":
739
+ """Import an official Boltz Lightning checkpoint into the inference model.
740
+
741
+ Lightning checkpoints require Python pickle deserialization. The caller
742
+ must opt in with the literal value ``True`` after independently verifying
743
+ that the checkpoint comes from a trusted source and matches its expected
744
+ cryptographic hash.
745
+ """
746
+
747
+ if allow_unsafe_pickle is not True:
748
+ raise ValueError(
749
+ "Boltz Lightning checkpoints contain Python pickle and cannot be loaded "
750
+ "safely. Pass allow_unsafe_pickle=True only for a trusted, hash-verified "
751
+ "checkpoint."
752
+ )
753
+
754
+ # The official Lightning checkpoints contain OmegaConf objects, so this explicit
755
+ # trusted-checkpoint boundary requires full Python unpickling.
756
+ checkpoint = torch.load(
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")
764
+ _require_key(checkpoint, "state_dict")
765
+
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
+
773
+ config = Boltz2Config.from_hyperparameters(
774
+ hparams,
775
+ use_kernels=use_kernels,
776
+ default_recycling_steps=default_recycling_steps,
777
+ default_sampling_steps=default_sampling_steps,
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:
785
+ raise RuntimeError(
786
+ "Boltz2 inference core unexpectedly uses v1 attention parameters. "
787
+ "Expected pairformer v2 architecture."
788
+ )
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:
796
+ raise RuntimeError(
797
+ "Checkpoint is missing required parameters for Boltz2 inference core. "
798
+ f"Missing keys (first 20): {missing[:20]}"
799
+ )
800
+
801
+ load_result = model.core.load_state_dict(filtered, strict=False)
802
+ loaded_missing = sorted(load_result.missing_keys)
803
+ if loaded_missing:
804
+ raise RuntimeError(
805
+ "Model has unexpected missing keys after load_state_dict. "
806
+ f"Missing keys (first 20): {loaded_missing[:20]}"
807
+ )
808
+ if load_result.unexpected_keys:
809
+ raise RuntimeError(
810
+ "Model has unexpected checkpoint keys after load_state_dict: "
811
+ f"{sorted(load_result.unexpected_keys)[:20]}"
812
+ )
813
+ model.eval()
814
+ return model
815
+
816
+ def forward(
817
+ self,
818
+ feats: dict[str, Tensor],
819
+ recycling_steps: int | None = None,
820
+ num_sampling_steps: int | None = None,
821
+ diffusion_samples: int | None = None,
822
+ max_parallel_samples: int | None = None,
823
+ run_confidence_sequentially: bool = True,
824
+ detach_confidence: bool = True,
825
+ output_attentions: bool | None = None,
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
+ )
832
+ if output_attentions:
833
+ raise NotImplementedError(
834
+ "Boltz2 does not expose normalized attention tensors from its structure "
835
+ "modules. output_attentions=True is unsupported."
836
+ )
837
+ output_hidden_states = (
838
+ self.config.output_hidden_states
839
+ if output_hidden_states is None
840
+ else output_hidden_states
841
+ )
842
+ return_dict = self.config.use_return_dict if return_dict is None else return_dict
843
+ if recycling_steps is None:
844
+ recycling_steps = self.config.default_recycling_steps
845
+ if num_sampling_steps is None:
846
+ num_sampling_steps = self.config.default_sampling_steps
847
+ if diffusion_samples is None:
848
+ diffusion_samples = self.config.default_diffusion_samples
849
+ raw_output = self.core(
850
+ feats=feats,
851
+ recycling_steps=recycling_steps,
852
+ num_sampling_steps=num_sampling_steps,
853
+ diffusion_samples=diffusion_samples,
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)
863
+ if output_hidden_states and token_state is not None and pair_state is not None
864
+ else None,
865
+ **raw_output,
866
+ )
867
+ return model_output if return_dict else model_output.to_tuple()
868
+
869
+ def _to_model_device(
870
+ self,
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,
887
+ amino_acid_sequence: str,
888
+ recycling_steps: int | None = None,
889
+ num_sampling_steps: int | None = None,
890
+ diffusion_samples: int | None = None,
891
+ max_parallel_samples: int | None = None,
892
+ run_confidence_sequentially: bool = True,
893
+ float_dtype: torch.dtype = torch.float32,
894
+ seed: int | None = None,
895
+ ) -> Boltz2StructureOutput:
896
+ if float_dtype != torch.float32:
897
+ raise ValueError(
898
+ "Boltz2 predict_structure requires FP32 features and parameter storage; "
899
+ "CUDA compute runs under the declared BF16 autocast policy."
900
+ )
901
+ parameter_dtypes = {
902
+ parameter.dtype for parameter in self.parameters() if parameter.is_floating_point()
903
+ }
904
+ if parameter_dtypes and parameter_dtypes != {torch.float32}:
905
+ raise ValueError(
906
+ "Boltz2 predict_structure requires FP32 parameter storage before CUDA "
907
+ f"BF16 autocast; found {sorted(map(str, parameter_dtypes))}."
908
+ )
909
+
910
+ with _seed_context(seed):
911
+ feats, template = build_boltz2_features(
912
+ amino_acid_sequence=amino_acid_sequence,
913
+ num_bins=self.config.num_bins,
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"
922
+ else nullcontext()
923
+ )
924
+ with torch.no_grad(), autocast_context:
925
+ output = self.forward(
926
+ feats=feats,
927
+ recycling_steps=recycling_steps,
928
+ num_sampling_steps=num_sampling_steps,
929
+ diffusion_samples=diffusion_samples,
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,
962
+ atom_pad_mask=atom_pad_mask,
963
+ plddt=plddt,
964
+ confidence_score=confidence_score,
965
+ complex_plddt=complex_plddt,
966
+ iptm=iptm,
967
+ ptm=ptm,
968
+ sequence=template.sequence,
969
+ structure_template=template,
970
+ raw_output={key: _to_cpu_detached(val) for key, val in output.items()},
971
+ seed=seed,
972
+ )
973
+
974
+ def save_as_cif(
975
+ self,
976
+ structure_output: Boltz2StructureOutput,
977
+ output_path: str,
978
+ sample_index: int = 0,
979
+ ) -> str:
980
+ if structure_output.structure_template is None:
981
+ raise ValueError("structure_output.structure_template is required for CIF export.")
982
+ if structure_output.sample_atom_coords is None:
983
+ raise ValueError("structure_output.sample_atom_coords is required for CIF export.")
984
+ if structure_output.atom_pad_mask is None:
985
+ raise ValueError("structure_output.atom_pad_mask is required for CIF export.")
986
+ return write_cif(
987
+ structure_template=structure_output.structure_template,
988
+ atom_coords=structure_output.sample_atom_coords,
989
+ atom_mask=structure_output.atom_pad_mask,
990
+ output_path=output_path,
991
+ plddt=structure_output.plddt,
992
+ sample_index=sample_index,
993
+ )
fastplms/models/boltz/vb_const.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal biological constants required by the local Boltz2 runtime.
2
+
3
+ The upstream project contains additional training-time curation tables. They
4
+ are intentionally excluded here because FastPLMs neither trains Boltz2 nor
5
+ uses upstream data-processing code. Keeping only runtime inputs makes this
6
+ module auditable and prevents parity-oracle data from becoming a package
7
+ dependency.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+
13
+ def _index(values: list[str]) -> dict[str, int]:
14
+ return {value: index for index, value in enumerate(values)}
15
+
16
+
17
+ chain_types = ["PROTEIN", "DNA", "RNA", "NONPOLYMER"]
18
+ chain_type_ids = _index(chain_types)
19
+
20
+
21
+ canonical_tokens = [
22
+ "ALA",
23
+ "ARG",
24
+ "ASN",
25
+ "ASP",
26
+ "CYS",
27
+ "GLN",
28
+ "GLU",
29
+ "GLY",
30
+ "HIS",
31
+ "ILE",
32
+ "LEU",
33
+ "LYS",
34
+ "MET",
35
+ "PHE",
36
+ "PRO",
37
+ "SER",
38
+ "THR",
39
+ "TRP",
40
+ "TYR",
41
+ "VAL",
42
+ "UNK",
43
+ ]
44
+ tokens = [
45
+ "<pad>",
46
+ "-",
47
+ *canonical_tokens,
48
+ "A",
49
+ "G",
50
+ "C",
51
+ "U",
52
+ "N",
53
+ "DA",
54
+ "DG",
55
+ "DC",
56
+ "DT",
57
+ "DN",
58
+ ]
59
+ token_ids = _index(tokens)
60
+ num_tokens = len(tokens)
61
+
62
+ prot_letter_to_token = dict(
63
+ zip(
64
+ "ARNDCEQGHILKMFPSTWYV",
65
+ [
66
+ "ALA",
67
+ "ARG",
68
+ "ASN",
69
+ "ASP",
70
+ "CYS",
71
+ "GLU",
72
+ "GLN",
73
+ "GLY",
74
+ "HIS",
75
+ "ILE",
76
+ "LEU",
77
+ "LYS",
78
+ "MET",
79
+ "PHE",
80
+ "PRO",
81
+ "SER",
82
+ "THR",
83
+ "TRP",
84
+ "TYR",
85
+ "VAL",
86
+ ],
87
+ strict=True,
88
+ )
89
+ )
90
+ prot_letter_to_token.update({letter: "UNK" for letter in "XJBZOU"} | {"-": "-"})
91
+
92
+
93
+ def _parse_atom_rows(rows: str) -> dict[str, list[str]]:
94
+ table: dict[str, list[str]] = {}
95
+ for row in rows.strip().splitlines():
96
+ residue, *atom_names = row.split()
97
+ table[residue] = atom_names
98
+ return table
99
+
100
+
101
+ ref_atoms = {"PAD": [], "-": []}
102
+ ref_atoms.update(
103
+ _parse_atom_rows(
104
+ """
105
+ UNK N CA C O CB
106
+ ALA N CA C O CB
107
+ ARG N CA C O CB CG CD NE CZ NH1 NH2
108
+ ASN N CA C O CB CG OD1 ND2
109
+ ASP N CA C O CB CG OD1 OD2
110
+ CYS N CA C O CB SG
111
+ GLN N CA C O CB CG CD OE1 NE2
112
+ GLU N CA C O CB CG CD OE1 OE2
113
+ GLY N CA C O
114
+ HIS N CA C O CB CG ND1 CD2 CE1 NE2
115
+ ILE N CA C O CB CG1 CG2 CD1
116
+ LEU N CA C O CB CG CD1 CD2
117
+ LYS N CA C O CB CG CD CE NZ
118
+ MET N CA C O CB CG SD CE
119
+ PHE N CA C O CB CG CD1 CD2 CE1 CE2 CZ
120
+ PRO N CA C O CB CG CD
121
+ SER N CA C O CB OG
122
+ THR N CA C O CB OG1 CG2
123
+ TRP N CA C O CB CG CD1 CD2 NE1 CE2 CE3 CZ2 CZ3 CH2
124
+ TYR N CA C O CB CG CD1 CD2 CE1 CE2 CZ OH
125
+ VAL N CA C O CB CG1 CG2
126
+ """
127
+ )
128
+ )
129
+
130
+ protein_backbone_atom_names = ["N", "CA", "C", "O"]
131
+ nucleic_backbone_atom_names = [
132
+ "P",
133
+ "OP1",
134
+ "OP2",
135
+ "O5'",
136
+ "C5'",
137
+ "C4'",
138
+ "O4'",
139
+ "C3'",
140
+ "O3'",
141
+ "C2'",
142
+ "O2'",
143
+ "C1'",
144
+ ]
145
+ protein_backbone_atom_index = _index(protein_backbone_atom_names)
146
+ nucleic_backbone_atom_index = _index(nucleic_backbone_atom_names)
147
+
148
+ _rna_backbone = nucleic_backbone_atom_names
149
+ _dna_backbone = [atom for atom in _rna_backbone if atom != "O2'"]
150
+ ref_atoms.update(
151
+ {
152
+ "A": [*_rna_backbone, "N9", "C8", "N7", "C5", "C6", "N6", "N1", "C2", "N3", "C4"],
153
+ "G": [*_rna_backbone, "N9", "C8", "N7", "C5", "C6", "O6", "N1", "C2", "N2", "N3", "C4"],
154
+ "C": [*_rna_backbone, "N1", "C2", "O2", "N3", "C4", "N4", "C5", "C6"],
155
+ "U": [*_rna_backbone, "N1", "C2", "O2", "N3", "C4", "O4", "C5", "C6"],
156
+ "N": list(_rna_backbone),
157
+ "DA": [*_dna_backbone, "N9", "C8", "N7", "C5", "C6", "N6", "N1", "C2", "N3", "C4"],
158
+ "DG": [*_dna_backbone, "N9", "C8", "N7", "C5", "C6", "O6", "N1", "C2", "N2", "N3", "C4"],
159
+ "DC": [*_dna_backbone, "N1", "C2", "O2", "N3", "C4", "N4", "C5", "C6"],
160
+ "DT": [*_dna_backbone, "N1", "C2", "O2", "N3", "C4", "O4", "C5", "C7", "C6"],
161
+ "DN": list(_dna_backbone),
162
+ }
163
+ )
164
+
165
+ _protein_tokens = ["UNK", *canonical_tokens[:-1]]
166
+ _nucleic_tokens = ["A", "G", "C", "U", "N", "DA", "DG", "DC", "DT", "DN"]
167
+ res_to_center_atom = {
168
+ **dict.fromkeys(_protein_tokens, "CA"),
169
+ **dict.fromkeys(_nucleic_tokens, "C1'"),
170
+ }
171
+ res_to_disto_atom = {
172
+ **dict.fromkeys(_protein_tokens, "CB"),
173
+ "GLY": "CA",
174
+ "A": "C4",
175
+ "G": "C4",
176
+ "C": "C2",
177
+ "U": "C2",
178
+ "N": "C1'",
179
+ "DA": "C4",
180
+ "DG": "C4",
181
+ "DC": "C2",
182
+ "DT": "C2",
183
+ "DN": "C1'",
184
+ }
185
+
186
+
187
+ num_elements = 128
188
+ bond_types = ["OTHER", "SINGLE", "DOUBLE", "TRIPLE", "AROMATIC", "COVALENT"]
189
+ contact_conditioning_info = {
190
+ "UNSPECIFIED": 0,
191
+ "UNSELECTED": 1,
192
+ "POCKET>BINDER": 2,
193
+ "BINDER>POCKET": 3,
194
+ "CONTACT": 4,
195
+ }
196
+ chunk_size_threshold = 384
197
+
198
+ _method_groups = {
199
+ 0: ("MD",),
200
+ 1: ("X-RAY DIFFRACTION",),
201
+ 2: ("ELECTRON MICROSCOPY",),
202
+ 3: ("SOLUTION NMR",),
203
+ 4: (
204
+ "SOLID-STATE NMR",
205
+ "NEUTRON DIFFRACTION",
206
+ "ELECTRON CRYSTALLOGRAPHY",
207
+ "FIBER DIFFRACTION",
208
+ "POWDER DIFFRACTION",
209
+ "INFRARED SPECTROSCOPY",
210
+ "FLUORESCENCE TRANSFER",
211
+ "EPR",
212
+ "THEORETICAL MODEL",
213
+ "SOLUTION SCATTERING",
214
+ "OTHER",
215
+ ),
216
+ 5: ("AFDB",),
217
+ 6: ("BOLTZ-1",),
218
+ 7: ("FUTURE1",),
219
+ 8: ("FUTURE2",),
220
+ 9: ("FUTURE3",),
221
+ 10: ("FUTURE4",),
222
+ 11: ("FUTURE5",),
223
+ }
224
+ method_types_ids = {
225
+ method.lower(): identifier
226
+ for identifier, methods in _method_groups.items()
227
+ for method in methods
228
+ }
229
+ num_method_types = len(_method_groups)
230
+
231
+ vdw_radii = [
232
+ float(radius)
233
+ for radius in [
234
+ "1.2",
235
+ "1.4",
236
+ "2.2",
237
+ "1.9",
238
+ "1.8",
239
+ "1.7",
240
+ "1.6",
241
+ "1.55",
242
+ "1.5",
243
+ "1.54",
244
+ "2.4",
245
+ "2.2",
246
+ "2.1",
247
+ "2.1",
248
+ "1.95",
249
+ "1.8",
250
+ "1.8",
251
+ "1.88",
252
+ "2.8",
253
+ "2.4",
254
+ "2.3",
255
+ "2.15",
256
+ "2.05",
257
+ "2.05",
258
+ "2.05",
259
+ "2.05",
260
+ "2.0",
261
+ "2.0",
262
+ "2.0",
263
+ "2.1",
264
+ "2.1",
265
+ "2.1",
266
+ "2.05",
267
+ "1.9",
268
+ "1.9",
269
+ "2.02",
270
+ "2.9",
271
+ "2.55",
272
+ "2.4",
273
+ "2.3",
274
+ "2.15",
275
+ "2.1",
276
+ "2.05",
277
+ "2.05",
278
+ "2.0",
279
+ "2.05",
280
+ "2.1",
281
+ "2.2",
282
+ "2.2",
283
+ "2.25",
284
+ "2.2",
285
+ "2.1",
286
+ "2.1",
287
+ "2.16",
288
+ "3.0",
289
+ "2.7",
290
+ "2.5",
291
+ "2.48",
292
+ "2.47",
293
+ "2.45",
294
+ "2.43",
295
+ "2.42",
296
+ "2.4",
297
+ "2.38",
298
+ "2.37",
299
+ "2.35",
300
+ "2.33",
301
+ "2.32",
302
+ "2.3",
303
+ "2.28",
304
+ "2.27",
305
+ "2.25",
306
+ "2.2",
307
+ "2.1",
308
+ "2.05",
309
+ "2.0",
310
+ "2.0",
311
+ "2.05",
312
+ "2.1",
313
+ "2.05",
314
+ "2.2",
315
+ "2.3",
316
+ "2.3",
317
+ "2.0",
318
+ "2.0",
319
+ "2.0",
320
+ "2.0",
321
+ "2.0",
322
+ "2.0",
323
+ "2.4",
324
+ "2.0",
325
+ "2.3",
326
+ "2.0",
327
+ "2.0",
328
+ "2.0",
329
+ "2.0",
330
+ "2.0",
331
+ "2.0",
332
+ "2.0",
333
+ "2.0",
334
+ "2.0",
335
+ "2.0",
336
+ "2.0",
337
+ "2.0",
338
+ "2.0",
339
+ "2.0",
340
+ "2.0",
341
+ "2.0",
342
+ "2.0",
343
+ "2.0",
344
+ "2.0",
345
+ "2.0",
346
+ "2.0",
347
+ "2.0",
348
+ "2.0",
349
+ "2.0",
350
+ "2.0",
351
+ "2.0",
352
+ ]
353
+ ]
fastplms/models/boltz/vb_layers_attention.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pair-biased self-attention used in the Boltz2 diffusion stack."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, MutableMapping
6
+
7
+ from einops.layers.torch import Rearrange
8
+ from torch import Tensor, nn
9
+
10
+ from . import vb_layers_initialize as init
11
+ from ._pair_attention import pair_biased_attention, reshape_heads
12
+
13
+
14
+ class AttentionPairBias(nn.Module):
15
+ """Attend over sequence states while adding a learned pair representation."""
16
+
17
+ def __init__(
18
+ self,
19
+ c_s: int,
20
+ c_z: int,
21
+ num_heads: int,
22
+ inf: float = 1e6,
23
+ initial_norm: bool = True,
24
+ ) -> None:
25
+ super().__init__()
26
+ if c_s % num_heads:
27
+ raise ValueError(f"c_s={c_s} must be divisible by num_heads={num_heads}")
28
+
29
+ self.c_s = c_s
30
+ self.num_heads = num_heads
31
+ self.head_dim = c_s // num_heads
32
+ self.inf = inf
33
+ self.initial_norm = initial_norm
34
+ if initial_norm:
35
+ self.norm_s = nn.LayerNorm(c_s)
36
+
37
+ self.proj_q = nn.Linear(c_s, c_s)
38
+ self.proj_k = nn.Linear(c_s, c_s, bias=False)
39
+ self.proj_v = nn.Linear(c_s, c_s, bias=False)
40
+ self.proj_g = nn.Linear(c_s, c_s, bias=False)
41
+ self.proj_z = nn.Sequential(
42
+ nn.LayerNorm(c_z),
43
+ nn.Linear(c_z, num_heads, bias=False),
44
+ Rearrange("b ... h -> b h ..."),
45
+ )
46
+ self.proj_o = nn.Linear(c_s, c_s, bias=False)
47
+ init.final_init_(self.proj_o.weight)
48
+
49
+ def _resolve_pair_bias(
50
+ self,
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,
63
+ s: Tensor,
64
+ z: Tensor,
65
+ mask: Tensor,
66
+ k_in: Tensor | None = None,
67
+ multiplicity: int = 1,
68
+ to_keys: Callable[[Tensor], Tensor] | None = None,
69
+ model_cache: MutableMapping[str, Tensor] | None = None,
70
+ ) -> Tensor:
71
+ """Transform S with shapes ``S: (b, l_q, d)`` and ``Z: (b, l, l, d_z)``."""
72
+
73
+ 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,
91
+ value,
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)
fastplms/models/boltz/vb_layers_attentionv2.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cross-attention variant of Boltz2 pair-biased attention."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from einops.layers.torch import Rearrange
6
+ from torch import Tensor, nn
7
+
8
+ from . import vb_layers_initialize as init
9
+ from ._pair_attention import pair_biased_attention, reshape_heads
10
+
11
+
12
+ class AttentionPairBias(nn.Module):
13
+ """Attend from S to K while adding either learned or precomputed pair bias."""
14
+
15
+ def __init__(
16
+ self,
17
+ c_s: int,
18
+ c_z: int | None = None,
19
+ num_heads: int | None = None,
20
+ inf: float = 1e6,
21
+ compute_pair_bias: bool = True,
22
+ ) -> None:
23
+ super().__init__()
24
+ if num_heads is None or c_s % num_heads:
25
+ raise ValueError("num_heads must divide c_s")
26
+ if compute_pair_bias and c_z is None:
27
+ raise ValueError("c_z is required when pair bias is learned")
28
+
29
+ self.c_s = c_s
30
+ self.num_heads = num_heads
31
+ self.head_dim = c_s // num_heads
32
+ self.inf = inf
33
+ self.proj_q = nn.Linear(c_s, c_s)
34
+ self.proj_k = nn.Linear(c_s, c_s, bias=False)
35
+ self.proj_v = nn.Linear(c_s, c_s, bias=False)
36
+ self.proj_g = nn.Linear(c_s, c_s, bias=False)
37
+ self.compute_pair_bias = compute_pair_bias
38
+ if compute_pair_bias:
39
+ self.proj_z = nn.Sequential(
40
+ nn.LayerNorm(c_z),
41
+ nn.Linear(c_z, num_heads, bias=False),
42
+ Rearrange("b ... h -> b h ..."),
43
+ )
44
+ else:
45
+ self.proj_z = Rearrange("b ... h -> b h ...")
46
+ self.proj_o = nn.Linear(c_s, c_s, bias=False)
47
+ init.final_init_(self.proj_o.weight)
48
+
49
+ def forward(
50
+ self,
51
+ s: Tensor,
52
+ z: Tensor,
53
+ mask: Tensor,
54
+ k_in: Tensor,
55
+ multiplicity: int = 1,
56
+ ) -> Tensor:
57
+ """Transform S with K and pair input Z, returning shape ``(b, l_q, d)``."""
58
+
59
+ 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,
66
+ value,
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)
fastplms/models/boltz/vb_layers_confidence_utils.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Confidence geometry and aggregate metrics for Boltz2 outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ from torch import nn
7
+
8
+ from . import vb_const as const
9
+
10
+
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:
23
+ with torch.amp.autocast("cuda", enabled=False):
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(
31
+ coordinates: torch.Tensor,
32
+ frame_indices: torch.Tensor,
33
+ feats: dict[str, torch.Tensor],
34
+ atom_chain_ids: torch.Tensor,
35
+ resolved_mask: torch.Tensor | None,
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 = (
53
+ feats["mol_type"][batch_index, token_offset] == const.chain_type_ids["NONPOLYMER"]
54
+ )
55
+ if is_nonpolymer and atom_count >= 3:
56
+ chain_resolved = (
57
+ feats["atom_pad_mask"][batch_index]
58
+ if inference
59
+ else (
60
+ feats["atom_resolved_mask"][batch_index]
61
+ if resolved_mask is None
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
+ :,
83
+ token_offset : token_offset + atom_count,
84
+ :,
85
+ ] = frames
86
+ token_offset += token_count
87
+ atom_offset += atom_count
88
+
89
+
90
+ def compute_frame_pred(
91
+ pred_atom_coords: torch.Tensor,
92
+ frames_idx_true: torch.Tensor,
93
+ feats: dict[str, torch.Tensor],
94
+ multiplicity: int,
95
+ resolved_mask: torch.Tensor | None = None,
96
+ inference: bool = False,
97
+ ) -> tuple[torch.Tensor, torch.Tensor]:
98
+ """Construct predicted local frames and their validity mask.
99
+
100
+ ``pred_atom_coords`` has shape ``(b * multiplicity, a, 3)``. Polymer
101
+ frames retain the supplied atom indices; nonpolymer frames use each
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,
114
+ feats,
115
+ atom_chain_ids,
116
+ resolved_mask,
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:
135
+ """Convert categorical confidence logits to bin-center expectations."""
136
+
137
+ num_bins = logits.shape[-1]
138
+ bin_width = end / num_bins
139
+ centers = torch.arange(
140
+ 0.5 * bin_width,
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(
166
+ logits: torch.Tensor,
167
+ x_preds: torch.Tensor,
168
+ feats: dict[str, torch.Tensor],
169
+ multiplicity: int,
170
+ ) -> tuple[
171
+ torch.Tensor,
172
+ torch.Tensor,
173
+ torch.Tensor,
174
+ torch.Tensor,
175
+ dict[int, dict[int, torch.Tensor]],
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(
194
+ 0.5 * (32.0 / num_bins),
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():
218
+ scores: dict[int, torch.Tensor] = {}
219
+ for second_chain in torch.unique(asym_id).tolist():
220
+ chain_mask = base_pair_mask
221
+ chain_mask = chain_mask * (asym_id[:, None, :] == first_chain)
222
+ chain_mask = chain_mask * (asym_id[:, :, None] == second_chain)
223
+ scores[second_chain] = _maximum_masked_tm(expected_tm, chain_mask)
224
+ chain_pair_iptm[first_chain] = scores
225
+
226
+ return ptm, iptm, ligand_iptm, protein_iptm, chain_pair_iptm
fastplms/models/boltz/vb_layers_dropout.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Broadcast dropout masks used by the Boltz pair stack.
2
+
3
+ The mask is sampled in ``float32`` even when the pair tensor uses a lower
4
+ precision dtype. This matches the checkpoint implementation while keeping
5
+ the broadcast axis explicit.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import torch
11
+ from torch import Tensor
12
+
13
+
14
+ def _broadcast_mask_shape(Z: Tensor, *, columnwise: bool) -> tuple[int, ...]:
15
+ """Return the row-wise or column-wise pair-mask shape for ``Z``."""
16
+
17
+ if Z.ndim != 4:
18
+ raise ValueError(f"pair tensor must have four dimensions, got {Z.ndim}")
19
+ b, rows, columns, _ = Z.shape
20
+ return (b, 1, columns, 1) if columnwise else (b, rows, 1, 1)
21
+
22
+
23
+ def get_dropout_mask(
24
+ dropout: float,
25
+ z: Tensor,
26
+ training: bool,
27
+ columnwise: bool = False,
28
+ ) -> Tensor:
29
+ """Sample an inverted-dropout mask that broadcasts over pair channels.
30
+
31
+ ``Z`` is the pair tensor with shape ``(b, n, n, d)``. Row-wise masks
32
+ vary along the first residue axis; column-wise masks vary along the
33
+ second. Evaluation returns an all-one mask while retaining the same RNG
34
+ call pattern as training.
35
+ """
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))
fastplms/models/boltz/vb_layers_initialize.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Torch-only parameter initialization for the Boltz2 runtime."""
2
+
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
+
14
+ def _calculate_fan(shape: torch.Size | tuple[int, ...], fan: FanMode = "fan_in") -> float:
15
+ """Resolve the selected fan for a two-dimensional linear weight tensor."""
16
+
17
+ if len(shape) != 2:
18
+ raise ValueError(f"linear weights must be two-dimensional, received {tuple(shape)}")
19
+ fan_out, fan_in = shape
20
+ if fan == "fan_in":
21
+ return float(fan_in)
22
+ if fan == "fan_out":
23
+ return float(fan_out)
24
+ if fan == "fan_avg":
25
+ return (fan_in + fan_out) / 2
26
+ raise ValueError(f"invalid fan mode: {fan!r}")
27
+
28
+
29
+ def trunc_normal_init_(
30
+ weights: Tensor,
31
+ scale: float = 1.0,
32
+ fan: FanMode = "fan_in",
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():
39
+ torch.nn.init.trunc_normal_(weights, mean=0.0, std=std, a=-2 * std, b=2 * std)
40
+
41
+
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
+
48
+ def he_normal_init_(weights: Tensor) -> None:
49
+ """Initialize W using twice the fan-in variance."""
50
+
51
+ trunc_normal_init_(weights, scale=2.0)
52
+
53
+
54
+ def glorot_uniform_init_(weights: Tensor) -> None:
55
+ """Initialize W with Xavier uniform values."""
56
+
57
+ torch.nn.init.xavier_uniform_(weights, gain=1.0)
58
+
59
+
60
+ def _fill_(tensor: Tensor, value: float) -> None:
61
+ with torch.no_grad():
62
+ tensor.fill_(value)
63
+
64
+
65
+ def final_init_(weights: Tensor) -> None:
66
+ """Zero the final projection W."""
67
+
68
+ _fill_(weights, 0.0)
69
+
70
+
71
+ def gating_init_(weights: Tensor) -> None:
72
+ """Zero the gating projection W."""
73
+
74
+ _fill_(weights, 0.0)
75
+
76
+
77
+ def bias_init_zero_(bias: Tensor) -> None:
78
+ """Set the bias tensor to zero."""
79
+
80
+ _fill_(bias, 0.0)
81
+
82
+
83
+ def bias_init_one_(bias: Tensor) -> None:
84
+ """Set the bias tensor to one."""
85
+
86
+ _fill_(bias, 1.0)
87
+
88
+
89
+ def normal_init_(weights: Tensor) -> None:
90
+ """Initialize W with linear Kaiming-normal values."""
91
+
92
+ torch.nn.init.kaiming_normal_(weights, nonlinearity="linear")
93
+
94
+
95
+ def ipa_point_weights_init_(weights: Tensor) -> None:
96
+ """Initialize W so applying softplus yields one."""
97
+
98
+ _fill_(weights, 0.541324854612918)
fastplms/models/boltz/vb_layers_outer_product_mean.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Outer-product aggregation from MSA states to pair states."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ from torch import Tensor, nn
7
+
8
+ from . import vb_layers_initialize as init
9
+
10
+
11
+ class OuterProductMean(nn.Module):
12
+ """Aggregate M with shape ``(b, s, l, d_m)`` into pair tensor Z."""
13
+
14
+ def __init__(self, c_in: int, c_hidden: int, c_out: int) -> None:
15
+ super().__init__()
16
+ self.c_hidden = c_hidden
17
+ self.norm = nn.LayerNorm(c_in)
18
+ self.proj_a = nn.Linear(c_in, c_hidden, bias=False)
19
+ self.proj_b = nn.Linear(c_in, c_hidden, bias=False)
20
+ self.proj_o = nn.Linear(c_hidden * c_hidden, c_out)
21
+ init.final_init_(self.proj_o.weight)
22
+ init.final_init_(self.proj_o.bias)
23
+
24
+ @staticmethod
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,
44
+ left: Tensor,
45
+ right: Tensor,
46
+ counts: Tensor,
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,
79
+ counts,
80
+ chunk_size,
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))
fastplms/models/boltz/vb_layers_pair_averaging.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pair-weighted aggregation of Boltz2 MSA states."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ from torch import Tensor, nn
7
+
8
+ from . import vb_layers_initialize as init
9
+
10
+
11
+ class PairWeightedAveraging(nn.Module):
12
+ """Update M with pair-derived attention weights while preserving its shape."""
13
+
14
+ def __init__(
15
+ self,
16
+ c_m: int,
17
+ c_z: int,
18
+ c_h: int,
19
+ num_heads: int,
20
+ inf: float = 1e6,
21
+ ) -> None:
22
+ super().__init__()
23
+ self.c_m = c_m
24
+ self.c_z = c_z
25
+ self.c_h = c_h
26
+ self.num_heads = num_heads
27
+ self.inf = inf
28
+ self.norm_m = nn.LayerNorm(c_m)
29
+ self.norm_z = nn.LayerNorm(c_z)
30
+ self.proj_m = nn.Linear(c_m, c_h * num_heads, bias=False)
31
+ self.proj_g = nn.Linear(c_m, c_h * num_heads, bias=False)
32
+ self.proj_z = nn.Linear(c_z, num_heads, bias=False)
33
+ self.proj_o = nn.Linear(c_h * num_heads, c_m, bias=False)
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,
73
+ m: Tensor,
74
+ z: Tensor,
75
+ mask: Tensor,
76
+ chunk_heads: bool = True,
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
fastplms/models/boltz/vb_layers_pairformer.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pairformer layers for joint or pair-only Boltz2 representations."""
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
+
11
+ from . import vb_const as const
12
+ from .vb_layers_attention import AttentionPairBias
13
+ from .vb_layers_attentionv2 import AttentionPairBias as AttentionPairBiasV2
14
+ from .vb_layers_dropout import get_dropout_mask
15
+ from .vb_layers_transition import Transition
16
+ from .vb_layers_triangular_mult import (
17
+ TriangleMultiplicationIncoming,
18
+ TriangleMultiplicationOutgoing,
19
+ )
20
+ from .vb_tri_attn_attention import (
21
+ TriangleAttentionEndingNode,
22
+ TriangleAttentionStartingNode,
23
+ )
24
+
25
+
26
+ def _pair_update(
27
+ modules: Any,
28
+ pair_states: Tensor,
29
+ pair_mask: Tensor,
30
+ chunk_size: int | None,
31
+ use_kernels: bool,
32
+ use_cuequiv_mul: bool,
33
+ use_cuequiv_attn: bool,
34
+ ) -> Tensor:
35
+ """Apply multiplicative, attention, and transition updates to pair tensor Z."""
36
+
37
+ 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:
71
+ if training:
72
+ return None
73
+ return 128 if pair_states.shape[1] > const.chunk_size_threshold else 512
74
+
75
+
76
+ class PairformerLayer(nn.Module):
77
+ """Update sequence tensor S and pair tensor Z once."""
78
+
79
+ def __init__(
80
+ self,
81
+ token_s: int,
82
+ token_z: int,
83
+ num_heads: int = 16,
84
+ dropout: float = 0.25,
85
+ pairwise_head_width: int = 32,
86
+ pairwise_num_heads: int = 4,
87
+ post_layer_norm: bool = False,
88
+ v2: bool = False,
89
+ ) -> None:
90
+ super().__init__()
91
+ self.token_z = token_z
92
+ self.dropout = dropout
93
+ self.num_heads = num_heads
94
+ self.post_layer_norm = post_layer_norm
95
+ self.pre_norm_s = nn.LayerNorm(token_s)
96
+ attention_class = AttentionPairBiasV2 if v2 else AttentionPairBias
97
+ self.attention = attention_class(token_s, token_z, num_heads)
98
+ self.tri_mul_out = TriangleMultiplicationOutgoing(token_z)
99
+ self.tri_mul_in = TriangleMultiplicationIncoming(token_z)
100
+ self.tri_att_start = TriangleAttentionStartingNode(
101
+ token_z,
102
+ pairwise_head_width,
103
+ pairwise_num_heads,
104
+ inf=1e9,
105
+ )
106
+ self.tri_att_end = TriangleAttentionEndingNode(
107
+ token_z,
108
+ pairwise_head_width,
109
+ pairwise_num_heads,
110
+ inf=1e9,
111
+ )
112
+ self.transition_s = Transition(token_s, token_s * 4)
113
+ self.transition_z = Transition(token_z, token_z * 4)
114
+ self.s_post_norm = nn.LayerNorm(token_s) if post_layer_norm else nn.Identity()
115
+
116
+ def forward(
117
+ self,
118
+ s: Tensor,
119
+ z: Tensor,
120
+ mask: Tensor,
121
+ pair_mask: Tensor,
122
+ chunk_size_tri_attn: int | None = None,
123
+ use_kernels: bool = False,
124
+ use_cuequiv_mul: bool = False,
125
+ use_cuequiv_attn: bool = False,
126
+ ) -> tuple[Tensor, Tensor]:
127
+ """Return updated ``S: (b, l, d_s)`` and ``Z: (b, l, l, d_z)``."""
128
+
129
+ pair_output = _pair_update(
130
+ self,
131
+ z,
132
+ pair_mask,
133
+ chunk_size_tri_attn,
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):
152
+ """Run a stack of joint sequence and pair updates."""
153
+
154
+ def __init__(
155
+ self,
156
+ token_s: int,
157
+ token_z: int,
158
+ num_blocks: int,
159
+ num_heads: int = 16,
160
+ dropout: float = 0.25,
161
+ pairwise_head_width: int = 32,
162
+ pairwise_num_heads: int = 4,
163
+ post_layer_norm: bool = False,
164
+ activation_checkpointing: bool = False,
165
+ v2: bool = False,
166
+ **kwargs: Any,
167
+ ) -> None:
168
+ del kwargs
169
+ super().__init__()
170
+ self.token_z = token_z
171
+ self.num_blocks = num_blocks
172
+ self.dropout = dropout
173
+ self.num_heads = num_heads
174
+ self.post_layer_norm = post_layer_norm
175
+ self.activation_checkpointing = activation_checkpointing
176
+ self.layers = nn.ModuleList(
177
+ [
178
+ PairformerLayer(
179
+ token_s,
180
+ token_z,
181
+ num_heads,
182
+ dropout,
183
+ pairwise_head_width,
184
+ pairwise_num_heads,
185
+ post_layer_norm,
186
+ v2,
187
+ )
188
+ for _ in range(num_blocks)
189
+ ]
190
+ )
191
+
192
+ def forward(
193
+ self,
194
+ s: Tensor,
195
+ z: Tensor,
196
+ mask: Tensor,
197
+ pair_mask: Tensor,
198
+ use_kernels: bool = False,
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(
207
+ layer,
208
+ sequence_output,
209
+ pair_output,
210
+ mask,
211
+ pair_mask,
212
+ chunk_size,
213
+ use_kernels,
214
+ use_reentrant=False,
215
+ )
216
+ else:
217
+ sequence_output, pair_output = layer(
218
+ sequence_output,
219
+ pair_output,
220
+ mask,
221
+ pair_mask,
222
+ chunk_size,
223
+ use_kernels,
224
+ )
225
+ return sequence_output, pair_output
226
+
227
+
228
+ class PairformerNoSeqLayer(nn.Module):
229
+ """Update pair tensor Z without a sequence track."""
230
+
231
+ def __init__(
232
+ self,
233
+ token_z: int,
234
+ dropout: float = 0.25,
235
+ pairwise_head_width: int = 32,
236
+ pairwise_num_heads: int = 4,
237
+ post_layer_norm: bool = False,
238
+ ) -> None:
239
+ super().__init__()
240
+ self.token_z = token_z
241
+ self.dropout = dropout
242
+ self.post_layer_norm = post_layer_norm
243
+ self.tri_mul_out = TriangleMultiplicationOutgoing(token_z)
244
+ self.tri_mul_in = TriangleMultiplicationIncoming(token_z)
245
+ self.tri_att_start = TriangleAttentionStartingNode(
246
+ token_z,
247
+ pairwise_head_width,
248
+ pairwise_num_heads,
249
+ inf=1e9,
250
+ )
251
+ self.tri_att_end = TriangleAttentionEndingNode(
252
+ token_z,
253
+ pairwise_head_width,
254
+ pairwise_num_heads,
255
+ inf=1e9,
256
+ )
257
+ self.transition_z = Transition(token_z, token_z * 4)
258
+
259
+ def forward(
260
+ self,
261
+ z: Tensor,
262
+ pair_mask: Tensor,
263
+ chunk_size_tri_attn: int | None = None,
264
+ use_kernels: bool = False,
265
+ use_cuequiv_mul: bool = False,
266
+ use_cuequiv_attn: bool = False,
267
+ ) -> Tensor:
268
+ """Return updated pair tensor Z with shape ``(b, l, l, d_z)``."""
269
+
270
+ return _pair_update(
271
+ self,
272
+ z,
273
+ pair_mask,
274
+ chunk_size_tri_attn,
275
+ use_kernels,
276
+ use_cuequiv_mul,
277
+ use_cuequiv_attn,
278
+ )
279
+
280
+
281
+ class PairformerNoSeqModule(nn.Module):
282
+ """Run a stack of pair-only updates."""
283
+
284
+ def __init__(
285
+ self,
286
+ token_z: int,
287
+ num_blocks: int,
288
+ dropout: float = 0.25,
289
+ pairwise_head_width: int = 32,
290
+ pairwise_num_heads: int = 4,
291
+ post_layer_norm: bool = False,
292
+ activation_checkpointing: bool = False,
293
+ **kwargs: Any,
294
+ ) -> None:
295
+ del kwargs
296
+ super().__init__()
297
+ self.token_z = token_z
298
+ self.num_blocks = num_blocks
299
+ self.dropout = dropout
300
+ self.post_layer_norm = post_layer_norm
301
+ self.activation_checkpointing = activation_checkpointing
302
+ self.layers = nn.ModuleList(
303
+ [
304
+ PairformerNoSeqLayer(
305
+ token_z,
306
+ dropout,
307
+ pairwise_head_width,
308
+ pairwise_num_heads,
309
+ post_layer_norm,
310
+ )
311
+ for _ in range(num_blocks)
312
+ ]
313
+ )
314
+
315
+ def forward(
316
+ self,
317
+ z: Tensor,
318
+ pair_mask: Tensor,
319
+ use_kernels: bool = False,
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(
328
+ layer,
329
+ output,
330
+ pair_mask,
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
fastplms/models/boltz/vb_layers_transition.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gated feed-forward transition used by Boltz2 pair representations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ from torch import Tensor, nn
7
+
8
+ from . import vb_layers_initialize as init
9
+
10
+
11
+ class Transition(nn.Module):
12
+ """Apply a normalized SwiGLU-style transition.
13
+
14
+ The checkpoint-facing module names remain ``norm`` and ``fc1`` through
15
+ ``fc3``. For an input tensor X with shape ``(..., d)``, the module returns
16
+ a tensor with shape ``(..., d_out)``.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ dim: int = 128,
22
+ hidden: int = 512,
23
+ out_dim: int | None = None,
24
+ ) -> None:
25
+ super().__init__()
26
+ output_dim = dim if out_dim is None else out_dim
27
+ self.norm = nn.LayerNorm(dim, eps=1e-5)
28
+ self.fc1 = nn.Linear(dim, hidden, bias=False)
29
+ self.fc2 = nn.Linear(dim, hidden, bias=False)
30
+ self.fc3 = nn.Linear(hidden, output_dim, bias=False)
31
+ self.silu = nn.SiLU()
32
+ self.hidden = hidden
33
+
34
+ init.bias_init_one_(self.norm.weight)
35
+ init.bias_init_zero_(self.norm.bias)
36
+ init.lecun_normal_init_(self.fc1.weight)
37
+ init.lecun_normal_init_(self.fc2.weight)
38
+ init.final_init_(self.fc3.weight)
39
+
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
+
59
+ output: Tensor | None = None
60
+ for start in range(0, self.hidden, chunk_size):
61
+ contribution = self._project_hidden_slice(
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
fastplms/models/boltz/vb_layers_triangular_mult.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Incoming and outgoing triangular multiplicative pair updates."""
2
+
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
+
17
+ @torch.compiler.disable
18
+ def kernel_triangular_mult(
19
+ x: Tensor,
20
+ direction: TriangleDirection,
21
+ mask: Tensor,
22
+ norm_in_weight: Tensor,
23
+ norm_in_bias: Tensor,
24
+ p_in_weight: Tensor,
25
+ g_in_weight: Tensor,
26
+ norm_out_weight: Tensor,
27
+ norm_out_bias: Tensor,
28
+ p_out_weight: Tensor,
29
+ g_out_weight: Tensor,
30
+ eps: float,
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
37
+ ):
38
+ raise RuntimeError(
39
+ "Boltz2 use_kernels=True requires cuequivariance_torch and the CUDA 13 "
40
+ "cuequivariance_ops_torch runtime from the 'structure,cueq' extras."
41
+ )
42
+ cueq = importlib.import_module("cuequivariance_torch")
43
+ return cueq.triangle_multiplicative_update(
44
+ x,
45
+ direction=direction,
46
+ mask=mask,
47
+ norm_in_weight=norm_in_weight,
48
+ norm_in_bias=norm_in_bias,
49
+ p_in_weight=p_in_weight,
50
+ g_in_weight=g_in_weight,
51
+ norm_out_weight=norm_out_weight,
52
+ norm_out_bias=norm_out_bias,
53
+ p_out_weight=p_out_weight,
54
+ g_out_weight=g_out_weight,
55
+ eps=eps,
56
+ )
57
+
58
+
59
+ class _TriangleMultiplication(nn.Module):
60
+ direction: TriangleDirection
61
+ equation: str
62
+
63
+ def __init__(self, dim: int, direction: TriangleDirection, equation: str) -> None:
64
+ super().__init__()
65
+ self.direction = direction
66
+ self.equation = equation
67
+ self.norm_in = nn.LayerNorm(dim, eps=1e-5)
68
+ self.p_in = nn.Linear(dim, 2 * dim, bias=False)
69
+ self.g_in = nn.Linear(dim, 2 * dim, bias=False)
70
+ self.norm_out = nn.LayerNorm(dim)
71
+ self.p_out = nn.Linear(dim, dim, bias=False)
72
+ self.g_out = nn.Linear(dim, dim, bias=False)
73
+
74
+ init.bias_init_one_(self.norm_in.weight)
75
+ init.bias_init_zero_(self.norm_in.bias)
76
+ init.lecun_normal_init_(self.p_in.weight)
77
+ init.gating_init_(self.g_in.weight)
78
+ init.bias_init_one_(self.norm_out.weight)
79
+ init.bias_init_zero_(self.norm_out.bias)
80
+ init.final_init_(self.p_out.weight)
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,
87
+ mask=mask,
88
+ norm_in_weight=self.norm_in.weight,
89
+ norm_in_bias=self.norm_in.bias,
90
+ p_in_weight=self.p_in.weight,
91
+ g_in_weight=self.g_in.weight,
92
+ norm_out_weight=self.norm_out.weight,
93
+ norm_out_bias=self.norm_out.bias,
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)``."""
101
+
102
+ if use_kernels:
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):
115
+ """Aggregate pair paths that share their destination index."""
116
+
117
+ def __init__(self, dim: int = 128) -> None:
118
+ super().__init__(dim, direction="outgoing", equation="bikd,bjkd->bijd")
119
+
120
+
121
+ class TriangleMultiplicationIncoming(_TriangleMultiplication):
122
+ """Aggregate pair paths that share their source index."""
123
+
124
+ def __init__(self, dim: int = 128) -> None:
125
+ super().__init__(dim, direction="incoming", equation="bkid,bkjd->bijd")
fastplms/models/boltz/vb_loss_diffusionv2.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Geometry objectives shared by Boltz diffusion and steering code.
2
+
3
+ The rigid-alignment mechanism is based on the Kabsch formulation used by
4
+ AlphaFold 3 implementations. The implementation is maintained locally and
5
+ does not import an upstream runtime package.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import warnings
11
+
12
+ import torch
13
+ import torch.nn.functional as functional
14
+ from einops import einsum
15
+
16
+
17
+ 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(
28
+ mask: torch.Tensor,
29
+ singular_values: torch.Tensor,
30
+ *,
31
+ num_points: int,
32
+ coordinate_dim: int,
33
+ ) -> None:
34
+ if torch.any(mask.sum(dim=-1) < coordinate_dim + 1):
35
+ warnings.warn(
36
+ "The size of one of the point clouds is <= dim+1. "
37
+ "`WeightedRigidAlign` cannot return a unique rotation.",
38
+ RuntimeWarning,
39
+ stacklevel=3,
40
+ )
41
+ if (singular_values.abs() <= 1e-15).any() and num_points >= coordinate_dim + 1:
42
+ warnings.warn(
43
+ "Excessively low rank of cross-correlation between aligned "
44
+ "point clouds. `WeightedRigidAlign` cannot return a unique rotation.",
45
+ RuntimeWarning,
46
+ stacklevel=3,
47
+ )
48
+
49
+
50
+ def weighted_rigid_align(
51
+ true_coords: torch.Tensor,
52
+ pred_coords: torch.Tensor,
53
+ weights: torch.Tensor,
54
+ mask: torch.Tensor,
55
+ ) -> torch.Tensor:
56
+ """Align true coordinates to predicted coordinates with weighted Kabsch.
57
+
58
+ ``true_coords`` and ``pred_coords`` have shape ``(..., n, 3)``. The
59
+ returned tensor is detached because alignment defines a fixed target for
60
+ the diffusion loss.
61
+ """
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,
87
+ num_points=num_points,
88
+ coordinate_dim=coordinate_dim,
89
+ )
90
+
91
+ preliminary_rotation = torch.einsum(
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(
115
+ pred_coords: torch.Tensor,
116
+ true_coords: torch.Tensor,
117
+ is_nucleotide: torch.Tensor,
118
+ coords_mask: torch.Tensor,
119
+ *,
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(
153
+ pred_coords: torch.Tensor,
154
+ true_coords: torch.Tensor,
155
+ is_nucleotide: torch.Tensor,
156
+ coords_mask: torch.Tensor,
157
+ nucleic_acid_cutoff: float = 30.0,
158
+ other_cutoff: float = 15.0,
159
+ multiplicity: int = 1,
160
+ ) -> torch.Tensor:
161
+ """Return one minus the smooth local-distance agreement.
162
+
163
+ Coordinate tensors have shape ``(b, n, 3)``. Sequence-level masks may
164
+ be shared across repeated diffusion samples through ``multiplicity``.
165
+ """
166
+
167
+ agreements = [
168
+ _smooth_lddt_for_example(
169
+ pred_coords[index],
170
+ true_coords[index],
171
+ is_nucleotide[index // multiplicity],
172
+ coords_mask[index // multiplicity],
173
+ nucleic_acid_cutoff=nucleic_acid_cutoff,
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)
fastplms/models/boltz/vb_modules_confidencev2.py ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+
4
+ from . import vb_const as const
5
+ from . import vb_layers_initialize as init
6
+ from .vb_layers_confidence_utils import (
7
+ compute_aggregated_metric,
8
+ compute_ptms,
9
+ )
10
+ from .vb_layers_pairformer import PairformerModule
11
+ from .vb_modules_encodersv2 import RelativePositionEncoder
12
+ from .vb_modules_trunkv2 import (
13
+ ContactConditioning,
14
+ )
15
+ from .vb_modules_utils import LinearNoBias
16
+
17
+
18
+ def _token_slot_logits_to_atom_logits(
19
+ token_logits: torch.Tensor,
20
+ atom_to_token: torch.Tensor,
21
+ atom_pad_mask: torch.Tensor,
22
+ *,
23
+ multiplicity: int,
24
+ ) -> torch.Tensor:
25
+ """Gather per-token atom-slot logits onto each example's atom table.
26
+
27
+ ``token_logits`` is ordered as ``(batch * multiplicity, token, slot,
28
+ channel)``. ``atom_to_token`` determines both the owning token and the
29
+ within-token slot of every atom, so heterogeneous atom counts do not leak
30
+ across batch rows.
31
+ """
32
+
33
+ if token_logits.ndim != 4:
34
+ raise ValueError(
35
+ "token_logits must have shape (batch * multiplicity, token, slot, channel), "
36
+ f"got {tuple(token_logits.shape)}."
37
+ )
38
+ if atom_to_token.ndim != 3:
39
+ raise ValueError(
40
+ "atom_to_token must have shape (batch, atom, token), "
41
+ f"got {tuple(atom_to_token.shape)}."
42
+ )
43
+ if atom_pad_mask.shape != atom_to_token.shape[:2]:
44
+ raise ValueError(
45
+ "atom_pad_mask must match the batch and atom axes of atom_to_token; "
46
+ f"got {tuple(atom_pad_mask.shape)} and {tuple(atom_to_token.shape)}."
47
+ )
48
+ if multiplicity < 1:
49
+ raise ValueError(f"multiplicity must be positive, got {multiplicity}.")
50
+
51
+ batch_size, atom_count, token_count = atom_to_token.shape
52
+ if token_logits.shape[0] != batch_size * multiplicity:
53
+ raise ValueError(
54
+ "token_logits batch axis must equal batch * multiplicity; "
55
+ f"got {token_logits.shape[0]} and {batch_size} * {multiplicity}."
56
+ )
57
+ if token_logits.shape[1] != token_count:
58
+ raise ValueError(
59
+ "token_logits and atom_to_token disagree on token count; "
60
+ f"got {token_logits.shape[1]} and {token_count}."
61
+ )
62
+
63
+ valid_atoms = atom_pad_mask.bool()
64
+ assignments = atom_to_token.bool() & valid_atoms.unsqueeze(-1)
65
+ token_index = assignments.to(dtype=torch.int64).argmax(dim=-1)
66
+ # Cumulative one-hot counts give each atom its ordinal within its owning
67
+ # token without assuming that atoms from different tokens are contiguous.
68
+ cumulative_slots = assignments.to(dtype=torch.int64).cumsum(dim=1) - 1
69
+ slot_index = (cumulative_slots * assignments).sum(dim=-1)
70
+
71
+ slots_per_token = token_logits.shape[2]
72
+ flattened_index = token_index * slots_per_token + slot_index
73
+ flattened_index = flattened_index.masked_fill(~valid_atoms, 0)
74
+ flattened_index = flattened_index.repeat_interleave(multiplicity, dim=0)
75
+ expanded_atom_mask = valid_atoms.repeat_interleave(multiplicity, dim=0)
76
+
77
+ flattened_logits = token_logits.flatten(1, 2)
78
+ gather_index = flattened_index.unsqueeze(-1).expand(
79
+ -1,
80
+ atom_count,
81
+ flattened_logits.shape[-1],
82
+ )
83
+ atom_logits = torch.gather(flattened_logits, dim=1, index=gather_index)
84
+ return atom_logits * expanded_atom_mask.unsqueeze(-1).to(dtype=atom_logits.dtype)
85
+
86
+
87
+ class ConfidenceModule(nn.Module):
88
+ """Algorithm 31"""
89
+
90
+ def __init__(
91
+ self,
92
+ token_s,
93
+ token_z,
94
+ pairformer_args: dict,
95
+ num_dist_bins=64,
96
+ token_level_confidence=True,
97
+ max_dist=22,
98
+ add_s_to_z_prod=False,
99
+ add_s_input_to_s=False,
100
+ add_z_input_to_z=False,
101
+ maximum_bond_distance=0,
102
+ bond_type_feature=False,
103
+ confidence_args: dict | None = None,
104
+ compile_pairformer=False,
105
+ fix_sym_check=False,
106
+ cyclic_pos_enc=False,
107
+ return_latent_feats=False,
108
+ conditioning_cutoff_min=None,
109
+ conditioning_cutoff_max=None,
110
+ **kwargs,
111
+ ):
112
+ super().__init__()
113
+ self.max_num_atoms_per_token = 23
114
+ self.no_update_s = pairformer_args.get("no_update_s", False)
115
+ boundaries = torch.linspace(2, max_dist, num_dist_bins - 1)
116
+ self.register_buffer("boundaries", boundaries)
117
+ self.dist_bin_pairwise_embed = nn.Embedding(num_dist_bins, token_z)
118
+ init.gating_init_(self.dist_bin_pairwise_embed.weight)
119
+ self.token_level_confidence = token_level_confidence
120
+
121
+ self.s_to_z = LinearNoBias(token_s, token_z)
122
+ self.s_to_z_transpose = LinearNoBias(token_s, token_z)
123
+ init.gating_init_(self.s_to_z.weight)
124
+ init.gating_init_(self.s_to_z_transpose.weight)
125
+
126
+ self.add_s_to_z_prod = add_s_to_z_prod
127
+ if add_s_to_z_prod:
128
+ self.s_to_z_prod_in1 = LinearNoBias(token_s, token_z)
129
+ self.s_to_z_prod_in2 = LinearNoBias(token_s, token_z)
130
+ self.s_to_z_prod_out = LinearNoBias(token_z, token_z)
131
+ init.gating_init_(self.s_to_z_prod_out.weight)
132
+
133
+ self.s_inputs_norm = nn.LayerNorm(token_s)
134
+ if not self.no_update_s:
135
+ self.s_norm = nn.LayerNorm(token_s)
136
+ self.z_norm = nn.LayerNorm(token_z)
137
+
138
+ self.add_s_input_to_s = add_s_input_to_s
139
+ if add_s_input_to_s:
140
+ self.s_input_to_s = LinearNoBias(token_s, token_s)
141
+ init.gating_init_(self.s_input_to_s.weight)
142
+
143
+ self.add_z_input_to_z = add_z_input_to_z
144
+ if add_z_input_to_z:
145
+ self.rel_pos = RelativePositionEncoder(
146
+ token_z, fix_sym_check=fix_sym_check, cyclic_pos_enc=cyclic_pos_enc
147
+ )
148
+ self.token_bonds = nn.Linear(
149
+ 1 if maximum_bond_distance == 0 else maximum_bond_distance + 2,
150
+ token_z,
151
+ bias=False,
152
+ )
153
+ self.bond_type_feature = bond_type_feature
154
+ if bond_type_feature:
155
+ self.token_bonds_type = nn.Embedding(len(const.bond_types) + 1, token_z)
156
+
157
+ self.contact_conditioning = ContactConditioning(
158
+ token_z=token_z,
159
+ cutoff_min=conditioning_cutoff_min,
160
+ cutoff_max=conditioning_cutoff_max,
161
+ )
162
+ pairformer_args["v2"] = True
163
+ self.pairformer_stack = PairformerModule(
164
+ token_s,
165
+ token_z,
166
+ **pairformer_args,
167
+ )
168
+ self.return_latent_feats = return_latent_feats
169
+
170
+ self.confidence_heads = ConfidenceHeads(
171
+ token_s,
172
+ token_z,
173
+ token_level_confidence=token_level_confidence,
174
+ **confidence_args,
175
+ )
176
+
177
+ def forward(
178
+ self,
179
+ s_inputs, # Float['b n ts']
180
+ s, # Float['b n ts']
181
+ z, # Float['b n n tz']
182
+ x_pred, # Float['bm m 3']
183
+ feats,
184
+ pred_distogram_logits,
185
+ multiplicity=1,
186
+ run_sequentially=False,
187
+ use_kernels: bool = False,
188
+ ):
189
+ if run_sequentially and multiplicity > 1:
190
+ batch_size = z.shape[0]
191
+ expected_shape = (batch_size, multiplicity)
192
+ if x_pred.ndim >= 4 and x_pred.shape[:2] == expected_shape:
193
+ sample_coordinates = x_pred
194
+ elif x_pred.shape[0] == batch_size * multiplicity:
195
+ sample_coordinates = x_pred.reshape(
196
+ batch_size,
197
+ multiplicity,
198
+ *x_pred.shape[1:],
199
+ )
200
+ else:
201
+ raise ValueError(
202
+ "Sequential confidence expected coordinates with leading shape "
203
+ f"{expected_shape} or {batch_size * multiplicity}, got "
204
+ f"{tuple(x_pred.shape)}."
205
+ )
206
+ out_dicts = []
207
+ for sample_idx in range(multiplicity):
208
+ out_dicts.append(
209
+ self.forward(
210
+ s_inputs,
211
+ s,
212
+ z,
213
+ sample_coordinates[:, sample_idx],
214
+ feats,
215
+ pred_distogram_logits,
216
+ multiplicity=1,
217
+ run_sequentially=False,
218
+ use_kernels=use_kernels,
219
+ )
220
+ )
221
+
222
+ out_dict = {}
223
+ for key in out_dicts[0]:
224
+ if key != "pair_chains_iptm":
225
+ values = [out[key] for out in out_dicts]
226
+ out_dict[key] = torch.stack(values, dim=1).flatten(0, 1)
227
+ else:
228
+ pair_chains_iptm = {}
229
+ for chain_idx1 in out_dicts[0][key]:
230
+ chains_iptm = {}
231
+ for chain_idx2 in out_dicts[0][key][chain_idx1]:
232
+ values = [
233
+ out[key][chain_idx1][chain_idx2]
234
+ for out in out_dicts
235
+ ]
236
+ chains_iptm[chain_idx2] = torch.stack(
237
+ values,
238
+ dim=1,
239
+ ).flatten(0, 1)
240
+ pair_chains_iptm[chain_idx1] = chains_iptm
241
+ out_dict[key] = pair_chains_iptm
242
+ return out_dict
243
+
244
+ s_inputs = self.s_inputs_norm(s_inputs)
245
+ if not self.no_update_s:
246
+ s = self.s_norm(s)
247
+
248
+ if self.add_s_input_to_s:
249
+ s = s + self.s_input_to_s(s_inputs)
250
+
251
+ z = self.z_norm(z)
252
+
253
+ if self.add_z_input_to_z:
254
+ relative_position_encoding = self.rel_pos(feats)
255
+ z = z + relative_position_encoding
256
+ z = z + self.token_bonds(feats["token_bonds"].float())
257
+ if self.bond_type_feature:
258
+ z = z + self.token_bonds_type(feats["type_bonds"].long())
259
+ z = z + self.contact_conditioning(feats)
260
+
261
+ s = s.repeat_interleave(multiplicity, 0)
262
+
263
+ z = (
264
+ z
265
+ + self.s_to_z(s_inputs)[:, :, None, :]
266
+ + self.s_to_z_transpose(s_inputs)[:, None, :, :]
267
+ )
268
+ if self.add_s_to_z_prod:
269
+ z = z + self.s_to_z_prod_out(
270
+ self.s_to_z_prod_in1(s_inputs)[:, :, None, :]
271
+ * self.s_to_z_prod_in2(s_inputs)[:, None, :, :]
272
+ )
273
+
274
+ z = z.repeat_interleave(multiplicity, 0)
275
+ s_inputs = s_inputs.repeat_interleave(multiplicity, 0)
276
+
277
+ token_to_rep_atom = feats["token_to_rep_atom"]
278
+ token_to_rep_atom = token_to_rep_atom.repeat_interleave(multiplicity, 0)
279
+ if len(x_pred.shape) == 4:
280
+ b, multiplicity, n, _ = x_pred.shape
281
+ x_pred = x_pred.reshape(b * multiplicity, n, -1)
282
+ else:
283
+ _, n, _ = x_pred.shape
284
+ x_pred_repr = torch.bmm(token_to_rep_atom.float(), x_pred)
285
+ d = torch.cdist(x_pred_repr, x_pred_repr)
286
+ distogram = (d.unsqueeze(-1) > self.boundaries).sum(dim=-1).long()
287
+ distogram = self.dist_bin_pairwise_embed(distogram)
288
+ z = z + distogram
289
+
290
+ mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0)
291
+ pair_mask = mask[:, :, None] * mask[:, None, :]
292
+
293
+ s_t, z_t = self.pairformer_stack(
294
+ s, z, mask=mask, pair_mask=pair_mask, use_kernels=use_kernels
295
+ )
296
+
297
+ # AF3 has residual connections, we remove them
298
+ s = s_t
299
+ z = z_t
300
+
301
+ out_dict = {}
302
+
303
+ if self.return_latent_feats:
304
+ out_dict["s_conf"] = s
305
+ out_dict["z_conf"] = z
306
+
307
+ # confidence heads
308
+ out_dict.update(
309
+ self.confidence_heads(
310
+ s=s,
311
+ z=z,
312
+ x_pred=x_pred,
313
+ d=d,
314
+ feats=feats,
315
+ multiplicity=multiplicity,
316
+ pred_distogram_logits=pred_distogram_logits,
317
+ )
318
+ )
319
+ return out_dict
320
+
321
+
322
+ class ConfidenceHeads(nn.Module):
323
+ def __init__(
324
+ self,
325
+ token_s,
326
+ token_z,
327
+ num_plddt_bins=50,
328
+ num_pde_bins=64,
329
+ num_pae_bins=64,
330
+ token_level_confidence=True,
331
+ use_separate_heads: bool = False,
332
+ **kwargs,
333
+ ):
334
+ super().__init__()
335
+ self.max_num_atoms_per_token = 23
336
+ self.token_level_confidence = token_level_confidence
337
+ self.use_separate_heads = use_separate_heads
338
+
339
+ if self.use_separate_heads:
340
+ self.to_pae_intra_logits = LinearNoBias(token_z, num_pae_bins)
341
+ self.to_pae_inter_logits = LinearNoBias(token_z, num_pae_bins)
342
+ else:
343
+ self.to_pae_logits = LinearNoBias(token_z, num_pae_bins)
344
+
345
+ if self.use_separate_heads:
346
+ self.to_pde_intra_logits = LinearNoBias(token_z, num_pde_bins)
347
+ self.to_pde_inter_logits = LinearNoBias(token_z, num_pde_bins)
348
+ else:
349
+ self.to_pde_logits = LinearNoBias(token_z, num_pde_bins)
350
+
351
+ if self.token_level_confidence:
352
+ self.to_plddt_logits = LinearNoBias(token_s, num_plddt_bins)
353
+ self.to_resolved_logits = LinearNoBias(token_s, 2)
354
+ else:
355
+ self.to_plddt_logits = LinearNoBias(
356
+ token_s, num_plddt_bins * self.max_num_atoms_per_token
357
+ )
358
+ self.to_resolved_logits = LinearNoBias(token_s, 2 * self.max_num_atoms_per_token)
359
+
360
+ def forward(
361
+ self,
362
+ s, # Float['b n ts']
363
+ z, # Float['b n n tz']
364
+ x_pred, # Float['bm m 3']
365
+ d,
366
+ feats,
367
+ pred_distogram_logits,
368
+ multiplicity=1,
369
+ ):
370
+ if self.use_separate_heads:
371
+ asym_id_token = feats["asym_id"]
372
+ is_same_chain = asym_id_token.unsqueeze(-1) == asym_id_token.unsqueeze(-2)
373
+ is_different_chain = ~is_same_chain
374
+
375
+ if self.use_separate_heads:
376
+ pae_intra_logits = self.to_pae_intra_logits(z)
377
+ pae_intra_logits = pae_intra_logits * is_same_chain.float().unsqueeze(-1)
378
+
379
+ pae_inter_logits = self.to_pae_inter_logits(z)
380
+ pae_inter_logits = pae_inter_logits * is_different_chain.float().unsqueeze(-1)
381
+
382
+ pae_logits = pae_inter_logits + pae_intra_logits
383
+ else:
384
+ pae_logits = self.to_pae_logits(z)
385
+
386
+ if self.use_separate_heads:
387
+ pde_intra_logits = self.to_pde_intra_logits(z + z.transpose(1, 2))
388
+ pde_intra_logits = pde_intra_logits * is_same_chain.float().unsqueeze(-1)
389
+
390
+ pde_inter_logits = self.to_pde_inter_logits(z + z.transpose(1, 2))
391
+ pde_inter_logits = pde_inter_logits * is_different_chain.float().unsqueeze(-1)
392
+
393
+ pde_logits = pde_inter_logits + pde_intra_logits
394
+ else:
395
+ pde_logits = self.to_pde_logits(z + z.transpose(1, 2))
396
+ resolved_logits = self.to_resolved_logits(s)
397
+ plddt_logits = self.to_plddt_logits(s)
398
+
399
+ ligand_weight = 20
400
+ non_interface_weight = 1
401
+ interface_weight = 10
402
+
403
+ token_type = feats["mol_type"]
404
+ token_type = token_type.repeat_interleave(multiplicity, 0)
405
+ is_ligand_token = (token_type == const.chain_type_ids["NONPOLYMER"]).float()
406
+
407
+ if self.token_level_confidence:
408
+ plddt = compute_aggregated_metric(plddt_logits)
409
+ token_pad_mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0)
410
+ complex_plddt = (plddt * token_pad_mask).sum(dim=-1) / token_pad_mask.sum(dim=-1)
411
+
412
+ is_contact = (d < 8).float()
413
+ is_different_chain = (
414
+ feats["asym_id"].unsqueeze(-1) != feats["asym_id"].unsqueeze(-2)
415
+ ).float()
416
+ is_different_chain = is_different_chain.repeat_interleave(multiplicity, 0)
417
+ token_interface_mask = torch.max(
418
+ is_contact * is_different_chain * (1 - is_ligand_token).unsqueeze(-1),
419
+ dim=-1,
420
+ ).values
421
+ token_non_interface_mask = (1 - token_interface_mask) * (1 - is_ligand_token)
422
+ iplddt_weight = (
423
+ is_ligand_token * ligand_weight
424
+ + token_interface_mask * interface_weight
425
+ + token_non_interface_mask * non_interface_weight
426
+ )
427
+ complex_iplddt = (plddt * token_pad_mask * iplddt_weight).sum(dim=-1) / torch.sum(
428
+ token_pad_mask * iplddt_weight, dim=-1
429
+ )
430
+
431
+ else:
432
+ # token to atom conversion for resolved logits
433
+ b, n, _ = resolved_logits.shape
434
+ resolved_logits = resolved_logits.reshape(b, n, self.max_num_atoms_per_token, 2)
435
+ resolved_logits = _token_slot_logits_to_atom_logits(
436
+ resolved_logits,
437
+ feats["atom_to_token"],
438
+ feats["atom_pad_mask"],
439
+ multiplicity=multiplicity,
440
+ )
441
+ plddt_logits = plddt_logits.reshape(b, n, self.max_num_atoms_per_token, -1)
442
+ plddt_logits = _token_slot_logits_to_atom_logits(
443
+ plddt_logits,
444
+ feats["atom_to_token"],
445
+ feats["atom_pad_mask"],
446
+ multiplicity=multiplicity,
447
+ )
448
+ atom_pad_mask = feats["atom_pad_mask"].repeat_interleave(multiplicity, 0)
449
+ plddt = compute_aggregated_metric(plddt_logits)
450
+
451
+ complex_plddt = (plddt * atom_pad_mask).sum(dim=-1) / atom_pad_mask.sum(dim=-1)
452
+ atom_to_token = feats["atom_to_token"].float().repeat_interleave(
453
+ multiplicity,
454
+ 0,
455
+ )
456
+ chain_id_token = feats["asym_id"].float().repeat_interleave(
457
+ multiplicity,
458
+ 0,
459
+ )
460
+ atom_type = torch.bmm(
461
+ atom_to_token,
462
+ token_type.float().unsqueeze(-1),
463
+ ).squeeze(-1)
464
+ is_ligand_atom = (atom_type == const.chain_type_ids["NONPOLYMER"]).float()
465
+ d_atom = torch.cdist(x_pred, x_pred)
466
+ is_contact = (d_atom < 8).float()
467
+ chain_id_atom = torch.bmm(atom_to_token, chain_id_token.unsqueeze(-1)).squeeze(-1)
468
+ is_different_chain = (
469
+ chain_id_atom.unsqueeze(-1) != chain_id_atom.unsqueeze(-2)
470
+ ).float()
471
+
472
+ atom_interface_mask = torch.max(
473
+ is_contact * is_different_chain * (1 - is_ligand_atom).unsqueeze(-1),
474
+ dim=-1,
475
+ ).values
476
+ atom_non_interface_mask = (1 - atom_interface_mask) * (1 - is_ligand_atom)
477
+ iplddt_weight = (
478
+ is_ligand_atom * ligand_weight
479
+ + atom_interface_mask * interface_weight
480
+ + atom_non_interface_mask * non_interface_weight
481
+ )
482
+
483
+ complex_iplddt = (plddt * atom_pad_mask * iplddt_weight).sum(dim=-1) / torch.sum(
484
+ atom_pad_mask * iplddt_weight,
485
+ dim=-1,
486
+ )
487
+
488
+ # Compute the gPDE and giPDE
489
+ pde = compute_aggregated_metric(pde_logits, end=32)
490
+ pred_distogram_prob = nn.functional.softmax(
491
+ pred_distogram_logits, dim=-1
492
+ ).repeat_interleave(multiplicity, 0)
493
+ contacts = torch.zeros((1, 1, 1, 64), dtype=pred_distogram_prob.dtype).to(
494
+ pred_distogram_prob.device
495
+ )
496
+ contacts[:, :, :, :20] = 1.0
497
+ prob_contact = (pred_distogram_prob * contacts).sum(-1)
498
+ token_pad_mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0)
499
+ token_pad_pair_mask = (
500
+ token_pad_mask.unsqueeze(-1)
501
+ * token_pad_mask.unsqueeze(-2)
502
+ * (1 - torch.eye(token_pad_mask.shape[1], device=token_pad_mask.device).unsqueeze(0))
503
+ )
504
+ token_pair_mask = token_pad_pair_mask * prob_contact
505
+ complex_pde_numerator = (pde * token_pair_mask).sum(dim=(1, 2))
506
+ complex_pde_denominator = token_pair_mask.sum(dim=(1, 2))
507
+ complex_pde = complex_pde_numerator / torch.where(
508
+ complex_pde_denominator > 0,
509
+ complex_pde_denominator,
510
+ torch.ones_like(complex_pde_denominator),
511
+ )
512
+ asym_id = feats["asym_id"].repeat_interleave(multiplicity, 0)
513
+ token_interface_pair_mask = token_pair_mask * (
514
+ asym_id.unsqueeze(-1) != asym_id.unsqueeze(-2)
515
+ )
516
+ complex_ipde = (pde * token_interface_pair_mask).sum(dim=(1, 2)) / (
517
+ token_interface_pair_mask.sum(dim=(1, 2)) + 1e-5
518
+ )
519
+ out_dict = dict(
520
+ pde_logits=pde_logits,
521
+ plddt_logits=plddt_logits,
522
+ resolved_logits=resolved_logits,
523
+ pde=pde,
524
+ plddt=plddt,
525
+ complex_plddt=complex_plddt,
526
+ complex_iplddt=complex_iplddt,
527
+ complex_pde=complex_pde,
528
+ complex_ipde=complex_ipde,
529
+ )
530
+ out_dict["pae_logits"] = pae_logits
531
+ out_dict["pae"] = compute_aggregated_metric(pae_logits, end=32)
532
+
533
+ ptm, iptm, ligand_iptm, protein_iptm, pair_chains_iptm = compute_ptms(
534
+ pae_logits, x_pred, feats, multiplicity
535
+ )
536
+ out_dict["ptm"] = ptm
537
+ out_dict["iptm"] = iptm
538
+ out_dict["ligand_iptm"] = ligand_iptm
539
+ out_dict["protein_iptm"] = protein_iptm
540
+ out_dict["pair_chains_iptm"] = pair_chains_iptm
541
+
542
+ return out_dict
fastplms/models/boltz/vb_modules_diffusion_conditioning.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Precompute pair and atom biases used by the Boltz2 diffusion stack."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ from torch import nn
7
+
8
+ from .vb_modules_encodersv2 import AtomEncoder, PairwiseConditioning
9
+
10
+
11
+ def _bias_projections(
12
+ depth: int,
13
+ input_dim: int,
14
+ num_heads: int,
15
+ ) -> nn.ModuleList:
16
+ """Build one normalized, bias-free projection per transformer block."""
17
+
18
+ return nn.ModuleList(
19
+ [
20
+ nn.Sequential(
21
+ nn.LayerNorm(input_dim),
22
+ nn.Linear(input_dim, num_heads, bias=False),
23
+ )
24
+ for _ in range(depth)
25
+ ]
26
+ )
27
+
28
+
29
+ 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):
37
+ """Prepare conditioned atom features and per-layer attention biases."""
38
+
39
+ def __init__(
40
+ self,
41
+ token_s: int,
42
+ token_z: int,
43
+ atom_s: int,
44
+ atom_z: int,
45
+ atoms_per_window_queries: int = 32,
46
+ atoms_per_window_keys: int = 128,
47
+ atom_encoder_depth: int = 3,
48
+ atom_encoder_heads: int = 4,
49
+ token_transformer_depth: int = 24,
50
+ token_transformer_heads: int = 8,
51
+ atom_decoder_depth: int = 3,
52
+ atom_decoder_heads: int = 4,
53
+ atom_feature_dim: int = 128,
54
+ conditioning_transition_layers: int = 2,
55
+ use_no_atom_char: bool = False,
56
+ use_atom_backbone_feat: bool = False,
57
+ use_residue_feats_atoms: bool = False,
58
+ ) -> None:
59
+ super().__init__()
60
+ self.pairwise_conditioner = PairwiseConditioning(
61
+ token_z=token_z,
62
+ dim_token_rel_pos_feats=token_z,
63
+ num_transitions=conditioning_transition_layers,
64
+ )
65
+ self.atom_encoder = AtomEncoder(
66
+ atom_s=atom_s,
67
+ atom_z=atom_z,
68
+ token_s=token_s,
69
+ token_z=token_z,
70
+ atoms_per_window_queries=atoms_per_window_queries,
71
+ atoms_per_window_keys=atoms_per_window_keys,
72
+ atom_feature_dim=atom_feature_dim,
73
+ structure_prediction=True,
74
+ use_no_atom_char=use_no_atom_char,
75
+ use_atom_backbone_feat=use_atom_backbone_feat,
76
+ use_residue_feats_atoms=use_residue_feats_atoms,
77
+ )
78
+ self.atom_enc_proj_z = _bias_projections(
79
+ atom_encoder_depth,
80
+ atom_z,
81
+ atom_encoder_heads,
82
+ )
83
+ self.atom_dec_proj_z = _bias_projections(
84
+ atom_decoder_depth,
85
+ atom_z,
86
+ atom_decoder_heads,
87
+ )
88
+ self.token_trans_proj_z = _bias_projections(
89
+ token_transformer_depth,
90
+ token_z,
91
+ token_transformer_heads,
92
+ )
93
+
94
+ def forward(
95
+ self,
96
+ s_trunk: torch.Tensor,
97
+ z_trunk: torch.Tensor,
98
+ relative_position_encoding: torch.Tensor,
99
+ feats: dict[str, torch.Tensor],
100
+ ) -> tuple[
101
+ torch.Tensor,
102
+ torch.Tensor,
103
+ torch.Tensor,
104
+ torch.Tensor,
105
+ torch.Tensor,
106
+ torch.Tensor,
107
+ ]:
108
+ """Return conditioned atom tensors and concatenated layer biases.
109
+
110
+ ``S`` has shape ``(b, n, d_s)`` and each ``Z`` tensor has shape
111
+ ``(b, n, n, d_z)``. Biases are concatenated in transformer-block
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,
133
+ to_keys,
134
+ atom_encoder_bias,
135
+ atom_decoder_bias,
136
+ token_transformer_bias,
137
+ )
fastplms/models/boltz/vb_modules_diffusionv2.py ADDED
@@ -0,0 +1,654 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Based on https://github.com/lucidrains/alphafold3-pytorch.
2
+ # MIT License, Copyright (c) 2024 Phil Wang.
3
+
4
+ from __future__ import annotations
5
+
6
+ from math import sqrt
7
+
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn.functional as F
11
+ from einops import rearrange
12
+ from torch import nn
13
+ from torch.nn import Module
14
+
15
+ from . import vb_const as const
16
+ from . import vb_layers_initialize as init
17
+ from .vb_loss_diffusionv2 import (
18
+ smooth_lddt_loss,
19
+ weighted_rigid_align,
20
+ )
21
+ from .vb_modules_encodersv2 import (
22
+ AtomAttentionDecoder,
23
+ AtomAttentionEncoder,
24
+ SingleConditioning,
25
+ )
26
+ from .vb_modules_transformersv2 import (
27
+ DiffusionTransformer,
28
+ )
29
+ from .vb_modules_utils import (
30
+ LinearNoBias,
31
+ center_random_augmentation,
32
+ compute_random_augmentation,
33
+ default,
34
+ log,
35
+ )
36
+ from .vb_potentials_potentials import get_potentials
37
+
38
+
39
+ class DiffusionModule(Module):
40
+ """Diffusion module"""
41
+
42
+ def __init__(
43
+ self,
44
+ token_s: int,
45
+ atom_s: int,
46
+ atoms_per_window_queries: int = 32,
47
+ atoms_per_window_keys: int = 128,
48
+ sigma_data: int = 16,
49
+ dim_fourier: int = 256,
50
+ atom_encoder_depth: int = 3,
51
+ atom_encoder_heads: int = 4,
52
+ token_transformer_depth: int = 24,
53
+ token_transformer_heads: int = 8,
54
+ atom_decoder_depth: int = 3,
55
+ atom_decoder_heads: int = 4,
56
+ conditioning_transition_layers: int = 2,
57
+ activation_checkpointing: bool = False,
58
+ transformer_post_ln: bool = False,
59
+ ) -> None:
60
+ super().__init__()
61
+
62
+ self.atoms_per_window_queries = atoms_per_window_queries
63
+ self.atoms_per_window_keys = atoms_per_window_keys
64
+ self.sigma_data = sigma_data
65
+ self.activation_checkpointing = activation_checkpointing
66
+
67
+ # conditioning
68
+ self.single_conditioner = SingleConditioning(
69
+ sigma_data=sigma_data,
70
+ token_s=token_s,
71
+ dim_fourier=dim_fourier,
72
+ num_transitions=conditioning_transition_layers,
73
+ )
74
+
75
+ self.atom_attention_encoder = AtomAttentionEncoder(
76
+ atom_s=atom_s,
77
+ token_s=token_s,
78
+ atoms_per_window_queries=atoms_per_window_queries,
79
+ atoms_per_window_keys=atoms_per_window_keys,
80
+ atom_encoder_depth=atom_encoder_depth,
81
+ atom_encoder_heads=atom_encoder_heads,
82
+ structure_prediction=True,
83
+ activation_checkpointing=activation_checkpointing,
84
+ transformer_post_layer_norm=transformer_post_ln,
85
+ )
86
+
87
+ self.s_to_a_linear = nn.Sequential(
88
+ nn.LayerNorm(2 * token_s), LinearNoBias(2 * token_s, 2 * token_s)
89
+ )
90
+ init.final_init_(self.s_to_a_linear[1].weight)
91
+
92
+ self.token_transformer = DiffusionTransformer(
93
+ dim=2 * token_s,
94
+ dim_single_cond=2 * token_s,
95
+ depth=token_transformer_depth,
96
+ heads=token_transformer_heads,
97
+ activation_checkpointing=activation_checkpointing,
98
+ # post_layer_norm=transformer_post_ln,
99
+ )
100
+
101
+ self.a_norm = nn.LayerNorm(2 * token_s) # if not transformer_post_ln else nn.Identity()
102
+
103
+ self.atom_attention_decoder = AtomAttentionDecoder(
104
+ atom_s=atom_s,
105
+ token_s=token_s,
106
+ attn_window_queries=atoms_per_window_queries,
107
+ attn_window_keys=atoms_per_window_keys,
108
+ atom_decoder_depth=atom_decoder_depth,
109
+ atom_decoder_heads=atom_decoder_heads,
110
+ activation_checkpointing=activation_checkpointing,
111
+ # transformer_post_layer_norm=transformer_post_ln,
112
+ )
113
+
114
+ def forward(
115
+ self,
116
+ s_inputs, # Float['b n ts']
117
+ s_trunk, # Float['b n ts']
118
+ r_noisy, # Float['bm m 3']
119
+ times, # Float['bm 1 1']
120
+ feats,
121
+ diffusion_conditioning,
122
+ multiplicity=1,
123
+ ):
124
+ if self.activation_checkpointing:
125
+ s, _normed_fourier = torch.utils.checkpoint.checkpoint(
126
+ self.single_conditioner,
127
+ times,
128
+ s_trunk.repeat_interleave(multiplicity, 0),
129
+ s_inputs.repeat_interleave(multiplicity, 0),
130
+ use_reentrant=False,
131
+ )
132
+ else:
133
+ s, _normed_fourier = self.single_conditioner(
134
+ times,
135
+ s_trunk.repeat_interleave(multiplicity, 0),
136
+ s_inputs.repeat_interleave(multiplicity, 0),
137
+ )
138
+
139
+ # Sequence-local Atom Attention and aggregation to coarse-grained tokens
140
+ a, q_skip, c_skip, to_keys = self.atom_attention_encoder(
141
+ feats=feats,
142
+ q=diffusion_conditioning["q"].float(),
143
+ c=diffusion_conditioning["c"].float(),
144
+ atom_enc_bias=diffusion_conditioning["atom_enc_bias"].float(),
145
+ to_keys=diffusion_conditioning["to_keys"],
146
+ r=r_noisy, # Float['b m 3'],
147
+ multiplicity=multiplicity,
148
+ )
149
+
150
+ # Full self-attention on token level
151
+ a = a + self.s_to_a_linear(s)
152
+
153
+ mask = feats["token_pad_mask"].repeat_interleave(multiplicity, 0)
154
+ a = self.token_transformer(
155
+ a,
156
+ mask=mask.float(),
157
+ s=s,
158
+ bias=diffusion_conditioning[
159
+ "token_trans_bias"
160
+ ].float(), # note z is not expanded with multiplicity until after bias is computed
161
+ multiplicity=multiplicity,
162
+ )
163
+ a = self.a_norm(a)
164
+
165
+ # Broadcast token activations to atoms and run Sequence-local Atom Attention
166
+ r_update = self.atom_attention_decoder(
167
+ a=a,
168
+ q=q_skip,
169
+ c=c_skip,
170
+ atom_dec_bias=diffusion_conditioning["atom_dec_bias"].float(),
171
+ feats=feats,
172
+ multiplicity=multiplicity,
173
+ to_keys=to_keys,
174
+ )
175
+
176
+ return r_update
177
+
178
+
179
+ class AtomDiffusion(Module):
180
+ def __init__(
181
+ self,
182
+ score_model_args,
183
+ num_sampling_steps: int = 5, # number of sampling steps
184
+ sigma_min: float = 0.0004, # min noise level
185
+ sigma_max: float = 160.0, # max noise level
186
+ sigma_data: float = 16.0, # standard deviation of data distribution
187
+ rho: float = 7, # controls the sampling schedule
188
+ # Mean of the log-normal training-noise distribution.
189
+ P_mean: float = -1.2,
190
+ # Standard deviation of the log-normal training-noise distribution.
191
+ P_std: float = 1.5,
192
+ gamma_0: float = 0.8,
193
+ gamma_min: float = 1.0,
194
+ noise_scale: float = 1.003,
195
+ step_scale: float = 1.5,
196
+ step_scale_random: list | None = None,
197
+ coordinate_augmentation: bool = True,
198
+ coordinate_augmentation_inference=None,
199
+ compile_score: bool = False,
200
+ alignment_reverse_diff: bool = False,
201
+ synchronize_sigmas: bool = False,
202
+ ):
203
+ super().__init__()
204
+ self.score_model = DiffusionModule(
205
+ **score_model_args,
206
+ )
207
+ if compile_score:
208
+ self.score_model = torch.compile(self.score_model, dynamic=False, fullgraph=False)
209
+
210
+ # parameters
211
+ self.sigma_min = sigma_min
212
+ self.sigma_max = sigma_max
213
+ self.sigma_data = sigma_data
214
+ self.rho = rho
215
+ self.P_mean = P_mean
216
+ self.P_std = P_std
217
+ self.num_sampling_steps = num_sampling_steps
218
+ self.gamma_0 = gamma_0
219
+ self.gamma_min = gamma_min
220
+ self.noise_scale = noise_scale
221
+ self.step_scale = step_scale
222
+ self.step_scale_random = step_scale_random
223
+ self.coordinate_augmentation = coordinate_augmentation
224
+ self.coordinate_augmentation_inference = (
225
+ coordinate_augmentation_inference
226
+ if coordinate_augmentation_inference is not None
227
+ else coordinate_augmentation
228
+ )
229
+ self.alignment_reverse_diff = alignment_reverse_diff
230
+ self.synchronize_sigmas = synchronize_sigmas
231
+
232
+ self.token_s = score_model_args["token_s"]
233
+ self.register_buffer("zero", torch.tensor(0.0), persistent=False)
234
+
235
+ @property
236
+ def device(self):
237
+ return next(self.score_model.parameters()).device
238
+
239
+ def c_skip(self, sigma):
240
+ return (self.sigma_data**2) / (sigma**2 + self.sigma_data**2)
241
+
242
+ def c_out(self, sigma):
243
+ return sigma * self.sigma_data / torch.sqrt(self.sigma_data**2 + sigma**2)
244
+
245
+ def c_in(self, sigma):
246
+ return 1 / torch.sqrt(sigma**2 + self.sigma_data**2)
247
+
248
+ def c_noise(self, sigma):
249
+ return log(sigma / self.sigma_data) * 0.25
250
+
251
+ def preconditioned_network_forward(
252
+ self,
253
+ noised_atom_coords, #: Float['b m 3'],
254
+ sigma, #: Float['b'] | Float[' '] | float,
255
+ network_condition_kwargs: dict,
256
+ ):
257
+ batch, device = noised_atom_coords.shape[0], noised_atom_coords.device
258
+
259
+ if isinstance(sigma, float):
260
+ sigma = torch.full((batch,), sigma, device=device)
261
+
262
+ padded_sigma = rearrange(sigma, "b -> b 1 1")
263
+
264
+ r_update = self.score_model(
265
+ r_noisy=self.c_in(padded_sigma) * noised_atom_coords,
266
+ times=self.c_noise(sigma),
267
+ **network_condition_kwargs,
268
+ )
269
+
270
+ denoised_coords = (
271
+ self.c_skip(padded_sigma) * noised_atom_coords + self.c_out(padded_sigma) * r_update
272
+ )
273
+ return denoised_coords
274
+
275
+ def sample_schedule(self, num_sampling_steps=None):
276
+ num_sampling_steps = default(num_sampling_steps, self.num_sampling_steps)
277
+ inv_rho = 1 / self.rho
278
+
279
+ steps = torch.arange(num_sampling_steps, device=self.device, dtype=torch.float32)
280
+ sigmas = (
281
+ self.sigma_max**inv_rho
282
+ + steps / (num_sampling_steps - 1) * (self.sigma_min**inv_rho - self.sigma_max**inv_rho)
283
+ ) ** self.rho
284
+
285
+ sigmas = sigmas * self.sigma_data
286
+
287
+ sigmas = F.pad(sigmas, (0, 1), value=0.0) # last step is sigma value of 0.
288
+ return sigmas
289
+
290
+ def sample(
291
+ self,
292
+ atom_mask,
293
+ num_sampling_steps=None,
294
+ multiplicity=1,
295
+ max_parallel_samples=None,
296
+ steering_args=None,
297
+ **network_condition_kwargs,
298
+ ):
299
+ if steering_args is not None and (
300
+ steering_args["fk_steering"]
301
+ or steering_args["physical_guidance_update"]
302
+ or steering_args["contact_guidance_update"]
303
+ ):
304
+ potentials = get_potentials(steering_args, boltz2=True)
305
+
306
+ if steering_args["fk_steering"]:
307
+ multiplicity = multiplicity * steering_args["num_particles"]
308
+ energy_traj = torch.empty((multiplicity, 0), device=self.device)
309
+ resample_weights = torch.ones(multiplicity, device=self.device).reshape(
310
+ -1, steering_args["num_particles"]
311
+ )
312
+ if steering_args["physical_guidance_update"] or steering_args["contact_guidance_update"]:
313
+ scaled_guidance_update = torch.zeros(
314
+ (multiplicity, *atom_mask.shape[1:], 3),
315
+ dtype=torch.float32,
316
+ device=self.device,
317
+ )
318
+ if max_parallel_samples is None:
319
+ max_parallel_samples = multiplicity
320
+
321
+ num_sampling_steps = default(num_sampling_steps, self.num_sampling_steps)
322
+ atom_mask = atom_mask.repeat_interleave(multiplicity, 0)
323
+
324
+ shape = (*atom_mask.shape, 3)
325
+
326
+ # Pair each schedule value with the next sigma and gamma.
327
+ sigmas = self.sample_schedule(num_sampling_steps)
328
+ gammas = torch.where(sigmas > self.gamma_min, self.gamma_0, 0.0)
329
+ sigmas_and_gammas = list(zip(sigmas[:-1], sigmas[1:], gammas[1:], strict=True))
330
+ if self.training and self.step_scale_random is not None:
331
+ step_scale = np.random.choice(self.step_scale_random)
332
+ else:
333
+ step_scale = self.step_scale
334
+
335
+ # atom position is noise at the beginning
336
+ init_sigma = sigmas[0]
337
+ atom_coords = init_sigma * torch.randn(shape, device=self.device)
338
+ token_repr = None
339
+ atom_coords_denoised = None
340
+
341
+ # gradually denoise
342
+ for step_idx, (sigma_tm, sigma_t, gamma) in enumerate(sigmas_and_gammas):
343
+ random_R, random_tr = compute_random_augmentation(
344
+ multiplicity, device=atom_coords.device, dtype=atom_coords.dtype
345
+ )
346
+ atom_coords = atom_coords - atom_coords.mean(dim=-2, keepdims=True)
347
+ atom_coords = torch.einsum("bmd,bds->bms", atom_coords, random_R) + random_tr
348
+ if atom_coords_denoised is not None:
349
+ atom_coords_denoised -= atom_coords_denoised.mean(dim=-2, keepdims=True)
350
+ atom_coords_denoised = (
351
+ torch.einsum("bmd,bds->bms", atom_coords_denoised, random_R) + random_tr
352
+ )
353
+ if (
354
+ steering_args["physical_guidance_update"]
355
+ or steering_args["contact_guidance_update"]
356
+ ) and scaled_guidance_update is not None:
357
+ scaled_guidance_update = torch.einsum(
358
+ "bmd,bds->bms", scaled_guidance_update, random_R
359
+ )
360
+
361
+ sigma_tm, sigma_t, gamma = sigma_tm.item(), sigma_t.item(), gamma.item()
362
+
363
+ t_hat = sigma_tm * (1 + gamma)
364
+ steering_t = 1.0 - (step_idx / num_sampling_steps)
365
+ noise_var = self.noise_scale**2 * (t_hat**2 - sigma_tm**2)
366
+ eps = sqrt(noise_var) * torch.randn(shape, device=self.device)
367
+ atom_coords_noisy = atom_coords + eps
368
+
369
+ with torch.no_grad():
370
+ atom_coords_denoised = torch.zeros_like(atom_coords_noisy)
371
+ sample_ids = torch.arange(multiplicity).to(atom_coords_noisy.device)
372
+ sample_ids_chunks = sample_ids.chunk(multiplicity % max_parallel_samples + 1)
373
+
374
+ for sample_ids_chunk in sample_ids_chunks:
375
+ atom_coords_denoised_chunk = self.preconditioned_network_forward(
376
+ atom_coords_noisy[sample_ids_chunk],
377
+ t_hat,
378
+ network_condition_kwargs=dict(
379
+ multiplicity=sample_ids_chunk.numel(),
380
+ **network_condition_kwargs,
381
+ ),
382
+ )
383
+ atom_coords_denoised[sample_ids_chunk] = atom_coords_denoised_chunk
384
+
385
+ if steering_args["fk_steering"] and (
386
+ (step_idx % steering_args["fk_resampling_interval"] == 0 and noise_var > 0)
387
+ or step_idx == num_sampling_steps - 1
388
+ ):
389
+ # Compute energy of x_0 prediction
390
+ energy = torch.zeros(multiplicity, device=self.device)
391
+ for potential in potentials:
392
+ parameters = potential.compute_parameters(steering_t)
393
+ if parameters["resampling_weight"] > 0:
394
+ component_energy = potential.compute(
395
+ atom_coords_denoised,
396
+ network_condition_kwargs["feats"],
397
+ parameters,
398
+ )
399
+ energy += parameters["resampling_weight"] * component_energy
400
+ energy_traj = torch.cat((energy_traj, energy.unsqueeze(1)), dim=1)
401
+
402
+ # Compute log G values
403
+ if step_idx == 0:
404
+ log_G = -1 * energy
405
+ else:
406
+ log_G = energy_traj[:, -2] - energy_traj[:, -1]
407
+
408
+ # Compute ll difference between guided and unguided transition distribution
409
+ if (
410
+ steering_args["physical_guidance_update"]
411
+ or steering_args["contact_guidance_update"]
412
+ ) and noise_var > 0:
413
+ ll_difference = (eps**2 - (eps + scaled_guidance_update) ** 2).sum(
414
+ dim=(-1, -2)
415
+ ) / (2 * noise_var)
416
+ else:
417
+ ll_difference = torch.zeros_like(energy)
418
+
419
+ # Compute resampling weights
420
+ resample_weights = F.softmax(
421
+ (ll_difference + steering_args["fk_lambda"] * log_G).reshape(
422
+ -1, steering_args["num_particles"]
423
+ ),
424
+ dim=1,
425
+ )
426
+
427
+ # Compute guidance update to x_0 prediction
428
+ if (
429
+ steering_args["physical_guidance_update"]
430
+ or steering_args["contact_guidance_update"]
431
+ ) and step_idx < num_sampling_steps - 1:
432
+ guidance_update = torch.zeros_like(atom_coords_denoised)
433
+ for guidance_step in range(steering_args["num_gd_steps"]):
434
+ energy_gradient = torch.zeros_like(atom_coords_denoised)
435
+ for potential in potentials:
436
+ parameters = potential.compute_parameters(steering_t)
437
+ if (
438
+ parameters["guidance_weight"] > 0
439
+ and (guidance_step) % parameters["guidance_interval"] == 0
440
+ ):
441
+ energy_gradient += parameters[
442
+ "guidance_weight"
443
+ ] * potential.compute_gradient(
444
+ atom_coords_denoised + guidance_update,
445
+ network_condition_kwargs["feats"],
446
+ parameters,
447
+ )
448
+ guidance_update -= energy_gradient
449
+ atom_coords_denoised += guidance_update
450
+ scaled_guidance_update = (
451
+ guidance_update * -1 * self.step_scale * (sigma_t - t_hat) / t_hat
452
+ )
453
+
454
+ if steering_args["fk_steering"] and (
455
+ (step_idx % steering_args["fk_resampling_interval"] == 0 and noise_var > 0)
456
+ or step_idx == num_sampling_steps - 1
457
+ ):
458
+ resample_indices = (
459
+ torch.multinomial(
460
+ resample_weights,
461
+ resample_weights.shape[1] if step_idx < num_sampling_steps - 1 else 1,
462
+ replacement=True,
463
+ )
464
+ + resample_weights.shape[1]
465
+ * torch.arange(
466
+ resample_weights.shape[0], device=resample_weights.device
467
+ ).unsqueeze(-1)
468
+ ).flatten()
469
+
470
+ atom_coords = atom_coords[resample_indices]
471
+ atom_coords_noisy = atom_coords_noisy[resample_indices]
472
+ atom_mask = atom_mask[resample_indices]
473
+ if atom_coords_denoised is not None:
474
+ atom_coords_denoised = atom_coords_denoised[resample_indices]
475
+ energy_traj = energy_traj[resample_indices]
476
+ if (
477
+ steering_args["physical_guidance_update"]
478
+ or steering_args["contact_guidance_update"]
479
+ ):
480
+ scaled_guidance_update = scaled_guidance_update[resample_indices]
481
+ if token_repr is not None:
482
+ token_repr = token_repr[resample_indices]
483
+
484
+ if self.alignment_reverse_diff:
485
+ with torch.autocast("cuda", enabled=False):
486
+ atom_coords_noisy = weighted_rigid_align(
487
+ atom_coords_noisy.float(),
488
+ atom_coords_denoised.float(),
489
+ atom_mask.float(),
490
+ atom_mask.float(),
491
+ )
492
+
493
+ atom_coords_noisy = atom_coords_noisy.to(atom_coords_denoised)
494
+
495
+ denoised_over_sigma = (atom_coords_noisy - atom_coords_denoised) / t_hat
496
+ atom_coords_next = (
497
+ atom_coords_noisy + step_scale * (sigma_t - t_hat) * denoised_over_sigma
498
+ )
499
+
500
+ atom_coords = atom_coords_next
501
+
502
+ return dict(sample_atom_coords=atom_coords, diff_token_repr=token_repr)
503
+
504
+ def loss_weight(self, sigma):
505
+ return (sigma**2 + self.sigma_data**2) / ((sigma * self.sigma_data) ** 2)
506
+
507
+ def noise_distribution(self, batch_size):
508
+ return (
509
+ self.sigma_data
510
+ * (self.P_mean + self.P_std * torch.randn((batch_size,), device=self.device)).exp()
511
+ )
512
+
513
+ def forward(
514
+ self,
515
+ s_inputs,
516
+ s_trunk,
517
+ feats,
518
+ diffusion_conditioning,
519
+ multiplicity=1,
520
+ ):
521
+ # training diffusion step
522
+ batch_size = feats["coords"].shape[0] // multiplicity
523
+
524
+ if self.synchronize_sigmas:
525
+ sigmas = self.noise_distribution(batch_size).repeat_interleave(multiplicity, 0)
526
+ else:
527
+ sigmas = self.noise_distribution(batch_size * multiplicity)
528
+ padded_sigmas = rearrange(sigmas, "b -> b 1 1")
529
+
530
+ atom_coords = feats["coords"]
531
+
532
+ atom_mask = feats["atom_pad_mask"]
533
+ atom_mask = atom_mask.repeat_interleave(multiplicity, 0)
534
+
535
+ atom_coords = center_random_augmentation(
536
+ atom_coords, atom_mask, augmentation=self.coordinate_augmentation
537
+ )
538
+
539
+ noise = torch.randn_like(atom_coords)
540
+ noised_atom_coords = atom_coords + padded_sigmas * noise
541
+
542
+ denoised_atom_coords = self.preconditioned_network_forward(
543
+ noised_atom_coords,
544
+ sigmas,
545
+ network_condition_kwargs={
546
+ "s_inputs": s_inputs,
547
+ "s_trunk": s_trunk,
548
+ "feats": feats,
549
+ "multiplicity": multiplicity,
550
+ "diffusion_conditioning": diffusion_conditioning,
551
+ },
552
+ )
553
+
554
+ return {
555
+ "denoised_atom_coords": denoised_atom_coords,
556
+ "sigmas": sigmas,
557
+ "aligned_true_atom_coords": atom_coords,
558
+ }
559
+
560
+ def compute_loss(
561
+ self,
562
+ feats,
563
+ out_dict,
564
+ add_smooth_lddt_loss=True,
565
+ nucleotide_loss_weight=5.0,
566
+ ligand_loss_weight=10.0,
567
+ multiplicity=1,
568
+ filter_by_plddt=0.0,
569
+ ):
570
+ with torch.autocast("cuda", enabled=False):
571
+ denoised_atom_coords = out_dict["denoised_atom_coords"].float()
572
+ sigmas = out_dict["sigmas"].float()
573
+
574
+ resolved_atom_mask_uni = feats["atom_resolved_mask"].float()
575
+
576
+ if filter_by_plddt > 0:
577
+ plddt_mask = feats["plddt"] > filter_by_plddt
578
+ resolved_atom_mask_uni = resolved_atom_mask_uni * plddt_mask.float()
579
+
580
+ resolved_atom_mask = resolved_atom_mask_uni.repeat_interleave(multiplicity, 0)
581
+
582
+ align_weights = denoised_atom_coords.new_ones(denoised_atom_coords.shape[:2])
583
+ atom_type = (
584
+ torch.bmm(
585
+ feats["atom_to_token"].float(),
586
+ feats["mol_type"].unsqueeze(-1).float(),
587
+ )
588
+ .squeeze(-1)
589
+ .long()
590
+ )
591
+ atom_type_mult = atom_type.repeat_interleave(multiplicity, 0)
592
+
593
+ align_weights = (
594
+ align_weights
595
+ * (
596
+ 1
597
+ + nucleotide_loss_weight
598
+ * (
599
+ torch.eq(atom_type_mult, const.chain_type_ids["DNA"]).float()
600
+ + torch.eq(atom_type_mult, const.chain_type_ids["RNA"]).float()
601
+ )
602
+ + ligand_loss_weight
603
+ * torch.eq(atom_type_mult, const.chain_type_ids["NONPOLYMER"]).float()
604
+ ).float()
605
+ )
606
+
607
+ atom_coords = out_dict["aligned_true_atom_coords"].float()
608
+ atom_coords_aligned_ground_truth = weighted_rigid_align(
609
+ atom_coords.detach(),
610
+ denoised_atom_coords.detach(),
611
+ align_weights.detach(),
612
+ mask=feats["atom_resolved_mask"]
613
+ .float()
614
+ .repeat_interleave(multiplicity, 0)
615
+ .detach(),
616
+ )
617
+
618
+ # Cast back
619
+ atom_coords_aligned_ground_truth = atom_coords_aligned_ground_truth.to(
620
+ denoised_atom_coords
621
+ )
622
+
623
+ # weighted MSE loss of denoised atom positions
624
+ mse_loss = ((denoised_atom_coords - atom_coords_aligned_ground_truth) ** 2).sum(dim=-1)
625
+ mse_loss = torch.sum(mse_loss * align_weights * resolved_atom_mask, dim=-1) / (
626
+ torch.sum(3 * align_weights * resolved_atom_mask, dim=-1) + 1e-5
627
+ )
628
+
629
+ # weight by sigma factor
630
+ loss_weights = self.loss_weight(sigmas)
631
+ mse_loss = (mse_loss * loss_weights).mean()
632
+
633
+ total_loss = mse_loss
634
+
635
+ # proposed auxiliary smooth lddt loss
636
+ lddt_loss = self.zero
637
+ if add_smooth_lddt_loss:
638
+ lddt_loss = smooth_lddt_loss(
639
+ denoised_atom_coords,
640
+ feats["coords"],
641
+ torch.eq(atom_type, const.chain_type_ids["DNA"]).float()
642
+ + torch.eq(atom_type, const.chain_type_ids["RNA"]).float(),
643
+ coords_mask=resolved_atom_mask_uni,
644
+ multiplicity=multiplicity,
645
+ )
646
+
647
+ total_loss = total_loss + lddt_loss
648
+
649
+ loss_breakdown = {
650
+ "mse_loss": mse_loss,
651
+ "smooth_lddt_loss": lddt_loss,
652
+ }
653
+
654
+ return {"loss": total_loss, "loss_breakdown": loss_breakdown}
fastplms/models/boltz/vb_modules_encodersv2.py ADDED
@@ -0,0 +1,577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Token, pair, and atom encoders used by the local Boltz2 runtime.
2
+
3
+ The classes retain checkpoint-facing submodule names while separating feature
4
+ assembly, window indexing, conditioning, and atom-token aggregation into small
5
+ mechanism-specific units. No production import depends on the upstream Boltz
6
+ package.
7
+
8
+ The Fourier and atom-attention mechanisms derive from AlphaFold 3 community
9
+ implementations under MIT terms. See ``THIRD_PARTY_NOTICES.md``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ 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
+
22
+ from . import vb_layers_initialize as init
23
+ from .vb_layers_transition import Transition
24
+ from .vb_modules_transformersv2 import AtomTransformer
25
+ from .vb_modules_utils import LinearNoBias
26
+
27
+
28
+ def _transition_stack(
29
+ count: int,
30
+ dim: int,
31
+ hidden_dim: int,
32
+ ) -> nn.ModuleList:
33
+ return nn.ModuleList([Transition(dim=dim, hidden=hidden_dim) for _ in range(count)])
34
+
35
+
36
+ class FourierEmbedding(nn.Module):
37
+ """Embed diffusion time with fixed random Fourier frequencies."""
38
+
39
+ def __init__(self, dim: int) -> None:
40
+ super().__init__()
41
+ self.proj = nn.Linear(1, dim)
42
+ nn.init.normal_(self.proj.weight, mean=0, std=1)
43
+ nn.init.normal_(self.proj.bias, mean=0, std=1)
44
+ self.proj.requires_grad_(False)
45
+
46
+ def forward(self, times: torch.Tensor) -> torch.Tensor:
47
+ """Map times with shape ``(b,)`` to an embedding ``H: (b, d)``."""
48
+
49
+ random_phase = self.proj(times.reshape(-1, 1))
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):
58
+ """Encode residue, token, entity, and symmetry relationships."""
59
+
60
+ def __init__(
61
+ self,
62
+ token_z: int,
63
+ r_max: int = 32,
64
+ s_max: int = 2,
65
+ fix_sym_check: bool = False,
66
+ cyclic_pos_enc: bool = False,
67
+ ) -> None:
68
+ super().__init__()
69
+ self.r_max = r_max
70
+ self.s_max = s_max
71
+ input_dim = 4 * (r_max + 1) + 2 * (s_max + 1) + 1
72
+ self.linear_layer = LinearNoBias(input_dim, token_z)
73
+ self.fix_sym_check = fix_sym_check
74
+ self.cyclic_pos_enc = cyclic_pos_enc
75
+
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
+ (
130
+ residue_features.float(),
131
+ token_features.float(),
132
+ same_entity.unsqueeze(-1).float(),
133
+ symmetry_features.float(),
134
+ ),
135
+ dim=-1,
136
+ )
137
+ return self.linear_layer(pair_features)
138
+
139
+
140
+ class SingleConditioning(nn.Module):
141
+ """Condition token features on trunk inputs and diffusion time."""
142
+
143
+ def __init__(
144
+ self,
145
+ sigma_data: float,
146
+ token_s: int = 384,
147
+ dim_fourier: int = 256,
148
+ num_transitions: int = 2,
149
+ transition_expansion_factor: int = 2,
150
+ eps: float = 1e-20,
151
+ disable_times: bool = False,
152
+ ) -> None:
153
+ super().__init__()
154
+ self.eps = eps
155
+ self.sigma_data = sigma_data
156
+ self.disable_times = disable_times
157
+ conditioning_dim = 2 * token_s
158
+ self.norm_single = nn.LayerNorm(conditioning_dim)
159
+ self.single_embed = nn.Linear(conditioning_dim, conditioning_dim)
160
+ if not disable_times:
161
+ self.fourier_embed = FourierEmbedding(dim_fourier)
162
+ self.norm_fourier = nn.LayerNorm(dim_fourier)
163
+ self.fourier_to_single = LinearNoBias(dim_fourier, conditioning_dim)
164
+ self.transitions = _transition_stack(
165
+ num_transitions,
166
+ conditioning_dim,
167
+ transition_expansion_factor * conditioning_dim,
168
+ )
169
+
170
+ def forward(
171
+ self,
172
+ times: torch.Tensor,
173
+ s_trunk: torch.Tensor,
174
+ s_inputs: torch.Tensor,
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):
192
+ """Fuse trunk pair features with relative-position features."""
193
+
194
+ def __init__(
195
+ self,
196
+ token_z: int,
197
+ dim_token_rel_pos_feats: int,
198
+ num_transitions: int = 2,
199
+ transition_expansion_factor: int = 2,
200
+ ) -> None:
201
+ super().__init__()
202
+ combined_dim = token_z + dim_token_rel_pos_feats
203
+ self.dim_pairwise_init_proj = nn.Sequential(
204
+ nn.LayerNorm(combined_dim),
205
+ LinearNoBias(combined_dim, token_z),
206
+ )
207
+ self.transitions = _transition_stack(
208
+ num_transitions,
209
+ token_z,
210
+ transition_expansion_factor * token_z,
211
+ )
212
+
213
+ def forward(
214
+ self,
215
+ z_trunk: torch.Tensor,
216
+ token_rel_pos_feats: torch.Tensor,
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(
227
+ k: int,
228
+ w: int,
229
+ h: int,
230
+ device: torch.device,
231
+ ) -> torch.Tensor:
232
+ """Build the atom-window gather matrix used for local attention keys."""
233
+
234
+ for name, value in (("k", k), ("w", w), ("h", h)):
235
+ if isinstance(value, bool) or not isinstance(value, int):
236
+ raise TypeError(f"{name} must be an int, got {type(value).__name__}.")
237
+ if value <= 0:
238
+ raise ValueError(f"{name} must be positive, got {value}.")
239
+ if w % 2 != 0:
240
+ raise ValueError(f"w must be even, got {w}.")
241
+ half_window = w // 2
242
+ if h % half_window != 0:
243
+ raise ValueError(f"h must be divisible by w // 2 ({half_window}), got {h}.")
244
+ key_blocks = h // half_window
245
+ if key_blocks % 2 != 0:
246
+ raise ValueError(
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(
260
+ single: torch.Tensor,
261
+ indexing_matrix: torch.Tensor,
262
+ w: int,
263
+ h: int,
264
+ ) -> torch.Tensor:
265
+ """Gather a sequence tensor into overlapping local key windows."""
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):
275
+ """Encode reference atoms and local atom-pair geometry."""
276
+
277
+ def __init__(
278
+ self,
279
+ atom_s: int,
280
+ atom_z: int,
281
+ token_s: int,
282
+ token_z: int,
283
+ atoms_per_window_queries: int,
284
+ atoms_per_window_keys: int,
285
+ atom_feature_dim: int,
286
+ structure_prediction: bool = True,
287
+ use_no_atom_char: bool = False,
288
+ use_atom_backbone_feat: bool = False,
289
+ use_residue_feats_atoms: bool = False,
290
+ ) -> None:
291
+ super().__init__()
292
+ self.embed_atom_features = nn.Linear(atom_feature_dim, atom_s)
293
+ self.embed_atompair_ref_pos = LinearNoBias(3, atom_z)
294
+ self.embed_atompair_ref_dist = LinearNoBias(1, atom_z)
295
+ self.embed_atompair_mask = LinearNoBias(1, atom_z)
296
+ self.atoms_per_window_queries = atoms_per_window_queries
297
+ self.atoms_per_window_keys = atoms_per_window_keys
298
+ self.use_no_atom_char = use_no_atom_char
299
+ self.use_atom_backbone_feat = use_atom_backbone_feat
300
+ self.use_residue_feats_atoms = use_residue_feats_atoms
301
+ self.structure_prediction = structure_prediction
302
+
303
+ if structure_prediction:
304
+ self.s_to_c_trans = nn.Sequential(
305
+ nn.LayerNorm(token_s),
306
+ LinearNoBias(token_s, atom_s),
307
+ )
308
+ init.final_init_(self.s_to_c_trans[1].weight)
309
+ self.z_to_p_trans = nn.Sequential(
310
+ nn.LayerNorm(token_z),
311
+ LinearNoBias(token_z, atom_z),
312
+ )
313
+ init.final_init_(self.z_to_p_trans[1].weight)
314
+
315
+ self.c_to_p_trans_k = nn.Sequential(nn.ReLU(), LinearNoBias(atom_s, atom_z))
316
+ init.final_init_(self.c_to_p_trans_k[1].weight)
317
+ self.c_to_p_trans_q = nn.Sequential(nn.ReLU(), LinearNoBias(atom_s, atom_z))
318
+ init.final_init_(self.c_to_p_trans_q[1].weight)
319
+ self.p_mlp = nn.Sequential(
320
+ nn.ReLU(),
321
+ LinearNoBias(atom_z, atom_z),
322
+ nn.ReLU(),
323
+ LinearNoBias(atom_z, atom_z),
324
+ nn.ReLU(),
325
+ LinearNoBias(atom_z, atom_z),
326
+ )
327
+ init.final_init_(self.p_mlp[5].weight)
328
+
329
+ def _assemble_atom_features(
330
+ self,
331
+ feats: dict[str, torch.Tensor],
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:
343
+ feature_parts.append(feats["atom_backbone_feat"])
344
+ if self.use_residue_feats_atoms:
345
+ residue_features = torch.cat(
346
+ (
347
+ feats["res_type"],
348
+ feats["modified"].unsqueeze(-1),
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,
358
+ feats: dict[str, torch.Tensor],
359
+ s_trunk: torch.Tensor | None = None,
360
+ z: torch.Tensor | None = None,
361
+ ) -> tuple[
362
+ torch.Tensor,
363
+ torch.Tensor,
364
+ torch.Tensor,
365
+ Callable[[torch.Tensor], torch.Tensor],
366
+ ]:
367
+ """Return atom queries, conditioned singles, pairs, and key gatherer."""
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,
429
+ k,
430
+ 1,
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):
440
+ """Run local atom attention and aggregate atoms to tokens."""
441
+
442
+ def __init__(
443
+ self,
444
+ atom_s: int,
445
+ token_s: int,
446
+ atoms_per_window_queries: int,
447
+ atoms_per_window_keys: int,
448
+ atom_encoder_depth: int = 3,
449
+ atom_encoder_heads: int = 4,
450
+ structure_prediction: bool = True,
451
+ activation_checkpointing: bool = False,
452
+ transformer_post_layer_norm: bool = False,
453
+ ) -> None:
454
+ super().__init__()
455
+ self.structure_prediction = structure_prediction
456
+ if structure_prediction:
457
+ self.r_to_q_trans = LinearNoBias(3, atom_s)
458
+ init.final_init_(self.r_to_q_trans.weight)
459
+ self.atom_encoder = AtomTransformer(
460
+ dim=atom_s,
461
+ dim_single_cond=atom_s,
462
+ attn_window_queries=atoms_per_window_queries,
463
+ attn_window_keys=atoms_per_window_keys,
464
+ depth=atom_encoder_depth,
465
+ heads=atom_encoder_heads,
466
+ activation_checkpointing=activation_checkpointing,
467
+ post_layer_norm=transformer_post_layer_norm,
468
+ )
469
+ output_dim = 2 * token_s if structure_prediction else token_s
470
+ self.atom_to_token_trans = nn.Sequential(
471
+ LinearNoBias(atom_s, output_dim),
472
+ nn.ReLU(),
473
+ )
474
+
475
+ def forward(
476
+ self,
477
+ feats: dict[str, torch.Tensor],
478
+ q: torch.Tensor,
479
+ c: torch.Tensor,
480
+ atom_enc_bias: torch.Tensor,
481
+ to_keys: Callable[[torch.Tensor], torch.Tensor],
482
+ r: torch.Tensor | None = None,
483
+ multiplicity: int = 1,
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,
498
+ c=c,
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):
514
+ """Map token updates back to atoms and predict coordinate displacements."""
515
+
516
+ def __init__(
517
+ self,
518
+ atom_s: int,
519
+ token_s: int,
520
+ attn_window_queries: int,
521
+ attn_window_keys: int,
522
+ atom_decoder_depth: int = 3,
523
+ atom_decoder_heads: int = 4,
524
+ activation_checkpointing: bool = False,
525
+ transformer_post_layer_norm: bool = False,
526
+ ) -> None:
527
+ super().__init__()
528
+ self.a_to_q_trans = LinearNoBias(2 * token_s, atom_s)
529
+ init.final_init_(self.a_to_q_trans.weight)
530
+ self.atom_decoder = AtomTransformer(
531
+ dim=atom_s,
532
+ dim_single_cond=atom_s,
533
+ attn_window_queries=attn_window_queries,
534
+ attn_window_keys=attn_window_keys,
535
+ depth=atom_decoder_depth,
536
+ heads=atom_decoder_heads,
537
+ activation_checkpointing=activation_checkpointing,
538
+ post_layer_norm=transformer_post_layer_norm,
539
+ )
540
+ if transformer_post_layer_norm:
541
+ self.atom_feat_to_atom_pos_update = LinearNoBias(atom_s, 3)
542
+ init.final_init_(self.atom_feat_to_atom_pos_update.weight)
543
+ else:
544
+ self.atom_feat_to_atom_pos_update = nn.Sequential(
545
+ nn.LayerNorm(atom_s),
546
+ LinearNoBias(atom_s, 3),
547
+ )
548
+ init.final_init_(self.atom_feat_to_atom_pos_update[1].weight)
549
+
550
+ def forward(
551
+ self,
552
+ a: torch.Tensor,
553
+ q: torch.Tensor,
554
+ c: torch.Tensor,
555
+ atom_dec_bias: torch.Tensor,
556
+ feats: dict[str, torch.Tensor],
557
+ to_keys: Callable[[torch.Tensor], torch.Tensor],
558
+ multiplicity: int = 1,
559
+ ) -> torch.Tensor:
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,
572
+ c=c,
573
+ bias=atom_dec_bias,
574
+ multiplicity=multiplicity,
575
+ to_keys=to_keys,
576
+ )
577
+ return self.atom_feat_to_atom_pos_update(q)
fastplms/models/boltz/vb_modules_transformersv2.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Conditioned transformer blocks used by Boltz2 diffusion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any, cast
7
+
8
+ from torch import Tensor, nn
9
+ from torch.utils.checkpoint import checkpoint
10
+
11
+ from .vb_layers_attentionv2 import AttentionPairBias
12
+ from .vb_modules_utils import LinearNoBias, SwiGLU
13
+
14
+
15
+ class AdaLN(nn.Module):
16
+ """Normalize activations and apply scale and shift from conditioning S."""
17
+
18
+ def __init__(self, dim: int, dim_single_cond: int) -> None:
19
+ super().__init__()
20
+ self.a_norm = nn.LayerNorm(dim, elementwise_affine=False, bias=False)
21
+ self.s_norm = nn.LayerNorm(dim_single_cond, bias=False)
22
+ self.s_scale = nn.Linear(dim_single_cond, dim)
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):
33
+ """Apply a conditioned gated transition without changing tensor shape."""
34
+
35
+ def __init__(
36
+ self,
37
+ dim_single: int,
38
+ dim_single_cond: int,
39
+ expansion_factor: float = 2,
40
+ ) -> None:
41
+ super().__init__()
42
+ self.adaln = AdaLN(dim_single, dim_single_cond)
43
+ inner_dim = int(dim_single * expansion_factor)
44
+ self.swish_gate = nn.Sequential(
45
+ LinearNoBias(dim_single, inner_dim * 2),
46
+ SwiGLU(),
47
+ )
48
+ self.a_to_b = LinearNoBias(dim_single, inner_dim)
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
+
62
+ class DiffusionTransformer(nn.Module):
63
+ """Run conditioned transformer layers over token or atom states."""
64
+
65
+ def __init__(
66
+ self,
67
+ depth: int,
68
+ heads: int,
69
+ dim: int = 384,
70
+ dim_single_cond: int | None = None,
71
+ pair_bias_attn: bool = True,
72
+ activation_checkpointing: bool = False,
73
+ post_layer_norm: bool = False,
74
+ ) -> None:
75
+ super().__init__()
76
+ conditioning_dim = dim if dim_single_cond is None else dim_single_cond
77
+ self.activation_checkpointing = activation_checkpointing
78
+ self.pair_bias_attn = pair_bias_attn
79
+ self.layers = nn.ModuleList(
80
+ [
81
+ DiffusionTransformerLayer(
82
+ heads,
83
+ dim,
84
+ conditioning_dim,
85
+ post_layer_norm,
86
+ )
87
+ for _ in range(depth)
88
+ ]
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(
103
+ self,
104
+ a: Tensor,
105
+ s: Tensor,
106
+ bias: Tensor | None = None,
107
+ mask: Tensor | None = None,
108
+ to_keys: Callable[[Tensor], Tensor] | None = None,
109
+ multiplicity: int = 1,
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,
122
+ layer_bias,
123
+ mask,
124
+ to_keys,
125
+ multiplicity,
126
+ use_reentrant=False,
127
+ )
128
+ else:
129
+ output = layer(
130
+ output,
131
+ s,
132
+ layer_bias,
133
+ mask,
134
+ to_keys,
135
+ multiplicity,
136
+ )
137
+ return output
138
+
139
+
140
+ class DiffusionTransformerLayer(nn.Module):
141
+ """One adaptive-normalization attention and transition block."""
142
+
143
+ def __init__(
144
+ self,
145
+ heads: int,
146
+ dim: int = 384,
147
+ dim_single_cond: int | None = None,
148
+ post_layer_norm: bool = False,
149
+ ) -> None:
150
+ super().__init__()
151
+ conditioning_dim = dim if dim_single_cond is None else dim_single_cond
152
+ self.adaln = AdaLN(dim, conditioning_dim)
153
+ self.pair_bias_attn = AttentionPairBias(
154
+ c_s=dim,
155
+ num_heads=heads,
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(),
164
+ )
165
+ self.transition = ConditionedTransitionBlock(dim, conditioning_dim)
166
+ self.post_lnorm = nn.LayerNorm(dim) if post_layer_norm else nn.Identity()
167
+
168
+ def forward(
169
+ self,
170
+ a: Tensor,
171
+ s: Tensor,
172
+ bias: Tensor | None = None,
173
+ mask: Tensor | None = None,
174
+ to_keys: Callable[[Tensor], Tensor] | None = None,
175
+ multiplicity: int = 1,
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):
201
+ """Apply the diffusion transformer over fixed atom-query windows."""
202
+
203
+ def __init__(
204
+ self,
205
+ attn_window_queries: int,
206
+ attn_window_keys: int,
207
+ **diffusion_transformer_kwargs: Any,
208
+ ) -> None:
209
+ super().__init__()
210
+ self.attn_window_queries = attn_window_queries
211
+ self.attn_window_keys = attn_window_keys
212
+ self.diffusion_transformer = DiffusionTransformer(
213
+ **diffusion_transformer_kwargs,
214
+ )
215
+
216
+ def forward(
217
+ self,
218
+ q: Tensor,
219
+ c: Tensor,
220
+ bias: Tensor,
221
+ to_keys: Callable[[Tensor], Tensor],
222
+ mask: Tensor,
223
+ multiplicity: int = 1,
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,
239
+ -1,
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,
250
+ mask=query_mask.float(),
251
+ multiplicity=1,
252
+ to_keys=windowed_keys,
253
+ )
254
+ return cast(Tensor, output.view(batch_size, window_count * query_window, width))
fastplms/models/boltz/vb_modules_trunkv2.py ADDED
@@ -0,0 +1,595 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Input, template, MSA, and prediction-head modules for the Boltz2 trunk."""
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
11
+
12
+ from . import vb_const as const
13
+ from .vb_layers_dropout import get_dropout_mask
14
+ from .vb_layers_outer_product_mean import OuterProductMean
15
+ from .vb_layers_pair_averaging import PairWeightedAveraging
16
+ from .vb_layers_pairformer import PairformerNoSeqLayer, PairformerNoSeqModule
17
+ from .vb_layers_transition import Transition
18
+ from .vb_modules_encodersv2 import (
19
+ AtomAttentionEncoder,
20
+ AtomEncoder,
21
+ FourierEmbedding,
22
+ )
23
+
24
+
25
+ class ContactConditioning(nn.Module):
26
+ """Encode selected, unselected, and distance-threshold contact constraints."""
27
+
28
+ def __init__(self, token_z: int, cutoff_min: float, cutoff_max: float) -> None:
29
+ super().__init__()
30
+ self.fourier_embedding = FourierEmbedding(token_z)
31
+ input_width = token_z + len(const.contact_conditioning_info) - 1
32
+ self.encoder = nn.Linear(input_width, token_z)
33
+ self.encoding_unspecified = nn.Parameter(torch.zeros(token_z))
34
+ self.encoding_unselected = nn.Parameter(torch.zeros(token_z))
35
+ self.cutoff_min = cutoff_min
36
+ self.cutoff_max = cutoff_max
37
+
38
+ def forward(self, feats: dict[str, Tensor]) -> Tensor:
39
+ """Return contact tensor C with shape ``(b, l, l, d_z)``."""
40
+
41
+ if const.contact_conditioning_info["UNSPECIFIED"] != 0:
42
+ raise ValueError("UNSPECIFIED contact conditioning must use channel zero")
43
+ if const.contact_conditioning_info["UNSELECTED"] != 1:
44
+ raise ValueError("UNSELECTED contact conditioning must use channel one")
45
+
46
+ 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):
65
+ """Combine atom, residue, profile, and optional experimental features."""
66
+
67
+ def __init__(
68
+ self,
69
+ atom_s: int,
70
+ atom_z: int,
71
+ token_s: int,
72
+ token_z: int,
73
+ atoms_per_window_queries: int,
74
+ atoms_per_window_keys: int,
75
+ atom_feature_dim: int,
76
+ atom_encoder_depth: int,
77
+ atom_encoder_heads: int,
78
+ activation_checkpointing: bool = False,
79
+ add_method_conditioning: bool = False,
80
+ add_modified_flag: bool = False,
81
+ add_cyclic_flag: bool = False,
82
+ add_mol_type_feat: bool = False,
83
+ use_no_atom_char: bool = False,
84
+ use_atom_backbone_feat: bool = False,
85
+ use_residue_feats_atoms: bool = False,
86
+ ) -> None:
87
+ super().__init__()
88
+ self.token_s = token_s
89
+ self.add_method_conditioning = add_method_conditioning
90
+ self.add_modified_flag = add_modified_flag
91
+ self.add_cyclic_flag = add_cyclic_flag
92
+ self.add_mol_type_feat = add_mol_type_feat
93
+ self.atom_encoder = AtomEncoder(
94
+ atom_s=atom_s,
95
+ atom_z=atom_z,
96
+ token_s=token_s,
97
+ token_z=token_z,
98
+ atoms_per_window_queries=atoms_per_window_queries,
99
+ atoms_per_window_keys=atoms_per_window_keys,
100
+ atom_feature_dim=atom_feature_dim,
101
+ structure_prediction=False,
102
+ use_no_atom_char=use_no_atom_char,
103
+ use_atom_backbone_feat=use_atom_backbone_feat,
104
+ use_residue_feats_atoms=use_residue_feats_atoms,
105
+ )
106
+ self.atom_enc_proj_z = nn.Sequential(
107
+ nn.LayerNorm(atom_z),
108
+ nn.Linear(atom_z, atom_encoder_depth * atom_encoder_heads, bias=False),
109
+ )
110
+ self.atom_attention_encoder = AtomAttentionEncoder(
111
+ atom_s=atom_s,
112
+ token_s=token_s,
113
+ atoms_per_window_queries=atoms_per_window_queries,
114
+ atoms_per_window_keys=atoms_per_window_keys,
115
+ atom_encoder_depth=atom_encoder_depth,
116
+ atom_encoder_heads=atom_encoder_heads,
117
+ structure_prediction=False,
118
+ activation_checkpointing=activation_checkpointing,
119
+ )
120
+ self.res_type_encoding = nn.Linear(const.num_tokens, token_s, bias=False)
121
+ self.msa_profile_encoding = nn.Linear(const.num_tokens + 1, token_s, bias=False)
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):
173
+ def __init__(
174
+ self,
175
+ token_z: int,
176
+ template_dim: int,
177
+ template_blocks: int,
178
+ dropout: float,
179
+ pairwise_head_width: int,
180
+ pairwise_num_heads: int,
181
+ post_layer_norm: bool,
182
+ activation_checkpointing: bool,
183
+ min_dist: float,
184
+ max_dist: float,
185
+ num_bins: int,
186
+ ) -> None:
187
+ super().__init__()
188
+ self.min_dist = min_dist
189
+ self.max_dist = max_dist
190
+ self.num_bins = num_bins
191
+ self.relu = nn.ReLU()
192
+ self.z_norm = nn.LayerNorm(token_z)
193
+ self.v_norm = nn.LayerNorm(template_dim)
194
+ self.z_proj = nn.Linear(token_z, template_dim, bias=False)
195
+ feature_width = const.num_tokens * 2 + num_bins + 5
196
+ self.a_proj = nn.Linear(feature_width, template_dim, bias=False)
197
+ self.u_proj = nn.Linear(template_dim, token_z, bias=False)
198
+ self.pairformer = PairformerNoSeqModule(
199
+ template_dim,
200
+ num_blocks=template_blocks,
201
+ dropout=dropout,
202
+ pairwise_head_width=pairwise_head_width,
203
+ pairwise_num_heads=pairwise_num_heads,
204
+ post_layer_norm=post_layer_norm,
205
+ activation_checkpointing=activation_checkpointing,
206
+ )
207
+
208
+ def _template_pair_mask(self, feats: dict[str, Tensor], count: int) -> Tensor:
209
+ raise NotImplementedError
210
+
211
+ def _template_features(
212
+ self,
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),
257
+ -1,
258
+ -1,
259
+ )
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,
267
+ z: Tensor,
268
+ feats: dict[str, Tensor],
269
+ pair_mask: Tensor,
270
+ use_kernels: bool = False,
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):
309
+ """Aggregate templates while restricting features to the same chain."""
310
+
311
+ def __init__(
312
+ self,
313
+ token_z: int,
314
+ template_dim: int,
315
+ template_blocks: int,
316
+ dropout: float = 0.25,
317
+ pairwise_head_width: int = 32,
318
+ pairwise_num_heads: int = 4,
319
+ post_layer_norm: bool = False,
320
+ activation_checkpointing: bool = False,
321
+ min_dist: float = 3.25,
322
+ max_dist: float = 50.75,
323
+ num_bins: int = 38,
324
+ **kwargs: Any,
325
+ ) -> None:
326
+ del kwargs
327
+ super().__init__(
328
+ token_z,
329
+ template_dim,
330
+ template_blocks,
331
+ dropout,
332
+ pairwise_head_width,
333
+ pairwise_num_heads,
334
+ post_layer_norm,
335
+ activation_checkpointing,
336
+ min_dist,
337
+ max_dist,
338
+ num_bins,
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):
348
+ """Aggregate templates under per-template visibility groups."""
349
+
350
+ def __init__(
351
+ self,
352
+ token_z: int,
353
+ template_dim: int,
354
+ template_blocks: int,
355
+ dropout: float = 0.25,
356
+ pairwise_head_width: int = 32,
357
+ pairwise_num_heads: int = 4,
358
+ post_layer_norm: bool = False,
359
+ activation_checkpointing: bool = False,
360
+ min_dist: float = 3.25,
361
+ max_dist: float = 50.75,
362
+ num_bins: int = 38,
363
+ **kwargs: Any,
364
+ ) -> None:
365
+ del kwargs
366
+ super().__init__(
367
+ token_z,
368
+ template_dim,
369
+ template_blocks,
370
+ dropout,
371
+ pairwise_head_width,
372
+ pairwise_num_heads,
373
+ post_layer_norm,
374
+ activation_checkpointing,
375
+ min_dist,
376
+ max_dist,
377
+ num_bins,
378
+ )
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):
387
+ """Embed and update an MSA before returning its accumulated pair update."""
388
+
389
+ def __init__(
390
+ self,
391
+ msa_s: int,
392
+ token_z: int,
393
+ token_s: int,
394
+ msa_blocks: int,
395
+ msa_dropout: float,
396
+ z_dropout: float,
397
+ pairwise_head_width: int = 32,
398
+ pairwise_num_heads: int = 4,
399
+ activation_checkpointing: bool = False,
400
+ use_paired_feature: bool = True,
401
+ subsample_msa: bool = False,
402
+ num_subsampled_msa: int = 1024,
403
+ **kwargs: Any,
404
+ ) -> None:
405
+ del kwargs
406
+ super().__init__()
407
+ self.msa_blocks = msa_blocks
408
+ self.msa_dropout = msa_dropout
409
+ self.z_dropout = z_dropout
410
+ self.use_paired_feature = use_paired_feature
411
+ self.activation_checkpointing = activation_checkpointing
412
+ self.subsample_msa = subsample_msa
413
+ self.num_subsampled_msa = num_subsampled_msa
414
+ self.s_proj = nn.Linear(token_s, msa_s, bias=False)
415
+ input_width = const.num_tokens + 2 + int(use_paired_feature)
416
+ self.msa_proj = nn.Linear(input_width, msa_s, bias=False)
417
+ self.layers = nn.ModuleList(
418
+ [
419
+ MSALayer(
420
+ msa_s,
421
+ token_z,
422
+ msa_dropout,
423
+ z_dropout,
424
+ pairwise_head_width,
425
+ pairwise_num_heads,
426
+ )
427
+ for _ in range(msa_blocks)
428
+ ]
429
+ )
430
+
431
+ @staticmethod
432
+ def _chunk_configuration(
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:
439
+ return True, 64, 32, 4, 128
440
+ return False, None, None, None, 512
441
+
442
+ def forward(
443
+ self,
444
+ z: Tensor,
445
+ emb: Tensor,
446
+ feats: dict[str, Tensor],
447
+ use_kernels: bool = False,
448
+ ) -> Tensor:
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,
477
+ pair_mask,
478
+ msa_mask,
479
+ *chunking,
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):
493
+ """Exchange information between MSA tensor M and pair tensor Z."""
494
+
495
+ def __init__(
496
+ self,
497
+ msa_s: int,
498
+ token_z: int,
499
+ msa_dropout: float,
500
+ z_dropout: float,
501
+ pairwise_head_width: int = 32,
502
+ pairwise_num_heads: int = 4,
503
+ ) -> None:
504
+ super().__init__()
505
+ self.msa_dropout = msa_dropout
506
+ self.msa_transition = Transition(msa_s, msa_s * 4)
507
+ self.pair_weighted_averaging = PairWeightedAveraging(
508
+ c_m=msa_s,
509
+ c_z=token_z,
510
+ c_h=32,
511
+ num_heads=8,
512
+ )
513
+ self.pairformer_layer = PairformerNoSeqLayer(
514
+ token_z=token_z,
515
+ dropout=z_dropout,
516
+ pairwise_head_width=pairwise_head_width,
517
+ pairwise_num_heads=pairwise_num_heads,
518
+ )
519
+ self.outer_product_mean = OuterProductMean(msa_s, 32, token_z)
520
+
521
+ def forward(
522
+ self,
523
+ z: Tensor,
524
+ m: Tensor,
525
+ token_mask: Tensor,
526
+ msa_mask: Tensor,
527
+ chunk_heads_pwa: bool = False,
528
+ chunk_size_transition_z: int | None = None,
529
+ chunk_size_transition_msa: int | None = None,
530
+ chunk_size_outer_product: int | None = None,
531
+ chunk_size_tri_attn: int | None = None,
532
+ use_kernels: bool = False,
533
+ ) -> tuple[Tensor, Tensor]:
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):
563
+ """Predict a per-token B-factor histogram."""
564
+
565
+ def __init__(self, token_s: int, num_bins: int) -> None:
566
+ super().__init__()
567
+ self.bfactor = nn.Linear(token_s, num_bins)
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):
575
+ """Predict symmetric residue-pair distance histograms."""
576
+
577
+ def __init__(self, token_z: int, num_bins: int, num_distograms: int = 1) -> None:
578
+ super().__init__()
579
+ self.distogram = nn.Linear(token_z, num_distograms * num_bins)
580
+ self.num_distograms = num_distograms
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(
589
+ symmetric.shape[0],
590
+ symmetric.shape[1],
591
+ symmetric.shape[2],
592
+ self.num_distograms,
593
+ self.num_bins,
594
+ ),
595
+ )
fastplms/models/boltz/vb_modules_utils.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Small geometry, augmentation, activation, and EMA utilities for Boltz2.
2
+
3
+ The quaternion behavior follows PyTorch3D's public rotation convention. License
4
+ and attribution records are retained in ``THIRD_PARTY_NOTICES.md``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ 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
+
20
+ def exists(value: object) -> bool:
21
+ """Return whether a value is not ``None``."""
22
+
23
+ return value is not None
24
+
25
+
26
+ def default(value: Any, fallback: Any) -> Any:
27
+ """Return ``value`` unless it is ``None``."""
28
+
29
+ return fallback if value is None else value
30
+
31
+
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,
51
+ )
52
+
53
+
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),
75
+ scale * (i_axis * j_axis + k_axis * real),
76
+ 1 - scale * (i_axis * i_axis + k_axis * k_axis),
77
+ scale * (j_axis * k_axis - i_axis * real),
78
+ scale * (i_axis * k_axis - j_axis * real),
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
+
85
+ def random_quaternions(
86
+ n: int,
87
+ dtype: torch.dtype | None = None,
88
+ device: torch.device | str | None = None,
89
+ ) -> Tensor:
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(
100
+ n: int,
101
+ dtype: torch.dtype | None = None,
102
+ device: torch.device | str | None = None,
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
+
109
+ def compute_random_augmentation(
110
+ multiplicity: int,
111
+ s_trans: float = 1.0,
112
+ device: torch.device | str | None = None,
113
+ dtype: torch.dtype = torch.float32,
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,
122
+ device=device,
123
+ )
124
+ * s_trans
125
+ )
126
+ return rotations, translations
127
+
128
+
129
+ def randomly_rotate(
130
+ coords: Tensor,
131
+ return_second_coords: bool = False,
132
+ second_coords: Tensor | None = None,
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(
147
+ atom_coords: Tensor,
148
+ atom_mask: Tensor,
149
+ s_trans: float = 1.0,
150
+ augmentation: bool = True,
151
+ centering: bool = True,
152
+ return_second_coords: bool = False,
153
+ second_coords: Tensor | None = None,
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
+
179
+ class ExponentialMovingAverage:
180
+ """Maintain detached exponential moving averages of trainable parameters."""
181
+
182
+ def __init__(
183
+ self,
184
+ parameters: Iterable[nn.Parameter],
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
206
+ if self.num_updates is not None:
207
+ self.num_updates += 1
208
+ decay = min(decay, (1 + self.num_updates) / (10 + self.num_updates))
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,
243
+ "shadow_params": self.shadow_params,
244
+ }
245
+
246
+ def load_state_dict(
247
+ self,
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]
fastplms/models/boltz/vb_potentials_potentials.py ADDED
@@ -0,0 +1,845 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Differentiable structure-steering potentials for Boltz2 diffusion.
2
+
3
+ Each potential follows the same mechanism: derive constraint indices and
4
+ bounds from features, evaluate a geometric variable, map that variable to an
5
+ energy, and optionally scatter its analytic derivative back to atom
6
+ coordinates. This module is maintained independently from the pinned parity
7
+ oracle while preserving the public class names used by converted checkpoints.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ 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 (
21
+ ExponentialInterpolation,
22
+ ParameterSchedule,
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(
45
+ coordinates: torch.Tensor,
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(
61
+ energy: torch.Tensor,
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,
69
+ ).scatter_reduce(
70
+ -1,
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(
81
+ coefficients: torch.Tensor,
82
+ variable_gradient: torch.Tensor,
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):
103
+ """Base contract for energy and analytic-gradient steering potentials."""
104
+
105
+ def __init__(self, parameters: ParameterMap | None = None) -> None:
106
+ self.parameters = parameters
107
+
108
+ def _prepare(
109
+ self,
110
+ coordinates: torch.Tensor,
111
+ feats: dict[str, torch.Tensor],
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,
138
+ function_args=args,
139
+ com_index=com_index,
140
+ atom_pad_mask=atom_pad_mask,
141
+ ref_coords=ref_coords,
142
+ ref_mask=ref_mask,
143
+ ref_token_index=ref_token_index,
144
+ negation_mask=negation_mask,
145
+ union_index=union_index,
146
+ )
147
+
148
+ def compute(
149
+ self,
150
+ coords: torch.Tensor,
151
+ feats: dict[str, torch.Tensor],
152
+ parameters: dict[str, Any],
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,
162
+ prepared.index,
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,
184
+ coords: torch.Tensor,
185
+ feats: dict[str, torch.Tensor],
186
+ parameters: dict[str, Any],
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,
196
+ prepared.index,
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,
216
+ ).scatter_reduce(
217
+ -1,
218
+ prepared.union_index.expand_as(energy),
219
+ energy * weights,
220
+ "sum",
221
+ )
222
+ coefficients = (
223
+ energy_derivative
224
+ * weights
225
+ * (
226
+ 1
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``."""
248
+
249
+ if self.parameters is None:
250
+ return None
251
+ return {
252
+ name: parameter.compute(t) if isinstance(parameter, ParameterSchedule) else parameter
253
+ for name, parameter in self.parameters.items()
254
+ }
255
+
256
+ @abstractmethod
257
+ def compute_function(
258
+ self,
259
+ value: torch.Tensor,
260
+ *args: Any,
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
267
+ def compute_variable(
268
+ self,
269
+ coords: torch.Tensor,
270
+ index: torch.Tensor,
271
+ ref_coords: torch.Tensor | None = None,
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
278
+ def compute_args(
279
+ self,
280
+ feats: dict[str, torch.Tensor],
281
+ parameters: dict[str, Any],
282
+ ) -> tuple[Any, ...]:
283
+ raise NotImplementedError
284
+
285
+ def get_reference_coords(
286
+ self,
287
+ feats: dict[str, torch.Tensor],
288
+ parameters: dict[str, Any],
289
+ ) -> tuple[None, None]:
290
+ del feats, parameters
291
+ return None, None
292
+
293
+
294
+ class FlatBottomPotential(Potential):
295
+ """Linear penalty outside an allowed lower-to-upper interval."""
296
+
297
+ def compute_function(
298
+ self,
299
+ value: torch.Tensor,
300
+ k: torch.Tensor,
301
+ lower_bounds: torch.Tensor | None,
302
+ upper_bounds: torch.Tensor | None,
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):
319
+ raise TypeError(
320
+ f"negation_mask must be a boolean tensor, got {type(negation_mask).__name__}."
321
+ )
322
+ if negation_mask.dtype != torch.bool:
323
+ raise TypeError(
324
+ f"negation_mask must be a boolean tensor, got {negation_mask.dtype}."
325
+ )
326
+ if negation_mask.device != value.device:
327
+ raise ValueError(
328
+ "negation_mask must be on the same device as value; "
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):
366
+ """Measure atom displacement after weighted rigid alignment."""
367
+
368
+ def compute_variable(
369
+ self,
370
+ coords: torch.Tensor,
371
+ index: torch.Tensor,
372
+ ref_coords: torch.Tensor,
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):
392
+ """Measure Euclidean distances for indexed atom pairs."""
393
+
394
+ def compute_variable(
395
+ self,
396
+ coords: torch.Tensor,
397
+ index: torch.Tensor,
398
+ ref_coords: torch.Tensor | None = None,
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
+
414
+ class DihedralPotential(Potential):
415
+ """Measure signed torsion angles for indexed atom quartets."""
416
+
417
+ def compute_variable(
418
+ self,
419
+ coords: torch.Tensor,
420
+ index: torch.Tensor,
421
+ ref_coords: torch.Tensor | None = None,
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
+
458
+ class AbsDihedralPotential(DihedralPotential):
459
+ """Measure the unsigned magnitude of indexed torsion angles."""
460
+
461
+ def compute_variable(
462
+ self,
463
+ coords: torch.Tensor,
464
+ index: torch.Tensor,
465
+ ref_coords: torch.Tensor | None = None,
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(),
499
+ feats["asym_id"].unsqueeze(-1).float(),
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),
594
+ (atom_chain_id, atom_pad_mask),
595
+ None,
596
+ None,
597
+ )
598
+
599
+
600
+ def _oriented_bounds(
601
+ orientations: torch.Tensor,
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),
654
+ None,
655
+ None,
656
+ None,
657
+ )
658
+
659
+
660
+ 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(
686
+ feats["atom_to_token"].float(),
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),
711
+ None,
712
+ (ref_coords, ref_mask, ref_atom_index, ref_token_index),
713
+ None,
714
+ )
715
+
716
+
717
+ class ContactPotentital(FlatBottomPotential, DistancePotential):
718
+ """Contact-union potential retaining the historical checkpoint name."""
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),
727
+ None,
728
+ None,
729
+ (
730
+ feats["contact_negation_mask"][0],
731
+ feats["contact_union_index"][0],
732
+ ),
733
+ )
734
+
735
+
736
+ ContactPotential = ContactPotentital
737
+
738
+
739
+ def get_potentials(
740
+ steering_args: dict[str, bool],
741
+ boltz2: bool = False,
742
+ ) -> list[Potential]:
743
+ """Build the ordered potential stack for requested steering modes."""
744
+
745
+ use_fk = steering_args["fk_steering"]
746
+ use_physical = steering_args["physical_guidance_update"]
747
+ use_contacts = steering_args.get("contact_guidance_update", False)
748
+ potentials: list[Potential] = []
749
+ if use_fk or use_physical:
750
+ potentials.extend(
751
+ (
752
+ SymmetricChainCOMPotential(
753
+ {
754
+ "guidance_interval": 4,
755
+ "guidance_weight": 0.5 if use_physical else 0.0,
756
+ "resampling_weight": 0.5,
757
+ "buffer": ExponentialInterpolation(1.0, 5.0, -2.0),
758
+ }
759
+ ),
760
+ VDWOverlapPotential(
761
+ {
762
+ "guidance_interval": 5,
763
+ "guidance_weight": PiecewiseStepFunction(
764
+ [0.4],
765
+ [0.125, 0.0],
766
+ )
767
+ if use_physical
768
+ else 0.0,
769
+ "resampling_weight": PiecewiseStepFunction(
770
+ [0.6],
771
+ [0.01, 0.0],
772
+ ),
773
+ "buffer": 0.225,
774
+ }
775
+ ),
776
+ ConnectionsPotential(
777
+ {
778
+ "guidance_interval": 1,
779
+ "guidance_weight": 0.15 if use_physical else 0.0,
780
+ "resampling_weight": 1.0,
781
+ "buffer": 2.0,
782
+ }
783
+ ),
784
+ PoseBustersPotential(
785
+ {
786
+ "guidance_interval": 1,
787
+ "guidance_weight": 0.01 if use_physical else 0.0,
788
+ "resampling_weight": 0.1,
789
+ "bond_buffer": 0.125,
790
+ "angle_buffer": 0.125,
791
+ "clash_buffer": 0.10,
792
+ }
793
+ ),
794
+ ChiralAtomPotential(
795
+ {
796
+ "guidance_interval": 1,
797
+ "guidance_weight": 0.1 if use_physical else 0.0,
798
+ "resampling_weight": 1.0,
799
+ "buffer": 0.52360,
800
+ }
801
+ ),
802
+ StereoBondPotential(
803
+ {
804
+ "guidance_interval": 1,
805
+ "guidance_weight": 0.05 if use_physical else 0.0,
806
+ "resampling_weight": 1.0,
807
+ "buffer": 0.52360,
808
+ }
809
+ ),
810
+ PlanarBondPotential(
811
+ {
812
+ "guidance_interval": 1,
813
+ "guidance_weight": 0.05 if use_physical else 0.0,
814
+ "resampling_weight": 1.0,
815
+ "buffer": 0.26180,
816
+ }
817
+ ),
818
+ )
819
+ )
820
+ if boltz2 and (use_fk or use_contacts):
821
+ potentials.extend(
822
+ (
823
+ ContactPotentital(
824
+ {
825
+ "guidance_interval": 4,
826
+ "guidance_weight": PiecewiseStepFunction(
827
+ [0.25, 0.75],
828
+ [0.0, 0.5, 1.0],
829
+ )
830
+ if use_contacts
831
+ else 0.0,
832
+ "resampling_weight": 1.0,
833
+ "union_lambda": ExponentialInterpolation(8.0, 0.0, -2.0),
834
+ }
835
+ ),
836
+ TemplateReferencePotential(
837
+ {
838
+ "guidance_interval": 2,
839
+ "guidance_weight": 0.1 if use_contacts else 0.0,
840
+ "resampling_weight": 1.0,
841
+ }
842
+ ),
843
+ )
844
+ )
845
+ return potentials
fastplms/models/boltz/vb_potentials_schedules.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Scalar schedules for structure-steering potentials."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from abc import ABC, abstractmethod
7
+ from collections.abc import Sequence
8
+
9
+
10
+ class ParameterSchedule(ABC):
11
+ """Map normalized diffusion time ``t`` to a potential parameter."""
12
+
13
+ @abstractmethod
14
+ def compute(self, t: float) -> float:
15
+ """Evaluate the schedule at ``t``."""
16
+
17
+
18
+ class ExponentialInterpolation(ParameterSchedule):
19
+ """Interpolate from ``start`` to ``end`` with exponential curvature."""
20
+
21
+ def __init__(self, start: float, end: float, alpha: float) -> None:
22
+ self.start = start
23
+ self.end = end
24
+ self.alpha = alpha
25
+
26
+ def compute(self, t: float) -> float:
27
+ span = self.end - self.start
28
+ if self.alpha == 0:
29
+ return self.start + span * t
30
+ numerator = math.exp(self.alpha * t) - 1
31
+ denominator = math.exp(self.alpha) - 1
32
+ return self.start + span * numerator / denominator
33
+
34
+
35
+ class PiecewiseStepFunction(ParameterSchedule):
36
+ """Select values separated by strict upper thresholds.
37
+
38
+ A time exactly equal to a threshold remains in the lower interval. This
39
+ boundary convention is part of the steering-input contract.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ thresholds: Sequence[float],
45
+ values: Sequence[float],
46
+ ) -> None:
47
+ self.thresholds = tuple(thresholds)
48
+ self.values = tuple(values)
49
+ if not self.thresholds:
50
+ raise ValueError("PiecewiseStepFunction requires at least one threshold.")
51
+ if len(self.values) != len(self.thresholds) + 1:
52
+ raise ValueError(
53
+ "PiecewiseStepFunction requires exactly one more value than threshold; "
54
+ f"received {len(self.values)} values and {len(self.thresholds)} thresholds."
55
+ )
56
+ if any(
57
+ current >= following
58
+ for current, following in zip(self.thresholds, self.thresholds[1:], strict=False)
59
+ ):
60
+ raise ValueError("PiecewiseStepFunction thresholds must be strictly increasing.")
61
+
62
+ def compute(self, t: float) -> float:
63
+ interval = next(
64
+ (index for index, threshold in enumerate(self.thresholds) if t <= threshold),
65
+ len(self.thresholds),
66
+ )
67
+ return self.values[interval]
fastplms/models/boltz/vb_tri_attn_attention.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Triangular row and column attention for Boltz2 pair states."""
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
11
+ from .vb_tri_attn_utils import chunk_layer, permute_final_dims
12
+
13
+
14
+ class TriangleAttention(nn.Module):
15
+ """Apply pair-biased attention along one axis of pair tensor X."""
16
+
17
+ def __init__(
18
+ self,
19
+ c_in: int,
20
+ c_hidden: int,
21
+ no_heads: int,
22
+ starting: bool = True,
23
+ inf: float = 1e9,
24
+ ) -> None:
25
+ super().__init__()
26
+ self.c_in = c_in
27
+ self.c_hidden = c_hidden
28
+ self.no_heads = no_heads
29
+ self.starting = starting
30
+ self.inf = inf
31
+ self.layer_norm = LayerNorm(c_in)
32
+ self.linear = Linear(c_in, no_heads, bias=False, init="normal")
33
+ self.mha = Attention(c_in, c_in, c_in, c_hidden, no_heads)
34
+
35
+ @torch.jit.ignore # type: ignore[untyped-decorator]
36
+ def _chunk(
37
+ self,
38
+ x: Tensor,
39
+ tri_bias: Tensor,
40
+ mask_bias: Tensor,
41
+ mask: Tensor,
42
+ chunk_size: int,
43
+ use_kernels: bool = False,
44
+ ) -> Tensor:
45
+ """Evaluate attention in slices of the batch-like leading axes."""
46
+
47
+ def attention_call(**inputs: Tensor) -> Tensor:
48
+ return cast(Tensor, self.mha(**inputs, use_kernels=use_kernels))
49
+
50
+ inputs = {
51
+ "q_x": x,
52
+ "kv_x": x,
53
+ "tri_bias": tri_bias,
54
+ "mask_bias": mask_bias,
55
+ "mask": mask,
56
+ }
57
+ return cast(
58
+ Tensor,
59
+ chunk_layer(
60
+ attention_call,
61
+ inputs,
62
+ chunk_size=chunk_size,
63
+ no_batch_dims=len(x.shape[:-2]),
64
+ _out=None,
65
+ ),
66
+ )
67
+
68
+ def _run_attention(
69
+ self,
70
+ states: Tensor,
71
+ triangle_bias: Tensor,
72
+ mask_bias: Tensor,
73
+ expanded_mask: Tensor,
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,
80
+ self._chunk(
81
+ states,
82
+ triangle_bias,
83
+ mask_bias,
84
+ expanded_mask,
85
+ chunk_size,
86
+ use_kernels=False,
87
+ ),
88
+ )
89
+ return cast(
90
+ Tensor,
91
+ self.mha(
92
+ states,
93
+ states,
94
+ triangle_bias,
95
+ mask_bias,
96
+ expanded_mask,
97
+ use_kernels=use_kernels,
98
+ ),
99
+ )
100
+
101
+ def forward(
102
+ self,
103
+ x: Tensor,
104
+ mask: Tensor | None = None,
105
+ chunk_size: int | None = None,
106
+ use_kernels: bool = False,
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,
124
+ mask_bias,
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
133
+
134
+
135
+ class TriangleAttentionEndingNode(TriangleAttention):
136
+ """Apply triangular attention around each pair's ending node."""
137
+
138
+ def __init__(
139
+ self,
140
+ c_in: int,
141
+ c_hidden: int,
142
+ no_heads: int,
143
+ inf: float = 1e9,
144
+ ) -> None:
145
+ super().__init__(c_in, c_hidden, no_heads, starting=False, inf=inf)
fastplms/models/boltz/vb_tri_attn_primitives.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Attention primitives for the Boltz pair stack.
2
+
3
+ The module retains the converted checkpoint names while providing a compact
4
+ implementation around PyTorch operations. Optional triangle kernels are
5
+ loaded only at the call site, so importing FastPLMs does not initialize CUDA.
6
+
7
+ Portions of the numerical contract follow OpenFold and DeepMind AlphaFold
8
+ under Apache-2.0. See ``THIRD_PARTY_NOTICES.md`` for provenance.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib
14
+ import math
15
+ 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
22
+ from .vb_tri_attn_utils import flatten_final_dims, permute_final_dims
23
+
24
+
25
+ def _initialize_weight(
26
+ weight: torch.Tensor,
27
+ bias: torch.Tensor | None,
28
+ method: str,
29
+ ) -> None:
30
+ initializers = {
31
+ "default": initialize.lecun_normal_init_,
32
+ "relu": initialize.he_normal_init_,
33
+ "glorot": initialize.glorot_uniform_init_,
34
+ "normal": initialize.normal_init_,
35
+ "final": initialize.final_init_,
36
+ "gating": initialize.gating_init_,
37
+ }
38
+ try:
39
+ initializers[method](weight)
40
+ except KeyError as error:
41
+ raise ValueError(f"unknown linear initialization {method!r}") from error
42
+ if method == "gating" and bias is not None:
43
+ bias.fill_(1.0)
44
+
45
+
46
+ class Linear(nn.Linear):
47
+ """Linear projection with the initializers used by converted checkpoints."""
48
+
49
+ def __init__(
50
+ self,
51
+ in_dim: int,
52
+ out_dim: int,
53
+ bias: bool = True,
54
+ init: str = "default",
55
+ init_fn: Callable[[torch.Tensor, torch.Tensor | None], None] | None = None,
56
+ precision: torch.dtype | None = None,
57
+ ) -> None:
58
+ super().__init__(in_dim, out_dim, bias=bias)
59
+ with torch.no_grad():
60
+ if self.bias is not None:
61
+ self.bias.zero_()
62
+ if init_fn is None:
63
+ _initialize_weight(self.weight, self.bias, init)
64
+ else:
65
+ init_fn(self.weight, self.bias)
66
+ self.precision = precision
67
+
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):
89
+ """Layer normalization that avoids an autocast upcast for BF16 inputs."""
90
+
91
+ def __init__(self, c_in: int, eps: float = 1e-5) -> None:
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(
102
+ x,
103
+ self.c_in,
104
+ self.weight.to(x.dtype),
105
+ self.bias.to(x.dtype),
106
+ self.eps,
107
+ )
108
+ return nn.functional.layer_norm(x, self.c_in, self.weight, self.bias, self.eps)
109
+
110
+
111
+ @torch.jit.ignore
112
+ def softmax_no_cast(tensor: torch.Tensor, dim: int = -1) -> torch.Tensor:
113
+ """Apply softmax without promoting BF16 inputs under CUDA autocast."""
114
+
115
+ if tensor.dtype is torch.bfloat16:
116
+ with torch.autocast("cuda", enabled=False):
117
+ return nn.functional.softmax(tensor, dim=dim)
118
+ return nn.functional.softmax(tensor, dim=dim)
119
+
120
+
121
+ def _attention(
122
+ query: torch.Tensor,
123
+ key: torch.Tensor,
124
+ value: torch.Tensor,
125
+ biases: Sequence[torch.Tensor],
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
137
+ def kernel_triangular_attn(
138
+ q: torch.Tensor,
139
+ k: torch.Tensor,
140
+ v: torch.Tensor,
141
+ tri_bias: torch.Tensor,
142
+ mask: torch.Tensor,
143
+ scale: float,
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
150
+ ):
151
+ raise RuntimeError(
152
+ "Boltz2 use_kernels=True requires cuequivariance_torch and the CUDA 13 "
153
+ "cuequivariance_ops_torch runtime from the 'structure,cueq' extras."
154
+ )
155
+ cueq = importlib.import_module("cuequivariance_torch")
156
+ return cueq.triangle_attention(q, k, v, tri_bias, mask=mask, scale=scale)
157
+
158
+
159
+ class Attention(nn.Module):
160
+ """Multi-head pair attention with optional query-dependent output gates."""
161
+
162
+ def __init__(
163
+ self,
164
+ c_q: int,
165
+ c_k: int,
166
+ c_v: int,
167
+ c_hidden: int,
168
+ no_heads: int,
169
+ gating: bool = True,
170
+ ) -> None:
171
+ super().__init__()
172
+ self.c_q = c_q
173
+ self.c_k = c_k
174
+ self.c_v = c_v
175
+ self.c_hidden = c_hidden
176
+ self.no_heads = no_heads
177
+ self.gating = gating
178
+
179
+ projected_dim = c_hidden * no_heads
180
+ self.linear_q = Linear(c_q, projected_dim, bias=False, init="glorot")
181
+ self.linear_k = Linear(c_k, projected_dim, bias=False, init="glorot")
182
+ self.linear_v = Linear(c_v, projected_dim, bias=False, init="glorot")
183
+ self.linear_o = Linear(projected_dim, c_q, bias=False, init="final")
184
+ self.linear_g = Linear(c_q, projected_dim, bias=False, init="gating") if gating else None
185
+ self.sigmoid = nn.Sigmoid()
186
+
187
+ def _prep_qkv(
188
+ self,
189
+ q_x: torch.Tensor,
190
+ kv_x: torch.Tensor,
191
+ apply_scale: bool = True,
192
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
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,
216
+ q_x: torch.Tensor,
217
+ kv_x: torch.Tensor,
218
+ tri_bias: torch.Tensor,
219
+ mask_bias: torch.Tensor,
220
+ mask: torch.Tensor,
221
+ use_kernels: bool = False,
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,
229
+ k,
230
+ v,
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)
fastplms/models/boltz/vb_tri_attn_utils.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tensor-tree and memory-bounded execution helpers for pair attention.
2
+
3
+ These utilities implement the small subset of OpenFold-style chunking needed
4
+ by the local Boltz pair stack. The implementation is independent, but keeps
5
+ the public function names expected by converted checkpoints.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ 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:
28
+ """Permute only the final ``len(inds)`` axes of ``tensor``."""
29
+
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:
37
+ """Return whether CUDA autocast currently targets ``float16``."""
38
+
39
+ return torch.is_autocast_enabled() and torch.get_autocast_dtype("cuda") == torch.float16
40
+
41
+
42
+ def dict_map(
43
+ fn: Callable[[Any], Any],
44
+ dic: dict[Any, Any],
45
+ leaf_type: type | tuple[type, ...],
46
+ ) -> dict[Any, Any]:
47
+ """Apply ``fn`` to leaves in a nested dictionary tree."""
48
+
49
+ return {key: tree_map(fn, value, leaf_type) for key, value in dic.items()}
50
+
51
+
52
+ def tree_map(
53
+ fn: Callable[[Any], Any],
54
+ tree: Any,
55
+ leaf_type: type | tuple[type, ...],
56
+ ) -> Any:
57
+ """Map a function over dict, list, and tuple containers."""
58
+
59
+ if isinstance(tree, leaf_type):
60
+ return fn(tree)
61
+ if isinstance(tree, dict):
62
+ return dict_map(fn, tree, leaf_type)
63
+ if isinstance(tree, list):
64
+ return [tree_map(fn, item, leaf_type) for item in tree]
65
+ if isinstance(tree, tuple):
66
+ return tuple(tree_map(fn, item, leaf_type) for item in tree)
67
+ raise ValueError(f"tree type {type(tree)!r} is not supported")
68
+
69
+
70
+ tensor_tree_map = partial(tree_map, leaf_type=torch.Tensor)
71
+
72
+
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]:
80
+ """Collect tensor shapes from a supported tree."""
81
+
82
+ if isinstance(tree, torch.Tensor):
83
+ return [tree.shape]
84
+ if isinstance(tree, dict):
85
+ children = tree.values()
86
+ elif isinstance(tree, (list, tuple)):
87
+ children = tree
88
+ else:
89
+ raise ValueError(f"tree type {type(tree)!r} is not supported")
90
+
91
+ shapes: list[torch.Size] = []
92
+ for child in children:
93
+ shapes.extend(_fetch_dims(child))
94
+ return shapes
95
+
96
+
97
+ @torch.jit.ignore
98
+ def _flat_idx_to_idx(flat_idx: int, dims: tuple[int, ...]) -> tuple[int, ...]:
99
+ """Convert a row-major flat index into an index tuple."""
100
+
101
+ coordinates = [0] * len(dims)
102
+ remainder = flat_idx
103
+ for axis in range(len(dims) - 1, -1, -1):
104
+ remainder, coordinates[axis] = divmod(remainder, dims[axis])
105
+ return tuple(coordinates)
106
+
107
+
108
+ def _ravel_index(index: Sequence[int], dims: Sequence[int]) -> int:
109
+ """Convert a row-major index tuple to a flat index."""
110
+
111
+ flat = 0
112
+ for coordinate, size in zip(index, dims, strict=True):
113
+ flat = flat * size + coordinate
114
+ return flat
115
+
116
+
117
+ def _cover_flat_interval(
118
+ start: int,
119
+ stop: int,
120
+ dims: tuple[int, ...],
121
+ ) -> list[tuple[slice, ...]]:
122
+ """Cover ``[start, stop)`` with ordered contiguous tensor slices."""
123
+
124
+ if not dims:
125
+ return [tuple()]
126
+ if len(dims) == 1:
127
+ return [(slice(start, stop),)]
128
+
129
+ child_size = prod(dims[1:])
130
+ first_child = start // child_size
131
+ last_child = (stop - 1) // child_size
132
+ if first_child == last_child:
133
+ tail = _cover_flat_interval(
134
+ start % child_size,
135
+ (stop - 1) % child_size + 1,
136
+ dims[1:],
137
+ )
138
+ prefix = slice(first_child, first_child + 1)
139
+ return [(prefix, *item) for item in tail]
140
+
141
+ slices: list[tuple[slice, ...]] = []
142
+ start_offset = start % child_size
143
+ first_full_child = first_child
144
+ if start_offset:
145
+ prefix = slice(first_child, first_child + 1)
146
+ slices.extend(
147
+ (prefix, *item) for item in _cover_flat_interval(start_offset, child_size, dims[1:])
148
+ )
149
+ first_full_child += 1
150
+
151
+ stop_offset = stop % child_size
152
+ full_stop = last_child if stop_offset else last_child + 1
153
+ if first_full_child < full_stop:
154
+ slices.append((slice(first_full_child, full_stop),))
155
+
156
+ if stop_offset:
157
+ prefix = slice(last_child, last_child + 1)
158
+ slices.extend((prefix, *item) for item in _cover_flat_interval(0, stop_offset, dims[1:]))
159
+ return slices
160
+
161
+
162
+ @torch.jit.ignore
163
+ def _get_minimal_slice_set(
164
+ start: Sequence[int],
165
+ end: Sequence[int],
166
+ dims: Sequence[int],
167
+ start_edges: Sequence[bool] | None = None,
168
+ end_edges: Sequence[bool] | None = None,
169
+ ) -> list[tuple[slice, ...]]:
170
+ """Return ordered slices covering the inclusive row-major interval.
171
+
172
+ ``start_edges`` and ``end_edges`` remain accepted for compatibility. The
173
+ interval decomposition derives the same information directly.
174
+ """
175
+
176
+ del start_edges, end_edges
177
+ shape = tuple(dims)
178
+ if not shape:
179
+ return [tuple()]
180
+ flat_start = _ravel_index(start, shape)
181
+ flat_stop = _ravel_index(end, shape) + 1
182
+ return _cover_flat_interval(flat_start, flat_stop, shape)
183
+
184
+
185
+ @torch.jit.ignore
186
+ def _chunk_slice(
187
+ tensor: torch.Tensor,
188
+ flat_start: int,
189
+ flat_end: int,
190
+ no_batch_dims: int,
191
+ ) -> torch.Tensor:
192
+ """Slice a flattened batch interval without flattening the full tensor."""
193
+
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(
203
+ tensor: torch.Tensor,
204
+ batch_shape: tuple[int, ...],
205
+ no_batch_dims: int,
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(
218
+ destination: Any,
219
+ source: Any,
220
+ start: int,
221
+ stop: int,
222
+ *,
223
+ add_into_out: bool,
224
+ ) -> None:
225
+ if isinstance(source, dict):
226
+ for key, value in source.items():
227
+ _write_chunk(
228
+ destination[key],
229
+ value,
230
+ start,
231
+ stop,
232
+ add_into_out=add_into_out,
233
+ )
234
+ return
235
+ if isinstance(source, (tuple, list)):
236
+ for destination_item, source_item in zip(destination, source, strict=True):
237
+ _write_chunk(
238
+ destination_item,
239
+ source_item,
240
+ start,
241
+ stop,
242
+ add_into_out=add_into_out,
243
+ )
244
+ return
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(
254
+ layer: Callable[..., Any],
255
+ inputs: dict[str, Any],
256
+ chunk_size: int,
257
+ no_batch_dims: int,
258
+ low_mem: bool = False,
259
+ _out: Any = None,
260
+ _add_into_out: bool = False,
261
+ ) -> Any:
262
+ """Apply ``layer`` to flattened batch chunks and reassemble its output."""
263
+
264
+ if not inputs:
265
+ raise ValueError("at least one input is required")
266
+ if chunk_size <= 0:
267
+ raise ValueError("chunk_size must be positive")
268
+
269
+ leading_shapes = [shape[:no_batch_dims] for shape in _fetch_dims(inputs)]
270
+ batch_shape = tuple(max(sizes) for sizes in zip(*leading_shapes, strict=True))
271
+ prepare = partial(
272
+ _prepare_input,
273
+ batch_shape=batch_shape,
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):
288
+ stop = min(flat_batch_size, start + chunk_size)
289
+ if low_mem:
290
+ select = partial(
291
+ _chunk_slice,
292
+ flat_start=start,
293
+ flat_end=stop,
294
+ no_batch_dims=len(batch_shape),
295
+ )
296
+ else:
297
+
298
+ def select(
299
+ tensor: torch.Tensor,
300
+ start: int = start,
301
+ stop: int = stop,
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,
316
+ start,
317
+ stop,
318
+ add_into_out=_add_into_out,
319
+ )
320
+
321
+ return tensor_tree_map(
322
+ lambda tensor: tensor.reshape(batch_shape + tuple(tensor.shape[1:])),
323
+ output,
324
+ )
fastplms/registry.py ADDED
@@ -0,0 +1,1486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Typed access to the FastPLMs model and provenance manifest.
2
+
3
+ The registry is intentionally independent of Torch and Transformers. Tooling can
4
+ therefore inspect supported checkpoints, licenses, and reference sources without
5
+ initializing a model runtime or downloading any files.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import tomllib
12
+ from collections.abc import Iterator, Mapping
13
+ from dataclasses import dataclass
14
+ from functools import lru_cache
15
+ from importlib import resources
16
+ from pathlib import Path, PurePosixPath, PureWindowsPath
17
+ 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.]+")
24
+ _WINDOWS_INVALID_PATH_CHARACTERS = frozenset('<>:"|?*')
25
+ _WINDOWS_RESERVED_PATH_NAMES = frozenset(
26
+ {"AUX", "CON", "NUL", "PRN"}
27
+ | {f"COM{index}" for index in range(1, 10)}
28
+ | {f"LPT{index}" for index in range(1, 10)}
29
+ )
30
+ _REPOSITORY_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*$")
31
+ _REFERENCE_CONTAINER_RE = re.compile(r"^reference-[a-z0-9]+(?:-[a-z0-9]+)*$")
32
+ _REFERENCE_ADAPTER_RE = re.compile(
33
+ r"^tests\.parity\.support\.reference_adapters\.[a-z_][a-z0-9_]*$"
34
+ )
35
+ _DOCUMENTATION_FRAGMENT_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
36
+ _ALLOWED_ATTENTION = frozenset(
37
+ {"eager", "sdpa", "flex_attention", "flash_attention_2", "flash_attention_3"}
38
+ )
39
+ _ALLOWED_DTYPES = frozenset({"float32", "bfloat16"})
40
+ _ALLOWED_PRECISIONS = frozenset({"default", "auto", "fp32", "bf16", "fp8"})
41
+ _ALLOWED_BF16_EXECUTIONS = frozenset({"static_parameters", "fp32_parameters_autocast"})
42
+ HUB_LICENSE_IDENTIFIERS = frozenset({"mit", "apache-2.0", "cc-by-nc-sa-4.0", "other"})
43
+ _ALLOWED_TOKENIZER_MODES = frozenset({"tokenizer", "sequence", "structure"})
44
+ _ALLOWED_SIZE_CATEGORIES = frozenset({"small", "medium", "large", "xlarge", "structure"})
45
+ RuntimeExtra = Literal["core", "structure"]
46
+ TestTier = Literal["check", "compliance", "structure", "feature", "artifact", "benchmark"]
47
+ VramTier = Literal["sequence", "large-sequence", "structure", "structure-6b"]
48
+ GenerationContract = Literal["not_applicable", "required", "official_unavailable"]
49
+ RuntimeAssetTrustKind = Literal["hash_pinned_pickle"]
50
+ Bf16Execution = Literal["static_parameters", "fp32_parameters_autocast"]
51
+ DtypeName = Literal["float32", "bfloat16"]
52
+ _ALLOWED_EXTRAS = frozenset({"core", "structure"})
53
+ _ALLOWED_TEST_TIERS = frozenset(
54
+ {"check", "compliance", "structure", "feature", "artifact", "benchmark"}
55
+ )
56
+ _ALLOWED_VRAM_TIERS = frozenset({"sequence", "large-sequence", "structure", "structure-6b"})
57
+ _ALLOWED_GENERATION_CONTRACTS = frozenset({"not_applicable", "required", "official_unavailable"})
58
+ _ALLOWED_RUNTIME_ASSET_TRUST_KINDS = frozenset({"hash_pinned_pickle"})
59
+ _ALLOWED_RUNTIME_ASSET_OFFLINE_BEHAVIORS = frozenset({"requires_cached_verified_file"})
60
+ _ALLOWED_AUTO_CLASSES = frozenset(
61
+ {
62
+ "AutoConfig",
63
+ "AutoModel",
64
+ "AutoModelForMaskedLM",
65
+ "AutoModelForProteinFolding",
66
+ "AutoModelForSequenceClassification",
67
+ "AutoModelForSeq2SeqLM",
68
+ "AutoModelForTokenClassification",
69
+ }
70
+ )
71
+ _WEIGHT_SUFFIXES = (".bin", ".ckpt", ".pt", ".pth", ".safetensors")
72
+ _ALLOWED_ORACLE_ASSET_ROLES = frozenset({"weights", "contact_regression"})
73
+ _FAIR_ESM_ASSET_HOST = "dl.fbaipublicfiles.com"
74
+ _ROOT_FIELDS = frozenset(
75
+ {
76
+ "schema_version",
77
+ "legal_files",
78
+ "attention_kernels",
79
+ "upstreams",
80
+ "families",
81
+ "models",
82
+ "runtime_assets",
83
+ }
84
+ )
85
+ _UPSTREAM_FIELDS = frozenset(
86
+ {
87
+ "id",
88
+ "path",
89
+ "url",
90
+ "revision",
91
+ "license",
92
+ "license_files",
93
+ "license_digests",
94
+ "distribution_files",
95
+ }
96
+ )
97
+ _FAMILY_FIELDS = frozenset(
98
+ {
99
+ "architecture",
100
+ "upstreams",
101
+ "tokenizer_mode",
102
+ "public_input",
103
+ "extra",
104
+ "reference_container",
105
+ "reference_adapter",
106
+ "attention",
107
+ "dtypes",
108
+ "bf16_execution",
109
+ "precisions",
110
+ "experimental_precisions",
111
+ "vram_tier",
112
+ "checkpoint_license",
113
+ "hub_license",
114
+ "hub_license_name",
115
+ "hub_license_link",
116
+ "state_transform",
117
+ "conversion_provenance",
118
+ "representative",
119
+ "documentation",
120
+ "test_tiers",
121
+ "runtime_paths",
122
+ "requires_complete_weight_publication",
123
+ "weights_publication_allowed",
124
+ "auto_map",
125
+ "tokenizer_class",
126
+ "backbone_model",
127
+ }
128
+ )
129
+ _MODEL_FIELDS = frozenset(
130
+ {
131
+ "id",
132
+ "family",
133
+ "size_category",
134
+ "generation_contract",
135
+ "fast_repo",
136
+ "fast_revision",
137
+ "fast_files",
138
+ "fast_unresolved_files",
139
+ "official_repo",
140
+ "official_revision",
141
+ "official_files",
142
+ "official_unresolved_files",
143
+ "oracle_assets",
144
+ "official_golden",
145
+ "artifact_source",
146
+ "canonical_state_sha256",
147
+ "tokenizer_source",
148
+ "auto_map",
149
+ "notes",
150
+ "msa_conditioning",
151
+ }
152
+ )
153
+ _RUNTIME_ASSET_FIELDS = frozenset(
154
+ {
155
+ "id",
156
+ "repository",
157
+ "revision",
158
+ "path",
159
+ "sha256",
160
+ "size",
161
+ "consumer_family",
162
+ "trust_kind",
163
+ "license",
164
+ "offline_behavior",
165
+ }
166
+ )
167
+
168
+
169
+ class RegistryError(ValueError):
170
+ """Raised when the model manifest is incomplete or internally inconsistent."""
171
+
172
+
173
+ def _portable_relative_path(value: str, context: str) -> PurePosixPath:
174
+ """Return one normalized cross-platform relative path or fail closed."""
175
+
176
+ posix = PurePosixPath(value)
177
+ windows = PureWindowsPath(value)
178
+ unsafe_windows_part = any(
179
+ part.rstrip(" .") != part
180
+ or part.split(".", maxsplit=1)[0].upper() in _WINDOWS_RESERVED_PATH_NAMES
181
+ or any(
182
+ ord(character) < 32 or character in _WINDOWS_INVALID_PATH_CHARACTERS
183
+ for character in part
184
+ )
185
+ for part in posix.parts
186
+ )
187
+ if (
188
+ not value
189
+ or not posix.parts
190
+ or posix == PurePosixPath(".")
191
+ or posix.is_absolute()
192
+ or windows.is_absolute()
193
+ or windows.drive
194
+ or "\\" in value
195
+ or "." in posix.parts
196
+ or ".." in posix.parts
197
+ or value != posix.as_posix()
198
+ or any(
199
+ part.lower() in {".git", ".cache", "__pycache__"}
200
+ for part in posix.parts
201
+ )
202
+ or unsafe_windows_part
203
+ ):
204
+ raise RegistryError(f"{context} is not portable: {value!r}")
205
+ return posix
206
+
207
+
208
+ @dataclass(frozen=True, slots=True)
209
+ class FileDigest:
210
+ """Expected content identity for one pinned file."""
211
+
212
+ path: str
213
+ algorithm: str
214
+ digest: str
215
+
216
+ @classmethod
217
+ def parse(cls, value: str) -> FileDigest:
218
+ try:
219
+ path, encoded_digest = value.split("=", maxsplit=1)
220
+ algorithm, digest = encoded_digest.split(":", maxsplit=1)
221
+ except ValueError as error:
222
+ raise RegistryError("File digests must use '<path>=<algorithm>:<digest>'.") from error
223
+
224
+ _portable_relative_path(path, "Checkpoint file path")
225
+
226
+ expected_length = {"git-sha1": 40, "sha256": 64}.get(algorithm)
227
+ if expected_length is None:
228
+ raise RegistryError(f"Unsupported file digest algorithm: {algorithm!r}")
229
+ if len(digest) != expected_length or _HEX_RE.fullmatch(digest) is None:
230
+ raise RegistryError(f"Invalid {algorithm} digest for {path!r}: {digest!r}")
231
+ return cls(path=path, algorithm=algorithm, digest=digest)
232
+
233
+ @property
234
+ def encoded(self) -> str:
235
+ return f"{self.algorithm}:{self.digest}"
236
+
237
+
238
+ @dataclass(frozen=True, slots=True)
239
+ class CheckpointSource:
240
+ """One immutable Hugging Face repository snapshot."""
241
+
242
+ repo_id: str
243
+ revision: str
244
+ files: tuple[FileDigest, ...]
245
+ unresolved_files: tuple[str, ...] = ()
246
+
247
+ @property
248
+ def file_map(self) -> Mapping[str, FileDigest]:
249
+ return MappingProxyType({item.path: item for item in self.files})
250
+
251
+
252
+ @dataclass(frozen=True, slots=True)
253
+ class OracleAsset:
254
+ """Hash-pinned external file required by a native parity oracle."""
255
+
256
+ role: str
257
+ path: str
258
+ url: str
259
+ sha256: str
260
+ size: int
261
+
262
+
263
+ @dataclass(frozen=True, slots=True)
264
+ class RuntimeAsset:
265
+ """Immutable runtime data with an explicit deserialization trust boundary."""
266
+
267
+ id: str
268
+ repository: str
269
+ revision: str
270
+ path: str
271
+ sha256: str
272
+ size: int
273
+ consumer_family: str
274
+ trust_kind: RuntimeAssetTrustKind
275
+ license_expression: str
276
+ offline_behavior: str
277
+
278
+
279
+ @dataclass(frozen=True, slots=True)
280
+ class OfficialGolden:
281
+ """Hash-pinned official output bundle required by the check tier."""
282
+
283
+ metadata: FileDigest
284
+ tensors: FileDigest
285
+
286
+
287
+ @dataclass(frozen=True, slots=True)
288
+ class UpstreamSource:
289
+ """Pinned official implementation used as a parity oracle."""
290
+
291
+ id: str
292
+ path: str
293
+ url: str
294
+ revision: str
295
+ license_expression: str
296
+ license_files: tuple[str, ...]
297
+ license_digests: tuple[FileDigest, ...] = ()
298
+ distribution_files: tuple[FileDigest, ...] = ()
299
+
300
+
301
+ @dataclass(frozen=True, slots=True)
302
+ class AttentionKernelSpec:
303
+ """Immutable Hugging Face kernel used by one attention backend."""
304
+
305
+ implementation: str
306
+ repository: str
307
+ revision: str
308
+ version: int
309
+ expected_variant: str
310
+ dtypes: tuple[DtypeName, ...]
311
+
312
+
313
+ @dataclass(frozen=True, slots=True)
314
+ class ModelFamily:
315
+ """Shared runtime and compliance contract for one architecture family."""
316
+
317
+ id: str
318
+ architecture: str
319
+ upstreams: tuple[str, ...]
320
+ tokenizer_mode: str
321
+ public_input: str
322
+ extra: RuntimeExtra
323
+ reference_container: str
324
+ reference_adapter: str
325
+ attention: tuple[str, ...]
326
+ dtypes: tuple[DtypeName, ...]
327
+ bf16_execution: Bf16Execution
328
+ precisions: tuple[str, ...]
329
+ vram_tier: VramTier
330
+ checkpoint_license: str
331
+ hub_license: str
332
+ state_transform: str
333
+ representative: str
334
+ documentation: str
335
+ test_tiers: tuple[TestTier, ...]
336
+ runtime_paths: tuple[str, ...]
337
+ auto_map_items: tuple[tuple[str, str], ...]
338
+ requires_complete_weight_publication: bool = False
339
+ weights_publication_allowed: bool = False
340
+ experimental_precisions: tuple[str, ...] = ()
341
+ tokenizer_class: str | None = None
342
+ hub_license_name: str | None = None
343
+ hub_license_link: str | None = None
344
+ conversion_provenance: str = ""
345
+ backbone_model: str | None = None
346
+
347
+ @property
348
+ def auto_map(self) -> Mapping[str, str]:
349
+ return MappingProxyType(dict(self.auto_map_items))
350
+
351
+ @property
352
+ def hub_license_metadata(self) -> Mapping[str, str]:
353
+ """Return valid Hugging Face model-card license fields."""
354
+
355
+ metadata = {"license": self.hub_license}
356
+ if self.hub_license_name is not None:
357
+ # Hugging Face validates custom license names as lowercase slugs,
358
+ # while the manifest retains the reader-facing display name used
359
+ # in generated prose.
360
+ metadata["license_name"] = _HUB_LICENSE_NAME_RE.sub(
361
+ "-",
362
+ self.hub_license_name.lower(),
363
+ ).strip("-.")
364
+ if self.hub_license_link is not None:
365
+ metadata["license_link"] = self.hub_license_link
366
+ return MappingProxyType(metadata)
367
+
368
+ @property
369
+ def stable_precisions(self) -> tuple[str, ...]:
370
+ """Return precision policies covered by the release contract."""
371
+
372
+ experimental = set(self.experimental_precisions)
373
+ return tuple(precision for precision in self.precisions if precision not in experimental)
374
+
375
+
376
+ @dataclass(frozen=True, slots=True)
377
+ class ModelSpec:
378
+ """Complete immutable source and runtime contract for one checkpoint."""
379
+
380
+ id: str
381
+ family: ModelFamily
382
+ fast: CheckpointSource
383
+ official: CheckpointSource
384
+ size_category: str
385
+ generation_contract: GenerationContract = "not_applicable"
386
+ oracle_assets: tuple[OracleAsset, ...] = ()
387
+ official_golden: OfficialGolden | None = None
388
+ artifact_source: str = "fast"
389
+ canonical_state_sha256: str | None = None
390
+ tokenizer_source_id: str | None = None
391
+ auto_map_items: tuple[tuple[str, str], ...] = ()
392
+ notes: str = ""
393
+ msa_conditioning: bool | None = None
394
+
395
+ @property
396
+ def is_deep_reference(self) -> bool:
397
+ return self.id == self.family.representative
398
+
399
+ @property
400
+ def auto_map(self) -> Mapping[str, str]:
401
+ if self.auto_map_items:
402
+ return MappingProxyType(dict(self.auto_map_items))
403
+ return self.family.auto_map
404
+
405
+ @property
406
+ def artifact_checkpoint(self) -> CheckpointSource:
407
+ """Return the checkpoint selected for local artifact construction."""
408
+
409
+ return self.fast if self.artifact_source == "fast" else self.official
410
+
411
+ @property
412
+ def oracle_asset_map(self) -> Mapping[str, OracleAsset]:
413
+ """Return native oracle assets keyed by their declared role."""
414
+
415
+ return MappingProxyType({asset.role: asset for asset in self.oracle_assets})
416
+
417
+
418
+ class ModelRegistry(Mapping[str, ModelSpec]):
419
+ """Validated mapping of model IDs to typed model specifications."""
420
+
421
+ def __init__(
422
+ self,
423
+ *,
424
+ schema_version: int,
425
+ upstreams: Mapping[str, UpstreamSource],
426
+ families: Mapping[str, ModelFamily],
427
+ models: Mapping[str, ModelSpec],
428
+ runtime_assets: Mapping[str, RuntimeAsset] = MappingProxyType({}),
429
+ attention_kernels: Mapping[str, AttentionKernelSpec] = MappingProxyType({}),
430
+ legal_files: tuple[FileDigest, ...] = (),
431
+ ) -> None:
432
+ self.schema_version = schema_version
433
+ self.upstreams = MappingProxyType(dict(upstreams))
434
+ self.attention_kernels = MappingProxyType(dict(attention_kernels))
435
+ self.families = MappingProxyType(dict(families))
436
+ self._models = MappingProxyType(dict(models))
437
+ self.runtime_assets = MappingProxyType(dict(runtime_assets))
438
+ self.legal_files = legal_files
439
+
440
+ def __getitem__(self, key: str) -> ModelSpec:
441
+ return self._models[key]
442
+
443
+ def __iter__(self) -> Iterator[str]:
444
+ return iter(self._models)
445
+
446
+ def __len__(self) -> int:
447
+ return len(self._models)
448
+
449
+ def by_family(self, family_id: str) -> tuple[ModelSpec, ...]:
450
+ if family_id not in self.families:
451
+ raise KeyError(family_id)
452
+ return tuple(model for model in self._models.values() if model.family.id == family_id)
453
+
454
+ def supported_attention_dtypes(
455
+ self,
456
+ family_id: str,
457
+ implementation: str,
458
+ ) -> tuple[DtypeName, ...]:
459
+ """Return manifest-supported dtypes for one family/backend pair."""
460
+
461
+ family = self.families[family_id]
462
+ if implementation not in family.attention:
463
+ raise KeyError(
464
+ f"Family {family_id!r} does not advertise attention backend "
465
+ f"{implementation!r}."
466
+ )
467
+ kernel = self.attention_kernels.get(implementation)
468
+ if kernel is None:
469
+ return family.dtypes
470
+ return tuple(dtype for dtype in family.dtypes if dtype in kernel.dtypes)
471
+
472
+ def require_resolved(self, model_id: str | None = None) -> None:
473
+ """Fail release validation when required file identities remain unresolved."""
474
+
475
+ selected = self._models.values() if model_id is None else (self._models[model_id],)
476
+ unresolved: list[str] = []
477
+ for model in selected:
478
+ for label, checkpoint in (("fast", model.fast), ("official", model.official)):
479
+ for path in checkpoint.unresolved_files:
480
+ unresolved.append(f"{model.id}.{label}:{path}")
481
+ if unresolved:
482
+ detail = ", ".join(unresolved)
483
+ raise RegistryError(f"Release provenance is unresolved: {detail}")
484
+
485
+
486
+ def _reject_unknown_fields(
487
+ table: Mapping[str, Any],
488
+ allowed: frozenset[str],
489
+ context: str,
490
+ ) -> None:
491
+ unknown = sorted(set(table).difference(allowed))
492
+ if unknown:
493
+ raise RegistryError(f"{context} contains unknown fields: {unknown}.")
494
+
495
+
496
+ def _require_str(table: Mapping[str, Any], key: str, context: str) -> str:
497
+ value = table.get(key)
498
+ if not isinstance(value, str) or not value.strip():
499
+ raise RegistryError(f"{context}.{key} must be a non-empty string.")
500
+ return value
501
+
502
+
503
+ def _require_enum(
504
+ table: Mapping[str, Any],
505
+ key: str,
506
+ context: str,
507
+ allowed: frozenset[str],
508
+ ) -> str:
509
+ value = _require_str(table, key, context)
510
+ if value not in allowed:
511
+ raise RegistryError(
512
+ f"{context}.{key} must be one of {sorted(allowed)}; received {value!r}."
513
+ )
514
+ return value
515
+
516
+
517
+ def _parse_reference_container(table: Mapping[str, Any], context: str) -> str:
518
+ value = _require_str(table, "reference_container", context)
519
+ if _REFERENCE_CONTAINER_RE.fullmatch(value) is None:
520
+ raise RegistryError(
521
+ f"{context}.reference_container must be a portable 'reference-<name>' target."
522
+ )
523
+ return value
524
+
525
+
526
+ def _parse_reference_adapter(table: Mapping[str, Any], context: str) -> str:
527
+ value = _require_str(table, "reference_adapter", context)
528
+ if _REFERENCE_ADAPTER_RE.fullmatch(value) is None:
529
+ raise RegistryError(
530
+ f"{context}.reference_adapter must name one module under "
531
+ "tests.parity.support.reference_adapters."
532
+ )
533
+ return value
534
+
535
+
536
+ def _parse_documentation_path(table: Mapping[str, Any], context: str) -> str:
537
+ value = _require_str(table, "documentation", context)
538
+ if value.count("#") > 1 or "\\" in value:
539
+ raise RegistryError(f"{context}.documentation must be a portable documentation path.")
540
+ raw_path, separator, fragment = value.partition("#")
541
+ path = PurePosixPath(raw_path)
542
+ if (
543
+ path.is_absolute()
544
+ or ".." in path.parts
545
+ or len(path.parts) < 2
546
+ or path.parts[0] != "docs"
547
+ or path.suffix != ".md"
548
+ or path.as_posix() != raw_path
549
+ ):
550
+ raise RegistryError(
551
+ f"{context}.documentation must reference a normalized Markdown file under docs/."
552
+ )
553
+ if separator and _DOCUMENTATION_FRAGMENT_RE.fullmatch(fragment) is None:
554
+ raise RegistryError(f"{context}.documentation has an invalid heading fragment.")
555
+ return value
556
+
557
+
558
+ def _require_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
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:
579
+ value = table.get(key)
580
+ if value is None:
581
+ return None
582
+ if (
583
+ not isinstance(value, str)
584
+ or not value.strip()
585
+ or value != value.strip()
586
+ or "\n" in value
587
+ or "\r" in value
588
+ ):
589
+ raise RegistryError(f"{context}.{key} must be a non-empty single-line string.")
590
+ return value
591
+
592
+
593
+ def _parse_hub_license(
594
+ table: Mapping[str, Any],
595
+ *,
596
+ checkpoint_license: str,
597
+ context: str,
598
+ ) -> tuple[str, str | None, str | None]:
599
+ expected_fields = {"hub_license", "hub_license_name", "hub_license_link"}
600
+ unknown_fields = sorted(
601
+ key for key in table if key.startswith("hub_") and key not in expected_fields
602
+ )
603
+ if unknown_fields:
604
+ raise RegistryError(f"{context} contains unsupported Hub license fields: {unknown_fields}.")
605
+ identifier = _require_str(table, "hub_license", context)
606
+ if identifier not in HUB_LICENSE_IDENTIFIERS:
607
+ raise RegistryError(
608
+ f"{context}.hub_license must be a supported Hugging Face license identifier."
609
+ )
610
+ expected_identifier: str | None = None
611
+ for prefix, candidate in (
612
+ ("MIT", "mit"),
613
+ ("Apache-2.0", "apache-2.0"),
614
+ ("CC-BY-NC-SA-4.0", "cc-by-nc-sa-4.0"),
615
+ ("Profluent-E1-Agreement", "other"),
616
+ ("Unresolved", "other"),
617
+ ):
618
+ if checkpoint_license.startswith(prefix):
619
+ expected_identifier = candidate
620
+ break
621
+ if expected_identifier is None:
622
+ raise RegistryError(
623
+ f"{context}.checkpoint_license has no declared Hugging Face identifier mapping."
624
+ )
625
+ if identifier != expected_identifier:
626
+ raise RegistryError(
627
+ f"{context}.hub_license must be {expected_identifier!r} for "
628
+ f"checkpoint terms {checkpoint_license!r}."
629
+ )
630
+
631
+ name = _optional_str(table, "hub_license_name", context)
632
+ link = _optional_str(table, "hub_license_link", context)
633
+ if identifier != "other":
634
+ if name is not None or link is not None:
635
+ raise RegistryError(
636
+ f"{context} may define hub_license_name and hub_license_link only "
637
+ "when hub_license='other'."
638
+ )
639
+ return identifier, None, None
640
+ if name is None or link is None:
641
+ raise RegistryError(
642
+ f"{context} must define hub_license_name and hub_license_link when hub_license='other'."
643
+ )
644
+ parsed_link = urlparse(link)
645
+ if (
646
+ parsed_link.scheme != "https"
647
+ or not parsed_link.netloc
648
+ or not parsed_link.path
649
+ or parsed_link.username is not None
650
+ or parsed_link.password is not None
651
+ ):
652
+ raise RegistryError(f"{context}.hub_license_link must be an absolute HTTPS URL.")
653
+ return identifier, name, link
654
+
655
+
656
+ 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:
668
+ if len(revision) != 40 or _HEX_RE.fullmatch(revision) is None:
669
+ raise RegistryError(f"{context} must be an immutable 40-character commit revision.")
670
+
671
+
672
+ def _parse_checkpoint(table: Mapping[str, Any], prefix: str, context: str) -> CheckpointSource:
673
+ repo_id = _require_str(table, f"{prefix}_repo", context)
674
+ if _REPOSITORY_ID_RE.fullmatch(repo_id) is None:
675
+ raise RegistryError(f"{context}.{prefix}_repo must be a Hugging Face repository ID.")
676
+ revision = _require_str(table, f"{prefix}_revision", context)
677
+ _validate_revision(revision, f"{context}.{prefix}_revision")
678
+ encoded_files = _require_str_list(table, f"{prefix}_files", context)
679
+ files = tuple(FileDigest.parse(value) for value in encoded_files)
680
+ paths = [item.path for item in files]
681
+ if len(paths) != len(set(paths)):
682
+ raise RegistryError(f"{context}.{prefix}_files contains duplicate paths.")
683
+ if not any(item.path.endswith(_WEIGHT_SUFFIXES) for item in files):
684
+ raise RegistryError(f"{context}.{prefix}_files does not identify a weight file.")
685
+ unresolved_files = _optional_str_list(table, f"{prefix}_unresolved_files", context)
686
+ for unresolved_path in unresolved_files:
687
+ _portable_relative_path(unresolved_path, "Unresolved checkpoint path")
688
+ if unresolved_path in paths:
689
+ raise RegistryError(
690
+ f"{context}.{prefix} marks {unresolved_path!r} both resolved and unresolved."
691
+ )
692
+ return CheckpointSource(
693
+ repo_id=repo_id,
694
+ revision=revision,
695
+ files=files,
696
+ unresolved_files=unresolved_files,
697
+ )
698
+
699
+
700
+ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[OracleAsset, ...]:
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):
708
+ raise RegistryError(f"{asset_context} must be a table.")
709
+ expected_fields = {"role", "path", "url", "sha256", "size"}
710
+ if set(value) != expected_fields:
711
+ raise RegistryError(f"{asset_context} must contain exactly {sorted(expected_fields)}.")
712
+ role = _require_str(value, "role", asset_context)
713
+ if role not in _ALLOWED_ORACLE_ASSET_ROLES:
714
+ raise RegistryError(f"Unsupported oracle asset role: {role!r}.")
715
+ path = _require_str(value, "path", asset_context)
716
+ try:
717
+ normalized_path = _portable_relative_path(path, "Oracle asset path")
718
+ except RegistryError as error:
719
+ raise RegistryError(f"Invalid oracle asset path: {path!r}.") from error
720
+ if normalized_path.suffix != ".pt":
721
+ raise RegistryError(f"Invalid oracle asset path: {path!r}.")
722
+ url = _require_str(value, "url", asset_context)
723
+ parsed_url = urlparse(url)
724
+ if (
725
+ parsed_url.scheme != "https"
726
+ or parsed_url.hostname != _FAIR_ESM_ASSET_HOST
727
+ or parsed_url.path != f"/fair-esm/{path}"
728
+ or parsed_url.params
729
+ or parsed_url.query
730
+ or parsed_url.fragment
731
+ ):
732
+ raise RegistryError(f"Invalid fair-esm oracle asset URL: {url!r}.")
733
+ sha256 = _require_str(value, "sha256", asset_context)
734
+ if len(sha256) != 64 or _HEX_RE.fullmatch(sha256) is None:
735
+ raise RegistryError(f"Invalid oracle asset SHA-256 for {path!r}.")
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,
743
+ url=url,
744
+ sha256=sha256,
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(
761
+ table: Mapping[str, Any],
762
+ model_id: str,
763
+ context: str,
764
+ ) -> OfficialGolden | None:
765
+ raw = table.get("official_golden")
766
+ if raw is None:
767
+ return None
768
+ if not isinstance(raw, dict) or set(raw) != {"metadata", "tensors"}:
769
+ raise RegistryError(
770
+ f"{context}.official_golden must contain exactly 'metadata' and 'tensors'."
771
+ )
772
+ parsed: dict[str, FileDigest] = {}
773
+ for role in ("metadata", "tensors"):
774
+ value = raw[role]
775
+ if not isinstance(value, str):
776
+ raise RegistryError(f"{context}.official_golden.{role} must be a file digest.")
777
+ digest = FileDigest.parse(value)
778
+ if digest.algorithm != "sha256":
779
+ raise RegistryError(
780
+ f"{context}.official_golden.{role} must use an immutable SHA-256 digest."
781
+ )
782
+ expected = f"tests/goldens/{model_id}.{'json' if role == 'metadata' else 'safetensors'}"
783
+ if digest.path != expected:
784
+ raise RegistryError(f"{context}.official_golden.{role} must use path {expected!r}.")
785
+ parsed[role] = digest
786
+ return OfficialGolden(metadata=parsed["metadata"], tensors=parsed["tensors"])
787
+
788
+
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",
796
+ }
797
+ for index, value in enumerate(raw):
798
+ context = f"attention_kernels[{index}]"
799
+ if not isinstance(value, dict):
800
+ raise RegistryError(f"{context} must be a table.")
801
+ expected_fields = frozenset(
802
+ {
803
+ "implementation",
804
+ "repository",
805
+ "revision",
806
+ "version",
807
+ "expected_variant",
808
+ "dtypes",
809
+ }
810
+ )
811
+ _reject_unknown_fields(value, expected_fields, context)
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:
819
+ raise RegistryError(f"Invalid attention-kernel repository {repository!r}.")
820
+ revision = _require_str(value, "revision", context)
821
+ _validate_revision(revision, f"{context}.revision")
822
+ kernel_version = value.get("version")
823
+ if (
824
+ isinstance(kernel_version, bool)
825
+ or not isinstance(kernel_version, int)
826
+ or kernel_version <= 0
827
+ ):
828
+ raise RegistryError(f"{context}.version must be a positive integer.")
829
+ expected_variant = _require_str(value, "expected_variant", context)
830
+ if expected_variant != expected_variants[implementation]:
831
+ raise RegistryError(
832
+ f"{context}.expected_variant must be {expected_variants[implementation]!r}."
833
+ )
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,
841
+ version=kernel_version,
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}]"
857
+ if not isinstance(value, dict):
858
+ raise RegistryError(f"{context} must be a table.")
859
+ _reject_unknown_fields(value, _UPSTREAM_FIELDS, context)
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")
867
+ path = _require_str(value, "path", context)
868
+ try:
869
+ normalized_path = _portable_relative_path(path, f"{context}.path")
870
+ except RegistryError as error:
871
+ raise RegistryError(
872
+ f"{context}.path must be a normalized directory directly under "
873
+ "'vendor/upstream/'."
874
+ ) from error
875
+ if (
876
+ normalized_path.parts[:2] != ("vendor", "upstream")
877
+ or len(normalized_path.parts) != 3
878
+ ):
879
+ raise RegistryError(
880
+ f"{context}.path must be a normalized directory directly under "
881
+ "'vendor/upstream/'."
882
+ )
883
+ if path in paths:
884
+ raise RegistryError(f"Duplicate upstream path: {path!r}")
885
+ paths.add(path)
886
+ url = _require_str(value, "url", context)
887
+ if not url.startswith("https://github.com/") or not url.endswith(".git"):
888
+ raise RegistryError(f"{context}.url must be an HTTPS GitHub clone URL.")
889
+ license_files = _require_str_list(value, "license_files", context)
890
+ license_digests = _require_digest_list(value, "license_digests", context)
891
+ if tuple(item.path for item in license_digests) != license_files:
892
+ raise RegistryError(
893
+ f"{context}.license_digests must cover license_files in the same order."
894
+ )
895
+ distribution_files = _require_digest_list(value, "distribution_files", context)
896
+ distribution_map = {item.path: item for item in distribution_files}
897
+ for canonical in license_digests:
898
+ distributed = distribution_map.get(canonical.path)
899
+ if distributed is None or distributed.encoded != canonical.encoded:
900
+ raise RegistryError(
901
+ f"{context}.distribution_files must include an exact copy of "
902
+ f"{canonical.path!r}."
903
+ )
904
+ if source_id == "e1":
905
+ required_e1 = {
906
+ "LICENSE",
907
+ "ATTRIBUTION",
908
+ "NOTICE",
909
+ "Apache-2.0.txt",
910
+ "BSD-3-Clause.txt",
911
+ "MODIFICATIONS.md",
912
+ }
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,
920
+ revision=revision,
921
+ license_expression=_require_str(value, "license", context),
922
+ license_files=license_files,
923
+ license_digests=license_digests,
924
+ distribution_files=distribution_files,
925
+ )
926
+ return result
927
+
928
+
929
+ def _parse_families(
930
+ raw: object,
931
+ upstreams: Mapping[str, UpstreamSource],
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):
939
+ raise RegistryError(f"Invalid family table: {family_id!r}")
940
+ checkpoint_license = _require_str(value, "checkpoint_license", context)
941
+ hub_license, hub_license_name, hub_license_link = _parse_hub_license(
942
+ value,
943
+ checkpoint_license=checkpoint_license,
944
+ context=context,
945
+ )
946
+ _reject_unknown_fields(value, _FAMILY_FIELDS, context)
947
+ source_ids = _require_str_list(value, "upstreams", context)
948
+ unknown_sources = sorted(set(source_ids).difference(upstreams))
949
+ if unknown_sources:
950
+ raise RegistryError(f"{context} references unknown upstreams: {unknown_sources}")
951
+ tokenizer_mode = _require_str(value, "tokenizer_mode", context)
952
+ if tokenizer_mode not in _ALLOWED_TOKENIZER_MODES:
953
+ raise RegistryError(f"Unsupported tokenizer mode in {context}: {tokenizer_mode!r}")
954
+ public_input = _require_str(value, "public_input", context)
955
+ attention = _require_str_list(value, "attention", context)
956
+ if not set(attention).issubset(_ALLOWED_ATTENTION):
957
+ raise RegistryError(f"Unsupported attention implementation in {context}.")
958
+ dtypes = _require_str_list(value, "dtypes", context)
959
+ if not set(dtypes).issubset(_ALLOWED_DTYPES):
960
+ raise RegistryError(f"Unsupported dtype in {context}.")
961
+ bf16_execution = cast(
962
+ Bf16Execution,
963
+ _require_enum(
964
+ value,
965
+ "bf16_execution",
966
+ context,
967
+ _ALLOWED_BF16_EXECUTIONS,
968
+ ),
969
+ )
970
+ precisions = _require_str_list(value, "precisions", context)
971
+ if not set(precisions).issubset(_ALLOWED_PRECISIONS):
972
+ raise RegistryError(f"Unsupported precision policy in {context}.")
973
+ experimental_precisions = _optional_str_list(
974
+ value,
975
+ "experimental_precisions",
976
+ context,
977
+ )
978
+ unknown_experimental_precisions = sorted(
979
+ set(experimental_precisions).difference(precisions)
980
+ )
981
+ if unknown_experimental_precisions:
982
+ raise RegistryError(
983
+ f"{context}.experimental_precisions must be a subset of precisions; "
984
+ f"unknown values: {unknown_experimental_precisions}."
985
+ )
986
+ extra = cast(RuntimeExtra, _require_enum(value, "extra", context, _ALLOWED_EXTRAS))
987
+ vram_tier = cast(
988
+ VramTier,
989
+ _require_enum(value, "vram_tier", context, _ALLOWED_VRAM_TIERS),
990
+ )
991
+ test_tiers_raw = _require_str_list(value, "test_tiers", context)
992
+ unknown_test_tiers = sorted(set(test_tiers_raw).difference(_ALLOWED_TEST_TIERS))
993
+ if unknown_test_tiers:
994
+ raise RegistryError(
995
+ f"{context}.test_tiers contains unsupported tiers: {unknown_test_tiers}."
996
+ )
997
+ test_tiers = cast(tuple[TestTier, ...], test_tiers_raw)
998
+ reference_container = _parse_reference_container(value, context)
999
+ reference_adapter = _parse_reference_adapter(value, context)
1000
+ documentation = _parse_documentation_path(value, context)
1001
+ runtime_paths = _require_str_list(value, "runtime_paths", context)
1002
+ if len(runtime_paths) != len(set(runtime_paths)):
1003
+ raise RegistryError(f"{context}.runtime_paths must not contain duplicates.")
1004
+ for runtime_path in runtime_paths:
1005
+ try:
1006
+ _portable_relative_path(runtime_path, f"{context}.runtime_paths entry")
1007
+ except RegistryError as error:
1008
+ raise RegistryError(
1009
+ f"Unsafe runtime path in {context}: {runtime_path!r}"
1010
+ ) from error
1011
+ if runtime_path.startswith("vendor/"):
1012
+ raise RegistryError(f"Unsafe runtime path in {context}: {runtime_path!r}")
1013
+ requires_complete_weight_publication = value.get(
1014
+ "requires_complete_weight_publication",
1015
+ False,
1016
+ )
1017
+ if not isinstance(requires_complete_weight_publication, bool):
1018
+ raise RegistryError(
1019
+ f"{context}.requires_complete_weight_publication must be a boolean."
1020
+ )
1021
+ if "weights_publication_allowed" not in value:
1022
+ raise RegistryError(
1023
+ f"{context}.weights_publication_allowed must be declared explicitly."
1024
+ )
1025
+ weights_publication_allowed = value["weights_publication_allowed"]
1026
+ if not isinstance(weights_publication_allowed, bool):
1027
+ raise RegistryError(f"{context}.weights_publication_allowed must be a boolean.")
1028
+ raw_auto_map = value.get("auto_map")
1029
+ if not isinstance(raw_auto_map, dict) or not raw_auto_map:
1030
+ raise RegistryError(f"{context}.auto_map must be a non-empty table.")
1031
+ auto_map: list[tuple[str, str]] = []
1032
+ for auto_class, class_path in raw_auto_map.items():
1033
+ if auto_class not in _ALLOWED_AUTO_CLASSES or not isinstance(class_path, str):
1034
+ raise RegistryError(f"Invalid AutoClass mapping in {context}: {auto_class!r}")
1035
+ if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
1036
+ raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
1037
+ auto_map.append((auto_class, class_path))
1038
+ tokenizer_class = value.get("tokenizer_class")
1039
+ if tokenizer_class is not None:
1040
+ if tokenizer_mode != "tokenizer":
1041
+ raise RegistryError(
1042
+ f"{context}.tokenizer_class requires tokenizer_mode='tokenizer'."
1043
+ )
1044
+ if (
1045
+ not isinstance(tokenizer_class, str)
1046
+ or not tokenizer_class.startswith("fastplms.")
1047
+ or tokenizer_class.count(".") < 2
1048
+ ):
1049
+ raise RegistryError(
1050
+ f"Invalid tokenizer class path in {context}: {tokenizer_class!r}"
1051
+ )
1052
+ backbone_model = value.get("backbone_model")
1053
+ if backbone_model is not None and (
1054
+ not isinstance(backbone_model, str)
1055
+ or _IDENTIFIER_RE.fullmatch(backbone_model) is None
1056
+ ):
1057
+ raise RegistryError(
1058
+ f"{context}.backbone_model must be a valid manifest model ID."
1059
+ )
1060
+ state_transform = _require_str(value, "state_transform", context)
1061
+ conversion_provenance = _require_str(value, "conversion_provenance", context)
1062
+ required_sections = ("Input:", "Transformation:", "Output:", "Validation:", "Limitation:")
1063
+ missing_sections = [
1064
+ section for section in required_sections if section not in conversion_provenance
1065
+ ]
1066
+ if missing_sections or state_transform not in conversion_provenance:
1067
+ raise RegistryError(
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,
1075
+ tokenizer_mode=tokenizer_mode,
1076
+ public_input=public_input,
1077
+ extra=extra,
1078
+ reference_container=reference_container,
1079
+ reference_adapter=reference_adapter,
1080
+ attention=attention,
1081
+ dtypes=cast(tuple[DtypeName, ...], dtypes),
1082
+ bf16_execution=bf16_execution,
1083
+ precisions=precisions,
1084
+ experimental_precisions=experimental_precisions,
1085
+ vram_tier=vram_tier,
1086
+ checkpoint_license=checkpoint_license,
1087
+ hub_license=hub_license,
1088
+ state_transform=state_transform,
1089
+ representative=_require_str(value, "representative", context),
1090
+ documentation=documentation,
1091
+ test_tiers=test_tiers,
1092
+ runtime_paths=runtime_paths,
1093
+ auto_map_items=tuple(auto_map),
1094
+ requires_complete_weight_publication=requires_complete_weight_publication,
1095
+ weights_publication_allowed=weights_publication_allowed,
1096
+ tokenizer_class=tokenizer_class,
1097
+ hub_license_name=hub_license_name,
1098
+ hub_license_link=hub_license_link,
1099
+ conversion_provenance=conversion_provenance,
1100
+ backbone_model=backbone_model,
1101
+ )
1102
+ return result
1103
+
1104
+
1105
+ def _parse_runtime_assets(
1106
+ raw: object,
1107
+ families: Mapping[str, ModelFamily],
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}]"
1115
+ if not isinstance(value, dict):
1116
+ raise RegistryError(f"{context} must be a table.")
1117
+ _reject_unknown_fields(value, _RUNTIME_ASSET_FIELDS, context)
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:
1125
+ raise RegistryError(f"{context}.repository must be a Hugging Face repository ID.")
1126
+ revision = _require_str(value, "revision", context)
1127
+ _validate_revision(revision, f"{context}.revision")
1128
+ path = _require_str(value, "path", context)
1129
+ try:
1130
+ normalized_path = _portable_relative_path(path, "Runtime asset path")
1131
+ except RegistryError as error:
1132
+ raise RegistryError(f"Runtime asset path is not portable: {path!r}") from error
1133
+ sha256 = _require_str(value, "sha256", context)
1134
+ if len(sha256) != 64 or _HEX_RE.fullmatch(sha256) is None:
1135
+ raise RegistryError(f"Invalid runtime asset SHA-256 for {path!r}.")
1136
+ size = value.get("size")
1137
+ if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
1138
+ raise RegistryError(f"{context}.size must be a positive byte count.")
1139
+ consumer_family = _require_str(value, "consumer_family", context)
1140
+ if consumer_family not in families:
1141
+ raise RegistryError(
1142
+ f"{context}.consumer_family references unknown family {consumer_family!r}."
1143
+ )
1144
+ trust_kind = cast(
1145
+ RuntimeAssetTrustKind,
1146
+ _require_enum(
1147
+ value,
1148
+ "trust_kind",
1149
+ context,
1150
+ _ALLOWED_RUNTIME_ASSET_TRUST_KINDS,
1151
+ ),
1152
+ )
1153
+ license_expression = _require_str(value, "license", context)
1154
+ offline_behavior = _require_str(value, "offline_behavior", context)
1155
+ if offline_behavior not in _ALLOWED_RUNTIME_ASSET_OFFLINE_BEHAVIORS:
1156
+ raise RegistryError(
1157
+ f"{context}.offline_behavior is unsupported: {offline_behavior!r}."
1158
+ )
1159
+ if trust_kind == "hash_pinned_pickle" and normalized_path.suffix != ".pkl":
1160
+ raise RegistryError(
1161
+ f"{context}.path must end in '.pkl' for trust_kind='hash_pinned_pickle'."
1162
+ )
1163
+ identity = (repository, revision, path)
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,
1171
+ path=path,
1172
+ sha256=sha256,
1173
+ size=size,
1174
+ consumer_family=consumer_family,
1175
+ trust_kind=trust_kind,
1176
+ license_expression=license_expression,
1177
+ offline_behavior=offline_behavior,
1178
+ )
1179
+ return result
1180
+
1181
+
1182
+ def _parse_models(
1183
+ raw: object,
1184
+ families: Mapping[str, ModelFamily],
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}]"
1192
+ if not isinstance(value, dict):
1193
+ raise RegistryError(f"{context} must be a table.")
1194
+ _reject_unknown_fields(value, _MODEL_FIELDS, context)
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:
1202
+ raise RegistryError(f"{context} references unknown family {family_id!r}.")
1203
+ fast = _parse_checkpoint(value, "fast", context)
1204
+ official = _parse_checkpoint(value, "official", context)
1205
+ if fast.repo_id in fast_repositories:
1206
+ raise RegistryError(f"Duplicate FastPLMs repository ID: {fast.repo_id!r}")
1207
+ fast_repositories.add(fast.repo_id)
1208
+ family = families[family_id]
1209
+ oracle_assets = _parse_oracle_assets(value, context)
1210
+ official_golden = _parse_official_golden(value, model_id, context)
1211
+ size_category = _require_str(value, "size_category", context)
1212
+ if size_category not in _ALLOWED_SIZE_CATEGORIES:
1213
+ raise RegistryError(f"Unsupported size category in {context}: {size_category!r}")
1214
+ generation_contract = cast(
1215
+ GenerationContract,
1216
+ _require_enum(
1217
+ value,
1218
+ "generation_contract",
1219
+ context,
1220
+ _ALLOWED_GENERATION_CONTRACTS,
1221
+ ),
1222
+ )
1223
+ if family.tokenizer_mode == "structure" and size_category != "structure":
1224
+ raise RegistryError(
1225
+ f"Structure checkpoint {model_id!r} must use size_category='structure'."
1226
+ )
1227
+ artifact_source = value.get("artifact_source", "fast")
1228
+ if artifact_source not in {"fast", "official"}:
1229
+ raise RegistryError(f"{context}.artifact_source must be 'fast' or 'official'.")
1230
+ canonical_state_sha256 = value.get("canonical_state_sha256")
1231
+ if artifact_source == "official":
1232
+ if (
1233
+ not isinstance(canonical_state_sha256, str)
1234
+ or len(canonical_state_sha256) != 64
1235
+ or _HEX_RE.fullmatch(canonical_state_sha256) is None
1236
+ ):
1237
+ raise RegistryError(
1238
+ f"{context}.canonical_state_sha256 must be a SHA-256 commitment "
1239
+ "for an official-source artifact."
1240
+ )
1241
+ elif canonical_state_sha256 is not None:
1242
+ raise RegistryError(
1243
+ f"{context}.canonical_state_sha256 is restricted to official-source artifacts."
1244
+ )
1245
+ if family.tokenizer_mode == "tokenizer" and not any(
1246
+ "tokenizer" in item.path or "vocab" in item.path for item in fast.files
1247
+ ):
1248
+ raise RegistryError(f"{context} does not pin a tokenizer asset.")
1249
+ tokenizer_source_id = value.get("tokenizer_source")
1250
+ if tokenizer_source_id is not None and (
1251
+ family.tokenizer_mode != "tokenizer"
1252
+ or not isinstance(tokenizer_source_id, str)
1253
+ or _IDENTIFIER_RE.fullmatch(tokenizer_source_id) is None
1254
+ ):
1255
+ raise RegistryError(f"{context}.tokenizer_source is invalid.")
1256
+ notes = value.get("notes", "")
1257
+ if not isinstance(notes, str):
1258
+ raise RegistryError(f"{context}.notes must be a string.")
1259
+ msa_conditioning = value.get("msa_conditioning")
1260
+ if family_id == "esmfold2":
1261
+ if not isinstance(msa_conditioning, bool):
1262
+ raise RegistryError(
1263
+ f"{context}.msa_conditioning must be an explicit boolean for "
1264
+ "ESMFold2 checkpoints."
1265
+ )
1266
+ elif "msa_conditioning" in value:
1267
+ raise RegistryError(
1268
+ f"{context}.msa_conditioning is only valid for ESMFold2 checkpoints."
1269
+ )
1270
+ raw_auto_map = value.get("auto_map")
1271
+ auto_map: list[tuple[str, str]] = []
1272
+ if raw_auto_map is not None:
1273
+ if not isinstance(raw_auto_map, dict) or not raw_auto_map:
1274
+ raise RegistryError(f"{context}.auto_map must be a non-empty table.")
1275
+ for auto_class, class_path in raw_auto_map.items():
1276
+ if auto_class not in _ALLOWED_AUTO_CLASSES or not isinstance(class_path, str):
1277
+ raise RegistryError(f"Invalid AutoClass mapping in {context}: {auto_class!r}")
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,
1285
+ official=official,
1286
+ size_category=size_category,
1287
+ generation_contract=generation_contract,
1288
+ oracle_assets=oracle_assets,
1289
+ official_golden=official_golden,
1290
+ artifact_source=artifact_source,
1291
+ canonical_state_sha256=canonical_state_sha256,
1292
+ tokenizer_source_id=tokenizer_source_id,
1293
+ auto_map_items=tuple(auto_map),
1294
+ notes=notes,
1295
+ msa_conditioning=msa_conditioning,
1296
+ )
1297
+ return result
1298
+
1299
+
1300
+ def _validate_registry(
1301
+ upstreams: Mapping[str, UpstreamSource],
1302
+ attention_kernels: Mapping[str, AttentionKernelSpec],
1303
+ families: Mapping[str, ModelFamily],
1304
+ models: Mapping[str, ModelSpec],
1305
+ ) -> None:
1306
+ for spec in models.values():
1307
+ if spec.tokenizer_source_id is None:
1308
+ continue
1309
+ source = models.get(spec.tokenizer_source_id)
1310
+ if source is None:
1311
+ raise RegistryError(
1312
+ f"Model {spec.id!r} references unknown tokenizer source "
1313
+ f"{spec.tokenizer_source_id!r}."
1314
+ )
1315
+ if not any(
1316
+ PurePosixPath(item.path).name
1317
+ in {
1318
+ "added_tokens.json",
1319
+ "merges.txt",
1320
+ "sentencepiece.bpe.model",
1321
+ "special_tokens_map.json",
1322
+ "spiece.model",
1323
+ "tokenizer.json",
1324
+ "tokenizer_config.json",
1325
+ "vocab.json",
1326
+ "vocab.txt",
1327
+ }
1328
+ for item in source.official.files
1329
+ ):
1330
+ raise RegistryError(
1331
+ f"Tokenizer source {source.id!r} has no official tokenizer assets."
1332
+ )
1333
+ expected_esmfold2 = {
1334
+ "esmfold2": ("Synthyra/ESMFold2", "biohub/ESMFold2"),
1335
+ "esmfold2_fast": ("Synthyra/ESMFold2-Fast", "biohub/ESMFold2-Fast"),
1336
+ "esmfold2_experimental_cutoff2025": (
1337
+ "Synthyra/ESMFold2-Experimental-Cutoff2025",
1338
+ "biohub/ESMFold2-Experimental-Cutoff2025",
1339
+ ),
1340
+ "esmfold2_experimental_fast_cutoff2025": (
1341
+ "Synthyra/ESMFold2-Experimental-Fast-Cutoff2025",
1342
+ "biohub/ESMFold2-Experimental-Fast-Cutoff2025",
1343
+ ),
1344
+ }
1345
+ actual_esmfold2 = {
1346
+ model.id: (model.fast.repo_id, model.official.repo_id)
1347
+ for model in models.values()
1348
+ if model.family.id == "esmfold2"
1349
+ }
1350
+ if actual_esmfold2 != expected_esmfold2:
1351
+ raise RegistryError(
1352
+ "ESMFold2 support must contain exactly the four approved model IDs and "
1353
+ "official/Synthyra repositories."
1354
+ )
1355
+
1356
+ golden_paths: list[str] = []
1357
+ for model in models.values():
1358
+ if model.official_golden is not None:
1359
+ golden_paths.extend(
1360
+ (
1361
+ model.official_golden.metadata.path,
1362
+ model.official_golden.tensors.path,
1363
+ )
1364
+ )
1365
+ if len(golden_paths) != len(set(golden_paths)):
1366
+ raise RegistryError("Official golden paths must be unique across model declarations.")
1367
+ unused_upstreams = sorted(
1368
+ set(upstreams).difference(
1369
+ source for family in families.values() for source in family.upstreams
1370
+ )
1371
+ )
1372
+ if unused_upstreams:
1373
+ raise RegistryError(
1374
+ f"Upstream sources are not connected to a model family: {unused_upstreams}"
1375
+ )
1376
+ advertised_flash = {
1377
+ implementation
1378
+ for family in families.values()
1379
+ for implementation in family.attention
1380
+ if implementation.startswith("flash_attention_")
1381
+ }
1382
+ missing_kernels = sorted(advertised_flash.difference(attention_kernels))
1383
+ if missing_kernels:
1384
+ raise RegistryError(
1385
+ f"Advertised FlashAttention backends lack kernel specs: {missing_kernels}."
1386
+ )
1387
+ for family in families.values():
1388
+ for implementation in family.attention:
1389
+ kernel = attention_kernels.get(implementation)
1390
+ if kernel is not None and not set(family.dtypes).intersection(kernel.dtypes):
1391
+ raise RegistryError(
1392
+ f"Family {family.id!r} and attention kernel {implementation!r} "
1393
+ "have no supported dtype in common."
1394
+ )
1395
+ family_models = [model for model in models.values() if model.family.id == family.id]
1396
+ if not family_models:
1397
+ raise RegistryError(f"Family {family.id!r} has no checkpoints.")
1398
+ representative = models.get(family.representative)
1399
+ if representative is None or representative.family.id != family.id:
1400
+ raise RegistryError(
1401
+ f"Family {family.id!r} has invalid representative {family.representative!r}."
1402
+ )
1403
+ if family.backbone_model is not None and family.backbone_model not in models:
1404
+ raise RegistryError(
1405
+ f"Family {family.id!r} references unknown backbone model "
1406
+ f"{family.backbone_model!r}."
1407
+ )
1408
+
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,
1430
+ upstreams=upstreams,
1431
+ attention_kernels=attention_kernels,
1432
+ families=families,
1433
+ models=models,
1434
+ runtime_assets=runtime_assets,
1435
+ legal_files=legal_files,
1436
+ )
1437
+
1438
+
1439
+ def load_model_registry(path: str | Path | None = None) -> ModelRegistry:
1440
+ """Load and validate a model manifest without importing model code."""
1441
+
1442
+ if path is None:
1443
+ manifest = resources.files("fastplms").joinpath("models.toml")
1444
+ return _load_manifest_bytes(manifest.read_bytes())
1445
+ return _load_manifest_bytes(Path(path).read_bytes())
1446
+
1447
+
1448
+ @lru_cache(maxsize=1)
1449
+ def get_model_registry() -> ModelRegistry:
1450
+ """Return the validated package registry, cached after its first read."""
1451
+
1452
+ return load_model_registry()
1453
+
1454
+
1455
+ def get_model_spec(model_id: str) -> ModelSpec:
1456
+ """Return one model specification by its stable manifest ID."""
1457
+
1458
+ try:
1459
+ return get_model_registry()[model_id]
1460
+ except KeyError as error:
1461
+ supported = ", ".join(get_model_registry())
1462
+ raise KeyError(
1463
+ f"Unknown FastPLMs model ID {model_id!r}. Supported IDs: {supported}"
1464
+ ) from error
1465
+
1466
+
1467
+ __all__ = [
1468
+ "HUB_LICENSE_IDENTIFIERS",
1469
+ "CheckpointSource",
1470
+ "FileDigest",
1471
+ "GenerationContract",
1472
+ "ModelFamily",
1473
+ "ModelRegistry",
1474
+ "ModelSpec",
1475
+ "OracleAsset",
1476
+ "RegistryError",
1477
+ "RuntimeAsset",
1478
+ "RuntimeAssetTrustKind",
1479
+ "RuntimeExtra",
1480
+ "TestTier",
1481
+ "UpstreamSource",
1482
+ "VramTier",
1483
+ "get_model_registry",
1484
+ "get_model_spec",
1485
+ "load_model_registry",
1486
+ ]
fastplms/runtime.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Explicit, reversible Torch runtime configuration.
2
+
3
+ Importing FastPLMs does not change global Torch settings. Callers that want a
4
+ runtime profile opt in with :func:`runtime_profile` and receive their previous
5
+ settings back when the context exits.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ 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
+
17
+
18
+ MatmulPrecision = Literal["highest", "high", "medium"]
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class RuntimeProfile:
23
+ """Requested Torch settings for a bounded inference or training block."""
24
+
25
+ float32_matmul_precision: MatmulPrecision = "highest"
26
+ allow_tf32: bool | None = None
27
+
28
+
29
+ @contextmanager
30
+ def runtime_profile(profile: RuntimeProfile | None = None) -> Iterator[None]:
31
+ """Apply a Torch runtime profile and restore the previous global settings.
32
+
33
+ The default profile requests the highest float32 matrix-multiplication
34
+ precision and leaves TF32 policy unchanged. Torch is imported only when the
35
+ context is entered.
36
+ """
37
+
38
+ import torch
39
+
40
+ selected = profile or RuntimeProfile()
41
+ previous_matmul_precision = torch.get_float32_matmul_precision()
42
+ matmul_backend = getattr(getattr(torch.backends, "cuda", None), "matmul", None)
43
+ cudnn_backend = getattr(torch.backends, "cudnn", None)
44
+ previous_matmul_tf32 = (
45
+ getattr(matmul_backend, "allow_tf32", None) if matmul_backend is not None else None
46
+ )
47
+ previous_cudnn_tf32 = (
48
+ getattr(cudnn_backend, "allow_tf32", None) if cudnn_backend is not None else None
49
+ )
50
+
51
+ torch.set_float32_matmul_precision(selected.float32_matmul_precision)
52
+ if selected.allow_tf32 is not None:
53
+ if matmul_backend is not None and hasattr(matmul_backend, "allow_tf32"):
54
+ matmul_backend.allow_tf32 = selected.allow_tf32
55
+ if cudnn_backend is not None and hasattr(cudnn_backend, "allow_tf32"):
56
+ cudnn_backend.allow_tf32 = selected.allow_tf32
57
+ try:
58
+ yield
59
+ finally:
60
+ torch.set_float32_matmul_precision(previous_matmul_precision)
61
+ if selected.allow_tf32 is not None:
62
+ if matmul_backend is not None and previous_matmul_tf32 is not None:
63
+ matmul_backend.allow_tf32 = previous_matmul_tf32
64
+ if cudnn_backend is not None and previous_cudnn_tf32 is not None:
65
+ cudnn_backend.allow_tf32 = previous_cudnn_tf32
66
+
67
+
68
+ __all__ = ["MatmulPrecision", "RuntimeProfile", "runtime_profile"]
fastplms_bundle.py ADDED
The diff for this file is too large to render. See raw diff
 
modeling_fastplms.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generated bridge to the unchanged FastPLMs package sources."""
2
+
3
+ import base64
4
+ import hashlib
5
+ 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 != "297af2aeeb817b6e3a343bf73dbc2eb3b3e29a2ca88ccba8af1aeb2e12243ffd":
17
+ raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
18
+
19
+ _RUNTIME_TEMPORARIES = []
20
+
21
+ def _archive_runtime_hashes(payload):
22
+ result = {}
23
+ with ZipFile(BytesIO(payload)) as archive:
24
+ for member in archive.infolist():
25
+ name = member.filename
26
+ parts = Path(name).parts
27
+ if (
28
+ member.is_dir()
29
+ or "\\" in name
30
+ or not parts
31
+ or parts[0] != "fastplms"
32
+ or len(parts) < 2
33
+ or any(part in {"", ".", ".."} for part in parts)
34
+ or Path(name).suffix in {".pyc", ".pyo"}
35
+ or member.flag_bits & 0x1
36
+ or member.compress_type != ZIP_DEFLATED
37
+ or member.external_attr >> 16 != 0o100644
38
+ ):
39
+ raise RuntimeError("Embedded FastPLMs archive has an unsafe path.")
40
+ relative = Path(*parts[1:]).as_posix()
41
+ if relative in result:
42
+ raise RuntimeError("Embedded FastPLMs archive repeats a path.")
43
+ result[relative] = hashlib.sha256(archive.read(member)).hexdigest()
44
+ return result
45
+
46
+ def _ensure_runtime():
47
+ payload = base64.b85decode("".join(RUNTIME_DATA))
48
+ if hashlib.sha256(payload).hexdigest() != RUNTIME_HASH:
49
+ raise RuntimeError("Embedded FastPLMs runtime hash mismatch.")
50
+ expected = _archive_runtime_hashes(payload)
51
+ temporary = tempfile.TemporaryDirectory(prefix="fastplms-artifact-runtime-")
52
+ try:
53
+ runtime_root = Path(temporary.name)
54
+ with ZipFile(BytesIO(payload)) as archive:
55
+ for member in archive.infolist():
56
+ target = runtime_root.joinpath(*Path(member.filename).parts)
57
+ target.parent.mkdir(parents=True, exist_ok=True)
58
+ with target.open("xb") as handle:
59
+ handle.write(archive.read(member))
60
+ package_root = runtime_root / "fastplms"
61
+ if _runtime_file_hashes(package_root) != expected:
62
+ raise RuntimeError(
63
+ "Private FastPLMs runtime differs from the embedded archive."
64
+ )
65
+ except BaseException:
66
+ temporary.cleanup()
67
+ raise
68
+ _RUNTIME_TEMPORARIES.append(temporary)
69
+ return package_root
70
+
71
+ def _runtime_file_hashes(package_root):
72
+ result = {}
73
+ for path in sorted(package_root.rglob("*")):
74
+ relative = path.relative_to(package_root)
75
+ if path.is_symlink():
76
+ raise RuntimeError("Private FastPLMs runtime contains a symlink.")
77
+ if path.is_dir():
78
+ continue
79
+ if path.suffix in {".pyc", ".pyo"}:
80
+ raise RuntimeError("Private FastPLMs runtime contains bytecode.")
81
+ if not path.is_file():
82
+ raise RuntimeError("Private FastPLMs runtime contains a non-file entry.")
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."):
107
+ continue
108
+ paths = getattr(module, "__path__", None)
109
+ if paths is None:
110
+ continue
111
+ relative = name.split(".")[1:]
112
+ candidate = package_root.joinpath(*relative)
113
+ candidate_text = str(candidate)
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()
143
+ if relative in known and known[relative] != digest
144
+ )
145
+ if conflicts:
146
+ raise RuntimeError(
147
+ "FastPLMs artifacts contain incompatible runtime sources at "
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
183
+ sys.dont_write_bytecode = True
184
+ try:
185
+ return importlib.import_module(module_name)
186
+ finally:
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",
200
+ submodule_search_locations=[str(package_root)],
201
+ )
202
+ if spec is None or spec.loader is None:
203
+ raise ImportError("Unable to load the embedded FastPLMs runtime.")
204
+ package = importlib.util.module_from_spec(spec)
205
+ package.__fastplms_artifact_runtime_hash__ = RUNTIME_HASH
206
+ package.__fastplms_artifact_runtime_hashes__ = frozenset({RUNTIME_HASH})
207
+ package.__fastplms_artifact_runtime_files__ = _runtime_file_hashes(package_root)
208
+ package.__fastplms_artifact_runtime_roots__ = (str(package_root),)
209
+ package.__fastplms_artifact_runtime_temporaries__ = tuple(
210
+ _RUNTIME_TEMPORARIES
211
+ )
212
+ sys.modules["fastplms"] = package
213
+ previous = sys.dont_write_bytecode
214
+ sys.dont_write_bytecode = True
215
+ try:
216
+ try:
217
+ spec.loader.exec_module(package)
218
+ except BaseException:
219
+ sys.modules.pop("fastplms", None)
220
+ raise
221
+ finally:
222
+ sys.dont_write_bytecode = previous
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__
runtime-attestation.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": {
3
+ "LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
4
+ "LICENSES/boltz/LICENSE": "sha256:f0667fd5e66c51e1ba8ddaa0249c6d7225b30037e02c45782d8f2c2943ac2617",
5
+ "README.md": "sha256:49eaec9931001fa9175755c386a28403a5215f3ef7e79f9331d9442246785614",
6
+ "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
7
+ "config.json": "sha256:f285511527e78e8800f52f948cf6b1779ac2444c84ee60f25c53ba098ab343af",
8
+ "fastplms/__init__.py": "sha256:4fb3196022ca8ec699d59d09bdbc5f0184195552b773698ab9b061fe3cd7df12",
9
+ "fastplms/models.toml": "sha256:05a8399f084a5babb5f0916cee7e564c4030767f3ff0230c4f46e539209847d1",
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:f432c0047654b903f893db123760044e99b5a4caf176c62807a78c0d20c3fa33",
44
+ "modeling_fastplms.py": "sha256:29988e291b3f7a779d722a37d8acec6114c130fe389feae43fb6485e58a2cccf"
45
+ },
46
+ "model_id": "boltz2",
47
+ "redistributable": true,
48
+ "release_tool_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
49
+ "release_tool_sha256": "1459b5d7d13d9b07bd97b3eee764f2ce73623e15e32d07ddf6825c2a9509afb9",
50
+ "runtime_bundle_sha256": "297af2aeeb817b6e3a343bf73dbc2eb3b3e29a2ca88ccba8af1aeb2e12243ffd",
51
+ "runtime_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
52
+ "schema_version": 2,
53
+ "scope": "runtime-only",
54
+ "source_tree_sha256": "21579947908f914e1b38ff1e69f9df9d22b7798899090355a53691a937b024e5",
55
+ "weights": {
56
+ "repo_id": "Synthyra/Boltz2",
57
+ "revision": "3b148fc5efea109c065ec82ba8683d024de7134e"
58
+ },
59
+ "weights_license_status": "resolved"
60
+ }