Feature Extraction
Transformers
PyTorch
Safetensors
boltz2_automodel
protein-language-model
fastplms
custom_code
Instructions to use Synthyra/Boltz2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Synthyra/Boltz2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="Synthyra/Boltz2", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Synthyra/Boltz2", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Update FastPLMs runtime files
Browse filesAdd-only FastPLMs files-only publication. Checkpoint weights and complete-artifact attestations are unchanged.
- LICENSES/FastPLMs-Apache-2.0.txt +203 -0
- LICENSES/boltz/LICENSE +21 -0
- README.md +143 -164
- THIRD_PARTY_NOTICES.md +99 -0
- config.json +12 -2
- fastplms/__init__.py +48 -0
- fastplms/models.toml +1223 -0
- fastplms/models/__init__.py +10 -0
- fastplms/models/boltz/__init__.py +13 -0
- fastplms/models/boltz/_pair_attention.py +46 -0
- fastplms/models/boltz/cif_writer.py +141 -0
- fastplms/models/boltz/minimal_featurizer.py +786 -0
- fastplms/models/boltz/minimal_structures.py +19 -0
- fastplms/models/boltz/modeling_boltz2.py +993 -0
- fastplms/models/boltz/vb_const.py +353 -0
- fastplms/models/boltz/vb_layers_attention.py +98 -0
- fastplms/models/boltz/vb_layers_attentionv2.py +73 -0
- fastplms/models/boltz/vb_layers_confidence_utils.py +226 -0
- fastplms/models/boltz/vb_layers_dropout.py +40 -0
- fastplms/models/boltz/vb_layers_initialize.py +98 -0
- fastplms/models/boltz/vb_layers_outer_product_mean.py +87 -0
- fastplms/models/boltz/vb_layers_pair_averaging.py +91 -0
- fastplms/models/boltz/vb_layers_pairformer.py +337 -0
- fastplms/models/boltz/vb_layers_transition.py +69 -0
- fastplms/models/boltz/vb_layers_triangular_mult.py +125 -0
- fastplms/models/boltz/vb_loss_diffusionv2.py +178 -0
- fastplms/models/boltz/vb_modules_confidencev2.py +542 -0
- fastplms/models/boltz/vb_modules_diffusion_conditioning.py +137 -0
- fastplms/models/boltz/vb_modules_diffusionv2.py +654 -0
- fastplms/models/boltz/vb_modules_encodersv2.py +577 -0
- fastplms/models/boltz/vb_modules_transformersv2.py +254 -0
- fastplms/models/boltz/vb_modules_trunkv2.py +595 -0
- fastplms/models/boltz/vb_modules_utils.py +257 -0
- fastplms/models/boltz/vb_potentials_potentials.py +845 -0
- fastplms/models/boltz/vb_potentials_schedules.py +67 -0
- fastplms/models/boltz/vb_tri_attn_attention.py +145 -0
- fastplms/models/boltz/vb_tri_attn_primitives.py +237 -0
- fastplms/models/boltz/vb_tri_attn_utils.py +324 -0
- fastplms/registry.py +1486 -0
- fastplms/runtime.py +68 -0
- fastplms_bundle.py +0 -0
- modeling_fastplms.py +230 -0
- 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
```
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
-
|
| 89 |
-
-
|
| 90 |
-
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
```
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
``
|
| 103 |
-
|
| 104 |
-
``
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
- `
|
| 114 |
-
- `
|
| 115 |
-
- `
|
| 116 |
-
- `
|
| 117 |
-
- `
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
-
|
| 121 |
-
-
|
| 122 |
-
- `
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 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": "
|
| 7 |
-
"AutoModel": "
|
| 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 |
+
}
|