Add files using upload-large-folder tool
Browse files- .gitattributes +63 -13
- LICENSE +10 -0
- README.md +277 -0
- configuration.json +1 -0
- flax_model/__init__.py +1 -0
- flax_model/alphafold3/CMakeLists.txt +161 -0
- flax_model/alphafold3/README.md +81 -0
- flax_model/alphafold3/__init__.py +26 -0
- flax_model/alphafold3/_build.py +649 -0
- flax_model/alphafold3/build_data.py +51 -0
- flax_model/alphafold3/build_extension.py +19 -0
- flax_model/alphafold3/common/__init__.py +0 -0
- flax_model/alphafold3/common/base_config.py +141 -0
- flax_model/alphafold3/common/folding_input.py +1528 -0
- flax_model/alphafold3/common/resources.py +68 -0
- flax_model/alphafold3/common/testing/__init__.py +0 -0
- flax_model/alphafold3/common/testing/data.py +61 -0
- flax_model/alphafold3/constants/__init__.py +0 -0
- flax_model/alphafold3/constants/atom_types.py +253 -0
- flax_model/alphafold3/constants/chemical_component_sets.py +29 -0
- flax_model/alphafold3/constants/chemical_components.py +190 -0
- flax_model/alphafold3/constants/converters/__init__.py +0 -0
- flax_model/alphafold3/constants/converters/ccd_pickle_gen.py +42 -0
- flax_model/alphafold3/constants/converters/chemical_component_sets_gen.py +72 -0
- flax_model/alphafold3/constants/mmcif_names.py +208 -0
- flax_model/alphafold3/constants/periodic_table.py +385 -0
- flax_model/alphafold3/constants/residue_names.py +411 -0
- flax_model/alphafold3/constants/side_chains.py +103 -0
- flax_model/alphafold3/cpp.cc +45 -0
- flax_model/alphafold3/jackhmmer_seq_limit.patch +32 -0
- flax_model/alphafold3/package_config.py +68 -0
- flax_model/alphafold3/structure/__init__.py +38 -0
- flax_model/alphafold3/structure/bioassemblies.py +319 -0
- flax_model/alphafold3/structure/bonds.py +227 -0
- flax_model/alphafold3/structure/chemical_components.py +276 -0
- flax_model/alphafold3/structure/cpp/__init__.py +0 -0
- flax_model/alphafold3/structure/cpp/aggregation.pyi +5 -0
- flax_model/alphafold3/structure/cpp/aggregation_pybind.cc +54 -0
- flax_model/alphafold3/structure/cpp/aggregation_pybind.h +24 -0
- flax_model/alphafold3/structure/cpp/mmcif_atom_site.pyi +15 -0
- flax_model/alphafold3/structure/cpp/mmcif_struct_conn.pyi +5 -0
- flax_model/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc +380 -0
- flax_model/alphafold3/structure/mmcif.py +321 -0
- flax_model/alphafold3/structure/parsing.py +1801 -0
- flax_model/alphafold3/structure/sterics.py +132 -0
- flax_model/alphafold3/structure/structure.py +0 -0
- flax_model/alphafold3/structure/structure_tables.py +811 -0
- flax_model/alphafold3/structure/table.py +555 -0
- flax_model/alphafold3/structure/test_utils.py +161 -0
- flax_model/alphafold3/version.py +13 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,85 @@
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
|
|
|
|
| 4 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
|
| 8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
|
| 4 |
+
*.bin.* filter=lfs diff=lfs merge=lfs -text
|
| 5 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
|
| 8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 11 |
*.model filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 13 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 14 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 15 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 16 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 17 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 18 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 19 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 20 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
|
|
|
|
| 22 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 24 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
|
| 26 |
+
*.zstandard filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.db* filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.ark* filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
**/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
**/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
**/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*.gguf* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.ggml filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.llamafile* filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.pt2 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
|
| 43 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
|
| 45 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 46 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 47 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
|
| 49 |
+
inputs/7r6r_data.json filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
|
| 51 |
+
flax_model/alphafold3/_data/libcifpp/components.cif filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
flax_model/alphafold3/_data/libcifpp/mmcif_ma.dic filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
flax_model/alphafold3/_data/libcifpp/mmcif_pdbx.dic filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
flax_model/alphafold3/_tools/hmmer/bin/hmmpgmd filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
flax_model/alphafold3/_tools/hmmer/bin/hmmpgmd_shard filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
flax_model/alphafold3/_tools/hmmer/bin/jackhmmer filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
flax_model/alphafold3/_tools/hmmer/bin/nhmmer filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
flax_model/alphafold3/_tools/hmmer/bin/phmmer filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
|
| 60 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
flax_model/alphafold3/cpp.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 66 |
+
flax_model/alphafold3/test_data/miniature_databases/nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq__subsampled_1000.fasta filter=lfs diff=lfs merge=lfs -text
|
| 67 |
+
flax_model/alphafold3/test_data/miniature_databases/rnacentral_active_seq_id_90_cov_80_linclust__subsampled_1000.fasta filter=lfs diff=lfs merge=lfs -text
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 71 |
+
|
| 72 |
+
weight/_dep/eigen/Eigen/src/misc/lapacke.h filter=lfs diff=lfs merge=lfs -text
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
weight/_dep/libcifpp/rsrc/components.cif filter=lfs diff=lfs merge=lfs -text
|
| 76 |
+
weight/_dep/libcifpp/rsrc/mmcif_ma.dic filter=lfs diff=lfs merge=lfs -text
|
| 77 |
+
weight/_dep/libcifpp/rsrc/mmcif_pdbx.dic filter=lfs diff=lfs merge=lfs -text
|
| 78 |
+
|
| 79 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 80 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 81 |
+
|
| 82 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 83 |
+
weight/libflash_atten_c.so filter=lfs diff=lfs merge=lfs -text
|
| 84 |
+
weight/mmseqs/bin/mmseqs filter=lfs diff=lfs merge=lfs -text
|
| 85 |
+
weight/mmseqs/lib/libmarv.so filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
AlphaFold3 source files in this split retain the license headers from the
|
| 2 |
+
upstream AlphaFold 3 implementation integrated in OneScience.
|
| 3 |
+
|
| 4 |
+
The copied AlphaFold3 source headers state that AlphaFold 3 source code is
|
| 5 |
+
licensed under CC BY-NC-SA 4.0 and refer to:
|
| 6 |
+
|
| 7 |
+
https://creativecommons.org/licenses/by-nc-sa/4.0/
|
| 8 |
+
|
| 9 |
+
Model parameters are not included in this repository and are subject to their
|
| 10 |
+
own terms of use.
|
README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-nc-sa-4.0
|
| 3 |
+
tasks:
|
| 4 |
+
- protein-complex-structure-prediction
|
| 5 |
+
frameworks:
|
| 6 |
+
- jax
|
| 7 |
+
language:
|
| 8 |
+
- en
|
| 9 |
+
- zh
|
| 10 |
+
tags:
|
| 11 |
+
- OneScience
|
| 12 |
+
- Life Sciences
|
| 13 |
+
- Protein Structure Prediction
|
| 14 |
+
- Biomolecular Interactions
|
| 15 |
+
- Complex Structure Prediction
|
| 16 |
+
- AlphaFold3
|
| 17 |
+
datasets:
|
| 18 |
+
- OneScience-Sugon/AlphaFold3_dataset
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
<p align="center">
|
| 22 |
+
<strong>
|
| 23 |
+
<span style="font-size: 30px;">AlphaFold3</span>
|
| 24 |
+
</strong>
|
| 25 |
+
</p>
|
| 26 |
+
|
| 27 |
+
# Model Introduction
|
| 28 |
+
|
| 29 |
+
AlphaFold 3 is a biomolecular structure prediction model developed by Google DeepMind and Isomorphic Labs. It predicts the three-dimensional structures and interactions of proteins, DNA, RNA, small-molecule ligands, and their complexes.
|
| 30 |
+
|
| 31 |
+
Paper: Accurate structure prediction of biomolecular interactions with AlphaFold 3
|
| 32 |
+
https://www.nature.com/articles/s41586-024-07487-w
|
| 33 |
+
|
| 34 |
+
# Model Description
|
| 35 |
+
|
| 36 |
+
AlphaFold 3 uses a Pairformer and a diffusion model to predict biomolecular complex structures. This model package provides a JAX / Flax inference implementation and database-search scripts, together with the accompanying Hugging Face dataset `OneScience-Sugon/AlphaFold3_dataset`.
|
| 37 |
+
|
| 38 |
+
# Use Cases
|
| 39 |
+
|
| 40 |
+
| Scenario | Description |
|
| 41 |
+
| :---: | :--- |
|
| 42 |
+
| Direct inference from existing features | Takes an AlphaFold 3 JSON file containing precomputed features such as MSAs and templates as input and produces structure predictions |
|
| 43 |
+
| Protein structure prediction | Takes a protein sequence as input, generates features by searching databases, and predicts its structure |
|
| 44 |
+
| Biomolecular complex modeling | Takes multicomponent systems comprising proteins, DNA, RNA, ligands, and other molecules as input and predicts their 3D structures |
|
| 45 |
+
| Database-search pipeline validation | Uses Jackhmmer / Nhmmer or MMseqs pipelines to verify database paths and the availability of search tools |
|
| 46 |
+
| Hugging Face / OneCode execution | After downloading the model project and complete dataset, quickly verifies that the scripts run correctly in a life-sciences runtime environment |
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# Usage Instructions
|
| 51 |
+
|
| 52 |
+
## 1. OneCode
|
| 53 |
+
|
| 54 |
+
Try one-click AI4S development in the OneCode online environment:
|
| 55 |
+
|
| 56 |
+
[Try one-click AI4S development](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 57 |
+
|
| 58 |
+
## 2. Manual Installation
|
| 59 |
+
|
| 60 |
+
**Hardware Requirements**
|
| 61 |
+
|
| 62 |
+
- GPU or DCU is recommended.
|
| 63 |
+
- A CPU can be used for import checks and lightweight configuration tests; full training and inference will be slow.
|
| 64 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or later is recommended, or a OneScience-recommended version matching the current cluster.
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
**Environment Check**
|
| 71 |
+
|
| 72 |
+
- NVIDIA GPU:
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
nvidia-smi
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
- Hygon DCU:
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
hy-smi
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
### Download the Model Package
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
hf download --model OneScience-Sugon/AlphaFold3 --local-dir ./AlphaFold3
|
| 88 |
+
cd AlphaFold3
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### Install the Runtime Environment
|
| 92 |
+
|
| 93 |
+
**DCU Environment**
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
# Activate DTK and CONDA first
|
| 97 |
+
conda create -n onescience311 python=3.11 -y
|
| 98 |
+
conda activate onescience311
|
| 99 |
+
# uv installation supported
|
| 100 |
+
pip install onescience[bio-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
After installation, return to the model package directory:
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
cd ./AlphaFold3
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
If the AlphaFold 3 C++ extension and runtime data files have not yet been built in the current environment, run:
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
python -m onescience.flax_model.alphafold3.build_extension
|
| 113 |
+
python -m onescience.flax_models.alphafold3.build_data
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
### Training and Inference Data Overview
|
| 117 |
+
|
| 118 |
+
The OneScience community has uploaded the complete data required for AlphaFold 3 inference and database searches to Hugging Face: [OneScience-Sugon/AlphaFold3_dataset](https://huggingface.co/datasets/OneScience-Sugon/AlphaFold3_dataset). This model package does not include a training entry point; the dataset is primarily used to construct MSA / template features and perform database searches before inference.
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
hf download --dataset OneScience-Sugon/AlphaFold3_dataset --local-dir ./data/alphafold3
|
| 122 |
+
```
|
| 123 |
+
### Model Weights
|
| 124 |
+
|
| 125 |
+
Model weights will be available soon.
|
| 126 |
+
|
| 127 |
+
### Preparing Weights
|
| 128 |
+
|
| 129 |
+
Place the AlphaFold 3 model weights in the following directory, or specify them via environment variables:
|
| 130 |
+
|
| 131 |
+
```text
|
| 132 |
+
weight/
|
| 133 |
+
AlphaFold3/
|
| 134 |
+
...
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
Default lookup order:
|
| 138 |
+
|
| 139 |
+
- `ALPHAFOLD3_MODEL_DIR`
|
| 140 |
+
- `${ONESCIENCE_MODELS_DIR}/AlphaFold3`
|
| 141 |
+
- `weight/AlphaFold3`
|
| 142 |
+
|
| 143 |
+
Example:
|
| 144 |
+
|
| 145 |
+
```bash
|
| 146 |
+
export ALPHAFOLD3_MODEL_DIR=/path/to/AlphaFold3
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
### Direct Inference
|
| 150 |
+
|
| 151 |
+
When the input JSON already contains features such as MSA and templates, you can run inference directly:
|
| 152 |
+
|
| 153 |
+
```bash
|
| 154 |
+
bash scripts/infer.sh
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
Equivalent Python command example:
|
| 158 |
+
|
| 159 |
+
```bash
|
| 160 |
+
python scripts/run_alphafold.py \
|
| 161 |
+
--json_path inputs/7r6r_data.json \
|
| 162 |
+
--model_dir weight/AlphaFold3 \
|
| 163 |
+
--output_dir outputs \
|
| 164 |
+
--run_data_pipeline=false \
|
| 165 |
+
--flash_attention_implementation=triton
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
Output is written to `outputs/` and includes the top-ranked structure, structures generated from different seed / sample combinations, a CSV file containing ranking scores, and a copy of the input JSON.
|
| 169 |
+
|
| 170 |
+
### Jackhmmer / Nhmmer Database Search
|
| 171 |
+
|
| 172 |
+
When the input JSON contains only sequences and requires a local database search, use:
|
| 173 |
+
|
| 174 |
+
```bash
|
| 175 |
+
bash scripts/infer_jackhmmer.sh
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
Common environment variables:
|
| 179 |
+
|
| 180 |
+
```bash
|
| 181 |
+
export ALPHAFOLD3_DATASET_ROOT=/path/to/alphafold3
|
| 182 |
+
export ALPHAFOLD3_MODEL_DIR=/path/to/AlphaFold3
|
| 183 |
+
export ALPHAFOLD3_JSON_PATH=inputs/t1119_search.json
|
| 184 |
+
export ALPHAFOLD3_OUTPUT_DIR=outputs
|
| 185 |
+
export ALPHAFOLD3_RUN_INFERENCE=false
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
`ALPHAFOLD3_DATASET_ROOT` is expected to contain directories such as `public_databases/`, `jackhmmer_split/`, and `mmseqsDB/`.
|
| 189 |
+
|
| 190 |
+
### MMseqs Database Search
|
| 191 |
+
|
| 192 |
+
If the runtime environment provides the MMseqs executable and the required MMseqs databases, use:
|
| 193 |
+
|
| 194 |
+
```bash
|
| 195 |
+
bash scripts/infer_mmseqs.sh
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
Common environment variables:
|
| 199 |
+
|
| 200 |
+
```bash
|
| 201 |
+
export ALPHAFOLD3_MMSEQS_HOME=/path/to/mmseqs
|
| 202 |
+
export ALPHAFOLD3_DATASET_ROOT=/path/to/alphafold3
|
| 203 |
+
export ALPHAFOLD3_MMSEQS_DB_DIR=/path/to/alphafold3/mmseqsDB
|
| 204 |
+
export ALPHAFOLD3_RUN_INFERENCE=false
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
To proceed with inference after the database search, set `ALPHAFOLD3_RUN_INFERENCE` to `true` and ensure that the weights directory is available.
|
| 208 |
+
|
| 209 |
+
# Data Format
|
| 210 |
+
|
| 211 |
+
AlphaFold 3 inputs are provided in JSON format with the following basic structure:
|
| 212 |
+
|
| 213 |
+
```json
|
| 214 |
+
{
|
| 215 |
+
"dialect": "alphafold3",
|
| 216 |
+
"version": 1,
|
| 217 |
+
"name": "example",
|
| 218 |
+
"sequences": [
|
| 219 |
+
{
|
| 220 |
+
"protein": {
|
| 221 |
+
"id": "A",
|
| 222 |
+
"sequence": "..."
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
],
|
| 226 |
+
"modelSeeds": [100],
|
| 227 |
+
"bondedAtomPairs": null,
|
| 228 |
+
"userCCD": null
|
| 229 |
+
}
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
This repository provides two examples:
|
| 233 |
+
|
| 234 |
+
- `inputs/7r6r_data.json`: contains sequence, MSA, and template information; suitable for direct inference.
|
| 235 |
+
- `inputs/t1119_search.json`: contains only sequences; suitable for database-search pipeline validation.
|
| 236 |
+
|
| 237 |
+
It is recommended to download the full Hugging Face dataset `OneScience-Sugon/AlphaFold3_dataset` to `data/alphafold3/` under the model package. The relative directory structure expected by the database-search pipeline is as follows:
|
| 238 |
+
|
| 239 |
+
```text
|
| 240 |
+
data/
|
| 241 |
+
alphafold3/
|
| 242 |
+
public_databases/
|
| 243 |
+
mmcif_files/
|
| 244 |
+
pdb_seqres_2022_09_28.fasta
|
| 245 |
+
...
|
| 246 |
+
jackhmmer_split/
|
| 247 |
+
bfd-first_non_consensus_sequences.fasta@64
|
| 248 |
+
mgy_clusters_2022_05.fa@512
|
| 249 |
+
uniprot_cluster_annot_2021_04.fa@256
|
| 250 |
+
uniref90_2022_05.fa@128
|
| 251 |
+
mmseqsDB/
|
| 252 |
+
small_bfd_db
|
| 253 |
+
mgnify_db
|
| 254 |
+
uniprot_cluster_annot_db
|
| 255 |
+
uniref90_db
|
| 256 |
+
```
|
| 257 |
+
|
| 258 |
+
# Verification
|
| 259 |
+
|
| 260 |
+
Static import check:
|
| 261 |
+
|
| 262 |
+
```bash
|
| 263 |
+
python tests/check_import_boundaries.py
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
# OneScience Official Information
|
| 267 |
+
|
| 268 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 269 |
+
| --- | --- | --- |
|
| 270 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 271 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 272 |
+
|
| 273 |
+
# Citation & License
|
| 274 |
+
|
| 275 |
+
- This repository is adapted from the open-source AlphaFold 3 model to support DCUs.
|
| 276 |
+
- The AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0; model parameters are subject to separate usage terms.
|
| 277 |
+
- For scientific use, please cite the original paper: [Accurate structure prediction of biomolecular interactions with AlphaFold 3](https://www.nature.com/articles/s41586-024-07487-w).
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"JAX","task":"protein-structure-prediction"}
|
flax_model/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Local flax model namespace for the split AlphaFold3 project."""
|
flax_model/alphafold3/CMakeLists.txt
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited
|
| 2 |
+
#
|
| 3 |
+
# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of
|
| 4 |
+
# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
|
| 5 |
+
#
|
| 6 |
+
# To request access to the AlphaFold 3 model parameters, follow the process set
|
| 7 |
+
# out at https://github.com/google-deepmind/alphafold3. You may only use these
|
| 8 |
+
# if received directly from Google. Use is subject to terms of use available at
|
| 9 |
+
# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md
|
| 10 |
+
|
| 11 |
+
cmake_minimum_required(VERSION 3.24)
|
| 12 |
+
project(
|
| 13 |
+
"${SKBUILD_PROJECT_NAME}"
|
| 14 |
+
LANGUAGES CXX
|
| 15 |
+
VERSION "${SKBUILD_PROJECT_VERSION}")
|
| 16 |
+
|
| 17 |
+
include(FetchContent)
|
| 18 |
+
set(CMAKE_CXX_STANDARD 20)
|
| 19 |
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
| 20 |
+
set(CMAKE_POSITION_INDEPENDENT_CODE TRUE)
|
| 21 |
+
set(ABSL_PROPAGATE_CXX_STD ON)
|
| 22 |
+
|
| 23 |
+
# Remove support for scan deps, which is only useful when using C++ modules.
|
| 24 |
+
unset(CMAKE_CXX_SCANDEP_SOURCE)
|
| 25 |
+
|
| 26 |
+
set(ENV{GIT_CONFIG_PARAMETERS} "'init.defaultRefFormat=files'")
|
| 27 |
+
|
| 28 |
+
if(DEFINED ENV{ALPHAFOLD3_DEP_DIR})
|
| 29 |
+
set(DEP_BASE_DIR "$ENV{ALPHAFOLD3_DEP_DIR}")
|
| 30 |
+
endif()
|
| 31 |
+
|
| 32 |
+
function(declare_af3_dependency dep_name local_path_env repo_env tag_env)
|
| 33 |
+
if(DEFINED ENV{${local_path_env}} AND EXISTS "$ENV{${local_path_env}}")
|
| 34 |
+
message(STATUS "Using local dependency for ${dep_name}: $ENV{${local_path_env}}")
|
| 35 |
+
FetchContent_Declare(
|
| 36 |
+
${dep_name}
|
| 37 |
+
SOURCE_DIR $ENV{${local_path_env}}
|
| 38 |
+
${ARGN})
|
| 39 |
+
else()
|
| 40 |
+
message(STATUS "Fetching remote dependency for ${dep_name}: $ENV{${repo_env}} @ $ENV{${tag_env}}")
|
| 41 |
+
FetchContent_Declare(
|
| 42 |
+
${dep_name}
|
| 43 |
+
GIT_REPOSITORY $ENV{${repo_env}}
|
| 44 |
+
GIT_TAG $ENV{${tag_env}}
|
| 45 |
+
${ARGN})
|
| 46 |
+
endif()
|
| 47 |
+
endfunction()
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
declare_af3_dependency(
|
| 51 |
+
abseil-cpp
|
| 52 |
+
ABSEIL_LOCAL_PATH
|
| 53 |
+
ABSEIL_REPO
|
| 54 |
+
ABSEIL_TAG
|
| 55 |
+
EXCLUDE_FROM_ALL)
|
| 56 |
+
|
| 57 |
+
declare_af3_dependency(
|
| 58 |
+
pybind11
|
| 59 |
+
PYBIND11_LOCAL_PATH
|
| 60 |
+
PYBIND11_REPO
|
| 61 |
+
PYBIND11_TAG
|
| 62 |
+
EXCLUDE_FROM_ALL)
|
| 63 |
+
|
| 64 |
+
declare_af3_dependency(
|
| 65 |
+
pybind11_abseil
|
| 66 |
+
PYBIND11_ABSEIL_LOCAL_PATH
|
| 67 |
+
PYBIND11_ABSEIL_REPO
|
| 68 |
+
PYBIND11_ABSEIL_TAG
|
| 69 |
+
EXCLUDE_FROM_ALL)
|
| 70 |
+
|
| 71 |
+
declare_af3_dependency(
|
| 72 |
+
cifpp
|
| 73 |
+
CIFPP_LOCAL_PATH
|
| 74 |
+
CIFPP_REPO
|
| 75 |
+
CIFPP_TAG)
|
| 76 |
+
|
| 77 |
+
declare_af3_dependency(
|
| 78 |
+
dssp
|
| 79 |
+
DSSP_LOCAL_PATH
|
| 80 |
+
DSSP_REPO
|
| 81 |
+
DSSP_TAG
|
| 82 |
+
EXCLUDE_FROM_ALL)
|
| 83 |
+
|
| 84 |
+
if(DEFINED ENV{ALPHAFOLD3_INSTALL_PREFIX})
|
| 85 |
+
set(CMAKE_INSTALL_PREFIX $ENV{ALPHAFOLD3_INSTALL_PREFIX} CACHE PATH "Install prefix" FORCE)
|
| 86 |
+
message(STATUS "Setting install prefix to AlphaFold3 package directory: $ENV{ALPHAFOLD3_INSTALL_PREFIX}")
|
| 87 |
+
elseif(DEFINED ENV{CONDA_PREFIX})
|
| 88 |
+
set(CMAKE_INSTALL_PREFIX $ENV{CONDA_PREFIX} CACHE PATH "Install prefix" FORCE)
|
| 89 |
+
message(STATUS "Setting install prefix to conda environment: $ENV{CONDA_PREFIX}")
|
| 90 |
+
endif()
|
| 91 |
+
|
| 92 |
+
if(DEFINED ENV{ALPHAFOLD3_CIFPP_DATA_DIR})
|
| 93 |
+
set(CIFPP_DATA_DIR "$ENV{ALPHAFOLD3_CIFPP_DATA_DIR}" CACHE PATH "CIFPP data directory" FORCE)
|
| 94 |
+
message(STATUS "Setting CIFPP_DATA_DIR to ${CIFPP_DATA_DIR}")
|
| 95 |
+
elseif(DEFINED ENV{CONDA_PREFIX})
|
| 96 |
+
execute_process(
|
| 97 |
+
COMMAND python -c "import site; print(site.getsitepackages()[0])"
|
| 98 |
+
OUTPUT_VARIABLE PYTHON_SITE_PACKAGES
|
| 99 |
+
OUTPUT_STRIP_TRAILING_WHITESPACE
|
| 100 |
+
ERROR_QUIET
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
if(PYTHON_SITE_PACKAGES)
|
| 104 |
+
set(CIFPP_DATA_DIR "${PYTHON_SITE_PACKAGES}/share/libcifpp" CACHE PATH "CIFPP data directory" FORCE)
|
| 105 |
+
message(STATUS "Setting CIFPP_DATA_DIR to Python site-packages: ${CIFPP_DATA_DIR}")
|
| 106 |
+
endif()
|
| 107 |
+
endif()
|
| 108 |
+
|
| 109 |
+
FetchContent_MakeAvailable(pybind11 abseil-cpp pybind11_abseil cifpp dssp)
|
| 110 |
+
|
| 111 |
+
find_package(
|
| 112 |
+
Python3
|
| 113 |
+
COMPONENTS Interpreter Development NumPy
|
| 114 |
+
REQUIRED)
|
| 115 |
+
|
| 116 |
+
message(STATUS "AF3 Python3_EXECUTABLE=${Python3_EXECUTABLE}")
|
| 117 |
+
message(STATUS "AF3 Python3_VERSION=${Python3_VERSION}")
|
| 118 |
+
message(STATUS "AF3 Python3_INCLUDE_DIRS=${Python3_INCLUDE_DIRS}")
|
| 119 |
+
message(STATUS "AF3 Python3_SITELIB=${Python3_SITELIB}")
|
| 120 |
+
message(STATUS "AF3 Python3_SITEARCH=${Python3_SITEARCH}")
|
| 121 |
+
message(STATUS "AF3 Python3_NumPy_INCLUDE_DIRS=${Python3_NumPy_INCLUDE_DIRS}")
|
| 122 |
+
|
| 123 |
+
include_directories(${Python3_INCLUDE_DIRS})
|
| 124 |
+
include_directories(${Python3_NumPy_INCLUDE_DIRS})
|
| 125 |
+
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..)
|
| 126 |
+
|
| 127 |
+
# Only compile AF3's own C++ sources.
|
| 128 |
+
file(GLOB_RECURSE cpp_srcs ${CMAKE_CURRENT_SOURCE_DIR}/*.cc)
|
| 129 |
+
# Drop any third-party / build-time sources such as benchmarks/tests/examples.
|
| 130 |
+
list(FILTER cpp_srcs EXCLUDE REGEX ".*/_deps/.*")
|
| 131 |
+
list(FILTER cpp_srcs EXCLUDE REGEX ".*/mirror_deps/.*")
|
| 132 |
+
list(FILTER cpp_srcs EXCLUDE REGEX ".*/CMake/install_test_project/.*")
|
| 133 |
+
list(FILTER cpp_srcs EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/test/.*")
|
| 134 |
+
#list(FILTER cpp_srcs EXCLUDE REGEX ".*\\(_test\\|_main\\|_benchmark\\).cc$")
|
| 135 |
+
list(FILTER cpp_srcs EXCLUDE REGEX ".*(_test|_main|_benchmark)\\.cc$")
|
| 136 |
+
message(STATUS "AF3 cpp_srcs after filter: ${cpp_srcs}")
|
| 137 |
+
|
| 138 |
+
add_compile_definitions(NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION)
|
| 139 |
+
|
| 140 |
+
pybind11_add_module(cpp ${cpp_srcs})
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
target_link_libraries(
|
| 145 |
+
cpp
|
| 146 |
+
PRIVATE absl::check
|
| 147 |
+
absl::flat_hash_map
|
| 148 |
+
absl::node_hash_map
|
| 149 |
+
absl::strings
|
| 150 |
+
absl::status
|
| 151 |
+
absl::statusor
|
| 152 |
+
absl::log
|
| 153 |
+
pybind11_abseil::absl_casters
|
| 154 |
+
Python3::NumPy
|
| 155 |
+
dssp::dssp
|
| 156 |
+
cifpp::cifpp)
|
| 157 |
+
|
| 158 |
+
target_compile_definitions(cpp PRIVATE VERSION_INFO=${PROJECT_VERSION})
|
| 159 |
+
install(TARGETS cpp LIBRARY DESTINATION .)
|
| 160 |
+
|
| 161 |
+
|
flax_model/alphafold3/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AlphaFold 3 Integration for OneScience
|
| 2 |
+
|
| 3 |
+
This is the AlphaFold 3 implementation integrated into the OneScience framework as a submodule.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
AlphaFold 3 is a state-of-the-art machine learning model for predicting protein structure, developed by DeepMind. This submodule integrates AlphaFold 3 into the OneScience framework, allowing it to be used as part of larger scientific computing workflows.
|
| 8 |
+
|
| 9 |
+
## Installation
|
| 10 |
+
|
| 11 |
+
### As Part of OneScience
|
| 12 |
+
|
| 13 |
+
The recommended way to install alphafold3 is as part of the complete OneScience package:
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
# Install OneScience with alphafold3 support
|
| 17 |
+
|
| 18 |
+
# install jackhmmer
|
| 19 |
+
# mkdir ~/hmmer_build ~/hmmer
|
| 20 |
+
# wget http://eddylab.org/software/hmmer/hmmer-3.4.tar.gz --directory-prefix ~/hmmer_build
|
| 21 |
+
# cd ~/hmmer_build && tar zxf hmmer-3.4.tar.gz && rm hmmer-3.4.tar.gz
|
| 22 |
+
# patch -p0 < jackhmmer_seq_limit.patch
|
| 23 |
+
# cd ~/hmmer-3.4
|
| 24 |
+
# ./configure --prefix ~/hmmer
|
| 25 |
+
# make -j && make install && cd ./easel && make install
|
| 26 |
+
# rm -R ~/hmmer_build
|
| 27 |
+
|
| 28 |
+
# # install extension
|
| 29 |
+
# pip install .[bio] -c constraints.txt
|
| 30 |
+
# cp -r /public/onestore/onedatasets/alphafold3/_dep xxx/
|
| 31 |
+
# export ALPHAFOLD3_DEP_DIR=/public/onestore/onedatasets/alphafold3/_dep
|
| 32 |
+
# cd src/onescience/flax_models/alphafold3/
|
| 33 |
+
# python build_extension.py
|
| 34 |
+
|
| 35 |
+
# optional create mmseqs2 database (please contact ai4s@sugon.com for mmseqs2 program)
|
| 36 |
+
export mmfasta=/root/public_databases
|
| 37 |
+
cd /root/public_databases && mkdir mmseqsDB
|
| 38 |
+
export mmdb=/root/public_databases/mmseqsDB
|
| 39 |
+
export CUDA_VISIBLE_DEVICES=0
|
| 40 |
+
mmseqs createdb $mmfasta/bfd-first_non_consensus_sequences.fasta $mmdb/small_bfd_db --gpu 1 --threads 32 --createdb-mode 2
|
| 41 |
+
mmseqs createdb $mmfasta/mgy_clusters_2022_05.fa $mmdb/mgnify_db --gpu 1 --threads 32 --createdb-mode 2
|
| 42 |
+
mmseqs createdb $mmfasta/uniprot_all_2021_04.fa $mmdb/uniprot_cluster_annot_db --gpu 1 --threads 32 --createdb-mode 2
|
| 43 |
+
mmseqs createdb $mmfasta/uniref90_2022_05.fa $mmdb/uniref90_db --gpu 1 --threads 32 --createdb-mode 2
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
## Usage
|
| 47 |
+
|
| 48 |
+
```python
|
| 49 |
+
# Import alphafold3 as part of onescience
|
| 50 |
+
import flax_model.alphafold3 as af3
|
| 51 |
+
|
| 52 |
+
# Access alphafold3 components
|
| 53 |
+
from flax_model.alphafold3 import structure, model, data
|
| 54 |
+
|
| 55 |
+
# Use alphafold3 functionality
|
| 56 |
+
print(f"AlphaFold3 version: {af3.__version__}")
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## Requirements
|
| 60 |
+
|
| 61 |
+
- Python 3.11+
|
| 62 |
+
- JAX with CUDA support (optional, for GPU acceleration)
|
| 63 |
+
- CMake 3.28+ (for building C++ extensions)
|
| 64 |
+
- Additional dependencies listed in pyproject.toml
|
| 65 |
+
|
| 66 |
+
## License
|
| 67 |
+
|
| 68 |
+
This code is licensed under CC BY-NC-SA 4.0. See the original AlphaFold 3 repository for more details on usage restrictions and licensing terms.
|
| 69 |
+
|
| 70 |
+
## Citation
|
| 71 |
+
|
| 72 |
+
If you use this code in your research, please cite the AlphaFold 3 paper:
|
| 73 |
+
|
| 74 |
+
```
|
| 75 |
+
Abramson, J., Adler, J., Dunger, J. et al. Accurate structure prediction of biomolecular interactions with AlphaFold 3. Nature 630, 493�?00 (2024).
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
## Links
|
| 79 |
+
|
| 80 |
+
- [Original AlphaFold 3 Repository](https://github.com/google-deepmind/alphafold3)
|
| 81 |
+
- [AlphaFold 3 Paper](https://www.nature.com/articles/s41586-024-07487-w)
|
flax_model/alphafold3/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""An implementation of the inference pipeline of AlphaFold 3."""
|
| 4 |
+
from importlib import resources
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import os
|
| 7 |
+
import warnings
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _data_artifacts_exist() -> bool:
|
| 11 |
+
try:
|
| 12 |
+
root = Path(
|
| 13 |
+
resources.files(__name__ + ".constants.converters")
|
| 14 |
+
)
|
| 15 |
+
except Exception:
|
| 16 |
+
return False
|
| 17 |
+
return (root / "ccd.pickle").exists() and (root / "chemical_component_sets.pickle").exists()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
if not _data_artifacts_exist():
|
| 21 |
+
warnings.warn(
|
| 22 |
+
"AlphaFold3 data files (ccd.pickle, chemical_component_sets.pickle) are missing.\n"
|
| 23 |
+
"Please run the following command once to build local artifacts:\n\n"
|
| 24 |
+
" python -m flax_model.alphafold3.build_extension\n",
|
| 25 |
+
stacklevel=1,
|
| 26 |
+
)
|
flax_model/alphafold3/_build.py
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import shutil
|
| 4 |
+
import subprocess
|
| 5 |
+
import sys
|
| 6 |
+
import sysconfig
|
| 7 |
+
import tarfile
|
| 8 |
+
import tempfile
|
| 9 |
+
import urllib.request
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 13 |
+
MIRROR_CONFIG_PATH = ROOT / "docs" / "af3_dependency_mirrors.template.json"
|
| 14 |
+
AF3_DIR = Path(__file__).resolve().parent
|
| 15 |
+
CONVERTERS_DIR = AF3_DIR / "constants" / "converters"
|
| 16 |
+
TMP_ROOT = Path(os.environ.get("ALPHAFOLD3_TMP_ROOT", tempfile.gettempdir())).resolve() / "alphafold3_split_af3"
|
| 17 |
+
BUILD_DIR = TMP_ROOT / "build"
|
| 18 |
+
REMOTE_DEPS_DIR = TMP_ROOT / "mirror_deps"
|
| 19 |
+
HMMER_INSTALL_DIR = AF3_DIR / "_tools" / "hmmer"
|
| 20 |
+
HMMER_BIN_DIR = HMMER_INSTALL_DIR / "bin"
|
| 21 |
+
HMMER_PATCH_PATH = AF3_DIR / "jackhmmer_seq_limit.patch"
|
| 22 |
+
HMMER_SOURCE_URL = "https://gitee.com/zhang-yuqi-sudo/hmmer-github/releases/download/hmmer-3.4/hmmer-3.4.tar.gz"
|
| 23 |
+
HMMER_ARCHIVE_NAME = "hmmer-3.4.tar.gz"
|
| 24 |
+
HMMER_SOURCE_DIRNAME = "hmmer-3.4"
|
| 25 |
+
HMMER_BINARIES = ("jackhmmer", "nhmmer", "hmmalign", "hmmsearch", "hmmbuild")
|
| 26 |
+
HMMER_BUILD_ROOT = TMP_ROOT / "hmmer_build"
|
| 27 |
+
HMMER_BUILD_DIR = HMMER_BUILD_ROOT / HMMER_SOURCE_DIRNAME
|
| 28 |
+
HMMER_ARCHIVE_PATH = HMMER_BUILD_ROOT / HMMER_ARCHIVE_NAME
|
| 29 |
+
HMMER_PATCH_MARKER = HMMER_BUILD_ROOT / ".patch_applied"
|
| 30 |
+
HMMER_BUILD_MARKER = HMMER_INSTALL_DIR / ".built"
|
| 31 |
+
DATA_FILES = (
|
| 32 |
+
CONVERTERS_DIR / "ccd.pickle",
|
| 33 |
+
CONVERTERS_DIR / "chemical_component_sets.pickle",
|
| 34 |
+
)
|
| 35 |
+
LIB_PATTERNS = ("cpp*.so", "cpp*.pyd", "cpp*.dll", "cpp*.dylib")
|
| 36 |
+
|
| 37 |
+
TOP_LEVEL_MIRROR_MAP = {
|
| 38 |
+
"abseil-cpp": "ABSEIL",
|
| 39 |
+
"pybind11": "PYBIND11",
|
| 40 |
+
"pybind11_abseil": "PYBIND11_ABSEIL",
|
| 41 |
+
"libcifpp": "CIFPP",
|
| 42 |
+
"dssp": "DSSP",
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
DEFAULT_DOWNSTREAM_TAGS = {
|
| 46 |
+
"boost-regex": "boost-1.87.0",
|
| 47 |
+
"libmcfp": "v1.4.2",
|
| 48 |
+
"catch2": "v3.4.0",
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
PREPARED_ENV: dict[str, str] | None = None
|
| 52 |
+
PREPARED_DEP_DIR: Path | None = None
|
| 53 |
+
PREPARED_REMOTE = False
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class AF3BuildError(RuntimeError):
|
| 57 |
+
pass
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _env_flag(name: str, default: str = "auto") -> str:
|
| 61 |
+
return os.environ.get(name, default).strip().lower()
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def load_mirror_config() -> dict:
|
| 65 |
+
if not MIRROR_CONFIG_PATH.exists():
|
| 66 |
+
return {}
|
| 67 |
+
return json.loads(MIRROR_CONFIG_PATH.read_text())
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def mirror_value(name: str, field: str, default: str | None = None) -> str | None:
|
| 71 |
+
entry = load_mirror_config().get(name, {})
|
| 72 |
+
value = entry.get(field)
|
| 73 |
+
if isinstance(value, str) and value.strip() and "<fill-" not in value and "<your-org>" not in value:
|
| 74 |
+
return value.strip()
|
| 75 |
+
return default
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def reset_prepared_state() -> None:
|
| 79 |
+
global PREPARED_ENV, PREPARED_DEP_DIR, PREPARED_REMOTE
|
| 80 |
+
PREPARED_ENV = None
|
| 81 |
+
PREPARED_DEP_DIR = None
|
| 82 |
+
PREPARED_REMOTE = False
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def should_build() -> bool:
|
| 86 |
+
if _env_flag("ALPHAFOLD3_SKIP_BUILD", "0") in {"1", "true", "on", "yes"}:
|
| 87 |
+
return False
|
| 88 |
+
if _env_flag("ONESCIENCE_SKIP_AF3_BUILD", "0") in {"1", "true", "on", "yes"}:
|
| 89 |
+
return False
|
| 90 |
+
mode = _env_flag("ALPHAFOLD3_BUILD", _env_flag("ONESCIENCE_BUILD_AF3", "auto"))
|
| 91 |
+
if mode in {"0", "false", "off", "no"}:
|
| 92 |
+
return False
|
| 93 |
+
if mode in {"1", "true", "on", "yes", "force"}:
|
| 94 |
+
return True
|
| 95 |
+
return AF3_DIR.exists()
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def is_strict() -> bool:
|
| 99 |
+
return _env_flag("ALPHAFOLD3_STRICT", _env_flag("ONESCIENCE_AF3_STRICT", "0")) in {"1", "true", "on", "yes"}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def force_rebuild() -> bool:
|
| 103 |
+
return _env_flag("ALPHAFOLD3_FORCE_REBUILD", _env_flag("ONESCIENCE_AF3_FORCE_REBUILD", "0")) in {"1", "true", "on", "yes"}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def resolve_dep_dir() -> Path | None:
|
| 107 |
+
dep_dir = os.environ.get("ALPHAFOLD3_DEP_DIR")
|
| 108 |
+
if dep_dir:
|
| 109 |
+
return Path(dep_dir).expanduser().resolve()
|
| 110 |
+
candidate = ROOT / "third_party" / "alphafold3_deps"
|
| 111 |
+
if candidate.exists():
|
| 112 |
+
return candidate
|
| 113 |
+
return None
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def using_remote_dependencies() -> bool:
|
| 117 |
+
return resolve_dep_dir() is None
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def install_destination() -> Path:
|
| 121 |
+
return AF3_DIR
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def build_data_output_dir() -> Path:
|
| 125 |
+
return CONVERTERS_DIR
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def cifpp_data_dir() -> Path | None:
|
| 129 |
+
env_path = os.environ.get("ALPHAFOLD3_CIFPP_DATA_DIR")
|
| 130 |
+
if env_path:
|
| 131 |
+
return Path(env_path)
|
| 132 |
+
return AF3_DIR / "_data" / "libcifpp"
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def cifpp_components_path() -> Path | None:
|
| 136 |
+
data_dir = cifpp_data_dir()
|
| 137 |
+
if data_dir is None:
|
| 138 |
+
return None
|
| 139 |
+
return data_dir / "components.cif"
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def current_python_include_dir() -> str:
|
| 143 |
+
paths = sysconfig.get_paths()
|
| 144 |
+
return paths.get("include", "")
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def current_numpy_include_dir() -> str | None:
|
| 148 |
+
try:
|
| 149 |
+
import numpy
|
| 150 |
+
return numpy.get_include()
|
| 151 |
+
except Exception:
|
| 152 |
+
return None
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _lib_exists() -> bool:
|
| 156 |
+
return any(AF3_DIR.glob(pattern) for pattern in LIB_PATTERNS)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _data_exists() -> bool:
|
| 160 |
+
return all(path.exists() for path in DATA_FILES)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def artifacts_exist() -> bool:
|
| 164 |
+
return _lib_exists() and _data_exists()
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def hmmer_binaries_exist() -> bool:
|
| 168 |
+
return all((HMMER_BIN_DIR / name).exists() for name in HMMER_BINARIES)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def download_hmmer_source() -> None:
|
| 172 |
+
if HMMER_ARCHIVE_PATH.exists():
|
| 173 |
+
return
|
| 174 |
+
HMMER_BUILD_ROOT.mkdir(parents=True, exist_ok=True)
|
| 175 |
+
print(f"[AF3] downloading HMMER source from {HMMER_SOURCE_URL}")
|
| 176 |
+
urllib.request.urlretrieve(HMMER_SOURCE_URL, HMMER_ARCHIVE_PATH)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def extract_hmmer_source() -> None:
|
| 180 |
+
if HMMER_BUILD_DIR.exists():
|
| 181 |
+
return
|
| 182 |
+
HMMER_BUILD_ROOT.mkdir(parents=True, exist_ok=True)
|
| 183 |
+
with tarfile.open(HMMER_ARCHIVE_PATH, "r:gz") as tar:
|
| 184 |
+
tar.extractall(HMMER_BUILD_ROOT)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def apply_hmmer_patch() -> None:
|
| 188 |
+
if HMMER_PATCH_MARKER.exists():
|
| 189 |
+
return
|
| 190 |
+
subprocess.run(
|
| 191 |
+
["patch", "-p0", "-i", str(HMMER_PATCH_PATH.resolve())],
|
| 192 |
+
cwd=HMMER_BUILD_ROOT,
|
| 193 |
+
check=True,
|
| 194 |
+
)
|
| 195 |
+
HMMER_PATCH_MARKER.write_text("patched\n")
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def build_hmmer_tools() -> None:
|
| 199 |
+
if hmmer_binaries_exist() and HMMER_BUILD_MARKER.exists():
|
| 200 |
+
return
|
| 201 |
+
if shutil.which("make") is None and shutil.which("gmake") is None:
|
| 202 |
+
raise AF3BuildError("make or gmake is required to build HMMER tools.")
|
| 203 |
+
make_program = shutil.which("gmake") or shutil.which("make")
|
| 204 |
+
download_hmmer_source()
|
| 205 |
+
extract_hmmer_source()
|
| 206 |
+
apply_hmmer_patch()
|
| 207 |
+
HMMER_INSTALL_DIR.mkdir(parents=True, exist_ok=True)
|
| 208 |
+
print(f"[AF3] building HMMER tools into {HMMER_INSTALL_DIR}")
|
| 209 |
+
try:
|
| 210 |
+
subprocess.run(["./configure", f"--prefix={HMMER_INSTALL_DIR}"], cwd=HMMER_BUILD_DIR, check=True)
|
| 211 |
+
build_parallelism = min(os.cpu_count() or 8, 64)
|
| 212 |
+
subprocess.run([make_program, "-j", str(build_parallelism)], cwd=HMMER_BUILD_DIR, check=True)
|
| 213 |
+
subprocess.run([make_program, "install"], cwd=HMMER_BUILD_DIR, check=True)
|
| 214 |
+
subprocess.run([make_program, "install"], cwd=HMMER_BUILD_DIR / "easel", check=True)
|
| 215 |
+
except subprocess.CalledProcessError as exc:
|
| 216 |
+
raise AF3BuildError(
|
| 217 |
+
f"HMMER tools build failed with exit code {exc.returncode}. "
|
| 218 |
+
f"Check HMMER build logs under {HMMER_BUILD_DIR} for details."
|
| 219 |
+
) from exc
|
| 220 |
+
HMMER_BUILD_MARKER.write_text("built\n")
|
| 221 |
+
print(f"[AF3] HMMER tools ready at {HMMER_BIN_DIR}")
|
| 222 |
+
|
| 223 |
+
def ensure_hmmer_tools_ready() -> None:
|
| 224 |
+
if hmmer_binaries_exist():
|
| 225 |
+
return
|
| 226 |
+
if shutil.which("patch") is None:
|
| 227 |
+
raise AF3BuildError("patch is required to build HMMER tools.")
|
| 228 |
+
build_hmmer_tools()
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def ensure_dependencies_ready() -> None:
|
| 232 |
+
dep_dir = resolve_dep_dir()
|
| 233 |
+
if dep_dir is None:
|
| 234 |
+
return
|
| 235 |
+
required = ("abseil-cpp", "pybind11", "pybind11_abseil", "libcifpp", "dssp")
|
| 236 |
+
missing = [name for name in required if not (dep_dir / name).exists()]
|
| 237 |
+
if missing:
|
| 238 |
+
raise AF3BuildError(
|
| 239 |
+
f"AlphaFold3 dependency directories are missing under {dep_dir}: {', '.join(missing)}"
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def ensure_build_prerequisites() -> None:
|
| 244 |
+
if shutil.which("cmake") is None:
|
| 245 |
+
raise AF3BuildError("CMake is required to build AlphaFold3.")
|
| 246 |
+
if using_remote_dependencies() and shutil.which("git") is None:
|
| 247 |
+
raise AF3BuildError(
|
| 248 |
+
"Git is required for remote AlphaFold3 dependency fetching when no local dependency directory is configured."
|
| 249 |
+
)
|
| 250 |
+
ensure_dependencies_ready()
|
| 251 |
+
ensure_hmmer_tools_ready()
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def resolve_fetch_base() -> str:
|
| 255 |
+
return os.environ.get("ALPHAFOLD3_FETCH_BASE", "https://gitee.com/zhang-yuqi-sudo").rstrip("/")
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def dependency_specs() -> dict[str, dict[str, str]]:
|
| 259 |
+
base = resolve_fetch_base()
|
| 260 |
+
return {
|
| 261 |
+
"ABSEIL": {"repo": f"{base}/abseil-cpp", "tag": "d7aaad83b488fd62bd51c81ecf16cd938532cc0a", "local_path": "abseil-cpp"},
|
| 262 |
+
"PYBIND11": {"repo": f"{base}/pybind11", "tag": "2e0815278cb899b20870a67ca8205996ef47e70f", "local_path": "pybind11"},
|
| 263 |
+
"PYBIND11_ABSEIL": {"repo": f"{base}/pybind11_abseil", "tag": "bddf30141f9fec8e577f515313caec45f559d319", "local_path": "pybind11_abseil"},
|
| 264 |
+
"CIFPP": {"repo": f"{base}/libcifpp", "tag": "ac98531a2fc8daf21131faa0c3d73766efa46180", "local_path": "libcifpp"},
|
| 265 |
+
"DSSP": {"repo": f"{base}/dssp", "tag": "57560472b4260dc41f457706bc45fc6ef0bc0f10", "local_path": "dssp"},
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def announce_build_mode() -> None:
|
| 270 |
+
if PREPARED_DEP_DIR is not None and PREPARED_REMOTE:
|
| 271 |
+
print(f"Using prepared mirrored dependencies from {PREPARED_DEP_DIR}.")
|
| 272 |
+
elif PREPARED_DEP_DIR is not None:
|
| 273 |
+
print(f"Using local AlphaFold3 dependencies from {PREPARED_DEP_DIR}.")
|
| 274 |
+
elif using_remote_dependencies():
|
| 275 |
+
print(f"Using remote AlphaFold3 dependencies from {resolve_fetch_base()}.")
|
| 276 |
+
else:
|
| 277 |
+
print(f"Using local AlphaFold3 dependencies from {resolve_dep_dir()}.")
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def announce_install_destination() -> None:
|
| 281 |
+
print(f"Installing AlphaFold3 artifacts into {install_destination()}.")
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def announce_data_destination() -> None:
|
| 285 |
+
print(f"Writing AlphaFold3 data files into {build_data_output_dir()}.")
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def announce_cifpp_data_location() -> None:
|
| 289 |
+
data_dir = cifpp_data_dir()
|
| 290 |
+
if data_dir is not None:
|
| 291 |
+
print(f"Using libcifpp data directory {data_dir}.")
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def announce_mirror_overrides() -> None:
|
| 295 |
+
config = load_mirror_config()
|
| 296 |
+
active = []
|
| 297 |
+
for name, entry in config.items():
|
| 298 |
+
mirror = entry.get("mirror")
|
| 299 |
+
if isinstance(mirror, str) and mirror.strip() and "<your-org>" not in mirror:
|
| 300 |
+
active.append(f"{name} -> {mirror}")
|
| 301 |
+
if active:
|
| 302 |
+
print("Using dependency mirror config:")
|
| 303 |
+
for item in active:
|
| 304 |
+
print(f" {item}")
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def announce_hmmer_location() -> None:
|
| 308 |
+
print(f"Using HMMER tool directory {HMMER_BIN_DIR}.")
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def ensure_install_dirs() -> None:
|
| 312 |
+
install_destination().mkdir(parents=True, exist_ok=True)
|
| 313 |
+
build_data_output_dir().mkdir(parents=True, exist_ok=True)
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def prepare_install_layout() -> None:
|
| 317 |
+
ensure_install_dirs()
|
| 318 |
+
announce_build_mode()
|
| 319 |
+
announce_install_destination()
|
| 320 |
+
announce_data_destination()
|
| 321 |
+
announce_cifpp_data_location()
|
| 322 |
+
announce_hmmer_location()
|
| 323 |
+
announce_mirror_overrides()
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def ensure_local_components_file() -> None:
|
| 327 |
+
components_path = cifpp_components_path()
|
| 328 |
+
if components_path is None or components_path.exists():
|
| 329 |
+
return
|
| 330 |
+
if shutil.which("wget") is None:
|
| 331 |
+
return
|
| 332 |
+
components_path.parent.mkdir(parents=True, exist_ok=True)
|
| 333 |
+
try:
|
| 334 |
+
subprocess.run(
|
| 335 |
+
[
|
| 336 |
+
"wget",
|
| 337 |
+
"-O",
|
| 338 |
+
str(components_path),
|
| 339 |
+
"https://files.wwpdb.org/pub/pdb/data/monomers/components.cif",
|
| 340 |
+
],
|
| 341 |
+
check=True,
|
| 342 |
+
)
|
| 343 |
+
except subprocess.CalledProcessError as exc:
|
| 344 |
+
raise AF3BuildError(f"Failed to download components.cif with wget: {exc}") from exc
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def ensure_components_file_available() -> None:
|
| 348 |
+
components_path = cifpp_components_path()
|
| 349 |
+
if components_path is None:
|
| 350 |
+
return
|
| 351 |
+
if components_path.exists():
|
| 352 |
+
return
|
| 353 |
+
raise AF3BuildError(
|
| 354 |
+
f"Expected libcifpp components file at {components_path}, but it was not created during dependency install."
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def finalise_install_layout() -> None:
|
| 359 |
+
ensure_components_file_available()
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def patch_text(path: Path, old: str, new: str) -> None:
|
| 363 |
+
if not path.exists() or not new:
|
| 364 |
+
return
|
| 365 |
+
text = path.read_text()
|
| 366 |
+
if old in text:
|
| 367 |
+
path.write_text(text.replace(old, new))
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def clone_or_update_repo(dep_name: str, repo: str, tag: str) -> Path:
|
| 371 |
+
target = REMOTE_DEPS_DIR / dep_name
|
| 372 |
+
if target.exists():
|
| 373 |
+
shutil.rmtree(target, ignore_errors=True)
|
| 374 |
+
target.parent.mkdir(parents=True, exist_ok=True)
|
| 375 |
+
subprocess.run(["git", "clone", repo, str(target)], check=True)
|
| 376 |
+
subprocess.run(["git", "-C", str(target), "checkout", tag], check=True)
|
| 377 |
+
return target
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def disable_tests(source_dir: Path) -> None:
|
| 381 |
+
patch_text(
|
| 382 |
+
source_dir / "CMakeLists.txt",
|
| 383 |
+
"if(BUILD_TESTING AND PROJECT_IS_TOP_LEVEL)",
|
| 384 |
+
"if(FALSE AND BUILD_TESTING AND PROJECT_IS_TOP_LEVEL)",
|
| 385 |
+
)
|
| 386 |
+
patch_text(
|
| 387 |
+
source_dir / "CMakeLists.txt",
|
| 388 |
+
"if(BUILD_PYTHON_MODULE)",
|
| 389 |
+
"if(FALSE AND BUILD_PYTHON_MODULE)",
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def patch_catch2_sources(test_cmake: Path, env: dict[str, str]) -> None:
|
| 394 |
+
patch_text(test_cmake, "https://github.com/catchorg/Catch2.git", env.get("ALPHAFOLD3_CATCH2_REPO", ""))
|
| 395 |
+
patch_text(test_cmake, "v3.4.0", env.get("ALPHAFOLD3_CATCH2_TAG", ""))
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def patch_regex_sources(source_dir: Path, env: dict[str, str]) -> None:
|
| 399 |
+
patch_text(source_dir / "CMakeLists.txt", "https://github.com/boostorg/regex", env.get("ALPHAFOLD3_BOOST_REGEX_REPO", ""))
|
| 400 |
+
patch_text(source_dir / "CMakeLists.txt", "boost-1.87.0", env.get("ALPHAFOLD3_BOOST_REGEX_TAG", ""))
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
# def patch_archive_urls(source_dir: Path, env: dict[str, str]) -> None:
|
| 404 |
+
# patch_text(
|
| 405 |
+
# source_dir / "pcre2-simple" / "CMakeLists.txt",
|
| 406 |
+
# "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.46/pcre2-10.46.tar.gz",
|
| 407 |
+
# env.get("ALPHAFOLD3_PCRE2_URL", ""),
|
| 408 |
+
# )
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def patch_eigen_sources(source_dir: Path, env: dict[str, str]) -> None:
|
| 412 |
+
cmake_path = source_dir / "CMakeLists.txt"
|
| 413 |
+
patch_text(
|
| 414 |
+
cmake_path,
|
| 415 |
+
"https://gitlab.com/libeigen/eigen.git",
|
| 416 |
+
env.get("ALPHAFOLD3_EIGEN_REPO", ""),
|
| 417 |
+
)
|
| 418 |
+
patch_text(
|
| 419 |
+
cmake_path,
|
| 420 |
+
"GIT_TAG 3.4.0",
|
| 421 |
+
f"GIT_TAG {env.get('ALPHAFOLD3_EIGEN_TAG', '')}",
|
| 422 |
+
)
|
| 423 |
+
patch_text(
|
| 424 |
+
cmake_path,
|
| 425 |
+
"\t# Create a private copy of eigen3 and populate it only, no need to build\n",
|
| 426 |
+
"\t# Create a private copy of eigen3 and populate it only, no need to build\n\tmessage(STATUS \"AF3 libcifpp: Eigen3 not found locally, populating my-eigen3 from GIT repository\")\n\tmessage(STATUS \"AF3 libcifpp: my-eigen3 repo=$ENV{ALPHAFOLD3_EIGEN_REPO} tag=$ENV{ALPHAFOLD3_EIGEN_TAG}\")\n",
|
| 427 |
+
)
|
| 428 |
+
patch_text(
|
| 429 |
+
cmake_path,
|
| 430 |
+
"\tFetchContent_GetProperties(my-eigen3)\n",
|
| 431 |
+
"\tFetchContent_GetProperties(my-eigen3)\n\tmessage(STATUS \"AF3 libcifpp: my-eigen3 populated=${my-eigen3_POPULATED}\")\n",
|
| 432 |
+
)
|
| 433 |
+
patch_text(
|
| 434 |
+
cmake_path,
|
| 435 |
+
"\tif(NOT my-eigen3_POPULATED)\n\t\tFetchContent_Populate(my-eigen3)\n\tendif()\n",
|
| 436 |
+
"\tif(NOT my-eigen3_POPULATED)\n\t\tmessage(STATUS \"AF3 libcifpp: starting FetchContent_Populate(my-eigen3)\")\n\t\tFetchContent_Populate(my-eigen3)\n\t\tmessage(STATUS \"AF3 libcifpp: finished FetchContent_Populate(my-eigen3), source=${my-eigen3_SOURCE_DIR}\")\n\tendif()\n",
|
| 437 |
+
)
|
| 438 |
+
patch_text(
|
| 439 |
+
cmake_path,
|
| 440 |
+
"\tset(EIGEN_INCLUDE_DIR ${my-eigen3_SOURCE_DIR})\n",
|
| 441 |
+
"\tset(EIGEN_INCLUDE_DIR ${my-eigen3_SOURCE_DIR})\n\tmessage(STATUS \"AF3 libcifpp: EIGEN_INCLUDE_DIR=${EIGEN_INCLUDE_DIR}\")\n",
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def patch_all_known_downstream_sources(source_dir: Path, env: dict[str, str]) -> None:
|
| 446 |
+
disable_tests(source_dir)
|
| 447 |
+
patch_regex_sources(source_dir, env)
|
| 448 |
+
# patch_archive_urls(source_dir, env)
|
| 449 |
+
patch_eigen_sources(source_dir, env)
|
| 450 |
+
test_cmake = source_dir / "test" / "CMakeLists.txt"
|
| 451 |
+
if test_cmake.exists():
|
| 452 |
+
patch_catch2_sources(test_cmake, env)
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
def patch_libcifpp_sources(source_dir: Path, env: dict[str, str]) -> None:
|
| 456 |
+
patch_all_known_downstream_sources(source_dir, env)
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def patch_dssp_sources(source_dir: Path, env: dict[str, str]) -> None:
|
| 460 |
+
patch_all_known_downstream_sources(source_dir, env)
|
| 461 |
+
patch_text(source_dir / "CMakeLists.txt", "https://github.com/mhekkel/libmcfp", env.get("ALPHAFOLD3_LIBMCFP_REPO", ""))
|
| 462 |
+
patch_text(source_dir / "CMakeLists.txt", "v1.4.2", env.get("ALPHAFOLD3_LIBMCFP_TAG", ""))
|
| 463 |
+
patch_text(source_dir / "CMakeLists.txt", "https://github.com/PDB-REDO/libcifpp", env.get("CIFPP_REPO", ""))
|
| 464 |
+
patch_text(source_dir / "CMakeLists.txt", "v10.0.1", env.get("CIFPP_TAG", ""))
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
def prepare_remote_dependency_sources(env: dict[str, str]) -> Path:
|
| 468 |
+
if REMOTE_DEPS_DIR.exists():
|
| 469 |
+
shutil.rmtree(REMOTE_DEPS_DIR, ignore_errors=True)
|
| 470 |
+
REMOTE_DEPS_DIR.mkdir(parents=True, exist_ok=True)
|
| 471 |
+
|
| 472 |
+
specs = dependency_specs()
|
| 473 |
+
prepared = {}
|
| 474 |
+
clone_order = ["ABSEIL", "PYBIND11", "PYBIND11_ABSEIL", "CIFPP", "DSSP"]
|
| 475 |
+
for key_prefix in clone_order:
|
| 476 |
+
spec = specs[key_prefix]
|
| 477 |
+
dep_name = spec["local_path"]
|
| 478 |
+
repo = env[f"{key_prefix}_REPO"]
|
| 479 |
+
tag = env[f"{key_prefix}_TAG"]
|
| 480 |
+
prepared[dep_name] = clone_or_update_repo(dep_name, repo, tag)
|
| 481 |
+
if dep_name == "libcifpp":
|
| 482 |
+
patch_libcifpp_sources(prepared[dep_name], env)
|
| 483 |
+
elif dep_name == "dssp":
|
| 484 |
+
patch_dssp_sources(prepared[dep_name], env)
|
| 485 |
+
|
| 486 |
+
return REMOTE_DEPS_DIR
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def prepare_build_env_once() -> dict[str, str]:
|
| 490 |
+
global PREPARED_ENV, PREPARED_DEP_DIR, PREPARED_REMOTE
|
| 491 |
+
if PREPARED_ENV is not None:
|
| 492 |
+
return PREPARED_ENV
|
| 493 |
+
|
| 494 |
+
env = os.environ.copy()
|
| 495 |
+
env["PATH"] = str(HMMER_BIN_DIR) + os.pathsep + env.get("PATH", "")
|
| 496 |
+
specs = dependency_specs()
|
| 497 |
+
for prefix, spec in specs.items():
|
| 498 |
+
env[f"{prefix}_REPO"] = mirror_value(spec["local_path"], "mirror", spec["repo"]) or spec["repo"]
|
| 499 |
+
env[f"{prefix}_TAG"] = mirror_value(spec["local_path"], "tag", spec["tag"]) or spec["tag"]
|
| 500 |
+
|
| 501 |
+
env["ALPHAFOLD3_BOOST_REGEX_REPO"] = mirror_value("boost-regex", "mirror", "https://github.com/boostorg/regex") or "https://github.com/boostorg/regex"
|
| 502 |
+
env["ALPHAFOLD3_BOOST_REGEX_TAG"] = mirror_value("boost-regex", "tag", DEFAULT_DOWNSTREAM_TAGS["boost-regex"]) or DEFAULT_DOWNSTREAM_TAGS["boost-regex"]
|
| 503 |
+
env["ALPHAFOLD3_LIBMCFP_REPO"] = mirror_value("libmcfp", "mirror", "https://github.com/mhekkel/libmcfp") or "https://github.com/mhekkel/libmcfp"
|
| 504 |
+
env["ALPHAFOLD3_LIBMCFP_TAG"] = mirror_value("libmcfp", "tag", DEFAULT_DOWNSTREAM_TAGS["libmcfp"]) or DEFAULT_DOWNSTREAM_TAGS["libmcfp"]
|
| 505 |
+
env["ALPHAFOLD3_CATCH2_REPO"] = mirror_value("catch2", "mirror", "https://github.com/catchorg/Catch2.git") or "https://github.com/catchorg/Catch2.git"
|
| 506 |
+
env["ALPHAFOLD3_CATCH2_TAG"] = mirror_value("catch2", "tag", DEFAULT_DOWNSTREAM_TAGS["catch2"]) or DEFAULT_DOWNSTREAM_TAGS["catch2"]
|
| 507 |
+
pcre2_url = mirror_value("pcre2", "mirror")
|
| 508 |
+
if pcre2_url:
|
| 509 |
+
env["ALPHAFOLD3_PCRE2_URL"] = pcre2_url
|
| 510 |
+
eigen_repo = mirror_value("eigen", "mirror", "https://gitlab.com/libeigen/eigen.git") or "https://gitlab.com/libeigen/eigen.git"
|
| 511 |
+
eigen_tag = mirror_value("eigen", "tag", "3.4.0") or "3.4.0"
|
| 512 |
+
env["ALPHAFOLD3_EIGEN_REPO"] = eigen_repo
|
| 513 |
+
env["ALPHAFOLD3_EIGEN_TAG"] = eigen_tag
|
| 514 |
+
dep_dir = resolve_dep_dir()
|
| 515 |
+
if dep_dir is None and using_remote_dependencies():
|
| 516 |
+
dep_dir = prepare_remote_dependency_sources(env)
|
| 517 |
+
PREPARED_REMOTE = True
|
| 518 |
+
if dep_dir is not None:
|
| 519 |
+
PREPARED_DEP_DIR = dep_dir
|
| 520 |
+
env["ALPHAFOLD3_DEP_DIR"] = str(dep_dir)
|
| 521 |
+
for prefix, spec in specs.items():
|
| 522 |
+
env[f"{prefix}_LOCAL_PATH"] = str(dep_dir / spec["local_path"])
|
| 523 |
+
|
| 524 |
+
env["ALPHAFOLD3_INSTALL_PREFIX"] = str(install_destination())
|
| 525 |
+
env["ALPHAFOLD3_DATA_OUTPUT_DIR"] = str(build_data_output_dir())
|
| 526 |
+
data_dir = cifpp_data_dir()
|
| 527 |
+
components_path = cifpp_components_path()
|
| 528 |
+
if data_dir is not None:
|
| 529 |
+
env["ALPHAFOLD3_CIFPP_DATA_DIR"] = str(data_dir)
|
| 530 |
+
if components_path is not None:
|
| 531 |
+
env["ALPHAFOLD3_CIFPP_COMPONENTS"] = str(components_path)
|
| 532 |
+
env["ALPHAFOLD3_PYTHON_EXECUTABLE"] = sys.executable
|
| 533 |
+
python_include_dir = current_python_include_dir()
|
| 534 |
+
if python_include_dir:
|
| 535 |
+
env["ALPHAFOLD3_PYTHON_INCLUDE_DIR"] = python_include_dir
|
| 536 |
+
numpy_include_dir = current_numpy_include_dir()
|
| 537 |
+
if numpy_include_dir:
|
| 538 |
+
env["ALPHAFOLD3_NUMPY_INCLUDE_DIR"] = numpy_include_dir
|
| 539 |
+
pythonpath_entries = [str(ROOT)]
|
| 540 |
+
existing_pythonpath = env.get("PYTHONPATH")
|
| 541 |
+
if existing_pythonpath:
|
| 542 |
+
pythonpath_entries.append(existing_pythonpath)
|
| 543 |
+
env["PYTHONPATH"] = os.pathsep.join(pythonpath_entries)
|
| 544 |
+
|
| 545 |
+
PREPARED_ENV = env
|
| 546 |
+
return env
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
def _build_env() -> dict[str, str]:
|
| 550 |
+
return prepare_build_env_once().copy()
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
def _run(command: list[str], cwd: Path) -> None:
|
| 554 |
+
env = _build_env()
|
| 555 |
+
if command and command[0] == "cmake":
|
| 556 |
+
resolved_cmake = shutil.which("cmake", path=env.get("PATH"))
|
| 557 |
+
print(f"[AF3] build python executable: {sys.executable}")
|
| 558 |
+
print(f"[AF3] build cwd: {cwd}")
|
| 559 |
+
print(f"[AF3] build PATH: {env.get('PATH', '')}")
|
| 560 |
+
print(f"[AF3] resolved cmake: {resolved_cmake}")
|
| 561 |
+
print(f"[AF3] build PYTHONPATH: {env.get('PYTHONPATH', '')}")
|
| 562 |
+
print(f"[AF3] build CONDA_PREFIX: {env.get('CONDA_PREFIX', '')}")
|
| 563 |
+
print(f"[AF3] build VIRTUAL_ENV: {env.get('VIRTUAL_ENV', '')}")
|
| 564 |
+
print(f"[AF3] ALPHAFOLD3_PYTHON_EXECUTABLE: {env.get('ALPHAFOLD3_PYTHON_EXECUTABLE', '')}")
|
| 565 |
+
print(f"[AF3] ALPHAFOLD3_PYTHON_INCLUDE_DIR: {env.get('ALPHAFOLD3_PYTHON_INCLUDE_DIR', '')}")
|
| 566 |
+
print(f"[AF3] ALPHAFOLD3_NUMPY_INCLUDE_DIR: {env.get('ALPHAFOLD3_NUMPY_INCLUDE_DIR', '')}")
|
| 567 |
+
subprocess.run(command, cwd=cwd, env=env, check=True)
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
def build_cpp_extension() -> None:
|
| 571 |
+
BUILD_DIR.mkdir(exist_ok=True)
|
| 572 |
+
try:
|
| 573 |
+
cmake_configure_command = [
|
| 574 |
+
"cmake",
|
| 575 |
+
str(AF3_DIR),
|
| 576 |
+
"-DCMAKE_BUILD_TYPE=Release",
|
| 577 |
+
"-DCMAKE_CXX_STANDARD=20",
|
| 578 |
+
"-DCMAKE_POSITION_INDEPENDENT_CODE=ON",
|
| 579 |
+
"-DBUILD_TESTING=OFF",
|
| 580 |
+
"-DCMAKE_CXX_SCAN_FOR_MODULES=OFF",
|
| 581 |
+
"-DSKBUILD_PROJECT_NAME=cpp",
|
| 582 |
+
"-DSKBUILD_PROJECT_VERSION=0.3.0",
|
| 583 |
+
f"-DPython3_EXECUTABLE={sys.executable}",
|
| 584 |
+
"-DPython3_FIND_STRATEGY=LOCATION",
|
| 585 |
+
]
|
| 586 |
+
python_include_dir = current_python_include_dir()
|
| 587 |
+
if python_include_dir:
|
| 588 |
+
cmake_configure_command.append(f"-DPython3_INCLUDE_DIR={python_include_dir}")
|
| 589 |
+
numpy_include_dir = current_numpy_include_dir()
|
| 590 |
+
if numpy_include_dir:
|
| 591 |
+
cmake_configure_command.append(f"-DPython3_NumPy_INCLUDE_DIR={numpy_include_dir}")
|
| 592 |
+
_run(cmake_configure_command, cwd=BUILD_DIR)
|
| 593 |
+
build_parallelism = min(os.cpu_count() or 8, 64)
|
| 594 |
+
_run(["cmake", "--build", ".", "--parallel", str(build_parallelism)], cwd=BUILD_DIR)
|
| 595 |
+
_run(["cmake", "--install", ".", "--prefix", str(install_destination())], cwd=BUILD_DIR)
|
| 596 |
+
except FileNotFoundError as exc:
|
| 597 |
+
raise AF3BuildError(f"CMake not found: {exc}") from exc
|
| 598 |
+
except subprocess.CalledProcessError as exc:
|
| 599 |
+
raise AF3BuildError(f"Failed to build AlphaFold3 C++ extension: {exc}") from exc
|
| 600 |
+
finally:
|
| 601 |
+
cleanup_build_artifacts()
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
def build_data_files() -> None:
|
| 605 |
+
try:
|
| 606 |
+
from flax_model.alphafold3.build_data import build_data
|
| 607 |
+
build_data()
|
| 608 |
+
except Exception as exc:
|
| 609 |
+
raise AF3BuildError(f"Failed to build AlphaFold3 data files: {exc}") from exc
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
def cleanup_build_artifacts() -> None:
|
| 613 |
+
for path in (
|
| 614 |
+
BUILD_DIR,
|
| 615 |
+
AF3_DIR / "include",
|
| 616 |
+
AF3_DIR / "lib",
|
| 617 |
+
AF3_DIR / "lib64",
|
| 618 |
+
AF3_DIR / "var",
|
| 619 |
+
AF3_DIR / "etc",
|
| 620 |
+
):
|
| 621 |
+
if path.exists():
|
| 622 |
+
shutil.rmtree(path, ignore_errors=True)
|
| 623 |
+
|
| 624 |
+
|
| 625 |
+
def cleanup_prepared_remote_deps() -> None:
|
| 626 |
+
if PREPARED_REMOTE and REMOTE_DEPS_DIR.exists():
|
| 627 |
+
shutil.rmtree(REMOTE_DEPS_DIR, ignore_errors=True)
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
def build_all() -> None:
|
| 631 |
+
reset_prepared_state()
|
| 632 |
+
ensure_build_prerequisites()
|
| 633 |
+
prepare_build_env_once()
|
| 634 |
+
ensure_local_components_file()
|
| 635 |
+
prepare_install_layout()
|
| 636 |
+
print("Building AlphaFold3 C++ extension...")
|
| 637 |
+
build_cpp_extension()
|
| 638 |
+
finalise_install_layout()
|
| 639 |
+
print("Building AlphaFold3 data files...")
|
| 640 |
+
build_data_files()
|
| 641 |
+
cleanup_prepared_remote_deps()
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
def build_if_needed() -> None:
|
| 645 |
+
if not should_build():
|
| 646 |
+
return
|
| 647 |
+
if artifacts_exist() and not force_rebuild():
|
| 648 |
+
return
|
| 649 |
+
build_all()
|
flax_model/alphafold3/build_data.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Script for building intermediate data."""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from importlib import resources
|
| 7 |
+
import pathlib
|
| 8 |
+
import site
|
| 9 |
+
|
| 10 |
+
import flax_model.alphafold3.constants.converters
|
| 11 |
+
from flax_model.alphafold3.constants.converters import ccd_pickle_gen
|
| 12 |
+
from flax_model.alphafold3.constants.converters import chemical_component_sets_gen
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def build_data():
|
| 16 |
+
"""Builds intermediate data."""
|
| 17 |
+
explicit_cif_path = os.environ.get('ALPHAFOLD3_CIFPP_COMPONENTS')
|
| 18 |
+
if explicit_cif_path:
|
| 19 |
+
cif_path = pathlib.Path(explicit_cif_path)
|
| 20 |
+
if not cif_path.exists():
|
| 21 |
+
raise ValueError(f'Configured components.cif does not exist: {cif_path}')
|
| 22 |
+
else:
|
| 23 |
+
for site_path in site.getsitepackages():
|
| 24 |
+
path = pathlib.Path(site_path) / 'share/libcifpp/components.cif'
|
| 25 |
+
if path.exists():
|
| 26 |
+
cif_path = path
|
| 27 |
+
break
|
| 28 |
+
else:
|
| 29 |
+
raise ValueError('Could not find components.cif')
|
| 30 |
+
|
| 31 |
+
output_root = os.environ.get('ALPHAFOLD3_DATA_OUTPUT_DIR')
|
| 32 |
+
if output_root:
|
| 33 |
+
out_root = pathlib.Path(output_root)
|
| 34 |
+
out_root.mkdir(parents=True, exist_ok=True)
|
| 35 |
+
else:
|
| 36 |
+
out_root = pathlib.Path(resources.files(flax_model.alphafold3.constants.converters))
|
| 37 |
+
ccd_pickle_path = out_root / 'ccd.pickle'
|
| 38 |
+
chemical_component_sets_pickle_path = out_root / 'chemical_component_sets.pickle'
|
| 39 |
+
ccd_pickle_gen.main(['', str(cif_path), str(ccd_pickle_path)])
|
| 40 |
+
|
| 41 |
+
chemical_component_sets_gen.main(
|
| 42 |
+
['', str(chemical_component_sets_pickle_path)]
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def main() -> None:
|
| 47 |
+
build_data()
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
if __name__ == "__main__":
|
| 51 |
+
main()
|
flax_model/alphafold3/build_extension.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Compatibility entry point for local AlphaFold3 build helpers."""
|
| 3 |
+
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 8 |
+
sys.path.insert(0, str(ROOT))
|
| 9 |
+
|
| 10 |
+
from flax_model.alphafold3._build import AF3BuildError, build_all
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
if __name__ == "__main__":
|
| 14 |
+
try:
|
| 15 |
+
build_all()
|
| 16 |
+
except AF3BuildError as exc:
|
| 17 |
+
print(f"Error: {exc}")
|
| 18 |
+
sys.exit(1)
|
| 19 |
+
sys.exit(0)
|
flax_model/alphafold3/common/__init__.py
ADDED
|
File without changes
|
flax_model/alphafold3/common/base_config.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Config for the protein folding model and experiment."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping
|
| 6 |
+
import copy
|
| 7 |
+
import dataclasses
|
| 8 |
+
import types
|
| 9 |
+
import typing
|
| 10 |
+
from typing import Any, ClassVar, TypeVar
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
_T = TypeVar('_T')
|
| 14 |
+
_ConfigT = TypeVar('_ConfigT', bound='BaseConfig')
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _strip_optional(t: type[Any]) -> type[Any]:
|
| 18 |
+
"""Transforms type annotations of the form `T | None` to `T`."""
|
| 19 |
+
if typing.get_origin(t) in (typing.Union, types.UnionType):
|
| 20 |
+
args = set(typing.get_args(t)) - {types.NoneType}
|
| 21 |
+
if len(args) == 1:
|
| 22 |
+
return args.pop()
|
| 23 |
+
return t
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
_NO_UPDATE = object()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class _Autocreate:
|
| 30 |
+
|
| 31 |
+
def __init__(self, **defaults: Any):
|
| 32 |
+
self.defaults = defaults
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def autocreate(**defaults: Any) -> Any:
|
| 36 |
+
"""Marks a field as having a default factory derived from its type."""
|
| 37 |
+
return _Autocreate(**defaults)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _clone_field(
|
| 41 |
+
field: dataclasses.Field[_T], new_default: _T
|
| 42 |
+
) -> dataclasses.Field[_T]:
|
| 43 |
+
if new_default is _NO_UPDATE:
|
| 44 |
+
return copy.copy(field)
|
| 45 |
+
return dataclasses.field(
|
| 46 |
+
default=new_default,
|
| 47 |
+
init=True,
|
| 48 |
+
kw_only=True,
|
| 49 |
+
repr=field.repr,
|
| 50 |
+
hash=field.hash,
|
| 51 |
+
compare=field.compare,
|
| 52 |
+
metadata=field.metadata,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@typing.dataclass_transform()
|
| 57 |
+
class ConfigMeta(type):
|
| 58 |
+
"""Metaclass that synthesizes a __post_init__ that coerces dicts to Config subclass instances."""
|
| 59 |
+
|
| 60 |
+
def __new__(mcs, name, bases, classdict):
|
| 61 |
+
cls = super().__new__(mcs, name, bases, classdict)
|
| 62 |
+
|
| 63 |
+
def _coercable_fields(self) -> Mapping[str, tuple[ConfigMeta, Any]]:
|
| 64 |
+
type_hints = typing.get_type_hints(self.__class__)
|
| 65 |
+
fields = dataclasses.fields(self.__class__)
|
| 66 |
+
field_to_type_and_default = {
|
| 67 |
+
field.name: (_strip_optional(type_hints[field.name]), field.default)
|
| 68 |
+
for field in fields
|
| 69 |
+
}
|
| 70 |
+
coercable_fields = {
|
| 71 |
+
f: t
|
| 72 |
+
for f, t in field_to_type_and_default.items()
|
| 73 |
+
if issubclass(type(t[0]), ConfigMeta)
|
| 74 |
+
}
|
| 75 |
+
return coercable_fields
|
| 76 |
+
|
| 77 |
+
cls._coercable_fields = property(_coercable_fields)
|
| 78 |
+
|
| 79 |
+
old_post_init = getattr(cls, '__post_init__', None)
|
| 80 |
+
|
| 81 |
+
def _post_init(self) -> None:
|
| 82 |
+
# Use get_type_hints instead of Field.type to ensure that forward
|
| 83 |
+
# references are resolved.
|
| 84 |
+
for field_name, (
|
| 85 |
+
field_type,
|
| 86 |
+
field_default,
|
| 87 |
+
) in self._coercable_fields.items(): # pylint: disable=protected-access
|
| 88 |
+
field_value = getattr(self, field_name)
|
| 89 |
+
if field_value is None:
|
| 90 |
+
continue
|
| 91 |
+
try:
|
| 92 |
+
match field_value:
|
| 93 |
+
case _Autocreate():
|
| 94 |
+
# Construct from field defaults.
|
| 95 |
+
setattr(self, field_name, field_type(**field_value.defaults))
|
| 96 |
+
case Mapping():
|
| 97 |
+
# Field value is not yet a `Config` instance; Assume we can create
|
| 98 |
+
# one by splatting keys and values.
|
| 99 |
+
args = {}
|
| 100 |
+
# Apply default args first, if present.
|
| 101 |
+
if isinstance(field_default, _Autocreate):
|
| 102 |
+
args.update(field_default.defaults)
|
| 103 |
+
args.update(field_value)
|
| 104 |
+
setattr(self, field_name, field_type(**args))
|
| 105 |
+
case _:
|
| 106 |
+
pass
|
| 107 |
+
except TypeError as e:
|
| 108 |
+
raise TypeError(
|
| 109 |
+
f'Failure while coercing field {field_name!r} of'
|
| 110 |
+
f' {self.__class__.__qualname__}'
|
| 111 |
+
) from e
|
| 112 |
+
if old_post_init:
|
| 113 |
+
old_post_init(self)
|
| 114 |
+
|
| 115 |
+
cls.__post_init__ = _post_init
|
| 116 |
+
|
| 117 |
+
return dataclasses.dataclass(kw_only=True)(cls)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class BaseConfig(metaclass=ConfigMeta):
|
| 121 |
+
"""Config base class.
|
| 122 |
+
|
| 123 |
+
Subclassing Config automatically makes the subclass a kw_only dataclass with
|
| 124 |
+
a `__post_init__` that coerces Config-subclass field values from mappings to
|
| 125 |
+
instances of the right type.
|
| 126 |
+
"""
|
| 127 |
+
# Provided by dataclasses.make_dataclass
|
| 128 |
+
__dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]]
|
| 129 |
+
|
| 130 |
+
# Overridden by metaclass
|
| 131 |
+
@property
|
| 132 |
+
def _coercable_fields(self) -> Mapping[str, tuple[type['BaseConfig'], Any]]:
|
| 133 |
+
return {}
|
| 134 |
+
|
| 135 |
+
def as_dict(self) -> Mapping[str, Any]:
|
| 136 |
+
result = dataclasses.asdict(self)
|
| 137 |
+
for field_name in self._coercable_fields:
|
| 138 |
+
field_value = getattr(self, field_name, None)
|
| 139 |
+
if isinstance(field_value, BaseConfig):
|
| 140 |
+
result[field_name] = field_value.as_dict()
|
| 141 |
+
return result
|
flax_model/alphafold3/common/folding_input.py
ADDED
|
@@ -0,0 +1,1528 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Model input dataclass."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Collection, Iterator, Mapping, Sequence
|
| 6 |
+
import dataclasses
|
| 7 |
+
import gzip
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
import lzma
|
| 11 |
+
import os
|
| 12 |
+
import pathlib
|
| 13 |
+
import random
|
| 14 |
+
import re
|
| 15 |
+
import string
|
| 16 |
+
from typing import Any, Final, Self, TypeAlias, cast
|
| 17 |
+
|
| 18 |
+
from flax_model.alphafold3 import structure
|
| 19 |
+
from flax_model.alphafold3.constants import chemical_components
|
| 20 |
+
from flax_model.alphafold3.constants import mmcif_names
|
| 21 |
+
from flax_model.alphafold3.constants import residue_names
|
| 22 |
+
from flax_model.alphafold3.cpp import cif_dict
|
| 23 |
+
from flax_model.alphafold3.structure import mmcif as mmcif_lib
|
| 24 |
+
import rdkit.Chem as rd_chem
|
| 25 |
+
import zstandard as zstd
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
BondAtomId: TypeAlias = tuple[str, int, str]
|
| 29 |
+
|
| 30 |
+
JSON_DIALECT: Final[str] = 'alphafold3'
|
| 31 |
+
JSON_VERSIONS: Final[tuple[int, ...]] = (1, 2, 3, 4)
|
| 32 |
+
JSON_VERSION: Final[int] = JSON_VERSIONS[-1]
|
| 33 |
+
|
| 34 |
+
ALPHAFOLDSERVER_JSON_DIALECT: Final[str] = 'alphafoldserver'
|
| 35 |
+
ALPHAFOLDSERVER_JSON_VERSION: Final[int] = 1
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _validate_keys(actual: Collection[str], expected: Collection[str]):
|
| 39 |
+
"""Validates that the JSON doesn't contain any extra unwanted keys."""
|
| 40 |
+
if bad_keys := set(actual) - set(expected):
|
| 41 |
+
raise ValueError(f'Unexpected JSON keys in: {", ".join(sorted(bad_keys))}')
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _read_file(path: pathlib.Path, json_path: pathlib.Path | None) -> str:
|
| 45 |
+
"""Reads a maybe compressed (gzip, xz, zstd) file from the given path.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
path: The path to the file to read. This can be either absolute path, or a
|
| 49 |
+
path relative to the JSON file path.
|
| 50 |
+
json_path: The path to the JSON file. If None, the path must be absolute.
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
The contents of the file.
|
| 54 |
+
"""
|
| 55 |
+
if not path.is_absolute():
|
| 56 |
+
if json_path is None:
|
| 57 |
+
raise ValueError('json_path must be specified if path is not absolute.')
|
| 58 |
+
path = (json_path.parent / path).resolve()
|
| 59 |
+
|
| 60 |
+
with open(path, 'rb') as f:
|
| 61 |
+
first_six_bytes = f.read(6)
|
| 62 |
+
f.seek(0)
|
| 63 |
+
|
| 64 |
+
# Detect the compression type using the magic number in the header.
|
| 65 |
+
if first_six_bytes[:2] == b'\x1f\x8b':
|
| 66 |
+
with gzip.open(f, 'rt') as gzip_f:
|
| 67 |
+
return cast(str, gzip_f.read())
|
| 68 |
+
elif first_six_bytes == b'\xfd\x37\x7a\x58\x5a\x00':
|
| 69 |
+
with lzma.open(f, 'rt') as xz_f:
|
| 70 |
+
return cast(str, xz_f.read())
|
| 71 |
+
elif first_six_bytes[:4] == b'\x28\xb5\x2f\xfd':
|
| 72 |
+
with zstd.open(f, 'rt') as zstd_f:
|
| 73 |
+
return cast(str, zstd_f.read())
|
| 74 |
+
else:
|
| 75 |
+
return f.read().decode('utf-8')
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class Template:
|
| 79 |
+
"""Structural template input."""
|
| 80 |
+
|
| 81 |
+
__slots__ = ('_mmcif', '_query_to_template')
|
| 82 |
+
|
| 83 |
+
def __init__(self, *, mmcif: str, query_to_template_map: Mapping[int, int]):
|
| 84 |
+
"""Initializes the template.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
mmcif: The structural template in mmCIF format. The mmCIF should have only
|
| 88 |
+
one protein chain.
|
| 89 |
+
query_to_template_map: A mapping from query residue index to template
|
| 90 |
+
residue index.
|
| 91 |
+
"""
|
| 92 |
+
self._mmcif = mmcif
|
| 93 |
+
# Needed to make the Template class hashable.
|
| 94 |
+
self._query_to_template = tuple(query_to_template_map.items())
|
| 95 |
+
|
| 96 |
+
@property
|
| 97 |
+
def query_to_template_map(self) -> Mapping[int, int]:
|
| 98 |
+
return dict(self._query_to_template)
|
| 99 |
+
|
| 100 |
+
@property
|
| 101 |
+
def mmcif(self) -> str:
|
| 102 |
+
return self._mmcif
|
| 103 |
+
|
| 104 |
+
def __hash__(self) -> int:
|
| 105 |
+
return hash((self._mmcif, tuple(sorted(self._query_to_template))))
|
| 106 |
+
|
| 107 |
+
def __eq__(self, other: Self) -> bool:
|
| 108 |
+
mmcifs_equal = self._mmcif == other._mmcif
|
| 109 |
+
maps_equal = sorted(self._query_to_template) == sorted(
|
| 110 |
+
other._query_to_template
|
| 111 |
+
)
|
| 112 |
+
return mmcifs_equal and maps_equal
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class ProteinChain:
|
| 116 |
+
"""Protein chain input."""
|
| 117 |
+
|
| 118 |
+
__slots__ = (
|
| 119 |
+
'_id',
|
| 120 |
+
'_sequence',
|
| 121 |
+
'_ptms',
|
| 122 |
+
'_description',
|
| 123 |
+
'_paired_msa',
|
| 124 |
+
'_unpaired_msa',
|
| 125 |
+
'_templates',
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
def __init__(
|
| 129 |
+
self,
|
| 130 |
+
*,
|
| 131 |
+
id: str, # pylint: disable=redefined-builtin
|
| 132 |
+
sequence: str,
|
| 133 |
+
ptms: Sequence[tuple[str, int]],
|
| 134 |
+
description: str | None = None,
|
| 135 |
+
paired_msa: str | None = None,
|
| 136 |
+
unpaired_msa: str | None = None,
|
| 137 |
+
templates: Sequence[Template] | None = None,
|
| 138 |
+
):
|
| 139 |
+
"""Initializes a single protein chain input.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
id: Unique protein chain identifier.
|
| 143 |
+
sequence: The amino acid sequence of the chain.
|
| 144 |
+
ptms: A list of tuples containing the post-translational modification type
|
| 145 |
+
and the (1-based) residue index where the modification is applied.
|
| 146 |
+
description: An optional textual description of the protein chain.
|
| 147 |
+
paired_msa: Paired A3M-formatted MSA for this chain. This MSA is not
|
| 148 |
+
deduplicated and will be used to compute paired features. If None, this
|
| 149 |
+
field is unset and must be filled in by the data pipeline before
|
| 150 |
+
featurisation. If set to an empty string, it will be treated as a custom
|
| 151 |
+
MSA with no sequences.
|
| 152 |
+
unpaired_msa: Unpaired A3M-formatted MSA for this chain. This will be
|
| 153 |
+
deduplicated and used to compute unpaired features. If None, this field
|
| 154 |
+
is unset and must be filled in by the data pipeline before
|
| 155 |
+
featurisation. If set to an empty string, it will be treated as a custom
|
| 156 |
+
MSA with no sequences.
|
| 157 |
+
templates: A list of structural templates for this chain. If None, this
|
| 158 |
+
field is unset and must be filled in by the data pipeline before
|
| 159 |
+
featurisation. The list can be empty or contain up to 20 templates.
|
| 160 |
+
"""
|
| 161 |
+
if not all(res.isalpha() for res in sequence):
|
| 162 |
+
raise ValueError(f'Protein must contain only letters, got "{sequence}"')
|
| 163 |
+
if any(not 0 < mod[1] <= len(sequence) for mod in ptms):
|
| 164 |
+
raise ValueError(f'Invalid protein modification index: {ptms}')
|
| 165 |
+
if any(mod[0].startswith('CCD_') for mod in ptms):
|
| 166 |
+
raise ValueError(
|
| 167 |
+
f'Protein ptms must not contain the "CCD_" prefix, got {ptms}'
|
| 168 |
+
)
|
| 169 |
+
# Use hashable containers for ptms and templates.
|
| 170 |
+
self._id = id
|
| 171 |
+
self._sequence = sequence
|
| 172 |
+
self._ptms = tuple(ptms)
|
| 173 |
+
self._description = description
|
| 174 |
+
self._paired_msa = paired_msa
|
| 175 |
+
self._unpaired_msa = unpaired_msa
|
| 176 |
+
self._templates = tuple(templates) if templates is not None else None
|
| 177 |
+
|
| 178 |
+
@property
|
| 179 |
+
def id(self) -> str:
|
| 180 |
+
return self._id
|
| 181 |
+
|
| 182 |
+
@property
|
| 183 |
+
def sequence(self) -> str:
|
| 184 |
+
"""Returns a single-letter sequence, taking modifications into account.
|
| 185 |
+
|
| 186 |
+
Uses 'X' for all unknown residues.
|
| 187 |
+
"""
|
| 188 |
+
return ''.join([
|
| 189 |
+
residue_names.letters_three_to_one(r, default='X')
|
| 190 |
+
for r in self.to_ccd_sequence()
|
| 191 |
+
])
|
| 192 |
+
|
| 193 |
+
@property
|
| 194 |
+
def ptms(self) -> Sequence[tuple[str, int]]:
|
| 195 |
+
return self._ptms
|
| 196 |
+
|
| 197 |
+
@property
|
| 198 |
+
def description(self) -> str | None:
|
| 199 |
+
return self._description
|
| 200 |
+
|
| 201 |
+
@property
|
| 202 |
+
def paired_msa(self) -> str | None:
|
| 203 |
+
return self._paired_msa
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def unpaired_msa(self) -> str | None:
|
| 207 |
+
return self._unpaired_msa
|
| 208 |
+
|
| 209 |
+
@property
|
| 210 |
+
def templates(self) -> Sequence[Template] | None:
|
| 211 |
+
return self._templates
|
| 212 |
+
|
| 213 |
+
def __len__(self) -> int:
|
| 214 |
+
return len(self._sequence)
|
| 215 |
+
|
| 216 |
+
def __eq__(self, other: Self) -> bool:
|
| 217 |
+
return (
|
| 218 |
+
self._id == other._id
|
| 219 |
+
and self._sequence == other._sequence
|
| 220 |
+
and self._ptms == other._ptms
|
| 221 |
+
and self._description == other._description
|
| 222 |
+
and self._paired_msa == other._paired_msa
|
| 223 |
+
and self._unpaired_msa == other._unpaired_msa
|
| 224 |
+
and self._templates == other._templates
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
def __hash__(self) -> int:
|
| 228 |
+
return hash((
|
| 229 |
+
self._id,
|
| 230 |
+
self._sequence,
|
| 231 |
+
self._ptms,
|
| 232 |
+
self._description,
|
| 233 |
+
self._paired_msa,
|
| 234 |
+
self._unpaired_msa,
|
| 235 |
+
self._templates,
|
| 236 |
+
))
|
| 237 |
+
|
| 238 |
+
def hash_without_id(self) -> int:
|
| 239 |
+
"""Returns a hash ignoring the ID - useful for deduplication."""
|
| 240 |
+
return hash((
|
| 241 |
+
self._sequence,
|
| 242 |
+
self._ptms,
|
| 243 |
+
self._description,
|
| 244 |
+
self._paired_msa,
|
| 245 |
+
self._unpaired_msa,
|
| 246 |
+
self._templates,
|
| 247 |
+
))
|
| 248 |
+
|
| 249 |
+
@classmethod
|
| 250 |
+
def from_alphafoldserver_dict(
|
| 251 |
+
cls, json_dict: Mapping[str, Any], seq_id: str
|
| 252 |
+
) -> Self:
|
| 253 |
+
"""Constructs ProteinChain from the AlphaFoldServer JSON dict."""
|
| 254 |
+
_validate_keys(
|
| 255 |
+
json_dict.keys(),
|
| 256 |
+
{
|
| 257 |
+
'sequence',
|
| 258 |
+
'glycans',
|
| 259 |
+
'modifications',
|
| 260 |
+
'count',
|
| 261 |
+
'maxTemplateDate',
|
| 262 |
+
'useStructureTemplate',
|
| 263 |
+
},
|
| 264 |
+
)
|
| 265 |
+
sequence = json_dict['sequence']
|
| 266 |
+
|
| 267 |
+
if 'glycans' in json_dict:
|
| 268 |
+
raise ValueError(
|
| 269 |
+
f'Specifying glycans in the `{ALPHAFOLDSERVER_JSON_DIALECT}` format'
|
| 270 |
+
' is not supported.'
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
if 'maxTemplateDate' in json_dict:
|
| 274 |
+
raise ValueError(
|
| 275 |
+
f'Specifying maxTemplateDate in the `{ALPHAFOLDSERVER_JSON_DIALECT}`'
|
| 276 |
+
' format is not supported, use the --max_template_date flag instead.'
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
templates = None # Search for templates unless explicitly disabled.
|
| 280 |
+
if not json_dict.get('useStructureTemplate', True):
|
| 281 |
+
templates = [] # Do not use any templates.
|
| 282 |
+
|
| 283 |
+
ptms = [
|
| 284 |
+
(mod['ptmType'].removeprefix('CCD_'), mod['ptmPosition'])
|
| 285 |
+
for mod in json_dict.get('modifications', [])
|
| 286 |
+
]
|
| 287 |
+
return cls(id=seq_id, sequence=sequence, ptms=ptms, templates=templates)
|
| 288 |
+
|
| 289 |
+
@classmethod
|
| 290 |
+
def from_dict(
|
| 291 |
+
cls,
|
| 292 |
+
json_dict: Mapping[str, Any],
|
| 293 |
+
json_path: pathlib.Path | None = None,
|
| 294 |
+
seq_id: str | None = None,
|
| 295 |
+
) -> Self:
|
| 296 |
+
"""Constructs ProteinChain from the AlphaFold JSON dict."""
|
| 297 |
+
json_dict = json_dict['protein']
|
| 298 |
+
_validate_keys(
|
| 299 |
+
json_dict.keys(),
|
| 300 |
+
{
|
| 301 |
+
'id',
|
| 302 |
+
'sequence',
|
| 303 |
+
'modifications',
|
| 304 |
+
'description',
|
| 305 |
+
'unpairedMsa',
|
| 306 |
+
'unpairedMsaPath',
|
| 307 |
+
'pairedMsa',
|
| 308 |
+
'pairedMsaPath',
|
| 309 |
+
'templates',
|
| 310 |
+
},
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
sequence = json_dict['sequence']
|
| 314 |
+
ptms = [
|
| 315 |
+
(mod['ptmType'], mod['ptmPosition'])
|
| 316 |
+
for mod in json_dict.get('modifications', [])
|
| 317 |
+
]
|
| 318 |
+
|
| 319 |
+
unpaired_msa = json_dict.get('unpairedMsa', None)
|
| 320 |
+
unpaired_msa_path = json_dict.get('unpairedMsaPath', None)
|
| 321 |
+
if unpaired_msa and unpaired_msa_path:
|
| 322 |
+
raise ValueError('Only one of unpairedMsa/unpairedMsaPath can be set.')
|
| 323 |
+
if (
|
| 324 |
+
unpaired_msa
|
| 325 |
+
and len(unpaired_msa) < 256
|
| 326 |
+
and os.path.exists(unpaired_msa)
|
| 327 |
+
):
|
| 328 |
+
raise ValueError(
|
| 329 |
+
'Set the unpaired MSA path using the "unpairedMsaPath" field.'
|
| 330 |
+
)
|
| 331 |
+
elif unpaired_msa_path:
|
| 332 |
+
unpaired_msa = _read_file(pathlib.Path(unpaired_msa_path), json_path)
|
| 333 |
+
|
| 334 |
+
paired_msa = json_dict.get('pairedMsa', None)
|
| 335 |
+
paired_msa_path = json_dict.get('pairedMsaPath', None)
|
| 336 |
+
if paired_msa and paired_msa_path:
|
| 337 |
+
raise ValueError('Only one of pairedMsa/pairedMsaPath can be set.')
|
| 338 |
+
if paired_msa and len(paired_msa) < 256 and os.path.exists(paired_msa):
|
| 339 |
+
raise ValueError(
|
| 340 |
+
'Set the paired MSA path using the "pairedMsaPath" field.'
|
| 341 |
+
)
|
| 342 |
+
elif paired_msa_path:
|
| 343 |
+
paired_msa = _read_file(pathlib.Path(paired_msa_path), json_path)
|
| 344 |
+
|
| 345 |
+
raw_templates = json_dict.get('templates', None)
|
| 346 |
+
|
| 347 |
+
if raw_templates is None:
|
| 348 |
+
templates = None
|
| 349 |
+
else:
|
| 350 |
+
templates = []
|
| 351 |
+
for raw_template in raw_templates:
|
| 352 |
+
_validate_keys(
|
| 353 |
+
raw_template.keys(),
|
| 354 |
+
{'mmcif', 'mmcifPath', 'queryIndices', 'templateIndices'},
|
| 355 |
+
)
|
| 356 |
+
mmcif = raw_template.get('mmcif', None)
|
| 357 |
+
mmcif_path = raw_template.get('mmcifPath', None)
|
| 358 |
+
if mmcif and mmcif_path:
|
| 359 |
+
raise ValueError('Only one of mmcif/mmcifPath can be set.')
|
| 360 |
+
if mmcif and len(mmcif) < 256 and os.path.exists(mmcif):
|
| 361 |
+
raise ValueError('Set the template path using the "mmcifPath" field.')
|
| 362 |
+
if mmcif_path:
|
| 363 |
+
mmcif = _read_file(pathlib.Path(mmcif_path), json_path)
|
| 364 |
+
query_to_template_map = dict(
|
| 365 |
+
zip(raw_template['queryIndices'], raw_template['templateIndices'])
|
| 366 |
+
)
|
| 367 |
+
templates.append(
|
| 368 |
+
Template(mmcif=mmcif, query_to_template_map=query_to_template_map)
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
return cls(
|
| 372 |
+
id=seq_id or json_dict['id'],
|
| 373 |
+
sequence=sequence,
|
| 374 |
+
ptms=ptms,
|
| 375 |
+
description=json_dict.get('description', None),
|
| 376 |
+
paired_msa=paired_msa,
|
| 377 |
+
unpaired_msa=unpaired_msa,
|
| 378 |
+
templates=templates,
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
def to_dict(
|
| 382 |
+
self, seq_id: str | Sequence[str] | None = None
|
| 383 |
+
) -> Mapping[str, Mapping[str, Any]]:
|
| 384 |
+
"""Converts ProteinChain to an AlphaFold JSON dict."""
|
| 385 |
+
if self._templates is None:
|
| 386 |
+
templates = None
|
| 387 |
+
else:
|
| 388 |
+
templates = [
|
| 389 |
+
{
|
| 390 |
+
'mmcif': template.mmcif,
|
| 391 |
+
'queryIndices': list(template.query_to_template_map.keys()),
|
| 392 |
+
'templateIndices': (
|
| 393 |
+
list(template.query_to_template_map.values()) or None
|
| 394 |
+
),
|
| 395 |
+
}
|
| 396 |
+
for template in self._templates
|
| 397 |
+
]
|
| 398 |
+
contents = {
|
| 399 |
+
'id': seq_id or self._id,
|
| 400 |
+
'sequence': self._sequence,
|
| 401 |
+
'modifications': [
|
| 402 |
+
{'ptmType': ptm[0], 'ptmPosition': ptm[1]} for ptm in self._ptms
|
| 403 |
+
],
|
| 404 |
+
'unpairedMsa': self._unpaired_msa,
|
| 405 |
+
'pairedMsa': self._paired_msa,
|
| 406 |
+
'templates': templates,
|
| 407 |
+
}
|
| 408 |
+
if self._description is not None:
|
| 409 |
+
contents['description'] = self._description
|
| 410 |
+
return {'protein': contents}
|
| 411 |
+
|
| 412 |
+
def to_ccd_sequence(self) -> Sequence[str]:
|
| 413 |
+
"""Converts to a sequence of CCD codes."""
|
| 414 |
+
ccd_coded_seq = [
|
| 415 |
+
residue_names.PROTEIN_COMMON_ONE_TO_THREE.get(res, residue_names.UNK)
|
| 416 |
+
for res in self._sequence
|
| 417 |
+
]
|
| 418 |
+
for ptm_code, ptm_index in self._ptms:
|
| 419 |
+
ccd_coded_seq[ptm_index - 1] = ptm_code
|
| 420 |
+
return ccd_coded_seq
|
| 421 |
+
|
| 422 |
+
def fill_missing_fields(self) -> Self:
|
| 423 |
+
"""Fill missing MSA and template fields with default values."""
|
| 424 |
+
return ProteinChain(
|
| 425 |
+
id=self.id,
|
| 426 |
+
sequence=self._sequence,
|
| 427 |
+
ptms=self._ptms,
|
| 428 |
+
description=self._description,
|
| 429 |
+
unpaired_msa=self._unpaired_msa or '',
|
| 430 |
+
paired_msa=self._paired_msa or '',
|
| 431 |
+
templates=self._templates or [],
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
class RnaChain:
|
| 436 |
+
"""RNA chain input."""
|
| 437 |
+
|
| 438 |
+
__slots__ = (
|
| 439 |
+
'_id',
|
| 440 |
+
'_sequence',
|
| 441 |
+
'_modifications',
|
| 442 |
+
'_description',
|
| 443 |
+
'_unpaired_msa',
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
def __init__(
|
| 447 |
+
self,
|
| 448 |
+
*,
|
| 449 |
+
id: str, # pylint: disable=redefined-builtin
|
| 450 |
+
sequence: str,
|
| 451 |
+
modifications: Sequence[tuple[str, int]],
|
| 452 |
+
description: str | None = None,
|
| 453 |
+
unpaired_msa: str | None = None,
|
| 454 |
+
):
|
| 455 |
+
"""Initializes a single strand RNA chain input.
|
| 456 |
+
|
| 457 |
+
Args:
|
| 458 |
+
id: Unique RNA chain identifier.
|
| 459 |
+
sequence: The RNA sequence of the chain.
|
| 460 |
+
modifications: A list of tuples containing the modification type and the
|
| 461 |
+
(1-based) residue index where the modification is applied.
|
| 462 |
+
description: An optional textual description of the RNA chain.
|
| 463 |
+
unpaired_msa: Unpaired A3M-formatted MSA for this chain. This will be
|
| 464 |
+
deduplicated and used to compute unpaired features. If None, this field
|
| 465 |
+
is unset and must be filled in by the data pipeline before
|
| 466 |
+
featurisation. If set to an empty string, it will be treated as a custom
|
| 467 |
+
MSA with no sequences.
|
| 468 |
+
"""
|
| 469 |
+
if not all(res.isalpha() for res in sequence):
|
| 470 |
+
raise ValueError(f'RNA must contain only letters, got "{sequence}"')
|
| 471 |
+
if any(not 0 < mod[1] <= len(sequence) for mod in modifications):
|
| 472 |
+
raise ValueError(f'Invalid RNA modification index: {modifications}')
|
| 473 |
+
if any(mod[0].startswith('CCD_') for mod in modifications):
|
| 474 |
+
raise ValueError(
|
| 475 |
+
'RNA modifications must not contain the "CCD_" prefix, got'
|
| 476 |
+
f' {modifications}'
|
| 477 |
+
)
|
| 478 |
+
self._id = id
|
| 479 |
+
self._sequence = sequence
|
| 480 |
+
# Use hashable container for modifications.
|
| 481 |
+
self._modifications = tuple(modifications)
|
| 482 |
+
self._description = description
|
| 483 |
+
self._unpaired_msa = unpaired_msa
|
| 484 |
+
|
| 485 |
+
@property
|
| 486 |
+
def id(self) -> str:
|
| 487 |
+
return self._id
|
| 488 |
+
|
| 489 |
+
@property
|
| 490 |
+
def sequence(self) -> str:
|
| 491 |
+
"""Returns a single-letter sequence, taking modifications into account.
|
| 492 |
+
|
| 493 |
+
Uses 'N' for all unknown residues.
|
| 494 |
+
"""
|
| 495 |
+
return ''.join([
|
| 496 |
+
residue_names.letters_three_to_one(r, default='N')
|
| 497 |
+
for r in self.to_ccd_sequence()
|
| 498 |
+
])
|
| 499 |
+
|
| 500 |
+
@property
|
| 501 |
+
def modifications(self) -> Sequence[tuple[str, int]]:
|
| 502 |
+
return self._modifications
|
| 503 |
+
|
| 504 |
+
@property
|
| 505 |
+
def description(self) -> str | None:
|
| 506 |
+
return self._description
|
| 507 |
+
|
| 508 |
+
@property
|
| 509 |
+
def unpaired_msa(self) -> str | None:
|
| 510 |
+
return self._unpaired_msa
|
| 511 |
+
|
| 512 |
+
def __len__(self) -> int:
|
| 513 |
+
return len(self._sequence)
|
| 514 |
+
|
| 515 |
+
def __eq__(self, other: Self) -> bool:
|
| 516 |
+
return (
|
| 517 |
+
self._id == other._id
|
| 518 |
+
and self._sequence == other._sequence
|
| 519 |
+
and self._modifications == other._modifications
|
| 520 |
+
and self._description == other._description
|
| 521 |
+
and self._unpaired_msa == other._unpaired_msa
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
def __hash__(self) -> int:
|
| 525 |
+
return hash((
|
| 526 |
+
self._id,
|
| 527 |
+
self._sequence,
|
| 528 |
+
self._modifications,
|
| 529 |
+
self._description,
|
| 530 |
+
self._unpaired_msa,
|
| 531 |
+
))
|
| 532 |
+
|
| 533 |
+
def hash_without_id(self) -> int:
|
| 534 |
+
"""Returns a hash ignoring the ID - useful for deduplication."""
|
| 535 |
+
return hash((
|
| 536 |
+
self._sequence,
|
| 537 |
+
self._modifications,
|
| 538 |
+
self._description,
|
| 539 |
+
self._unpaired_msa,
|
| 540 |
+
))
|
| 541 |
+
|
| 542 |
+
@classmethod
|
| 543 |
+
def from_alphafoldserver_dict(
|
| 544 |
+
cls, json_dict: Mapping[str, Any], seq_id: str
|
| 545 |
+
) -> Self:
|
| 546 |
+
"""Constructs RnaChain from the AlphaFoldServer JSON dict."""
|
| 547 |
+
_validate_keys(json_dict.keys(), {'sequence', 'modifications', 'count'})
|
| 548 |
+
sequence = json_dict['sequence']
|
| 549 |
+
modifications = [
|
| 550 |
+
(mod['modificationType'].removeprefix('CCD_'), mod['basePosition'])
|
| 551 |
+
for mod in json_dict.get('modifications', [])
|
| 552 |
+
]
|
| 553 |
+
return cls(id=seq_id, sequence=sequence, modifications=modifications)
|
| 554 |
+
|
| 555 |
+
@classmethod
|
| 556 |
+
def from_dict(
|
| 557 |
+
cls,
|
| 558 |
+
json_dict: Mapping[str, Any],
|
| 559 |
+
json_path: pathlib.Path | None = None,
|
| 560 |
+
seq_id: str | None = None,
|
| 561 |
+
) -> Self:
|
| 562 |
+
"""Constructs RnaChain from the AlphaFold JSON dict."""
|
| 563 |
+
json_dict = json_dict['rna']
|
| 564 |
+
_validate_keys(
|
| 565 |
+
json_dict.keys(),
|
| 566 |
+
{
|
| 567 |
+
'id',
|
| 568 |
+
'sequence',
|
| 569 |
+
'modifications',
|
| 570 |
+
'description',
|
| 571 |
+
'unpairedMsa',
|
| 572 |
+
'unpairedMsaPath',
|
| 573 |
+
},
|
| 574 |
+
)
|
| 575 |
+
sequence = json_dict['sequence']
|
| 576 |
+
modifications = [
|
| 577 |
+
(mod['modificationType'], mod['basePosition'])
|
| 578 |
+
for mod in json_dict.get('modifications', [])
|
| 579 |
+
]
|
| 580 |
+
|
| 581 |
+
unpaired_msa = json_dict.get('unpairedMsa', None)
|
| 582 |
+
unpaired_msa_path = json_dict.get('unpairedMsaPath', None)
|
| 583 |
+
if unpaired_msa and unpaired_msa_path:
|
| 584 |
+
raise ValueError('Only one of unpairedMsa/unpairedMsaPath can be set.')
|
| 585 |
+
if (
|
| 586 |
+
unpaired_msa
|
| 587 |
+
and len(unpaired_msa) < 256
|
| 588 |
+
and os.path.exists(unpaired_msa)
|
| 589 |
+
):
|
| 590 |
+
raise ValueError(
|
| 591 |
+
'Set the unpaired MSA path using the "unpairedMsaPath" field.'
|
| 592 |
+
)
|
| 593 |
+
elif unpaired_msa_path:
|
| 594 |
+
unpaired_msa = _read_file(pathlib.Path(unpaired_msa_path), json_path)
|
| 595 |
+
|
| 596 |
+
return cls(
|
| 597 |
+
id=seq_id or json_dict['id'],
|
| 598 |
+
sequence=sequence,
|
| 599 |
+
modifications=modifications,
|
| 600 |
+
description=json_dict.get('description', None),
|
| 601 |
+
unpaired_msa=unpaired_msa,
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
def to_dict(
|
| 605 |
+
self, seq_id: str | Sequence[str] | None = None
|
| 606 |
+
) -> Mapping[str, Mapping[str, Any]]:
|
| 607 |
+
"""Converts RnaChain to an AlphaFold JSON dict."""
|
| 608 |
+
contents = {
|
| 609 |
+
'id': seq_id or self._id,
|
| 610 |
+
'sequence': self._sequence,
|
| 611 |
+
'modifications': [
|
| 612 |
+
{'modificationType': mod[0], 'basePosition': mod[1]}
|
| 613 |
+
for mod in self._modifications
|
| 614 |
+
],
|
| 615 |
+
'unpairedMsa': self._unpaired_msa,
|
| 616 |
+
}
|
| 617 |
+
if self._description is not None:
|
| 618 |
+
contents['description'] = self._description
|
| 619 |
+
return {'rna': contents}
|
| 620 |
+
|
| 621 |
+
def to_ccd_sequence(self) -> Sequence[str]:
|
| 622 |
+
"""Converts to a sequence of CCD codes."""
|
| 623 |
+
mapping = {r: r for r in residue_names.RNA_TYPES} # Same 1-letter and CCD.
|
| 624 |
+
ccd_coded_seq = [
|
| 625 |
+
mapping.get(res, residue_names.UNK_RNA) for res in self._sequence
|
| 626 |
+
]
|
| 627 |
+
for ccd_code, modification_index in self._modifications:
|
| 628 |
+
ccd_coded_seq[modification_index - 1] = ccd_code
|
| 629 |
+
return ccd_coded_seq
|
| 630 |
+
|
| 631 |
+
def fill_missing_fields(self) -> Self:
|
| 632 |
+
"""Fill missing MSA fields with default values."""
|
| 633 |
+
return RnaChain(
|
| 634 |
+
id=self.id,
|
| 635 |
+
sequence=self.sequence,
|
| 636 |
+
modifications=self.modifications,
|
| 637 |
+
unpaired_msa=self._unpaired_msa or '',
|
| 638 |
+
)
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
class DnaChain:
|
| 642 |
+
"""Single strand DNA chain input."""
|
| 643 |
+
|
| 644 |
+
__slots__ = ('_id', '_sequence', '_modifications', '_description')
|
| 645 |
+
|
| 646 |
+
def __init__(
|
| 647 |
+
self,
|
| 648 |
+
*,
|
| 649 |
+
id: str, # pylint: disable=redefined-builtin
|
| 650 |
+
sequence: str,
|
| 651 |
+
modifications: Sequence[tuple[str, int]],
|
| 652 |
+
description: str | None = None,
|
| 653 |
+
):
|
| 654 |
+
"""Initializes a single strand DNA chain input.
|
| 655 |
+
|
| 656 |
+
Args:
|
| 657 |
+
id: Unique DNA chain identifier.
|
| 658 |
+
sequence: The DNA sequence of the chain.
|
| 659 |
+
modifications: A list of tuples containing the modification type and the
|
| 660 |
+
(1-based) residue index where the modification is applied.
|
| 661 |
+
description: An optional textual description of the DNA chain.
|
| 662 |
+
"""
|
| 663 |
+
if not all(res.isalpha() for res in sequence):
|
| 664 |
+
raise ValueError(f'DNA must contain only letters, got "{sequence}"')
|
| 665 |
+
if any(not 0 < mod[1] <= len(sequence) for mod in modifications):
|
| 666 |
+
raise ValueError(f'Invalid DNA modification index: {modifications}')
|
| 667 |
+
if any(mod[0].startswith('CCD_') for mod in modifications):
|
| 668 |
+
raise ValueError(
|
| 669 |
+
'DNA modifications must not contain the "CCD_" prefix, got'
|
| 670 |
+
f' {modifications}'
|
| 671 |
+
)
|
| 672 |
+
self._id = id
|
| 673 |
+
self._sequence = sequence
|
| 674 |
+
# Use hashable container for modifications.
|
| 675 |
+
self._modifications = tuple(modifications)
|
| 676 |
+
self._description = description
|
| 677 |
+
|
| 678 |
+
@property
|
| 679 |
+
def id(self) -> str:
|
| 680 |
+
return self._id
|
| 681 |
+
|
| 682 |
+
@property
|
| 683 |
+
def sequence(self) -> str:
|
| 684 |
+
"""Returns a single-letter sequence, taking modifications into account.
|
| 685 |
+
|
| 686 |
+
Uses 'N' for all unknown residues.
|
| 687 |
+
"""
|
| 688 |
+
return ''.join([
|
| 689 |
+
residue_names.letters_three_to_one(r, default='N')
|
| 690 |
+
for r in self.to_ccd_sequence()
|
| 691 |
+
])
|
| 692 |
+
|
| 693 |
+
@property
|
| 694 |
+
def description(self) -> str | None:
|
| 695 |
+
return self._description
|
| 696 |
+
|
| 697 |
+
def __len__(self) -> int:
|
| 698 |
+
return len(self._sequence)
|
| 699 |
+
|
| 700 |
+
def __eq__(self, other: Self) -> bool:
|
| 701 |
+
return (
|
| 702 |
+
self._id == other._id
|
| 703 |
+
and self._sequence == other._sequence
|
| 704 |
+
and self._modifications == other._modifications
|
| 705 |
+
and self._description == other._description
|
| 706 |
+
)
|
| 707 |
+
|
| 708 |
+
def __hash__(self) -> int:
|
| 709 |
+
return hash(
|
| 710 |
+
(self._id, self._sequence, self._modifications, self._description)
|
| 711 |
+
)
|
| 712 |
+
|
| 713 |
+
def modifications(self) -> Sequence[tuple[str, int]]:
|
| 714 |
+
return self._modifications
|
| 715 |
+
|
| 716 |
+
def hash_without_id(self) -> int:
|
| 717 |
+
"""Returns a hash ignoring the ID - useful for deduplication."""
|
| 718 |
+
return hash((self._sequence, self._modifications, self._description))
|
| 719 |
+
|
| 720 |
+
@classmethod
|
| 721 |
+
def from_alphafoldserver_dict(
|
| 722 |
+
cls, json_dict: Mapping[str, Any], seq_id: str
|
| 723 |
+
) -> Self:
|
| 724 |
+
"""Constructs DnaChain from the AlphaFoldServer JSON dict."""
|
| 725 |
+
_validate_keys(json_dict.keys(), {'sequence', 'modifications', 'count'})
|
| 726 |
+
sequence = json_dict['sequence']
|
| 727 |
+
modifications = [
|
| 728 |
+
(mod['modificationType'].removeprefix('CCD_'), mod['basePosition'])
|
| 729 |
+
for mod in json_dict.get('modifications', [])
|
| 730 |
+
]
|
| 731 |
+
return cls(id=seq_id, sequence=sequence, modifications=modifications)
|
| 732 |
+
|
| 733 |
+
@classmethod
|
| 734 |
+
def from_dict(
|
| 735 |
+
cls, json_dict: Mapping[str, Any], seq_id: str | None = None
|
| 736 |
+
) -> Self:
|
| 737 |
+
"""Constructs DnaChain from the AlphaFold JSON dict."""
|
| 738 |
+
json_dict = json_dict['dna']
|
| 739 |
+
_validate_keys(
|
| 740 |
+
json_dict.keys(), {'id', 'sequence', 'modifications', 'description'}
|
| 741 |
+
)
|
| 742 |
+
sequence = json_dict['sequence']
|
| 743 |
+
modifications = [
|
| 744 |
+
(mod['modificationType'], mod['basePosition'])
|
| 745 |
+
for mod in json_dict.get('modifications', [])
|
| 746 |
+
]
|
| 747 |
+
return cls(
|
| 748 |
+
id=seq_id or json_dict['id'],
|
| 749 |
+
sequence=sequence,
|
| 750 |
+
modifications=modifications,
|
| 751 |
+
description=json_dict.get('description', None),
|
| 752 |
+
)
|
| 753 |
+
|
| 754 |
+
def to_dict(
|
| 755 |
+
self, seq_id: str | Sequence[str] | None = None
|
| 756 |
+
) -> Mapping[str, Mapping[str, Any]]:
|
| 757 |
+
"""Converts DnaChain to an AlphaFold JSON dict."""
|
| 758 |
+
contents = {
|
| 759 |
+
'id': seq_id or self._id,
|
| 760 |
+
'sequence': self._sequence,
|
| 761 |
+
'modifications': [
|
| 762 |
+
{'modificationType': mod[0], 'basePosition': mod[1]}
|
| 763 |
+
for mod in self._modifications
|
| 764 |
+
],
|
| 765 |
+
}
|
| 766 |
+
if self._description is not None:
|
| 767 |
+
contents['description'] = self._description
|
| 768 |
+
return {'dna': contents}
|
| 769 |
+
|
| 770 |
+
def to_ccd_sequence(self) -> Sequence[str]:
|
| 771 |
+
"""Converts to a sequence of CCD codes."""
|
| 772 |
+
ccd_coded_seq = [
|
| 773 |
+
residue_names.DNA_COMMON_ONE_TO_TWO.get(res, residue_names.UNK_DNA)
|
| 774 |
+
for res in self._sequence
|
| 775 |
+
]
|
| 776 |
+
for ccd_code, modification_index in self._modifications:
|
| 777 |
+
ccd_coded_seq[modification_index - 1] = ccd_code
|
| 778 |
+
return ccd_coded_seq
|
| 779 |
+
|
| 780 |
+
|
| 781 |
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
| 782 |
+
class Ligand:
|
| 783 |
+
"""Ligand input.
|
| 784 |
+
|
| 785 |
+
Attributes:
|
| 786 |
+
id: Unique ligand "chain" identifier.
|
| 787 |
+
ccd_ids: The Chemical Component Dictionary or user-defined CCD IDs of the
|
| 788 |
+
chemical components of the ligand. Typically, this is just a single ID,
|
| 789 |
+
but some ligands are composed of multiple components. If that is the case,
|
| 790 |
+
a bond linking these components should be added to the bonded_atom_pairs
|
| 791 |
+
Input field.
|
| 792 |
+
smiles: The SMILES representation of the ligand.
|
| 793 |
+
description: An optional textual description of the ligand.
|
| 794 |
+
"""
|
| 795 |
+
|
| 796 |
+
id: str
|
| 797 |
+
ccd_ids: Sequence[str] | None = None
|
| 798 |
+
smiles: str | None = None
|
| 799 |
+
description: str | None = None
|
| 800 |
+
|
| 801 |
+
def __post_init__(self):
|
| 802 |
+
if (self.ccd_ids is None) == (self.smiles is None):
|
| 803 |
+
raise ValueError('Ligand must have one of CCD ID or SMILES set.')
|
| 804 |
+
|
| 805 |
+
if self.smiles is not None:
|
| 806 |
+
mol = rd_chem.MolFromSmiles(self.smiles)
|
| 807 |
+
if not mol:
|
| 808 |
+
raise ValueError(f'Unable to make RDKit Mol from SMILES: {self.smiles}')
|
| 809 |
+
|
| 810 |
+
# Use hashable types for ccd_ids.
|
| 811 |
+
if self.ccd_ids is not None:
|
| 812 |
+
object.__setattr__(self, 'ccd_ids', tuple(self.ccd_ids))
|
| 813 |
+
|
| 814 |
+
def __len__(self) -> int:
|
| 815 |
+
if self.ccd_ids is not None:
|
| 816 |
+
return len(self.ccd_ids)
|
| 817 |
+
else:
|
| 818 |
+
return 1
|
| 819 |
+
|
| 820 |
+
def hash_without_id(self) -> int:
|
| 821 |
+
"""Returns a hash ignoring the ID - useful for deduplication."""
|
| 822 |
+
return hash((self.ccd_ids, self.smiles, self.description))
|
| 823 |
+
|
| 824 |
+
@classmethod
|
| 825 |
+
def from_alphafoldserver_dict(
|
| 826 |
+
cls, json_dict: Mapping[str, Any], seq_id: str
|
| 827 |
+
) -> Self:
|
| 828 |
+
"""Constructs Ligand from the AlphaFoldServer JSON dict."""
|
| 829 |
+
# Ligand can be specified either as a ligand, or ion (special-case).
|
| 830 |
+
_validate_keys(json_dict.keys(), {'ligand', 'ion', 'count'})
|
| 831 |
+
if 'ligand' in json_dict:
|
| 832 |
+
return cls(id=seq_id, ccd_ids=[json_dict['ligand'].removeprefix('CCD_')])
|
| 833 |
+
elif 'ion' in json_dict:
|
| 834 |
+
return cls(id=seq_id, ccd_ids=[json_dict['ion']])
|
| 835 |
+
else:
|
| 836 |
+
raise ValueError(f'Unknown ligand type: {json_dict}')
|
| 837 |
+
|
| 838 |
+
@classmethod
|
| 839 |
+
def from_dict(
|
| 840 |
+
cls, json_dict: Mapping[str, Any], seq_id: str | None = None
|
| 841 |
+
) -> Self:
|
| 842 |
+
"""Constructs Ligand from the AlphaFold JSON dict."""
|
| 843 |
+
json_dict = json_dict['ligand']
|
| 844 |
+
_validate_keys(
|
| 845 |
+
json_dict.keys(), {'id', 'ccdCodes', 'smiles', 'description'}
|
| 846 |
+
)
|
| 847 |
+
if json_dict.get('ccdCodes') and json_dict.get('smiles'):
|
| 848 |
+
raise ValueError(
|
| 849 |
+
'Ligand cannot have both CCD code and SMILES set at the same time, '
|
| 850 |
+
f'got CCD: {json_dict["ccdCodes"]} and SMILES: {json_dict["smiles"]}'
|
| 851 |
+
)
|
| 852 |
+
|
| 853 |
+
if 'ccdCodes' in json_dict:
|
| 854 |
+
ccd_codes = json_dict['ccdCodes']
|
| 855 |
+
if not isinstance(ccd_codes, (list, tuple)):
|
| 856 |
+
raise ValueError(
|
| 857 |
+
'CCD codes must be a list of strings, got '
|
| 858 |
+
f'{type(ccd_codes).__name__} instead: {ccd_codes}'
|
| 859 |
+
)
|
| 860 |
+
return cls(
|
| 861 |
+
id=seq_id or json_dict['id'],
|
| 862 |
+
ccd_ids=ccd_codes,
|
| 863 |
+
description=json_dict.get('description', None),
|
| 864 |
+
)
|
| 865 |
+
elif 'smiles' in json_dict:
|
| 866 |
+
return cls(
|
| 867 |
+
id=seq_id or json_dict['id'],
|
| 868 |
+
smiles=json_dict['smiles'],
|
| 869 |
+
description=json_dict.get('description', None),
|
| 870 |
+
)
|
| 871 |
+
else:
|
| 872 |
+
raise ValueError(f'Unknown ligand type: {json_dict}')
|
| 873 |
+
|
| 874 |
+
def to_dict(
|
| 875 |
+
self, seq_id: str | Sequence[str] | None = None
|
| 876 |
+
) -> Mapping[str, Mapping[str, Any]]:
|
| 877 |
+
"""Converts Ligand to an AlphaFold JSON dict."""
|
| 878 |
+
contents = {'id': seq_id or self.id}
|
| 879 |
+
if self.ccd_ids is not None:
|
| 880 |
+
contents['ccdCodes'] = self.ccd_ids
|
| 881 |
+
if self.smiles is not None:
|
| 882 |
+
contents['smiles'] = self.smiles
|
| 883 |
+
if self.description is not None:
|
| 884 |
+
contents['description'] = self.description
|
| 885 |
+
return {'ligand': contents}
|
| 886 |
+
|
| 887 |
+
|
| 888 |
+
def _sample_rng_seed() -> int:
|
| 889 |
+
"""Sample a random seed for AlphaFoldServer job."""
|
| 890 |
+
# See https://alphafoldserver.com/faq#what-are-seeds-and-how-are-they-set.
|
| 891 |
+
return random.randint(0, 2**32 - 1)
|
| 892 |
+
|
| 893 |
+
|
| 894 |
+
def _validate_user_ccd_keys(keys: Sequence[str], component_name: str) -> None:
|
| 895 |
+
"""Validates the keys of the user-defined CCD dictionary."""
|
| 896 |
+
mandatory_keys = (
|
| 897 |
+
'_chem_comp.id',
|
| 898 |
+
'_chem_comp.name',
|
| 899 |
+
'_chem_comp.type',
|
| 900 |
+
'_chem_comp.formula',
|
| 901 |
+
'_chem_comp.mon_nstd_parent_comp_id',
|
| 902 |
+
'_chem_comp.pdbx_synonyms',
|
| 903 |
+
'_chem_comp.formula_weight',
|
| 904 |
+
'_chem_comp_atom.comp_id',
|
| 905 |
+
'_chem_comp_atom.atom_id',
|
| 906 |
+
'_chem_comp_atom.type_symbol',
|
| 907 |
+
'_chem_comp_atom.charge',
|
| 908 |
+
'_chem_comp_atom.pdbx_model_Cartn_x_ideal',
|
| 909 |
+
'_chem_comp_atom.pdbx_model_Cartn_y_ideal',
|
| 910 |
+
'_chem_comp_atom.pdbx_model_Cartn_z_ideal',
|
| 911 |
+
'_chem_comp_bond.atom_id_1',
|
| 912 |
+
'_chem_comp_bond.atom_id_2',
|
| 913 |
+
'_chem_comp_bond.value_order',
|
| 914 |
+
'_chem_comp_bond.pdbx_aromatic_flag',
|
| 915 |
+
)
|
| 916 |
+
if missing_keys := set(mandatory_keys) - set(keys):
|
| 917 |
+
raise ValueError(
|
| 918 |
+
f'Component {component_name} in the user-defined CCD is missing these'
|
| 919 |
+
f' keys: {missing_keys}'
|
| 920 |
+
)
|
| 921 |
+
|
| 922 |
+
|
| 923 |
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
| 924 |
+
class Input:
|
| 925 |
+
"""AlphaFold input.
|
| 926 |
+
|
| 927 |
+
Attributes:
|
| 928 |
+
name: The name of the target.
|
| 929 |
+
chains: Protein chains, RNA chains, DNA chains, or ligands.
|
| 930 |
+
protein_chains: Protein chains.
|
| 931 |
+
rna_chains: RNA chains.
|
| 932 |
+
dna_chains: Single strand DNA chains.
|
| 933 |
+
ligands: Ligand (including ion) inputs.
|
| 934 |
+
rng_seeds: Random number generator seeds, one for each model execution.
|
| 935 |
+
bonded_atom_pairs: A list of tuples of atoms that are bonded to each other.
|
| 936 |
+
Each atom is defined by a tuple of (chain_id, res_id, atom_name). Chain
|
| 937 |
+
IDs must be set if there are any bonded atoms. Residue IDs are 1-indexed.
|
| 938 |
+
Atoms in ligands defined by SMILES can't be bonded since SMILES doesn't
|
| 939 |
+
define unique atom names.
|
| 940 |
+
user_ccd: Optional user-defined chemical component dictionary in the CIF
|
| 941 |
+
format. This can be used to provide additional CCD entries that are not
|
| 942 |
+
present in the default CCD and thus define arbitrary new ligands. This is
|
| 943 |
+
more expressive than SMILES since it allows to name all atoms within the
|
| 944 |
+
ligand which in turn makes it possible to define bonds using those atoms.
|
| 945 |
+
"""
|
| 946 |
+
|
| 947 |
+
name: str
|
| 948 |
+
chains: Sequence[ProteinChain | RnaChain | DnaChain | Ligand]
|
| 949 |
+
rng_seeds: Sequence[int]
|
| 950 |
+
bonded_atom_pairs: Sequence[tuple[BondAtomId, BondAtomId]] | None = None
|
| 951 |
+
user_ccd: str | None = None
|
| 952 |
+
|
| 953 |
+
def __post_init__(self):
|
| 954 |
+
if not self.rng_seeds:
|
| 955 |
+
raise ValueError('Input must have at least one RNG seed.')
|
| 956 |
+
|
| 957 |
+
if not self.name.strip() or not self.sanitised_name():
|
| 958 |
+
raise ValueError(
|
| 959 |
+
'Input name must be non-empty and contain at least one valid'
|
| 960 |
+
' character (letters, numbers, dots, dashes, underscores).'
|
| 961 |
+
)
|
| 962 |
+
|
| 963 |
+
chain_ids = [c.id for c in self.chains]
|
| 964 |
+
if any(not c.id.isalpha() or c.id.islower() for c in self.chains):
|
| 965 |
+
raise ValueError(f'IDs must be upper case letters, got: {chain_ids}')
|
| 966 |
+
if len(set(chain_ids)) != len(chain_ids):
|
| 967 |
+
raise ValueError('Input JSON contains sequences with duplicate IDs.')
|
| 968 |
+
|
| 969 |
+
# Use hashable types for chains, rng_seeds, and bonded_atom_pairs.
|
| 970 |
+
object.__setattr__(self, 'chains', tuple(self.chains))
|
| 971 |
+
object.__setattr__(self, 'rng_seeds', tuple(self.rng_seeds))
|
| 972 |
+
if self.bonded_atom_pairs is not None:
|
| 973 |
+
object.__setattr__(
|
| 974 |
+
self, 'bonded_atom_pairs', tuple(self.bonded_atom_pairs)
|
| 975 |
+
)
|
| 976 |
+
|
| 977 |
+
if self.user_ccd is not None:
|
| 978 |
+
for component_name, component_cif in cif_dict.parse_multi_data_cif(
|
| 979 |
+
self.user_ccd
|
| 980 |
+
).items():
|
| 981 |
+
_validate_user_ccd_keys(component_cif.keys(), component_name)
|
| 982 |
+
|
| 983 |
+
@property
|
| 984 |
+
def protein_chains(self) -> Sequence[ProteinChain]:
|
| 985 |
+
return [chain for chain in self.chains if isinstance(chain, ProteinChain)]
|
| 986 |
+
|
| 987 |
+
@property
|
| 988 |
+
def rna_chains(self) -> Sequence[RnaChain]:
|
| 989 |
+
return [chain for chain in self.chains if isinstance(chain, RnaChain)]
|
| 990 |
+
|
| 991 |
+
@property
|
| 992 |
+
def dna_chains(self) -> Sequence[DnaChain]:
|
| 993 |
+
return [chain for chain in self.chains if isinstance(chain, DnaChain)]
|
| 994 |
+
|
| 995 |
+
@property
|
| 996 |
+
def ligands(self) -> Sequence[Ligand]:
|
| 997 |
+
return [chain for chain in self.chains if isinstance(chain, Ligand)]
|
| 998 |
+
|
| 999 |
+
def sanitised_name(self) -> str:
|
| 1000 |
+
"""Returns sanitised version of the name that can be used as a filename."""
|
| 1001 |
+
spaceless_name = self.name.replace(' ', '_')
|
| 1002 |
+
allowed_chars = set(string.ascii_letters + string.digits + '_-.')
|
| 1003 |
+
return ''.join(l for l in spaceless_name if l in allowed_chars)
|
| 1004 |
+
|
| 1005 |
+
@classmethod
|
| 1006 |
+
def from_alphafoldserver_fold_job(cls, fold_job: Mapping[str, Any]) -> Self:
|
| 1007 |
+
"""Constructs Input from an AlphaFoldServer fold job."""
|
| 1008 |
+
|
| 1009 |
+
# Validate the fold job has the correct format.
|
| 1010 |
+
_validate_keys(
|
| 1011 |
+
fold_job.keys(),
|
| 1012 |
+
{'name', 'modelSeeds', 'sequences', 'dialect', 'version'},
|
| 1013 |
+
)
|
| 1014 |
+
if 'dialect' not in fold_job and 'version' not in fold_job:
|
| 1015 |
+
dialect = ALPHAFOLDSERVER_JSON_DIALECT
|
| 1016 |
+
version = ALPHAFOLDSERVER_JSON_VERSION
|
| 1017 |
+
elif 'dialect' in fold_job and 'version' in fold_job:
|
| 1018 |
+
dialect = fold_job['dialect']
|
| 1019 |
+
version = fold_job['version']
|
| 1020 |
+
else:
|
| 1021 |
+
raise ValueError(
|
| 1022 |
+
'AlphaFold Server input JSON must either contain both `dialect` and'
|
| 1023 |
+
' `version` fields, or neither. If neither is specified, it is'
|
| 1024 |
+
f' assumed that `dialect="{ALPHAFOLDSERVER_JSON_DIALECT}"` and'
|
| 1025 |
+
f' `version="{ALPHAFOLDSERVER_JSON_VERSION}"`.'
|
| 1026 |
+
)
|
| 1027 |
+
|
| 1028 |
+
if dialect != ALPHAFOLDSERVER_JSON_DIALECT:
|
| 1029 |
+
raise ValueError(
|
| 1030 |
+
f'AlphaFold Server input JSON has unsupported dialect: {dialect}, '
|
| 1031 |
+
f'expected {ALPHAFOLDSERVER_JSON_DIALECT}.'
|
| 1032 |
+
)
|
| 1033 |
+
|
| 1034 |
+
# For now, there is only one AlphaFold Server JSON version.
|
| 1035 |
+
if version != ALPHAFOLDSERVER_JSON_VERSION:
|
| 1036 |
+
raise ValueError(
|
| 1037 |
+
f'AlphaFold Server input JSON has unsupported version: {version}, '
|
| 1038 |
+
f'expected {ALPHAFOLDSERVER_JSON_VERSION}.'
|
| 1039 |
+
)
|
| 1040 |
+
|
| 1041 |
+
# Parse the chains.
|
| 1042 |
+
chains = []
|
| 1043 |
+
for sequence in fold_job['sequences']:
|
| 1044 |
+
if 'proteinChain' in sequence:
|
| 1045 |
+
for _ in range(sequence['proteinChain'].get('count', 1)):
|
| 1046 |
+
chains.append(
|
| 1047 |
+
ProteinChain.from_alphafoldserver_dict(
|
| 1048 |
+
sequence['proteinChain'],
|
| 1049 |
+
seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1),
|
| 1050 |
+
)
|
| 1051 |
+
)
|
| 1052 |
+
elif 'rnaSequence' in sequence:
|
| 1053 |
+
for _ in range(sequence['rnaSequence'].get('count', 1)):
|
| 1054 |
+
chains.append(
|
| 1055 |
+
RnaChain.from_alphafoldserver_dict(
|
| 1056 |
+
sequence['rnaSequence'],
|
| 1057 |
+
seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1),
|
| 1058 |
+
)
|
| 1059 |
+
)
|
| 1060 |
+
elif 'dnaSequence' in sequence:
|
| 1061 |
+
for _ in range(sequence['dnaSequence'].get('count', 1)):
|
| 1062 |
+
chains.append(
|
| 1063 |
+
DnaChain.from_alphafoldserver_dict(
|
| 1064 |
+
sequence['dnaSequence'],
|
| 1065 |
+
seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1),
|
| 1066 |
+
)
|
| 1067 |
+
)
|
| 1068 |
+
elif 'ion' in sequence:
|
| 1069 |
+
for _ in range(sequence['ion'].get('count', 1)):
|
| 1070 |
+
chains.append(
|
| 1071 |
+
Ligand.from_alphafoldserver_dict(
|
| 1072 |
+
sequence['ion'],
|
| 1073 |
+
seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1),
|
| 1074 |
+
)
|
| 1075 |
+
)
|
| 1076 |
+
elif 'ligand' in sequence:
|
| 1077 |
+
for _ in range(sequence['ligand'].get('count', 1)):
|
| 1078 |
+
chains.append(
|
| 1079 |
+
Ligand.from_alphafoldserver_dict(
|
| 1080 |
+
sequence['ligand'],
|
| 1081 |
+
seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1),
|
| 1082 |
+
)
|
| 1083 |
+
)
|
| 1084 |
+
else:
|
| 1085 |
+
raise ValueError(f'Unknown sequence type: {sequence}')
|
| 1086 |
+
|
| 1087 |
+
if 'modelSeeds' in fold_job and fold_job['modelSeeds']:
|
| 1088 |
+
rng_seeds = [int(seed) for seed in fold_job['modelSeeds']]
|
| 1089 |
+
else:
|
| 1090 |
+
rng_seeds = [_sample_rng_seed()]
|
| 1091 |
+
|
| 1092 |
+
return cls(name=fold_job['name'], chains=chains, rng_seeds=rng_seeds)
|
| 1093 |
+
|
| 1094 |
+
@classmethod
|
| 1095 |
+
def from_json(
|
| 1096 |
+
cls, json_str: str, json_path: pathlib.Path | None = None
|
| 1097 |
+
) -> Self:
|
| 1098 |
+
"""Loads the input from the AlphaFold JSON string."""
|
| 1099 |
+
raw_json = json.loads(json_str)
|
| 1100 |
+
|
| 1101 |
+
_validate_keys(
|
| 1102 |
+
raw_json.keys(),
|
| 1103 |
+
{
|
| 1104 |
+
'dialect',
|
| 1105 |
+
'version',
|
| 1106 |
+
'name',
|
| 1107 |
+
'modelSeeds',
|
| 1108 |
+
'sequences',
|
| 1109 |
+
'bondedAtomPairs',
|
| 1110 |
+
'userCCD',
|
| 1111 |
+
'userCCDPath',
|
| 1112 |
+
},
|
| 1113 |
+
)
|
| 1114 |
+
|
| 1115 |
+
if 'dialect' not in raw_json or 'version' not in raw_json:
|
| 1116 |
+
raise ValueError(
|
| 1117 |
+
'AlphaFold 3 input JSON must contain `dialect` and `version` fields.'
|
| 1118 |
+
)
|
| 1119 |
+
|
| 1120 |
+
if raw_json['dialect'] != JSON_DIALECT:
|
| 1121 |
+
raise ValueError(
|
| 1122 |
+
'AlphaFold 3 input JSON has unsupported dialect:'
|
| 1123 |
+
f' {raw_json["dialect"]}, expected {JSON_DIALECT}.'
|
| 1124 |
+
)
|
| 1125 |
+
|
| 1126 |
+
if raw_json['version'] not in JSON_VERSIONS:
|
| 1127 |
+
raise ValueError(
|
| 1128 |
+
'AlphaFold 3 input JSON has unsupported version:'
|
| 1129 |
+
f' {raw_json["version"]}, expected one of {JSON_VERSIONS}.'
|
| 1130 |
+
)
|
| 1131 |
+
|
| 1132 |
+
if 'sequences' not in raw_json:
|
| 1133 |
+
raise ValueError('AlphaFold 3 input JSON does not contain any sequences.')
|
| 1134 |
+
|
| 1135 |
+
if 'modelSeeds' not in raw_json or not raw_json['modelSeeds']:
|
| 1136 |
+
raise ValueError(
|
| 1137 |
+
'AlphaFold 3 input JSON must specify at least one rng seed in'
|
| 1138 |
+
' `modelSeeds`.'
|
| 1139 |
+
)
|
| 1140 |
+
|
| 1141 |
+
sequences = raw_json['sequences']
|
| 1142 |
+
|
| 1143 |
+
# Make sure sequence IDs are all set.
|
| 1144 |
+
raw_sequence_ids = [next(iter(s.values())).get('id') for s in sequences]
|
| 1145 |
+
if all(raw_sequence_ids):
|
| 1146 |
+
sequence_ids = []
|
| 1147 |
+
for sequence_id in raw_sequence_ids:
|
| 1148 |
+
if isinstance(sequence_id, list):
|
| 1149 |
+
sequence_ids.append(sequence_id)
|
| 1150 |
+
else:
|
| 1151 |
+
sequence_ids.append([sequence_id])
|
| 1152 |
+
else:
|
| 1153 |
+
raise ValueError(
|
| 1154 |
+
'AlphaFold 3 input JSON contains sequences with unset IDs.'
|
| 1155 |
+
)
|
| 1156 |
+
|
| 1157 |
+
flat_seq_ids = []
|
| 1158 |
+
for seq_ids in sequence_ids:
|
| 1159 |
+
flat_seq_ids.extend(seq_ids)
|
| 1160 |
+
|
| 1161 |
+
chains = []
|
| 1162 |
+
for seq_ids, sequence in zip(sequence_ids, sequences, strict=True):
|
| 1163 |
+
if len(sequence) != 1:
|
| 1164 |
+
raise ValueError(f'Chain {seq_ids} has more than 1 sequence.')
|
| 1165 |
+
for seq_id in seq_ids:
|
| 1166 |
+
if 'protein' in sequence:
|
| 1167 |
+
chains.append(ProteinChain.from_dict(sequence, json_path, seq_id))
|
| 1168 |
+
elif 'rna' in sequence:
|
| 1169 |
+
chains.append(RnaChain.from_dict(sequence, json_path, seq_id))
|
| 1170 |
+
elif 'dna' in sequence:
|
| 1171 |
+
chains.append(DnaChain.from_dict(sequence, seq_id=seq_id))
|
| 1172 |
+
elif 'ligand' in sequence:
|
| 1173 |
+
chains.append(Ligand.from_dict(sequence, seq_id=seq_id))
|
| 1174 |
+
else:
|
| 1175 |
+
raise ValueError(f'Unknown sequence type: {sequence}')
|
| 1176 |
+
|
| 1177 |
+
smiles_ligand_ids = set(
|
| 1178 |
+
c.id for c in chains if isinstance(c, Ligand) and c.smiles is not None
|
| 1179 |
+
)
|
| 1180 |
+
chain_lengths = {chain.id: len(chain) for chain in chains}
|
| 1181 |
+
bonded_atom_pairs = None
|
| 1182 |
+
if bonds := raw_json.get('bondedAtomPairs'):
|
| 1183 |
+
bonded_atom_pairs = []
|
| 1184 |
+
for bond in bonds:
|
| 1185 |
+
if len(bond) != 2:
|
| 1186 |
+
raise ValueError(f'Bond {bond} must have 2 atoms, got {len(bond)}.')
|
| 1187 |
+
bond_beg, bond_end = bond
|
| 1188 |
+
if (
|
| 1189 |
+
len(bond_beg) != 3
|
| 1190 |
+
or not isinstance(bond_beg[0], str)
|
| 1191 |
+
or not isinstance(bond_beg[1], int)
|
| 1192 |
+
or not isinstance(bond_beg[2], str)
|
| 1193 |
+
):
|
| 1194 |
+
raise ValueError(
|
| 1195 |
+
f'Atom {bond_beg} in bond {bond} must have 3 components: '
|
| 1196 |
+
'(chain_id: str, res_id: int, atom_name: str).'
|
| 1197 |
+
)
|
| 1198 |
+
if (
|
| 1199 |
+
len(bond_end) != 3
|
| 1200 |
+
or not isinstance(bond_end[0], str)
|
| 1201 |
+
or not isinstance(bond_end[1], int)
|
| 1202 |
+
or not isinstance(bond_end[2], str)
|
| 1203 |
+
):
|
| 1204 |
+
raise ValueError(
|
| 1205 |
+
f'Atom {bond_end} in bond {bond} must have 3 components: '
|
| 1206 |
+
'(chain_id: str, res_id: int, atom_name: str).'
|
| 1207 |
+
)
|
| 1208 |
+
if bond_beg[0] not in flat_seq_ids or bond_end[0] not in flat_seq_ids:
|
| 1209 |
+
raise ValueError(f'Invalid chain ID(s) in bond {bond}')
|
| 1210 |
+
if (
|
| 1211 |
+
not 0 < bond_beg[1] <= chain_lengths[bond_beg[0]]
|
| 1212 |
+
or not 0 < bond_end[1] <= chain_lengths[bond_end[0]]
|
| 1213 |
+
):
|
| 1214 |
+
raise ValueError(f'Invalid residue ID(s) in bond {bond}')
|
| 1215 |
+
if bond_beg[0] in smiles_ligand_ids:
|
| 1216 |
+
raise ValueError(
|
| 1217 |
+
f'Bond {bond} involves an unsupported SMILES ligand {bond_beg[0]}'
|
| 1218 |
+
)
|
| 1219 |
+
if bond_end[0] in smiles_ligand_ids:
|
| 1220 |
+
raise ValueError(
|
| 1221 |
+
f'Bond {bond} involves an unsupported SMILES ligand {bond_end[0]}'
|
| 1222 |
+
)
|
| 1223 |
+
bonded_atom_pairs.append((tuple(bond_beg), tuple(bond_end)))
|
| 1224 |
+
|
| 1225 |
+
if len(bonded_atom_pairs) != len(set(bonded_atom_pairs)):
|
| 1226 |
+
raise ValueError(f'Bonds are not unique: {bonded_atom_pairs}')
|
| 1227 |
+
|
| 1228 |
+
user_ccd = raw_json.get('userCCD')
|
| 1229 |
+
user_ccd_path = raw_json.get('userCCDPath')
|
| 1230 |
+
if user_ccd and user_ccd_path:
|
| 1231 |
+
raise ValueError('Only one of userCCD/userCCDPath can be set.')
|
| 1232 |
+
if user_ccd and len(user_ccd) < 256 and os.path.exists(user_ccd):
|
| 1233 |
+
raise ValueError('Set the user CCD path using the "userCCDPath" field.')
|
| 1234 |
+
elif user_ccd_path:
|
| 1235 |
+
user_ccd = _read_file(pathlib.Path(user_ccd_path), json_path)
|
| 1236 |
+
|
| 1237 |
+
return cls(
|
| 1238 |
+
name=raw_json['name'],
|
| 1239 |
+
chains=chains,
|
| 1240 |
+
rng_seeds=[int(seed) for seed in raw_json['modelSeeds']],
|
| 1241 |
+
bonded_atom_pairs=bonded_atom_pairs,
|
| 1242 |
+
user_ccd=user_ccd,
|
| 1243 |
+
)
|
| 1244 |
+
|
| 1245 |
+
@classmethod
|
| 1246 |
+
def from_mmcif(cls, mmcif_str: str, ccd: chemical_components.Ccd) -> Self:
|
| 1247 |
+
"""Loads the input from an mmCIF string.
|
| 1248 |
+
|
| 1249 |
+
WARNING: Since rng seeds are not stored in mmCIFs, an rng seed is sampled
|
| 1250 |
+
in the returned `Input`.
|
| 1251 |
+
|
| 1252 |
+
Args:
|
| 1253 |
+
mmcif_str: The mmCIF string.
|
| 1254 |
+
ccd: The chemical components dictionary.
|
| 1255 |
+
|
| 1256 |
+
Returns:
|
| 1257 |
+
The input in an Input format.
|
| 1258 |
+
"""
|
| 1259 |
+
|
| 1260 |
+
struc = structure.from_mmcif(
|
| 1261 |
+
mmcif_str,
|
| 1262 |
+
# Change MSE residues to MET residues.
|
| 1263 |
+
fix_mse_residues=True,
|
| 1264 |
+
# Fix arginine atom names. This is not needed since the input discards
|
| 1265 |
+
# any atom-level data, but kept for consistency with the paper.
|
| 1266 |
+
fix_arginines=True,
|
| 1267 |
+
# Fix unknown DNA residues to the correct unknown DNA residue type.
|
| 1268 |
+
fix_unknown_dna=True,
|
| 1269 |
+
# Do not include water molecules.
|
| 1270 |
+
include_water=False,
|
| 1271 |
+
# Do not include things like DNA/RNA hybrids. This will be changed once
|
| 1272 |
+
# we have a way of handling these in the AlphaFold 3 input format.
|
| 1273 |
+
include_other=False,
|
| 1274 |
+
# Include the specific bonds defined in the mmCIF bond table, e.g.
|
| 1275 |
+
# covalent bonds for PTMs.
|
| 1276 |
+
include_bonds=True,
|
| 1277 |
+
)
|
| 1278 |
+
|
| 1279 |
+
# Create default bioassembly, expanding structures implied by stoichiometry.
|
| 1280 |
+
struc = struc.generate_bioassembly(None)
|
| 1281 |
+
|
| 1282 |
+
sequences = struc.chain_single_letter_sequence(
|
| 1283 |
+
include_missing_residues=True
|
| 1284 |
+
)
|
| 1285 |
+
|
| 1286 |
+
chains = []
|
| 1287 |
+
for chain_id, chain_type in zip(
|
| 1288 |
+
struc.group_by_chain.chain_id, struc.group_by_chain.chain_type
|
| 1289 |
+
):
|
| 1290 |
+
sequence = sequences[chain_id]
|
| 1291 |
+
|
| 1292 |
+
if chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES:
|
| 1293 |
+
residues = list(struc.chain_res_name_sequence()[chain_id])
|
| 1294 |
+
if all(ccd.get(res) is not None for res in residues):
|
| 1295 |
+
chains.append(Ligand(id=chain_id, ccd_ids=residues))
|
| 1296 |
+
elif len(residues) == 1:
|
| 1297 |
+
comp_name = residues[0]
|
| 1298 |
+
comps = struc.chemical_components_data
|
| 1299 |
+
if comps is None:
|
| 1300 |
+
raise ValueError(
|
| 1301 |
+
'Missing mmCIF chemical components data - this is required for '
|
| 1302 |
+
f'a non-CCD ligand {comp_name} defined using SMILES string.'
|
| 1303 |
+
)
|
| 1304 |
+
chains.append(
|
| 1305 |
+
Ligand(id=chain_id, smiles=comps.chem_comp[comp_name].pdbx_smiles)
|
| 1306 |
+
)
|
| 1307 |
+
else:
|
| 1308 |
+
raise ValueError(
|
| 1309 |
+
'Multi-component ligand must be defined using CCD IDs, defining'
|
| 1310 |
+
' using SMILES is supported only for single-component ligands. '
|
| 1311 |
+
f'Got {residues}'
|
| 1312 |
+
)
|
| 1313 |
+
else:
|
| 1314 |
+
residues = struc.chain_res_name_sequence()[chain_id]
|
| 1315 |
+
fixed = struc.chain_res_name_sequence(
|
| 1316 |
+
fix_non_standard_polymer_res=True
|
| 1317 |
+
)[chain_id]
|
| 1318 |
+
modifications = [
|
| 1319 |
+
(orig, i + 1)
|
| 1320 |
+
for i, (orig, fixed) in enumerate(zip(residues, fixed, strict=True))
|
| 1321 |
+
if orig != fixed
|
| 1322 |
+
]
|
| 1323 |
+
|
| 1324 |
+
if chain_type == mmcif_names.PROTEIN_CHAIN:
|
| 1325 |
+
chains.append(
|
| 1326 |
+
ProteinChain(id=chain_id, sequence=sequence, ptms=modifications)
|
| 1327 |
+
)
|
| 1328 |
+
elif chain_type == mmcif_names.RNA_CHAIN:
|
| 1329 |
+
chains.append(
|
| 1330 |
+
RnaChain(
|
| 1331 |
+
id=chain_id, sequence=sequence, modifications=modifications
|
| 1332 |
+
)
|
| 1333 |
+
)
|
| 1334 |
+
elif chain_type == mmcif_names.DNA_CHAIN:
|
| 1335 |
+
chains.append(
|
| 1336 |
+
DnaChain(
|
| 1337 |
+
id=chain_id, sequence=sequence, modifications=modifications
|
| 1338 |
+
)
|
| 1339 |
+
)
|
| 1340 |
+
|
| 1341 |
+
bonded_atom_pairs = []
|
| 1342 |
+
chain_ids = set(c.id for c in chains)
|
| 1343 |
+
for atom_a, atom_b, _ in struc.iter_bonds():
|
| 1344 |
+
if atom_a['chain_id'] in chain_ids and atom_b['chain_id'] in chain_ids:
|
| 1345 |
+
beg = (atom_a['chain_id'], int(atom_a['res_id']), atom_a['atom_name'])
|
| 1346 |
+
end = (atom_b['chain_id'], int(atom_b['res_id']), atom_b['atom_name'])
|
| 1347 |
+
bonded_atom_pairs.append((beg, end))
|
| 1348 |
+
|
| 1349 |
+
return cls(
|
| 1350 |
+
name=struc.name,
|
| 1351 |
+
chains=chains,
|
| 1352 |
+
# mmCIFs don't store rng seeds, so we need to sample one here.
|
| 1353 |
+
rng_seeds=[_sample_rng_seed()],
|
| 1354 |
+
bonded_atom_pairs=bonded_atom_pairs or None,
|
| 1355 |
+
)
|
| 1356 |
+
|
| 1357 |
+
def to_structure(self, ccd: chemical_components.Ccd) -> structure.Structure:
|
| 1358 |
+
"""Converts Input to a Structure.
|
| 1359 |
+
|
| 1360 |
+
WARNING: This method does not preserve the rng seeds.
|
| 1361 |
+
|
| 1362 |
+
Args:
|
| 1363 |
+
ccd: The chemical components dictionary.
|
| 1364 |
+
|
| 1365 |
+
Returns:
|
| 1366 |
+
The input in a structure.Structure format.
|
| 1367 |
+
"""
|
| 1368 |
+
ids: list[str] = []
|
| 1369 |
+
sequences: list[str] = []
|
| 1370 |
+
poly_types: list[str] = []
|
| 1371 |
+
formats: list[structure.SequenceFormat] = []
|
| 1372 |
+
|
| 1373 |
+
for chain in self.chains:
|
| 1374 |
+
ids.append(chain.id)
|
| 1375 |
+
match chain:
|
| 1376 |
+
case ProteinChain():
|
| 1377 |
+
sequences.append('(' + ')('.join(chain.to_ccd_sequence()) + ')')
|
| 1378 |
+
poly_types.append(mmcif_names.PROTEIN_CHAIN)
|
| 1379 |
+
formats.append(structure.SequenceFormat.CCD_CODES)
|
| 1380 |
+
case RnaChain():
|
| 1381 |
+
sequences.append('(' + ')('.join(chain.to_ccd_sequence()) + ')')
|
| 1382 |
+
poly_types.append(mmcif_names.RNA_CHAIN)
|
| 1383 |
+
formats.append(structure.SequenceFormat.CCD_CODES)
|
| 1384 |
+
case DnaChain():
|
| 1385 |
+
sequences.append('(' + ')('.join(chain.to_ccd_sequence()) + ')')
|
| 1386 |
+
poly_types.append(mmcif_names.DNA_CHAIN)
|
| 1387 |
+
formats.append(structure.SequenceFormat.CCD_CODES)
|
| 1388 |
+
case Ligand():
|
| 1389 |
+
if chain.ccd_ids is not None:
|
| 1390 |
+
sequences.append('(' + ')('.join(chain.ccd_ids) + ')')
|
| 1391 |
+
if len(chain.ccd_ids) == 1:
|
| 1392 |
+
poly_types.append(mmcif_names.NON_POLYMER_CHAIN)
|
| 1393 |
+
else:
|
| 1394 |
+
poly_types.append(mmcif_names.BRANCHED_CHAIN)
|
| 1395 |
+
formats.append(structure.SequenceFormat.CCD_CODES)
|
| 1396 |
+
elif chain.smiles is not None:
|
| 1397 |
+
# Convert to `<unique ligand ID>:<smiles>` format that is expected
|
| 1398 |
+
# by structure.from_sequences_and_bonds.
|
| 1399 |
+
sequences.append(f'LIG_{chain.id}:{chain.smiles}')
|
| 1400 |
+
poly_types.append(mmcif_names.NON_POLYMER_CHAIN)
|
| 1401 |
+
formats.append(structure.SequenceFormat.LIGAND_SMILES)
|
| 1402 |
+
else:
|
| 1403 |
+
raise ValueError('Ligand must have one of CCD ID or SMILES set.')
|
| 1404 |
+
|
| 1405 |
+
# Remap bond chain IDs from chain IDs to chain indices and convert to
|
| 1406 |
+
# 0-based residue indexing.
|
| 1407 |
+
bonded_atom_pairs = []
|
| 1408 |
+
chain_indices = {cid: i for i, cid in enumerate(ids)}
|
| 1409 |
+
if self.bonded_atom_pairs is not None:
|
| 1410 |
+
for bond_beg, bond_end in self.bonded_atom_pairs:
|
| 1411 |
+
bonded_atom_pairs.append((
|
| 1412 |
+
(chain_indices[bond_beg[0]], bond_beg[1] - 1, bond_beg[2]),
|
| 1413 |
+
(chain_indices[bond_end[0]], bond_end[1] - 1, bond_end[2]),
|
| 1414 |
+
))
|
| 1415 |
+
|
| 1416 |
+
return structure.from_sequences_and_bonds(
|
| 1417 |
+
sequences=sequences,
|
| 1418 |
+
chain_types=poly_types,
|
| 1419 |
+
sequence_formats=formats,
|
| 1420 |
+
chain_ids=ids,
|
| 1421 |
+
bonded_atom_pairs=bonded_atom_pairs,
|
| 1422 |
+
ccd=ccd,
|
| 1423 |
+
name=self.sanitised_name(),
|
| 1424 |
+
bond_type=mmcif_names.COVALENT_BOND,
|
| 1425 |
+
release_date=None,
|
| 1426 |
+
)
|
| 1427 |
+
|
| 1428 |
+
def to_json(self) -> str:
|
| 1429 |
+
"""Converts Input to an AlphaFold JSON."""
|
| 1430 |
+
deduped_chains = {}
|
| 1431 |
+
deduped_chain_ids = {}
|
| 1432 |
+
for chain in self.chains:
|
| 1433 |
+
deduped_chains[chain.hash_without_id()] = chain
|
| 1434 |
+
deduped_chain_ids.setdefault(chain.hash_without_id(), []).append(chain.id)
|
| 1435 |
+
|
| 1436 |
+
sequences = []
|
| 1437 |
+
for chain_content_hash, ids in deduped_chain_ids.items():
|
| 1438 |
+
chain = deduped_chains[chain_content_hash]
|
| 1439 |
+
sequences.append(chain.to_dict(seq_id=ids if len(ids) > 1 else ids[0]))
|
| 1440 |
+
|
| 1441 |
+
alphafold_json = json.dumps(
|
| 1442 |
+
{
|
| 1443 |
+
'dialect': JSON_DIALECT,
|
| 1444 |
+
'version': JSON_VERSION,
|
| 1445 |
+
'name': self.name,
|
| 1446 |
+
'sequences': sequences,
|
| 1447 |
+
'modelSeeds': self.rng_seeds,
|
| 1448 |
+
'bondedAtomPairs': self.bonded_atom_pairs,
|
| 1449 |
+
'userCCD': self.user_ccd,
|
| 1450 |
+
},
|
| 1451 |
+
indent=2,
|
| 1452 |
+
)
|
| 1453 |
+
# Remove newlines from the query/template indices arrays. We match the
|
| 1454 |
+
# queryIndices/templatesIndices with a non-capturing group. We then match
|
| 1455 |
+
# the entire region between the square brackets by looking for lines
|
| 1456 |
+
# containing only whitespace, number, or a comma.
|
| 1457 |
+
return re.sub(
|
| 1458 |
+
r'("(?:queryIndices|templateIndices)": \[)([\s\n\d,]+)(\],?)',
|
| 1459 |
+
lambda mtch: mtch[1] + re.sub(r'\n\s+', ' ', mtch[2].strip()) + mtch[3],
|
| 1460 |
+
alphafold_json,
|
| 1461 |
+
)
|
| 1462 |
+
|
| 1463 |
+
def fill_missing_fields(self) -> Self:
|
| 1464 |
+
"""Fill missing MSA and template fields with default values."""
|
| 1465 |
+
with_missing_fields = [
|
| 1466 |
+
c.fill_missing_fields()
|
| 1467 |
+
if isinstance(c, (ProteinChain, RnaChain))
|
| 1468 |
+
else c
|
| 1469 |
+
for c in self.chains
|
| 1470 |
+
]
|
| 1471 |
+
return dataclasses.replace(self, chains=with_missing_fields)
|
| 1472 |
+
|
| 1473 |
+
def with_multiple_seeds(self, num_seeds: int) -> Self:
|
| 1474 |
+
"""Returns a copy of the input with num_seeds rng seeds."""
|
| 1475 |
+
if num_seeds <= 1:
|
| 1476 |
+
raise ValueError('Number of seeds must be greater than 1.')
|
| 1477 |
+
if len(self.rng_seeds) != 1:
|
| 1478 |
+
raise ValueError('Input must have one rng seed to set multiple seeds.')
|
| 1479 |
+
|
| 1480 |
+
return dataclasses.replace(
|
| 1481 |
+
self,
|
| 1482 |
+
rng_seeds=list(range(self.rng_seeds[0], self.rng_seeds[0] + num_seeds)),
|
| 1483 |
+
)
|
| 1484 |
+
|
| 1485 |
+
|
| 1486 |
+
def load_fold_inputs_from_path(json_path: pathlib.Path) -> Iterator[Input]:
|
| 1487 |
+
"""Loads multiple fold inputs from a JSON string."""
|
| 1488 |
+
with open(json_path, 'r') as f:
|
| 1489 |
+
json_str = f.read()
|
| 1490 |
+
|
| 1491 |
+
# Parse the JSON string, so we can detect its format.
|
| 1492 |
+
raw_json = json.loads(json_str)
|
| 1493 |
+
|
| 1494 |
+
if isinstance(raw_json, list):
|
| 1495 |
+
# AlphaFold Server JSON.
|
| 1496 |
+
logging.info('Loading %d fold jobs from %s', len(raw_json), json_path)
|
| 1497 |
+
for fold_job_idx, fold_job in enumerate(raw_json):
|
| 1498 |
+
try:
|
| 1499 |
+
yield Input.from_alphafoldserver_fold_job(fold_job)
|
| 1500 |
+
except ValueError as e:
|
| 1501 |
+
raise ValueError(
|
| 1502 |
+
f'Failed to load fold job {fold_job_idx} from {json_path}'
|
| 1503 |
+
f' (AlphaFold Server dialect): {e}'
|
| 1504 |
+
) from e
|
| 1505 |
+
else:
|
| 1506 |
+
# AlphaFold 3 JSON.
|
| 1507 |
+
try:
|
| 1508 |
+
yield Input.from_json(json_str, json_path)
|
| 1509 |
+
except ValueError as e:
|
| 1510 |
+
raise ValueError(
|
| 1511 |
+
f'Failed to load input from {json_path} (AlphaFold 3 dialect): {e}'
|
| 1512 |
+
) from e
|
| 1513 |
+
|
| 1514 |
+
|
| 1515 |
+
def load_fold_inputs_from_dir(input_dir: pathlib.Path) -> Iterator[Input]:
|
| 1516 |
+
"""Loads multiple fold inputs from all JSON files in a given input_dir.
|
| 1517 |
+
|
| 1518 |
+
Args:
|
| 1519 |
+
input_dir: The directory containing the JSON files.
|
| 1520 |
+
|
| 1521 |
+
Yields:
|
| 1522 |
+
The fold inputs from all JSON files in the input directory.
|
| 1523 |
+
"""
|
| 1524 |
+
for file_path in sorted(input_dir.glob('*.json')):
|
| 1525 |
+
if not file_path.is_file():
|
| 1526 |
+
continue
|
| 1527 |
+
|
| 1528 |
+
yield from load_fold_inputs_from_path(file_path)
|
flax_model/alphafold3/common/resources.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Load external resources, such as external tools or data resources."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Iterator
|
| 6 |
+
import os
|
| 7 |
+
import pathlib
|
| 8 |
+
import typing
|
| 9 |
+
from typing import BinaryIO, Final, Literal, TextIO
|
| 10 |
+
|
| 11 |
+
from importlib import resources
|
| 12 |
+
import flax_model.alphafold3.common
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
_DATA_ROOT: Final[pathlib.Path] = (
|
| 16 |
+
resources.files(flax_model.alphafold3.common).joinpath('..').resolve()
|
| 17 |
+
)
|
| 18 |
+
ROOT = _DATA_ROOT
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def filename(name: str | os.PathLike[str]) -> str:
|
| 22 |
+
"""Returns the absolute path to an external resource.
|
| 23 |
+
|
| 24 |
+
Note that this calls resources.GetResourceFilename under the hood and hence
|
| 25 |
+
causes par file unpacking, which might be unfriendly on diskless machines.
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
name: the name of the resource corresponding to its path relative to the
|
| 30 |
+
root of the repository.
|
| 31 |
+
"""
|
| 32 |
+
return (_DATA_ROOT / name).as_posix()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@typing.overload
|
| 36 |
+
def open_resource(
|
| 37 |
+
name: str | os.PathLike[str], mode: Literal['r', 'rt'] = 'rt'
|
| 38 |
+
) -> TextIO:
|
| 39 |
+
...
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@typing.overload
|
| 43 |
+
def open_resource(
|
| 44 |
+
name: str | os.PathLike[str], mode: Literal['rb']
|
| 45 |
+
) -> BinaryIO:
|
| 46 |
+
...
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def open_resource(
|
| 50 |
+
name: str | os.PathLike[str], mode: str = 'rb'
|
| 51 |
+
) -> TextIO | BinaryIO:
|
| 52 |
+
"""Returns an open file object for the named resource.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
name: the name of the resource corresponding to its path relative to the
|
| 56 |
+
root of the repository.
|
| 57 |
+
mode: the mode to use when opening the file.
|
| 58 |
+
"""
|
| 59 |
+
return (_DATA_ROOT / name).open(mode)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def get_resource_dir(path: str | os.PathLike[str]) -> os.PathLike[str]:
|
| 63 |
+
return _DATA_ROOT / path
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def walk(path: str) -> Iterator[tuple[str, list[str], list[str]]]:
|
| 67 |
+
"""Walks the directory tree of resources similar to os.walk."""
|
| 68 |
+
return os.walk((_DATA_ROOT / path).as_posix())
|
flax_model/alphafold3/common/testing/__init__.py
ADDED
|
File without changes
|
flax_model/alphafold3/common/testing/data.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Module that provides an abstraction for accessing test data."""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import pathlib
|
| 7 |
+
from typing import Literal, overload
|
| 8 |
+
|
| 9 |
+
from absl.testing import absltest
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Data:
|
| 13 |
+
"""Provides an abstraction for accessing test data."""
|
| 14 |
+
|
| 15 |
+
def __init__(self, data_dir: os.PathLike[str] | str):
|
| 16 |
+
"""Initiailizes data wrapper, providing users with high level data access.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
data_dir: Directory containing test data.
|
| 20 |
+
"""
|
| 21 |
+
self._data_dir = pathlib.Path(data_dir)
|
| 22 |
+
|
| 23 |
+
def path(self, data_name: str | os.PathLike[str] | None = None) -> str:
|
| 24 |
+
"""Returns the path to a given test data.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
data_name: the name of the test data file relative to data_dir. If not
|
| 28 |
+
set, this will return the absolute path to the data directory.
|
| 29 |
+
"""
|
| 30 |
+
data_dir_path = (
|
| 31 |
+
pathlib.Path(absltest.get_default_test_srcdir()) / self._data_dir
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
if data_name:
|
| 35 |
+
return str(data_dir_path / data_name)
|
| 36 |
+
|
| 37 |
+
return str(data_dir_path)
|
| 38 |
+
|
| 39 |
+
@overload
|
| 40 |
+
def load(
|
| 41 |
+
self, data_name: str | os.PathLike[str], mode: Literal['rt'] = 'rt'
|
| 42 |
+
) -> str:
|
| 43 |
+
...
|
| 44 |
+
|
| 45 |
+
@overload
|
| 46 |
+
def load(
|
| 47 |
+
self, data_name: str | os.PathLike[str], mode: Literal['rb'] = 'rb'
|
| 48 |
+
) -> bytes:
|
| 49 |
+
...
|
| 50 |
+
|
| 51 |
+
def load(
|
| 52 |
+
self, data_name: str | os.PathLike[str], mode: str = 'rt'
|
| 53 |
+
) -> str | bytes:
|
| 54 |
+
"""Returns the contents of a given test data.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
data_name: the name of the test data file relative to data_dir.
|
| 58 |
+
mode: the mode in which to read the data file. Defaults to text ('rt').
|
| 59 |
+
"""
|
| 60 |
+
with open(self.path(data_name), mode=mode) as f:
|
| 61 |
+
return f.read()
|
flax_model/alphafold3/constants/__init__.py
ADDED
|
File without changes
|
flax_model/alphafold3/constants/atom_types.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""List of atom types with reverse look-up."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping, Sequence, Set
|
| 6 |
+
import itertools
|
| 7 |
+
import sys
|
| 8 |
+
from typing import Final
|
| 9 |
+
from flax_model.alphafold3.constants import residue_names
|
| 10 |
+
|
| 11 |
+
# Note:
|
| 12 |
+
# `sys.intern` places the values in the Python internal db for fast lookup.
|
| 13 |
+
|
| 14 |
+
# 37 common residue atoms.
|
| 15 |
+
N = sys.intern('N')
|
| 16 |
+
CA = sys.intern('CA')
|
| 17 |
+
C = sys.intern('C')
|
| 18 |
+
CB = sys.intern('CB')
|
| 19 |
+
O = sys.intern('O')
|
| 20 |
+
CG = sys.intern('CG')
|
| 21 |
+
CG1 = sys.intern('CG1')
|
| 22 |
+
CG2 = sys.intern('CG2')
|
| 23 |
+
OG = sys.intern('OG')
|
| 24 |
+
OG1 = sys.intern('OG1')
|
| 25 |
+
SG = sys.intern('SG')
|
| 26 |
+
CD = sys.intern('CD')
|
| 27 |
+
CD1 = sys.intern('CD1')
|
| 28 |
+
CD2 = sys.intern('CD2')
|
| 29 |
+
ND1 = sys.intern('ND1')
|
| 30 |
+
ND2 = sys.intern('ND2')
|
| 31 |
+
OD1 = sys.intern('OD1')
|
| 32 |
+
OD2 = sys.intern('OD2')
|
| 33 |
+
SD = sys.intern('SD')
|
| 34 |
+
CE = sys.intern('CE')
|
| 35 |
+
CE1 = sys.intern('CE1')
|
| 36 |
+
CE2 = sys.intern('CE2')
|
| 37 |
+
CE3 = sys.intern('CE3')
|
| 38 |
+
NE = sys.intern('NE')
|
| 39 |
+
NE1 = sys.intern('NE1')
|
| 40 |
+
NE2 = sys.intern('NE2')
|
| 41 |
+
OE1 = sys.intern('OE1')
|
| 42 |
+
OE2 = sys.intern('OE2')
|
| 43 |
+
CH2 = sys.intern('CH2')
|
| 44 |
+
NH1 = sys.intern('NH1')
|
| 45 |
+
NH2 = sys.intern('NH2')
|
| 46 |
+
OH = sys.intern('OH')
|
| 47 |
+
CZ = sys.intern('CZ')
|
| 48 |
+
CZ2 = sys.intern('CZ2')
|
| 49 |
+
CZ3 = sys.intern('CZ3')
|
| 50 |
+
NZ = sys.intern('NZ')
|
| 51 |
+
OXT = sys.intern('OXT')
|
| 52 |
+
|
| 53 |
+
# 29 common nucleic acid atoms.
|
| 54 |
+
C1PRIME = sys.intern("C1'")
|
| 55 |
+
C2 = sys.intern('C2')
|
| 56 |
+
C2PRIME = sys.intern("C2'")
|
| 57 |
+
C3PRIME = sys.intern("C3'")
|
| 58 |
+
C4 = sys.intern('C4')
|
| 59 |
+
C4PRIME = sys.intern("C4'")
|
| 60 |
+
C5 = sys.intern('C5')
|
| 61 |
+
C5PRIME = sys.intern("C5'")
|
| 62 |
+
C6 = sys.intern('C6')
|
| 63 |
+
C7 = sys.intern('C7')
|
| 64 |
+
C8 = sys.intern('C8')
|
| 65 |
+
N1 = sys.intern('N1')
|
| 66 |
+
N2 = sys.intern('N2')
|
| 67 |
+
N3 = sys.intern('N3')
|
| 68 |
+
N4 = sys.intern('N4')
|
| 69 |
+
N6 = sys.intern('N6')
|
| 70 |
+
N7 = sys.intern('N7')
|
| 71 |
+
N9 = sys.intern('N9')
|
| 72 |
+
O2 = sys.intern('O2')
|
| 73 |
+
O2PRIME = sys.intern("O2'")
|
| 74 |
+
O3PRIME = sys.intern("O3'")
|
| 75 |
+
O4 = sys.intern('O4')
|
| 76 |
+
O4PRIME = sys.intern("O4'")
|
| 77 |
+
O5PRIME = sys.intern("O5'")
|
| 78 |
+
O6 = sys.intern('O6')
|
| 79 |
+
OP1 = sys.intern('OP1')
|
| 80 |
+
OP2 = sys.intern('OP2')
|
| 81 |
+
OP3 = sys.intern('OP3')
|
| 82 |
+
P = sys.intern('P')
|
| 83 |
+
|
| 84 |
+
# A list of atoms (excluding hydrogen) for each AA type. PDB naming convention.
|
| 85 |
+
RESIDUE_ATOMS: Mapping[str, tuple[str, ...]] = {
|
| 86 |
+
residue_names.ALA: (C, CA, CB, N, O),
|
| 87 |
+
residue_names.ARG: (C, CA, CB, CG, CD, CZ, N, NE, O, NH1, NH2),
|
| 88 |
+
residue_names.ASN: (C, CA, CB, CG, N, ND2, O, OD1),
|
| 89 |
+
residue_names.ASP: (C, CA, CB, CG, N, O, OD1, OD2),
|
| 90 |
+
residue_names.CYS: (C, CA, CB, N, O, SG),
|
| 91 |
+
residue_names.GLN: (C, CA, CB, CG, CD, N, NE2, O, OE1),
|
| 92 |
+
residue_names.GLU: (C, CA, CB, CG, CD, N, O, OE1, OE2),
|
| 93 |
+
residue_names.GLY: (C, CA, N, O),
|
| 94 |
+
residue_names.HIS: (C, CA, CB, CG, CD2, CE1, N, ND1, NE2, O),
|
| 95 |
+
residue_names.ILE: (C, CA, CB, CG1, CG2, CD1, N, O),
|
| 96 |
+
residue_names.LEU: (C, CA, CB, CG, CD1, CD2, N, O),
|
| 97 |
+
residue_names.LYS: (C, CA, CB, CG, CD, CE, N, NZ, O),
|
| 98 |
+
residue_names.MET: (C, CA, CB, CG, CE, N, O, SD),
|
| 99 |
+
residue_names.PHE: (C, CA, CB, CG, CD1, CD2, CE1, CE2, CZ, N, O),
|
| 100 |
+
residue_names.PRO: (C, CA, CB, CG, CD, N, O),
|
| 101 |
+
residue_names.SER: (C, CA, CB, N, O, OG),
|
| 102 |
+
residue_names.THR: (C, CA, CB, CG2, N, O, OG1),
|
| 103 |
+
residue_names.TRP:
|
| 104 |
+
(C, CA, CB, CG, CD1, CD2, CE2, CE3, CZ2, CZ3, CH2, N, NE1, O),
|
| 105 |
+
residue_names.TYR: (C, CA, CB, CG, CD1, CD2, CE1, CE2, CZ, N, O, OH),
|
| 106 |
+
residue_names.VAL: (C, CA, CB, CG1, CG2, N, O),
|
| 107 |
+
} # pyformat: disable
|
| 108 |
+
|
| 109 |
+
# Used to identify backbone for alignment and distance calculation for sterics.
|
| 110 |
+
PROTEIN_BACKBONE_ATOMS: tuple[str, ...] = (N, CA, C)
|
| 111 |
+
|
| 112 |
+
# Naming swaps for ambiguous atom names. Due to symmetries in the amino acids
|
| 113 |
+
# the naming of atoms is ambiguous in 4 of the 20 amino acids. (The LDDT paper
|
| 114 |
+
# lists 7 amino acids as ambiguous, but the naming ambiguities in LEU, VAL and
|
| 115 |
+
# ARG can be resolved by using the 3D constellations of the 'ambiguous' atoms
|
| 116 |
+
# and their neighbours)
|
| 117 |
+
AMBIGUOUS_ATOM_NAMES: Mapping[str, Mapping[str, str]] = {
|
| 118 |
+
residue_names.ASP: {OD1: OD2},
|
| 119 |
+
residue_names.GLU: {OE1: OE2},
|
| 120 |
+
residue_names.PHE: {CD1: CD2, CE1: CE2},
|
| 121 |
+
residue_names.TYR: {CD1: CD2, CE1: CE2},
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
# Used when we need to store atom data in a format that requires fixed atom data
|
| 125 |
+
# size for every protein residue (e.g. a numpy array).
|
| 126 |
+
ATOM37: tuple[str, ...] = (
|
| 127 |
+
N, CA, C, CB, O, CG, CG1, CG2, OG, OG1, SG, CD, CD1, CD2, ND1, ND2, OD1,
|
| 128 |
+
OD2, SD, CE, CE1, CE2, CE3, NE, NE1, NE2, OE1, OE2, CH2, NH1, NH2, OH, CZ,
|
| 129 |
+
CZ2, CZ3, NZ, OXT) # pyformat: disable
|
| 130 |
+
ATOM37_ORDER: Mapping[str, int] = {name: i for i, name in enumerate(ATOM37)}
|
| 131 |
+
ATOM37_NUM: Final[int] = len(ATOM37) # := 37.
|
| 132 |
+
|
| 133 |
+
# Used when we need to store protein atom data in a format that requires fixed
|
| 134 |
+
# atom data size for any residue but takes less space than ATOM37 by having 14
|
| 135 |
+
# fields, which is sufficient for storing atoms of all protein residues (e.g. a
|
| 136 |
+
# numpy array).
|
| 137 |
+
ATOM14: Mapping[str, tuple[str, ...]] = {
|
| 138 |
+
residue_names.ALA: (N, CA, C, O, CB),
|
| 139 |
+
residue_names.ARG: (N, CA, C, O, CB, CG, CD, NE, CZ, NH1, NH2),
|
| 140 |
+
residue_names.ASN: (N, CA, C, O, CB, CG, OD1, ND2),
|
| 141 |
+
residue_names.ASP: (N, CA, C, O, CB, CG, OD1, OD2),
|
| 142 |
+
residue_names.CYS: (N, CA, C, O, CB, SG),
|
| 143 |
+
residue_names.GLN: (N, CA, C, O, CB, CG, CD, OE1, NE2),
|
| 144 |
+
residue_names.GLU: (N, CA, C, O, CB, CG, CD, OE1, OE2),
|
| 145 |
+
residue_names.GLY: (N, CA, C, O),
|
| 146 |
+
residue_names.HIS: (N, CA, C, O, CB, CG, ND1, CD2, CE1, NE2),
|
| 147 |
+
residue_names.ILE: (N, CA, C, O, CB, CG1, CG2, CD1),
|
| 148 |
+
residue_names.LEU: (N, CA, C, O, CB, CG, CD1, CD2),
|
| 149 |
+
residue_names.LYS: (N, CA, C, O, CB, CG, CD, CE, NZ),
|
| 150 |
+
residue_names.MET: (N, CA, C, O, CB, CG, SD, CE),
|
| 151 |
+
residue_names.PHE: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ),
|
| 152 |
+
residue_names.PRO: (N, CA, C, O, CB, CG, CD),
|
| 153 |
+
residue_names.SER: (N, CA, C, O, CB, OG),
|
| 154 |
+
residue_names.THR: (N, CA, C, O, CB, OG1, CG2),
|
| 155 |
+
residue_names.TRP:
|
| 156 |
+
(N, CA, C, O, CB, CG, CD1, CD2, NE1, CE2, CE3, CZ2, CZ3, CH2),
|
| 157 |
+
residue_names.TYR: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ, OH),
|
| 158 |
+
residue_names.VAL: (N, CA, C, O, CB, CG1, CG2),
|
| 159 |
+
residue_names.UNK: (),
|
| 160 |
+
} # pyformat: disable
|
| 161 |
+
|
| 162 |
+
# A compact atom encoding with 14 columns, padded with '' in empty slots.
|
| 163 |
+
ATOM14_PADDED: Mapping[str, Sequence[str]] = {
|
| 164 |
+
k: [v for _, v in itertools.zip_longest(range(14), values, fillvalue='')]
|
| 165 |
+
for k, values in ATOM14.items()
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
ATOM14_ORDER: Mapping[str, Mapping[str, int]] = {
|
| 169 |
+
k: {name: i for i, name in enumerate(v)} for k, v in ATOM14.items()
|
| 170 |
+
}
|
| 171 |
+
ATOM14_NUM: Final[int] = max(len(v) for v in ATOM14.values())
|
| 172 |
+
|
| 173 |
+
# Used when we need to store protein and nucleic atom library.
|
| 174 |
+
DENSE_ATOM: Mapping[str, tuple[str, ...]] = {
|
| 175 |
+
# Protein.
|
| 176 |
+
residue_names.ALA: (N, CA, C, O, CB),
|
| 177 |
+
residue_names.ARG: (N, CA, C, O, CB, CG, CD, NE, CZ, NH1, NH2),
|
| 178 |
+
residue_names.ASN: (N, CA, C, O, CB, CG, OD1, ND2),
|
| 179 |
+
residue_names.ASP: (N, CA, C, O, CB, CG, OD1, OD2),
|
| 180 |
+
residue_names.CYS: (N, CA, C, O, CB, SG),
|
| 181 |
+
residue_names.GLN: (N, CA, C, O, CB, CG, CD, OE1, NE2),
|
| 182 |
+
residue_names.GLU: (N, CA, C, O, CB, CG, CD, OE1, OE2),
|
| 183 |
+
residue_names.GLY: (N, CA, C, O),
|
| 184 |
+
residue_names.HIS: (N, CA, C, O, CB, CG, ND1, CD2, CE1, NE2),
|
| 185 |
+
residue_names.ILE: (N, CA, C, O, CB, CG1, CG2, CD1),
|
| 186 |
+
residue_names.LEU: (N, CA, C, O, CB, CG, CD1, CD2),
|
| 187 |
+
residue_names.LYS: (N, CA, C, O, CB, CG, CD, CE, NZ),
|
| 188 |
+
residue_names.MET: (N, CA, C, O, CB, CG, SD, CE),
|
| 189 |
+
residue_names.PHE: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ),
|
| 190 |
+
residue_names.PRO: (N, CA, C, O, CB, CG, CD),
|
| 191 |
+
residue_names.SER: (N, CA, C, O, CB, OG),
|
| 192 |
+
residue_names.THR: (N, CA, C, O, CB, OG1, CG2),
|
| 193 |
+
residue_names.TRP:
|
| 194 |
+
(N, CA, C, O, CB, CG, CD1, CD2, NE1, CE2, CE3, CZ2, CZ3, CH2),
|
| 195 |
+
residue_names.TYR: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ, OH),
|
| 196 |
+
residue_names.VAL: (N, CA, C, O, CB, CG1, CG2),
|
| 197 |
+
residue_names.UNK: (),
|
| 198 |
+
# RNA.
|
| 199 |
+
residue_names.A:
|
| 200 |
+
(OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME,
|
| 201 |
+
C2PRIME, O2PRIME, C1PRIME, N9, C8, N7, C5, C6, N6, N1, C2, N3, C4),
|
| 202 |
+
residue_names.C:
|
| 203 |
+
(OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME,
|
| 204 |
+
C2PRIME, O2PRIME, C1PRIME, N1, C2, O2, N3, C4, N4, C5, C6),
|
| 205 |
+
residue_names.G:
|
| 206 |
+
(OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME,
|
| 207 |
+
C2PRIME, O2PRIME, C1PRIME, N9, C8, N7, C5, C6, O6, N1, C2, N2, N3, C4),
|
| 208 |
+
residue_names.U:
|
| 209 |
+
(OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME,
|
| 210 |
+
C2PRIME, O2PRIME, C1PRIME, N1, C2, O2, N3, C4, O4, C5, C6),
|
| 211 |
+
residue_names.UNK_RNA: (),
|
| 212 |
+
# DNA.
|
| 213 |
+
residue_names.DA:
|
| 214 |
+
(OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME,
|
| 215 |
+
C2PRIME, C1PRIME, N9, C8, N7, C5, C6, N6, N1, C2, N3, C4),
|
| 216 |
+
residue_names.DC:
|
| 217 |
+
(OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME,
|
| 218 |
+
C2PRIME, C1PRIME, N1, C2, O2, N3, C4, N4, C5, C6),
|
| 219 |
+
residue_names.DG:
|
| 220 |
+
(OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME,
|
| 221 |
+
C2PRIME, C1PRIME, N9, C8, N7, C5, C6, O6, N1, C2, N2, N3, C4),
|
| 222 |
+
residue_names.DT:
|
| 223 |
+
(OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME,
|
| 224 |
+
C2PRIME, C1PRIME, N1, C2, O2, N3, C4, O4, C5, C7, C6),
|
| 225 |
+
# Unknown nucleic.
|
| 226 |
+
residue_names.UNK_DNA: (),
|
| 227 |
+
} # pyformat: disable
|
| 228 |
+
|
| 229 |
+
DENSE_ATOM_ORDER: Mapping[str, Mapping[str, int]] = {
|
| 230 |
+
k: {name: i for i, name in enumerate(v)} for k, v in DENSE_ATOM.items()
|
| 231 |
+
}
|
| 232 |
+
DENSE_ATOM_NUM: Final[int] = max(len(v) for v in DENSE_ATOM.values())
|
| 233 |
+
|
| 234 |
+
# Used when we need to store atom data in a format that requires fixed atom data
|
| 235 |
+
# size for every nucleic molecule (e.g. a numpy array).
|
| 236 |
+
ATOM29: tuple[str, ...] = (
|
| 237 |
+
"C1'", 'C2', "C2'", "C3'", 'C4', "C4'", 'C5', "C5'", 'C6', 'C7', 'C8', 'N1',
|
| 238 |
+
'N2', 'N3', 'N4', 'N6', 'N7', 'N9', 'OP3', 'O2', "O2'", "O3'", 'O4', "O4'",
|
| 239 |
+
"O5'", 'O6', 'OP1', 'OP2', 'P') # pyformat: disable
|
| 240 |
+
ATOM29_ORDER: Mapping[str, int] = {
|
| 241 |
+
atom_type: i for i, atom_type in enumerate(ATOM29)
|
| 242 |
+
}
|
| 243 |
+
ATOM29_NUM: Final[int] = len(ATOM29) # := 29
|
| 244 |
+
|
| 245 |
+
# Hydrogens that exist depending on the protonation state of the residue.
|
| 246 |
+
# Extracted from third_party/py/openmm/app/data/hydrogens.xml
|
| 247 |
+
PROTONATION_HYDROGENS: Mapping[str, Set[str]] = {
|
| 248 |
+
'ASP': {'HD2'},
|
| 249 |
+
'CYS': {'HG'},
|
| 250 |
+
'GLU': {'HE2'},
|
| 251 |
+
'HIS': {'HD1', 'HE2'},
|
| 252 |
+
'LYS': {'HZ3'},
|
| 253 |
+
}
|
flax_model/alphafold3/constants/chemical_component_sets.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Sets of chemical components."""
|
| 4 |
+
|
| 5 |
+
import pickle
|
| 6 |
+
from typing import Final
|
| 7 |
+
|
| 8 |
+
from flax_model.alphafold3.common import resources
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
_CCD_SETS_CCD_PICKLE_FILE = resources.filename(
|
| 12 |
+
resources.ROOT / 'constants/converters/chemical_component_sets.pickle'
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
_CCD_SET = pickle.load(open(_CCD_SETS_CCD_PICKLE_FILE, 'rb'))
|
| 16 |
+
|
| 17 |
+
# Glycan (or 'Saccharide') ligands.
|
| 18 |
+
# _chem_comp.type containing 'saccharide' and 'linking' (when lower-case).
|
| 19 |
+
GLYCAN_LINKING_LIGANDS: Final[frozenset[str]] = _CCD_SET['glycans_linking']
|
| 20 |
+
|
| 21 |
+
# _chem_comp.type containing 'saccharide' and not 'linking' (when lower-case).
|
| 22 |
+
GLYCAN_OTHER_LIGANDS: Final[frozenset[str]] = _CCD_SET['glycans_other']
|
| 23 |
+
|
| 24 |
+
# Each of these molecules appears in over 1k PDB structures, are used to
|
| 25 |
+
# facilitate crystallization conditions, but do not have biological relevance.
|
| 26 |
+
COMMON_CRYSTALLIZATION_AIDS: Final[frozenset[str]] = frozenset({
|
| 27 |
+
'SO4', 'GOL', 'EDO', 'PO4', 'ACT', 'PEG', 'DMS', 'TRS', 'PGE', 'PG4', 'FMT',
|
| 28 |
+
'EPE', 'MPD', 'MES', 'CD', 'IOD',
|
| 29 |
+
}) # pyformat: disable
|
flax_model/alphafold3/constants/chemical_components.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Chemical Components found in PDB (CCD) constants."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import ItemsView, Iterator, KeysView, Mapping, Sequence, ValuesView
|
| 6 |
+
import dataclasses
|
| 7 |
+
import functools
|
| 8 |
+
import os
|
| 9 |
+
import pickle
|
| 10 |
+
|
| 11 |
+
from flax_model.alphafold3.common import resources
|
| 12 |
+
from flax_model.alphafold3.cpp import cif_dict
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
_CCD_PICKLE_FILE = resources.filename(
|
| 16 |
+
resources.ROOT / 'constants/converters/ccd.pickle'
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@functools.cache
|
| 21 |
+
def _load_ccd_pickle_cached(
|
| 22 |
+
path: os.PathLike[str],
|
| 23 |
+
) -> dict[str, Mapping[str, Sequence[str]]]:
|
| 24 |
+
"""Loads the CCD pickle file and caches it so that it is only loaded once."""
|
| 25 |
+
with open(path, 'rb') as f:
|
| 26 |
+
return pickle.loads(f.read())
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class Ccd(Mapping[str, Mapping[str, Sequence[str]]]):
|
| 30 |
+
"""Chemical Components found in PDB (CCD) constants.
|
| 31 |
+
|
| 32 |
+
See https://academic.oup.com/bioinformatics/article/31/8/1274/212200 for CCD
|
| 33 |
+
CIF format documentation.
|
| 34 |
+
|
| 35 |
+
Wraps the dict to prevent accidental mutation.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
__slots__ = ('_dict', '_ccd_pickle_path')
|
| 39 |
+
|
| 40 |
+
def __init__(
|
| 41 |
+
self,
|
| 42 |
+
ccd_pickle_path: os.PathLike[str] | None = None,
|
| 43 |
+
user_ccd: str | None = None,
|
| 44 |
+
):
|
| 45 |
+
"""Initialises the chemical components dictionary.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
ccd_pickle_path: Path to the CCD pickle file. If None, uses the default
|
| 49 |
+
CCD pickle file included in the source code.
|
| 50 |
+
user_ccd: A string containing the user-provided CCD. This has to conform
|
| 51 |
+
to the same format as the CCD, see https://www.wwpdb.org/data/ccd. If
|
| 52 |
+
provided, takes precedence over the CCD for the the same key. This can
|
| 53 |
+
be used to override specific entries in the CCD if desired.
|
| 54 |
+
"""
|
| 55 |
+
self._ccd_pickle_path = ccd_pickle_path or _CCD_PICKLE_FILE
|
| 56 |
+
self._dict = _load_ccd_pickle_cached(self._ccd_pickle_path)
|
| 57 |
+
|
| 58 |
+
if user_ccd is not None:
|
| 59 |
+
if not user_ccd:
|
| 60 |
+
raise ValueError('User CCD cannot be an empty string.')
|
| 61 |
+
user_ccd_cifs = {
|
| 62 |
+
key: {k: tuple(v) for k, v in value.items()}
|
| 63 |
+
for key, value in cif_dict.parse_multi_data_cif(user_ccd).items()
|
| 64 |
+
}
|
| 65 |
+
self._dict.update(user_ccd_cifs)
|
| 66 |
+
|
| 67 |
+
def __getitem__(self, key: str) -> Mapping[str, Sequence[str]]:
|
| 68 |
+
return self._dict[key]
|
| 69 |
+
|
| 70 |
+
def __contains__(self, key: str) -> bool:
|
| 71 |
+
return key in self._dict
|
| 72 |
+
|
| 73 |
+
def __iter__(self) -> Iterator[str]:
|
| 74 |
+
return self._dict.__iter__()
|
| 75 |
+
|
| 76 |
+
def __len__(self) -> int:
|
| 77 |
+
return len(self._dict)
|
| 78 |
+
|
| 79 |
+
def __hash__(self) -> int:
|
| 80 |
+
return id(self) # Ok since this is immutable.
|
| 81 |
+
|
| 82 |
+
def get(
|
| 83 |
+
self, key: str, default: None | Mapping[str, Sequence[str]] = None
|
| 84 |
+
) -> Mapping[str, Sequence[str]] | None:
|
| 85 |
+
return self._dict.get(key, default)
|
| 86 |
+
|
| 87 |
+
def items(self) -> ItemsView[str, Mapping[str, Sequence[str]]]:
|
| 88 |
+
return self._dict.items()
|
| 89 |
+
|
| 90 |
+
def values(self) -> ValuesView[Mapping[str, Sequence[str]]]:
|
| 91 |
+
return self._dict.values()
|
| 92 |
+
|
| 93 |
+
def keys(self) -> KeysView[str]:
|
| 94 |
+
return self._dict.keys()
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
| 98 |
+
class ComponentInfo:
|
| 99 |
+
name: str
|
| 100 |
+
type: str
|
| 101 |
+
pdbx_synonyms: str
|
| 102 |
+
formula: str
|
| 103 |
+
formula_weight: str
|
| 104 |
+
mon_nstd_parent_comp_id: str
|
| 105 |
+
mon_nstd_flag: str
|
| 106 |
+
pdbx_smiles: str
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def mmcif_to_info(mmcif: Mapping[str, Sequence[str]]) -> ComponentInfo:
|
| 110 |
+
"""Converts CCD mmCIFs to component info. Missing fields are left empty."""
|
| 111 |
+
names = mmcif['_chem_comp.name']
|
| 112 |
+
types = mmcif['_chem_comp.type']
|
| 113 |
+
mon_nstd_parent_comp_ids = mmcif['_chem_comp.mon_nstd_parent_comp_id']
|
| 114 |
+
pdbx_synonyms = mmcif['_chem_comp.pdbx_synonyms']
|
| 115 |
+
formulas = mmcif['_chem_comp.formula']
|
| 116 |
+
formula_weights = mmcif['_chem_comp.formula_weight']
|
| 117 |
+
|
| 118 |
+
def front_or_empty(values: Sequence[str]) -> str:
|
| 119 |
+
return values[0] if values else ''
|
| 120 |
+
|
| 121 |
+
type_ = front_or_empty(types)
|
| 122 |
+
mon_nstd_parent_comp_id = front_or_empty(mon_nstd_parent_comp_ids)
|
| 123 |
+
if type_.lower() == 'non-polymer':
|
| 124 |
+
# Unset for non-polymers, e.g. water or ions.
|
| 125 |
+
mon_nstd_flag = '.'
|
| 126 |
+
elif mon_nstd_parent_comp_id == '?':
|
| 127 |
+
# A standard component - it doesn't have a standard parent, e.g. MET.
|
| 128 |
+
mon_nstd_flag = 'y'
|
| 129 |
+
else:
|
| 130 |
+
# A non-standard component, e.g. MSE.
|
| 131 |
+
mon_nstd_flag = 'n'
|
| 132 |
+
|
| 133 |
+
# Default SMILES is the canonical SMILES, but we fall back to the SMILES if a
|
| 134 |
+
# canonical SMILES is not available. Of canonical SMILES, we prefer ones from
|
| 135 |
+
# the OpenEye OEToolkits program.
|
| 136 |
+
canonical_pdbx_smiles = ''
|
| 137 |
+
fallback_pdbx_smiles = ''
|
| 138 |
+
descriptor_types = mmcif.get('_pdbx_chem_comp_descriptor.type', [])
|
| 139 |
+
descriptors = mmcif.get('_pdbx_chem_comp_descriptor.descriptor', [])
|
| 140 |
+
programs = mmcif.get('_pdbx_chem_comp_descriptor.program', [])
|
| 141 |
+
for descriptor_type, descriptor, program in zip(
|
| 142 |
+
descriptor_types, descriptors, programs
|
| 143 |
+
):
|
| 144 |
+
if descriptor_type == 'SMILES_CANONICAL':
|
| 145 |
+
if (not canonical_pdbx_smiles) or program == 'OpenEye OEToolkits':
|
| 146 |
+
canonical_pdbx_smiles = descriptor
|
| 147 |
+
if not fallback_pdbx_smiles and descriptor_type == 'SMILES':
|
| 148 |
+
fallback_pdbx_smiles = descriptor
|
| 149 |
+
pdbx_smiles = canonical_pdbx_smiles or fallback_pdbx_smiles
|
| 150 |
+
|
| 151 |
+
return ComponentInfo(
|
| 152 |
+
name=front_or_empty(names),
|
| 153 |
+
type=type_,
|
| 154 |
+
pdbx_synonyms=front_or_empty(pdbx_synonyms),
|
| 155 |
+
formula=front_or_empty(formulas),
|
| 156 |
+
formula_weight=front_or_empty(formula_weights),
|
| 157 |
+
mon_nstd_parent_comp_id=mon_nstd_parent_comp_id,
|
| 158 |
+
mon_nstd_flag=mon_nstd_flag,
|
| 159 |
+
pdbx_smiles=pdbx_smiles,
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@functools.lru_cache(maxsize=128)
|
| 164 |
+
def component_name_to_info(ccd: Ccd, res_name: str) -> ComponentInfo | None:
|
| 165 |
+
component = ccd.get(res_name)
|
| 166 |
+
if component is None:
|
| 167 |
+
return None
|
| 168 |
+
return mmcif_to_info(component)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def type_symbol(ccd: Ccd, res_name: str, atom_name: str) -> str:
|
| 172 |
+
"""Returns the element type for the given component name and atom name.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
ccd: The chemical components dictionary.
|
| 176 |
+
res_name: The component name, e.g. ARG.
|
| 177 |
+
atom_name: The atom name, e.g. CB, OXT, or NH1.
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
Element type, e.g. C for (ARG, CB), O for (ARG, OXT), N for (ARG, NH1).
|
| 181 |
+
"""
|
| 182 |
+
res = ccd.get(res_name)
|
| 183 |
+
if res is None:
|
| 184 |
+
return '?'
|
| 185 |
+
try:
|
| 186 |
+
return res['_chem_comp_atom.type_symbol'][
|
| 187 |
+
res['_chem_comp_atom.atom_id'].index(atom_name)
|
| 188 |
+
]
|
| 189 |
+
except (ValueError, IndexError, KeyError):
|
| 190 |
+
return '?'
|
flax_model/alphafold3/constants/converters/__init__.py
ADDED
|
File without changes
|
flax_model/alphafold3/constants/converters/ccd_pickle_gen.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Reads Chemical Components gz file and generates a CCD pickle file."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Sequence
|
| 6 |
+
import gzip
|
| 7 |
+
import pickle
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
from flax_model.alphafold3.cpp import cif_dict
|
| 11 |
+
import tqdm
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main(argv: Sequence[str]) -> None:
|
| 15 |
+
if len(argv) != 3:
|
| 16 |
+
raise ValueError('Must specify input_file components.cif and output_file')
|
| 17 |
+
|
| 18 |
+
_, input_file, output_file = argv
|
| 19 |
+
|
| 20 |
+
print(f'Parsing {input_file}', flush=True)
|
| 21 |
+
if input_file.endswith('.gz'):
|
| 22 |
+
opener = gzip.open
|
| 23 |
+
else:
|
| 24 |
+
opener = open
|
| 25 |
+
|
| 26 |
+
with opener(input_file, 'rb') as f:
|
| 27 |
+
whole_file = f.read()
|
| 28 |
+
result = {
|
| 29 |
+
key: {k: tuple(v) for k, v in value.items()}
|
| 30 |
+
for key, value in tqdm.tqdm(
|
| 31 |
+
cif_dict.parse_multi_data_cif(whole_file).items(), disable=None
|
| 32 |
+
)
|
| 33 |
+
}
|
| 34 |
+
assert len(result) == whole_file.count(b'data_')
|
| 35 |
+
|
| 36 |
+
print(f'Writing {output_file}', flush=True)
|
| 37 |
+
with open(output_file, 'wb') as f:
|
| 38 |
+
pickle.dump(result, f, protocol=pickle.HIGHEST_PROTOCOL)
|
| 39 |
+
print('Done', flush=True)
|
| 40 |
+
|
| 41 |
+
if __name__ == '__main__':
|
| 42 |
+
main(sys.argv)
|
flax_model/alphafold3/constants/converters/chemical_component_sets_gen.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Script for updating chemical_component_sets.py."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping, Sequence
|
| 6 |
+
import pathlib
|
| 7 |
+
import pickle
|
| 8 |
+
import re
|
| 9 |
+
import sys
|
| 10 |
+
|
| 11 |
+
from flax_model.alphafold3.common import resources
|
| 12 |
+
import tqdm
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
_CCD_PICKLE_FILE = resources.filename(
|
| 16 |
+
'constants/converters/ccd.pickle'
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def find_ions_and_glycans_in_ccd(
|
| 21 |
+
ccd: Mapping[str, Mapping[str, Sequence[str]]],
|
| 22 |
+
) -> dict[str, frozenset[str]]:
|
| 23 |
+
"""Finds glycans and ions in all version of CCD."""
|
| 24 |
+
glycans_linking = []
|
| 25 |
+
glycans_other = []
|
| 26 |
+
ions = []
|
| 27 |
+
for name, comp in tqdm.tqdm(ccd.items(), disable=None):
|
| 28 |
+
if name == 'UNX':
|
| 29 |
+
continue # Skip "unknown atom or ion".
|
| 30 |
+
comp_type = comp['_chem_comp.type'][0].lower()
|
| 31 |
+
# Glycans have the type 'saccharide'.
|
| 32 |
+
if re.findall(r'\bsaccharide\b', comp_type):
|
| 33 |
+
# Separate out linking glycans from others.
|
| 34 |
+
if 'linking' in comp_type:
|
| 35 |
+
glycans_linking.append(name)
|
| 36 |
+
else:
|
| 37 |
+
glycans_other.append(name)
|
| 38 |
+
|
| 39 |
+
# Ions have the word 'ion' in their name.
|
| 40 |
+
comp_name = comp['_chem_comp.name'][0].lower()
|
| 41 |
+
if re.findall(r'\bion\b', comp_name):
|
| 42 |
+
ions.append(name)
|
| 43 |
+
result = dict(
|
| 44 |
+
glycans_linking=frozenset(glycans_linking),
|
| 45 |
+
glycans_other=frozenset(glycans_other),
|
| 46 |
+
ions=frozenset(ions),
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
return result
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def main(argv: Sequence[str]) -> None:
|
| 53 |
+
if len(argv) != 2:
|
| 54 |
+
raise ValueError(
|
| 55 |
+
'Directory to write to must be specified as a command-line arguments.'
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
print(f'Loading {_CCD_PICKLE_FILE}', flush=True)
|
| 59 |
+
with open(_CCD_PICKLE_FILE, 'rb') as f:
|
| 60 |
+
ccd: Mapping[str, Mapping[str, Sequence[str]]] = pickle.load(f)
|
| 61 |
+
output_path = pathlib.Path(argv[1])
|
| 62 |
+
output_path.parent.mkdir(exist_ok=True)
|
| 63 |
+
print('Finding ions and glycans', flush=True)
|
| 64 |
+
result = find_ions_and_glycans_in_ccd(ccd)
|
| 65 |
+
print(f'writing to {output_path}', flush=True)
|
| 66 |
+
with output_path.open('wb') as f:
|
| 67 |
+
pickle.dump(result, f)
|
| 68 |
+
print('Done', flush=True)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
if __name__ == '__main__':
|
| 72 |
+
main(sys.argv)
|
flax_model/alphafold3/constants/mmcif_names.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Names of things in mmCIF format.
|
| 4 |
+
|
| 5 |
+
See https://www.iucr.org/__data/iucr/cifdic_html/2/cif_mm.dic/index.html
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from collections.abc import Mapping, Sequence, Set
|
| 9 |
+
from typing import Final
|
| 10 |
+
|
| 11 |
+
from flax_model.alphafold3.constants import atom_types
|
| 12 |
+
from flax_model.alphafold3.constants import residue_names
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# The following are all possible values for the "_entity.type".
|
| 16 |
+
# https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Items/_entity.type.html
|
| 17 |
+
BRANCHED_CHAIN: Final[str] = 'branched'
|
| 18 |
+
MACROLIDE_CHAIN: Final[str] = 'macrolide'
|
| 19 |
+
NON_POLYMER_CHAIN: Final[str] = 'non-polymer'
|
| 20 |
+
POLYMER_CHAIN: Final[str] = 'polymer'
|
| 21 |
+
WATER: Final[str] = 'water'
|
| 22 |
+
|
| 23 |
+
CYCLIC_PSEUDO_PEPTIDE_CHAIN: Final[str] = 'cyclic-pseudo-peptide'
|
| 24 |
+
DNA_CHAIN: Final[str] = 'polydeoxyribonucleotide'
|
| 25 |
+
DNA_RNA_HYBRID_CHAIN: Final[str] = (
|
| 26 |
+
'polydeoxyribonucleotide/polyribonucleotide hybrid'
|
| 27 |
+
)
|
| 28 |
+
OTHER_CHAIN: Final[str] = 'other'
|
| 29 |
+
PEPTIDE_NUCLEIC_ACID_CHAIN: Final[str] = 'peptide nucleic acid'
|
| 30 |
+
POLYPEPTIDE_D_CHAIN: Final[str] = 'polypeptide(D)'
|
| 31 |
+
PROTEIN_CHAIN: Final[str] = 'polypeptide(L)'
|
| 32 |
+
RNA_CHAIN: Final[str] = 'polyribonucleotide'
|
| 33 |
+
|
| 34 |
+
# Most common _entity_poly.types.
|
| 35 |
+
STANDARD_POLYMER_CHAIN_TYPES: Final[Set[str]] = {
|
| 36 |
+
PROTEIN_CHAIN,
|
| 37 |
+
DNA_CHAIN,
|
| 38 |
+
RNA_CHAIN,
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
# Possible values for _entity.type other than polymer and water.
|
| 42 |
+
LIGAND_CHAIN_TYPES: Final[Set[str]] = {
|
| 43 |
+
BRANCHED_CHAIN,
|
| 44 |
+
MACROLIDE_CHAIN,
|
| 45 |
+
NON_POLYMER_CHAIN,
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
# Possible values for _entity.type other than polymer.
|
| 49 |
+
NON_POLYMER_CHAIN_TYPES: Final[Set[str]] = {
|
| 50 |
+
*LIGAND_CHAIN_TYPES,
|
| 51 |
+
WATER,
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
# Peptide possible values for _entity_poly.type.
|
| 55 |
+
PEPTIDE_CHAIN_TYPES: Final[Set[str]] = {
|
| 56 |
+
CYCLIC_PSEUDO_PEPTIDE_CHAIN,
|
| 57 |
+
POLYPEPTIDE_D_CHAIN,
|
| 58 |
+
PROTEIN_CHAIN,
|
| 59 |
+
PEPTIDE_NUCLEIC_ACID_CHAIN,
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# Nucleic-acid possible values for _entity_poly.type.
|
| 64 |
+
NUCLEIC_ACID_CHAIN_TYPES: Final[Set[str]] = {
|
| 65 |
+
RNA_CHAIN,
|
| 66 |
+
DNA_CHAIN,
|
| 67 |
+
DNA_RNA_HYBRID_CHAIN,
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
# All possible values for _entity_poly.type.
|
| 71 |
+
POLYMER_CHAIN_TYPES: Final[Set[str]] = {
|
| 72 |
+
*NUCLEIC_ACID_CHAIN_TYPES,
|
| 73 |
+
*PEPTIDE_CHAIN_TYPES,
|
| 74 |
+
OTHER_CHAIN,
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
TERMINAL_OXYGENS: Final[Mapping[str, str]] = {
|
| 79 |
+
PROTEIN_CHAIN: 'OXT',
|
| 80 |
+
DNA_CHAIN: 'OP3',
|
| 81 |
+
RNA_CHAIN: 'OP3',
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# For each chain type, which atom should be used to represent each residue.
|
| 86 |
+
RESIDUE_REPRESENTATIVE_ATOMS: Final[Mapping[str, str]] = {
|
| 87 |
+
PROTEIN_CHAIN: atom_types.CA,
|
| 88 |
+
DNA_CHAIN: atom_types.C1PRIME,
|
| 89 |
+
RNA_CHAIN: atom_types.C1PRIME,
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
# Methods involving crystallization. See the documentation at
|
| 93 |
+
# mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Items/_exptl.method.html
|
| 94 |
+
# for the full list of experimental methods.
|
| 95 |
+
CRYSTALLIZATION_METHODS: Final[Set[str]] = {
|
| 96 |
+
'X-RAY DIFFRACTION',
|
| 97 |
+
'NEUTRON DIFFRACTION',
|
| 98 |
+
'ELECTRON CRYSTALLOGRAPHY',
|
| 99 |
+
'POWDER CRYSTALLOGRAPHY',
|
| 100 |
+
'FIBER DIFFRACTION',
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# Possible bond types.
|
| 104 |
+
COVALENT_BOND: Final[str] = 'covale'
|
| 105 |
+
HYDROGEN_BOND: Final[str] = 'hydrog'
|
| 106 |
+
METAL_COORDINATION: Final[str] = 'metalc'
|
| 107 |
+
DISULFIDE_BRIDGE: Final[str] = 'disulf'
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def is_standard_polymer_type(chain_type: str) -> bool:
|
| 111 |
+
"""Returns if chain type is a protein, DNA or RNA chain type.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
chain_type: The type of the chain.
|
| 115 |
+
|
| 116 |
+
Returns:
|
| 117 |
+
A bool for if the chain_type matches protein, DNA, or RNA.
|
| 118 |
+
"""
|
| 119 |
+
return chain_type in STANDARD_POLYMER_CHAIN_TYPES
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def guess_polymer_type(chain_residues: Sequence[str]) -> str:
|
| 123 |
+
"""Guess the polymer type (protein/rna/dna/other) based on the residues.
|
| 124 |
+
|
| 125 |
+
The polymer type is guessed by first checking for any of the standard
|
| 126 |
+
protein residues. If one is present then the chain is considered to be a
|
| 127 |
+
polypeptide. Otherwise we decide by counting residue types and deciding by
|
| 128 |
+
majority voting (e.g. mostly DNA residues -> DNA). If there is a tie between
|
| 129 |
+
the counts, the ordering is rna > dna > other.
|
| 130 |
+
|
| 131 |
+
Note that we count MSE and UNK as protein residues.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
chain_residues: A sequence of full residue name (1-letter for DNA, 2-letters
|
| 135 |
+
for RNA, 3 for protein). The _atom_site.label_comp_id column in mmCIF.
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
The most probable chain type as set in the _entity_poly mmCIF table:
|
| 139 |
+
protein - polypeptide(L), rna - polyribonucleotide,
|
| 140 |
+
dna - polydeoxyribonucleotide or other.
|
| 141 |
+
"""
|
| 142 |
+
residue_types = {
|
| 143 |
+
**{r: RNA_CHAIN for r in residue_names.RNA_TYPES},
|
| 144 |
+
**{r: DNA_CHAIN for r in residue_names.DNA_TYPES},
|
| 145 |
+
**{r: PROTEIN_CHAIN for r in residue_names.PROTEIN_TYPES_WITH_UNKNOWN},
|
| 146 |
+
residue_names.MSE: PROTEIN_CHAIN,
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
counts = {PROTEIN_CHAIN: 0, RNA_CHAIN: 0, DNA_CHAIN: 0, OTHER_CHAIN: 0}
|
| 150 |
+
for residue in chain_residues:
|
| 151 |
+
residue_type = residue_types.get(residue, OTHER_CHAIN)
|
| 152 |
+
# If we ever see a protein residue we'll consider this a polypeptide(L).
|
| 153 |
+
if residue_type == PROTEIN_CHAIN:
|
| 154 |
+
return residue_type
|
| 155 |
+
counts[residue_type] += 1
|
| 156 |
+
|
| 157 |
+
# Make sure protein > rna > dna > other if there is a tie.
|
| 158 |
+
tie_braker = {PROTEIN_CHAIN: 3, RNA_CHAIN: 2, DNA_CHAIN: 1, OTHER_CHAIN: 0}
|
| 159 |
+
|
| 160 |
+
def order_fn(item):
|
| 161 |
+
name, count = item
|
| 162 |
+
return count, tie_braker[name]
|
| 163 |
+
|
| 164 |
+
most_probable_type = max(counts.items(), key=order_fn)[0]
|
| 165 |
+
return most_probable_type
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def fix_non_standard_polymer_res(*, res_name: str, chain_type: str) -> str:
|
| 169 |
+
"""Returns the res_name of the closest standard protein/RNA/DNA residue.
|
| 170 |
+
|
| 171 |
+
Optimized for the case where a single residue needs to be converted.
|
| 172 |
+
|
| 173 |
+
If res_name is already a standard type, it is returned unaltered.
|
| 174 |
+
If a match cannot be found, returns 'UNK' for protein chains and 'N' for
|
| 175 |
+
RNA/DNA chains.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
res_name: A residue_name (monomer code from the CCD).
|
| 179 |
+
chain_type: The type of the chain, must be PROTEIN_CHAIN, RNA_CHAIN or
|
| 180 |
+
DNA_CHAIN.
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
An element from PROTEIN_TYPES_WITH_UNKNOWN | RNA_TYPES | DNA_TYPES | {'N'}.
|
| 184 |
+
|
| 185 |
+
Raises:
|
| 186 |
+
ValueError: If chain_type not in PEPTIDE_CHAIN_TYPES or
|
| 187 |
+
{OTHER_CHAIN, RNA_CHAIN, DNA_CHAIN, DNA_RNA_HYBRID_CHAIN}.
|
| 188 |
+
"""
|
| 189 |
+
# Map to one letter code, then back to common res_names.
|
| 190 |
+
one_letter_code = residue_names.letters_three_to_one(res_name, default='X')
|
| 191 |
+
|
| 192 |
+
if chain_type in PEPTIDE_CHAIN_TYPES or chain_type == OTHER_CHAIN:
|
| 193 |
+
return residue_names.PROTEIN_COMMON_ONE_TO_THREE.get(one_letter_code, 'UNK')
|
| 194 |
+
elif chain_type == RNA_CHAIN:
|
| 195 |
+
# RNA's CCD monomer code is single-letter.
|
| 196 |
+
return (
|
| 197 |
+
one_letter_code if one_letter_code in residue_names.RNA_TYPES else 'N'
|
| 198 |
+
)
|
| 199 |
+
elif chain_type == DNA_CHAIN:
|
| 200 |
+
return residue_names.DNA_COMMON_ONE_TO_TWO.get(one_letter_code, 'N')
|
| 201 |
+
elif chain_type == DNA_RNA_HYBRID_CHAIN:
|
| 202 |
+
return (
|
| 203 |
+
res_name
|
| 204 |
+
if res_name in residue_names.NUCLEIC_TYPES_WITH_UNKNOWN
|
| 205 |
+
else 'N'
|
| 206 |
+
)
|
| 207 |
+
else:
|
| 208 |
+
raise ValueError(f'Expected a protein/DNA/RNA chain but got {chain_type}')
|
flax_model/alphafold3/constants/periodic_table.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Periodic table of elements."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping, Sequence
|
| 6 |
+
import dataclasses
|
| 7 |
+
from typing import Final
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
| 13 |
+
class Element:
|
| 14 |
+
name: str
|
| 15 |
+
number: int
|
| 16 |
+
symbol: str
|
| 17 |
+
weight: float
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# Weights taken from rdkit/Code/GraphMol/atomic_data.cpp for compatibility.
|
| 21 |
+
# pylint: disable=invalid-name
|
| 22 |
+
|
| 23 |
+
# X is an unknown element that can be present in the CCD,
|
| 24 |
+
# https://www.rcsb.org/ligand/UNX.
|
| 25 |
+
X: Final[Element] = Element(name='Unknown', number=0, symbol='X', weight=0.0)
|
| 26 |
+
H: Final[Element] = Element(name='Hydrogen', number=1, symbol='H', weight=1.008)
|
| 27 |
+
He: Final[Element] = Element(name='Helium', number=2, symbol='He', weight=4.003)
|
| 28 |
+
Li: Final[Element] = Element(
|
| 29 |
+
name='Lithium', number=3, symbol='Li', weight=6.941
|
| 30 |
+
)
|
| 31 |
+
Be: Final[Element] = Element(
|
| 32 |
+
name='Beryllium', number=4, symbol='Be', weight=9.012
|
| 33 |
+
)
|
| 34 |
+
B: Final[Element] = Element(name='Boron', number=5, symbol='B', weight=10.812)
|
| 35 |
+
C: Final[Element] = Element(name='Carbon', number=6, symbol='C', weight=12.011)
|
| 36 |
+
N: Final[Element] = Element(
|
| 37 |
+
name='Nitrogen', number=7, symbol='N', weight=14.007
|
| 38 |
+
)
|
| 39 |
+
O: Final[Element] = Element(name='Oxygen', number=8, symbol='O', weight=15.999)
|
| 40 |
+
F: Final[Element] = Element(
|
| 41 |
+
name='Fluorine', number=9, symbol='F', weight=18.998
|
| 42 |
+
)
|
| 43 |
+
Ne: Final[Element] = Element(name='Neon', number=10, symbol='Ne', weight=20.18)
|
| 44 |
+
Na: Final[Element] = Element(
|
| 45 |
+
name='Sodium', number=11, symbol='Na', weight=22.99
|
| 46 |
+
)
|
| 47 |
+
Mg: Final[Element] = Element(
|
| 48 |
+
name='Magnesium', number=12, symbol='Mg', weight=24.305
|
| 49 |
+
)
|
| 50 |
+
Al: Final[Element] = Element(
|
| 51 |
+
name='Aluminium', number=13, symbol='Al', weight=26.982
|
| 52 |
+
)
|
| 53 |
+
Si: Final[Element] = Element(
|
| 54 |
+
name='Silicon', number=14, symbol='Si', weight=28.086
|
| 55 |
+
)
|
| 56 |
+
P: Final[Element] = Element(
|
| 57 |
+
name='Phosphorus', number=15, symbol='P', weight=30.974
|
| 58 |
+
)
|
| 59 |
+
S: Final[Element] = Element(name='Sulfur', number=16, symbol='S', weight=32.067)
|
| 60 |
+
Cl: Final[Element] = Element(
|
| 61 |
+
name='Chlorine', number=17, symbol='Cl', weight=35.453
|
| 62 |
+
)
|
| 63 |
+
Ar: Final[Element] = Element(
|
| 64 |
+
name='Argon', number=18, symbol='Ar', weight=39.948
|
| 65 |
+
)
|
| 66 |
+
K: Final[Element] = Element(
|
| 67 |
+
name='Potassium', number=19, symbol='K', weight=39.098
|
| 68 |
+
)
|
| 69 |
+
Ca: Final[Element] = Element(
|
| 70 |
+
name='Calcium', number=20, symbol='Ca', weight=40.078
|
| 71 |
+
)
|
| 72 |
+
Sc: Final[Element] = Element(
|
| 73 |
+
name='Scandium', number=21, symbol='Sc', weight=44.956
|
| 74 |
+
)
|
| 75 |
+
Ti: Final[Element] = Element(
|
| 76 |
+
name='Titanium', number=22, symbol='Ti', weight=47.867
|
| 77 |
+
)
|
| 78 |
+
V: Final[Element] = Element(
|
| 79 |
+
name='Vanadium', number=23, symbol='V', weight=50.942
|
| 80 |
+
)
|
| 81 |
+
Cr: Final[Element] = Element(
|
| 82 |
+
name='Chromium', number=24, symbol='Cr', weight=51.996
|
| 83 |
+
)
|
| 84 |
+
Mn: Final[Element] = Element(
|
| 85 |
+
name='Manganese', number=25, symbol='Mn', weight=54.938
|
| 86 |
+
)
|
| 87 |
+
Fe: Final[Element] = Element(name='Iron', number=26, symbol='Fe', weight=55.845)
|
| 88 |
+
Co: Final[Element] = Element(
|
| 89 |
+
name='Cobalt', number=27, symbol='Co', weight=58.933
|
| 90 |
+
)
|
| 91 |
+
Ni: Final[Element] = Element(
|
| 92 |
+
name='Nickel', number=28, symbol='Ni', weight=58.693
|
| 93 |
+
)
|
| 94 |
+
Cu: Final[Element] = Element(
|
| 95 |
+
name='Copper', number=29, symbol='Cu', weight=63.546
|
| 96 |
+
)
|
| 97 |
+
Zn: Final[Element] = Element(name='Zinc', number=30, symbol='Zn', weight=65.39)
|
| 98 |
+
Ga: Final[Element] = Element(
|
| 99 |
+
name='Gallium', number=31, symbol='Ga', weight=69.723
|
| 100 |
+
)
|
| 101 |
+
Ge: Final[Element] = Element(
|
| 102 |
+
name='Germanium', number=32, symbol='Ge', weight=72.61
|
| 103 |
+
)
|
| 104 |
+
As: Final[Element] = Element(
|
| 105 |
+
name='Arsenic', number=33, symbol='As', weight=74.922
|
| 106 |
+
)
|
| 107 |
+
Se: Final[Element] = Element(
|
| 108 |
+
name='Selenium', number=34, symbol='Se', weight=78.96
|
| 109 |
+
)
|
| 110 |
+
Br: Final[Element] = Element(
|
| 111 |
+
name='Bromine', number=35, symbol='Br', weight=79.904
|
| 112 |
+
)
|
| 113 |
+
Kr: Final[Element] = Element(
|
| 114 |
+
name='Krypton', number=36, symbol='Kr', weight=83.8
|
| 115 |
+
)
|
| 116 |
+
Rb: Final[Element] = Element(
|
| 117 |
+
name='Rubidium', number=37, symbol='Rb', weight=85.468
|
| 118 |
+
)
|
| 119 |
+
Sr: Final[Element] = Element(
|
| 120 |
+
name='Strontium', number=38, symbol='Sr', weight=87.62
|
| 121 |
+
)
|
| 122 |
+
Y: Final[Element] = Element(
|
| 123 |
+
name='Yttrium', number=39, symbol='Y', weight=88.906
|
| 124 |
+
)
|
| 125 |
+
Zr: Final[Element] = Element(
|
| 126 |
+
name='Zirconium', number=40, symbol='Zr', weight=91.224
|
| 127 |
+
)
|
| 128 |
+
Nb: Final[Element] = Element(
|
| 129 |
+
name='Niobiu', number=41, symbol='Nb', weight=92.906
|
| 130 |
+
)
|
| 131 |
+
Mo: Final[Element] = Element(
|
| 132 |
+
name='Molybdenum', number=42, symbol='Mo', weight=95.94
|
| 133 |
+
)
|
| 134 |
+
Tc: Final[Element] = Element(
|
| 135 |
+
name='Technetium', number=43, symbol='Tc', weight=98
|
| 136 |
+
)
|
| 137 |
+
Ru: Final[Element] = Element(
|
| 138 |
+
name='Ruthenium', number=44, symbol='Ru', weight=101.07
|
| 139 |
+
)
|
| 140 |
+
Rh: Final[Element] = Element(
|
| 141 |
+
name='Rhodium', number=45, symbol='Rh', weight=102.906
|
| 142 |
+
)
|
| 143 |
+
Pd: Final[Element] = Element(
|
| 144 |
+
name='Palladium', number=46, symbol='Pd', weight=106.42
|
| 145 |
+
)
|
| 146 |
+
Ag: Final[Element] = Element(
|
| 147 |
+
name='Silver', number=47, symbol='Ag', weight=107.868
|
| 148 |
+
)
|
| 149 |
+
Cd: Final[Element] = Element(
|
| 150 |
+
name='Cadmium', number=48, symbol='Cd', weight=112.412
|
| 151 |
+
)
|
| 152 |
+
In: Final[Element] = Element(
|
| 153 |
+
name='Indium', number=49, symbol='In', weight=114.818
|
| 154 |
+
)
|
| 155 |
+
Sn: Final[Element] = Element(name='Tin', number=50, symbol='Sn', weight=118.711)
|
| 156 |
+
Sb: Final[Element] = Element(
|
| 157 |
+
name='Antimony', number=51, symbol='Sb', weight=121.76
|
| 158 |
+
)
|
| 159 |
+
Te: Final[Element] = Element(
|
| 160 |
+
name='Tellurium', number=52, symbol='Te', weight=127.6
|
| 161 |
+
)
|
| 162 |
+
I: Final[Element] = Element(
|
| 163 |
+
name='Iodine', number=53, symbol='I', weight=126.904
|
| 164 |
+
)
|
| 165 |
+
Xe: Final[Element] = Element(
|
| 166 |
+
name='Xenon', number=54, symbol='Xe', weight=131.29
|
| 167 |
+
)
|
| 168 |
+
Cs: Final[Element] = Element(
|
| 169 |
+
name='Caesium', number=55, symbol='Cs', weight=132.905
|
| 170 |
+
)
|
| 171 |
+
Ba: Final[Element] = Element(
|
| 172 |
+
name='Barium', number=56, symbol='Ba', weight=137.328
|
| 173 |
+
)
|
| 174 |
+
La: Final[Element] = Element(
|
| 175 |
+
name='Lanthanum', number=57, symbol='La', weight=138.906
|
| 176 |
+
)
|
| 177 |
+
Ce: Final[Element] = Element(
|
| 178 |
+
name='Cerium', number=58, symbol='Ce', weight=140.116
|
| 179 |
+
)
|
| 180 |
+
Pr: Final[Element] = Element(
|
| 181 |
+
name='Praseodymium', number=59, symbol='Pr', weight=140.908
|
| 182 |
+
)
|
| 183 |
+
Nd: Final[Element] = Element(
|
| 184 |
+
name='Neodymium', number=60, symbol='Nd', weight=144.24
|
| 185 |
+
)
|
| 186 |
+
Pm: Final[Element] = Element(
|
| 187 |
+
name='Promethium', number=61, symbol='Pm', weight=145
|
| 188 |
+
)
|
| 189 |
+
Sm: Final[Element] = Element(
|
| 190 |
+
name='Samarium', number=62, symbol='Sm', weight=150.36
|
| 191 |
+
)
|
| 192 |
+
Eu: Final[Element] = Element(
|
| 193 |
+
name='Europium', number=63, symbol='Eu', weight=151.964
|
| 194 |
+
)
|
| 195 |
+
Gd: Final[Element] = Element(
|
| 196 |
+
name='Gadolinium', number=64, symbol='Gd', weight=157.25
|
| 197 |
+
)
|
| 198 |
+
Tb: Final[Element] = Element(
|
| 199 |
+
name='Terbium', number=65, symbol='Tb', weight=158.925
|
| 200 |
+
)
|
| 201 |
+
Dy: Final[Element] = Element(
|
| 202 |
+
name='Dysprosium', number=66, symbol='Dy', weight=162.5
|
| 203 |
+
)
|
| 204 |
+
Ho: Final[Element] = Element(
|
| 205 |
+
name='Holmium', number=67, symbol='Ho', weight=164.93
|
| 206 |
+
)
|
| 207 |
+
Er: Final[Element] = Element(
|
| 208 |
+
name='Erbium', number=68, symbol='Er', weight=167.26
|
| 209 |
+
)
|
| 210 |
+
Tm: Final[Element] = Element(
|
| 211 |
+
name='Thulium', number=69, symbol='Tm', weight=168.934
|
| 212 |
+
)
|
| 213 |
+
Yb: Final[Element] = Element(
|
| 214 |
+
name='Ytterbium', number=70, symbol='Yb', weight=173.04
|
| 215 |
+
)
|
| 216 |
+
Lu: Final[Element] = Element(
|
| 217 |
+
name='Lutetium', number=71, symbol='Lu', weight=174.967
|
| 218 |
+
)
|
| 219 |
+
Hf: Final[Element] = Element(
|
| 220 |
+
name='Hafnium', number=72, symbol='Hf', weight=178.49
|
| 221 |
+
)
|
| 222 |
+
Ta: Final[Element] = Element(
|
| 223 |
+
name='Tantalum', number=73, symbol='Ta', weight=180.948
|
| 224 |
+
)
|
| 225 |
+
W: Final[Element] = Element(
|
| 226 |
+
name='Tungsten', number=74, symbol='W', weight=183.84
|
| 227 |
+
)
|
| 228 |
+
Re: Final[Element] = Element(
|
| 229 |
+
name='Rhenium', number=75, symbol='Re', weight=186.207
|
| 230 |
+
)
|
| 231 |
+
Os: Final[Element] = Element(
|
| 232 |
+
name='Osmium', number=76, symbol='Os', weight=190.23
|
| 233 |
+
)
|
| 234 |
+
Ir: Final[Element] = Element(
|
| 235 |
+
name='Iridium', number=77, symbol='Ir', weight=192.217
|
| 236 |
+
)
|
| 237 |
+
Pt: Final[Element] = Element(
|
| 238 |
+
name='Platinum', number=78, symbol='Pt', weight=195.078
|
| 239 |
+
)
|
| 240 |
+
Au: Final[Element] = Element(
|
| 241 |
+
name='Gold', number=79, symbol='Au', weight=196.967
|
| 242 |
+
)
|
| 243 |
+
Hg: Final[Element] = Element(
|
| 244 |
+
name='Mercury', number=80, symbol='Hg', weight=200.59
|
| 245 |
+
)
|
| 246 |
+
Tl: Final[Element] = Element(
|
| 247 |
+
name='Thallium', number=81, symbol='Tl', weight=204.383
|
| 248 |
+
)
|
| 249 |
+
Pb: Final[Element] = Element(name='Lead', number=82, symbol='Pb', weight=207.2)
|
| 250 |
+
Bi: Final[Element] = Element(
|
| 251 |
+
name='Bismuth', number=83, symbol='Bi', weight=208.98
|
| 252 |
+
)
|
| 253 |
+
Po: Final[Element] = Element(
|
| 254 |
+
name='Polonium', number=84, symbol='Po', weight=209
|
| 255 |
+
)
|
| 256 |
+
At: Final[Element] = Element(
|
| 257 |
+
name='Astatine', number=85, symbol='At', weight=210
|
| 258 |
+
)
|
| 259 |
+
Rn: Final[Element] = Element(name='Radon', number=86, symbol='Rn', weight=222)
|
| 260 |
+
Fr: Final[Element] = Element(
|
| 261 |
+
name='Francium', number=87, symbol='Fr', weight=223
|
| 262 |
+
)
|
| 263 |
+
Ra: Final[Element] = Element(name='Radium', number=88, symbol='Ra', weight=226)
|
| 264 |
+
Ac: Final[Element] = Element(
|
| 265 |
+
name='Actinium', number=89, symbol='Ac', weight=227
|
| 266 |
+
)
|
| 267 |
+
Th: Final[Element] = Element(
|
| 268 |
+
name='Thorium', number=90, symbol='Th', weight=232.038
|
| 269 |
+
)
|
| 270 |
+
Pa: Final[Element] = Element(
|
| 271 |
+
name='Protactinium', number=91, symbol='Pa', weight=231.036
|
| 272 |
+
)
|
| 273 |
+
U: Final[Element] = Element(
|
| 274 |
+
name='Uranium', number=92, symbol='U', weight=238.029
|
| 275 |
+
)
|
| 276 |
+
Np: Final[Element] = Element(
|
| 277 |
+
name='Neptunium', number=93, symbol='Np', weight=237
|
| 278 |
+
)
|
| 279 |
+
Pu: Final[Element] = Element(
|
| 280 |
+
name='Plutonium', number=94, symbol='Pu', weight=244
|
| 281 |
+
)
|
| 282 |
+
Am: Final[Element] = Element(
|
| 283 |
+
name='Americium', number=95, symbol='Am', weight=243
|
| 284 |
+
)
|
| 285 |
+
Cm: Final[Element] = Element(name='Curium', number=96, symbol='Cm', weight=247)
|
| 286 |
+
Bk: Final[Element] = Element(
|
| 287 |
+
name='Berkelium', number=97, symbol='Bk', weight=247
|
| 288 |
+
)
|
| 289 |
+
Cf: Final[Element] = Element(
|
| 290 |
+
name='Californium', number=98, symbol='Cf', weight=251
|
| 291 |
+
)
|
| 292 |
+
Es: Final[Element] = Element(
|
| 293 |
+
name='Einsteinium', number=99, symbol='Es', weight=252
|
| 294 |
+
)
|
| 295 |
+
Fm: Final[Element] = Element(
|
| 296 |
+
name='Fermium', number=100, symbol='Fm', weight=257
|
| 297 |
+
)
|
| 298 |
+
Md: Final[Element] = Element(
|
| 299 |
+
name='Mendelevium', number=101, symbol='Md', weight=258
|
| 300 |
+
)
|
| 301 |
+
No: Final[Element] = Element(
|
| 302 |
+
name='Nobelium', number=102, symbol='No', weight=259
|
| 303 |
+
)
|
| 304 |
+
Lr: Final[Element] = Element(
|
| 305 |
+
name='Lawrencium', number=103, symbol='Lr', weight=262
|
| 306 |
+
)
|
| 307 |
+
Rf: Final[Element] = Element(
|
| 308 |
+
name='Rutherfordium', number=104, symbol='Rf', weight=267
|
| 309 |
+
)
|
| 310 |
+
Db: Final[Element] = Element(
|
| 311 |
+
name='Dubnium', number=105, symbol='Db', weight=268
|
| 312 |
+
)
|
| 313 |
+
Sg: Final[Element] = Element(
|
| 314 |
+
name='Seaborgium', number=106, symbol='Sg', weight=269
|
| 315 |
+
)
|
| 316 |
+
Bh: Final[Element] = Element(
|
| 317 |
+
name='Bohrium', number=107, symbol='Bh', weight=270
|
| 318 |
+
)
|
| 319 |
+
Hs: Final[Element] = Element(
|
| 320 |
+
name='Hassium', number=108, symbol='Hs', weight=269
|
| 321 |
+
)
|
| 322 |
+
Mt: Final[Element] = Element(
|
| 323 |
+
name='Meitnerium', number=109, symbol='Mt', weight=278
|
| 324 |
+
)
|
| 325 |
+
Ds: Final[Element] = Element(
|
| 326 |
+
name='Darmstadtium', number=110, symbol='Ds', weight=281
|
| 327 |
+
)
|
| 328 |
+
Rg: Final[Element] = Element(
|
| 329 |
+
name='Roentgenium', number=111, symbol='Rg', weight=281
|
| 330 |
+
)
|
| 331 |
+
Cn: Final[Element] = Element(
|
| 332 |
+
name='Copernicium', number=112, symbol='Cn', weight=285
|
| 333 |
+
)
|
| 334 |
+
Nh: Final[Element] = Element(
|
| 335 |
+
name='Nihonium', number=113, symbol='Nh', weight=284
|
| 336 |
+
)
|
| 337 |
+
Fl: Final[Element] = Element(
|
| 338 |
+
name='Flerovium', number=114, symbol='Fl', weight=289
|
| 339 |
+
)
|
| 340 |
+
Mc: Final[Element] = Element(
|
| 341 |
+
name='Moscovium', number=115, symbol='Mc', weight=288
|
| 342 |
+
)
|
| 343 |
+
Lv: Final[Element] = Element(
|
| 344 |
+
name='Livermorium', number=116, symbol='Lv', weight=293
|
| 345 |
+
)
|
| 346 |
+
Ts: Final[Element] = Element(
|
| 347 |
+
name='Tennessine', number=117, symbol='Ts', weight=292
|
| 348 |
+
)
|
| 349 |
+
Og: Final[Element] = Element(
|
| 350 |
+
name='Oganesson', number=118, symbol='Og', weight=294
|
| 351 |
+
)
|
| 352 |
+
# pylint: enable=invalid-name
|
| 353 |
+
|
| 354 |
+
# fmt: off
|
| 355 |
+
# Lanthanides
|
| 356 |
+
_L: Final[Sequence[Element]] = (
|
| 357 |
+
La, Ce, Pr, Nd, Pm, Sm, Eu, Gd, Tb, Dy, Ho, Er, Tm, Yb, Lu)
|
| 358 |
+
# Actinides
|
| 359 |
+
_A: Final[Sequence[Element]] = (
|
| 360 |
+
Ac, Th, Pa, U, Np, Pu, Am, Cm, Bk, Cf, Es, Fm, Md, No, Lr)
|
| 361 |
+
|
| 362 |
+
# pylint: disable=bad-whitespace
|
| 363 |
+
PERIODIC_TABLE: Final[Sequence[Element]] = (
|
| 364 |
+
X, # Unknown
|
| 365 |
+
H, He,
|
| 366 |
+
Li, Be, B, C, N, O, F, Ne,
|
| 367 |
+
Na, Mg, Al, Si, P, S, Cl, Ar,
|
| 368 |
+
K, Ca, Sc, Ti, V, Cr, Mn, Fe, Co, Ni, Cu, Zn, Ga, Ge, As, Se, Br, Kr,
|
| 369 |
+
Rb, Sr, Y, Zr, Nb, Mo, Tc, Ru, Rh, Pd, Ag, Cd, In, Sn, Sb, Te, I, Xe,
|
| 370 |
+
Cs, Ba, *_L, Hf, Ta, W, Re, Os, Ir, Pt, Au, Hg, Tl, Pb, Bi, Po, At, Rn,
|
| 371 |
+
Fr, Ra, *_A, Rf, Db, Sg, Bh, Hs, Mt, Ds, Rg, Cn, Nh, Fl, Mc, Lv, Ts, Og
|
| 372 |
+
)
|
| 373 |
+
# pylint: enable=bad-whitespace
|
| 374 |
+
# fmt: on
|
| 375 |
+
ATOMIC_SYMBOL: Mapping[int, str] = {e.number: e.symbol for e in PERIODIC_TABLE}
|
| 376 |
+
ATOMIC_NUMBER = {e.symbol: e.number for e in PERIODIC_TABLE}
|
| 377 |
+
# Add Deuterium as previous table contained it.
|
| 378 |
+
ATOMIC_NUMBER['D'] = 1
|
| 379 |
+
|
| 380 |
+
ATOMIC_NUMBER: Mapping[str, int] = ATOMIC_NUMBER
|
| 381 |
+
ATOMIC_WEIGHT: np.ndarray = np.zeros(len(PERIODIC_TABLE), dtype=np.float64)
|
| 382 |
+
|
| 383 |
+
for e in PERIODIC_TABLE:
|
| 384 |
+
ATOMIC_WEIGHT[e.number] = e.weight
|
| 385 |
+
ATOMIC_WEIGHT.setflags(write=False)
|
flax_model/alphafold3/constants/residue_names.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Constants associated with residue names."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping
|
| 6 |
+
import functools
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
# pyformat: disable
|
| 10 |
+
# common_typos_disable
|
| 11 |
+
CCD_NAME_TO_ONE_LETTER: Mapping[str, str] = {
|
| 12 |
+
'00C': 'C', '01W': 'X', '02K': 'A', '03Y': 'C', '07O': 'C', '08P': 'C',
|
| 13 |
+
'0A0': 'D', '0A1': 'Y', '0A2': 'K', '0A8': 'C', '0AA': 'V', '0AB': 'V',
|
| 14 |
+
'0AC': 'G', '0AD': 'G', '0AF': 'W', '0AG': 'L', '0AH': 'S', '0AK': 'D',
|
| 15 |
+
'0AM': 'A', '0AP': 'C', '0AU': 'U', '0AV': 'A', '0AZ': 'P', '0BN': 'F',
|
| 16 |
+
'0C': 'C', '0CS': 'A', '0DC': 'C', '0DG': 'G', '0DT': 'T', '0FL': 'A',
|
| 17 |
+
'0G': 'G', '0NC': 'A', '0SP': 'A', '0U': 'U', '10C': 'C', '125': 'U',
|
| 18 |
+
'126': 'U', '127': 'U', '128': 'N', '12A': 'A', '143': 'C', '193': 'X',
|
| 19 |
+
'1AP': 'A', '1MA': 'A', '1MG': 'G', '1PA': 'F', '1PI': 'A', '1PR': 'N',
|
| 20 |
+
'1SC': 'C', '1TQ': 'W', '1TY': 'Y', '1X6': 'S', '200': 'F', '23F': 'F',
|
| 21 |
+
'23S': 'X', '26B': 'T', '2AD': 'X', '2AG': 'A', '2AO': 'X', '2AR': 'A',
|
| 22 |
+
'2AS': 'X', '2AT': 'T', '2AU': 'U', '2BD': 'I', '2BT': 'T', '2BU': 'A',
|
| 23 |
+
'2CO': 'C', '2DA': 'A', '2DF': 'N', '2DM': 'N', '2DO': 'X', '2DT': 'T',
|
| 24 |
+
'2EG': 'G', '2FE': 'N', '2FI': 'N', '2FM': 'M', '2GT': 'T', '2HF': 'H',
|
| 25 |
+
'2LU': 'L', '2MA': 'A', '2MG': 'G', '2ML': 'L', '2MR': 'R', '2MT': 'P',
|
| 26 |
+
'2MU': 'U', '2NT': 'T', '2OM': 'U', '2OT': 'T', '2PI': 'X', '2PR': 'G',
|
| 27 |
+
'2SA': 'N', '2SI': 'X', '2ST': 'T', '2TL': 'T', '2TY': 'Y', '2VA': 'V',
|
| 28 |
+
'2XA': 'C', '32S': 'X', '32T': 'X', '3AH': 'H', '3AR': 'X', '3CF': 'F',
|
| 29 |
+
'3DA': 'A', '3DR': 'N', '3GA': 'A', '3MD': 'D', '3ME': 'U', '3NF': 'Y',
|
| 30 |
+
'3QN': 'K', '3TY': 'X', '3XH': 'G', '4AC': 'N', '4BF': 'Y', '4CF': 'F',
|
| 31 |
+
'4CY': 'M', '4DP': 'W', '4FB': 'P', '4FW': 'W', '4HT': 'W', '4IN': 'W',
|
| 32 |
+
'4MF': 'N', '4MM': 'X', '4OC': 'C', '4PC': 'C', '4PD': 'C', '4PE': 'C',
|
| 33 |
+
'4PH': 'F', '4SC': 'C', '4SU': 'U', '4TA': 'N', '4U7': 'A', '56A': 'H',
|
| 34 |
+
'5AA': 'A', '5AB': 'A', '5AT': 'T', '5BU': 'U', '5CG': 'G', '5CM': 'C',
|
| 35 |
+
'5CS': 'C', '5FA': 'A', '5FC': 'C', '5FU': 'U', '5HP': 'E', '5HT': 'T',
|
| 36 |
+
'5HU': 'U', '5IC': 'C', '5IT': 'T', '5IU': 'U', '5MC': 'C', '5MD': 'N',
|
| 37 |
+
'5MU': 'U', '5NC': 'C', '5PC': 'C', '5PY': 'T', '5SE': 'U', '64T': 'T',
|
| 38 |
+
'6CL': 'K', '6CT': 'T', '6CW': 'W', '6HA': 'A', '6HC': 'C', '6HG': 'G',
|
| 39 |
+
'6HN': 'K', '6HT': 'T', '6IA': 'A', '6MA': 'A', '6MC': 'A', '6MI': 'N',
|
| 40 |
+
'6MT': 'A', '6MZ': 'N', '6OG': 'G', '70U': 'U', '7DA': 'A', '7GU': 'G',
|
| 41 |
+
'7JA': 'I', '7MG': 'G', '8AN': 'A', '8FG': 'G', '8MG': 'G', '8OG': 'G',
|
| 42 |
+
'9NE': 'E', '9NF': 'F', '9NR': 'R', '9NV': 'V', 'A': 'A', 'A1P': 'N',
|
| 43 |
+
'A23': 'A', 'A2L': 'A', 'A2M': 'A', 'A34': 'A', 'A35': 'A', 'A38': 'A',
|
| 44 |
+
'A39': 'A', 'A3A': 'A', 'A3P': 'A', 'A40': 'A', 'A43': 'A', 'A44': 'A',
|
| 45 |
+
'A47': 'A', 'A5L': 'A', 'A5M': 'C', 'A5N': 'N', 'A5O': 'A', 'A66': 'X',
|
| 46 |
+
'AA3': 'A', 'AA4': 'A', 'AAR': 'R', 'AB7': 'X', 'ABA': 'A', 'ABR': 'A',
|
| 47 |
+
'ABS': 'A', 'ABT': 'N', 'ACB': 'D', 'ACL': 'R', 'AD2': 'A', 'ADD': 'X',
|
| 48 |
+
'ADX': 'N', 'AEA': 'X', 'AEI': 'D', 'AET': 'A', 'AFA': 'N', 'AFF': 'N',
|
| 49 |
+
'AFG': 'G', 'AGM': 'R', 'AGT': 'C', 'AHB': 'N', 'AHH': 'X', 'AHO': 'A',
|
| 50 |
+
'AHP': 'A', 'AHS': 'X', 'AHT': 'X', 'AIB': 'A', 'AKL': 'D', 'AKZ': 'D',
|
| 51 |
+
'ALA': 'A', 'ALC': 'A', 'ALM': 'A', 'ALN': 'A', 'ALO': 'T', 'ALQ': 'X',
|
| 52 |
+
'ALS': 'A', 'ALT': 'A', 'ALV': 'A', 'ALY': 'K', 'AN8': 'A', 'AP7': 'A',
|
| 53 |
+
'APE': 'X', 'APH': 'A', 'API': 'K', 'APK': 'K', 'APM': 'X', 'APP': 'X',
|
| 54 |
+
'AR2': 'R', 'AR4': 'E', 'AR7': 'R', 'ARG': 'R', 'ARM': 'R', 'ARO': 'R',
|
| 55 |
+
'ARV': 'X', 'AS': 'A', 'AS2': 'D', 'AS9': 'X', 'ASA': 'D', 'ASB': 'D',
|
| 56 |
+
'ASI': 'D', 'ASK': 'D', 'ASL': 'D', 'ASM': 'X', 'ASN': 'N', 'ASP': 'D',
|
| 57 |
+
'ASQ': 'D', 'ASU': 'N', 'ASX': 'B', 'ATD': 'T', 'ATL': 'T', 'ATM': 'T',
|
| 58 |
+
'AVC': 'A', 'AVN': 'X', 'AYA': 'A', 'AZK': 'K', 'AZS': 'S', 'AZY': 'Y',
|
| 59 |
+
'B1F': 'F', 'B1P': 'N', 'B2A': 'A', 'B2F': 'F', 'B2I': 'I', 'B2V': 'V',
|
| 60 |
+
'B3A': 'A', 'B3D': 'D', 'B3E': 'E', 'B3K': 'K', 'B3L': 'X', 'B3M': 'X',
|
| 61 |
+
'B3Q': 'X', 'B3S': 'S', 'B3T': 'X', 'B3U': 'H', 'B3X': 'N', 'B3Y': 'Y',
|
| 62 |
+
'BB6': 'C', 'BB7': 'C', 'BB8': 'F', 'BB9': 'C', 'BBC': 'C', 'BCS': 'C',
|
| 63 |
+
'BE2': 'X', 'BFD': 'D', 'BG1': 'S', 'BGM': 'G', 'BH2': 'D', 'BHD': 'D',
|
| 64 |
+
'BIF': 'F', 'BIL': 'X', 'BIU': 'I', 'BJH': 'X', 'BLE': 'L', 'BLY': 'K',
|
| 65 |
+
'BMP': 'N', 'BMT': 'T', 'BNN': 'F', 'BNO': 'X', 'BOE': 'T', 'BOR': 'R',
|
| 66 |
+
'BPE': 'C', 'BRU': 'U', 'BSE': 'S', 'BT5': 'N', 'BTA': 'L', 'BTC': 'C',
|
| 67 |
+
'BTR': 'W', 'BUC': 'C', 'BUG': 'V', 'BVP': 'U', 'BZG': 'N', 'C': 'C',
|
| 68 |
+
'C1X': 'K', 'C25': 'C', 'C2L': 'C', 'C2S': 'C', 'C31': 'C', 'C32': 'C',
|
| 69 |
+
'C34': 'C', 'C36': 'C', 'C37': 'C', 'C38': 'C', 'C3Y': 'C', 'C42': 'C',
|
| 70 |
+
'C43': 'C', 'C45': 'C', 'C46': 'C', 'C49': 'C', 'C4R': 'C', 'C4S': 'C',
|
| 71 |
+
'C5C': 'C', 'C66': 'X', 'C6C': 'C', 'CAF': 'C', 'CAL': 'X', 'CAR': 'C',
|
| 72 |
+
'CAS': 'C', 'CAV': 'X', 'CAY': 'C', 'CB2': 'C', 'CBR': 'C', 'CBV': 'C',
|
| 73 |
+
'CCC': 'C', 'CCL': 'K', 'CCS': 'C', 'CDE': 'X', 'CDV': 'X', 'CDW': 'C',
|
| 74 |
+
'CEA': 'C', 'CFL': 'C', 'CG1': 'G', 'CGA': 'E', 'CGU': 'E', 'CH': 'C',
|
| 75 |
+
'CHF': 'X', 'CHG': 'X', 'CHP': 'G', 'CHS': 'X', 'CIR': 'R', 'CLE': 'L',
|
| 76 |
+
'CLG': 'K', 'CLH': 'K', 'CM0': 'N', 'CME': 'C', 'CMH': 'C', 'CML': 'C',
|
| 77 |
+
'CMR': 'C', 'CMT': 'C', 'CNU': 'U', 'CP1': 'C', 'CPC': 'X', 'CPI': 'X',
|
| 78 |
+
'CR5': 'G', 'CS0': 'C', 'CS1': 'C', 'CS3': 'C', 'CS4': 'C', 'CS8': 'N',
|
| 79 |
+
'CSA': 'C', 'CSB': 'C', 'CSD': 'C', 'CSE': 'C', 'CSF': 'C', 'CSI': 'G',
|
| 80 |
+
'CSJ': 'C', 'CSL': 'C', 'CSO': 'C', 'CSP': 'C', 'CSR': 'C', 'CSS': 'C',
|
| 81 |
+
'CSU': 'C', 'CSW': 'C', 'CSX': 'C', 'CSZ': 'C', 'CTE': 'W', 'CTG': 'T',
|
| 82 |
+
'CTH': 'T', 'CUC': 'X', 'CWR': 'S', 'CXM': 'M', 'CY0': 'C', 'CY1': 'C',
|
| 83 |
+
'CY3': 'C', 'CY4': 'C', 'CYA': 'C', 'CYD': 'C', 'CYF': 'C', 'CYG': 'C',
|
| 84 |
+
'CYJ': 'X', 'CYM': 'C', 'CYQ': 'C', 'CYR': 'C', 'CYS': 'C', 'CZ2': 'C',
|
| 85 |
+
'CZZ': 'C', 'D11': 'T', 'D1P': 'N', 'D3': 'N', 'D33': 'N', 'D3P': 'G',
|
| 86 |
+
'D3T': 'T', 'D4M': 'T', 'D4P': 'X', 'DA': 'A', 'DA2': 'X', 'DAB': 'A',
|
| 87 |
+
'DAH': 'F', 'DAL': 'A', 'DAR': 'R', 'DAS': 'D', 'DBB': 'T', 'DBM': 'N',
|
| 88 |
+
'DBS': 'S', 'DBU': 'T', 'DBY': 'Y', 'DBZ': 'A', 'DC': 'C', 'DC2': 'C',
|
| 89 |
+
'DCG': 'G', 'DCI': 'X', 'DCL': 'X', 'DCT': 'C', 'DCY': 'C', 'DDE': 'H',
|
| 90 |
+
'DDG': 'G', 'DDN': 'U', 'DDX': 'N', 'DFC': 'C', 'DFG': 'G', 'DFI': 'X',
|
| 91 |
+
'DFO': 'X', 'DFT': 'N', 'DG': 'G', 'DGH': 'G', 'DGI': 'G', 'DGL': 'E',
|
| 92 |
+
'DGN': 'Q', 'DHA': 'S', 'DHI': 'H', 'DHL': 'X', 'DHN': 'V', 'DHP': 'X',
|
| 93 |
+
'DHU': 'U', 'DHV': 'V', 'DI': 'I', 'DIL': 'I', 'DIR': 'R', 'DIV': 'V',
|
| 94 |
+
'DLE': 'L', 'DLS': 'K', 'DLY': 'K', 'DM0': 'K', 'DMH': 'N', 'DMK': 'D',
|
| 95 |
+
'DMT': 'X', 'DN': 'N', 'DNE': 'L', 'DNG': 'L', 'DNL': 'K', 'DNM': 'L',
|
| 96 |
+
'DNP': 'A', 'DNR': 'C', 'DNS': 'K', 'DOA': 'X', 'DOC': 'C', 'DOH': 'D',
|
| 97 |
+
'DON': 'L', 'DPB': 'T', 'DPH': 'F', 'DPL': 'P', 'DPP': 'A', 'DPQ': 'Y',
|
| 98 |
+
'DPR': 'P', 'DPY': 'N', 'DRM': 'U', 'DRP': 'N', 'DRT': 'T', 'DRZ': 'N',
|
| 99 |
+
'DSE': 'S', 'DSG': 'N', 'DSN': 'S', 'DSP': 'D', 'DT': 'T', 'DTH': 'T',
|
| 100 |
+
'DTR': 'W', 'DTY': 'Y', 'DU': 'U', 'DVA': 'V', 'DXD': 'N', 'DXN': 'N',
|
| 101 |
+
'DYS': 'C', 'DZM': 'A', 'E': 'A', 'E1X': 'A', 'ECC': 'Q', 'EDA': 'A',
|
| 102 |
+
'EFC': 'C', 'EHP': 'F', 'EIT': 'T', 'ENP': 'N', 'ESB': 'Y', 'ESC': 'M',
|
| 103 |
+
'EXB': 'X', 'EXY': 'L', 'EY5': 'N', 'EYS': 'X', 'F2F': 'F', 'FA2': 'A',
|
| 104 |
+
'FA5': 'N', 'FAG': 'N', 'FAI': 'N', 'FB5': 'A', 'FB6': 'A', 'FCL': 'F',
|
| 105 |
+
'FFD': 'N', 'FGA': 'E', 'FGL': 'G', 'FGP': 'S', 'FHL': 'X', 'FHO': 'K',
|
| 106 |
+
'FHU': 'U', 'FLA': 'A', 'FLE': 'L', 'FLT': 'Y', 'FME': 'M', 'FMG': 'G',
|
| 107 |
+
'FMU': 'N', 'FOE': 'C', 'FOX': 'G', 'FP9': 'P', 'FPA': 'F', 'FRD': 'X',
|
| 108 |
+
'FT6': 'W', 'FTR': 'W', 'FTY': 'Y', 'FVA': 'V', 'FZN': 'K', 'G': 'G',
|
| 109 |
+
'G25': 'G', 'G2L': 'G', 'G2S': 'G', 'G31': 'G', 'G32': 'G', 'G33': 'G',
|
| 110 |
+
'G36': 'G', 'G38': 'G', 'G42': 'G', 'G46': 'G', 'G47': 'G', 'G48': 'G',
|
| 111 |
+
'G49': 'G', 'G4P': 'N', 'G7M': 'G', 'GAO': 'G', 'GAU': 'E', 'GCK': 'C',
|
| 112 |
+
'GCM': 'X', 'GDP': 'G', 'GDR': 'G', 'GFL': 'G', 'GGL': 'E', 'GH3': 'G',
|
| 113 |
+
'GHG': 'Q', 'GHP': 'G', 'GL3': 'G', 'GLH': 'Q', 'GLJ': 'E', 'GLK': 'E',
|
| 114 |
+
'GLM': 'X', 'GLN': 'Q', 'GLQ': 'E', 'GLU': 'E', 'GLX': 'Z', 'GLY': 'G',
|
| 115 |
+
'GLZ': 'G', 'GMA': 'E', 'GMS': 'G', 'GMU': 'U', 'GN7': 'G', 'GND': 'X',
|
| 116 |
+
'GNE': 'N', 'GOM': 'G', 'GPL': 'K', 'GS': 'G', 'GSC': 'G', 'GSR': 'G',
|
| 117 |
+
'GSS': 'G', 'GSU': 'E', 'GT9': 'C', 'GTP': 'G', 'GVL': 'X', 'H2U': 'U',
|
| 118 |
+
'H5M': 'P', 'HAC': 'A', 'HAR': 'R', 'HBN': 'H', 'HCS': 'X', 'HDP': 'U',
|
| 119 |
+
'HEU': 'U', 'HFA': 'X', 'HGL': 'X', 'HHI': 'H', 'HIA': 'H', 'HIC': 'H',
|
| 120 |
+
'HIP': 'H', 'HIQ': 'H', 'HIS': 'H', 'HL2': 'L', 'HLU': 'L', 'HMR': 'R',
|
| 121 |
+
'HOL': 'N', 'HPC': 'F', 'HPE': 'F', 'HPH': 'F', 'HPQ': 'F', 'HQA': 'A',
|
| 122 |
+
'HRG': 'R', 'HRP': 'W', 'HS8': 'H', 'HS9': 'H', 'HSE': 'S', 'HSL': 'S',
|
| 123 |
+
'HSO': 'H', 'HTI': 'C', 'HTN': 'N', 'HTR': 'W', 'HV5': 'A', 'HVA': 'V',
|
| 124 |
+
'HY3': 'P', 'HYP': 'P', 'HZP': 'P', 'I': 'I', 'I2M': 'I', 'I58': 'K',
|
| 125 |
+
'I5C': 'C', 'IAM': 'A', 'IAR': 'R', 'IAS': 'D', 'IC': 'C', 'IEL': 'K',
|
| 126 |
+
'IG': 'G', 'IGL': 'G', 'IGU': 'G', 'IIL': 'I', 'ILE': 'I', 'ILG': 'E',
|
| 127 |
+
'ILX': 'I', 'IMC': 'C', 'IML': 'I', 'IOY': 'F', 'IPG': 'G', 'IPN': 'N',
|
| 128 |
+
'IRN': 'N', 'IT1': 'K', 'IU': 'U', 'IYR': 'Y', 'IYT': 'T', 'IZO': 'M',
|
| 129 |
+
'JJJ': 'C', 'JJK': 'C', 'JJL': 'C', 'JW5': 'N', 'K1R': 'C', 'KAG': 'G',
|
| 130 |
+
'KCX': 'K', 'KGC': 'K', 'KNB': 'A', 'KOR': 'M', 'KPI': 'K', 'KST': 'K',
|
| 131 |
+
'KYQ': 'K', 'L2A': 'X', 'LA2': 'K', 'LAA': 'D', 'LAL': 'A', 'LBY': 'K',
|
| 132 |
+
'LC': 'C', 'LCA': 'A', 'LCC': 'N', 'LCG': 'G', 'LCH': 'N', 'LCK': 'K',
|
| 133 |
+
'LCX': 'K', 'LDH': 'K', 'LED': 'L', 'LEF': 'L', 'LEH': 'L', 'LEI': 'V',
|
| 134 |
+
'LEM': 'L', 'LEN': 'L', 'LET': 'X', 'LEU': 'L', 'LEX': 'L', 'LG': 'G',
|
| 135 |
+
'LGP': 'G', 'LHC': 'X', 'LHU': 'U', 'LKC': 'N', 'LLP': 'K', 'LLY': 'K',
|
| 136 |
+
'LME': 'E', 'LMF': 'K', 'LMQ': 'Q', 'LMS': 'N', 'LP6': 'K', 'LPD': 'P',
|
| 137 |
+
'LPG': 'G', 'LPL': 'X', 'LPS': 'S', 'LSO': 'X', 'LTA': 'X', 'LTR': 'W',
|
| 138 |
+
'LVG': 'G', 'LVN': 'V', 'LYF': 'K', 'LYK': 'K', 'LYM': 'K', 'LYN': 'K',
|
| 139 |
+
'LYR': 'K', 'LYS': 'K', 'LYX': 'K', 'LYZ': 'K', 'M0H': 'C', 'M1G': 'G',
|
| 140 |
+
'M2G': 'G', 'M2L': 'K', 'M2S': 'M', 'M30': 'G', 'M3L': 'K', 'M5M': 'C',
|
| 141 |
+
'MA': 'A', 'MA6': 'A', 'MA7': 'A', 'MAA': 'A', 'MAD': 'A', 'MAI': 'R',
|
| 142 |
+
'MBQ': 'Y', 'MBZ': 'N', 'MC1': 'S', 'MCG': 'X', 'MCL': 'K', 'MCS': 'C',
|
| 143 |
+
'MCY': 'C', 'MD3': 'C', 'MD6': 'G', 'MDH': 'X', 'MDR': 'N', 'MEA': 'F',
|
| 144 |
+
'MED': 'M', 'MEG': 'E', 'MEN': 'N', 'MEP': 'U', 'MEQ': 'Q', 'MET': 'M',
|
| 145 |
+
'MEU': 'G', 'MF3': 'X', 'MG1': 'G', 'MGG': 'R', 'MGN': 'Q', 'MGQ': 'A',
|
| 146 |
+
'MGV': 'G', 'MGY': 'G', 'MHL': 'L', 'MHO': 'M', 'MHS': 'H', 'MIA': 'A',
|
| 147 |
+
'MIS': 'S', 'MK8': 'L', 'ML3': 'K', 'MLE': 'L', 'MLL': 'L', 'MLY': 'K',
|
| 148 |
+
'MLZ': 'K', 'MME': 'M', 'MMO': 'R', 'MMT': 'T', 'MND': 'N', 'MNL': 'L',
|
| 149 |
+
'MNU': 'U', 'MNV': 'V', 'MOD': 'X', 'MP8': 'P', 'MPH': 'X', 'MPJ': 'X',
|
| 150 |
+
'MPQ': 'G', 'MRG': 'G', 'MSA': 'G', 'MSE': 'M', 'MSL': 'M', 'MSO': 'M',
|
| 151 |
+
'MSP': 'X', 'MT2': 'M', 'MTR': 'T', 'MTU': 'A', 'MTY': 'Y', 'MVA': 'V',
|
| 152 |
+
'N': 'N', 'N10': 'S', 'N2C': 'X', 'N5I': 'N', 'N5M': 'C', 'N6G': 'G',
|
| 153 |
+
'N7P': 'P', 'NA8': 'A', 'NAL': 'A', 'NAM': 'A', 'NB8': 'N', 'NBQ': 'Y',
|
| 154 |
+
'NC1': 'S', 'NCB': 'A', 'NCX': 'N', 'NCY': 'X', 'NDF': 'F', 'NDN': 'U',
|
| 155 |
+
'NEM': 'H', 'NEP': 'H', 'NF2': 'N', 'NFA': 'F', 'NHL': 'E', 'NIT': 'X',
|
| 156 |
+
'NIY': 'Y', 'NLE': 'L', 'NLN': 'L', 'NLO': 'L', 'NLP': 'L', 'NLQ': 'Q',
|
| 157 |
+
'NMC': 'G', 'NMM': 'R', 'NMS': 'T', 'NMT': 'T', 'NNH': 'R', 'NP3': 'N',
|
| 158 |
+
'NPH': 'C', 'NPI': 'A', 'NSK': 'X', 'NTY': 'Y', 'NVA': 'V', 'NYM': 'N',
|
| 159 |
+
'NYS': 'C', 'NZH': 'H', 'O12': 'X', 'O2C': 'N', 'O2G': 'G', 'OAD': 'N',
|
| 160 |
+
'OAS': 'S', 'OBF': 'X', 'OBS': 'X', 'OCS': 'C', 'OCY': 'C', 'ODP': 'N',
|
| 161 |
+
'OHI': 'H', 'OHS': 'D', 'OIC': 'X', 'OIP': 'I', 'OLE': 'X', 'OLT': 'T',
|
| 162 |
+
'OLZ': 'S', 'OMC': 'C', 'OMG': 'G', 'OMT': 'M', 'OMU': 'U', 'ONE': 'U',
|
| 163 |
+
'ONH': 'A', 'ONL': 'X', 'OPR': 'R', 'ORN': 'A', 'ORQ': 'R', 'OSE': 'S',
|
| 164 |
+
'OTB': 'X', 'OTH': 'T', 'OTY': 'Y', 'OXX': 'D', 'P': 'G', 'P1L': 'C',
|
| 165 |
+
'P1P': 'N', 'P2T': 'T', 'P2U': 'U', 'P2Y': 'P', 'P5P': 'A', 'PAQ': 'Y',
|
| 166 |
+
'PAS': 'D', 'PAT': 'W', 'PAU': 'A', 'PBB': 'C', 'PBF': 'F', 'PBT': 'N',
|
| 167 |
+
'PCA': 'E', 'PCC': 'P', 'PCE': 'X', 'PCS': 'F', 'PDL': 'X', 'PDU': 'U',
|
| 168 |
+
'PEC': 'C', 'PF5': 'F', 'PFF': 'F', 'PFX': 'X', 'PG1': 'S', 'PG7': 'G',
|
| 169 |
+
'PG9': 'G', 'PGL': 'X', 'PGN': 'G', 'PGP': 'G', 'PGY': 'G', 'PHA': 'F',
|
| 170 |
+
'PHD': 'D', 'PHE': 'F', 'PHI': 'F', 'PHL': 'F', 'PHM': 'F', 'PIV': 'X',
|
| 171 |
+
'PLE': 'L', 'PM3': 'F', 'PMT': 'C', 'POM': 'P', 'PPN': 'F', 'PPU': 'A',
|
| 172 |
+
'PPW': 'G', 'PQ1': 'N', 'PR3': 'C', 'PR5': 'A', 'PR9': 'P', 'PRN': 'A',
|
| 173 |
+
'PRO': 'P', 'PRS': 'P', 'PSA': 'F', 'PSH': 'H', 'PST': 'T', 'PSU': 'U',
|
| 174 |
+
'PSW': 'C', 'PTA': 'X', 'PTH': 'Y', 'PTM': 'Y', 'PTR': 'Y', 'PU': 'A',
|
| 175 |
+
'PUY': 'N', 'PVH': 'H', 'PVL': 'X', 'PYA': 'A', 'PYO': 'U', 'PYX': 'C',
|
| 176 |
+
'PYY': 'N', 'QMM': 'Q', 'QPA': 'C', 'QPH': 'F', 'QUO': 'G', 'R': 'A',
|
| 177 |
+
'R1A': 'C', 'R4K': 'W', 'RE0': 'W', 'RE3': 'W', 'RIA': 'A', 'RMP': 'A',
|
| 178 |
+
'RON': 'X', 'RT': 'T', 'RTP': 'N', 'S1H': 'S', 'S2C': 'C', 'S2D': 'A',
|
| 179 |
+
'S2M': 'T', 'S2P': 'A', 'S4A': 'A', 'S4C': 'C', 'S4G': 'G', 'S4U': 'U',
|
| 180 |
+
'S6G': 'G', 'SAC': 'S', 'SAH': 'C', 'SAR': 'G', 'SBL': 'S', 'SC': 'C',
|
| 181 |
+
'SCH': 'C', 'SCS': 'C', 'SCY': 'C', 'SD2': 'X', 'SDG': 'G', 'SDP': 'S',
|
| 182 |
+
'SEB': 'S', 'SEC': 'A', 'SEG': 'A', 'SEL': 'S', 'SEM': 'S', 'SEN': 'S',
|
| 183 |
+
'SEP': 'S', 'SER': 'S', 'SET': 'S', 'SGB': 'S', 'SHC': 'C', 'SHP': 'G',
|
| 184 |
+
'SHR': 'K', 'SIB': 'C', 'SLA': 'P', 'SLR': 'P', 'SLZ': 'K', 'SMC': 'C',
|
| 185 |
+
'SME': 'M', 'SMF': 'F', 'SMP': 'A', 'SMT': 'T', 'SNC': 'C', 'SNN': 'N',
|
| 186 |
+
'SOC': 'C', 'SOS': 'N', 'SOY': 'S', 'SPT': 'T', 'SRA': 'A', 'SSU': 'U',
|
| 187 |
+
'STY': 'Y', 'SUB': 'X', 'SUN': 'S', 'SUR': 'U', 'SVA': 'S', 'SVV': 'S',
|
| 188 |
+
'SVW': 'S', 'SVX': 'S', 'SVY': 'S', 'SVZ': 'X', 'SYS': 'C', 'T': 'T',
|
| 189 |
+
'T11': 'F', 'T23': 'T', 'T2S': 'T', 'T2T': 'N', 'T31': 'U', 'T32': 'T',
|
| 190 |
+
'T36': 'T', 'T37': 'T', 'T38': 'T', 'T39': 'T', 'T3P': 'T', 'T41': 'T',
|
| 191 |
+
'T48': 'T', 'T49': 'T', 'T4S': 'T', 'T5O': 'U', 'T5S': 'T', 'T66': 'X',
|
| 192 |
+
'T6A': 'A', 'TA3': 'T', 'TA4': 'X', 'TAF': 'T', 'TAL': 'N', 'TAV': 'D',
|
| 193 |
+
'TBG': 'V', 'TBM': 'T', 'TC1': 'C', 'TCP': 'T', 'TCQ': 'Y', 'TCR': 'W',
|
| 194 |
+
'TCY': 'A', 'TDD': 'L', 'TDY': 'T', 'TFE': 'T', 'TFO': 'A', 'TFQ': 'F',
|
| 195 |
+
'TFT': 'T', 'TGP': 'G', 'TH6': 'T', 'THC': 'T', 'THO': 'X', 'THR': 'T',
|
| 196 |
+
'THX': 'N', 'THZ': 'R', 'TIH': 'A', 'TLB': 'N', 'TLC': 'T', 'TLN': 'U',
|
| 197 |
+
'TMB': 'T', 'TMD': 'T', 'TNB': 'C', 'TNR': 'S', 'TOX': 'W', 'TP1': 'T',
|
| 198 |
+
'TPC': 'C', 'TPG': 'G', 'TPH': 'X', 'TPL': 'W', 'TPO': 'T', 'TPQ': 'Y',
|
| 199 |
+
'TQI': 'W', 'TQQ': 'W', 'TRF': 'W', 'TRG': 'K', 'TRN': 'W', 'TRO': 'W',
|
| 200 |
+
'TRP': 'W', 'TRQ': 'W', 'TRW': 'W', 'TRX': 'W', 'TS': 'N', 'TST': 'X',
|
| 201 |
+
'TT': 'N', 'TTD': 'T', 'TTI': 'U', 'TTM': 'T', 'TTQ': 'W', 'TTS': 'Y',
|
| 202 |
+
'TY1': 'Y', 'TY2': 'Y', 'TY3': 'Y', 'TY5': 'Y', 'TYB': 'Y', 'TYI': 'Y',
|
| 203 |
+
'TYJ': 'Y', 'TYN': 'Y', 'TYO': 'Y', 'TYQ': 'Y', 'TYR': 'Y', 'TYS': 'Y',
|
| 204 |
+
'TYT': 'Y', 'TYU': 'N', 'TYW': 'Y', 'TYX': 'X', 'TYY': 'Y', 'TZB': 'X',
|
| 205 |
+
'TZO': 'X', 'U': 'U', 'U25': 'U', 'U2L': 'U', 'U2N': 'U', 'U2P': 'U',
|
| 206 |
+
'U31': 'U', 'U33': 'U', 'U34': 'U', 'U36': 'U', 'U37': 'U', 'U8U': 'U',
|
| 207 |
+
'UAR': 'U', 'UCL': 'U', 'UD5': 'U', 'UDP': 'N', 'UFP': 'N', 'UFR': 'U',
|
| 208 |
+
'UFT': 'U', 'UMA': 'A', 'UMP': 'U', 'UMS': 'U', 'UN1': 'X', 'UN2': 'X',
|
| 209 |
+
'UNK': 'X', 'UR3': 'U', 'URD': 'U', 'US1': 'U', 'US2': 'U', 'US3': 'T',
|
| 210 |
+
'US5': 'U', 'USM': 'U', 'VAD': 'V', 'VAF': 'V', 'VAL': 'V', 'VB1': 'K',
|
| 211 |
+
'VDL': 'X', 'VLL': 'X', 'VLM': 'X', 'VMS': 'X', 'VOL': 'X', 'X': 'G',
|
| 212 |
+
'X2W': 'E', 'X4A': 'N', 'XAD': 'A', 'XAE': 'N', 'XAL': 'A', 'XAR': 'N',
|
| 213 |
+
'XCL': 'C', 'XCN': 'C', 'XCP': 'X', 'XCR': 'C', 'XCS': 'N', 'XCT': 'C',
|
| 214 |
+
'XCY': 'C', 'XGA': 'N', 'XGL': 'G', 'XGR': 'G', 'XGU': 'G', 'XPR': 'P',
|
| 215 |
+
'XSN': 'N', 'XTH': 'T', 'XTL': 'T', 'XTR': 'T', 'XTS': 'G', 'XTY': 'N',
|
| 216 |
+
'XUA': 'A', 'XUG': 'G', 'XX1': 'K', 'Y': 'A', 'YCM': 'C', 'YG': 'G',
|
| 217 |
+
'YOF': 'Y', 'YRR': 'N', 'YYG': 'G', 'Z': 'C', 'Z01': 'A', 'ZAD': 'A',
|
| 218 |
+
'ZAL': 'A', 'ZBC': 'C', 'ZBU': 'U', 'ZCL': 'F', 'ZCY': 'C', 'ZDU': 'U',
|
| 219 |
+
'ZFB': 'X', 'ZGU': 'G', 'ZHP': 'N', 'ZTH': 'T', 'ZU0': 'T', 'ZZJ': 'A',
|
| 220 |
+
}
|
| 221 |
+
# common_typos_enable
|
| 222 |
+
# pyformat: enable
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
@functools.lru_cache(maxsize=64)
|
| 226 |
+
def letters_three_to_one(restype: str, *, default: str) -> str:
|
| 227 |
+
"""Returns single letter name if one exists otherwise returns default."""
|
| 228 |
+
return CCD_NAME_TO_ONE_LETTER.get(restype, default)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
ALA = sys.intern('ALA')
|
| 232 |
+
ARG = sys.intern('ARG')
|
| 233 |
+
ASN = sys.intern('ASN')
|
| 234 |
+
ASP = sys.intern('ASP')
|
| 235 |
+
CYS = sys.intern('CYS')
|
| 236 |
+
GLN = sys.intern('GLN')
|
| 237 |
+
GLU = sys.intern('GLU')
|
| 238 |
+
GLY = sys.intern('GLY')
|
| 239 |
+
HIS = sys.intern('HIS')
|
| 240 |
+
ILE = sys.intern('ILE')
|
| 241 |
+
LEU = sys.intern('LEU')
|
| 242 |
+
LYS = sys.intern('LYS')
|
| 243 |
+
MET = sys.intern('MET')
|
| 244 |
+
PHE = sys.intern('PHE')
|
| 245 |
+
PRO = sys.intern('PRO')
|
| 246 |
+
SER = sys.intern('SER')
|
| 247 |
+
THR = sys.intern('THR')
|
| 248 |
+
TRP = sys.intern('TRP')
|
| 249 |
+
TYR = sys.intern('TYR')
|
| 250 |
+
VAL = sys.intern('VAL')
|
| 251 |
+
UNK = sys.intern('UNK')
|
| 252 |
+
GAP = sys.intern('-')
|
| 253 |
+
|
| 254 |
+
# Unknown ligand.
|
| 255 |
+
UNL = sys.intern('UNL')
|
| 256 |
+
|
| 257 |
+
# Non-standard version of MET (with Se instead of S), but often appears in PDB.
|
| 258 |
+
MSE = sys.intern('MSE')
|
| 259 |
+
|
| 260 |
+
# 20 standard protein amino acids (no unknown).
|
| 261 |
+
PROTEIN_TYPES: tuple[str, ...] = (
|
| 262 |
+
ALA, ARG, ASN, ASP, CYS, GLN, GLU, GLY, HIS, ILE, LEU, LYS, MET, PHE, PRO,
|
| 263 |
+
SER, THR, TRP, TYR, VAL,
|
| 264 |
+
) # pyformat: disable
|
| 265 |
+
|
| 266 |
+
# 20 standard protein amino acids plus the unknown (UNK) amino acid.
|
| 267 |
+
PROTEIN_TYPES_WITH_UNKNOWN: tuple[str, ...] = PROTEIN_TYPES + (UNK,)
|
| 268 |
+
|
| 269 |
+
# This is the standard residue order when coding AA type as a number.
|
| 270 |
+
# Reproduce it by taking 3-letter AA codes and sorting them alphabetically.
|
| 271 |
+
# For legacy reasons this only refers to protein residues.
|
| 272 |
+
|
| 273 |
+
PROTEIN_TYPES_ONE_LETTER: tuple[str, ...] = (
|
| 274 |
+
'A', 'R', 'N', 'D', 'C', 'Q', 'E', 'G', 'H', 'I', 'L', 'K', 'M', 'F', 'P',
|
| 275 |
+
'S', 'T', 'W', 'Y', 'V',
|
| 276 |
+
) # pyformat: disable
|
| 277 |
+
|
| 278 |
+
PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN: tuple[str, ...] = (
|
| 279 |
+
PROTEIN_TYPES_ONE_LETTER + ('X',)
|
| 280 |
+
)
|
| 281 |
+
PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP: tuple[str, ...] = (
|
| 282 |
+
PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN + (GAP,)
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
PROTEIN_TYPES_ONE_LETTER_TO_INT: Mapping[str, int] = {
|
| 286 |
+
r: i for i, r in enumerate(PROTEIN_TYPES_ONE_LETTER)
|
| 287 |
+
}
|
| 288 |
+
PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_TO_INT: Mapping[str, int] = {
|
| 289 |
+
r: i for i, r in enumerate(PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN)
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP_TO_INT: Mapping[str, int] = {
|
| 293 |
+
r: i for i, r in enumerate(PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP)
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
PROTEIN_COMMON_ONE_TO_THREE: Mapping[str, str] = {
|
| 298 |
+
'A': ALA,
|
| 299 |
+
'R': ARG,
|
| 300 |
+
'N': ASN,
|
| 301 |
+
'D': ASP,
|
| 302 |
+
'C': CYS,
|
| 303 |
+
'Q': GLN,
|
| 304 |
+
'E': GLU,
|
| 305 |
+
'G': GLY,
|
| 306 |
+
'H': HIS,
|
| 307 |
+
'I': ILE,
|
| 308 |
+
'L': LEU,
|
| 309 |
+
'K': LYS,
|
| 310 |
+
'M': MET,
|
| 311 |
+
'F': PHE,
|
| 312 |
+
'P': PRO,
|
| 313 |
+
'S': SER,
|
| 314 |
+
'T': THR,
|
| 315 |
+
'W': TRP,
|
| 316 |
+
'Y': TYR,
|
| 317 |
+
'V': VAL,
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
PROTEIN_COMMON_THREE_TO_ONE: Mapping[str, str] = {
|
| 321 |
+
v: k for k, v in PROTEIN_COMMON_ONE_TO_THREE.items()
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
A = sys.intern('A')
|
| 325 |
+
G = sys.intern('G')
|
| 326 |
+
C = sys.intern('C')
|
| 327 |
+
U = sys.intern('U')
|
| 328 |
+
T = sys.intern('T')
|
| 329 |
+
|
| 330 |
+
DA = sys.intern('DA')
|
| 331 |
+
DG = sys.intern('DG')
|
| 332 |
+
DC = sys.intern('DC')
|
| 333 |
+
DT = sys.intern('DT')
|
| 334 |
+
|
| 335 |
+
UNK_NUCLEIC_ONE_LETTER = sys.intern('N') # Unknown nucleic acid single letter.
|
| 336 |
+
UNK_RNA = sys.intern('N') # Unknown RNA.
|
| 337 |
+
UNK_DNA = sys.intern('DN') # Unknown DNA residue (differs from N).
|
| 338 |
+
|
| 339 |
+
RNA_TYPES: tuple[str, ...] = (A, G, C, U)
|
| 340 |
+
DNA_TYPES: tuple[str, ...] = (DA, DG, DC, DT)
|
| 341 |
+
|
| 342 |
+
NUCLEIC_TYPES: tuple[str, ...] = RNA_TYPES + DNA_TYPES
|
| 343 |
+
# Without UNK DNA.
|
| 344 |
+
NUCLEIC_TYPES_WITH_UNKNOWN: tuple[str, ...] = NUCLEIC_TYPES + (
|
| 345 |
+
UNK_NUCLEIC_ONE_LETTER,
|
| 346 |
+
)
|
| 347 |
+
NUCLEIC_TYPES_WITH_2_UNKS: tuple[str, ...] = NUCLEIC_TYPES + (
|
| 348 |
+
UNK_RNA,
|
| 349 |
+
UNK_DNA,
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
RNA_TYPES_ONE_LETTER_WITH_UNKNOWN: tuple[str, ...] = RNA_TYPES + (UNK_RNA,)
|
| 353 |
+
RNA_TYPES_ONE_LETTER_WITH_UNKNOWN_TO_INT: Mapping[str, int] = {
|
| 354 |
+
r: i for i, r in enumerate(RNA_TYPES_ONE_LETTER_WITH_UNKNOWN)
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
DNA_TYPES_WITH_UNKNOWN: tuple[str, ...] = DNA_TYPES + (UNK_DNA,)
|
| 358 |
+
DNA_TYPES_ONE_LETTER: tuple[str, ...] = (A, G, C, T)
|
| 359 |
+
DNA_TYPES_ONE_LETTER_WITH_UNKNOWN: tuple[str, ...] = DNA_TYPES_ONE_LETTER + (
|
| 360 |
+
UNK_NUCLEIC_ONE_LETTER,
|
| 361 |
+
)
|
| 362 |
+
DNA_TYPES_ONE_LETTER_WITH_UNKNOWN_TO_INT: Mapping[str, int] = {
|
| 363 |
+
r: i for i, r in enumerate(DNA_TYPES_ONE_LETTER_WITH_UNKNOWN)
|
| 364 |
+
}
|
| 365 |
+
DNA_COMMON_ONE_TO_TWO: Mapping[str, str] = {
|
| 366 |
+
'A': 'DA',
|
| 367 |
+
'G': 'DG',
|
| 368 |
+
'C': 'DC',
|
| 369 |
+
'T': 'DT',
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
STANDARD_POLYMER_TYPES: tuple[str, ...] = PROTEIN_TYPES + NUCLEIC_TYPES
|
| 373 |
+
POLYMER_TYPES: tuple[str, ...] = PROTEIN_TYPES_WITH_UNKNOWN + NUCLEIC_TYPES
|
| 374 |
+
POLYMER_TYPES_WITH_UNKNOWN: tuple[str, ...] = (
|
| 375 |
+
PROTEIN_TYPES_WITH_UNKNOWN + NUCLEIC_TYPES_WITH_UNKNOWN
|
| 376 |
+
)
|
| 377 |
+
POLYMER_TYPES_WITH_GAP: tuple[str, ...] = PROTEIN_TYPES + (GAP,) + NUCLEIC_TYPES
|
| 378 |
+
POLYMER_TYPES_WITH_UNKNOWN_AND_GAP: tuple[str, ...] = (
|
| 379 |
+
PROTEIN_TYPES_WITH_UNKNOWN + (GAP,) + NUCLEIC_TYPES_WITH_UNKNOWN
|
| 380 |
+
)
|
| 381 |
+
POLYMER_TYPES_WITH_ALL_UNKS_AND_GAP: tuple[str, ...] = (
|
| 382 |
+
PROTEIN_TYPES_WITH_UNKNOWN + (GAP,) + NUCLEIC_TYPES_WITH_2_UNKS
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
POLYMER_TYPES_ORDER = {restype: i for i, restype in enumerate(POLYMER_TYPES)}
|
| 386 |
+
|
| 387 |
+
POLYMER_TYPES_ORDER_WITH_UNKNOWN = {
|
| 388 |
+
restype: i for i, restype in enumerate(POLYMER_TYPES_WITH_UNKNOWN)
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP = {
|
| 392 |
+
restype: i for i, restype in enumerate(POLYMER_TYPES_WITH_UNKNOWN_AND_GAP)
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
POLYMER_TYPES_ORDER_WITH_ALL_UNKS_AND_GAP = {
|
| 396 |
+
restype: i for i, restype in enumerate(POLYMER_TYPES_WITH_ALL_UNKS_AND_GAP)
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
POLYMER_TYPES_NUM = len(POLYMER_TYPES) # := 29.
|
| 400 |
+
POLYMER_TYPES_NUM_WITH_UNKNOWN = len(POLYMER_TYPES_WITH_UNKNOWN) # := 30.
|
| 401 |
+
POLYMER_TYPES_NUM_WITH_GAP = len(POLYMER_TYPES_WITH_GAP) # := 29.
|
| 402 |
+
POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP = len(
|
| 403 |
+
POLYMER_TYPES_WITH_UNKNOWN_AND_GAP
|
| 404 |
+
) # := 31.
|
| 405 |
+
POLYMER_TYPES_NUM_ORDER_WITH_ALL_UNKS_AND_GAP = len(
|
| 406 |
+
POLYMER_TYPES_WITH_ALL_UNKS_AND_GAP
|
| 407 |
+
) # := 32.
|
| 408 |
+
|
| 409 |
+
WATER_TYPES: tuple[str, ...] = ('HOH', 'DOD')
|
| 410 |
+
|
| 411 |
+
UNKNOWN_TYPES: tuple[str, ...] = (UNK, UNK_RNA, UNK_DNA, UNL)
|
flax_model/alphafold3/constants/side_chains.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Constants associated with side chains."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping, Sequence
|
| 6 |
+
import itertools
|
| 7 |
+
|
| 8 |
+
# Format: The list for each AA type contains chi1, chi2, chi3, chi4 in
|
| 9 |
+
# this order (or a relevant subset from chi1 onwards). ALA and GLY don't have
|
| 10 |
+
# chi angles so their chi angle lists are empty.
|
| 11 |
+
CHI_ANGLES_ATOMS: Mapping[str, Sequence[tuple[str, ...]]] = {
|
| 12 |
+
'ALA': [],
|
| 13 |
+
# Chi5 in arginine is always 0 +- 5 degrees, so ignore it.
|
| 14 |
+
'ARG': [
|
| 15 |
+
('N', 'CA', 'CB', 'CG'),
|
| 16 |
+
('CA', 'CB', 'CG', 'CD'),
|
| 17 |
+
('CB', 'CG', 'CD', 'NE'),
|
| 18 |
+
('CG', 'CD', 'NE', 'CZ'),
|
| 19 |
+
],
|
| 20 |
+
'ASN': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'OD1')],
|
| 21 |
+
'ASP': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'OD1')],
|
| 22 |
+
'CYS': [('N', 'CA', 'CB', 'SG')],
|
| 23 |
+
'GLN': [
|
| 24 |
+
('N', 'CA', 'CB', 'CG'),
|
| 25 |
+
('CA', 'CB', 'CG', 'CD'),
|
| 26 |
+
('CB', 'CG', 'CD', 'OE1'),
|
| 27 |
+
],
|
| 28 |
+
'GLU': [
|
| 29 |
+
('N', 'CA', 'CB', 'CG'),
|
| 30 |
+
('CA', 'CB', 'CG', 'CD'),
|
| 31 |
+
('CB', 'CG', 'CD', 'OE1'),
|
| 32 |
+
],
|
| 33 |
+
'GLY': [],
|
| 34 |
+
'HIS': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'ND1')],
|
| 35 |
+
'ILE': [('N', 'CA', 'CB', 'CG1'), ('CA', 'CB', 'CG1', 'CD1')],
|
| 36 |
+
'LEU': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')],
|
| 37 |
+
'LYS': [
|
| 38 |
+
('N', 'CA', 'CB', 'CG'),
|
| 39 |
+
('CA', 'CB', 'CG', 'CD'),
|
| 40 |
+
('CB', 'CG', 'CD', 'CE'),
|
| 41 |
+
('CG', 'CD', 'CE', 'NZ'),
|
| 42 |
+
],
|
| 43 |
+
'MET': [
|
| 44 |
+
('N', 'CA', 'CB', 'CG'),
|
| 45 |
+
('CA', 'CB', 'CG', 'SD'),
|
| 46 |
+
('CB', 'CG', 'SD', 'CE'),
|
| 47 |
+
],
|
| 48 |
+
'PHE': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')],
|
| 49 |
+
'PRO': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD')],
|
| 50 |
+
'SER': [('N', 'CA', 'CB', 'OG')],
|
| 51 |
+
'THR': [('N', 'CA', 'CB', 'OG1')],
|
| 52 |
+
'TRP': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')],
|
| 53 |
+
'TYR': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')],
|
| 54 |
+
'VAL': [('N', 'CA', 'CB', 'CG1')],
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
CHI_GROUPS_FOR_ATOM = {}
|
| 58 |
+
for res_name, chi_angle_atoms_for_res in CHI_ANGLES_ATOMS.items():
|
| 59 |
+
for chi_group_i, chi_group in enumerate(chi_angle_atoms_for_res):
|
| 60 |
+
for atom_i, atom in enumerate(chi_group):
|
| 61 |
+
CHI_GROUPS_FOR_ATOM.setdefault((res_name, atom), []).append(
|
| 62 |
+
(chi_group_i, atom_i)
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Mapping from (residue_name, atom_name) pairs to the atom's chi group index
|
| 66 |
+
# and atom index within that group.
|
| 67 |
+
CHI_GROUPS_FOR_ATOM: Mapping[tuple[str, str], Sequence[tuple[int, int]]] = (
|
| 68 |
+
CHI_GROUPS_FOR_ATOM
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
MAX_NUM_CHI_ANGLES: int = 4
|
| 72 |
+
ATOMS_PER_CHI_ANGLE: int = 4
|
| 73 |
+
|
| 74 |
+
# A list of atoms for each AA type that are involved in chi angle calculations.
|
| 75 |
+
CHI_ATOM_SETS: Mapping[str, set[str]] = {
|
| 76 |
+
residue_name: set(itertools.chain(*atoms))
|
| 77 |
+
for residue_name, atoms in CHI_ANGLES_ATOMS.items()
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
# If chi angles given in fixed-length array, this matrix determines how to mask
|
| 81 |
+
# them for each AA type. The order is as per restype_order (see below).
|
| 82 |
+
CHI_ANGLES_MASK: Sequence[Sequence[float]] = (
|
| 83 |
+
(0.0, 0.0, 0.0, 0.0), # ALA
|
| 84 |
+
(1.0, 1.0, 1.0, 1.0), # ARG
|
| 85 |
+
(1.0, 1.0, 0.0, 0.0), # ASN
|
| 86 |
+
(1.0, 1.0, 0.0, 0.0), # ASP
|
| 87 |
+
(1.0, 0.0, 0.0, 0.0), # CYS
|
| 88 |
+
(1.0, 1.0, 1.0, 0.0), # GLN
|
| 89 |
+
(1.0, 1.0, 1.0, 0.0), # GLU
|
| 90 |
+
(0.0, 0.0, 0.0, 0.0), # GLY
|
| 91 |
+
(1.0, 1.0, 0.0, 0.0), # HIS
|
| 92 |
+
(1.0, 1.0, 0.0, 0.0), # ILE
|
| 93 |
+
(1.0, 1.0, 0.0, 0.0), # LEU
|
| 94 |
+
(1.0, 1.0, 1.0, 1.0), # LYS
|
| 95 |
+
(1.0, 1.0, 1.0, 0.0), # MET
|
| 96 |
+
(1.0, 1.0, 0.0, 0.0), # PHE
|
| 97 |
+
(1.0, 1.0, 0.0, 0.0), # PRO
|
| 98 |
+
(1.0, 0.0, 0.0, 0.0), # SER
|
| 99 |
+
(1.0, 0.0, 0.0, 0.0), # THR
|
| 100 |
+
(1.0, 1.0, 0.0, 0.0), # TRP
|
| 101 |
+
(1.0, 1.0, 0.0, 0.0), # TYR
|
| 102 |
+
(1.0, 0.0, 0.0, 0.0), # VAL
|
| 103 |
+
)
|
flax_model/alphafold3/cpp.cc
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright 2024 DeepMind Technologies Limited
|
| 2 |
+
//
|
| 3 |
+
// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of
|
| 4 |
+
// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
|
| 5 |
+
//
|
| 6 |
+
// To request access to the AlphaFold 3 model parameters, follow the process set
|
| 7 |
+
// out at https://github.com/google-deepmind/alphafold3. You may only use these
|
| 8 |
+
// if received directly from Google. Use is subject to terms of use available at
|
| 9 |
+
// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md
|
| 10 |
+
|
| 11 |
+
#include "alphafold3/data/cpp/msa_profile_pybind.h"
|
| 12 |
+
#include "alphafold3/model/mkdssp_pybind.h"
|
| 13 |
+
#include "alphafold3/parsers/cpp/cif_dict_pybind.h"
|
| 14 |
+
#include "alphafold3/parsers/cpp/fasta_iterator_pybind.h"
|
| 15 |
+
#include "alphafold3/parsers/cpp/msa_conversion_pybind.h"
|
| 16 |
+
#include "alphafold3/structure/cpp/aggregation_pybind.h"
|
| 17 |
+
#include "alphafold3/structure/cpp/membership_pybind.h"
|
| 18 |
+
#include "alphafold3/structure/cpp/mmcif_atom_site_pybind.h"
|
| 19 |
+
#include "alphafold3/structure/cpp/mmcif_layout_pybind.h"
|
| 20 |
+
#include "alphafold3/structure/cpp/mmcif_struct_conn_pybind.h"
|
| 21 |
+
#include "alphafold3/structure/cpp/mmcif_utils_pybind.h"
|
| 22 |
+
#include "alphafold3/structure/cpp/string_array_pybind.h"
|
| 23 |
+
#include "pybind11/pybind11.h"
|
| 24 |
+
|
| 25 |
+
namespace alphafold3 {
|
| 26 |
+
namespace {
|
| 27 |
+
|
| 28 |
+
// Include all modules as submodules to simplify building.
|
| 29 |
+
PYBIND11_MODULE(cpp, m) {
|
| 30 |
+
RegisterModuleCifDict(m.def_submodule("cif_dict"));
|
| 31 |
+
RegisterModuleFastaIterator(m.def_submodule("fasta_iterator"));
|
| 32 |
+
RegisterModuleMsaConversion(m.def_submodule("msa_conversion"));
|
| 33 |
+
RegisterModuleMmcifLayout(m.def_submodule("mmcif_layout"));
|
| 34 |
+
RegisterModuleMmcifStructConn(m.def_submodule("mmcif_struct_conn"));
|
| 35 |
+
RegisterModuleMembership(m.def_submodule("membership"));
|
| 36 |
+
RegisterModuleMmcifUtils(m.def_submodule("mmcif_utils"));
|
| 37 |
+
RegisterModuleAggregation(m.def_submodule("aggregation"));
|
| 38 |
+
RegisterModuleStringArray(m.def_submodule("string_array"));
|
| 39 |
+
RegisterModuleMmcifAtomSite(m.def_submodule("mmcif_atom_site"));
|
| 40 |
+
RegisterModuleMkdssp(m.def_submodule("mkdssp"));
|
| 41 |
+
RegisterModuleMsaProfile(m.def_submodule("msa_profile"));
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
} // namespace
|
| 45 |
+
} // namespace alphafold3
|
flax_model/alphafold3/jackhmmer_seq_limit.patch
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--- hmmer-3.4/src/jackhmmer.c
|
| 2 |
+
+++ hmmer-3.4/src/jackhmmer.c
|
| 3 |
+
@@ -73,6 +73,7 @@ static ESL_OPTIONS options[] = {
|
| 4 |
+
{ "--noali", eslARG_NONE, FALSE, NULL, NULL, NULL, NULL, NULL, "don't output alignments, so output is smaller", 2 },
|
| 5 |
+
{ "--notextw", eslARG_NONE, NULL, NULL, NULL, NULL, NULL, "--textw", "unlimit ASCII text output line width", 2 },
|
| 6 |
+
{ "--textw", eslARG_INT, "120", NULL, "n>=120", NULL, NULL, "--notextw", "set max width of ASCII text output lines", 2 },
|
| 7 |
+
+ { "--seq_limit", eslARG_INT, NULL, NULL, NULL, NULL, NULL, "--seq_limit", "if set, truncate all hits after this value is reached", 2 },
|
| 8 |
+
/* Control of scoring system */
|
| 9 |
+
{ "--popen", eslARG_REAL, "0.02", NULL, "0<=x<0.5",NULL, NULL, NULL, "gap open probability", 3 },
|
| 10 |
+
{ "--pextend", eslARG_REAL, "0.4", NULL, "0<=x<1", NULL, NULL, NULL, "gap extend probability", 3 },
|
| 11 |
+
@@ -298,6 +299,7 @@ output_header(FILE *ofp, ESL_GETOPTS *go
|
| 12 |
+
if (esl_opt_IsUsed(go, "--noali") && fprintf(ofp, "# show alignments in output: no\n") < 0) ESL_EXCEPTION_SYS(eslEWRITE, "write failed");
|
| 13 |
+
if (esl_opt_IsUsed(go, "--notextw") && fprintf(ofp, "# max ASCII text line length: unlimited\n") < 0) ESL_EXCEPTION_SYS(eslEWRITE, "write failed");
|
| 14 |
+
if (esl_opt_IsUsed(go, "--textw") && fprintf(ofp, "# max ASCII text line length: %d\n", esl_opt_GetInteger(go, "--textw")) < 0) ESL_EXCEPTION_SYS(eslEWRITE, "write failed");
|
| 15 |
+
+ if (esl_opt_IsUsed(go, "--seq_limit") && fprintf(ofp, "# set max sequence hits to return: %d\n", esl_opt_GetInteger(go, "--seq_limit")) < 0) ESL_EXCEPTION_SYS(eslEWRITE, "write failed");
|
| 16 |
+
if (esl_opt_IsUsed(go, "--popen") && fprintf(ofp, "# gap open probability: %f\n", esl_opt_GetReal (go, "--popen")) < 0) ESL_EXCEPTION_SYS(eslEWRITE, "write failed");
|
| 17 |
+
if (esl_opt_IsUsed(go, "--pextend") && fprintf(ofp, "# gap extend probability: %f\n", esl_opt_GetReal (go, "--pextend")) < 0) ESL_EXCEPTION_SYS(eslEWRITE, "write failed");
|
| 18 |
+
if (esl_opt_IsUsed(go, "--mx") && fprintf(ofp, "# subst score matrix (built-in): %s\n", esl_opt_GetString (go, "--mx")) < 0) ESL_EXCEPTION_SYS(eslEWRITE, "write failed");
|
| 19 |
+
@@ -674,6 +676,13 @@ serial_master(ESL_GETOPTS *go, struct cf
|
| 20 |
+
/* Print the results. */
|
| 21 |
+
p7_tophits_SortBySortkey(info->th);
|
| 22 |
+
p7_tophits_Threshold(info->th, info->pli);
|
| 23 |
+
+ /* Limit the number of hits if specified. */
|
| 24 |
+
+ if (esl_opt_IsOn(go, "--seq_limit"))
|
| 25 |
+
+ {
|
| 26 |
+
+ int seq_limit = esl_opt_GetInteger(go, "--seq_limit");
|
| 27 |
+
+ info->th->N = ESL_MIN(info->th->N, seq_limit);
|
| 28 |
+
+ }
|
| 29 |
+
+
|
| 30 |
+
p7_tophits_CompareRanking(info->th, kh, &nnew_targets);
|
| 31 |
+
p7_tophits_Targets(ofp, info->th, info->pli, textw); if (fprintf(ofp, "\n\n") < 0) ESL_EXCEPTION_SYS(eslEWRITE, "write failed");
|
| 32 |
+
p7_tophits_Domains(ofp, info->th, info->pli, textw); if (fprintf(ofp, "\n\n") < 0) ESL_EXCEPTION_SYS(eslEWRITE, "write failed");
|
flax_model/alphafold3/package_config.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Package metadata helpers for the split AlphaFold3 module."""
|
| 3 |
+
|
| 4 |
+
ALPHAFOLD3_PACKAGE_DATA = {
|
| 5 |
+
"flax_model.alphafold3": [
|
| 6 |
+
"*.so",
|
| 7 |
+
"*.pyd",
|
| 8 |
+
"*.dll",
|
| 9 |
+
"*.dylib",
|
| 10 |
+
"README.md",
|
| 11 |
+
"test_data/**/*",
|
| 12 |
+
"**/*.pyi",
|
| 13 |
+
],
|
| 14 |
+
"flax_model.alphafold3.constants.converters": [
|
| 15 |
+
"*.pickle",
|
| 16 |
+
],
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
ALPHAFOLD3_MANIFEST_RULES = [
|
| 20 |
+
"include flax_model/alphafold3/*.so",
|
| 21 |
+
"include flax_model/alphafold3/*.pyd",
|
| 22 |
+
"include flax_model/alphafold3/*.dll",
|
| 23 |
+
"include flax_model/alphafold3/*.dylib",
|
| 24 |
+
"recursive-include flax_model/alphafold3/constants/converters *.pickle",
|
| 25 |
+
"include flax_model/alphafold3/README.md",
|
| 26 |
+
"recursive-include flax_model/alphafold3/test_data *",
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def get_package_data():
|
| 31 |
+
return ALPHAFOLD3_PACKAGE_DATA
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def get_manifest_rules():
|
| 35 |
+
return ALPHAFOLD3_MANIFEST_RULES
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
_build_done = False
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def build_hook(project_root, env, python_executable, subprocess_module):
|
| 42 |
+
del project_root, env, python_executable, subprocess_module
|
| 43 |
+
global _build_done
|
| 44 |
+
if _build_done:
|
| 45 |
+
return
|
| 46 |
+
|
| 47 |
+
from flax_model.alphafold3 import _build as af3_build
|
| 48 |
+
|
| 49 |
+
print("[AF3] local build hook triggered")
|
| 50 |
+
try:
|
| 51 |
+
print(
|
| 52 |
+
"[AF3] "
|
| 53 |
+
f"should_build={af3_build.should_build()} "
|
| 54 |
+
f"artifacts_exist={af3_build.artifacts_exist()} "
|
| 55 |
+
f"force_rebuild={af3_build.force_rebuild()}"
|
| 56 |
+
)
|
| 57 |
+
af3_build.build_if_needed()
|
| 58 |
+
print("[AF3] local build hook finished")
|
| 59 |
+
except af3_build.AF3BuildError as exc:
|
| 60 |
+
print(f"[AF3] local build skipped or failed: {exc}")
|
| 61 |
+
if af3_build.is_strict():
|
| 62 |
+
raise
|
| 63 |
+
|
| 64 |
+
_build_done = True
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def get_build_hook():
|
| 68 |
+
return build_hook
|
flax_model/alphafold3/structure/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Structure module initialization."""
|
| 4 |
+
|
| 5 |
+
# pylint: disable=g-importing-member
|
| 6 |
+
from flax_model.alphafold3.structure.bioassemblies import BioassemblyData
|
| 7 |
+
from flax_model.alphafold3.structure.bonds import Bonds
|
| 8 |
+
from flax_model.alphafold3.structure.chemical_components import ChemCompEntry
|
| 9 |
+
from flax_model.alphafold3.structure.chemical_components import ChemicalComponentsData
|
| 10 |
+
from flax_model.alphafold3.structure.chemical_components import get_data_for_ccd_components
|
| 11 |
+
from flax_model.alphafold3.structure.chemical_components import populate_missing_ccd_data
|
| 12 |
+
from flax_model.alphafold3.structure.mmcif import BondParsingError
|
| 13 |
+
from flax_model.alphafold3.structure.parsing import BondAtomId
|
| 14 |
+
from flax_model.alphafold3.structure.parsing import from_atom_arrays
|
| 15 |
+
from flax_model.alphafold3.structure.parsing import from_mmcif
|
| 16 |
+
from flax_model.alphafold3.structure.parsing import from_parsed_mmcif
|
| 17 |
+
from flax_model.alphafold3.structure.parsing import from_res_arrays
|
| 18 |
+
from flax_model.alphafold3.structure.parsing import from_sequences_and_bonds
|
| 19 |
+
from flax_model.alphafold3.structure.parsing import ModelID
|
| 20 |
+
from flax_model.alphafold3.structure.parsing import NoAtomsError
|
| 21 |
+
from flax_model.alphafold3.structure.parsing import SequenceFormat
|
| 22 |
+
from flax_model.alphafold3.structure.structure import ARRAY_FIELDS
|
| 23 |
+
from flax_model.alphafold3.structure.structure import AuthorNamingScheme
|
| 24 |
+
from flax_model.alphafold3.structure.structure import Bond
|
| 25 |
+
from flax_model.alphafold3.structure.structure import CascadeDelete
|
| 26 |
+
from flax_model.alphafold3.structure.structure import concat
|
| 27 |
+
from flax_model.alphafold3.structure.structure import enumerate_residues
|
| 28 |
+
from flax_model.alphafold3.structure.structure import fix_non_standard_polymer_residues
|
| 29 |
+
from flax_model.alphafold3.structure.structure import GLOBAL_FIELDS
|
| 30 |
+
from flax_model.alphafold3.structure.structure import make_empty_structure
|
| 31 |
+
from flax_model.alphafold3.structure.structure import MissingAtomError
|
| 32 |
+
from flax_model.alphafold3.structure.structure import MissingAuthorResidueIdError
|
| 33 |
+
from flax_model.alphafold3.structure.structure import multichain_residue_index
|
| 34 |
+
from flax_model.alphafold3.structure.structure import stack
|
| 35 |
+
from flax_model.alphafold3.structure.structure import Structure
|
| 36 |
+
from flax_model.alphafold3.structure.structure_tables import Atoms
|
| 37 |
+
from flax_model.alphafold3.structure.structure_tables import Chains
|
| 38 |
+
from flax_model.alphafold3.structure.structure_tables import Residues
|
flax_model/alphafold3/structure/bioassemblies.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Utilities for parsing and manipulating bioassembly data."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping, Sequence
|
| 6 |
+
import copy
|
| 7 |
+
import dataclasses
|
| 8 |
+
from typing import Self
|
| 9 |
+
|
| 10 |
+
from flax_model.alphafold3.structure import mmcif
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclasses.dataclass(frozen=True)
|
| 15 |
+
class Operation:
|
| 16 |
+
"""A rigid transformation operation."""
|
| 17 |
+
|
| 18 |
+
trans: np.ndarray # shape: (3,)
|
| 19 |
+
rot: np.ndarray # shape: (3, 3)
|
| 20 |
+
|
| 21 |
+
def apply_to_coords(self, coords: np.ndarray) -> np.ndarray:
|
| 22 |
+
"""Applies the rotation followed by the translation to `coords`."""
|
| 23 |
+
return np.dot(coords, self.rot.T) + self.trans[np.newaxis, :]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclasses.dataclass(frozen=True)
|
| 27 |
+
class Transform:
|
| 28 |
+
"""A rigid transformation composed of a sequence of `Operation`s."""
|
| 29 |
+
|
| 30 |
+
# The sequence of operations that form the transform. These will be applied
|
| 31 |
+
# right-to-left (last-to-first).
|
| 32 |
+
operations: Sequence[Operation]
|
| 33 |
+
|
| 34 |
+
# The chain IDs that this transform should be applied to. These are
|
| 35 |
+
# label_asym_ids in the mmCIF spec.
|
| 36 |
+
chain_ids: Sequence[str]
|
| 37 |
+
|
| 38 |
+
# A mapping from chain IDs (of chains that participate in this transform)
|
| 39 |
+
# to their new values in the bioassembly.
|
| 40 |
+
chain_id_rename_map: Mapping[str, str]
|
| 41 |
+
|
| 42 |
+
def apply_to_coords(self, coords: np.ndarray) -> np.ndarray:
|
| 43 |
+
"""Applies the `operations` in right-to-left order."""
|
| 44 |
+
for operation in reversed(self.operations):
|
| 45 |
+
coords = operation.apply_to_coords(coords)
|
| 46 |
+
return coords
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _get_operation(oper_data: Mapping[str, str]) -> Operation:
|
| 50 |
+
"""Parses an `Operation` from a mmCIF _pdbx_struct_oper_list row."""
|
| 51 |
+
trans = np.zeros((3,), dtype=np.float32)
|
| 52 |
+
rot = np.zeros((3, 3), dtype=np.float32)
|
| 53 |
+
for i in range(3):
|
| 54 |
+
trans[i] = float(oper_data[f'_pdbx_struct_oper_list.vector[{i + 1}]'])
|
| 55 |
+
for i in range(3):
|
| 56 |
+
for j in range(3):
|
| 57 |
+
rot[i][j] = float(
|
| 58 |
+
oper_data[f'_pdbx_struct_oper_list.matrix[{i + 1}][{j + 1}]']
|
| 59 |
+
)
|
| 60 |
+
return Operation(trans=trans, rot=rot)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class MissingBioassemblyDataError(Exception):
|
| 64 |
+
"""Raised when bioassembly data is missing from an mmCIF."""
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class BioassemblyData:
|
| 68 |
+
"""Stores and processes bioassembly data from mmCIF tables."""
|
| 69 |
+
|
| 70 |
+
# Not all of these columns are required for internal operations, but all
|
| 71 |
+
# should be present whenever bioassemblies are defined in an mmCIF to stay
|
| 72 |
+
# consistent with external mmCIFs.
|
| 73 |
+
_REQUIRED_COLUMNS = (
|
| 74 |
+
'_pdbx_struct_assembly.id',
|
| 75 |
+
'_pdbx_struct_assembly.details',
|
| 76 |
+
'_pdbx_struct_assembly.method_details',
|
| 77 |
+
'_pdbx_struct_assembly.oligomeric_details',
|
| 78 |
+
'_pdbx_struct_assembly.oligomeric_count',
|
| 79 |
+
'_pdbx_struct_assembly_gen.assembly_id',
|
| 80 |
+
'_pdbx_struct_assembly_gen.oper_expression',
|
| 81 |
+
'_pdbx_struct_assembly_gen.asym_id_list',
|
| 82 |
+
'_pdbx_struct_oper_list.id',
|
| 83 |
+
'_pdbx_struct_oper_list.type',
|
| 84 |
+
'_pdbx_struct_oper_list.name',
|
| 85 |
+
'_pdbx_struct_oper_list.symmetry_operation',
|
| 86 |
+
'_pdbx_struct_oper_list.matrix[1][1]',
|
| 87 |
+
'_pdbx_struct_oper_list.matrix[1][2]',
|
| 88 |
+
'_pdbx_struct_oper_list.matrix[1][3]',
|
| 89 |
+
'_pdbx_struct_oper_list.vector[1]',
|
| 90 |
+
'_pdbx_struct_oper_list.matrix[2][1]',
|
| 91 |
+
'_pdbx_struct_oper_list.matrix[2][2]',
|
| 92 |
+
'_pdbx_struct_oper_list.matrix[2][3]',
|
| 93 |
+
'_pdbx_struct_oper_list.vector[2]',
|
| 94 |
+
'_pdbx_struct_oper_list.matrix[3][1]',
|
| 95 |
+
'_pdbx_struct_oper_list.matrix[3][2]',
|
| 96 |
+
'_pdbx_struct_oper_list.matrix[3][3]',
|
| 97 |
+
'_pdbx_struct_oper_list.vector[3]',
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
def __init__(
|
| 101 |
+
self,
|
| 102 |
+
*,
|
| 103 |
+
pdbx_struct_assembly: Mapping[str, Mapping[str, str]],
|
| 104 |
+
pdbx_struct_assembly_gen: Mapping[str, Sequence[Mapping[str, str]]],
|
| 105 |
+
pdbx_struct_oper_list: Mapping[str, Mapping[str, str]],
|
| 106 |
+
assembly_ids: Sequence[str],
|
| 107 |
+
oper_ids: Sequence[str],
|
| 108 |
+
):
|
| 109 |
+
for assembly_id in assembly_ids:
|
| 110 |
+
for table, table_name in (
|
| 111 |
+
(pdbx_struct_assembly, '_pdbx_struct_assembly'),
|
| 112 |
+
(pdbx_struct_assembly_gen, '_pdbx_struct_assembly_gen'),
|
| 113 |
+
):
|
| 114 |
+
if assembly_id not in table:
|
| 115 |
+
raise ValueError(
|
| 116 |
+
f'Assembly ID "{assembly_id}" missing from {table_name} '
|
| 117 |
+
f'with keys: {table.keys()}'
|
| 118 |
+
)
|
| 119 |
+
for oper_id in oper_ids:
|
| 120 |
+
if oper_id not in pdbx_struct_oper_list:
|
| 121 |
+
raise ValueError(
|
| 122 |
+
f'Oper ID "{oper_id}" missing from _pdbx_struct_oper_list '
|
| 123 |
+
f'with keys: {pdbx_struct_oper_list.keys()}'
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
self._pdbx_struct_assembly = pdbx_struct_assembly
|
| 127 |
+
self._pdbx_struct_assembly_gen = pdbx_struct_assembly_gen
|
| 128 |
+
self._pdbx_struct_oper_list = pdbx_struct_oper_list
|
| 129 |
+
self._operations = {
|
| 130 |
+
oper_id: _get_operation(oper_data)
|
| 131 |
+
for oper_id, oper_data in self._pdbx_struct_oper_list.items()
|
| 132 |
+
}
|
| 133 |
+
self._assembly_ids = assembly_ids
|
| 134 |
+
self._oper_ids = oper_ids
|
| 135 |
+
|
| 136 |
+
@classmethod
|
| 137 |
+
def from_mmcif(cls, cif: mmcif.Mmcif) -> Self:
|
| 138 |
+
"""Constructs an instance of `BioassemblyData` from an `Mmcif` object."""
|
| 139 |
+
for col in cls._REQUIRED_COLUMNS:
|
| 140 |
+
if col not in cif:
|
| 141 |
+
raise MissingBioassemblyDataError(col)
|
| 142 |
+
|
| 143 |
+
pdbx_struct_assembly = cif.extract_loop_as_dict(
|
| 144 |
+
prefix='_pdbx_struct_assembly.', index='_pdbx_struct_assembly.id'
|
| 145 |
+
)
|
| 146 |
+
pdbx_struct_oper_list = cif.extract_loop_as_dict(
|
| 147 |
+
prefix='_pdbx_struct_oper_list.', index='_pdbx_struct_oper_list.id'
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
# _pdbx_struct_assembly_gen is unlike the other two tables because it can
|
| 151 |
+
# have multiple rows share the same assembly ID. This can happen when an
|
| 152 |
+
# assembly is constructed by applying different sets of transforms to
|
| 153 |
+
# different sets of chain IDs. Each of these would have its own row.
|
| 154 |
+
# Here we group rows by their assembly_id.
|
| 155 |
+
pdbx_struct_assembly_gen = {}
|
| 156 |
+
for assembly_id, oper_expression, asym_id_list in zip(
|
| 157 |
+
cif['_pdbx_struct_assembly_gen.assembly_id'],
|
| 158 |
+
cif['_pdbx_struct_assembly_gen.oper_expression'],
|
| 159 |
+
cif['_pdbx_struct_assembly_gen.asym_id_list'],
|
| 160 |
+
):
|
| 161 |
+
pdbx_struct_assembly_gen.setdefault(assembly_id, []).append({
|
| 162 |
+
'_pdbx_struct_assembly_gen.assembly_id': assembly_id,
|
| 163 |
+
'_pdbx_struct_assembly_gen.oper_expression': oper_expression,
|
| 164 |
+
'_pdbx_struct_assembly_gen.asym_id_list': asym_id_list,
|
| 165 |
+
})
|
| 166 |
+
|
| 167 |
+
# We provide these separately to keep track of the original order that they
|
| 168 |
+
# appear in the mmCIF.
|
| 169 |
+
assembly_ids = cif['_pdbx_struct_assembly.id']
|
| 170 |
+
oper_ids = cif['_pdbx_struct_oper_list.id']
|
| 171 |
+
return cls(
|
| 172 |
+
pdbx_struct_assembly=pdbx_struct_assembly,
|
| 173 |
+
pdbx_struct_assembly_gen=pdbx_struct_assembly_gen,
|
| 174 |
+
pdbx_struct_oper_list=pdbx_struct_oper_list,
|
| 175 |
+
assembly_ids=assembly_ids,
|
| 176 |
+
oper_ids=oper_ids,
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
@property
|
| 180 |
+
def assembly_ids(self) -> Sequence[str]:
|
| 181 |
+
return self._assembly_ids
|
| 182 |
+
|
| 183 |
+
def asym_id_by_assembly_chain_id(self, assembly_id: str) -> Mapping[str, str]:
|
| 184 |
+
asym_id_by_assembly_chain_id = {}
|
| 185 |
+
for transform in self.get_transforms(assembly_id):
|
| 186 |
+
for asym_id, assembly_chain_id in transform.chain_id_rename_map.items():
|
| 187 |
+
asym_id_by_assembly_chain_id[assembly_chain_id] = asym_id
|
| 188 |
+
return asym_id_by_assembly_chain_id
|
| 189 |
+
|
| 190 |
+
def assembly_chain_ids_by_asym_id(
|
| 191 |
+
self, assembly_id: str
|
| 192 |
+
) -> Mapping[str, set[str]]:
|
| 193 |
+
assembly_chain_ids_by_asym_id = {}
|
| 194 |
+
for transform in self.get_transforms(assembly_id):
|
| 195 |
+
for asym_id, assembly_chain_id in transform.chain_id_rename_map.items():
|
| 196 |
+
assembly_chain_ids_by_asym_id.setdefault(asym_id, set()).add(
|
| 197 |
+
assembly_chain_id
|
| 198 |
+
)
|
| 199 |
+
return assembly_chain_ids_by_asym_id
|
| 200 |
+
|
| 201 |
+
def get_default_assembly_id(self) -> str:
|
| 202 |
+
"""Gets a default assembly ID."""
|
| 203 |
+
# The first assembly is usually (though not always) the best choice.
|
| 204 |
+
# If we find a better heuristic for picking bioassemblies then this
|
| 205 |
+
# method should be updated.
|
| 206 |
+
return min(self._assembly_ids)
|
| 207 |
+
|
| 208 |
+
def get_assembly_info(self, assembly_id: str) -> Mapping[str, str]:
|
| 209 |
+
return {
|
| 210 |
+
k.replace('_pdbx_struct_assembly.', ''): v
|
| 211 |
+
for k, v in self._pdbx_struct_assembly[assembly_id].items()
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
def get_transforms(self, assembly_id: str) -> Sequence[Transform]:
|
| 215 |
+
"""Returns the transforms required to generate the given assembly."""
|
| 216 |
+
partial_transforms = []
|
| 217 |
+
all_chain_ids = set()
|
| 218 |
+
for row in self._pdbx_struct_assembly_gen[assembly_id]:
|
| 219 |
+
oper_expression = row['_pdbx_struct_assembly_gen.oper_expression']
|
| 220 |
+
parsed_oper_id_seqs = mmcif.parse_oper_expr(oper_expression)
|
| 221 |
+
label_asym_ids = row['_pdbx_struct_assembly_gen.asym_id_list'].split(',')
|
| 222 |
+
all_chain_ids |= set(label_asym_ids)
|
| 223 |
+
for parsed_oper_id_seq in parsed_oper_id_seqs:
|
| 224 |
+
partial_transforms.append((parsed_oper_id_seq, label_asym_ids))
|
| 225 |
+
|
| 226 |
+
# We start assigning new chain IDs by finding the largest chain ID in
|
| 227 |
+
# the original structure that is involved in this bioassembly, and then
|
| 228 |
+
# starting from the next one.
|
| 229 |
+
max_int_chain_id = max(mmcif.str_id_to_int_id(c) for c in all_chain_ids)
|
| 230 |
+
next_int_chain_id = max_int_chain_id + 1
|
| 231 |
+
|
| 232 |
+
transforms = []
|
| 233 |
+
has_been_renamed = set()
|
| 234 |
+
for parsed_oper_id_seq, label_asym_ids in partial_transforms:
|
| 235 |
+
chain_id_rename_map = {}
|
| 236 |
+
for label_asym_id in label_asym_ids:
|
| 237 |
+
if label_asym_id not in has_been_renamed:
|
| 238 |
+
# The first time we see a label_asym_id we don't need to rename it.
|
| 239 |
+
# This isn't strictly necessary since we don't provide any
|
| 240 |
+
# guarantees about chain naming after bioassembly extraction but
|
| 241 |
+
# can make it a bit easier to inspect and compare structures
|
| 242 |
+
# pre and post bioassembly extraction.
|
| 243 |
+
chain_id_rename_map[label_asym_id] = label_asym_id
|
| 244 |
+
has_been_renamed.add(label_asym_id)
|
| 245 |
+
else:
|
| 246 |
+
chain_id_rename_map[label_asym_id] = mmcif.int_id_to_str_id(
|
| 247 |
+
next_int_chain_id
|
| 248 |
+
)
|
| 249 |
+
next_int_chain_id += 1
|
| 250 |
+
transforms.append(
|
| 251 |
+
Transform(
|
| 252 |
+
operations=[
|
| 253 |
+
self._operations[oper_id] for oper_id in parsed_oper_id_seq
|
| 254 |
+
],
|
| 255 |
+
chain_ids=label_asym_ids,
|
| 256 |
+
chain_id_rename_map=chain_id_rename_map,
|
| 257 |
+
)
|
| 258 |
+
)
|
| 259 |
+
return transforms
|
| 260 |
+
|
| 261 |
+
def to_mmcif_dict(self) -> Mapping[str, Sequence[str]]:
|
| 262 |
+
"""Returns the bioassembly data as a dict suitable for `mmcif.Mmcif`."""
|
| 263 |
+
mmcif_dict = {}
|
| 264 |
+
for assembly_id in self._assembly_ids:
|
| 265 |
+
for column, val in self._pdbx_struct_assembly[assembly_id].items():
|
| 266 |
+
mmcif_dict.setdefault(column, []).append(val)
|
| 267 |
+
for row in self._pdbx_struct_assembly_gen[assembly_id]:
|
| 268 |
+
for column, val in row.items():
|
| 269 |
+
mmcif_dict.setdefault(column, []).append(val)
|
| 270 |
+
for oper_id in self._oper_ids:
|
| 271 |
+
for column, val in self._pdbx_struct_oper_list[oper_id].items():
|
| 272 |
+
mmcif_dict.setdefault(column, []).append(val)
|
| 273 |
+
return mmcif_dict
|
| 274 |
+
|
| 275 |
+
def rename_label_asym_ids(
|
| 276 |
+
self,
|
| 277 |
+
mapping: Mapping[str, str],
|
| 278 |
+
present_chains: set[str],
|
| 279 |
+
) -> Self:
|
| 280 |
+
"""Returns a new BioassemblyData with renamed label_asym_ids.
|
| 281 |
+
|
| 282 |
+
Args:
|
| 283 |
+
mapping: A mapping from original label_asym_ids to their new values. Any
|
| 284 |
+
label_asym_ids in this BioassemblyData that are not in this mapping will
|
| 285 |
+
remain unchanged.
|
| 286 |
+
present_chains: A set of label_asym_ids that are actually present in the
|
| 287 |
+
atom site list. All label_asym_ids that are in the BioassemblyData but
|
| 288 |
+
not in present_chains won't be included in the output BioassemblyData.
|
| 289 |
+
|
| 290 |
+
Returns:
|
| 291 |
+
A new BioassemblyData with renamed label_asym_ids.
|
| 292 |
+
|
| 293 |
+
Raises:
|
| 294 |
+
ValueError: If any two previously distinct chains do not have unique names
|
| 295 |
+
anymore after the rename.
|
| 296 |
+
"""
|
| 297 |
+
new_pdbx_struct_assembly_gen = copy.deepcopy(self._pdbx_struct_assembly_gen)
|
| 298 |
+
for rows in new_pdbx_struct_assembly_gen.values():
|
| 299 |
+
for row in rows:
|
| 300 |
+
old_asym_ids = row['_pdbx_struct_assembly_gen.asym_id_list'].split(',')
|
| 301 |
+
new_asym_ids = [
|
| 302 |
+
mapping.get(label_asym_id, label_asym_id)
|
| 303 |
+
for label_asym_id in old_asym_ids
|
| 304 |
+
if label_asym_id in present_chains
|
| 305 |
+
]
|
| 306 |
+
if len(set(old_asym_ids) & present_chains) != len(set(new_asym_ids)):
|
| 307 |
+
raise ValueError(
|
| 308 |
+
'Can not rename chains, the new names are not unique: '
|
| 309 |
+
f'{sorted(new_asym_ids)}.'
|
| 310 |
+
)
|
| 311 |
+
row['_pdbx_struct_assembly_gen.asym_id_list'] = ','.join(new_asym_ids) # pytype: disable=unsupported-operands
|
| 312 |
+
|
| 313 |
+
return BioassemblyData(
|
| 314 |
+
pdbx_struct_assembly=copy.deepcopy(self._pdbx_struct_assembly),
|
| 315 |
+
pdbx_struct_assembly_gen=new_pdbx_struct_assembly_gen,
|
| 316 |
+
pdbx_struct_oper_list=copy.deepcopy(self._pdbx_struct_oper_list),
|
| 317 |
+
assembly_ids=copy.deepcopy(self._assembly_ids),
|
| 318 |
+
oper_ids=copy.deepcopy(self._oper_ids),
|
| 319 |
+
)
|
flax_model/alphafold3/structure/bonds.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Bond representation for structure module."""
|
| 4 |
+
|
| 5 |
+
import collections
|
| 6 |
+
from collections.abc import Mapping, Sequence
|
| 7 |
+
import dataclasses
|
| 8 |
+
import typing
|
| 9 |
+
from typing import Self
|
| 10 |
+
|
| 11 |
+
from flax_model.alphafold3.structure import table
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
| 16 |
+
class Bonds(table.Table):
|
| 17 |
+
"""Table of atomic bonds."""
|
| 18 |
+
|
| 19 |
+
# mmCIF column: _struct_conn.conn_type_id
|
| 20 |
+
# mmCIF desc: This data item is a pointer to _struct_conn_type.id in the
|
| 21 |
+
# STRUCT_CONN_TYPE category.
|
| 22 |
+
# E.g.: "covale", "disulf", "hydrog", "metalc".
|
| 23 |
+
type: np.ndarray
|
| 24 |
+
|
| 25 |
+
# mmCIF column: _struct_conn.pdbx_role
|
| 26 |
+
# mmCIF desc: The chemical or structural role of the interaction.
|
| 27 |
+
# E.g.: "N-Glycosylation", "O-Glycosylation".
|
| 28 |
+
role: np.ndarray
|
| 29 |
+
|
| 30 |
+
# mmCIF columns: _struct_conn.ptnr1_*
|
| 31 |
+
from_atom_key: np.ndarray
|
| 32 |
+
|
| 33 |
+
# mmCIF columns: _struct_conn.ptnr2_*
|
| 34 |
+
dest_atom_key: np.ndarray
|
| 35 |
+
|
| 36 |
+
@classmethod
|
| 37 |
+
def make_empty(cls) -> Self:
|
| 38 |
+
return cls(
|
| 39 |
+
key=np.empty((0,), dtype=np.int64),
|
| 40 |
+
from_atom_key=np.empty((0,), dtype=np.int64),
|
| 41 |
+
dest_atom_key=np.empty((0,), dtype=np.int64),
|
| 42 |
+
type=np.empty((0,), dtype=object),
|
| 43 |
+
role=np.empty((0,), dtype=object),
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
def get_atom_indices(
|
| 47 |
+
self,
|
| 48 |
+
atom_key: np.ndarray,
|
| 49 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 50 |
+
"""Returns the indices of the from/dest atoms in the atom_key array."""
|
| 51 |
+
from_atom_missing = ~np.isin(self.from_atom_key, atom_key)
|
| 52 |
+
dest_atom_missing = ~np.isin(self.dest_atom_key, atom_key)
|
| 53 |
+
if np.any(from_atom_missing):
|
| 54 |
+
raise ValueError(
|
| 55 |
+
f'No atoms for from_atom_key {self.from_atom_key[from_atom_missing]}'
|
| 56 |
+
)
|
| 57 |
+
if np.any(dest_atom_missing):
|
| 58 |
+
raise ValueError(
|
| 59 |
+
f'No atoms for dest_atom_key {self.dest_atom_key[dest_atom_missing]}'
|
| 60 |
+
)
|
| 61 |
+
sort_indices = np.argsort(atom_key)
|
| 62 |
+
from_indices_sorted = np.searchsorted(
|
| 63 |
+
atom_key, self.from_atom_key, sorter=sort_indices
|
| 64 |
+
)
|
| 65 |
+
dest_indices_sorted = np.searchsorted(
|
| 66 |
+
atom_key, self.dest_atom_key, sorter=sort_indices
|
| 67 |
+
)
|
| 68 |
+
from_indices = sort_indices[from_indices_sorted]
|
| 69 |
+
dest_indices = sort_indices[dest_indices_sorted]
|
| 70 |
+
return from_indices, dest_indices
|
| 71 |
+
|
| 72 |
+
def restrict_to_atoms(self, atom_key: np.ndarray) -> Self:
|
| 73 |
+
if not self.size: # Early-out for empty table.
|
| 74 |
+
return self
|
| 75 |
+
from_atom_mask = np.isin(self.from_atom_key, atom_key)
|
| 76 |
+
dest_atom_mask = np.isin(self.dest_atom_key, atom_key)
|
| 77 |
+
mask = np.logical_and(from_atom_mask, dest_atom_mask)
|
| 78 |
+
return typing.cast(Bonds, self.filter(mask=mask))
|
| 79 |
+
|
| 80 |
+
def to_mmcif_dict_from_atom_arrays(
|
| 81 |
+
self,
|
| 82 |
+
atom_key: np.ndarray,
|
| 83 |
+
chain_id: np.ndarray,
|
| 84 |
+
res_id: np.ndarray,
|
| 85 |
+
res_name: np.ndarray,
|
| 86 |
+
atom_name: np.ndarray,
|
| 87 |
+
auth_asym_id: np.ndarray,
|
| 88 |
+
auth_seq_id: np.ndarray,
|
| 89 |
+
insertion_code: np.ndarray,
|
| 90 |
+
) -> Mapping[str, Sequence[str] | np.ndarray]:
|
| 91 |
+
"""Returns a dict suitable for building a CifDict, representing bonds.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
atom_key: A (num_atom,) integer array of atom_keys.
|
| 95 |
+
chain_id: A (num_atom,) array of label_asym_id strings.
|
| 96 |
+
res_id: A (num_atom,) array of label_seq_id strings.
|
| 97 |
+
res_name: A (num_atom,) array of label_comp_id strings.
|
| 98 |
+
atom_name: A (num_atom,) array of label_atom_id strings.
|
| 99 |
+
auth_asym_id: A (num_atom,) array of auth_asym_id strings.
|
| 100 |
+
auth_seq_id: A (num_atom,) array of auth_seq_id strings.
|
| 101 |
+
insertion_code: A (num_atom,) array of insertion code strings.
|
| 102 |
+
"""
|
| 103 |
+
mmcif_dict = collections.defaultdict(list)
|
| 104 |
+
ptnr1_indices, ptnr2_indices = self.get_atom_indices(atom_key)
|
| 105 |
+
|
| 106 |
+
mmcif_dict['_struct_conn.ptnr1_label_asym_id'] = chain_id[ptnr1_indices]
|
| 107 |
+
mmcif_dict['_struct_conn.ptnr2_label_asym_id'] = chain_id[ptnr2_indices]
|
| 108 |
+
mmcif_dict['_struct_conn.ptnr1_label_comp_id'] = res_name[ptnr1_indices]
|
| 109 |
+
mmcif_dict['_struct_conn.ptnr2_label_comp_id'] = res_name[ptnr2_indices]
|
| 110 |
+
mmcif_dict['_struct_conn.ptnr1_label_seq_id'] = res_id[ptnr1_indices]
|
| 111 |
+
mmcif_dict['_struct_conn.ptnr2_label_seq_id'] = res_id[ptnr2_indices]
|
| 112 |
+
mmcif_dict['_struct_conn.ptnr1_label_atom_id'] = atom_name[ptnr1_indices]
|
| 113 |
+
mmcif_dict['_struct_conn.ptnr2_label_atom_id'] = atom_name[ptnr2_indices]
|
| 114 |
+
|
| 115 |
+
mmcif_dict['_struct_conn.ptnr1_auth_asym_id'] = auth_asym_id[ptnr1_indices]
|
| 116 |
+
mmcif_dict['_struct_conn.ptnr2_auth_asym_id'] = auth_asym_id[ptnr2_indices]
|
| 117 |
+
mmcif_dict['_struct_conn.ptnr1_auth_seq_id'] = auth_seq_id[ptnr1_indices]
|
| 118 |
+
mmcif_dict['_struct_conn.ptnr2_auth_seq_id'] = auth_seq_id[ptnr2_indices]
|
| 119 |
+
mmcif_dict['_struct_conn.pdbx_ptnr1_PDB_ins_code'] = insertion_code[
|
| 120 |
+
ptnr1_indices
|
| 121 |
+
]
|
| 122 |
+
mmcif_dict['_struct_conn.pdbx_ptnr2_PDB_ins_code'] = insertion_code[
|
| 123 |
+
ptnr2_indices
|
| 124 |
+
]
|
| 125 |
+
|
| 126 |
+
label_alt_id = ['?'] * self.size
|
| 127 |
+
mmcif_dict['_struct_conn.pdbx_ptnr1_label_alt_id'] = label_alt_id
|
| 128 |
+
mmcif_dict['_struct_conn.pdbx_ptnr2_label_alt_id'] = label_alt_id
|
| 129 |
+
|
| 130 |
+
# We need to set this to make visualisation work in NGL/PyMOL.
|
| 131 |
+
mmcif_dict['_struct_conn.pdbx_value_order'] = ['?'] * self.size
|
| 132 |
+
|
| 133 |
+
# We use a symmetry of 1_555 which is the no-op transformation. Other
|
| 134 |
+
# values are used when bonds involve atoms that only exist after expanding
|
| 135 |
+
# the bioassembly, but we don't support this kind of bond at the moment.
|
| 136 |
+
symmetry = ['1_555'] * self.size
|
| 137 |
+
mmcif_dict['_struct_conn.ptnr1_symmetry'] = symmetry
|
| 138 |
+
mmcif_dict['_struct_conn.ptnr2_symmetry'] = symmetry
|
| 139 |
+
bond_type_counter = collections.Counter()
|
| 140 |
+
for bond_row in self.iterrows():
|
| 141 |
+
bond_type = bond_row['type']
|
| 142 |
+
bond_type_counter[bond_type] += 1
|
| 143 |
+
mmcif_dict['_struct_conn.id'].append(
|
| 144 |
+
f'{bond_type}{bond_type_counter[bond_type]}'
|
| 145 |
+
)
|
| 146 |
+
mmcif_dict['_struct_conn.pdbx_role'].append(bond_row['role'])
|
| 147 |
+
mmcif_dict['_struct_conn.conn_type_id'].append(bond_type)
|
| 148 |
+
|
| 149 |
+
bond_types = np.unique(self.type)
|
| 150 |
+
mmcif_dict['_struct_conn_type.id'] = bond_types
|
| 151 |
+
unknown = ['?'] * len(bond_types)
|
| 152 |
+
mmcif_dict['_struct_conn_type.criteria'] = unknown
|
| 153 |
+
mmcif_dict['_struct_conn_type.reference'] = unknown
|
| 154 |
+
|
| 155 |
+
return dict(mmcif_dict)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def concat_with_atom_keys(
|
| 159 |
+
bonds_tables: Sequence[Bonds | None],
|
| 160 |
+
atom_key_arrays: Sequence[np.ndarray],
|
| 161 |
+
) -> tuple[Bonds | None, np.ndarray]:
|
| 162 |
+
"""Concatenates bonds tables and atom keys simultaneously.
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
bonds_tables: A sequence of `Bonds` instances to concatenate. If any are
|
| 166 |
+
None then these are skipped.
|
| 167 |
+
atom_key_arrays: A sequence of integer `atom_key` arrays, where the n-th
|
| 168 |
+
bonds_table referrs to the atoms in the n-th atom_key array. These must
|
| 169 |
+
all be non-None.
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
A pair of (bonds, atom_key) where atom_key is a unique atom_key array with
|
| 173 |
+
length equal to the sum of the input atom array sizes, and the bonds table
|
| 174 |
+
contains all the bonds from the individual bonds table inputs.
|
| 175 |
+
"""
|
| 176 |
+
if not bonds_tables or not atom_key_arrays:
|
| 177 |
+
if bonds_tables or atom_key_arrays:
|
| 178 |
+
raise ValueError(
|
| 179 |
+
'bonds_tables and atom_keys must have same length but got'
|
| 180 |
+
f' {len(bonds_tables)=} and {len(atom_key_arrays)=}'
|
| 181 |
+
)
|
| 182 |
+
return None, np.array([], dtype=np.int64)
|
| 183 |
+
max_key = -1
|
| 184 |
+
atom_keys_to_concat = []
|
| 185 |
+
types_to_concat = []
|
| 186 |
+
roles_to_concat = []
|
| 187 |
+
from_atom_keys_to_concat = []
|
| 188 |
+
dest_atom_keys_to_concat = []
|
| 189 |
+
for bonds, atom_key in zip(bonds_tables, atom_key_arrays, strict=True):
|
| 190 |
+
if not atom_key.size:
|
| 191 |
+
assert bonds is None or bonds.size == 0
|
| 192 |
+
continue
|
| 193 |
+
assert np.min(atom_key, initial=0) >= 0 # Should always be non-negative!
|
| 194 |
+
offset = max_key + 1
|
| 195 |
+
offset_atom_key = atom_key + offset
|
| 196 |
+
atom_keys_to_concat.append(offset_atom_key)
|
| 197 |
+
max_key = np.max(offset_atom_key)
|
| 198 |
+
if bonds is not None:
|
| 199 |
+
types_to_concat.append(bonds.type)
|
| 200 |
+
roles_to_concat.append(bonds.role)
|
| 201 |
+
from_atom_keys_to_concat.append(bonds.from_atom_key + offset)
|
| 202 |
+
dest_atom_keys_to_concat.append(bonds.dest_atom_key + offset)
|
| 203 |
+
|
| 204 |
+
if atom_keys_to_concat:
|
| 205 |
+
concatted_atom_keys = np.concatenate(atom_keys_to_concat, axis=0)
|
| 206 |
+
else:
|
| 207 |
+
concatted_atom_keys = np.array([], dtype=np.int64)
|
| 208 |
+
|
| 209 |
+
if types_to_concat:
|
| 210 |
+
assert (
|
| 211 |
+
len(types_to_concat)
|
| 212 |
+
== len(roles_to_concat)
|
| 213 |
+
== len(from_atom_keys_to_concat)
|
| 214 |
+
== len(dest_atom_keys_to_concat)
|
| 215 |
+
)
|
| 216 |
+
num_bonds = sum(b.size for b in bonds_tables if b is not None)
|
| 217 |
+
concatted_bonds = Bonds(
|
| 218 |
+
key=np.arange(num_bonds, dtype=np.int64),
|
| 219 |
+
type=np.concatenate(types_to_concat, axis=0),
|
| 220 |
+
role=np.concatenate(roles_to_concat, axis=0),
|
| 221 |
+
from_atom_key=np.concatenate(from_atom_keys_to_concat, axis=0),
|
| 222 |
+
dest_atom_key=np.concatenate(dest_atom_keys_to_concat, axis=0),
|
| 223 |
+
)
|
| 224 |
+
else:
|
| 225 |
+
concatted_bonds = None
|
| 226 |
+
|
| 227 |
+
return concatted_bonds, concatted_atom_keys
|
flax_model/alphafold3/structure/chemical_components.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Utilities for manipulating chemical components data."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Iterable, Mapping, Sequence
|
| 6 |
+
import dataclasses
|
| 7 |
+
import functools
|
| 8 |
+
from typing import Self
|
| 9 |
+
|
| 10 |
+
from flax_model.alphafold3.constants import chemical_components
|
| 11 |
+
from flax_model.alphafold3.constants import residue_names
|
| 12 |
+
from flax_model.alphafold3.structure import mmcif
|
| 13 |
+
import rdkit.Chem as rd_chem
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclasses.dataclass(frozen=True)
|
| 17 |
+
class ChemCompEntry:
|
| 18 |
+
"""Items of _chem_comp category.
|
| 19 |
+
|
| 20 |
+
For the full list of items and their semantics see
|
| 21 |
+
http://mmcif.rcsb.org/dictionaries/mmcif_pdbx_v50.dic/Categories/chem_comp.html
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
type: str
|
| 25 |
+
name: str = '?'
|
| 26 |
+
pdbx_synonyms: str = '?'
|
| 27 |
+
formula: str = '?'
|
| 28 |
+
formula_weight: str = '?'
|
| 29 |
+
mon_nstd_flag: str = '?'
|
| 30 |
+
pdbx_smiles: str | None = None
|
| 31 |
+
|
| 32 |
+
def __post_init__(self):
|
| 33 |
+
for field, value in vars(self).items():
|
| 34 |
+
if not value and value is not None:
|
| 35 |
+
raise ValueError(f"{field} value can't be an empty string.")
|
| 36 |
+
|
| 37 |
+
def extends(self, other: Self) -> bool:
|
| 38 |
+
"""Checks whether this ChemCompEntry extends another one."""
|
| 39 |
+
for field, value in vars(self).items():
|
| 40 |
+
other_value = getattr(other, field)
|
| 41 |
+
if _value_is_missing(other_value):
|
| 42 |
+
continue
|
| 43 |
+
if value != other_value:
|
| 44 |
+
return False
|
| 45 |
+
return True
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def rdkit_mol(self) -> rd_chem.Mol:
|
| 49 |
+
"""Returns an RDKit Mol, created via RDKit from entry SMILES string."""
|
| 50 |
+
if not self.pdbx_smiles:
|
| 51 |
+
raise ValueError('Cannot construct RDKit Mol with empty pdbx_smiles')
|
| 52 |
+
return rd_chem.MolFromSmiles(self.pdbx_smiles)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
_REQUIRED_MMCIF_COLUMNS = ('_chem_comp.id', '_chem_comp.type')
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class MissingChemicalComponentsDataError(Exception):
|
| 59 |
+
"""Raised when chemical components data is missing from an mmCIF."""
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@dataclasses.dataclass(frozen=True)
|
| 63 |
+
class ChemicalComponentsData:
|
| 64 |
+
"""Extra information for chemical components occurring in mmCIF.
|
| 65 |
+
|
| 66 |
+
Fields:
|
| 67 |
+
chem_comp: A mapping from _chem_comp.id to associated items in the
|
| 68 |
+
chem_comp category.
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
chem_comp: Mapping[str, ChemCompEntry]
|
| 72 |
+
|
| 73 |
+
@classmethod
|
| 74 |
+
def from_mmcif(
|
| 75 |
+
cls, cif: mmcif.Mmcif, fix_mse: bool, fix_unknown_dna: bool
|
| 76 |
+
) -> Self:
|
| 77 |
+
"""Constructs an instance of ChemicalComponentsData from an Mmcif object."""
|
| 78 |
+
for col in _REQUIRED_MMCIF_COLUMNS:
|
| 79 |
+
if col not in cif:
|
| 80 |
+
raise MissingChemicalComponentsDataError(col)
|
| 81 |
+
|
| 82 |
+
id_ = cif['_chem_comp.id'] # Guaranteed to be present.
|
| 83 |
+
type_ = cif['_chem_comp.type'] # Guaranteed to be present.
|
| 84 |
+
name = cif.get('_chem_comp.name', ['?'] * len(id_))
|
| 85 |
+
synonyms = cif.get('_chem_comp.pdbx_synonyms', ['?'] * len(id_))
|
| 86 |
+
formula = cif.get('_chem_comp.formula', ['?'] * len(id_))
|
| 87 |
+
weight = cif.get('_chem_comp.formula_weight', ['?'] * len(id_))
|
| 88 |
+
mon_nstd_flag = cif.get('_chem_comp.mon_nstd_flag', ['?'] * len(id_))
|
| 89 |
+
smiles = cif.get('_chem_comp.pdbx_smiles', ['?'] * len(id_))
|
| 90 |
+
smiles = [None if s == '?' else s for s in smiles]
|
| 91 |
+
|
| 92 |
+
chem_comp = {
|
| 93 |
+
component_name: ChemCompEntry(*entry)
|
| 94 |
+
for component_name, *entry in zip(
|
| 95 |
+
id_, type_, name, synonyms, formula, weight, mon_nstd_flag, smiles
|
| 96 |
+
)
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
if fix_mse and 'MSE' in chem_comp:
|
| 100 |
+
if 'MET' not in chem_comp:
|
| 101 |
+
chem_comp['MET'] = ChemCompEntry(
|
| 102 |
+
type='L-PEPTIDE LINKING',
|
| 103 |
+
name='METHIONINE',
|
| 104 |
+
pdbx_synonyms='?',
|
| 105 |
+
formula='C5 H11 N O2 S',
|
| 106 |
+
formula_weight='149.211',
|
| 107 |
+
mon_nstd_flag='y',
|
| 108 |
+
pdbx_smiles=None,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
if fix_unknown_dna and 'N' in chem_comp:
|
| 112 |
+
# Do not delete 'N' as it may be needed for RNA in the system.
|
| 113 |
+
if 'DN' not in chem_comp:
|
| 114 |
+
chem_comp['DN'] = ChemCompEntry(
|
| 115 |
+
type='DNA LINKING',
|
| 116 |
+
name="UNKNOWN 2'-DEOXYNUCLEOTIDE",
|
| 117 |
+
pdbx_synonyms='?',
|
| 118 |
+
formula='C5 H11 O6 P',
|
| 119 |
+
formula_weight='198.111',
|
| 120 |
+
mon_nstd_flag='y',
|
| 121 |
+
pdbx_smiles=None,
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
return ChemicalComponentsData(chem_comp)
|
| 125 |
+
|
| 126 |
+
def to_mmcif_dict(self) -> Mapping[str, Sequence[str]]:
|
| 127 |
+
"""Returns chemical components data as a dict suitable for `mmcif.Mmcif`."""
|
| 128 |
+
mmcif_dict = {}
|
| 129 |
+
|
| 130 |
+
mmcif_fields = set()
|
| 131 |
+
for entry in self.chem_comp.values():
|
| 132 |
+
for field, value in vars(entry).items():
|
| 133 |
+
if value:
|
| 134 |
+
mmcif_fields.add(field)
|
| 135 |
+
chem_comp_ids = []
|
| 136 |
+
for component_id in sorted(self.chem_comp):
|
| 137 |
+
entry = self.chem_comp[component_id]
|
| 138 |
+
chem_comp_ids.append(component_id)
|
| 139 |
+
for field in mmcif_fields:
|
| 140 |
+
mmcif_dict.setdefault(f'_chem_comp.{field}', []).append(
|
| 141 |
+
getattr(entry, field) or '?'
|
| 142 |
+
)
|
| 143 |
+
if chem_comp_ids:
|
| 144 |
+
mmcif_dict['_chem_comp.id'] = chem_comp_ids
|
| 145 |
+
return mmcif_dict
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _value_is_missing(value: str) -> bool:
|
| 149 |
+
return not value or value in ('.', '?')
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def get_data_for_ccd_components(
|
| 153 |
+
ccd: chemical_components.Ccd,
|
| 154 |
+
chemical_component_ids: Iterable[str],
|
| 155 |
+
populate_pdbx_smiles: bool = False,
|
| 156 |
+
) -> ChemicalComponentsData:
|
| 157 |
+
"""Returns `ChemicalComponentsData` for chemical components known by PDB."""
|
| 158 |
+
chem_comp = {}
|
| 159 |
+
for chemical_component_id in chemical_component_ids:
|
| 160 |
+
chem_data = chemical_components.component_name_to_info(
|
| 161 |
+
ccd=ccd, res_name=chemical_component_id
|
| 162 |
+
)
|
| 163 |
+
if not chem_data:
|
| 164 |
+
continue
|
| 165 |
+
chem_comp[chemical_component_id] = ChemCompEntry(
|
| 166 |
+
type=chem_data.type,
|
| 167 |
+
name=chem_data.name,
|
| 168 |
+
pdbx_synonyms=chem_data.pdbx_synonyms,
|
| 169 |
+
formula=chem_data.formula,
|
| 170 |
+
formula_weight=chem_data.formula_weight,
|
| 171 |
+
mon_nstd_flag=chem_data.mon_nstd_flag,
|
| 172 |
+
pdbx_smiles=(
|
| 173 |
+
chem_data.pdbx_smiles or None if populate_pdbx_smiles else None
|
| 174 |
+
),
|
| 175 |
+
)
|
| 176 |
+
return ChemicalComponentsData(chem_comp=chem_comp)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def populate_missing_ccd_data(
|
| 180 |
+
ccd: chemical_components.Ccd,
|
| 181 |
+
chemical_components_data: ChemicalComponentsData,
|
| 182 |
+
chemical_component_ids: Iterable[str] | None = None,
|
| 183 |
+
populate_pdbx_smiles: bool = False,
|
| 184 |
+
) -> ChemicalComponentsData:
|
| 185 |
+
"""Populates missing data for the chemical components from CCD.
|
| 186 |
+
|
| 187 |
+
Args:
|
| 188 |
+
ccd: The chemical components database.
|
| 189 |
+
chemical_components_data: ChemicalComponentsData to populate missing values
|
| 190 |
+
for. This function doesn't modify the object, extended version is provided
|
| 191 |
+
as a return value.
|
| 192 |
+
chemical_component_ids: chemical components to populate missing values for.
|
| 193 |
+
If not specified, the function will consider all chemical components which
|
| 194 |
+
are already present in `chemical_components_data`.
|
| 195 |
+
populate_pdbx_smiles: whether to populate `pdbx_smiles` field using SMILES
|
| 196 |
+
descriptors from _pdbx_chem_comp_descriptor CCD table. If CCD provides
|
| 197 |
+
multiple SMILES strings, any of them could be used.
|
| 198 |
+
|
| 199 |
+
Returns:
|
| 200 |
+
New instance of ChemicalComponentsData without missing values for CCD
|
| 201 |
+
entries.
|
| 202 |
+
"""
|
| 203 |
+
if chemical_component_ids is None:
|
| 204 |
+
chemical_component_ids = chemical_components_data.chem_comp.keys()
|
| 205 |
+
|
| 206 |
+
ccd_data = get_data_for_ccd_components(
|
| 207 |
+
ccd, chemical_component_ids, populate_pdbx_smiles
|
| 208 |
+
)
|
| 209 |
+
chem_comp = dict(chemical_components_data.chem_comp)
|
| 210 |
+
for component_id, ccd_entry in ccd_data.chem_comp.items():
|
| 211 |
+
if component_id not in chem_comp:
|
| 212 |
+
chem_comp[component_id] = ccd_entry
|
| 213 |
+
else:
|
| 214 |
+
already_specified_fields = {
|
| 215 |
+
field: value
|
| 216 |
+
for field, value in vars(chem_comp[component_id]).items()
|
| 217 |
+
if not _value_is_missing(value)
|
| 218 |
+
}
|
| 219 |
+
chem_comp[component_id] = ChemCompEntry(
|
| 220 |
+
**{**vars(ccd_entry), **already_specified_fields}
|
| 221 |
+
)
|
| 222 |
+
return ChemicalComponentsData(chem_comp=chem_comp)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def get_all_atoms_in_entry(
|
| 226 |
+
ccd: chemical_components.Ccd, res_name: str
|
| 227 |
+
) -> Mapping[str, Sequence[str]]:
|
| 228 |
+
"""Get all possible atoms and bonds for this residue in a standard order.
|
| 229 |
+
|
| 230 |
+
Args:
|
| 231 |
+
ccd: The chemical components dictionary.
|
| 232 |
+
res_name: Full CCD name.
|
| 233 |
+
|
| 234 |
+
Returns:
|
| 235 |
+
A dictionary table of the atoms and bonds for this residue in this residue
|
| 236 |
+
type.
|
| 237 |
+
"""
|
| 238 |
+
# The CCD version of 'UNK' is weird. It has a CB and a CG atom. We just want
|
| 239 |
+
# the minimal amino-acid here which is GLY.
|
| 240 |
+
if res_name == 'UNK':
|
| 241 |
+
res_name = 'GLY'
|
| 242 |
+
ccd_data = ccd.get(res_name)
|
| 243 |
+
if not ccd_data:
|
| 244 |
+
raise ValueError(f'Unknown residue type {res_name}')
|
| 245 |
+
|
| 246 |
+
keys = (
|
| 247 |
+
'_chem_comp_atom.atom_id',
|
| 248 |
+
'_chem_comp_atom.type_symbol',
|
| 249 |
+
'_chem_comp_bond.atom_id_1',
|
| 250 |
+
'_chem_comp_bond.atom_id_2',
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
# Add terminal hydrogens for protonation of the N-terminal
|
| 254 |
+
if res_name == 'PRO':
|
| 255 |
+
res_atoms = {key: [*ccd_data.get(key, [])] for key in keys}
|
| 256 |
+
res_atoms['_chem_comp_atom.atom_id'].extend(['H2', 'H3'])
|
| 257 |
+
res_atoms['_chem_comp_atom.type_symbol'].extend(['H', 'H'])
|
| 258 |
+
res_atoms['_chem_comp_bond.atom_id_1'].extend(['N', 'N'])
|
| 259 |
+
res_atoms['_chem_comp_bond.atom_id_2'].extend(['H2', 'H3'])
|
| 260 |
+
elif res_name in residue_names.PROTEIN_TYPES_WITH_UNKNOWN:
|
| 261 |
+
res_atoms = {key: [*ccd_data.get(key, [])] for key in keys}
|
| 262 |
+
res_atoms['_chem_comp_atom.atom_id'].append('H3')
|
| 263 |
+
res_atoms['_chem_comp_atom.type_symbol'].append('H')
|
| 264 |
+
res_atoms['_chem_comp_bond.atom_id_1'].append('N')
|
| 265 |
+
res_atoms['_chem_comp_bond.atom_id_2'].append('H3')
|
| 266 |
+
else:
|
| 267 |
+
res_atoms = {key: ccd_data.get(key, []) for key in keys}
|
| 268 |
+
|
| 269 |
+
return res_atoms
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
@functools.lru_cache(maxsize=128)
|
| 273 |
+
def get_res_atom_names(ccd: chemical_components.Ccd, res_name: str) -> set[str]:
|
| 274 |
+
"""Gets the names of the atoms in a given CCD residue."""
|
| 275 |
+
atoms = get_all_atoms_in_entry(ccd, res_name)['_chem_comp_atom.atom_id']
|
| 276 |
+
return set(atoms)
|
flax_model/alphafold3/structure/cpp/__init__.py
ADDED
|
File without changes
|
flax_model/alphafold3/structure/cpp/aggregation.pyi
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
from collections.abc import Sequence
|
| 4 |
+
|
| 5 |
+
def indices_grouped_by_value(values: Sequence[int]) -> dict[int, list[int]]: ...
|
flax_model/alphafold3/structure/cpp/aggregation_pybind.cc
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright 2024 DeepMind Technologies Limited
|
| 2 |
+
//
|
| 3 |
+
// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of
|
| 4 |
+
// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
|
| 5 |
+
//
|
| 6 |
+
// To request access to the AlphaFold 3 model parameters, follow the process set
|
| 7 |
+
// out at https://github.com/google-deepmind/alphafold3. You may only use these
|
| 8 |
+
// if received directly from Google. Use is subject to terms of use available at
|
| 9 |
+
// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md
|
| 10 |
+
|
| 11 |
+
#include <cstdint>
|
| 12 |
+
#include <vector>
|
| 13 |
+
|
| 14 |
+
#include "absl/container/flat_hash_map.h"
|
| 15 |
+
#include "absl/types/span.h"
|
| 16 |
+
#include "pybind11/cast.h"
|
| 17 |
+
#include "pybind11/numpy.h"
|
| 18 |
+
#include "pybind11/pybind11.h"
|
| 19 |
+
#include "pybind11_abseil/absl_casters.h"
|
| 20 |
+
|
| 21 |
+
namespace {
|
| 22 |
+
|
| 23 |
+
namespace py = pybind11;
|
| 24 |
+
|
| 25 |
+
absl::flat_hash_map<int64_t, std::vector<int64_t>> IndicesGroupedByValue(
|
| 26 |
+
absl::Span<const int64_t> values) {
|
| 27 |
+
absl::flat_hash_map<int64_t, std::vector<int64_t>> group_indices;
|
| 28 |
+
for (int64_t i = 0, e = values.size(); i < e; ++i) {
|
| 29 |
+
group_indices[values[i]].push_back(i);
|
| 30 |
+
}
|
| 31 |
+
return group_indices;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
constexpr char kIndicesGroupedByValue[] = R"(
|
| 35 |
+
Returns a map from value to a list of indices this value occupies.
|
| 36 |
+
|
| 37 |
+
E.g. indices_grouped_by_value([1, 1, 2, 3, 3, 1, 1]) returns:
|
| 38 |
+
{1: [0, 1, 5, 6], 2: [2], 3: [3, 4]}
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
values: a list of values to group.
|
| 42 |
+
)";
|
| 43 |
+
|
| 44 |
+
} // namespace
|
| 45 |
+
|
| 46 |
+
namespace alphafold3 {
|
| 47 |
+
|
| 48 |
+
void RegisterModuleAggregation(py::module m) {
|
| 49 |
+
m.def("indices_grouped_by_value", &IndicesGroupedByValue, py::arg("values"),
|
| 50 |
+
py::doc(kIndicesGroupedByValue + 1),
|
| 51 |
+
py::call_guard<py::gil_scoped_release>());
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
} // namespace alphafold3
|
flax_model/alphafold3/structure/cpp/aggregation_pybind.h
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Copyright 2024 DeepMind Technologies Limited
|
| 3 |
+
*
|
| 4 |
+
* AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of
|
| 5 |
+
* this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
|
| 6 |
+
*
|
| 7 |
+
* To request access to the AlphaFold 3 model parameters, follow the process set
|
| 8 |
+
* out at https://github.com/google-deepmind/alphafold3. You may only use these
|
| 9 |
+
* if received directly from Google. Use is subject to terms of use available at
|
| 10 |
+
* https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_AGGREGATION_PYBIND_H_
|
| 14 |
+
#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_AGGREGATION_PYBIND_H_
|
| 15 |
+
|
| 16 |
+
#include "pybind11/pybind11.h"
|
| 17 |
+
|
| 18 |
+
namespace alphafold3 {
|
| 19 |
+
|
| 20 |
+
void RegisterModuleAggregation(pybind11::module m);
|
| 21 |
+
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_AGGREGATION_PYBIND_H_
|
flax_model/alphafold3/structure/cpp/mmcif_atom_site.pyi
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
from collections.abc import Callable
|
| 4 |
+
from flax_model.alphafold3.cpp import cif_dict
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def get_internal_to_author_chain_id_map(
|
| 8 |
+
mmcif: cif_dict.CifDict
|
| 9 |
+
) -> dict[str,str]: ...
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_or_infer_type_symbol(
|
| 13 |
+
mmcif: cif_dict.CifDict,
|
| 14 |
+
atom_id_to_type_symbol: Callable[[str, str], str],
|
| 15 |
+
) -> list[str]: ...
|
flax_model/alphafold3/structure/cpp/mmcif_struct_conn.pyi
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
from flax_model.alphafold3.cpp import cif_dict
|
| 4 |
+
|
| 5 |
+
def get_bond_atom_indices(mmcif_dict: cif_dict.CifDict, model_id: str) -> tuple[list[int],list[int]]: ...
|
flax_model/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright 2024 DeepMind Technologies Limited
|
| 2 |
+
//
|
| 3 |
+
// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of
|
| 4 |
+
// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
|
| 5 |
+
//
|
| 6 |
+
// To request access to the AlphaFold 3 model parameters, follow the process set
|
| 7 |
+
// out at https://github.com/google-deepmind/alphafold3. You may only use these
|
| 8 |
+
// if received directly from Google. Use is subject to terms of use available at
|
| 9 |
+
// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md
|
| 10 |
+
|
| 11 |
+
#include <cstddef>
|
| 12 |
+
#include <iterator>
|
| 13 |
+
#include <optional>
|
| 14 |
+
#include <string>
|
| 15 |
+
#include <utility>
|
| 16 |
+
#include <vector>
|
| 17 |
+
|
| 18 |
+
#include "absl/algorithm/container.h"
|
| 19 |
+
#include "absl/container/flat_hash_map.h"
|
| 20 |
+
#include "absl/container/flat_hash_set.h"
|
| 21 |
+
#include "absl/status/status.h"
|
| 22 |
+
#include "absl/status/statusor.h"
|
| 23 |
+
#include "absl/strings/str_cat.h"
|
| 24 |
+
#include "absl/strings/string_view.h"
|
| 25 |
+
#include "absl/types/span.h"
|
| 26 |
+
#include "alphafold3/parsers/cpp/cif_dict_lib.h"
|
| 27 |
+
#include "alphafold3/structure/cpp/mmcif_struct_conn.h"
|
| 28 |
+
|
| 29 |
+
namespace alphafold3 {
|
| 30 |
+
|
| 31 |
+
namespace {
|
| 32 |
+
|
| 33 |
+
struct AtomId {
|
| 34 |
+
absl::string_view chain_id;
|
| 35 |
+
absl::string_view res_id_1;
|
| 36 |
+
absl::string_view res_id_2;
|
| 37 |
+
absl::string_view atom_name;
|
| 38 |
+
absl::string_view alt_id;
|
| 39 |
+
|
| 40 |
+
friend bool operator==(const AtomId&, const AtomId&) = default;
|
| 41 |
+
template <typename H>
|
| 42 |
+
friend H AbslHashValue(H h, const AtomId& m) {
|
| 43 |
+
return H::combine(std::move(h), m.chain_id, m.res_id_1, m.res_id_2,
|
| 44 |
+
m.atom_name, m.alt_id);
|
| 45 |
+
}
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
using StringArrayRef = absl::Span<const std::string>;
|
| 49 |
+
using BondIndexByAtom = absl::flat_hash_map<AtomId, std::vector<std::size_t>>;
|
| 50 |
+
using BondAtomIndices = std::vector<std::size_t>;
|
| 51 |
+
|
| 52 |
+
// Returns whether each container is the same size.
|
| 53 |
+
template <typename C, typename... Cs>
|
| 54 |
+
bool AreSameSize(const C& c, const Cs&... cs) {
|
| 55 |
+
return ((c.size() == cs.size()) && ...);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
struct ColumnSpec {
|
| 59 |
+
absl::string_view chain_id_col;
|
| 60 |
+
absl::string_view res_id_1_col;
|
| 61 |
+
absl::string_view res_id_2_col;
|
| 62 |
+
absl::string_view atom_name_col;
|
| 63 |
+
std::optional<absl::string_view> alt_id_col; // Not used by OpenMM.
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
class AtomColumns {
|
| 67 |
+
public:
|
| 68 |
+
static absl::StatusOr<AtomColumns> Create(const CifDict& mmcif,
|
| 69 |
+
const ColumnSpec& column_spec) {
|
| 70 |
+
StringArrayRef chain_id = mmcif[column_spec.chain_id_col];
|
| 71 |
+
StringArrayRef res_id_1 = mmcif[column_spec.res_id_1_col];
|
| 72 |
+
StringArrayRef res_id_2 = mmcif[column_spec.res_id_2_col];
|
| 73 |
+
StringArrayRef atom_name = mmcif[column_spec.atom_name_col];
|
| 74 |
+
if (!AreSameSize(chain_id, res_id_1, res_id_2, atom_name)) {
|
| 75 |
+
return absl::InvalidArgumentError(absl::StrCat(
|
| 76 |
+
"Atom columns are not the same size. ", //
|
| 77 |
+
"len(", column_spec.chain_id_col, ")=", chain_id.size(), //
|
| 78 |
+
", len(", column_spec.res_id_1_col, ")=", res_id_1.size(), //
|
| 79 |
+
", len(", column_spec.res_id_2_col, ")=", res_id_2.size(), //
|
| 80 |
+
", len(", column_spec.atom_name_col, ")=", atom_name.size(), //
|
| 81 |
+
"."));
|
| 82 |
+
}
|
| 83 |
+
if (column_spec.alt_id_col.has_value()) {
|
| 84 |
+
StringArrayRef alt_id = mmcif[*column_spec.alt_id_col];
|
| 85 |
+
if (!AreSameSize(alt_id, chain_id)) {
|
| 86 |
+
return absl::InvalidArgumentError(absl::StrCat(
|
| 87 |
+
"Atom columns are not the same size. ", //
|
| 88 |
+
"len(", column_spec.chain_id_col, ")=", chain_id.size(), //
|
| 89 |
+
", len(", *column_spec.alt_id_col, ")=", alt_id.size(), //
|
| 90 |
+
"."));
|
| 91 |
+
}
|
| 92 |
+
return AtomColumns(chain_id, res_id_1, res_id_2, atom_name, alt_id,
|
| 93 |
+
column_spec);
|
| 94 |
+
} else {
|
| 95 |
+
return AtomColumns(chain_id, res_id_1, res_id_2, atom_name, std::nullopt,
|
| 96 |
+
column_spec);
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
inline std::size_t size() const { return size_; }
|
| 101 |
+
|
| 102 |
+
absl::string_view GetNormalizedAltId(const std::size_t index) const {
|
| 103 |
+
constexpr absl::string_view kFullStop = ".";
|
| 104 |
+
if (alt_id_.has_value()) {
|
| 105 |
+
absl::string_view alt_id = (*alt_id_)[index];
|
| 106 |
+
return alt_id == "?" ? kFullStop : alt_id;
|
| 107 |
+
} else {
|
| 108 |
+
return kFullStop;
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
AtomId GetAtom(const std::size_t index) const {
|
| 113 |
+
return {.chain_id = chain_id_[index],
|
| 114 |
+
.res_id_1 = res_id_1_[index],
|
| 115 |
+
.res_id_2 = res_id_2_[index],
|
| 116 |
+
.atom_name = atom_name_[index],
|
| 117 |
+
.alt_id = GetNormalizedAltId(index)};
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
std::string GetAtomString(const std::size_t index) const {
|
| 121 |
+
std::string alt_id_col;
|
| 122 |
+
if (column_spec_.alt_id_col.has_value()) {
|
| 123 |
+
alt_id_col = *column_spec_.alt_id_col;
|
| 124 |
+
} else {
|
| 125 |
+
alt_id_col = "default label_alt_id";
|
| 126 |
+
}
|
| 127 |
+
return absl::StrCat(
|
| 128 |
+
column_spec_.chain_id_col, "=", chain_id_[index], ", ", //
|
| 129 |
+
column_spec_.res_id_1_col, "=", res_id_1_[index], ", ", //
|
| 130 |
+
column_spec_.res_id_2_col, "=", res_id_2_[index], ", ", //
|
| 131 |
+
column_spec_.atom_name_col, "=", atom_name_[index], ", ", //
|
| 132 |
+
alt_id_col, "=", GetNormalizedAltId(index)); //
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
private:
|
| 136 |
+
AtomColumns(StringArrayRef chain_id, StringArrayRef res_id_1,
|
| 137 |
+
StringArrayRef res_id_2, StringArrayRef atom_name,
|
| 138 |
+
std::optional<StringArrayRef> alt_id,
|
| 139 |
+
const ColumnSpec& column_spec)
|
| 140 |
+
: chain_id_(chain_id),
|
| 141 |
+
res_id_1_(res_id_1),
|
| 142 |
+
res_id_2_(res_id_2),
|
| 143 |
+
atom_name_(atom_name),
|
| 144 |
+
alt_id_(alt_id),
|
| 145 |
+
column_spec_(column_spec),
|
| 146 |
+
size_(chain_id.size()) {}
|
| 147 |
+
StringArrayRef chain_id_;
|
| 148 |
+
StringArrayRef res_id_1_;
|
| 149 |
+
StringArrayRef res_id_2_;
|
| 150 |
+
StringArrayRef atom_name_;
|
| 151 |
+
std::optional<StringArrayRef> alt_id_;
|
| 152 |
+
ColumnSpec column_spec_;
|
| 153 |
+
std::size_t size_;
|
| 154 |
+
};
|
| 155 |
+
|
| 156 |
+
// Adds the atom index to any rows in the bond table involving that atom.
|
| 157 |
+
absl::Status FillInBondsForAtom(const BondIndexByAtom& bond_index_by_atom,
|
| 158 |
+
const AtomId& atom,
|
| 159 |
+
const std::size_t atom_index,
|
| 160 |
+
BondAtomIndices& bond_atom_indices) {
|
| 161 |
+
if (auto bond_index_it = bond_index_by_atom.find(atom);
|
| 162 |
+
bond_index_it != bond_index_by_atom.end()) {
|
| 163 |
+
for (std::size_t bond_index : bond_index_it->second) {
|
| 164 |
+
if (bond_index < 0 || bond_index >= bond_atom_indices.size()) {
|
| 165 |
+
return absl::OutOfRangeError(
|
| 166 |
+
absl::StrCat("Bond index out of range: ", bond_index));
|
| 167 |
+
}
|
| 168 |
+
bond_atom_indices[bond_index] = atom_index;
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
return absl::OkStatus();
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// Checks that the CifDict has all of the columns in the column spec.
|
| 175 |
+
bool HasAllColumns(const CifDict& mmcif, const ColumnSpec& columns) {
|
| 176 |
+
return mmcif.Contains(columns.chain_id_col) &&
|
| 177 |
+
mmcif.Contains(columns.res_id_1_col) &&
|
| 178 |
+
mmcif.Contains(columns.res_id_2_col) &&
|
| 179 |
+
mmcif.Contains(columns.atom_name_col) &&
|
| 180 |
+
(!columns.alt_id_col.has_value() ||
|
| 181 |
+
mmcif.Contains(*columns.alt_id_col));
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// Fully specified ptnr1 atom.
|
| 185 |
+
constexpr ColumnSpec kStructConnPtnr1ColumnsFull{
|
| 186 |
+
.chain_id_col = "_struct_conn.ptnr1_label_asym_id",
|
| 187 |
+
.res_id_1_col = "_struct_conn.ptnr1_auth_seq_id",
|
| 188 |
+
.res_id_2_col = "_struct_conn.pdbx_ptnr1_PDB_ins_code",
|
| 189 |
+
.atom_name_col = "_struct_conn.ptnr1_label_atom_id",
|
| 190 |
+
.alt_id_col = "_struct_conn.pdbx_ptnr1_label_alt_id",
|
| 191 |
+
};
|
| 192 |
+
|
| 193 |
+
// Fully specified ptnr2 atom.
|
| 194 |
+
constexpr ColumnSpec kStructConnPtnr2ColumnsFull{
|
| 195 |
+
.chain_id_col = "_struct_conn.ptnr2_label_asym_id",
|
| 196 |
+
.res_id_1_col = "_struct_conn.ptnr2_auth_seq_id",
|
| 197 |
+
.res_id_2_col = "_struct_conn.pdbx_ptnr2_PDB_ins_code",
|
| 198 |
+
.atom_name_col = "_struct_conn.ptnr2_label_atom_id",
|
| 199 |
+
.alt_id_col = "_struct_conn.pdbx_ptnr2_label_alt_id",
|
| 200 |
+
};
|
| 201 |
+
|
| 202 |
+
// Columns used by OpenMM for ptnr1 atoms.
|
| 203 |
+
constexpr ColumnSpec kStructConnPtnr1OpenMM{
|
| 204 |
+
.chain_id_col = "_struct_conn.ptnr1_label_asym_id",
|
| 205 |
+
.res_id_1_col = "_struct_conn.ptnr1_label_seq_id",
|
| 206 |
+
.res_id_2_col = "_struct_conn.ptnr1_label_comp_id",
|
| 207 |
+
.atom_name_col = "_struct_conn.ptnr1_label_atom_id",
|
| 208 |
+
.alt_id_col = std::nullopt,
|
| 209 |
+
};
|
| 210 |
+
|
| 211 |
+
// Columns used by OpenMM for ptnr2 atoms.
|
| 212 |
+
constexpr ColumnSpec kStructConnPtnr2OpenMM{
|
| 213 |
+
.chain_id_col = "_struct_conn.ptnr2_label_asym_id",
|
| 214 |
+
.res_id_1_col = "_struct_conn.ptnr2_label_seq_id",
|
| 215 |
+
.res_id_2_col = "_struct_conn.ptnr2_label_comp_id",
|
| 216 |
+
.atom_name_col = "_struct_conn.ptnr2_label_atom_id",
|
| 217 |
+
.alt_id_col = std::nullopt,
|
| 218 |
+
};
|
| 219 |
+
|
| 220 |
+
// Fully specified atom sites.
|
| 221 |
+
constexpr ColumnSpec kAtomSiteColumnsFull{
|
| 222 |
+
.chain_id_col = "_atom_site.label_asym_id",
|
| 223 |
+
.res_id_1_col = "_atom_site.auth_seq_id",
|
| 224 |
+
.res_id_2_col = "_atom_site.pdbx_PDB_ins_code",
|
| 225 |
+
.atom_name_col = "_atom_site.label_atom_id",
|
| 226 |
+
.alt_id_col = "_atom_site.label_alt_id",
|
| 227 |
+
};
|
| 228 |
+
|
| 229 |
+
// Atom site columns used to match OpenMM _struct_conn tables.
|
| 230 |
+
constexpr ColumnSpec kAtomSiteColumnsOpenMM{
|
| 231 |
+
.chain_id_col = "_atom_site.label_asym_id",
|
| 232 |
+
.res_id_1_col = "_atom_site.label_seq_id",
|
| 233 |
+
.res_id_2_col = "_atom_site.label_comp_id",
|
| 234 |
+
.atom_name_col = "_atom_site.label_atom_id",
|
| 235 |
+
.alt_id_col = "_atom_site.label_alt_id",
|
| 236 |
+
};
|
| 237 |
+
|
| 238 |
+
} // namespace
|
| 239 |
+
|
| 240 |
+
absl::StatusOr<std::pair<BondAtomIndices, BondAtomIndices>> GetBondAtomIndices(
|
| 241 |
+
const CifDict& mmcif, absl::string_view model_id) {
|
| 242 |
+
ColumnSpec ptnr1_columns, ptnr2_columns, atom_site_columns;
|
| 243 |
+
|
| 244 |
+
if (HasAllColumns(mmcif, kStructConnPtnr1ColumnsFull) &&
|
| 245 |
+
HasAllColumns(mmcif, kStructConnPtnr2ColumnsFull)) {
|
| 246 |
+
ptnr1_columns = kStructConnPtnr1ColumnsFull;
|
| 247 |
+
ptnr2_columns = kStructConnPtnr2ColumnsFull;
|
| 248 |
+
atom_site_columns = kAtomSiteColumnsFull;
|
| 249 |
+
} else {
|
| 250 |
+
ptnr1_columns = kStructConnPtnr1OpenMM;
|
| 251 |
+
ptnr2_columns = kStructConnPtnr2OpenMM;
|
| 252 |
+
atom_site_columns = kAtomSiteColumnsOpenMM;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
absl::StatusOr<AtomColumns> ptnr1_atoms =
|
| 256 |
+
AtomColumns::Create(mmcif, ptnr1_columns);
|
| 257 |
+
if (!ptnr1_atoms.ok()) {
|
| 258 |
+
return ptnr1_atoms.status();
|
| 259 |
+
}
|
| 260 |
+
absl::StatusOr<AtomColumns> ptnr2_atoms =
|
| 261 |
+
AtomColumns::Create(mmcif, ptnr2_columns);
|
| 262 |
+
if (!ptnr2_atoms.ok()) {
|
| 263 |
+
return ptnr2_atoms.status();
|
| 264 |
+
}
|
| 265 |
+
StringArrayRef struct_conn_id = mmcif["_struct_conn.id"];
|
| 266 |
+
if (!AreSameSize(struct_conn_id, *ptnr1_atoms, *ptnr2_atoms)) {
|
| 267 |
+
return absl::InvalidArgumentError(absl::StrCat(
|
| 268 |
+
"Invalid '_struct_conn.' loop. ", //
|
| 269 |
+
"len(id) = ", struct_conn_id.size(), ", ", //
|
| 270 |
+
"len(ptnr1_atoms) = ", ptnr1_atoms->size(), ", ", //
|
| 271 |
+
"len(ptnr2_atoms) = ", ptnr2_atoms->size(), "." //
|
| 272 |
+
));
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
absl::StatusOr<AtomColumns> atoms =
|
| 276 |
+
AtomColumns::Create(mmcif, atom_site_columns);
|
| 277 |
+
if (!atoms.ok()) {
|
| 278 |
+
return atoms.status();
|
| 279 |
+
}
|
| 280 |
+
StringArrayRef atom_site_id = mmcif["_atom_site.id"];
|
| 281 |
+
StringArrayRef atom_site_model_id = mmcif["_atom_site.pdbx_PDB_model_num"];
|
| 282 |
+
if (!AreSameSize(atom_site_id, atom_site_model_id, *atoms)) {
|
| 283 |
+
return absl::InvalidArgumentError(absl::StrCat(
|
| 284 |
+
"Invalid '_atom_site.' loop. ", //
|
| 285 |
+
"len(id)= ", atom_site_id.size(), ", ", //
|
| 286 |
+
"len(pdbx_PDB_model_num)= ", atom_site_model_id.size(), ", ", //
|
| 287 |
+
"len(atoms)= ", atoms->size(), ".")); //
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
// Build maps from atom ID tuples to the rows in _struct_conn where that
|
| 291 |
+
// atom appears (NB could be multiple).
|
| 292 |
+
const std::size_t struct_conn_size = struct_conn_id.size();
|
| 293 |
+
BondIndexByAtom ptnr1_rows_by_atom(struct_conn_size);
|
| 294 |
+
BondIndexByAtom ptnr2_rows_by_atom(struct_conn_size);
|
| 295 |
+
for (std::size_t i = 0; i < struct_conn_size; ++i) {
|
| 296 |
+
ptnr1_rows_by_atom[ptnr1_atoms->GetAtom(i)].push_back(i);
|
| 297 |
+
ptnr2_rows_by_atom[ptnr2_atoms->GetAtom(i)].push_back(i);
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
// Allocate two output arrays with one element per row in struct_conn, where
|
| 301 |
+
// each element will be the index of that atom in the atom_site table.
|
| 302 |
+
// Fill the arrays with atom_site_size, which is an invalid value, so that
|
| 303 |
+
// we can check at the end that each atom has been found.
|
| 304 |
+
const std::size_t atom_site_size = atom_site_id.size();
|
| 305 |
+
BondAtomIndices ptnr1_atom_indices(struct_conn_size, atom_site_size);
|
| 306 |
+
BondAtomIndices ptnr2_atom_indices(struct_conn_size, atom_site_size);
|
| 307 |
+
|
| 308 |
+
bool model_id_ecountered = false;
|
| 309 |
+
absl::flat_hash_set<absl::string_view> seen_alt_ids;
|
| 310 |
+
for (std::size_t atom_i = 0; atom_i < atom_site_size; ++atom_i) {
|
| 311 |
+
if (atom_site_model_id[atom_i] != model_id) {
|
| 312 |
+
if (!model_id_ecountered) {
|
| 313 |
+
continue;
|
| 314 |
+
} else {
|
| 315 |
+
// Models are contiguous so once we see a different model ID after
|
| 316 |
+
// encountering our model ID then we can exit early.
|
| 317 |
+
break;
|
| 318 |
+
}
|
| 319 |
+
} else {
|
| 320 |
+
model_id_ecountered = true;
|
| 321 |
+
}
|
| 322 |
+
AtomId atom = atoms->GetAtom(atom_i);
|
| 323 |
+
seen_alt_ids.insert(atom.alt_id);
|
| 324 |
+
|
| 325 |
+
if (auto fill_in_bonds_status1 = FillInBondsForAtom(
|
| 326 |
+
ptnr1_rows_by_atom, atom, atom_i, ptnr1_atom_indices);
|
| 327 |
+
!fill_in_bonds_status1.ok()) {
|
| 328 |
+
return fill_in_bonds_status1;
|
| 329 |
+
}
|
| 330 |
+
if (auto fill_in_bonds_status2 = FillInBondsForAtom(
|
| 331 |
+
ptnr2_rows_by_atom, atom, atom_i, ptnr2_atom_indices);
|
| 332 |
+
!fill_in_bonds_status2.ok()) {
|
| 333 |
+
return fill_in_bonds_status2;
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
// The seen_alt_ids check is a workaround for a known PDB issue: some mmCIFs
|
| 337 |
+
// (2evw, 2g0v, 2g0x, 2g0z, 2g10, 2g11, 2g12, 2g14, 2grz, 2ntw as of 2024)
|
| 338 |
+
// have multiple models and they set different whole-chain altloc in each
|
| 339 |
+
// model. The bond table however doesn't distinguish between models, so there
|
| 340 |
+
// are bonds that are valid only for some models. E.g. 2grz has model 1 with
|
| 341 |
+
// chain A with altloc A, and model 2 with chain A with altloc B. The bonds
|
| 342 |
+
// table lists a bond for each of these.
|
| 343 |
+
|
| 344 |
+
// Check that a ptnr1 atom was found for every bond.
|
| 345 |
+
if (auto row_it = absl::c_find(ptnr1_atom_indices, atom_site_size);
|
| 346 |
+
row_it != ptnr1_atom_indices.end()) {
|
| 347 |
+
if (seen_alt_ids.size() > 1 || seen_alt_ids.contains(".") ||
|
| 348 |
+
seen_alt_ids.contains("?")) {
|
| 349 |
+
std::size_t i = std::distance(ptnr1_atom_indices.begin(), row_it);
|
| 350 |
+
return absl::InvalidArgumentError(
|
| 351 |
+
absl::StrCat("Error parsing \"", mmcif.GetDataName(), "\". ",
|
| 352 |
+
"Cannot find atom for bond ID ", struct_conn_id[i], ": ",
|
| 353 |
+
ptnr1_atoms->GetAtomString(i)));
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// Check that a ptnr2 atom was found for every bond.
|
| 358 |
+
if (auto row_it = absl::c_find(ptnr2_atom_indices, atom_site_size);
|
| 359 |
+
row_it != ptnr2_atom_indices.end()) {
|
| 360 |
+
if (seen_alt_ids.size() > 1 || seen_alt_ids.contains(".") ||
|
| 361 |
+
seen_alt_ids.contains("?")) {
|
| 362 |
+
std::size_t i = std::distance(ptnr2_atom_indices.begin(), row_it);
|
| 363 |
+
return absl::InvalidArgumentError(
|
| 364 |
+
absl::StrCat("Error parsing \"", mmcif.GetDataName(), "\". ",
|
| 365 |
+
"Cannot find atom for bond ID ", struct_conn_id[i], ": ",
|
| 366 |
+
ptnr2_atoms->GetAtomString(i)));
|
| 367 |
+
}
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
if (!model_id_ecountered) {
|
| 371 |
+
return absl::InvalidArgumentError(absl::StrCat(
|
| 372 |
+
"Error parsing \"", mmcif.GetDataName(), "\". model_id \"", model_id,
|
| 373 |
+
"\" not found in _atom_site.pdbx_PDB_model_num."));
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
return std::make_pair(std::move(ptnr1_atom_indices),
|
| 377 |
+
std::move(ptnr2_atom_indices));
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
} // namespace alphafold3
|
flax_model/alphafold3/structure/mmcif.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Low level mmCIF parsing operations and wrappers for nicer C++/Py errors.
|
| 4 |
+
|
| 5 |
+
Note that the cif_dict.CifDict class has many useful methods to help with data
|
| 6 |
+
extraction which are not shown in this file. You can find them in cif_dict.clif
|
| 7 |
+
together with docstrings. The cif_dict.CifDict class behaves like an immutable
|
| 8 |
+
Python dictionary (some methods are not implemented though).
|
| 9 |
+
"""
|
| 10 |
+
from collections.abc import Callable, Mapping, Sequence
|
| 11 |
+
import functools
|
| 12 |
+
import itertools
|
| 13 |
+
import re
|
| 14 |
+
from typing import ParamSpec, TypeAlias, TypeVar
|
| 15 |
+
|
| 16 |
+
from flax_model.alphafold3.constants import chemical_components
|
| 17 |
+
from flax_model.alphafold3.cpp import cif_dict
|
| 18 |
+
from flax_model.alphafold3.cpp import mmcif_atom_site
|
| 19 |
+
from flax_model.alphafold3.cpp import mmcif_struct_conn
|
| 20 |
+
from flax_model.alphafold3.cpp import string_array
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
Mmcif = cif_dict.CifDict
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
_P = ParamSpec('_P')
|
| 27 |
+
_T = TypeVar('_T')
|
| 28 |
+
_WappedFn: TypeAlias = Callable[_P, _T]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@functools.lru_cache(maxsize=256)
|
| 32 |
+
def int_id_to_str_id(num: int) -> str:
|
| 33 |
+
"""Encodes a number as a string, using reverse spreadsheet style naming.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
num: A positive integer.
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
A string that encodes the positive integer using reverse spreadsheet style,
|
| 40 |
+
naming e.g. 1 = A, 2 = B, ..., 27 = AA, 28 = BA, 29 = CA, ... This is the
|
| 41 |
+
usual way to encode chain IDs in mmCIF files.
|
| 42 |
+
"""
|
| 43 |
+
if num <= 0:
|
| 44 |
+
raise ValueError(f'Only positive integers allowed, got {num}.')
|
| 45 |
+
|
| 46 |
+
num = num - 1 # 1-based indexing.
|
| 47 |
+
output = []
|
| 48 |
+
while num >= 0:
|
| 49 |
+
output.append(chr(num % 26 + ord('A')))
|
| 50 |
+
num = num // 26 - 1
|
| 51 |
+
return ''.join(output)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@functools.lru_cache(maxsize=256)
|
| 55 |
+
def str_id_to_int_id(str_id: str) -> int:
|
| 56 |
+
"""Encodes an mmCIF-style string chain ID as an integer.
|
| 57 |
+
|
| 58 |
+
The integer IDs are one based so this function is the inverse of
|
| 59 |
+
int_id_to_str_id.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
str_id: A string chain ID consisting only of upper case letters A-Z.
|
| 63 |
+
|
| 64 |
+
Returns:
|
| 65 |
+
An integer that can be used to order mmCIF chain IDs in the standard
|
| 66 |
+
(reverse spreadsheet style) ordering.
|
| 67 |
+
"""
|
| 68 |
+
if not re.match('^[A-Z]+$', str_id):
|
| 69 |
+
raise ValueError(f'String ID must be upper case letters, got {str_id}.')
|
| 70 |
+
|
| 71 |
+
offset = ord('A') - 1
|
| 72 |
+
output = 0
|
| 73 |
+
for i, c in enumerate(str_id):
|
| 74 |
+
output += (ord(c) - offset) * int(26**i)
|
| 75 |
+
return output
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def from_string(mmcif_string: str | bytes) -> Mmcif:
|
| 79 |
+
return cif_dict.from_string(mmcif_string)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def parse_multi_data_cif(cif_string: str) -> dict[str, Mmcif]:
|
| 83 |
+
"""Parses a CIF string with multiple data records.
|
| 84 |
+
|
| 85 |
+
For instance, the CIF string:
|
| 86 |
+
|
| 87 |
+
```
|
| 88 |
+
data_001
|
| 89 |
+
_foo bar
|
| 90 |
+
#
|
| 91 |
+
data_002
|
| 92 |
+
_foo baz
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
is parsed as:
|
| 96 |
+
|
| 97 |
+
```
|
| 98 |
+
{'001': Mmcif({'_foo': ['bar']}), '002': Mmcif({'_foo': ['baz']})}
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
cif_string: The multi-data CIF string to be parsed.
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
A dictionary mapping record names to Mmcif objects with data.
|
| 106 |
+
"""
|
| 107 |
+
return cif_dict.parse_multi_data_cif(cif_string)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# def tokenize(mmcif_string: str) -> list[str]:
|
| 111 |
+
# return cif_dict.tokenize(mmcif_string)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# def split_line(line: str) -> list[str]:
|
| 115 |
+
# return cif_dict.split_line(line)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class BondParsingError(Exception):
|
| 119 |
+
"""Exception raised by errors when getting bond atom indices."""
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def get_bond_atom_indices(
|
| 123 |
+
mmcif: Mmcif,
|
| 124 |
+
model_id: str = '1',
|
| 125 |
+
) -> tuple[Sequence[int], Sequence[int]]:
|
| 126 |
+
"""Extracts the indices of the atoms that participate in bonds.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
mmcif: The mmCIF object to process.
|
| 130 |
+
model_id: The ID of the model that the returned atoms will belong to. This
|
| 131 |
+
should be a value in the mmCIF's _atom_site.pdbx_PDB_model_num column.
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
Two lists of atom indices, `from_atoms` and `to_atoms`, each one having
|
| 135 |
+
length num_bonds (as defined by _struct_conn, the bonds table). The bond
|
| 136 |
+
i, defined by the i'th row in _struct_conn, is a bond from atom at index
|
| 137 |
+
from_atoms[i], to the atom at index to_atoms[i]. The indices are simple
|
| 138 |
+
0-based indexes into the columns of the _atom_site table in the input
|
| 139 |
+
mmCIF, and do not necessarily correspond to the values in _atom_site.id,
|
| 140 |
+
or any other column.
|
| 141 |
+
|
| 142 |
+
Raises:
|
| 143 |
+
BondParsingError: If any of the required tables or columns are not present
|
| 144 |
+
in
|
| 145 |
+
the mmCIF, or if the _struct_conn table refers to atoms that cannot
|
| 146 |
+
be found in the _atom_site table.
|
| 147 |
+
"""
|
| 148 |
+
try:
|
| 149 |
+
return mmcif_struct_conn.get_bond_atom_indices(mmcif, model_id)
|
| 150 |
+
except ValueError as e:
|
| 151 |
+
raise BondParsingError(str(e)) from e
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def get_or_infer_type_symbol(
|
| 155 |
+
mmcif: Mmcif, ccd: chemical_components.Ccd | None = None
|
| 156 |
+
) -> Sequence[str]:
|
| 157 |
+
"""Returns the type symbol (element) for all of the atoms.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
mmcif: A parsed mmCIF file in the Mmcif format.
|
| 161 |
+
ccd: The chemical component dictionary. If not provided, defaults to the
|
| 162 |
+
cached CCD.
|
| 163 |
+
|
| 164 |
+
If present, returns the _atom_site.type_symbol. If not, infers it using
|
| 165 |
+
_atom_site.label_comp_id (residue name), _atom_site.label_atom_id (atom name)
|
| 166 |
+
and the CCD.
|
| 167 |
+
"""
|
| 168 |
+
ccd = ccd or chemical_components.Ccd()
|
| 169 |
+
type_symbol_fn = lambda res_name, atom_name: chemical_components.type_symbol(
|
| 170 |
+
ccd, res_name, atom_name
|
| 171 |
+
)
|
| 172 |
+
return mmcif_atom_site.get_or_infer_type_symbol(mmcif, type_symbol_fn)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def get_chain_type_by_entity_id(mmcif: Mmcif) -> Mapping[str, str]:
|
| 176 |
+
"""Returns mapping from entity ID to its type or polymer type if available.
|
| 177 |
+
|
| 178 |
+
If the entity is in the _entity_poly table, returns its polymer chain type.
|
| 179 |
+
If not, returns the type as specified in the _entity table.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
mmcif: CifDict holding the mmCIF.
|
| 183 |
+
"""
|
| 184 |
+
poly_entity_id = mmcif.get('_entity_poly.entity_id', [])
|
| 185 |
+
poly_type = mmcif.get('_entity_poly.type', [])
|
| 186 |
+
poly_type_by_entity_id = dict(zip(poly_entity_id, poly_type, strict=True))
|
| 187 |
+
|
| 188 |
+
chain_type_by_entity_id = {}
|
| 189 |
+
for entity_id, entity_type in zip(
|
| 190 |
+
mmcif.get('_entity.id', []), mmcif.get('_entity.type', []), strict=True
|
| 191 |
+
):
|
| 192 |
+
chain_type = poly_type_by_entity_id.get(entity_id) or entity_type
|
| 193 |
+
chain_type_by_entity_id[entity_id] = chain_type
|
| 194 |
+
|
| 195 |
+
return chain_type_by_entity_id
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def get_internal_to_author_chain_id_map(mmcif: Mmcif) -> Mapping[str, str]:
|
| 199 |
+
"""Returns a mapping from internal chain ID to the author chain ID.
|
| 200 |
+
|
| 201 |
+
Note that this is not a bijection. One author chain ID can map to multiple
|
| 202 |
+
internal chain IDs. For example, a protein chain and a ligand bound to it will
|
| 203 |
+
share the same author chain ID, but they will each have a unique internal
|
| 204 |
+
chain ID).
|
| 205 |
+
|
| 206 |
+
Args:
|
| 207 |
+
mmcif: CifDict holding the mmCIF.
|
| 208 |
+
"""
|
| 209 |
+
return mmcif_atom_site.get_internal_to_author_chain_id_map(mmcif)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def get_experimental_method(mmcif: Mmcif) -> str | None:
|
| 213 |
+
field = '_exptl.method'
|
| 214 |
+
return ','.join(mmcif[field]).lower() if field in mmcif else None
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def get_release_date(mmcif: Mmcif) -> str | None:
|
| 218 |
+
"""Returns the oldest revision date."""
|
| 219 |
+
if '_pdbx_audit_revision_history.revision_date' not in mmcif:
|
| 220 |
+
return None
|
| 221 |
+
|
| 222 |
+
# Release dates are ISO-8601, hence sort well.
|
| 223 |
+
return min(mmcif['_pdbx_audit_revision_history.revision_date'])
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def get_resolution(mmcif: Mmcif) -> float | None:
|
| 227 |
+
"""Returns the resolution of the structure.
|
| 228 |
+
|
| 229 |
+
More than one resolution can be reported in an mmCIF. This function returns
|
| 230 |
+
the first one (in the order _refine.ls_d_res_high,
|
| 231 |
+
_em_3d_reconstruction.resolution, _reflns.d_resolution_high) that appears
|
| 232 |
+
in the mmCIF as is parseable as a float.
|
| 233 |
+
|
| 234 |
+
Args:
|
| 235 |
+
mmcif: An `Mmcif` object.
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
The resolution as reported in the mmCIF.
|
| 239 |
+
"""
|
| 240 |
+
for res_key in ('_refine.ls_d_res_high',
|
| 241 |
+
'_em_3d_reconstruction.resolution',
|
| 242 |
+
'_reflns.d_resolution_high'):
|
| 243 |
+
if res_key in mmcif:
|
| 244 |
+
try:
|
| 245 |
+
raw_resolution = mmcif[res_key][0]
|
| 246 |
+
return float(raw_resolution)
|
| 247 |
+
except ValueError:
|
| 248 |
+
continue
|
| 249 |
+
return None
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def parse_oper_expr(oper_expression: str) -> list[tuple[str, ...]]:
|
| 253 |
+
"""Determines which transforms to apply based on an MMCIF oper_expression str.
|
| 254 |
+
|
| 255 |
+
Args:
|
| 256 |
+
oper_expression: the field oper_expression from MMCIF format data.
|
| 257 |
+
Transform ids may be either numbers or single letters. Hyphens are used to
|
| 258 |
+
denote a numeric range of transforms to apply, and commas are used to
|
| 259 |
+
delimit a sequence of transforms. Where two sets of parentheses are
|
| 260 |
+
adjacent without a comma, the two sets of transforms should be combined as
|
| 261 |
+
a cartesian product, i.e. all possible pairs.
|
| 262 |
+
example 1,2,3 -> generate 3 copies of each chain by applying 1, 2 or 3.
|
| 263 |
+
example (1-3) -> generate 3 copies of each chain by applying 1, 2 or 3.
|
| 264 |
+
example (1-3)(4-6) -> generate 9 copies of each chain by applying one of
|
| 265 |
+
[(1,4), (1,5), (1,6),
|
| 266 |
+
(2,4), (2,5), (2,6),
|
| 267 |
+
(3,4), (3,5), (3,6)]
|
| 268 |
+
example (P) -> apply transform with id P.
|
| 269 |
+
|
| 270 |
+
Raises:
|
| 271 |
+
ValueError: Failure to parse oper_expression.
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
A list with one element for each chain copy that should be generated.
|
| 275 |
+
Each element is a list of transform ids to apply.
|
| 276 |
+
"""
|
| 277 |
+
# Expand ranges, e.g. 1-4 -> 1,2,3,4.
|
| 278 |
+
def range_expander(match):
|
| 279 |
+
return ','.join(
|
| 280 |
+
[str(i) for i in range(int(match.group(1)),
|
| 281 |
+
int(match.group(2)) + 1)])
|
| 282 |
+
|
| 283 |
+
ranges_expanded = re.sub(r'\b(\d+)-(\d+)', range_expander, oper_expression)
|
| 284 |
+
|
| 285 |
+
if re.fullmatch(r'(\w+,)*\w+', ranges_expanded):
|
| 286 |
+
# No brackets, just a single range, e.g. "1,2,3".
|
| 287 |
+
return [(t,) for t in ranges_expanded.split(',')]
|
| 288 |
+
elif re.fullmatch(r'\((\w+,)*\w+\)', ranges_expanded):
|
| 289 |
+
# Single range in brackets, e.g. "(1,2,3)".
|
| 290 |
+
return [(t,) for t in ranges_expanded[1:-1].split(',')]
|
| 291 |
+
elif re.fullmatch(r'\((\w+,)*\w+\)\((\w+,)*\w+\)', ranges_expanded):
|
| 292 |
+
# Cartesian product of two ranges, e.g. "(1,2,3)(4,5)".
|
| 293 |
+
part1, part2 = ranges_expanded[1:-1].split(')(')
|
| 294 |
+
return list(itertools.product(part1.split(','), part2.split(',')))
|
| 295 |
+
else:
|
| 296 |
+
raise ValueError(f'Unsupported oper_expression format: {oper_expression}')
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def format_float_array(
|
| 300 |
+
values: np.ndarray, num_decimal_places: int) -> Sequence[str]:
|
| 301 |
+
"""Converts 1D array to a list of strings with the given number of decimals.
|
| 302 |
+
|
| 303 |
+
This function is faster than converting via Python list comprehension, e.g.:
|
| 304 |
+
atoms_x = ['%.3f' % x for x in atoms_x]
|
| 305 |
+
|
| 306 |
+
Args:
|
| 307 |
+
values: A numpy array with values to convert. This array is casted to
|
| 308 |
+
float32 before doing the conversion.
|
| 309 |
+
num_decimal_places: The number of decimal points to keep, including trailing
|
| 310 |
+
zeros. E.g. for 1.07 and num_decimal_places=1: 1.1,
|
| 311 |
+
num_decimal_places=2: 1.07, num_decimal_places=3: 1.070.
|
| 312 |
+
|
| 313 |
+
Returns:
|
| 314 |
+
A list of formatted strings.
|
| 315 |
+
"""
|
| 316 |
+
if values.ndim != 1:
|
| 317 |
+
raise ValueError(f'The given array must be 1D, got {values.ndim}D')
|
| 318 |
+
|
| 319 |
+
return string_array.format_float_array(
|
| 320 |
+
values=values.astype(np.float32), num_decimal_places=num_decimal_places
|
| 321 |
+
)
|
flax_model/alphafold3/structure/parsing.py
ADDED
|
@@ -0,0 +1,1801 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Module for parsing various data sources and producing Structures."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Collection, Mapping, MutableMapping, Sequence
|
| 6 |
+
import dataclasses
|
| 7 |
+
import datetime
|
| 8 |
+
import enum
|
| 9 |
+
import functools
|
| 10 |
+
import itertools
|
| 11 |
+
from typing import TypeAlias
|
| 12 |
+
|
| 13 |
+
from flax_model.alphafold3.constants import chemical_components
|
| 14 |
+
from flax_model.alphafold3.constants import mmcif_names
|
| 15 |
+
from flax_model.alphafold3.constants import residue_names
|
| 16 |
+
from flax_model.alphafold3.cpp import mmcif_utils
|
| 17 |
+
from flax_model.alphafold3.cpp import string_array
|
| 18 |
+
from flax_model.alphafold3.structure import bioassemblies
|
| 19 |
+
from flax_model.alphafold3.structure import bonds
|
| 20 |
+
from flax_model.alphafold3.structure import chemical_components as struc_chem_comps
|
| 21 |
+
from flax_model.alphafold3.structure import mmcif
|
| 22 |
+
from flax_model.alphafold3.structure import structure
|
| 23 |
+
from flax_model.alphafold3.structure import structure_tables
|
| 24 |
+
import numpy as np
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
ChainIndex: TypeAlias = int
|
| 28 |
+
ResIndex: TypeAlias = int
|
| 29 |
+
AtomName: TypeAlias = str
|
| 30 |
+
BondAtomId: TypeAlias = tuple[ChainIndex, ResIndex, AtomName]
|
| 31 |
+
|
| 32 |
+
_INSERTION_CODE_REMAP: Mapping[str, str] = {'.': '?'}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class NoAtomsError(Exception):
|
| 36 |
+
"""Raise when the mmCIF does not have any atoms."""
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
| 40 |
+
class BondIndices:
|
| 41 |
+
from_indices: list[int]
|
| 42 |
+
dest_indices: list[int]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@enum.unique
|
| 46 |
+
class ModelID(enum.Enum):
|
| 47 |
+
"""Values for specifying model IDs when parsing."""
|
| 48 |
+
|
| 49 |
+
FIRST = 1 # The first model in the file.
|
| 50 |
+
ALL = 2 # All models in the file.
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@enum.unique
|
| 54 |
+
class SequenceFormat(enum.Enum):
|
| 55 |
+
"""The possible formats for an input sequence."""
|
| 56 |
+
|
| 57 |
+
FASTA = 'fasta' # One-letter code used in FASTA.
|
| 58 |
+
CCD_CODES = 'ccd_codes' # Multiple-letter chemical components dictionary ids.
|
| 59 |
+
LIGAND_SMILES = 'ligand_smiles' # SMILES string defining a molecule.
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _create_bond_lookup(
|
| 63 |
+
bonded_atom_pairs: Sequence[tuple[BondAtomId, BondAtomId]],
|
| 64 |
+
) -> Mapping[tuple[ChainIndex, ResIndex], Mapping[AtomName, BondIndices]]:
|
| 65 |
+
"""Creates maps to help find bonds during a loop over residues."""
|
| 66 |
+
bond_lookup = {}
|
| 67 |
+
for bond_i, (from_atom_id, dest_atom_id) in enumerate(bonded_atom_pairs):
|
| 68 |
+
from_chain_i, from_res_i, from_atom_name = from_atom_id
|
| 69 |
+
dest_chain_i, dest_res_i, dest_atom_name = dest_atom_id
|
| 70 |
+
bonds_by_from_atom_name = bond_lookup.setdefault(
|
| 71 |
+
(from_chain_i, from_res_i), {}
|
| 72 |
+
)
|
| 73 |
+
bonds_by_dest_atom_name = bond_lookup.setdefault(
|
| 74 |
+
(dest_chain_i, dest_res_i), {}
|
| 75 |
+
)
|
| 76 |
+
bonds_by_from_atom_name.setdefault(
|
| 77 |
+
from_atom_name, BondIndices(from_indices=[], dest_indices=[])
|
| 78 |
+
).from_indices.append(bond_i)
|
| 79 |
+
bonds_by_dest_atom_name.setdefault(
|
| 80 |
+
dest_atom_name, BondIndices(from_indices=[], dest_indices=[])
|
| 81 |
+
).dest_indices.append(bond_i)
|
| 82 |
+
return bond_lookup
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _get_atom_element(
|
| 86 |
+
ccd: chemical_components.Ccd, res_name: str, atom_name: str
|
| 87 |
+
) -> str:
|
| 88 |
+
type_symbol = chemical_components.type_symbol(
|
| 89 |
+
ccd, res_name=res_name, atom_name=atom_name
|
| 90 |
+
)
|
| 91 |
+
return type_symbol or '?'
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _get_representative_atom(
|
| 95 |
+
ccd: chemical_components.Ccd,
|
| 96 |
+
res_name: str,
|
| 97 |
+
chain_type: str,
|
| 98 |
+
sequence_format: SequenceFormat,
|
| 99 |
+
) -> tuple[str, str]:
|
| 100 |
+
match sequence_format:
|
| 101 |
+
case SequenceFormat.CCD_CODES:
|
| 102 |
+
atom_name = _get_first_non_leaving_atom(ccd=ccd, res_name=res_name)
|
| 103 |
+
atom_element = _get_atom_element(
|
| 104 |
+
ccd=ccd, res_name=res_name, atom_name=atom_name
|
| 105 |
+
)
|
| 106 |
+
return atom_name, atom_element
|
| 107 |
+
case SequenceFormat.LIGAND_SMILES:
|
| 108 |
+
return '', '?'
|
| 109 |
+
case SequenceFormat.FASTA:
|
| 110 |
+
if chain_type in mmcif_names.PEPTIDE_CHAIN_TYPES:
|
| 111 |
+
return 'CA', 'C'
|
| 112 |
+
if chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES:
|
| 113 |
+
return "C1'", 'C'
|
| 114 |
+
else:
|
| 115 |
+
raise ValueError(chain_type)
|
| 116 |
+
case _:
|
| 117 |
+
raise ValueError(sequence_format)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@functools.lru_cache(maxsize=128)
|
| 121 |
+
def _get_first_non_leaving_atom(
|
| 122 |
+
ccd: chemical_components.Ccd, res_name: str
|
| 123 |
+
) -> str:
|
| 124 |
+
"""Returns first definitely non-leaving atom if exists, as a stand-in."""
|
| 125 |
+
all_atoms = struc_chem_comps.get_all_atoms_in_entry(ccd, res_name=res_name)[
|
| 126 |
+
'_chem_comp_atom.atom_id'
|
| 127 |
+
]
|
| 128 |
+
representative_atom = all_atoms[0]
|
| 129 |
+
if representative_atom == 'O1' and len(all_atoms) > 1:
|
| 130 |
+
representative_atom = all_atoms[1]
|
| 131 |
+
return representative_atom
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _add_ligand_to_chem_comp(
|
| 135 |
+
chem_comp: MutableMapping[str, struc_chem_comps.ChemCompEntry],
|
| 136 |
+
ligand_id: str,
|
| 137 |
+
ligand_smiles: str,
|
| 138 |
+
):
|
| 139 |
+
"""Adds a ligand to chemical components. Raises ValueError on mismatch."""
|
| 140 |
+
new_entry = struc_chem_comps.ChemCompEntry(
|
| 141 |
+
type='non-polymer', pdbx_smiles=ligand_smiles
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
existing_entry = chem_comp.get(ligand_id)
|
| 145 |
+
if existing_entry is None:
|
| 146 |
+
chem_comp[ligand_id] = new_entry
|
| 147 |
+
elif existing_entry != new_entry:
|
| 148 |
+
raise ValueError(
|
| 149 |
+
f'Mismatching data for ligand {ligand_id}: '
|
| 150 |
+
f'{new_entry} != {existing_entry}'
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _get_first_model_id(cif: mmcif.Mmcif) -> str:
|
| 155 |
+
"""Returns cheaply the first model ID from the mmCIF."""
|
| 156 |
+
return cif.get_array(
|
| 157 |
+
'_atom_site.pdbx_PDB_model_num', dtype=object, gather=slice(1)
|
| 158 |
+
)[0]
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _get_str_model_id(
|
| 162 |
+
cif: mmcif.Mmcif,
|
| 163 |
+
model_id: ModelID | int,
|
| 164 |
+
) -> str:
|
| 165 |
+
"""Converts a user-specified model_id argument into a string."""
|
| 166 |
+
match model_id:
|
| 167 |
+
case int():
|
| 168 |
+
str_model_id = str(model_id)
|
| 169 |
+
case enum.Enum():
|
| 170 |
+
# We compare the enum's value attribute since regular enum comparison
|
| 171 |
+
# breaks when adhoc importing.
|
| 172 |
+
match model_id.value:
|
| 173 |
+
case ModelID.FIRST.value:
|
| 174 |
+
try:
|
| 175 |
+
str_model_id = _get_first_model_id(cif)
|
| 176 |
+
except IndexError as e:
|
| 177 |
+
raise NoAtomsError(
|
| 178 |
+
'The mmCIF does not have any atoms or'
|
| 179 |
+
' _atom_site.pdbx_PDB_model_num is missing.'
|
| 180 |
+
) from e
|
| 181 |
+
case ModelID.ALL.value:
|
| 182 |
+
str_model_id = ''
|
| 183 |
+
case _:
|
| 184 |
+
raise ValueError(
|
| 185 |
+
f'Model ID {model_id} with value {model_id.value} not recognized.'
|
| 186 |
+
)
|
| 187 |
+
case _:
|
| 188 |
+
raise ValueError(
|
| 189 |
+
f'Model ID {model_id} with type {type(model_id)} not recognized.'
|
| 190 |
+
)
|
| 191 |
+
return str_model_id
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def _parse_bonds(
|
| 195 |
+
cif: mmcif.Mmcif,
|
| 196 |
+
atom_key: np.ndarray,
|
| 197 |
+
model_id: str,
|
| 198 |
+
) -> bonds.Bonds:
|
| 199 |
+
"""Returns the bonds table extracted from the mmCIF.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
cif: The raw mmCIF to extract the bond information from.
|
| 203 |
+
atom_key: A numpy array defining atom key for each atom in _atom_site. Note
|
| 204 |
+
that the atom key must be computed before resolving alt-locs since this
|
| 205 |
+
function operates on the raw mmCIF!
|
| 206 |
+
model_id: The ID of the model to get bonds for.
|
| 207 |
+
"""
|
| 208 |
+
if '_struct_conn.id' not in cif:
|
| 209 |
+
# This is the category key item for the _struct_conn table, therefore
|
| 210 |
+
# we use it to determine whether to parse bond info.
|
| 211 |
+
return bonds.Bonds.make_empty()
|
| 212 |
+
from_atom, dest_atom = mmcif.get_bond_atom_indices(cif, model_id)
|
| 213 |
+
from_atom = np.array(from_atom, dtype=np.int64)
|
| 214 |
+
dest_atom = np.array(dest_atom, dtype=np.int64)
|
| 215 |
+
num_bonds = from_atom.shape[0]
|
| 216 |
+
bond_key = np.arange(num_bonds, dtype=np.int64)
|
| 217 |
+
bond_type = cif.get_array('_struct_conn.conn_type_id', dtype=object)
|
| 218 |
+
if '_struct_conn.pdbx_role' in cif: # This column isn't always present.
|
| 219 |
+
bond_role = cif.get_array('_struct_conn.pdbx_role', dtype=object)
|
| 220 |
+
else:
|
| 221 |
+
bond_role = np.full((num_bonds,), '?', dtype=object)
|
| 222 |
+
|
| 223 |
+
bonds_mask = np.ones((num_bonds,), dtype=bool)
|
| 224 |
+
# Symmetries other than 1_555 imply the atom is not part of the asymmetric
|
| 225 |
+
# unit, and therefore this is a bond that only exists in the expanded
|
| 226 |
+
# bioassembly.
|
| 227 |
+
# We do not currently support parsing these types of bonds.
|
| 228 |
+
if '_struct_conn.ptnr1_symmetry' in cif:
|
| 229 |
+
ptnr1_symmetry = cif.get_array('_struct_conn.ptnr1_symmetry', dtype=object)
|
| 230 |
+
np.logical_and(bonds_mask, ptnr1_symmetry == '1_555', out=bonds_mask)
|
| 231 |
+
if '_struct_conn.ptnr2_symmetry' in cif:
|
| 232 |
+
ptnr2_symmetry = cif.get_array('_struct_conn.ptnr2_symmetry', dtype=object)
|
| 233 |
+
np.logical_and(bonds_mask, ptnr2_symmetry == '1_555', out=bonds_mask)
|
| 234 |
+
# Remove bonds that involve atoms that are not part of the structure,
|
| 235 |
+
# e.g. waters if include_water=False. In a rare case this also removes invalid
|
| 236 |
+
# bonds that are indicated by a key that is set to _atom_site size.
|
| 237 |
+
np.logical_and(bonds_mask, np.isin(from_atom, atom_key), out=bonds_mask)
|
| 238 |
+
np.logical_and(bonds_mask, np.isin(dest_atom, atom_key), out=bonds_mask)
|
| 239 |
+
return bonds.Bonds(
|
| 240 |
+
key=bond_key[bonds_mask],
|
| 241 |
+
type=bond_type[bonds_mask],
|
| 242 |
+
role=bond_role[bonds_mask],
|
| 243 |
+
from_atom_key=from_atom[bonds_mask],
|
| 244 |
+
dest_atom_key=dest_atom[bonds_mask],
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
@dataclasses.dataclass(frozen=True, slots=True)
|
| 249 |
+
class _MmcifHeader:
|
| 250 |
+
name: str
|
| 251 |
+
resolution: float | None
|
| 252 |
+
release_date: datetime.date | None
|
| 253 |
+
structure_method: str | None
|
| 254 |
+
bioassembly_data: bioassemblies.BioassemblyData | None
|
| 255 |
+
chemical_components_data: struc_chem_comps.ChemicalComponentsData | None
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def _get_mmcif_header(
|
| 259 |
+
cif: mmcif.Mmcif,
|
| 260 |
+
fix_mse: bool,
|
| 261 |
+
fix_unknown_dna: bool,
|
| 262 |
+
) -> _MmcifHeader:
|
| 263 |
+
"""Extract header fields from an mmCIF object."""
|
| 264 |
+
entry_id = cif.get('_entry.id')
|
| 265 |
+
name = entry_id[0] if entry_id else cif.get_data_name()
|
| 266 |
+
resolution = mmcif.get_resolution(cif)
|
| 267 |
+
|
| 268 |
+
release_date = mmcif.get_release_date(cif)
|
| 269 |
+
if release_date is not None:
|
| 270 |
+
release_date = datetime.date.fromisoformat(release_date)
|
| 271 |
+
|
| 272 |
+
experiments = cif.get('_exptl.method')
|
| 273 |
+
structure_method = ','.join(experiments) if experiments else None
|
| 274 |
+
|
| 275 |
+
try:
|
| 276 |
+
bioassembly_data = bioassemblies.BioassemblyData.from_mmcif(cif)
|
| 277 |
+
except bioassemblies.MissingBioassemblyDataError:
|
| 278 |
+
bioassembly_data = None
|
| 279 |
+
|
| 280 |
+
try:
|
| 281 |
+
chemical_components_data = (
|
| 282 |
+
struc_chem_comps.ChemicalComponentsData.from_mmcif(
|
| 283 |
+
cif, fix_mse=fix_mse, fix_unknown_dna=fix_unknown_dna
|
| 284 |
+
)
|
| 285 |
+
)
|
| 286 |
+
except struc_chem_comps.MissingChemicalComponentsDataError:
|
| 287 |
+
chemical_components_data = None
|
| 288 |
+
|
| 289 |
+
return _MmcifHeader(
|
| 290 |
+
name=name,
|
| 291 |
+
resolution=resolution,
|
| 292 |
+
release_date=release_date,
|
| 293 |
+
structure_method=structure_method,
|
| 294 |
+
bioassembly_data=bioassembly_data,
|
| 295 |
+
chemical_components_data=chemical_components_data,
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def from_parsed_mmcif(
|
| 300 |
+
mmcif_object: mmcif.Mmcif,
|
| 301 |
+
*,
|
| 302 |
+
name: str | None = None,
|
| 303 |
+
fix_mse_residues: bool = False,
|
| 304 |
+
fix_arginines: bool = False,
|
| 305 |
+
fix_unknown_dna: bool = False,
|
| 306 |
+
include_water: bool = False,
|
| 307 |
+
include_other: bool = False,
|
| 308 |
+
include_bonds: bool = False,
|
| 309 |
+
model_id: int | ModelID = ModelID.FIRST,
|
| 310 |
+
) -> structure.Structure:
|
| 311 |
+
"""Construct a Structure from a parsed mmCIF object.
|
| 312 |
+
|
| 313 |
+
This function is called by `from_mmcif` but can be useful when an mmCIF has
|
| 314 |
+
already been parsed e.g. to extract extra information from the header before
|
| 315 |
+
then converting to Structure for further manipulation.
|
| 316 |
+
|
| 317 |
+
Args:
|
| 318 |
+
mmcif_object: A parsed mmcif.Mmcif object.
|
| 319 |
+
name: Optional name for the structure. If not provided, the name will be
|
| 320 |
+
taken from the mmCIF data_ field.
|
| 321 |
+
fix_mse_residues: If True, selenium atom sites (SE) in selenomethionine
|
| 322 |
+
(MSE) residues will be changed to sulphur atom sites (SD). This is because
|
| 323 |
+
methionine (MET) residues are often replaced with MSE to aid X-Ray
|
| 324 |
+
crystallography. If False, the SE MSE atom sites won't be modified.
|
| 325 |
+
fix_arginines: If True, NH1 and NH2 in arginine will be swapped if needed so
|
| 326 |
+
that NH1 is always closer to CD than NH2. If False, no atom sites in
|
| 327 |
+
arginine will be touched. Note that HH11, HH12, HH21, HH22 are fixed too.
|
| 328 |
+
fix_unknown_dna: If True, residues with name N in DNA chains will have their
|
| 329 |
+
res_name replaced with DN. Atoms are not changed.
|
| 330 |
+
include_water: If True, water (HOH) molecules will be parsed. Water
|
| 331 |
+
molecules may be grouped into chains, where number of residues > 1. Water
|
| 332 |
+
molecules are usually grouped into chains but do not necessarily all share
|
| 333 |
+
the same chain ID.
|
| 334 |
+
include_other: If True, all other atoms that are not included by any of the
|
| 335 |
+
above parameters will be included. This covers e.g. "polypeptide(D)" and
|
| 336 |
+
"macrolide" entities, as well as all other non-standard types.
|
| 337 |
+
include_bonds: If True, bond information will be parsed from the mmCIF and
|
| 338 |
+
stored in the Structure.
|
| 339 |
+
model_id: Either the integer model ID to parse, or one of ModelID.FIRST to
|
| 340 |
+
parse the first model, or ModelID.ALL to parse all models.
|
| 341 |
+
|
| 342 |
+
Returns:
|
| 343 |
+
A Structure representation of the mmCIF object.
|
| 344 |
+
"""
|
| 345 |
+
str_model_id = _get_str_model_id(cif=mmcif_object, model_id=model_id)
|
| 346 |
+
header = _get_mmcif_header(
|
| 347 |
+
mmcif_object, fix_mse=fix_mse_residues, fix_unknown_dna=fix_unknown_dna
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
chains, residues, atoms = get_tables(
|
| 351 |
+
cif=mmcif_object,
|
| 352 |
+
fix_mse_residues=fix_mse_residues,
|
| 353 |
+
fix_arginines=fix_arginines,
|
| 354 |
+
fix_unknown_dna=fix_unknown_dna,
|
| 355 |
+
include_water=include_water,
|
| 356 |
+
include_other=include_other,
|
| 357 |
+
model_id=str_model_id,
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
if include_bonds and atoms.size > 0:
|
| 361 |
+
# NB: parsing the atom table before the bonds table allows for a more
|
| 362 |
+
# informative error message when dealing with bad multi-model mmCIFs.
|
| 363 |
+
# Also always use a specific model ID, even when parsing all models.
|
| 364 |
+
if str_model_id == '': # pylint: disable=g-explicit-bool-comparison
|
| 365 |
+
bonds_model_id = _get_first_model_id(mmcif_object)
|
| 366 |
+
else:
|
| 367 |
+
bonds_model_id = str_model_id
|
| 368 |
+
|
| 369 |
+
bonds_table = _parse_bonds(
|
| 370 |
+
mmcif_object,
|
| 371 |
+
atom_key=atoms.key,
|
| 372 |
+
model_id=bonds_model_id,
|
| 373 |
+
)
|
| 374 |
+
else:
|
| 375 |
+
bonds_table = bonds.Bonds.make_empty()
|
| 376 |
+
|
| 377 |
+
return structure.Structure(
|
| 378 |
+
name=name if name is not None else header.name,
|
| 379 |
+
resolution=header.resolution,
|
| 380 |
+
release_date=header.release_date,
|
| 381 |
+
structure_method=header.structure_method,
|
| 382 |
+
bioassembly_data=header.bioassembly_data,
|
| 383 |
+
chemical_components_data=header.chemical_components_data,
|
| 384 |
+
bonds=bonds_table,
|
| 385 |
+
chains=chains,
|
| 386 |
+
residues=residues,
|
| 387 |
+
atoms=atoms,
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def from_mmcif(
|
| 392 |
+
mmcif_string: str | bytes,
|
| 393 |
+
*,
|
| 394 |
+
name: str | None = None,
|
| 395 |
+
fix_mse_residues: bool = False,
|
| 396 |
+
fix_arginines: bool = False,
|
| 397 |
+
fix_unknown_dna: bool = False,
|
| 398 |
+
include_water: bool = False,
|
| 399 |
+
include_other: bool = False,
|
| 400 |
+
include_bonds: bool = False,
|
| 401 |
+
model_id: int | ModelID = ModelID.FIRST,
|
| 402 |
+
) -> structure.Structure:
|
| 403 |
+
"""Construct a Structure from a mmCIF string.
|
| 404 |
+
|
| 405 |
+
Args:
|
| 406 |
+
mmcif_string: The string contents of an mmCIF file.
|
| 407 |
+
name: Optional name for the structure. If not provided, the name will be
|
| 408 |
+
taken from the mmCIF data_ field.
|
| 409 |
+
fix_mse_residues: If True, selenium atom sites (SE) in selenomethionine
|
| 410 |
+
(MSE) residues will be changed to sulphur atom sites (SD). This is because
|
| 411 |
+
methionine (MET) residues are often replaced with MSE to aid X-Ray
|
| 412 |
+
crystallography. If False, the SE MSE atom sites won't be modified.
|
| 413 |
+
fix_arginines: If True, NH1 and NH2 in arginine will be swapped if needed so
|
| 414 |
+
that NH1 is always closer to CD than NH2. If False, no atom sites in
|
| 415 |
+
arginine will be touched. Note that HH11, HH12, HH21, HH22 are fixed too.
|
| 416 |
+
fix_unknown_dna: If True, residues with name N in DNA chains will have their
|
| 417 |
+
res_name replaced with DN. Atoms are not changed.
|
| 418 |
+
include_water: If True, water (HOH) molecules will be parsed. Water
|
| 419 |
+
molecules may be grouped into chains, where number of residues > 1. Water
|
| 420 |
+
molecules are usually grouped into chains but do not necessarily all share
|
| 421 |
+
the same chain ID.
|
| 422 |
+
include_other: If True, all other atoms that are not included by any of the
|
| 423 |
+
above parameters will be included. This covers e.g. "polypeptide(D)" and
|
| 424 |
+
"macrolide" entities, as well as all other non-standard types.
|
| 425 |
+
include_bonds: If True, bond information will be parsed from the mmCIF and
|
| 426 |
+
stored in the Structure.
|
| 427 |
+
model_id: Either the integer model ID to parse, or one of ModelID.FIRST to
|
| 428 |
+
parse the first model, or ModelID.ALL to parse all models.
|
| 429 |
+
|
| 430 |
+
Returns:
|
| 431 |
+
A Structure representation of the mmCIF string.
|
| 432 |
+
"""
|
| 433 |
+
mmcif_object = mmcif.from_string(mmcif_string)
|
| 434 |
+
|
| 435 |
+
return from_parsed_mmcif(
|
| 436 |
+
mmcif_object,
|
| 437 |
+
name=name,
|
| 438 |
+
fix_mse_residues=fix_mse_residues,
|
| 439 |
+
fix_arginines=fix_arginines,
|
| 440 |
+
fix_unknown_dna=fix_unknown_dna,
|
| 441 |
+
include_water=include_water,
|
| 442 |
+
include_other=include_other,
|
| 443 |
+
include_bonds=include_bonds,
|
| 444 |
+
model_id=model_id,
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
def from_res_arrays(atom_mask: np.ndarray, **kwargs) -> structure.Structure:
|
| 449 |
+
"""Returns Structure created from from arrays with a residue dimension.
|
| 450 |
+
|
| 451 |
+
All unset fields are filled with defaults (e.g. 1.0 for occupancy) or
|
| 452 |
+
unset/unknown values (e.g. UNK for residue type, or '.' for atom element).
|
| 453 |
+
|
| 454 |
+
Args:
|
| 455 |
+
atom_mask: A array with shape (num_res, num_atom). This is used to decide
|
| 456 |
+
which atoms in the atom dimension are present in a given residue. Present
|
| 457 |
+
atoms should have a nonzero value, e.g. 1.0 or True.
|
| 458 |
+
**kwargs: A mapping from field name to values. For all array-valued fields
|
| 459 |
+
these arrays must have a dimension of length num_res. Chain and residue
|
| 460 |
+
fields should have this as their only dimension and atom fields should be
|
| 461 |
+
shaped (num_res, num_atom). Coordinate fields may also have arbitrary
|
| 462 |
+
leading dimensions (they must be the same across all coordinate fields).
|
| 463 |
+
See structure.{CHAIN,RESIDUE,ATOM}_FIELDS for a list of allowed fields.
|
| 464 |
+
"""
|
| 465 |
+
num_res, num_atom = atom_mask.shape
|
| 466 |
+
included_indices = np.flatnonzero(atom_mask)
|
| 467 |
+
|
| 468 |
+
array_fields = (
|
| 469 |
+
structure.CHAIN_FIELDS.keys()
|
| 470 |
+
| structure.RESIDUE_FIELDS.keys()
|
| 471 |
+
| structure.ATOM_FIELDS.keys()
|
| 472 |
+
)
|
| 473 |
+
initializer_kwargs = {}
|
| 474 |
+
fields = {}
|
| 475 |
+
for k, val in kwargs.items():
|
| 476 |
+
if k not in array_fields:
|
| 477 |
+
# The kwarg key isn't an array field name. Such kwargs are forwarded as-is
|
| 478 |
+
# to the constructor. They are expected to be global fields (e.g. name).
|
| 479 |
+
# Other values will raise an error when the constructor is called.
|
| 480 |
+
if k in structure.TABLE_FIELDS:
|
| 481 |
+
raise ValueError(f'Table fields must not be set. Got {k}.')
|
| 482 |
+
initializer_kwargs[k] = val
|
| 483 |
+
continue
|
| 484 |
+
elif val is None:
|
| 485 |
+
raise ValueError(f'{k} must be non-None.')
|
| 486 |
+
|
| 487 |
+
if not isinstance(val, np.ndarray):
|
| 488 |
+
raise TypeError(f'Value for {k} must be a NumPy array. Got {type(val)}.')
|
| 489 |
+
if k in structure.CHAIN_FIELDS or k in structure.RESIDUE_FIELDS:
|
| 490 |
+
if val.shape != (num_res,):
|
| 491 |
+
raise ValueError(
|
| 492 |
+
f'{k} must have shape ({num_res=},). Got {val.shape=}.'
|
| 493 |
+
)
|
| 494 |
+
# Do not reshape the chain/residue arrays, they have the shape we need.
|
| 495 |
+
fields[k] = val
|
| 496 |
+
else:
|
| 497 |
+
assert k in structure.ATOM_FIELDS
|
| 498 |
+
if val.shape[-2:] != (num_res, num_atom):
|
| 499 |
+
raise ValueError(
|
| 500 |
+
f'{k} must have final two dimensions of length '
|
| 501 |
+
f'{(num_res, num_atom)=}. Got {val.shape=}.'
|
| 502 |
+
)
|
| 503 |
+
leading_dims = val.shape[:-2]
|
| 504 |
+
flat_val = val.reshape(leading_dims + (-1,), order='C')
|
| 505 |
+
masked_val = flat_val[..., included_indices]
|
| 506 |
+
fields[k] = masked_val
|
| 507 |
+
|
| 508 |
+
# Get chain IDs or assume this is a single-chain structure.
|
| 509 |
+
chain_id = kwargs.get('chain_id', np.array(['A'] * num_res, dtype=object))
|
| 510 |
+
# Find chain starts in res-sized arrays, use these to make chain-sized arrays.
|
| 511 |
+
chain_start = np.concatenate(
|
| 512 |
+
([0], np.where(chain_id[1:] != chain_id[:-1])[0] + 1)
|
| 513 |
+
)
|
| 514 |
+
if len(set(chain_id)) != len(chain_start):
|
| 515 |
+
raise ValueError(f'Chain IDs must be contiguous, but got {chain_id}')
|
| 516 |
+
|
| 517 |
+
chain_lengths = np.diff(chain_start, append=len(chain_id))
|
| 518 |
+
chain_key = np.repeat(np.arange(len(chain_start)), chain_lengths)
|
| 519 |
+
|
| 520 |
+
chain_entity_id = fields.get('chain_entity_id')
|
| 521 |
+
if chain_entity_id is not None:
|
| 522 |
+
entity_id = chain_entity_id[chain_start]
|
| 523 |
+
else:
|
| 524 |
+
entity_id = np.array(
|
| 525 |
+
[str(mmcif.str_id_to_int_id(cid)) for cid in chain_id[chain_start]],
|
| 526 |
+
dtype=object,
|
| 527 |
+
)
|
| 528 |
+
chain_str_empty = np.full((num_res,), '.', dtype=object)
|
| 529 |
+
chains_table = structure_tables.Chains(
|
| 530 |
+
key=chain_key[chain_start],
|
| 531 |
+
id=chain_id[chain_start],
|
| 532 |
+
type=fields.get('chain_type', chain_str_empty)[chain_start],
|
| 533 |
+
auth_asym_id=fields.get('chain_auth_asym_id', chain_id)[chain_start],
|
| 534 |
+
entity_id=entity_id,
|
| 535 |
+
entity_desc=fields.get('chain_entity_desc', chain_str_empty)[chain_start],
|
| 536 |
+
)
|
| 537 |
+
|
| 538 |
+
# Since all arrays are residue-shaped, we can use them directly.
|
| 539 |
+
res_key = np.arange(num_res, dtype=np.int64)
|
| 540 |
+
res_id = fields.get('res_id', res_key + 1).astype(np.int32)
|
| 541 |
+
residues_table = structure_tables.Residues(
|
| 542 |
+
key=res_key,
|
| 543 |
+
chain_key=chain_key,
|
| 544 |
+
id=res_id,
|
| 545 |
+
name=fields.get('res_name', np.full(num_res, 'UNK', dtype=object)),
|
| 546 |
+
auth_seq_id=fields.get(
|
| 547 |
+
'res_auth_seq_id', np.char.mod('%d', res_id).astype(object)
|
| 548 |
+
),
|
| 549 |
+
insertion_code=fields.get(
|
| 550 |
+
'res_insertion_code', np.full(num_res, '?', dtype=object)
|
| 551 |
+
),
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
# The atom-sized arrays have already been masked and reshaped.
|
| 555 |
+
num_atoms_per_res = np.sum(atom_mask, axis=1, dtype=np.int32)
|
| 556 |
+
num_atoms_total = np.sum(num_atoms_per_res, dtype=np.int32)
|
| 557 |
+
# Structure is immutable, so use the same array multiple times to save RAM.
|
| 558 |
+
atom_str_empty = np.full(num_atoms_total, '.', dtype=object)
|
| 559 |
+
atom_float32_zeros = np.zeros(num_atoms_total, dtype=np.float32)
|
| 560 |
+
atom_float32_ones = np.ones(num_atoms_total, dtype=np.float32)
|
| 561 |
+
atoms_table = structure_tables.Atoms(
|
| 562 |
+
key=np.arange(num_atoms_total, dtype=np.int64),
|
| 563 |
+
chain_key=np.repeat(chain_key, num_atoms_per_res),
|
| 564 |
+
res_key=np.repeat(res_key, num_atoms_per_res),
|
| 565 |
+
name=fields.get('atom_name', atom_str_empty),
|
| 566 |
+
element=fields.get('atom_element', atom_str_empty),
|
| 567 |
+
x=fields.get('atom_x', atom_float32_zeros),
|
| 568 |
+
y=fields.get('atom_y', atom_float32_zeros),
|
| 569 |
+
z=fields.get('atom_z', atom_float32_zeros),
|
| 570 |
+
b_factor=fields.get('atom_b_factor', atom_float32_zeros),
|
| 571 |
+
occupancy=fields.get('atom_occupancy', atom_float32_ones),
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
return structure.Structure(
|
| 575 |
+
chains=chains_table,
|
| 576 |
+
residues=residues_table,
|
| 577 |
+
atoms=atoms_table,
|
| 578 |
+
bonds=structure_tables.Bonds.make_empty(), # Currently not set.
|
| 579 |
+
**initializer_kwargs,
|
| 580 |
+
)
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
def expand_sequence(
|
| 584 |
+
sequence: str, chain_type: str, sequence_format: SequenceFormat
|
| 585 |
+
) -> Sequence[str]:
|
| 586 |
+
"""Returns full residue names based on a sequence string.
|
| 587 |
+
|
| 588 |
+
Args:
|
| 589 |
+
sequence: A string representing the sequence.
|
| 590 |
+
chain_type: The chain type of the sequence.
|
| 591 |
+
sequence_format: The format of the sequence argument.
|
| 592 |
+
"""
|
| 593 |
+
match sequence_format:
|
| 594 |
+
case SequenceFormat.FASTA:
|
| 595 |
+
if not all(c.isalpha() for c in sequence):
|
| 596 |
+
raise ValueError(f'Sequence "{sequence}" has non-alphabetic characters')
|
| 597 |
+
match chain_type:
|
| 598 |
+
case mmcif_names.PROTEIN_CHAIN:
|
| 599 |
+
res_name_map = residue_names.PROTEIN_COMMON_ONE_TO_THREE
|
| 600 |
+
default_res_name = residue_names.UNK
|
| 601 |
+
case mmcif_names.RNA_CHAIN:
|
| 602 |
+
res_name_map = {r: r for r in residue_names.RNA_TYPES}
|
| 603 |
+
default_res_name = residue_names.UNK_RNA
|
| 604 |
+
case mmcif_names.DNA_CHAIN:
|
| 605 |
+
res_name_map = residue_names.DNA_COMMON_ONE_TO_TWO
|
| 606 |
+
default_res_name = residue_names.UNK_DNA
|
| 607 |
+
case _:
|
| 608 |
+
raise ValueError(f'{chain_type=} not supported for FASTA format.')
|
| 609 |
+
return [
|
| 610 |
+
res_name_map.get(one_letter_res, default_res_name)
|
| 611 |
+
for one_letter_res in sequence
|
| 612 |
+
]
|
| 613 |
+
case SequenceFormat.CCD_CODES:
|
| 614 |
+
return sequence.strip('()').split(')(')
|
| 615 |
+
case SequenceFormat.LIGAND_SMILES:
|
| 616 |
+
ligand_id, _ = sequence.split(':', maxsplit=1)
|
| 617 |
+
return [ligand_id]
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
def from_sequences_and_bonds(
|
| 621 |
+
*,
|
| 622 |
+
sequences: Sequence[str],
|
| 623 |
+
chain_types: Sequence[str],
|
| 624 |
+
sequence_formats: Sequence[SequenceFormat],
|
| 625 |
+
bonded_atom_pairs: Sequence[tuple[BondAtomId, BondAtomId]] | None,
|
| 626 |
+
ccd: chemical_components.Ccd,
|
| 627 |
+
chain_ids: Sequence[str] | None = None,
|
| 628 |
+
name: str = 'from_sequences_and_bonds',
|
| 629 |
+
bond_type: str | None = None,
|
| 630 |
+
**constructor_args,
|
| 631 |
+
) -> structure.Structure:
|
| 632 |
+
"""Returns a minimal structure for the input sequences and bonds.
|
| 633 |
+
|
| 634 |
+
The returned structure will have at least one atom per residue. If the
|
| 635 |
+
residue has any bonded atoms, according to `bonded_atom_pairs`, then
|
| 636 |
+
all (and only) those atoms will be present for that residue. If the residue
|
| 637 |
+
is not involved in any bond then an arbitrary atom will be created.
|
| 638 |
+
|
| 639 |
+
Args:
|
| 640 |
+
sequences: A sequence of strings, each one representing a single chain.
|
| 641 |
+
chain_types: The types of each chain, e.g. polypeptide(L). The n-th element
|
| 642 |
+
describes the n-th sequence in `sequences`.
|
| 643 |
+
sequence_formats: The format of each sequence. The n-th element describes
|
| 644 |
+
the n-th sequence in `sequences`.
|
| 645 |
+
bonded_atom_pairs: A sequence of bonded atom pairs. Each atom is described
|
| 646 |
+
as a tuple of (chain_index, res_index, atom_name), where the first two
|
| 647 |
+
values are 0-based indices. The chain_index is the index of the chain in
|
| 648 |
+
the `sequences` argument, and the res_index is the index of the residue in
|
| 649 |
+
that sequence. The atom_name is the name of the atom in the residue, e.g.
|
| 650 |
+
CA. If the atom is not found in the standard atoms for that residue
|
| 651 |
+
(according to the CCD) then an error is raised.
|
| 652 |
+
ccd: The chemical components dictionary.
|
| 653 |
+
chain_ids: A sequence of chain IDs, one for each chain in `sequences`. If
|
| 654 |
+
not provided, then the chain IDs will be generated automatically based on
|
| 655 |
+
sequence indices.
|
| 656 |
+
name: A name for the returned structure.
|
| 657 |
+
bond_type: This type will be used for all bonds in the structure, where type
|
| 658 |
+
follows PDB scheme, e.g. unknown (?), hydrog, metalc, covale, disulf.
|
| 659 |
+
**constructor_args: These arguments are passed directly to the
|
| 660 |
+
structure.Structure constructor.
|
| 661 |
+
"""
|
| 662 |
+
chain_id = []
|
| 663 |
+
chain_type = []
|
| 664 |
+
chain_res_count = []
|
| 665 |
+
res_id = []
|
| 666 |
+
res_name = []
|
| 667 |
+
res_atom_count = []
|
| 668 |
+
atom_name = []
|
| 669 |
+
atom_element = []
|
| 670 |
+
chem_comp = {}
|
| 671 |
+
|
| 672 |
+
num_bonds = len(bonded_atom_pairs or ())
|
| 673 |
+
from_atom_key = np.full((num_bonds,), -1, dtype=np.int64)
|
| 674 |
+
dest_atom_key = np.full((num_bonds,), -1, dtype=np.int64)
|
| 675 |
+
|
| 676 |
+
# Create map (chain_i, res_i) -> {atom_name -> (from_idxs dest_idxs)}.
|
| 677 |
+
# This allows quick lookup of whether a residue has any bonded atoms, and
|
| 678 |
+
# which bonds those atoms participate in.
|
| 679 |
+
bond_lookup = _create_bond_lookup(bonded_atom_pairs or ())
|
| 680 |
+
|
| 681 |
+
current_atom_key = 0
|
| 682 |
+
for chain_i, (sequence, curr_chain_type, sequence_format) in enumerate(
|
| 683 |
+
zip(sequences, chain_types, sequence_formats, strict=True)
|
| 684 |
+
):
|
| 685 |
+
if chain_ids is not None:
|
| 686 |
+
current_chain_id = chain_ids[chain_i]
|
| 687 |
+
else:
|
| 688 |
+
current_chain_id = mmcif.int_id_to_str_id(chain_i + 1)
|
| 689 |
+
num_chain_residues = 0
|
| 690 |
+
for res_i, full_res_name in enumerate(
|
| 691 |
+
expand_sequence(sequence, curr_chain_type, sequence_format)
|
| 692 |
+
):
|
| 693 |
+
current_res_id = res_i + 1
|
| 694 |
+
num_res_atoms = 0
|
| 695 |
+
|
| 696 |
+
# Look for bonded atoms in the bond lookup and if any are found, add
|
| 697 |
+
# their atom keys to the bond atom_key columns.
|
| 698 |
+
if bond_indices_by_atom_name := bond_lookup.get((chain_i, res_i)):
|
| 699 |
+
comp_atoms = None
|
| 700 |
+
if sequence_format != SequenceFormat.LIGAND_SMILES:
|
| 701 |
+
comp_atoms = set(ccd.get(full_res_name)['_chem_comp_atom.atom_id'])
|
| 702 |
+
for bond_atom_name, bond_indices in bond_indices_by_atom_name.items():
|
| 703 |
+
if comp_atoms is not None and bond_atom_name not in comp_atoms:
|
| 704 |
+
raise ValueError(
|
| 705 |
+
f'Bonded atom "{bond_atom_name}" was not found in the list of'
|
| 706 |
+
f' atoms of the chemical component {full_res_name}. Valid atom'
|
| 707 |
+
f' names for {full_res_name} are: {sorted(comp_atoms)}.'
|
| 708 |
+
' This is likely caused by an invalid atom name in the bonded'
|
| 709 |
+
f' atom (chain_id={current_chain_id}, res_id={current_res_id},'
|
| 710 |
+
f' atom_name={bond_atom_name}) specified in `bondedAtomPairs`'
|
| 711 |
+
' in the input JSON.'
|
| 712 |
+
)
|
| 713 |
+
atom_name.append(bond_atom_name)
|
| 714 |
+
atom_element.append(
|
| 715 |
+
_get_atom_element(
|
| 716 |
+
ccd=ccd, res_name=full_res_name, atom_name=bond_atom_name
|
| 717 |
+
)
|
| 718 |
+
)
|
| 719 |
+
for from_bond_i in bond_indices.from_indices:
|
| 720 |
+
from_atom_key[from_bond_i] = current_atom_key
|
| 721 |
+
for dest_bond_i in bond_indices.dest_indices:
|
| 722 |
+
dest_atom_key[dest_bond_i] = current_atom_key
|
| 723 |
+
current_atom_key += 1
|
| 724 |
+
num_res_atoms += 1
|
| 725 |
+
else:
|
| 726 |
+
# If this residue has no bonded atoms then we need to add one atom
|
| 727 |
+
# like in from_sequences.
|
| 728 |
+
assert num_res_atoms == 0
|
| 729 |
+
rep_atom_name, rep_atom_element = _get_representative_atom(
|
| 730 |
+
ccd=ccd,
|
| 731 |
+
res_name=full_res_name,
|
| 732 |
+
chain_type=curr_chain_type,
|
| 733 |
+
sequence_format=sequence_format,
|
| 734 |
+
)
|
| 735 |
+
atom_name.append(rep_atom_name)
|
| 736 |
+
atom_element.append(rep_atom_element)
|
| 737 |
+
num_res_atoms += 1
|
| 738 |
+
current_atom_key += 1
|
| 739 |
+
|
| 740 |
+
if sequence_format == SequenceFormat.LIGAND_SMILES:
|
| 741 |
+
# Sequence expect to be in the format <ligand_id>:<ligand_smiles>,
|
| 742 |
+
# which always corresponds to a single-residue chain.
|
| 743 |
+
ligand_id, ligand_smiles = sequence.split(':', maxsplit=1)
|
| 744 |
+
if ccd.get(ligand_id) is not None:
|
| 745 |
+
raise ValueError(
|
| 746 |
+
f'Ligand name {ligand_id} is in CCD - it is not supported to give'
|
| 747 |
+
' ligands created from SMILES the same name as CCD components.'
|
| 748 |
+
)
|
| 749 |
+
# We need to provide additional chemical components metadata for
|
| 750 |
+
# ligands specified via SMILES strings since they might not be in CCD.
|
| 751 |
+
_add_ligand_to_chem_comp(chem_comp, ligand_id, ligand_smiles)
|
| 752 |
+
|
| 753 |
+
assert num_res_atoms >= 1
|
| 754 |
+
res_atom_count.append(num_res_atoms)
|
| 755 |
+
num_chain_residues += 1
|
| 756 |
+
res_id.append(current_res_id)
|
| 757 |
+
res_name.append(full_res_name)
|
| 758 |
+
|
| 759 |
+
chain_id.append(current_chain_id)
|
| 760 |
+
chain_type.append(curr_chain_type)
|
| 761 |
+
chain_res_count.append(num_chain_residues)
|
| 762 |
+
|
| 763 |
+
chem_comp_data = struc_chem_comps.ChemicalComponentsData(chem_comp)
|
| 764 |
+
chem_comp_data = struc_chem_comps.populate_missing_ccd_data(
|
| 765 |
+
ccd=ccd,
|
| 766 |
+
chemical_components_data=chem_comp_data,
|
| 767 |
+
chemical_component_ids=set(res_name),
|
| 768 |
+
)
|
| 769 |
+
|
| 770 |
+
if bonded_atom_pairs is not None:
|
| 771 |
+
unknown_bond_col = np.full((num_bonds,), '?', dtype=object)
|
| 772 |
+
if bond_type is None:
|
| 773 |
+
bond_type_col = unknown_bond_col
|
| 774 |
+
else:
|
| 775 |
+
bond_type_col = np.full((num_bonds,), bond_type, dtype=object)
|
| 776 |
+
bonds_table = bonds.Bonds(
|
| 777 |
+
key=np.arange(num_bonds, dtype=np.int64),
|
| 778 |
+
type=bond_type_col,
|
| 779 |
+
role=unknown_bond_col,
|
| 780 |
+
from_atom_key=from_atom_key,
|
| 781 |
+
dest_atom_key=dest_atom_key,
|
| 782 |
+
)
|
| 783 |
+
else:
|
| 784 |
+
bonds_table = structure_tables.Bonds.make_empty()
|
| 785 |
+
|
| 786 |
+
chain_key = np.arange(len(sequences), dtype=np.int64) # 1 chain per sequence.
|
| 787 |
+
chain_id = np.array(chain_id, dtype=object)
|
| 788 |
+
chains_table = structure_tables.Chains(
|
| 789 |
+
key=chain_key,
|
| 790 |
+
id=chain_id,
|
| 791 |
+
type=np.array(chain_type, dtype=object),
|
| 792 |
+
auth_asym_id=chain_id,
|
| 793 |
+
entity_id=np.char.mod('%d', chain_key + 1).astype(object),
|
| 794 |
+
entity_desc=np.array(['.'] * len(chain_key), dtype=object),
|
| 795 |
+
)
|
| 796 |
+
|
| 797 |
+
res_key = np.arange(len(res_name), dtype=np.int64)
|
| 798 |
+
res_chain_key = np.repeat(chain_key, chain_res_count)
|
| 799 |
+
residues_table = structure_tables.Residues(
|
| 800 |
+
key=res_key,
|
| 801 |
+
chain_key=res_chain_key,
|
| 802 |
+
id=np.array(res_id, dtype=np.int32),
|
| 803 |
+
name=np.array(res_name, dtype=object),
|
| 804 |
+
auth_seq_id=np.char.mod('%d', res_id).astype(object),
|
| 805 |
+
insertion_code=np.full(len(res_name), '?', dtype=object),
|
| 806 |
+
)
|
| 807 |
+
|
| 808 |
+
num_atoms = current_atom_key
|
| 809 |
+
atom_float32_zeros = np.zeros(num_atoms, dtype=np.float32)
|
| 810 |
+
atoms_table = structure_tables.Atoms(
|
| 811 |
+
key=np.arange(num_atoms, dtype=np.int64),
|
| 812 |
+
chain_key=np.repeat(res_chain_key, res_atom_count),
|
| 813 |
+
res_key=np.repeat(res_key, res_atom_count),
|
| 814 |
+
name=np.array(atom_name, dtype=object),
|
| 815 |
+
element=np.array(atom_element, dtype=object),
|
| 816 |
+
x=atom_float32_zeros,
|
| 817 |
+
y=atom_float32_zeros,
|
| 818 |
+
z=atom_float32_zeros,
|
| 819 |
+
b_factor=atom_float32_zeros,
|
| 820 |
+
occupancy=np.ones(num_atoms, np.float32),
|
| 821 |
+
)
|
| 822 |
+
|
| 823 |
+
return structure.Structure(
|
| 824 |
+
name=name,
|
| 825 |
+
atoms=atoms_table,
|
| 826 |
+
residues=residues_table,
|
| 827 |
+
chains=chains_table,
|
| 828 |
+
bonds=bonds_table,
|
| 829 |
+
chemical_components_data=chem_comp_data,
|
| 830 |
+
**constructor_args,
|
| 831 |
+
)
|
| 832 |
+
|
| 833 |
+
|
| 834 |
+
class _ChainResBuilder:
|
| 835 |
+
"""Class for incrementally building chain and residue tables."""
|
| 836 |
+
|
| 837 |
+
def __init__(
|
| 838 |
+
self,
|
| 839 |
+
*,
|
| 840 |
+
chain_key_by_chain_id: Mapping[str, int],
|
| 841 |
+
entity_id_by_chain_id: Mapping[str, str],
|
| 842 |
+
chain_type_by_entity_id: Mapping[str, str],
|
| 843 |
+
entity_desc_by_entity_id: Mapping[str, str],
|
| 844 |
+
fix_mse_residues: bool,
|
| 845 |
+
fix_unknown_dna: bool,
|
| 846 |
+
):
|
| 847 |
+
# Len: num_chains.
|
| 848 |
+
self.chain_key = []
|
| 849 |
+
self.chain_id = []
|
| 850 |
+
self.chain_type = []
|
| 851 |
+
self.chain_auth_asym_id = []
|
| 852 |
+
self.chain_entity_id = []
|
| 853 |
+
self.chain_entity_desc = []
|
| 854 |
+
|
| 855 |
+
# Len: num_residues.
|
| 856 |
+
self.res_key = []
|
| 857 |
+
self.res_chain_key = []
|
| 858 |
+
self.res_id = []
|
| 859 |
+
self.res_name = []
|
| 860 |
+
self.res_auth_seq_id = []
|
| 861 |
+
self.res_insertion_code = []
|
| 862 |
+
|
| 863 |
+
self.chain_key_by_chain_id = chain_key_by_chain_id
|
| 864 |
+
self.entity_id_by_chain_id = entity_id_by_chain_id
|
| 865 |
+
self.chain_type_by_entity_id = chain_type_by_entity_id
|
| 866 |
+
self.entity_desc_by_entity_id = entity_desc_by_entity_id
|
| 867 |
+
self.key_for_res: dict[tuple[str, str, str, str], int] = {}
|
| 868 |
+
|
| 869 |
+
self._fix_mse_residues = fix_mse_residues
|
| 870 |
+
self._fix_unknown_dna = fix_unknown_dna
|
| 871 |
+
|
| 872 |
+
def add_residues(
|
| 873 |
+
self,
|
| 874 |
+
*,
|
| 875 |
+
chain_ids: np.ndarray,
|
| 876 |
+
chain_auth_asym_ids: np.ndarray,
|
| 877 |
+
res_ids: np.ndarray,
|
| 878 |
+
res_names: np.ndarray,
|
| 879 |
+
res_auth_seq_ids: np.ndarray,
|
| 880 |
+
res_ins_codes: np.ndarray,
|
| 881 |
+
):
|
| 882 |
+
"""Adds a residue (and its chain) to the tables."""
|
| 883 |
+
# Create chain table data.
|
| 884 |
+
if chain_ids.size == 0:
|
| 885 |
+
return
|
| 886 |
+
|
| 887 |
+
chain_ids_with_prev = np.concatenate(
|
| 888 |
+
(([self.chain_id[-1] if self.chain_id else None], chain_ids))
|
| 889 |
+
)
|
| 890 |
+
chain_change_mask = chain_ids_with_prev[:-1] != chain_ids_with_prev[1:]
|
| 891 |
+
chain_change_ids = chain_ids[chain_change_mask]
|
| 892 |
+
chain_keys = string_array.remap(
|
| 893 |
+
chain_change_ids, self.chain_key_by_chain_id, inplace=False
|
| 894 |
+
)
|
| 895 |
+
self.chain_key.extend(chain_keys)
|
| 896 |
+
self.chain_id.extend(chain_change_ids)
|
| 897 |
+
self.chain_auth_asym_id.extend(chain_auth_asym_ids[chain_change_mask])
|
| 898 |
+
chain_entity_id = string_array.remap(
|
| 899 |
+
chain_change_ids, self.entity_id_by_chain_id, inplace=False
|
| 900 |
+
)
|
| 901 |
+
self.chain_entity_id.extend(chain_entity_id)
|
| 902 |
+
chain_type = string_array.remap(
|
| 903 |
+
chain_entity_id, self.chain_type_by_entity_id, inplace=False
|
| 904 |
+
)
|
| 905 |
+
self.chain_type.extend(chain_type)
|
| 906 |
+
chain_entity_desc = string_array.remap(
|
| 907 |
+
chain_entity_id, self.entity_desc_by_entity_id, inplace=False
|
| 908 |
+
)
|
| 909 |
+
self.chain_entity_desc.extend(chain_entity_desc)
|
| 910 |
+
|
| 911 |
+
# Create residue table data.
|
| 912 |
+
num_prev_res = len(self.res_id)
|
| 913 |
+
res_keys = np.arange(num_prev_res, num_prev_res + len(res_ids))
|
| 914 |
+
res_iter = zip(
|
| 915 |
+
chain_ids,
|
| 916 |
+
res_auth_seq_ids,
|
| 917 |
+
res_names,
|
| 918 |
+
res_ins_codes,
|
| 919 |
+
strict=True,
|
| 920 |
+
)
|
| 921 |
+
key_for_res_update = {
|
| 922 |
+
res_unique_id: res_key
|
| 923 |
+
for res_key, res_unique_id in enumerate(res_iter, num_prev_res)
|
| 924 |
+
}
|
| 925 |
+
self.key_for_res.update(key_for_res_update)
|
| 926 |
+
self.res_key.extend(res_keys)
|
| 927 |
+
self.res_chain_key.extend(
|
| 928 |
+
string_array.remap(chain_ids, self.chain_key_by_chain_id, inplace=False)
|
| 929 |
+
)
|
| 930 |
+
self.res_id.extend(res_ids)
|
| 931 |
+
self.res_name.extend(res_names)
|
| 932 |
+
self.res_auth_seq_id.extend(res_auth_seq_ids)
|
| 933 |
+
self.res_insertion_code.extend(res_ins_codes)
|
| 934 |
+
|
| 935 |
+
def make_chains_table(self) -> structure_tables.Chains:
|
| 936 |
+
"""Returns the Structure chains table."""
|
| 937 |
+
chain_key = np.array(self.chain_key, dtype=np.int64)
|
| 938 |
+
if not np.all(chain_key[:-1] <= chain_key[1:]):
|
| 939 |
+
# If the order is inconsistent with the atoms table, sort so that it is.
|
| 940 |
+
order = np.argsort(self.chain_key, kind='stable')
|
| 941 |
+
return structure_tables.Chains(
|
| 942 |
+
key=chain_key[order],
|
| 943 |
+
id=np.array(self.chain_id, dtype=object)[order],
|
| 944 |
+
type=np.array(self.chain_type, dtype=object)[order],
|
| 945 |
+
auth_asym_id=np.array(self.chain_auth_asym_id, dtype=object)[order],
|
| 946 |
+
entity_id=np.array(self.chain_entity_id, dtype=object)[order],
|
| 947 |
+
entity_desc=np.array(self.chain_entity_desc, dtype=object)[order],
|
| 948 |
+
)
|
| 949 |
+
return structure_tables.Chains(
|
| 950 |
+
key=chain_key,
|
| 951 |
+
id=np.array(self.chain_id, dtype=object),
|
| 952 |
+
type=np.array(self.chain_type, dtype=object),
|
| 953 |
+
auth_asym_id=np.array(self.chain_auth_asym_id, dtype=object),
|
| 954 |
+
entity_id=np.array(self.chain_entity_id, dtype=object),
|
| 955 |
+
entity_desc=np.array(self.chain_entity_desc, dtype=object),
|
| 956 |
+
)
|
| 957 |
+
|
| 958 |
+
def make_residues_table(self) -> structure_tables.Residues:
|
| 959 |
+
"""Returns the Structure residues table."""
|
| 960 |
+
res_name = np.array(self.res_name, dtype=object)
|
| 961 |
+
res_chain_key = np.array(self.res_chain_key, dtype=np.int64)
|
| 962 |
+
|
| 963 |
+
if self._fix_mse_residues:
|
| 964 |
+
string_array.remap(res_name, mapping={'MSE': 'MET'}, inplace=True)
|
| 965 |
+
|
| 966 |
+
if self._fix_unknown_dna:
|
| 967 |
+
# Remap residues from N -> DN in DNA chains only.
|
| 968 |
+
dna_chain_mask = (
|
| 969 |
+
np.array(self.chain_type, dtype=object) == mmcif_names.DNA_CHAIN
|
| 970 |
+
)
|
| 971 |
+
dna_chain_key = np.array(self.chain_key, dtype=object)[dna_chain_mask]
|
| 972 |
+
res_name[(res_name == 'N') & np.isin(res_chain_key, dna_chain_key)] = 'DN'
|
| 973 |
+
|
| 974 |
+
if not np.all(res_chain_key[:-1] <= res_chain_key[1:]):
|
| 975 |
+
# If the order is inconsistent with the atoms table, sort so that it is.
|
| 976 |
+
order = np.argsort(res_chain_key, kind='stable')
|
| 977 |
+
return structure_tables.Residues(
|
| 978 |
+
key=np.array(self.res_key, dtype=np.int64)[order],
|
| 979 |
+
chain_key=res_chain_key[order],
|
| 980 |
+
id=np.array(self.res_id, dtype=np.int32)[order],
|
| 981 |
+
name=res_name[order],
|
| 982 |
+
auth_seq_id=np.array(self.res_auth_seq_id, dtype=object)[order],
|
| 983 |
+
insertion_code=np.array(self.res_insertion_code, dtype=object)[order],
|
| 984 |
+
)
|
| 985 |
+
return structure_tables.Residues(
|
| 986 |
+
key=np.array(self.res_key, dtype=np.int64),
|
| 987 |
+
chain_key=res_chain_key,
|
| 988 |
+
id=np.array(self.res_id, dtype=np.int32),
|
| 989 |
+
name=res_name,
|
| 990 |
+
auth_seq_id=np.array(self.res_auth_seq_id, dtype=object),
|
| 991 |
+
insertion_code=np.array(self.res_insertion_code, dtype=object),
|
| 992 |
+
)
|
| 993 |
+
|
| 994 |
+
|
| 995 |
+
def _get_string_array_default(cif: mmcif.Mmcif, key: str, default: list[str]):
|
| 996 |
+
try:
|
| 997 |
+
return cif.get_array(key, dtype=object)
|
| 998 |
+
except KeyError:
|
| 999 |
+
return default
|
| 1000 |
+
|
| 1001 |
+
|
| 1002 |
+
def _generate_required_tables_if_missing(
|
| 1003 |
+
cif: mmcif.Mmcif,
|
| 1004 |
+
) -> Mapping[str, Sequence[str]]:
|
| 1005 |
+
"""Generates all required tables and columns if missing."""
|
| 1006 |
+
update = {}
|
| 1007 |
+
|
| 1008 |
+
atom_site_entities = _get_string_array_default(
|
| 1009 |
+
cif, '_atom_site.label_entity_id', []
|
| 1010 |
+
)
|
| 1011 |
+
|
| 1012 |
+
# OpenMM produces files that don't have any of the tables and also have
|
| 1013 |
+
# _atom_site.label_entity_id set to '?' for all atoms. We infer the entities
|
| 1014 |
+
# based on the _atom_site.label_asym_id column. We start with cheaper O(1)
|
| 1015 |
+
# checks to prevent running the expensive O(n) check on most files.
|
| 1016 |
+
if (
|
| 1017 |
+
len(atom_site_entities) > 0 # pylint: disable=g-explicit-length-test
|
| 1018 |
+
and '_entity.id' not in cif # Ignore if the _entity table exists.
|
| 1019 |
+
and atom_site_entities[0] == '?' # Cheap check.
|
| 1020 |
+
and set(atom_site_entities) == {'?'} # Expensive check.
|
| 1021 |
+
):
|
| 1022 |
+
label_asym_ids = cif.get_array('_atom_site.label_asym_id', dtype=object)
|
| 1023 |
+
atom_site_entities = [
|
| 1024 |
+
str(mmcif.str_id_to_int_id(cid)) for cid in label_asym_ids
|
| 1025 |
+
]
|
| 1026 |
+
# Update _atom_site.label_entity_id to be consistent with the new tables.
|
| 1027 |
+
update['_atom_site.label_entity_id'] = atom_site_entities
|
| 1028 |
+
|
| 1029 |
+
# Check table existence by checking the presence of its primary key.
|
| 1030 |
+
if '_struct_asym.id' not in cif:
|
| 1031 |
+
# Infer the _struct_asym table using the _atom_site table.
|
| 1032 |
+
asym_ids = _get_string_array_default(cif, '_atom_site.label_asym_id', [])
|
| 1033 |
+
|
| 1034 |
+
if len(atom_site_entities) == 0 or len(asym_ids) == 0: # pylint: disable=g-explicit-length-test
|
| 1035 |
+
raise ValueError(
|
| 1036 |
+
'Could not parse an mmCIF with no _struct_asym table and also no '
|
| 1037 |
+
'_atom_site.label_entity_id or _atom_site.label_asym_id columns.'
|
| 1038 |
+
)
|
| 1039 |
+
|
| 1040 |
+
# Deduplicate, but keep the order intact - dict.fromkeys maintains order.
|
| 1041 |
+
entity_id_chain_id_pairs = list(
|
| 1042 |
+
dict.fromkeys(zip(atom_site_entities, asym_ids, strict=True))
|
| 1043 |
+
)
|
| 1044 |
+
update['_struct_asym.entity_id'] = [e for e, _ in entity_id_chain_id_pairs]
|
| 1045 |
+
update['_struct_asym.id'] = [c for _, c in entity_id_chain_id_pairs]
|
| 1046 |
+
|
| 1047 |
+
if '_entity.id' not in cif:
|
| 1048 |
+
# Infer the _entity_poly and _entity tables using the _atom_site table.
|
| 1049 |
+
residues = _get_string_array_default(cif, '_atom_site.label_comp_id', [])
|
| 1050 |
+
group_pdb = _get_string_array_default(cif, '_atom_site.group_PDB', [])
|
| 1051 |
+
if '_atom_site.label_entity_id' in cif:
|
| 1052 |
+
entities = atom_site_entities
|
| 1053 |
+
else:
|
| 1054 |
+
# If _atom_site.label_entity_id not set, use the asym_id -> entity_id map.
|
| 1055 |
+
asym_to_entity = dict(
|
| 1056 |
+
zip(
|
| 1057 |
+
cif['_struct_asym.id'], cif['_struct_asym.entity_id'], strict=True
|
| 1058 |
+
)
|
| 1059 |
+
)
|
| 1060 |
+
entities = string_array.remap(
|
| 1061 |
+
cif.get_array('_atom_site.label_asym_id', dtype=object),
|
| 1062 |
+
mapping=asym_to_entity,
|
| 1063 |
+
)
|
| 1064 |
+
|
| 1065 |
+
entity_ids = []
|
| 1066 |
+
entity_types = []
|
| 1067 |
+
entity_poly_entity_ids = []
|
| 1068 |
+
entity_poly_types = []
|
| 1069 |
+
entity_poly_table_missing = '_entity_poly.entity_id' not in cif
|
| 1070 |
+
for entity_id, group in itertools.groupby(
|
| 1071 |
+
zip(entities, residues, group_pdb, strict=True), key=lambda e: e[0]
|
| 1072 |
+
):
|
| 1073 |
+
_, entity_residues, entity_group_pdb = zip(*group, strict=True)
|
| 1074 |
+
entity_type = _guess_entity_type(
|
| 1075 |
+
chain_residues=entity_residues, atom_types=entity_group_pdb
|
| 1076 |
+
)
|
| 1077 |
+
entity_ids.append(entity_id)
|
| 1078 |
+
entity_types.append(entity_type)
|
| 1079 |
+
|
| 1080 |
+
if entity_poly_table_missing and entity_type == mmcif_names.POLYMER_CHAIN:
|
| 1081 |
+
polymer_type = mmcif_names.guess_polymer_type(entity_residues)
|
| 1082 |
+
entity_poly_entity_ids.append(entity_id)
|
| 1083 |
+
entity_poly_types.append(polymer_type)
|
| 1084 |
+
|
| 1085 |
+
update['_entity.id'] = entity_ids
|
| 1086 |
+
update['_entity.type'] = entity_types
|
| 1087 |
+
if entity_poly_table_missing:
|
| 1088 |
+
update['_entity_poly.entity_id'] = entity_poly_entity_ids
|
| 1089 |
+
update['_entity_poly.type'] = entity_poly_types
|
| 1090 |
+
|
| 1091 |
+
if '_atom_site.type_symbol' not in cif:
|
| 1092 |
+
update['_atom_site.type_symbol'] = mmcif.get_or_infer_type_symbol(cif)
|
| 1093 |
+
|
| 1094 |
+
return update
|
| 1095 |
+
|
| 1096 |
+
|
| 1097 |
+
def _maybe_add_missing_scheme_tables(
|
| 1098 |
+
cif: mmcif.Mmcif,
|
| 1099 |
+
res_starts: Sequence[int],
|
| 1100 |
+
label_asym_ids: np.ndarray,
|
| 1101 |
+
label_seq_ids: np.ndarray,
|
| 1102 |
+
label_comp_ids: np.ndarray,
|
| 1103 |
+
auth_seq_ids: np.ndarray,
|
| 1104 |
+
pdb_ins_codes: np.ndarray,
|
| 1105 |
+
) -> Mapping[str, Sequence[str]]:
|
| 1106 |
+
"""If missing, infers the scheme tables from the _atom_site table."""
|
| 1107 |
+
update = {}
|
| 1108 |
+
|
| 1109 |
+
required_poly_seq_scheme_cols = (
|
| 1110 |
+
'_pdbx_poly_seq_scheme.asym_id',
|
| 1111 |
+
'_pdbx_poly_seq_scheme.pdb_seq_num',
|
| 1112 |
+
'_pdbx_poly_seq_scheme.pdb_ins_code',
|
| 1113 |
+
'_pdbx_poly_seq_scheme.seq_id',
|
| 1114 |
+
'_pdbx_poly_seq_scheme.mon_id',
|
| 1115 |
+
'_pdbx_poly_seq_scheme.pdb_strand_id',
|
| 1116 |
+
)
|
| 1117 |
+
if not all(col in cif for col in required_poly_seq_scheme_cols):
|
| 1118 |
+
# Create a mask for atoms where each polymer residue start.
|
| 1119 |
+
entity_id_by_chain_id = dict(
|
| 1120 |
+
zip(cif['_struct_asym.id'], cif['_struct_asym.entity_id'], strict=True)
|
| 1121 |
+
)
|
| 1122 |
+
chain_type_by_entity_id = dict(
|
| 1123 |
+
zip(cif['_entity.id'], cif['_entity.type'], strict=True)
|
| 1124 |
+
)
|
| 1125 |
+
# Remap asym ID -> entity ID.
|
| 1126 |
+
label_entity_id = string_array.remap(
|
| 1127 |
+
label_asym_ids, mapping=entity_id_by_chain_id, inplace=False
|
| 1128 |
+
)
|
| 1129 |
+
# Remap entity ID -> chain type.
|
| 1130 |
+
chain_type = string_array.remap(
|
| 1131 |
+
label_entity_id, mapping=chain_type_by_entity_id, inplace=False
|
| 1132 |
+
)
|
| 1133 |
+
res_mask = np.zeros_like(label_seq_ids, dtype=bool)
|
| 1134 |
+
res_mask[res_starts] = True
|
| 1135 |
+
res_mask &= chain_type == mmcif_names.POLYMER_CHAIN
|
| 1136 |
+
|
| 1137 |
+
entity_poly_seq_cols = (
|
| 1138 |
+
'_entity_poly_seq.entity_id',
|
| 1139 |
+
'_entity_poly_seq.num',
|
| 1140 |
+
'_entity_poly_seq.mon_id',
|
| 1141 |
+
)
|
| 1142 |
+
if all(col in cif for col in entity_poly_seq_cols):
|
| 1143 |
+
# Use _entity_poly_seq if available.
|
| 1144 |
+
poly_seq_num = cif.get_array('_entity_poly_seq.num', dtype=object)
|
| 1145 |
+
poly_seq_mon_id = cif.get_array('_entity_poly_seq.mon_id', dtype=object)
|
| 1146 |
+
poly_seq_entity_id = cif.get_array(
|
| 1147 |
+
'_entity_poly_seq.entity_id', dtype=object
|
| 1148 |
+
)
|
| 1149 |
+
# We have to add the entity ID to the residue ID because multiple residues
|
| 1150 |
+
# can share the same ID. This also allows using string_array.remap.
|
| 1151 |
+
label_seq_id_to_auth_seq_id = dict(
|
| 1152 |
+
zip(
|
| 1153 |
+
np.char.add(label_entity_id[res_mask], label_seq_ids[res_mask]),
|
| 1154 |
+
auth_seq_ids[res_mask],
|
| 1155 |
+
strict=True,
|
| 1156 |
+
)
|
| 1157 |
+
)
|
| 1158 |
+
scheme_pdb_seq_num = string_array.remap(
|
| 1159 |
+
np.char.add(poly_seq_entity_id, poly_seq_num),
|
| 1160 |
+
mapping=label_seq_id_to_auth_seq_id,
|
| 1161 |
+
default_value='.',
|
| 1162 |
+
)
|
| 1163 |
+
label_seq_id_to_ins_code = dict(
|
| 1164 |
+
zip(label_seq_ids[res_mask], pdb_ins_codes[res_mask], strict=True)
|
| 1165 |
+
)
|
| 1166 |
+
scheme_pdb_ins_code = string_array.remap(
|
| 1167 |
+
poly_seq_num, mapping=label_seq_id_to_ins_code, default_value='.'
|
| 1168 |
+
)
|
| 1169 |
+
|
| 1170 |
+
# The _entity_poly_seq table is entity-based, while _pdbx_poly_seq_scheme
|
| 1171 |
+
# is chain-based. A single entity could mean multiple chains (asym_ids),
|
| 1172 |
+
# we therefore need to replicate each entity for all of the chains.
|
| 1173 |
+
scheme_asym_id = []
|
| 1174 |
+
select = []
|
| 1175 |
+
indices = np.arange(len(poly_seq_entity_id), dtype=np.int32)
|
| 1176 |
+
for asym_id, entity_id in zip(
|
| 1177 |
+
cif['_struct_asym.id'], cif['_struct_asym.entity_id'], strict=True
|
| 1178 |
+
):
|
| 1179 |
+
entity_mask = poly_seq_entity_id == entity_id
|
| 1180 |
+
select.extend(indices[entity_mask])
|
| 1181 |
+
scheme_asym_id.extend([asym_id] * sum(entity_mask))
|
| 1182 |
+
|
| 1183 |
+
scheme_pdb_strand_id = string_array.remap(
|
| 1184 |
+
np.array(scheme_asym_id, dtype=object),
|
| 1185 |
+
mapping=mmcif.get_internal_to_author_chain_id_map(cif),
|
| 1186 |
+
inplace=False,
|
| 1187 |
+
)
|
| 1188 |
+
|
| 1189 |
+
update['_pdbx_poly_seq_scheme.asym_id'] = scheme_asym_id
|
| 1190 |
+
update['_pdbx_poly_seq_scheme.pdb_strand_id'] = scheme_pdb_strand_id
|
| 1191 |
+
update['_pdbx_poly_seq_scheme.pdb_seq_num'] = scheme_pdb_seq_num[select]
|
| 1192 |
+
update['_pdbx_poly_seq_scheme.pdb_ins_code'] = scheme_pdb_ins_code[select]
|
| 1193 |
+
update['_pdbx_poly_seq_scheme.seq_id'] = poly_seq_num[select]
|
| 1194 |
+
update['_pdbx_poly_seq_scheme.mon_id'] = poly_seq_mon_id[select]
|
| 1195 |
+
else:
|
| 1196 |
+
# _entity_poly_seq not available, fallback to _atom_site.
|
| 1197 |
+
res_asym_ids = label_asym_ids[res_mask]
|
| 1198 |
+
res_strand_ids = string_array.remap(
|
| 1199 |
+
array=res_asym_ids,
|
| 1200 |
+
mapping=mmcif.get_internal_to_author_chain_id_map(cif),
|
| 1201 |
+
inplace=False,
|
| 1202 |
+
)
|
| 1203 |
+
update['_pdbx_poly_seq_scheme.asym_id'] = res_asym_ids
|
| 1204 |
+
update['_pdbx_poly_seq_scheme.pdb_strand_id'] = res_strand_ids
|
| 1205 |
+
update['_pdbx_poly_seq_scheme.pdb_seq_num'] = auth_seq_ids[res_mask]
|
| 1206 |
+
update['_pdbx_poly_seq_scheme.pdb_ins_code'] = pdb_ins_codes[res_mask]
|
| 1207 |
+
update['_pdbx_poly_seq_scheme.seq_id'] = label_seq_ids[res_mask]
|
| 1208 |
+
update['_pdbx_poly_seq_scheme.mon_id'] = label_comp_ids[res_mask]
|
| 1209 |
+
|
| 1210 |
+
required_nonpoly_scheme_cols = (
|
| 1211 |
+
'_pdbx_nonpoly_scheme.mon_id',
|
| 1212 |
+
'_pdbx_nonpoly_scheme.asym_id',
|
| 1213 |
+
'_pdbx_nonpoly_scheme.pdb_seq_num',
|
| 1214 |
+
'_pdbx_nonpoly_scheme.pdb_ins_code',
|
| 1215 |
+
)
|
| 1216 |
+
required_branch_scheme_cols = (
|
| 1217 |
+
'_pdbx_branch_scheme.mon_id',
|
| 1218 |
+
'_pdbx_branch_scheme.asym_id',
|
| 1219 |
+
'_pdbx_branch_scheme.pdb_seq_num',
|
| 1220 |
+
)
|
| 1221 |
+
|
| 1222 |
+
# Generate _pdbx_nonpoly_scheme only if both tables are missing.
|
| 1223 |
+
if not (
|
| 1224 |
+
all(col in cif for col in required_nonpoly_scheme_cols)
|
| 1225 |
+
or all(col in cif for col in required_branch_scheme_cols)
|
| 1226 |
+
):
|
| 1227 |
+
# To be strictly semantically correct, multi-residue ligands should be
|
| 1228 |
+
# written in _pdbx_branch_scheme. However, Structure parsing handles
|
| 1229 |
+
# correctly multi-residue ligands in _pdbx_nonpoly_scheme and the tables
|
| 1230 |
+
# constructed here live only while parsing, hence this is unnecessary.
|
| 1231 |
+
entity_id_by_chain_id = dict(
|
| 1232 |
+
zip(cif['_struct_asym.id'], cif['_struct_asym.entity_id'], strict=True)
|
| 1233 |
+
)
|
| 1234 |
+
chain_type_by_entity_id = dict(
|
| 1235 |
+
zip(cif['_entity.id'], cif['_entity.type'], strict=True)
|
| 1236 |
+
)
|
| 1237 |
+
# Remap asym ID -> entity ID.
|
| 1238 |
+
chain_type = string_array.remap(
|
| 1239 |
+
label_asym_ids, mapping=entity_id_by_chain_id, inplace=False
|
| 1240 |
+
)
|
| 1241 |
+
# Remap entity ID -> chain type.
|
| 1242 |
+
string_array.remap(
|
| 1243 |
+
chain_type, mapping=chain_type_by_entity_id, inplace=True
|
| 1244 |
+
)
|
| 1245 |
+
res_mask = np.zeros_like(label_seq_ids, dtype=bool)
|
| 1246 |
+
res_mask[res_starts] = True
|
| 1247 |
+
res_mask &= chain_type != mmcif_names.POLYMER_CHAIN
|
| 1248 |
+
|
| 1249 |
+
if not np.any(res_mask):
|
| 1250 |
+
return update # Shortcut: no non-polymer residues.
|
| 1251 |
+
|
| 1252 |
+
ins_codes = string_array.remap(
|
| 1253 |
+
pdb_ins_codes[res_mask], mapping={'?': '.'}, inplace=False
|
| 1254 |
+
)
|
| 1255 |
+
|
| 1256 |
+
update['_pdbx_nonpoly_scheme.asym_id'] = label_asym_ids[res_mask]
|
| 1257 |
+
update['_pdbx_nonpoly_scheme.pdb_seq_num'] = auth_seq_ids[res_mask]
|
| 1258 |
+
update['_pdbx_nonpoly_scheme.pdb_ins_code'] = ins_codes
|
| 1259 |
+
update['_pdbx_nonpoly_scheme.mon_id'] = label_comp_ids[res_mask]
|
| 1260 |
+
|
| 1261 |
+
return update
|
| 1262 |
+
|
| 1263 |
+
|
| 1264 |
+
def _get_chain_key_by_chain_id(
|
| 1265 |
+
resolved_chain_ids: np.ndarray, struct_asym_chain_ids: np.ndarray
|
| 1266 |
+
) -> Mapping[str, int]:
|
| 1267 |
+
"""Returns chain key for each chain ID respecting resolved chain ordering."""
|
| 1268 |
+
# Check that all chain IDs found in the (potentially filtered) _atom_site
|
| 1269 |
+
# table are present in the _struct_asym table.
|
| 1270 |
+
unique_resolved_chain_ids = set(resolved_chain_ids)
|
| 1271 |
+
if not unique_resolved_chain_ids.issubset(set(struct_asym_chain_ids)):
|
| 1272 |
+
unique_resolved_chain_ids = sorted(unique_resolved_chain_ids)
|
| 1273 |
+
unique_struct_asym_chain_ids = sorted(set(struct_asym_chain_ids))
|
| 1274 |
+
raise ValueError(
|
| 1275 |
+
'Bad mmCIF: chain IDs in _atom_site.label_asym_id '
|
| 1276 |
+
f'{unique_resolved_chain_ids} is not a subset of chain IDs in '
|
| 1277 |
+
f'_struct_asym.id {unique_struct_asym_chain_ids}.'
|
| 1278 |
+
)
|
| 1279 |
+
|
| 1280 |
+
resolved_mask = string_array.isin(
|
| 1281 |
+
struct_asym_chain_ids, unique_resolved_chain_ids
|
| 1282 |
+
)
|
| 1283 |
+
# For all resolved chains, use the _atom_site order they appear in. E.g.
|
| 1284 |
+
# resolved_chain_ids = [B A E D F]
|
| 1285 |
+
# struct_asym_chain_ids = [A B C D E F]
|
| 1286 |
+
# consistent_chain_order = [B A C E D F]
|
| 1287 |
+
# chain_keys = [0 1 2 3 4 5]
|
| 1288 |
+
consistent_chain_order = struct_asym_chain_ids.copy()
|
| 1289 |
+
consistent_chain_order[resolved_mask] = resolved_chain_ids
|
| 1290 |
+
return dict(zip(consistent_chain_order, range(len(struct_asym_chain_ids))))
|
| 1291 |
+
|
| 1292 |
+
|
| 1293 |
+
def get_tables(
|
| 1294 |
+
cif: mmcif.Mmcif,
|
| 1295 |
+
fix_mse_residues: bool,
|
| 1296 |
+
fix_arginines: bool,
|
| 1297 |
+
fix_unknown_dna: bool,
|
| 1298 |
+
include_water: bool,
|
| 1299 |
+
include_other: bool,
|
| 1300 |
+
model_id: str,
|
| 1301 |
+
) -> tuple[
|
| 1302 |
+
structure_tables.Chains, structure_tables.Residues, structure_tables.Atoms
|
| 1303 |
+
]:
|
| 1304 |
+
"""Returns chain, residue, and atom tables from a parsed mmcif.
|
| 1305 |
+
|
| 1306 |
+
Args:
|
| 1307 |
+
cif: A parsed mmcif.Mmcif.
|
| 1308 |
+
fix_mse_residues: See from_mmcif.
|
| 1309 |
+
fix_arginines: See from_mmcif.
|
| 1310 |
+
fix_unknown_dna: See from_mmcif.
|
| 1311 |
+
include_water: See from_mmcif.
|
| 1312 |
+
include_other: See from_mmcif.
|
| 1313 |
+
model_id: A string defining which model ID to use. If set, only coordinates,
|
| 1314 |
+
b-factors and occupancies for the given model are returned. If empty,
|
| 1315 |
+
coordinates, b-factors and occupanciesall for models are returned with a
|
| 1316 |
+
leading dimension of num_models. Note that the model_id argument in
|
| 1317 |
+
from_mmcif is an integer and has slightly different use (see from_mmcif).
|
| 1318 |
+
"""
|
| 1319 |
+
# Add any missing tables and columns we require for parsing.
|
| 1320 |
+
if cif_update := _generate_required_tables_if_missing(cif):
|
| 1321 |
+
cif = cif.copy_and_update(cif_update)
|
| 1322 |
+
|
| 1323 |
+
# Resolve alt-locs, selecting only a single option for each residue. Also
|
| 1324 |
+
# computes the layout, which defines where chain and residue boundaries are.
|
| 1325 |
+
atom_site_all_models, layout = mmcif_utils.filter(
|
| 1326 |
+
cif,
|
| 1327 |
+
include_nucleotides=True,
|
| 1328 |
+
include_ligands=True,
|
| 1329 |
+
include_water=include_water,
|
| 1330 |
+
include_other=include_other,
|
| 1331 |
+
model_id=model_id,
|
| 1332 |
+
)
|
| 1333 |
+
atom_site_first_model = atom_site_all_models[0]
|
| 1334 |
+
|
| 1335 |
+
# Get atom information from the _atom_site table.
|
| 1336 |
+
def _first_model_string_array(col: str) -> np.ndarray:
|
| 1337 |
+
return cif.get_array(col, dtype=object, gather=atom_site_first_model)
|
| 1338 |
+
|
| 1339 |
+
def _requested_models_float_array(col: str) -> np.ndarray:
|
| 1340 |
+
if not model_id:
|
| 1341 |
+
# Return data for all models with a leading dimension of num_models.
|
| 1342 |
+
return cif.get_array(col, dtype=np.float32, gather=atom_site_all_models)
|
| 1343 |
+
else:
|
| 1344 |
+
# Return data only for the single requested model.
|
| 1345 |
+
return cif.get_array(col, dtype=np.float32, gather=atom_site_first_model)
|
| 1346 |
+
|
| 1347 |
+
# These columns are the same for all models, fetch them just for the 1st one.
|
| 1348 |
+
label_comp_ids = _first_model_string_array('_atom_site.label_comp_id')
|
| 1349 |
+
label_asym_ids = _first_model_string_array('_atom_site.label_asym_id')
|
| 1350 |
+
label_seq_ids = _first_model_string_array('_atom_site.label_seq_id')
|
| 1351 |
+
label_atom_ids = _first_model_string_array('_atom_site.label_atom_id')
|
| 1352 |
+
if '_atom_site.auth_seq_id' in cif:
|
| 1353 |
+
auth_seq_ids = _first_model_string_array('_atom_site.auth_seq_id')
|
| 1354 |
+
else:
|
| 1355 |
+
auth_seq_ids = label_seq_ids # auth_seq_id unset, fallback to label_seq_id.
|
| 1356 |
+
type_symbols = _first_model_string_array('_atom_site.type_symbol')
|
| 1357 |
+
pdbx_pdb_ins_codes = _first_model_string_array('_atom_site.pdbx_PDB_ins_code')
|
| 1358 |
+
|
| 1359 |
+
# These columns are different for all models, fetch them as requested.
|
| 1360 |
+
atom_x = _requested_models_float_array('_atom_site.Cartn_x')
|
| 1361 |
+
atom_y = _requested_models_float_array('_atom_site.Cartn_y')
|
| 1362 |
+
atom_z = _requested_models_float_array('_atom_site.Cartn_z')
|
| 1363 |
+
atom_b_factor = _requested_models_float_array('_atom_site.B_iso_or_equiv')
|
| 1364 |
+
atom_occupancy = _requested_models_float_array('_atom_site.occupancy')
|
| 1365 |
+
|
| 1366 |
+
# Make sure the scheme (residue) tables exist in case they are not present.
|
| 1367 |
+
if cif_update := _maybe_add_missing_scheme_tables(
|
| 1368 |
+
cif,
|
| 1369 |
+
res_starts=layout.residue_starts(),
|
| 1370 |
+
label_asym_ids=label_asym_ids,
|
| 1371 |
+
label_seq_ids=label_seq_ids,
|
| 1372 |
+
label_comp_ids=label_comp_ids,
|
| 1373 |
+
auth_seq_ids=auth_seq_ids,
|
| 1374 |
+
pdb_ins_codes=pdbx_pdb_ins_codes,
|
| 1375 |
+
):
|
| 1376 |
+
cif = cif.copy_and_update(cif_update)
|
| 1377 |
+
|
| 1378 |
+
# Fix common issues found in mmCIF files, like swapped arginine NH atoms.
|
| 1379 |
+
mmcif_utils.fix_residues(
|
| 1380 |
+
layout,
|
| 1381 |
+
comp_id=label_comp_ids,
|
| 1382 |
+
atom_id=label_atom_ids,
|
| 1383 |
+
atom_x=atom_x[0] if not model_id else atom_x,
|
| 1384 |
+
atom_y=atom_y[0] if not model_id else atom_y,
|
| 1385 |
+
atom_z=atom_z[0] if not model_id else atom_z,
|
| 1386 |
+
fix_arg=fix_arginines,
|
| 1387 |
+
)
|
| 1388 |
+
|
| 1389 |
+
# Get keys for chains in the order they appear in _atom_site while also
|
| 1390 |
+
# dealing with empty chains.
|
| 1391 |
+
resolved_chain_ids = label_asym_ids[layout.chain_starts()]
|
| 1392 |
+
struct_asym_chain_ids = cif.get_array('_struct_asym.id', dtype=object)
|
| 1393 |
+
|
| 1394 |
+
chain_key_by_chain_id = _get_chain_key_by_chain_id(
|
| 1395 |
+
resolved_chain_ids=resolved_chain_ids,
|
| 1396 |
+
struct_asym_chain_ids=struct_asym_chain_ids,
|
| 1397 |
+
)
|
| 1398 |
+
entity_id_by_chain_id = dict(
|
| 1399 |
+
zip(struct_asym_chain_ids, cif['_struct_asym.entity_id'], strict=True)
|
| 1400 |
+
)
|
| 1401 |
+
entity_description = cif.get(
|
| 1402 |
+
'_entity.pdbx_description', ['?'] * len(cif['_entity.id'])
|
| 1403 |
+
)
|
| 1404 |
+
entity_desc_by_entity_id = dict(
|
| 1405 |
+
zip(cif['_entity.id'], entity_description, strict=True)
|
| 1406 |
+
)
|
| 1407 |
+
chain_type_by_entity_id = mmcif.get_chain_type_by_entity_id(cif)
|
| 1408 |
+
auth_asym_id_by_chain_id = mmcif.get_internal_to_author_chain_id_map(cif)
|
| 1409 |
+
|
| 1410 |
+
chain_res_builder = _ChainResBuilder(
|
| 1411 |
+
chain_key_by_chain_id=chain_key_by_chain_id,
|
| 1412 |
+
entity_id_by_chain_id=entity_id_by_chain_id,
|
| 1413 |
+
chain_type_by_entity_id=chain_type_by_entity_id,
|
| 1414 |
+
entity_desc_by_entity_id=entity_desc_by_entity_id,
|
| 1415 |
+
fix_mse_residues=fix_mse_residues,
|
| 1416 |
+
fix_unknown_dna=fix_unknown_dna,
|
| 1417 |
+
)
|
| 1418 |
+
|
| 1419 |
+
# Collect data for polymer chain and residue tables. _pdbx_poly_seq_scheme is
|
| 1420 |
+
# guaranteed to be present thanks to _maybe_add_missing_scheme_tables.
|
| 1421 |
+
def _get_poly_seq_scheme_col(col: str) -> np.ndarray:
|
| 1422 |
+
return cif.get_array(key=f'_pdbx_poly_seq_scheme.{col}', dtype=object)
|
| 1423 |
+
|
| 1424 |
+
poly_seq_asym_ids = _get_poly_seq_scheme_col('asym_id')
|
| 1425 |
+
poly_seq_pdb_seq_nums = _get_poly_seq_scheme_col('pdb_seq_num')
|
| 1426 |
+
poly_seq_seq_ids = _get_poly_seq_scheme_col('seq_id')
|
| 1427 |
+
poly_seq_mon_ids = _get_poly_seq_scheme_col('mon_id')
|
| 1428 |
+
poly_seq_pdb_strand_ids = _get_poly_seq_scheme_col('pdb_strand_id')
|
| 1429 |
+
poly_seq_pdb_ins_codes = _get_poly_seq_scheme_col('pdb_ins_code')
|
| 1430 |
+
string_array.remap(
|
| 1431 |
+
poly_seq_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True
|
| 1432 |
+
)
|
| 1433 |
+
|
| 1434 |
+
# We resolved alt-locs earlier for the atoms table. In cases of heterogeneous
|
| 1435 |
+
# residues (a residue with an alt-loc that is of different residue type), we
|
| 1436 |
+
# need to also do the same resolution in the residues table. Compute a mask
|
| 1437 |
+
# for the residues that were selected in the atoms table.
|
| 1438 |
+
poly_seq_mask = mmcif_utils.selected_polymer_residue_mask(
|
| 1439 |
+
layout=layout,
|
| 1440 |
+
atom_site_label_asym_ids=label_asym_ids[layout.residue_starts()],
|
| 1441 |
+
atom_site_label_seq_ids=label_seq_ids[layout.residue_starts()],
|
| 1442 |
+
atom_site_label_comp_ids=label_comp_ids[layout.residue_starts()],
|
| 1443 |
+
poly_seq_asym_ids=poly_seq_asym_ids,
|
| 1444 |
+
poly_seq_seq_ids=poly_seq_seq_ids,
|
| 1445 |
+
poly_seq_mon_ids=poly_seq_mon_ids,
|
| 1446 |
+
)
|
| 1447 |
+
|
| 1448 |
+
if not include_other and poly_seq_mask:
|
| 1449 |
+
# Mask filtered-out residues so that they are not treated as missing.
|
| 1450 |
+
# Instead, we don't want them included in the chains/residues tables at all.
|
| 1451 |
+
keep_mask = string_array.remap(
|
| 1452 |
+
poly_seq_asym_ids,
|
| 1453 |
+
mapping={cid: True for cid in resolved_chain_ids},
|
| 1454 |
+
default_value=False,
|
| 1455 |
+
inplace=False,
|
| 1456 |
+
).astype(bool)
|
| 1457 |
+
poly_seq_mask &= keep_mask
|
| 1458 |
+
|
| 1459 |
+
chain_res_builder.add_residues(
|
| 1460 |
+
chain_ids=poly_seq_asym_ids[poly_seq_mask],
|
| 1461 |
+
chain_auth_asym_ids=poly_seq_pdb_strand_ids[poly_seq_mask],
|
| 1462 |
+
res_ids=poly_seq_seq_ids[poly_seq_mask].astype(np.int32),
|
| 1463 |
+
res_names=poly_seq_mon_ids[poly_seq_mask],
|
| 1464 |
+
res_auth_seq_ids=poly_seq_pdb_seq_nums[poly_seq_mask],
|
| 1465 |
+
res_ins_codes=poly_seq_pdb_ins_codes[poly_seq_mask],
|
| 1466 |
+
)
|
| 1467 |
+
|
| 1468 |
+
# Collect data for ligand chain and residue tables. _pdbx_nonpoly_scheme
|
| 1469 |
+
# could be empty/unset if there are only branched ligands.
|
| 1470 |
+
def _get_nonpoly_scheme_col(col: str) -> np.ndarray:
|
| 1471 |
+
key = f'_pdbx_nonpoly_scheme.{col}'
|
| 1472 |
+
if f'_pdbx_nonpoly_scheme.{col}' in cif:
|
| 1473 |
+
return cif.get_array(key=key, dtype=object)
|
| 1474 |
+
else:
|
| 1475 |
+
return np.array([], dtype=object)
|
| 1476 |
+
|
| 1477 |
+
nonpoly_asym_ids = _get_nonpoly_scheme_col('asym_id')
|
| 1478 |
+
nonpoly_auth_seq_ids = _get_nonpoly_scheme_col('pdb_seq_num')
|
| 1479 |
+
nonpoly_pdb_ins_codes = _get_nonpoly_scheme_col('pdb_ins_code')
|
| 1480 |
+
nonpoly_mon_ids = _get_nonpoly_scheme_col('mon_id')
|
| 1481 |
+
nonpoly_auth_asym_id = string_array.remap(
|
| 1482 |
+
nonpoly_asym_ids, mapping=auth_asym_id_by_chain_id, inplace=False
|
| 1483 |
+
)
|
| 1484 |
+
|
| 1485 |
+
def _get_branch_scheme_col(col: str) -> np.ndarray:
|
| 1486 |
+
key = f'_pdbx_branch_scheme.{col}'
|
| 1487 |
+
if f'_pdbx_branch_scheme.{col}' in cif:
|
| 1488 |
+
return cif.get_array(key=key, dtype=object)
|
| 1489 |
+
else:
|
| 1490 |
+
return np.array([], dtype=object)
|
| 1491 |
+
|
| 1492 |
+
branch_asym_ids = _get_branch_scheme_col('asym_id')
|
| 1493 |
+
branch_auth_seq_ids = _get_branch_scheme_col('pdb_seq_num')
|
| 1494 |
+
branch_pdb_ins_codes = _get_branch_scheme_col('pdb_ins_code')
|
| 1495 |
+
branch_mon_ids = _get_branch_scheme_col('mon_id')
|
| 1496 |
+
branch_auth_asym_id = string_array.remap(
|
| 1497 |
+
branch_asym_ids, mapping=auth_asym_id_by_chain_id, inplace=False
|
| 1498 |
+
)
|
| 1499 |
+
|
| 1500 |
+
if branch_asym_ids.size > 0 and branch_pdb_ins_codes.size == 0:
|
| 1501 |
+
branch_pdb_ins_codes = np.array(['.'] * branch_asym_ids.size, dtype=object)
|
| 1502 |
+
|
| 1503 |
+
# Compute the heterogeneous residue masks as above, this time for ligands.
|
| 1504 |
+
nonpoly_mask, branch_mask = mmcif_utils.selected_ligand_residue_mask(
|
| 1505 |
+
layout=layout,
|
| 1506 |
+
atom_site_label_asym_ids=label_asym_ids[layout.residue_starts()],
|
| 1507 |
+
atom_site_label_seq_ids=label_seq_ids[layout.residue_starts()],
|
| 1508 |
+
atom_site_auth_seq_ids=auth_seq_ids[layout.residue_starts()],
|
| 1509 |
+
atom_site_label_comp_ids=label_comp_ids[layout.residue_starts()],
|
| 1510 |
+
atom_site_pdbx_pdb_ins_codes=pdbx_pdb_ins_codes[layout.residue_starts()],
|
| 1511 |
+
nonpoly_asym_ids=nonpoly_asym_ids,
|
| 1512 |
+
nonpoly_auth_seq_ids=nonpoly_auth_seq_ids,
|
| 1513 |
+
nonpoly_pdb_ins_codes=nonpoly_pdb_ins_codes,
|
| 1514 |
+
nonpoly_mon_ids=nonpoly_mon_ids,
|
| 1515 |
+
branch_asym_ids=branch_asym_ids,
|
| 1516 |
+
branch_auth_seq_ids=branch_auth_seq_ids,
|
| 1517 |
+
branch_pdb_ins_codes=branch_pdb_ins_codes,
|
| 1518 |
+
branch_mon_ids=branch_mon_ids,
|
| 1519 |
+
)
|
| 1520 |
+
|
| 1521 |
+
if not include_water:
|
| 1522 |
+
if nonpoly_mask:
|
| 1523 |
+
nonpoly_mask &= (nonpoly_mon_ids != 'HOH') & (nonpoly_mon_ids != 'DOD')
|
| 1524 |
+
if branch_mask:
|
| 1525 |
+
# Fix for bad mmCIFs that have water in the branch scheme table.
|
| 1526 |
+
branch_mask &= (branch_mon_ids != 'HOH') & (branch_mon_ids != 'DOD')
|
| 1527 |
+
|
| 1528 |
+
string_array.remap(
|
| 1529 |
+
pdbx_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True
|
| 1530 |
+
)
|
| 1531 |
+
string_array.remap(
|
| 1532 |
+
nonpoly_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True
|
| 1533 |
+
)
|
| 1534 |
+
string_array.remap(
|
| 1535 |
+
branch_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True
|
| 1536 |
+
)
|
| 1537 |
+
|
| 1538 |
+
def _ligand_residue_ids(chain_ids: np.ndarray) -> np.ndarray:
|
| 1539 |
+
"""Computes internal residue ID for ligand residues that don't have it."""
|
| 1540 |
+
|
| 1541 |
+
# E.g. chain_ids=[A, A, A, B, C, C, D, D, D] -> [1, 2, 3, 1, 1, 2, 1, 2, 3].
|
| 1542 |
+
indices = np.arange(chain_ids.size, dtype=np.int32)
|
| 1543 |
+
return (indices + 1) - np.maximum.accumulate(
|
| 1544 |
+
indices * (chain_ids != np.roll(chain_ids, 1))
|
| 1545 |
+
)
|
| 1546 |
+
|
| 1547 |
+
branch_residue_ids = _ligand_residue_ids(branch_asym_ids[branch_mask])
|
| 1548 |
+
nonpoly_residue_ids = _ligand_residue_ids(nonpoly_asym_ids[nonpoly_mask])
|
| 1549 |
+
|
| 1550 |
+
chain_res_builder.add_residues(
|
| 1551 |
+
chain_ids=branch_asym_ids[branch_mask],
|
| 1552 |
+
chain_auth_asym_ids=branch_auth_asym_id[branch_mask],
|
| 1553 |
+
res_ids=branch_residue_ids,
|
| 1554 |
+
res_names=branch_mon_ids[branch_mask],
|
| 1555 |
+
res_auth_seq_ids=branch_auth_seq_ids[branch_mask],
|
| 1556 |
+
res_ins_codes=branch_pdb_ins_codes[branch_mask],
|
| 1557 |
+
)
|
| 1558 |
+
|
| 1559 |
+
chain_res_builder.add_residues(
|
| 1560 |
+
chain_ids=nonpoly_asym_ids[nonpoly_mask],
|
| 1561 |
+
chain_auth_asym_ids=nonpoly_auth_asym_id[nonpoly_mask],
|
| 1562 |
+
res_ids=nonpoly_residue_ids,
|
| 1563 |
+
res_names=nonpoly_mon_ids[nonpoly_mask],
|
| 1564 |
+
res_auth_seq_ids=nonpoly_auth_seq_ids[nonpoly_mask],
|
| 1565 |
+
res_ins_codes=nonpoly_pdb_ins_codes[nonpoly_mask],
|
| 1566 |
+
)
|
| 1567 |
+
|
| 1568 |
+
chains = chain_res_builder.make_chains_table()
|
| 1569 |
+
residues = chain_res_builder.make_residues_table()
|
| 1570 |
+
|
| 1571 |
+
# Construct foreign residue keys for the atoms table.
|
| 1572 |
+
res_ends = np.array(layout.residues(), dtype=np.int32)
|
| 1573 |
+
res_starts = np.array(layout.residue_starts(), dtype=np.int32)
|
| 1574 |
+
res_lengths = res_ends - res_starts
|
| 1575 |
+
|
| 1576 |
+
# Check just for HOH, DOD can be part e.g. of hydroxycysteine.
|
| 1577 |
+
if include_water:
|
| 1578 |
+
res_chain_types = chains.apply_array_to_column(
|
| 1579 |
+
column_name='type', arr=residues.chain_key
|
| 1580 |
+
)
|
| 1581 |
+
water_mask = res_chain_types != mmcif_names.WATER
|
| 1582 |
+
if 'HOH' in set(residues.name[water_mask]):
|
| 1583 |
+
raise ValueError('Bad mmCIF file: non-water entity has water molecules.')
|
| 1584 |
+
else:
|
| 1585 |
+
# Include resolved and unresolved residues.
|
| 1586 |
+
if 'HOH' in set(residues.name) | set(label_comp_ids[res_starts]):
|
| 1587 |
+
raise ValueError('Bad mmCIF file: non-water entity has water molecules.')
|
| 1588 |
+
|
| 1589 |
+
atom_chain_key = string_array.remap(
|
| 1590 |
+
label_asym_ids, mapping=chain_res_builder.chain_key_by_chain_id
|
| 1591 |
+
).astype(int)
|
| 1592 |
+
|
| 1593 |
+
# If any of the residue lookups failed, the mmCIF is corrupted.
|
| 1594 |
+
try:
|
| 1595 |
+
atom_res_key_per_res = string_array.remap_multiple(
|
| 1596 |
+
(
|
| 1597 |
+
label_asym_ids[res_starts],
|
| 1598 |
+
auth_seq_ids[res_starts],
|
| 1599 |
+
label_comp_ids[res_starts],
|
| 1600 |
+
pdbx_pdb_ins_codes[res_starts],
|
| 1601 |
+
),
|
| 1602 |
+
mapping=chain_res_builder.key_for_res,
|
| 1603 |
+
)
|
| 1604 |
+
except KeyError as e:
|
| 1605 |
+
raise ValueError(
|
| 1606 |
+
'Lookup for the following atom from the _atom_site table failed: '
|
| 1607 |
+
f'(label_asym_id, auth_seq_id, res_name, ins_code)={e}. This typically '
|
| 1608 |
+
'indicates that the _pdbx_poly_seq_scheme, _pdbx_nonpoly_scheme, or '
|
| 1609 |
+
'_pdbx_branch_scheme tables do not have data for all residues present '
|
| 1610 |
+
'in the _atom_site table. It could also be due to a known issue with '
|
| 1611 |
+
'a small number of multi-model mmCIFs.'
|
| 1612 |
+
) from e
|
| 1613 |
+
|
| 1614 |
+
# The residue ID will be shared for all atoms within that residue.
|
| 1615 |
+
atom_res_key = np.repeat(atom_res_key_per_res, repeats=res_lengths)
|
| 1616 |
+
|
| 1617 |
+
if fix_mse_residues:
|
| 1618 |
+
met_residues_mask = (residues.name == 'MET')[atom_res_key]
|
| 1619 |
+
unfixed_mse_selenium_mask = met_residues_mask & (label_atom_ids == 'SE')
|
| 1620 |
+
label_atom_ids[unfixed_mse_selenium_mask] = 'SD'
|
| 1621 |
+
type_symbols[unfixed_mse_selenium_mask] = 'S'
|
| 1622 |
+
|
| 1623 |
+
atoms = structure_tables.Atoms(
|
| 1624 |
+
key=atom_site_first_model,
|
| 1625 |
+
chain_key=atom_chain_key,
|
| 1626 |
+
res_key=atom_res_key,
|
| 1627 |
+
name=label_atom_ids,
|
| 1628 |
+
element=type_symbols,
|
| 1629 |
+
x=atom_x,
|
| 1630 |
+
y=atom_y,
|
| 1631 |
+
z=atom_z,
|
| 1632 |
+
b_factor=atom_b_factor,
|
| 1633 |
+
occupancy=atom_occupancy,
|
| 1634 |
+
)
|
| 1635 |
+
|
| 1636 |
+
return chains, residues, atoms
|
| 1637 |
+
|
| 1638 |
+
|
| 1639 |
+
def from_atom_arrays(
|
| 1640 |
+
*,
|
| 1641 |
+
res_id: np.ndarray,
|
| 1642 |
+
name: str = 'unset',
|
| 1643 |
+
release_date: datetime.date | None = None,
|
| 1644 |
+
resolution: float | None = None,
|
| 1645 |
+
structure_method: str | None = None,
|
| 1646 |
+
all_residues: Mapping[str, Sequence[tuple[str, int]]] | None = None,
|
| 1647 |
+
bioassembly_data: bioassemblies.BioassemblyData | None = None,
|
| 1648 |
+
chemical_components_data: (
|
| 1649 |
+
struc_chem_comps.ChemicalComponentsData | None
|
| 1650 |
+
) = None,
|
| 1651 |
+
bond_table: structure_tables.Bonds | None = None,
|
| 1652 |
+
chain_id: np.ndarray | None = None,
|
| 1653 |
+
chain_type: np.ndarray | None = None,
|
| 1654 |
+
res_name: np.ndarray | None = None,
|
| 1655 |
+
atom_key: np.ndarray | None = None,
|
| 1656 |
+
atom_name: np.ndarray | None = None,
|
| 1657 |
+
atom_element: np.ndarray | None = None,
|
| 1658 |
+
atom_x: np.ndarray | None = None,
|
| 1659 |
+
atom_y: np.ndarray | None = None,
|
| 1660 |
+
atom_z: np.ndarray | None = None,
|
| 1661 |
+
atom_b_factor: np.ndarray | None = None,
|
| 1662 |
+
atom_occupancy: np.ndarray | None = None,
|
| 1663 |
+
) -> structure.Structure:
|
| 1664 |
+
"""Returns a Structure constructed from atom array level data.
|
| 1665 |
+
|
| 1666 |
+
All fields except name and, res_id are optional, all array fields consist of a
|
| 1667 |
+
value for each atom in the structure - so residue and chain values should hold
|
| 1668 |
+
the same value for each atom in the chain or residue. Fields which are not
|
| 1669 |
+
defined are filled with default values.
|
| 1670 |
+
|
| 1671 |
+
Validation is performed by the Structure constructor where possible - but
|
| 1672 |
+
author_naming scheme and all_residues must be checked in this function.
|
| 1673 |
+
|
| 1674 |
+
It is not possible to construct structures with chains that do not contain
|
| 1675 |
+
any resolved residues using this function. If this is necessary, use the
|
| 1676 |
+
structure.Structure constructor directly.
|
| 1677 |
+
|
| 1678 |
+
Args:
|
| 1679 |
+
res_id: Integer array of shape [num_atom]. The unique residue identifier for
|
| 1680 |
+
each residue. mmCIF field - _atom_site.label_seq_id.
|
| 1681 |
+
name: The name of the structure. E.g. a PDB ID.
|
| 1682 |
+
release_date: The release date of the structure as a `datetime.date`.
|
| 1683 |
+
resolution: The resolution of the structure in Angstroms.
|
| 1684 |
+
structure_method: The method used to solve this structure's coordinates.
|
| 1685 |
+
all_residues: An optional mapping from each chain ID (i.e. label_asym_id) to
|
| 1686 |
+
a sequence of (label_comp_id, label_seq_id) tuples, one per residue. This
|
| 1687 |
+
can contain residues that aren't present in the atom arrays. This is
|
| 1688 |
+
common in experimental data where some residues are not resolved but are
|
| 1689 |
+
known to be present.
|
| 1690 |
+
bioassembly_data: An optional instance of bioassembly.BioassemblyData. If
|
| 1691 |
+
present then a new Structure representing a specific bioassembly can be
|
| 1692 |
+
extracted using `Structure.generate_bioassembly(assembly_id)`.
|
| 1693 |
+
chemical_components_data: An optional instance of ChemicalComponentsData.
|
| 1694 |
+
Its content will be used for providing metadata about chemical components
|
| 1695 |
+
in this Structure instance. If not specified information will be retrieved
|
| 1696 |
+
from the standard chemical component dictionary (CCD, for more details see
|
| 1697 |
+
https://www.wwpdb.org/data/ccd).
|
| 1698 |
+
bond_table: A table representing manually-specified bonds. This corresponds
|
| 1699 |
+
to the _struct_conn table in an mmCIF. Atoms are identified by their key,
|
| 1700 |
+
as specified by the atom_key column. If this table is provided then the
|
| 1701 |
+
atom_key column must also be defined.
|
| 1702 |
+
chain_id: String array of shape [num_atom] of unique chain identifiers.
|
| 1703 |
+
mmCIF field - _atom_site.label_asym_id.
|
| 1704 |
+
chain_type: String array of shape [num_atom]. The molecular type of the
|
| 1705 |
+
current chain (e.g. polyribonucleotide). mmCIF field - _entity_poly.type
|
| 1706 |
+
OR _entity.type (for non-polymers).
|
| 1707 |
+
res_name: String array of shape [num_atom].. The name of each residue,
|
| 1708 |
+
typically a 3 letter string for polypeptides or 1-2 letter strings for
|
| 1709 |
+
polynucleotides. mmCIF field - _atom_site.label_comp_id.
|
| 1710 |
+
atom_key: A unique sorted integer array, used only by the bonds table to
|
| 1711 |
+
identify the atoms participating in each bond. If the bonds table is
|
| 1712 |
+
specified then this column must be non-None.
|
| 1713 |
+
atom_name: String array of shape [num_atom]. The name of each atom (e.g CA,
|
| 1714 |
+
O2', etc.). mmCIF field - _atom_site.label_atom_id.
|
| 1715 |
+
atom_element: String array of shape [num_atom]. The element type of each
|
| 1716 |
+
atom (e.g. C, O, N, etc.). mmCIF field - _atom_site.type_symbol.
|
| 1717 |
+
atom_x: Float array of shape [..., num_atom] of atom x coordinates. May have
|
| 1718 |
+
arbitrary leading dimensions, provided that these are consistent across
|
| 1719 |
+
all coordinate fields.
|
| 1720 |
+
atom_y: Float array of shape [..., num_atom] of atom y coordinates. May have
|
| 1721 |
+
arbitrary leading dimensions, provided that these are consistent across
|
| 1722 |
+
all coordinate fields.
|
| 1723 |
+
atom_z: Float array of shape [..., num_atom] of atom z coordinates. May have
|
| 1724 |
+
arbitrary leading dimensions, provided that these are consistent across
|
| 1725 |
+
all coordinate fields.
|
| 1726 |
+
atom_b_factor: Float array of shape [..., num_atom] or [num_atom] of atom
|
| 1727 |
+
b-factors or equivalent. If there are no extra leading dimensions then
|
| 1728 |
+
these values are assumed to apply to all coordinates for a given atom. If
|
| 1729 |
+
there are leading dimensions then these must match those used by the
|
| 1730 |
+
coordinate fields.
|
| 1731 |
+
atom_occupancy: Float array of shape [..., num_atom] or [num_atom] of atom
|
| 1732 |
+
occupancies or equivalent. If there are no extra leading dimensions then
|
| 1733 |
+
these values are assumed to apply to all coordinates for a given atom. If
|
| 1734 |
+
there are leading dimensions then these must match those used by the
|
| 1735 |
+
coordinate fields.
|
| 1736 |
+
"""
|
| 1737 |
+
|
| 1738 |
+
atoms, residues, chains = structure_tables.tables_from_atom_arrays(
|
| 1739 |
+
res_id=res_id,
|
| 1740 |
+
all_residues=all_residues,
|
| 1741 |
+
chain_id=chain_id,
|
| 1742 |
+
chain_type=chain_type,
|
| 1743 |
+
res_name=res_name,
|
| 1744 |
+
atom_key=atom_key,
|
| 1745 |
+
atom_name=atom_name,
|
| 1746 |
+
atom_element=atom_element,
|
| 1747 |
+
atom_x=atom_x,
|
| 1748 |
+
atom_y=atom_y,
|
| 1749 |
+
atom_z=atom_z,
|
| 1750 |
+
atom_b_factor=atom_b_factor,
|
| 1751 |
+
atom_occupancy=atom_occupancy,
|
| 1752 |
+
)
|
| 1753 |
+
|
| 1754 |
+
return structure.Structure(
|
| 1755 |
+
name=name,
|
| 1756 |
+
release_date=release_date,
|
| 1757 |
+
resolution=resolution,
|
| 1758 |
+
structure_method=structure_method,
|
| 1759 |
+
bioassembly_data=bioassembly_data,
|
| 1760 |
+
chemical_components_data=chemical_components_data,
|
| 1761 |
+
atoms=atoms,
|
| 1762 |
+
chains=chains,
|
| 1763 |
+
residues=residues,
|
| 1764 |
+
bonds=bond_table or structure_tables.Bonds.make_empty(),
|
| 1765 |
+
)
|
| 1766 |
+
|
| 1767 |
+
|
| 1768 |
+
def _guess_entity_type(
|
| 1769 |
+
chain_residues: Collection[str], atom_types: Collection[str]
|
| 1770 |
+
) -> str:
|
| 1771 |
+
"""Guess the entity type (polymer/non-polymer/water) based on residues/atoms.
|
| 1772 |
+
|
| 1773 |
+
We treat both arguments as unordered collections since we care only whether
|
| 1774 |
+
all elements satisfy come conditions. The chain_residues can be either
|
| 1775 |
+
grouped by residue (length num_res), or it can be raw (length num_atoms).
|
| 1776 |
+
Atom type is unique for each atom in a residue, so don't group atom_types.
|
| 1777 |
+
|
| 1778 |
+
Args:
|
| 1779 |
+
chain_residues: A sequence of full residue name (1-letter for DNA, 2-letters
|
| 1780 |
+
for RNA, 3 for protein). The _atom_site.label_comp_id column in mmCIF.
|
| 1781 |
+
atom_types: Atom type: ATOM or HETATM. The _atom_site.group_PDB column in
|
| 1782 |
+
mmCIF.
|
| 1783 |
+
|
| 1784 |
+
Returns:
|
| 1785 |
+
One of polymer/non-polymer/water based on the following criteria:
|
| 1786 |
+
* If all atoms are HETATMs and all residues are water -> water.
|
| 1787 |
+
* If all atoms are HETATMs and not all residues are water -> non-polymer.
|
| 1788 |
+
* Otherwise -> polymer.
|
| 1789 |
+
"""
|
| 1790 |
+
if not chain_residues or not atom_types:
|
| 1791 |
+
raise ValueError(
|
| 1792 |
+
f'chain_residues (len {len(chain_residues)}) and atom_types (len '
|
| 1793 |
+
f'{len(atom_types)}) must be both non-empty. Got: {chain_residues=} '
|
| 1794 |
+
f'and {atom_types=}'
|
| 1795 |
+
)
|
| 1796 |
+
|
| 1797 |
+
if all(a == 'HETATM' for a in atom_types):
|
| 1798 |
+
if all(c in residue_names.WATER_TYPES for c in chain_residues):
|
| 1799 |
+
return mmcif_names.WATER
|
| 1800 |
+
return mmcif_names.NON_POLYMER_CHAIN
|
| 1801 |
+
return mmcif_names.POLYMER_CHAIN
|
flax_model/alphafold3/structure/sterics.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Functions relating to spatial locations of atoms within a structure."""
|
| 4 |
+
|
| 5 |
+
from collections.abc import Collection, Sequence
|
| 6 |
+
|
| 7 |
+
from flax_model.alphafold3 import structure
|
| 8 |
+
from flax_model.alphafold3.structure import mmcif
|
| 9 |
+
import numpy as np
|
| 10 |
+
import scipy
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _make_atom_has_clash_mask(
|
| 14 |
+
kd_query_result: np.ndarray,
|
| 15 |
+
struc: structure.Structure,
|
| 16 |
+
ignore_chains: Collection[str],
|
| 17 |
+
) -> np.ndarray:
|
| 18 |
+
"""Returns a boolean NumPy array representing whether each atom has a clash.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
kd_query_result: NumPy array containing N-atoms arrays, each array
|
| 22 |
+
containing indices to atoms that clash with the N'th atom.
|
| 23 |
+
struc: Structure over which clashes were detected.
|
| 24 |
+
ignore_chains: Collection of chains that should not be considered clashing.
|
| 25 |
+
A boolean NumPy array of length N atoms.
|
| 26 |
+
"""
|
| 27 |
+
atom_is_clashing = np.zeros((struc.num_atoms,), dtype=bool)
|
| 28 |
+
for atom_index, clashes in enumerate(kd_query_result):
|
| 29 |
+
chain_i = struc.chain_id[atom_index]
|
| 30 |
+
if chain_i in ignore_chains:
|
| 31 |
+
continue
|
| 32 |
+
islig_i = struc.is_ligand_mask[atom_index]
|
| 33 |
+
for clashing_atom_index in clashes:
|
| 34 |
+
chain_c = struc.chain_id[clashing_atom_index]
|
| 35 |
+
if chain_c in ignore_chains:
|
| 36 |
+
continue
|
| 37 |
+
islig_c = struc.is_ligand_mask[clashing_atom_index]
|
| 38 |
+
if (
|
| 39 |
+
clashing_atom_index == atom_index
|
| 40 |
+
or chain_i == chain_c
|
| 41 |
+
or islig_i != islig_c
|
| 42 |
+
):
|
| 43 |
+
# Ignore clashes within chain or between ligand and polymer.
|
| 44 |
+
continue
|
| 45 |
+
atom_is_clashing[atom_index] = True
|
| 46 |
+
return atom_is_clashing
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def find_clashing_chains(
|
| 50 |
+
struc: structure.Structure,
|
| 51 |
+
clash_thresh_angstrom: float = 1.7,
|
| 52 |
+
clash_thresh_fraction: float = 0.3,
|
| 53 |
+
) -> Sequence[str]:
|
| 54 |
+
"""Finds chains that clash with others.
|
| 55 |
+
|
| 56 |
+
Clashes are defined by polymer backbone atoms and all ligand atoms.
|
| 57 |
+
Ligand-polymer clashes are not dropped.
|
| 58 |
+
|
| 59 |
+
Will not find clashes if all coordinates are 0. Coordinates are all 0s if
|
| 60 |
+
the structure is generated from sequences only, as done for inference in
|
| 61 |
+
dendro for example.
|
| 62 |
+
|
| 63 |
+
Args:
|
| 64 |
+
struc: The structure defining the chains and atom positions.
|
| 65 |
+
clash_thresh_angstrom: Below this distance, atoms are considered clashing.
|
| 66 |
+
clash_thresh_fraction: Chains with more than this fraction of their atoms
|
| 67 |
+
considered clashing will be dropped. This value should be in the range (0,
|
| 68 |
+
1].
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
A sequence of chain ids for chains that clash.
|
| 72 |
+
|
| 73 |
+
Raises:
|
| 74 |
+
ValueError: If `clash_thresh_fraction` is not in range (0,1].
|
| 75 |
+
"""
|
| 76 |
+
if not 0 < clash_thresh_fraction <= 1:
|
| 77 |
+
raise ValueError('clash_thresh_fraction must be in range (0,1]')
|
| 78 |
+
|
| 79 |
+
struc_backbone = struc.filter_polymers_to_single_atom_per_res()
|
| 80 |
+
if struc_backbone.num_chains == 0:
|
| 81 |
+
return []
|
| 82 |
+
|
| 83 |
+
# If the coordinates are all 0, do not search for clashes.
|
| 84 |
+
if not np.any(struc_backbone.coords):
|
| 85 |
+
return []
|
| 86 |
+
|
| 87 |
+
coord_kdtree = scipy.spatial.cKDTree(struc_backbone.coords)
|
| 88 |
+
|
| 89 |
+
# For each atom coordinate, find all atoms within the clash thresh radius.
|
| 90 |
+
clashing_per_atom = coord_kdtree.query_ball_point(
|
| 91 |
+
struc_backbone.coords, r=clash_thresh_angstrom
|
| 92 |
+
)
|
| 93 |
+
chain_ids = struc_backbone.chains
|
| 94 |
+
if struc_backbone.atom_occupancy is not None:
|
| 95 |
+
chain_occupancy = np.array([
|
| 96 |
+
np.mean(struc_backbone.atom_occupancy[start:end])
|
| 97 |
+
for start, end in struc_backbone.iter_chain_ranges()
|
| 98 |
+
])
|
| 99 |
+
else:
|
| 100 |
+
chain_occupancy = None
|
| 101 |
+
|
| 102 |
+
# Remove chains until no more significant clashing.
|
| 103 |
+
chains_to_remove = set()
|
| 104 |
+
for _ in range(len(chain_ids)):
|
| 105 |
+
# Calculate maximally clashing.
|
| 106 |
+
atom_has_clash = _make_atom_has_clash_mask(
|
| 107 |
+
clashing_per_atom, struc_backbone, chains_to_remove
|
| 108 |
+
)
|
| 109 |
+
clashes_per_chain = np.array([
|
| 110 |
+
atom_has_clash[start:end].mean()
|
| 111 |
+
for start, end in struc_backbone.iter_chain_ranges()
|
| 112 |
+
])
|
| 113 |
+
max_clash = np.max(clashes_per_chain)
|
| 114 |
+
if max_clash <= clash_thresh_fraction:
|
| 115 |
+
# None of the remaining chains exceed the clash fraction threshold, so
|
| 116 |
+
# we can exit.
|
| 117 |
+
break
|
| 118 |
+
|
| 119 |
+
# Greedily remove worst with the lowest occupancy.
|
| 120 |
+
most_clashes = np.nonzero(clashes_per_chain == max_clash)[0]
|
| 121 |
+
if chain_occupancy is not None:
|
| 122 |
+
occupancy_clashing = chain_occupancy[most_clashes]
|
| 123 |
+
last_lowest_occupancy = (
|
| 124 |
+
len(occupancy_clashing) - np.argmin(occupancy_clashing[::-1]) - 1
|
| 125 |
+
)
|
| 126 |
+
worst_and_last = most_clashes[last_lowest_occupancy]
|
| 127 |
+
else:
|
| 128 |
+
worst_and_last = most_clashes[-1]
|
| 129 |
+
|
| 130 |
+
chains_to_remove.add(chain_ids[worst_and_last])
|
| 131 |
+
|
| 132 |
+
return sorted(chains_to_remove, key=mmcif.str_id_to_int_id)
|
flax_model/alphafold3/structure/structure.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
flax_model/alphafold3/structure/structure_tables.py
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Table implementations for the Structure class."""
|
| 4 |
+
|
| 5 |
+
import collections
|
| 6 |
+
from collections.abc import Mapping, Sequence
|
| 7 |
+
import dataclasses
|
| 8 |
+
import functools
|
| 9 |
+
import itertools
|
| 10 |
+
import typing
|
| 11 |
+
from typing import Any, ClassVar, Self
|
| 12 |
+
|
| 13 |
+
from flax_model.alphafold3.constants import mmcif_names
|
| 14 |
+
from flax_model.alphafold3.constants import residue_names
|
| 15 |
+
from flax_model.alphafold3.cpp import aggregation
|
| 16 |
+
from flax_model.alphafold3.cpp import string_array
|
| 17 |
+
from flax_model.alphafold3.structure import bonds as bonds_module
|
| 18 |
+
from flax_model.alphafold3.structure import mmcif
|
| 19 |
+
from flax_model.alphafold3.structure import table
|
| 20 |
+
import numpy as np
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
Bonds = bonds_module.Bonds
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _residue_name_to_record_name(
|
| 27 |
+
residue_name: np.ndarray,
|
| 28 |
+
polymer_mask: np.ndarray,
|
| 29 |
+
) -> np.ndarray:
|
| 30 |
+
"""Returns record names (ATOM/HETATM) given residue names and polymer mask."""
|
| 31 |
+
record_name = np.array(['HETATM'] * len(residue_name), dtype=object)
|
| 32 |
+
record_name[polymer_mask] = string_array.remap(
|
| 33 |
+
residue_name[polymer_mask],
|
| 34 |
+
mapping={r: 'ATOM' for r in residue_names.STANDARD_POLYMER_TYPES},
|
| 35 |
+
default_value='HETATM',
|
| 36 |
+
)
|
| 37 |
+
return record_name
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
| 41 |
+
class AuthorNamingScheme:
|
| 42 |
+
"""A mapping from internal values to author values in a mmCIF.
|
| 43 |
+
|
| 44 |
+
Fields:
|
| 45 |
+
auth_asym_id: A mapping from label_asym_id to auth_asym_id.
|
| 46 |
+
auth_seq_id: A mapping from label_asym_id to a mapping from
|
| 47 |
+
label_seq_id to auth_seq_id.
|
| 48 |
+
insertion_code: A mapping from label_asym_id to a mapping from
|
| 49 |
+
label_seq_id to insertion codes.
|
| 50 |
+
entity_id: A mapping from label_asym_id to _entity.id.
|
| 51 |
+
entity_desc: A mapping from _entity.id to _entity.pdbx_description.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
auth_asym_id: Mapping[str, str]
|
| 55 |
+
auth_seq_id: Mapping[str, Mapping[int, str]]
|
| 56 |
+
insertion_code: Mapping[str, Mapping[int, str | None]]
|
| 57 |
+
entity_id: Mapping[str, str]
|
| 58 |
+
entity_desc: Mapping[str, str]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _default(
|
| 62 |
+
candidate_value: np.ndarray | None, default_value: Sequence[Any], dtype: Any
|
| 63 |
+
) -> np.ndarray:
|
| 64 |
+
if candidate_value is None:
|
| 65 |
+
return np.array(default_value, dtype=dtype)
|
| 66 |
+
return np.array(candidate_value, dtype=dtype)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
| 70 |
+
class Atoms(table.Table):
|
| 71 |
+
"""Table of atoms in a Structure."""
|
| 72 |
+
|
| 73 |
+
chain_key: np.ndarray
|
| 74 |
+
res_key: np.ndarray
|
| 75 |
+
name: np.ndarray
|
| 76 |
+
element: np.ndarray
|
| 77 |
+
x: np.ndarray
|
| 78 |
+
y: np.ndarray
|
| 79 |
+
z: np.ndarray
|
| 80 |
+
b_factor: np.ndarray
|
| 81 |
+
occupancy: np.ndarray
|
| 82 |
+
multimodel_cols: ClassVar[tuple[str, ...]] = (
|
| 83 |
+
'x',
|
| 84 |
+
'y',
|
| 85 |
+
'z',
|
| 86 |
+
'b_factor',
|
| 87 |
+
'occupancy',
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
def __post_init__(self):
|
| 91 |
+
# Validates that the atom coordinates, b-factors and occupancies are finite.
|
| 92 |
+
for column_name in ('x', 'y', 'z', 'b_factor', 'occupancy'):
|
| 93 |
+
column = self.get_column(column_name)
|
| 94 |
+
if not np.isfinite(column).all():
|
| 95 |
+
raise ValueError(
|
| 96 |
+
f'Column {column_name} must not contain NaN/inf values.'
|
| 97 |
+
)
|
| 98 |
+
# super().__post_init__() can't be used as that causes the following error:
|
| 99 |
+
# TypeError: super(type, obj): obj must be an instance or subtype of type
|
| 100 |
+
super(Atoms, self).__post_init__()
|
| 101 |
+
|
| 102 |
+
@classmethod
|
| 103 |
+
def make_empty(cls) -> Self:
|
| 104 |
+
return cls(
|
| 105 |
+
key=np.array([], dtype=np.int64),
|
| 106 |
+
chain_key=np.array([], dtype=np.int64),
|
| 107 |
+
res_key=np.array([], dtype=np.int64),
|
| 108 |
+
name=np.array([], dtype=object),
|
| 109 |
+
element=np.array([], dtype=object),
|
| 110 |
+
x=np.array([], dtype=np.float32),
|
| 111 |
+
y=np.array([], dtype=np.float32),
|
| 112 |
+
z=np.array([], dtype=np.float32),
|
| 113 |
+
b_factor=np.array([], dtype=np.float32),
|
| 114 |
+
occupancy=np.array([], dtype=np.float32),
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
@classmethod
|
| 118 |
+
def from_defaults(
|
| 119 |
+
cls,
|
| 120 |
+
*,
|
| 121 |
+
chain_key: np.ndarray,
|
| 122 |
+
res_key: np.ndarray,
|
| 123 |
+
key: np.ndarray | None = None,
|
| 124 |
+
name: np.ndarray | None = None,
|
| 125 |
+
element: np.ndarray | None = None,
|
| 126 |
+
x: np.ndarray | None = None,
|
| 127 |
+
y: np.ndarray | None = None,
|
| 128 |
+
z: np.ndarray | None = None,
|
| 129 |
+
b_factor: np.ndarray | None = None,
|
| 130 |
+
occupancy: np.ndarray | None = None,
|
| 131 |
+
) -> Self:
|
| 132 |
+
"""Create an Atoms table with minimal user inputs."""
|
| 133 |
+
num_atoms = len(chain_key)
|
| 134 |
+
if not num_atoms:
|
| 135 |
+
return cls.make_empty()
|
| 136 |
+
return Atoms(
|
| 137 |
+
chain_key=chain_key,
|
| 138 |
+
res_key=res_key,
|
| 139 |
+
key=_default(key, np.arange(num_atoms), np.int64),
|
| 140 |
+
name=_default(name, ['?'] * num_atoms, object),
|
| 141 |
+
element=_default(element, ['?'] * num_atoms, object),
|
| 142 |
+
x=_default(x, [0.0] * num_atoms, np.float32),
|
| 143 |
+
y=_default(y, [0.0] * num_atoms, np.float32),
|
| 144 |
+
z=_default(z, [0.0] * num_atoms, np.float32),
|
| 145 |
+
b_factor=_default(b_factor, [0.0] * num_atoms, np.float32),
|
| 146 |
+
occupancy=_default(occupancy, [1.0] * num_atoms, np.float32),
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
def get_value_by_index(
|
| 150 |
+
self, column_name: str, index: int
|
| 151 |
+
) -> table.TableEntry | np.ndarray:
|
| 152 |
+
if column_name in self.multimodel_cols:
|
| 153 |
+
return self.get_column(column_name)[..., index]
|
| 154 |
+
else:
|
| 155 |
+
return self.get_column(column_name)[index]
|
| 156 |
+
|
| 157 |
+
def copy_and_update_coords(self, coords: np.ndarray) -> Self:
|
| 158 |
+
"""Returns a copy with the x, y and z columns updated."""
|
| 159 |
+
if coords.shape[-1] != 3:
|
| 160 |
+
raise ValueError(
|
| 161 |
+
f'Expecting 3-dimensional coordinates, got {coords.shape}'
|
| 162 |
+
)
|
| 163 |
+
return typing.cast(
|
| 164 |
+
Atoms,
|
| 165 |
+
self.copy_and_update(
|
| 166 |
+
x=coords[..., 0], y=coords[..., 1], z=coords[..., 2]
|
| 167 |
+
),
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
@property
|
| 171 |
+
def shape(self) -> tuple[int, ...]:
|
| 172 |
+
return self.x.shape
|
| 173 |
+
|
| 174 |
+
@property
|
| 175 |
+
def ndim(self) -> int:
|
| 176 |
+
return len(self.shape)
|
| 177 |
+
|
| 178 |
+
@functools.cached_property
|
| 179 |
+
def num_models(self) -> int:
|
| 180 |
+
"""The number of models of this Structure."""
|
| 181 |
+
leading_dims = self.shape[:-1]
|
| 182 |
+
match leading_dims:
|
| 183 |
+
case ():
|
| 184 |
+
return 1
|
| 185 |
+
case (single_leading_dim_size,):
|
| 186 |
+
return single_leading_dim_size
|
| 187 |
+
case _:
|
| 188 |
+
raise ValueError(
|
| 189 |
+
'num_models not defined for atom tables with more than one '
|
| 190 |
+
'leading dimension.'
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
| 195 |
+
class Residues(table.Table):
|
| 196 |
+
"""Table of residues in a Structure."""
|
| 197 |
+
|
| 198 |
+
chain_key: np.ndarray
|
| 199 |
+
id: np.ndarray
|
| 200 |
+
name: np.ndarray
|
| 201 |
+
auth_seq_id: np.ndarray
|
| 202 |
+
insertion_code: np.ndarray
|
| 203 |
+
|
| 204 |
+
@classmethod
|
| 205 |
+
def make_empty(cls) -> Self:
|
| 206 |
+
return cls(
|
| 207 |
+
key=np.array([], dtype=np.int64),
|
| 208 |
+
chain_key=np.array([], dtype=np.int64),
|
| 209 |
+
id=np.array([], dtype=np.int32),
|
| 210 |
+
name=np.array([], dtype=object),
|
| 211 |
+
auth_seq_id=np.array([], dtype=object),
|
| 212 |
+
insertion_code=np.array([], dtype=object),
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
@classmethod
|
| 216 |
+
def from_defaults(
|
| 217 |
+
cls,
|
| 218 |
+
*,
|
| 219 |
+
id: np.ndarray, # pylint:disable=redefined-builtin
|
| 220 |
+
chain_key: np.ndarray,
|
| 221 |
+
key: np.ndarray | None = None,
|
| 222 |
+
name: np.ndarray | None = None,
|
| 223 |
+
auth_seq_id: np.ndarray | None = None,
|
| 224 |
+
insertion_code: np.ndarray | None = None,
|
| 225 |
+
) -> Self:
|
| 226 |
+
"""Create a Residues table with minimal user inputs."""
|
| 227 |
+
num_res = len(id)
|
| 228 |
+
if not num_res:
|
| 229 |
+
return cls.make_empty()
|
| 230 |
+
return Residues(
|
| 231 |
+
key=_default(key, np.arange(num_res), np.int64),
|
| 232 |
+
id=id,
|
| 233 |
+
chain_key=chain_key,
|
| 234 |
+
name=_default(name, ['UNK'] * num_res, object),
|
| 235 |
+
auth_seq_id=_default(auth_seq_id, id.astype(str), object),
|
| 236 |
+
insertion_code=_default(insertion_code, ['?'] * num_res, object),
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
| 241 |
+
class Chains(table.Table):
|
| 242 |
+
"""Table of chains in a Structure."""
|
| 243 |
+
|
| 244 |
+
id: np.ndarray
|
| 245 |
+
type: np.ndarray
|
| 246 |
+
auth_asym_id: np.ndarray
|
| 247 |
+
entity_id: np.ndarray
|
| 248 |
+
entity_desc: np.ndarray
|
| 249 |
+
|
| 250 |
+
@classmethod
|
| 251 |
+
def make_empty(cls) -> Self:
|
| 252 |
+
return cls(
|
| 253 |
+
key=np.array([], dtype=np.int64),
|
| 254 |
+
id=np.array([], dtype=object),
|
| 255 |
+
type=np.array([], dtype=object),
|
| 256 |
+
auth_asym_id=np.array([], dtype=object),
|
| 257 |
+
entity_id=np.array([], dtype=object),
|
| 258 |
+
entity_desc=np.array([], dtype=object),
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
@classmethod
|
| 262 |
+
def from_defaults(
|
| 263 |
+
cls,
|
| 264 |
+
*,
|
| 265 |
+
id: np.ndarray, # pylint:disable=redefined-builtin
|
| 266 |
+
key: np.ndarray | None = None,
|
| 267 |
+
type: np.ndarray | None = None, # pylint:disable=redefined-builtin
|
| 268 |
+
auth_asym_id: np.ndarray | None = None,
|
| 269 |
+
entity_id: np.ndarray | None = None,
|
| 270 |
+
entity_desc: np.ndarray | None = None,
|
| 271 |
+
) -> Self:
|
| 272 |
+
"""Create a Chains table with minimal user inputs."""
|
| 273 |
+
num_chains = len(id)
|
| 274 |
+
if not num_chains:
|
| 275 |
+
return cls.make_empty()
|
| 276 |
+
|
| 277 |
+
return Chains(
|
| 278 |
+
key=_default(key, np.arange(num_chains), np.int64),
|
| 279 |
+
id=id,
|
| 280 |
+
type=_default(type, [mmcif_names.PROTEIN_CHAIN] * num_chains, object),
|
| 281 |
+
auth_asym_id=_default(auth_asym_id, id, object),
|
| 282 |
+
entity_id=_default(
|
| 283 |
+
entity_id, np.arange(1, num_chains + 1).astype(str), object
|
| 284 |
+
),
|
| 285 |
+
entity_desc=_default(entity_desc, ['.'] * num_chains, object),
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def to_mmcif_sequence_and_entity_tables(
|
| 290 |
+
chains: Chains,
|
| 291 |
+
residues: Residues,
|
| 292 |
+
atom_res_key: np.ndarray,
|
| 293 |
+
) -> Mapping[str, Sequence[str]]:
|
| 294 |
+
"""Returns raw sequence and entity mmCIF tables."""
|
| 295 |
+
raw_mmcif = collections.defaultdict(list)
|
| 296 |
+
chains_by_entity_id = {}
|
| 297 |
+
written_entity_poly_seq_ids = set()
|
| 298 |
+
present_res_keys = set(atom_res_key)
|
| 299 |
+
|
| 300 |
+
# Performance optimisation: Find residue indices for each chain in advance, so
|
| 301 |
+
# that we don't have to do redunant masking work for each chain.
|
| 302 |
+
res_indices_for_chain = aggregation.indices_grouped_by_value(
|
| 303 |
+
residues.chain_key
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
for chain in chains.iterrows():
|
| 307 |
+
# Add all chain information to the _struct_asym table.
|
| 308 |
+
chain_id = chain['id'] # Saves multiple dict lookups.
|
| 309 |
+
auth_asym_id = chain['auth_asym_id']
|
| 310 |
+
entity_id = chain['entity_id']
|
| 311 |
+
chains_by_entity_id.setdefault(entity_id, []).append(chain)
|
| 312 |
+
raw_mmcif['_struct_asym.id'].append(chain_id)
|
| 313 |
+
raw_mmcif['_struct_asym.entity_id'].append(entity_id)
|
| 314 |
+
|
| 315 |
+
res_chain_indices = res_indices_for_chain[chain['key']]
|
| 316 |
+
chain_type = chain['type']
|
| 317 |
+
is_polymer = chain_type in mmcif_names.POLYMER_CHAIN_TYPES
|
| 318 |
+
is_water = chain_type == mmcif_names.WATER
|
| 319 |
+
is_branched = len(res_chain_indices) > 1 and not is_polymer and not is_water
|
| 320 |
+
write_entity_poly_seq = entity_id not in written_entity_poly_seq_ids
|
| 321 |
+
|
| 322 |
+
# Iterate over the individual masked residue table columns, as that doesn't
|
| 323 |
+
# create a copy (only a view), while residues[res_chain_indices] does.
|
| 324 |
+
for res_key, res_name, res_id, pdb_seq_num, res_ins_code in zip(
|
| 325 |
+
residues.key[res_chain_indices],
|
| 326 |
+
residues.name[res_chain_indices],
|
| 327 |
+
residues.id[res_chain_indices],
|
| 328 |
+
residues.auth_seq_id[res_chain_indices],
|
| 329 |
+
residues.insertion_code[res_chain_indices],
|
| 330 |
+
strict=True,
|
| 331 |
+
):
|
| 332 |
+
is_missing = res_key not in present_res_keys
|
| 333 |
+
str_res_id = str(res_id)
|
| 334 |
+
# While atom_site uses "?" for insertion codes, scheme tables use ".".
|
| 335 |
+
ins_code = (res_ins_code or '.').replace('?', '.')
|
| 336 |
+
auth_seq_num = '?' if is_missing else pdb_seq_num
|
| 337 |
+
|
| 338 |
+
if is_polymer:
|
| 339 |
+
raw_mmcif['_pdbx_poly_seq_scheme.asym_id'].append(chain_id)
|
| 340 |
+
raw_mmcif['_pdbx_poly_seq_scheme.entity_id'].append(entity_id)
|
| 341 |
+
raw_mmcif['_pdbx_poly_seq_scheme.seq_id'].append(str_res_id)
|
| 342 |
+
raw_mmcif['_pdbx_poly_seq_scheme.mon_id'].append(res_name)
|
| 343 |
+
raw_mmcif['_pdbx_poly_seq_scheme.pdb_seq_num'].append(pdb_seq_num)
|
| 344 |
+
raw_mmcif['_pdbx_poly_seq_scheme.auth_seq_num'].append(auth_seq_num)
|
| 345 |
+
raw_mmcif['_pdbx_poly_seq_scheme.pdb_strand_id'].append(auth_asym_id)
|
| 346 |
+
raw_mmcif['_pdbx_poly_seq_scheme.pdb_ins_code'].append(ins_code)
|
| 347 |
+
# Structure doesn't support heterogeneous sequences.
|
| 348 |
+
raw_mmcif['_pdbx_poly_seq_scheme.hetero'].append('n')
|
| 349 |
+
if write_entity_poly_seq:
|
| 350 |
+
raw_mmcif['_entity_poly_seq.entity_id'].append(entity_id)
|
| 351 |
+
raw_mmcif['_entity_poly_seq.num'].append(str_res_id)
|
| 352 |
+
raw_mmcif['_entity_poly_seq.mon_id'].append(res_name)
|
| 353 |
+
# Structure doesn't support heterogeneous sequences.
|
| 354 |
+
raw_mmcif['_entity_poly_seq.hetero'].append('n')
|
| 355 |
+
written_entity_poly_seq_ids.add(entity_id)
|
| 356 |
+
elif is_branched:
|
| 357 |
+
raw_mmcif['_pdbx_branch_scheme.asym_id'].append(chain_id)
|
| 358 |
+
raw_mmcif['_pdbx_branch_scheme.entity_id'].append(entity_id)
|
| 359 |
+
raw_mmcif['_pdbx_branch_scheme.mon_id'].append(res_name)
|
| 360 |
+
raw_mmcif['_pdbx_branch_scheme.num'].append(str_res_id)
|
| 361 |
+
raw_mmcif['_pdbx_branch_scheme.pdb_asym_id'].append(auth_asym_id)
|
| 362 |
+
raw_mmcif['_pdbx_branch_scheme.pdb_seq_num'].append(pdb_seq_num)
|
| 363 |
+
raw_mmcif['_pdbx_branch_scheme.auth_asym_id'].append(auth_asym_id)
|
| 364 |
+
raw_mmcif['_pdbx_branch_scheme.auth_seq_num'].append(auth_seq_num)
|
| 365 |
+
raw_mmcif['_pdbx_branch_scheme.pdb_ins_code'].append(ins_code)
|
| 366 |
+
# Structure doesn't support heterogeneous sequences.
|
| 367 |
+
raw_mmcif['_pdbx_branch_scheme.hetero'].append('n')
|
| 368 |
+
else:
|
| 369 |
+
raw_mmcif['_pdbx_nonpoly_scheme.asym_id'].append(chain_id)
|
| 370 |
+
raw_mmcif['_pdbx_nonpoly_scheme.entity_id'].append(entity_id)
|
| 371 |
+
raw_mmcif['_pdbx_nonpoly_scheme.mon_id'].append(res_name)
|
| 372 |
+
raw_mmcif['_pdbx_nonpoly_scheme.pdb_seq_num'].append(pdb_seq_num)
|
| 373 |
+
raw_mmcif['_pdbx_nonpoly_scheme.auth_seq_num'].append(auth_seq_num)
|
| 374 |
+
raw_mmcif['_pdbx_nonpoly_scheme.pdb_strand_id'].append(auth_asym_id)
|
| 375 |
+
raw_mmcif['_pdbx_nonpoly_scheme.pdb_ins_code'].append(ins_code)
|
| 376 |
+
|
| 377 |
+
# Add _entity and _entity_poly tables.
|
| 378 |
+
for entity_id, chains in chains_by_entity_id.items():
|
| 379 |
+
# chains should always be a non-empty list because of how we constructed
|
| 380 |
+
# chains_by_entity_id.
|
| 381 |
+
assert chains
|
| 382 |
+
# All chains for a given entity should have the same type and sequence
|
| 383 |
+
# so we can pick the first one without losing information.
|
| 384 |
+
key_chain = chains[0]
|
| 385 |
+
raw_mmcif['_entity.id'].append(entity_id)
|
| 386 |
+
raw_mmcif['_entity.pdbx_description'].append(key_chain['entity_desc'])
|
| 387 |
+
entity_type = key_chain['type']
|
| 388 |
+
if entity_type not in mmcif_names.POLYMER_CHAIN_TYPES:
|
| 389 |
+
raw_mmcif['_entity.type'].append(entity_type)
|
| 390 |
+
else:
|
| 391 |
+
raw_mmcif['_entity.type'].append('polymer')
|
| 392 |
+
raw_mmcif['_entity_poly.entity_id'].append(entity_id)
|
| 393 |
+
raw_mmcif['_entity_poly.type'].append(entity_type)
|
| 394 |
+
|
| 395 |
+
# _entity_poly.pdbx_strand_id is a comma-separated list of
|
| 396 |
+
# auth_asym_ids that are part of the entity.
|
| 397 |
+
raw_mmcif['_entity_poly.pdbx_strand_id'].append(
|
| 398 |
+
','.join(chain['auth_asym_id'] for chain in chains)
|
| 399 |
+
)
|
| 400 |
+
return raw_mmcif
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def to_mmcif_atom_site_and_bonds_table(
|
| 404 |
+
*,
|
| 405 |
+
chains: Chains,
|
| 406 |
+
residues: Residues,
|
| 407 |
+
atoms: Atoms,
|
| 408 |
+
bonds: Bonds,
|
| 409 |
+
coords_decimal_places: int,
|
| 410 |
+
) -> Mapping[str, Sequence[str]]:
|
| 411 |
+
"""Returns raw _atom_site and _struct_conn mmCIF tables."""
|
| 412 |
+
raw_mmcif = collections.defaultdict(list)
|
| 413 |
+
# Use [value] * num wherever possible since it is about 10x faster than list
|
| 414 |
+
# comprehension in such cases. Also use f-strings instead of str() - faster.
|
| 415 |
+
total_atoms = atoms.size * atoms.num_models
|
| 416 |
+
raw_mmcif['_atom_site.id'] = [f'{i}' for i in range(1, total_atoms + 1)]
|
| 417 |
+
raw_mmcif['_atom_site.label_alt_id'] = ['.'] * total_atoms
|
| 418 |
+
# Use format_float_array instead of list comprehension for performance.
|
| 419 |
+
raw_mmcif['_atom_site.Cartn_x'] = mmcif.format_float_array(
|
| 420 |
+
values=atoms.x.ravel(), num_decimal_places=coords_decimal_places
|
| 421 |
+
)
|
| 422 |
+
raw_mmcif['_atom_site.Cartn_y'] = mmcif.format_float_array(
|
| 423 |
+
values=atoms.y.ravel(), num_decimal_places=coords_decimal_places
|
| 424 |
+
)
|
| 425 |
+
raw_mmcif['_atom_site.Cartn_z'] = mmcif.format_float_array(
|
| 426 |
+
values=atoms.z.ravel(), num_decimal_places=coords_decimal_places
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
# atoms.b_factor or atoms.occupancy can be flat even when the coordinates have
|
| 430 |
+
# leading dimensions. In this case we tile it to match.
|
| 431 |
+
if atoms.b_factor.ndim == 1:
|
| 432 |
+
atom_b_factor = np.tile(atoms.b_factor, atoms.num_models)
|
| 433 |
+
else:
|
| 434 |
+
atom_b_factor = atoms.b_factor.ravel()
|
| 435 |
+
raw_mmcif['_atom_site.B_iso_or_equiv'] = mmcif.format_float_array(
|
| 436 |
+
values=atom_b_factor, num_decimal_places=2
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
if atoms.occupancy.ndim == 1:
|
| 440 |
+
atom_occupancy = np.tile(atoms.occupancy, atoms.num_models)
|
| 441 |
+
else:
|
| 442 |
+
atom_occupancy = atoms.occupancy.ravel()
|
| 443 |
+
raw_mmcif['_atom_site.occupancy'] = mmcif.format_float_array(
|
| 444 |
+
values=atom_occupancy.ravel(), num_decimal_places=2
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
label_atom_id = atoms.name
|
| 448 |
+
type_symbol = atoms.element
|
| 449 |
+
label_comp_id = residues.apply_array_to_column('name', atoms.res_key)
|
| 450 |
+
label_asym_id = chains.apply_array_to_column('id', atoms.chain_key)
|
| 451 |
+
label_entity_id = chains.apply_array_to_column('entity_id', atoms.chain_key)
|
| 452 |
+
# Performance optimisation: Do the int->str conversion on num_residue-sized,
|
| 453 |
+
# array, then select instead of selecting and then converting.
|
| 454 |
+
label_seq_id = residues.id.astype('str').astype(object)[
|
| 455 |
+
..., residues.index_by_key[atoms.res_key]
|
| 456 |
+
]
|
| 457 |
+
|
| 458 |
+
# _atom_site.label_seq_id is '.' for non-polymers.
|
| 459 |
+
non_polymer_chain_mask = string_array.isin(
|
| 460 |
+
chains.type, mmcif_names.POLYMER_CHAIN_TYPES, invert=True
|
| 461 |
+
)
|
| 462 |
+
non_polymer_chain_keys = chains.key[non_polymer_chain_mask]
|
| 463 |
+
non_polymer_atom_mask = np.isin(atoms.chain_key, non_polymer_chain_keys)
|
| 464 |
+
label_seq_id[non_polymer_atom_mask] = '.'
|
| 465 |
+
|
| 466 |
+
auth_asym_id = chains.apply_array_to_column('auth_asym_id', atoms.chain_key)
|
| 467 |
+
auth_seq_id = residues.apply_array_to_column('auth_seq_id', atoms.res_key)
|
| 468 |
+
pdbx_pdb_ins_code = residues.apply_array_to_column(
|
| 469 |
+
'insertion_code', atoms.res_key
|
| 470 |
+
)
|
| 471 |
+
string_array.remap(pdbx_pdb_ins_code, mapping={None: '?'}, inplace=True)
|
| 472 |
+
|
| 473 |
+
group_pdb = _residue_name_to_record_name(
|
| 474 |
+
residue_name=label_comp_id, polymer_mask=~non_polymer_atom_mask
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
def tile_for_models(arr: np.ndarray) -> list[str]:
|
| 478 |
+
if atoms.num_models == 1:
|
| 479 |
+
return arr.tolist() # Memory optimisation: np.tile(arr, 1) does a copy.
|
| 480 |
+
return np.tile(arr, atoms.num_models).tolist()
|
| 481 |
+
|
| 482 |
+
raw_mmcif['_atom_site.group_PDB'] = tile_for_models(group_pdb)
|
| 483 |
+
raw_mmcif['_atom_site.label_atom_id'] = tile_for_models(label_atom_id)
|
| 484 |
+
raw_mmcif['_atom_site.type_symbol'] = tile_for_models(type_symbol)
|
| 485 |
+
raw_mmcif['_atom_site.label_comp_id'] = tile_for_models(label_comp_id)
|
| 486 |
+
raw_mmcif['_atom_site.label_asym_id'] = tile_for_models(label_asym_id)
|
| 487 |
+
raw_mmcif['_atom_site.label_entity_id'] = tile_for_models(label_entity_id)
|
| 488 |
+
raw_mmcif['_atom_site.label_seq_id'] = tile_for_models(label_seq_id)
|
| 489 |
+
raw_mmcif['_atom_site.auth_asym_id'] = tile_for_models(auth_asym_id)
|
| 490 |
+
raw_mmcif['_atom_site.auth_seq_id'] = tile_for_models(auth_seq_id)
|
| 491 |
+
raw_mmcif['_atom_site.pdbx_PDB_ins_code'] = tile_for_models(pdbx_pdb_ins_code)
|
| 492 |
+
model_id = np.array(
|
| 493 |
+
[str(i + 1) for i in range(atoms.num_models)], dtype=object
|
| 494 |
+
)
|
| 495 |
+
raw_mmcif['_atom_site.pdbx_PDB_model_num'] = np.repeat(
|
| 496 |
+
model_id, [atoms.size] * atoms.num_models
|
| 497 |
+
).tolist()
|
| 498 |
+
|
| 499 |
+
if bonds.key.size > 0:
|
| 500 |
+
raw_mmcif.update(
|
| 501 |
+
bonds.to_mmcif_dict_from_atom_arrays(
|
| 502 |
+
atom_key=atoms.key,
|
| 503 |
+
chain_id=label_asym_id,
|
| 504 |
+
res_id=label_seq_id,
|
| 505 |
+
res_name=label_comp_id,
|
| 506 |
+
atom_name=label_atom_id,
|
| 507 |
+
auth_asym_id=auth_asym_id,
|
| 508 |
+
auth_seq_id=auth_seq_id,
|
| 509 |
+
insertion_code=np.array(pdbx_pdb_ins_code),
|
| 510 |
+
)
|
| 511 |
+
)
|
| 512 |
+
return raw_mmcif
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def _flatten_author_naming_scheme_table(
|
| 516 |
+
res_table: Mapping[str, Mapping[int, str]],
|
| 517 |
+
chain_ids: np.ndarray,
|
| 518 |
+
res_chain_ids: np.ndarray,
|
| 519 |
+
res_ids: np.ndarray,
|
| 520 |
+
default_if_missing: str,
|
| 521 |
+
table_name: str,
|
| 522 |
+
) -> np.ndarray:
|
| 523 |
+
"""Flattens an author naming scheme table consistently with res_ids."""
|
| 524 |
+
if not set(chain_ids).issubset(res_table):
|
| 525 |
+
raise ValueError(
|
| 526 |
+
f'Chain IDs in the chain_id array must be a subset of {table_name} in '
|
| 527 |
+
'author naming scheme:\n'
|
| 528 |
+
f'chain_ids: {sorted(chain_ids)}\n'
|
| 529 |
+
f'{table_name} keys: {sorted(res_table.keys())}'
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
chain_change_mask = res_chain_ids[1:] != res_chain_ids[:-1]
|
| 533 |
+
res_chain_boundaries = np.concatenate(
|
| 534 |
+
([0], np.where(chain_change_mask)[0] + 1, [len(res_chain_ids)])
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
flat_vals = np.empty(len(res_ids), dtype=object)
|
| 538 |
+
for chain_start, chain_end in itertools.pairwise(res_chain_boundaries):
|
| 539 |
+
chain_id = res_chain_ids[chain_start]
|
| 540 |
+
chain_res_ids = res_ids[chain_start:chain_end]
|
| 541 |
+
chain_mapping = res_table[chain_id]
|
| 542 |
+
flat_vals[chain_start:chain_end] = [
|
| 543 |
+
chain_mapping.get(r, default_if_missing) for r in chain_res_ids
|
| 544 |
+
]
|
| 545 |
+
|
| 546 |
+
return flat_vals
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
def tables_from_atom_arrays(
|
| 550 |
+
*,
|
| 551 |
+
res_id: np.ndarray,
|
| 552 |
+
author_naming_scheme: AuthorNamingScheme | None = None,
|
| 553 |
+
all_residues: Mapping[str, Sequence[tuple[str, int]]] | None = None,
|
| 554 |
+
chain_id: np.ndarray | None = None,
|
| 555 |
+
chain_type: np.ndarray | None = None,
|
| 556 |
+
res_name: np.ndarray | None = None,
|
| 557 |
+
atom_key: np.ndarray | None = None,
|
| 558 |
+
atom_name: np.ndarray | None = None,
|
| 559 |
+
atom_element: np.ndarray | None = None,
|
| 560 |
+
atom_x: np.ndarray | None = None,
|
| 561 |
+
atom_y: np.ndarray | None = None,
|
| 562 |
+
atom_z: np.ndarray | None = None,
|
| 563 |
+
atom_b_factor: np.ndarray | None = None,
|
| 564 |
+
atom_occupancy: np.ndarray | None = None,
|
| 565 |
+
) -> tuple[Atoms, Residues, Chains]:
|
| 566 |
+
"""Returns Structure tables constructed from atom array level data.
|
| 567 |
+
|
| 568 |
+
All fields except name and, res_id are optional, all array fields consist of a
|
| 569 |
+
value for each atom in the structure - so residue and chain values should hold
|
| 570 |
+
the same value for each atom in the chain or residue. Fields which are not
|
| 571 |
+
defined are filled with default values.
|
| 572 |
+
|
| 573 |
+
Validation is performed by the Structure constructor where possible - but
|
| 574 |
+
author_naming scheme and all_residues must be checked in this function.
|
| 575 |
+
|
| 576 |
+
It is not possible to construct structures with chains that do not contain
|
| 577 |
+
any resolved residues using this function. If this is necessary, use the
|
| 578 |
+
structure.Structure constructor directly.
|
| 579 |
+
|
| 580 |
+
Args:
|
| 581 |
+
res_id: Integer array of shape [num_atom]. The unique residue identifier for
|
| 582 |
+
each residue. mmCIF field - _atom_site.label_seq_id.
|
| 583 |
+
author_naming_scheme: An optional instance of AuthorNamingScheme to use when
|
| 584 |
+
converting this structure to mmCIF.
|
| 585 |
+
all_residues: An optional mapping from each chain ID (i.e. label_asym_id) to
|
| 586 |
+
a sequence of (label_comp_id, label_seq_id) tuples, one per residue. This
|
| 587 |
+
can contain residues that aren't present in the atom arrays. This is
|
| 588 |
+
common in experimental data where some residues are not resolved but are
|
| 589 |
+
known to be present.
|
| 590 |
+
chain_id: String array of shape [num_atom] of unique chain identifiers.
|
| 591 |
+
mmCIF field - _atom_site.label_asym_id.
|
| 592 |
+
chain_type: String array of shape [num_atom]. The molecular type of the
|
| 593 |
+
current chain (e.g. polyribonucleotide). mmCIF field - _entity_poly.type
|
| 594 |
+
OR _entity.type (for non-polymers).
|
| 595 |
+
res_name: String array of shape [num_atom].. The name of each residue,
|
| 596 |
+
typically a 3 letter string for polypeptides or 1-2 letter strings for
|
| 597 |
+
polynucleotides. mmCIF field - _atom_site.label_comp_id.
|
| 598 |
+
atom_key: A unique sorted integer array, used only by the bonds table to
|
| 599 |
+
identify the atoms participating in each bond. If the bonds table is
|
| 600 |
+
specified then this column must be non-None.
|
| 601 |
+
atom_name: String array of shape [num_atom]. The name of each atom (e.g CA,
|
| 602 |
+
O2', etc.). mmCIF field - _atom_site.label_atom_id.
|
| 603 |
+
atom_element: String array of shape [num_atom]. The element type of each
|
| 604 |
+
atom (e.g. C, O, N, etc.). mmCIF field - _atom_site.type_symbol.
|
| 605 |
+
atom_x: Float array of shape [..., num_atom] of atom x coordinates. May have
|
| 606 |
+
arbitrary leading dimensions, provided that these are consistent across
|
| 607 |
+
all coordinate fields.
|
| 608 |
+
atom_y: Float array of shape [..., num_atom] of atom y coordinates. May have
|
| 609 |
+
arbitrary leading dimensions, provided that these are consistent across
|
| 610 |
+
all coordinate fields.
|
| 611 |
+
atom_z: Float array of shape [..., num_atom] of atom z coordinates. May have
|
| 612 |
+
arbitrary leading dimensions, provided that these are consistent across
|
| 613 |
+
all coordinate fields.
|
| 614 |
+
atom_b_factor: Float array of shape [..., num_atom] or [num_atom] of atom
|
| 615 |
+
b-factors or equivalent. If there are no extra leading dimensions then
|
| 616 |
+
these values are assumed to apply to all coordinates for a given atom. If
|
| 617 |
+
there are leading dimensions then these must match those used by the
|
| 618 |
+
coordinate fields.
|
| 619 |
+
atom_occupancy: Float array of shape [..., num_atom] or [num_atom] of atom
|
| 620 |
+
occupancies or equivalent. If there are no extra leading dimensions then
|
| 621 |
+
these values are assumed to apply to all coordinates for a given atom. If
|
| 622 |
+
there are leading dimensions then these must match those used by the
|
| 623 |
+
coordinate fields.
|
| 624 |
+
"""
|
| 625 |
+
num_atoms = len(res_id)
|
| 626 |
+
|
| 627 |
+
for arr_name, array, dtype in (
|
| 628 |
+
('chain_id', chain_id, object),
|
| 629 |
+
('chain_type', chain_type, object),
|
| 630 |
+
('res_id', res_id, np.int32),
|
| 631 |
+
('res_name', res_name, object),
|
| 632 |
+
('atom_key', atom_key, np.int64),
|
| 633 |
+
('atom_name', atom_name, object),
|
| 634 |
+
('atom_element', atom_element, object),
|
| 635 |
+
):
|
| 636 |
+
if array is not None and array.shape != (num_atoms,):
|
| 637 |
+
raise ValueError(f'{arr_name} shape {array.shape} != ({num_atoms},)')
|
| 638 |
+
if array is not None and array.dtype != dtype:
|
| 639 |
+
raise ValueError(f'{arr_name} dtype {array.dtype} != {dtype}')
|
| 640 |
+
|
| 641 |
+
for arr_name, array in (
|
| 642 |
+
('atom_x', atom_x),
|
| 643 |
+
('atom_y', atom_y),
|
| 644 |
+
('atom_z', atom_z),
|
| 645 |
+
('atom_b_factor', atom_b_factor),
|
| 646 |
+
('atom_occupancy', atom_occupancy),
|
| 647 |
+
):
|
| 648 |
+
if array is not None and array.shape[-1] != num_atoms:
|
| 649 |
+
raise ValueError(f'{arr_name} last dim {array.shape[-1]} != {num_atoms=}')
|
| 650 |
+
if (
|
| 651 |
+
array is not None
|
| 652 |
+
and array.dtype != np.float32
|
| 653 |
+
and array.dtype != np.float64
|
| 654 |
+
):
|
| 655 |
+
raise ValueError(
|
| 656 |
+
f'{arr_name} must be np.float32 or np.float64, got {array.dtype=}'
|
| 657 |
+
)
|
| 658 |
+
|
| 659 |
+
if all_residues is not None and (res_name is None or res_id is None):
|
| 660 |
+
raise ValueError(
|
| 661 |
+
'If all_residues != None, res_name and res_id must not be None either.'
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
if num_atoms == 0:
|
| 665 |
+
return Atoms.make_empty(), Residues.make_empty(), Chains.make_empty()
|
| 666 |
+
|
| 667 |
+
if chain_id is None:
|
| 668 |
+
chain_id = np.full(shape=num_atoms, fill_value='A', dtype=object)
|
| 669 |
+
if res_name is None:
|
| 670 |
+
res_name = np.full(shape=num_atoms, fill_value='UNK', dtype=object)
|
| 671 |
+
|
| 672 |
+
chain_change_mask = chain_id[1:] != chain_id[:-1]
|
| 673 |
+
chain_start = np.concatenate(([0], np.where(chain_change_mask)[0] + 1))
|
| 674 |
+
res_start = np.concatenate(
|
| 675 |
+
([0], np.where((res_id[1:] != res_id[:-1]) | chain_change_mask)[0] + 1)
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
if len(set(chain_id)) != len(chain_start):
|
| 679 |
+
raise ValueError(f'Chain IDs must be contiguous, but got {chain_id}')
|
| 680 |
+
|
| 681 |
+
# We do not support chains with unresolved residues-only in this function.
|
| 682 |
+
chain_ids = chain_id[chain_start]
|
| 683 |
+
if all_residues and set(all_residues.keys()) != set(chain_ids):
|
| 684 |
+
raise ValueError(
|
| 685 |
+
'all_residues must contain the same set of chain IDs as the chain_id '
|
| 686 |
+
f'array:\nall_residues keys: {sorted(all_residues.keys())}\n'
|
| 687 |
+
f'chain_ids: {sorted(chain_ids)}.'
|
| 688 |
+
)
|
| 689 |
+
# Make sure all_residue ordering is consistent with chain_id.
|
| 690 |
+
if all_residues and np.any(list(all_residues.keys()) != chain_ids):
|
| 691 |
+
all_residues = {cid: all_residues[cid] for cid in chain_ids}
|
| 692 |
+
|
| 693 |
+
# Create the chains table.
|
| 694 |
+
num_chains = len(chain_ids)
|
| 695 |
+
chain_keys = np.arange(num_chains, dtype=np.int64)
|
| 696 |
+
chain_key_by_chain_id = dict(zip(chain_ids, chain_keys, strict=True))
|
| 697 |
+
|
| 698 |
+
if chain_type is not None:
|
| 699 |
+
chain_types = chain_type[chain_start]
|
| 700 |
+
else:
|
| 701 |
+
chain_types = np.full(num_chains, mmcif_names.PROTEIN_CHAIN, dtype=object)
|
| 702 |
+
|
| 703 |
+
if author_naming_scheme is not None:
|
| 704 |
+
auth_asym_id = string_array.remap(
|
| 705 |
+
chain_ids, author_naming_scheme.auth_asym_id
|
| 706 |
+
)
|
| 707 |
+
entity_id = string_array.remap(
|
| 708 |
+
chain_ids, author_naming_scheme.entity_id, default_value='.'
|
| 709 |
+
)
|
| 710 |
+
entity_desc = string_array.remap(
|
| 711 |
+
entity_id, author_naming_scheme.entity_desc, default_value='.'
|
| 712 |
+
)
|
| 713 |
+
else:
|
| 714 |
+
auth_asym_id = chain_ids
|
| 715 |
+
entity_id = (chain_keys + 1).astype(str).astype(object)
|
| 716 |
+
entity_desc = np.full(num_chains, '.', dtype=object)
|
| 717 |
+
|
| 718 |
+
chains = Chains(
|
| 719 |
+
key=chain_keys,
|
| 720 |
+
id=chain_ids,
|
| 721 |
+
type=chain_types,
|
| 722 |
+
auth_asym_id=auth_asym_id,
|
| 723 |
+
entity_id=entity_id,
|
| 724 |
+
entity_desc=entity_desc,
|
| 725 |
+
)
|
| 726 |
+
|
| 727 |
+
# Create the residues table.
|
| 728 |
+
if all_residues is not None:
|
| 729 |
+
residue_order = []
|
| 730 |
+
for cid, residues in all_residues.items():
|
| 731 |
+
residue_order.extend((cid, rname, int(rid)) for (rname, rid) in residues)
|
| 732 |
+
res_chain_ids, res_names, res_ids = zip(*residue_order)
|
| 733 |
+
res_chain_ids = np.array(res_chain_ids, dtype=object)
|
| 734 |
+
res_ids = np.array(res_ids, dtype=np.int32)
|
| 735 |
+
res_names = np.array(res_names, dtype=object)
|
| 736 |
+
else:
|
| 737 |
+
res_chain_ids = chain_id[res_start]
|
| 738 |
+
res_ids = res_id[res_start]
|
| 739 |
+
res_names = res_name[res_start]
|
| 740 |
+
residue_order = list(zip(res_chain_ids, res_names, res_ids))
|
| 741 |
+
|
| 742 |
+
if author_naming_scheme is not None and author_naming_scheme.auth_seq_id:
|
| 743 |
+
auth_seq_id = _flatten_author_naming_scheme_table(
|
| 744 |
+
author_naming_scheme.auth_seq_id,
|
| 745 |
+
chain_ids=chain_ids,
|
| 746 |
+
res_chain_ids=res_chain_ids,
|
| 747 |
+
res_ids=res_ids,
|
| 748 |
+
default_if_missing='.',
|
| 749 |
+
table_name='auth_seq_id',
|
| 750 |
+
)
|
| 751 |
+
else:
|
| 752 |
+
auth_seq_id = res_ids.astype(str).astype(object)
|
| 753 |
+
|
| 754 |
+
if author_naming_scheme is not None and author_naming_scheme.insertion_code:
|
| 755 |
+
insertion_code = _flatten_author_naming_scheme_table(
|
| 756 |
+
author_naming_scheme.insertion_code,
|
| 757 |
+
chain_ids=chain_ids,
|
| 758 |
+
res_chain_ids=res_chain_ids,
|
| 759 |
+
res_ids=res_ids,
|
| 760 |
+
default_if_missing='?',
|
| 761 |
+
table_name='insertion_code',
|
| 762 |
+
)
|
| 763 |
+
# Make sure insertion code of None is mapped to '.'.
|
| 764 |
+
insertion_code = string_array.remap(insertion_code, {None: '?'})
|
| 765 |
+
else:
|
| 766 |
+
insertion_code = np.full(shape=len(res_ids), fill_value='?', dtype=object)
|
| 767 |
+
|
| 768 |
+
res_key_by_res = {res: i for i, res in enumerate(residue_order)}
|
| 769 |
+
res_keys = np.arange(len(residue_order), dtype=np.int64)
|
| 770 |
+
res_chain_keys = string_array.remap(
|
| 771 |
+
res_chain_ids, chain_key_by_chain_id
|
| 772 |
+
).astype(np.int64)
|
| 773 |
+
residues = Residues(
|
| 774 |
+
chain_key=res_chain_keys,
|
| 775 |
+
key=res_keys,
|
| 776 |
+
id=res_ids,
|
| 777 |
+
name=res_names,
|
| 778 |
+
auth_seq_id=auth_seq_id,
|
| 779 |
+
insertion_code=insertion_code,
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
+
if atom_key is None:
|
| 783 |
+
atom_key = np.arange(num_atoms, dtype=np.int64)
|
| 784 |
+
|
| 785 |
+
atom_chain_keys = string_array.remap(chain_id, chain_key_by_chain_id).astype(
|
| 786 |
+
np.int64
|
| 787 |
+
)
|
| 788 |
+
|
| 789 |
+
try:
|
| 790 |
+
atom_res_keys = [res_key_by_res[r] for r in zip(chain_id, res_name, res_id)]
|
| 791 |
+
except KeyError as e:
|
| 792 |
+
missing_chain_id, missing_res_name, missing_res_id = e.args[0]
|
| 793 |
+
raise ValueError(
|
| 794 |
+
'Inconsistent res_name, res_id and all_residues. Could not find '
|
| 795 |
+
f'residue with chain_id={missing_chain_id}, '
|
| 796 |
+
f'res_name={missing_res_name}, res_id={missing_res_id} in all_residues.'
|
| 797 |
+
) from e
|
| 798 |
+
|
| 799 |
+
atoms = Atoms(
|
| 800 |
+
key=atom_key,
|
| 801 |
+
chain_key=atom_chain_keys,
|
| 802 |
+
res_key=np.array(atom_res_keys, dtype=np.int64),
|
| 803 |
+
name=_default(atom_name, ['?'] * num_atoms, object),
|
| 804 |
+
element=_default(atom_element, ['?'] * num_atoms, object),
|
| 805 |
+
x=_default(atom_x, [0.0] * num_atoms, np.float32),
|
| 806 |
+
y=_default(atom_y, [0.0] * num_atoms, np.float32),
|
| 807 |
+
z=_default(atom_z, [0.0] * num_atoms, np.float32),
|
| 808 |
+
b_factor=_default(atom_b_factor, [0.0] * num_atoms, np.float32),
|
| 809 |
+
occupancy=_default(atom_occupancy, [1.0] * num_atoms, np.float32),
|
| 810 |
+
)
|
| 811 |
+
return atoms, residues, chains
|
flax_model/alphafold3/structure/table.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Table module for atom/residue/chain tables in Structure.
|
| 4 |
+
|
| 5 |
+
Tables are intended to be lightweight collections of columns, loosely based
|
| 6 |
+
on a pandas dataframe, for use in the Structure class.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import abc
|
| 10 |
+
from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, Sequence
|
| 11 |
+
import dataclasses
|
| 12 |
+
import functools
|
| 13 |
+
import graphlib
|
| 14 |
+
import typing
|
| 15 |
+
from typing import Any, Protocol, Self, TypeAlias, TypeVar, overload
|
| 16 |
+
|
| 17 |
+
from flax_model.alphafold3.cpp import string_array
|
| 18 |
+
import numpy as np
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
TableEntry: TypeAlias = str | int | float | None
|
| 22 |
+
FilterPredicate: TypeAlias = (
|
| 23 |
+
TableEntry
|
| 24 |
+
| Iterable[Any] # Workaround for b/326384670. Tighten once fixed.
|
| 25 |
+
| Callable[[Any], bool] # Workaround for b/326384670. Tighten once fixed.
|
| 26 |
+
| Callable[[np.ndarray], bool]
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class RowLookup(Protocol):
|
| 31 |
+
|
| 32 |
+
def get_row_by_key(
|
| 33 |
+
self,
|
| 34 |
+
key: int,
|
| 35 |
+
column_name_map: Mapping[str, str] | None = None,
|
| 36 |
+
) -> Mapping[str, Any]:
|
| 37 |
+
...
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
| 41 |
+
class Table:
|
| 42 |
+
"""Parent class for structure tables.
|
| 43 |
+
|
| 44 |
+
A table is a collection of columns of equal length, where one column is the
|
| 45 |
+
key. The key uniquely identifies each row in the table.
|
| 46 |
+
|
| 47 |
+
A table can refer to other tables by including a foreign key column, whose
|
| 48 |
+
values are key values from the other table's key column. These column can have
|
| 49 |
+
arbitrary names and are treated like any other integer-valued column.
|
| 50 |
+
|
| 51 |
+
See the `Database` class in this module for utilities for handing sets of
|
| 52 |
+
tables that are related via foreign keys.
|
| 53 |
+
|
| 54 |
+
NB: This does not correspond to an mmCIF table.
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
key: np.ndarray
|
| 58 |
+
|
| 59 |
+
def __post_init__(self):
|
| 60 |
+
for col_name in self.columns:
|
| 61 |
+
if (col_len := self.get_column(col_name).shape[-1]) != self.size:
|
| 62 |
+
raise ValueError(
|
| 63 |
+
f'All columns should have length {self.size} but got "{col_name}"'
|
| 64 |
+
f' with length {col_len}.'
|
| 65 |
+
)
|
| 66 |
+
self.get_column(col_name).flags.writeable = False # Make col immutable.
|
| 67 |
+
if self.key.size and self.key.min() < 0:
|
| 68 |
+
raise ValueError(
|
| 69 |
+
'Key values must be non-negative. Got negative values:'
|
| 70 |
+
f' {set(self.key[self.key < 0])}'
|
| 71 |
+
)
|
| 72 |
+
self.key.flags.writeable = False # Make key immutable.
|
| 73 |
+
|
| 74 |
+
def __getstate__(self) -> dict[str, Any]:
|
| 75 |
+
"""Returns members with cached properties removed for pickling."""
|
| 76 |
+
cached_props = {
|
| 77 |
+
k
|
| 78 |
+
for k, v in self.__class__.__dict__.items()
|
| 79 |
+
if isinstance(v, functools.cached_property)
|
| 80 |
+
}
|
| 81 |
+
return {k: v for k, v in self.__dict__.items() if k not in cached_props}
|
| 82 |
+
|
| 83 |
+
@functools.cached_property
|
| 84 |
+
def index_by_key(self) -> np.ndarray:
|
| 85 |
+
"""Mapping from key values to their index in the column arrays.
|
| 86 |
+
|
| 87 |
+
i.e.: self.key[index_by_key[k]] == k
|
| 88 |
+
"""
|
| 89 |
+
if not self.key.size:
|
| 90 |
+
return np.array([], dtype=np.int64)
|
| 91 |
+
else:
|
| 92 |
+
index_by_key = np.zeros(np.max(self.key) + 1, dtype=np.int64)
|
| 93 |
+
index_by_key[self.key] = np.arange(self.size)
|
| 94 |
+
return index_by_key
|
| 95 |
+
|
| 96 |
+
@functools.cached_property
|
| 97 |
+
def columns(self) -> tuple[str, ...]:
|
| 98 |
+
"""The names of the columns in the table, including the key column."""
|
| 99 |
+
return tuple(field.name for field in dataclasses.fields(self))
|
| 100 |
+
|
| 101 |
+
@functools.cached_property
|
| 102 |
+
def items(self) -> Mapping[str, np.ndarray]:
|
| 103 |
+
"""Returns the mapping from column names to column values."""
|
| 104 |
+
return {col: getattr(self, col) for col in self.columns}
|
| 105 |
+
|
| 106 |
+
@functools.cached_property
|
| 107 |
+
def size(self) -> int:
|
| 108 |
+
"""The number of rows in the table."""
|
| 109 |
+
return self.key.shape[-1]
|
| 110 |
+
|
| 111 |
+
def __len__(self) -> int:
|
| 112 |
+
return self.size
|
| 113 |
+
|
| 114 |
+
def get_column(self, column_name: str) -> np.ndarray:
|
| 115 |
+
"""Gets a column by name."""
|
| 116 |
+
# Performance optimisation: use the cached columns, instead of getattr.
|
| 117 |
+
return self.items[column_name]
|
| 118 |
+
|
| 119 |
+
def apply_array(self, arr: np.ndarray) -> Self:
|
| 120 |
+
"""Returns a sliced table using a key (!= index) array or a boolean mask."""
|
| 121 |
+
if arr.dtype == bool and np.all(arr):
|
| 122 |
+
return self # Shortcut: No-op, so just return.
|
| 123 |
+
|
| 124 |
+
return self.copy_and_update(**{
|
| 125 |
+
column_name: self.apply_array_to_column(column_name, arr)
|
| 126 |
+
for column_name in self.columns
|
| 127 |
+
})
|
| 128 |
+
|
| 129 |
+
def apply_index(self, index_arr: np.ndarray) -> Self:
|
| 130 |
+
"""Returns a sliced table using an index (!= key) array."""
|
| 131 |
+
if index_arr.dtype == bool:
|
| 132 |
+
raise ValueError('The index array must not be a boolean mask.')
|
| 133 |
+
|
| 134 |
+
return self.copy_and_update(
|
| 135 |
+
**{col: self.get_column(col)[..., index_arr] for col in self.columns}
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
def apply_array_to_column(
|
| 139 |
+
self,
|
| 140 |
+
column_name: str,
|
| 141 |
+
arr: np.ndarray,
|
| 142 |
+
) -> np.ndarray:
|
| 143 |
+
"""Returns a sliced column array using a key array or a boolean mask."""
|
| 144 |
+
if arr.dtype == bool:
|
| 145 |
+
return self.get_column(column_name)[..., arr]
|
| 146 |
+
else:
|
| 147 |
+
return self.get_column(column_name)[..., self.index_by_key[arr]]
|
| 148 |
+
|
| 149 |
+
def get_value_by_index(self, column_name: str, index: int) -> Any:
|
| 150 |
+
return self.get_column(column_name)[index]
|
| 151 |
+
|
| 152 |
+
def get_value_by_key(
|
| 153 |
+
self,
|
| 154 |
+
column_name: str,
|
| 155 |
+
key: int | np.integer,
|
| 156 |
+
) -> TableEntry:
|
| 157 |
+
"""Gets the value of a column at the row with specified key value."""
|
| 158 |
+
return self.get_value_by_index(column_name, self.index_by_key[key])
|
| 159 |
+
|
| 160 |
+
@overload
|
| 161 |
+
def __getitem__(self, key: str) -> np.ndarray:
|
| 162 |
+
...
|
| 163 |
+
|
| 164 |
+
@overload
|
| 165 |
+
def __getitem__(self, key: np.ndarray) -> 'Table':
|
| 166 |
+
...
|
| 167 |
+
|
| 168 |
+
@overload
|
| 169 |
+
def __getitem__(self, key: tuple[str, int | np.integer]) -> TableEntry:
|
| 170 |
+
...
|
| 171 |
+
|
| 172 |
+
@overload
|
| 173 |
+
def __getitem__(self, key: tuple[str, np.ndarray]) -> np.ndarray:
|
| 174 |
+
...
|
| 175 |
+
|
| 176 |
+
def __getitem__(self, key):
|
| 177 |
+
match key:
|
| 178 |
+
case str():
|
| 179 |
+
return self.get_column(key)
|
| 180 |
+
case np.ndarray() as key_arr_or_mask:
|
| 181 |
+
return self.apply_array(key_arr_or_mask)
|
| 182 |
+
case str() as col, int() | np.integer() as key_val:
|
| 183 |
+
return self.get_value_by_key(col, key_val)
|
| 184 |
+
case str() as col, np.ndarray() as key_arr_or_mask:
|
| 185 |
+
return self.apply_array_to_column(col, key_arr_or_mask)
|
| 186 |
+
case _:
|
| 187 |
+
if isinstance(key, tuple):
|
| 188 |
+
err_msg = f'{key}, type: tuple({[type(v) for v in key]})'
|
| 189 |
+
else:
|
| 190 |
+
err_msg = f'{key}, type: {type(key)}'
|
| 191 |
+
raise KeyError(err_msg)
|
| 192 |
+
|
| 193 |
+
def get_row_by_key(
|
| 194 |
+
self,
|
| 195 |
+
key: int,
|
| 196 |
+
column_name_map: Mapping[str, str] | None = None,
|
| 197 |
+
) -> dict[str, Any]:
|
| 198 |
+
"""Gets the row with specified key value."""
|
| 199 |
+
return self.get_row_by_index(
|
| 200 |
+
self.index_by_key[key], column_name_map=column_name_map
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
def get_row_by_index(
|
| 204 |
+
self,
|
| 205 |
+
index: int,
|
| 206 |
+
column_name_map: Mapping[str, str] | None = None,
|
| 207 |
+
) -> dict[str, Any]:
|
| 208 |
+
"""Gets the row at the specified index."""
|
| 209 |
+
if column_name_map is not None:
|
| 210 |
+
return {
|
| 211 |
+
renamed_col: self.get_value_by_index(col, index)
|
| 212 |
+
for renamed_col, col in column_name_map.items()
|
| 213 |
+
}
|
| 214 |
+
else:
|
| 215 |
+
return {col: self.get_value_by_index(col, index) for col in self.columns}
|
| 216 |
+
|
| 217 |
+
def iterrows(
|
| 218 |
+
self,
|
| 219 |
+
*,
|
| 220 |
+
row_keys: np.ndarray | None = None,
|
| 221 |
+
column_name_map: Mapping[str, str] | None = None,
|
| 222 |
+
**table_by_foreign_key_col: RowLookup,
|
| 223 |
+
) -> Iterator[Mapping[str, Any]]:
|
| 224 |
+
"""Yields rows from the table.
|
| 225 |
+
|
| 226 |
+
This can be used to easily convert a table to a Pandas dataframe:
|
| 227 |
+
|
| 228 |
+
```py
|
| 229 |
+
df = pd.DataFrame(table.iterrows())
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
row_keys: An optional array of keys of rows to yield. If None, all rows
|
| 234 |
+
will be yielded.
|
| 235 |
+
column_name_map: An optional mapping from desired keys in the row dicts to
|
| 236 |
+
the names of the columns they correspond to.
|
| 237 |
+
**table_by_foreign_key_col: An optional mapping from column names in this
|
| 238 |
+
table, which are expected to be columns of foreign keys, to the table
|
| 239 |
+
that the foreign keys point into. If provided, then the yielded rows
|
| 240 |
+
will include data from the foreign tables at the appropriate key.
|
| 241 |
+
"""
|
| 242 |
+
if row_keys is not None:
|
| 243 |
+
row_indices = self.index_by_key[row_keys]
|
| 244 |
+
else:
|
| 245 |
+
row_indices = range(self.size)
|
| 246 |
+
for i in row_indices:
|
| 247 |
+
row = self.get_row_by_index(i, column_name_map=column_name_map)
|
| 248 |
+
for key_col, table in table_by_foreign_key_col.items():
|
| 249 |
+
foreign_key = self[key_col][i]
|
| 250 |
+
foreign_row = table.get_row_by_key(foreign_key)
|
| 251 |
+
row.update(foreign_row)
|
| 252 |
+
yield row
|
| 253 |
+
|
| 254 |
+
def with_column_names(
|
| 255 |
+
self, column_name_map: Mapping[str, str]
|
| 256 |
+
) -> 'RenamedTableView':
|
| 257 |
+
"""Returns a view of this table with mapped column names."""
|
| 258 |
+
return RenamedTableView(self, column_name_map=column_name_map)
|
| 259 |
+
|
| 260 |
+
def make_filter_mask(
|
| 261 |
+
self,
|
| 262 |
+
mask: np.ndarray | None = None,
|
| 263 |
+
*,
|
| 264 |
+
apply_per_element: bool = False,
|
| 265 |
+
**predicate_by_col: FilterPredicate,
|
| 266 |
+
) -> np.ndarray | None:
|
| 267 |
+
"""Returns a boolean array of rows to keep, or None if all can be kept.
|
| 268 |
+
|
| 269 |
+
Args:
|
| 270 |
+
mask: See `Table.filter`.
|
| 271 |
+
apply_per_element: See `Table.filter`.
|
| 272 |
+
**predicate_by_col: See `Table.filter`.
|
| 273 |
+
|
| 274 |
+
Returns:
|
| 275 |
+
Either a boolean NumPy array of length `(self.size,)` denoting which rows
|
| 276 |
+
should be kept according to the input mask and predicates, or None. None
|
| 277 |
+
implies there is no filtering required, and is used where possible
|
| 278 |
+
instead of an all-True array to save time and space.
|
| 279 |
+
"""
|
| 280 |
+
if mask is None:
|
| 281 |
+
if not predicate_by_col:
|
| 282 |
+
return None
|
| 283 |
+
else:
|
| 284 |
+
mask = np.ones((self.size,), dtype=bool)
|
| 285 |
+
else:
|
| 286 |
+
if mask.shape != (self.size,):
|
| 287 |
+
raise ValueError(
|
| 288 |
+
f'mask must have shape ({self.size},). Got: {mask.shape}.'
|
| 289 |
+
)
|
| 290 |
+
if mask.dtype != bool:
|
| 291 |
+
raise ValueError(f'mask must have dtype bool. Got: {mask.dtype}.')
|
| 292 |
+
|
| 293 |
+
for col, predicate in predicate_by_col.items():
|
| 294 |
+
if self[col].ndim > 1:
|
| 295 |
+
raise ValueError(
|
| 296 |
+
f'Cannot filter by column {col} with more than 1 dimension.'
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
callable_predicates = []
|
| 300 |
+
if not callable(predicate):
|
| 301 |
+
if isinstance(predicate, Iterable) and not isinstance(predicate, str):
|
| 302 |
+
target_vals = predicate
|
| 303 |
+
else:
|
| 304 |
+
target_vals = [predicate]
|
| 305 |
+
for target_val in target_vals:
|
| 306 |
+
callable_predicates.append(lambda x, target=target_val: x == target)
|
| 307 |
+
else:
|
| 308 |
+
callable_predicates.append(predicate)
|
| 309 |
+
|
| 310 |
+
field_mask = np.zeros_like(mask)
|
| 311 |
+
for callable_predicate in callable_predicates:
|
| 312 |
+
if not apply_per_element:
|
| 313 |
+
callable_predicate = typing.cast(
|
| 314 |
+
Callable[[np.ndarray], bool], callable_predicate
|
| 315 |
+
)
|
| 316 |
+
predicate_result = callable_predicate(self.get_column(col))
|
| 317 |
+
else:
|
| 318 |
+
predicate_result = np.array(
|
| 319 |
+
[callable_predicate(elem) for elem in self.get_column(col)]
|
| 320 |
+
)
|
| 321 |
+
np.logical_or(field_mask, predicate_result, out=field_mask)
|
| 322 |
+
np.logical_and(mask, field_mask, out=mask) # Update in-place.
|
| 323 |
+
return mask
|
| 324 |
+
|
| 325 |
+
def filter(
|
| 326 |
+
self,
|
| 327 |
+
mask: np.ndarray | None = None,
|
| 328 |
+
*,
|
| 329 |
+
apply_per_element: bool = False,
|
| 330 |
+
invert: bool = False,
|
| 331 |
+
**predicate_by_col: FilterPredicate,
|
| 332 |
+
) -> Self:
|
| 333 |
+
"""Filters the table using mask and/or predicates and returns a new table.
|
| 334 |
+
|
| 335 |
+
Predicates can be either:
|
| 336 |
+
1. A constant value, e.g. `'CA'`. In this case then only rows that match
|
| 337 |
+
this value for the given column are retained.
|
| 338 |
+
2. A (non-string) iterable e.g. `('A', 'B')`. In this
|
| 339 |
+
case then rows are retained if they match any of the provided values for
|
| 340 |
+
the given column.
|
| 341 |
+
3. A boolean function e.g. `lambda b_fac: b_fac < 100.0`.
|
| 342 |
+
In this case then only rows that evaluate to `True` are retained. By
|
| 343 |
+
default this function's parameter is expected to be an array, unless
|
| 344 |
+
`apply_per_element=True`.
|
| 345 |
+
|
| 346 |
+
Args:
|
| 347 |
+
mask: An optional boolean NumPy array with length equal to the table size.
|
| 348 |
+
If provided then this will be combined with the other predicates so that
|
| 349 |
+
a row is included if it is masked-in *and* matches all the predicates.
|
| 350 |
+
apply_per_element: Whether apply predicates to each element in the column
|
| 351 |
+
individually, or to pass the whole column array to the predicate.
|
| 352 |
+
invert: If True then the returned table will contain exactly those rows
|
| 353 |
+
that would be removed if this was `False`.
|
| 354 |
+
**predicate_by_col: A mapping from column name to a predicate. Filtered
|
| 355 |
+
columns must be 1D arrays. If multiple columns are provided as keyword
|
| 356 |
+
arguments then each predicate is applied and the results are combined
|
| 357 |
+
using a boolean AND operation, so an atom is only retained if it passes
|
| 358 |
+
all predicates.
|
| 359 |
+
|
| 360 |
+
Returns:
|
| 361 |
+
A new table with the desired rows retained (or filtered out if
|
| 362 |
+
`invert=True`).
|
| 363 |
+
|
| 364 |
+
Raises:
|
| 365 |
+
ValueError: If mask is provided and is not a bool array with shape
|
| 366 |
+
`(num_atoms,)`.
|
| 367 |
+
"""
|
| 368 |
+
filter_mask = self.make_filter_mask(
|
| 369 |
+
mask, apply_per_element=apply_per_element, **predicate_by_col
|
| 370 |
+
)
|
| 371 |
+
if filter_mask is None:
|
| 372 |
+
# No mask or predicate was specified, so we can return early.
|
| 373 |
+
if not invert:
|
| 374 |
+
return self
|
| 375 |
+
else:
|
| 376 |
+
return self[np.array((), dtype=np.int64)]
|
| 377 |
+
else:
|
| 378 |
+
return self[~filter_mask if invert else filter_mask]
|
| 379 |
+
|
| 380 |
+
def _validate_keys_are_column_names(self, keys: Collection[str]) -> None:
|
| 381 |
+
"""Raises an error if any of the keys are not column names."""
|
| 382 |
+
if mismatches := set(keys) - set(self.columns):
|
| 383 |
+
raise ValueError(f'Invalid column names: {sorted(mismatches)}.')
|
| 384 |
+
|
| 385 |
+
def copy_and_update(self, **new_column_by_column_name: np.ndarray) -> Self:
|
| 386 |
+
"""Returns a copy of this table with the specified changes applied.
|
| 387 |
+
|
| 388 |
+
Args:
|
| 389 |
+
**new_column_by_column_name: New values for the specified columns.
|
| 390 |
+
|
| 391 |
+
Raises:
|
| 392 |
+
ValueError: If a specified column name is not a column in this table.
|
| 393 |
+
"""
|
| 394 |
+
self._validate_keys_are_column_names(new_column_by_column_name)
|
| 395 |
+
return dataclasses.replace(self, **new_column_by_column_name)
|
| 396 |
+
|
| 397 |
+
def copy_and_remap(
|
| 398 |
+
self, **mapping_by_col: Mapping[TableEntry, TableEntry]
|
| 399 |
+
) -> Self:
|
| 400 |
+
"""Returns a copy of the table with the specified columns remapped.
|
| 401 |
+
|
| 402 |
+
Args:
|
| 403 |
+
**mapping_by_col: Each kwarg key should be the name of one of this table's
|
| 404 |
+
columns, and each value should be a mapping. The values in the column
|
| 405 |
+
will be looked up in the mapping and replaced with the result if one is
|
| 406 |
+
found.
|
| 407 |
+
|
| 408 |
+
Raises:
|
| 409 |
+
ValueError: If a specified column name is not a column in this table.
|
| 410 |
+
"""
|
| 411 |
+
self._validate_keys_are_column_names(mapping_by_col)
|
| 412 |
+
if not self.size:
|
| 413 |
+
return self
|
| 414 |
+
remapped_cols = {}
|
| 415 |
+
for column_name, mapping in mapping_by_col.items():
|
| 416 |
+
col_arr = self.get_column(column_name)
|
| 417 |
+
if col_arr.dtype == object:
|
| 418 |
+
remapped = string_array.remap(col_arr, mapping)
|
| 419 |
+
else:
|
| 420 |
+
remapped = np.vectorize(lambda x: mapping.get(x, x))(col_arr) # pylint: disable=cell-var-from-loop
|
| 421 |
+
remapped_cols[column_name] = remapped
|
| 422 |
+
return self.copy_and_update(**remapped_cols)
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
class RenamedTableView:
|
| 426 |
+
"""View of a table with renamed column names."""
|
| 427 |
+
|
| 428 |
+
def __init__(self, table: Table, column_name_map: Mapping[str, str]):
|
| 429 |
+
self._table = table
|
| 430 |
+
self._column_name_map = column_name_map
|
| 431 |
+
|
| 432 |
+
def get_row_by_key(
|
| 433 |
+
self,
|
| 434 |
+
key: int,
|
| 435 |
+
column_name_map: Mapping[str, str] | None = None,
|
| 436 |
+
) -> Mapping[str, Any]:
|
| 437 |
+
del column_name_map
|
| 438 |
+
return self._table.get_row_by_key(
|
| 439 |
+
key, column_name_map=self._column_name_map
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
_DatabaseT = TypeVar('_DatabaseT', bound='Database')
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
class Database(abc.ABC):
|
| 447 |
+
"""Relational database base class."""
|
| 448 |
+
|
| 449 |
+
@property
|
| 450 |
+
@abc.abstractmethod
|
| 451 |
+
def tables(self) -> Collection[str]:
|
| 452 |
+
"""The names of the tables in this database."""
|
| 453 |
+
|
| 454 |
+
@abc.abstractmethod
|
| 455 |
+
def get_table(self, table_name: str) -> Table:
|
| 456 |
+
"""Gets the table with the given name."""
|
| 457 |
+
|
| 458 |
+
@property
|
| 459 |
+
@abc.abstractmethod
|
| 460 |
+
def foreign_keys(self) -> Mapping[str, Collection[tuple[str, str]]]:
|
| 461 |
+
"""Describes the relationship between keys in the database.
|
| 462 |
+
|
| 463 |
+
Returns:
|
| 464 |
+
A map from table names to pairs of `(column_name, foreign_table_name)`
|
| 465 |
+
where `column_name` is a column containing foreign keys in the table named
|
| 466 |
+
by the key, and the `foreign_table_name` is the name of the table that
|
| 467 |
+
those foreign keys refer to.
|
| 468 |
+
"""
|
| 469 |
+
|
| 470 |
+
@abc.abstractmethod
|
| 471 |
+
def copy_and_update(
|
| 472 |
+
self: _DatabaseT,
|
| 473 |
+
**new_field_by_field_name: ...,
|
| 474 |
+
) -> _DatabaseT:
|
| 475 |
+
"""Returns a copy of this database with the specified changes applied."""
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
def table_dependency_order(db: Database) -> Iterable[str]:
|
| 479 |
+
"""Yields the names of the tables in the database in dependency order.
|
| 480 |
+
|
| 481 |
+
This order guarantees that a table appears after all other tables that
|
| 482 |
+
it refers to using foreign keys. Specifically A < B implies that A contains
|
| 483 |
+
no column that refers to B.key as a foreign key.
|
| 484 |
+
|
| 485 |
+
Args:
|
| 486 |
+
db: The database that defines the table names and foreign keys.
|
| 487 |
+
"""
|
| 488 |
+
connections: dict[str, set[str]] = {}
|
| 489 |
+
for table_name in db.tables:
|
| 490 |
+
connection_set = set()
|
| 491 |
+
for _, foreign_table in db.foreign_keys.get(table_name, ()):
|
| 492 |
+
connection_set.add(foreign_table)
|
| 493 |
+
connections[table_name] = connection_set
|
| 494 |
+
yield from graphlib.TopologicalSorter(connections).static_order()
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
def concat_databases(dbs: Sequence[_DatabaseT]) -> _DatabaseT:
|
| 498 |
+
"""Concatenates the tables across a sequence of databases.
|
| 499 |
+
|
| 500 |
+
Args:
|
| 501 |
+
dbs: A non-empty sequence of database instances of the same type.
|
| 502 |
+
|
| 503 |
+
Returns:
|
| 504 |
+
A new database containing the concatenated tables from the input databases.
|
| 505 |
+
|
| 506 |
+
Raises:
|
| 507 |
+
ValueError: If `dbs` is empty or `dbs` contains different Database
|
| 508 |
+
types.
|
| 509 |
+
"""
|
| 510 |
+
if not dbs:
|
| 511 |
+
raise ValueError('Need at least one value to concatenate.')
|
| 512 |
+
distinct_db_types = {type(db) for db in dbs}
|
| 513 |
+
if len(distinct_db_types) > 1:
|
| 514 |
+
raise ValueError(
|
| 515 |
+
f'All `dbs` must be of the same type, got: {distinct_db_types}'
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
first_db, *other_dbs = dbs
|
| 519 |
+
concatted_tables: dict[str, Table] = {}
|
| 520 |
+
key_offsets: dict[str, list[int]] = {}
|
| 521 |
+
for table_name in table_dependency_order(first_db):
|
| 522 |
+
first_table = first_db.get_table(table_name)
|
| 523 |
+
columns: dict[str, list[np.ndarray]] = {
|
| 524 |
+
column_name: [first_table.get_column(column_name)]
|
| 525 |
+
for column_name in first_table.columns
|
| 526 |
+
}
|
| 527 |
+
key_offsets[table_name] = [
|
| 528 |
+
first_table.key.max() + 1 if first_table.size else 0
|
| 529 |
+
]
|
| 530 |
+
|
| 531 |
+
for prev_index, db in enumerate(other_dbs):
|
| 532 |
+
table = db.get_table(table_name)
|
| 533 |
+
for col_name in table.columns:
|
| 534 |
+
columns[col_name].append(table.get_column(col_name))
|
| 535 |
+
key_offset = key_offsets[table_name][prev_index]
|
| 536 |
+
offset_key = table.key + key_offset
|
| 537 |
+
columns['key'][-1] = offset_key
|
| 538 |
+
if table.size:
|
| 539 |
+
key_offsets[table_name].append(offset_key.max() + 1)
|
| 540 |
+
else:
|
| 541 |
+
key_offsets[table_name].append(key_offsets[table_name][prev_index])
|
| 542 |
+
for fkey_col_name, foreign_table_name in first_db.foreign_keys.get(
|
| 543 |
+
table_name, []
|
| 544 |
+
):
|
| 545 |
+
fkey_columns = columns[fkey_col_name]
|
| 546 |
+
fkey_columns[-1] = (
|
| 547 |
+
fkey_columns[-1] + key_offsets[foreign_table_name][prev_index]
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
concatted_columns = {
|
| 551 |
+
column_name: np.concatenate(values, axis=-1)
|
| 552 |
+
for column_name, values in columns.items()
|
| 553 |
+
}
|
| 554 |
+
concatted_tables[table_name] = (type(first_table))(**concatted_columns)
|
| 555 |
+
return first_db.copy_and_update(**concatted_tables)
|
flax_model/alphafold3/structure/test_utils.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
"""Utilities for structure module testing."""
|
| 4 |
+
|
| 5 |
+
import dataclasses
|
| 6 |
+
|
| 7 |
+
from absl.testing import parameterized
|
| 8 |
+
from flax_model.alphafold3 import structure
|
| 9 |
+
from flax_model.alphafold3.common.testing import data
|
| 10 |
+
import numpy as np
|
| 11 |
+
import tree
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class StructureTestCase(parameterized.TestCase):
|
| 15 |
+
"""Testing utilities for working with structure.Structure."""
|
| 16 |
+
|
| 17 |
+
def assertAuthorNamingSchemeEqual(self, ans1, ans2): # pylint: disable=invalid-name
|
| 18 |
+
"""Walks naming scheme, making sure all elements are equal."""
|
| 19 |
+
if ans1 is None or ans2 is None:
|
| 20 |
+
self.assertIsNone(ans1)
|
| 21 |
+
self.assertIsNone(ans2)
|
| 22 |
+
return
|
| 23 |
+
flat_ans1 = dict(tree.flatten_with_path(dataclasses.asdict(ans1)))
|
| 24 |
+
flat_ans2 = dict(tree.flatten_with_path(dataclasses.asdict(ans2)))
|
| 25 |
+
for k, v in flat_ans1.items():
|
| 26 |
+
self.assertEqual(v, flat_ans2[k], msg=str(k))
|
| 27 |
+
for k, v in flat_ans2.items():
|
| 28 |
+
self.assertEqual(v, flat_ans1[k], msg=str(k))
|
| 29 |
+
|
| 30 |
+
def assertAllResiduesEqual(self, all_res1, all_res2): # pylint: disable=invalid-name
|
| 31 |
+
"""Walks all residues, making sure alll elements are equal."""
|
| 32 |
+
if all_res1 is None or all_res2 is None:
|
| 33 |
+
self.assertIsNone(all_res1)
|
| 34 |
+
self.assertIsNone(all_res2)
|
| 35 |
+
return
|
| 36 |
+
self.assertSameElements(all_res1.keys(), all_res2.keys())
|
| 37 |
+
for chain_id, chain_res in all_res1.items():
|
| 38 |
+
self.assertSequenceEqual(chain_res, all_res2[chain_id], msg=chain_id)
|
| 39 |
+
|
| 40 |
+
def assertBioassemblyDataEqual(self, data1, data2): # pylint: disable=invalid-name
|
| 41 |
+
if data1 is None or data2 is None:
|
| 42 |
+
self.assertIsNone(data1)
|
| 43 |
+
self.assertIsNone(data2)
|
| 44 |
+
return
|
| 45 |
+
self.assertDictEqual(data1.to_mmcif_dict(), data2.to_mmcif_dict())
|
| 46 |
+
|
| 47 |
+
def assertChemicalComponentsDataEqual( # pylint: disable=invalid-name
|
| 48 |
+
self,
|
| 49 |
+
data1,
|
| 50 |
+
data2,
|
| 51 |
+
allow_chem_comp_data_extension,
|
| 52 |
+
):
|
| 53 |
+
"""Checks whether two ChemicalComponentData objects are considered equal."""
|
| 54 |
+
if data1 is None or data2 is None:
|
| 55 |
+
self.assertIsNone(data1)
|
| 56 |
+
self.assertIsNone(data2)
|
| 57 |
+
return
|
| 58 |
+
if (not allow_chem_comp_data_extension) or (
|
| 59 |
+
data1.chem_comp.keys() ^ data2.chem_comp.keys()
|
| 60 |
+
):
|
| 61 |
+
self.assertDictEqual(data1.chem_comp, data2.chem_comp)
|
| 62 |
+
else:
|
| 63 |
+
mismatching_values = []
|
| 64 |
+
for component_id in data1.chem_comp:
|
| 65 |
+
found = data1.chem_comp[component_id]
|
| 66 |
+
expected = data2.chem_comp[component_id]
|
| 67 |
+
if not found.extends(expected):
|
| 68 |
+
mismatching_values.append((component_id, expected, found))
|
| 69 |
+
|
| 70 |
+
if mismatching_values:
|
| 71 |
+
mismatch_err_msgs = '\n'.join(
|
| 72 |
+
f'{component_id}: {expected} or its extension expected,'
|
| 73 |
+
f' but {found} found.'
|
| 74 |
+
for component_id, expected, found in mismatching_values
|
| 75 |
+
)
|
| 76 |
+
self.fail(
|
| 77 |
+
f'Mismatching values for `_chem_comp` table: {mismatch_err_msgs}',
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
def assertBondsEqual(self, bonds1, bonds2, atom_key1, atom_key2): # pylint: disable=invalid-name
|
| 81 |
+
"""Checks whether two Bonds objects are considered equal."""
|
| 82 |
+
# An empty bonds table is functionally equivalent to an empty bonds table.
|
| 83 |
+
# NB: this can only ever be None in structure v1.
|
| 84 |
+
if bonds1 is None or not bonds1.size or bonds2 is None or not bonds2.size:
|
| 85 |
+
self.assertTrue(bonds1 is None or not bonds1.size, msg=f'{bonds1=}')
|
| 86 |
+
self.assertTrue(bonds2 is None or not bonds2.size, msg=f'{bonds2=}')
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
+
ptnr1_indices1, ptnr2_indices1 = bonds1.get_atom_indices(atom_key1)
|
| 90 |
+
ptnr1_indices2, ptnr2_indices2 = bonds2.get_atom_indices(atom_key2)
|
| 91 |
+
np.testing.assert_array_equal(ptnr1_indices1, ptnr1_indices2)
|
| 92 |
+
np.testing.assert_array_equal(ptnr2_indices1, ptnr2_indices2)
|
| 93 |
+
np.testing.assert_array_equal(bonds1.type, bonds2.type)
|
| 94 |
+
np.testing.assert_array_equal(bonds1.role, bonds2.role)
|
| 95 |
+
|
| 96 |
+
def assertStructuresEqual( # pylint: disable=invalid-name
|
| 97 |
+
self,
|
| 98 |
+
struc1,
|
| 99 |
+
struc2,
|
| 100 |
+
*,
|
| 101 |
+
ignore_fields=None,
|
| 102 |
+
allow_chem_comp_data_extension=False,
|
| 103 |
+
atol=0,
|
| 104 |
+
):
|
| 105 |
+
"""Checks whether two Structure objects could be considered equal.
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
struc1: First Structure object.
|
| 109 |
+
struc2: Second Structure object.
|
| 110 |
+
ignore_fields: Fields not taken into account during comparison.
|
| 111 |
+
allow_chem_comp_data_extension: Whether to allow data of `_chem_comp`
|
| 112 |
+
table to differ if `struc2` is missing some fields, but `struc1` has
|
| 113 |
+
specific values for them.
|
| 114 |
+
atol: Absolute tolerance for floating point comparisons (in
|
| 115 |
+
np.testing.assert_allclose).
|
| 116 |
+
"""
|
| 117 |
+
for field in sorted(structure.GLOBAL_FIELDS):
|
| 118 |
+
if ignore_fields and field in ignore_fields:
|
| 119 |
+
continue
|
| 120 |
+
if field == 'author_naming_scheme':
|
| 121 |
+
self.assertAuthorNamingSchemeEqual(struc1[field], struc2[field])
|
| 122 |
+
elif field == 'all_residues':
|
| 123 |
+
self.assertAllResiduesEqual(struc1[field], struc2[field])
|
| 124 |
+
elif field == 'bioassembly_data':
|
| 125 |
+
self.assertBioassemblyDataEqual(struc1[field], struc2[field])
|
| 126 |
+
elif field == 'chemical_components_data':
|
| 127 |
+
self.assertChemicalComponentsDataEqual(
|
| 128 |
+
struc1[field], struc2[field], allow_chem_comp_data_extension
|
| 129 |
+
)
|
| 130 |
+
elif field == 'bonds':
|
| 131 |
+
self.assertBondsEqual(
|
| 132 |
+
struc1.bonds, struc2.bonds, struc1.atom_key, struc2.atom_key
|
| 133 |
+
)
|
| 134 |
+
else:
|
| 135 |
+
self.assertEqual(struc1[field], struc2[field], msg=field)
|
| 136 |
+
|
| 137 |
+
# The chain order within a structure is arbitrary so in order to
|
| 138 |
+
# directly compare arrays we first align struc1 to struc2 and check that
|
| 139 |
+
# the number of atoms doesn't change.
|
| 140 |
+
num_atoms = struc1.num_atoms
|
| 141 |
+
self.assertEqual(struc2.num_atoms, num_atoms)
|
| 142 |
+
struc1 = struc1.order_and_drop_atoms_to_match(struc2)
|
| 143 |
+
self.assertEqual(struc1.num_atoms, num_atoms)
|
| 144 |
+
|
| 145 |
+
for field in sorted(structure.ARRAY_FIELDS):
|
| 146 |
+
if field == 'atom_key':
|
| 147 |
+
# atom_key has no external meaning, so it doesn't matter whether it
|
| 148 |
+
# differs between two structures.
|
| 149 |
+
continue
|
| 150 |
+
if ignore_fields and field in ignore_fields:
|
| 151 |
+
continue
|
| 152 |
+
self.assertEqual(struc1[field] is None, struc2[field] is None, msg=field)
|
| 153 |
+
|
| 154 |
+
if np.issubdtype(struc1[field].dtype, np.inexact):
|
| 155 |
+
np.testing.assert_allclose(
|
| 156 |
+
struc1[field], struc2[field], err_msg=field, atol=atol
|
| 157 |
+
)
|
| 158 |
+
else:
|
| 159 |
+
np.testing.assert_array_equal(
|
| 160 |
+
struc1[field], struc2[field], err_msg=field
|
| 161 |
+
)
|
flax_model/alphafold3/version.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited
|
| 2 |
+
#
|
| 3 |
+
# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of
|
| 4 |
+
# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
|
| 5 |
+
#
|
| 6 |
+
# To request access to the AlphaFold 3 model parameters, follow the process set
|
| 7 |
+
# out at https://github.com/google-deepmind/alphafold3. You may only use these
|
| 8 |
+
# if received directly from Google. Use is subject to terms of use available at
|
| 9 |
+
# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md
|
| 10 |
+
|
| 11 |
+
"""Single source of truth for the AlphaFold version."""
|
| 12 |
+
|
| 13 |
+
__version__ = '3.0.1'
|