wuxing0105 commited on
Commit
1fdc49a
·
verified ·
1 Parent(s): e353e32

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. .gitignore +136 -0
  3. .ms_upload_cache +1 -0
  4. LICENSE.md +24 -0
  5. MANIFEST.in +2 -0
  6. README.md +459 -0
  7. conf/config.json +8 -0
  8. configuration.json +13 -0
  9. model/IgFoldRunner.py +158 -0
  10. model/__init__.py +3 -0
  11. model/model/IgFold.py +488 -0
  12. model/model/__init__.py +0 -0
  13. model/model/components/GraphTransformer.py +255 -0
  14. model/model/components/IPABlock.py +248 -0
  15. model/model/components/IPATransformer.py +174 -0
  16. model/model/components/TriangleGraphTransformer.py +62 -0
  17. model/model/components/TriangleMultiplicativeModule.py +109 -0
  18. model/model/components/__init__.py +4 -0
  19. model/model/interface.py +40 -0
  20. model/refine/__init__.py +0 -0
  21. model/refine/openmm_ref.py +44 -0
  22. model/refine/pyrosetta_ref.py +125 -0
  23. model/training/__init__.py +0 -0
  24. model/training/utils.py +287 -0
  25. model/utils/__init__.py +0 -0
  26. model/utils/ab_metrics.py +60 -0
  27. model/utils/abnumber_.py +88 -0
  28. model/utils/constants.py +22 -0
  29. model/utils/coordinates.py +76 -0
  30. model/utils/embed.py +53 -0
  31. model/utils/fasta.py +43 -0
  32. model/utils/folding.py +231 -0
  33. model/utils/general.py +57 -0
  34. model/utils/geometry.py +77 -0
  35. model/utils/pdb.py +515 -0
  36. model/utils/tensor.py +42 -0
  37. model/utils/transforms.py +520 -0
  38. model/utils/visualize.py +183 -0
  39. pyproject.toml +3 -0
  40. requirements.txt +16 -0
  41. scripts/IgFold.ipynb +199 -0
  42. scripts/inference.py +88 -0
  43. setup.cfg +20 -0
  44. weight/IgFold/LICENSE.md +24 -0
  45. weight/IgFold/igfold_1.ckpt +3 -0
  46. weight/IgFold/igfold_2.ckpt +3 -0
  47. weight/IgFold/igfold_3.ckpt +3 -0
  48. weight/IgFold/igfold_5.ckpt +3 -0
  49. weight/wheels/antiberty-0.1.3-py3-none-any.whl +3 -0
  50. weight/wheels/igfold-0.4.0-py3-none-any.whl +3 -0
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ weight/wheels/antiberty-0.1.3-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
37
+ weight/wheels/igfold-0.4.0-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
38
+ weight/wheels/setuptools-81.0.0-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ # Other
132
+ .DS_Store
133
+ .vscode/
134
+ trained_models/
135
+ demo_data/
136
+ scripts/
.ms_upload_cache ADDED
@@ -0,0 +1 @@
 
 
1
+ {"version": 3, "repo_id": "OneScience/IgFold", "files": {".gitignore|1784526611.0|1862": {"hash": "5ed5ad2f1435210a66884db6ef33f08eac7b6fdfba7cf2b596e8e17bda93db90", "size": 1862, "status": "c"}, "README.md|1787713837.0|11346": {"hash": "cc24643909ffc55c453b64627fd24187513cc13eead8bf7bbbc9588df5d2b25b", "size": 11346, "status": "c"}, "MANIFEST.in|1787713268.0|55": {"hash": "4f09d2f77ba3b962461deeb420affa542b90585d86fce4d90691b445172e4845", "size": 55, "status": "c"}, "conf/config.json|1787713764.0|150": {"hash": "94e90467e86bbccb199440403341bcc42c6a865c7027b6b4d806e8646b5aff3d", "size": 150, "status": "c"}, "configuration.json|1787713766.0|335": {"hash": "65ca5d35ec4141fcfa197add0014d91d5879bf434b72dedf2b2fc9ad720a846a", "size": 335, "status": "c"}, "model/IgFoldRunner.py|1787713781.0|4907": {"hash": "3629ab4bf7789778cb6890b73bffb89ea9a049df04452b6ec1ab8ae70e1bfb18", "size": 4907, "status": "c"}, "LICENSE.md|1784526611.0|3330": {"hash": "02cd4b17669c8294425925009ccdf6a6a6c8bef1486d580d5288ee7837d11fca", "size": 3330, "status": "c"}, "model/model/__init__.py|1784526611.0|0": {"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0, "status": "c"}, "model/model/IgFold.py|1784526611.0|15121": {"hash": "1510dcc0d76cd149719843e930545b784621bdfbbeb4779a3e77ba8f44cb0027", "size": 15121, "status": "c"}, "model/__init__.py|1784526611.0|126": {"hash": "a284b2be34a18aa671e4fc984bde975832133d5fb41a2eafa45fc09952d1ffe7", "size": 126, "status": "c"}, "model/model/components/GraphTransformer.py|1784526611.0|5439": {"hash": "8f1d561ef520c0f04d634200261a449c0e5ef67979365457c2088d065717c104", "size": 5439, "status": "c"}, "model/model/components/TriangleGraphTransformer.py|1784526611.0|1763": {"hash": "d0b6f107416c2269764bf11a99a5b53ed1f21e4a64b36d4f935e062e47c8fa19", "size": 1763, "status": "c"}, "model/model/components/IPABlock.py|1784526611.0|8775": {"hash": "6f224f9cf09e450ed7ac24b379ce16ea4abbdc53909591a92b460cfc6f64ca5e", "size": 8775, "status": "c"}, "model/refine/__init__.py|1784526611.0|0": {"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0, "status": "c"}, "model/model/components/IPATransformer.py|1784526611.0|4603": {"hash": "4d3a943699c886c84eaeb64f83f17c627da91bb1b376bee90bf08cc2eb208af1", "size": 4603, "status": "c"}, "model/training/__init__.py|1784526611.0|0": {"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0, "status": "c"}, "model/model/components/TriangleMultiplicativeModule.py|1784526611.0|2648": {"hash": "85769957c68d4f7131d2d3d4df00e516efcc648c7b622a890be62f2a3b5ec349", "size": 2648, "status": "c"}, "model/model/interface.py|1784526611.0|1248": {"hash": "4b7b0f1c7b2a961915a3abacff5c134371f4c575fbb935683ae24f8aceededef", "size": 1248, "status": "c"}, "model/utils/__init__.py|1784526611.0|0": {"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0, "status": "c"}, "model/model/components/__init__.py|1784526611.0|146": {"hash": "90edde297923dc2de725e0ca765f99d8fe92e26b14ef26809d5ffeba995b4122", "size": 146, "status": "c"}, "model/refine/openmm_ref.py|1784526611.0|1653": {"hash": "083d98ec88c711cb3d96be786a7693a443ca62d3b22f6d9e1ebe407ce37a5657", "size": 1653, "status": "c"}, "model/refine/pyrosetta_ref.py|1784526611.0|3222": {"hash": "8cb3ec32244fb094749a002544a4f2e19b239fed929d021c2e3304adc1d5df03", "size": 3222, "status": "c"}, "model/training/utils.py|1784526611.0|6991": {"hash": "7eb3552824f976d7d8a80a869388b23fef363727ed97a8f20f580ee531f85d44", "size": 6991, "status": "c"}, "model/utils/abnumber_.py|1784526611.0|2334": {"hash": "1837911edccc67c1b1db5b4742e223ab46dfba146687279c577ace811efba571", "size": 2334, "status": "c"}, "model/utils/ab_metrics.py|1784526611.0|1724": {"hash": "0c3fd30611103df673a40d4aaa23db07364289cc1ea6cbaa57e2ca69498b624a", "size": 1724, "status": "c"}, "model/utils/fasta.py|1784526611.0|934": {"hash": "a447498b09bf75dc603cad25a86987f439b3a080fefddddcd2da0860d6c52524", "size": 934, "status": "c"}, "model/utils/coordinates.py|1784526611.0|1755": {"hash": "735f8e452600dc948e7249d60b1f6e391f8eced00f53a680375e70ec7960f2d1", "size": 1755, "status": "c"}, "model/utils/embed.py|1784526611.0|1183": {"hash": "55f95ec50afffebcb3f029bad68895b693f7154038bcfa01d4de6ee2107fd405", "size": 1183, "status": "c"}, "model/utils/constants.py|1784526611.0|368": {"hash": "34e058fc09c6b1976b188b870a8e8302fef5ebbdceef98497afe9ff32fec6415", "size": 368, "status": "c"}, "model/utils/general.py|1784526611.0|771": {"hash": "63de9d287bd8c54b30fca2db2e7bcf6e8ffbfdfce16b5ad575ff1480d3e75319", "size": 771, "status": "c"}, "model/utils/geometry.py|1784526611.0|1688": {"hash": "c7af1aed0310414620e1683bf22e5af9e84dc02c99633236491f2bc885e421fa", "size": 1688, "status": "c"}, "model/utils/pdb.py|1784526611.0|14536": {"hash": "e7be074946baac32b5561f8f8fa872586d6ffc299b0ce2dd1dd4b6ccabea9ff2", "size": 14536, "status": "c"}, "model/utils/folding.py|1784526611.0|6376": {"hash": "060f6f8a5f5049540dde81ddc954cec766f4c9a21e5a1d69e89469d60b829579", "size": 6376, "status": "c"}, "model/utils/tensor.py|1784526611.0|1127": {"hash": "7eb084b9c878e2cefa0d7519262d534430086142862d7b0feb343c2093af6c42", "size": 1127, "status": "c"}, "model/utils/visualize.py|1784526611.0|4867": {"hash": "052c0beae7f3ad9e1c375f42d2535d3d665c507713b839d4e0abbf315876ceea", "size": 4867, "status": "c"}, "model/utils/transforms.py|1784526611.0|18138": {"hash": "04573d0b32168a6e94de13e2580f0f412b121b3d54a6f8617fca358631de7558", "size": 18138, "status": "c"}, "pyproject.toml|1784526611.0|80": {"hash": "1c557fcb2d563b509abf60fdb4132d72373474dbf6d059d752b2b780788d43ed", "size": 80, "status": "c"}, "requirements.txt|1784526611.0|206": {"hash": "a696fb4a79d4a004d8277aa222f3c074c8bea524b7692f93a03edfda5c4a8551", "size": 206, "status": "c"}, "setup.cfg|1787713267.0|375": {"hash": "d18e548322261df23f3b45867e5173f7136744d36d137c027368755f8b062b3a", "size": 375, "status": "c"}, "scripts/inference.py|1787713264.0|3051": {"hash": "986639e5231e2e7462ccbeafe160d12faf480c759646653eee6f09b5ebccd553", "size": 3051, "status": "c"}, "scripts/IgFold.ipynb|1784526611.0|7614": {"hash": "5ed28dc41b9b50edda8f1624f0999f783c7af65139721acb25968fab48531053", "size": 7614, "status": "c"}, "weight/IgFold/LICENSE.md|1650791114.0|3330": {"hash": "02cd4b17669c8294425925009ccdf6a6a6c8bef1486d580d5288ee7837d11fca", "size": 3330, "status": "c"}, "weight/wheels/setuptools-81.0.0-py3-none-any.whl|1785393298.0|1062021": {"hash": "fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", "size": 1062021, "status": "c"}, "weight/IgFold/igfold_1.ckpt|1676739842.0|6358637": {"hash": "66c2aa5a4fd4ef485e99f661e6c605037fa6bfefc02464395836c23263948249", "size": 6358637, "status": "c"}, "weight/IgFold/igfold_2.ckpt|1676739842.0|6358637": {"hash": "3917eb78a3b8f9a84d53e3dae1132e0096b6ded0868a081a60d0603d502028bc", "size": 6358637, "status": "c"}, "weight/IgFold/igfold_3.ckpt|1676739842.0|6358637": {"hash": "d9b60988f1400c12f715b440c7be0bebab3b3be73dc39657c35c2029c3174c52", "size": 6358637, "status": "c"}, "weight/IgFold/igfold_5.ckpt|1676739842.0|6358637": {"hash": "b75865aef284973d47af61598a2fd50cbcfb02c67015b636f7ecd23ed842b2b5", "size": 6358637, "status": "c"}, "weight/wheels/igfold-0.4.0-py3-none-any.whl|1693722179.0|23355225": {"hash": "ddf73636cf86ca2ef91eab25f79bf8052a3deaeae0ee4e056c8eecd152582c9c", "size": 23355225, "status": "c"}, "weight/wheels/antiberty-0.1.3-py3-none-any.whl|1689549108.0|96631471": {"hash": "30d910992b190013871bac49cdc032e01a19339f7d2b958ab99b0eb44638352a", "size": 96631471, "status": "c"}}}
LICENSE.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # JHU Academic Software License Agreement
2
+
3
+ This license agreement ("License") is effective automatically between you ("Licensee") and The Johns Hopkins University (“JHU”) for use of the software with which this License is distributed (“Software”) so long as Licensee complies with the following terms and conditions:
4
+
5
+ The requirement to acknowledge the copyright of JHU as follows: “© 2022 The Johns Hopkins University” and copyrights of any incorporated third party software as described in the associated documentation.
6
+
7
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the source code must retain the above copyright notice, and these terms and conditions.
8
+
9
+ Neither the name of JHU nor its affiliates may be used to endorse or promote products derived from this Software without specific prior written permission from an authorized JHU representative.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY JHU "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JHU BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LICENSEE AGREES TO DEFEND, INDEMNIFY AND HOLD HARMLESS JHU FOR ANY CLAIMS ARISING FROM LICENSEE’S USE OF THE SOFTWARE TO THE FULLEST EXTENT PERMITTED BY LAW.
12
+
13
+ Only a non-exclusive, nontransferable license is granted to Licensee to use the Software for non-commercial purposes. Commercial use of the Software requires a separately executed written license agreement.
14
+
15
+ Licensee agrees that it will use the Software, and any modifications, improvements, or derivatives to the Software that the Licensee may create (collectively, "Improvements") solely for non-commercial purposes and shall not distribute or transfer the Software or Improvements to any third party without requiring that such third parties adhere to the terms of this License. Licensee agrees that any Improvements made by Licensee shall be subject to the same terms and conditions as the Software.
16
+
17
+ Licensee acknowledges that JHU holds copyright in the Software or portions of the Software, and that the Software may incorporate third party software which may be subject to additional terms and conditions. Licensee shall comply with any additional terms and conditions appliable to such third party software.
18
+
19
+ Licensee agrees that any publication of results obtained with the Software will acknowledge its use by an appropriate citation.
20
+
21
+ Licensee’s rights under this License terminate automatically without notice from JHU if Licensee fail to comply with any term(s) of this License.
22
+
23
+ This License shall be governed by the laws of the State of Maryland, excluding the application of its conflicts of law rules. Licensee agrees that any dispute shall be appropriate only in the state and federal courts located within the State of Maryland.
24
+
MANIFEST.in ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ include LICENSE.md
2
+ recursive-include model/igfold *.py
README.md ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ frameworks:
3
+ - PyTorch
4
+ language:
5
+ - en
6
+ license: other
7
+ tags:
8
+ - OneScience
9
+ - bioscience
10
+ - antibody-structure-prediction
11
+ - protein-structure-prediction
12
+ - IgFold
13
+ - AntiBERTy
14
+ tasks: []
15
+ ---
16
+
17
+ <p align="center">
18
+ <strong>
19
+ <span style="font-size: 30px;">IgFold</span>
20
+ </strong>
21
+ </p>
22
+
23
+ # Model Introduction
24
+
25
+ IgFold is an open-source antibody structure prediction model developed by the Gray Lab. It rapidly predicts three-dimensional antibody structures directly from amino acid sequences.
26
+
27
+ The model supports paired heavy/light-chain antibodies, single-chain antibodies, and nanobodies. It can also return residue-level predicted RMSD values and multiple levels of antibody sequence representations.
28
+
29
+ Paper: [Fast, accurate antibody structure prediction from deep learning on massive set of natural antibodies](https://www.nature.com/articles/s41467-023-38063-x)
30
+
31
+ # Model Description
32
+
33
+ IgFold uses AntiBERTy to extract antibody sequence representations and predicts antibody structures through graph Transformer layers, template feature integration, and an invariant point attention-based structure module.
34
+
35
+ The model provides the following main capabilities:
36
+
37
+ - Predict paired antibody structures from heavy-chain and light-chain sequences;
38
+ - Predict single-chain antibody or nanobody structures from a single heavy-chain or light-chain sequence;
39
+ - Output predicted RMSD values for the N, CA, C, and CB atoms of each residue;
40
+ - Output intermediate representations from AntiBERTy, the graph Transformer, and the structure module;
41
+ - Support template structures;
42
+ - Support structural refinement using PyRosetta or OpenMM;
43
+ - Support conversion of predicted structures to Chothia numbering.
44
+
45
+ # Use Cases
46
+
47
+ | Use Case | Description |
48
+ | :---: | :--- |
49
+ | Paired antibody structure prediction | Generate antibody PDB structures from heavy-chain and light-chain sequences. |
50
+ | Single-chain antibody prediction | Predict a structure from a single heavy-chain or light-chain sequence. |
51
+ | Nanobody structure prediction | Generate a PDB structure from a nanobody heavy-chain sequence. |
52
+ | Prediction error analysis | Obtain residue-level predicted RMSD values, which are also written to the B-factor column of the output PDB file. |
53
+ | Antibody representation extraction | Extract AntiBERTy, graph Transformer, and structure-module embeddings. |
54
+
55
+ # Usage
56
+
57
+ ## Manual Installation
58
+
59
+ ### Hardware Requirements
60
+
61
+ - The model can run on CPU or accelerator devices supported by PyTorch;
62
+ - GPU or other compatible accelerator devices are recommended for structure prediction;
63
+ - PyRosetta refinement mainly uses CPU resources, and runtime depends on sequence length and CPU performance;
64
+ - The actual accelerator configuration and installation procedure depend on the locally installed PyTorch version, drivers, and runtime environment.
65
+
66
+ ### Download the Model Package
67
+
68
+ Install the Hugging Face command-line tool and download the model repository:
69
+
70
+ ```bash
71
+ python -m pip install -U huggingface_hub
72
+
73
+ hf download OneScience-Group/IgFold --local-dir ./IgFold
74
+ cd IgFold
75
+ ```
76
+
77
+
78
+
79
+ ### Install the Runtime Environment
80
+
81
+ **DCU Environment**
82
+
83
+ ```bash
84
+ # Activate DTK and Conda first
85
+ conda create -n onescience311 python=3.11 -y
86
+ conda activate onescience311
87
+
88
+ python -m pip install "onescience[bio-dcu]" \
89
+ -i http://mirrors.onescience.ai:3141/pypi/simple/ \
90
+ --trusted-host mirrors.onescience.ai
91
+ ```
92
+ **GPU Environment**
93
+
94
+ ```bash
95
+ # Activate Conda first
96
+ conda create -n onescience311 python=3.11 -y
97
+ conda activate onescience311
98
+
99
+ python -m pip install "onescience[bio-gpu]" \
100
+ -i http://mirrors.onescience.ai:3141/pypi/simple/ \
101
+ --trusted-host mirrors.onescience.ai
102
+ ```
103
+
104
+ Install the additional dependencies required by EpHod:
105
+
106
+ ```bash
107
+ python -m pip install --no-deps -r requirements.txt
108
+ ```
109
+
110
+ The `weight/wheels/` directory also contains offline-installable IgFold and AntiBERTy wheel packages together with their official pretrained assets:
111
+
112
+ ```bash
113
+ python -m pip install --no-deps \
114
+ weight/wheels/antiberty-0.1.3-py3-none-any.whl \
115
+ weight/wheels/igfold-0.4.0-py3-none-any.whl
116
+ ```
117
+
118
+ PyTorch installation requirements vary across hardware platforms.
119
+
120
+ To use GPU or another accelerator device, install a PyTorch build compatible with the corresponding hardware platform and ensure that the remaining dependencies satisfy the versions declared in `requirements.txt`.
121
+
122
+ ### Optional Dependencies
123
+
124
+ #### PyRosetta Refinement
125
+
126
+ IgFold supports structural refinement using PyRosetta.
127
+
128
+ Install a PyRosetta version compatible with the current Python version and operating system according to the [official PyRosetta installation instructions](https://www.pyrosetta.org/downloads).
129
+
130
+ #### OpenMM Refinement
131
+
132
+ If PyRosetta is not used, OpenMM and PDBFixer can be installed instead:
133
+
134
+ ```bash
135
+ conda install -c conda-forge openmm==7.7.0 pdbfixer
136
+ ```
137
+
138
+ #### Chothia Numbering
139
+
140
+ To convert predicted structures to Chothia numbering, install AbNumber:
141
+
142
+ ```bash
143
+ conda install -c bioconda abnumber
144
+ ```
145
+
146
+ ### Quick Inference
147
+
148
+ The model package provides a directly executable inference entry point.
149
+
150
+ If neither sequences nor a FASTA file are specified, the script uses the paired heavy/light-chain example from the official IgFold README and writes the predicted structure to:
151
+
152
+ ```text
153
+ output/inference/antibody.pdb
154
+ ```
155
+
156
+ Run:
157
+
158
+ ```bash
159
+ python scripts/inference.py
160
+ ```
161
+
162
+ To use a custom FASTA file:
163
+
164
+ ```bash
165
+ python scripts/inference.py \
166
+ --fasta /path/to/antibody.fasta \
167
+ --output output/inference/my_antibody.pdb
168
+ ```
169
+
170
+ Chain identifiers in the FASTA file should be `H` and `L`.
171
+
172
+ Example:
173
+
174
+ ```text
175
+ >sample:H
176
+ EVQLVQSGPEVKKPGTSVKVSCKAS...
177
+ >sample:L
178
+ DVVMTQTPFSLPVSLGDQASISCR...
179
+ ```
180
+
181
+ When optional features are disabled, basic inference does not require:
182
+
183
+ - SAbDab PDB data;
184
+ - PyRosetta;
185
+ - OpenMM;
186
+ - AbNumber.
187
+
188
+ After installing the corresponding optional dependencies, structural refinement and renumbering can be enabled through options such as:
189
+
190
+ ```text
191
+ --refine
192
+ --openmm
193
+ --renum
194
+ ```
195
+
196
+ ### Paired Antibody Structure Prediction
197
+
198
+ Heavy-chain and light-chain sequences are provided as a dictionary using `H` and `L` as keys:
199
+
200
+ ```python
201
+ from igfold import IgFoldRunner
202
+ from igfold.refine.pyrosetta_ref import init_pyrosetta
203
+
204
+ init_pyrosetta()
205
+
206
+ sequences = {
207
+ "H": "EVQLVQSGPEVKKPGTSVKVSCKASGFTFMSSAVQWVRQARGQRLEWIGWIVIGSGNTNYAQKFQERVTITRDMSTSTAYMELSSLRSEDTAVYYCAAPYCSSISCNDGFDIWGQGTMVTVS",
208
+ "L": "DVVMTQTPFSLPVSLGDQASISCRSSQSLVHSNGNTYLHWYLQKPGQSPKLLIYKVSNRFSGVPDRFSGSGSGTDFTLKISRVEAEDLGVYFCSQSTHVPYTFGGGTKLEIK",
209
+ }
210
+
211
+ pred_pdb = "my_antibody.pdb"
212
+
213
+ igfold = IgFoldRunner()
214
+
215
+ igfold.fold(
216
+ pred_pdb,
217
+ sequences=sequences,
218
+ do_refine=True,
219
+ do_renum=True,
220
+ )
221
+ ```
222
+
223
+ After successful execution, the predicted structure is saved to:
224
+
225
+ ```text
226
+ my_antibody.pdb
227
+ ```
228
+
229
+ ### Nanobody or Single-Chain Antibody Prediction
230
+
231
+ For nanobody or single-chain heavy/light-chain prediction, only one sequence is required:
232
+
233
+ ```python
234
+ from igfold import IgFoldRunner
235
+ from igfold.refine.pyrosetta_ref import init_pyrosetta
236
+
237
+ init_pyrosetta()
238
+
239
+ sequences = {
240
+ "H": "QVQLQESGGGLVQAGGSLTLSCAVSGLTFSNYAMGWFRQAPGKEREFVAAITWDGGNTYYTDSVKGRFTISRDNAKNTVFLQMNSLKPEDTAVYYCAAKLLGSSRYELALAGYDYWGQGTQVTVS",
241
+ }
242
+
243
+ pred_pdb = "my_nanobody.pdb"
244
+
245
+ igfold = IgFoldRunner()
246
+
247
+ igfold.fold(
248
+ pred_pdb,
249
+ sequences=sequences,
250
+ do_refine=True,
251
+ do_renum=True,
252
+ )
253
+ ```
254
+
255
+ ### Inference Without Structural Refinement
256
+
257
+ If PyRosetta or OpenMM refinement is not required, set:
258
+
259
+ ```python
260
+ do_refine=False
261
+ ```
262
+
263
+ If Chothia renumbering is also unnecessary, set:
264
+
265
+ ```python
266
+ do_renum=False
267
+ ```
268
+
269
+ Example:
270
+
271
+ ```python
272
+ from igfold import IgFoldRunner
273
+
274
+ sequences = {
275
+ "H": "QVQLQESGGGLVQAGGSLTLSCAVSGLTFSNYAMGWFRQAPGKEREFVAAITWDGGNTYYTDSVKGRFTISRDNAKNTVFLQMNSLKPEDTAVYYCAAKLLGSSRYELALAGYDYWGQGTQVTVS",
276
+ }
277
+
278
+ pred_pdb = "my_nanobody.pdb"
279
+
280
+ igfold = IgFoldRunner()
281
+
282
+ igfold.fold(
283
+ pred_pdb,
284
+ sequences=sequences,
285
+ do_refine=False,
286
+ do_renum=False,
287
+ )
288
+ ```
289
+
290
+ In this configuration, PyRosetta, OpenMM, and AbNumber are not required.
291
+
292
+ ### Predicted RMSD
293
+
294
+ IgFold predicts residue-level RMSD values and stores them in the B-factor column of the output PDB file.
295
+
296
+ The same values are also returned by `fold()`:
297
+
298
+ ```python
299
+ from igfold import IgFoldRunner
300
+
301
+ sequences = {
302
+ "H": "EVQLVQSGPEVKKPGTSVKVSCKASGFTFMSSAVQWVRQARGQRLEWIGWIVIGSGNTNYAQKFQERVTITRDMSTSTAYMELSSLRSEDTAVYYCAAPYCSSISCNDGFDIWGQGTMVTVS",
303
+ "L": "DVVMTQTPFSLPVSLGDQASISCRSSQSLVHSNGNTYLHWYLQKPGQSPKLLIYKVSNRFSGVPDRFSGSGSGTDFTLKISRVEAEDLGVYFCSQSTHVPYTFGGGTKLEIK",
304
+ }
305
+
306
+ pred_pdb = "my_antibody.pdb"
307
+
308
+ igfold = IgFoldRunner()
309
+
310
+ out = igfold.fold(
311
+ pred_pdb,
312
+ sequences=sequences,
313
+ do_refine=False,
314
+ do_renum=False,
315
+ )
316
+
317
+ print(out.prmsd)
318
+ # Predicted RMSD for the N, CA, C, and CB atoms of each residue.
319
+ # Shape: [1, L, 4]
320
+ ```
321
+
322
+ `prmsd` is a model-predicted error estimate.
323
+
324
+ It is not the actual RMSD obtained by aligning the predicted structure with an experimentally determined structure.
325
+
326
+ ### Antibody Sequence Embeddings
327
+
328
+ The `embed()` method provides antibody representations from multiple stages of the IgFold pipeline:
329
+
330
+ ```python
331
+ from igfold import IgFoldRunner
332
+
333
+ sequences = {
334
+ "H": "EVQLVQSGPEVKKPGTSVKVSCKASGFTFMSSAVQWVRQARGQRLEWIGWIVIGSGNTNYAQKFQERVTITRDMSTSTAYMELSSLRSEDTAVYYCAAPYCSSISCNDGFDIWGQGTMVTVS",
335
+ "L": "DVVMTQTPFSLPVSLGDQASISCRSSQSLVHSNGNTYLHWYLQKPGQSPKLLIYKVSNRFSGVPDRFSGSGSGTDFTLKISRVEAEDLGVYFCSQSTHVPYTFGGGTKLEIK",
336
+ }
337
+
338
+ igfold = IgFoldRunner()
339
+ emb = igfold.embed(sequences=sequences)
340
+
341
+ print(emb.bert_embs.shape)
342
+ # AntiBERTy final-layer representations: [1, L, 512]
343
+
344
+ print(emb.gt_embs.shape)
345
+ # Graph Transformer representations: [1, L, 64]
346
+
347
+ print(emb.structure_embs.shape)
348
+ # Structure-module representations: [1, L, 64]
349
+ ```
350
+
351
+ ### Prefer OpenMM Refinement
352
+
353
+ After installing OpenMM and PDBFixer, OpenMM refinement can be selected by setting:
354
+
355
+ ```python
356
+ use_openmm=True
357
+ ```
358
+
359
+ Example:
360
+
361
+ ```python
362
+ from igfold import IgFoldRunner
363
+
364
+ sequences = {
365
+ "H": "EVQLVQSGPEVKKPGTSVKVSCKASGFTFMSSAVQWVRQARGQRLEWIGWIVIGSGNTNYAQKFQERVTITRDMSTSTAYMELSSLRSEDTAVYYCAAPYCSSISCNDGFDIWGQGTMVTVS",
366
+ "L": "DVVMTQTPFSLPVSLGDQASISCRSSQSLVHSNGNTYLHWYLQKPGQSPKLLIYKVSNRFSGVPDRFSGSGSGTDFTLKISRVEAEDLGVYFCSQSTHVPYTFGGGTKLEIK",
367
+ }
368
+
369
+ pred_pdb = "my_antibody.pdb"
370
+
371
+ igfold = IgFoldRunner()
372
+
373
+ igfold.fold(
374
+ pred_pdb,
375
+ sequences=sequences,
376
+ do_refine=True,
377
+ use_openmm=True,
378
+ do_renum=True,
379
+ )
380
+ ```
381
+
382
+ ### Training
383
+
384
+ The official IgFold repository does not provide a complete directly executable training entry point, dataset class, or end-to-end training script.
385
+
386
+ Therefore, this Hugging Face model package does not provide a training command.
387
+
388
+ The `model/training/` directory contains utility functions used for model loss computation but does not constitute a complete training program.
389
+
390
+ The official structural training data used in the IgFold paper is available from Zenodo:
391
+
392
+ https://doi.org/10.5281/zenodo.7820263
393
+
394
+ The dataset includes:
395
+
396
+ - Experimentally determined SAbDab antibody structures;
397
+ - Predicted structures generated from paired OAS sequences;
398
+ - Predicted structures generated from unpaired OAS sequences.
399
+
400
+ These datasets are useful for reproducing the training methodology described in the paper or for structural evaluation.
401
+
402
+ They are not required for sequence-to-structure inference.
403
+
404
+ Because this model package does not provide a complete training entry point, the full SAbDab `.fasta` and `.pdb` datasets do not need to be included in the Hugging Face model repository.
405
+
406
+ To independently reproduce IgFold training, users would need to implement or reconstruct the following components using the methodology described in the paper, upstream loss functions, and official training data:
407
+
408
+ - Dataset;
409
+ - DataLoader;
410
+ - Optimizer;
411
+ - Training loop;
412
+ - Validation procedure;
413
+ - Checkpoint management.
414
+
415
+ ### Predicted Antibody Structure Datasets
416
+
417
+ The IgFold authors also released two large collections of predicted antibody structures:
418
+
419
+ - 104K non-redundant paired antibody structures from OAS:
420
+ https://data.graylab.jhu.edu/OAS_paired.tar.gz
421
+ - 1.3M predicted human paired antibody structures from the Jaffe et al. dataset:
422
+ https://data.graylab.jhu.edu/Jaffe2022.tar.gz
423
+
424
+ # OneScience Official Resources
425
+
426
+ | Platform | OneScience Main Repository | Skills Repository |
427
+ | --- | --- | --- |
428
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
429
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
430
+
431
+ # Citation and License
432
+
433
+ IgFold source code, pretrained models, and associated materials are distributed under the [JHU Academic Software License Agreement](https://github.com/Graylab/IgFold/blob/main/LICENSE.md).
434
+
435
+ The license permits use under the academic and non-commercial terms specified by Johns Hopkins University.
436
+
437
+ Commercial use may require a separate license obtained through Johns Hopkins Technology Ventures.
438
+
439
+ This Hugging Face model package does not modify or extend the original licensing terms of IgFold, AntiBERTy, PyRosetta, SAbDab, OAS, pretrained models, datasets, or other third-party resources.
440
+
441
+ ```bibtex
442
+ @article{ruffolo2023fast,
443
+ title={Fast, accurate antibody structure prediction from deep learning on massive set of natural antibodies},
444
+ author={Ruffolo, Jeffrey A and Chu, Lee-Shin and Mahajan, Sai Pooja and Gray, Jeffrey J},
445
+ journal={Nature Communications},
446
+ volume={14},
447
+ number={1},
448
+ pages={2389},
449
+ year={2023},
450
+ publisher={Nature Publishing Group UK London}
451
+ }
452
+
453
+ @article{ruffolo2021deciphering,
454
+ title={Deciphering antibody affinity maturation with language models and weakly supervised learning},
455
+ author={Ruffolo, Jeffrey A and Gray, Jeffrey J and Sulam, Jeremias},
456
+ journal={arXiv},
457
+ year={2021}
458
+ }
459
+ ```
conf/config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "num_models": 4,
3
+ "output": "output/inference/antibody.pdb",
4
+ "refine": false,
5
+ "use_openmm": false,
6
+ "renumber": false,
7
+ "device": "auto"
8
+ }
configuration.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "IgFold",
3
+ "framework": "PyTorch",
4
+ "task": "antibody-structure-prediction",
5
+ "entry_points": {
6
+ "inference": "scripts/inference.py",
7
+ "predict": "scripts/inference.py"
8
+ },
9
+ "source_package": "model",
10
+ "config": "conf/config.json",
11
+ "weight_dir": "weight",
12
+ "license": "JHU-Academic-Software-License"
13
+ }
model/IgFoldRunner.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from time import time
3
+ from glob import glob
4
+ import torch
5
+
6
+ from antiberty import AntiBERTyRunner
7
+
8
+ import igfold
9
+ from igfold.model.IgFold import IgFold
10
+ from igfold.utils.folding import fold
11
+ from igfold.utils.embed import embed
12
+ from igfold.utils.general import exists
13
+
14
+
15
+ def display_license():
16
+ license_url = "https://github.com/Graylab/IgFold/blob/main/LICENSE.md"
17
+ license_message = f"""
18
+ The code, data, and weights for this work are made available for non-commercial use
19
+ (including at commercial entities) under the terms of the JHU Academic Software License
20
+ Agreement. For commercial inquiries, please contact awichma2[at]jhu.edu.
21
+ License: {license_url}
22
+ """
23
+ print(license_message)
24
+
25
+
26
+ class IgFoldRunner():
27
+ """
28
+ Wrapper for IgFold model predictions.
29
+ """
30
+ def __init__(self, num_models=4, model_ckpts=None, try_gpu=True):
31
+ """
32
+ Initialize IgFoldRunner.
33
+
34
+ :param num_models: Number of pre-trained IgFold models to use for prediction.
35
+ :param model_ckpts: List of model checkpoints to use (instead of pre-trained).
36
+ """
37
+
38
+ display_license()
39
+
40
+ if exists(model_ckpts):
41
+ num_models = len(model_ckpts)
42
+ else:
43
+ if num_models < 1 or num_models > 4:
44
+ raise ValueError("num_models must be between 1 and 4.")
45
+
46
+ if not exists(model_ckpts):
47
+ project_path = os.path.dirname(
48
+ os.path.realpath(igfold.__file__))
49
+
50
+ ckpt_path = os.path.join(
51
+ project_path,
52
+ "trained_models/IgFold/*.ckpt",
53
+ )
54
+ model_ckpts = list(glob(ckpt_path))
55
+
56
+ model_ckpts = list(sorted(model_ckpts))[:num_models]
57
+
58
+ print(f"Loading {num_models} IgFold models...")
59
+
60
+ device = torch.device(
61
+ "cuda:0" if torch.cuda.is_available() and try_gpu else "cpu")
62
+ print(f"Using device: {device}")
63
+
64
+ self.models = []
65
+ for ckpt_file in model_ckpts:
66
+ print(f"Loading {ckpt_file}...")
67
+ self.models.append(
68
+ IgFold.load_from_checkpoint(ckpt_file).eval().to(device))
69
+
70
+ print(f"Successfully loaded {num_models} IgFold models.")
71
+
72
+ self.antiberty = AntiBERTyRunner()
73
+ self.antiberty.model.eval()
74
+ self.antiberty.model.to(device)
75
+ print("Loaded AntiBERTy model.")
76
+
77
+ def fold(
78
+ self,
79
+ pdb_file,
80
+ fasta_file=None,
81
+ sequences=None,
82
+ template_pdb=None,
83
+ ignore_cdrs=None,
84
+ ignore_chain=None,
85
+ skip_pdb=False,
86
+ do_refine=True,
87
+ use_openmm=False,
88
+ do_renum=True,
89
+ truncate_sequences=False,
90
+ ):
91
+ """
92
+ Predict antibody structure with IgFold.
93
+
94
+ :param pdb_file: PDB file to predict.
95
+ :param fasta_file: FASTA file containing sequences.
96
+ :param sequences: Dictionary of sequences.
97
+ :param template_pdb: PDB file containing template structure.
98
+ :param ignore_cdrs: List of CDRs to ignore.
99
+ :param ignore_chain: Chain to ignore.
100
+ :param skip_pdb: Skip PDB processing.
101
+ :param do_refine: Perform PyRosetta refinement.
102
+ :param do_renum: Renumber PDB to Chothia with AbNum.
103
+ :param truncate_sequences: Truncate sequences with AbNumber.
104
+ """
105
+ start_time = time()
106
+ model_out = fold(
107
+ self.antiberty,
108
+ self.models,
109
+ pdb_file=pdb_file,
110
+ fasta_file=fasta_file,
111
+ sequences=sequences,
112
+ template_pdb=template_pdb,
113
+ ignore_cdrs=ignore_cdrs,
114
+ ignore_chain=ignore_chain,
115
+ skip_pdb=skip_pdb,
116
+ do_refine=do_refine,
117
+ use_openmm=use_openmm,
118
+ do_renum=do_renum,
119
+ truncate_sequences=truncate_sequences,
120
+ )
121
+
122
+ print(f"Completed folding in {time() - start_time:.2f} seconds.")
123
+
124
+ return model_out
125
+
126
+ def embed(
127
+ self,
128
+ model_idx=0,
129
+ fasta_file=None,
130
+ sequences=None,
131
+ template_pdb=None,
132
+ ignore_cdrs=None,
133
+ ignore_chain=None,
134
+ ):
135
+ """
136
+ Embed antibody sequences with IgFold.
137
+
138
+ :param fasta_file: FASTA file containing sequences.
139
+ :param sequences: Dictionary of sequences.
140
+ :param template_pdb: PDB file containing template structure.
141
+ :param ignore_cdrs: List of CDRs to ignore.
142
+ :param ignore_chain: Chain to ignore.
143
+ """
144
+
145
+ start_time = time()
146
+ model_out = embed(
147
+ self.antiberty,
148
+ self.models[model_idx],
149
+ fasta_file=fasta_file,
150
+ sequences=sequences,
151
+ template_pdb=template_pdb,
152
+ ignore_cdrs=ignore_cdrs,
153
+ ignore_chain=ignore_chain,
154
+ )
155
+
156
+ print(f"Completed embedding in {time() - start_time:.2f} seconds.")
157
+
158
+ return model_out
model/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .model.IgFold import IgFold
2
+ from .model.interface import IgFoldInput, IgFoldOutput
3
+ from .IgFoldRunner import IgFoldRunner
model/model/IgFold.py ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from einops import rearrange, repeat
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ import pytorch_lightning as pl
8
+
9
+ from igfold.model.interface import *
10
+ from igfold.model.components import TriangleGraphTransformer, IPAEncoder, IPATransformer
11
+ from igfold.utils.coordinates import get_ideal_coords, place_o_coords
12
+ from igfold.utils.transforms import quaternion_to_matrix
13
+ from igfold.training.utils import *
14
+ from igfold.utils.general import exists
15
+
16
+ ATOM_DIM = 3
17
+
18
+
19
+ class IgFold(pl.LightningModule):
20
+ def __init__(
21
+ self,
22
+ config,
23
+ config_overwrite=None,
24
+ ):
25
+ super().__init__()
26
+
27
+ self.save_hyperparameters()
28
+ config = self.hparams.config
29
+ if exists(config_overwrite):
30
+ config.update(config_overwrite)
31
+
32
+ self.bert_feat_dim = 512
33
+ self.bert_attn_dim = 64
34
+
35
+ self.node_dim = config["node_dim"]
36
+
37
+ self.depth = config["depth"]
38
+ self.gt_depth = config["gt_depth"]
39
+ self.gt_heads = config["gt_heads"]
40
+
41
+ self.temp_ipa_depth = config["temp_ipa_depth"]
42
+ self.temp_ipa_heads = config["temp_ipa_heads"]
43
+
44
+ self.str_ipa_depth = config["str_ipa_depth"]
45
+ self.str_ipa_heads = config["str_ipa_heads"]
46
+
47
+ self.dev_ipa_depth = config["dev_ipa_depth"]
48
+ self.dev_ipa_heads = config["dev_ipa_heads"]
49
+
50
+ self.str_node_transform = nn.Sequential(
51
+ nn.Linear(
52
+ self.bert_feat_dim,
53
+ self.node_dim,
54
+ ),
55
+ nn.ReLU(),
56
+ nn.LayerNorm(self.node_dim),
57
+ )
58
+ self.str_edge_transform = nn.Sequential(
59
+ nn.Linear(
60
+ self.bert_attn_dim,
61
+ self.node_dim,
62
+ ),
63
+ nn.ReLU(),
64
+ nn.LayerNorm(self.node_dim),
65
+ )
66
+
67
+ self.main_block = TriangleGraphTransformer(
68
+ dim=self.node_dim,
69
+ edge_dim=self.node_dim,
70
+ depth=self.depth,
71
+ tri_dim_hidden=2 * self.node_dim,
72
+ gt_depth=self.gt_depth,
73
+ gt_heads=self.gt_heads,
74
+ gt_dim_head=self.node_dim // 2,
75
+ )
76
+ self.template_ipa = IPAEncoder(
77
+ dim=self.node_dim,
78
+ depth=self.temp_ipa_depth,
79
+ heads=self.temp_ipa_heads,
80
+ require_pairwise_repr=True,
81
+ )
82
+
83
+ self.structure_ipa = IPATransformer(
84
+ dim=self.node_dim,
85
+ depth=self.str_ipa_depth,
86
+ heads=self.str_ipa_heads,
87
+ require_pairwise_repr=True,
88
+ )
89
+
90
+ self.dev_node_transform = nn.Sequential(
91
+ nn.Linear(self.bert_feat_dim, self.node_dim),
92
+ nn.ReLU(),
93
+ nn.LayerNorm(self.node_dim),
94
+ )
95
+ self.dev_edge_transform = nn.Sequential(
96
+ nn.Linear(
97
+ self.bert_attn_dim,
98
+ self.node_dim,
99
+ ),
100
+ nn.ReLU(),
101
+ nn.LayerNorm(self.node_dim),
102
+ )
103
+ self.dev_ipa = IPAEncoder(
104
+ dim=self.node_dim,
105
+ depth=self.dev_ipa_depth,
106
+ heads=self.dev_ipa_heads,
107
+ require_pairwise_repr=True,
108
+ )
109
+ self.dev_linear = nn.Linear(
110
+ self.node_dim,
111
+ 4,
112
+ )
113
+
114
+ def get_coords_tran_rot(
115
+ self,
116
+ temp_coords,
117
+ batch_size,
118
+ seq_len,
119
+ ):
120
+ res_coords = rearrange(
121
+ temp_coords,
122
+ "b (l a) d -> b l a d",
123
+ l=seq_len,
124
+ ).to(self.device)
125
+ ideal_coords = get_ideal_coords()
126
+ res_ideal_coords = repeat(
127
+ ideal_coords,
128
+ "a d -> b l a d",
129
+ b=batch_size,
130
+ l=seq_len,
131
+ ).to(self.device)
132
+ _, rotations, translations = kabsch(
133
+ res_ideal_coords,
134
+ res_coords,
135
+ return_translation_rotation=True,
136
+ )
137
+ translations = rearrange(
138
+ translations,
139
+ "b l () d -> b l d",
140
+ )
141
+
142
+ return translations, rotations
143
+
144
+ def clean_input(
145
+ self,
146
+ input: IgFoldInput,
147
+ ):
148
+ embeddings = input.embeddings
149
+ temp_coords = input.template_coords
150
+ temp_mask = input.template_mask
151
+ batch_mask = input.batch_mask
152
+ align_mask = input.align_mask
153
+
154
+ batch_size = embeddings[0].shape[0]
155
+ seq_lens = [max(e.shape[1], 0) for e in embeddings]
156
+ seq_len = sum(seq_lens)
157
+
158
+ if not exists(temp_coords):
159
+ temp_coords = torch.zeros(
160
+ batch_size,
161
+ 4 * seq_len,
162
+ ATOM_DIM,
163
+ device=self.device,
164
+ ).float()
165
+ if not exists(temp_mask):
166
+ temp_mask = torch.zeros(
167
+ batch_size,
168
+ 4 * seq_len,
169
+ device=self.device,
170
+ ).bool()
171
+ if not exists(batch_mask):
172
+ batch_mask = torch.ones(
173
+ batch_size,
174
+ 4 * seq_len,
175
+ device=self.device,
176
+ ).bool()
177
+ if not exists(align_mask):
178
+ align_mask = torch.ones(
179
+ batch_size,
180
+ 4 * seq_len,
181
+ device=self.device,
182
+ ).bool()
183
+
184
+ align_mask = align_mask & batch_mask # Should already be masked by batch_mask anyway
185
+ temp_coords[~temp_mask] = 0.
186
+ for i, (tc, m) in enumerate(zip(temp_coords, temp_mask)):
187
+ temp_coords[i][m] -= tc[m].mean(-2)
188
+
189
+ input.template_coords = temp_coords
190
+ input.template_mask = temp_mask
191
+ input.batch_mask = batch_mask
192
+ input.align_mask = align_mask
193
+
194
+ return input, batch_size, seq_lens, seq_len
195
+
196
+ def forward(
197
+ self,
198
+ input: IgFoldInput,
199
+ ):
200
+ input, batch_size, seq_lens, seq_len = self.clean_input(input)
201
+ embeddings = input.embeddings
202
+ attentions = input.attentions
203
+ temp_coords = input.template_coords
204
+ temp_mask = input.template_mask
205
+ coords_label = input.coords_label
206
+ batch_mask = input.batch_mask
207
+ align_mask = input.align_mask
208
+ return_embeddings = input.return_embeddings
209
+
210
+ res_batch_mask = rearrange(
211
+ batch_mask,
212
+ "b (l a) -> b l a",
213
+ a=4,
214
+ ).all(-1).to(self.device)
215
+ res_temp_mask = rearrange(
216
+ temp_mask,
217
+ "b (l a) -> b l a",
218
+ a=4,
219
+ ).all(-1).to(self.device)
220
+
221
+ ### Model forward pass
222
+
223
+ bert_feats = torch.cat(embeddings, dim=1).to(self.device)
224
+ bert_attn = torch.zeros(
225
+ (batch_size, seq_len, seq_len, self.bert_attn_dim),
226
+ device=self.device,
227
+ )
228
+ for i, (a, l) in enumerate(zip(attentions, seq_lens)):
229
+ a = rearrange(a, "b n h l1 l2 -> b l1 l2 (n h)")
230
+ cum_l = sum(seq_lens[:i])
231
+ bert_attn[:, cum_l:cum_l + l, cum_l:cum_l + l, :] = a
232
+
233
+ temp_translations, temp_rotations = self.get_coords_tran_rot(
234
+ temp_coords,
235
+ batch_size,
236
+ seq_len,
237
+ )
238
+
239
+ str_nodes = self.str_node_transform(bert_feats)
240
+ str_edges = self.str_edge_transform(bert_attn)
241
+ str_nodes, str_edges = self.main_block(
242
+ str_nodes,
243
+ str_edges,
244
+ mask=res_batch_mask,
245
+ )
246
+ gt_embs = str_nodes
247
+ str_nodes = self.template_ipa(
248
+ str_nodes,
249
+ translations=temp_translations,
250
+ rotations=temp_rotations,
251
+ pairwise_repr=str_edges,
252
+ mask=res_temp_mask,
253
+ )
254
+ structure_embs = str_nodes
255
+
256
+ ipa_coords, ipa_translations, ipa_quaternions = self.structure_ipa(
257
+ str_nodes,
258
+ translations=None,
259
+ quaternions=None,
260
+ pairwise_repr=str_edges,
261
+ mask=res_batch_mask,
262
+ )
263
+ ipa_rotations = quaternion_to_matrix(ipa_quaternions)
264
+
265
+ dev_nodes = self.dev_node_transform(bert_feats)
266
+ dev_edges = self.dev_edge_transform(bert_attn)
267
+ dev_out_feats = self.dev_ipa(
268
+ dev_nodes,
269
+ translations=ipa_translations.detach(),
270
+ rotations=ipa_rotations.detach(),
271
+ pairwise_repr=dev_edges,
272
+ mask=res_batch_mask,
273
+ )
274
+ dev_pred = F.relu(self.dev_linear(dev_out_feats))
275
+ dev_pred = rearrange(dev_pred, "b l a -> b (l a)", a=4)
276
+
277
+ bb_coords = rearrange(
278
+ ipa_coords[:, :, :3],
279
+ "b l a d -> b (l a) d",
280
+ )
281
+ flat_coords = rearrange(
282
+ ipa_coords[:, :, :4],
283
+ "b l a d -> b (l a) d",
284
+ )
285
+
286
+ ### Calculate losses if given labels
287
+ loss = torch.zeros(
288
+ batch_size,
289
+ device=self.device,
290
+ )
291
+ if exists(coords_label):
292
+ rmsd_clamp = self.hparams.config["rmsd_clamp"]
293
+ coords_loss = kabsch_mse(
294
+ flat_coords,
295
+ coords_label,
296
+ align_mask=batch_mask,
297
+ mask=batch_mask,
298
+ clamp=rmsd_clamp,
299
+ )
300
+
301
+ bb_coords_label = rearrange(
302
+ rearrange(coords_label, "b (l a) d -> b l a d", a=4)[:, :, :3],
303
+ "b l a d -> b (l a) d")
304
+ bb_batch_mask = rearrange(
305
+ rearrange(batch_mask, "b (l a) -> b l a", a=4)[:, :, :3],
306
+ "b l a -> b (l a)")
307
+ bondlen_loss = bond_length_l1(
308
+ bb_coords,
309
+ bb_coords_label,
310
+ bb_batch_mask,
311
+ )
312
+
313
+ prmsd_loss = []
314
+ cum_seq_lens = np.cumsum([0] + seq_lens)
315
+ for sl_i, sl in enumerate(seq_lens):
316
+ align_mask_ = align_mask.clone()
317
+ align_mask_[:, :cum_seq_lens[sl_i]] = False
318
+ align_mask_[:, cum_seq_lens[sl_i + 1]:] = False
319
+ res_batch_mask_ = res_batch_mask.clone()
320
+ res_batch_mask_[:, :cum_seq_lens[sl_i]] = False
321
+ res_batch_mask_[:, cum_seq_lens[sl_i + 1]:] = False
322
+
323
+ if sl == 0 or align_mask_.sum() == 0 or res_batch_mask_.sum(
324
+ ) == 0:
325
+ continue
326
+
327
+ prmsd_loss.append(
328
+ bb_prmsd_l1(
329
+ dev_pred,
330
+ flat_coords.detach(),
331
+ coords_label,
332
+ align_mask=align_mask_,
333
+ mask=res_batch_mask_,
334
+ ))
335
+ prmsd_loss = sum(prmsd_loss)
336
+
337
+ coords_loss, bondlen_loss = list(
338
+ map(
339
+ lambda l: rearrange(l, "(c b) -> b c", b=batch_size).mean(
340
+ 1),
341
+ [coords_loss, bondlen_loss],
342
+ ))
343
+
344
+ loss += sum([coords_loss, bondlen_loss, prmsd_loss])
345
+ else:
346
+ prmsd_loss, coords_loss, bondlen_loss = None, None, None
347
+
348
+ if not exists(coords_label):
349
+ loss = None
350
+
351
+ bert_embs = bert_feats if return_embeddings else None
352
+ bert_attn = bert_attn if return_embeddings else None
353
+ gt_embs = gt_embs if return_embeddings else None
354
+ structure_embs = structure_embs if return_embeddings else None
355
+ output = IgFoldOutput(
356
+ coords=ipa_coords,
357
+ prmsd=dev_pred,
358
+ translations=ipa_translations,
359
+ rotations=ipa_rotations,
360
+ coords_loss=coords_loss,
361
+ bondlen_loss=bondlen_loss,
362
+ prmsd_loss=prmsd_loss,
363
+ loss=loss,
364
+ bert_embs=bert_embs,
365
+ bert_attn=bert_attn,
366
+ gt_embs=gt_embs,
367
+ structure_embs=structure_embs,
368
+ )
369
+
370
+ return output
371
+
372
+ def score_coords(
373
+ self,
374
+ input: IgFoldInput,
375
+ output: IgFoldOutput,
376
+ ):
377
+ input, _, _, _ = self.clean_input(input)
378
+ batch_mask = input.batch_mask
379
+
380
+ res_batch_mask = rearrange(
381
+ batch_mask,
382
+ "b (l a) -> b l a",
383
+ a=4,
384
+ ).all(-1)
385
+
386
+ str_translations, str_rotations = output.translations, output.rotations
387
+
388
+ bert_feats = output.bert_embs
389
+ bert_attn = output.bert_attn
390
+
391
+ dev_nodes = self.dev_node_transform(bert_feats)
392
+ dev_edges = self.dev_edge_transform(bert_attn)
393
+ dev_out_feats = self.dev_ipa(
394
+ dev_nodes,
395
+ translations=str_translations.detach(),
396
+ rotations=str_rotations.detach(),
397
+ pairwise_repr=dev_edges,
398
+ mask=res_batch_mask,
399
+ )
400
+ dev_pred = F.relu(self.dev_linear(dev_out_feats)).squeeze(-1)
401
+ dev_pred = rearrange(dev_pred, "b l a -> b (l a)", a=4)
402
+
403
+ return dev_pred
404
+
405
+ def transform_ideal_coords(self, translations, rotations):
406
+ b, n, d = translations.shape
407
+ device = translations.device
408
+
409
+ ideal_coords = get_ideal_coords().to(device)
410
+ ideal_coords = repeat(
411
+ ideal_coords,
412
+ "a d -> b l a d",
413
+ b=b,
414
+ l=n,
415
+ )
416
+ points_global = torch.einsum(
417
+ 'b n a c, b n c d -> b n a d',
418
+ ideal_coords,
419
+ rotations,
420
+ ) + rearrange(
421
+ translations,
422
+ "b l d -> b l () d",
423
+ )
424
+
425
+ return points_global
426
+
427
+ def gradient_refine(
428
+ self,
429
+ input: IgFoldInput,
430
+ output: IgFoldOutput,
431
+ num_steps: int = 80,
432
+ ):
433
+ input_, _, seq_lens, _ = self.clean_input(input)
434
+ batch_mask = input_.batch_mask
435
+ res_batch_mask = rearrange(
436
+ batch_mask,
437
+ "b (l a) -> b l a",
438
+ a=4,
439
+ ).all(-1)
440
+ translations, rotations = output.translations, output.rotations
441
+
442
+ in_coords = self.transform_ideal_coords(translations,
443
+ rotations).detach()
444
+ in_flat_coords = rearrange(
445
+ in_coords[:, :, :4],
446
+ "b l a d -> b (l a) d",
447
+ )
448
+
449
+ with torch.enable_grad():
450
+ translations.requires_grad = True
451
+ rotations.requires_grad = True
452
+
453
+ translations = nn.parameter.Parameter(translations)
454
+ rotations = nn.parameter.Parameter(rotations)
455
+
456
+ optimizer = torch.optim.Adam([translations, rotations], lr=2e-2)
457
+ for _ in range(num_steps):
458
+ optimizer.zero_grad()
459
+
460
+ coords = self.transform_ideal_coords(translations, rotations)
461
+ viol_loss = violation_loss(coords, seq_lens, res_batch_mask)
462
+
463
+ flat_coords = rearrange(
464
+ coords[:, :, :4],
465
+ "b l a d -> b (l a) d",
466
+ )
467
+ rmsd = kabsch_mse(
468
+ flat_coords,
469
+ in_flat_coords,
470
+ align_mask=batch_mask,
471
+ mask=batch_mask,
472
+ )
473
+
474
+ output.translations = translations
475
+ output.rotations = rotations
476
+
477
+ loss = rmsd + viol_loss
478
+
479
+ loss.backward()
480
+ optimizer.step()
481
+
482
+ prmsd = self.score_coords(input, output)
483
+
484
+ coords = place_o_coords(coords)
485
+ output.coords = coords
486
+ output.prmsd = prmsd
487
+
488
+ return output
model/model/__init__.py ADDED
File without changes
model/model/components/GraphTransformer.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ###
2
+ # Inspired by graph transformer implementation from https://github.com/lucidrains/graph-transformer-pytorch
3
+ ###
4
+
5
+ import torch
6
+ from torch import nn, einsum
7
+ from einops import rearrange, repeat
8
+
9
+ from igfold.utils.general import exists, default
10
+
11
+ List = nn.ModuleList
12
+
13
+
14
+ class PreNorm(nn.Module):
15
+ def __init__(
16
+ self,
17
+ dim,
18
+ fn,
19
+ ):
20
+ super().__init__()
21
+ self.fn = fn
22
+ self.norm = nn.LayerNorm(dim)
23
+
24
+ def forward(
25
+ self,
26
+ x,
27
+ *args,
28
+ **kwargs,
29
+ ):
30
+ x = self.norm(x)
31
+ return self.fn(
32
+ x,
33
+ *args,
34
+ **kwargs,
35
+ )
36
+
37
+
38
+ # gated residual
39
+
40
+
41
+ class Residual(nn.Module):
42
+ def forward(
43
+ self,
44
+ x,
45
+ res,
46
+ ):
47
+ return x + res
48
+
49
+
50
+ class GatedResidual(nn.Module):
51
+ def __init__(
52
+ self,
53
+ dim,
54
+ ):
55
+ super().__init__()
56
+ self.proj = nn.Sequential(
57
+ nn.Linear(dim * 3, 1, bias=False),
58
+ nn.Sigmoid(),
59
+ )
60
+
61
+ def forward(self, x, res):
62
+ gate_input = torch.cat((x, res, x - res), dim=-1)
63
+ gate = self.proj(gate_input)
64
+ return x * gate + res * (1 - gate)
65
+
66
+
67
+ # attention
68
+
69
+
70
+ class Attention(nn.Module):
71
+ def __init__(
72
+ self,
73
+ dim,
74
+ dim_head=64,
75
+ heads=8,
76
+ edge_dim=None,
77
+ ):
78
+ super().__init__()
79
+ edge_dim = default(
80
+ edge_dim,
81
+ dim,
82
+ )
83
+
84
+ inner_dim = dim_head * heads
85
+ self.heads = heads
86
+ self.scale = dim_head**-0.5
87
+
88
+ self.to_q = nn.Linear(
89
+ dim,
90
+ inner_dim,
91
+ )
92
+ self.to_kv = nn.Linear(
93
+ dim,
94
+ inner_dim * 2,
95
+ )
96
+ self.edges_to_kv = nn.Linear(
97
+ edge_dim,
98
+ inner_dim,
99
+ )
100
+
101
+ self.to_out = nn.Linear(
102
+ inner_dim,
103
+ dim,
104
+ )
105
+
106
+ def forward(
107
+ self,
108
+ nodes,
109
+ edges,
110
+ mask=None,
111
+ ):
112
+ h = self.heads
113
+
114
+ q = self.to_q(nodes)
115
+ k, v = self.to_kv(nodes).chunk(
116
+ 2,
117
+ dim=-1,
118
+ )
119
+
120
+ e_kv = self.edges_to_kv(edges)
121
+
122
+ q, k, v, e_kv = map(
123
+ lambda t: rearrange(
124
+ t,
125
+ 'b ... (h d) -> (b h) ... d',
126
+ h=h,
127
+ ),
128
+ (q, k, v, e_kv),
129
+ )
130
+
131
+ ek, ev = e_kv, e_kv
132
+
133
+ k, v = map(
134
+ lambda t: rearrange(
135
+ t,
136
+ 'b j d -> b () j d ',
137
+ ),
138
+ (k, v),
139
+ )
140
+ k = k + ek
141
+ v = v + ev
142
+
143
+ sim = einsum(
144
+ 'b i d, b i j d -> b i j',
145
+ q,
146
+ k,
147
+ ) * self.scale
148
+
149
+ if exists(mask):
150
+ mask = rearrange(
151
+ mask,
152
+ 'b i -> b i ()',
153
+ ) & rearrange(
154
+ mask,
155
+ 'b j -> b () j',
156
+ )
157
+ mask = repeat(
158
+ mask,
159
+ "b ... -> (b h) ...",
160
+ h=self.heads,
161
+ )
162
+ max_neg_value = -torch.finfo(sim.dtype).max
163
+ sim.masked_fill_(~mask, max_neg_value)
164
+
165
+ attn = sim.softmax(dim=-1)
166
+ out = einsum(
167
+ 'b i j, b i j d -> b i d',
168
+ attn,
169
+ v,
170
+ )
171
+ out = rearrange(
172
+ out,
173
+ '(b h) n d -> b n (h d)',
174
+ h=h,
175
+ )
176
+ return self.to_out(out)
177
+
178
+
179
+ def FeedForward(dim, ff_mult=4):
180
+ return nn.Sequential(
181
+ nn.Linear(dim, dim * ff_mult),
182
+ nn.GELU(),
183
+ nn.Linear(dim * ff_mult, dim),
184
+ )
185
+
186
+
187
+ class GraphTransformer(nn.Module):
188
+ def __init__(
189
+ self,
190
+ dim,
191
+ depth,
192
+ dim_head=64,
193
+ edge_dim=None,
194
+ heads=8,
195
+ with_feedforwards=False,
196
+ norm_edges=False,
197
+ ):
198
+ super().__init__()
199
+ self.layers = List([])
200
+ edge_dim = default(
201
+ edge_dim,
202
+ dim,
203
+ )
204
+ self.norm_edges = nn.LayerNorm(
205
+ edge_dim) if norm_edges else nn.Identity()
206
+
207
+ for _ in range(depth):
208
+ self.layers.append(
209
+ List([
210
+ List([
211
+ PreNorm(
212
+ dim,
213
+ Attention(
214
+ dim,
215
+ edge_dim=edge_dim,
216
+ dim_head=dim_head,
217
+ heads=heads,
218
+ )),
219
+ GatedResidual(dim)
220
+ ]),
221
+ List(
222
+ [PreNorm(
223
+ dim,
224
+ FeedForward(dim),
225
+ ),
226
+ GatedResidual(dim)]) if with_feedforwards else None
227
+ ]))
228
+
229
+ def forward(
230
+ self,
231
+ nodes,
232
+ edges,
233
+ mask=None,
234
+ ):
235
+ edges = self.norm_edges(edges)
236
+
237
+ for attn_block, ff_block in self.layers:
238
+ attn, attn_residual = attn_block
239
+ nodes = attn_residual(
240
+ attn(
241
+ nodes,
242
+ edges,
243
+ mask=mask,
244
+ ),
245
+ nodes,
246
+ )
247
+
248
+ if exists(ff_block):
249
+ ff, ff_residual = ff_block
250
+ nodes = ff_residual(
251
+ ff(nodes),
252
+ nodes,
253
+ )
254
+
255
+ return nodes, edges
model/model/components/IPABlock.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ###
2
+ # Inspired by IPA implementation from https://github.com/lucidrains/invariant-point-attention
3
+ ###
4
+
5
+ import torch
6
+ import torch.nn.functional as F
7
+ from torch.cuda.amp import autocast
8
+ from contextlib import contextmanager
9
+ from torch import nn, einsum
10
+
11
+ from einops.layers.torch import Rearrange
12
+ from einops import rearrange, repeat
13
+
14
+ # helpers
15
+
16
+ def exists(val):
17
+ return val is not None
18
+
19
+ def default(val, d):
20
+ return val if exists(val) else d
21
+
22
+ def max_neg_value(t):
23
+ return -torch.finfo(t.dtype).max
24
+
25
+ @contextmanager
26
+ def disable_tf32():
27
+ orig_value = torch.backends.cuda.matmul.allow_tf32
28
+ torch.backends.cuda.matmul.allow_tf32 = False
29
+ yield
30
+ torch.backends.cuda.matmul.allow_tf32 = orig_value
31
+
32
+ # classes
33
+
34
+ class InvariantPointAttention(nn.Module):
35
+ def __init__(
36
+ self,
37
+ *,
38
+ dim,
39
+ heads = 8,
40
+ scalar_key_dim = 16,
41
+ scalar_value_dim = 16,
42
+ point_key_dim = 4,
43
+ point_value_dim = 4,
44
+ pairwise_repr_dim = None,
45
+ require_pairwise_repr = True,
46
+ eps = 1e-8
47
+ ):
48
+ super().__init__()
49
+ self.eps = eps
50
+ self.heads = heads
51
+ self.require_pairwise_repr = require_pairwise_repr
52
+
53
+ # num attention contributions
54
+
55
+ num_attn_logits = 3 if require_pairwise_repr else 2
56
+
57
+ # qkv projection for scalar attention (normal)
58
+
59
+ self.scalar_attn_logits_scale = (num_attn_logits * scalar_key_dim) ** -0.5
60
+
61
+ self.to_scalar_q = nn.Linear(dim, scalar_key_dim * heads, bias = False)
62
+ self.to_scalar_k = nn.Linear(dim, scalar_key_dim * heads, bias = False)
63
+ self.to_scalar_v = nn.Linear(dim, scalar_value_dim * heads, bias = False)
64
+
65
+ # qkv projection for point attention (coordinate and orientation aware)
66
+
67
+ point_weight_init_value = torch.log(torch.exp(torch.full((heads,), 1.)) - 1.)
68
+ self.point_weights = nn.Parameter(point_weight_init_value)
69
+
70
+ self.point_attn_logits_scale = ((num_attn_logits * point_key_dim) * (9 / 2)) ** -0.5
71
+
72
+ self.to_point_q = nn.Linear(dim, point_key_dim * heads * 3, bias = False)
73
+ self.to_point_k = nn.Linear(dim, point_key_dim * heads * 3, bias = False)
74
+ self.to_point_v = nn.Linear(dim, point_value_dim * heads * 3, bias = False)
75
+
76
+ # pairwise representation projection to attention bias
77
+
78
+ pairwise_repr_dim = default(pairwise_repr_dim, dim) if require_pairwise_repr else 0
79
+
80
+ if require_pairwise_repr:
81
+ self.pairwise_attn_logits_scale = num_attn_logits ** -0.5
82
+
83
+ self.to_pairwise_attn_bias = nn.Sequential(
84
+ nn.Linear(pairwise_repr_dim, heads),
85
+ Rearrange('b ... h -> (b h) ...')
86
+ )
87
+
88
+ # combine out - scalar dim + pairwise dim + point dim * (3 for coordinates in R3 and then 1 for norm)
89
+
90
+ self.to_out = nn.Linear(heads * (scalar_value_dim + pairwise_repr_dim + point_value_dim * (3 + 1)), dim)
91
+
92
+ def forward(
93
+ self,
94
+ single_repr,
95
+ pairwise_repr = None,
96
+ *,
97
+ rotations,
98
+ translations,
99
+ mask = None
100
+ ):
101
+ x, b, h, eps, require_pairwise_repr = single_repr, single_repr.shape[0], self.heads, self.eps, self.require_pairwise_repr
102
+ assert not (require_pairwise_repr and not exists(pairwise_repr)), 'pairwise representation must be given as second argument'
103
+
104
+ # get queries, keys, values for scalar and point (coordinate-aware) attention pathways
105
+
106
+ q_scalar, k_scalar, v_scalar = self.to_scalar_q(x), self.to_scalar_k(x), self.to_scalar_v(x)
107
+
108
+ q_point, k_point, v_point = self.to_point_q(x), self.to_point_k(x), self.to_point_v(x)
109
+
110
+ # split out heads
111
+
112
+ q_scalar, k_scalar, v_scalar = map(lambda t: rearrange(t, 'b n (h d) -> (b h) n d', h = h), (q_scalar, k_scalar, v_scalar))
113
+ q_point, k_point, v_point = map(lambda t: rearrange(t, 'b n (h d c) -> (b h) n d c', h = h, c = 3), (q_point, k_point, v_point))
114
+
115
+ rotations = repeat(rotations, 'b n r1 r2 -> (b h) n r1 r2', h = h)
116
+ translations = repeat(translations, 'b n c -> (b h) n () c', h = h)
117
+
118
+ # rotate qkv points into global frame
119
+
120
+ q_point = einsum('b n d c, b n c r -> b n d r', q_point, rotations) + translations
121
+ k_point = einsum('b n d c, b n c r -> b n d r', k_point, rotations) + translations
122
+ v_point = einsum('b n d c, b n c r -> b n d r', v_point, rotations) + translations
123
+
124
+ # derive attn logits for scalar and pairwise
125
+
126
+ attn_logits_scalar = einsum('b i d, b j d -> b i j', q_scalar, k_scalar) * self.scalar_attn_logits_scale
127
+
128
+ if require_pairwise_repr:
129
+ attn_logits_pairwise = self.to_pairwise_attn_bias(pairwise_repr) * self.pairwise_attn_logits_scale
130
+
131
+ # derive attn logits for point attention
132
+
133
+ point_qk_diff = rearrange(q_point, 'b i d c -> b i () d c') - rearrange(k_point, 'b j d c -> b () j d c')
134
+ point_dist = (point_qk_diff ** 2).sum(dim = -2)
135
+
136
+ point_weights = F.softplus(self.point_weights)
137
+ point_weights = repeat(point_weights, 'h -> (b h) () () ()', b = b)
138
+
139
+ attn_logits_points = -0.5 * (point_dist * point_weights * self.point_attn_logits_scale).sum(dim = -1)
140
+
141
+ # combine attn logits
142
+
143
+ attn_logits = attn_logits_scalar + attn_logits_points
144
+
145
+ if require_pairwise_repr:
146
+ attn_logits = attn_logits + attn_logits_pairwise
147
+
148
+ # mask
149
+
150
+ if exists(mask):
151
+ mask = rearrange(mask, 'b i -> b i ()') * rearrange(mask, 'b j -> b () j')
152
+ mask = repeat(mask, 'b i j -> (b h) i j', h = h)
153
+ mask_value = max_neg_value(attn_logits)
154
+ attn_logits = attn_logits.masked_fill(~mask, mask_value)
155
+
156
+ # attention
157
+
158
+ attn = attn_logits.softmax(dim = - 1)
159
+
160
+ with disable_tf32(), autocast(enabled = False):
161
+ # disable TF32 for precision
162
+
163
+ # aggregate values
164
+
165
+ results_scalar = einsum('b i j, b j d -> b i d', attn, v_scalar)
166
+
167
+ attn_with_heads = rearrange(attn, '(b h) i j -> b h i j', h = h)
168
+
169
+ if require_pairwise_repr:
170
+ results_pairwise = einsum('b h i j, b i j d -> b h i d', attn_with_heads, pairwise_repr)
171
+
172
+ # aggregate point values
173
+
174
+ results_points = einsum('b i j, b j d c -> b i d c', attn, v_point)
175
+
176
+ # rotate aggregated point values back into local frame
177
+
178
+ results_points = einsum('b n d c, b n c r -> b n d r', results_points - translations, rotations.transpose(-1, -2))
179
+ results_points_norm = torch.sqrt( torch.square(results_points).sum(dim=-1) + eps )
180
+
181
+ # merge back heads
182
+
183
+ results_scalar = rearrange(results_scalar, '(b h) n d -> b n (h d)', h = h)
184
+ results_points = rearrange(results_points, '(b h) n d c -> b n (h d c)', h = h)
185
+ results_points_norm = rearrange(results_points_norm, '(b h) n d -> b n (h d)', h = h)
186
+
187
+ results = (results_scalar, results_points, results_points_norm)
188
+
189
+ if require_pairwise_repr:
190
+ results_pairwise = rearrange(results_pairwise, 'b h n d -> b n (h d)', h = h)
191
+ results = (*results, results_pairwise)
192
+
193
+ # concat results and project out
194
+
195
+ results = torch.cat(results, dim = -1)
196
+ return self.to_out(results)
197
+
198
+ # one transformer block based on IPA
199
+
200
+ def FeedForward(dim, mult = 1., num_layers = 2, act = nn.ReLU):
201
+ layers = []
202
+ dim_hidden = dim * mult
203
+
204
+ for ind in range(num_layers):
205
+ is_first = ind == 0
206
+ is_last = ind == (num_layers - 1)
207
+ dim_in = dim if is_first else dim_hidden
208
+ dim_out = dim if is_last else dim_hidden
209
+
210
+ layers.append(nn.Linear(dim_in, dim_out))
211
+
212
+ if is_last:
213
+ continue
214
+
215
+ layers.append(act())
216
+
217
+ return nn.Sequential(*layers)
218
+
219
+ class IPABlock(nn.Module):
220
+ def __init__(
221
+ self,
222
+ *,
223
+ dim,
224
+ ff_mult = 1,
225
+ ff_num_layers = 3, # in the paper, they used 3 layer transition (feedforward) block
226
+ post_norm = True, # in the paper, they used post-layernorm - offering pre-norm as well
227
+ **kwargs
228
+ ):
229
+ super().__init__()
230
+ self.post_norm = post_norm
231
+
232
+ self.attn_norm = nn.LayerNorm(dim)
233
+ self.attn = InvariantPointAttention(dim = dim, **kwargs)
234
+
235
+ self.ff_norm = nn.LayerNorm(dim)
236
+ self.ff = FeedForward(dim, mult = ff_mult, num_layers = ff_num_layers)
237
+
238
+ def forward(self, x, **kwargs):
239
+ post_norm = self.post_norm
240
+
241
+ attn_input = x if post_norm else self.attn_norm(x)
242
+ x = self.attn(attn_input, **kwargs) + x
243
+ x = self.attn_norm(x) if post_norm else x
244
+
245
+ ff_input = x if post_norm else self.ff_norm(x)
246
+ x = self.ff(ff_input) + x
247
+ x = self.ff_norm(x) if post_norm else x
248
+ return x
model/model/components/IPATransformer.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ###
2
+ # Inspired by IPA implementation from https://github.com/lucidrains/invariant-point-attention
3
+ ###
4
+
5
+ import torch
6
+ import torch.nn.functional as F
7
+ from torch import nn, einsum
8
+ from einops import rearrange, repeat
9
+
10
+ from igfold.model.components.IPABlock import IPABlock
11
+ from igfold.utils.coordinates import get_ideal_coords, place_o_coords
12
+ from igfold.utils.general import exists
13
+ from igfold.utils.transforms import quaternion_multiply, quaternion_to_matrix
14
+
15
+
16
+ class IPAEncoder(nn.Module):
17
+ def __init__(
18
+ self,
19
+ *,
20
+ dim,
21
+ depth,
22
+ **kwargs,
23
+ ):
24
+ super().__init__()
25
+
26
+ # layers
27
+ self.layers = nn.ModuleList([])
28
+ for _ in range(depth):
29
+ self.layers.append(IPABlock(
30
+ dim=dim,
31
+ **kwargs,
32
+ ))
33
+
34
+ def forward(
35
+ self,
36
+ x,
37
+ *,
38
+ translations=None,
39
+ rotations=None,
40
+ pairwise_repr=None,
41
+ mask=None,
42
+ ):
43
+ for block in self.layers:
44
+ x = block(
45
+ x,
46
+ pairwise_repr=pairwise_repr,
47
+ rotations=rotations,
48
+ translations=translations,
49
+ mask=mask,
50
+ )
51
+
52
+ return x
53
+
54
+
55
+ class IPATransformer(nn.Module):
56
+ def __init__(
57
+ self,
58
+ *,
59
+ dim,
60
+ depth,
61
+ stop_rotation_grad=False,
62
+ **kwargs,
63
+ ):
64
+ super().__init__()
65
+
66
+ self.stop_rotation_grad = stop_rotation_grad
67
+
68
+ self.quaternion_to_matrix = quaternion_to_matrix
69
+ self.quaternion_multiply = quaternion_multiply
70
+
71
+ # layers
72
+ self.layers = nn.ModuleList([])
73
+ for _ in range(depth):
74
+ ipa_block = IPABlock(
75
+ dim=dim,
76
+ **kwargs,
77
+ )
78
+ linear = nn.Linear(dim, 6)
79
+ torch.nn.init.zeros_(linear.weight.data)
80
+ torch.nn.init.zeros_(linear.bias.data)
81
+ self.layers.append(nn.ModuleList([ipa_block, linear]))
82
+
83
+ def forward(
84
+ self,
85
+ single_repr,
86
+ *,
87
+ translations=None,
88
+ quaternions=None,
89
+ pairwise_repr=None,
90
+ mask=None,
91
+ ):
92
+ x, device, quaternion_multiply, quaternion_to_matrix = single_repr, single_repr.device, self.quaternion_multiply, self.quaternion_to_matrix
93
+ b, n, *_ = x.shape
94
+
95
+ # if no initial quaternions passed in, start from identity
96
+
97
+ if not exists(quaternions):
98
+ quaternions = torch.tensor(
99
+ [1., 0., 0., 0.],
100
+ device=device,
101
+ ) # initial rotations
102
+ quaternions = repeat(
103
+ quaternions,
104
+ 'd -> b n d',
105
+ b=b,
106
+ n=n,
107
+ )
108
+
109
+ # if not translations passed in, start from identity
110
+
111
+ if not exists(translations):
112
+ translations = torch.zeros(
113
+ (b, n, 3),
114
+ device=device,
115
+ )
116
+
117
+ # go through the layers and apply invariant point attention and feedforward
118
+
119
+ for block, to_update in self.layers:
120
+ rotations = quaternion_to_matrix(quaternions)
121
+ if self.stop_rotation_grad:
122
+ rotations = rotations.detach()
123
+
124
+ x = block(
125
+ x,
126
+ pairwise_repr=pairwise_repr,
127
+ rotations=rotations,
128
+ translations=translations,
129
+ mask=mask,
130
+ )
131
+
132
+ # update quaternion and translation
133
+
134
+ quaternion_update, translation_update = to_update(x).chunk(
135
+ 2,
136
+ dim=-1,
137
+ )
138
+ quaternion_update = F.pad(
139
+ quaternion_update,
140
+ (1, 0),
141
+ value=1.,
142
+ )
143
+
144
+ quaternions = quaternion_multiply(
145
+ quaternions,
146
+ quaternion_update,
147
+ )
148
+ translations = translations + einsum(
149
+ 'b n c, b n c r -> b n r',
150
+ translation_update,
151
+ rotations,
152
+ )
153
+
154
+ ideal_coords = get_ideal_coords().to(device)
155
+ ideal_coords = repeat(
156
+ ideal_coords,
157
+ "a d -> b l a d",
158
+ b=b,
159
+ l=n,
160
+ )
161
+
162
+ rotations = quaternion_to_matrix(quaternions)
163
+ points_global = einsum(
164
+ 'b n a c, b n c d -> b n a d',
165
+ ideal_coords,
166
+ rotations,
167
+ ) + rearrange(
168
+ translations,
169
+ "b l d -> b l () d",
170
+ )
171
+
172
+ points_global = place_o_coords(points_global)
173
+
174
+ return points_global, translations, quaternions
model/model/components/TriangleGraphTransformer.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from torch import nn
2
+
3
+ from .GraphTransformer import GraphTransformer
4
+ from .TriangleMultiplicativeModule import TriangleMultiplicativeModule
5
+ from igfold.utils.general import exists
6
+
7
+
8
+ class TriangleGraphTransformer(nn.Module):
9
+ def __init__(
10
+ self,
11
+ dim,
12
+ edge_dim,
13
+ depth,
14
+ gt_depth=1,
15
+ gt_dim_head=32,
16
+ gt_heads=8,
17
+ tri_dim_hidden=None,
18
+ ):
19
+ super().__init__()
20
+
21
+ self.layers = nn.ModuleList([])
22
+ for _ in range(depth):
23
+ graph_transformer = GraphTransformer(
24
+ dim=dim,
25
+ edge_dim=edge_dim,
26
+ depth=gt_depth,
27
+ heads=gt_heads,
28
+ dim_head=gt_dim_head,
29
+ with_feedforwards=True,
30
+ )
31
+ triangle_out = TriangleMultiplicativeModule(
32
+ dim=edge_dim,
33
+ hidden_dim=tri_dim_hidden,
34
+ mix='outgoing',
35
+ )
36
+ triangle_in = TriangleMultiplicativeModule(
37
+ dim=edge_dim,
38
+ hidden_dim=tri_dim_hidden,
39
+ mix='ingoing',
40
+ )
41
+
42
+ self.layers.append(
43
+ nn.ModuleList([graph_transformer, triangle_out, triangle_in]))
44
+
45
+ def forward(self, nodes, edges, mask=None):
46
+ for gt, tri_out, tri_in in self.layers:
47
+ if exists(mask):
48
+ tri_mask = mask.unsqueeze(-2) & mask.unsqueeze(-1)
49
+ else:
50
+ tri_mask = None
51
+
52
+ nodes, _ = gt(nodes, edges, mask=mask)
53
+ edges = edges + tri_out(
54
+ edges,
55
+ mask=tri_mask,
56
+ )
57
+ edges = edges + tri_in(
58
+ edges,
59
+ mask=tri_mask,
60
+ )
61
+
62
+ return nodes, edges
model/model/components/TriangleMultiplicativeModule.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ###
2
+ # Inspired by triangle multiplicative update implementation from https://github.com/lucidrains/triangle-multiplicative-module
3
+ ###
4
+
5
+ from torch import nn, einsum
6
+ import torch.nn.functional as F
7
+ from einops import rearrange
8
+
9
+ from igfold.utils.general import exists, default
10
+
11
+
12
+ class TriangleMultiplicativeModule(nn.Module):
13
+ def __init__(
14
+ self,
15
+ *,
16
+ dim,
17
+ hidden_dim=None,
18
+ mix='ingoing',
19
+ ):
20
+ super().__init__()
21
+ assert mix in {'ingoing',
22
+ 'outgoing'}, 'mix must be either ingoing or outgoing'
23
+
24
+ hidden_dim = default(
25
+ hidden_dim,
26
+ dim,
27
+ )
28
+ self.norm = nn.LayerNorm(dim)
29
+
30
+ self.left_proj = nn.Linear(
31
+ dim,
32
+ hidden_dim,
33
+ )
34
+ self.right_proj = nn.Linear(
35
+ dim,
36
+ hidden_dim,
37
+ )
38
+
39
+ self.left_gate = nn.Linear(
40
+ dim,
41
+ hidden_dim,
42
+ )
43
+ self.right_gate = nn.Linear(
44
+ dim,
45
+ hidden_dim,
46
+ )
47
+ self.out_gate = nn.Linear(dim, dim)
48
+
49
+ # initialize all gating to be identity
50
+
51
+ for gate in (
52
+ self.left_gate,
53
+ self.right_gate,
54
+ self.out_gate,
55
+ ):
56
+ nn.init.constant_(
57
+ gate.weight,
58
+ 0.,
59
+ )
60
+ nn.init.constant_(
61
+ gate.bias,
62
+ 1.,
63
+ )
64
+
65
+ if mix == 'outgoing':
66
+ self.mix_einsum_eq = '... i k d, ... j k d -> ... i j d'
67
+ elif mix == 'ingoing':
68
+ self.mix_einsum_eq = '... k j d, ... k i d -> ... i j d'
69
+
70
+ self.to_out_norm = nn.LayerNorm(hidden_dim)
71
+ self.to_out = nn.Linear(
72
+ hidden_dim,
73
+ dim,
74
+ )
75
+
76
+ def forward(self, x, mask=None):
77
+ assert x.shape[1] == x.shape[2], 'feature map must be symmetrical'
78
+ if exists(mask):
79
+ mask = rearrange(
80
+ mask,
81
+ 'b i j -> b i j ()',
82
+ )
83
+
84
+ x = self.norm(x)
85
+
86
+ left = self.left_proj(x)
87
+ right = self.right_proj(x)
88
+
89
+ if exists(mask):
90
+ left = left * mask
91
+ right = right * mask
92
+
93
+ left_gate = self.left_gate(x).sigmoid()
94
+ right_gate = self.right_gate(x).sigmoid()
95
+ out_gate = self.out_gate(x).sigmoid()
96
+
97
+ left = left * left_gate
98
+ right = right * right_gate
99
+
100
+ out = einsum(
101
+ self.mix_einsum_eq,
102
+ left,
103
+ right,
104
+ )
105
+
106
+ out = self.to_out_norm(out)
107
+ out = self.to_out(out)
108
+ out = out * out_gate
109
+ return out
model/model/components/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .GraphTransformer import *
2
+ from .IPATransformer import *
3
+ from .TriangleMultiplicativeModule import *
4
+ from .TriangleGraphTransformer import *
model/model/interface.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import List, Optional, Union
3
+ import torch
4
+
5
+
6
+ @dataclass
7
+ class IgFoldInput():
8
+ """
9
+ Input type of for IgFold model.
10
+ """
11
+
12
+ embeddings: List[List[torch.FloatTensor]]
13
+ attentions: List[List[torch.FloatTensor]]
14
+ template_coords: Optional[torch.FloatTensor] = None
15
+ template_mask: Optional[torch.BoolTensor] = None
16
+ batch_mask: Optional[torch.BoolTensor] = None
17
+ align_mask: Optional[torch.BoolTensor] = None
18
+ coords_label: Optional[torch.FloatTensor] = None
19
+ return_embeddings: Optional[bool] = False
20
+
21
+
22
+ @dataclass
23
+ class IgFoldOutput():
24
+ """
25
+ Output type of for IgFold model.
26
+ """
27
+
28
+ coords: torch.FloatTensor
29
+ prmsd: torch.FloatTensor
30
+ translations: torch.FloatTensor
31
+ rotations: torch.FloatTensor
32
+ coords_loss: Optional[torch.FloatTensor] = None
33
+ torsion_loss: Optional[torch.FloatTensor] = None
34
+ bondlen_loss: Optional[torch.FloatTensor] = None
35
+ prmsd_loss: Optional[torch.FloatTensor] = None
36
+ loss: Optional[torch.FloatTensor] = None
37
+ bert_embs: Optional[torch.FloatTensor] = None
38
+ bert_attn: Optional[torch.FloatTensor] = None
39
+ gt_embs: Optional[torch.FloatTensor] = None
40
+ structure_embs: Optional[torch.FloatTensor] = None
model/refine/__init__.py ADDED
File without changes
model/refine/openmm_ref.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pdbfixer
2
+ import openmm
3
+
4
+ ENERGY = openmm.unit.kilocalories_per_mole
5
+ LENGTH = openmm.unit.angstroms
6
+
7
+ def refine(pdb_file, stiffness=10., tolerance=2.39, use_gpu=False):
8
+ tolerance = tolerance * ENERGY
9
+ stiffness = stiffness * ENERGY / (LENGTH**2)
10
+
11
+ fixer = pdbfixer.PDBFixer(pdb_file)
12
+ fixer.findMissingResidues()
13
+ fixer.findMissingAtoms()
14
+ fixer.addMissingAtoms()
15
+
16
+ force_field = openmm.app.ForceField("amber14/protein.ff14SB.xml")
17
+ modeller = openmm.app.Modeller(fixer.topology, fixer.positions)
18
+ modeller.addHydrogens(force_field)
19
+ system = force_field.createSystem(modeller.topology)
20
+
21
+ force = openmm.CustomExternalForce("0.5 * k * ((x-x0)^2 + (y-y0)^2 + (z-z0)^2)")
22
+ force.addGlobalParameter("k", stiffness)
23
+ for p in ["x0", "y0", "z0"]:
24
+ force.addPerParticleParameter(p)
25
+ for residue in modeller.topology.residues():
26
+ for atom in residue.atoms():
27
+ if atom.name in ["N", "CA", "C", "CB"]:
28
+ force.addParticle(atom.index,
29
+ modeller.positions[atom.index])
30
+ system.addForce(force)
31
+
32
+ integrator = openmm.LangevinIntegrator(0, 0.01, 1.0)
33
+ platform = openmm.Platform.getPlatformByName("CUDA" if use_gpu else "CPU")
34
+
35
+ simulation = openmm.app.Simulation(modeller.topology, system, integrator, platform)
36
+ simulation.context.setPositions(modeller.positions)
37
+ simulation.minimizeEnergy(tolerance)
38
+
39
+ with open(pdb_file, "w") as f:
40
+ openmm.app.PDBFile.writeFile(
41
+ simulation.topology,
42
+ simulation.context.getState(getPositions=True).getPositions(),
43
+ f,
44
+ keepIds=True,)
model/refine/pyrosetta_ref.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pyrosetta
2
+
3
+ from igfold.utils.general import exists
4
+
5
+
6
+ def init_pyrosetta(init_string=None, silent=True):
7
+ if not exists(init_string):
8
+ init_string = "-mute all -ignore_zero_occupancy false -detect_disulf true -detect_disulf_tolerance 1.5 -check_cdr_chainbreaks false"
9
+ pyrosetta.init(init_string, silent=silent)
10
+
11
+
12
+ def get_min_mover(
13
+ max_iter: int = 1000,
14
+ sf_name: str = "ref2015_cst",
15
+ coord_cst_weight: float = 1,
16
+ dih_cst_weight: float = 1,
17
+ ) -> pyrosetta.rosetta.protocols.moves.Mover:
18
+ """
19
+ Create full-atom minimization mover
20
+ """
21
+
22
+ sf = pyrosetta.create_score_function(sf_name)
23
+ sf.set_weight(
24
+ pyrosetta.rosetta.core.scoring.ScoreType.cart_bonded,
25
+ 1,
26
+ )
27
+ sf.set_weight(
28
+ pyrosetta.rosetta.core.scoring.ScoreType.pro_close,
29
+ 0,
30
+ )
31
+ sf.set_weight(
32
+ pyrosetta.rosetta.core.scoring.ScoreType.coordinate_constraint,
33
+ coord_cst_weight,
34
+ )
35
+ sf.set_weight(
36
+ pyrosetta.rosetta.core.scoring.ScoreType.dihedral_constraint,
37
+ dih_cst_weight,
38
+ )
39
+
40
+ mmap = pyrosetta.rosetta.core.kinematics.MoveMap()
41
+ mmap.set_bb(True)
42
+ mmap.set_chi(False)
43
+ mmap.set_jump(False)
44
+ min_mover = pyrosetta.rosetta.protocols.minimization_packing.MinMover(
45
+ mmap,
46
+ sf,
47
+ 'lbfgs_armijo_nonmonotone',
48
+ 0.0001,
49
+ True,
50
+ )
51
+ min_mover.max_iter(max_iter)
52
+ min_mover.cartesian(True)
53
+
54
+ return min_mover
55
+
56
+
57
+ def get_fa_relax_mover(
58
+ max_iter: int = 100) -> pyrosetta.rosetta.protocols.moves.Mover:
59
+ """
60
+ Create full-atom relax mover
61
+ """
62
+
63
+ sf = pyrosetta.create_score_function('ref2015_cst')
64
+
65
+ mmap = pyrosetta.rosetta.core.kinematics.MoveMap()
66
+ mmap.set_bb(True)
67
+ mmap.set_chi(True)
68
+ mmap.set_jump(True)
69
+
70
+ relax = pyrosetta.rosetta.protocols.relax.FastRelax()
71
+ relax.set_scorefxn(sf)
72
+ relax.max_iter(max_iter)
73
+ relax.set_movemap(mmap)
74
+
75
+ return relax
76
+
77
+
78
+ def get_repack_mover():
79
+ tf = pyrosetta.rosetta.core.pack.task.TaskFactory()
80
+ tf.push_back(
81
+ pyrosetta.rosetta.core.pack.task.operation.InitializeFromCommandline())
82
+ tf.push_back(
83
+ pyrosetta.rosetta.core.pack.task.operation.RestrictToRepacking())
84
+
85
+ packer = pyrosetta.rosetta.protocols.minimization_packing.PackRotamersMover(
86
+ )
87
+ packer.task_factory(tf)
88
+
89
+ return packer
90
+
91
+
92
+ def refine(out_pdb_file,
93
+ pdb_string,
94
+ minimization_iter=100,
95
+ constrain=True,
96
+ idealize=False):
97
+ # create new pose
98
+ pose = pyrosetta.rosetta.core.pose.Pose()
99
+ pyrosetta.rosetta.core.import_pose.pose_from_pdbstring(
100
+ pose,
101
+ pdb_string,
102
+ )
103
+
104
+ if constrain:
105
+ cst_mover = pyrosetta.rosetta.protocols.relax.AtomCoordinateCstMover()
106
+ cst_mover.cst_sidechain(False)
107
+ cst_mover.apply(pose)
108
+
109
+ min_mover = get_min_mover(
110
+ max_iter=minimization_iter,
111
+ coord_cst_weight=1,
112
+ dih_cst_weight=0,
113
+ )
114
+ min_mover.apply(pose)
115
+
116
+ if idealize:
117
+ idealize_mover = pyrosetta.rosetta.protocols.idealize.IdealizeMover()
118
+ idealize_mover.apply(pose)
119
+
120
+ packer = get_repack_mover()
121
+ packer.apply(pose)
122
+
123
+ min_mover.apply(pose)
124
+
125
+ pose.dump_pdb(out_pdb_file)
model/training/__init__.py ADDED
File without changes
model/training/utils.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from einops import rearrange, repeat
2
+ import numpy as np
3
+ import torch
4
+ import torch.nn.functional as F
5
+
6
+ from igfold.utils.constants import *
7
+ from igfold.utils.general import exists
8
+ from igfold.utils.geometry import dist, angle, dihedral
9
+
10
+
11
+ def kabsch(
12
+ mobile,
13
+ stationary,
14
+ return_translation_rotation=False,
15
+ ):
16
+ X = rearrange(
17
+ mobile,
18
+ "... l d -> ... d l",
19
+ )
20
+ Y = rearrange(
21
+ stationary,
22
+ "... l d -> ... d l",
23
+ )
24
+
25
+ # center X and Y to the origin
26
+ XT, YT = X.mean(dim=-1, keepdim=True), Y.mean(dim=-1, keepdim=True)
27
+ X_ = X - XT
28
+ Y_ = Y - YT
29
+
30
+ # calculate convariance matrix
31
+ C = torch.einsum("... x l, ... y l -> ... x y", X_, Y_)
32
+
33
+ # Optimal rotation matrix via SVD
34
+ if int(torch.__version__.split(".")[1]) < 8:
35
+ # warning! int torch 1.<8 : W must be transposed
36
+ V, S, W = torch.svd(C)
37
+ W = rearrange(W, "... a b -> ... b a")
38
+ else:
39
+ V, S, W = torch.linalg.svd(C)
40
+
41
+ # determinant sign for direction correction
42
+ v_det = torch.det(V.to("cpu")).to(X.device)
43
+ w_det = torch.det(W.to("cpu")).to(X.device)
44
+ d = (v_det * w_det) < 0.0
45
+ if d.any():
46
+ S[d] = S[d] * (-1)
47
+ V[d, :] = V[d, :] * (-1)
48
+
49
+ # Create Rotation matrix U
50
+ U = torch.matmul(V, W) #.to(device)
51
+
52
+ U = rearrange(
53
+ U,
54
+ "... d x -> ... x d",
55
+ )
56
+ XT = rearrange(
57
+ XT,
58
+ "... d x -> ... x d",
59
+ )
60
+ YT = rearrange(
61
+ YT,
62
+ "... d x -> ... x d",
63
+ )
64
+
65
+ if return_translation_rotation:
66
+ return XT, U, YT
67
+
68
+ transform = lambda coords: torch.einsum(
69
+ "... l d, ... x d -> ... l x",
70
+ coords - XT,
71
+ U,
72
+ ) + YT
73
+ mobile = transform(mobile)
74
+
75
+ return mobile, transform
76
+
77
+
78
+ def do_kabsch(
79
+ mobile,
80
+ stationary,
81
+ align_mask=None,
82
+ ):
83
+ mobile_, stationary_ = mobile.clone(), stationary.clone()
84
+ if exists(align_mask):
85
+ mobile_[~align_mask] = mobile_[align_mask].mean(dim=-2)
86
+ stationary_[~align_mask] = stationary_[align_mask].mean(dim=-2)
87
+ _, kabsch_xform = kabsch(
88
+ mobile_,
89
+ stationary_,
90
+ )
91
+ else:
92
+ _, kabsch_xform = kabsch(
93
+ mobile_,
94
+ stationary_,
95
+ )
96
+
97
+ return kabsch_xform(mobile)
98
+
99
+
100
+ def kabsch_mse(
101
+ pred,
102
+ target,
103
+ align_mask=None,
104
+ mask=None,
105
+ clamp=0.,
106
+ sqrt=False,
107
+ ):
108
+ aligned_target = do_kabsch(
109
+ mobile=target,
110
+ stationary=pred.detach(),
111
+ align_mask=align_mask,
112
+ )
113
+ mse = F.mse_loss(
114
+ pred,
115
+ aligned_target,
116
+ reduction='none',
117
+ ).mean(-1)
118
+
119
+ if clamp > 0:
120
+ mse = torch.clamp(mse, max=clamp**2)
121
+
122
+ if exists(mask):
123
+ mse = torch.sum(
124
+ mse * mask,
125
+ dim=-1,
126
+ ) / torch.sum(
127
+ mask,
128
+ dim=-1,
129
+ )
130
+ else:
131
+ mse = mse.mean(-1)
132
+
133
+ if sqrt:
134
+ mse = mse.sqrt()
135
+
136
+ return mse
137
+
138
+
139
+ def bond_length_l1(
140
+ pred,
141
+ target,
142
+ mask,
143
+ offsets=[1, 2],
144
+ ):
145
+ losses = []
146
+ for c in range(pred.shape[0]):
147
+ m, p, t = mask[c], pred[c], target[c]
148
+ for o in offsets:
149
+ m_ = (torch.stack([m[:-o], m[o:]])).all(0)
150
+ pred_lens = torch.norm(p[:-o] - p[o:], dim=-1)
151
+ target_lens = torch.norm(t[:-o] - t[o:], dim=-1)
152
+
153
+ losses.append(
154
+ torch.abs(pred_lens[m_] - target_lens[m_], ).mean() / o)
155
+
156
+ return torch.stack(losses)
157
+
158
+
159
+ def bb_prmsd_l1(
160
+ pdev,
161
+ pred,
162
+ target,
163
+ align_mask=None,
164
+ mask=None,
165
+ ):
166
+ aligned_target = do_kabsch(
167
+ mobile=target,
168
+ stationary=pred,
169
+ align_mask=align_mask,
170
+ )
171
+ bb_dev = (pred - aligned_target).norm(dim=-1)
172
+ loss = F.l1_loss(
173
+ pdev,
174
+ bb_dev,
175
+ reduction='none',
176
+ )
177
+
178
+ if exists(mask):
179
+ mask = repeat(mask, "b l -> b (l 4)")
180
+ loss = torch.sum(
181
+ loss * mask,
182
+ dim=-1,
183
+ ) / torch.sum(
184
+ mask,
185
+ dim=-1,
186
+ )
187
+ else:
188
+ loss = loss.mean(-1)
189
+
190
+ loss = loss.mean(-1).unsqueeze(0)
191
+
192
+ return loss
193
+
194
+ def bond_len_loss(pred, seq_lens, mask, eps=EPS):
195
+ b, l, a, d = pred.shape
196
+
197
+ pred_bb = pred[:, :, :3]
198
+ mask = repeat(mask, "b l -> b (l 3)")
199
+ for seq_len in seq_lens:
200
+ mask[:, 3 * seq_len - 1] = 0
201
+ mask_bb = mask[:, :-1] * mask[:, 1:]
202
+
203
+ pred_bond_lens = dist(
204
+ rearrange(pred_bb, "b l a d -> b (l a) d")[:, :-1],
205
+ rearrange(pred_bb, "b l a d -> b (l a) d")[:, 1:],
206
+ )
207
+ lit_bond_lens = repeat(
208
+ torch.tensor([BL_N_CA, BL_CA_C, BL_C_N]),
209
+ "bl -> b (l bl)",
210
+ b=b,
211
+ l=l,
212
+ )[:, :-1]
213
+ lit_bond_lens = lit_bond_lens.to(pred_bond_lens.device)
214
+
215
+ bl_loss = torch.abs(pred_bond_lens - lit_bond_lens) * mask_bb
216
+ bl_loss = bl_loss.sum(-1) / (mask.sum(-1) + eps)
217
+
218
+ return bl_loss
219
+
220
+
221
+ def bond_angle_loss(pred, seq_lens, mask, eps=EPS):
222
+ b, l, a, d = pred.shape
223
+
224
+ for seq_len in seq_lens:
225
+ mask[:, seq_len - 1] = 0
226
+ mask_ = mask[:, 1:] * mask[:, :-1]
227
+
228
+ N, CA, C, CB = pred.unbind(-2)
229
+ ba_CA_C_N = angle(CA[:, :-1], C[:, :-1], N[:, 1:], eps=eps)
230
+ ba_CA_C_N_loss = 1 - torch.cos(ba_CA_C_N - BA_CA_C_N * np.pi / 180)
231
+ ba_CA_C_N_loss = ba_CA_C_N_loss * mask_
232
+
233
+ ba_C_N_CA = angle(C[:, :-1], N[:, 1:], CA[:, 1:], eps=eps)
234
+ ba_C_N_CA_loss = 1 - torch.cos(ba_C_N_CA - BA_C_N_CA * np.pi / 180)
235
+ ba_C_N_CA_loss = ba_C_N_CA_loss * mask_
236
+
237
+ loss = ba_CA_C_N_loss + ba_C_N_CA_loss
238
+ loss = loss.sum(-1) / (mask_.sum(-1) + eps)
239
+
240
+ return loss
241
+
242
+
243
+ def vdw_clash_loss(pred, mask, tol=1.5, eps=EPS):
244
+ b, l, a, d = pred.shape
245
+
246
+ mask_ = repeat(mask, "b l -> b (l a)", a=a)
247
+ mask_ = (mask_.unsqueeze(-1) * mask_.unsqueeze(-2))
248
+
249
+ vdw_radii = torch.tensor([VDW_N, VDW_C, VDW_C, VDW_C])
250
+ vdw_radii = repeat(vdw_radii, "a -> b (l a)", b=b, l=l)
251
+ vdw_distances = (vdw_radii.unsqueeze(-2) + vdw_radii.unsqueeze(-3))
252
+ vdw_distances = vdw_distances.to(pred.device)
253
+
254
+ pred_ = rearrange(pred, "b l a d -> b (l a) d")
255
+ atomic_distances = (pred_.unsqueeze(-2) - pred_.unsqueeze(-3)).norm(dim=-1)
256
+
257
+ loss = (vdw_distances - tol - atomic_distances).clamp(min=0)
258
+ loss = loss.sum(dim=(-1, -2)) / (mask_.sum(dim=(-1, -2)) + eps)
259
+
260
+ return loss
261
+
262
+
263
+ def cis_peptide_loss(pred, seq_lens, mask, eps=EPS):
264
+ for seq_len in seq_lens:
265
+ mask[:, seq_len - 1] = 0
266
+ mask_ = mask[:, 1:] * mask[:, :-1]
267
+
268
+ N, CA, C, _ = pred.unbind(-2)
269
+ dih = dihedral(CA[:, :-1], C[:, :-1], N[:, 1:], CA[:, 1:], eps=0)
270
+
271
+ loss = 1 - torch.cos(dih - np.pi)
272
+ loss = loss.sum(dim=(-1, -2)) / (mask_.sum(dim=(-1, -2)) + eps)
273
+
274
+ return loss
275
+
276
+
277
+ def violation_loss(pred, seq_lens, mask, eps=EPS):
278
+ b, l, a, d = pred.shape
279
+
280
+ bl_loss = bond_len_loss(pred, seq_lens, mask, eps=eps)
281
+ ba_loss = bond_angle_loss(pred, seq_lens, mask, eps=eps)
282
+ vdw_loss = vdw_clash_loss(pred, mask)
283
+ cis_loss = cis_peptide_loss(pred, seq_lens, mask, eps=eps)
284
+
285
+ loss = bl_loss + ba_loss + vdw_loss + cis_loss
286
+
287
+ return loss
model/utils/__init__.py ADDED
File without changes
model/utils/ab_metrics.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pyrosetta
2
+
3
+
4
+ def get_vh_vl_orientation(pose_1):
5
+ pose_i1 = pyrosetta.rosetta.protocols.antibody.AntibodyInfo(pose_1)
6
+
7
+ #vl_vh_distance, opening_angle, opposite_angle, packing_angle
8
+ results = pyrosetta.protocols.antibody.vl_vh_orientation_coords(
9
+ pose_1,
10
+ pose_i1,
11
+ )
12
+
13
+ results_labels = [
14
+ 'vl-vh_distance', 'opening_angle', 'opposite_angle', 'packing_angle'
15
+ ]
16
+ results_dict = {}
17
+ for i in range(4):
18
+ results_dict[results_labels[i]] = results[i + 1]
19
+ return results_dict
20
+
21
+
22
+ def get_ab_metrics(
23
+ pose_1,
24
+ pose_2,
25
+ ):
26
+ pose_i1 = pyrosetta.rosetta.protocols.antibody.AntibodyInfo(pose_1)
27
+ pose_i2 = pyrosetta.rosetta.protocols.antibody.AntibodyInfo(pose_2)
28
+
29
+ results = pyrosetta.rosetta.protocols.antibody.cdr_backbone_rmsds(
30
+ pose_1,
31
+ pose_2,
32
+ pose_i1,
33
+ pose_i2,
34
+ )
35
+
36
+ results_labels = [
37
+ 'ocd', 'frh_rms', 'h1_rms', 'h2_rms', 'h3_rms', 'frl_rms', 'l1_rms',
38
+ 'l2_rms', 'l3_rms'
39
+ ]
40
+ results_dict = {}
41
+ for i in range(9):
42
+ results_dict[results_labels[i]] = results[i + 1]
43
+
44
+ return results_dict
45
+
46
+
47
+ def get_pose_cdr_clusters(pose):
48
+ clus_name = {1: 'h1', 2: 'h2', 3: 'h3', 4: 'l1', 5: 'l2', 6: 'l3'}
49
+ ab_info = pyrosetta.rosetta.protocols.antibody.AntibodyInfo(
50
+ pose, pyrosetta.rosetta.protocols.antibody.CDRDefinitionEnum.North)
51
+ ab_info.setup_CDR_clusters(pose)
52
+
53
+ clusters = {}
54
+ for enum in range(1, ab_info.get_total_num_CDRs() + 1):
55
+ result = ab_info.get_CDR_cluster(
56
+ pyrosetta.rosetta.protocols.antibody.CDRNameEnum(enum))
57
+ clus = ab_info.get_cluster_name(result.cluster())
58
+ clusters[clus_name[enum]] = clus
59
+
60
+ return clusters
model/utils/abnumber_.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import warnings
2
+ from abnumber import Chain
3
+ from Bio.PDB import PDBParser, PDBIO
4
+ from Bio.SeqUtils import seq1
5
+
6
+ from igfold.utils.pdb import clean_pdb
7
+
8
+
9
+ def is_heavy(seq):
10
+ chain = Chain(seq, scheme='chothia')
11
+
12
+ return chain.is_heavy_chain()
13
+
14
+
15
+ def rechain_pdb(pdb_file):
16
+ parser = PDBParser()
17
+ with warnings.catch_warnings(record=True):
18
+ structure = parser.get_structure("_", pdb_file)
19
+
20
+ for chain in structure.get_chains():
21
+ seq = seq1(''.join([residue.resname for residue in chain]))
22
+ abnum_chain = Chain(seq, scheme='chothia')
23
+ chain_id = "H" if abnum_chain.is_heavy_chain() else "L"
24
+ try:
25
+ chain.id = chain_id
26
+ except ValueError:
27
+ chain.id = chain_id + "_"
28
+ for chain in structure.get_chains():
29
+ if "_" in chain.id:
30
+ chain.id = chain.id.replace("_", "")
31
+
32
+ io = PDBIO()
33
+ io.set_structure(structure)
34
+ io.save(pdb_file)
35
+
36
+
37
+ def renumber_pdb(
38
+ in_pdb_file,
39
+ out_pdb_file=None,
40
+ scheme="chothia",
41
+ ):
42
+ """
43
+ Renumber the pdb file.
44
+ """
45
+ if out_pdb_file is None:
46
+ out_pdb_file = in_pdb_file
47
+
48
+ clean_pdb(in_pdb_file)
49
+
50
+ parser = PDBParser()
51
+ with warnings.catch_warnings(record=True):
52
+ structure = parser.get_structure(
53
+ "_",
54
+ in_pdb_file,
55
+ )
56
+
57
+ for chain in structure.get_chains():
58
+ seq = seq1(''.join([residue.resname for residue in chain]))
59
+ abnum_chain = Chain(seq, scheme=scheme)
60
+ numbering = abnum_chain.positions.items()
61
+
62
+ chain_res = list(chain.get_residues())
63
+ assert len(chain_res) == len(numbering)
64
+
65
+ for pdb_r, (pos, aa) in zip(chain_res, numbering):
66
+ if aa != seq1(pdb_r.get_resname()):
67
+ raise Exception(f"Failed to renumber PDB file {in_pdb_file}")
68
+ pos = str(pos)[1:]
69
+ if not pos[-1].isnumeric():
70
+ ins = pos[-1]
71
+ pos = int(pos[:-1])
72
+ else:
73
+ pos = int(pos)
74
+ ins = ' '
75
+
76
+ pdb_r._id = (' ', pos, ins)
77
+
78
+ io = PDBIO()
79
+ io.set_structure(structure)
80
+ io.save(out_pdb_file)
81
+
82
+
83
+ def truncate_seq(seq, scheme="chothia"):
84
+ abnum_chain = Chain(seq, scheme=scheme)
85
+ numbering = abnum_chain.positions.items()
86
+ seq = "".join([r[1] for r in list(numbering)])
87
+
88
+ return seq
model/utils/constants.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ EPS = 1e-6
2
+
3
+ # Values from Structure quality and target parameters (Engh and Huber)
4
+
5
+ # Backbone bond lengths
6
+ BL_N_CA = 1.459
7
+ BL_CA_C = 1.525
8
+ BL_C_N = 1.336
9
+ BL_C_O = 1.229
10
+
11
+ # Backbone bond angles
12
+ BA_N_CA_C = 111.0
13
+ BA_N_CA_CB = 110.6
14
+ BA_CA_C_N = 117.2
15
+ BA_CA_C_O = 120.1
16
+ BA_O_C_N = 122.7
17
+ BA_C_CA_CB = 110.6
18
+ BA_C_N_CA = 121.7
19
+
20
+ # Van der Waals radii
21
+ VDW_N = 1.55
22
+ VDW_C = 1.7
model/utils/coordinates.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+
4
+ def place_fourth_atom(
5
+ a_coord: torch.Tensor,
6
+ b_coord: torch.Tensor,
7
+ c_coord: torch.Tensor,
8
+ length: torch.Tensor,
9
+ planar: torch.Tensor,
10
+ dihedral: torch.Tensor,
11
+ ) -> torch.Tensor:
12
+ """
13
+ Given 3 coords + a length + a planar angle + a dihedral angle, compute a fourth coord
14
+ """
15
+ bc_vec = b_coord - c_coord
16
+ bc_vec = bc_vec / bc_vec.norm(dim=-1, keepdim=True)
17
+
18
+ n_vec = (b_coord - a_coord).expand(bc_vec.shape).cross(bc_vec)
19
+ n_vec = n_vec / n_vec.norm(dim=-1, keepdim=True)
20
+
21
+ m_vec = [bc_vec, n_vec.cross(bc_vec), n_vec]
22
+ d_vec = [
23
+ length * torch.cos(planar),
24
+ length * torch.sin(planar) * torch.cos(dihedral),
25
+ -length * torch.sin(planar) * torch.sin(dihedral)
26
+ ]
27
+
28
+ d_coord = c_coord + sum([m * d for m, d in zip(m_vec, d_vec)])
29
+
30
+ return d_coord
31
+
32
+
33
+ def get_ideal_coords(center=False):
34
+ N = torch.tensor([[0, 0, -1.458]], dtype=float)
35
+ A = torch.tensor([[0, 0, 0]], dtype=float)
36
+ B = torch.tensor([[0, 1.426, 0.531]], dtype=float)
37
+ C = place_fourth_atom(
38
+ B,
39
+ A,
40
+ N,
41
+ torch.tensor(2.460),
42
+ torch.tensor(0.615),
43
+ torch.tensor(-2.143),
44
+ )
45
+
46
+ coords = torch.cat([N, A, C, B]).float()
47
+
48
+ if center:
49
+ coords -= coords.mean(
50
+ dim=0,
51
+ keepdim=True,
52
+ )
53
+
54
+ return coords
55
+
56
+
57
+ def place_o_coords(coords):
58
+ N = coords[:, :, 0]
59
+ A = coords[:, :, 1]
60
+ C = coords[:, :, 2]
61
+
62
+ o_coords = place_fourth_atom(
63
+ torch.roll(N, shifts=-1, dims=1),
64
+ A,
65
+ C,
66
+ torch.tensor(1.231),
67
+ torch.tensor(2.108),
68
+ torch.tensor(-3.142),
69
+ ).unsqueeze(2)
70
+
71
+ coords = torch.cat(
72
+ [coords, o_coords],
73
+ dim=2,
74
+ )
75
+
76
+ return coords
model/utils/embed.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from einops import rearrange
2
+
3
+ from igfold import IgFoldInput
4
+ from igfold.utils.folding import get_sequence_dict, process_template
5
+
6
+
7
+ def embed(
8
+ antiberty,
9
+ model,
10
+ fasta_file=None,
11
+ sequences=None,
12
+ template_pdb=None,
13
+ ignore_cdrs=None,
14
+ ignore_chain=None,
15
+ mask=None,
16
+ ):
17
+ seq_dict = get_sequence_dict(
18
+ sequences,
19
+ fasta_file,
20
+ )
21
+
22
+ embeddings, attentions = antiberty.embed(
23
+ seq_dict.values(),
24
+ return_attention=True,
25
+ )
26
+ embeddings = [e[1:-1].unsqueeze(0) for e in embeddings]
27
+ attentions = [a[:, :, 1:-1, 1:-1].unsqueeze(0) for a in attentions]
28
+
29
+ temp_coords, temp_mask = process_template(
30
+ template_pdb,
31
+ fasta_file,
32
+ ignore_cdrs=ignore_cdrs,
33
+ ignore_chain=ignore_chain,
34
+ )
35
+ model_in = IgFoldInput(
36
+ embeddings=embeddings,
37
+ attentions=attentions,
38
+ template_coords=temp_coords,
39
+ template_mask=temp_mask,
40
+ return_embeddings=True,
41
+ batch_mask=mask,
42
+ )
43
+
44
+ model_out = model(model_in)
45
+
46
+ prmsd = rearrange(
47
+ model_out.prmsd,
48
+ "b (l a) -> b l a",
49
+ a=4,
50
+ )
51
+ model_out.prmsd = prmsd
52
+
53
+ return model_out
model/utils/fasta.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from Bio import SeqIO, pairwise2
2
+
3
+
4
+ def get_fasta_chain_seq(
5
+ fasta_file,
6
+ chain_id,
7
+ ):
8
+ for chain in SeqIO.parse(fasta_file, 'fasta'):
9
+ if ":{}".format(chain_id) in chain.id:
10
+ return str(chain.seq)
11
+
12
+
13
+ def get_fasta_chain_dict(fasta_file):
14
+ seq_dict = {}
15
+ for chain in SeqIO.parse(fasta_file, 'fasta'):
16
+ seq_dict[chain.id] = str(chain.seq)
17
+
18
+ return seq_dict
19
+
20
+
21
+ def pairwise_align(
22
+ seq1,
23
+ seq2,
24
+ ):
25
+ ###
26
+ # Aligns two sequences using the Needleman-Wunsch algorithm
27
+ # Returns alignment of seq2 into seq1
28
+ ###
29
+ ali = pairwise2.align.globalxx(
30
+ seq1,
31
+ seq2,
32
+ )[0]
33
+ ali_list = []
34
+ seq1_i, seq2_i = 0, 0
35
+ for ali_seq in ali.seqB.split("-"):
36
+ if len(ali_seq) == 0:
37
+ seq1_i += 1
38
+ else:
39
+ l = len(ali_seq)
40
+ ali_list.append((seq1_i, seq1_i + l, seq2_i, seq2_i + l))
41
+ seq1_i += l
42
+
43
+ return ali_list
model/utils/folding.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List
3
+ from einops import rearrange
4
+ import torch
5
+ import numpy as np
6
+
7
+ from igfold.model.interface import IgFoldInput
8
+ from igfold.utils.fasta import get_fasta_chain_dict
9
+ from igfold.utils.general import exists
10
+ from igfold.utils.pdb import get_atom_coords, save_PDB, write_pdb_bfactor, cdr_indices
11
+
12
+
13
+ def get_sequence_dict(
14
+ sequences,
15
+ fasta_file,
16
+ ):
17
+ if exists(sequences) and exists(fasta_file):
18
+ print("Both sequences and fasta file provided. Using fasta file.")
19
+ seq_dict = get_fasta_chain_dict(fasta_file)
20
+ elif not exists(sequences) and exists(fasta_file):
21
+ seq_dict = get_fasta_chain_dict(fasta_file)
22
+ elif exists(sequences):
23
+ seq_dict = sequences
24
+ else:
25
+ exit("Must provide sequences or fasta file.")
26
+
27
+ return seq_dict
28
+
29
+
30
+ def process_template(
31
+ pdb_file,
32
+ fasta_file,
33
+ ignore_cdrs=None,
34
+ ignore_chain=None,
35
+ ):
36
+ temp_coords, temp_mask = None, None
37
+ if exists(pdb_file):
38
+ temp_coords = get_atom_coords(
39
+ pdb_file,
40
+ fasta_file=fasta_file,
41
+ )
42
+ temp_coords = torch.stack(
43
+ [
44
+ temp_coords['N'], temp_coords['CA'], temp_coords['C'],
45
+ temp_coords['CB']
46
+ ],
47
+ dim=1,
48
+ ).view(-1, 3).unsqueeze(0)
49
+
50
+ temp_mask = torch.ones(temp_coords.shape[:2]).bool()
51
+ temp_mask[temp_coords.isnan().any(-1)] = False
52
+ temp_mask[temp_coords.sum(-1) == 0] = False
53
+
54
+ if exists(ignore_cdrs):
55
+ cdr_names = ["h1", "h2", "h3", "l1", "l2", "l3"]
56
+ if ignore_cdrs == False:
57
+ cdr_names = []
58
+ elif isinstance(ignore_cdrs, list):
59
+ cdr_names = ignore_cdrs
60
+ elif isinstance(ignore_cdrs, str):
61
+ cdr_names = [ignore_cdrs]
62
+
63
+ for cdr in cdr_names:
64
+ cdr_range = cdr_indices(pdb_file, cdr)
65
+ temp_mask[:, (cdr_range[0] - 1) * 4:(cdr_range[1] + 2) *
66
+ 4] = False
67
+ if exists(ignore_chain) and ignore_chain in ["H", "L"]:
68
+ seq_dict = get_fasta_chain_dict(fasta_file)
69
+ hlen = len(seq_dict["H"])
70
+ if ignore_chain == "H":
71
+ temp_mask[:, :hlen * 4] = False
72
+ elif ignore_chain == "L":
73
+ temp_mask[:, hlen * 4:] = False
74
+
75
+ return temp_coords, temp_mask
76
+
77
+
78
+ def process_prediction(
79
+ model_out,
80
+ pdb_file,
81
+ fasta_file,
82
+ skip_pdb=False,
83
+ do_refine=True,
84
+ use_openmm=False,
85
+ do_renum=False,
86
+ ):
87
+ prmsd = rearrange(
88
+ model_out.prmsd,
89
+ "b (l a) -> b l a",
90
+ a=4,
91
+ )
92
+ model_out.prmsd = prmsd
93
+
94
+ if skip_pdb:
95
+ return model_out
96
+
97
+ coords = model_out.coords.squeeze(0).detach()
98
+ res_rmsd = prmsd.square().mean(dim=-1).sqrt().squeeze(0)
99
+
100
+ seq_dict = get_fasta_chain_dict(fasta_file)
101
+ full_seq = "".join(list(seq_dict.values()))
102
+ chains = list(seq_dict.keys())
103
+ delims = np.cumsum([len(s) for s in seq_dict.values()]).tolist()
104
+
105
+ write_pdb = not do_refine or use_openmm
106
+ pdb_string = save_PDB(
107
+ pdb_file,
108
+ coords,
109
+ full_seq,
110
+ chains=chains,
111
+ atoms=['N', 'CA', 'C', 'CB', 'O'],
112
+ error=res_rmsd,
113
+ delim=delims,
114
+ write_pdb=write_pdb,
115
+ )
116
+
117
+ if do_refine:
118
+ if use_openmm:
119
+ try:
120
+ from igfold.refine.openmm_ref import refine
121
+ refine_input = [pdb_file]
122
+ except:
123
+ exit("OpenMM not installed. Please install OpenMM to use refinement.")
124
+ else:
125
+ try:
126
+ from igfold.refine.pyrosetta_ref import refine
127
+ refine_input = [pdb_file, pdb_string]
128
+ except:
129
+ exit("PyRosetta not installed. Please install PyRosetta to use refinement.")
130
+
131
+ refine(*refine_input)
132
+
133
+ if do_renum:
134
+ try:
135
+ from igfold.utils.abnumber_ import renumber_pdb
136
+ except:
137
+ exit("AbNumber not installed. Please install AbNumber to use renumbering.")
138
+
139
+ renumber_pdb(
140
+ pdb_file,
141
+ pdb_file,
142
+ )
143
+
144
+ write_pdb_bfactor(
145
+ pdb_file,
146
+ pdb_file,
147
+ bfactor=res_rmsd,
148
+ )
149
+
150
+ return model_out
151
+
152
+
153
+ def fold(
154
+ antiberty,
155
+ models,
156
+ pdb_file,
157
+ fasta_file=None,
158
+ sequences=None,
159
+ template_pdb=None,
160
+ ignore_cdrs=None,
161
+ ignore_chain=None,
162
+ skip_pdb=False,
163
+ do_refine=True,
164
+ use_openmm=False,
165
+ do_renum=True,
166
+ truncate_sequences=False,
167
+ ):
168
+ seq_dict = get_sequence_dict(
169
+ sequences,
170
+ fasta_file,
171
+ )
172
+
173
+ if truncate_sequences:
174
+ try:
175
+ from igfold.utils.abnumber_ import truncate_seq
176
+ except:
177
+ exit("AbNumber not installed. Please install AbNumber to use truncation.")
178
+
179
+ seq_dict = {k: truncate_seq(v) for k, v in seq_dict.items()}
180
+
181
+ if not exists(fasta_file):
182
+ fasta_file = pdb_file.replace(".pdb", ".fasta")
183
+ with open(fasta_file, "w") as f:
184
+ for chain, seq in seq_dict.items():
185
+ f.write(">{}\n{}\n".format(
186
+ chain,
187
+ seq,
188
+ ))
189
+
190
+ embeddings, attentions = antiberty.embed(
191
+ seq_dict.values(),
192
+ return_attention=True,
193
+ )
194
+ embeddings = [e[1:-1].unsqueeze(0) for e in embeddings]
195
+ attentions = [a[:, :, 1:-1, 1:-1].unsqueeze(0) for a in attentions]
196
+
197
+ temp_coords, temp_mask = process_template(
198
+ template_pdb,
199
+ fasta_file,
200
+ ignore_cdrs=ignore_cdrs,
201
+ ignore_chain=ignore_chain,
202
+ )
203
+ model_in = IgFoldInput(
204
+ embeddings=embeddings,
205
+ attentions=attentions,
206
+ template_coords=temp_coords,
207
+ template_mask=temp_mask,
208
+ return_embeddings=True,
209
+ )
210
+
211
+ model_outs, scores = [], []
212
+ with torch.no_grad():
213
+ for i, model in enumerate(models):
214
+ model_out = model(model_in)
215
+ model_out = model.gradient_refine(model_in, model_out)
216
+ scores.append(model_out.prmsd.quantile(0.9))
217
+ model_outs.append(model_out)
218
+
219
+ best_model_i = scores.index(min(scores))
220
+ model_out = model_outs[best_model_i]
221
+ process_prediction(
222
+ model_out,
223
+ pdb_file,
224
+ fasta_file,
225
+ skip_pdb=skip_pdb,
226
+ do_refine=do_refine,
227
+ use_openmm=use_openmm,
228
+ do_renum=do_renum,
229
+ )
230
+
231
+ return model_out
model/utils/general.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _aa_dict = {
2
+ 'A': '0',
3
+ 'C': '1',
4
+ 'D': '2',
5
+ 'E': '3',
6
+ 'F': '4',
7
+ 'G': '5',
8
+ 'H': '6',
9
+ 'I': '7',
10
+ 'K': '8',
11
+ 'L': '9',
12
+ 'M': '10',
13
+ 'N': '11',
14
+ 'P': '12',
15
+ 'Q': '13',
16
+ 'R': '14',
17
+ 'S': '15',
18
+ 'T': '16',
19
+ 'V': '17',
20
+ 'W': '18',
21
+ 'Y': '19'
22
+ }
23
+
24
+ _aa_1_3_dict = {
25
+ 'A': 'ALA',
26
+ 'C': 'CYS',
27
+ 'D': 'ASP',
28
+ 'E': 'GLU',
29
+ 'F': 'PHE',
30
+ 'G': 'GLY',
31
+ 'H': 'HIS',
32
+ 'I': 'ILE',
33
+ 'K': 'LYS',
34
+ 'L': 'LEU',
35
+ 'M': 'MET',
36
+ 'N': 'ASN',
37
+ 'P': 'PRO',
38
+ 'Q': 'GLN',
39
+ 'R': 'ARG',
40
+ 'S': 'SER',
41
+ 'T': 'THR',
42
+ 'V': 'VAL',
43
+ 'W': 'TRP',
44
+ 'Y': 'TYR',
45
+ '-': 'GAP'
46
+ }
47
+
48
+
49
+ def exists(x):
50
+ return x is not None
51
+
52
+
53
+ def default(
54
+ val,
55
+ d,
56
+ ):
57
+ return val if exists(val) else d
model/utils/geometry.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from einops import rearrange
3
+
4
+ from igfold.utils.constants import EPS
5
+
6
+
7
+ def normed_vec(vec, eps=EPS):
8
+ mag_sq = torch.sum(vec**2, dim=-1, keepdim=True)
9
+ mag = torch.sqrt(mag_sq + eps)
10
+ vec = vec / mag
11
+
12
+ return vec
13
+
14
+
15
+ def normed_cross(vec1, vec2, eps=EPS):
16
+ vec1 = normed_vec(vec1, eps=eps)
17
+ vec2 = normed_vec(vec2, eps=eps)
18
+ cross = torch.cross(vec1, vec2, dim=-1)
19
+
20
+ return cross
21
+
22
+
23
+ def dist(x_1, x_2, eps=EPS):
24
+ d_sq = (x_1 - x_2)**2
25
+ d = torch.sqrt(d_sq.sum(-1) + eps)
26
+
27
+ return d
28
+
29
+
30
+ def dist_mat(c1, c2, dim=-3, eps=EPS):
31
+ c1 = c1.unsqueeze(dim)
32
+ c2 = c2.unsqueeze(dim - 1)
33
+ d = dist(c1, c2, eps=eps)
34
+
35
+ return d
36
+
37
+
38
+ def angle(x_1, x_2, x_3, eps=EPS):
39
+ a = normed_vec(x_1 - x_2, eps=eps)
40
+ b = normed_vec(x_3 - x_2, eps=eps)
41
+ ang = torch.arccos((a * b).sum(-1))
42
+
43
+ return ang
44
+
45
+
46
+ def dihedral(x_1, x_2, x_3, x_4, eps=EPS):
47
+ b1 = normed_vec(x_1 - x_2, eps=eps)
48
+ b2 = normed_vec(x_2 - x_3, eps=eps)
49
+ b3 = normed_vec(x_3 - x_4, eps=eps)
50
+ n1 = normed_cross(b1, b2, eps=eps)
51
+ n2 = normed_cross(b2, b3, eps=eps)
52
+ m1 = normed_cross(n1, b2, eps=eps)
53
+ x = (n1 * n2).sum(-1)
54
+ y = (m1 * n2).sum(-1)
55
+
56
+ dih = torch.atan2(y, x)
57
+
58
+ return dih
59
+
60
+
61
+ def coords_to_frame(coords, eps=EPS):
62
+ if len(coords.shape) == 3:
63
+ coords = rearrange(
64
+ coords,
65
+ "b (l a) d -> b l a d",
66
+ l=coords.shape[-2] // 4,
67
+ )
68
+
69
+ N, CA, C, _ = coords.unbind(-2)
70
+ CA_N = normed_vec(N - CA, eps=eps)
71
+ CA_C = normed_vec(C - CA, eps=eps)
72
+ n1 = CA_N
73
+ n2 = normed_cross(n1, CA_C, eps=eps)
74
+ n3 = normed_cross(n1, n2, eps=eps)
75
+ rot = torch.stack([n1, n2, n3], -1)
76
+
77
+ return CA, rot
model/utils/pdb.py ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import sys
3
+ import io
4
+ from typing import Union, List
5
+ import requests
6
+ import warnings
7
+ from os.path import splitext, basename
8
+ from Bio import PDB
9
+ from Bio.PDB import PDBParser, PDBIO
10
+ from Bio.SeqUtils import seq1
11
+ from Bio import SeqIO
12
+ from bisect import bisect_left, bisect_right
13
+ import torch
14
+ import numpy as np
15
+
16
+ from igfold.utils.coordinates import place_fourth_atom
17
+ from igfold.utils.fasta import get_fasta_chain_seq
18
+ from igfold.utils.general import _aa_1_3_dict, exists
19
+
20
+
21
+ def renumber_pdb(old_pdb, renum_pdb=None):
22
+ if not exists(renum_pdb):
23
+ renum_pdb = old_pdb
24
+
25
+ success = False
26
+ time.sleep(5)
27
+ for i in range(10):
28
+ try:
29
+ with open(old_pdb, 'rb') as f:
30
+ response = requests.post(
31
+ 'http://www.bioinf.org.uk/abs/abnum/abnumpdb.cgi',
32
+ params={
33
+ "plain": "1",
34
+ "output": "-HL",
35
+ "scheme": "-c"
36
+ },
37
+ files={"pdb": f},
38
+ )
39
+
40
+ success = response.status_code == 200 and not ("<html>"
41
+ in response.text)
42
+
43
+ if success:
44
+ break
45
+ else:
46
+ time.sleep((i + 1) * 5)
47
+ except requests.exceptions.ConnectionError:
48
+ time.sleep(60)
49
+
50
+ # if success:
51
+ if success:
52
+ new_pdb_data = response.text
53
+ with open(renum_pdb, "w") as f:
54
+ f.write(new_pdb_data)
55
+ else:
56
+ print(
57
+ "Failed to renumber PDB. This is likely due to a connection error or a timeout with the AbNum server."
58
+ )
59
+
60
+
61
+ def count_pdb_chains(pdb_file):
62
+ parser = PDBParser()
63
+ with warnings.catch_warnings(record=True):
64
+ structure = parser.get_structure("_", pdb_file)
65
+
66
+ l = len(list(structure.get_chains()))
67
+
68
+ return l
69
+
70
+
71
+ def reorder_pdb_chains(pdb_file, chain_order):
72
+ """Reorder the chains in a PDB file and update residue numbers"""
73
+
74
+ parser = PDBParser()
75
+ with warnings.catch_warnings(record=True):
76
+ structure = parser.get_structure("_", pdb_file)
77
+
78
+ chains = list(structure.get_chains())
79
+ if len(chains) != len(chain_order):
80
+ raise ValueError(
81
+ f"Number of chains in PDB file ({len(chains)}) does not match number of chains in chain order ({len(chain_order)})"
82
+ )
83
+
84
+ chain_order = [c.upper() for c in chain_order]
85
+ sorted_chains = sorted(chains, key=lambda c: chain_order.index(c.id))
86
+
87
+ new_structure = PDB.Structure.Structure("_")
88
+ new_model = PDB.Model.Model(0)
89
+ new_structure.add(new_model)
90
+ atom_num = 1
91
+ for chain in sorted_chains:
92
+ new_chain = PDB.Chain.Chain(chain.id)
93
+ new_model.add(new_chain)
94
+ for residue in chain.get_residues():
95
+ new_residue = PDB.Residue.Residue(
96
+ residue.id,
97
+ residue.resname,
98
+ residue.segid,
99
+ )
100
+ new_chain.add(new_residue)
101
+ for atom in residue:
102
+ new_atom = PDB.Atom.Atom(
103
+ atom.name,
104
+ atom.coord,
105
+ atom.occupancy,
106
+ atom.bfactor,
107
+ atom.altloc,
108
+ atom.fullname,
109
+ atom_num,
110
+ atom.element,
111
+ )
112
+ new_residue.add(new_atom)
113
+ atom_num += 1
114
+
115
+ io = PDBIO()
116
+ io.set_structure(new_structure)
117
+ io.save(pdb_file)
118
+
119
+
120
+ def get_atom_coord(residue, atom_type):
121
+ if exists(residue) and atom_type in residue:
122
+ return residue[atom_type].get_coord()
123
+ else:
124
+ return [0, 0, 0]
125
+
126
+
127
+ def get_cb_or_ca_coord(residue):
128
+ if not exists(residue):
129
+ return [0, 0, 0]
130
+
131
+ if 'CB' in residue:
132
+ return residue['CB'].get_coord()
133
+ elif 'CA' in residue:
134
+ return residue['CA'].get_coord()
135
+ else:
136
+ return [0, 0, 0]
137
+
138
+
139
+ def get_continuous_ranges(residues):
140
+ """ Returns ranges of residues which are continuously connected (peptide bond length 1.2-1.45 Å) """
141
+ dists = []
142
+ for res_i in range(len(residues) - 1):
143
+ dists.append(
144
+ np.linalg.norm(
145
+ np.array(get_atom_coord(residues[res_i], "C")) -
146
+ np.array(get_atom_coord(residues[res_i + 1], "N"))))
147
+
148
+ ranges = []
149
+ start_i = 0
150
+ for d_i, d in enumerate(dists):
151
+ if d > 1.45 or d < 1.2:
152
+ ranges.append((start_i, d_i + 1))
153
+ start_i = d_i + 1
154
+ if d_i == len(dists) - 1:
155
+ ranges.append((start_i, None))
156
+
157
+ return ranges
158
+
159
+
160
+ def place_missing_cb_o(atom_coords):
161
+ cb_coords = place_fourth_atom(
162
+ atom_coords['C'],
163
+ atom_coords['N'],
164
+ atom_coords['CA'],
165
+ torch.tensor(1.522),
166
+ torch.tensor(1.927),
167
+ torch.tensor(-2.143),
168
+ )
169
+ o_coords = place_fourth_atom(
170
+ torch.roll(atom_coords['N'], shifts=-1, dims=0),
171
+ atom_coords['CA'],
172
+ atom_coords['C'],
173
+ torch.tensor(1.231),
174
+ torch.tensor(2.108),
175
+ torch.tensor(-3.142),
176
+ )
177
+
178
+ bb_mask = get_atom_coords_mask(atom_coords['N']) & get_atom_coords_mask(
179
+ atom_coords['CA']) & get_atom_coords_mask(atom_coords['C'])
180
+ missing_cb = (get_atom_coords_mask(atom_coords['CB']) & bb_mask) == 0
181
+ atom_coords['CB'][missing_cb] = cb_coords[missing_cb]
182
+
183
+ bb_mask = get_atom_coords_mask(
184
+ torch.roll(
185
+ atom_coords['N'],
186
+ shifts=-1,
187
+ dims=0,
188
+ )) & get_atom_coords_mask(atom_coords['CA']) & get_atom_coords_mask(
189
+ atom_coords['C'])
190
+ missing_o = (get_atom_coords_mask(atom_coords['O']) & bb_mask) == 0
191
+ atom_coords['O'][missing_o] = o_coords[missing_o]
192
+
193
+
194
+ def get_atom_coords(pdb_file, fasta_file=None):
195
+ p = PDBParser()
196
+ file_name = splitext(basename(pdb_file))[0]
197
+ structure = p.get_structure(
198
+ file_name,
199
+ pdb_file,
200
+ )
201
+
202
+ if fasta_file:
203
+ residues = []
204
+ for chain in structure.get_chains():
205
+ pdb_seq = get_pdb_chain_seq(
206
+ pdb_file,
207
+ chain.id,
208
+ )
209
+
210
+ chain_dict = {"A": "H", "B": "L", "H": "H", "L": "L"}
211
+ fasta_seq = get_fasta_chain_seq(
212
+ fasta_file,
213
+ chain_dict[chain.id],
214
+ )
215
+
216
+ chain_residues = list(chain.get_residues())
217
+ continuous_ranges = get_continuous_ranges(chain_residues)
218
+
219
+ fasta_residues = [None for _ in range(len(fasta_seq))]
220
+ fasta_r = (0, 0)
221
+ for pdb_r in continuous_ranges:
222
+ fasta_r_start = fasta_seq[fasta_r[1]:].index(
223
+ pdb_seq[pdb_r[0]:pdb_r[1]]) + fasta_r[1]
224
+ fasta_r_end = (len(pdb_seq) if pdb_r[1] == None else
225
+ pdb_r[1]) - pdb_r[0] + fasta_r_start
226
+ fasta_r = (fasta_r_start, fasta_r_end)
227
+ fasta_residues[fasta_r[0]:fasta_r[1]] = chain_residues[
228
+ pdb_r[0]:pdb_r[1]]
229
+
230
+ residues += fasta_residues
231
+ else:
232
+ residues = list(structure.get_residues())
233
+
234
+ n_coords = torch.tensor([get_atom_coord(r, 'N') for r in residues])
235
+ ca_coords = torch.tensor([get_atom_coord(r, 'CA') for r in residues])
236
+ c_coords = torch.tensor([get_atom_coord(r, 'C') for r in residues])
237
+ cb_coords = torch.tensor([get_atom_coord(r, 'CB') for r in residues])
238
+ cb_ca_coords = torch.tensor([get_cb_or_ca_coord(r) for r in residues])
239
+ o_coords = torch.tensor([get_atom_coord(r, 'O') for r in residues])
240
+
241
+ atom_coords = {}
242
+ atom_coords['N'] = n_coords
243
+ atom_coords['CA'] = ca_coords
244
+ atom_coords['C'] = c_coords
245
+ atom_coords['CB'] = cb_coords
246
+ atom_coords['CBCA'] = cb_ca_coords
247
+ atom_coords['O'] = o_coords
248
+
249
+ place_missing_cb_o(atom_coords)
250
+
251
+ return atom_coords
252
+
253
+
254
+ def get_atom_coords_mask(coords):
255
+ mask = torch.ByteTensor([1 if sum(_) != 0 else 0 for _ in coords])
256
+ mask = mask & (1 - torch.any(torch.isnan(coords), dim=1).byte())
257
+ return mask
258
+
259
+
260
+ def get_atom_coords_mask_for_dict(atom_coords):
261
+ atom_coords_masks = {}
262
+ for atom, coords in atom_coords.items():
263
+ atom_coords_masks[atom] = get_atom_coords_mask(coords)
264
+
265
+ return atom_coords_masks
266
+
267
+
268
+ def pdb2fasta(pdb_file, num_chains=None):
269
+ """Converts a PDB file to a fasta formatted string using its ATOM data"""
270
+ pdb_id = basename(pdb_file).split('.')[0]
271
+ parser = PDBParser()
272
+ structure = parser.get_structure(
273
+ pdb_id,
274
+ pdb_file,
275
+ )
276
+
277
+ real_num_chains = len([0 for _ in structure.get_chains()])
278
+ if num_chains is not None and num_chains != real_num_chains:
279
+ print('WARNING: Skipping {}. Expected {} chains, got {}'.format(
280
+ pdb_file, num_chains, real_num_chains))
281
+ return ''
282
+
283
+ fasta = ''
284
+ for chain in structure.get_chains():
285
+ id_ = chain.id
286
+ seq = seq1(''.join([residue.resname for residue in chain]))
287
+ fasta += '>{}:{}\t{}\n'.format(pdb_id, id_, len(seq))
288
+ max_line_length = 80
289
+ for i in range(0, len(seq), max_line_length):
290
+ fasta += f'{seq[i:i + max_line_length]}\n'
291
+ return fasta
292
+
293
+
294
+ def get_pdb_chain_seq(
295
+ pdb_file,
296
+ chain_id,
297
+ ):
298
+ p = PDBParser()
299
+ file_name = splitext(basename(pdb_file))[0]
300
+ structure = p.get_structure(
301
+ file_name,
302
+ pdb_file,
303
+ )
304
+
305
+ pdb_seq = None
306
+ for chain in structure.get_chains():
307
+ if chain.id == chain_id:
308
+ pdb_seq = "".join(
309
+ [seq1(r.get_resname()) for r in chain.get_residues()])
310
+
311
+ return pdb_seq
312
+
313
+
314
+ def cdr_indices(
315
+ chothia_pdb_file,
316
+ cdr,
317
+ offset_heavy=True,
318
+ ):
319
+ """Gets the index of a given CDR loop"""
320
+ cdr_chothia_range_dict = {
321
+ "h1": (26, 32),
322
+ "h2": (52, 56),
323
+ "h3": (95, 102),
324
+ "l1": (24, 34),
325
+ "l2": (50, 56),
326
+ "l3": (89, 97)
327
+ }
328
+
329
+ cdr = str.lower(cdr)
330
+ assert cdr in cdr_chothia_range_dict.keys()
331
+
332
+ chothia_range = cdr_chothia_range_dict[cdr]
333
+ chain_id = cdr[0].upper()
334
+
335
+ parser = PDBParser()
336
+ pdb_id = basename(chothia_pdb_file).split('.')[0]
337
+ structure = parser.get_structure(
338
+ pdb_id,
339
+ chothia_pdb_file,
340
+ )
341
+ cdr_chain_structure = None
342
+ for chain in structure.get_chains():
343
+ if chain.id == chain_id:
344
+ cdr_chain_structure = chain
345
+ break
346
+ if cdr_chain_structure is None:
347
+ print("PDB must have a chain with chain id \"[PBD ID]:{}\"".format(
348
+ chain_id))
349
+ sys.exit(-1)
350
+
351
+ residue_id_nums = [res.get_id()[1] for res in cdr_chain_structure]
352
+
353
+ # Binary search to find the start and end of the CDR loop
354
+ cdr_start = bisect_left(
355
+ residue_id_nums,
356
+ chothia_range[0],
357
+ )
358
+ cdr_end = bisect_right(
359
+ residue_id_nums,
360
+ chothia_range[1],
361
+ ) - 1
362
+
363
+ if len(get_pdb_chain_seq(
364
+ chothia_pdb_file,
365
+ chain_id=chain_id,
366
+ )) != len(residue_id_nums):
367
+ print('ERROR in PDB file ' + chothia_pdb_file)
368
+ print('residue id len', len(residue_id_nums))
369
+
370
+ if chain_id == "L" and offset_heavy:
371
+ heavy_seq_len = get_pdb_chain_seq(
372
+ chothia_pdb_file,
373
+ chain_id="H",
374
+ )
375
+ cdr_start += len(heavy_seq_len)
376
+ cdr_end += len(heavy_seq_len)
377
+
378
+ return cdr_start, cdr_end
379
+
380
+
381
+ def get_cdr_range_dict(
382
+ chothia_pdb_file,
383
+ heavy_only=False,
384
+ light_only=False,
385
+ offset_heavy=True,
386
+ ):
387
+ cdr_names = ["h1", "h2", "h3", "l1", "l2", "l3"]
388
+ if heavy_only:
389
+ cdr_names = cdr_names[:3]
390
+ if light_only:
391
+ cdr_names = cdr_names[3:]
392
+
393
+ cdr_range_dict = {
394
+ cdr: cdr_indices(
395
+ chothia_pdb_file,
396
+ cdr,
397
+ offset_heavy=offset_heavy,
398
+ )
399
+ for cdr in cdr_names
400
+ }
401
+
402
+ return cdr_range_dict
403
+
404
+
405
+ def h3_indices(chothia_pdb_file):
406
+ """Gets the index of the CDR H3 loop"""
407
+
408
+ return cdr_indices(chothia_pdb_file, cdr="h3")
409
+
410
+
411
+ def get_chain_numbering(
412
+ pdb_file,
413
+ chain_id,
414
+ ):
415
+ seq = []
416
+ parser = PDBParser()
417
+ structure = parser.get_structure("_", pdb_file)
418
+ for chain in structure.get_chains():
419
+ if chain.id == chain_id:
420
+ for r in chain.get_residues():
421
+ res_num = str(r._id[1]) + r._id[2]
422
+ res_num = res_num.replace(" ", "")
423
+ seq.append(res_num)
424
+
425
+ return seq
426
+
427
+
428
+ def save_PDB(
429
+ out_pdb: str,
430
+ coords: torch.Tensor,
431
+ seq: str,
432
+ chains: List[str] = None,
433
+ error: torch.Tensor = None,
434
+ delim: Union[int, List[int]] = None,
435
+ atoms=['N', 'CA', 'C', 'O', 'CB'],
436
+ write_pdb=True,
437
+ ) -> None:
438
+ """
439
+ Write set of N, CA, C, O, CB coords to PDB file
440
+ """
441
+
442
+ if not exists(chains):
443
+ chains = ["H", "L"]
444
+
445
+ if type(delim) == type(None):
446
+ delim = -1
447
+ elif type(delim) == int:
448
+ delim = [delim]
449
+
450
+ if not exists(error):
451
+ error = torch.zeros(len(seq))
452
+
453
+ pdb_string = ""
454
+ k = 0
455
+ for r, residue in enumerate(coords):
456
+ AA = _aa_1_3_dict[seq[r]]
457
+ for a, atom in enumerate(residue):
458
+ chain_num = np.where(np.array(delim) - r > 0)[0][0]
459
+ chain_id = chains[chain_num]
460
+
461
+ if AA == "GLY" and atoms[a] == "CB": continue
462
+ x, y, z = atom
463
+ pdb_string += "ATOM %5d %-2s %3s %s%4d %8.3f%8.3f%8.3f %4.2f %4.2f %s \n" % (
464
+ k + 1, atoms[a], AA, chain_id, r + 1, x, y, z, 1, error[r], atoms[a][0])
465
+ k += 1
466
+
467
+ if r + 1 == delim[chain_num]:
468
+ pdb_string += "TER %5d %3s %s%4d\n" % (
469
+ k + 1, AA, chain_id, r + 1)
470
+ k += 1
471
+
472
+ pdb_string += "END\n"
473
+
474
+ if write_pdb:
475
+ with open(out_pdb, "w") as f:
476
+ f.write(pdb_string)
477
+
478
+ return pdb_string
479
+
480
+
481
+ def write_pdb_bfactor(
482
+ in_pdb_file,
483
+ out_pdb_file,
484
+ bfactor,
485
+ b_chain=None,
486
+ ):
487
+ parser = PDBParser()
488
+ with warnings.catch_warnings(record=True):
489
+ structure = parser.get_structure(
490
+ "_",
491
+ in_pdb_file,
492
+ )
493
+
494
+ i = 0
495
+ for chain in structure.get_chains():
496
+ if exists(b_chain) and chain._id != b_chain:
497
+ continue
498
+
499
+ for r in chain.get_residues():
500
+ [a.set_bfactor(bfactor[i]) for a in r.get_atoms()]
501
+ i += 1
502
+
503
+ io = PDBIO()
504
+ io.set_structure(structure)
505
+ io.save(out_pdb_file)
506
+
507
+
508
+ def clean_pdb(pdb_file):
509
+ with open(pdb_file, "r") as f:
510
+ lines = f.readlines()
511
+
512
+ with open(pdb_file, "w") as f:
513
+ for l in lines:
514
+ if "ATOM" in l:
515
+ f.write(l)
model/utils/tensor.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+
4
+
5
+ def max_shape(data):
6
+ """Gets the maximum length along all dimensions in a list of Tensors"""
7
+ shapes = torch.Tensor([_.shape for _ in data])
8
+ return torch.max(
9
+ shapes.transpose(0, 1),
10
+ dim=1,
11
+ )[0].int()
12
+
13
+
14
+ def pad_data_to_same_shape(
15
+ tensor_list,
16
+ pad_value=0,
17
+ ):
18
+ target_shape = max_shape(tensor_list)
19
+
20
+ padded_dataset_shape = [len(tensor_list)] + list(target_shape)
21
+ padded_dataset = torch.Tensor(*padded_dataset_shape).type_as(
22
+ tensor_list[0])
23
+
24
+ for i, data in enumerate(tensor_list):
25
+ # Get how much padding is needed per dimension
26
+ padding = reversed(target_shape - torch.Tensor(list(data.shape)).int())
27
+
28
+ # Add 0 every other index to indicate only right padding
29
+ padding = F.pad(
30
+ padding.unsqueeze(0).t(),
31
+ (1, 0, 0, 0),
32
+ ).view(-1, 1)
33
+ padding = padding.view(1, -1)[0].tolist()
34
+
35
+ padded_data = F.pad(
36
+ data,
37
+ padding,
38
+ value=pad_value,
39
+ )
40
+ padded_dataset[i] = padded_data
41
+
42
+ return padded_dataset
model/utils/transforms.py ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from typing import Optional
8
+
9
+ import torch
10
+ import torch.nn.functional as F
11
+ """
12
+ The transformation matrices returned from the functions in this file assume
13
+ the points on which the transformation will be applied are column vectors.
14
+ i.e. the R matrix is structured as
15
+
16
+ R = [
17
+ [Rxx, Rxy, Rxz],
18
+ [Ryx, Ryy, Ryz],
19
+ [Rzx, Rzy, Rzz],
20
+ ] # (3, 3)
21
+
22
+ This matrix can be applied to column vectors by post multiplication
23
+ by the points e.g.
24
+
25
+ points = [[0], [1], [2]] # (3 x 1) xyz coordinates of a point
26
+ transformed_points = R * points
27
+
28
+ To apply the same matrix to points which are row vectors, the R matrix
29
+ can be transposed and pre multiplied by the points:
30
+
31
+ e.g.
32
+ points = [[0, 1, 2]] # (1 x 3) xyz coordinates of a point
33
+ transformed_points = points * R.transpose(1, 0)
34
+ """
35
+
36
+
37
+ def quaternion_to_matrix(quaternions: torch.Tensor) -> torch.Tensor:
38
+ """
39
+ Convert rotations given as quaternions to rotation matrices.
40
+
41
+ Args:
42
+ quaternions: quaternions with real part first,
43
+ as tensor of shape (..., 4).
44
+
45
+ Returns:
46
+ Rotation matrices as tensor of shape (..., 3, 3).
47
+ """
48
+ r, i, j, k = torch.unbind(quaternions, -1)
49
+ two_s = 2.0 / (quaternions * quaternions).sum(-1)
50
+
51
+ o = torch.stack(
52
+ (
53
+ 1 - two_s * (j * j + k * k),
54
+ two_s * (i * j - k * r),
55
+ two_s * (i * k + j * r),
56
+ two_s * (i * j + k * r),
57
+ 1 - two_s * (i * i + k * k),
58
+ two_s * (j * k - i * r),
59
+ two_s * (i * k - j * r),
60
+ two_s * (j * k + i * r),
61
+ 1 - two_s * (i * i + j * j),
62
+ ),
63
+ -1,
64
+ )
65
+ return o.reshape(quaternions.shape[:-1] + (3, 3))
66
+
67
+
68
+ def _copysign(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
69
+ """
70
+ Return a tensor where each element has the absolute value taken from the,
71
+ corresponding element of a, with sign taken from the corresponding
72
+ element of b. This is like the standard copysign floating-point operation,
73
+ but is not careful about negative 0 and NaN.
74
+
75
+ Args:
76
+ a: source tensor.
77
+ b: tensor whose signs will be used, of the same shape as a.
78
+
79
+ Returns:
80
+ Tensor of the same shape as a with the signs of b.
81
+ """
82
+ signs_differ = (a < 0) != (b < 0)
83
+ return torch.where(signs_differ, -a, a)
84
+
85
+
86
+ def _sqrt_positive_part(x: torch.Tensor) -> torch.Tensor:
87
+ """
88
+ Returns torch.sqrt(torch.max(0, x))
89
+ but with a zero subgradient where x is 0.
90
+ """
91
+ ret = torch.zeros_like(x)
92
+ positive_mask = x > 0
93
+ ret[positive_mask] = torch.sqrt(x[positive_mask])
94
+ return ret
95
+
96
+
97
+ def matrix_to_quaternion(matrix: torch.Tensor) -> torch.Tensor:
98
+ """
99
+ Convert rotations given as rotation matrices to quaternions.
100
+
101
+ Args:
102
+ matrix: Rotation matrices as tensor of shape (..., 3, 3).
103
+
104
+ Returns:
105
+ quaternions with real part first, as tensor of shape (..., 4).
106
+ """
107
+ if matrix.size(-1) != 3 or matrix.size(-2) != 3:
108
+ raise ValueError(f"Invalid rotation matrix shape {matrix.shape}.")
109
+
110
+ batch_dim = matrix.shape[:-2]
111
+ m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind(
112
+ matrix.reshape(batch_dim + (9, )), dim=-1)
113
+
114
+ q_abs = _sqrt_positive_part(
115
+ torch.stack(
116
+ [
117
+ 1.0 + m00 + m11 + m22,
118
+ 1.0 + m00 - m11 - m22,
119
+ 1.0 - m00 + m11 - m22,
120
+ 1.0 - m00 - m11 + m22,
121
+ ],
122
+ dim=-1,
123
+ ))
124
+
125
+ # we produce the desired quaternion multiplied by each of r, i, j, k
126
+ quat_by_rijk = torch.stack(
127
+ [
128
+ torch.stack([q_abs[..., 0]**2, m21 - m12, m02 - m20, m10 - m01],
129
+ dim=-1),
130
+ torch.stack([m21 - m12, q_abs[..., 1]**2, m10 + m01, m02 + m20],
131
+ dim=-1),
132
+ torch.stack([m02 - m20, m10 + m01, q_abs[..., 2]**2, m12 + m21],
133
+ dim=-1),
134
+ torch.stack([m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3]**2],
135
+ dim=-1),
136
+ ],
137
+ dim=-2,
138
+ )
139
+
140
+ # We floor here at 0.1 but the exact level is not important; if q_abs is small,
141
+ # the candidate won't be picked.
142
+ flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device)
143
+ quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr))
144
+
145
+ # if not for numerical problems, quat_candidates[i] should be same (up to a sign),
146
+ # forall i; we pick the best-conditioned one (with the largest denominator)
147
+
148
+ return quat_candidates[F.one_hot(q_abs.argmax(
149
+ dim=-1), num_classes=4) > 0.5, : # pyre-ignore[16]
150
+ ].reshape(batch_dim + (4, ))
151
+
152
+
153
+ def _axis_angle_rotation(axis: str, angle: torch.Tensor) -> torch.Tensor:
154
+ """
155
+ Return the rotation matrices for one of the rotations about an axis
156
+ of which Euler angles describe, for each value of the angle given.
157
+
158
+ Args:
159
+ axis: Axis label "X" or "Y or "Z".
160
+ angle: any shape tensor of Euler angles in radians
161
+
162
+ Returns:
163
+ Rotation matrices as tensor of shape (..., 3, 3).
164
+ """
165
+
166
+ cos = torch.cos(angle)
167
+ sin = torch.sin(angle)
168
+ one = torch.ones_like(angle)
169
+ zero = torch.zeros_like(angle)
170
+
171
+ if axis == "X":
172
+ R_flat = (one, zero, zero, zero, cos, -sin, zero, sin, cos)
173
+ elif axis == "Y":
174
+ R_flat = (cos, zero, sin, zero, one, zero, -sin, zero, cos)
175
+ elif axis == "Z":
176
+ R_flat = (cos, -sin, zero, sin, cos, zero, zero, zero, one)
177
+ else:
178
+ raise ValueError("letter must be either X, Y or Z.")
179
+
180
+ return torch.stack(R_flat, -1).reshape(angle.shape + (3, 3))
181
+
182
+
183
+ def euler_angles_to_matrix(euler_angles: torch.Tensor,
184
+ convention: str) -> torch.Tensor:
185
+ """
186
+ Convert rotations given as Euler angles in radians to rotation matrices.
187
+
188
+ Args:
189
+ euler_angles: Euler angles in radians as tensor of shape (..., 3).
190
+ convention: Convention string of three uppercase letters from
191
+ {"X", "Y", and "Z"}.
192
+
193
+ Returns:
194
+ Rotation matrices as tensor of shape (..., 3, 3).
195
+ """
196
+ if euler_angles.dim() == 0 or euler_angles.shape[-1] != 3:
197
+ raise ValueError("Invalid input euler angles.")
198
+ if len(convention) != 3:
199
+ raise ValueError("Convention must have 3 letters.")
200
+ if convention[1] in (convention[0], convention[2]):
201
+ raise ValueError(f"Invalid convention {convention}.")
202
+ for letter in convention:
203
+ if letter not in ("X", "Y", "Z"):
204
+ raise ValueError(f"Invalid letter {letter} in convention string.")
205
+ matrices = [
206
+ _axis_angle_rotation(c, e)
207
+ for c, e in zip(convention, torch.unbind(euler_angles, -1))
208
+ ]
209
+ # return functools.reduce(torch.matmul, matrices)
210
+ return torch.matmul(torch.matmul(matrices[0], matrices[1]), matrices[2])
211
+
212
+
213
+ def _angle_from_tan(axis: str, other_axis: str, data, horizontal: bool,
214
+ tait_bryan: bool) -> torch.Tensor:
215
+ """
216
+ Extract the first or third Euler angle from the two members of
217
+ the matrix which are positive constant times its sine and cosine.
218
+
219
+ Args:
220
+ axis: Axis label "X" or "Y or "Z" for the angle we are finding.
221
+ other_axis: Axis label "X" or "Y or "Z" for the middle axis in the
222
+ convention.
223
+ data: Rotation matrices as tensor of shape (..., 3, 3).
224
+ horizontal: Whether we are looking for the angle for the third axis,
225
+ which means the relevant entries are in the same row of the
226
+ rotation matrix. If not, they are in the same column.
227
+ tait_bryan: Whether the first and third axes in the convention differ.
228
+
229
+ Returns:
230
+ Euler Angles in radians for each matrix in data as a tensor
231
+ of shape (...).
232
+ """
233
+
234
+ i1, i2 = {"X": (2, 1), "Y": (0, 2), "Z": (1, 0)}[axis]
235
+ if horizontal:
236
+ i2, i1 = i1, i2
237
+ even = (axis + other_axis) in ["XY", "YZ", "ZX"]
238
+ if horizontal == even:
239
+ return torch.atan2(data[..., i1], data[..., i2])
240
+ if tait_bryan:
241
+ return torch.atan2(-data[..., i2], data[..., i1])
242
+ return torch.atan2(data[..., i2], -data[..., i1])
243
+
244
+
245
+ def _index_from_letter(letter: str) -> int:
246
+ if letter == "X":
247
+ return 0
248
+ if letter == "Y":
249
+ return 1
250
+ if letter == "Z":
251
+ return 2
252
+ raise ValueError("letter must be either X, Y or Z.")
253
+
254
+
255
+ def matrix_to_euler_angles(matrix: torch.Tensor,
256
+ convention: str) -> torch.Tensor:
257
+ """
258
+ Convert rotations given as rotation matrices to Euler angles in radians.
259
+
260
+ Args:
261
+ matrix: Rotation matrices as tensor of shape (..., 3, 3).
262
+ convention: Convention string of three uppercase letters.
263
+
264
+ Returns:
265
+ Euler angles in radians as tensor of shape (..., 3).
266
+ """
267
+ if len(convention) != 3:
268
+ raise ValueError("Convention must have 3 letters.")
269
+ if convention[1] in (convention[0], convention[2]):
270
+ raise ValueError(f"Invalid convention {convention}.")
271
+ for letter in convention:
272
+ if letter not in ("X", "Y", "Z"):
273
+ raise ValueError(f"Invalid letter {letter} in convention string.")
274
+ if matrix.size(-1) != 3 or matrix.size(-2) != 3:
275
+ raise ValueError(f"Invalid rotation matrix shape {matrix.shape}.")
276
+ i0 = _index_from_letter(convention[0])
277
+ i2 = _index_from_letter(convention[2])
278
+ tait_bryan = i0 != i2
279
+ if tait_bryan:
280
+ central_angle = torch.asin(matrix[..., i0, i2] *
281
+ (-1.0 if i0 - i2 in [-1, 2] else 1.0))
282
+ else:
283
+ central_angle = torch.acos(matrix[..., i0, i0])
284
+
285
+ o = (
286
+ _angle_from_tan(convention[0], convention[1], matrix[..., i2], False,
287
+ tait_bryan),
288
+ central_angle,
289
+ _angle_from_tan(convention[2], convention[1], matrix[..., i0, :], True,
290
+ tait_bryan),
291
+ )
292
+ return torch.stack(o, -1)
293
+
294
+
295
+ def standardize_quaternion(quaternions: torch.Tensor) -> torch.Tensor:
296
+ """
297
+ Convert a unit quaternion to a standard form: one in which the real
298
+ part is non negative.
299
+
300
+ Args:
301
+ quaternions: Quaternions with real part first,
302
+ as tensor of shape (..., 4).
303
+
304
+ Returns:
305
+ Standardized quaternions as tensor of shape (..., 4).
306
+ """
307
+ return torch.where(quaternions[..., 0:1] < 0, -quaternions, quaternions)
308
+
309
+
310
+ def quaternion_raw_multiply(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
311
+ """
312
+ Multiply two quaternions.
313
+ Usual torch rules for broadcasting apply.
314
+
315
+ Args:
316
+ a: Quaternions as tensor of shape (..., 4), real part first.
317
+ b: Quaternions as tensor of shape (..., 4), real part first.
318
+
319
+ Returns:
320
+ The product of a and b, a tensor of quaternions shape (..., 4).
321
+ """
322
+ aw, ax, ay, az = torch.unbind(a, -1)
323
+ bw, bx, by, bz = torch.unbind(b, -1)
324
+ ow = aw * bw - ax * bx - ay * by - az * bz
325
+ ox = aw * bx + ax * bw + ay * bz - az * by
326
+ oy = aw * by - ax * bz + ay * bw + az * bx
327
+ oz = aw * bz + ax * by - ay * bx + az * bw
328
+ return torch.stack((ow, ox, oy, oz), -1)
329
+
330
+
331
+ def quaternion_multiply(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
332
+ """
333
+ Multiply two quaternions representing rotations, returning the quaternion
334
+ representing their composition, i.e. the versor with nonnegative real part.
335
+ Usual torch rules for broadcasting apply.
336
+
337
+ Args:
338
+ a: Quaternions as tensor of shape (..., 4), real part first.
339
+ b: Quaternions as tensor of shape (..., 4), real part first.
340
+
341
+ Returns:
342
+ The product of a and b, a tensor of quaternions of shape (..., 4).
343
+ """
344
+ ab = quaternion_raw_multiply(a, b)
345
+ return standardize_quaternion(ab)
346
+
347
+
348
+ def quaternion_invert(quaternion: torch.Tensor) -> torch.Tensor:
349
+ """
350
+ Given a quaternion representing rotation, get the quaternion representing
351
+ its inverse.
352
+
353
+ Args:
354
+ quaternion: Quaternions as tensor of shape (..., 4), with real part
355
+ first, which must be versors (unit quaternions).
356
+
357
+ Returns:
358
+ The inverse, a tensor of quaternions of shape (..., 4).
359
+ """
360
+
361
+ scaling = torch.tensor([1, -1, -1, -1], device=quaternion.device)
362
+ return quaternion * scaling
363
+
364
+
365
+ def quaternion_apply(quaternion: torch.Tensor,
366
+ point: torch.Tensor) -> torch.Tensor:
367
+ """
368
+ Apply the rotation given by a quaternion to a 3D point.
369
+ Usual torch rules for broadcasting apply.
370
+
371
+ Args:
372
+ quaternion: Tensor of quaternions, real part first, of shape (..., 4).
373
+ point: Tensor of 3D points of shape (..., 3).
374
+
375
+ Returns:
376
+ Tensor of rotated points of shape (..., 3).
377
+ """
378
+ if point.size(-1) != 3:
379
+ raise ValueError(f"Points are not in 3D, {point.shape}.")
380
+ real_parts = point.new_zeros(point.shape[:-1] + (1, ))
381
+ point_as_quaternion = torch.cat((real_parts, point), -1)
382
+ out = quaternion_raw_multiply(
383
+ quaternion_raw_multiply(quaternion, point_as_quaternion),
384
+ quaternion_invert(quaternion),
385
+ )
386
+ return out[..., 1:]
387
+
388
+
389
+ def axis_angle_to_matrix(axis_angle: torch.Tensor) -> torch.Tensor:
390
+ """
391
+ Convert rotations given as axis/angle to rotation matrices.
392
+
393
+ Args:
394
+ axis_angle: Rotations given as a vector in axis angle form,
395
+ as a tensor of shape (..., 3), where the magnitude is
396
+ the angle turned anticlockwise in radians around the
397
+ vector's direction.
398
+
399
+ Returns:
400
+ Rotation matrices as tensor of shape (..., 3, 3).
401
+ """
402
+ return quaternion_to_matrix(axis_angle_to_quaternion(axis_angle))
403
+
404
+
405
+ def matrix_to_axis_angle(matrix: torch.Tensor) -> torch.Tensor:
406
+ """
407
+ Convert rotations given as rotation matrices to axis/angle.
408
+
409
+ Args:
410
+ matrix: Rotation matrices as tensor of shape (..., 3, 3).
411
+
412
+ Returns:
413
+ Rotations given as a vector in axis angle form, as a tensor
414
+ of shape (..., 3), where the magnitude is the angle
415
+ turned anticlockwise in radians around the vector's
416
+ direction.
417
+ """
418
+ return quaternion_to_axis_angle(matrix_to_quaternion(matrix))
419
+
420
+
421
+ def axis_angle_to_quaternion(axis_angle: torch.Tensor) -> torch.Tensor:
422
+ """
423
+ Convert rotations given as axis/angle to quaternions.
424
+
425
+ Args:
426
+ axis_angle: Rotations given as a vector in axis angle form,
427
+ as a tensor of shape (..., 3), where the magnitude is
428
+ the angle turned anticlockwise in radians around the
429
+ vector's direction.
430
+
431
+ Returns:
432
+ quaternions with real part first, as tensor of shape (..., 4).
433
+ """
434
+ angles = torch.norm(axis_angle, p=2, dim=-1, keepdim=True)
435
+ half_angles = angles * 0.5
436
+ eps = 1e-6
437
+ small_angles = angles.abs() < eps
438
+ sin_half_angles_over_angles = torch.empty_like(angles)
439
+ sin_half_angles_over_angles[~small_angles] = (
440
+ torch.sin(half_angles[~small_angles]) / angles[~small_angles])
441
+ # for x small, sin(x/2) is about x/2 - (x/2)^3/6
442
+ # so sin(x/2)/x is about 1/2 - (x*x)/48
443
+ sin_half_angles_over_angles[small_angles] = (
444
+ 0.5 - (angles[small_angles] * angles[small_angles]) / 48)
445
+ quaternions = torch.cat(
446
+ [torch.cos(half_angles), axis_angle * sin_half_angles_over_angles],
447
+ dim=-1)
448
+ return quaternions
449
+
450
+
451
+ def quaternion_to_axis_angle(quaternions: torch.Tensor) -> torch.Tensor:
452
+ """
453
+ Convert rotations given as quaternions to axis/angle.
454
+
455
+ Args:
456
+ quaternions: quaternions with real part first,
457
+ as tensor of shape (..., 4).
458
+
459
+ Returns:
460
+ Rotations given as a vector in axis angle form, as a tensor
461
+ of shape (..., 3), where the magnitude is the angle
462
+ turned anticlockwise in radians around the vector's
463
+ direction.
464
+ """
465
+ norms = torch.norm(quaternions[..., 1:], p=2, dim=-1, keepdim=True)
466
+ half_angles = torch.atan2(norms, quaternions[..., :1])
467
+ angles = 2 * half_angles
468
+ eps = 1e-6
469
+ small_angles = angles.abs() < eps
470
+ sin_half_angles_over_angles = torch.empty_like(angles)
471
+ sin_half_angles_over_angles[~small_angles] = (
472
+ torch.sin(half_angles[~small_angles]) / angles[~small_angles])
473
+ # for x small, sin(x/2) is about x/2 - (x/2)^3/6
474
+ # so sin(x/2)/x is about 1/2 - (x*x)/48
475
+ sin_half_angles_over_angles[small_angles] = (
476
+ 0.5 - (angles[small_angles] * angles[small_angles]) / 48)
477
+ return quaternions[..., 1:] / sin_half_angles_over_angles
478
+
479
+
480
+ def rotation_6d_to_matrix(d6: torch.Tensor) -> torch.Tensor:
481
+ """
482
+ Converts 6D rotation representation by Zhou et al. [1] to rotation matrix
483
+ using Gram--Schmidt orthogonalization per Section B of [1].
484
+ Args:
485
+ d6: 6D rotation representation, of size (*, 6)
486
+
487
+ Returns:
488
+ batch of rotation matrices of size (*, 3, 3)
489
+
490
+ [1] Zhou, Y., Barnes, C., Lu, J., Yang, J., & Li, H.
491
+ On the Continuity of Rotation Representations in Neural Networks.
492
+ IEEE Conference on Computer Vision and Pattern Recognition, 2019.
493
+ Retrieved from http://arxiv.org/abs/1812.07035
494
+ """
495
+
496
+ a1, a2 = d6[..., :3], d6[..., 3:]
497
+ b1 = F.normalize(a1, dim=-1)
498
+ b2 = a2 - (b1 * a2).sum(-1, keepdim=True) * b1
499
+ b2 = F.normalize(b2, dim=-1)
500
+ b3 = torch.cross(b1, b2, dim=-1)
501
+ return torch.stack((b1, b2, b3), dim=-2)
502
+
503
+
504
+ def matrix_to_rotation_6d(matrix: torch.Tensor) -> torch.Tensor:
505
+ """
506
+ Converts rotation matrices to 6D rotation representation by Zhou et al. [1]
507
+ by dropping the last row. Note that 6D representation is not unique.
508
+ Args:
509
+ matrix: batch of rotation matrices of size (*, 3, 3)
510
+
511
+ Returns:
512
+ 6D rotation representation, of size (*, 6)
513
+
514
+ [1] Zhou, Y., Barnes, C., Lu, J., Yang, J., & Li, H.
515
+ On the Continuity of Rotation Representations in Neural Networks.
516
+ IEEE Conference on Computer Vision and Pattern Recognition, 2019.
517
+ Retrieved from http://arxiv.org/abs/1812.07035
518
+ """
519
+ batch_dim = matrix.size()[:-2]
520
+ return matrix[..., :2, :].clone().reshape(batch_dim + (6, ))
model/utils/visualize.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+ import numpy as np
4
+ import py3Dmol
5
+ import matplotlib.pyplot as plt
6
+ import seaborn as sns
7
+
8
+ from igfold.utils.folding import get_sequence_dict
9
+ from igfold.utils.general import exists
10
+ from igfold.utils.pdb import get_cdr_range_dict
11
+
12
+
13
+ def show_pdb(
14
+ pdb_filename: str,
15
+ num_sequences,
16
+ bb_sticks=False,
17
+ sc_sticks=False,
18
+ color="b",
19
+ view_size=(500, 500),
20
+ ):
21
+ return show_pdbs(
22
+ [pdb_filename],
23
+ num_sequences,
24
+ bb_sticks=bb_sticks,
25
+ sc_sticks=sc_sticks,
26
+ color=color,
27
+ view_size=view_size,
28
+ )
29
+
30
+
31
+ ###
32
+ # Inspired by ColabFold visualization from https://github.com/sokrypton/ColabFold
33
+ ###
34
+
35
+
36
+ def show_pdbs(
37
+ pdb_filenames,
38
+ num_sequences,
39
+ bb_sticks=False,
40
+ sc_sticks=False,
41
+ color="b",
42
+ view_size=(800, 800),
43
+ ):
44
+ grid_width = math.ceil(math.sqrt(len(pdb_filenames)))
45
+ grid_height = math.ceil(len(pdb_filenames) / grid_width)
46
+ view = py3Dmol.view(
47
+ js="https://3dmol.org/build/3Dmol.js",
48
+ viewergrid=(grid_height, grid_width),
49
+ width=view_size[0],
50
+ height=view_size[1],
51
+ )
52
+
53
+ for pdb_i, pdb_filename in enumerate(pdb_filenames):
54
+ grid_row, grid_col = pdb_i // grid_width, pdb_i % grid_width
55
+ view.addModel(
56
+ open(pdb_filename, "r").read(),
57
+ "pdb",
58
+ viewer=(grid_row, grid_col),
59
+ )
60
+
61
+ if color == "b":
62
+ view.setStyle({
63
+ "cartoon": {
64
+ "colorscheme": {
65
+ "prop": "b",
66
+ "gradient": "roygb",
67
+ "min": 1.5,
68
+ "max": 0.5,
69
+ }
70
+ }
71
+ })
72
+ elif color == "rainbow":
73
+ view.setStyle({"cartoon": {"color": "spectrum"}})
74
+ elif color == "chain":
75
+ for n, chain, color_ in zip(
76
+ range(num_sequences),
77
+ list("ABCDEFGH"),
78
+ [
79
+ "lime", "cyan", "magenta", "yellow", "salmon", "white", "blue",
80
+ "orange"
81
+ ],
82
+ ):
83
+ view.setStyle({"chain": chain}, {"cartoon": {"color": color_}})
84
+ if sc_sticks:
85
+ BB = ["C", "O", "N"]
86
+ view.addStyle(
87
+ {
88
+ "and": [
89
+ {
90
+ "resn": ["GLY", "PRO"],
91
+ "invert": True
92
+ },
93
+ {
94
+ "atom": BB,
95
+ "invert": True
96
+ },
97
+ ]
98
+ },
99
+ {"stick": {
100
+ "colorscheme": f"WhiteCarbon",
101
+ "radius": 0.2
102
+ }},
103
+ )
104
+ view.addStyle(
105
+ {"and": [{
106
+ "resn": "GLY"
107
+ }, {
108
+ "atom": "CA"
109
+ }]},
110
+ {"sphere": {
111
+ "colorscheme": f"WhiteCarbon",
112
+ "radius": 0.3
113
+ }},
114
+ )
115
+ view.addStyle(
116
+ {"and": [{
117
+ "resn": "PRO"
118
+ }, {
119
+ "atom": ["C", "O"],
120
+ "invert": True
121
+ }]},
122
+ {"stick": {
123
+ "colorscheme": f"WhiteCarbon",
124
+ "radius": 0.3
125
+ }},
126
+ )
127
+ if bb_sticks:
128
+ BB = ["C", "O", "N", "CA"]
129
+ view.addStyle(
130
+ {"atom": BB},
131
+ {"stick": {
132
+ "colorscheme": f"WhiteCarbon",
133
+ "radius": 0.3
134
+ }},
135
+ )
136
+
137
+ view.zoomTo()
138
+ return view
139
+
140
+
141
+ def plot_prmsd(
142
+ sequences,
143
+ prmsd,
144
+ out_file=None,
145
+ shade_cdr=False,
146
+ pdb_file=None,
147
+ ):
148
+ seq_dict = get_sequence_dict(sequences, None)
149
+ delims = np.cumsum([len(s) for s in seq_dict.values()]).tolist()
150
+
151
+ res_rmsd = prmsd.cpu().square().mean(dim=-1).sqrt().squeeze(0)
152
+ chain_res_rmsd = np.split(res_rmsd, delims)
153
+
154
+ if shade_cdr and exists(pdb_file):
155
+ heavy_only = len(sequences) == 1 and "H" in sequences
156
+ light_only = len(sequences) == 1 and "L" in sequences
157
+ cdr_range_dict = get_cdr_range_dict(
158
+ pdb_file,
159
+ heavy_only=heavy_only,
160
+ light_only=light_only,
161
+ offset_heavy=False,
162
+ )
163
+ cdr_ranges = np.split(np.array(list(cdr_range_dict.values())), [3])
164
+
165
+ plt.figure(figsize=(8, 4))
166
+ for i, (chain, rmsd) in enumerate(zip(seq_dict.keys(), chain_res_rmsd)):
167
+ plt.subplot(1, len(seq_dict), i + 1)
168
+
169
+ res_nums = torch.arange(1, len(rmsd) + 1)
170
+ sns.lineplot(x=res_nums, y=rmsd)
171
+
172
+ if shade_cdr and exists(pdb_file):
173
+ for r in cdr_ranges[i]:
174
+ plt.axvspan(r[0], r[1], color="gray", alpha=0.5)
175
+
176
+ plt.xlabel("Residue Number")
177
+ plt.ylabel("Predicted RMSD (A)")
178
+ plt.title(f"Chain {chain}")
179
+
180
+ plt.tight_layout()
181
+
182
+ if exists(out_file):
183
+ plt.savefig(out_file, dpi=400)
pyproject.toml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies already provided by the OneScience bio-dcu environment are kept
2
+ # below as comments for traceability. Only uncommented packages need installing.
3
+
4
+ antiberty>=0.1.1
5
+ # biopython>=1.81 # provided by OneScience (1.84)
6
+ # einops>=0.3.0 # provided by OneScience (>=0.7.0)
7
+ future>=0.18.2
8
+ # matplotlib>=3.7 # provided by OneScience
9
+ # numpy==1.21.2 # provided by OneScience (1.26.3)
10
+ # pytorch-lightning>=1.5.10,<1.9.0 # OneScience provides 2.0.6; keep its version
11
+ # requests>=2.26.0 # provided by OneScience
12
+ # seaborn>=0.12 # provided by OneScience
13
+ # torch==1.7.1 # provided by the OneScience bio-dcu base environment
14
+ # tqdm>=4.62.1 # provided by OneScience (>=4.60.0)
15
+ yapf>=0.31.0
16
+ # py3Dmol>=1.8.0 # provided by OneScience (2.5.2)
scripts/IgFold.ipynb ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {
6
+ "id": "5AEsv1z5KXXA"
7
+ },
8
+ "source": [
9
+ "# **IgFold**: Fast, accurate antibody structure prediction\n",
10
+ "\n",
11
+ "Official notebook for [IgFold](https://www.biorxiv.org/content/10.1101/2022.04.20.488972): Fast, accurate antibody structure prediction from deep learning on massive set of natural antibodies. The code, data, and weights for this work are made available for non-commercial use. For commercial inquiries, please contact `jruffolo[at]jhu.edu`."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "metadata": {
18
+ "cellView": "form",
19
+ "id": "0PsLNGK57LDq"
20
+ },
21
+ "outputs": [],
22
+ "source": [
23
+ "#@title Input antibody Fv sequences then press `Runtime` -> `Run all`\n",
24
+ "\n",
25
+ "import os\n",
26
+ "import sys\n",
27
+ "\n",
28
+ "python_version = f\"{sys.version_info.major}.{sys.version_info.minor}\"\n",
29
+ "\n",
30
+ "name = \"my_antibody\" #@param {type:\"string\"}\n",
31
+ "pred_dir = name\n",
32
+ "os.makedirs(pred_dir, exist_ok=True)\n",
33
+ "\n",
34
+ "#@markdown Enter antibody sequences for structure prediction. To predict a nanobody structure (or an individual heavy or light chain), simply provide one sequence.\n",
35
+ "heavy_sequence = \"EVQLVQSGPEVKKPGTSVKVSCKASGFTFMSSAVQWVRQARGQRLEWIGWIVIGSGNTNYAQKFQERVTITRDMSTSTAYMELSSLRSEDTAVYYCAAPYCSSISCNDGFDIWGQGTMVTVS\" #@param {type:\"string\"}\n",
36
+ "light_sequence = \"DVVMTQTPFSLPVSLGDQASISCRSSQSLVHSNGNTYLHWYLQKPGQSPKLLIYKVSNRFSGVPDRFSGSGSGTDFTLKISRVEAEDLGVYFCSQSTHVPYTFGGGTKLEIK\" #@param {type:\"string\"}\n",
37
+ "\n",
38
+ "sequences = {}\n",
39
+ "if len(heavy_sequence) > 0:\n",
40
+ " sequences[\"H\"] = heavy_sequence\n",
41
+ "if len(light_sequence) > 0:\n",
42
+ " sequences[\"L\"] = light_sequence\n",
43
+ "\n",
44
+ "#@markdown Perform structural refinement with OpenMM\n",
45
+ "do_refine = True #@param {type:\"boolean\"}\n",
46
+ "#@markdown Renumber predicted antibody structure (Chothia) with AbNumber\n",
47
+ "do_renum = False #@param {type:\"boolean\"}\n",
48
+ "#@markdown Use only a single model for predictions (instead of model ensemble)\n",
49
+ "single_model = False #@param {type:\"boolean\"}"
50
+ ]
51
+ },
52
+ {
53
+ "cell_type": "code",
54
+ "execution_count": null,
55
+ "metadata": {
56
+ "cellView": "form",
57
+ "id": "LsJNdVE87Go2"
58
+ },
59
+ "outputs": [],
60
+ "source": [
61
+ "#@title Install dependencies\n",
62
+ "\n",
63
+ "PYTHON_VERSION = python_version\n",
64
+ "\n",
65
+ "if not os.path.isfile(\"CONDA_READY\"):\n",
66
+ " print(\"installing conda...\")\n",
67
+ " os.system(\"wget -qnc https://github.com/jaimergp/miniforge/releases/latest/download/Mambaforge-colab-Linux-x86_64.sh\")\n",
68
+ " os.system(\"bash Mambaforge-colab-Linux-x86_64.sh -bfp /usr/local\")\n",
69
+ " os.system(\"mamba config --set auto_update_conda false\")\n",
70
+ " os.system(\"touch CONDA_READY\")\n",
71
+ "\n",
72
+ "if not os.path.isfile(\"CODE_READY\"):\n",
73
+ " print(\"installing igfold...\")\n",
74
+ " torch_string = \"torch==1.11.0+cu113 torchvision==0.12.0+cu113 -f https://download.pytorch.org/whl/torch_stable.html\"\n",
75
+ " os.system(f\"pip3 install {torch_string}\")\n",
76
+ " os.system(f\"pip install 'igfold>=0.3.0' {torch_string}\")\n",
77
+ " os.system(\"pip install -q --no-warn-conflicts 'py3Dmol>=2.0.1' matplotlib seaborn\")\n",
78
+ " os.system(\"touch CODE_READY\")\n",
79
+ "\n",
80
+ "if do_refine and not os.path.isfile(\"AMBER_READY\"):\n",
81
+ " print(\"installing amber...\")\n",
82
+ " os.system(f\"mamba install -y -q -c conda-forge openmm=7.7.0 python='{PYTHON_VERSION}' pdbfixer 2>&1 1>/dev/null\")\n",
83
+ " os.system(\"touch AMBER_READY\")\n",
84
+ "\n",
85
+ "if do_renum and not os.path.isfile(\"ABNUMBER_READY\"):\n",
86
+ " print(\"installing abnumber...\")\n",
87
+ " os.system(f\"mamba install -y -q -c bioconda abnumber python='{PYTHON_VERSION}' 2>&1 1>/dev/null\")\n",
88
+ " os.system(\"pip install pandas --force-reinstall\")\n",
89
+ " os.system(\"touch ABNUMBER_READY\")"
90
+ ]
91
+ },
92
+ {
93
+ "cell_type": "code",
94
+ "execution_count": null,
95
+ "metadata": {
96
+ "cellView": "form",
97
+ "id": "a2a3BsiE9AXI"
98
+ },
99
+ "outputs": [],
100
+ "source": [
101
+ "#@title Predict antibody structure with IgFold\n",
102
+ "\n",
103
+ "if f\"/usr/local/lib/python{python_version}/site-packages/\" not in sys.path:\n",
104
+ " sys.path.insert(0, f\"/usr/local/lib/python{python_version}/site-packages/\")\n",
105
+ "\n",
106
+ "from igfold.utils.visualize import *\n",
107
+ "from igfold import IgFoldRunner\n",
108
+ "\n",
109
+ "num_models = 1 if single_model else 4\n",
110
+ "igfold = IgFoldRunner(num_models=num_models)\n",
111
+ "\n",
112
+ "pred_pdb = os.path.join(pred_dir, f\"{name}.pdb\")\n",
113
+ "pred = igfold.fold(\n",
114
+ " pred_pdb,\n",
115
+ " sequences=sequences,\n",
116
+ " do_refine=do_refine,\n",
117
+ " use_openmm=True,\n",
118
+ " do_renum=do_renum,\n",
119
+ ")\n",
120
+ "show_pdb(pred_pdb, len(sequences), bb_sticks=False, sc_sticks=True, color=\"rainbow\")"
121
+ ]
122
+ },
123
+ {
124
+ "cell_type": "code",
125
+ "execution_count": null,
126
+ "metadata": {
127
+ "cellView": "form",
128
+ "id": "xFOTYxsP9Cz1"
129
+ },
130
+ "outputs": [],
131
+ "source": [
132
+ "#@title Plot per-residue predicted RMSD\n",
133
+ "\n",
134
+ "prmsd_fig_file = os.path.join(pred_dir, f\"{name}_prmsd.png\")\n",
135
+ "plot_prmsd(sequences, pred.prmsd.cpu(), prmsd_fig_file, shade_cdr=do_renum, pdb_file=pred_pdb)"
136
+ ]
137
+ },
138
+ {
139
+ "cell_type": "code",
140
+ "execution_count": null,
141
+ "metadata": {
142
+ "cellView": "form",
143
+ "id": "ajyElWbZ9EFF"
144
+ },
145
+ "outputs": [],
146
+ "source": [
147
+ "#@title Show predicted structure with predicted RMSD\n",
148
+ "\n",
149
+ "#@markdown Structure is colored from low (blue) to high (red) pRMSD.\n",
150
+ "\n",
151
+ "show_pdb(pred_pdb, len(sequences), bb_sticks=False, sc_sticks=True, color=\"b\")"
152
+ ]
153
+ },
154
+ {
155
+ "cell_type": "code",
156
+ "execution_count": null,
157
+ "metadata": {
158
+ "cellView": "form",
159
+ "id": "gZBzjpMdJ77q"
160
+ },
161
+ "outputs": [],
162
+ "source": [
163
+ "#@title Download results\n",
164
+ "\n",
165
+ "#@markdown Download zip file containing structure prediction and annotation results. If download fails, results are also accessible from file explorer on the left panel of the notebook.\n",
166
+ "\n",
167
+ "from google.colab import files\n",
168
+ "import locale\n",
169
+ "locale.getpreferredencoding = lambda: \"UTF-8\"\n",
170
+ "\n",
171
+ "!zip -FSr $name\".result.zip\" $pred_dir/ &> /dev/null\n",
172
+ "files.download(f\"{name}.result.zip\")"
173
+ ]
174
+ }
175
+ ],
176
+ "metadata": {
177
+ "colab": {
178
+ "collapsed_sections": [],
179
+ "name": "IgFold.ipynb",
180
+ "provenance": []
181
+ },
182
+ "kernelspec": {
183
+ "display_name": "Python 3.9.12 ('igfold_public')",
184
+ "language": "python",
185
+ "name": "python3"
186
+ },
187
+ "language_info": {
188
+ "name": "python",
189
+ "version": "3.9.12"
190
+ },
191
+ "vscode": {
192
+ "interpreter": {
193
+ "hash": "84181e5f1827f203c248bfcd3a60e7e3a4ffc08f0a7dd8a443bd855d4ab14b5d"
194
+ }
195
+ }
196
+ },
197
+ "nbformat": 4,
198
+ "nbformat_minor": 0
199
+ }
scripts/inference.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run IgFold structure prediction from antibody sequences or a FASTA file."""
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import torch
8
+
9
+ from igfold import IgFoldRunner
10
+
11
+
12
+ DEFAULT_HEAVY = (
13
+ "EVQLVQSGPEVKKPGTSVKVSCKASGFTFMSSAVQWVRQARGQRLEWIGWIVIGSGNTNYAQKF"
14
+ "QERVTITRDMSTSTAYMELSSLRSEDTAVYYCAAPYCSSISCNDGFDIWGQGTMVTVS"
15
+ )
16
+ DEFAULT_LIGHT = (
17
+ "DVVMTQTPFSLPVSLGDQASISCRSSQSLVHSNGNTYLHWYLQKPGQSPKLLIYKVSNRFSGV"
18
+ "PDRFSGSGSGTDFTLKISRVEAEDLGVYFCSQSTHVPYTFGGGTKLEIK"
19
+ )
20
+
21
+
22
+ def parse_args():
23
+ parser = argparse.ArgumentParser(description=__doc__)
24
+ parser.add_argument("--fasta", type=Path, help="Input FASTA containing H/L chains.")
25
+ parser.add_argument("--heavy", help="Heavy-chain amino-acid sequence.")
26
+ parser.add_argument("--light", help="Light-chain amino-acid sequence.")
27
+ parser.add_argument("--output", type=Path, default=Path("output/inference/antibody.pdb"))
28
+ parser.add_argument("--num-models", type=int, choices=range(1, 5), default=4)
29
+ parser.add_argument("--refine", action="store_true", help="Enable structure refinement.")
30
+ parser.add_argument("--openmm", action="store_true", help="Use OpenMM for refinement.")
31
+ parser.add_argument("--renum", action="store_true", help="Apply Chothia numbering.")
32
+ parser.add_argument("--cpu", action="store_true", help="Force CPU inference.")
33
+ return parser.parse_args()
34
+
35
+
36
+ def main():
37
+ args = parse_args()
38
+ root = Path(__file__).resolve().parents[1]
39
+ checkpoints = sorted((root / "weight" / "IgFold").glob("*.ckpt"))[: args.num_models]
40
+ if len(checkpoints) != args.num_models:
41
+ raise FileNotFoundError(
42
+ f"Expected {args.num_models} checkpoints in {root / 'weight' / 'IgFold'}, "
43
+ f"found {len(checkpoints)}."
44
+ )
45
+
46
+ if args.fasta is not None:
47
+ fasta_file = str(args.fasta.resolve())
48
+ sequences = None
49
+ else:
50
+ fasta_file = None
51
+ sequences = {"H": args.heavy or DEFAULT_HEAVY}
52
+ light = args.light if args.light is not None else DEFAULT_LIGHT
53
+ if light:
54
+ sequences["L"] = light
55
+
56
+ if args.refine and not args.openmm:
57
+ from igfold.refine.pyrosetta_ref import init_pyrosetta
58
+
59
+ init_pyrosetta()
60
+
61
+ args.output.parent.mkdir(parents=True, exist_ok=True)
62
+ runner = IgFoldRunner(
63
+ model_ckpts=[str(path) for path in checkpoints],
64
+ try_gpu=not args.cpu,
65
+ )
66
+ output = runner.fold(
67
+ str(args.output),
68
+ fasta_file=fasta_file,
69
+ sequences=sequences,
70
+ do_refine=args.refine,
71
+ use_openmm=args.openmm,
72
+ do_renum=args.renum,
73
+ )
74
+
75
+ result = {
76
+ "status": "PASS",
77
+ "output_pdb": str(args.output.resolve()),
78
+ "models": len(runner.models),
79
+ "device": str(next(runner.models[0].parameters()).device),
80
+ "torch": torch.__version__,
81
+ "coords_shape": list(output.coords.shape),
82
+ "prmsd_shape": list(output.prmsd.shape),
83
+ }
84
+ print("IGFOLD_INFERENCE_RESULT=" + json.dumps(result, sort_keys=True))
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()
setup.cfg ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [metadata]
2
+ name = igfold
3
+ version = 0.4.0
4
+ long-description = file: README.md
5
+ long_description_content_type = text/markdown
6
+
7
+ [options]
8
+ packages = find:
9
+ package_dir =
10
+ = model
11
+ install_requires =
12
+ antiberty>=0.1.1
13
+ biopython>=1.79
14
+ einops>=0.3.0
15
+ pytorch-lightning>=1.5.10,<1.9.0
16
+ torch>=1.7.1
17
+ include_package_data = True
18
+
19
+ [options.packages.find]
20
+ where = model
weight/IgFold/LICENSE.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # JHU Academic Software License Agreement
2
+
3
+ This license agreement ("License") is effective automatically between you ("Licensee") and The Johns Hopkins University (“JHU”) for use of the software with which this License is distributed (“Software”) so long as Licensee complies with the following terms and conditions:
4
+
5
+ The requirement to acknowledge the copyright of JHU as follows: “© 2022 The Johns Hopkins University” and copyrights of any incorporated third party software as described in the associated documentation.
6
+
7
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the source code must retain the above copyright notice, and these terms and conditions.
8
+
9
+ Neither the name of JHU nor its affiliates may be used to endorse or promote products derived from this Software without specific prior written permission from an authorized JHU representative.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY JHU "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JHU BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LICENSEE AGREES TO DEFEND, INDEMNIFY AND HOLD HARMLESS JHU FOR ANY CLAIMS ARISING FROM LICENSEE’S USE OF THE SOFTWARE TO THE FULLEST EXTENT PERMITTED BY LAW.
12
+
13
+ Only a non-exclusive, nontransferable license is granted to Licensee to use the Software for non-commercial purposes. Commercial use of the Software requires a separately executed written license agreement.
14
+
15
+ Licensee agrees that it will use the Software, and any modifications, improvements, or derivatives to the Software that the Licensee may create (collectively, "Improvements") solely for non-commercial purposes and shall not distribute or transfer the Software or Improvements to any third party without requiring that such third parties adhere to the terms of this License. Licensee agrees that any Improvements made by Licensee shall be subject to the same terms and conditions as the Software.
16
+
17
+ Licensee acknowledges that JHU holds copyright in the Software or portions of the Software, and that the Software may incorporate third party software which may be subject to additional terms and conditions. Licensee shall comply with any additional terms and conditions appliable to such third party software.
18
+
19
+ Licensee agrees that any publication of results obtained with the Software will acknowledge its use by an appropriate citation.
20
+
21
+ Licensee’s rights under this License terminate automatically without notice from JHU if Licensee fail to comply with any term(s) of this License.
22
+
23
+ This License shall be governed by the laws of the State of Maryland, excluding the application of its conflicts of law rules. Licensee agrees that any dispute shall be appropriate only in the state and federal courts located within the State of Maryland.
24
+
weight/IgFold/igfold_1.ckpt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:66c2aa5a4fd4ef485e99f661e6c605037fa6bfefc02464395836c23263948249
3
+ size 6358637
weight/IgFold/igfold_2.ckpt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3917eb78a3b8f9a84d53e3dae1132e0096b6ded0868a081a60d0603d502028bc
3
+ size 6358637
weight/IgFold/igfold_3.ckpt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d9b60988f1400c12f715b440c7be0bebab3b3be73dc39657c35c2029c3174c52
3
+ size 6358637
weight/IgFold/igfold_5.ckpt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b75865aef284973d47af61598a2fd50cbcfb02c67015b636f7ecd23ed842b2b5
3
+ size 6358637
weight/wheels/antiberty-0.1.3-py3-none-any.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:30d910992b190013871bac49cdc032e01a19339f7d2b958ab99b0eb44638352a
3
+ size 96631471
weight/wheels/igfold-0.4.0-py3-none-any.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ddf73636cf86ca2ef91eab25f79bf8052a3deaeae0ee4e056c8eecd152582c9c
3
+ size 23355225