wuxing0105 commited on
Commit
40d185e
·
verified ·
1 Parent(s): 896ff69

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. .gitattributes +63 -13
  2. LICENSE +10 -0
  3. README.md +277 -0
  4. configuration.json +1 -0
  5. flax_model/__init__.py +1 -0
  6. flax_model/alphafold3/CMakeLists.txt +161 -0
  7. flax_model/alphafold3/README.md +81 -0
  8. flax_model/alphafold3/__init__.py +26 -0
  9. flax_model/alphafold3/_build.py +649 -0
  10. flax_model/alphafold3/build_data.py +51 -0
  11. flax_model/alphafold3/build_extension.py +19 -0
  12. flax_model/alphafold3/common/__init__.py +0 -0
  13. flax_model/alphafold3/common/base_config.py +141 -0
  14. flax_model/alphafold3/common/folding_input.py +1528 -0
  15. flax_model/alphafold3/common/resources.py +68 -0
  16. flax_model/alphafold3/common/testing/__init__.py +0 -0
  17. flax_model/alphafold3/common/testing/data.py +61 -0
  18. flax_model/alphafold3/constants/__init__.py +0 -0
  19. flax_model/alphafold3/constants/atom_types.py +253 -0
  20. flax_model/alphafold3/constants/chemical_component_sets.py +29 -0
  21. flax_model/alphafold3/constants/chemical_components.py +190 -0
  22. flax_model/alphafold3/constants/converters/__init__.py +0 -0
  23. flax_model/alphafold3/constants/converters/ccd_pickle_gen.py +42 -0
  24. flax_model/alphafold3/constants/converters/chemical_component_sets_gen.py +72 -0
  25. flax_model/alphafold3/constants/mmcif_names.py +208 -0
  26. flax_model/alphafold3/constants/periodic_table.py +385 -0
  27. flax_model/alphafold3/constants/residue_names.py +411 -0
  28. flax_model/alphafold3/constants/side_chains.py +103 -0
  29. flax_model/alphafold3/cpp.cc +45 -0
  30. flax_model/alphafold3/jackhmmer_seq_limit.patch +32 -0
  31. flax_model/alphafold3/package_config.py +68 -0
  32. flax_model/alphafold3/structure/__init__.py +38 -0
  33. flax_model/alphafold3/structure/bioassemblies.py +319 -0
  34. flax_model/alphafold3/structure/bonds.py +227 -0
  35. flax_model/alphafold3/structure/chemical_components.py +276 -0
  36. flax_model/alphafold3/structure/cpp/__init__.py +0 -0
  37. flax_model/alphafold3/structure/cpp/aggregation.pyi +5 -0
  38. flax_model/alphafold3/structure/cpp/aggregation_pybind.cc +54 -0
  39. flax_model/alphafold3/structure/cpp/aggregation_pybind.h +24 -0
  40. flax_model/alphafold3/structure/cpp/mmcif_atom_site.pyi +15 -0
  41. flax_model/alphafold3/structure/cpp/mmcif_struct_conn.pyi +5 -0
  42. flax_model/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc +380 -0
  43. flax_model/alphafold3/structure/mmcif.py +321 -0
  44. flax_model/alphafold3/structure/parsing.py +1801 -0
  45. flax_model/alphafold3/structure/sterics.py +132 -0
  46. flax_model/alphafold3/structure/structure.py +0 -0
  47. flax_model/alphafold3/structure/structure_tables.py +811 -0
  48. flax_model/alphafold3/structure/table.py +555 -0
  49. flax_model/alphafold3/structure/test_utils.py +161 -0
  50. 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
- *.bin filter=lfs diff=lfs merge=lfs -text
 
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
- *.gz filter=lfs diff=lfs merge=lfs -text
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
- *.tar.* filter=lfs diff=lfs merge=lfs -text
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
- *.zip filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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'