Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -0
- .gitignore +136 -0
- .ms_upload_cache +1 -0
- LICENSE.md +24 -0
- MANIFEST.in +2 -0
- README.md +459 -0
- conf/config.json +8 -0
- configuration.json +13 -0
- model/IgFoldRunner.py +158 -0
- model/__init__.py +3 -0
- model/model/IgFold.py +488 -0
- model/model/__init__.py +0 -0
- model/model/components/GraphTransformer.py +255 -0
- model/model/components/IPABlock.py +248 -0
- model/model/components/IPATransformer.py +174 -0
- model/model/components/TriangleGraphTransformer.py +62 -0
- model/model/components/TriangleMultiplicativeModule.py +109 -0
- model/model/components/__init__.py +4 -0
- model/model/interface.py +40 -0
- model/refine/__init__.py +0 -0
- model/refine/openmm_ref.py +44 -0
- model/refine/pyrosetta_ref.py +125 -0
- model/training/__init__.py +0 -0
- model/training/utils.py +287 -0
- model/utils/__init__.py +0 -0
- model/utils/ab_metrics.py +60 -0
- model/utils/abnumber_.py +88 -0
- model/utils/constants.py +22 -0
- model/utils/coordinates.py +76 -0
- model/utils/embed.py +53 -0
- model/utils/fasta.py +43 -0
- model/utils/folding.py +231 -0
- model/utils/general.py +57 -0
- model/utils/geometry.py +77 -0
- model/utils/pdb.py +515 -0
- model/utils/tensor.py +42 -0
- model/utils/transforms.py +520 -0
- model/utils/visualize.py +183 -0
- pyproject.toml +3 -0
- requirements.txt +16 -0
- scripts/IgFold.ipynb +199 -0
- scripts/inference.py +88 -0
- setup.cfg +20 -0
- weight/IgFold/LICENSE.md +24 -0
- weight/IgFold/igfold_1.ckpt +3 -0
- weight/IgFold/igfold_2.ckpt +3 -0
- weight/IgFold/igfold_3.ckpt +3 -0
- weight/IgFold/igfold_5.ckpt +3 -0
- weight/wheels/antiberty-0.1.3-py3-none-any.whl +3 -0
- 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
|