From ea0509a449a47f5872e766f2c53da466dd74e268 Mon Sep 17 00:00:00 2001 From: zhaopengnian2025 <13566980606@163.com> Date: Tue, 9 Dec 2025 23:52:26 +0900 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=BC=80=E6=BA=90=E5=AE=9E=E4=B9=A0?= =?UTF-8?q?=E3=80=91=E6=9B=B4=E6=96=B0dft=E4=B8=AD=E7=9A=84README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jenkins/check/config/filter_cppcheck.txt | 11 - .jenkins/check/config/filter_cpplint.txt | 21 - .jenkins/check/config/filter_linklint.txt | 4 +- .jenkins/check/config/filter_pylint.txt | 137 - .jenkins/check/config/whitelizard.txt | 34 - MindChem/applications/diffcsp/README.md | 13 +- MindChem/applications/diffcsp/README_EN.md | 6 +- .../applications/diffcsp/data/__init__.py | 0 .../applications/diffcsp/data/crysloader.py | 3 +- .../diffcsp/mindchemistry/__init__.py | 59 + .../mindchemistry}/graph/__init__.py | 0 .../graph}/dataloader.py | 0 .../{models => mindchemistry/graph}/graph.py | 0 .../applications/diffcsp/models/__init__.py | 0 .../applications/diffcsp/models/cspnet.py | 12 +- .../applications/diffcsp/models/diffusion.py | 6 +- MindChem/applications/diffcsp/train.py | 4 +- MindChem/applications/matformer/README.md | 16 +- MindChem/applications/matformer/README_CN.md | 16 +- MindChem/applications/matformer/config.yaml | 2 +- .../applications/matformer/data/__init__.py | 22 - .../matformer/matformer_application.ipynb | 312 +- .../matformer/matformer_application_EN.ipynb | 10 +- .../matformer/mindchemistry/__init__.py | 64 + .../matformer/mindchemistry/cell/__init__.py | 28 +- .../mindchemistry/cell/activation.py | 38 + .../mindchemistry/cell/basic_block.py | 600 ++++ .../mindchemistry/cell}/convolution.py | 2 +- .../mindchemistry/cell}/embedding.py | 0 .../mindchemistry/cell/matformer/__init__.py | 19 + .../cell/matformer}/matformer.py | 6 +- .../cell/matformer}/transformer.py | 4 +- .../cell/matformer}/utils.py | 0 .../mindchemistry/cell/message_passing.py | 165 + .../mindchemistry/cell}/nequip.py | 2 +- .../matformer/mindchemistry/graph/__init__.py | 15 + .../graph/dataloader.py | 0 .../{models => mindchemistry}/graph/graph.py | 0 .../mindchemistry/graph}/loss.py | 0 .../graph/normlization.py | 0 .../mindchemistry/so2_conv/__init__.py | 19 + .../so2_conv/init_edge_rot_mat.py | 64 + .../matformer/mindchemistry/so2_conv/jd.pkl | Bin 0 -> 9925 bytes .../matformer/mindchemistry/so2_conv/so2.py | 244 ++ .../matformer/mindchemistry/so2_conv/so3.py | 156 + .../mindchemistry/so2_conv/wigner.py | 61 + .../matformer/mindchemistry/utils/__init__.py | 18 + .../mindchemistry/utils/check_func.py | 128 + .../mindchemistry/utils}/load_config.py | 2 +- .../applications/matformer/models/__init__.py | 20 - MindChem/applications/matformer/predict.py | 8 +- MindChem/applications/matformer/train.py | 8 +- .../nequip/mindchemistry/__init__.py | 64 + .../nequip/mindchemistry/cell/__init__.py | 44 +- .../nequip/mindchemistry/cell/activation.py | 38 + .../nequip/mindchemistry/cell/basic_block.py | 600 ++++ .../nequip/mindchemistry/cell/convolution.py | 120 + .../nequip/mindchemistry/cell/embedding.py | 146 + .../mindchemistry/cell/matformer/__init__.py | 19 + .../mindchemistry/cell/matformer/matformer.py | 101 + .../cell/matformer/transformer.py | 158 + .../cell/matformer}/utils.py | 0 .../cell}/message_passing.py | 0 .../nequip/mindchemistry/cell/nequip.py | 141 + .../nequip/mindchemistry/graph/__init__.py | 15 + .../nequip/mindchemistry/graph/dataloader.py | 408 +++ .../{models => mindchemistry/graph}/graph.py | 0 .../mindchemistry}/graph/loss.py | 0 .../mindchemistry/graph/normlization.py | 278 ++ .../nequip/mindchemistry/so2_conv/__init__.py | 19 + .../so2_conv/init_edge_rot_mat.py | 64 + .../nequip/mindchemistry/so2_conv/jd.pkl | Bin 0 -> 9925 bytes .../nequip/mindchemistry/so2_conv/so2.py | 260 ++ .../nequip/mindchemistry/so2_conv/so3.py | 156 + .../nequip/mindchemistry/so2_conv/wigner.py | 61 + .../nequip/mindchemistry/utils/__init__.py | 18 + .../nequip/mindchemistry/utils/check_func.py | 128 + .../nequip/mindchemistry/utils/load_config.py | 85 + .../applications/nequip/models/__init__.py | 6 - MindChem/applications/nequip/nequip.ipynb | 140 +- MindChem/applications/nequip/nequip_en.ipynb | 4 +- MindChem/applications/nequip/predict.py | 2 +- MindChem/applications/nequip/rmd.yaml | 2 +- MindChem/applications/nequip/src/predicter.py | 2 +- MindChem/applications/nequip/src/trainer.py | 2 +- MindChem/applications/nequip/train.py | 2 +- MindChem/applications/orb/README.md | 64 +- MindChem/applications/orb/README_CN.md | 64 +- .../applications/orb/configs/config_eval.yaml | 2 +- .../orb/configs/config_parallel.yaml | 2 +- .../orb/mindchemistry/__init__.py | 66 + .../orb/mindchemistry/cell/__init__.py | 22 + .../orb/mindchemistry/cell/activation.py | 38 + .../orb/mindchemistry/cell/basic_block.py | 600 ++++ .../orb/mindchemistry/cell/convolution.py | 120 + .../orb/mindchemistry/cell/embedding.py | 146 + .../orb/mindchemistry/cell/message_passing.py | 166 + .../cell/orb}/__init__.py | 0 .../{models => mindchemistry/cell/orb}/gns.py | 2 +- .../{models => mindchemistry/cell/orb}/orb.py | 7 +- .../cell/orb}/utils.py | 0 .../orb/mindchemistry/graph/__init__.py | 15 + .../orb/mindchemistry/graph/dataloader.py | 408 +++ .../orb/mindchemistry/graph/graph.py | 294 ++ .../orb/mindchemistry/graph/loss.py | 78 + .../orb/mindchemistry/graph/normlization.py | 278 ++ .../orb/mindchemistry/so2_conv/__init__.py | 19 + .../so2_conv/init_edge_rot_mat.py | 64 + .../orb/mindchemistry/so2_conv/jd.pkl | Bin 0 -> 9925 bytes .../orb/mindchemistry/so2_conv/so2.py | 260 ++ .../orb/mindchemistry/so2_conv/so3.py | 156 + .../orb/mindchemistry/so2_conv/wigner.py | 61 + .../orb/mindchemistry/utils/__init__.py | 18 + .../orb/mindchemistry/utils/check_func.py | 128 + .../orb/mindchemistry/utils/load_config.py | 85 + MindChem/applications/orb/src/pretrained.py | 2 +- MindSPONGE/README.md | 2 +- MindSPONGE/README_en.md | 2 +- .../applications/AlphaFold3/CMakeLists.txt | 95 - MindSPONGE/applications/AlphaFold3/LICENSE | 437 --- MindSPONGE/applications/AlphaFold3/README.md | 267 -- .../applications/AlphaFold3/README_EN.md | 268 -- .../AlphaFold3/alphafold3/__init__.py | 0 .../AlphaFold3/alphafold3/build_data.py | 45 - .../alphafold3/common/base_config.py | 151 - .../alphafold3/common/folding_input.py | 1115 ------ .../AlphaFold3/alphafold3/common/resources.py | 77 - .../alphafold3/common/testing/data.py | 71 - .../alphafold3/constants/atom_types.py | 262 -- .../constants/chemical_component_sets.py | 39 - .../constants/chemical_components.py | 193 - .../constants/converters/ccd_pickle_gen.py | 52 - .../converters/chemical_component_sets_gen.py | 81 - .../alphafold3/constants/mmcif_names.py | 219 -- .../alphafold3/constants/periodic_table.py | 399 --- .../alphafold3/constants/residue_names.py | 421 --- .../alphafold3/constants/side_chains.py | 112 - .../applications/AlphaFold3/alphafold3/cpp.cc | 48 - .../alphafold3/data/cpp/msa_profile_pybind.cc | 79 - .../alphafold3/data/cpp/msa_profile_pybind.h | 25 - .../alphafold3/data/featurisation.py | 91 - .../AlphaFold3/alphafold3/data/msa.py | 346 -- .../AlphaFold3/alphafold3/data/msa_config.py | 170 - .../alphafold3/data/msa_features.py | 204 -- .../alphafold3/data/msa_identifiers.py | 87 - .../AlphaFold3/alphafold3/data/msa_store.py | 67 - .../AlphaFold3/alphafold3/data/parsers.py | 179 - .../AlphaFold3/alphafold3/data/pipeline.py | 543 --- .../alphafold3/data/structure_stores.py | 101 - .../alphafold3/data/template_realign.py | 170 - .../alphafold3/data/template_store.py | 47 - .../AlphaFold3/alphafold3/data/templates.py | 969 ----- .../alphafold3/data/tools/hmmalign.py | 145 - .../alphafold3/data/tools/hmmbuild.py | 148 - .../alphafold3/data/tools/hmmsearch.py | 153 - .../alphafold3/data/tools/jackhmmer.py | 138 - .../alphafold3/data/tools/msa_tool.py | 31 - .../alphafold3/data/tools/nhmmer.py | 175 - .../alphafold3/data/tools/rdkit_utils.py | 524 --- .../alphafold3/data/tools/subprocess_utils.py | 108 - .../model/atom_layout/atom_layout.py | 1191 ------ .../alphafold3/model/base_config.py | 153 - .../alphafold3/model/components/base_model.py | 51 - .../model/components/base_modules.py | 149 - .../alphafold3/model/components/mapping.py | 355 -- .../alphafold3/model/components/utils.py | 59 - .../alphafold3/model/confidence_types.py | 310 -- .../alphafold3/model/confidences.py | 660 ---- .../AlphaFold3/alphafold3/model/data3.py | 127 - .../alphafold3/model/data_constants.py | 27 - .../model/diffusion/atom_cross_attention.py | 469 --- .../model/diffusion/confidence_head.py | 293 -- .../model/diffusion/diffusion_head.py | 336 -- .../model/diffusion/diffusion_transformer.py | 520 --- .../model/diffusion/distogram_head.py | 90 - .../model/diffusion/featurization.py | 218 -- .../alphafold3/model/diffusion/load_ckpt.py | 611 ---- .../alphafold3/model/diffusion/model.py | 762 ---- .../alphafold3/model/diffusion/modules.py | 560 --- .../model/diffusion/template_modules.py | 327 -- .../alphafold3/model/diffusion/triangle.py | 231 -- .../AlphaFold3/alphafold3/model/feat_batch.py | 184 - .../AlphaFold3/alphafold3/model/features.py | 2081 ----------- .../AlphaFold3/alphafold3/model/load_batch.py | 22 - .../alphafold3/model/merging_features.py | 92 - .../alphafold3/model/mkdssp_pybind.cc | 63 - .../alphafold3/model/mkdssp_pybind.h | 26 - .../alphafold3/model/mmcif_metadata.py | 202 -- .../alphafold3/model/model_config.py | 30 - .../alphafold3/model/msa_pairing.py | 314 -- .../AlphaFold3/alphafold3/model/params.py | 218 -- .../model/pipeline/inter_chain_bonds.py | 348 -- .../alphafold3/model/pipeline/pipeline.py | 446 --- .../model/pipeline/structure_cleaning.py | 370 -- .../alphafold3/model/post_processing.py | 115 - .../model/protein_data_processing.py | 128 - .../alphafold3/model/scoring/alignment.py | 143 - .../model/scoring/covalent_bond_cleaning.py | 264 -- .../alphafold3/model/scoring/scoring.py | 68 - .../alphafold3/parsers/cpp/cif_dict.pyi | 125 - .../alphafold3/parsers/cpp/cif_dict_lib.cc | 648 ---- .../alphafold3/parsers/cpp/cif_dict_lib.h | 149 - .../alphafold3/parsers/cpp/cif_dict_pybind.cc | 652 ---- .../alphafold3/parsers/cpp/cif_dict_pybind.h | 24 - .../alphafold3/parsers/cpp/fasta_iterator.pyi | 22 - .../parsers/cpp/fasta_iterator_lib.cc | 121 - .../parsers/cpp/fasta_iterator_lib.h | 94 - .../parsers/cpp/fasta_iterator_pybind.cc | 127 - .../parsers/cpp/fasta_iterator_pybind.h | 24 - .../alphafold3/parsers/cpp/msa_conversion.pyi | 26 - .../parsers/cpp/msa_conversion_pybind.cc | 162 - .../parsers/cpp/msa_conversion_pybind.h | 24 - .../alphafold3/structure/__init__.py | 46 - .../alphafold3/structure/bioassemblies.py | 333 -- .../AlphaFold3/alphafold3/structure/bonds.py | 231 -- .../structure/chemical_components.py | 287 -- .../alphafold3/structure/cpp/aggregation.pyi | 13 - .../structure/cpp/aggregation_pybind.cc | 54 - .../structure/cpp/aggregation_pybind.h | 24 - .../alphafold3/structure/cpp/membership.pyi | 18 - .../structure/cpp/membership_pybind.cc | 82 - .../structure/cpp/membership_pybind.h | 24 - .../alphafold3/structure/cpp/mmcif_altlocs.cc | 249 -- .../alphafold3/structure/cpp/mmcif_altlocs.h | 51 - .../structure/cpp/mmcif_atom_site.pyi | 23 - .../structure/cpp/mmcif_atom_site_pybind.cc | 83 - .../structure/cpp/mmcif_atom_site_pybind.h | 24 - .../alphafold3/structure/cpp/mmcif_layout.h | 146 - .../alphafold3/structure/cpp/mmcif_layout.pyi | 26 - .../structure/cpp/mmcif_layout_lib.cc | 213 -- .../structure/cpp/mmcif_layout_pybind.cc | 49 - .../structure/cpp/mmcif_layout_pybind.h | 24 - .../structure/cpp/mmcif_struct_conn.h | 34 - .../structure/cpp/mmcif_struct_conn.pyi | 13 - .../structure/cpp/mmcif_struct_conn_lib.cc | 380 -- .../structure/cpp/mmcif_struct_conn_pybind.cc | 68 - .../structure/cpp/mmcif_struct_conn_pybind.h | 24 - .../alphafold3/structure/cpp/mmcif_utils.pyi | 71 - .../structure/cpp/mmcif_utils_pybind.cc | 787 ---- .../structure/cpp/mmcif_utils_pybind.h | 24 - .../alphafold3/structure/cpp/string_array.pyi | 50 - .../structure/cpp/string_array_pybind.cc | 329 -- .../structure/cpp/string_array_pybind.h | 24 - .../AlphaFold3/alphafold3/structure/mmcif.py | 333 -- .../alphafold3/structure/parsing.py | 1802 ---------- .../alphafold3/structure/sterics.py | 142 - .../alphafold3/structure/structure.py | 3179 ----------------- .../alphafold3/structure/structure_tables.py | 841 ----- .../AlphaFold3/alphafold3/structure/table.py | 565 --- .../AlphaFold3/alphafold3/utils/attention.py | 189 - .../alphafold3/utils/common/precision.py | 91 - .../alphafold3/utils/gated_linear_unit.py | 47 - .../alphafold3/utils/geometry/__init__.py | 30 - .../utils/geometry/rigid_matrix_vector.py | 194 - .../utils/geometry/rotation_matrix.py | 255 -- .../utils/geometry/struct_of_array.py | 229 -- .../alphafold3/utils/geometry/utils.py | 152 - .../alphafold3/utils/geometry/vector.py | 255 -- .../AlphaFold3/example_input.json | 14 - .../AlphaFold3/image/af3_structure.jpg | Bin 1669512 -> 0 bytes .../applications/AlphaFold3/requirements.txt | 6 - .../applications/AlphaFold3/run_alphafold.py | 664 ---- .../applications/AlphaFold3/set_path.sh | 36 - mindscience/common/__init__.py | 4 +- mindscience/common/memory_reduce.py | 44 - mindscience/models/layers/__init__.py | 3 +- mindscience/sciops/dft/README.md | 138 + 267 files changed, 8085 insertions(+), 37206 deletions(-) delete mode 100644 MindChem/applications/diffcsp/data/__init__.py create mode 100644 MindChem/applications/diffcsp/mindchemistry/__init__.py rename MindChem/applications/{matformer/models => diffcsp/mindchemistry}/graph/__init__.py (100%) rename MindChem/applications/diffcsp/{data => mindchemistry/graph}/dataloader.py (100%) rename MindChem/applications/diffcsp/{models => mindchemistry/graph}/graph.py (100%) delete mode 100644 MindChem/applications/diffcsp/models/__init__.py delete mode 100644 MindChem/applications/matformer/data/__init__.py create mode 100644 MindChem/applications/matformer/mindchemistry/__init__.py rename mindscience/common/initializer.py => MindChem/applications/matformer/mindchemistry/cell/__init__.py (45%) create mode 100644 MindChem/applications/matformer/mindchemistry/cell/activation.py create mode 100644 MindChem/applications/matformer/mindchemistry/cell/basic_block.py rename MindChem/applications/{nequip/models => matformer/mindchemistry/cell}/convolution.py (99%) mode change 100755 => 100644 rename MindChem/applications/{nequip/models => matformer/mindchemistry/cell}/embedding.py (100%) mode change 100755 => 100644 create mode 100644 MindChem/applications/matformer/mindchemistry/cell/matformer/__init__.py rename MindChem/applications/matformer/{models => mindchemistry/cell/matformer}/matformer.py (95%) rename MindChem/applications/matformer/{models => mindchemistry/cell/matformer}/transformer.py (97%) rename MindChem/applications/matformer/{models => mindchemistry/cell/matformer}/utils.py (100%) create mode 100644 MindChem/applications/matformer/mindchemistry/cell/message_passing.py rename MindChem/applications/{nequip/models => matformer/mindchemistry/cell}/nequip.py (99%) create mode 100644 MindChem/applications/matformer/mindchemistry/graph/__init__.py rename MindChem/applications/matformer/{models => mindchemistry}/graph/dataloader.py (100%) rename MindChem/applications/matformer/{models => mindchemistry}/graph/graph.py (100%) rename MindChem/applications/{diffcsp/models => matformer/mindchemistry/graph}/loss.py (100%) rename MindChem/applications/matformer/{models => mindchemistry}/graph/normlization.py (100%) create mode 100644 MindChem/applications/matformer/mindchemistry/so2_conv/__init__.py create mode 100644 MindChem/applications/matformer/mindchemistry/so2_conv/init_edge_rot_mat.py create mode 100644 MindChem/applications/matformer/mindchemistry/so2_conv/jd.pkl create mode 100644 MindChem/applications/matformer/mindchemistry/so2_conv/so2.py create mode 100644 MindChem/applications/matformer/mindchemistry/so2_conv/so3.py create mode 100644 MindChem/applications/matformer/mindchemistry/so2_conv/wigner.py create mode 100644 MindChem/applications/matformer/mindchemistry/utils/__init__.py create mode 100644 MindChem/applications/matformer/mindchemistry/utils/check_func.py rename MindChem/applications/{nequip/models => matformer/mindchemistry/utils}/load_config.py (97%) delete mode 100644 MindChem/applications/matformer/models/__init__.py create mode 100644 MindChem/applications/nequip/mindchemistry/__init__.py rename mindscience/models/layers/mask.py => MindChem/applications/nequip/mindchemistry/cell/__init__.py (35%) create mode 100644 MindChem/applications/nequip/mindchemistry/cell/activation.py create mode 100644 MindChem/applications/nequip/mindchemistry/cell/basic_block.py create mode 100644 MindChem/applications/nequip/mindchemistry/cell/convolution.py create mode 100644 MindChem/applications/nequip/mindchemistry/cell/embedding.py create mode 100644 MindChem/applications/nequip/mindchemistry/cell/matformer/__init__.py create mode 100644 MindChem/applications/nequip/mindchemistry/cell/matformer/matformer.py create mode 100644 MindChem/applications/nequip/mindchemistry/cell/matformer/transformer.py rename MindChem/applications/nequip/{models => mindchemistry/cell/matformer}/utils.py (100%) rename MindChem/applications/nequip/{models => mindchemistry/cell}/message_passing.py (100%) mode change 100755 => 100644 create mode 100644 MindChem/applications/nequip/mindchemistry/cell/nequip.py create mode 100644 MindChem/applications/nequip/mindchemistry/graph/__init__.py create mode 100644 MindChem/applications/nequip/mindchemistry/graph/dataloader.py rename MindChem/applications/nequip/{models => mindchemistry/graph}/graph.py (100%) rename MindChem/applications/{matformer/models => nequip/mindchemistry}/graph/loss.py (100%) create mode 100644 MindChem/applications/nequip/mindchemistry/graph/normlization.py create mode 100644 MindChem/applications/nequip/mindchemistry/so2_conv/__init__.py create mode 100644 MindChem/applications/nequip/mindchemistry/so2_conv/init_edge_rot_mat.py create mode 100644 MindChem/applications/nequip/mindchemistry/so2_conv/jd.pkl create mode 100644 MindChem/applications/nequip/mindchemistry/so2_conv/so2.py create mode 100644 MindChem/applications/nequip/mindchemistry/so2_conv/so3.py create mode 100644 MindChem/applications/nequip/mindchemistry/so2_conv/wigner.py create mode 100644 MindChem/applications/nequip/mindchemistry/utils/__init__.py create mode 100644 MindChem/applications/nequip/mindchemistry/utils/check_func.py create mode 100644 MindChem/applications/nequip/mindchemistry/utils/load_config.py delete mode 100644 MindChem/applications/nequip/models/__init__.py create mode 100644 MindChem/applications/orb/mindchemistry/__init__.py create mode 100644 MindChem/applications/orb/mindchemistry/cell/__init__.py create mode 100644 MindChem/applications/orb/mindchemistry/cell/activation.py create mode 100644 MindChem/applications/orb/mindchemistry/cell/basic_block.py create mode 100644 MindChem/applications/orb/mindchemistry/cell/convolution.py create mode 100644 MindChem/applications/orb/mindchemistry/cell/embedding.py create mode 100644 MindChem/applications/orb/mindchemistry/cell/message_passing.py rename MindChem/applications/orb/{models => mindchemistry/cell/orb}/__init__.py (100%) rename MindChem/applications/orb/{models => mindchemistry/cell/orb}/gns.py (99%) rename MindChem/applications/orb/{models => mindchemistry/cell/orb}/orb.py (99%) rename MindChem/applications/orb/{models => mindchemistry/cell/orb}/utils.py (100%) create mode 100644 MindChem/applications/orb/mindchemistry/graph/__init__.py create mode 100644 MindChem/applications/orb/mindchemistry/graph/dataloader.py create mode 100644 MindChem/applications/orb/mindchemistry/graph/graph.py create mode 100644 MindChem/applications/orb/mindchemistry/graph/loss.py create mode 100644 MindChem/applications/orb/mindchemistry/graph/normlization.py create mode 100644 MindChem/applications/orb/mindchemistry/so2_conv/__init__.py create mode 100644 MindChem/applications/orb/mindchemistry/so2_conv/init_edge_rot_mat.py create mode 100644 MindChem/applications/orb/mindchemistry/so2_conv/jd.pkl create mode 100644 MindChem/applications/orb/mindchemistry/so2_conv/so2.py create mode 100644 MindChem/applications/orb/mindchemistry/so2_conv/so3.py create mode 100644 MindChem/applications/orb/mindchemistry/so2_conv/wigner.py create mode 100644 MindChem/applications/orb/mindchemistry/utils/__init__.py create mode 100644 MindChem/applications/orb/mindchemistry/utils/check_func.py create mode 100644 MindChem/applications/orb/mindchemistry/utils/load_config.py delete mode 100644 MindSPONGE/applications/AlphaFold3/CMakeLists.txt delete mode 100644 MindSPONGE/applications/AlphaFold3/LICENSE delete mode 100644 MindSPONGE/applications/AlphaFold3/README.md delete mode 100644 MindSPONGE/applications/AlphaFold3/README_EN.md delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/__init__.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/build_data.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/common/base_config.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/common/folding_input.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/common/resources.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/common/testing/data.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/constants/atom_types.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_component_sets.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_components.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/constants/converters/ccd_pickle_gen.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/constants/converters/chemical_component_sets_gen.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/constants/mmcif_names.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/constants/periodic_table.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/constants/residue_names.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/constants/side_chains.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/cpp.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/cpp/msa_profile_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/cpp/msa_profile_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/featurisation.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/msa.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_config.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_features.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_identifiers.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_store.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/parsers.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/pipeline.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/structure_stores.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/template_realign.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/template_store.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/templates.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmalign.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmbuild.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmsearch.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/jackhmmer.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/msa_tool.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/nhmmer.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/rdkit_utils.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/subprocess_utils.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/atom_layout/atom_layout.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/base_config.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_model.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_modules.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/components/utils.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/confidence_types.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/confidences.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/data3.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/data_constants.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/atom_cross_attention.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/confidence_head.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_head.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_transformer.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/distogram_head.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/featurization.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/load_ckpt.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/model.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/modules.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/template_modules.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/triangle.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/feat_batch.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/features.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/load_batch.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/merging_features.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/mmcif_metadata.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/model_config.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/msa_pairing.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/params.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/inter_chain_bonds.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/pipeline.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/structure_cleaning.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/post_processing.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/protein_data_processing.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/alignment.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/covalent_bond_cleaning.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/scoring.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict.pyi delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator.pyi delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_lib.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_lib.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion.pyi delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/__init__.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/bioassemblies.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/bonds.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/chemical_components.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation.pyi delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership.pyi delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site.pyi delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout.pyi delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_lib.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn.pyi delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils.pyi delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array.pyi delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.cc delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.h delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/mmcif.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/parsing.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/sterics.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure_tables.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/structure/table.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/utils/attention.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/utils/common/precision.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/utils/gated_linear_unit.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/__init__.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/rigid_matrix_vector.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/rotation_matrix.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/struct_of_array.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/utils.py delete mode 100644 MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/vector.py delete mode 100644 MindSPONGE/applications/AlphaFold3/example_input.json delete mode 100644 MindSPONGE/applications/AlphaFold3/image/af3_structure.jpg delete mode 100644 MindSPONGE/applications/AlphaFold3/requirements.txt delete mode 100644 MindSPONGE/applications/AlphaFold3/run_alphafold.py delete mode 100644 MindSPONGE/applications/AlphaFold3/set_path.sh delete mode 100644 mindscience/common/memory_reduce.py create mode 100644 mindscience/sciops/dft/README.md diff --git a/.jenkins/check/config/filter_cppcheck.txt b/.jenkins/check/config/filter_cppcheck.txt index a156d6c3a..5ceb4144c 100644 --- a/.jenkins/check/config/filter_cppcheck.txt +++ b/.jenkins/check/config/filter_cppcheck.txt @@ -2,14 +2,3 @@ "mindscience/MindElec/mindelec/ccsrc/api/python/pybind_register.cc" "syntaxError" "mindscience/MindElec/mindelec/ccsrc/scientific_compute/pointcloud/material_analyse.cc" "useStlAlgorithm" "mindscience/MindElec/mindelec/ccsrc/scientific_compute/pointcloud/tensor_initializer.cc" "useStlAlgorithm" -#MindSPONGE -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.cc" "shadowFunction" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.cc" "useStlAlgorithm" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion_pybind.cc" "variableScope" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.cc" "shadowVariable" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.cc" "useStlAlgorithm" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc" "unsignedLessThanZero" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.cc" "shadowVariable" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.cc" "useStlAlgorithm" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.cc" "pointerSize" - diff --git a/.jenkins/check/config/filter_cpplint.txt b/.jenkins/check/config/filter_cpplint.txt index e5c33963f..9ceab3d1e 100644 --- a/.jenkins/check/config/filter_cpplint.txt +++ b/.jenkins/check/config/filter_cpplint.txt @@ -597,24 +597,3 @@ "mindscience/MindSPONGE/mindsponge/ccsrc/molecular_dynamics/barostats/MC_barostat.cu" "whitespace/parens" "mindscience/MindSPONGE/mindsponge/ccsrc/molecular_dynamics/thermostats/Andersen_thermostat.cu" "whitespace/parens" "mindscience/MindSPONGE/mindsponge/ccsrc/molecular_dynamics/common.cuh" "build/include_subdir" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc" "whitespace/parens" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc" "runtime/references" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_pybind.cc" "build/include" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.cc" "build/include_order" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.cc" "whitespace/ending_newline" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion_pybind.cc" "build/include" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.cc" "whitespace/braces" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.cc" "whitespace/parens" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.cc" "build/include" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/cpp/msa_profile_pybind.cc" "build/include" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/cpp/msa_profile_pybind.cc" "whitespace/ending_newline" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.cc" "build/include" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site_pybind.cc" "build/include" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_pybind.cc" "build/include" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation_pybind.cc" "build/include" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.cc" "whitespace/braces" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_pybind.cc" "build/include" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership_pybind.cc" "build/include" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.cc" "runtime/references" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.cc" "build/c++17" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.cc" "build/c++17" diff --git a/.jenkins/check/config/filter_linklint.txt b/.jenkins/check/config/filter_linklint.txt index f733aa150..2480046c4 100644 --- a/.jenkins/check/config/filter_linklint.txt +++ b/.jenkins/check/config/filter_linklint.txt @@ -4,6 +4,4 @@ https://api.colabfold.com https://a3m.mmseqs.com https://www.mindspore.cn/community/SIG/detail/?name=mindflow+SIG -https://www.mindspore.cn/sig/MindSpore%20Science -https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Items/_entity.type.html -https://gitee.com/mindspore/mindscience/tree/main/MindSPONGE/applications/AlphaFold3 +https://www.mindspore.cn/sig/MindSpore%20Science \ No newline at end of file diff --git a/.jenkins/check/config/filter_pylint.txt b/.jenkins/check/config/filter_pylint.txt index b84bc056a..3ee231a48 100644 --- a/.jenkins/check/config/filter_pylint.txt +++ b/.jenkins/check/config/filter_pylint.txt @@ -398,143 +398,6 @@ "mindscience/MindSPONGE/applications/rf_diffusion/run_inference.py" "no-value-for-parameter" "mindscience/MindSPONGE/applications/rf_diffusion/rfdiffusion/inference/ab_util.py" "missing-function-docstring" "mindscience/MindSPONGE/applications/rf_diffusion/run_inference.py" "invalid-name" - -# DeepMind AlphaFold3 -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_components.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/nhmmer.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/structure_cleaning.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/mmcif.py" "multiple-statements" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/base_config.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/__init__.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/resources.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/table.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_components.py" "assigning-non-slot" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/post_processing.py" "unused-variable" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmalign.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/jackhmmer.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/post_processing.py" "unexpected-keyword-arg" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/template_store.py" "bad-continuation" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/covalent_bond_cleaning.py" "no-else-return" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/scoring.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/mmcif.py" "bad-continuation" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_features.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_identifiers.py" "no-else-return" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/base_config.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/bonds.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_store.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/testing/data.py" "pointless-statement" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/parsers.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/mmcif.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/template_realign.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/resources.py" "function-redefined" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_components.py" "unexpected-keyword-arg" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/pipeline.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/protein_data_processing.py" "bad-continuation" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/merging_features.py" "no-else-return" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/confidence_types.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/scoring.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/folding_input.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/templates.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/pipeline.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/chemical_components.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/resources.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmsearch.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/featurisation.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/post_processing.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/resources.py" "pointless-statement" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/structure_stores.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_config.py" "unexpected-keyword-arg" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/subprocess_utils.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/mmcif.py" "no-else-return" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/parsing.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/testing/data.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/alignment.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/mmcif.py" "invalid-name" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/test_utils.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/inter_chain_bonds.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/testing/data.py" "function-redefined" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmsearch.py" "bad-continuation" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmsearch.py" "useless-object-inheritance" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/post_processing.py" "bad-continuation" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/constants/periodic_table.py" "unexpected-keyword-arg" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure_tables.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmbuild.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/constants/mmcif_names.py" "no-else-return" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/testing/data.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/msa_tool.py" "unexpected-keyword-arg" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/attention/attention_base.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/struct_of_array.py" "no-else-return" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/struct_of_array.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/feat_batch.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_head.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py" "invalid-name" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "pointless-statement" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/atom_layout/atom_layout.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_modules.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/featurization.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_modules.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_modules.py" "unused-import" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/params.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/atom_cross_attention.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/attention/ms_attention.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/attention/ms_attention.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/attention/attention.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/utils.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/atom_cross_attention.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/common/precision.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/__init__.py" "invalid-name" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/vector.py" "unused-import" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py" "unused-import" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold_data_test.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/struct_of_array.py" "cell-var-from-loop" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/vector.py" "invalid-name" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_head.py" "unused-import" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "unexpected-keyword-arg" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold_test_v2.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/featurization.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "unused-import" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "function-redefined" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py" "len-as-condition" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "unsupported-membership-test" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/featurization.py" "redefined-argument-from-local" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/distogram_head.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "unsupported-assignment-operation" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/struct_of_array.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/__init__.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "multiple-statements" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py" "no-else-return" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "too-many-function-args" -"mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py" "unused-variable" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/features.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/model.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/template_modules.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/confidence_head.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_transformer.py" "syntax-error" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/utils.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/mmcif_metadata.py" "bad-continuation" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/template_modules.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/load_ckpt.py" "unused-import" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/load_ckpt.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/rigid_matrix_vector.py" "bad-whitespace" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/load_ckpt.py" "no-value-for-parameter" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/model.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/modules.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/gated_linear_unit/gated_linear_unit_base.py" "pointless-statement" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/modules.py" "missing-docstring" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/load_ckpt.py" "protected-access" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/model.py" "redefined-argument-from-local" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/triangle.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/utils/gated_linear_unit/gated_linear_unit_base.py" "unused-argument" -"mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/mmcif_metadata.py" "unused-argument" "mindscience/MindSPONGE/applications/proteinmpnn/helper_scripts/assign_fixed_chains.py" "invalid-name" "mindscience/MindSPONGE/applications/proteinmpnn/helper_scripts/make_bias_AA.py" "invalid-name" "mindscience/MindSPONGE/applications/proteinmpnn/helper_scripts/make_fixed_positions_dict.py" "invalid-name" diff --git a/.jenkins/check/config/whitelizard.txt b/.jenkins/check/config/whitelizard.txt index 5b18fd492..7e2fc3c69 100644 --- a/.jenkins/check/config/whitelizard.txt +++ b/.jenkins/check/config/whitelizard.txt @@ -26,40 +26,6 @@ mindscience/MindSPONGE/applications/rf_diffusion/env/se3_transformer/model/layer mindscience/MindSPONGE/applications/rf_diffusion/env/se3_transformer/model/layers/convolution.py:construct mindscience/MindSPONGE/applications/rf_diffusion/run_inference.py:main mindscience/MindSPONGE/applications/rf_diffusion/rfdiffusion/inference/ab_util.py:ab_write_pdblines -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/folding_input.py:from_alphafoldserver_fold_job -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/folding_input.py:from_json -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.cc:alphafold3::GetEscapeQuote -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.cc:alphafold3::CifDict::ToString -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.cc:alphafold3::Gather -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.cc:alphafold3::CifDictGetArray -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.cc:alphafold3::RegisterModuleCifDict -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.cc:alphafold3::FixArginine::Fix -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_lib.cc:alphafold3::MmcifLayout::Create -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc:alphafold3::GetBondAtomIndices -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/pipeline.py:__init__ -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/pipeline.py:process_item -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/common/folding_input.py:from_json -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/structure_cleaning.py:clean_structure -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/mmcif_metadata.py:add_metadata_to_mmcif -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.cc:alphafold3::CifDict::ToString -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.cc:alphafold3::RegisterModuleCifDict -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_lib.cc:alphafold3::MmcifLayout::Create -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc:alphafold3::GetBondAtomIndices -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/parsing.py:from_res_arrays, from_atom_arrays, _maybe_add_missing_scheme_tables, from_sequences_and_bonds -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure.py:filter -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure.py:merge_chains -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure.py:order_and_drop_atoms_to_match -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure_tables.py:to_mmcif_sequence_and_entity_tables -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure_tables.py:tables_from_atom_arrays -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/model.py:get_inference_result -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/features.py:compute_features -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/features.py:get_reference -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/features.py:tokenizer -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/atom_layout/atom_layout.py:residues_from_structure -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/atom_layout/atom_layout.py:make_flat_atom_layout -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/atom_cross_attention.py:construct -mindscience/MindSPONGE/applications/AlphaFold3/alphafold3/data/parsers.py:convert_stockholm_to_a3m -mindscience/MindSPONGE/applications/AlphaFold3/run_alphafold_test_v2.py:test_inference mindscience/MindSPONGE/applications/proteinmpnn/proteinmpnn/util_protein_mpnn.py:ab_write_pdblines mindscience/MindSPONGE/applications/proteinmpnn/proteinmpnn/util_protein_mpnn.py:parse_PDB_biounits mindscience/MindSPONGE/applications/proteinmpnn/proteinmpnn/util_protein_mpnn.py:parse_PDB diff --git a/MindChem/applications/diffcsp/README.md b/MindChem/applications/diffcsp/README.md index ea467c921..858aa417c 100644 --- a/MindChem/applications/diffcsp/README.md +++ b/MindChem/applications/diffcsp/README.md @@ -9,7 +9,7 @@ ## 环境要求 -> 1. 安装`mindspore(2.7.0)` +> 1. 安装`mindspore(2.3.0)` > 2. 安装依赖包:`pip install -r requirement.txt` ## 快速入门 @@ -37,16 +37,12 @@ diffcsp data_utils.py 数据集处理工具 dataset.py 读取数据集 crysloader.py 数据集载入器 - dataloader.py 构建数据集 └─models cspnet.py 基于图神经网络的去噪器模块 diffusion.py 扩散模型模块 diff_utils.py 工具模块 infer_utils.py 推理工具模块 train_utils.py 训练工具模块 - graph.py 工具 - loss.py 损失模块 - ``` @@ -74,10 +70,9 @@ diffcsp 更改config文件,设置训练参数: > 1. 设置训练的dataset,见dataset字段 -> 2. 设置训练的轮次,见epoch_size字段 -> 3. 设置去噪器模型的配置,见model字段 -> 4. 设置训练保存的权重文件,更改train.ckpt_dir文件夹名称和checkpoint.last_path权重文件名称 -> 5. 其它训练设置见train字段 +> 2. 设置去噪器模型的配置,见model字段 +> 3. 设置训练保存的权重文件,更改train.ckpt_dir文件夹名称和checkpoint.last_path权重文件名称 +> 4. 其它训练设置见train字段 ```bash pip install -r requirement.txt diff --git a/MindChem/applications/diffcsp/README_EN.md b/MindChem/applications/diffcsp/README_EN.md index fb92ac6da..24e9d0358 100644 --- a/MindChem/applications/diffcsp/README_EN.md +++ b/MindChem/applications/diffcsp/README_EN.md @@ -8,7 +8,7 @@ DiffCSP is a diffusion-model-based deep learning framework for crystal structure ## Environment Requirements -1. Install `mindspore (2.7.0)` +1. Install `mindspore (2.3.0)` 2. Install dependencies: `pip install -r requirement.txt` ## Quick Start @@ -37,15 +37,12 @@ diffcsp data_utils.py Dataset processing utilities dataset.py Dataset reader crysloader.py Dataset loader - dataloader.py Dataset construction └─models cspnet.py GNN-based denoiser module diffusion.py Diffusion model module diff_utils.py Utilities infer_utils.py Inference utilities train_utils.py Training utilities - graph.py Utilities - loss.py Loss ``` ## Dataset Download @@ -73,7 +70,6 @@ Download the `Mindchemistry/mindchemistry` package to the current directory. Edit the config file to set training parameters: - Set the training dataset (see the `dataset` field). -- To set the training rounds (see the epoch_size field). - Configure the denoiser model (see the `model` field). - Set the directory and filename for saving checkpoints by editing `train.ckpt_dir` and `checkpoint.last_path`. - Other training settings are under the `train` field. diff --git a/MindChem/applications/diffcsp/data/__init__.py b/MindChem/applications/diffcsp/data/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/MindChem/applications/diffcsp/data/crysloader.py b/MindChem/applications/diffcsp/data/crysloader.py index 58128dbd5..198e39958 100644 --- a/MindChem/applications/diffcsp/data/crysloader.py +++ b/MindChem/applications/diffcsp/data/crysloader.py @@ -17,7 +17,7 @@ import numpy as np from mindspore import Tensor, ops import mindspore as ms -from data.dataloader import DataLoaderBase, CommonData +from mindchemistry.graph.dataloader import DataLoaderBase, CommonData class Crysloader(DataLoaderBase): @@ -42,7 +42,6 @@ class Crysloader(DataLoaderBase): shuffle_dataset=True, max_node=None, max_edge=None): - super().__init__(batch_size, node_attr, edge_attr, edge_index) self.batch_size = batch_size self.edge_index = edge_index self.index = 0 diff --git a/MindChem/applications/diffcsp/mindchemistry/__init__.py b/MindChem/applications/diffcsp/mindchemistry/__init__.py new file mode 100644 index 000000000..61282dad2 --- /dev/null +++ b/MindChem/applications/diffcsp/mindchemistry/__init__.py @@ -0,0 +1,59 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Initialization for MindChemistry APIs.""" + +import time +import mindspore as ms +from mindspore import log as logger +from mindscience.e3nn import * +from .graph import * + +__all__ = [] + +def _mindspore_version_check(): + """ + Check MindSpore version for MindChemistry. + + Raises: + ImportError: If MindSpore cannot be imported. + """ + try: + _ = ms.__version__ + except ImportError as exc: + raise ImportError( + "Cannot find MindSpore in the current environment. Please install " + "MindSpore before using MindChemistry, by following the instruction at " + "https://www.mindspore.cn/install" + ) from exc + + ms_version = ms.__version__[:5] + required_mindspore_version = "1.8.1" + + if ms_version < required_mindspore_version: + logger.warning( + f"Current version of MindSpore ({ms_version}) is not compatible with MindChemistry. " + f"Some functions might not work or even raise errors. Please install MindSpore " + f"version >= {required_mindspore_version}. For more details about dependency settings, " + f"please check the instructions at the MindSpore official website " + f"https://www.mindspore.cn/install or check the README.md at " + f"https://gitee.com/mindspore/mindscience" + ) + + for i in range(3, 0, -1): + logger.warning(f"Please pay attention to the above warning, countdown: {i}") + time.sleep(1) + + +_mindspore_version_check() diff --git a/MindChem/applications/matformer/models/graph/__init__.py b/MindChem/applications/diffcsp/mindchemistry/graph/__init__.py similarity index 100% rename from MindChem/applications/matformer/models/graph/__init__.py rename to MindChem/applications/diffcsp/mindchemistry/graph/__init__.py diff --git a/MindChem/applications/diffcsp/data/dataloader.py b/MindChem/applications/diffcsp/mindchemistry/graph/dataloader.py similarity index 100% rename from MindChem/applications/diffcsp/data/dataloader.py rename to MindChem/applications/diffcsp/mindchemistry/graph/dataloader.py diff --git a/MindChem/applications/diffcsp/models/graph.py b/MindChem/applications/diffcsp/mindchemistry/graph/graph.py similarity index 100% rename from MindChem/applications/diffcsp/models/graph.py rename to MindChem/applications/diffcsp/mindchemistry/graph/graph.py diff --git a/MindChem/applications/diffcsp/models/__init__.py b/MindChem/applications/diffcsp/models/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/MindChem/applications/diffcsp/models/cspnet.py b/MindChem/applications/diffcsp/models/cspnet.py index e86792b21..a12731b41 100644 --- a/MindChem/applications/diffcsp/models/cspnet.py +++ b/MindChem/applications/diffcsp/models/cspnet.py @@ -17,8 +17,10 @@ import math import numpy as np import mindspore -from mindspore import Tensor,nn,ops -from models.graph import (AggregateEdgeToNode, +import mindspore.nn as nn +import mindspore.ops as ops +from mindspore import Tensor +from mindchemistry.graph.graph import (AggregateEdgeToNode, AggregateNodeToGlobal, LiftGlobalToNode) MAX_ATOMIC_NUM = 100 @@ -36,7 +38,7 @@ class SinusoidsEmbedding(nn.Cell): """ def __init__(self, n_frequencies=10, n_space=3): - super().__init__() + super(SinusoidsEmbedding, self).__init__() self.n_frequencies = n_frequencies self.n_space = n_space self.frequencies = 2 * math.pi * np.arange(self.n_frequencies) @@ -100,7 +102,7 @@ class CSPLayer(nn.Cell): act_fn (nn): The activation function used in the layer. Defaults to nn.SiLU(). dis_emb (object): The embbing method used for edge features. Defaults to None. """ - super().__init__() + super(CSPLayer, self).__init__() self.dis_dim = 3 self.dis_emb = dis_emb if dis_emb is not None: @@ -207,7 +209,7 @@ class CSPNet(nn.Cell): num_freqs (int): The number of frequencies for Fourier embedding for edge features. Defaults to 128. """ - super().__init__() + super(CSPNet, self).__init__() self.node_embedding = nn.Embedding(max_atoms, hidden_dim) self.atom_latent_emb = nn.Dense(hidden_dim + latent_dim, hidden_dim) self.act_fn = nn.SiLU() diff --git a/MindChem/applications/diffcsp/models/diffusion.py b/MindChem/applications/diffcsp/models/diffusion.py index d4354788a..21577030c 100644 --- a/MindChem/applications/diffcsp/models/diffusion.py +++ b/MindChem/applications/diffcsp/models/diffusion.py @@ -18,7 +18,7 @@ import math import mindspore as ms import mindspore.numpy as mnp from mindspore import nn, ops -from models.graph import (AggregateNodeToGlobal, LiftGlobalToNode) +from mindchemistry.graph.graph import (AggregateNodeToGlobal, LiftGlobalToNode) from models.diff_utils import (BetaScheduler, SigmaScheduler, d_log_p_wrapped_normal_ms) @@ -38,7 +38,7 @@ class SinusoidalTimeEmbeddings(nn.Cell): Referring the implementation details in the paper Attention is all you need. """ def __init__(self, dim): - super().__init__() + super(SinusoidalTimeEmbeddings, self).__init__() self.dim = dim def construct(self, time): @@ -118,7 +118,7 @@ class CSPDiffusion(nn.Cell): sigma_end (float): The ending sigma used in fractiaonal coordinates SDEs. Defaults to 0.5. """ - super().__init__() + super(CSPDiffusion, self).__init__() self.beta_scheduler = BetaScheduler(timesteps=timesteps, scheduler_mode=scheduler_mode) self.sigma_scheduler = SigmaScheduler(timesteps=timesteps, diff --git a/MindChem/applications/diffcsp/train.py b/MindChem/applications/diffcsp/train.py index caaab12b5..fd21606a6 100644 --- a/MindChem/applications/diffcsp/train.py +++ b/MindChem/applications/diffcsp/train.py @@ -22,7 +22,7 @@ import numpy as np import mindspore as ms from mindspore import nn, set_seed from mindspore.amp import all_finite -from models.loss import L2LossMask +from mindchemistry.graph.loss import L2LossMask from models.cspnet import CSPNet from models.diffusion import CSPDiffusion from models.train_utils import LossRecord @@ -46,7 +46,7 @@ def main(): args = parse_args() ms.set_context(device_target=args.device_target, device_id=args.device_id) - with open(args.config, 'r', encoding="utf-8") as stream: + with open(args.config, 'r') as stream: config = yaml.safe_load(stream) ckpt_dir = config['train']["ckpt_dir"] diff --git a/MindChem/applications/matformer/README.md b/MindChem/applications/matformer/README.md index a59999b55..f89604cf2 100644 --- a/MindChem/applications/matformer/README.md +++ b/MindChem/applications/matformer/README.md @@ -152,20 +152,16 @@ The figure below shows the formation energy predictions from a fully trained Mat Log output from `train.py`: ```log -INFO:root:Loading from saved file... INFO:root:The model you built has 2786689 parameters. -INFO:root:load from existing check point................ -INFO:root:finish load from existing checkpoint, start training from epoch: 1 -INFO:root:change learning rate to current step: 953 -INFO:root:current learning rate: 9.345746e-08 -INFO:root:Start to initialise train_loader -INFO:root:Start to initialise eval_loader +INFO:root:Starting new training process +INFO:root:Start to initialise train loader +INFO:root:Start to initialise eval loader INFO:root:+++++++++++++++ start traning +++++++++++++++++++++ INFO:root:==============================step: 0 ,epoch: 0 -INFO:root:learning rate: 9.345746e-08 -INFO:root:train mse loss: 0.09808009 +INFO:root:learning rate: 4e-05 +INFO:root:train mse loss: 0.8999285 INFO:root:is_finite: True -INFO:root:traning time: 22.13266158103943 +INFO:root:training time: 51.66963744163513 . . . diff --git a/MindChem/applications/matformer/README_CN.md b/MindChem/applications/matformer/README_CN.md index 7071e39f0..345fc35f3 100644 --- a/MindChem/applications/matformer/README_CN.md +++ b/MindChem/applications/matformer/README_CN.md @@ -152,20 +152,16 @@ python predict.py `train.py`运行日志如下: ```log -INFO:root:Loading from saved file... INFO:root:The model you built has 2786689 parameters. -INFO:root:load from existing check point................ -INFO:root:finish load from existing checkpoint, start training from epoch: 1 -INFO:root:change learning rate to current step: 953 -INFO:root:current learning rate: 9.345746e-08 -INFO:root:Start to initialise train_loader -INFO:root:Start to initialise eval_loader +INFO:root:Starting new training process +INFO:root:Start to initialise train loader +INFO:root:Start to initialise eval loader INFO:root:+++++++++++++++ start traning +++++++++++++++++++++ INFO:root:==============================step: 0 ,epoch: 0 -INFO:root:learning rate: 9.345746e-08 -INFO:root:train mse loss: 0.09808009 +INFO:root:learning rate: 4e-05 +INFO:root:train mse loss: 0.8999285 INFO:root:is_finite: True -INFO:root:traning time: 22.13266158103943 +INFO:root:training time: 51.66963744163513 . . . diff --git a/MindChem/applications/matformer/config.yaml b/MindChem/applications/matformer/config.yaml index d1389d9a8..d2cb44bac 100644 --- a/MindChem/applications/matformer/config.yaml +++ b/MindChem/applications/matformer/config.yaml @@ -1,6 +1,6 @@ train: device: Ascend - device_id: 1 + device_id: 0 dataset_dir: "./dataset" ckpt_dir: "./ckpt" props: "formation_energy_peratom" diff --git a/MindChem/applications/matformer/data/__init__.py b/MindChem/applications/matformer/data/__init__.py deleted file mode 100644 index b50081797..000000000 --- a/MindChem/applications/matformer/data/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Matformer Data Module ---------------------- - -This subpackage provides data loading, preprocessing, and feature construction -for the **Matformer** model. It is independent of MindScience's core `data` module -to allow customized graph-based molecular representations. - -Main Components: - - data.py: Core dataset management and property mapping. - - features.py: Feature engineering for atomic and bond attributes. - - generate.py: Functions to build and prepare training datasets. - - graphs.py: Molecular graph construction and neighborhood computation. - -Typical Usage: - from data.generate import get_prop_model - - dataset = get_prop_model( - dataset_path="datasets/mptrj_ase.db", - task="property_prediction" - ) -""" diff --git a/MindChem/applications/matformer/matformer_application.ipynb b/MindChem/applications/matformer/matformer_application.ipynb index 95c68a849..0348c96e4 100644 --- a/MindChem/applications/matformer/matformer_application.ipynb +++ b/MindChem/applications/matformer/matformer_application.ipynb @@ -99,7 +99,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "a7fc7b46", "metadata": {}, "outputs": [ @@ -115,8 +115,8 @@ " setattr(self, word, getattr(machar, word).flat[0])\n", "/home/zdai/miniconda3/envs/mindc_py39/lib/python3.9/site-packages/numpy/core/getlimits.py:89: UserWarning: The value of the smallest subnormal for type is zero.\n", " return self._float_to_str(self.smallest_subnormal)\n", - "[WARNING] ME(147086:281473265917984,MainProcess):2025-12-04-16:57:27.120.059 [mindspore/context.py:1412] For 'context.set_context', the parameter 'device_target' will be deprecated and removed in a future version. Please use the api mindspore.set_device() instead.\n", - "[WARNING] ME(147086:281473265917984,MainProcess):2025-12-04-16:57:27.121.825 [mindspore/context.py:1412] For 'context.set_context', the parameter 'device_id' will be deprecated and removed in a future version. Please use the api mindspore.set_device() instead.\n" + "[WARNING] ME(2180565:281473433980960,MainProcess):2025-10-25-21:56:43.609.935 [mindspore/context.py:1412] For 'context.set_context', the parameter 'device_target' will be deprecated and removed in a future version. Please use the api mindspore.set_device() instead.\n", + "[WARNING] ME(2180565:281473433980960,MainProcess):2025-10-25-21:56:43.611.958 [mindspore/context.py:1412] For 'context.set_context', the parameter 'device_id' will be deprecated and removed in a future version. Please use the api mindspore.set_device() instead.\n" ] } ], @@ -129,10 +129,10 @@ "from mindspore import set_seed\n", "from mindspore import nn\n", "from mindspore.amp import all_finite\n", - "from models.matformer import Matformer\n", - "from models.utils import LossRecord, OneCycleLr\n", - "from models.graph.loss import L1LossMask, L2LossMask\n", - "from models.graph.dataloader import DataLoaderBase as DataLoader\n", + "from mindchemistry.cell.matformer.matformer import Matformer\n", + "from mindchemistry.cell.matformer.utils import LossRecord, OneCycleLr\n", + "from mindchemistry.graph.loss import L1LossMask, L2LossMask\n", + "from mindchemistry.graph.dataloader import DataLoaderBase as DataLoader\n", "\n", "config_path = \"config.yaml\" # 加载配置文件\n", "with open(config_path, 'r', encoding='utf-8') as stream:\n", @@ -241,10 +241,43 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "4220126d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:No existing saved file...Generate from scratch\n", + "INFO:get train data ................................\n", + "100%|██████████| 60794/60794 [01:56<00:00, 523.75it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "warning: could not load CGCNN features for 103\n", + "Setting it to max atomic number available here, 103\n", + "warning: could not load CGCNN features for 101\n", + "Setting it to max atomic number available here, 103\n", + "warning: could not load CGCNN features for 102\n", + "Setting it to max atomic number available here, 103\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 60794/60794 [00:00<00:00, 2014286.42it/s]\n", + "INFO:get val data ................................\n", + "100%|██████████| 7599/7599 [00:15<00:00, 477.21it/s]\n", + "100%|██████████| 7599/7599 [00:00<00:00, 2058948.07it/s]\n", + "INFO:start dumping the data.....\n" + ] + } + ], "source": [ "from data.generate import get_prop_model\n", "# 创建目录\n", @@ -259,7 +292,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "1991bcc3", "metadata": {}, "outputs": [ @@ -268,8 +301,9 @@ "output_type": "stream", "text": [ "Model trainable parameters: %s 2786689\n", - "Loading existing checkpoint from: %s ./ckpt/best_matformer.ckpt\n", - "Resuming training from epoch: %d 1\n", + "Starting new training process\n", + ".Saved best model at epoch %d, MSE: %.6f 0 0.0991247\n", + "Epoch 0 | Train MSE: 0.152263 | Val MSE: 0.099125 | Val MAE: 0.211994\n", "Training completed. Best model saved to: %s ./ckpt/best_matformer.ckpt\n" ] } @@ -395,7 +429,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "e531d317", "metadata": {}, "outputs": [ @@ -433,7 +467,7 @@ ")" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -465,7 +499,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "1ae6db19", "metadata": {}, "outputs": [], @@ -483,7 +517,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "472cb866", "metadata": {}, "outputs": [ @@ -492,127 +526,127 @@ "output_type": "stream", "text": [ "Starting inference...\n", - "Batch MSE: %.6f, MAE: %.6f 0.043750834 0.14395\n", - "Batch MSE: %.6f, MAE: %.6f 0.10943773 0.18327576\n", - "Batch MSE: %.6f, MAE: %.6f 0.039151095 0.14835942\n", - "Batch MSE: %.6f, MAE: %.6f 0.043602776 0.13516876\n", - "Batch MSE: %.6f, MAE: %.6f 0.041445397 0.14045432\n", - "Batch MSE: %.6f, MAE: %.6f 0.036282226 0.13990684\n", - "Batch MSE: %.6f, MAE: %.6f 0.047720864 0.14253876\n", - "Batch MSE: %.6f, MAE: %.6f 0.03105317 0.12102785\n", - "Batch MSE: %.6f, MAE: %.6f 0.02921421 0.11850074\n", - "Batch MSE: %.6f, MAE: %.6f 0.11715439 0.17308599\n", - "Batch MSE: %.6f, MAE: %.6f 0.043699227 0.16304001\n", - "Batch MSE: %.6f, MAE: %.6f 0.08008011 0.16171753\n", - "Batch MSE: %.6f, MAE: %.6f 0.069183365 0.1429166\n", - "Batch MSE: %.6f, MAE: %.6f 0.042589102 0.1505639\n", - "Batch MSE: %.6f, MAE: %.6f 0.046605058 0.16049895\n", - "Batch MSE: %.6f, MAE: %.6f 0.042094924 0.14214431\n", - "Batch MSE: %.6f, MAE: %.6f 0.03430771 0.13636498\n", - "Batch MSE: %.6f, MAE: %.6f 0.036877137 0.1417602\n", - "Batch MSE: %.6f, MAE: %.6f 0.053245373 0.12846707\n", - "Batch MSE: %.6f, MAE: %.6f 0.068187326 0.17410573\n", - "Batch MSE: %.6f, MAE: %.6f 0.037448812 0.14101818\n", - "Batch MSE: %.6f, MAE: %.6f 0.06939717 0.1556837\n", - "Batch MSE: %.6f, MAE: %.6f 0.03542845 0.1408842\n", - "Batch MSE: %.6f, MAE: %.6f 0.04656729 0.14660026\n", - "Batch MSE: %.6f, MAE: %.6f 0.03372931 0.13814378\n", - "Batch MSE: %.6f, MAE: %.6f 0.028955694 0.11927238\n", - "Batch MSE: %.6f, MAE: %.6f 0.021746937 0.11557756\n", - "Batch MSE: %.6f, MAE: %.6f 0.054371268 0.15786316\n", - "Batch MSE: %.6f, MAE: %.6f 0.07291359 0.16074933\n", - "Batch MSE: %.6f, MAE: %.6f 0.064554855 0.14010759\n", - "Batch MSE: %.6f, MAE: %.6f 0.08507447 0.17717317\n", - "Batch MSE: %.6f, MAE: %.6f 0.047637634 0.1335406\n", - "Batch MSE: %.6f, MAE: %.6f 0.0403093 0.13961352\n", - "Batch MSE: %.6f, MAE: %.6f 0.05325311 0.1430237\n", - "Batch MSE: %.6f, MAE: %.6f 0.057906054 0.15423179\n", - "Batch MSE: %.6f, MAE: %.6f 0.04740964 0.15765269\n", - "Batch MSE: %.6f, MAE: %.6f 0.036351778 0.14101231\n", - "Batch MSE: %.6f, MAE: %.6f 0.027886406 0.13058525\n", - "Batch MSE: %.6f, MAE: %.6f 0.09949611 0.17857507\n", - "Batch MSE: %.6f, MAE: %.6f 0.03710895 0.1439766\n", - "Batch MSE: %.6f, MAE: %.6f 0.06362608 0.16692403\n", - "Batch MSE: %.6f, MAE: %.6f 0.055476423 0.15701085\n", - "Batch MSE: %.6f, MAE: %.6f 0.056997593 0.16068782\n", - "Batch MSE: %.6f, MAE: %.6f 0.06990511 0.15264383\n", - "Batch MSE: %.6f, MAE: %.6f 0.04807016 0.14310849\n", - "Batch MSE: %.6f, MAE: %.6f 0.04987032 0.15396821\n", - "Batch MSE: %.6f, MAE: %.6f 0.038182907 0.126055\n", - "Batch MSE: %.6f, MAE: %.6f 0.030847073 0.13464943\n", - "Batch MSE: %.6f, MAE: %.6f 0.056946903 0.15084305\n", - "Batch MSE: %.6f, MAE: %.6f 0.056902558 0.15447894\n", - "Batch MSE: %.6f, MAE: %.6f 0.039766952 0.15105784\n", - "Batch MSE: %.6f, MAE: %.6f 0.039343096 0.13144812\n", - "Batch MSE: %.6f, MAE: %.6f 0.09950091 0.18500975\n", - "Batch MSE: %.6f, MAE: %.6f 0.036507826 0.13795191\n", - "Batch MSE: %.6f, MAE: %.6f 0.11742509 0.20485994\n", - "Batch MSE: %.6f, MAE: %.6f 0.03943647 0.14260328\n", - "Batch MSE: %.6f, MAE: %.6f 0.030588578 0.1242434\n", - "Batch MSE: %.6f, MAE: %.6f 0.036409285 0.13610879\n", - "Batch MSE: %.6f, MAE: %.6f 0.034640126 0.14429228\n", - "Batch MSE: %.6f, MAE: %.6f 0.029471612 0.13224417\n", - "Batch MSE: %.6f, MAE: %.6f 0.04000043 0.13145725\n", - "Batch MSE: %.6f, MAE: %.6f 0.030381605 0.12395114\n", - "Batch MSE: %.6f, MAE: %.6f 0.043774195 0.1385051\n", - "Batch MSE: %.6f, MAE: %.6f 0.05596337 0.17198707\n", - "Batch MSE: %.6f, MAE: %.6f 0.0652495 0.15619862\n", - "Batch MSE: %.6f, MAE: %.6f 0.041097883 0.13771905\n", - "Batch MSE: %.6f, MAE: %.6f 0.030853733 0.12920311\n", - "Batch MSE: %.6f, MAE: %.6f 0.07896672 0.17389815\n", - "Batch MSE: %.6f, MAE: %.6f 0.04251761 0.14951175\n", - "Batch MSE: %.6f, MAE: %.6f 0.024283802 0.11816917\n", - "Batch MSE: %.6f, MAE: %.6f 0.028855955 0.12581116\n", - "Batch MSE: %.6f, MAE: %.6f 0.037392963 0.14267088\n", - "Batch MSE: %.6f, MAE: %.6f 0.037532795 0.14732085\n", - "Batch MSE: %.6f, MAE: %.6f 0.025044627 0.12840271\n", - "Batch MSE: %.6f, MAE: %.6f 0.047694992 0.1436991\n", - "Batch MSE: %.6f, MAE: %.6f 0.044528704 0.14829797\n", - "Batch MSE: %.6f, MAE: %.6f 0.023793671 0.11760427\n", - "Batch MSE: %.6f, MAE: %.6f 0.055712394 0.15186241\n", - "Batch MSE: %.6f, MAE: %.6f 0.05332315 0.15364599\n", - "Batch MSE: %.6f, MAE: %.6f 0.026557475 0.12529078\n", - "Batch MSE: %.6f, MAE: %.6f 0.041560203 0.13957995\n", - "Batch MSE: %.6f, MAE: %.6f 0.059879344 0.15970594\n", - "Batch MSE: %.6f, MAE: %.6f 0.03152769 0.1309886\n", - "Batch MSE: %.6f, MAE: %.6f 0.042435423 0.14668447\n", - "Batch MSE: %.6f, MAE: %.6f 0.05181498 0.14885086\n", - "Batch MSE: %.6f, MAE: %.6f 0.036299862 0.14639042\n", - "Batch MSE: %.6f, MAE: %.6f 0.03689944 0.13688514\n", - "Batch MSE: %.6f, MAE: %.6f 0.036253124 0.13244948\n", - "Batch MSE: %.6f, MAE: %.6f 0.05026803 0.15210028\n", - "Batch MSE: %.6f, MAE: %.6f 0.029536642 0.1314708\n", - "Batch MSE: %.6f, MAE: %.6f 0.032934867 0.1287741\n", - "Batch MSE: %.6f, MAE: %.6f 0.03319533 0.13248947\n", - "Batch MSE: %.6f, MAE: %.6f 0.024982594 0.109414935\n", - "Batch MSE: %.6f, MAE: %.6f 0.022288697 0.10845525\n", - "Batch MSE: %.6f, MAE: %.6f 0.054391243 0.15119968\n", - "Batch MSE: %.6f, MAE: %.6f 0.030487854 0.12351191\n", - "Batch MSE: %.6f, MAE: %.6f 0.026544685 0.11667685\n", - "Batch MSE: %.6f, MAE: %.6f 0.04514885 0.13067845\n", - "Batch MSE: %.6f, MAE: %.6f 0.03454086 0.14461507\n", - "Batch MSE: %.6f, MAE: %.6f 0.05007938 0.15063569\n", - "Batch MSE: %.6f, MAE: %.6f 0.03144935 0.12715763\n", - "Batch MSE: %.6f, MAE: %.6f 0.03932115 0.15008056\n", - "Batch MSE: %.6f, MAE: %.6f 0.030269112 0.13254303\n", - "Batch MSE: %.6f, MAE: %.6f 0.04247802 0.14620948\n", - "Batch MSE: %.6f, MAE: %.6f 0.01563965 0.096747264\n", - "Batch MSE: %.6f, MAE: %.6f 0.05275118 0.15437622\n", - "Batch MSE: %.6f, MAE: %.6f 0.02525454 0.12186646\n", - "Batch MSE: %.6f, MAE: %.6f 0.16396788 0.17117047\n", - "Batch MSE: %.6f, MAE: %.6f 0.031031651 0.13427159\n", - "Batch MSE: %.6f, MAE: %.6f 0.0541057 0.1616255\n", - "Batch MSE: %.6f, MAE: %.6f 0.112874724 0.22950253\n", - "Batch MSE: %.6f, MAE: %.6f 0.035396863 0.12797889\n", - "Batch MSE: %.6f, MAE: %.6f 0.058534585 0.14975882\n", - "Batch MSE: %.6f, MAE: %.6f 0.029289056 0.12839442\n", - "Batch MSE: %.6f, MAE: %.6f 0.037997685 0.13646151\n", - "Batch MSE: %.6f, MAE: %.6f 0.041734952 0.14224492\n", - "Batch MSE: %.6f, MAE: %.6f 0.035853915 0.11676617\n", - "Batch MSE: %.6f, MAE: %.6f 0.033657588 0.1291771\n", + "Batch MSE: %.6f, MAE: %.6f 0.070725024 0.19663534\n", + "Batch MSE: %.6f, MAE: %.6f 0.24275717 0.2518391\n", + "Batch MSE: %.6f, MAE: %.6f 0.07755304 0.20984045\n", + "Batch MSE: %.6f, MAE: %.6f 0.08718463 0.211746\n", + "Batch MSE: %.6f, MAE: %.6f 0.069737345 0.19181544\n", + "Batch MSE: %.6f, MAE: %.6f 0.08872927 0.21920583\n", + "Batch MSE: %.6f, MAE: %.6f 0.08436994 0.1983262\n", + "Batch MSE: %.6f, MAE: %.6f 0.05113437 0.16753575\n", + "Batch MSE: %.6f, MAE: %.6f 0.07896744 0.21468684\n", + "Batch MSE: %.6f, MAE: %.6f 0.34971648 0.26292208\n", + "Batch MSE: %.6f, MAE: %.6f 0.12429382 0.24302137\n", + "Batch MSE: %.6f, MAE: %.6f 0.10492228 0.22669525\n", + "Batch MSE: %.6f, MAE: %.6f 0.1625251 0.23461409\n", + "Batch MSE: %.6f, MAE: %.6f 0.09076611 0.2220452\n", + "Batch MSE: %.6f, MAE: %.6f 0.10143504 0.22198504\n", + "Batch MSE: %.6f, MAE: %.6f 0.08181367 0.18540242\n", + "Batch MSE: %.6f, MAE: %.6f 0.072169304 0.21477415\n", + "Batch MSE: %.6f, MAE: %.6f 0.10248673 0.1943995\n", + "Batch MSE: %.6f, MAE: %.6f 0.123326614 0.23460506\n", + "Batch MSE: %.6f, MAE: %.6f 0.18733443 0.2850645\n", + "Batch MSE: %.6f, MAE: %.6f 0.09706067 0.2187456\n", + "Batch MSE: %.6f, MAE: %.6f 0.1729122 0.2187581\n", + "Batch MSE: %.6f, MAE: %.6f 0.07984987 0.1983538\n", + "Batch MSE: %.6f, MAE: %.6f 0.09521852 0.22871321\n", + "Batch MSE: %.6f, MAE: %.6f 0.07362223 0.2155208\n", + "Batch MSE: %.6f, MAE: %.6f 0.043237846 0.17015703\n", + "Batch MSE: %.6f, MAE: %.6f 0.08708128 0.21998827\n", + "Batch MSE: %.6f, MAE: %.6f 0.10044293 0.22751254\n", + "Batch MSE: %.6f, MAE: %.6f 0.19108173 0.23296222\n", + "Batch MSE: %.6f, MAE: %.6f 0.19890489 0.19952354\n", + "Batch MSE: %.6f, MAE: %.6f 0.08181734 0.22385815\n", + "Batch MSE: %.6f, MAE: %.6f 0.07472603 0.18713321\n", + "Batch MSE: %.6f, MAE: %.6f 0.11205362 0.2363978\n", + "Batch MSE: %.6f, MAE: %.6f 0.08420921 0.20906554\n", + "Batch MSE: %.6f, MAE: %.6f 0.15037972 0.25528532\n", + "Batch MSE: %.6f, MAE: %.6f 0.083993666 0.208972\n", + "Batch MSE: %.6f, MAE: %.6f 0.09709878 0.22851193\n", + "Batch MSE: %.6f, MAE: %.6f 0.06393953 0.19179778\n", + "Batch MSE: %.6f, MAE: %.6f 0.26187998 0.29153222\n", + "Batch MSE: %.6f, MAE: %.6f 0.09076169 0.2314304\n", + "Batch MSE: %.6f, MAE: %.6f 0.1540373 0.24349618\n", + "Batch MSE: %.6f, MAE: %.6f 0.08363533 0.20778248\n", + "Batch MSE: %.6f, MAE: %.6f 0.124374956 0.23529653\n", + "Batch MSE: %.6f, MAE: %.6f 0.0969419 0.21097831\n", + "Batch MSE: %.6f, MAE: %.6f 0.05773235 0.18551444\n", + "Batch MSE: %.6f, MAE: %.6f 0.07637855 0.20789327\n", + "Batch MSE: %.6f, MAE: %.6f 0.11196412 0.2185465\n", + "Batch MSE: %.6f, MAE: %.6f 0.067040004 0.19458279\n", + "Batch MSE: %.6f, MAE: %.6f 0.07974026 0.18797798\n", + "Batch MSE: %.6f, MAE: %.6f 0.09172576 0.22419518\n", + "Batch MSE: %.6f, MAE: %.6f 0.1101581 0.22808634\n", + "Batch MSE: %.6f, MAE: %.6f 0.061570488 0.19367743\n", + "Batch MSE: %.6f, MAE: %.6f 0.1348338 0.234665\n", + "Batch MSE: %.6f, MAE: %.6f 0.08274542 0.20886873\n", + "Batch MSE: %.6f, MAE: %.6f 0.09154258 0.20656306\n", + "Batch MSE: %.6f, MAE: %.6f 0.071649626 0.20816484\n", + "Batch MSE: %.6f, MAE: %.6f 0.059656523 0.18715087\n", + "Batch MSE: %.6f, MAE: %.6f 0.06559457 0.1992346\n", + "Batch MSE: %.6f, MAE: %.6f 0.060599018 0.18150224\n", + "Batch MSE: %.6f, MAE: %.6f 0.102835864 0.1924825\n", + "Batch MSE: %.6f, MAE: %.6f 0.064230904 0.17498668\n", + "Batch MSE: %.6f, MAE: %.6f 0.06948519 0.18372948\n", + "Batch MSE: %.6f, MAE: %.6f 0.08482786 0.21691777\n", + "Batch MSE: %.6f, MAE: %.6f 0.07467872 0.20106737\n", + "Batch MSE: %.6f, MAE: %.6f 0.102776766 0.22381547\n", + "Batch MSE: %.6f, MAE: %.6f 0.06190313 0.17998596\n", + "Batch MSE: %.6f, MAE: %.6f 0.06609647 0.20770283\n", + "Batch MSE: %.6f, MAE: %.6f 0.10321584 0.21499527\n", + "Batch MSE: %.6f, MAE: %.6f 0.06058496 0.18522331\n", + "Batch MSE: %.6f, MAE: %.6f 0.058009677 0.1738572\n", + "Batch MSE: %.6f, MAE: %.6f 0.05354975 0.18511978\n", + "Batch MSE: %.6f, MAE: %.6f 0.06748298 0.1977003\n", + "Batch MSE: %.6f, MAE: %.6f 0.07445188 0.19744211\n", + "Batch MSE: %.6f, MAE: %.6f 0.07519908 0.21514264\n", + "Batch MSE: %.6f, MAE: %.6f 0.122985624 0.2383165\n", + "Batch MSE: %.6f, MAE: %.6f 0.094054796 0.21155931\n", + "Batch MSE: %.6f, MAE: %.6f 0.06364514 0.18733943\n", + "Batch MSE: %.6f, MAE: %.6f 0.13308582 0.25394255\n", + "Batch MSE: %.6f, MAE: %.6f 0.3328262 0.2796916\n", + "Batch MSE: %.6f, MAE: %.6f 0.061871458 0.18493441\n", + "Batch MSE: %.6f, MAE: %.6f 0.08251292 0.19105017\n", + "Batch MSE: %.6f, MAE: %.6f 0.11805928 0.24008796\n", + "Batch MSE: %.6f, MAE: %.6f 0.061221845 0.17849262\n", + "Batch MSE: %.6f, MAE: %.6f 0.06829566 0.20483744\n", + "Batch MSE: %.6f, MAE: %.6f 0.072616234 0.20534179\n", + "Batch MSE: %.6f, MAE: %.6f 0.13656346 0.21518663\n", + "Batch MSE: %.6f, MAE: %.6f 0.122859545 0.2599476\n", + "Batch MSE: %.6f, MAE: %.6f 0.0961164 0.21464485\n", + "Batch MSE: %.6f, MAE: %.6f 0.09907257 0.21406531\n", + "Batch MSE: %.6f, MAE: %.6f 0.05511003 0.18155563\n", + "Batch MSE: %.6f, MAE: %.6f 0.08438632 0.20503673\n", + "Batch MSE: %.6f, MAE: %.6f 0.07392317 0.20789784\n", + "Batch MSE: %.6f, MAE: %.6f 0.05105378 0.17417866\n", + "Batch MSE: %.6f, MAE: %.6f 0.074250236 0.21349187\n", + "Batch MSE: %.6f, MAE: %.6f 0.07819778 0.21257493\n", + "Batch MSE: %.6f, MAE: %.6f 0.03804079 0.14963137\n", + "Batch MSE: %.6f, MAE: %.6f 0.084136836 0.21345618\n", + "Batch MSE: %.6f, MAE: %.6f 0.11200511 0.23238355\n", + "Batch MSE: %.6f, MAE: %.6f 0.08319594 0.20916802\n", + "Batch MSE: %.6f, MAE: %.6f 0.065127745 0.18015994\n", + "Batch MSE: %.6f, MAE: %.6f 0.060536943 0.18460588\n", + "Batch MSE: %.6f, MAE: %.6f 0.108970165 0.25947753\n", + "Batch MSE: %.6f, MAE: %.6f 0.11962972 0.20790091\n", + "Batch MSE: %.6f, MAE: %.6f 0.12101042 0.23867048\n", + "Batch MSE: %.6f, MAE: %.6f 0.054923728 0.18108687\n", + "Batch MSE: %.6f, MAE: %.6f 0.11708981 0.23226917\n", + "Batch MSE: %.6f, MAE: %.6f 0.06869913 0.18220729\n", + "Batch MSE: %.6f, MAE: %.6f 0.2685594 0.25276893\n", + "Batch MSE: %.6f, MAE: %.6f 0.10197212 0.22111583\n", + "Batch MSE: %.6f, MAE: %.6f 0.11209624 0.19556287\n", + "Batch MSE: %.6f, MAE: %.6f 0.16791901 0.27960837\n", + "Batch MSE: %.6f, MAE: %.6f 0.06938132 0.18391755\n", + "Batch MSE: %.6f, MAE: %.6f 0.088869214 0.21327406\n", + "Batch MSE: %.6f, MAE: %.6f 0.056702934 0.18592325\n", + "Batch MSE: %.6f, MAE: %.6f 0.1060355 0.2266976\n", + "Batch MSE: %.6f, MAE: %.6f 0.080637224 0.20904863\n", + "Batch MSE: %.6f, MAE: %.6f 0.05775518 0.17394236\n", + "Batch MSE: %.6f, MAE: %.6f 0.06784132 0.20381372\n", "Inference completed.\n", - "Average MSE: %.6f 0.047113106\n", - "Average MAE: %.6f 0.14361368\n" + "Average MSE: %.6f 0.09904503\n", + "Average MAE: %.6f 0.21175078\n" ] } ], @@ -669,13 +703,13 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "db83f0e9", "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhcAAAIjCAYAAACwMjnzAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAADIKklEQVR4nOzdd3gc1fXw8e/MbN9VL7bcK8XYpjmht9BrSEyHXzAkhCS0wAsESEgg1EAK6bSEEEI1EEhosemdEGyMDRjcbclNdVfbd2bu+8do15Il2ZK90kry+TxPgjXacne00py999xzNKWUQgghhBAiT/RCD0AIIYQQQ4sEF0IIIYTIKwkuhBBCCJFXElwIIYQQIq8kuBBCCCFEXklwIYQQQoi8kuBCCCGEEHklwYUQQggh8kqCCyGEEELklQQXYocwbtw4Zs2alfv69ddfR9M0Xn/99bw9h6Zp3HDDDXl7PNE7a9aswefz8c477/TZc/ztb39D0zRWrlyZO3booYdy6KGHbvW+ffGegx3jfffSSy8RCoWor68v9FBED0lwIfpc9g9y9n8+n4+ddtqJiy++mA0bNhR6eL3ywgsvDPk/5FnZi2FP/jcQ/PznP2efffbhgAMOIJPJUFlZyYEHHtjt7ZVSjB49mr322qsfR7ltBur77u233+bYY49l5MiR+Hw+xowZw4knnsgjjzyyTY/3pz/9ib/97W+djh9zzDFMmjSJ2267bTtHLPqLq9ADEDuOn//854wfP55kMsnbb7/Nn//8Z1544QUWLVpEIBDo17EcfPDBJBIJPB5Pr+73wgsv8Mc//rHLP/SJRAKXa+j8Su2666489NBDHY5de+21hEIhfvzjHxdoVF2rr6/nwQcf5MEHHwTA7XZz6qmncs8997Bq1SrGjh3b6T5vvvkmtbW1XH755dv13HPmzNmu+/fEQHzfzZ49m9NPP5099tiDyy67jLKyMlasWMGbb77Jfffdx1lnndXrx/zTn/5EZWVlh1nGrAsvvJArr7ySG2+8kaKiojy8AtGXhs5fQjHgHXvsscyYMQOA73znO1RUVPDrX/+aZ599ljPPPLPL+8RiMYLBYN7Hous6Pp8vr4+Z78crtGHDhnHOOed0OHb77bdTWVnZ6Xh7tm2TTqf79Xz84x//wOVyceKJJ+aOnX322dx99908+uijXHPNNZ3u88gjj6DrOmecccZ2PXdvA9R8K9T77oYbbmDKlCm8//77nc7Bxo0b8/58M2fO5JJLLmH27Nmcf/75eX98kV+yLCIK5mtf+xoAK1asAGDWrFmEQiGWLVvGcccdR1FREWeffTbgXLDuuusudtttN3w+H8OGDePCCy+kubm5w2Mqpbj55psZNWoUgUCAww47jE8//bTTc3e3/v3BBx9w3HHHUVZWRjAYZPr06fz2t7/Nje+Pf/wjQJdLAl2tfc+fP59jjz2W4uJiQqEQhx9+OO+//36H22SXjd555x2uuOIKqqqqCAaDfOMb39jqGvMvf/lLNE1j1apVnb537bXX4vF4cudoyZIlzJw5k+HDh+Pz+Rg1ahRnnHEG4XB4i8+xNZqmcfHFF/Pwww+z22674fV6eemll7o9xytXrkTTtE7T34sXL+aUU06hvLwcn8/HjBkz+Ne//tWjMTzzzDPss88+hEKh3LEDDjiAcePGdTlFn8lkePLJJznssMMYMWIEn3zyCbNmzWLChAn4fD6GDx/O+eefT2Nj41afu6uci9raWk4++WSCwSDV1dVcfvnlpFKpTvd96623OPXUUxkzZgxer5fRo0dz+eWXk0gkcrcZiO87gGXLlvGVr3yly+Cqurq6w9c9+f0dN24cn376KW+88UbuNbY/r9XV1UyfPp1nn312q2MThSczF6Jgli1bBkBFRUXumGmaHH300Rx44IH88pe/zC2XXHjhhfztb3/jvPPO49JLL2XFihX84Q9/YP78+bzzzju43W4AfvrTn3LzzTdz3HHHcdxxxzFv3jyOOuoo0un0Vsczd+5cTjjhBGpqarjssssYPnw4n3/+Oc899xyXXXYZF154IWvXrmXu3Lmdlgu68umnn3LQQQdRXFzM1Vdfjdvt5p577uHQQw/ljTfeYJ999ulw+0suuYSysjJ+9rOfsXLlSu666y4uvvhiHn/88W6f47TTTuPqq6/miSee4KqrrurwvSeeeIKjjjqKsrIy0uk0Rx99NKlUiksuuYThw4dTV1fHc889R0tLCyUlJVt9PVvy6quv8sQTT3DxxRdTWVnJuHHjaGlp6fH9P/30Uw444ABGjhzJNddcQzAY5IknnuDkk0/mqaee4hvf+Ea3981kMnz44Yd8//vf73Bc0zTOOussbr31Vj799FN222233PdeeuklmpqacsHr3LlzWb58Oeeddx7Dhw/n008/5d577+XTTz/l/fff71VeSSKR4PDDD2f16tVceumljBgxgoceeohXX321021nz55NPB7n+9//PhUVFfz3v//l97//PbW1tcyePRtgQL7vAMaOHcsrr7xCbW0to0aN2uJte/L7e9ddd3HJJZd0WHYbNmxYh8fZe++9eeaZZ7Z6DsQAoIToYw888IAC1Msvv6zq6+vVmjVr1GOPPaYqKiqU3+9XtbW1Simlzj33XAWoa665psP933rrLQWohx9+uMPxl156qcPxjRs3Ko/Ho44//nhl23budtddd50C1Lnnnps79tprrylAvfbaa0oppUzTVOPHj1djx45Vzc3NHZ6n/WNddNFFqrtfG0D97Gc/y3198sknK4/Ho5YtW5Y7tnbtWlVUVKQOPvjgTufniCOO6PBcl19+uTIMQ7W0tHT5fFn77bef2nvvvTsc++9//6sA9fe//10ppdT8+fMVoGbPnr3Fx9qa3XbbTR1yyCEdjgFK13X16aefdji++TnOWrFihQLUAw88kDt2+OGHq2nTpqlkMpk7Ztu22n///dXkyZO3OKalS5cqQP3+97/v9L1PP/1UAeraa6/tcPyMM85QPp9PhcNhpZRS8Xi8030fffRRBag333wzdyz7s1qxYkXu2CGHHNLhnNx1110KUE888UTuWCwWU5MmTep0Prp63ttuu01pmqZWrVqVOzYQ33d/+ctfFKA8Ho867LDD1PXXX6/eeustZVlWh9v19PdXqa7fX+3deuutClAbNmzY4thE4cmyiOg3RxxxBFVVVYwePZozzjiDUCjEP//5T0aOHNnhdpt/Ap09ezYlJSUceeSRNDQ05P639957EwqFeO211wB4+eWXSafTXHLJJR0+af7whz/c6tjmz5/PihUr+OEPf0hpaWmH723LbgjLspgzZw4nn3wyEyZMyB2vqanhrLPO4u233yYSiXS4z3e/+90Oz3XQQQdhWVaXSx7tnX766Xz00Ue5mSCAxx9/HK/Xy9e//nWA3MzEf/7zH+LxeK9fz9YccsghTJkyZZvu29TUxKuvvsppp51Ga2tr7ufb2NjI0UcfzZIlS6irq+v2/tmli7Kysk7fmzJlCnvuuSePPfZY7lgsFuNf//oXJ5xwAsXFxQD4/f7c95PJJA0NDey7774AzJs3r1ev54UXXqCmpoZTTjkldywQCPDd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", + "image/png": "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S5Zw1llnMXLkSObPn88999zD/Pnzef/993uVV5JMJjnooINYsWIFF110EaNHj+bBBx/k1Vdf7XTbxx9/nEQiwQ9/+EPKy8v573//y+9//3tWrVrF448/DjAoX3cA48eP55VXXmHVqlWMGTNmo7ftyb/f22+/nQsvvLDDstuIESM6/J7ddtuNp59+epPnQAwCSoh+dv/99ytAvfzyy6qurk6tXLlSPfLII6q8vFyFQiG1atUqpZRSZ5xxhgLUFVdc0eH+b731lgLUP/7xjw7HX3zxxQ7Ha2trld/vV9/85jeV67q521111VUKUGeccUbu2GuvvaYA9dprrymllLJtW02cOFGNHz9eNTU1dXic9r/r/PPPV939swHUNddck/v+mGOOUX6/Xy1evDh3bPXq1aqwsFDtv//+nc7PwQcf3OGxLrnkEmUYhmpubu7y8bL22msvtdtuu3U49t///lcB6m9/+5tSSqk5c+YoQD3++OMb/V2bsuOOO6oDDjigwzFA6bqu5s+f3+H4huc4a+nSpQpQ999/f+7YQQcdpKZPn65SqVTumOu6au+991ZTpkzZ6JgWLVqkAPX73/++08/mz5+vAHXllVd2OH7SSSepYDCootGoUkqpRCLR6b4PP/ywAtSbb76ZO5b9Wy1dujR37IADDuhwTm6//XYFqMceeyx3LB6Pq8mTJ3c6H1097s0336w0TVPLly/PHRuMr7u//OUvClB+v18deOCB6uqrr1ZvvfWWchynw+16+u9Xqa5fX+3ddNNNClDr1q3b6NhE/smyiBgwBx98MJWVlYwdO5aTTjqJSCTCU089RXV1dYfbbfgJ9PHHH6e4uJhDDjmE+vr63P922203IpEIr732GgAvv/wymUyGCy+8sMMnzR/96EebHNucOXNYunQpP/rRjygpKenws83ZDeE4Di+99BLHHHMM22yzTe74qFGjOOWUU3j77bdpaWnpcJ/vf//7HR5rv/32w3GcLpc82jvxxBP56KOPcjNBAI8++iiBQIBvfetbALmZif/85z8kEoleP59NOeCAA5g6depm3bexsZFXX32VE044gdbW1tzft6GhgcMOO4yFCxdSU1PT7f2zSxelpaWdfjZ16lR22WUXHnnkkdyxeDzOs88+y5FHHklRUREAoVAo9/NUKkV9fT177rknAB9//HGvns/zzz/PqFGjOO6443LHCgoK+P73v9/ptu0fNx6PU19fz957741Sijlz5vTqcWFgX3dnn302L774IrNmzeLtt9/m+uuvZ7/99mPKlCm8++67udv19N9vT2T/xvX19T2+j8gPCS7EgLnzzjuZPXs2r732Gp9//jlLlizhsMMO63Ab0zQ7TbEuXLiQaDRKVVUVlZWVHf4Xi8VyyWPZN8MpU6Z0uH9lZWWXF572shfmadOmbdFzzKqrqyORSLDddtt1+tkOO+yA67qsXLmyw/Fx48Z1+D475g3zSjZ0/PHHo+t6bhpbKcXjjz+eW3MHmDhxIpdeeil//vOfqaio4LDDDuPOO+/c4nyLrIkTJ272fRctWoRSiquvvrrT3/eaa64BepYgqJTq8vipp57K0qVLcxe8p59+mkQikVsSAS/AufjiixkxYgShUIjKysrcc+rtOVq+fDmTJ0/uFJR29VpYsWIFZ555JmVlZUQiESorKznggAM263FhYF93AIcddhj/+c9/aG5u5s033+T8889n+fLlHHnkkbm/WU///fZE9m88WLY/i+5JzoUYMF/72tdyu0W6EwgE0PWOMa/rulRVVfGPf/yjy/tUVlb22RjzyTCMLo93d9HMGj16NPvttx+PPfYYV111Fe+//z4rVqzg1ltv7XC7X//615x55pk888wzvPTSS1x00UXcfPPNvP/++5tcM9+U9p/As7q7ADiO0+F713UBuOyyyzoFm1mTJ0/u9rGzOTvdXQxPPvlkLr/8ch566CH23ntvHnroIUpLSzniiCNytznhhBN49913+fGPf8zOO+9MJBLBdV0OP/zw3Pj6muM4HHLIITQ2NvKTn/yE7bffnnA4TE1NDWeeeWa/Pe6GNvd1115BQQH77bcf++23HxUVFVx33XW88MILnHHGGX367zf7N66oqOjxfUR+SHAhBr1Jkybx8ssvs88++3R5EcvK1jJYuHBhhynhurq6TX4KmzRpEgDz5s3j4IMP7vZ2Pf3EVFlZSUFBAV999VWnn3355Zfous7YsWN79Lt64sQTT+S8887jq6++4tFHH6WgoKDDtsys6dOnM336dH72s5/x7rvvss8++3DXXXdxww039NlYsrKfgDdM7Nxwuj37t/L5fBs9990ZN24coVAot+toQ6NHj+bAAw/k8ccf5+qrr2b27NmceeaZuV0OTU1NvPLKK1x33XX8/Oc/z91v4cKFvR4LeK/DefPmoZTq8HrZ8LXw2WefsWDBAh544AFOP/303PHZs2d3+p2D9XXXlewHiDVr1gA9//cLm36eS5cupaKiYth8oBjOZFlEDHonnHACjuNw/fXXd/qZbdu5i9fBBx+Mz+fj97//fYdPXbfffvsmH2PXXXdl4sSJ3H777Z0uhu1/V7bmxqZ2QhiGwaGHHsozzzzToVT0unXreOihh9h3331zSxZ94dhjj8UwDB5++GEef/xxjjzyyA71QVpaWrBtu8N9pk+fjq7rXW6R7Avjx4/HMAzefPPNDsf/+Mc/dvi+qqqKWbNmcffdd+cuSO1talukz+dj5syZ/O9//+v2Nqeeeiq1tbWce+65WJbVYUkk+8l9w0/qPXnddOWII45g9erV/POf/8wdSyQS3HPPPR1u19XjKqVyW5/bG4yvu1deeaXL488//zywfhmop/9+wXueG3uOH330EXvttdfmD1oMGJm5EIPeAQccwLnnnsvNN9/MJ598wqGHHorP52PhwoU8/vjj3HHHHRx33HFUVlZy2WWXcfPNN3PkkUdyxBFHMGfOHF544YVNTqPqus6f/vQnjjrqKHbeeWfOOussRo0axZdffsn8+fP5z3/+A3hb4QAuuugiDjvsMAzD6LYI0w033MDs2bPZd999Oe+88zBNk7vvvpt0Os0vf/nLPj1HVVVVHHjggfzmN7+htbWVE088scPPX331VS644AKOP/54tt12W2zb5sEHH8QwDI499tg+HUtWcXExxx9/PL///e/RNI1Jkybx3HPPdbnGfuedd7Lvvvsyffp0zjnnHLbZZhvWrVvHe++9x6pVq5g7d+5GH+tb3/oWP/3pT2lpaeny4nnsscdy3nnn8cwzzzB27Fj233//3M+KiorYf//9+eUvf4llWVRXV/PSSy91OxOyKeeccw5/+MMfOP300/noo48YNWoUDz74YKcqtNtvvz2TJk3isssuo6amhqKiIp544okuZ9kG4+vuW9/6FhMnTuSoo45i0qRJxONxXn75Zf71r3+x++6752bOevrvN/s8//SnP3HDDTcwefJkqqqqcvVwamtr+fTTTzn//PP77DmIfpSXPSpiq5Ld8vbhhx9u9HZnnHGGCofD3f78nnvuUbvttpsKhUKqsLBQTZ8+XV1++eVq9erVuds4jqOuu+46NWrUKBUKhdSs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EEIIUVESLoQQQghRURIuhBBCCFFREi6EEEIIUVESLoQQQghRURIuhBBCCFFREi6EEEIIUVESLoQQQghRURIuhBBCCFFREi6EEEIIUVESLoQQQghRURIuhBBCCFFREi6EEEIIUVESLoQQQghRURIuhBBCCFFREi6EEEIIUVF6tU9gMnmeB0AqlarymRx6LMsil8uRSqUwDKPapzPlyPWvHrn21SXXv3qGX/vSc2fpuXRfplS4SKfTAMyaNavKZyKEEEIcmtLpNDU1Nfv8HsUbSwQ5TLiuy86dO4nH4yiKUu3TOaSkUilmzZrF9u3bSSQS1T6dKUeuf/XIta8uuf7VM/zae55HOp1m5syZqOq+qyqm1MqFqqq0trZW+zQOaYlEQv4DryK5/tUj17665PpXz+Br/3orFiVS0CmEEEKIipJwIYQQQoiKknAhxiQYDHLdddcRDAarfSpTklz/6pFrX11y/avnQK79lCroFEIIIcTEk5ULIYQQQlSUhAshhBBCVJSECyGEEEJUlIQLIYQQQlSUhAux37Zs2cJFF11EW1sb4XCY+fPnc91112GaZrVP7bD0gx/8gLlz5xIKhTj++OP5+9//Xu1TmhJuueUWVq5cSTwep7m5mfe85z2sXbu22qc1JX39619HURQuv/zyap/KlNHe3s4FF1xAQ0MD4XCYpUuX8swzz4z58RIuxH5bs2YNruty55138sorr/Cd73yHO+64gy9+8YvVPrXDzs9//nM+97nPcd111/Hcc8+xfPlyzjzzTDo7O6t9aoe9J598kksvvZS//vWvPP7441iWxRlnnEE2m632qU0p//jHP7jzzjtZtmxZtU9lyujr6+PEE0/EMAx+97vf8eqrr/Ktb32Lurq6MR9DtqKKivjmN7/J7bffzqZNm6p9KoeV448/npUrV/L9738f8OfjzJo1i09/+tNcc801VT67qaWrq4vm5maefPJJ3vrWt1b7dKaETCbDscceyw9/+ENuuukmjj76aG677bZqn9Zh75prruFPf/oT//u//zvuY8jKhaiIZDJJfX19tU/jsGKaJs8++yynnXZa+XOqqnLaaafxl7/8pYpnNjUlk0kA+Xs+iS699FLe8Y53DPlvQEy8X//61xx33HF88IMfpLm5mWOOOYYf/ehH+3UMCRfigG3YsIHvfe97fPKTn6z2qRxWuru7cRyHadOmDfn8tGnT2L17d5XOampyXZfLL7+cE088kSVLllT7dKaEBx54gOeee45bbrml2qcy5WzatInbb7+dhQsX8thjj/HP//zPfOYzn+Hee+8d8zEkXIiya665BkVR9vm2Zs2aIY9pb2/nrLPO4oMf/CAXX3xxlc5ciIl16aWXsnr1ah544IFqn8qUsH37dj772c/y05/+lFAoVO3TmXJc1+XYY4/l5ptv5phjjuGSSy7h4osv5o477hjzMabUyHWxb1deeSWrVq3a5/fMmzev/P7OnTs55ZRTOOGEE7jrrrsm+OymnsbGRjRNo6OjY8jnOzo6mD59epXOauq57LLL+K//+i+eeuopWltbq306U8Kzzz5LZ2cnxx57bPlzjuPw1FNP8f3vf59isYimaVU8w8PbjBkzWLx48ZDPHXXUUTz44INjPoaEC1HW1NREU1PTmL63vb2dU045hRUrVnDPPfegqrIIVmmBQIAVK1bwxBNP8J73vAfwX1E88cQTXHbZZdU9uSnA8zw+/elP8/DDD/PHP/6Rtra2ap/SlPG2t72Nl19+ecjnLrzwQhYtWsTVV18twWKCnXjiiXttu163bh1z5swZ8zEkXIj91t7ezsknn8ycOXO49dZb6erqKn9NXlFX1uc+9zk+/vGPc9xxx/HGN76R2267jWw2y4UXXljtUzvsXXrppfzsZz/jV7/6FfF4vFznUlNTQzgcrvLZHd7i8fhetS3RaJSGhgapeZkEV1xxBSeccAI333wzH/rQh/j73//OXXfdtV8r1BIuxH57/PHH2bBhAxs2bNhrmVh2NlfWhz/8Ybq6uvjKV77C7t27Ofroo3n00Uf3KvIUlXf77bcDcPLJJw/5/D333PO6tw+FOJStXLmShx9+mC984QvceOONtLW1cdttt/GRj3xkzMeQPhdCCCGEqCi5US6EEEKIipJwIYQQQoiKknAhhBBCiIqScCGEEEKIipJwIYQQQoiKknAhhBBCiIqScCGEEEKIipJwIYQQQoiKknAhhDhszJ07l9tuu63apyHElCfhQogpTFGUfb5df/31k3IeS5cu5VOf+tSIX/vJT35CMBiku7t7Us5FCHHgJFwIMYXt2rWr/HbbbbeRSCSGfO6qq64qf6/nedi2PSHncdFFF/HAAw+Qz+f3+to999zDu9/9bhobGyfkZwshKk/ChRBT2PTp08tvNTU1KIpS/njNmjXE43F+97vfsWLFCoLBIE8//TSrVq0qj4Avufzyy4cM+HJdl1tuuYW2tjbC4TDLly/nl7/85ajnccEFF5DP53nwwQeHfH7z5s388Y9/5KKLLmLjxo2cc845TJs2jVgsxsqVK/n9738/6jG3bNmCoii88MIL5c/19/ejKAp//OMfy59bvXo1Z599NrFYjGnTpvHRj35UVkmEOEASLoQQ+3TNNdfw9a9/nddee41ly5aN6TG33HIL9913H3fccQevvPIKV1xxBRdccAFPPvnkiN/f2NjIOeecw9133z3k8//+7/9Oa2srZ5xxBplMhre//e088cQTPP/885x11lm8613vYtu2beP+s/X393PqqadyzDHH8Mwzz/Doo4/S0dHBhz70oXEfUwghI9eFEK/jxhtv5PTTTx/z9xeLRW6++WZ+//vf8+Y3vxmAefPm8fTTT3PnnXdy0kknjfi4iy66iLPPPpvNmzfT1taG53nce++9fPzjH0dVVZYvX87y5cvL3//Vr36Vhx9+mF//+tdcdtll4/qzff/73+eYY47h5ptvLn/u7rvvZtasWaxbt44jjjhiXMcVYqqTlQshxD4dd9xx+/X9GzZsIJfLcfrppxOLxcpv9913Hxs3bhz1caeffjqtra3cc889ADzxxBNs27aNCy+8EIBMJsNVV13FUUcdRW1tLbFYjNdee+2AVi5efPFF/vCHPww5z0WLFgHs81yFEPsmKxdCiH2KRqNDPlZVFc/zhnzOsqzy+5lMBoBHHnmElpaWId8XDAZH/TmqqrJq1Sruvfderr/+eu655x5OOeUU5s2bB8BVV13F448/zq233sqCBQsIh8N84AMfwDTNUY8HDDnXwedZOtd3vetdfOMb39jr8TNmzBj1XIUQ+ybhQgixX5qamli9evWQz73wwgsYhgHA4sWLCQaDbNu2bdRbIKO58MILuemmm3jooYd4+OGH+fGPf1z+2p/+9CdWrVrFe9/7XsAPBlu2bNnneYK/I+aYY44pn+dgxx57LA8++CBz585F1+WfQyEqRW6LCCH2y6mnnsozzzzDfffdx/r167nuuuuGhI14PM5VV13FFVdcwb333svGjRt57rnn+N73vse99967z2O3tbVx6qmncskllxAMBnnf+95X/trChQt56KGHeOGFF3jxxRc5//zzcV131GOFw2He9KY3lYtRn3zySb785S8P+Z5LL72U3t5ezjvvPP7xj3+wceNGHnvsMS688EIcxxnnFRJCSLgQQuyXM888k2uvvZbPf/7zrFy5knQ6zcc+9rEh3/PVr36Va6+9lltuuYWjjjqKs846i0ceeYS2trbXPf5FF11EX18f559/PqFQqPz5b3/729TV1XHCCSfwrne9izPPPJNjjz12n8e6++67sW2bFStWcPnll3PTTTcN+frMmTP505/+hOM4nHHGGSxdupTLL7+c2tra8m0VIcT+U7zhN0+FEEIIIQ6ARHMhhBBCVJSECyGEEEJUlIQLIYQQQlSUhAshhBBCVJSECyGEEEJUlIQLIYQQQlSUhAshhBBCVJSECyGEEEJUlIQLIYQQQlSUhAshhBBCVJSECyGEEEJU1P8PoYm1Xm+zYW0AAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -710,7 +744,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "2aa9bc9b", "metadata": {}, "outputs": [ diff --git a/MindChem/applications/matformer/matformer_application_EN.ipynb b/MindChem/applications/matformer/matformer_application_EN.ipynb index 24c45aba9..369300ba3 100644 --- a/MindChem/applications/matformer/matformer_application_EN.ipynb +++ b/MindChem/applications/matformer/matformer_application_EN.ipynb @@ -99,7 +99,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "a7fc7b46", "metadata": {}, "outputs": [ @@ -129,10 +129,10 @@ "from mindspore import set_seed\n", "from mindspore import nn\n", "from mindspore.amp import all_finite\n", - "from models.matformer import Matformer\n", - "from models.utils import LossRecord, OneCycleLr\n", - "from models.graph.loss import L1LossMask, L2LossMask\n", - "from models.graph.dataloader import DataLoaderBase as DataLoader\n", + "from mindchemistry.cell.matformer.matformer import Matformer\n", + "from mindchemistry.cell.matformer.utils import LossRecord, OneCycleLr\n", + "from mindchemistry.graph.loss import L1LossMask, L2LossMask\n", + "from mindchemistry.graph.dataloader import DataLoaderBase as DataLoader\n", "\n", "config_path = \"config.yaml\"\n", "with open(config_path, 'r', encoding='utf-8') as stream:\n", diff --git a/MindChem/applications/matformer/mindchemistry/__init__.py b/MindChem/applications/matformer/mindchemistry/__init__.py new file mode 100644 index 000000000..c54166ca1 --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/__init__.py @@ -0,0 +1,64 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Initialization for MindChemistry APIs.""" + +import time +import mindspore as ms +from mindspore import log as logger +from mindscience.e3nn import * +from .cell import * +from .utils import * +from .graph import * +from .so2_conv import * + +__all__ = [] +__all__.extend(cell.__all__) +__all__.extend(utils.__all__) + +def _mindspore_version_check(): + """ + Check MindSpore version for MindChemistry. + + Raises: + ImportError: If MindSpore cannot be imported. + """ + try: + _ = ms.__version__ + except ImportError as exc: + raise ImportError( + "Cannot find MindSpore in the current environment. Please install " + "MindSpore before using MindChemistry, by following the instruction at " + "https://www.mindspore.cn/install" + ) from exc + + ms_version = ms.__version__[:5] + required_mindspore_version = "1.8.1" + + if ms_version < required_mindspore_version: + logger.warning( + f"Current version of MindSpore ({ms_version}) is not compatible with MindChemistry. " + f"Some functions might not work or even raise errors. Please install MindSpore " + f"version >= {required_mindspore_version}. For more details about dependency settings, " + f"please check the instructions at the MindSpore official website " + f"https://www.mindspore.cn/install or check the README.md at " + f"https://gitee.com/mindspore/mindscience" + ) + + for i in range(3, 0, -1): + logger.warning(f"Please pay attention to the above warning, countdown: {i}") + time.sleep(1) + + +_mindspore_version_check() diff --git a/mindscience/common/initializer.py b/MindChem/applications/matformer/mindchemistry/cell/__init__.py similarity index 45% rename from mindscience/common/initializer.py rename to MindChem/applications/matformer/mindchemistry/cell/__init__.py index 718aa5f11..f94ddbf12 100644 --- a/mindscience/common/initializer.py +++ b/MindChem/applications/matformer/mindchemistry/cell/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2023 The AIMM Group at Shenzhen Bay Laboratory & Peking University & Huawei Technologies Co., Ltd +# Copyright 2022 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,24 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""initializer""" +"""initialization for cells""" -import numpy as np -from mindspore.common.initializer import TruncatedNormal +from .nequip import Nequip +from .basic_block import AutoEncoder, FCNet, MLPNet +from .matformer import * -TRUNCATED_NORMAL_STDDEV_FACTOR = np.asarray(.87962566103423978, dtype=np.float32) +__all__ = [ + "Nequip", 'AutoEncoder' +] - -def lecun_init(fan_in, initializer_name='linear'): - """lecun init""" - scale = 1.0 - if initializer_name == 'relu': - scale *= 2 - weight_init = TruncatedNormal(sigma=np.sqrt(scale / fan_in) / TRUNCATED_NORMAL_STDDEV_FACTOR) - return weight_init - - -def glorot_uniform(fan_in, fan_out, weight_shape): - """glorot uniform""" - limit = np.sqrt(6 / (fan_in + fan_out)) - return np.random.uniform(-limit, limit, size=weight_shape) +__all__.extend(matformer.__all__) diff --git a/MindChem/applications/matformer/mindchemistry/cell/activation.py b/MindChem/applications/matformer/mindchemistry/cell/activation.py new file mode 100644 index 000000000..d09b35831 --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/cell/activation.py @@ -0,0 +1,38 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""get activation function.""" +from __future__ import absolute_import + +from mindspore import ops +from mindspore.nn.layer import activation + +_activation = { + 'softmax': activation.Softmax, + 'logsoftmax': activation.LogSoftmax, + 'relu': activation.ReLU, + 'silu': activation.SiLU, + 'relu6': activation.ReLU6, + 'tanh': activation.Tanh, + 'gelu': activation.GELU, + 'fast_gelu': activation.FastGelu, + 'elu': activation.ELU, + 'sigmoid': activation.Sigmoid, + 'prelu': activation.PReLU, + 'leakyrelu': activation.LeakyReLU, + 'hswish': activation.HSwish, + 'hsigmoid': activation.HSigmoid, + 'logsigmoid': activation.LogSigmoid, + 'sin': ops.Sin +} diff --git a/MindChem/applications/matformer/mindchemistry/cell/basic_block.py b/MindChem/applications/matformer/mindchemistry/cell/basic_block.py new file mode 100644 index 000000000..6a83f67e0 --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/cell/basic_block.py @@ -0,0 +1,600 @@ +# Copyright 2023 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""basic""" +from __future__ import absolute_import + +from collections.abc import Sequence +from typing import Union + +from mindspore import nn +from mindspore.nn.layer import activation +from mindspore import ops, float16, float32, Tensor +from mindspore.common.initializer import Initializer + +from .activation import _activation + + +def _get_dropout(dropout_rate): + """ + Gets the dropout functions. + + Inputs: + dropout_rate (Union[int, float]): The dropout rate of the dropout function. + If dropout_rate was int or not in range (0,1], it would be rectify to closest float value. + + Returns: + Function, the dropout function. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_dropout + >>> dropout = get_dropout(0.5) + >>> dropout.set_train + Dropout + """ + dropout_rate = float(max(min(dropout_rate, 1.), 1e-7)) + return nn.Dropout(keep_prob=dropout_rate) + + +def _get_layernorm(channel, epsilon): + """ + Gets the layer normalization functions. + + Inputs: + channel (Union[int, list]): The normalized shape of the layer normalization function. + If channel was int, it would be wrap into a list. + epsilon (float): The epsilon of the layer normalization function. + + Returns: + Function, the layer normalization function. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_layernorm + >>> from mindspore import Tensor + >>> input_x = Tensor(np.array([[1.2, 0.1], [0.2, 3.2]], dtype=np.float32)) + >>> layernorm = get_layernorm([2], 1e-7) + >>> output = layernorm(input_x) + >>> print(output) + [[ 9.99999881e-01, -9.99999881e-01], + [-1.00000000e+00, 1.00000000e+00]] + """ + if isinstance(channel, int): + channel = [channel] + return nn.LayerNorm(channel, epsilon=epsilon) + + +def _get_activation(name): + """ + Gets the activation function. + + Inputs: + name (Union[str, None]): The name of the activation function. If name was None, it would return []. + + Returns: + Function, the activation function. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_activation + >>> from mindspore import Tensor + >>> input_x = Tensor(np.array([[1.2, 0.1], [0.2, 3.2]], dtype=np.float32)) + >>> sigmoid = _get_activation('sigmoid') + >>> output = sigmoid(input_x) + >>> print(output) + [[0.7685248 0.5249792 ] + [0.54983395 0.96083426]] + """ + if name is None: + return [] + if isinstance(name, str): + name = name.lower() + if name not in _activation: + return activation.get_activation(name) + return _activation.get(name)() + return name + + +def _get_layer_arg(arguments, index): + """ + Gets the argument of each network layers. + + Inputs: + arguments (Union[str, int, float, List, None]): The arguments of each layers. + If arguments was List return the argument at the index of the List. + index (int): The index of layer in the network + + Returns: + Argument of the indexed layer. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_layer_arg + >>> from mindspore import Tensor + >>> dropout_rate = _get_layer_arg([0.1, 0.2, 0.3], index=2) + >>> print(dropout_rate) + 0.2 + >>> dropout_rate = _get_layer_arg(0.2, index=2) + >>> print(dropout_rate) + 0.2 + """ + if isinstance(arguments, list): + if len(arguments) <= index: + if len(arguments) == 1: + return [] if arguments[0] is None else arguments[0] + return [] + return [] if arguments[index] is None else arguments[index] + return [] if arguments is None else arguments + + +def get_linear_block( + in_channels, + out_channels, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu' +): + """ + Gets the linear block list. + + Inputs: + in_channels (int): The number of input channel. + out_channels (int): The number of output channel. + weight_init (Union[str, float, mindspore.common.initializer]): The initializer of the weights of dense layer + has_bias (bool): The switch for whether dense layer has bias. + bias_init (Union[str, float, mindspore.common.initializer]): The initializer of the bias of dense layer + has_dropout (bool): The switch for whether linear block has a dropout layer. + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] + has_layernorm (bool): The switch for whether linear block has a layer normalization layer. + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + has_activation (bool): The switch for whether linear block has an activation layer. + act (Union[str, None]): The activation function in linear block + + Returns: + List of mindspore.nn.Cell, linear block list . + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import get_layer_arg + >>> from mindspore import Tensor + >>> dropout_rate = get_layer_arg([0.1, 0.2, 0.3], index=2) + >>> print(dropout_rate) + 0.2 + >>> dropout_rate = get_layer_arg(0.2, index=2) + >>> print(dropout_rate) + 0.2 + """ + dense = nn.Dense( + in_channels, out_channels, weight_init=weight_init, bias_init=bias_init, has_bias=has_bias, activation=None + ) + dropout = _get_dropout(dropout_rate) if (has_dropout is True) else [] + layernorm = _get_layernorm(out_channels, layernorm_epsilon) if (has_layernorm is True) else [] + act = _get_activation(act) if (has_activation is True) else [] + block_list = [dense, dropout, layernorm, act] + while [] in block_list: + block_list.remove([]) + return block_list + + +class FCNet(nn.Cell): + r""" + The Fully Connected Network. Applies a series of fully connected layers to the incoming data. + + Args: + channels (List): the list of numbers of channel of each fully connected layers. + weight_init (Union[str, float, mindspore.common.initializer, List]): initialize layer weights. + If weight_init was List, each element corresponds to each layer. Default: ``'normal'`` . + has_bias (Union[bool, List]): The switch for whether the dense layers has bias. + If has_bias was List, each element corresponds to each dense layer. Default: ``True`` . + bias_init (Union[str, float, mindspore.common.initializer, List]): The initializer of the bias of dense + layer. If bias_init was List, each element corresponds to each dense layer. Default: ``'zeros'`` . + has_dropout (Union[bool, List]): The switch for whether linear block has a dropout layer. + If has_dropout was List, each element corresponds to each layer. Default: ``False`` . + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] + If dropout_rate was List, each element corresponds to each dropout layer. Default: ``0.5`` . + has_layernorm (Union[bool, List]): The switch for whether linear block has a layer normalization layer. + If has_layernorm was List, each element corresponds to each layer. Default: ``False`` . + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + If layernorm_epsilon was List, each element corresponds to each layer normalization layer. + Default: ``1e-7`` . + has_activation (Union[bool, List]): The switch for whether linear block has an activation layer. + If has_activation was List, each element corresponds to each layer. Default: ``True`` . + act (Union[str, None, List]): The activation function in linear block. + If act was List, each element corresponds to each activation layer. Default: ``'relu'`` . + + Inputs: + - **input** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Outputs: + - **output** (Tensor) - The shape of Tensor is :math:`(*, channels[-1])`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import FCNet + >>> from mindspore import Tensor + >>> inputs = Tensor(np.array([[180, 234, 154], [244, 48, 247]], np.float32)) + >>> net = FCNet([3, 16, 32, 16, 8]) + >>> output = net(inputs) + >>> print(output.shape) + (2, 8) + + """ + + def __init__( + self, + channels, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu' + ): + super().__init__() + self.channels = channels + self.weight_init = weight_init + self.has_bias = has_bias + self.bias_init = bias_init + self.has_dropout = has_dropout + self.dropout_rate = dropout_rate + self.has_layernorm = has_layernorm + self.layernorm_epsilon = layernorm_epsilon + self.has_activation = has_activation + self.activation = act + self.network = nn.SequentialCell(self._create_network()) + + def _create_network(self): + """ create the network """ + cell_list = [] + for i in range(len(self.channels) - 1): + cell_list += get_linear_block( + self.channels[i], + self.channels[i + 1], + weight_init=_get_layer_arg(self.weight_init, i), + has_bias=_get_layer_arg(self.has_bias, i), + bias_init=_get_layer_arg(self.bias_init, i), + has_dropout=_get_layer_arg(self.has_dropout, i), + dropout_rate=_get_layer_arg(self.dropout_rate, i), + has_layernorm=_get_layer_arg(self.has_layernorm, i), + layernorm_epsilon=_get_layer_arg(self.layernorm_epsilon, i), + has_activation=_get_layer_arg(self.has_activation, i), + act=_get_layer_arg(self.activation, i) + ) + return cell_list + + def construct(self, x): + return self.network(x) + + +class MLPNet(nn.Cell): + r""" + The MLPNet Network. Applies a series of fully connected layers to the incoming data among which hidden layers have + same number of channels. + + Args: + in_channels (int): the number of input layer channel. + out_channels (int): the number of output layer channel. + layers (int): the number of layers. + neurons (int): the number of channels of hidden layers. + weight_init (Union[str, float, mindspore.common.initializer, List]): initialize layer weights. + If weight_init was List, each element corresponds to each layer. Default: ``'normal'`` . + has_bias (Union[bool, List]): The switch for whether the dense layers has bias. + If has_bias was List, each element corresponds to each dense layer. Default: ``True`` . + bias_init (Union[str, float, mindspore.common.initializer, List]): The initializer of the bias of dense + layer. If bias_init was List, each element corresponds to each dense layer. Default: ``'zeros'`` . + has_dropout (Union[bool, List]): The switch for whether linear block has a dropout layer. + If has_dropout was List, each element corresponds to each layer. Default: ``False`` . + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] . + If dropout_rate was List, each element corresponds to each dropout layer. Default: ``0.5`` . + has_layernorm (Union[bool, List]): The switch for whether linear block has a layer normalization layer. + If has_layernorm was List, each element corresponds to each layer. Default: ``False`` . + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + If layernorm_epsilon was List, each element corresponds to each layer normalization layer. + Default: ``1e-7`` . + has_activation (Union[bool, List]): The switch for whether linear block has an activation layer. + If has_activation was List, each element corresponds to each layer. Default: ``True`` . + act (Union[str, None, List]): The activation function in linear block. + If act was List, each element corresponds to each activation layer. Default: ``'relu'`` . + + Inputs: + - **input** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Outputs: + - **output** (Tensor) - The shape of Tensor is :math:`(*, channels[-1])`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import FCNet + >>> from mindspore import Tensor + >>> inputs = Tensor(np.array([[180, 234, 154], [244, 48, 247]], np.float32)) + >>> net = MLPNet(in_channels=3, out_channels=8, layers=5, neurons=32) + >>> output = net(inputs) + >>> print(output.shape) + (2, 8) + + """ + + def __init__( + self, + in_channels, + out_channels, + layers, + neurons, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu' + ): + super().__init__() + self.channels = (in_channels,) + (layers - 2) * \ + (neurons,) + (out_channels,) + self.network = FCNet( + channels=self.channels, + weight_init=weight_init, + has_bias=has_bias, + bias_init=bias_init, + has_dropout=has_dropout, + dropout_rate=dropout_rate, + has_layernorm=has_layernorm, + layernorm_epsilon=layernorm_epsilon, + has_activation=has_activation, + act=act + ) + + def construct(self, x): + return self.network(x) + + +class MLPMixPrecision(nn.Cell): + """MLPMixPrecision + """ + + def __init__( + self, + input_dim: int, + hidden_dims: Sequence, + short_cut=False, + batch_norm=False, + activation_fn='relu', + has_bias=False, + weight_init: Union[Initializer, str] = 'xavier_uniform', + bias_init: Union[Initializer, str] = 'zeros', + dropout=0, + dtype=float32 + ): + super().__init__() + self.dtype = dtype + self.div = ops.Div() + + self.dims = [input_dim] + hidden_dims + self.short_cut = short_cut + self.nonlinear_const = 1.0 + if isinstance(activation_fn, str): + self.activation = _activation.get(activation_fn)() + if activation_fn is not None and activation_fn == 'silu': + self.nonlinear_const = 1.679177 + else: + self.activation = activation_fn + self.dropout = None + if dropout: + self.dropout = nn.Dropout(dropout) + fcs = [ + nn.Dense(dim, self.dims[i + 1], weight_init=weight_init, bias_init=bias_init, + has_bias=has_bias).to_float(self.dtype) for i, dim in enumerate(self.dims[:-1]) + ] + self.layers = nn.CellList(fcs) + self.batch_norms = None + if batch_norm: + bns = [nn.BatchNorm1d(dim) for dim in self.dims[1:-1]] + self.batch_norms = nn.CellList(bns) + + def construct(self, inputs): + """construct + + Args: + inputs: inputs + + Returns: + inputs + """ + hidden = inputs + norm_from_last = 1.0 + for i, layer in enumerate(self.layers): + sqrt_dim = ops.sqrt(Tensor(float(self.dims[i]))) + layer_hidden = layer(hidden) + if self.dtype == float16: + layer_hidden = layer_hidden.astype(float16) + hidden = self.div(layer_hidden * norm_from_last, sqrt_dim) + norm_from_last = self.nonlinear_const + if i < len(self.layers) - 1: + if self.batch_norms is not None: + x = hidden.flatten(0, -2) + hidden = self.batch_norms[i](x).view_as(hidden) + if self.activation is not None: + hidden = self.activation(hidden) + if self.dropout is not None: + hidden = self.dropout(hidden) + if self.short_cut and hidden.shape == hidden.shape: + hidden += inputs + return hidden + + +class AutoEncoder(nn.Cell): + r""" + The AutoEncoder Network. + Applies an encoder to get the latent code and applies a decoder to get the reconstruct data. + + Args: + channels (list): The number of channels of each encoder and decoder layer. + weight_init (Union[str, float, mindspore.common.initializer, List]): initialize layer parameters. + If weight_init was List, each element corresponds to each layer. Default: ``'normal'`` . + has_bias (Union[bool, List]): The switch for whether the dense layers has bias. + If has_bias was List, each element corresponds to each dense layer. Default: ``True`` . + bias_init (Union[str, float, mindspore.common.initializer, List]): initialize layer parameters. + If bias_init was List, each element corresponds to each dense layer. Default: ``'zeros'`` . + has_dropout (Union[bool, List]): The switch for whether linear block has a dropout layer. + If has_dropout was List, each element corresponds to each layer. Default: ``False`` . + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] + If dropout_rate was List, each element corresponds to each dropout layer. Default: ``0.5`` . + has_layernorm (Union[bool, List]): The switch for whether linear block has a layer normalization layer. + If has_layernorm was List, each element corresponds to each layer. Default: ``False`` . + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + If layernorm_epsilon was List, each element corresponds to each layer normalization layer. + Default: ``1e-7`` . + has_activation (Union[bool, List]): The switch for whether linear block has an activation layer. + If has_activation was List, each element corresponds to each layer. Default: ``True`` . + act (Union[str, None, List]): The activation function in linear block. + If act was List, each element corresponds to each activation layer. Default: ``'relu'`` . + out_act (Union[None, str, mindspore.nn.Cell]): The activation function to output layer. Default: ``None`` . + + Inputs: + - **x** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Outputs: + - **latents** (Tensor) - The shape of Tensor is :math:`(*, channels[-1])`. + - **x_recon** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindchemistry import AutoEncoder + >>> from mindspore import Tensor + >>> inputs = Tensor(np.array([[180, 234, 154], [244, 48, 247]], np.float32)) + >>> net = AutoEncoder([3, 6, 2]) + >>> output = net(inputs) + >>> print(output[0].shape, output[1].shape) + (2, 2) (2, 3) + + """ + + def __init__( + self, + channels, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu', + out_act=None + ): + super().__init__() + self.channels = channels + self.weight_init = weight_init + self.bias_init = bias_init + self.has_bias = has_bias + self.has_dropout = has_dropout + self.dropout_rate = dropout_rate + self.has_layernorm = has_layernorm + self.has_activation = has_activation + self.layernorm_epsilon = layernorm_epsilon + self.activation = act + self.output_activation = out_act + self.encoder = nn.SequentialCell(self._create_encoder()) + self.decoder = nn.SequentialCell(self._create_decoder()) + + def _create_encoder(self): + """ create the network encoder """ + encoder_cell_list = [] + for i in range(len(self.channels) - 1): + encoder_cell_list += get_linear_block( + self.channels[i], + self.channels[i + 1], + weight_init=_get_layer_arg(self.weight_init, i), + has_bias=_get_layer_arg(self.has_bias, i), + bias_init=_get_layer_arg(self.bias_init, i), + has_dropout=_get_layer_arg(self.has_dropout, i), + dropout_rate=_get_layer_arg(self.dropout_rate, i), + has_layernorm=_get_layer_arg(self.has_layernorm, i), + layernorm_epsilon=_get_layer_arg(self.layernorm_epsilon, i), + has_activation=_get_layer_arg(self.has_activation, i), + act=_get_layer_arg(self.activation, i) + ) + return encoder_cell_list + + def _create_decoder(self): + """ create the network decoder """ + decoder_channels = self.channels[::-1] + decoder_weight_init = self.weight_init[::-1] if isinstance(self.weight_init, list) else self.weight_init + decoder_bias_init = self.bias_init[::-1] if isinstance(self.bias_init, list) else self.bias_init + decoder_cell_list = [] + for i in range(len(decoder_channels) - 1): + decoder_cell_list += get_linear_block( + decoder_channels[i], + decoder_channels[i + 1], + weight_init=_get_layer_arg(decoder_weight_init, i), + has_bias=_get_layer_arg(self.has_bias, i), + bias_init=_get_layer_arg(decoder_bias_init, i), + has_dropout=_get_layer_arg(self.has_dropout, i), + dropout_rate=_get_layer_arg(self.dropout_rate, i), + has_layernorm=_get_layer_arg(self.has_layernorm, i), + layernorm_epsilon=_get_layer_arg(self.layernorm_epsilon, i), + has_activation=_get_layer_arg(self.has_activation, i), + act=_get_layer_arg(self.activation, i) + ) + if self.output_activation is not None: + decoder_cell_list.append(_get_activation(self.output_activation)) + return decoder_cell_list + + def encode(self, x): + return self.encoder(x) + + def decode(self, z): + return self.decoder(z) + + def construct(self, x): + latents = self.encode(x) + x_recon = self.decode(latents) + return x_recon, latents diff --git a/MindChem/applications/nequip/models/convolution.py b/MindChem/applications/matformer/mindchemistry/cell/convolution.py old mode 100755 new mode 100644 similarity index 99% rename from MindChem/applications/nequip/models/convolution.py rename to MindChem/applications/matformer/mindchemistry/cell/convolution.py index 4525db92b..1fa168da2 --- a/MindChem/applications/nequip/models/convolution.py +++ b/MindChem/applications/matformer/mindchemistry/cell/convolution.py @@ -16,7 +16,7 @@ from mindspore import nn, ops, float32 from mindscience.e3nn.o3 import TensorProduct, Irreps, Linear from mindscience.e3nn.nn import FullyConnectedNet -from .graph import AggregateEdgeToNode +from ..graph.graph import AggregateEdgeToNode softplus = ops.Softplus() diff --git a/MindChem/applications/nequip/models/embedding.py b/MindChem/applications/matformer/mindchemistry/cell/embedding.py old mode 100755 new mode 100644 similarity index 100% rename from MindChem/applications/nequip/models/embedding.py rename to MindChem/applications/matformer/mindchemistry/cell/embedding.py diff --git a/MindChem/applications/matformer/mindchemistry/cell/matformer/__init__.py b/MindChem/applications/matformer/mindchemistry/cell/matformer/__init__.py new file mode 100644 index 000000000..23a86799d --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/cell/matformer/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""initialization for matformer""" + +from .matformer import Matformer + +__all__ = ['Matformer'] diff --git a/MindChem/applications/matformer/models/matformer.py b/MindChem/applications/matformer/mindchemistry/cell/matformer/matformer.py similarity index 95% rename from MindChem/applications/matformer/models/matformer.py rename to MindChem/applications/matformer/mindchemistry/cell/matformer/matformer.py index b83bfadcf..61c0a11ba 100644 --- a/MindChem/applications/matformer/models/matformer.py +++ b/MindChem/applications/matformer/mindchemistry/cell/matformer/matformer.py @@ -15,9 +15,9 @@ """Matformer""" from mindspore import nn, ops import mindspore as ms -from models.graph.graph import AggregateNodeToGlobal -from models.utils import RBFExpansion -from models.transformer import MatformerConv, Silu +from mindchemistry.graph.graph import AggregateNodeToGlobal +from mindchemistry.cell.matformer.utils import RBFExpansion +from mindchemistry.cell.matformer.transformer import MatformerConv, Silu class Matformer(nn.Cell): diff --git a/MindChem/applications/matformer/models/transformer.py b/MindChem/applications/matformer/mindchemistry/cell/matformer/transformer.py similarity index 97% rename from MindChem/applications/matformer/models/transformer.py rename to MindChem/applications/matformer/mindchemistry/cell/matformer/transformer.py index 9425b5547..fce3e9ccd 100644 --- a/MindChem/applications/matformer/models/transformer.py +++ b/MindChem/applications/matformer/mindchemistry/cell/matformer/transformer.py @@ -15,8 +15,8 @@ """transformer file""" import mindspore as ms from mindspore import ops, Tensor, nn -from models.graph.graph import LiftNodeToEdge, AggregateEdgeToNode -from models.graph.normlization import BatchNormMask +from mindchemistry.graph.graph import LiftNodeToEdge, AggregateEdgeToNode +from mindchemistry.graph.normlization import BatchNormMask class Silu(nn.Cell): diff --git a/MindChem/applications/matformer/models/utils.py b/MindChem/applications/matformer/mindchemistry/cell/matformer/utils.py similarity index 100% rename from MindChem/applications/matformer/models/utils.py rename to MindChem/applications/matformer/mindchemistry/cell/matformer/utils.py diff --git a/MindChem/applications/matformer/mindchemistry/cell/message_passing.py b/MindChem/applications/matformer/mindchemistry/cell/message_passing.py new file mode 100644 index 000000000..901cec030 --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/cell/message_passing.py @@ -0,0 +1,165 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""MessagePassing""" +from mindspore import nn, ops, float32 + +from mindscience.e3nn.o3 import Irreps +from mindscience.e3nn.nn import Gate, NormActivation +from .convolution import Convolution, shift_softplus + +acts = { + "abs": ops.abs, + "tanh": ops.tanh, + "ssp": shift_softplus, + "silu": ops.silu, +} + + +class Compose(nn.Cell): + def __init__(self, first, second): + super().__init__() + self.first = first + self.second = second + + def construct(self, *inputs): + x = self.first(*inputs) + x = self.second(x) + return x + + +class MessagePassing(nn.Cell): + """MessagePassing""" + # pylint: disable=W0102 + def __init__( + self, + irreps_node_input, + irreps_node_attr, + irreps_node_hidden, + irreps_node_output, + irreps_edge_attr, + irreps_edge_scalars, + convolution_kwargs={}, + num_layers=3, + resnet=False, + nonlin_type="gate", + nonlin_scalars={"e": "ssp", "o": "tanh"}, + nonlin_gates={"e": "ssp", "o": "abs"}, + dtype=float32, + ncon_dtype=float32 + ): + super().__init__() + if nonlin_type not in ('gate', 'norm'): + raise ValueError(f"Unexpected nonlin_type {nonlin_type}.") + + nonlin_scalars = { + 1: nonlin_scalars["e"], + -1: nonlin_scalars["o"], + } + nonlin_gates = { + 1: nonlin_gates["e"], + -1: nonlin_gates["o"], + } + + self.irreps_node_input = Irreps(irreps_node_input) + self.irreps_node_hidden = Irreps(irreps_node_hidden) + self.irreps_node_output = Irreps(irreps_node_output) + self.irreps_node_attr = Irreps(irreps_node_attr) + self.irreps_edge_attr = Irreps(irreps_edge_attr) + self.irreps_edge_scalars = Irreps(irreps_edge_scalars) + + irreps_node = self.irreps_node_input + irreps_prev = irreps_node + self.layers = nn.CellList() + self.resnets = [] + + for _ in range(num_layers): + tmp_irreps = irreps_node * self.irreps_edge_attr + + irreps_scalars = Irreps( + [ + (mul, ir) + for mul, ir in self.irreps_node_hidden + if ir.l == 0 and ir in tmp_irreps + ] + ).simplify() + irreps_gated = Irreps( + [ + (mul, ir) + for mul, ir in self.irreps_node_hidden + if ir.l > 0 and ir in tmp_irreps + ] + ) + + if nonlin_type == "gate": + ir = "0e" if Irreps("0e") in tmp_irreps else "0o" + irreps_gates = Irreps([(mul, ir) + for mul, _ in irreps_gated]).simplify() + + nonlinear = Gate( + irreps_scalars, + [acts[nonlin_scalars[ir.p]] for _, ir in irreps_scalars], + irreps_gates, + [acts[nonlin_gates[ir.p]] for _, ir in irreps_gates], + irreps_gated, + dtype=dtype, + ncon_dtype=ncon_dtype + ) + + conv_irreps_out = nonlinear.irreps_in + else: + conv_irreps_out = (irreps_scalars + irreps_gated).simplify() + + nonlinear = NormActivation( + irreps_in=conv_irreps_out, + act=acts[nonlin_scalars[1]], + normalize=True, + epsilon=1e-8, + bias=False, + dtype=dtype, + ncon_dtype=ncon_dtype + ) + + conv = Convolution( + irreps_node_input=irreps_node, + irreps_node_attr=self.irreps_node_attr, + irreps_node_output=conv_irreps_out, + irreps_edge_attr=self.irreps_edge_attr, + irreps_edge_scalars=self.irreps_edge_scalars, + **convolution_kwargs, + dtype=dtype, + ncon_dtype=ncon_dtype + ) + irreps_node = nonlinear.irreps_out + + self.layers.append(Compose(conv, nonlinear)) + + if irreps_prev == irreps_node and resnet: + self.resnets.append(True) + else: + self.resnets.append(False) + irreps_prev = irreps_node + + def construct(self, node_input, node_attr, edge_src, edge_dst, edge_attr, edge_scalars): + """construct""" + layer_in = node_input + for i in enumerate(self.layers): + layer_out = self.layers[i]( + layer_in, node_attr, edge_src, edge_dst, edge_attr, edge_scalars) + + if self.resnets[i]: + layer_in = layer_out + layer_in + else: + layer_in = layer_out + return layer_in diff --git a/MindChem/applications/nequip/models/nequip.py b/MindChem/applications/matformer/mindchemistry/cell/nequip.py similarity index 99% rename from MindChem/applications/nequip/models/nequip.py rename to MindChem/applications/matformer/mindchemistry/cell/nequip.py index c8159c356..c1876b333 100644 --- a/MindChem/applications/nequip/models/nequip.py +++ b/MindChem/applications/matformer/mindchemistry/cell/nequip.py @@ -17,7 +17,7 @@ from mindspore import nn, float32, int32 from mindscience.e3nn.o3 import Irreps, SphericalHarmonics, Linear from mindscience.e3nn.nn import OneHot from mindscience.e3nn.utils import radius_graph -from .graph import AggregateNodeToGlobal +from ..graph.graph import AggregateNodeToGlobal from .message_passing import MessagePassing from .embedding import RadialEdgeEmbedding diff --git a/MindChem/applications/matformer/mindchemistry/graph/__init__.py b/MindChem/applications/matformer/mindchemistry/graph/__init__.py new file mode 100644 index 000000000..1ae7d9a34 --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/graph/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""graph""" diff --git a/MindChem/applications/matformer/models/graph/dataloader.py b/MindChem/applications/matformer/mindchemistry/graph/dataloader.py similarity index 100% rename from MindChem/applications/matformer/models/graph/dataloader.py rename to MindChem/applications/matformer/mindchemistry/graph/dataloader.py diff --git a/MindChem/applications/matformer/models/graph/graph.py b/MindChem/applications/matformer/mindchemistry/graph/graph.py similarity index 100% rename from MindChem/applications/matformer/models/graph/graph.py rename to MindChem/applications/matformer/mindchemistry/graph/graph.py diff --git a/MindChem/applications/diffcsp/models/loss.py b/MindChem/applications/matformer/mindchemistry/graph/loss.py similarity index 100% rename from MindChem/applications/diffcsp/models/loss.py rename to MindChem/applications/matformer/mindchemistry/graph/loss.py diff --git a/MindChem/applications/matformer/models/graph/normlization.py b/MindChem/applications/matformer/mindchemistry/graph/normlization.py similarity index 100% rename from MindChem/applications/matformer/models/graph/normlization.py rename to MindChem/applications/matformer/mindchemistry/graph/normlization.py diff --git a/MindChem/applications/matformer/mindchemistry/so2_conv/__init__.py b/MindChem/applications/matformer/mindchemistry/so2_conv/__init__.py new file mode 100644 index 000000000..e5542a477 --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/so2_conv/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +init file +""" +from .so3 import SO3Rotation +from .so2 import SO2Convolution diff --git a/MindChem/applications/matformer/mindchemistry/so2_conv/init_edge_rot_mat.py b/MindChem/applications/matformer/mindchemistry/so2_conv/init_edge_rot_mat.py new file mode 100644 index 000000000..a05a2264b --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/so2_conv/init_edge_rot_mat.py @@ -0,0 +1,64 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +file to get rotating matrix from edge distance vector +""" +from mindspore import ops +import mindspore.numpy as ms_np + + +def init_edge_rot_mat(edge_distance_vec): + """ + get rotating matrix from edge distance vector + """ + epsilon = 0.00000001 + edge_vec_0 = edge_distance_vec + edge_vec_0_distance = ops.sqrt(ops.maximum(ops.sum(edge_vec_0 ** 2, dim=1), epsilon)) + # Make sure the atoms are far enough apart + norm_x = ops.div(edge_vec_0, edge_vec_0_distance.view(-1, 1)) + edge_vec_2 = ops.rand_like(edge_vec_0) - 0.5 + + edge_vec_2 = ops.div(edge_vec_2, ops.sqrt(ops.maximum(ops.sum(edge_vec_2 ** 2, dim=1), epsilon)).view(-1, 1)) + # Create two rotated copies of the random vectors in case the random vector is aligned with norm_x + # With two 90 degree rotated vectors, at least one should not be aligned with norm_x + edge_vec_2b = edge_vec_2.copy() + edge_vec_2b[:, 0] = -edge_vec_2[:, 1] + edge_vec_2b[:, 1] = edge_vec_2[:, 0] + edge_vec_2c = edge_vec_2.copy() + edge_vec_2c[:, 1] = -edge_vec_2[:, 2] + edge_vec_2c[:, 2] = edge_vec_2[:, 1] + vec_dot_b = ops.abs(ops.sum(edge_vec_2b * norm_x, dim=1)).view(-1, 1) + vec_dot_c = ops.abs(ops.sum(edge_vec_2c * norm_x, dim=1)).view(-1, 1) + vec_dot = ops.abs(ops.sum(edge_vec_2 * norm_x, dim=1)).view(-1, 1) + edge_vec_2 = ops.where(ops.broadcast_to(ops.gt(vec_dot, vec_dot_b), edge_vec_2b.shape), edge_vec_2b, edge_vec_2) + vec_dot = ops.abs(ops.sum(edge_vec_2 * norm_x, dim=1)).view(-1, 1) + edge_vec_2 = ops.where(ops.broadcast_to(ops.gt(vec_dot, vec_dot_c), edge_vec_2c.shape), edge_vec_2c, edge_vec_2) + vec_dot = ops.abs(ops.sum(edge_vec_2 * norm_x, dim=1)) + # Check the vectors aren't aligned + + norm_z = ms_np.cross(norm_x, edge_vec_2, axis=1) + norm_z = ops.div(norm_z, ops.sqrt(ops.maximum(ops.sum(norm_z ** 2, dim=1, keepdim=True), epsilon))) + norm_z = ops.div(norm_z, ops.sqrt(ops.maximum(ops.sum(norm_z ** 2, dim=1), epsilon)).view(-1, 1)) + + norm_y = ms_np.cross(norm_x, norm_z, axis=1) + norm_y = ops.div(norm_y, ops.sqrt(ops.maximum(ops.sum(norm_y ** 2, dim=1, keepdim=True), epsilon))) + # Construct the 3D rotation matrix + norm_x = norm_x.view(-1, 3, 1) + norm_y = -norm_y.view(-1, 3, 1) + norm_z = norm_z.view(-1, 3, 1) + edge_rot_mat_inv = ops.cat([norm_z, norm_x, norm_y], axis=2) + + edge_rot_mat = ops.swapaxes(edge_rot_mat_inv, 1, 2) + return edge_rot_mat diff --git a/MindChem/applications/matformer/mindchemistry/so2_conv/jd.pkl b/MindChem/applications/matformer/mindchemistry/so2_conv/jd.pkl new file mode 100644 index 0000000000000000000000000000000000000000..1b762ad4369564e2f21b808850cc8013e6e41385 GIT binary patch literal 9925 zcmcIq4RBP|6}}+^l0qOM4QmT_sGvls0WH-G4ZA=R&}>7NA2T`k zy?5?+zWd#C&U<;GqVt*^_b41GRj#5L#rdVPeI*5{(|komzT(ufg5pwNiB;8Qq8Y5V z?tRvJ#;S^$mpf~2fmM}UJhy0ex%SpmissHLn~_^ml3Q+7b)Q;NFwIw7T2?Z5TA8(4 zPk^IU)wMX^xU9CkYN?eGm1ixtCRi!nDEYU{DvYa&$uBFPT_BS>O&?}et}yqbtD@4a zoSdBO8~VTxuPU?hDlbMx8t09} zf=6dY-5hoNd>__ux$|Ga$9RpzwbsY(4aBj?n`{J_Bxak&AWNK8O@v8(0swVA43mc))3(9eGk#z za1Kdqq2TCPdnyLFbLw9K-V2+&z?aD(c<^xq7;w6J`fK1(hj|-&0fXycT>5ncTl-?q zhvyxHedFY>Jk*8jVBC)25>Kgd(BS1QNBz{9x^NvG!4-buFL-8N#Rvy=81IcYIkBpNlq=+WL9;&AWlW>OT)4-sBBy-QufzaY$~D#I?bL`@<)0k_U6z!gO-|;C+{Y z`Kjwg$vaS2kJteZDY27hw+p$?{4!hBtqlEeOlejGwwV0fx5Jh-nIwV$9PKp2wG3xaEG6H z%{=42b06A9ZU2twDPFQZu@XvJ zq;@|+eQn$l@~4-*fgMg9l-!69$mVOZ|f$G(0Z<`1t?Kk{#XrR{q#zV^i0 zA3XR+@Ze18Kg?g|5BHDy(f4*N*KfyP@4lqBP7!6(6 zUv=H#`l7T3Smq)3ojP+}#IaW#j?q)dE&Xoq7PNDtt6?wg{SvnSkntYY1@n;mPMx`~ zaIhf;`6%@Tc)YSd347O&wa@VL#MZG+Sr^Ph?mKlRW;iVMAs?O3;Is1ahp@M2GpD%O zH&~~v3+5sBJsf<9!Tq8>>I38)&=vBnBlOJKZ0{f@0VE7N38G> ztKUO^>fk*z*_&*7dQ%rO%jwSn%|Aoj><7Dn$6H;?{W3SL$8zMo{`MEr+Y@l@p@PTY z!#VpK%=5T)?Aze(GEm8eSmTzN z=9}y<795Wl&pj=#f~Qj7LCjr!ew!|w?(v#$0{a_w1nw(E{m4(L7TnXCV=V)&nI0qa z!+mr)HWsly{LI8Gxpp1rDb+i6NvurQP-rJ(?l1KtKczOi<$gl;*RV1U_whvQ9`M=M z@I3fki*174I4BAFe!l$&uo9Q~$Ni;#Hoxl;;<*5v#x>T$ZXIRbk17||y|U0mWkz1MtqS=RSXiz6j><6qj<}>pp5GT)kyt~B3Jw3cr6 zQ}!$N1J)b!ITAdXf81Z{M}A7pa!cI#zd5;+{hs}l{fhk{wBEwbRo91LO8$wJ{S_SEGchyPOmh0Ovi!S@aebUL&T3Cdl&d@Y3YkP(cjUF@ag`d`TL934H$c-*nbbO=Y)5(s@Z%W)p~(L?rS{|d)!BNVLhk2Rpa{NoEhLR z(aBd2;j^yrZp%xcMJmgmdww zzBb=<+#6Wm2#l6(rdNn>q>k-VYZ0sQz^kxsD-Il%9x{J7^d0&K&SzF9g1_)!K9Cdn z>T~!kx%>xU@qQ$1O}6>QmN~{~_3In65z{pw1JBw6JKqEEMaPB(bAkEGe4xHI-;{{+ zZhWZ*F&cgUhJCr_9PD3OPs0vq{s4BvoY!Gl$E*wH8TXAk5H}K9+n6cPR<+!K&3gX{ zzua3 0 coefficients + for m in range(self.global_max_order): + if m == 0: + continue + x_m = m_list_merge[m] + x_m = x_m.reshape(num_edges, 2, -1) + x_m = self.so2_m_conv[m - 1](x_m) + out.append(x_m) + + ###################### start fill 0 ###################### + if self.max_order_out + 1 > len(m_list_merge): + for m in range(len(m_list_merge), self.max_order_out + 1): + extra_zero = ops.zeros( + (num_edges, 2, int(self.m_shape_dict_out.get(m, None) / 2))) + out.append(extra_zero) + ###################### finish fill 0 ###################### + + ###################### start _l_primary ######################### + l_primary_list_0 = [] + l_primary_list_left = [] + l_primary_list_right = [] + + for _ in range(self.irreps_out_length): + l_primary_list_0.append([]) + l_primary_list_left.append([]) + l_primary_list_right.append([]) + + m_0 = out[0] + offset = 0 + index = 0 + + for key_val in self.irreps_out_data: + key = key_val[0] + value = key_val[1] + if key >= 0: + l_primary_list_0[index].append( + ops.unsqueeze(m_0[:, offset:offset + value], -1)) + offset = offset + value + index = index + 1 + + for m in range(1, len(out)): + right = out[m][:, 1] + offset = 0 + index = 0 + + for key_val in self.irreps_out_data: + key = key_val[0] + value = key_val[1] + if key >= m: + l_primary_list_right[index].append( + ops.unsqueeze(right[:, offset:offset + value], -1)) + offset = offset + value + index = index + 1 + + for m in range(len(out) - 1, 0, -1): + left = out[m][:, 0] + offset = 0 + index = 0 + + for key_val in self.irreps_out_data: + key = key_val[0] + value = key_val[1] + if key >= m: + l_primary_list_left[index].append( + ops.unsqueeze(left[:, offset:offset + value], -1)) + offset = offset + value + index = index + 1 + + l_primary_list = [] + for i in range(self.irreps_out_length): + if i == 0: + tmp = ops.cat(l_primary_list_0[i], -1) + l_primary_list.append(tmp) + else: + tmp = ops.cat( + (ops.cat((ops.cat(l_primary_list_left[i], + -1), ops.cat(l_primary_list_0[i], -1)), + -1), ops.cat(l_primary_list_right[i], -1)), -1) + l_primary_list.append(tmp) + + ##################### finish _l_primary ######################### + return tuple(l_primary_list) diff --git a/MindChem/applications/matformer/mindchemistry/so2_conv/so3.py b/MindChem/applications/matformer/mindchemistry/so2_conv/so3.py new file mode 100644 index 000000000..0ffae5a5f --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/so2_conv/so3.py @@ -0,0 +1,156 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +so3 file +""" +import mindspore as ms +from mindspore import nn, ops, vmap, jit_class +from mindspore.numpy import tensordot +from mindscience.e3nn import o3 +from mindscience.e3nn.o3 import Irreps + +from .wigner import wigner_D + + +class SO3Embedding(nn.Cell): + """ + SO3Embedding class + """ + + def __init__(self): + self.embedding = None + + def _rotate(self, so3rotation, lmax_list, max_list): + """ + SO3Embedding rotate + """ + embedding_rotate = so3rotation[0].rotate(self.embedding, lmax_list[0], + max_list[0]) + self.embedding = embedding_rotate + + def _rotate_inv(self, so3rotation): + """ + SO3Embedding rotate inverse + """ + embedding_rotate = so3rotation[0].rotate_inv(self.embedding, + self.lmax_list[0], + self.mmax_list[0]) + self.embedding = embedding_rotate + + +@jit_class +class SO3Rotation: + """ + SO3_Rotation class + """ + + def __init__(self, lmax, irreps_in, irreps_out): + self.lmax = lmax + self.irreps_in1 = Irreps(irreps_in) + self.irreps_out = Irreps(irreps_out) + self.tensordot_vmap = vmap(tensordot, (0, 0, None), 0) + + @staticmethod + def narrow(inputs, axis, start, length): + """ + SO3_Rotation narrow class + """ + begins = [0] * inputs.ndim + begins[axis] = start + + sizes = list(inputs.shape) + + sizes[axis] = length + res = ops.slice(inputs, begins, sizes) + return res + + @staticmethod + def rotation_to_wigner_d_matrix(edge_rot_mat, start_lmax, end_lmax): + """ + SO3_Rotation rotation_to_wigner_d_matrix + """ + x = edge_rot_mat @ ms.Tensor([0.0, 1.0, 0.0]) + alpha, beta = o3.xyz_to_angles(x) + rvalue = (ops.swapaxes( + o3.angles_to_matrix(alpha, beta, ops.zeros_like(alpha)), -1, -2) + @ edge_rot_mat) + gamma = ops.atan2(rvalue[..., 0, 2], rvalue[..., 0, 0]) + + block_list = [] + for lmax in range(start_lmax, end_lmax + 1): + block = wigner_D(lmax, alpha, beta, gamma).astype(ms.float32) + block_list.append(block) + return block_list + + def set_wigner(self, rot_mat3x3): + """ + SO3_Rotation set_wigner + """ + wigner = self.rotation_to_wigner_d_matrix(rot_mat3x3, 0, self.lmax) + wigner_inv = [] + length = len(wigner) + for i in range(length): + wigner_inv.append(ops.swapaxes(wigner[i], 1, 2)) + return tuple(wigner), tuple(wigner_inv) + + def rotate(self, embedding, wigner): + """ + SO3_Rotation rotate + """ + res = [] + batch_shape = embedding.shape[:-1] + for (s, l), mir in zip(self.irreps_in1.slice_tuples, + self.irreps_in1.data): + v_slice = self.narrow(embedding, -1, s, l) + if embedding.ndim == 1: + res.append((v_slice.reshape((1,) + batch_shape + + (mir.mul, mir.ir.dim)), mir.ir)) + else: + res.append( + (v_slice.reshape(batch_shape + (mir.mul, mir.ir.dim)), + mir.ir)) + rotate_data_list = [] + for data, ir in res: + self.tensordot_vmap(data.astype(ms.float16), + wigner[ir.l].astype(ms.float16), ([1], [1])) + rotate_data = self.tensordot_vmap(data.astype(ms.float16), + wigner[ir.l].astype(ms.float16), + ((1), (1))).astype(ms.float32) + rotate_data_list.append(rotate_data) + return tuple(rotate_data_list) + + def rotate_inv(self, embedding, wigner_inv): + """ + SO3_Rotation rotate_inv + """ + res = [] + batch_shape = embedding[0].shape[0:1] + index = 0 + for (_, _), mir in zip(self.irreps_out.slice_tuples, + self.irreps_out.data): + v_slice = embedding[index] + if embedding[0].ndim == 1: + res.append((v_slice, mir.ir)) + else: + res.append((v_slice, mir.ir)) + index = index + 1 + rotate_back_data_list = [] + for data, ir in res: + rotate_back_data = self.tensordot_vmap( + data.astype(ms.float16), wigner_inv[ir.l].astype(ms.float16), + ((1), (1))).astype(ms.float32) + rotate_back_data_list.append( + rotate_back_data.view(batch_shape + (-1,))) + return ops.cat(rotate_back_data_list, -1) diff --git a/MindChem/applications/matformer/mindchemistry/so2_conv/wigner.py b/MindChem/applications/matformer/mindchemistry/so2_conv/wigner.py new file mode 100644 index 000000000..c3e08615c --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/so2_conv/wigner.py @@ -0,0 +1,61 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +wigner file +""" + +# pylint: disable=C0103 +import pickle +from mindspore import ops +import mindspore as ms +from mindscience.e3nn.utils.func import broadcast_args + + +def wigner_D(lv, alpha, beta, gamma): + """ + # Borrowed from e3nn @ 0.4.0: + # https://github.com/e3nn/e3nn/blob/0.4.0/e3nn/o3/_wigner.py#L10 + # jd is a list of tensors of shape (2l+1, 2l+1) + + # Borrowed from e3nn @ 0.4.0: + # https://github.com/e3nn/e3nn/blob/0.4.0/e3nn/o3/_wigner.py#L37 + # + # In 0.5.0, e3nn shifted to torch.matrix_exp which is significantly slower: + # https://github.com/e3nn/e3nn/blob/0.5.0/e3nn/o3/_wigner.py#L92 + """ + jd = None + with open("jd.pkl", "rb") as f: + jd = pickle.load(f) + if not lv < len(jd): + raise NotImplementedError( + f"wigner D maximum l implemented is {len(jd) - 1}, send us an email to ask for more" + ) + alpha, beta, gamma = broadcast_args(alpha, beta, gamma) + j = jd[lv] + xa = _z_rot_mat(alpha, lv) + xb = _z_rot_mat(beta, lv) + xc = _z_rot_mat(gamma, lv) + return xa @ j.astype(ms.float16) @ xb @ j.astype(ms.float16) @ xc + + +def _z_rot_mat(angle, lv): + shape = angle.shape + m = ops.zeros((shape[0], 2 * lv + 1, 2 * lv + 1)) + inds = ops.arange(0, 2 * lv + 1, 1) + reversed_inds = ops.arange(2 * lv, -1, -1) + frequencies = ops.arange(lv, -lv - 1, -1) + m[..., inds, reversed_inds] = ops.sin(frequencies * angle[..., None]) + m[..., inds, inds] = ops.cos(frequencies * angle[..., None]) + return m.astype(ms.float16) diff --git a/MindChem/applications/matformer/mindchemistry/utils/__init__.py b/MindChem/applications/matformer/mindchemistry/utils/__init__.py new file mode 100644 index 000000000..3f15063d7 --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/utils/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this filepio[] except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""init""" +from .load_config import load_yaml_config + +__all__ = ['load_yaml_config'] diff --git a/MindChem/applications/matformer/mindchemistry/utils/check_func.py b/MindChem/applications/matformer/mindchemistry/utils/check_func.py new file mode 100644 index 000000000..711a441fe --- /dev/null +++ b/MindChem/applications/matformer/mindchemistry/utils/check_func.py @@ -0,0 +1,128 @@ +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""functions""" +from __future__ import absolute_import + +from mindspore import context + +_SPACE = " " + + +def _convert_to_tuple(params): + if params is None: + return params + if not isinstance(params, (list, tuple)): + params = (params,) + if isinstance(params, list): + params_out = tuple(params) + else: + params_out = params # ✅ 防止未定义 + return params_out + + +def check_param_type(param, param_name, data_type=None, exclude_type=None): + """Check parameter's data type""" + data_type = _convert_to_tuple(data_type) + exclude_type = _convert_to_tuple(exclude_type) + + if data_type and not isinstance(param, data_type): + raise TypeError( + f"The type of {param_name} should be instance of {data_type}, but got {param} with type {type(param)}" + ) + if exclude_type and type(param) in exclude_type: + raise TypeError( + f"The type of {param_name} should not be instance of {exclude_type},but got {param} with type {type(param)}" + ) + + +def check_param_value(param, param_name, valid_value): + """check parameter's value""" + valid_value = _convert_to_tuple(valid_value) + if param not in valid_value: + raise ValueError(f"The value of {param_name} should be in {valid_value}, but got {param}") + + +def check_param_type_value(param, param_name, valid_value, data_type=None, exclude_type=None): + """check both data type and value""" + check_param_type(param, param_name, data_type=data_type, exclude_type=exclude_type) + check_param_value(param, param_name, valid_value) + + +def check_dict_type(param_dict, param_name, key_type=None, value_type=None): + """check data type for key and value of the specified dict""" + check_param_type(param_dict, param_name, data_type=dict) + + for key in param_dict.keys(): + if key_type: + check_param_type(key, _SPACE.join(("key of", param_name)), data_type=key_type) + if value_type: + values = _convert_to_tuple(param_dict[key]) + for value in values: + check_param_type(value, _SPACE.join(("value of", param_name)), data_type=value_type) + + +def check_dict_value(param_dict, param_name, key_value=None, value_value=None): + """check values for key and value of specified dict""" + check_param_type(param_dict, param_name, data_type=dict) + + for key in param_dict.keys(): + if key_value: + check_param_value(key, _SPACE.join(("key of", param_name)), key_value) + if value_value: + values = _convert_to_tuple(param_dict[key]) + for value in values: + check_param_value(value, _SPACE.join(("value of", param_name)), value_value) + + +def check_dict_type_value(param_dict, param_name, key_type=None, value_type=None, key_value=None, value_value=None): + """check values for key and value of specified dict""" + check_dict_type(param_dict, param_name, key_type=key_type, value_type=value_type) + check_dict_value(param_dict, param_name, key_value=key_value, value_value=value_value) + + +def check_mode(api_name): + """check running mode""" + if context.get_context("mode") == context.PYNATIVE_MODE: + raise RuntimeError(f"{api_name} is only supported GRAPH_MODE now but got PYNATIVE_MODE") + + +def check_param_no_greater(param, param_name, compared_value): + """ Check whether the param less than the given compared_value""" + if param > compared_value: + raise ValueError(f"The value of {param_name} should be no greater than {compared_value}, but got {param}") + + +def check_param_odd(param, param_name): + """ Check whether the param is an odd number""" + if param % 2 == 0: + raise ValueError(f"The value of {param_name} should be an odd number, but got {param}") + + +def check_param_even(param, param_name): + """ Check whether the param is an even number""" + for value in param: + if value % 2 != 0: + raise ValueError(f"The value of {param_name} should be an even number, but got {param}") + + +def check_lr_param_type_value(param, param_name, param_type, thresh_hold=0, restrict=False, exclude=None): + if (exclude and isinstance(param, exclude)) or not isinstance(param, param_type): + raise TypeError(f"the type of {param_name} should be {param_type}, but got {type(param)}") + if restrict: + if param <= thresh_hold: + raise ValueError(f"the value of {param_name} should be > {thresh_hold}, but got: {param}") + else: + if param < thresh_hold: + raise ValueError(f"the value of {param_name} should be >= {thresh_hold}, but got: {param}") diff --git a/MindChem/applications/nequip/models/load_config.py b/MindChem/applications/matformer/mindchemistry/utils/load_config.py similarity index 97% rename from MindChem/applications/nequip/models/load_config.py rename to MindChem/applications/matformer/mindchemistry/utils/load_config.py index e61f0ca39..3ddc76e42 100644 --- a/MindChem/applications/nequip/models/load_config.py +++ b/MindChem/applications/matformer/mindchemistry/utils/load_config.py @@ -53,7 +53,7 @@ def load_yaml_config(file_path): ``Ascend`` ``CPU`` ``GPU`` Examples: - >>> from models.utils import load_yaml_config + >>> from mindchemistry.utils import load_yaml_config >>> config_file_path = 'xxx' # 'xxx' is the file_path >>> configs = load_yaml_config(config_file_path) """ diff --git a/MindChem/applications/matformer/models/__init__.py b/MindChem/applications/matformer/models/__init__.py deleted file mode 100644 index ca193c0ee..000000000 --- a/MindChem/applications/matformer/models/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Matformer Model Package ------------------------ - -This package defines the core neural network architectures used in the -Matformer application of MindChem. - -Typical contents of this package include: - - Backbone definitions for the Matformer encoder. - - Embedding and positional encoding modules. - - Readout / property head for energy, force or other scalar targets. - - Utility functions for building models from config dictionaries. - -The package is designed to be: - - Modular: different components (embedding, attention blocks, heads) - can be swapped or extended. - - Config-driven: model hyper-parameters are usually specified in - YAML / JSON config files and parsed into constructor arguments. - - MindSpore-friendly: all models are implemented with MindSpore - `nn.Cell`, supporting graph mode and Ascend devices.""" diff --git a/MindChem/applications/matformer/predict.py b/MindChem/applications/matformer/predict.py index ed9e67486..d6c45fe2d 100644 --- a/MindChem/applications/matformer/predict.py +++ b/MindChem/applications/matformer/predict.py @@ -23,10 +23,10 @@ import numpy as np import mindspore as ms from mindspore import set_seed from data.generate import get_prop_model -from models.matformer import Matformer -from models.utils import LossRecord -from models.graph.loss import L1LossMask, L2LossMask -from models.graph.dataloader import DataLoaderBase as DataLoader +from mindchemistry.cell.matformer.matformer import Matformer +from mindchemistry.cell.matformer.utils import LossRecord +from mindchemistry.graph.loss import L1LossMask, L2LossMask +from mindchemistry.graph.dataloader import DataLoaderBase as DataLoader logging.basicConfig(level=logging.INFO) diff --git a/MindChem/applications/matformer/train.py b/MindChem/applications/matformer/train.py index 8cfb8fbb1..4311e3264 100644 --- a/MindChem/applications/matformer/train.py +++ b/MindChem/applications/matformer/train.py @@ -24,10 +24,10 @@ import mindspore as ms from mindspore import nn, set_seed from mindspore.amp import all_finite from data.generate import get_prop_model -from models.matformer import Matformer -from models.utils import LossRecord, OneCycleLr -from models.graph.loss import L1LossMask, L2LossMask -from models.graph.dataloader import DataLoaderBase as DataLoader +from mindchemistry.cell.matformer.matformer import Matformer +from mindchemistry.cell.matformer.utils import LossRecord, OneCycleLr +from mindchemistry.graph.loss import L1LossMask, L2LossMask +from mindchemistry.graph.dataloader import DataLoaderBase as DataLoader logging.basicConfig(level=logging.INFO) diff --git a/MindChem/applications/nequip/mindchemistry/__init__.py b/MindChem/applications/nequip/mindchemistry/__init__.py new file mode 100644 index 000000000..c54166ca1 --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/__init__.py @@ -0,0 +1,64 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Initialization for MindChemistry APIs.""" + +import time +import mindspore as ms +from mindspore import log as logger +from mindscience.e3nn import * +from .cell import * +from .utils import * +from .graph import * +from .so2_conv import * + +__all__ = [] +__all__.extend(cell.__all__) +__all__.extend(utils.__all__) + +def _mindspore_version_check(): + """ + Check MindSpore version for MindChemistry. + + Raises: + ImportError: If MindSpore cannot be imported. + """ + try: + _ = ms.__version__ + except ImportError as exc: + raise ImportError( + "Cannot find MindSpore in the current environment. Please install " + "MindSpore before using MindChemistry, by following the instruction at " + "https://www.mindspore.cn/install" + ) from exc + + ms_version = ms.__version__[:5] + required_mindspore_version = "1.8.1" + + if ms_version < required_mindspore_version: + logger.warning( + f"Current version of MindSpore ({ms_version}) is not compatible with MindChemistry. " + f"Some functions might not work or even raise errors. Please install MindSpore " + f"version >= {required_mindspore_version}. For more details about dependency settings, " + f"please check the instructions at the MindSpore official website " + f"https://www.mindspore.cn/install or check the README.md at " + f"https://gitee.com/mindspore/mindscience" + ) + + for i in range(3, 0, -1): + logger.warning(f"Please pay attention to the above warning, countdown: {i}") + time.sleep(1) + + +_mindspore_version_check() diff --git a/mindscience/models/layers/mask.py b/MindChem/applications/nequip/mindchemistry/cell/__init__.py similarity index 35% rename from mindscience/models/layers/mask.py rename to MindChem/applications/nequip/mindchemistry/cell/__init__.py index 660e8197e..5383fbab0 100644 --- a/mindscience/models/layers/mask.py +++ b/MindChem/applications/nequip/mindchemistry/cell/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2023 The AIMM Group at Shenzhen Bay Laboratory & Peking University & Huawei Technologies Co., Ltd +# Copyright 2022 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,39 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""Mask""" -from mindspore.ops import operations as P -from mindspore.ops import functional as F -from mindspore import nn +"""initialization for cells""" +from .nequip import Nequip +from .basic_block import AutoEncoder, FCNet, MLPNet +from .matformer import * -class LayerNormProcess(nn.Cell): - def __init__(self,): - super().__init__() - self.layernorm = P.LayerNorm(begin_norm_axis=-1, begin_params_axis=-1, epsilon=1e-5) - - def construct(self, msa_act, query_norm_gamma, query_norm_beta): - output, _, _ = self.layernorm(msa_act, query_norm_gamma, query_norm_beta) - return output - - -class MaskedLayerNorm(nn.Cell): - '''masked_layer_norm''' - - def __init__(self): - super().__init__() - self.norm = LayerNormProcess() - - def construct(self, act, gamma, beta, mask=None): - '''construct''' - ones = P.Ones()(act.shape[:-1] + (1,), act.dtype) - if mask is not None: - mask = F.expand_dims(mask, -1) - mask = mask * ones - else: - mask = ones - - act = act * mask - act = self.norm(act, gamma, beta) - act = act * mask - return act +__all__ = [ + "Nequip" +] +__all__.extend(matformer.__all__) diff --git a/MindChem/applications/nequip/mindchemistry/cell/activation.py b/MindChem/applications/nequip/mindchemistry/cell/activation.py new file mode 100644 index 000000000..d09b35831 --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/cell/activation.py @@ -0,0 +1,38 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""get activation function.""" +from __future__ import absolute_import + +from mindspore import ops +from mindspore.nn.layer import activation + +_activation = { + 'softmax': activation.Softmax, + 'logsoftmax': activation.LogSoftmax, + 'relu': activation.ReLU, + 'silu': activation.SiLU, + 'relu6': activation.ReLU6, + 'tanh': activation.Tanh, + 'gelu': activation.GELU, + 'fast_gelu': activation.FastGelu, + 'elu': activation.ELU, + 'sigmoid': activation.Sigmoid, + 'prelu': activation.PReLU, + 'leakyrelu': activation.LeakyReLU, + 'hswish': activation.HSwish, + 'hsigmoid': activation.HSigmoid, + 'logsigmoid': activation.LogSigmoid, + 'sin': ops.Sin +} diff --git a/MindChem/applications/nequip/mindchemistry/cell/basic_block.py b/MindChem/applications/nequip/mindchemistry/cell/basic_block.py new file mode 100644 index 000000000..6a83f67e0 --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/cell/basic_block.py @@ -0,0 +1,600 @@ +# Copyright 2023 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""basic""" +from __future__ import absolute_import + +from collections.abc import Sequence +from typing import Union + +from mindspore import nn +from mindspore.nn.layer import activation +from mindspore import ops, float16, float32, Tensor +from mindspore.common.initializer import Initializer + +from .activation import _activation + + +def _get_dropout(dropout_rate): + """ + Gets the dropout functions. + + Inputs: + dropout_rate (Union[int, float]): The dropout rate of the dropout function. + If dropout_rate was int or not in range (0,1], it would be rectify to closest float value. + + Returns: + Function, the dropout function. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_dropout + >>> dropout = get_dropout(0.5) + >>> dropout.set_train + Dropout + """ + dropout_rate = float(max(min(dropout_rate, 1.), 1e-7)) + return nn.Dropout(keep_prob=dropout_rate) + + +def _get_layernorm(channel, epsilon): + """ + Gets the layer normalization functions. + + Inputs: + channel (Union[int, list]): The normalized shape of the layer normalization function. + If channel was int, it would be wrap into a list. + epsilon (float): The epsilon of the layer normalization function. + + Returns: + Function, the layer normalization function. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_layernorm + >>> from mindspore import Tensor + >>> input_x = Tensor(np.array([[1.2, 0.1], [0.2, 3.2]], dtype=np.float32)) + >>> layernorm = get_layernorm([2], 1e-7) + >>> output = layernorm(input_x) + >>> print(output) + [[ 9.99999881e-01, -9.99999881e-01], + [-1.00000000e+00, 1.00000000e+00]] + """ + if isinstance(channel, int): + channel = [channel] + return nn.LayerNorm(channel, epsilon=epsilon) + + +def _get_activation(name): + """ + Gets the activation function. + + Inputs: + name (Union[str, None]): The name of the activation function. If name was None, it would return []. + + Returns: + Function, the activation function. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_activation + >>> from mindspore import Tensor + >>> input_x = Tensor(np.array([[1.2, 0.1], [0.2, 3.2]], dtype=np.float32)) + >>> sigmoid = _get_activation('sigmoid') + >>> output = sigmoid(input_x) + >>> print(output) + [[0.7685248 0.5249792 ] + [0.54983395 0.96083426]] + """ + if name is None: + return [] + if isinstance(name, str): + name = name.lower() + if name not in _activation: + return activation.get_activation(name) + return _activation.get(name)() + return name + + +def _get_layer_arg(arguments, index): + """ + Gets the argument of each network layers. + + Inputs: + arguments (Union[str, int, float, List, None]): The arguments of each layers. + If arguments was List return the argument at the index of the List. + index (int): The index of layer in the network + + Returns: + Argument of the indexed layer. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_layer_arg + >>> from mindspore import Tensor + >>> dropout_rate = _get_layer_arg([0.1, 0.2, 0.3], index=2) + >>> print(dropout_rate) + 0.2 + >>> dropout_rate = _get_layer_arg(0.2, index=2) + >>> print(dropout_rate) + 0.2 + """ + if isinstance(arguments, list): + if len(arguments) <= index: + if len(arguments) == 1: + return [] if arguments[0] is None else arguments[0] + return [] + return [] if arguments[index] is None else arguments[index] + return [] if arguments is None else arguments + + +def get_linear_block( + in_channels, + out_channels, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu' +): + """ + Gets the linear block list. + + Inputs: + in_channels (int): The number of input channel. + out_channels (int): The number of output channel. + weight_init (Union[str, float, mindspore.common.initializer]): The initializer of the weights of dense layer + has_bias (bool): The switch for whether dense layer has bias. + bias_init (Union[str, float, mindspore.common.initializer]): The initializer of the bias of dense layer + has_dropout (bool): The switch for whether linear block has a dropout layer. + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] + has_layernorm (bool): The switch for whether linear block has a layer normalization layer. + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + has_activation (bool): The switch for whether linear block has an activation layer. + act (Union[str, None]): The activation function in linear block + + Returns: + List of mindspore.nn.Cell, linear block list . + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import get_layer_arg + >>> from mindspore import Tensor + >>> dropout_rate = get_layer_arg([0.1, 0.2, 0.3], index=2) + >>> print(dropout_rate) + 0.2 + >>> dropout_rate = get_layer_arg(0.2, index=2) + >>> print(dropout_rate) + 0.2 + """ + dense = nn.Dense( + in_channels, out_channels, weight_init=weight_init, bias_init=bias_init, has_bias=has_bias, activation=None + ) + dropout = _get_dropout(dropout_rate) if (has_dropout is True) else [] + layernorm = _get_layernorm(out_channels, layernorm_epsilon) if (has_layernorm is True) else [] + act = _get_activation(act) if (has_activation is True) else [] + block_list = [dense, dropout, layernorm, act] + while [] in block_list: + block_list.remove([]) + return block_list + + +class FCNet(nn.Cell): + r""" + The Fully Connected Network. Applies a series of fully connected layers to the incoming data. + + Args: + channels (List): the list of numbers of channel of each fully connected layers. + weight_init (Union[str, float, mindspore.common.initializer, List]): initialize layer weights. + If weight_init was List, each element corresponds to each layer. Default: ``'normal'`` . + has_bias (Union[bool, List]): The switch for whether the dense layers has bias. + If has_bias was List, each element corresponds to each dense layer. Default: ``True`` . + bias_init (Union[str, float, mindspore.common.initializer, List]): The initializer of the bias of dense + layer. If bias_init was List, each element corresponds to each dense layer. Default: ``'zeros'`` . + has_dropout (Union[bool, List]): The switch for whether linear block has a dropout layer. + If has_dropout was List, each element corresponds to each layer. Default: ``False`` . + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] + If dropout_rate was List, each element corresponds to each dropout layer. Default: ``0.5`` . + has_layernorm (Union[bool, List]): The switch for whether linear block has a layer normalization layer. + If has_layernorm was List, each element corresponds to each layer. Default: ``False`` . + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + If layernorm_epsilon was List, each element corresponds to each layer normalization layer. + Default: ``1e-7`` . + has_activation (Union[bool, List]): The switch for whether linear block has an activation layer. + If has_activation was List, each element corresponds to each layer. Default: ``True`` . + act (Union[str, None, List]): The activation function in linear block. + If act was List, each element corresponds to each activation layer. Default: ``'relu'`` . + + Inputs: + - **input** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Outputs: + - **output** (Tensor) - The shape of Tensor is :math:`(*, channels[-1])`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import FCNet + >>> from mindspore import Tensor + >>> inputs = Tensor(np.array([[180, 234, 154], [244, 48, 247]], np.float32)) + >>> net = FCNet([3, 16, 32, 16, 8]) + >>> output = net(inputs) + >>> print(output.shape) + (2, 8) + + """ + + def __init__( + self, + channels, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu' + ): + super().__init__() + self.channels = channels + self.weight_init = weight_init + self.has_bias = has_bias + self.bias_init = bias_init + self.has_dropout = has_dropout + self.dropout_rate = dropout_rate + self.has_layernorm = has_layernorm + self.layernorm_epsilon = layernorm_epsilon + self.has_activation = has_activation + self.activation = act + self.network = nn.SequentialCell(self._create_network()) + + def _create_network(self): + """ create the network """ + cell_list = [] + for i in range(len(self.channels) - 1): + cell_list += get_linear_block( + self.channels[i], + self.channels[i + 1], + weight_init=_get_layer_arg(self.weight_init, i), + has_bias=_get_layer_arg(self.has_bias, i), + bias_init=_get_layer_arg(self.bias_init, i), + has_dropout=_get_layer_arg(self.has_dropout, i), + dropout_rate=_get_layer_arg(self.dropout_rate, i), + has_layernorm=_get_layer_arg(self.has_layernorm, i), + layernorm_epsilon=_get_layer_arg(self.layernorm_epsilon, i), + has_activation=_get_layer_arg(self.has_activation, i), + act=_get_layer_arg(self.activation, i) + ) + return cell_list + + def construct(self, x): + return self.network(x) + + +class MLPNet(nn.Cell): + r""" + The MLPNet Network. Applies a series of fully connected layers to the incoming data among which hidden layers have + same number of channels. + + Args: + in_channels (int): the number of input layer channel. + out_channels (int): the number of output layer channel. + layers (int): the number of layers. + neurons (int): the number of channels of hidden layers. + weight_init (Union[str, float, mindspore.common.initializer, List]): initialize layer weights. + If weight_init was List, each element corresponds to each layer. Default: ``'normal'`` . + has_bias (Union[bool, List]): The switch for whether the dense layers has bias. + If has_bias was List, each element corresponds to each dense layer. Default: ``True`` . + bias_init (Union[str, float, mindspore.common.initializer, List]): The initializer of the bias of dense + layer. If bias_init was List, each element corresponds to each dense layer. Default: ``'zeros'`` . + has_dropout (Union[bool, List]): The switch for whether linear block has a dropout layer. + If has_dropout was List, each element corresponds to each layer. Default: ``False`` . + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] . + If dropout_rate was List, each element corresponds to each dropout layer. Default: ``0.5`` . + has_layernorm (Union[bool, List]): The switch for whether linear block has a layer normalization layer. + If has_layernorm was List, each element corresponds to each layer. Default: ``False`` . + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + If layernorm_epsilon was List, each element corresponds to each layer normalization layer. + Default: ``1e-7`` . + has_activation (Union[bool, List]): The switch for whether linear block has an activation layer. + If has_activation was List, each element corresponds to each layer. Default: ``True`` . + act (Union[str, None, List]): The activation function in linear block. + If act was List, each element corresponds to each activation layer. Default: ``'relu'`` . + + Inputs: + - **input** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Outputs: + - **output** (Tensor) - The shape of Tensor is :math:`(*, channels[-1])`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import FCNet + >>> from mindspore import Tensor + >>> inputs = Tensor(np.array([[180, 234, 154], [244, 48, 247]], np.float32)) + >>> net = MLPNet(in_channels=3, out_channels=8, layers=5, neurons=32) + >>> output = net(inputs) + >>> print(output.shape) + (2, 8) + + """ + + def __init__( + self, + in_channels, + out_channels, + layers, + neurons, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu' + ): + super().__init__() + self.channels = (in_channels,) + (layers - 2) * \ + (neurons,) + (out_channels,) + self.network = FCNet( + channels=self.channels, + weight_init=weight_init, + has_bias=has_bias, + bias_init=bias_init, + has_dropout=has_dropout, + dropout_rate=dropout_rate, + has_layernorm=has_layernorm, + layernorm_epsilon=layernorm_epsilon, + has_activation=has_activation, + act=act + ) + + def construct(self, x): + return self.network(x) + + +class MLPMixPrecision(nn.Cell): + """MLPMixPrecision + """ + + def __init__( + self, + input_dim: int, + hidden_dims: Sequence, + short_cut=False, + batch_norm=False, + activation_fn='relu', + has_bias=False, + weight_init: Union[Initializer, str] = 'xavier_uniform', + bias_init: Union[Initializer, str] = 'zeros', + dropout=0, + dtype=float32 + ): + super().__init__() + self.dtype = dtype + self.div = ops.Div() + + self.dims = [input_dim] + hidden_dims + self.short_cut = short_cut + self.nonlinear_const = 1.0 + if isinstance(activation_fn, str): + self.activation = _activation.get(activation_fn)() + if activation_fn is not None and activation_fn == 'silu': + self.nonlinear_const = 1.679177 + else: + self.activation = activation_fn + self.dropout = None + if dropout: + self.dropout = nn.Dropout(dropout) + fcs = [ + nn.Dense(dim, self.dims[i + 1], weight_init=weight_init, bias_init=bias_init, + has_bias=has_bias).to_float(self.dtype) for i, dim in enumerate(self.dims[:-1]) + ] + self.layers = nn.CellList(fcs) + self.batch_norms = None + if batch_norm: + bns = [nn.BatchNorm1d(dim) for dim in self.dims[1:-1]] + self.batch_norms = nn.CellList(bns) + + def construct(self, inputs): + """construct + + Args: + inputs: inputs + + Returns: + inputs + """ + hidden = inputs + norm_from_last = 1.0 + for i, layer in enumerate(self.layers): + sqrt_dim = ops.sqrt(Tensor(float(self.dims[i]))) + layer_hidden = layer(hidden) + if self.dtype == float16: + layer_hidden = layer_hidden.astype(float16) + hidden = self.div(layer_hidden * norm_from_last, sqrt_dim) + norm_from_last = self.nonlinear_const + if i < len(self.layers) - 1: + if self.batch_norms is not None: + x = hidden.flatten(0, -2) + hidden = self.batch_norms[i](x).view_as(hidden) + if self.activation is not None: + hidden = self.activation(hidden) + if self.dropout is not None: + hidden = self.dropout(hidden) + if self.short_cut and hidden.shape == hidden.shape: + hidden += inputs + return hidden + + +class AutoEncoder(nn.Cell): + r""" + The AutoEncoder Network. + Applies an encoder to get the latent code and applies a decoder to get the reconstruct data. + + Args: + channels (list): The number of channels of each encoder and decoder layer. + weight_init (Union[str, float, mindspore.common.initializer, List]): initialize layer parameters. + If weight_init was List, each element corresponds to each layer. Default: ``'normal'`` . + has_bias (Union[bool, List]): The switch for whether the dense layers has bias. + If has_bias was List, each element corresponds to each dense layer. Default: ``True`` . + bias_init (Union[str, float, mindspore.common.initializer, List]): initialize layer parameters. + If bias_init was List, each element corresponds to each dense layer. Default: ``'zeros'`` . + has_dropout (Union[bool, List]): The switch for whether linear block has a dropout layer. + If has_dropout was List, each element corresponds to each layer. Default: ``False`` . + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] + If dropout_rate was List, each element corresponds to each dropout layer. Default: ``0.5`` . + has_layernorm (Union[bool, List]): The switch for whether linear block has a layer normalization layer. + If has_layernorm was List, each element corresponds to each layer. Default: ``False`` . + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + If layernorm_epsilon was List, each element corresponds to each layer normalization layer. + Default: ``1e-7`` . + has_activation (Union[bool, List]): The switch for whether linear block has an activation layer. + If has_activation was List, each element corresponds to each layer. Default: ``True`` . + act (Union[str, None, List]): The activation function in linear block. + If act was List, each element corresponds to each activation layer. Default: ``'relu'`` . + out_act (Union[None, str, mindspore.nn.Cell]): The activation function to output layer. Default: ``None`` . + + Inputs: + - **x** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Outputs: + - **latents** (Tensor) - The shape of Tensor is :math:`(*, channels[-1])`. + - **x_recon** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindchemistry import AutoEncoder + >>> from mindspore import Tensor + >>> inputs = Tensor(np.array([[180, 234, 154], [244, 48, 247]], np.float32)) + >>> net = AutoEncoder([3, 6, 2]) + >>> output = net(inputs) + >>> print(output[0].shape, output[1].shape) + (2, 2) (2, 3) + + """ + + def __init__( + self, + channels, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu', + out_act=None + ): + super().__init__() + self.channels = channels + self.weight_init = weight_init + self.bias_init = bias_init + self.has_bias = has_bias + self.has_dropout = has_dropout + self.dropout_rate = dropout_rate + self.has_layernorm = has_layernorm + self.has_activation = has_activation + self.layernorm_epsilon = layernorm_epsilon + self.activation = act + self.output_activation = out_act + self.encoder = nn.SequentialCell(self._create_encoder()) + self.decoder = nn.SequentialCell(self._create_decoder()) + + def _create_encoder(self): + """ create the network encoder """ + encoder_cell_list = [] + for i in range(len(self.channels) - 1): + encoder_cell_list += get_linear_block( + self.channels[i], + self.channels[i + 1], + weight_init=_get_layer_arg(self.weight_init, i), + has_bias=_get_layer_arg(self.has_bias, i), + bias_init=_get_layer_arg(self.bias_init, i), + has_dropout=_get_layer_arg(self.has_dropout, i), + dropout_rate=_get_layer_arg(self.dropout_rate, i), + has_layernorm=_get_layer_arg(self.has_layernorm, i), + layernorm_epsilon=_get_layer_arg(self.layernorm_epsilon, i), + has_activation=_get_layer_arg(self.has_activation, i), + act=_get_layer_arg(self.activation, i) + ) + return encoder_cell_list + + def _create_decoder(self): + """ create the network decoder """ + decoder_channels = self.channels[::-1] + decoder_weight_init = self.weight_init[::-1] if isinstance(self.weight_init, list) else self.weight_init + decoder_bias_init = self.bias_init[::-1] if isinstance(self.bias_init, list) else self.bias_init + decoder_cell_list = [] + for i in range(len(decoder_channels) - 1): + decoder_cell_list += get_linear_block( + decoder_channels[i], + decoder_channels[i + 1], + weight_init=_get_layer_arg(decoder_weight_init, i), + has_bias=_get_layer_arg(self.has_bias, i), + bias_init=_get_layer_arg(decoder_bias_init, i), + has_dropout=_get_layer_arg(self.has_dropout, i), + dropout_rate=_get_layer_arg(self.dropout_rate, i), + has_layernorm=_get_layer_arg(self.has_layernorm, i), + layernorm_epsilon=_get_layer_arg(self.layernorm_epsilon, i), + has_activation=_get_layer_arg(self.has_activation, i), + act=_get_layer_arg(self.activation, i) + ) + if self.output_activation is not None: + decoder_cell_list.append(_get_activation(self.output_activation)) + return decoder_cell_list + + def encode(self, x): + return self.encoder(x) + + def decode(self, z): + return self.decoder(z) + + def construct(self, x): + latents = self.encode(x) + x_recon = self.decode(latents) + return x_recon, latents diff --git a/MindChem/applications/nequip/mindchemistry/cell/convolution.py b/MindChem/applications/nequip/mindchemistry/cell/convolution.py new file mode 100644 index 000000000..1fa168da2 --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/cell/convolution.py @@ -0,0 +1,120 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""convolution""" +from mindspore import nn, ops, float32 +from mindscience.e3nn.o3 import TensorProduct, Irreps, Linear +from mindscience.e3nn.nn import FullyConnectedNet +from ..graph.graph import AggregateEdgeToNode + +softplus = ops.Softplus() + + +def shift_softplus(x): + return softplus(x) - 0.6931471805599453 + + +def silu(x): + return x * ops.sigmoid(x) + + +class Convolution(nn.Cell): + r""" + InteractionBlock. + + Args: + irreps_node_input: Input Features, default = None + irreps_node_attr: Nodes attribute irreps + irreps_node_output: Output irreps, in our case typically a single scalar + irreps_edge_attr: Edge attribute irreps + invariant_layers: Number of invariant layers, default = 1 + invariant_neurons: Number of hidden neurons in invariant function, default = 8 + avg_num_neighbors: Number of neighbors to divide by, default None => no normalization. + use_sc(bool): use self-connection or not + """ + + def __init__(self, + irreps_node_input, + irreps_node_attr, + irreps_node_output, + irreps_edge_attr, + irreps_edge_scalars, + invariant_layers=1, + invariant_neurons=8, + avg_num_neighbors=None, + use_sc=True, + nonlin_scalars=None, + dtype=float32, + ncon_dtype=float32): + super().__init__() + self.avg_num_neighbors = avg_num_neighbors + self.use_sc = use_sc + + self.irreps_node_input = Irreps(irreps_node_input) + self.irreps_node_attr = Irreps(irreps_node_attr) + self.irreps_node_output = Irreps(irreps_node_output) + self.irreps_edge_attr = Irreps(irreps_edge_attr) + self.irreps_edge_scalars = Irreps([(irreps_edge_scalars.num_irreps, (0, 1))]) + + self.lin1 = Linear(self.irreps_node_input, self.irreps_node_input, dtype=dtype, ncon_dtype=ncon_dtype) + + tp = TensorProduct(self.irreps_node_input, + self.irreps_edge_attr, + self.irreps_node_output, + 'merge', + weight_mode='custom', + dtype=dtype, + ncon_dtype=ncon_dtype) + + self.fc = FullyConnectedNet([self.irreps_edge_scalars.num_irreps] + invariant_layers * [invariant_neurons] + + [tp.weight_numel], { + "ssp": shift_softplus, + "silu": ops.silu, + }.get(nonlin_scalars.get("e", None), None), dtype=dtype) + + self.tp = tp + self.scatter = AggregateEdgeToNode(dim=1) + + self.lin2 = Linear(tp.irreps_out.simplify(), self.irreps_node_output, dtype=dtype, ncon_dtype=ncon_dtype) + + self.sc = None + if self.use_sc: + self.sc = TensorProduct(self.irreps_node_input, + self.irreps_node_attr, + self.irreps_node_output, + 'connect', + dtype=dtype, + ncon_dtype=ncon_dtype) + + def construct(self, node_input, node_attr, edge_src, edge_dst, edge_attr, edge_scalars): + """Evaluate interaction Block with resnet""" + weight = self.fc(edge_scalars) + + node_features = self.lin1(node_input) + + edge_features = self.tp(node_features[edge_src], edge_attr, weight) + + node_features = self.scatter(edge_attr=edge_features, edge_index=[edge_src, edge_dst], + dim_size=node_input.shape[0]) + + if self.avg_num_neighbors is not None: + node_features = node_features.div(self.avg_num_neighbors**0.5) + + node_features = self.lin2(node_features) + + if self.sc is not None: + sc = self.sc(node_input, node_attr) + node_features = node_features + sc + + return node_features diff --git a/MindChem/applications/nequip/mindchemistry/cell/embedding.py b/MindChem/applications/nequip/mindchemistry/cell/embedding.py new file mode 100644 index 000000000..ec725abaa --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/cell/embedding.py @@ -0,0 +1,146 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""embedding +""" +import math + +import numpy as np +from mindspore import nn, ops, Tensor, Parameter, float32 + +from mindscience.e3nn.o3 import Irreps + + +def _poly_cutoff(x, factor, p=6.0): + x = x * factor + out = 1.0 + out = out - (((p + 1.0) * (p + 2.0) / 2.0) * ops.pow(x, p)) + out = out + (p * (p + 2.0) * ops.pow(x, p + 1.0)) + out = out - ((p * (p + 1.0) / 2) * ops.pow(x, p + 2.0)) + return out * (x < 1.0) + + +class PolyCutoff(nn.Cell): + + def __init__(self, r_max, p=6): + super().__init__() + self.p = float(p) + self._factor = 1.0 / float(r_max) + + def construct(self, x): + return _poly_cutoff(x, self._factor, p=self.p) + + +class MaskPolynomialCutoff(nn.Cell): + """MaskPolynomialCutoff + """ + _factor: float + p: float + + def __init__(self, r_max: float, p: float = 6): + super().__init__() + self.p = float(p) + self._factor = 1.0 / float(r_max) + self.r_max = Tensor(r_max, dtype=float32) + + self.cutoff = r_max + + def construct(self, distance: Tensor, mask: Tensor = None): + decay = _poly_cutoff(distance, self._factor, p=self.p) + + mask_lower = distance < self.cutoff + if mask is not None: + mask_lower &= mask + + return decay, mask_lower + + +class BesselBasis(nn.Cell): + """BesselBasis + """ + + def __init__(self, r_max, num_basis=8, dtype=float32): + super().__init__() + self.r_max = r_max + self.num_basis = num_basis + self.prefactor = 2.0 / self.r_max + bessel_weights = Tensor(np.linspace(1., num_basis, num_basis) * math.pi, dtype=dtype) + self.bessel_weights = Parameter(bessel_weights) + + def construct(self, x): + numerator = ops.sin(self.bessel_weights * x.unsqueeze(-1) / self.r_max) + return self.prefactor * (numerator / x.unsqueeze(-1)) + + +class NormBesselBasis(nn.Cell): + """NormBesselBasis + """ + + def __init__(self, r_max, num_basis=8, norm_num=4000): + super().__init__() + + self.basis = BesselBasis(r_max=r_max, num_basis=num_basis) + self.rs = Tensor(np.linspace(0.0, r_max, num=norm_num + 1), float32)[1:] + + self.sqrt = ops.Sqrt() + self.sq = ops.Square() + self.div = ops.Div() + + bessel_weights = Tensor(np.linspace(1.0, num_basis, num=num_basis), float32) + + bessel_weights = bessel_weights * Tensor(math.pi, float32) + edge_length = self.rs + edge_length_unsqueeze = edge_length.unsqueeze(-1) + bessel_edge_length = bessel_weights * edge_length_unsqueeze + if r_max != 0: + bessel_edge_length = bessel_edge_length / r_max + prefactor = 2.0 / r_max + else: + raise ValueError + self.sin = ops.Sin() + numerator = self.sin(bessel_edge_length) + bs = prefactor * self.div(numerator, edge_length_unsqueeze) + + basis_mean = Tensor(np.mean(bs.asnumpy(), axis=0), float32) + basis_std = self.sqrt(Tensor(np.mean(self.sq(bs - basis_mean).asnumpy(), 0), float32)) + inv_std = ops.reciprocal(basis_std) + + self.basis_mean = basis_mean + self.inv_std = inv_std + + def construct(self, edge_length): + basis_length = self.basis(edge_length) + return (basis_length - self.basis_mean) * self.inv_std + + +class RadialEdgeEmbedding(nn.Cell): + """RadialEdgeEmbedding + """ + + def __init__(self, r_max, num_basis=8, p=6, dtype=float32): + super().__init__() + self.num_basis = num_basis + self.cutoff_p = p + self.basis = BesselBasis(r_max, num_basis, dtype=dtype) + self.cutoff = PolyCutoff(r_max, p) + + self.irreps_out = Irreps([(self.basis.num_basis, (0, 1))]) + + def construct(self, edge_length): + edge_length_embedded = self.basis(edge_length) * self.cutoff(edge_length).unsqueeze(-1) + return edge_length_embedded + + def __repr__(self): + return f'RadialEdgeEmbedding [num_basis: {self.num_basis}, cutoff_p: ' \ + + f'{self.cutoff_p}] ( -> {self.irreps_out} | {self.basis.num_basis} weights)' diff --git a/MindChem/applications/nequip/mindchemistry/cell/matformer/__init__.py b/MindChem/applications/nequip/mindchemistry/cell/matformer/__init__.py new file mode 100644 index 000000000..23a86799d --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/cell/matformer/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""initialization for matformer""" + +from .matformer import Matformer + +__all__ = ['Matformer'] diff --git a/MindChem/applications/nequip/mindchemistry/cell/matformer/matformer.py b/MindChem/applications/nequip/mindchemistry/cell/matformer/matformer.py new file mode 100644 index 000000000..61c0a11ba --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/cell/matformer/matformer.py @@ -0,0 +1,101 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Matformer""" +from mindspore import nn, ops +import mindspore as ms +from mindchemistry.graph.graph import AggregateNodeToGlobal +from mindchemistry.cell.matformer.utils import RBFExpansion +from mindchemistry.cell.matformer.transformer import MatformerConv, Silu + + +class Matformer(nn.Cell): + """Matformer class""" + + def __init__(self, config): + """init""" + super().__init__() + + use_fp16 = config['use_fp16'] + self.classification = config['classification'] + self.use_angle = config['use_angle'] + + self.global_dtype = ms.float32 + if use_fp16: + self.global_dtype = ms.float16 + + self.atom_embedding = nn.Dense( + config['atom_input_features'], config['node_features'] + ).to_float(self.global_dtype) + + ################## + self.rbf_0 = RBFExpansion(vmin=0, vmax=8.0, bins=config['edge_features'],) + self.rbf_1 = nn.Dense(config['edge_features'], config['node_features']).to_float(self.global_dtype) + self.rbf_2 = ops.Softplus() + self.rbf_3 = nn.Dense(config['node_features'], config['node_features']).to_float(self.global_dtype) + + self.angle_lattice = config["angle_lattice"] + + self.att_layers = nn.CellList( + [ + MatformerConv(in_channels=config['node_features'], out_channels=config['node_features'], + heads=config['node_layer_head'], edge_dim=config['node_features']) + for _ in range(config['conv_layers']) + ] + ) + + self.fc = nn.SequentialCell( + nn.Dense(config['node_features'], config['fc_features']).to_float(self.global_dtype), + Silu().to_float(ms.float32) + ) + + self.fc_out = nn.Dense( + config['fc_features'], config['output_features'] + ) + + self.link = None + self.link_name = config['link'] + if config['link'] == "identity": + self.link = lambda x: x + + self.dim_size = config["batch_size_max"] + self.aggregator_to_global = AggregateNodeToGlobal("mean") + + def construct(self, data_x, data_edge_attr, data_edge_index, data_batch, node_mask, edge_mask, node_num): + """construct""" + node_features = self.atom_embedding(data_x) + edge_feat = ops.norm(data_edge_attr, dim=1) + edge_features = self.rbf_0(edge_feat) + edge_features = ops.mul(edge_features, ops.reshape(edge_mask, (-1, 1))) + edge_features = self.rbf_1(edge_features) + edge_features = self.rbf_2(edge_features) + edge_features = self.rbf_3(edge_features) + + node_features = self.att_layers[0](node_features, data_edge_index, edge_features, node_mask, edge_mask, + node_num) + node_features = self.att_layers[1](node_features, data_edge_index, edge_features, node_mask, edge_mask, + node_num) + node_features = self.att_layers[2](node_features, data_edge_index, edge_features, node_mask, edge_mask, + node_num) + node_features = self.att_layers[3](node_features, data_edge_index, edge_features, node_mask, edge_mask, + node_num) + node_features = self.att_layers[4](node_features, data_edge_index, edge_features, node_mask, edge_mask, + node_num) + + features = self.aggregator_to_global(node_features, data_batch, dim_size=self.dim_size, mask=node_mask) + + features = self.fc(features) + out = self.fc_out(features) + res = ops.squeeze(out) + return res diff --git a/MindChem/applications/nequip/mindchemistry/cell/matformer/transformer.py b/MindChem/applications/nequip/mindchemistry/cell/matformer/transformer.py new file mode 100644 index 000000000..fce3e9ccd --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/cell/matformer/transformer.py @@ -0,0 +1,158 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""transformer file""" +import mindspore as ms +from mindspore import ops, Tensor, nn +from mindchemistry.graph.graph import LiftNodeToEdge, AggregateEdgeToNode +from mindchemistry.graph.normlization import BatchNormMask + + +class Silu(nn.Cell): + """Silu""" + + def __init__(self): + """init""" + super().__init__() + self.sigmoid = nn.Sigmoid() + + def construct(self, x): + """construct""" + return ops.mul(x, self.sigmoid(x)) + + +class MatformerConv(nn.Cell): + """MatformerConv""" + + def __init__( + self, + in_channels=None, + out_channels=None, + heads=1, + concat=True, + beta=False, + edge_dim=None, + bias=True, + root_weight=True, + use_fp16=False + ): + """init""" + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.heads = heads + self.beta = beta and root_weight + self.root_weight = root_weight + self.concat = concat + self.edge_dim = edge_dim + self.global_dtype = ms.float32 + if use_fp16: + self.global_dtype = ms.float16 + + if isinstance(in_channels, int): + in_channels = (in_channels, in_channels) + + self.lin_key = nn.Dense(in_channels[0], heads * out_channels).to_float(self.global_dtype) + self.lin_query = nn.Dense(in_channels[1], heads * out_channels).to_float(self.global_dtype) + self.lin_value = nn.Dense(in_channels[0], heads * out_channels).to_float(self.global_dtype) + + if edge_dim is not None: + self.lin_edge = nn.Dense(edge_dim, heads * out_channels, has_bias=False).to_float(self.global_dtype) + + if concat: + self.lin_concate = nn.Dense(heads * out_channels, out_channels).to_float(self.global_dtype) + + self.lin_skip = nn.Dense(in_channels[1], out_channels, has_bias=bias).to_float(self.global_dtype) + if self.beta: + self.lin_beta = nn.Dense(3 * out_channels, 1, has_bias=False).to_float(self.global_dtype) + + self.lin_msg_update = nn.Dense(out_channels * 3, out_channels * 3).to_float(self.global_dtype) + + self.msg_layer = nn.SequentialCell(nn.Dense(out_channels * 3, out_channels).to_float(self.global_dtype), + nn.LayerNorm((out_channels,), epsilon=0.00001).to_float(self.global_dtype)) + + self.bn = BatchNormMask(out_channels).to_float(ms.float32) + self.sigmoid = nn.Sigmoid() + self.layer_norm = nn.LayerNorm((out_channels * 3,), epsilon=0.00001).to_float(ms.float32) + self.silu = Silu() + self.lift_to_edge_i = LiftNodeToEdge(dim=1) + self.lift_to_edge_j = LiftNodeToEdge(dim=0) + self.aggregate_to_node = AggregateEdgeToNode(mode="add", dim=1) + + self.layer_norm_after_lin_msg_update = nn.LayerNorm((out_channels * 3,), epsilon=0.00001).to_float(ms.float32) + self.gelu = nn.GELU() + + def construct(self, x, edge_index, edge_attr, node_mask, edge_mask, node_num): + """construct""" + h_num, c_num = self.heads, self.out_channels + if isinstance(x, Tensor): + x = (x, x) + query = self.lin_query(x[1]).view(-1, h_num, c_num) + key = self.lin_key(x[0]).view(-1, h_num, c_num) + value = self.lin_value(x[0]).view(-1, h_num, c_num) + out = self.propagate(edge_index, edge_mask, query, key, value, edge_attr) + if self.concat: + out = out.view(-1, self.heads * self.out_channels) + out = self.lin_concate(out) + else: + out = ops.mean(out, axis=1) + + node_mask = ops.reshape(node_mask, (-1, 1)) + out = self.bn(out, node_mask, node_num) + out = self.silu(out) + + if self.root_weight: + x_r = self.lin_skip(x[1]) + x_r = ops.mul(x_r, node_mask).astype(ms.float32) + if self.beta: + beta = self.lin_beta(ops.cat([out, x_r, out - x_r], axis=-1)) + beta = self.sigmoid(beta) + out = beta * x_r + (1 - beta) * out + else: + out += x_r + + return out + + def message(self, query_i, key_i, key_j, value_j, value_i, + edge_attr) -> Tensor: + """message""" + if self.lin_edge is not None: + edge_attr = self.lin_edge(edge_attr).view(-1, self.heads, self.out_channels) + + query_i = ops.cat((query_i, query_i, query_i), axis=-1) + key_j = ops.cat((key_i, key_j, edge_attr), axis=-1) + alpha = ops.div(ops.mul(query_i.astype(ms.float32), key_j.astype(ms.float32)), + ops.sqrt(ms.Tensor(self.out_channels * 3, ms.float32))) + out = ops.cat((value_i, value_j, edge_attr), axis=-1) + + out_norm = self.gelu(self.layer_norm_after_lin_msg_update(self.lin_msg_update(out))) + out = out_norm * self.sigmoid(self.layer_norm(alpha.view(-1, self.heads, 3 * self.out_channels))) + out = self.msg_layer(out) + return out + + def propagate(self, edge_index, trans_scatter_mask, query, key, value, edge_attr): + """propagate""" + query_i = self.lift_to_edge_i(query, edge_index) + key_i = self.lift_to_edge_i(key, edge_index) + value_i = self.lift_to_edge_i(value, edge_index) + key_j = self.lift_to_edge_j(key, edge_index) + value_j = self.lift_to_edge_j(value, edge_index) + out = self.message(query_i, key_i, key_j, value_j, value_i, edge_attr) + out = self.aggregate_to_node(out, edge_index, dim_size=query.shape[0], mask=trans_scatter_mask) + return out + + def __repr__(self) -> str: + """__repr__""" + return (f'{self.__class__.__name__}({self.in_channels}, ' + f'{self.out_channels}, heads={self.heads})') diff --git a/MindChem/applications/nequip/models/utils.py b/MindChem/applications/nequip/mindchemistry/cell/matformer/utils.py similarity index 100% rename from MindChem/applications/nequip/models/utils.py rename to MindChem/applications/nequip/mindchemistry/cell/matformer/utils.py diff --git a/MindChem/applications/nequip/models/message_passing.py b/MindChem/applications/nequip/mindchemistry/cell/message_passing.py old mode 100755 new mode 100644 similarity index 100% rename from MindChem/applications/nequip/models/message_passing.py rename to MindChem/applications/nequip/mindchemistry/cell/message_passing.py diff --git a/MindChem/applications/nequip/mindchemistry/cell/nequip.py b/MindChem/applications/nequip/mindchemistry/cell/nequip.py new file mode 100644 index 000000000..c1876b333 --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/cell/nequip.py @@ -0,0 +1,141 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""network""" +from mindspore import nn, float32, int32 +from mindscience.e3nn.o3 import Irreps, SphericalHarmonics, Linear +from mindscience.e3nn.nn import OneHot +from mindscience.e3nn.utils import radius_graph +from ..graph.graph import AggregateNodeToGlobal +from .message_passing import MessagePassing +from .embedding import RadialEdgeEmbedding + + +class AtomwiseLinear(nn.Cell): + """AtomwiseLinear""" + + def __init__(self, irreps_in, irreps_out, dtype=float32, ncon_dtype=float32): + super().__init__() + self.irreps_in = Irreps(irreps_in) + self.irreps_out = Irreps(irreps_out) + self.linear = Linear(self.irreps_in, self.irreps_out, dtype=dtype, ncon_dtype=ncon_dtype) + + def construct(self, node_input): + return self.linear(node_input) + + def __repr__(self): + return self.linear.__repr__() + + +class Nequip(nn.Cell): + """EnergyNet""" + + def __init__( + self, + irreps_embedding_out, + irreps_conv_out='16x0e', + chemical_embedding_irreps_out='64x0e', + r_max=4.0, + num_layers=3, + num_type=4, + num_basis=8, + cutoff_p=6, + hidden_mul=50, + lmax=2, + pred_force=False, + dtype=float32, + ncon_dtype=float32 + ): + super().__init__() + self.r_max = r_max + self.irreps_conv_out = Irreps(irreps_conv_out) + self.pred_force = pred_force + self.irreps_embedding_out = Irreps(irreps_embedding_out) + if pred_force: + self.irreps_embedding_out += Irreps([(self.irreps_embedding_out.data[0].mul, (1, -1))]) + + irreps_node_hidden = Irreps([(hidden_mul, (l, p)) + for l in range(lmax + 1) for p in [-1, 1]]) + + self.one_hot = OneHot(num_type, dtype=dtype) + self.sh = SphericalHarmonics(range(lmax + 1), True, normalization="component", dtype=dtype) + self.radial_embedding = RadialEdgeEmbedding(r_max, num_basis, cutoff_p, dtype=dtype) + + irreps_output = Irreps(chemical_embedding_irreps_out) + self.lin_input = AtomwiseLinear(self.one_hot.irreps_output, irreps_output, dtype=dtype, ncon_dtype=ncon_dtype) + + irreps_edge_scalars = self.radial_embedding.irreps_out + + irrep_node_features = irreps_output + + self.mp = MessagePassing( + irreps_node_input=irrep_node_features, + irreps_node_attr=self.one_hot.irreps_output, + irreps_node_hidden=irreps_node_hidden, + irreps_node_output=self.irreps_conv_out, + irreps_edge_attr=self.sh.irreps_out, + irreps_edge_scalars=irreps_edge_scalars, + num_layers=num_layers, + resnet=False, + convolution_kwargs={'invariant_layers': 3, 'invariant_neurons': 64, 'avg_num_neighbors': 9, + 'nonlin_scalars': {"e": "silu"}}, + nonlin_scalars={"e": "silu", "o": "tanh"}, + nonlin_gates={"e": "silu", "o": "tanh"}, + dtype=dtype, + ncon_dtype=ncon_dtype + ) + self.lin1 = AtomwiseLinear(self.irreps_conv_out, self.irreps_embedding_out, dtype=dtype, ncon_dtype=ncon_dtype) + + irreps_out = '1x0e+1x1o' if pred_force else '1x0e' + + self.lin2 = AtomwiseLinear(self.irreps_embedding_out, irreps_out, dtype=dtype, ncon_dtype=ncon_dtype) + + self.scatter = AggregateNodeToGlobal() + + def preprocess(self, data): + """preprocess""" + if "batch" in data: + batch = data["batch"] + else: + batch = data["pos"].new_zeros(data["pos"].shape[0], dtype=int32) + + edge_index = radius_graph( + data["pos"], self.r_max, batch, max_num_neighbors=len(data["pos"]) - 1) + edge_src = edge_index[0] + edge_dst = edge_index[1] + + return batch, edge_src, edge_dst + + def construct(self, batch, atom_type, atom_pos, edge_src, edge_dst, batch_size): + """construct""" + edge_vec = atom_pos[edge_dst] - atom_pos[edge_src] + node_inputs = self.one_hot(atom_type) + node_attr = node_inputs.copy() + edge_attr = self.sh(edge_vec) + + edge_length = edge_vec.norm(None, 1) + edge_length_embedding = self.radial_embedding(edge_length) + + node_features = self.lin_input(node_inputs) + node_features = self.mp(node_features, node_attr, edge_src, edge_dst, edge_attr, edge_length_embedding) + node_features = self.lin1(node_features) + node_features = self.lin2(node_features) + + if self.pred_force: + energy = self.scatter(node_attr=node_features[:, :1], batch=batch, dim_size=batch_size) + forces = node_features[:, 1:] + return energy, forces + + energy = self.scatter(node_attr=node_features, batch=batch, dim_size=batch_size) + return energy diff --git a/MindChem/applications/nequip/mindchemistry/graph/__init__.py b/MindChem/applications/nequip/mindchemistry/graph/__init__.py new file mode 100644 index 000000000..1ae7d9a34 --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/graph/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""graph""" diff --git a/MindChem/applications/nequip/mindchemistry/graph/dataloader.py b/MindChem/applications/nequip/mindchemistry/graph/dataloader.py new file mode 100644 index 000000000..6af294afb --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/graph/dataloader.py @@ -0,0 +1,408 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""dataloader +""" +import random +import numpy as np +from mindspore import Tensor +import mindspore as ms + + +class DataLoaderBase: + r""" + DataLoader that stacks a batch of graph data to fixed-size Tensors + + For specific dataset, usually the following functions should be customized to include different fields: + __init__, shuffle_action, __iter__ + + """ + + def __init__(self, + batch_size, + edge_index, + label=None, + node_attr=None, + edge_attr=None, + padding_std_ratio=3.5, + dynamic_batch_size=True, + shuffle_dataset=True, + max_node=None, + max_edge=None): + self.batch_size = batch_size + self.edge_index = edge_index + self.index = 0 + self.step = 0 + self.padding_std_ratio = padding_std_ratio + self.batch_change_num = 0 + self.batch_exceeding_num = 0 + self.dynamic_batch_size = dynamic_batch_size + self.shuffle_dataset = shuffle_dataset + + ### can be customized to specific dataset + self.label = label + self.node_attr = node_attr + self.edge_attr = edge_attr + self.sample_num = len(self.node_attr) + batch_size_div = self.batch_size + if batch_size_div != 0: + self.step_num = int(self.sample_num / batch_size_div) + else: + raise ValueError + + if dynamic_batch_size: + self.max_start_sample = self.sample_num + else: + self.max_start_sample = self.sample_num - self.batch_size + 1 + + self.set_global_max_node_edge_num(self.node_attr, self.edge_attr, max_node, max_edge, shuffle_dataset, + dynamic_batch_size) + ####### + + def __len__(self): + return self.sample_num + + ### example of generating data of each step, can be customized to specific dataset + def __iter__(self): + if self.shuffle_dataset: + self.shuffle() + else: + self.restart() + + while self.index < self.max_start_sample: + # pylint: disable=W0612 + edge_index_step, node_batch_step, node_mask, edge_mask, batch_size_mask, node_num, edge_num, batch_size \ + = self.gen_common_data(self.node_attr, self.edge_attr) + + ### can be customized to generate different attributes or labels according to specific dataset + node_attr_step = self.gen_node_attr(self.node_attr, batch_size, node_num) + edge_attr_step = self.gen_edge_attr(self.edge_attr, batch_size, edge_num) + label_step = self.gen_global_attr(self.label, batch_size) + + self.add_step_index(batch_size) + + ### make number to Tensor, if it is used as a Tensor in the network + node_num = Tensor(node_num) + batch_size = Tensor(batch_size) + + yield node_attr_step, edge_attr_step, label_step, edge_index_step, node_batch_step, \ + node_mask, edge_mask, node_num, batch_size + + @staticmethod + def pad_zero_to_end(src, axis, zeros_len): + """pad_zero_to_end""" + pad_shape = [] + for i in range(src.ndim): + if i == axis: + pad_shape.append((0, zeros_len)) + else: + pad_shape.append((0, 0)) + return np.pad(src, pad_shape) + + @staticmethod + def gen_mask(total_len, real_len): + """gen_mask""" + mask = np.concatenate((np.full((real_len,), np.float32(1)), np.full((total_len - real_len,), np.float32(0)))) + return mask + + ### example of computing global max length of node_attr and edge_attr, can be customized to specific dataset + def set_global_max_node_edge_num(self, + node_attr, + edge_attr, + max_node=None, + max_edge=None, + shuffle_dataset=True, + dynamic_batch_size=True): + """set_global_max_node_edge_num + + Args: + node_attr: node_attr + edge_attr: edge_attr + max_node: max_node. Defaults to None. + max_edge: max_edge. Defaults to None. + shuffle_dataset: shuffle_dataset. Defaults to True. + dynamic_batch_size: dynamic_batch_size. Defaults to True. + + Raises: + ValueError: ValueError + """ + if not shuffle_dataset: + max_node_num, max_edge_num = self.get_max_node_edge_num(node_attr, edge_attr, dynamic_batch_size) + self.max_node_num_global = max_node_num if max_node is None else max(max_node, max_node_num) + self.max_edge_num_global = max_edge_num if max_edge is None else max(max_edge, max_edge_num) + return + + sum_node = 0 + sum_edge = 0 + count = 0 + max_node_single = 0 + max_edge_single = 0 + for step in range(self.sample_num): + node_len = len(node_attr[step]) + edge_len = len(edge_attr[step]) + sum_node += node_len + sum_edge += edge_len + max_node_single = max(max_node_single, node_len) + max_edge_single = max(max_edge_single, edge_len) + count += 1 + if count != 0: + mean_node = sum_node / count + mean_edge = sum_edge / count + else: + raise ValueError + + if max_node is not None and max_edge is not None: + if max_node < max_node_single: + raise ValueError( + f"the max_node {max_node} is less than the max length of a single sample {max_node_single}") + if max_edge < max_edge_single: + raise ValueError( + f"the max_edge {max_edge} is less than the max length of a single sample {max_edge_single}") + + self.max_node_num_global = max_node + self.max_edge_num_global = max_edge + elif max_node is None and max_edge is None: + sum_node = 0 + sum_edge = 0 + for step in range(self.sample_num): + sum_node += (len(node_attr[step]) - mean_node) ** 2 + sum_edge += (len(edge_attr[step]) - mean_edge) ** 2 + + if count != 0: + std_node = np.sqrt(sum_node / count) + std_edge = np.sqrt(sum_edge / count) + else: + raise ValueError + + self.max_node_num_global = int(self.batch_size * mean_node + + self.padding_std_ratio * np.sqrt(self.batch_size) * std_node) + self.max_edge_num_global = int(self.batch_size * mean_edge + + self.padding_std_ratio * np.sqrt(self.batch_size) * std_edge) + self.max_node_num_global = max(self.max_node_num_global, max_node_single) + self.max_edge_num_global = max(self.max_edge_num_global, max_edge_single) + elif max_node is None: + if max_edge < max_edge_single: + raise ValueError( + f"the max_edge {max_edge} is less than the max length of a single sample {max_edge_single}") + + if mean_edge != 0: + self.max_node_num_global = int(max_edge * mean_node / mean_edge) + else: + raise ValueError + self.max_node_num_global = max(self.max_node_num_global, max_node_single) + self.max_edge_num_global = max_edge + else: + if max_node < max_node_single: + raise ValueError( + f"the max_node {max_node} is less than the max length of a single sample {max_node_single}") + + self.max_node_num_global = max_node + if mean_node != 0: + self.max_edge_num_global = int(max_node * mean_edge / mean_node) + else: + raise ValueError + self.max_edge_num_global = max(self.max_edge_num_global, max_edge_single) + + def get_max_node_edge_num(self, node_attr, edge_attr, remainder=True): + """get_max_node_edge_num + + Args: + node_attr: node_attr + edge_attr: edge_attr + remainder (bool, optional): remainder. Defaults to True. + + Returns: + max_node_num, max_edge_num + """ + max_node_num = 0 + max_edge_num = 0 + index = 0 + for _ in range(self.step_num): + node_num = 0 + edge_num = 0 + for _ in range(self.batch_size): + node_num += len(node_attr[index]) + edge_num += len(edge_attr[index]) + index += 1 + max_node_num = max(max_node_num, node_num) + max_edge_num = max(max_edge_num, edge_num) + + if remainder: + remain_num = self.sample_num - index - 1 + node_num = 0 + edge_num = 0 + for _ in range(remain_num): + node_num += len(node_attr[index]) + edge_num += len(edge_attr[index]) + index += 1 + max_node_num = max(max_node_num, node_num) + max_edge_num = max(max_edge_num, edge_num) + + return max_node_num, max_edge_num + + def shuffle_index(self): + """shuffle_index""" + indices = list(range(self.sample_num)) + random.shuffle(indices) + return indices + + ### example of shuffling the input dataset, can be customized to specific dataset + def shuffle_action(self): + """shuffle_action""" + indices = self.shuffle_index() + self.edge_index = [self.edge_index[i] for i in indices] + self.label = [self.label[i] for i in indices] + self.node_attr = [self.node_attr[i] for i in indices] + self.edge_attr = [self.edge_attr[i] for i in indices] + + ### example of generating the final shuffled dataset, can be customized to specific dataset + def shuffle(self): + """shuffle""" + self.shuffle_action() + if not self.dynamic_batch_size: + max_node_num, max_edge_num = self.get_max_node_edge_num(self.node_attr, self.edge_attr, remainder=False) + while max_node_num > self.max_node_num_global or max_edge_num > self.max_edge_num_global: + self.shuffle_action() + max_node_num, max_edge_num = self.get_max_node_edge_num(self.node_attr, self.edge_attr, remainder=False) + + self.step = 0 + self.index = 0 + + def restart(self): + """restart""" + self.step = 0 + self.index = 0 + + ### example of calculating dynamic batch size to avoid exceeding the max length of node and edge, can be customized to specific dataset + def get_batch_size(self, node_attr, edge_attr, start_batch_size): + """get_batch_size + + Args: + node_attr: node_attr + edge_attr: edge_attr + start_batch_size: start_batch_size + + Returns: + batch_size + """ + node_num = 0 + edge_num = 0 + for i in range(start_batch_size): + index = self.index + i + node_num += len(node_attr[index]) + edge_num += len(edge_attr[index]) + + exceeding = False + while node_num > self.max_node_num_global or edge_num > self.max_edge_num_global: + node_num -= len(node_attr[index]) + edge_num -= len(edge_attr[index]) + index -= 1 + exceeding = True + self.batch_exceeding_num += 1 + if exceeding: + self.batch_change_num += 1 + + return index - self.index + 1 + + def gen_common_data(self, node_attr, edge_attr): + """gen_common_data + + Args: + node_attr: node_attr + edge_attr: edge_attr + + Returns: + common_data + """ + if self.dynamic_batch_size: + if self.step >= self.step_num: + batch_size = self.get_batch_size(node_attr, edge_attr, + min((self.sample_num - self.index), self.batch_size)) + else: + batch_size = self.get_batch_size(node_attr, edge_attr, self.batch_size) + else: + batch_size = self.batch_size + + ######################## node_batch + node_batch_step = [] + sample_num = 0 + for i in range(self.index, self.index + batch_size): + node_batch_step.extend([sample_num] * node_attr[i].shape[0]) + sample_num += 1 + node_batch_step = np.array(node_batch_step) + node_num = node_batch_step.shape[0] + + ######################## edge_index + edge_index_step = np.array([[], []], dtype=np.int64) + max_edge_index = 0 + for i in range(self.index, self.index + batch_size): + edge_index_step = np.concatenate((edge_index_step, self.edge_index[i] + max_edge_index), 1) + max_edge_index = np.max(edge_index_step) + 1 + edge_num = edge_index_step.shape[1] + + ######################### padding + edge_index_step = self.pad_zero_to_end(edge_index_step, 1, self.max_edge_num_global - edge_num) + node_batch_step = self.pad_zero_to_end(node_batch_step, 0, self.max_node_num_global - node_num) + + ######################### mask + node_mask = self.gen_mask(self.max_node_num_global, node_num) + edge_mask = self.gen_mask(self.max_edge_num_global, edge_num) + batch_size_mask = self.gen_mask(self.batch_size, batch_size) + + ######################### make Tensor + edge_index_step = Tensor(edge_index_step, ms.int32) + node_batch_step = Tensor(node_batch_step, ms.int32) + node_mask = Tensor(node_mask) + edge_mask = Tensor(edge_mask) + batch_size_mask = Tensor(batch_size_mask) + + return CommonData(edge_index_step, node_batch_step, node_mask, edge_mask, batch_size_mask, node_num, edge_num, + batch_size).get_tuple_data() + + def gen_node_attr(self, node_attr, batch_size, node_num): + """gen_node_attr""" + node_attr_step = np.concatenate(node_attr[self.index:self.index + batch_size], 0) + node_attr_step = self.pad_zero_to_end(node_attr_step, 0, self.max_node_num_global - node_num) + node_attr_step = Tensor(node_attr_step) + return node_attr_step + + def gen_edge_attr(self, edge_attr, batch_size, edge_num): + """gen_edge_attr""" + edge_attr_step = np.concatenate(edge_attr[self.index:self.index + batch_size], 0) + edge_attr_step = self.pad_zero_to_end(edge_attr_step, 0, self.max_edge_num_global - edge_num) + edge_attr_step = Tensor(edge_attr_step) + return edge_attr_step + + def gen_global_attr(self, global_attr, batch_size): + """gen_global_attr""" + global_attr_step = np.stack(global_attr[self.index:self.index + batch_size], 0) + global_attr_step = self.pad_zero_to_end(global_attr_step, 0, self.batch_size - batch_size) + global_attr_step = Tensor(global_attr_step) + return global_attr_step + + def add_step_index(self, batch_size): + """add_step_index""" + self.index = self.index + batch_size + self.step += 1 + +class CommonData: + """CommonData""" + def __init__(self, edge_index_step, node_batch_step, node_mask, edge_mask, batch_size_mask, node_num, edge_num, + batch_size): + self.tuple_data = (edge_index_step, node_batch_step, node_mask, edge_mask, batch_size_mask, node_num, edge_num, + batch_size) + + def get_tuple_data(self): + """get_tuple_data""" + return self.tuple_data diff --git a/MindChem/applications/nequip/models/graph.py b/MindChem/applications/nequip/mindchemistry/graph/graph.py similarity index 100% rename from MindChem/applications/nequip/models/graph.py rename to MindChem/applications/nequip/mindchemistry/graph/graph.py diff --git a/MindChem/applications/matformer/models/graph/loss.py b/MindChem/applications/nequip/mindchemistry/graph/loss.py similarity index 100% rename from MindChem/applications/matformer/models/graph/loss.py rename to MindChem/applications/nequip/mindchemistry/graph/loss.py diff --git a/MindChem/applications/nequip/mindchemistry/graph/normlization.py b/MindChem/applications/nequip/mindchemistry/graph/normlization.py new file mode 100644 index 000000000..caccfdb42 --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/graph/normlization.py @@ -0,0 +1,278 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""norm""" +import mindspore as ms +from mindspore import ops, Parameter, nn +from .graph import AggregateNodeToGlobal, LiftGlobalToNode + + +class BatchNormMask(nn.Cell): + """BatchNormMask""" + + def __init__(self, num_features, eps=1e-5, momentum=0.9, affine=True): + super().__init__() + self.num_features = num_features + self.eps = eps + self.momentum = momentum + self.affine = affine + self.moving_mean = Parameter(ops.zeros((num_features,), ms.float32), name="moving_mean", requires_grad=False) + self.moving_variance = Parameter(ops.ones((num_features,), ms.float32), + name="moving_variance", + requires_grad=False) + if affine: + self.gamma = Parameter(ops.ones((num_features,), ms.float32), name="gamma", requires_grad=True) + self.beta = Parameter(ops.zeros((num_features,), ms.float32), name="beta", requires_grad=True) + + def construct(self, x, mask, num): + """construct""" + if x.shape[1] != self.num_features: + raise ValueError(f"x.shape[1] {x.shape[1]} is not equal to num_features {self.num_features}") + if x.shape[0] != mask.shape[0]: + raise ValueError(f"x.shape[0] {x.shape[0]} is not equal to mask.shape[0] {mask.shape[0]}") + + if x.ndim != mask.ndim: + if mask.size != mask.shape[0]: + raise ValueError("mask.ndim dose not match src.ndim, and cannot be broadcasted to the same") + shape = [1] * x.ndim + shape[0] = -1 + mask = ops.reshape(mask, shape).astype(x.dtype) + x = ops.mul(x, mask) + + # pylint: disable=R1705 + if x.ndim > 2: + norm_axis = [] + shape = [-1] + for i in range(2, x.ndim): + norm_axis.append(i) + shape.append(1) + + if self.training: + mean = ops.div(ops.sum(x, 0), num) + mean = ops.mean(mean, norm_axis) + self.moving_mean = self.momentum * self.moving_mean + (1 - self.momentum) * mean + mean = ops.reshape(mean, shape) + mean = ops.mul(mean, mask) + x = x - mean + + var = ops.div(ops.sum(ops.pow(x, 2), 0), num) + var = ops.mean(var, norm_axis) + self.moving_variance = self.momentum * self.moving_variance + (1 - self.momentum) * var + std = ops.sqrt(ops.add(var, self.eps)) + std = ops.reshape(std, shape) + y = ops.true_divide(x, std) + else: + mean = ops.reshape(self.moving_mean.astype(x.dtype), shape) + mean = ops.mul(mean, mask) + std = ops.sqrt(ops.add(self.moving_variance.astype(x.dtype), self.eps)) + std = ops.reshape(std, shape) + y = ops.true_divide(ops.sub(x, mean), std) + + if self.affine: + gamma = ops.reshape(self.gamma.astype(x.dtype), shape) + beta = ops.reshape(self.beta.astype(x.dtype), shape) * mask + y = y * gamma + beta + + return y + else: + if self.training: + mean = ops.div(ops.sum(x, 0), num) + self.moving_mean = self.momentum * self.moving_mean + (1 - self.momentum) * mean + mean = ops.mul(mean, mask) + x = x - mean + + var = ops.div(ops.sum(ops.pow(x, 2), 0), num) + self.moving_variance = self.momentum * self.moving_variance + (1 - self.momentum) * var + std = ops.sqrt(ops.add(var, self.eps)) + y = ops.true_divide(x, std) + else: + mean = ops.mul(self.moving_mean.astype(x.dtype), mask) + std = ops.sqrt(ops.add(self.moving_variance.astype(x.dtype), self.eps)) + y = ops.true_divide(ops.sub(x, mean), std) + + if self.affine: + beta = self.beta.astype(x.dtype) * mask + y = y * self.gamma.astype(x.dtype) + beta + + return y + + +class GraphLayerNormMask(nn.Cell): + """GraphLayerNormMask""" + + def __init__(self, + normalized_shape, + begin_norm_axis=-1, + eps=1e-5, + sub_mean=True, + divide_std=True, + affine_weight=True, + affine_bias=True, + aggr_mode="mean"): + super().__init__() + self.normalized_shape = normalized_shape + self.begin_norm_axis = begin_norm_axis + self.eps = eps + self.sub_mean = sub_mean + self.divide_std = divide_std + self.affine_weight = affine_weight + self.affine_bias = affine_bias + self.mean = ops.ReduceMean(keep_dims=True) + self.aggregate = AggregateNodeToGlobal(mode=aggr_mode) + self.lift = LiftGlobalToNode(mode="multi_graph") + + if affine_weight: + self.gamma = Parameter(ops.ones(normalized_shape, ms.float32), name="gamma", requires_grad=True) + if affine_bias: + self.beta = Parameter(ops.zeros(normalized_shape, ms.float32), name="beta", requires_grad=True) + + def construct(self, x, batch, mask, dim_size, scale=None): + """construct""" + begin_norm_axis = self.begin_norm_axis if self.begin_norm_axis >= 0 else self.begin_norm_axis + x.ndim + if begin_norm_axis not in range(1, x.ndim): + raise ValueError(f"begin_norm_axis {begin_norm_axis} is not in range 1 to {x.ndim}") + + norm_axis = [] + for i in range(begin_norm_axis, x.ndim): + norm_axis.append(i) + if self.normalized_shape[i - begin_norm_axis] != x.shape[i]: + raise ValueError(f"x.shape[{i}] {x.shape[i]} is not equal to normalized_shape[{i - begin_norm_axis}] " + f"{self.normalized_shape[i - begin_norm_axis]}") + + if x.shape[0] != mask.shape[0]: + raise ValueError(f"x.shape[0] {x.shape[0]} is not equal to mask.shape[0] {mask.shape[0]}") + if x.shape[0] != batch.shape[0]: + raise ValueError(f"x.shape[0] {x.shape[0]} is not equal to batch.shape[0] {batch.shape[0]}") + + if x.ndim != mask.ndim: + if mask.size != mask.shape[0]: + raise ValueError("mask.ndim dose not match src.ndim, and cannot be broadcasted to the same") + shape = [1] * x.ndim + shape[0] = -1 + mask = ops.reshape(mask, shape).astype(x.dtype) + x = ops.mul(x, mask) + + if self.sub_mean: + mean = self.aggregate(x, batch, dim_size=dim_size, mask=mask) + mean = self.mean(mean, norm_axis) + mean = self.lift(mean, batch) + mean = ops.mul(mean, mask) + x = x - mean + + if self.divide_std: + var = self.aggregate(ops.square(x), batch, dim_size=dim_size, mask=mask) + var = self.mean(var, norm_axis) + if scale is not None: + var = var * scale + std = ops.sqrt(var + self.eps) + std = self.lift(std, batch) + x = ops.true_divide(x, std) + + if self.affine_weight: + x = x * self.gamma.astype(x.dtype) + + if self.affine_bias: + beta = ops.mul(self.beta.astype(x.dtype), mask) + x = x + beta + + return x + + +class GraphInstanceNormMask(nn.Cell): + """GraphInstanceNormMask""" + + def __init__(self, + num_features, + eps=1e-5, + sub_mean=True, + divide_std=True, + affine_weight=True, + affine_bias=True, + aggr_mode="mean"): + super().__init__() + self.num_features = num_features + self.eps = eps + self.sub_mean = sub_mean + self.divide_std = divide_std + self.affine_weight = affine_weight + self.affine_bias = affine_bias + self.mean = ops.ReduceMean(keep_dims=True) + self.aggregate = AggregateNodeToGlobal(mode=aggr_mode) + self.lift = LiftGlobalToNode(mode="multi_graph") + + if affine_weight: + self.gamma = Parameter(ops.ones((self.num_features,), ms.float32), name="gamma", requires_grad=True) + if affine_bias: + self.beta = Parameter(ops.zeros((self.num_features,), ms.float32), name="beta", requires_grad=True) + + def construct(self, x, batch, mask, dim_size, scale=None): + """construct""" + if x.shape[1] != self.num_features: + raise ValueError(f"x.shape[1] {x.shape[1]} is not equal to num_features {self.num_features}") + if x.shape[0] != mask.shape[0]: + raise ValueError(f"x.shape[0] {x.shape[0]} is not equal to mask.shape[0] {mask.shape[0]}") + if x.shape[0] != batch.shape[0]: + raise ValueError(f"x.shape[0] {x.shape[0]} is not equal to batch.shape[0] {batch.shape[0]}") + + if x.ndim != mask.ndim: + if mask.size != mask.shape[0]: + raise ValueError("mask.ndim dose not match src.ndim, and cannot be broadcasted to the same") + shape = [1] * x.ndim + shape[0] = -1 + mask = ops.reshape(mask, shape).astype(x.dtype) + x = ops.mul(x, mask) + gamma = None # 后来添加,防止未定义报错 + if x.ndim > 2: + norm_axis = [] + shape = [-1] + for i in range(2, x.ndim): + norm_axis.append(i) + shape.append(1) + + if self.affine_weight: + gamma = ops.reshape(self.gamma.astype(x.dtype), shape) + if self.affine_bias: + beta = ops.reshape(self.beta.astype(x.dtype), shape) + else: + if self.affine_weight: + gamma = self.gamma.astype(x.dtype) + if self.affine_bias: + beta = self.beta.astype(x.dtype) + + if self.sub_mean: + mean = self.aggregate(x, batch, dim_size=dim_size, mask=mask) + if x.ndim > 2: + mean = self.mean(mean, norm_axis) + mean = self.lift(mean, batch) + mean = ops.mul(mean, mask) + x = x - mean + + if self.divide_std: + var = self.aggregate(ops.square(x), batch, dim_size=dim_size, mask=mask) + if x.ndim > 2: + var = self.mean(var, norm_axis) + if scale is not None: + var = var * scale + std = ops.sqrt(var + self.eps) + std = self.lift(std, batch) + x = ops.true_divide(x, std) + + if self.affine_weight: + x = x * gamma + + if self.affine_bias: + beta = ops.mul(beta, mask) + x = x + beta + + return x diff --git a/MindChem/applications/nequip/mindchemistry/so2_conv/__init__.py b/MindChem/applications/nequip/mindchemistry/so2_conv/__init__.py new file mode 100644 index 000000000..e5542a477 --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/so2_conv/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +init file +""" +from .so3 import SO3Rotation +from .so2 import SO2Convolution diff --git a/MindChem/applications/nequip/mindchemistry/so2_conv/init_edge_rot_mat.py b/MindChem/applications/nequip/mindchemistry/so2_conv/init_edge_rot_mat.py new file mode 100644 index 000000000..a05a2264b --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/so2_conv/init_edge_rot_mat.py @@ -0,0 +1,64 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +file to get rotating matrix from edge distance vector +""" +from mindspore import ops +import mindspore.numpy as ms_np + + +def init_edge_rot_mat(edge_distance_vec): + """ + get rotating matrix from edge distance vector + """ + epsilon = 0.00000001 + edge_vec_0 = edge_distance_vec + edge_vec_0_distance = ops.sqrt(ops.maximum(ops.sum(edge_vec_0 ** 2, dim=1), epsilon)) + # Make sure the atoms are far enough apart + norm_x = ops.div(edge_vec_0, edge_vec_0_distance.view(-1, 1)) + edge_vec_2 = ops.rand_like(edge_vec_0) - 0.5 + + edge_vec_2 = ops.div(edge_vec_2, ops.sqrt(ops.maximum(ops.sum(edge_vec_2 ** 2, dim=1), epsilon)).view(-1, 1)) + # Create two rotated copies of the random vectors in case the random vector is aligned with norm_x + # With two 90 degree rotated vectors, at least one should not be aligned with norm_x + edge_vec_2b = edge_vec_2.copy() + edge_vec_2b[:, 0] = -edge_vec_2[:, 1] + edge_vec_2b[:, 1] = edge_vec_2[:, 0] + edge_vec_2c = edge_vec_2.copy() + edge_vec_2c[:, 1] = -edge_vec_2[:, 2] + edge_vec_2c[:, 2] = edge_vec_2[:, 1] + vec_dot_b = ops.abs(ops.sum(edge_vec_2b * norm_x, dim=1)).view(-1, 1) + vec_dot_c = ops.abs(ops.sum(edge_vec_2c * norm_x, dim=1)).view(-1, 1) + vec_dot = ops.abs(ops.sum(edge_vec_2 * norm_x, dim=1)).view(-1, 1) + edge_vec_2 = ops.where(ops.broadcast_to(ops.gt(vec_dot, vec_dot_b), edge_vec_2b.shape), edge_vec_2b, edge_vec_2) + vec_dot = ops.abs(ops.sum(edge_vec_2 * norm_x, dim=1)).view(-1, 1) + edge_vec_2 = ops.where(ops.broadcast_to(ops.gt(vec_dot, vec_dot_c), edge_vec_2c.shape), edge_vec_2c, edge_vec_2) + vec_dot = ops.abs(ops.sum(edge_vec_2 * norm_x, dim=1)) + # Check the vectors aren't aligned + + norm_z = ms_np.cross(norm_x, edge_vec_2, axis=1) + norm_z = ops.div(norm_z, ops.sqrt(ops.maximum(ops.sum(norm_z ** 2, dim=1, keepdim=True), epsilon))) + norm_z = ops.div(norm_z, ops.sqrt(ops.maximum(ops.sum(norm_z ** 2, dim=1), epsilon)).view(-1, 1)) + + norm_y = ms_np.cross(norm_x, norm_z, axis=1) + norm_y = ops.div(norm_y, ops.sqrt(ops.maximum(ops.sum(norm_y ** 2, dim=1, keepdim=True), epsilon))) + # Construct the 3D rotation matrix + norm_x = norm_x.view(-1, 3, 1) + norm_y = -norm_y.view(-1, 3, 1) + norm_z = norm_z.view(-1, 3, 1) + edge_rot_mat_inv = ops.cat([norm_z, norm_x, norm_y], axis=2) + + edge_rot_mat = ops.swapaxes(edge_rot_mat_inv, 1, 2) + return edge_rot_mat diff --git a/MindChem/applications/nequip/mindchemistry/so2_conv/jd.pkl b/MindChem/applications/nequip/mindchemistry/so2_conv/jd.pkl new file mode 100644 index 0000000000000000000000000000000000000000..1b762ad4369564e2f21b808850cc8013e6e41385 GIT binary patch literal 9925 zcmcIq4RBP|6}}+^l0qOM4QmT_sGvls0WH-G4ZA=R&}>7NA2T`k zy?5?+zWd#C&U<;GqVt*^_b41GRj#5L#rdVPeI*5{(|komzT(ufg5pwNiB;8Qq8Y5V z?tRvJ#;S^$mpf~2fmM}UJhy0ex%SpmissHLn~_^ml3Q+7b)Q;NFwIw7T2?Z5TA8(4 zPk^IU)wMX^xU9CkYN?eGm1ixtCRi!nDEYU{DvYa&$uBFPT_BS>O&?}et}yqbtD@4a zoSdBO8~VTxuPU?hDlbMx8t09} zf=6dY-5hoNd>__ux$|Ga$9RpzwbsY(4aBj?n`{J_Bxak&AWNK8O@v8(0swVA43mc))3(9eGk#z za1Kdqq2TCPdnyLFbLw9K-V2+&z?aD(c<^xq7;w6J`fK1(hj|-&0fXycT>5ncTl-?q zhvyxHedFY>Jk*8jVBC)25>Kgd(BS1QNBz{9x^NvG!4-buFL-8N#Rvy=81IcYIkBpNlq=+WL9;&AWlW>OT)4-sBBy-QufzaY$~D#I?bL`@<)0k_U6z!gO-|;C+{Y z`Kjwg$vaS2kJteZDY27hw+p$?{4!hBtqlEeOlejGwwV0fx5Jh-nIwV$9PKp2wG3xaEG6H z%{=42b06A9ZU2twDPFQZu@XvJ zq;@|+eQn$l@~4-*fgMg9l-!69$mVOZ|f$G(0Z<`1t?Kk{#XrR{q#zV^i0 zA3XR+@Ze18Kg?g|5BHDy(f4*N*KfyP@4lqBP7!6(6 zUv=H#`l7T3Smq)3ojP+}#IaW#j?q)dE&Xoq7PNDtt6?wg{SvnSkntYY1@n;mPMx`~ zaIhf;`6%@Tc)YSd347O&wa@VL#MZG+Sr^Ph?mKlRW;iVMAs?O3;Is1ahp@M2GpD%O zH&~~v3+5sBJsf<9!Tq8>>I38)&=vBnBlOJKZ0{f@0VE7N38G> ztKUO^>fk*z*_&*7dQ%rO%jwSn%|Aoj><7Dn$6H;?{W3SL$8zMo{`MEr+Y@l@p@PTY z!#VpK%=5T)?Aze(GEm8eSmTzN z=9}y<795Wl&pj=#f~Qj7LCjr!ew!|w?(v#$0{a_w1nw(E{m4(L7TnXCV=V)&nI0qa z!+mr)HWsly{LI8Gxpp1rDb+i6NvurQP-rJ(?l1KtKczOi<$gl;*RV1U_whvQ9`M=M z@I3fki*174I4BAFe!l$&uo9Q~$Ni;#Hoxl;;<*5v#x>T$ZXIRbk17||y|U0mWkz1MtqS=RSXiz6j><6qj<}>pp5GT)kyt~B3Jw3cr6 zQ}!$N1J)b!ITAdXf81Z{M}A7pa!cI#zd5;+{hs}l{fhk{wBEwbRo91LO8$wJ{S_SEGchyPOmh0Ovi!S@aebUL&T3Cdl&d@Y3YkP(cjUF@ag`d`TL934H$c-*nbbO=Y)5(s@Z%W)p~(L?rS{|d)!BNVLhk2Rpa{NoEhLR z(aBd2;j^yrZp%xcMJmgmdww zzBb=<+#6Wm2#l6(rdNn>q>k-VYZ0sQz^kxsD-Il%9x{J7^d0&K&SzF9g1_)!K9Cdn z>T~!kx%>xU@qQ$1O}6>QmN~{~_3In65z{pw1JBw6JKqEEMaPB(bAkEGe4xHI-;{{+ zZhWZ*F&cgUhJCr_9PD3OPs0vq{s4BvoY!Gl$E*wH8TXAk5H}K9+n6cPR<+!K&3gX{ zzua3 0 coefficients + for m in range(self.global_max_order): + if m == 0: + continue + x_m = m_list_merge[m] + x_m = x_m.reshape(num_edges, 2, -1) + x_m = self.so2_m_conv[m - 1](x_m) + out.append(x_m) + + ###################### start fill 0 ###################### + if self.max_order_out + 1 > len(m_list_merge): + for m in range(len(m_list_merge), self.max_order_out + 1): + extra_zero = ops.zeros( + (num_edges, 2, int(self.m_shape_dict_out.get(m, None) / 2))) + out.append(extra_zero) + ###################### finish fill 0 ###################### + + ###################### start _l_primary ######################### + l_primary_list_0 = [] + l_primary_list_left = [] + l_primary_list_right = [] + + for _ in range(self.irreps_out_length): + l_primary_list_0.append([]) + l_primary_list_left.append([]) + l_primary_list_right.append([]) + + m_0 = out[0] + offset = 0 + index = 0 + + for key_val in self.irreps_out_data: + key = key_val[0] + value = key_val[1] + if key >= 0: + l_primary_list_0[index].append( + ops.unsqueeze(m_0[:, offset:offset + value], -1)) + offset = offset + value + index = index + 1 + + for m in range(1, len(out)): + right = out[m][:, 1] + offset = 0 + index = 0 + + for key_val in self.irreps_out_data: + key = key_val[0] + value = key_val[1] + if key >= m: + l_primary_list_right[index].append( + ops.unsqueeze(right[:, offset:offset + value], -1)) + offset = offset + value + index = index + 1 + + for m in range(len(out) - 1, 0, -1): + left = out[m][:, 0] + offset = 0 + index = 0 + + for key_val in self.irreps_out_data: + key = key_val[0] + value = key_val[1] + if key >= m: + l_primary_list_left[index].append( + ops.unsqueeze(left[:, offset:offset + value], -1)) + offset = offset + value + index = index + 1 + + l_primary_list = [] + for i in range(self.irreps_out_length): + if i == 0: + tmp = ops.cat(l_primary_list_0[i], -1) + l_primary_list.append(tmp) + else: + tmp = ops.cat( + (ops.cat((ops.cat(l_primary_list_left[i], + -1), ops.cat(l_primary_list_0[i], -1)), + -1), ops.cat(l_primary_list_right[i], -1)), -1) + l_primary_list.append(tmp) + + ##################### finish _l_primary ######################### + return tuple(l_primary_list) diff --git a/MindChem/applications/nequip/mindchemistry/so2_conv/so3.py b/MindChem/applications/nequip/mindchemistry/so2_conv/so3.py new file mode 100644 index 000000000..0ffae5a5f --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/so2_conv/so3.py @@ -0,0 +1,156 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +so3 file +""" +import mindspore as ms +from mindspore import nn, ops, vmap, jit_class +from mindspore.numpy import tensordot +from mindscience.e3nn import o3 +from mindscience.e3nn.o3 import Irreps + +from .wigner import wigner_D + + +class SO3Embedding(nn.Cell): + """ + SO3Embedding class + """ + + def __init__(self): + self.embedding = None + + def _rotate(self, so3rotation, lmax_list, max_list): + """ + SO3Embedding rotate + """ + embedding_rotate = so3rotation[0].rotate(self.embedding, lmax_list[0], + max_list[0]) + self.embedding = embedding_rotate + + def _rotate_inv(self, so3rotation): + """ + SO3Embedding rotate inverse + """ + embedding_rotate = so3rotation[0].rotate_inv(self.embedding, + self.lmax_list[0], + self.mmax_list[0]) + self.embedding = embedding_rotate + + +@jit_class +class SO3Rotation: + """ + SO3_Rotation class + """ + + def __init__(self, lmax, irreps_in, irreps_out): + self.lmax = lmax + self.irreps_in1 = Irreps(irreps_in) + self.irreps_out = Irreps(irreps_out) + self.tensordot_vmap = vmap(tensordot, (0, 0, None), 0) + + @staticmethod + def narrow(inputs, axis, start, length): + """ + SO3_Rotation narrow class + """ + begins = [0] * inputs.ndim + begins[axis] = start + + sizes = list(inputs.shape) + + sizes[axis] = length + res = ops.slice(inputs, begins, sizes) + return res + + @staticmethod + def rotation_to_wigner_d_matrix(edge_rot_mat, start_lmax, end_lmax): + """ + SO3_Rotation rotation_to_wigner_d_matrix + """ + x = edge_rot_mat @ ms.Tensor([0.0, 1.0, 0.0]) + alpha, beta = o3.xyz_to_angles(x) + rvalue = (ops.swapaxes( + o3.angles_to_matrix(alpha, beta, ops.zeros_like(alpha)), -1, -2) + @ edge_rot_mat) + gamma = ops.atan2(rvalue[..., 0, 2], rvalue[..., 0, 0]) + + block_list = [] + for lmax in range(start_lmax, end_lmax + 1): + block = wigner_D(lmax, alpha, beta, gamma).astype(ms.float32) + block_list.append(block) + return block_list + + def set_wigner(self, rot_mat3x3): + """ + SO3_Rotation set_wigner + """ + wigner = self.rotation_to_wigner_d_matrix(rot_mat3x3, 0, self.lmax) + wigner_inv = [] + length = len(wigner) + for i in range(length): + wigner_inv.append(ops.swapaxes(wigner[i], 1, 2)) + return tuple(wigner), tuple(wigner_inv) + + def rotate(self, embedding, wigner): + """ + SO3_Rotation rotate + """ + res = [] + batch_shape = embedding.shape[:-1] + for (s, l), mir in zip(self.irreps_in1.slice_tuples, + self.irreps_in1.data): + v_slice = self.narrow(embedding, -1, s, l) + if embedding.ndim == 1: + res.append((v_slice.reshape((1,) + batch_shape + + (mir.mul, mir.ir.dim)), mir.ir)) + else: + res.append( + (v_slice.reshape(batch_shape + (mir.mul, mir.ir.dim)), + mir.ir)) + rotate_data_list = [] + for data, ir in res: + self.tensordot_vmap(data.astype(ms.float16), + wigner[ir.l].astype(ms.float16), ([1], [1])) + rotate_data = self.tensordot_vmap(data.astype(ms.float16), + wigner[ir.l].astype(ms.float16), + ((1), (1))).astype(ms.float32) + rotate_data_list.append(rotate_data) + return tuple(rotate_data_list) + + def rotate_inv(self, embedding, wigner_inv): + """ + SO3_Rotation rotate_inv + """ + res = [] + batch_shape = embedding[0].shape[0:1] + index = 0 + for (_, _), mir in zip(self.irreps_out.slice_tuples, + self.irreps_out.data): + v_slice = embedding[index] + if embedding[0].ndim == 1: + res.append((v_slice, mir.ir)) + else: + res.append((v_slice, mir.ir)) + index = index + 1 + rotate_back_data_list = [] + for data, ir in res: + rotate_back_data = self.tensordot_vmap( + data.astype(ms.float16), wigner_inv[ir.l].astype(ms.float16), + ((1), (1))).astype(ms.float32) + rotate_back_data_list.append( + rotate_back_data.view(batch_shape + (-1,))) + return ops.cat(rotate_back_data_list, -1) diff --git a/MindChem/applications/nequip/mindchemistry/so2_conv/wigner.py b/MindChem/applications/nequip/mindchemistry/so2_conv/wigner.py new file mode 100644 index 000000000..c3e08615c --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/so2_conv/wigner.py @@ -0,0 +1,61 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +wigner file +""" + +# pylint: disable=C0103 +import pickle +from mindspore import ops +import mindspore as ms +from mindscience.e3nn.utils.func import broadcast_args + + +def wigner_D(lv, alpha, beta, gamma): + """ + # Borrowed from e3nn @ 0.4.0: + # https://github.com/e3nn/e3nn/blob/0.4.0/e3nn/o3/_wigner.py#L10 + # jd is a list of tensors of shape (2l+1, 2l+1) + + # Borrowed from e3nn @ 0.4.0: + # https://github.com/e3nn/e3nn/blob/0.4.0/e3nn/o3/_wigner.py#L37 + # + # In 0.5.0, e3nn shifted to torch.matrix_exp which is significantly slower: + # https://github.com/e3nn/e3nn/blob/0.5.0/e3nn/o3/_wigner.py#L92 + """ + jd = None + with open("jd.pkl", "rb") as f: + jd = pickle.load(f) + if not lv < len(jd): + raise NotImplementedError( + f"wigner D maximum l implemented is {len(jd) - 1}, send us an email to ask for more" + ) + alpha, beta, gamma = broadcast_args(alpha, beta, gamma) + j = jd[lv] + xa = _z_rot_mat(alpha, lv) + xb = _z_rot_mat(beta, lv) + xc = _z_rot_mat(gamma, lv) + return xa @ j.astype(ms.float16) @ xb @ j.astype(ms.float16) @ xc + + +def _z_rot_mat(angle, lv): + shape = angle.shape + m = ops.zeros((shape[0], 2 * lv + 1, 2 * lv + 1)) + inds = ops.arange(0, 2 * lv + 1, 1) + reversed_inds = ops.arange(2 * lv, -1, -1) + frequencies = ops.arange(lv, -lv - 1, -1) + m[..., inds, reversed_inds] = ops.sin(frequencies * angle[..., None]) + m[..., inds, inds] = ops.cos(frequencies * angle[..., None]) + return m.astype(ms.float16) diff --git a/MindChem/applications/nequip/mindchemistry/utils/__init__.py b/MindChem/applications/nequip/mindchemistry/utils/__init__.py new file mode 100644 index 000000000..3f15063d7 --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/utils/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this filepio[] except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""init""" +from .load_config import load_yaml_config + +__all__ = ['load_yaml_config'] diff --git a/MindChem/applications/nequip/mindchemistry/utils/check_func.py b/MindChem/applications/nequip/mindchemistry/utils/check_func.py new file mode 100644 index 000000000..711a441fe --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/utils/check_func.py @@ -0,0 +1,128 @@ +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""functions""" +from __future__ import absolute_import + +from mindspore import context + +_SPACE = " " + + +def _convert_to_tuple(params): + if params is None: + return params + if not isinstance(params, (list, tuple)): + params = (params,) + if isinstance(params, list): + params_out = tuple(params) + else: + params_out = params # ✅ 防止未定义 + return params_out + + +def check_param_type(param, param_name, data_type=None, exclude_type=None): + """Check parameter's data type""" + data_type = _convert_to_tuple(data_type) + exclude_type = _convert_to_tuple(exclude_type) + + if data_type and not isinstance(param, data_type): + raise TypeError( + f"The type of {param_name} should be instance of {data_type}, but got {param} with type {type(param)}" + ) + if exclude_type and type(param) in exclude_type: + raise TypeError( + f"The type of {param_name} should not be instance of {exclude_type},but got {param} with type {type(param)}" + ) + + +def check_param_value(param, param_name, valid_value): + """check parameter's value""" + valid_value = _convert_to_tuple(valid_value) + if param not in valid_value: + raise ValueError(f"The value of {param_name} should be in {valid_value}, but got {param}") + + +def check_param_type_value(param, param_name, valid_value, data_type=None, exclude_type=None): + """check both data type and value""" + check_param_type(param, param_name, data_type=data_type, exclude_type=exclude_type) + check_param_value(param, param_name, valid_value) + + +def check_dict_type(param_dict, param_name, key_type=None, value_type=None): + """check data type for key and value of the specified dict""" + check_param_type(param_dict, param_name, data_type=dict) + + for key in param_dict.keys(): + if key_type: + check_param_type(key, _SPACE.join(("key of", param_name)), data_type=key_type) + if value_type: + values = _convert_to_tuple(param_dict[key]) + for value in values: + check_param_type(value, _SPACE.join(("value of", param_name)), data_type=value_type) + + +def check_dict_value(param_dict, param_name, key_value=None, value_value=None): + """check values for key and value of specified dict""" + check_param_type(param_dict, param_name, data_type=dict) + + for key in param_dict.keys(): + if key_value: + check_param_value(key, _SPACE.join(("key of", param_name)), key_value) + if value_value: + values = _convert_to_tuple(param_dict[key]) + for value in values: + check_param_value(value, _SPACE.join(("value of", param_name)), value_value) + + +def check_dict_type_value(param_dict, param_name, key_type=None, value_type=None, key_value=None, value_value=None): + """check values for key and value of specified dict""" + check_dict_type(param_dict, param_name, key_type=key_type, value_type=value_type) + check_dict_value(param_dict, param_name, key_value=key_value, value_value=value_value) + + +def check_mode(api_name): + """check running mode""" + if context.get_context("mode") == context.PYNATIVE_MODE: + raise RuntimeError(f"{api_name} is only supported GRAPH_MODE now but got PYNATIVE_MODE") + + +def check_param_no_greater(param, param_name, compared_value): + """ Check whether the param less than the given compared_value""" + if param > compared_value: + raise ValueError(f"The value of {param_name} should be no greater than {compared_value}, but got {param}") + + +def check_param_odd(param, param_name): + """ Check whether the param is an odd number""" + if param % 2 == 0: + raise ValueError(f"The value of {param_name} should be an odd number, but got {param}") + + +def check_param_even(param, param_name): + """ Check whether the param is an even number""" + for value in param: + if value % 2 != 0: + raise ValueError(f"The value of {param_name} should be an even number, but got {param}") + + +def check_lr_param_type_value(param, param_name, param_type, thresh_hold=0, restrict=False, exclude=None): + if (exclude and isinstance(param, exclude)) or not isinstance(param, param_type): + raise TypeError(f"the type of {param_name} should be {param_type}, but got {type(param)}") + if restrict: + if param <= thresh_hold: + raise ValueError(f"the value of {param_name} should be > {thresh_hold}, but got: {param}") + else: + if param < thresh_hold: + raise ValueError(f"the value of {param_name} should be >= {thresh_hold}, but got: {param}") diff --git a/MindChem/applications/nequip/mindchemistry/utils/load_config.py b/MindChem/applications/nequip/mindchemistry/utils/load_config.py new file mode 100644 index 000000000..3ddc76e42 --- /dev/null +++ b/MindChem/applications/nequip/mindchemistry/utils/load_config.py @@ -0,0 +1,85 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +utility functions +""" +import os +import yaml + + +def _make_paths_absolute(dir_, config): + """ + Make all values for keys ending with `_path` absolute to dir_. + + Args: + dir_ (str): The path of yaml configuration file. + config (dict): The yaml for configuration file. + + Returns: + Dict. The configuration information in dict format. + """ + for key in config.keys(): + if key.endswith("_path"): + config[key] = os.path.join(dir_, config[key]) + config[key] = os.path.abspath(config[key]) + if isinstance(config[key], dict): + config[key] = _make_paths_absolute(dir_, config[key]) + return config + + +def load_yaml_config(file_path): + """ + Load a YAML configuration file. + + Args: + file_path (str): The path of yaml configuration file. + + Returns: + Dict. The configuration information in dict format. + + Supported Platforms: + ``Ascend`` ``CPU`` ``GPU`` + + Examples: + >>> from mindchemistry.utils import load_yaml_config + >>> config_file_path = 'xxx' # 'xxx' is the file_path + >>> configs = load_yaml_config(config_file_path) + """ + # Read YAML experiment definition file + with open(file_path, 'r', encoding='utf-8') as stream: + config = yaml.safe_load(stream) + config = _make_paths_absolute(os.path.join( + os.path.dirname(file_path), ".."), config) + return config + + +def load_yaml_config_from_path(file_path): + """ + Load a YAML configuration file. + + Args: + file_path (str): The path of yaml configuration file. + + Returns: + Dict. The configuration information in dict format. + + Supported Platforms: + ``Ascend`` ``CPU`` ``GPU`` + """ + # Read YAML experiment definition file + with open(file_path, 'r', encoding='utf-8') as stream: + config = yaml.safe_load(stream) + + return config diff --git a/MindChem/applications/nequip/models/__init__.py b/MindChem/applications/nequip/models/__init__.py deleted file mode 100644 index 5bd32c7d8..000000000 --- a/MindChem/applications/nequip/models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# models/__init__.py -"""Nequip models package. - -Modules are imported explicitly, e.g.: - from models.nequip import Nequip -""" diff --git a/MindChem/applications/nequip/nequip.ipynb b/MindChem/applications/nequip/nequip.ipynb index 914f77b7c..137351194 100644 --- a/MindChem/applications/nequip/nequip.ipynb +++ b/MindChem/applications/nequip/nequip.ipynb @@ -32,10 +32,25 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 1, "id": "27d5afd4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/zdai/miniconda3/envs/mindc_py39/lib/python3.9/site-packages/numpy/core/getlimits.py:549: UserWarning: The value of the smallest subnormal for type is zero.\n", + " setattr(self, word, getattr(machar, word).flat[0])\n", + "/home/zdai/miniconda3/envs/mindc_py39/lib/python3.9/site-packages/numpy/core/getlimits.py:89: UserWarning: The value of the smallest subnormal for type is zero.\n", + " return self._float_to_str(self.smallest_subnormal)\n", + "/home/zdai/miniconda3/envs/mindc_py39/lib/python3.9/site-packages/numpy/core/getlimits.py:549: UserWarning: The value of the smallest subnormal for type is zero.\n", + " setattr(self, word, getattr(machar, word).flat[0])\n", + "/home/zdai/miniconda3/envs/mindc_py39/lib/python3.9/site-packages/numpy/core/getlimits.py:89: UserWarning: The value of the smallest subnormal for type is zero.\n", + " return self._float_to_str(self.smallest_subnormal)\n" + ] + } + ], "source": [ "import logging\n", "import math\n", @@ -48,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 2, "id": "edf71a8f", "metadata": {}, "outputs": [], @@ -132,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 3, "id": "d0af49e8", "metadata": {}, "outputs": [ @@ -169,7 +184,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 4, "id": "c9ba202b", "metadata": {}, "outputs": [], @@ -178,7 +193,7 @@ "import logging\n", "from mindspore import nn, Profiler\n", "from src.dataset import _unpack\n", - "from models.nequip import Nequip\n", + "from mindchemistry.cell import Nequip\n", "from tqdm.notebook import tqdm\n", "\n", "def generate_learning_rate(learning_rate, warmup_steps, step_num):\n", @@ -323,14 +338,14 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 5, "id": "a998db9f", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d84e6c0ef583401e8ecb172ffcc2c498", + "model_id": "c5da6ca2f23047ceb1b9ad26723dc0ca", "version_major": 2, "version_minor": 0 }, @@ -341,10 +356,17 @@ "metadata": {}, "output_type": "display_data" }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "......." + ] + }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ba9ceac7856642078bd57a868c005966", + "model_id": "8d458ccb5cfd4b0384776db5cd33e593", "version_major": 2, "version_minor": 0 }, @@ -369,13 +391,13 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 6, "id": "6c82ce40", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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22+/xf79+9GiRYuKLLPaKGs/r169ihs3bqBPnz6qMaVSCQDQ09NDSkoKnJycKrboKq48r1E7Ozvo6+tDKpWqxlxdXZGWlob8/HzIZLIKrbkqK08/p0+fjiFDhmDUqFEAAHd3dzx79gwffvghvv76a0gkXDcoq5J+N5mammp8VQfgyk61IpPJ0LZtWxw4cEA1plQqceDAAXh6eha7jaenp9p8AIiOji5xfk1Tnp4CwIIFCzB37lzs3bsXHh4elVFqtVDWfrq4uODMmTNITk5W3fr27Ys33ngDycnJcHBwqMzyq6TyvEY7deqEK1euqIIjAFy6dAl2dnY1OugA5etnTk5OkUDzMkiK/HrJcqn0300VctkzVZjNmzeLcrlcXL9+vXj+/Hnxww8/FM3NzcW0tDRRFEVxyJAh4pdffqmaf/ToUVFPT09ctGiReOHCBXHmzJl86/m/lLWn3377rSiTycTt27eL9+7dU92ys7O19RSqlLL289/4bqyiytrT1NRU0cTERJwwYYKYkpIi7tq1S7S2thb/97//aespVCll7efMmTNFExMT8ffffxevXbsm7tu3T3RychIDAgK09RSqnOzsbDEpKUlMSkoSAYhLliwRk5KSxJs3b4qiKIpffvmlOGTIENX8l289nzx5snjhwgVx5cqVfOs5qVuxYoVYv359USaTie3btxePHTumeqxr167isGHD1OZv3bpVbNq0qSiTycTmzZuLu3fvruSKq76y9LRBgwYigCK3mTNnVn7hVVRZX6P/xLBTvLL2NC4uTuzQoYMol8vFRo0aid98841YUFBQyVVXXWXpp0KhEGfNmiU6OTmJBgYGooODgzhu3Djx0aNHlV94FXXw4MFi/1182cdhw4aJXbt2LbJNq1atRJlMJjZq1Ehct25dhdUniCLX4IiIiEh38ZodIiIi0mkMO0RERKTTGHaIiIhIpzHsEBERkU5j2CEiIiKdxrBDREREOo1hh4iIiHQaww4RERHpNIYdIiINEwQB4eHh2i6DiP4/hh0i0ilBQUEQBKHIzc/PT9ulEZGW6Gm7ACIiTfPz88O6devUxuRyuZaqISJt48oOEekcuVwOW1tbtVvt2rUBvDjFtGrVKrz99tswNDREo0aNsH37drXtz5w5gzfffBOGhoawtLTEhx9+iKdPn6rNWbt2LZo3bw65XA47OztMmDBB7fGMjAz0798fRkZGaNKkCSIiIir2SRNRiRh2iKjGmT59Ot59912cPn0a77//Pt577z1cuHABAPDs2TP4+vqidu3aOHnyJLZt24b9+/erhZlVq1Zh/Pjx+PDDD3HmzBlERESgcePGaseYPXs2AgIC8Pfff6Nnz554//338fDhw0p9nkT0/1XY96kTEWnBsGHDRKlUKhobG6vdvvnmG1EURRGA+NFHH6lt06FDB3Hs2LGiKIpiaGioWLt2bfHp06eqx3fv3i1KJBIxLS1NFEVRtLe3F7/++usSawAgTps2TXX/6dOnIgBxz549GnueRFR6vGaHiHTOG2+8gVWrVqmNWVhYqP7s6emp9pinpyeSk5MBABcuXEDLli1hbGyserxTp05QKpVISUmBIAi4e/cu3nrrrVfW0KJFC9WfjY2NYWpqivv375f3KRHRa2DYISKdY2xsXOS0kqYYGhqWap6+vr7afUEQoFQqK6IkIvoPvGaHiGqcY8eOFbnv6uoKAHB1dcXp06fx7Nkz1eNHjx6FRCKBs7MzTExM4OjoiAMHDlRqzURUflzZISKdk5eXh7S0NLUxPT09WFlZAQC2bdsGDw8PdO7cGRs3bsSJEyewZs0aAMD777+PmTNnYtiwYZg1axYePHiAjz/+GEOGDIGNjQ0AYNasWfjoo49gbW2Nt99+G9nZ2Th69Cg+/vjjyn2iRFQqDDtEpHP27t0LOzs7tTFnZ2dcvHgRwIt3Sm3evBnjxo2DnZ0dfv/9dzRr1gwAYGRkhKioKEycOBHt2rWDkZER3n33XSxZskS1r2HDhiE3NxdLly7F559/DisrKwwYMKDyniARlYkgiqKo7SKIiCqLIAjYsWMH/P39tV0KEVUSXrNDREREOo1hh4iIiHQar9khohqFZ+6Jah6u7BAREZFOY9ghIiIincawQ0RERDqNYYeIiIh0GsMOERER6TSGHSIiItJpDDtERESk0xh2iIiISKf9PwFIzTasMWMeAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -407,7 +429,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 7, "id": "07bf6541", "metadata": {}, "outputs": [], @@ -548,7 +570,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 8, "id": "d5f4d14b", "metadata": {}, "outputs": [ @@ -566,50 +588,50 @@ "[INFO] Loading checkpoint from ./checkpoints/NequIP_rmd.ckpt ...\n", "[INFO] Checkpoint loaded successfully.\n", "[INFO] Starting evaluation loop...\n", - "pred_list: [array([[-0.00918175],\n", - " [-0.0069659 ],\n", - " [-0.0093594 ],\n", - " [-0.01029804],\n", - " [-0.00693889]], dtype=float32), array([[-0.00628732],\n", - " [-0.00432905],\n", - " [-0.00658406],\n", - " [-0.00915391],\n", - " [-0.00554573]], dtype=float32), array([[-0.0037866 ],\n", - " [-0.00855389],\n", - " [-0.00967694],\n", - " [-0.00387427],\n", - " [-0.00867008]], dtype=float32), array([[-0.00807515],\n", - " [-0.00734411],\n", - " [-0.00621774],\n", - " [-0.00358224],\n", - " [-0.00764645]], dtype=float32), array([[-0.01184925],\n", - " [-0.00891224],\n", - " [-0.00440742],\n", - " [-0.0059823 ],\n", - " [-0.00486465]], dtype=float32), array([[-0.00610668],\n", - " [-0.00731197],\n", - " [-0.0069651 ],\n", - " [-0.00702901],\n", - " [-0.00208446]], dtype=float32), array([[-0.00405062],\n", - " [-0.00664095],\n", - " [-0.007243 ],\n", - " [-0.00803612],\n", - " [-0.00245329]], dtype=float32), array([[-0.01103315],\n", - " [-0.00645517],\n", - " [-0.00555688],\n", - " [-0.00808087],\n", - " [-0.00549903]], dtype=float32), array([[-0.00511416],\n", - " [-0.00660476],\n", - " [-0.00612792],\n", - " [-0.00850576],\n", - " [-0.00551609]], dtype=float32), array([[-0.00403762],\n", - " [-0.00629103],\n", - " [-0.00982532],\n", - " [-0.00859255],\n", - " [-0.00804539]], dtype=float32)]\n", + "pred_list: [array([[0.01096156],\n", + " [0.01152362],\n", + " [0.0088922 ],\n", + " [0.00950281],\n", + " [0.00832928]], dtype=float32), array([[0.00859195],\n", + " [0.0075928 ],\n", + " [0.00821682],\n", + " [0.0109686 ],\n", + " [0.00735036]], dtype=float32), array([[0.01072004],\n", + " [0.00882988],\n", + " [0.00881849],\n", + " [0.00874564],\n", + " [0.01059538]], dtype=float32), array([[0.00778127],\n", + " [0.00994838],\n", + " [0.00931875],\n", + " [0.0101115 ],\n", + " [0.01051393]], dtype=float32), array([[0.00808171],\n", + " [0.00788514],\n", + " [0.00946121],\n", + " [0.00925998],\n", + " [0.01006626]], dtype=float32), array([[0.00661376],\n", + " [0.00876329],\n", + " [0.00891308],\n", + " [0.00958218],\n", + " [0.00865374]], dtype=float32), array([[0.00685227],\n", + " [0.00931741],\n", + " [0.00876838],\n", + " [0.01059075],\n", + " [0.00979946]], dtype=float32), array([[0.00971784],\n", + " [0.010439 ],\n", + " [0.00853684],\n", + " [0.01099091],\n", + " [0.00990788]], dtype=float32), array([[0.00750377],\n", + " [0.01113972],\n", + " [0.01076543],\n", + " [0.00947448],\n", + " [0.0092768 ]], dtype=float32), array([[0.00842626],\n", + " [0.00970375],\n", + " [0.0102906 ],\n", + " [0.00866342],\n", + " [0.00942835]], dtype=float32)]\n", "[INFO] Predictions and true values saved to: ./results\n", - "[RESULT] loss_mean: 0.02835126 metric_mean: 0.12784850\n", - "Returned: True 0.028351258439943194 0.12784849815070629\n" + "[RESULT] loss_mean: 0.02837591 metric_mean: 0.13121371\n", + "Returned: True 0.02837591350544244 0.1312137119472027\n" ] } ], @@ -620,13 +642,13 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 9, "id": "a8cd2e7f", "metadata": {}, "outputs": [ { "data": { - "image/png": "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EMKPJGQJIYQQQgghhB9JyBJCCNHnaDQaPvzww0A3g0WLFnHWWWf16mvk5uZy55139upr9KbXX3+diIiIQDdDCCH6FAlZQggxCFVVVXH77bczbNgwTCYTcXFxzJw5k40bNwa6aX5RUlKCRqNh27ZtgW7KSf3zn/9kyZIlp7WPG264AY1G4/sZMmQIs2bNYvv27T3az5kIlUIIMRhIyBJCiEHoqquuYuvWrbzxxhsUFRXx0UcfkZubS01NTaCbNuhERUVhsVhOez+zZs2ioqKCiooKPv/8c/R6PT/72c/80EIhhBA9JSFLCCEGmfr6er788ksef/xxpk+fTkpKCpMmTeK+++7jF7/4hW+7pUuXkpOTQ0hICMnJyfzud7+jubnZ9/jRYWIff/wxGRkZBAcH86tf/Qqr1cobb7xBamoqkZGR3HHHHXg8Ht/zUlNTWbJkCddccw0hISEkJiaybNmyLtt86NAhrr76aiIiIoiKiuLyyy+npKSk2+85Ly8PjUbD559/zsSJEwkODmbq1KkUFha22e6xxx4jNjYWi8XCTTfdhN1ub7evV199lczMTMxmM6NHj+b//b//53vsxhtvZOzYsTgcDgCcTidnn302v/71rztt24nDBVNTU3nkkUe48cYbsVgsDBs2jJdffvmk7/Foj2RcXBxnnXUWCxYs4NChQ1RVVfm2+Z//+R9GjRpFcHAww4cPZ+HChbhcLsD7/3Px4sX8+OOPvh6x119/HfCeM7fddhuxsbGYzWays7P5+OOP27z+mjVryMzMJDQ01Bf4hBBisJKQJYQQg0xoaCihoaF8+OGHvjDQEa1Wy3PPPcfOnTt54403+OKLL7j33nvbbGO1WnnuuedYsWIFq1evJi8vjyuvvJL//Oc//Oc//+Gtt97ipZde4h//+Eeb5z355JOMGzeOrVu3smDBAubPn8/atWs7bIfL5WLmzJlYLBa+/PJLNm7c6Psi73Q6e/Te77//fp5++mk2b96MXq/nxhtv9D32/vvvs2jRIh555BE2b95MfHx8mwAF8M477/DAAw/w8MMPU1BQwCOPPMLChQt54403AHjuuedoaWlhwYIFvterr6/nhRde6FE7n376aSZOnMjWrVv53e9+x+23394uEHalubmZt99+m/T0dIYMGeK732Kx8Prrr7Nr1y6effZZXnnlFZ555hkAZs+ezd13382YMWN8PWKzZ89GURQuvfRSNm7cyNtvv82uXbt47LHH0Ol0vv1arVaeeuop3nrrLTZs2MDBgwe55557evSehRBiQFGFEEIMOv/4xz/UyMhI1Ww2q1OnTlXvu+8+9ccff+zyOStXrlSHDBniu718+XIVUPfu3eu777bbblODg4PVpqYm330zZ85Ub7vtNt/tlJQUddasWW32PXv2bPXSSy/13QbUVatWqaqqqm+99ZaakZGhKorie9zhcKhBQUHqmjVrOmxrcXGxCqhbt25VVVVV161bpwLqZ5995tvm3//+twqoNptNVVVVnTJlivq73/2uzX4mT56sjhs3znd7xIgR6rvvvttmmyVLlqhTpkzx3f76669Vg8GgLly4UNXr9eqXX37ZYRuPmjZtmjp//nzf7ZSUFPXaa6/13VYURR06dKj617/+tdN9XH/99apOp1NDQkLUkJAQFVDj4+PVLVu2dPnaTz75pDphwgTf7QcffLDN+1VVVV2zZo2q1WrVwsLCDvfR0XmwbNkyNTY2tsvXFkKIgUx6soQQYhC66qqrKC8v56OPPmLWrFnk5eUxfvx43/AwgM8++4wZM2aQmJiIxWLhuuuuo6amBqvV6tsmODiYESNG+G7HxsaSmppKaGhom/uOHDnS5vWnTJnS7nZBQUGHbf3xxx/Zu3cvFovF1wsXFRWF3W5n3759PXrfY8eO9f13fHw8gK9tBQUFTJ48udN2trS0sG/fPm666SZfO0JDQ3nooYfatGPKlCncc889LFmyhLvvvpvzzz+/R208sZ0ajYa4uLh2x/BE06dPZ9u2bWzbto1NmzYxc+ZMLr30Ug4cOODb5r333uO8884jLi6O0NBQ/vznP3Pw4MEu97tt2zaSkpIYNWpUp9uceB7Ex8eftL1CCDGQ6QPdACGEEIFhNpu5+OKLufjii1m4cCE333wzDz74IDfccAMlJSX87Gc/4/bbb+fhhx8mKiqKr776iptuugmn00lwcDAABoOhzT41Gk2H9ymKcsrtbG5uZsKECbzzzjvtHouJienRvo5vm0ajAeh2247OR3vllVfahbHjh84pisLGjRvR6XTs3bu3R+3rqJ1H23qydoaEhJCenu67/eqrrxIeHs4rr7zCQw89xDfffMOcOXNYvHgxM2fOJDw8nBUrVvD00093ud+goKBTaq+qqid9nhBCDFQSsoQQQgCQlZXlW5tqy5YtKIrC008/jVbrHfTw/vvv++21vv3223a3MzMzO9x2/PjxvPfeewwdOpSwsDC/teFEmZmZfPfdd22KVBzfztjYWBISEti/fz9z5szpdD9PPvkku3fvZv369cycOZPly5fzm9/8ptfa3RmNRoNWq8VmswHw9ddfk5KSwv333+/b5vheLgCj0dimSAl4e9VKS0spKirqsjdLCCHEMTJcUAghBpmamhouvPBC3n77bbZv305xcTErV67kiSee4PLLLwcgPT0dl8vF888/z/79+3nrrbd48cUX/daGjRs38sQTT1BUVMSyZctYuXIl8+fP73DbOXPmEB0dzeWXX86XX35JcXExeXl53HHHHZSWlvqtTfPnz+dvf/sby5cvp6ioiAcffJCdO3e22Wbx4sU8+uijPPfccxQVFZGfn8/y5ctZunQpAFu3buWBBx7g1Vdf5bzzzmPp0qXMnz+f/fv3+62dnXE4HFRWVlJZWUlBQQG///3vaW5u5uc//zkAI0eO5ODBg6xYsYJ9+/bx3HPPsWrVqjb7SE1Npbi4mG3btlFdXY3D4WDatGlccMEFXHXVVaxdu5bi4mI++eQTVq9e3evvSQgh+isJWUIIMciEhoYyefJknnnmGS644AKys7NZuHAht9xyi68K3rhx41i6dCmPP/442dnZvPPOOzz66KN+a8Pdd9/N5s2bOfvss3nooYdYunQpM2fO7HDb4OBgNmzYwLBhw/jlL39JZmamr7y6P3u2Zs+ezcKFC7n33nuZMGECBw4c4Pbbb2+zzc0338yrr77K8uXLycnJYdq0abz++uukpaVht9u59tprueGGG3zB5tZbb2X69Olcd9117XqI/G316tXEx8cTHx/P5MmT+f7771m5ciW5ubkA/OIXv+APf/gD8+bN46yzzuLrr79m4cKFbfZx1VVXMWvWLKZPn05MTAx///vfAfjggw8455xzuOaaa8jKyuLee+/t9fcjhBD9mUaVQdNCCCHOoNTUVO688842a0MJIYQQA4n0ZAkhhBBCCCGEH0nIEkIIIYQQQgg/kuGCQgghhBBCCOFH0pMlhBBCCCGEEH4kIUsIIYQQQggh/EhClhBCCCGEEEL4kYQsIYQQQgghhPAjCVlCCCGEEEII4UcSsoQQQgghhBDCjyRkCSGEEEIIIYQfScgSQgghhBBCCD/6/wH2DHPKvKMlAAAAAABJRU5ErkJggg==", + "image/png": "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rl8KWUPIl19+iaeffhpr167F2bNnMW7cOKSlpaGmpsZu++PHj+P+++/HkiVLcO7cOdxzzz245557cP78+UHuuWvo6/kFLDuLV1VVWX9KS0sHsceupbW1FePGjcOWLVt61b64uBh33HEHZs+ejYyMDKxcuRIPP/ww9u3bN8A9dU19Pb/tcnNzba7hUaNGDVAPXdfhw4exdOlS/PLLL9i/fz+MRiNuvfVWtLa2dvscev/tm/6cY4Deg3srNDQUr776Ks6cOYPTp0/jpptuwt13340LFy7YbU/Xb9/09fwCdO32x6lTp/DBBx9g7NixPbZzmeuXkSFj6tSpbOnSpdbbZrOZBQcHs40bN9ptf99997E77rjD5r5p06axxx57bED76ar6en63bdvGPD09B6l3wwsAtmfPnh7brFq1io0ZM8bmvvnz57O0tLQB7Nnw0Jvze+jQIQaANTY2DkqfhpOamhoGgB0+fLjbNvT+e216c47pPfjaeHt7s61bt9p9jK7fa9fT+aVrt+9UKhWLjY1l+/fvZ7NmzWIrVqzotq2rXL80kjVEGAwGnDlzBjfffLP1PoFAgJtvvhknTpyw+5wTJ07YtAeAtLS0btuPZP05vwCgVqsRERGBsLCwq35rRfqGrt/BMX78eAQFBeGWW27BsWPHnN0dl9Dc3AwA8PHx6bYNXb/XpjfnGKD34P4wm83YtWsXWltbMX36dLtt6Prtv96cX4Cu3b5aunQp7rjjji7XpT2ucv1SyBoi6urqYDabERAQYHN/QEBAt2soqqur+9R+JOvP+Y2Pj8c//vEP/N///R8+++wz8DyPGTNmoLy8fDC6POx1d/22tLRAq9U6qVfDR1BQEN5//3188803+OabbxAWFobU1FScPXvW2V0b0niex8qVK3H99dcjOTm523b0/tt/vT3H9B7cN1lZWVAoFJBKpfjzn/+MPXv2ICkpyW5bun77ri/nl67dvtm1axfOnj2LjRs39qq9q1y/Imd3gJChavr06TbfUs2YMQOJiYn44IMPsGHDBif2jJCri4+PR3x8vPX2jBkzUFhYiLfffhs7duxwYs+GtqVLl+L8+fP4+eefnd2VYau355jeg/smPj4eGRkZaG5uxtdff42FCxfi8OHD3QYB0jd9Ob907fZeWVkZVqxYgf379w+74iAUsoYIPz8/CIVCXL582eb+y5cvIzAw0O5zAgMD+9R+JOvP+e1MLBZjwoQJKCgoGIgujjjdXb8eHh5wc3NzUq+Gt6lTp1J46MGyZcvw/fff48iRIwgNDe2xLb3/9k9fznFn9B7cM4lEgpiYGADApEmTcOrUKbzzzjv44IMPurSl67fv+nJ+O6Nrt3tnzpxBTU0NJk6caL3PbDbjyJEjeO+996DX6yEUCm2e4yrXL00XHCIkEgkmTZqEAwcOWO/jeR4HDhzods7v9OnTbdoDwP79+3ucIzxS9ef8dmY2m5GVlYWgoKCB6uaIQtfv4MvIyKDr1w7GGJYtW4Y9e/bg4MGDiIqKuupz6Prtm/6c487oPbhveJ6HXq+3+xhdv9eup/PbGV273ZszZw6ysrKQkZFh/Zk8eTIWLFiAjIyMLgELcKHr19mVN8gVu3btYlKplG3fvp1lZ2ezRx99lHl5ebHq6mrGGGMPPvggW716tbX9sWPHmEgkYm+++SbLyclha9euZWKxmGVlZTnrJQxpfT2/69evZ/v27WOFhYXszJkz7I9//COTyWTswoULznoJQ5pKpWLnzp1j586dYwDYpk2b2Llz51hpaSljjLHVq1ezBx980Nq+qKiIyeVy9txzz7GcnBy2ZcsWJhQK2d69e531Eoa0vp7ft99+m3377bcsPz+fZWVlsRUrVjCBQMB++uknZ72EIevxxx9nnp6eLD09nVVVVVl/NBqNtQ29/16b/pxjeg/uvdWrV7PDhw+z4uJilpmZyVavXs04jmP/+c9/GGN0/V6rvp5funavTefqgq56/VLIGmLeffddFh4eziQSCZs6dSr75ZdfrI/NmjWLLVy40Kb9V199xeLi4phEImFjxoxh//73vwe5x66lL+d35cqV1rYBAQHs9ttvZ2fPnnVCr11De8nwzj/t53ThwoVs1qxZXZ4zfvx4JpFI2OjRo9m2bdsGvd+uoq/n97XXXmPR0dFMJpMxHx8flpqayg4ePOiczg9x9s4rAJvrkd5/r01/zjG9B/fe4sWLWUREBJNIJMzf35/NmTPHGgAYo+v3WvX1/NK1e206hyxXvX45xhgbvHEzQgghhBBCCBneaE0WIYQQQgghhDgQhSxCCCGEEEIIcSAKWYQQQgghhBDiQBSyCCGEEEIIIcSBKGQRQgghhBBCiANRyCKEEEIIIYQQB6KQRQghhBBCCCEORCGLEEIIIYQQQhyIQhYhhJAhh+M4fPvtt87uBtatW4fx48cP6O9ITU3FypUrB/R3DKTt27fDy8vL2d0ghJAhhUIWIYSMQLW1tXj88ccRHh4OqVSKwMBApKWl4dixY87umkOUlJSA4zhkZGQ4uytX9c9//hMbNmy4pmMsWrQIHMdZf3x9fTF37lxkZmb26TiDESoJIWQkoJBFCCEj0Lx583Du3Dl88sknyMvLw3fffYfU1FTU19c7u2sjjo+PD5RK5TUfZ+7cuaiqqkJVVRUOHDgAkUiE3/3udw7oISGEkL6ikEUIISNMU1MTjh49itdeew2zZ89GREQEpk6diueffx533XWXtd2mTZuQkpICd3d3hIWF4YknnoBarbY+3j5N7Pvvv0d8fDzkcjn+8Ic/QKPR4JNPPkFkZCS8vb2xfPlymM1m6/MiIyOxYcMG3H///XB3d0dISAi2bNnSY5/Lyspw3333wcvLCz4+Prj77rtRUlLS69ecnp4OjuNw4MABTJ48GXK5HDNmzEBubq5Nu1dffRUBAQFQKpVYsmQJdDpdl2Nt3boViYmJkMlkSEhIwP/+7/9aH1u8eDHGjh0LvV4PADAYDJgwYQL+9Kc/ddu3ztMFIyMj8corr2Dx4sVQKpUIDw/Hhx9+eNXX2D4iGRgYiPHjx2P16tUoKytDbW2ttc1///d/Iy4uDnK5HKNHj8aaNWtgNBoBWP5/rl+/Hr/99pt1RGz79u0ALNfMY489hoCAAMhkMiQnJ+P777+3+f379u1DYmIiFAqFNfARQshIRSGLEEJGGIVCAYVCgW+//dYaBuwRCATYvHkzLly4gE8++QQHDx7EqlWrbNpoNBps3rwZu3btwt69e5Geno57770XP/zwA3744Qfs2LEDH3zwAb7++mub573xxhsYN24czp07h9WrV2PFihXYv3+/3X4YjUakpaVBqVTi6NGjOHbsmPWDvMFg6NNrf+GFF/DWW2/h9OnTEIlEWLx4sfWxr776CuvWrcMrr7yC06dPIygoyCZAAcDOnTvx4osv4uWXX0ZOTg5eeeUVrFmzBp988gkAYPPmzWhtbcXq1autv6+pqQnvvfden/r51ltvYfLkyTh37hyeeOIJPP74410CYU/UajU+++wzxMTEwNfX13q/UqnE9u3bkZ2djXfeeQcfffQR3n77bQDA/Pnz8cwzz2DMmDHWEbH58+eD53ncdtttOHbsGD777DNkZ2fj1VdfhVAotB5Xo9HgzTffxI4dO3DkyBFcunQJzz77bJ9eMyGEDCuMEELIiPP1118zb29vJpPJ2IwZM9jzzz/Pfvvttx6fs3v3bubr62u9vW3bNgaAFRQUWO977LHHmFwuZyqVynpfWloae+yxx6y3IyIi2Ny5c22OPX/+fHbbbbdZbwNge/bsYYwxtmPHDhYfH894nrc+rtfrmZubG9u3b5/dvhYXFzMA7Ny5c4wxxg4dOsQAsJ9++sna5t///jcDwLRaLWOMsenTp7MnnnjC5jjTpk1j48aNs96Ojo5mn3/+uU2bDRs2sOnTp1tvHz9+nInFYrZmzRomEonY0aNH7fax3axZs9iKFSustyMiItgDDzxgvc3zPBs1ahT7+9//3u0xFi5cyIRCIXN3d2fu7u4MAAsKCmJnzpzp8Xe/8cYbbNKkSdbba9eutXm9jDG2b98+JhAIWG5urt1j2LsOtmzZwgICAnr83YQQMpzRSBYhhIxA8+bNQ2VlJb777jvMnTsX6enpmDhxonV6GAD89NNPmDNnDkJCQqBUKvHggw+ivr4eGo3G2kYulyM6Otp6OyAgAJGRkVAoFDb31dTU2Pz+6dOnd7mdk5Njt6+//fYbCgoKoFQqraNwPj4+0Ol0KCws7NPrHjt2rPW/g4KCAMDat5ycHEybNq3bfra2tqKwsBBLliyx9kOhUOCll16y6cf06dPx7LPPYsOGDXjmmWdwww039KmPnfvJcRwCAwO7nMPOZs+ejYyMDGRkZODkyZNIS0vDbbfdhtLSUmubL7/8Etdffz0CAwOhUCjw17/+FZcuXerxuBkZGQgNDUVcXFy3bTpfB0FBQVftLyGEDGciZ3eAEEKIc8hkMtxyyy245ZZbsGbNGjz88MNYu3YtFi1ahJKSEvzud7/D448/jpdffhk+Pj74+eefsWTJEhgMBsjlcgCAWCy2OSbHcXbv43m+3/1Uq9WYNGkSdu7c2eUxf3//Ph2rY984jgOAXvetfT3aRx991CWMdZw6x/M8jh07BqFQiIKCgj71z14/2/t6tX66u7sjJibGenvr1q3w9PTERx99hJdeegknTpzAggULsH79eqSlpcHT0xO7du3CW2+91eNx3dzc+tVfxthVn0cIIcMVhSxCCCEAgKSkJOveVGfOnAHP83jrrbcgEFgmPXz11VcO+12//PJLl9uJiYl2206cOBFffvklRo0aBQ8PD4f1obPExET8+uuvNkUqOvYzICAAwcHBKCoqwoIFC7o9zhtvvIGLFy/i8OHDSEtLw7Zt2/DQQw8NWL+7w3EcBAIBtFotAOD48eOIiIjACy+8YG3TcZQLACQSiU2REsAyqlZeXo68vLweR7MIIYRcQdMFCSFkhKmvr8dNN92Ezz77DJmZmSguLsbu3bvx+uuv4+677wYAxMTEwGg04t1330VRURF27NiB999/32F9OHbsGF5//XXk5eVhy5Yt2L17N1asWGG37YIFC+Dn54e7774bR48eRXFxMdLT07F8+XKUl5c7rE8rVqzAP/7xD2zbtg15eXlYu3YtLly4YNNm/fr12LhxIzZv3oy8vDxkZWVh27Zt2LRpEwDg3LlzePHFF7F161Zcf/312LRpE1asWIGioiKH9bM7er0e1dXVqK6uRk5ODp588kmo1WrceeedAIDY2FhcunQJu3btQmFhITZv3ow9e/bYHCMyMhLFxcXIyMhAXV0d9Ho9Zs2ahZkzZ2LevHnYv38/iouL8eOPP2Lv3r0D/poIIcRVUcgihJARRqFQYNq0aXj77bcxc+ZMJCcnY82aNXjkkUesVfDGjRuHTZs24bXXXkNycjJ27tyJjRs3OqwPzzzzDE6fPo0JEybgpZdewqZNm5CWlma3rVwux5EjRxAeHo7f//73SExMtJZXd+TI1vz587FmzRqsWrUKkyZNQmlpKR5//HGbNg8//DC2bt2Kbdu2ISUlBbNmzcL27dsRFRUFnU6HBx54AIsWLbIGm0cffRSzZ8/Ggw8+2GWEyNH27t2LoKAgBAUFYdq0aTh16hR2796N1NRUAMBdd92Fp556CsuWLcP48eNx/PhxrFmzxuYY8+bNw9y5czF79mz4+/vjiy++AAB88803mDJlCu6//34kJSVh1apVA/56CCHElXGMJk0TQggZRJGRkVi5cqXN3lCEEELIcEIjWYQQQgghhBDiQBSyCCGEEEIIIcSBaLogIYQQQgghhDgQjWQRQgghhBBCiANRyCKEEEIIIYQQB6KQRQghhBBCCCEORCGLEEIIIYQQQhyIQhYhhBBCCCGEOBCFLEIIIYQQQghxIApZhBBCCCGEEOJAFLIIIYQQQgghxIH+PzNQ8ph+iw8JAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -657,7 +679,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 10, "id": "46bf15c2", "metadata": {}, "outputs": [ diff --git a/MindChem/applications/nequip/nequip_en.ipynb b/MindChem/applications/nequip/nequip_en.ipynb index 05cf2c90c..734e5fbf4 100644 --- a/MindChem/applications/nequip/nequip_en.ipynb +++ b/MindChem/applications/nequip/nequip_en.ipynb @@ -185,7 +185,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "c9ba202b", "metadata": {}, "outputs": [], @@ -196,7 +196,7 @@ "import mindspore as ms\n", "from mindspore import nn, Profiler\n", "from src.dataset import _unpack, create_training_dataset\n", - "from models.nequip import Nequip\n", + "from mindchemistry.cell import Nequip\n", "from tqdm.notebook import tqdm\n", "\n", "\n", diff --git a/MindChem/applications/nequip/predict.py b/MindChem/applications/nequip/predict.py index 40e0d7a62..d8d5e2945 100644 --- a/MindChem/applications/nequip/predict.py +++ b/MindChem/applications/nequip/predict.py @@ -21,7 +21,7 @@ import argparse import numpy as np import mindspore as ms -from models.load_config import load_yaml_config_from_path +from mindchemistry.utils.load_config import load_yaml_config_from_path from src import predicter from src.plot import print_configuration from src.utils import log_config diff --git a/MindChem/applications/nequip/rmd.yaml b/MindChem/applications/nequip/rmd.yaml index 71c4edd07..786c9d50e 100644 --- a/MindChem/applications/nequip/rmd.yaml +++ b/MindChem/applications/nequip/rmd.yaml @@ -17,7 +17,7 @@ model: hidden_mul: 64 optimizer: - num_epoch: 850 # It is recommended to adjust it to above 800 when in use + num_epoch: 2 # 为快速验证功能,设置较小的epoch数,具体使用时建议调整至800以上 eval_steps: 10 warmup_steps: 6400 learning_rate: 0.01 # learning rate diff --git a/MindChem/applications/nequip/src/predicter.py b/MindChem/applications/nequip/src/predicter.py index e8df1b22d..754ae307e 100644 --- a/MindChem/applications/nequip/src/predicter.py +++ b/MindChem/applications/nequip/src/predicter.py @@ -23,7 +23,7 @@ import mindspore as ms from mindspore import nn from src.dataset import create_training_dataset, _unpack -from models.nequip import Nequip +from mindchemistry.cell import Nequip def evaluation(dtype, configs): diff --git a/MindChem/applications/nequip/src/trainer.py b/MindChem/applications/nequip/src/trainer.py index 435100b50..be3554f59 100644 --- a/MindChem/applications/nequip/src/trainer.py +++ b/MindChem/applications/nequip/src/trainer.py @@ -26,7 +26,7 @@ from mindspore import nn from src.dataset import create_training_dataset, _unpack from src.utils import training_bar -from models.nequip import Nequip +from mindchemistry.cell import Nequip def generate_learning_rate(learning_rate, warmup_steps, step_num): diff --git a/MindChem/applications/nequip/train.py b/MindChem/applications/nequip/train.py index dc97416e5..cefbce107 100644 --- a/MindChem/applications/nequip/train.py +++ b/MindChem/applications/nequip/train.py @@ -21,7 +21,7 @@ import argparse import numpy as np import mindspore as ms -from models.load_config import load_yaml_config_from_path +from mindchemistry.utils.load_config import load_yaml_config_from_path from src import trainer from src.plot import plot_loss, plot_lr, print_configuration from src.utils import log_config diff --git a/MindChem/applications/orb/README.md b/MindChem/applications/orb/README.md index 97268885a..a6598369f 100644 --- a/MindChem/applications/orb/README.md +++ b/MindChem/applications/orb/README.md @@ -10,7 +10,7 @@ ## Environment Requirements -> 1. Install `mindspore (2.7.0)` +> 1. Install `mindspore (2.5.0)` > 2. Install dependencies: `pip install -r requirement.txt` ## Quick Start @@ -28,44 +28,32 @@ ```text The main code modules are in the src folder, with the dataset folder containing the datasets, the orb_ckpts folder containing pre-trained models and trained model weight files, and the configs folder containing parameter configuration files for each code. -orb_models # ORB pre-training / fine-tuning project +orb_models # Model name ├── dataset -│ ├── train_mptrj_ase.db # Training dataset for fine-tuning (ASE trajectories, SQLite) -│ └── val_mptrj_ase.db # Validation / test dataset for fine-tuning -│ -├── orb_ckpts # Directory for pre-trained & fine-tuned checkpoints -│ └── orb-mptraj-only-v2.ckpt # Pre-trained ORB checkpoint (mptraj-only task) -│ -├── configs # Config files for training / inference -│ ├── config.yaml # Single-card training configuration (lr, batch_size, etc.) -│ ├── config_parallel.yaml # Multi-card data-parallel training configuration -│ └── config_eval.yaml # Inference / evaluation configuration -│ -├── src # Core code for data processing and training -│ ├── __init__.py # Package initializer for src -│ ├── ase_dataset.py # Load and wrap ASE datasets (read SQLite, build atomic graphs) -│ ├── atomic_system.py # Data structures for atomic systems (positions, species, cell, etc.) -│ ├── base.py # Common base classes and utilities (e.g., batch_graphs) -│ ├── featurization_utilities.py # Tools to convert atomic systems into model input features -│ ├── pretrained.py # Interfaces for building and loading pre-trained ORB models -│ ├── property_definitions.py # Config and naming rules for energy / forces / stress, etc. -│ ├── trainer.py # Training loop and loss wrappers (e.g., OrbLoss) -│ ├── segment_ops.py # Segment-wise reduction ops (segment_sum / mean / max) -│ └── utils.py # Utility functions (seeding, logging, optimizer & LR scheduler) -│ -├── models # Model definitions (GNN / ORB networks) -│ ├── __init__.py # Package initializer for orb -│ ├── gns.py # GNS (Graph Network Simulator) related structures / APIs -│ ├── orb.py # Main ORB architecture (encoder + heads) -│ └── utils.py # Internal utilities and helper modules for ORB -│ -├── finetune.py # Entry script for model fine-tuning -├── evaluate.py # Entry script for model inference / evaluation -│ -├── run.sh # Single-card training launcher (wraps finetune.py + config.yaml) -├── run_parallel.sh # Multi-card training launcher (msrun + config_parallel.yaml) -└── requirement.txt # Python dependency list for environment setup - + ├── train_mptrj_ase.db # Training dataset for fine-tuning stage + └── val_mptrj_ase.db # Test dataset for fine-tuning stage +├── orb_ckpts + └── orb-mptraj-only-v2.ckpt # Pre-trained model checkpoint +├── configs + ├── config.yaml # Single-card training parameter configuration file + ├── config_parallel.yaml # Multi-card parallel training parameter configuration file + └── config_eval.yaml # Inference parameter configuration file +├── src + ├── __init__.py + ├── ase_dataset.py # Process and load datasets + ├── atomic_system.py # Define data structure for atomic systems + ├── base.py # Base class definitions + ├── featurization_utilities.py # Provide tools to convert atomic systems into feature vectors + ├── pretrained.py # Pre-trained model related functions + ├── property_definitions.py # Define calculation methods and naming rules for various physical properties in atomic systems + ├── trainer.py # Model loss class definitions + ├── segment_ops.py # Provide tools for segmenting data + └── utils.py # Utility module +├── finetune.py # Model fine-tuning code +├── evaluate.py # Model inference code +├── run.sh # Single-card training startup script +├── run_parallel.sh # Multi-card parallel training startup script +└── requirement.txt # Environment ``` ## Download Dataset diff --git a/MindChem/applications/orb/README_CN.md b/MindChem/applications/orb/README_CN.md index d93722b81..6812131cf 100644 --- a/MindChem/applications/orb/README_CN.md +++ b/MindChem/applications/orb/README_CN.md @@ -10,7 +10,7 @@ ## 环境要求 -> 1. 安装`mindspore(2.7.0)` +> 1. 安装`mindspore(2.5.0)` > 2. 安装依赖包:`pip install -r requirement.txt` ## 快速入门 @@ -28,44 +28,32 @@ ```text 代码主要模块在src文件夹下,其中dataset文件夹下是数据集,orb_ckpts文件夹下是预训练模型和训练好的模型权重文件,configs文件夹下是各代码的参数配置文件。 -orb_models # ORB 预训练 / 微调工程 +orb_models # 模型名 ├── dataset -│ ├── train_mptrj_ase.db # 微调训练集(ASE 轨迹,SQLite 格式) -│ └── val_mptrj_ase.db # 微调验证 / 测试集 -│ -├── orb_ckpts # 预训练 & 微调模型 ckpt 存放目录 -│ └── orb-mptraj-only-v2.ckpt # 仅 mptraj 任务的预训练 ORB 模型 -│ -├── configs # 训练 / 推理配置 -│ ├── config.yaml # 单卡训练配置(学习率、batch_size 等) -│ ├── config_parallel.yaml # 多卡数据并行训练配置 -│ └── config_eval.yaml # 推理 / 评估配置 -│ -├── src # 数据处理与训练核心源码 -│ ├── __init__.py # src 包初始化 -│ ├── ase_dataset.py # ASE 数据集读取与封装(读 SQLite、组装原子图) -│ ├── atomic_system.py # 原子系统数据结构定义(坐标、原子种类、晶胞信息等) -│ ├── base.py # 通用基类与工具(batch_graphs 等图数据打包) -│ ├── featurization_utilities.py # 原子系统 → 模型输入特征张量的特征化工具 -│ ├── pretrained.py # 预训练 ORB 模型构造与加载接口 -│ ├── property_definitions.py # 能量 / 力 / 应力等物理量配置与命名 -│ ├── trainer.py # 训练循环与 OrbLoss 等损失封装 -│ ├── segment_ops.py # segment_sum / mean / max 等分段归约算子 -│ └── utils.py # 通用工具函数(随机种子、日志、优化器、LR scheduler 等) -│ -├── models # 模型结构定义(GNN / ORB 等) -│ ├── __init__.py # orb 子包初始化 -│ ├── gns.py # GNS(Graph Network Simulator) 相关结构 / 接口 -│ ├── orb.py # ORB 主体网络(encoder + heads) -│ └── utils.py # ORB 内部工具与辅助模块 -│ -├── finetune.py # 模型微调入口脚本 -├── evaluate.py # 推理 / 评估入口脚本 -│ -├── run.sh # 单卡训练启动脚本(调用 finetune.py + config.yaml) -├── run_parallel.sh # 多卡并行训练启动脚本(msrun + config_parallel.yaml) -└── requirement.txt # Python 依赖列表(环境搭建用) - + ├── train_mptrj_ase.db # 微调阶段训练数据集 + └── val_mptrj_ase.db # 微调阶段测试数据集 +├── orb_ckpts + └── orb-mptraj-only-v2.ckpt # 预训练模型checkpoint +├── configs + ├── config.yaml # 单卡训练参数配置文件 + ├── config_parallel.yaml # 多卡并行训练参数配置文件 + └── config_eval.yaml # 推理参数配置文件 +├── src + ├── __init__.py + ├── ase_dataset.py # 处理和加载数据集 + ├── atomic_system.py # 定义原子系统的数据结构 + ├── base.py # 基础类定义 + ├── featurization_utilities.py # 提供将原子系统转换为特征向量的工具 + ├── pretrained.py # 预训练模型相关函数 + ├── property_definitions.py # 定义原子系统中各种物理性质的计算方式和命名规则 + ├── trainer.py # 模型loss类定义 + ├── segment_ops.py # 提供对数据进行分段处理的工具 + └── utils.py # 工具模块 +├── finetune.py # 模型微调代码 +├── evaluate.py # 模型推理代码 +├── run.sh # 单卡训练启动脚本 +├── run_parallel.sh # 多卡并行训练启动脚本 +└── requirement.txt # 环境 ``` ## 下载数据集 diff --git a/MindChem/applications/orb/configs/config_eval.yaml b/MindChem/applications/orb/configs/config_eval.yaml index e0ffea036..1e98c5f0b 100644 --- a/MindChem/applications/orb/configs/config_eval.yaml +++ b/MindChem/applications/orb/configs/config_eval.yaml @@ -6,7 +6,7 @@ device_id: 0 val_data_path: dataset/val_mptrj_ase.db num_workers: 8 batch_size: 64 -checkpoint_path: orb_ckpts/orb-mptraj-only-v2.ckpt +checkpoint_path: orb_ckpts/orb-ft-checkpoint_epoch99.ckpt random_seed: 1234 output_dir: results/ diff --git a/MindChem/applications/orb/configs/config_parallel.yaml b/MindChem/applications/orb/configs/config_parallel.yaml index fabc1eed2..c6a5e0857 100644 --- a/MindChem/applications/orb/configs/config_parallel.yaml +++ b/MindChem/applications/orb/configs/config_parallel.yaml @@ -2,7 +2,7 @@ train_data_path: dataset/train_mptrj_ase.db val_data_path: dataset/val_mptrj_ase.db num_workers: 8 -batch_size: 64 +batch_size: 256 gradient_clip_val: 0.5 max_epochs: 100 checkpoint_path: orb_ckpts/ diff --git a/MindChem/applications/orb/mindchemistry/__init__.py b/MindChem/applications/orb/mindchemistry/__init__.py new file mode 100644 index 000000000..29f929195 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/__init__.py @@ -0,0 +1,66 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Initialization for MindChemistry APIs.""" + +import time +import mindspore as ms +from mindspore import log as logger +from mindscience.e3nn import * +from .cell import * +from .utils import * +from .graph import * +from .so2_conv import * + +__all__ = [] +__all__.extend(cell.__all__) +__all__.extend(utils.__all__) + + + +def _mindspore_version_check(): + """ + Check MindSpore version for MindChemistry. + + Raises: + ImportError: If MindSpore cannot be imported. + """ + try: + _ = ms.__version__ + except ImportError as exc: + raise ImportError( + "Cannot find MindSpore in the current environment. Please install " + "MindSpore before using MindChemistry, by following the instruction at " + "https://www.mindspore.cn/install" + ) from exc + + ms_version = ms.__version__[:5] + required_mindspore_version = "1.8.1" + + if ms_version < required_mindspore_version: + logger.warning( + f"Current version of MindSpore ({ms_version}) is not compatible with MindChemistry. " + f"Some functions might not work or even raise errors. Please install MindSpore " + f"version >= {required_mindspore_version}. For more details about dependency settings, " + f"please check the instructions at the MindSpore official website " + f"https://www.mindspore.cn/install or check the README.md at " + f"https://gitee.com/mindspore/mindscience" + ) + + for i in range(3, 0, -1): + logger.warning(f"Please pay attention to the above warning, countdown: {i}") + time.sleep(1) + + +_mindspore_version_check() diff --git a/MindChem/applications/orb/mindchemistry/cell/__init__.py b/MindChem/applications/orb/mindchemistry/cell/__init__.py new file mode 100644 index 000000000..14cf69dc1 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/cell/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""initialization for cells""" +from .basic_block import AutoEncoder, FCNet, MLPNet +from .orb import * + +__all__ = [ + 'AutoEncoder', 'FCNet', 'MLPNet' +] +__all__.extend(orb.__all__) diff --git a/MindChem/applications/orb/mindchemistry/cell/activation.py b/MindChem/applications/orb/mindchemistry/cell/activation.py new file mode 100644 index 000000000..d09b35831 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/cell/activation.py @@ -0,0 +1,38 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""get activation function.""" +from __future__ import absolute_import + +from mindspore import ops +from mindspore.nn.layer import activation + +_activation = { + 'softmax': activation.Softmax, + 'logsoftmax': activation.LogSoftmax, + 'relu': activation.ReLU, + 'silu': activation.SiLU, + 'relu6': activation.ReLU6, + 'tanh': activation.Tanh, + 'gelu': activation.GELU, + 'fast_gelu': activation.FastGelu, + 'elu': activation.ELU, + 'sigmoid': activation.Sigmoid, + 'prelu': activation.PReLU, + 'leakyrelu': activation.LeakyReLU, + 'hswish': activation.HSwish, + 'hsigmoid': activation.HSigmoid, + 'logsigmoid': activation.LogSigmoid, + 'sin': ops.Sin +} diff --git a/MindChem/applications/orb/mindchemistry/cell/basic_block.py b/MindChem/applications/orb/mindchemistry/cell/basic_block.py new file mode 100644 index 000000000..6a83f67e0 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/cell/basic_block.py @@ -0,0 +1,600 @@ +# Copyright 2023 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""basic""" +from __future__ import absolute_import + +from collections.abc import Sequence +from typing import Union + +from mindspore import nn +from mindspore.nn.layer import activation +from mindspore import ops, float16, float32, Tensor +from mindspore.common.initializer import Initializer + +from .activation import _activation + + +def _get_dropout(dropout_rate): + """ + Gets the dropout functions. + + Inputs: + dropout_rate (Union[int, float]): The dropout rate of the dropout function. + If dropout_rate was int or not in range (0,1], it would be rectify to closest float value. + + Returns: + Function, the dropout function. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_dropout + >>> dropout = get_dropout(0.5) + >>> dropout.set_train + Dropout + """ + dropout_rate = float(max(min(dropout_rate, 1.), 1e-7)) + return nn.Dropout(keep_prob=dropout_rate) + + +def _get_layernorm(channel, epsilon): + """ + Gets the layer normalization functions. + + Inputs: + channel (Union[int, list]): The normalized shape of the layer normalization function. + If channel was int, it would be wrap into a list. + epsilon (float): The epsilon of the layer normalization function. + + Returns: + Function, the layer normalization function. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_layernorm + >>> from mindspore import Tensor + >>> input_x = Tensor(np.array([[1.2, 0.1], [0.2, 3.2]], dtype=np.float32)) + >>> layernorm = get_layernorm([2], 1e-7) + >>> output = layernorm(input_x) + >>> print(output) + [[ 9.99999881e-01, -9.99999881e-01], + [-1.00000000e+00, 1.00000000e+00]] + """ + if isinstance(channel, int): + channel = [channel] + return nn.LayerNorm(channel, epsilon=epsilon) + + +def _get_activation(name): + """ + Gets the activation function. + + Inputs: + name (Union[str, None]): The name of the activation function. If name was None, it would return []. + + Returns: + Function, the activation function. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_activation + >>> from mindspore import Tensor + >>> input_x = Tensor(np.array([[1.2, 0.1], [0.2, 3.2]], dtype=np.float32)) + >>> sigmoid = _get_activation('sigmoid') + >>> output = sigmoid(input_x) + >>> print(output) + [[0.7685248 0.5249792 ] + [0.54983395 0.96083426]] + """ + if name is None: + return [] + if isinstance(name, str): + name = name.lower() + if name not in _activation: + return activation.get_activation(name) + return _activation.get(name)() + return name + + +def _get_layer_arg(arguments, index): + """ + Gets the argument of each network layers. + + Inputs: + arguments (Union[str, int, float, List, None]): The arguments of each layers. + If arguments was List return the argument at the index of the List. + index (int): The index of layer in the network + + Returns: + Argument of the indexed layer. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import _get_layer_arg + >>> from mindspore import Tensor + >>> dropout_rate = _get_layer_arg([0.1, 0.2, 0.3], index=2) + >>> print(dropout_rate) + 0.2 + >>> dropout_rate = _get_layer_arg(0.2, index=2) + >>> print(dropout_rate) + 0.2 + """ + if isinstance(arguments, list): + if len(arguments) <= index: + if len(arguments) == 1: + return [] if arguments[0] is None else arguments[0] + return [] + return [] if arguments[index] is None else arguments[index] + return [] if arguments is None else arguments + + +def get_linear_block( + in_channels, + out_channels, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu' +): + """ + Gets the linear block list. + + Inputs: + in_channels (int): The number of input channel. + out_channels (int): The number of output channel. + weight_init (Union[str, float, mindspore.common.initializer]): The initializer of the weights of dense layer + has_bias (bool): The switch for whether dense layer has bias. + bias_init (Union[str, float, mindspore.common.initializer]): The initializer of the bias of dense layer + has_dropout (bool): The switch for whether linear block has a dropout layer. + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] + has_layernorm (bool): The switch for whether linear block has a layer normalization layer. + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + has_activation (bool): The switch for whether linear block has an activation layer. + act (Union[str, None]): The activation function in linear block + + Returns: + List of mindspore.nn.Cell, linear block list . + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import get_layer_arg + >>> from mindspore import Tensor + >>> dropout_rate = get_layer_arg([0.1, 0.2, 0.3], index=2) + >>> print(dropout_rate) + 0.2 + >>> dropout_rate = get_layer_arg(0.2, index=2) + >>> print(dropout_rate) + 0.2 + """ + dense = nn.Dense( + in_channels, out_channels, weight_init=weight_init, bias_init=bias_init, has_bias=has_bias, activation=None + ) + dropout = _get_dropout(dropout_rate) if (has_dropout is True) else [] + layernorm = _get_layernorm(out_channels, layernorm_epsilon) if (has_layernorm is True) else [] + act = _get_activation(act) if (has_activation is True) else [] + block_list = [dense, dropout, layernorm, act] + while [] in block_list: + block_list.remove([]) + return block_list + + +class FCNet(nn.Cell): + r""" + The Fully Connected Network. Applies a series of fully connected layers to the incoming data. + + Args: + channels (List): the list of numbers of channel of each fully connected layers. + weight_init (Union[str, float, mindspore.common.initializer, List]): initialize layer weights. + If weight_init was List, each element corresponds to each layer. Default: ``'normal'`` . + has_bias (Union[bool, List]): The switch for whether the dense layers has bias. + If has_bias was List, each element corresponds to each dense layer. Default: ``True`` . + bias_init (Union[str, float, mindspore.common.initializer, List]): The initializer of the bias of dense + layer. If bias_init was List, each element corresponds to each dense layer. Default: ``'zeros'`` . + has_dropout (Union[bool, List]): The switch for whether linear block has a dropout layer. + If has_dropout was List, each element corresponds to each layer. Default: ``False`` . + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] + If dropout_rate was List, each element corresponds to each dropout layer. Default: ``0.5`` . + has_layernorm (Union[bool, List]): The switch for whether linear block has a layer normalization layer. + If has_layernorm was List, each element corresponds to each layer. Default: ``False`` . + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + If layernorm_epsilon was List, each element corresponds to each layer normalization layer. + Default: ``1e-7`` . + has_activation (Union[bool, List]): The switch for whether linear block has an activation layer. + If has_activation was List, each element corresponds to each layer. Default: ``True`` . + act (Union[str, None, List]): The activation function in linear block. + If act was List, each element corresponds to each activation layer. Default: ``'relu'`` . + + Inputs: + - **input** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Outputs: + - **output** (Tensor) - The shape of Tensor is :math:`(*, channels[-1])`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import FCNet + >>> from mindspore import Tensor + >>> inputs = Tensor(np.array([[180, 234, 154], [244, 48, 247]], np.float32)) + >>> net = FCNet([3, 16, 32, 16, 8]) + >>> output = net(inputs) + >>> print(output.shape) + (2, 8) + + """ + + def __init__( + self, + channels, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu' + ): + super().__init__() + self.channels = channels + self.weight_init = weight_init + self.has_bias = has_bias + self.bias_init = bias_init + self.has_dropout = has_dropout + self.dropout_rate = dropout_rate + self.has_layernorm = has_layernorm + self.layernorm_epsilon = layernorm_epsilon + self.has_activation = has_activation + self.activation = act + self.network = nn.SequentialCell(self._create_network()) + + def _create_network(self): + """ create the network """ + cell_list = [] + for i in range(len(self.channels) - 1): + cell_list += get_linear_block( + self.channels[i], + self.channels[i + 1], + weight_init=_get_layer_arg(self.weight_init, i), + has_bias=_get_layer_arg(self.has_bias, i), + bias_init=_get_layer_arg(self.bias_init, i), + has_dropout=_get_layer_arg(self.has_dropout, i), + dropout_rate=_get_layer_arg(self.dropout_rate, i), + has_layernorm=_get_layer_arg(self.has_layernorm, i), + layernorm_epsilon=_get_layer_arg(self.layernorm_epsilon, i), + has_activation=_get_layer_arg(self.has_activation, i), + act=_get_layer_arg(self.activation, i) + ) + return cell_list + + def construct(self, x): + return self.network(x) + + +class MLPNet(nn.Cell): + r""" + The MLPNet Network. Applies a series of fully connected layers to the incoming data among which hidden layers have + same number of channels. + + Args: + in_channels (int): the number of input layer channel. + out_channels (int): the number of output layer channel. + layers (int): the number of layers. + neurons (int): the number of channels of hidden layers. + weight_init (Union[str, float, mindspore.common.initializer, List]): initialize layer weights. + If weight_init was List, each element corresponds to each layer. Default: ``'normal'`` . + has_bias (Union[bool, List]): The switch for whether the dense layers has bias. + If has_bias was List, each element corresponds to each dense layer. Default: ``True`` . + bias_init (Union[str, float, mindspore.common.initializer, List]): The initializer of the bias of dense + layer. If bias_init was List, each element corresponds to each dense layer. Default: ``'zeros'`` . + has_dropout (Union[bool, List]): The switch for whether linear block has a dropout layer. + If has_dropout was List, each element corresponds to each layer. Default: ``False`` . + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] . + If dropout_rate was List, each element corresponds to each dropout layer. Default: ``0.5`` . + has_layernorm (Union[bool, List]): The switch for whether linear block has a layer normalization layer. + If has_layernorm was List, each element corresponds to each layer. Default: ``False`` . + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + If layernorm_epsilon was List, each element corresponds to each layer normalization layer. + Default: ``1e-7`` . + has_activation (Union[bool, List]): The switch for whether linear block has an activation layer. + If has_activation was List, each element corresponds to each layer. Default: ``True`` . + act (Union[str, None, List]): The activation function in linear block. + If act was List, each element corresponds to each activation layer. Default: ``'relu'`` . + + Inputs: + - **input** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Outputs: + - **output** (Tensor) - The shape of Tensor is :math:`(*, channels[-1])`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindchemistry.cell import FCNet + >>> from mindspore import Tensor + >>> inputs = Tensor(np.array([[180, 234, 154], [244, 48, 247]], np.float32)) + >>> net = MLPNet(in_channels=3, out_channels=8, layers=5, neurons=32) + >>> output = net(inputs) + >>> print(output.shape) + (2, 8) + + """ + + def __init__( + self, + in_channels, + out_channels, + layers, + neurons, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu' + ): + super().__init__() + self.channels = (in_channels,) + (layers - 2) * \ + (neurons,) + (out_channels,) + self.network = FCNet( + channels=self.channels, + weight_init=weight_init, + has_bias=has_bias, + bias_init=bias_init, + has_dropout=has_dropout, + dropout_rate=dropout_rate, + has_layernorm=has_layernorm, + layernorm_epsilon=layernorm_epsilon, + has_activation=has_activation, + act=act + ) + + def construct(self, x): + return self.network(x) + + +class MLPMixPrecision(nn.Cell): + """MLPMixPrecision + """ + + def __init__( + self, + input_dim: int, + hidden_dims: Sequence, + short_cut=False, + batch_norm=False, + activation_fn='relu', + has_bias=False, + weight_init: Union[Initializer, str] = 'xavier_uniform', + bias_init: Union[Initializer, str] = 'zeros', + dropout=0, + dtype=float32 + ): + super().__init__() + self.dtype = dtype + self.div = ops.Div() + + self.dims = [input_dim] + hidden_dims + self.short_cut = short_cut + self.nonlinear_const = 1.0 + if isinstance(activation_fn, str): + self.activation = _activation.get(activation_fn)() + if activation_fn is not None and activation_fn == 'silu': + self.nonlinear_const = 1.679177 + else: + self.activation = activation_fn + self.dropout = None + if dropout: + self.dropout = nn.Dropout(dropout) + fcs = [ + nn.Dense(dim, self.dims[i + 1], weight_init=weight_init, bias_init=bias_init, + has_bias=has_bias).to_float(self.dtype) for i, dim in enumerate(self.dims[:-1]) + ] + self.layers = nn.CellList(fcs) + self.batch_norms = None + if batch_norm: + bns = [nn.BatchNorm1d(dim) for dim in self.dims[1:-1]] + self.batch_norms = nn.CellList(bns) + + def construct(self, inputs): + """construct + + Args: + inputs: inputs + + Returns: + inputs + """ + hidden = inputs + norm_from_last = 1.0 + for i, layer in enumerate(self.layers): + sqrt_dim = ops.sqrt(Tensor(float(self.dims[i]))) + layer_hidden = layer(hidden) + if self.dtype == float16: + layer_hidden = layer_hidden.astype(float16) + hidden = self.div(layer_hidden * norm_from_last, sqrt_dim) + norm_from_last = self.nonlinear_const + if i < len(self.layers) - 1: + if self.batch_norms is not None: + x = hidden.flatten(0, -2) + hidden = self.batch_norms[i](x).view_as(hidden) + if self.activation is not None: + hidden = self.activation(hidden) + if self.dropout is not None: + hidden = self.dropout(hidden) + if self.short_cut and hidden.shape == hidden.shape: + hidden += inputs + return hidden + + +class AutoEncoder(nn.Cell): + r""" + The AutoEncoder Network. + Applies an encoder to get the latent code and applies a decoder to get the reconstruct data. + + Args: + channels (list): The number of channels of each encoder and decoder layer. + weight_init (Union[str, float, mindspore.common.initializer, List]): initialize layer parameters. + If weight_init was List, each element corresponds to each layer. Default: ``'normal'`` . + has_bias (Union[bool, List]): The switch for whether the dense layers has bias. + If has_bias was List, each element corresponds to each dense layer. Default: ``True`` . + bias_init (Union[str, float, mindspore.common.initializer, List]): initialize layer parameters. + If bias_init was List, each element corresponds to each dense layer. Default: ``'zeros'`` . + has_dropout (Union[bool, List]): The switch for whether linear block has a dropout layer. + If has_dropout was List, each element corresponds to each layer. Default: ``False`` . + dropout_rate (float): The dropout rate for dropout layer, the dropout rate must be a float in range (0, 1] + If dropout_rate was List, each element corresponds to each dropout layer. Default: ``0.5`` . + has_layernorm (Union[bool, List]): The switch for whether linear block has a layer normalization layer. + If has_layernorm was List, each element corresponds to each layer. Default: ``False`` . + layernorm_epsilon (float): The hyper parameter epsilon for layer normalization layer. + If layernorm_epsilon was List, each element corresponds to each layer normalization layer. + Default: ``1e-7`` . + has_activation (Union[bool, List]): The switch for whether linear block has an activation layer. + If has_activation was List, each element corresponds to each layer. Default: ``True`` . + act (Union[str, None, List]): The activation function in linear block. + If act was List, each element corresponds to each activation layer. Default: ``'relu'`` . + out_act (Union[None, str, mindspore.nn.Cell]): The activation function to output layer. Default: ``None`` . + + Inputs: + - **x** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Outputs: + - **latents** (Tensor) - The shape of Tensor is :math:`(*, channels[-1])`. + - **x_recon** (Tensor) - The shape of Tensor is :math:`(*, channels[0])`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindchemistry import AutoEncoder + >>> from mindspore import Tensor + >>> inputs = Tensor(np.array([[180, 234, 154], [244, 48, 247]], np.float32)) + >>> net = AutoEncoder([3, 6, 2]) + >>> output = net(inputs) + >>> print(output[0].shape, output[1].shape) + (2, 2) (2, 3) + + """ + + def __init__( + self, + channels, + weight_init='normal', + has_bias=True, + bias_init='zeros', + has_dropout=False, + dropout_rate=0.5, + has_layernorm=False, + layernorm_epsilon=1e-7, + has_activation=True, + act='relu', + out_act=None + ): + super().__init__() + self.channels = channels + self.weight_init = weight_init + self.bias_init = bias_init + self.has_bias = has_bias + self.has_dropout = has_dropout + self.dropout_rate = dropout_rate + self.has_layernorm = has_layernorm + self.has_activation = has_activation + self.layernorm_epsilon = layernorm_epsilon + self.activation = act + self.output_activation = out_act + self.encoder = nn.SequentialCell(self._create_encoder()) + self.decoder = nn.SequentialCell(self._create_decoder()) + + def _create_encoder(self): + """ create the network encoder """ + encoder_cell_list = [] + for i in range(len(self.channels) - 1): + encoder_cell_list += get_linear_block( + self.channels[i], + self.channels[i + 1], + weight_init=_get_layer_arg(self.weight_init, i), + has_bias=_get_layer_arg(self.has_bias, i), + bias_init=_get_layer_arg(self.bias_init, i), + has_dropout=_get_layer_arg(self.has_dropout, i), + dropout_rate=_get_layer_arg(self.dropout_rate, i), + has_layernorm=_get_layer_arg(self.has_layernorm, i), + layernorm_epsilon=_get_layer_arg(self.layernorm_epsilon, i), + has_activation=_get_layer_arg(self.has_activation, i), + act=_get_layer_arg(self.activation, i) + ) + return encoder_cell_list + + def _create_decoder(self): + """ create the network decoder """ + decoder_channels = self.channels[::-1] + decoder_weight_init = self.weight_init[::-1] if isinstance(self.weight_init, list) else self.weight_init + decoder_bias_init = self.bias_init[::-1] if isinstance(self.bias_init, list) else self.bias_init + decoder_cell_list = [] + for i in range(len(decoder_channels) - 1): + decoder_cell_list += get_linear_block( + decoder_channels[i], + decoder_channels[i + 1], + weight_init=_get_layer_arg(decoder_weight_init, i), + has_bias=_get_layer_arg(self.has_bias, i), + bias_init=_get_layer_arg(decoder_bias_init, i), + has_dropout=_get_layer_arg(self.has_dropout, i), + dropout_rate=_get_layer_arg(self.dropout_rate, i), + has_layernorm=_get_layer_arg(self.has_layernorm, i), + layernorm_epsilon=_get_layer_arg(self.layernorm_epsilon, i), + has_activation=_get_layer_arg(self.has_activation, i), + act=_get_layer_arg(self.activation, i) + ) + if self.output_activation is not None: + decoder_cell_list.append(_get_activation(self.output_activation)) + return decoder_cell_list + + def encode(self, x): + return self.encoder(x) + + def decode(self, z): + return self.decoder(z) + + def construct(self, x): + latents = self.encode(x) + x_recon = self.decode(latents) + return x_recon, latents diff --git a/MindChem/applications/orb/mindchemistry/cell/convolution.py b/MindChem/applications/orb/mindchemistry/cell/convolution.py new file mode 100644 index 000000000..723ef1c95 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/cell/convolution.py @@ -0,0 +1,120 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""convolution""" +from mindspore import nn, ops, float32 +from ..graph.graph import AggregateEdgeToNode +from ..e3.o3 import TensorProduct, Irreps, Linear +from ..e3.nn import FullyConnectedNet + +softplus = ops.Softplus() + + +def shift_softplus(x): + return softplus(x) - 0.6931471805599453 + + +def silu(x): + return x * ops.sigmoid(x) + + +class Convolution(nn.Cell): + r""" + InteractionBlock. + + Args: + irreps_node_input: Input Features, default = None + irreps_node_attr: Nodes attribute irreps + irreps_node_output: Output irreps, in our case typically a single scalar + irreps_edge_attr: Edge attribute irreps + invariant_layers: Number of invariant layers, default = 1 + invariant_neurons: Number of hidden neurons in invariant function, default = 8 + avg_num_neighbors: Number of neighbors to divide by, default None => no normalization. + use_sc(bool): use self-connection or not + """ + + def __init__(self, + irreps_node_input, + irreps_node_attr, + irreps_node_output, + irreps_edge_attr, + irreps_edge_scalars, + invariant_layers=1, + invariant_neurons=8, + avg_num_neighbors=None, + use_sc=True, + nonlin_scalars=None, + dtype=float32, + ncon_dtype=float32): + super().__init__() + self.avg_num_neighbors = avg_num_neighbors + self.use_sc = use_sc + + self.irreps_node_input = Irreps(irreps_node_input) + self.irreps_node_attr = Irreps(irreps_node_attr) + self.irreps_node_output = Irreps(irreps_node_output) + self.irreps_edge_attr = Irreps(irreps_edge_attr) + self.irreps_edge_scalars = Irreps([(irreps_edge_scalars.num_irreps, (0, 1))]) + + self.lin1 = Linear(self.irreps_node_input, self.irreps_node_input, dtype=dtype, ncon_dtype=ncon_dtype) + + tp = TensorProduct(self.irreps_node_input, + self.irreps_edge_attr, + self.irreps_node_output, + 'merge', + weight_mode='custom', + dtype=dtype, + ncon_dtype=ncon_dtype) + + self.fc = FullyConnectedNet([self.irreps_edge_scalars.num_irreps] + invariant_layers * [invariant_neurons] + + [tp.weight_numel], { + "ssp": shift_softplus, + "silu": ops.silu, + }.get(nonlin_scalars.get("e", None), None), dtype=dtype) + + self.tp = tp + self.scatter = AggregateEdgeToNode(dim=1) + + self.lin2 = Linear(tp.irreps_out.simplify(), self.irreps_node_output, dtype=dtype, ncon_dtype=ncon_dtype) + + self.sc = None + if self.use_sc: + self.sc = TensorProduct(self.irreps_node_input, + self.irreps_node_attr, + self.irreps_node_output, + 'connect', + dtype=dtype, + ncon_dtype=ncon_dtype) + + def construct(self, node_input, node_attr, edge_src, edge_dst, edge_attr, edge_scalars): + """Evaluate interaction Block with resnet""" + weight = self.fc(edge_scalars) + + node_features = self.lin1(node_input) + + edge_features = self.tp(node_features[edge_src], edge_attr, weight) + + node_features = self.scatter(edge_attr=edge_features, edge_index=[edge_src, edge_dst], + dim_size=node_input.shape[0]) + + if self.avg_num_neighbors is not None: + node_features = node_features.div(self.avg_num_neighbors**0.5) + + node_features = self.lin2(node_features) + + if self.sc is not None: + sc = self.sc(node_input, node_attr) + node_features = node_features + sc + + return node_features diff --git a/MindChem/applications/orb/mindchemistry/cell/embedding.py b/MindChem/applications/orb/mindchemistry/cell/embedding.py new file mode 100644 index 000000000..12e527335 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/cell/embedding.py @@ -0,0 +1,146 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""embedding +""" +import math + +import numpy as np +from mindspore import nn, ops, Tensor, Parameter, float32 + +from ..e3.o3 import Irreps + + +def _poly_cutoff(x, factor, p=6.0): + x = x * factor + out = 1.0 + out = out - (((p + 1.0) * (p + 2.0) / 2.0) * ops.pow(x, p)) + out = out + (p * (p + 2.0) * ops.pow(x, p + 1.0)) + out = out - ((p * (p + 1.0) / 2) * ops.pow(x, p + 2.0)) + return out * (x < 1.0) + + +class PolyCutoff(nn.Cell): + + def __init__(self, r_max, p=6): + super().__init__() + self.p = float(p) + self._factor = 1.0 / float(r_max) + + def construct(self, x): + return _poly_cutoff(x, self._factor, p=self.p) + + +class MaskPolynomialCutoff(nn.Cell): + """MaskPolynomialCutoff + """ + _factor: float + p: float + + def __init__(self, r_max: float, p: float = 6): + super().__init__() + self.p = float(p) + self._factor = 1.0 / float(r_max) + self.r_max = Tensor(r_max, dtype=float32) + + self.cutoff = r_max + + def construct(self, distance: Tensor, mask: Tensor = None): + decay = _poly_cutoff(distance, self._factor, p=self.p) + + mask_lower = distance < self.cutoff + if mask is not None: + mask_lower &= mask + + return decay, mask_lower + + +class BesselBasis(nn.Cell): + """BesselBasis + """ + + def __init__(self, r_max, num_basis=8, dtype=float32): + super().__init__() + self.r_max = r_max + self.num_basis = num_basis + self.prefactor = 2.0 / self.r_max + bessel_weights = Tensor(np.linspace(1., num_basis, num_basis) * math.pi, dtype=dtype) + self.bessel_weights = Parameter(bessel_weights) + + def construct(self, x): + numerator = ops.sin(self.bessel_weights * x.unsqueeze(-1) / self.r_max) + return self.prefactor * (numerator / x.unsqueeze(-1)) + + +class NormBesselBasis(nn.Cell): + """NormBesselBasis + """ + + def __init__(self, r_max, num_basis=8, norm_num=4000): + super().__init__() + + self.basis = BesselBasis(r_max=r_max, num_basis=num_basis) + self.rs = Tensor(np.linspace(0.0, r_max, num=norm_num + 1), float32)[1:] + + self.sqrt = ops.Sqrt() + self.sq = ops.Square() + self.div = ops.Div() + + bessel_weights = Tensor(np.linspace(1.0, num_basis, num=num_basis), float32) + + bessel_weights = bessel_weights * Tensor(math.pi, float32) + edge_length = self.rs + edge_length_unsqueeze = edge_length.unsqueeze(-1) + bessel_edge_length = bessel_weights * edge_length_unsqueeze + if r_max != 0: + bessel_edge_length = bessel_edge_length / r_max + prefactor = 2.0 / r_max + else: + raise ValueError + self.sin = ops.Sin() + numerator = self.sin(bessel_edge_length) + bs = prefactor * self.div(numerator, edge_length_unsqueeze) + + basis_mean = Tensor(np.mean(bs.asnumpy(), axis=0), float32) + basis_std = self.sqrt(Tensor(np.mean(self.sq(bs - basis_mean).asnumpy(), 0), float32)) + inv_std = ops.reciprocal(basis_std) + + self.basis_mean = basis_mean + self.inv_std = inv_std + + def construct(self, edge_length): + basis_length = self.basis(edge_length) + return (basis_length - self.basis_mean) * self.inv_std + + +class RadialEdgeEmbedding(nn.Cell): + """RadialEdgeEmbedding + """ + + def __init__(self, r_max, num_basis=8, p=6, dtype=float32): + super().__init__() + self.num_basis = num_basis + self.cutoff_p = p + self.basis = BesselBasis(r_max, num_basis, dtype=dtype) + self.cutoff = PolyCutoff(r_max, p) + + self.irreps_out = Irreps([(self.basis.num_basis, (0, 1))]) + + def construct(self, edge_length): + edge_length_embedded = self.basis(edge_length) * self.cutoff(edge_length).unsqueeze(-1) + return edge_length_embedded + + def __repr__(self): + return f'RadialEdgeEmbedding [num_basis: {self.num_basis}, cutoff_p: ' \ + + f'{self.cutoff_p}] ( -> {self.irreps_out} | {self.basis.num_basis} weights)' diff --git a/MindChem/applications/orb/mindchemistry/cell/message_passing.py b/MindChem/applications/orb/mindchemistry/cell/message_passing.py new file mode 100644 index 000000000..2faf6f86f --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/cell/message_passing.py @@ -0,0 +1,166 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""MessagePassing""" +from mindspore import nn, ops, float32 + +from mindscience.e3nn.o3 import Irreps +from mindscience.e3nn.nn import Gate, NormActivation +from .convolution import Convolution, shift_softplus + +acts = { + "abs": ops.abs, + "tanh": ops.tanh, + "ssp": shift_softplus, + "silu": ops.silu, +} + + +class Compose(nn.Cell): + def __init__(self, first, second): + super().__init__() + self.first = first + self.second = second + + def construct(self, *inputs): + x = self.first(*inputs) + x = self.second(x) + return x + + +class MessagePassing(nn.Cell): + """MessagePassing""" + # pylint: disable=W0102 + def __init__( + self, + irreps_node_input, + irreps_node_attr, + irreps_node_hidden, + irreps_node_output, + irreps_edge_attr, + irreps_edge_scalars, + convolution_kwargs={}, + num_layers=3, + resnet=False, + nonlin_type="gate", + nonlin_scalars={"e": "ssp", "o": "tanh"}, + nonlin_gates={"e": "ssp", "o": "abs"}, + dtype=float32, + ncon_dtype=float32 + ): + super().__init__() + if nonlin_type not in ('gate', 'norm'): + raise ValueError(f"Unexpected nonlin_type {nonlin_type}.") + + nonlin_scalars = { + 1: nonlin_scalars["e"], + -1: nonlin_scalars["o"], + } + nonlin_gates = { + 1: nonlin_gates["e"], + -1: nonlin_gates["o"], + } + + self.irreps_node_input = Irreps(irreps_node_input) + self.irreps_node_hidden = Irreps(irreps_node_hidden) + self.irreps_node_output = Irreps(irreps_node_output) + self.irreps_node_attr = Irreps(irreps_node_attr) + self.irreps_edge_attr = Irreps(irreps_edge_attr) + self.irreps_edge_scalars = Irreps(irreps_edge_scalars) + + irreps_node = self.irreps_node_input + irreps_prev = irreps_node + self.layers = nn.CellList() + self.resnets = [] + + for _ in range(num_layers): + tmp_irreps = irreps_node * self.irreps_edge_attr + + irreps_scalars = Irreps( + [ + (mul, ir) + for mul, ir in self.irreps_node_hidden + if ir.l == 0 and ir in tmp_irreps + ] + ).simplify() + irreps_gated = Irreps( + [ + (mul, ir) + for mul, ir in self.irreps_node_hidden + if ir.l > 0 and ir in tmp_irreps + ] + ) + + if nonlin_type == "gate": + ir = "0e" if Irreps("0e") in tmp_irreps else "0o" + irreps_gates = Irreps([(mul, ir) + for mul, _ in irreps_gated]).simplify() + + nonlinear = Gate( + irreps_scalars, + [acts[nonlin_scalars[ir.p]] for _, ir in irreps_scalars], + irreps_gates, + [acts[nonlin_gates[ir.p]] for _, ir in irreps_gates], + irreps_gated, + dtype=dtype, + ncon_dtype=ncon_dtype + ) + + conv_irreps_out = nonlinear.irreps_in + else: + conv_irreps_out = (irreps_scalars + irreps_gated).simplify() + + nonlinear = NormActivation( + irreps_in=conv_irreps_out, + act=acts[nonlin_scalars[1]], + normalize=True, + epsilon=1e-8, + bias=False, + dtype=dtype, + ncon_dtype=ncon_dtype + ) + + conv = Convolution( + irreps_node_input=irreps_node, + irreps_node_attr=self.irreps_node_attr, + irreps_node_output=conv_irreps_out, + irreps_edge_attr=self.irreps_edge_attr, + irreps_edge_scalars=self.irreps_edge_scalars, + **convolution_kwargs, + dtype=dtype, + ncon_dtype=ncon_dtype + ) + irreps_node = nonlinear.irreps_out + + self.layers.append(Compose(conv, nonlinear)) + + if irreps_prev == irreps_node and resnet: + self.resnets.append(True) + else: + self.resnets.append(False) + irreps_prev = irreps_node + + def construct(self, node_input, node_attr, edge_src, edge_dst, edge_attr, edge_scalars): + """construct""" + layer_in = node_input + for i in enumerate(self.layers): + layer_out = self.layers[i]( + layer_in, node_attr, edge_src, edge_dst, edge_attr, edge_scalars) + + if self.resnets[i]: + layer_in = layer_out + layer_in + else: + layer_in = layer_out + + return layer_in diff --git a/MindChem/applications/orb/models/__init__.py b/MindChem/applications/orb/mindchemistry/cell/orb/__init__.py similarity index 100% rename from MindChem/applications/orb/models/__init__.py rename to MindChem/applications/orb/mindchemistry/cell/orb/__init__.py diff --git a/MindChem/applications/orb/models/gns.py b/MindChem/applications/orb/mindchemistry/cell/orb/gns.py similarity index 99% rename from MindChem/applications/orb/models/gns.py rename to MindChem/applications/orb/mindchemistry/cell/orb/gns.py index ad58d061b..ab53083f8 100644 --- a/MindChem/applications/orb/models/gns.py +++ b/MindChem/applications/orb/mindchemistry/cell/orb/gns.py @@ -24,7 +24,7 @@ from mindspore import nn, ops, Tensor, mint from mindspore.common.initializer import Uniform import mindspore.ops.operations as P -from models.utils import build_mlp +from mindchemistry.cell.orb.utils import build_mlp _KEY = "feat" diff --git a/MindChem/applications/orb/models/orb.py b/MindChem/applications/orb/mindchemistry/cell/orb/orb.py similarity index 99% rename from MindChem/applications/orb/models/orb.py rename to MindChem/applications/orb/mindchemistry/cell/orb/orb.py index 6d9d30076..8afc55dbf 100644 --- a/MindChem/applications/orb/models/orb.py +++ b/MindChem/applications/orb/mindchemistry/cell/orb/orb.py @@ -21,8 +21,8 @@ import numpy import mindspore as ms from mindspore import Parameter, ops, Tensor, mint -from models.gns import _KEY, MoleculeGNS -from models.utils import ( +from mindchemistry.cell.orb.gns import _KEY, MoleculeGNS +from mindchemistry.cell.orb.utils import ( aggregate_nodes, build_mlp, REFERENCE_ENERGIES, @@ -123,7 +123,7 @@ class ScalarNormalizer(ms.nn.Cell): self.bn = mint.nn.BatchNorm1d(1, affine=False, momentum=None) self.bn.running_mean = Parameter(Tensor([0], ms.float32)) self.bn.running_var = Parameter(Tensor([1], ms.float32)) - self.bn.num_batches_tracked = Parameter(Tensor([1000], ms.float32), requires_grad=False) + self.bn.num_batches_tracked = Parameter(Tensor([1000], ms.float32)) self.stastics = { "running_mean": init_mean if init_mean is not None else 0.0, "running_var": init_std**2 if init_std is not None else 1.0, @@ -152,6 +152,7 @@ class ScalarNormalizer(ms.nn.Cell): return x * mint.sqrt(self.running_var) + self.running_mean return x * mint.sqrt(self.bn.running_var) + self.bn.running_mean + # pylint: disable=C0301 class NodeHead(ms.nn.Cell): r""" diff --git a/MindChem/applications/orb/models/utils.py b/MindChem/applications/orb/mindchemistry/cell/orb/utils.py similarity index 100% rename from MindChem/applications/orb/models/utils.py rename to MindChem/applications/orb/mindchemistry/cell/orb/utils.py diff --git a/MindChem/applications/orb/mindchemistry/graph/__init__.py b/MindChem/applications/orb/mindchemistry/graph/__init__.py new file mode 100644 index 000000000..1ae7d9a34 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/graph/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""graph""" diff --git a/MindChem/applications/orb/mindchemistry/graph/dataloader.py b/MindChem/applications/orb/mindchemistry/graph/dataloader.py new file mode 100644 index 000000000..6af294afb --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/graph/dataloader.py @@ -0,0 +1,408 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""dataloader +""" +import random +import numpy as np +from mindspore import Tensor +import mindspore as ms + + +class DataLoaderBase: + r""" + DataLoader that stacks a batch of graph data to fixed-size Tensors + + For specific dataset, usually the following functions should be customized to include different fields: + __init__, shuffle_action, __iter__ + + """ + + def __init__(self, + batch_size, + edge_index, + label=None, + node_attr=None, + edge_attr=None, + padding_std_ratio=3.5, + dynamic_batch_size=True, + shuffle_dataset=True, + max_node=None, + max_edge=None): + self.batch_size = batch_size + self.edge_index = edge_index + self.index = 0 + self.step = 0 + self.padding_std_ratio = padding_std_ratio + self.batch_change_num = 0 + self.batch_exceeding_num = 0 + self.dynamic_batch_size = dynamic_batch_size + self.shuffle_dataset = shuffle_dataset + + ### can be customized to specific dataset + self.label = label + self.node_attr = node_attr + self.edge_attr = edge_attr + self.sample_num = len(self.node_attr) + batch_size_div = self.batch_size + if batch_size_div != 0: + self.step_num = int(self.sample_num / batch_size_div) + else: + raise ValueError + + if dynamic_batch_size: + self.max_start_sample = self.sample_num + else: + self.max_start_sample = self.sample_num - self.batch_size + 1 + + self.set_global_max_node_edge_num(self.node_attr, self.edge_attr, max_node, max_edge, shuffle_dataset, + dynamic_batch_size) + ####### + + def __len__(self): + return self.sample_num + + ### example of generating data of each step, can be customized to specific dataset + def __iter__(self): + if self.shuffle_dataset: + self.shuffle() + else: + self.restart() + + while self.index < self.max_start_sample: + # pylint: disable=W0612 + edge_index_step, node_batch_step, node_mask, edge_mask, batch_size_mask, node_num, edge_num, batch_size \ + = self.gen_common_data(self.node_attr, self.edge_attr) + + ### can be customized to generate different attributes or labels according to specific dataset + node_attr_step = self.gen_node_attr(self.node_attr, batch_size, node_num) + edge_attr_step = self.gen_edge_attr(self.edge_attr, batch_size, edge_num) + label_step = self.gen_global_attr(self.label, batch_size) + + self.add_step_index(batch_size) + + ### make number to Tensor, if it is used as a Tensor in the network + node_num = Tensor(node_num) + batch_size = Tensor(batch_size) + + yield node_attr_step, edge_attr_step, label_step, edge_index_step, node_batch_step, \ + node_mask, edge_mask, node_num, batch_size + + @staticmethod + def pad_zero_to_end(src, axis, zeros_len): + """pad_zero_to_end""" + pad_shape = [] + for i in range(src.ndim): + if i == axis: + pad_shape.append((0, zeros_len)) + else: + pad_shape.append((0, 0)) + return np.pad(src, pad_shape) + + @staticmethod + def gen_mask(total_len, real_len): + """gen_mask""" + mask = np.concatenate((np.full((real_len,), np.float32(1)), np.full((total_len - real_len,), np.float32(0)))) + return mask + + ### example of computing global max length of node_attr and edge_attr, can be customized to specific dataset + def set_global_max_node_edge_num(self, + node_attr, + edge_attr, + max_node=None, + max_edge=None, + shuffle_dataset=True, + dynamic_batch_size=True): + """set_global_max_node_edge_num + + Args: + node_attr: node_attr + edge_attr: edge_attr + max_node: max_node. Defaults to None. + max_edge: max_edge. Defaults to None. + shuffle_dataset: shuffle_dataset. Defaults to True. + dynamic_batch_size: dynamic_batch_size. Defaults to True. + + Raises: + ValueError: ValueError + """ + if not shuffle_dataset: + max_node_num, max_edge_num = self.get_max_node_edge_num(node_attr, edge_attr, dynamic_batch_size) + self.max_node_num_global = max_node_num if max_node is None else max(max_node, max_node_num) + self.max_edge_num_global = max_edge_num if max_edge is None else max(max_edge, max_edge_num) + return + + sum_node = 0 + sum_edge = 0 + count = 0 + max_node_single = 0 + max_edge_single = 0 + for step in range(self.sample_num): + node_len = len(node_attr[step]) + edge_len = len(edge_attr[step]) + sum_node += node_len + sum_edge += edge_len + max_node_single = max(max_node_single, node_len) + max_edge_single = max(max_edge_single, edge_len) + count += 1 + if count != 0: + mean_node = sum_node / count + mean_edge = sum_edge / count + else: + raise ValueError + + if max_node is not None and max_edge is not None: + if max_node < max_node_single: + raise ValueError( + f"the max_node {max_node} is less than the max length of a single sample {max_node_single}") + if max_edge < max_edge_single: + raise ValueError( + f"the max_edge {max_edge} is less than the max length of a single sample {max_edge_single}") + + self.max_node_num_global = max_node + self.max_edge_num_global = max_edge + elif max_node is None and max_edge is None: + sum_node = 0 + sum_edge = 0 + for step in range(self.sample_num): + sum_node += (len(node_attr[step]) - mean_node) ** 2 + sum_edge += (len(edge_attr[step]) - mean_edge) ** 2 + + if count != 0: + std_node = np.sqrt(sum_node / count) + std_edge = np.sqrt(sum_edge / count) + else: + raise ValueError + + self.max_node_num_global = int(self.batch_size * mean_node + + self.padding_std_ratio * np.sqrt(self.batch_size) * std_node) + self.max_edge_num_global = int(self.batch_size * mean_edge + + self.padding_std_ratio * np.sqrt(self.batch_size) * std_edge) + self.max_node_num_global = max(self.max_node_num_global, max_node_single) + self.max_edge_num_global = max(self.max_edge_num_global, max_edge_single) + elif max_node is None: + if max_edge < max_edge_single: + raise ValueError( + f"the max_edge {max_edge} is less than the max length of a single sample {max_edge_single}") + + if mean_edge != 0: + self.max_node_num_global = int(max_edge * mean_node / mean_edge) + else: + raise ValueError + self.max_node_num_global = max(self.max_node_num_global, max_node_single) + self.max_edge_num_global = max_edge + else: + if max_node < max_node_single: + raise ValueError( + f"the max_node {max_node} is less than the max length of a single sample {max_node_single}") + + self.max_node_num_global = max_node + if mean_node != 0: + self.max_edge_num_global = int(max_node * mean_edge / mean_node) + else: + raise ValueError + self.max_edge_num_global = max(self.max_edge_num_global, max_edge_single) + + def get_max_node_edge_num(self, node_attr, edge_attr, remainder=True): + """get_max_node_edge_num + + Args: + node_attr: node_attr + edge_attr: edge_attr + remainder (bool, optional): remainder. Defaults to True. + + Returns: + max_node_num, max_edge_num + """ + max_node_num = 0 + max_edge_num = 0 + index = 0 + for _ in range(self.step_num): + node_num = 0 + edge_num = 0 + for _ in range(self.batch_size): + node_num += len(node_attr[index]) + edge_num += len(edge_attr[index]) + index += 1 + max_node_num = max(max_node_num, node_num) + max_edge_num = max(max_edge_num, edge_num) + + if remainder: + remain_num = self.sample_num - index - 1 + node_num = 0 + edge_num = 0 + for _ in range(remain_num): + node_num += len(node_attr[index]) + edge_num += len(edge_attr[index]) + index += 1 + max_node_num = max(max_node_num, node_num) + max_edge_num = max(max_edge_num, edge_num) + + return max_node_num, max_edge_num + + def shuffle_index(self): + """shuffle_index""" + indices = list(range(self.sample_num)) + random.shuffle(indices) + return indices + + ### example of shuffling the input dataset, can be customized to specific dataset + def shuffle_action(self): + """shuffle_action""" + indices = self.shuffle_index() + self.edge_index = [self.edge_index[i] for i in indices] + self.label = [self.label[i] for i in indices] + self.node_attr = [self.node_attr[i] for i in indices] + self.edge_attr = [self.edge_attr[i] for i in indices] + + ### example of generating the final shuffled dataset, can be customized to specific dataset + def shuffle(self): + """shuffle""" + self.shuffle_action() + if not self.dynamic_batch_size: + max_node_num, max_edge_num = self.get_max_node_edge_num(self.node_attr, self.edge_attr, remainder=False) + while max_node_num > self.max_node_num_global or max_edge_num > self.max_edge_num_global: + self.shuffle_action() + max_node_num, max_edge_num = self.get_max_node_edge_num(self.node_attr, self.edge_attr, remainder=False) + + self.step = 0 + self.index = 0 + + def restart(self): + """restart""" + self.step = 0 + self.index = 0 + + ### example of calculating dynamic batch size to avoid exceeding the max length of node and edge, can be customized to specific dataset + def get_batch_size(self, node_attr, edge_attr, start_batch_size): + """get_batch_size + + Args: + node_attr: node_attr + edge_attr: edge_attr + start_batch_size: start_batch_size + + Returns: + batch_size + """ + node_num = 0 + edge_num = 0 + for i in range(start_batch_size): + index = self.index + i + node_num += len(node_attr[index]) + edge_num += len(edge_attr[index]) + + exceeding = False + while node_num > self.max_node_num_global or edge_num > self.max_edge_num_global: + node_num -= len(node_attr[index]) + edge_num -= len(edge_attr[index]) + index -= 1 + exceeding = True + self.batch_exceeding_num += 1 + if exceeding: + self.batch_change_num += 1 + + return index - self.index + 1 + + def gen_common_data(self, node_attr, edge_attr): + """gen_common_data + + Args: + node_attr: node_attr + edge_attr: edge_attr + + Returns: + common_data + """ + if self.dynamic_batch_size: + if self.step >= self.step_num: + batch_size = self.get_batch_size(node_attr, edge_attr, + min((self.sample_num - self.index), self.batch_size)) + else: + batch_size = self.get_batch_size(node_attr, edge_attr, self.batch_size) + else: + batch_size = self.batch_size + + ######################## node_batch + node_batch_step = [] + sample_num = 0 + for i in range(self.index, self.index + batch_size): + node_batch_step.extend([sample_num] * node_attr[i].shape[0]) + sample_num += 1 + node_batch_step = np.array(node_batch_step) + node_num = node_batch_step.shape[0] + + ######################## edge_index + edge_index_step = np.array([[], []], dtype=np.int64) + max_edge_index = 0 + for i in range(self.index, self.index + batch_size): + edge_index_step = np.concatenate((edge_index_step, self.edge_index[i] + max_edge_index), 1) + max_edge_index = np.max(edge_index_step) + 1 + edge_num = edge_index_step.shape[1] + + ######################### padding + edge_index_step = self.pad_zero_to_end(edge_index_step, 1, self.max_edge_num_global - edge_num) + node_batch_step = self.pad_zero_to_end(node_batch_step, 0, self.max_node_num_global - node_num) + + ######################### mask + node_mask = self.gen_mask(self.max_node_num_global, node_num) + edge_mask = self.gen_mask(self.max_edge_num_global, edge_num) + batch_size_mask = self.gen_mask(self.batch_size, batch_size) + + ######################### make Tensor + edge_index_step = Tensor(edge_index_step, ms.int32) + node_batch_step = Tensor(node_batch_step, ms.int32) + node_mask = Tensor(node_mask) + edge_mask = Tensor(edge_mask) + batch_size_mask = Tensor(batch_size_mask) + + return CommonData(edge_index_step, node_batch_step, node_mask, edge_mask, batch_size_mask, node_num, edge_num, + batch_size).get_tuple_data() + + def gen_node_attr(self, node_attr, batch_size, node_num): + """gen_node_attr""" + node_attr_step = np.concatenate(node_attr[self.index:self.index + batch_size], 0) + node_attr_step = self.pad_zero_to_end(node_attr_step, 0, self.max_node_num_global - node_num) + node_attr_step = Tensor(node_attr_step) + return node_attr_step + + def gen_edge_attr(self, edge_attr, batch_size, edge_num): + """gen_edge_attr""" + edge_attr_step = np.concatenate(edge_attr[self.index:self.index + batch_size], 0) + edge_attr_step = self.pad_zero_to_end(edge_attr_step, 0, self.max_edge_num_global - edge_num) + edge_attr_step = Tensor(edge_attr_step) + return edge_attr_step + + def gen_global_attr(self, global_attr, batch_size): + """gen_global_attr""" + global_attr_step = np.stack(global_attr[self.index:self.index + batch_size], 0) + global_attr_step = self.pad_zero_to_end(global_attr_step, 0, self.batch_size - batch_size) + global_attr_step = Tensor(global_attr_step) + return global_attr_step + + def add_step_index(self, batch_size): + """add_step_index""" + self.index = self.index + batch_size + self.step += 1 + +class CommonData: + """CommonData""" + def __init__(self, edge_index_step, node_batch_step, node_mask, edge_mask, batch_size_mask, node_num, edge_num, + batch_size): + self.tuple_data = (edge_index_step, node_batch_step, node_mask, edge_mask, batch_size_mask, node_num, edge_num, + batch_size) + + def get_tuple_data(self): + """get_tuple_data""" + return self.tuple_data diff --git a/MindChem/applications/orb/mindchemistry/graph/graph.py b/MindChem/applications/orb/mindchemistry/graph/graph.py new file mode 100644 index 000000000..1f13b8869 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/graph/graph.py @@ -0,0 +1,294 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""graph""" +import mindspore as ms +from mindspore import ops, nn + + +def degree(index, dim_size, mask=None): + r""" + Computes the degree of a one-dimensional index tensor. + """ + if index.ndim != 1: + raise ValueError(f"the dimension of index {index.ndim} is not equal to 1") + + if mask is not None: + if mask.shape[0] != index.shape[0]: + raise ValueError(f"mask.shape[0] {mask.shape[0]} is not equal to index.shape[0] {index.shape[0]}") + if mask.ndim != 1: + st = [0] * mask.ndim + slice_size = [1] * mask.ndim + slice_size[0] = mask.shape[0] + mask = ops.slice(mask, st, slice_size).squeeze() + src = mask.astype(ms.int32) + else: + src = ops.ones(index.shape, ms.int32) + + index = index.unsqueeze(-1) + out = ops.zeros((dim_size,), ms.int32) + + return ops.tensor_scatter_add(out, index, src) + + +class Aggregate(nn.Cell): + r""" + Easy-use version of scatter. + + Args: + mode (str): {'add', 'sum', 'mean', 'avg'}, scatter mode. + + Raises: + ValueError: If `mode` is not legal. + + Supported Platforms: + ``CPU`` ``GPU`` ``Ascend`` + + """ + + def __init__(self, mode='add'): + super().__init__() + self.mode = mode + if mode in ('add', 'sum'): + self.scatter = self.scatter_sum + elif mode in ('mean', 'avg'): + self.scatter = self.scatter_mean + else: + raise ValueError(f"Unexpected scatter mode {mode}") + + @staticmethod + def scatter_sum(src, index, out=None, dim_size=None, mask=None): + r""" + Computes the scatter sum of a source tensor. The index should be one-dimensional + """ + if index.ndim != 1: + raise ValueError(f"the dimension of index {index.ndim} is not equal to 1") + if index.shape[0] != src.shape[0]: + raise ValueError(f"index.shape[0] {index.shape[0]} is not equal to src.shape[0] {src.shape[0]}") + if out is None and dim_size is None: + raise ValueError("the out Tensor and out dim_size cannot be both None") + + index = index.unsqueeze(-1) + + if out is None: + out = ops.zeros((dim_size,) + src.shape[1:], dtype=src.dtype) + elif dim_size is not None and out.shape[0] != dim_size: + raise ValueError(f"the out.shape[0] {out.shape[0]} is not equal to dim_size {dim_size}") + + if mask is not None: + if mask.shape[0] != src.shape[0]: + raise ValueError(f"mask.shape[0] {mask.shape[0]} is not equal to src.shape[0] {src.shape[0]}") + if src.ndim != mask.ndim: + if mask.size != mask.shape[0]: + raise ValueError("mask.ndim dose not match src.ndim, and cannot be broadcasted to the same") + shape = [1] * src.ndim + shape[0] = -1 + mask = ops.reshape(mask, shape) + src = ops.mul(src, mask.astype(src.dtype)) + + return ops.tensor_scatter_add(out, index, src) + + @staticmethod + def scatter_mean(src, index, out=None, dim_size=None, mask=None): + r""" + Computes the scatter mean of a source tensor. The index should be one-dimensional + """ + if out is None and dim_size is None: + raise ValueError("the out Tensor and out dim_size cannot be both None") + + if dim_size is None: + dim_size = out.shape[0] + elif out is not None and out.shape[0] != dim_size: + raise ValueError(f"the out.shape[0] {out.shape[0]} is not equal to dim_size {dim_size}") + + count = degree(index, dim_size, mask=mask) + eps = 1e-5 + count = ops.maximum(count, eps) + + scatter_sum = Aggregate.scatter_sum(src, index, dim_size=dim_size, mask=mask) + + shape = [1] * scatter_sum.ndim + shape[0] = -1 + count = ops.reshape(count, shape).astype(scatter_sum.dtype) + res = ops.true_divide(scatter_sum, count) + + if out is not None: + res = res + out + + return res + + +class AggregateNodeToGlobal(Aggregate): + """AggregateNodeToGlobal""" + + def __init__(self, mode='add'): + super().__init__(mode=mode) + + def construct(self, node_attr, batch, out=None, dim_size=None, mask=None): + r""" + Args: + node_attr (Tensor): The source tensor of node attributes. + batch (Tensor): The indices of sample to scatter to. + out (Tensor): The destination tensor. Default: None. + dim_size (int): If `out` is not given, automatically create output with size `dim_size`. Default: None. + out and dim_size cannot be both None. + mask (Tensor): The mask of the node_attr tensor + Returns: + Tensor. + """ + return self.scatter(node_attr, batch, out=out, dim_size=dim_size, mask=mask) + + +class AggregateEdgeToGlobal(Aggregate): + """AggregateEdgeToGlobal""" + + def __init__(self, mode='add'): + super().__init__(mode=mode) + + def construct(self, edge_attr, batch_edge, out=None, dim_size=None, mask=None): + r""" + Args: + edge_attr (Tensor): The source tensor of edge attributes. + batch_edge (Tensor): The indices of sample to scatter to. + out (Tensor): The destination tensor. Default: None. + dim_size (int): If `out` is not given, automatically create output with size `dim_size`. Default: None. + out and dim_size cannot be both None. + mask (Tensor): The mask of the node_attr tensor + Returns: + Tensor. + """ + return self.scatter(edge_attr, batch_edge, out=out, dim_size=dim_size, mask=mask) + + +class AggregateEdgeToNode(Aggregate): + """AggregateEdgeToNode""" + + def __init__(self, mode='add', dim=0): + super().__init__(mode=mode) + self.dim = dim + + def construct(self, edge_attr, edge_index, out=None, dim_size=None, mask=None): + r""" + Args: + edge_attr (Tensor): The source tensor of edge attributes. + edge_index (Tensor): The indices of nodes in each edge. + out (Tensor): The destination tensor. Default: None. + dim_size (int): If `out` is not given, automatically create output with size `dim_size`. Default: None. + out and dim_size cannot be both None. + mask (Tensor): The mask of the node_attr tensor + Returns: + Tensor. + """ + return self.scatter(edge_attr, edge_index[self.dim], out=out, dim_size=dim_size, mask=mask) + + +class Lift(nn.Cell): + """Lift""" + + def __init__(self, mode="multi_graph"): + super().__init__() + self.mode = mode + if mode not in ["multi_graph", "single_graph"]: + raise ValueError(f"Unexpected lift mode {mode}") + + @staticmethod + def lift(src, index, axis=0, mask=None): + """lift""" + res = ops.index_select(src, axis, index) + + if mask is not None: + if mask.shape[0] != res.shape[0]: + raise ValueError(f"mask.shape[0] {mask.shape[0]} is not equal to res.shape[0] {res.shape[0]}") + if res.ndim != mask.ndim: + if mask.size != mask.shape[0]: + raise ValueError("mask.ndim dose not match src.ndim, and cannot be broadcasted to the same") + shape = [1] * res.ndim + shape[0] = -1 + mask = ops.reshape(mask, shape) + res = ops.mul(res, mask.astype(res.dtype)) + + return res + + @staticmethod + def repeat(src, num, axis=0, max_len=None): + res = ops.repeat_elements(src, num, axis) + + if (max_len is not None) and (max_len > num): + padding = ops.zeros((max_len - num,) + res.shape[1:], dtype=res.dtype) + res = ops.cat((res, padding), axis=0) + + return res + + +class LiftGlobalToNode(Lift): + """LiftGlobalToNode""" + + def __init__(self, mode="multi_graph"): + super().__init__(mode=mode) + + def construct(self, global_attr, batch=None, num_node=None, mask=None, max_len=None): + r""" + Args: + global_attr (Tensor): The source tensor of global attributes. + batch (Tensor): The indices of samples to get. + num_node (Int): The number of node in the graph, when there is only 1 graph. + mask (Tensor): The mask of the output tensor. + max_len (Int): The output length. + Returns: + Tensor. + """ + if global_attr.shape[0] > 1 or self.mode == "multi_graph": + return self.lift(global_attr, batch, mask=mask) + return self.repeat(global_attr, num_node, max_len=max_len) + + +class LiftGlobalToEdge(Lift): + """LiftGlobalToEdge""" + + def __init__(self, mode="multi_graph"): + super().__init__(mode=mode) + + def construct(self, global_attr, batch_edge=None, num_edge=None, mask=None, max_len=None): + r""" + Args: + global_attr (Tensor): The source tensor of global attributes. + batch_edge (Tensor): The indices of samples to get. + num_edge (Int): The number of edge in the graph, when there is only 1 graph. + mask (Tensor): The mask of the output tensor. + max_len (Int): The output length. + Returns: + Tensor. + """ + if global_attr.shape[0] > 1 or self.mode == "multi_graph": + return self.lift(global_attr, batch_edge, mask=mask) + return self.repeat(global_attr, num_edge, max_len=max_len) + + +class LiftNodeToEdge(Lift): + """LiftNodeToEdge""" + + def __init__(self, dim=0): + super().__init__(mode="multi_graph") + self.dim = dim + + def construct(self, global_attr, edge_index, mask=None): + r""" + Args: + global_attr (Tensor): The source tensor of global attributes. + edge_index (Tensor): The indices of nodes for each edge. + mask (Tensor): The mask of the output tensor. + Returns: + Tensor. + """ + return self.lift(global_attr, edge_index[self.dim], mask=mask) diff --git a/MindChem/applications/orb/mindchemistry/graph/loss.py b/MindChem/applications/orb/mindchemistry/graph/loss.py new file mode 100644 index 000000000..28e241e6f --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/graph/loss.py @@ -0,0 +1,78 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""loss""" +import mindspore as ms +from mindspore import ops, nn + + +class LossMaskBase(nn.Cell): + """LossMaskBase""" + + def __init__(self, reduction='mean', dtype=None): + super().__init__() + self.reduction = reduction + if reduction not in ["mean", "sum"]: + raise ValueError(f"Unexpected reduction mode {reduction}") + + self.dtype = dtype if dtype is not None else ms.float32 + + def construct(self, logits, labels, mask=None, num=None): + """construct""" + if logits.shape != labels.shape: + raise ValueError(f"logits.shape {logits.shape} is not equal to labels.shape {labels.shape}") + + x = self.loss(logits.astype(self.dtype), labels.astype(self.dtype)) + + if mask is not None: + if mask.shape[0] != x.shape[0]: + raise ValueError(f"mask.shape[0] {mask.shape[0]} is not equal to input.shape[0] {x.shape[0]}") + if x.ndim != mask.ndim: + if mask.size != mask.shape[0]: + raise ValueError("mask.ndim dose not match src.ndim, and cannot be broadcasted to the same") + shape = [1] * x.ndim + shape[0] = -1 + mask = ops.reshape(mask, shape) + x = ops.mul(x, mask.astype(x.dtype)) + + # pylint: disable=W0622 + sum = ops.sum(x) + if self.reduction == "sum": + return sum + if num is None: + num = x.size + else: + num_div = x.shape[0] + if num_div != 0: + num = x.size / num_div * num + else: + raise ValueError + return ops.true_divide(sum, num) + +class L1LossMask(LossMaskBase): + + def __init__(self, reduction='mean'): + super().__init__(reduction) + + def loss(self, logits, labels): + return ops.abs(logits - labels) + + +class L2LossMask(LossMaskBase): + + def __init__(self, reduction='mean'): + super().__init__(reduction) + + def loss(self, logits, labels): + return ops.square(logits - labels) diff --git a/MindChem/applications/orb/mindchemistry/graph/normlization.py b/MindChem/applications/orb/mindchemistry/graph/normlization.py new file mode 100644 index 000000000..caccfdb42 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/graph/normlization.py @@ -0,0 +1,278 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""norm""" +import mindspore as ms +from mindspore import ops, Parameter, nn +from .graph import AggregateNodeToGlobal, LiftGlobalToNode + + +class BatchNormMask(nn.Cell): + """BatchNormMask""" + + def __init__(self, num_features, eps=1e-5, momentum=0.9, affine=True): + super().__init__() + self.num_features = num_features + self.eps = eps + self.momentum = momentum + self.affine = affine + self.moving_mean = Parameter(ops.zeros((num_features,), ms.float32), name="moving_mean", requires_grad=False) + self.moving_variance = Parameter(ops.ones((num_features,), ms.float32), + name="moving_variance", + requires_grad=False) + if affine: + self.gamma = Parameter(ops.ones((num_features,), ms.float32), name="gamma", requires_grad=True) + self.beta = Parameter(ops.zeros((num_features,), ms.float32), name="beta", requires_grad=True) + + def construct(self, x, mask, num): + """construct""" + if x.shape[1] != self.num_features: + raise ValueError(f"x.shape[1] {x.shape[1]} is not equal to num_features {self.num_features}") + if x.shape[0] != mask.shape[0]: + raise ValueError(f"x.shape[0] {x.shape[0]} is not equal to mask.shape[0] {mask.shape[0]}") + + if x.ndim != mask.ndim: + if mask.size != mask.shape[0]: + raise ValueError("mask.ndim dose not match src.ndim, and cannot be broadcasted to the same") + shape = [1] * x.ndim + shape[0] = -1 + mask = ops.reshape(mask, shape).astype(x.dtype) + x = ops.mul(x, mask) + + # pylint: disable=R1705 + if x.ndim > 2: + norm_axis = [] + shape = [-1] + for i in range(2, x.ndim): + norm_axis.append(i) + shape.append(1) + + if self.training: + mean = ops.div(ops.sum(x, 0), num) + mean = ops.mean(mean, norm_axis) + self.moving_mean = self.momentum * self.moving_mean + (1 - self.momentum) * mean + mean = ops.reshape(mean, shape) + mean = ops.mul(mean, mask) + x = x - mean + + var = ops.div(ops.sum(ops.pow(x, 2), 0), num) + var = ops.mean(var, norm_axis) + self.moving_variance = self.momentum * self.moving_variance + (1 - self.momentum) * var + std = ops.sqrt(ops.add(var, self.eps)) + std = ops.reshape(std, shape) + y = ops.true_divide(x, std) + else: + mean = ops.reshape(self.moving_mean.astype(x.dtype), shape) + mean = ops.mul(mean, mask) + std = ops.sqrt(ops.add(self.moving_variance.astype(x.dtype), self.eps)) + std = ops.reshape(std, shape) + y = ops.true_divide(ops.sub(x, mean), std) + + if self.affine: + gamma = ops.reshape(self.gamma.astype(x.dtype), shape) + beta = ops.reshape(self.beta.astype(x.dtype), shape) * mask + y = y * gamma + beta + + return y + else: + if self.training: + mean = ops.div(ops.sum(x, 0), num) + self.moving_mean = self.momentum * self.moving_mean + (1 - self.momentum) * mean + mean = ops.mul(mean, mask) + x = x - mean + + var = ops.div(ops.sum(ops.pow(x, 2), 0), num) + self.moving_variance = self.momentum * self.moving_variance + (1 - self.momentum) * var + std = ops.sqrt(ops.add(var, self.eps)) + y = ops.true_divide(x, std) + else: + mean = ops.mul(self.moving_mean.astype(x.dtype), mask) + std = ops.sqrt(ops.add(self.moving_variance.astype(x.dtype), self.eps)) + y = ops.true_divide(ops.sub(x, mean), std) + + if self.affine: + beta = self.beta.astype(x.dtype) * mask + y = y * self.gamma.astype(x.dtype) + beta + + return y + + +class GraphLayerNormMask(nn.Cell): + """GraphLayerNormMask""" + + def __init__(self, + normalized_shape, + begin_norm_axis=-1, + eps=1e-5, + sub_mean=True, + divide_std=True, + affine_weight=True, + affine_bias=True, + aggr_mode="mean"): + super().__init__() + self.normalized_shape = normalized_shape + self.begin_norm_axis = begin_norm_axis + self.eps = eps + self.sub_mean = sub_mean + self.divide_std = divide_std + self.affine_weight = affine_weight + self.affine_bias = affine_bias + self.mean = ops.ReduceMean(keep_dims=True) + self.aggregate = AggregateNodeToGlobal(mode=aggr_mode) + self.lift = LiftGlobalToNode(mode="multi_graph") + + if affine_weight: + self.gamma = Parameter(ops.ones(normalized_shape, ms.float32), name="gamma", requires_grad=True) + if affine_bias: + self.beta = Parameter(ops.zeros(normalized_shape, ms.float32), name="beta", requires_grad=True) + + def construct(self, x, batch, mask, dim_size, scale=None): + """construct""" + begin_norm_axis = self.begin_norm_axis if self.begin_norm_axis >= 0 else self.begin_norm_axis + x.ndim + if begin_norm_axis not in range(1, x.ndim): + raise ValueError(f"begin_norm_axis {begin_norm_axis} is not in range 1 to {x.ndim}") + + norm_axis = [] + for i in range(begin_norm_axis, x.ndim): + norm_axis.append(i) + if self.normalized_shape[i - begin_norm_axis] != x.shape[i]: + raise ValueError(f"x.shape[{i}] {x.shape[i]} is not equal to normalized_shape[{i - begin_norm_axis}] " + f"{self.normalized_shape[i - begin_norm_axis]}") + + if x.shape[0] != mask.shape[0]: + raise ValueError(f"x.shape[0] {x.shape[0]} is not equal to mask.shape[0] {mask.shape[0]}") + if x.shape[0] != batch.shape[0]: + raise ValueError(f"x.shape[0] {x.shape[0]} is not equal to batch.shape[0] {batch.shape[0]}") + + if x.ndim != mask.ndim: + if mask.size != mask.shape[0]: + raise ValueError("mask.ndim dose not match src.ndim, and cannot be broadcasted to the same") + shape = [1] * x.ndim + shape[0] = -1 + mask = ops.reshape(mask, shape).astype(x.dtype) + x = ops.mul(x, mask) + + if self.sub_mean: + mean = self.aggregate(x, batch, dim_size=dim_size, mask=mask) + mean = self.mean(mean, norm_axis) + mean = self.lift(mean, batch) + mean = ops.mul(mean, mask) + x = x - mean + + if self.divide_std: + var = self.aggregate(ops.square(x), batch, dim_size=dim_size, mask=mask) + var = self.mean(var, norm_axis) + if scale is not None: + var = var * scale + std = ops.sqrt(var + self.eps) + std = self.lift(std, batch) + x = ops.true_divide(x, std) + + if self.affine_weight: + x = x * self.gamma.astype(x.dtype) + + if self.affine_bias: + beta = ops.mul(self.beta.astype(x.dtype), mask) + x = x + beta + + return x + + +class GraphInstanceNormMask(nn.Cell): + """GraphInstanceNormMask""" + + def __init__(self, + num_features, + eps=1e-5, + sub_mean=True, + divide_std=True, + affine_weight=True, + affine_bias=True, + aggr_mode="mean"): + super().__init__() + self.num_features = num_features + self.eps = eps + self.sub_mean = sub_mean + self.divide_std = divide_std + self.affine_weight = affine_weight + self.affine_bias = affine_bias + self.mean = ops.ReduceMean(keep_dims=True) + self.aggregate = AggregateNodeToGlobal(mode=aggr_mode) + self.lift = LiftGlobalToNode(mode="multi_graph") + + if affine_weight: + self.gamma = Parameter(ops.ones((self.num_features,), ms.float32), name="gamma", requires_grad=True) + if affine_bias: + self.beta = Parameter(ops.zeros((self.num_features,), ms.float32), name="beta", requires_grad=True) + + def construct(self, x, batch, mask, dim_size, scale=None): + """construct""" + if x.shape[1] != self.num_features: + raise ValueError(f"x.shape[1] {x.shape[1]} is not equal to num_features {self.num_features}") + if x.shape[0] != mask.shape[0]: + raise ValueError(f"x.shape[0] {x.shape[0]} is not equal to mask.shape[0] {mask.shape[0]}") + if x.shape[0] != batch.shape[0]: + raise ValueError(f"x.shape[0] {x.shape[0]} is not equal to batch.shape[0] {batch.shape[0]}") + + if x.ndim != mask.ndim: + if mask.size != mask.shape[0]: + raise ValueError("mask.ndim dose not match src.ndim, and cannot be broadcasted to the same") + shape = [1] * x.ndim + shape[0] = -1 + mask = ops.reshape(mask, shape).astype(x.dtype) + x = ops.mul(x, mask) + gamma = None # 后来添加,防止未定义报错 + if x.ndim > 2: + norm_axis = [] + shape = [-1] + for i in range(2, x.ndim): + norm_axis.append(i) + shape.append(1) + + if self.affine_weight: + gamma = ops.reshape(self.gamma.astype(x.dtype), shape) + if self.affine_bias: + beta = ops.reshape(self.beta.astype(x.dtype), shape) + else: + if self.affine_weight: + gamma = self.gamma.astype(x.dtype) + if self.affine_bias: + beta = self.beta.astype(x.dtype) + + if self.sub_mean: + mean = self.aggregate(x, batch, dim_size=dim_size, mask=mask) + if x.ndim > 2: + mean = self.mean(mean, norm_axis) + mean = self.lift(mean, batch) + mean = ops.mul(mean, mask) + x = x - mean + + if self.divide_std: + var = self.aggregate(ops.square(x), batch, dim_size=dim_size, mask=mask) + if x.ndim > 2: + var = self.mean(var, norm_axis) + if scale is not None: + var = var * scale + std = ops.sqrt(var + self.eps) + std = self.lift(std, batch) + x = ops.true_divide(x, std) + + if self.affine_weight: + x = x * gamma + + if self.affine_bias: + beta = ops.mul(beta, mask) + x = x + beta + + return x diff --git a/MindChem/applications/orb/mindchemistry/so2_conv/__init__.py b/MindChem/applications/orb/mindchemistry/so2_conv/__init__.py new file mode 100644 index 000000000..e5542a477 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/so2_conv/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +init file +""" +from .so3 import SO3Rotation +from .so2 import SO2Convolution diff --git a/MindChem/applications/orb/mindchemistry/so2_conv/init_edge_rot_mat.py b/MindChem/applications/orb/mindchemistry/so2_conv/init_edge_rot_mat.py new file mode 100644 index 000000000..a05a2264b --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/so2_conv/init_edge_rot_mat.py @@ -0,0 +1,64 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +file to get rotating matrix from edge distance vector +""" +from mindspore import ops +import mindspore.numpy as ms_np + + +def init_edge_rot_mat(edge_distance_vec): + """ + get rotating matrix from edge distance vector + """ + epsilon = 0.00000001 + edge_vec_0 = edge_distance_vec + edge_vec_0_distance = ops.sqrt(ops.maximum(ops.sum(edge_vec_0 ** 2, dim=1), epsilon)) + # Make sure the atoms are far enough apart + norm_x = ops.div(edge_vec_0, edge_vec_0_distance.view(-1, 1)) + edge_vec_2 = ops.rand_like(edge_vec_0) - 0.5 + + edge_vec_2 = ops.div(edge_vec_2, ops.sqrt(ops.maximum(ops.sum(edge_vec_2 ** 2, dim=1), epsilon)).view(-1, 1)) + # Create two rotated copies of the random vectors in case the random vector is aligned with norm_x + # With two 90 degree rotated vectors, at least one should not be aligned with norm_x + edge_vec_2b = edge_vec_2.copy() + edge_vec_2b[:, 0] = -edge_vec_2[:, 1] + edge_vec_2b[:, 1] = edge_vec_2[:, 0] + edge_vec_2c = edge_vec_2.copy() + edge_vec_2c[:, 1] = -edge_vec_2[:, 2] + edge_vec_2c[:, 2] = edge_vec_2[:, 1] + vec_dot_b = ops.abs(ops.sum(edge_vec_2b * norm_x, dim=1)).view(-1, 1) + vec_dot_c = ops.abs(ops.sum(edge_vec_2c * norm_x, dim=1)).view(-1, 1) + vec_dot = ops.abs(ops.sum(edge_vec_2 * norm_x, dim=1)).view(-1, 1) + edge_vec_2 = ops.where(ops.broadcast_to(ops.gt(vec_dot, vec_dot_b), edge_vec_2b.shape), edge_vec_2b, edge_vec_2) + vec_dot = ops.abs(ops.sum(edge_vec_2 * norm_x, dim=1)).view(-1, 1) + edge_vec_2 = ops.where(ops.broadcast_to(ops.gt(vec_dot, vec_dot_c), edge_vec_2c.shape), edge_vec_2c, edge_vec_2) + vec_dot = ops.abs(ops.sum(edge_vec_2 * norm_x, dim=1)) + # Check the vectors aren't aligned + + norm_z = ms_np.cross(norm_x, edge_vec_2, axis=1) + norm_z = ops.div(norm_z, ops.sqrt(ops.maximum(ops.sum(norm_z ** 2, dim=1, keepdim=True), epsilon))) + norm_z = ops.div(norm_z, ops.sqrt(ops.maximum(ops.sum(norm_z ** 2, dim=1), epsilon)).view(-1, 1)) + + norm_y = ms_np.cross(norm_x, norm_z, axis=1) + norm_y = ops.div(norm_y, ops.sqrt(ops.maximum(ops.sum(norm_y ** 2, dim=1, keepdim=True), epsilon))) + # Construct the 3D rotation matrix + norm_x = norm_x.view(-1, 3, 1) + norm_y = -norm_y.view(-1, 3, 1) + norm_z = norm_z.view(-1, 3, 1) + edge_rot_mat_inv = ops.cat([norm_z, norm_x, norm_y], axis=2) + + edge_rot_mat = ops.swapaxes(edge_rot_mat_inv, 1, 2) + return edge_rot_mat diff --git a/MindChem/applications/orb/mindchemistry/so2_conv/jd.pkl b/MindChem/applications/orb/mindchemistry/so2_conv/jd.pkl new file mode 100644 index 0000000000000000000000000000000000000000..1b762ad4369564e2f21b808850cc8013e6e41385 GIT binary patch literal 9925 zcmcIq4RBP|6}}+^l0qOM4QmT_sGvls0WH-G4ZA=R&}>7NA2T`k zy?5?+zWd#C&U<;GqVt*^_b41GRj#5L#rdVPeI*5{(|komzT(ufg5pwNiB;8Qq8Y5V z?tRvJ#;S^$mpf~2fmM}UJhy0ex%SpmissHLn~_^ml3Q+7b)Q;NFwIw7T2?Z5TA8(4 zPk^IU)wMX^xU9CkYN?eGm1ixtCRi!nDEYU{DvYa&$uBFPT_BS>O&?}et}yqbtD@4a zoSdBO8~VTxuPU?hDlbMx8t09} zf=6dY-5hoNd>__ux$|Ga$9RpzwbsY(4aBj?n`{J_Bxak&AWNK8O@v8(0swVA43mc))3(9eGk#z za1Kdqq2TCPdnyLFbLw9K-V2+&z?aD(c<^xq7;w6J`fK1(hj|-&0fXycT>5ncTl-?q zhvyxHedFY>Jk*8jVBC)25>Kgd(BS1QNBz{9x^NvG!4-buFL-8N#Rvy=81IcYIkBpNlq=+WL9;&AWlW>OT)4-sBBy-QufzaY$~D#I?bL`@<)0k_U6z!gO-|;C+{Y z`Kjwg$vaS2kJteZDY27hw+p$?{4!hBtqlEeOlejGwwV0fx5Jh-nIwV$9PKp2wG3xaEG6H z%{=42b06A9ZU2twDPFQZu@XvJ zq;@|+eQn$l@~4-*fgMg9l-!69$mVOZ|f$G(0Z<`1t?Kk{#XrR{q#zV^i0 zA3XR+@Ze18Kg?g|5BHDy(f4*N*KfyP@4lqBP7!6(6 zUv=H#`l7T3Smq)3ojP+}#IaW#j?q)dE&Xoq7PNDtt6?wg{SvnSkntYY1@n;mPMx`~ zaIhf;`6%@Tc)YSd347O&wa@VL#MZG+Sr^Ph?mKlRW;iVMAs?O3;Is1ahp@M2GpD%O zH&~~v3+5sBJsf<9!Tq8>>I38)&=vBnBlOJKZ0{f@0VE7N38G> ztKUO^>fk*z*_&*7dQ%rO%jwSn%|Aoj><7Dn$6H;?{W3SL$8zMo{`MEr+Y@l@p@PTY z!#VpK%=5T)?Aze(GEm8eSmTzN z=9}y<795Wl&pj=#f~Qj7LCjr!ew!|w?(v#$0{a_w1nw(E{m4(L7TnXCV=V)&nI0qa z!+mr)HWsly{LI8Gxpp1rDb+i6NvurQP-rJ(?l1KtKczOi<$gl;*RV1U_whvQ9`M=M z@I3fki*174I4BAFe!l$&uo9Q~$Ni;#Hoxl;;<*5v#x>T$ZXIRbk17||y|U0mWkz1MtqS=RSXiz6j><6qj<}>pp5GT)kyt~B3Jw3cr6 zQ}!$N1J)b!ITAdXf81Z{M}A7pa!cI#zd5;+{hs}l{fhk{wBEwbRo91LO8$wJ{S_SEGchyPOmh0Ovi!S@aebUL&T3Cdl&d@Y3YkP(cjUF@ag`d`TL934H$c-*nbbO=Y)5(s@Z%W)p~(L?rS{|d)!BNVLhk2Rpa{NoEhLR z(aBd2;j^yrZp%xcMJmgmdww zzBb=<+#6Wm2#l6(rdNn>q>k-VYZ0sQz^kxsD-Il%9x{J7^d0&K&SzF9g1_)!K9Cdn z>T~!kx%>xU@qQ$1O}6>QmN~{~_3In65z{pw1JBw6JKqEEMaPB(bAkEGe4xHI-;{{+ zZhWZ*F&cgUhJCr_9PD3OPs0vq{s4BvoY!Gl$E*wH8TXAk5H}K9+n6cPR<+!K&3gX{ zzua3 0 coefficients + for m in range(self.global_max_order): + if m == 0: + continue + x_m = m_list_merge[m] + x_m = x_m.reshape(num_edges, 2, -1) + x_m = self.so2_m_conv[m - 1](x_m) + out.append(x_m) + + ###################### start fill 0 ###################### + if self.max_order_out + 1 > len(m_list_merge): + for m in range(len(m_list_merge), self.max_order_out + 1): + extra_zero = ops.zeros( + (num_edges, 2, int(self.m_shape_dict_out.get(m, None) / 2))) + out.append(extra_zero) + ###################### finish fill 0 ###################### + + ###################### start _l_primary ######################### + l_primary_list_0 = [] + l_primary_list_left = [] + l_primary_list_right = [] + + for _ in range(self.irreps_out_length): + l_primary_list_0.append([]) + l_primary_list_left.append([]) + l_primary_list_right.append([]) + + m_0 = out[0] + offset = 0 + index = 0 + + for key_val in self.irreps_out_data: + key = key_val[0] + value = key_val[1] + if key >= 0: + l_primary_list_0[index].append( + ops.unsqueeze(m_0[:, offset:offset + value], -1)) + offset = offset + value + index = index + 1 + + for m in range(1, len(out)): + right = out[m][:, 1] + offset = 0 + index = 0 + + for key_val in self.irreps_out_data: + key = key_val[0] + value = key_val[1] + if key >= m: + l_primary_list_right[index].append( + ops.unsqueeze(right[:, offset:offset + value], -1)) + offset = offset + value + index = index + 1 + + for m in range(len(out) - 1, 0, -1): + left = out[m][:, 0] + offset = 0 + index = 0 + + for key_val in self.irreps_out_data: + key = key_val[0] + value = key_val[1] + if key >= m: + l_primary_list_left[index].append( + ops.unsqueeze(left[:, offset:offset + value], -1)) + offset = offset + value + index = index + 1 + + l_primary_list = [] + for i in range(self.irreps_out_length): + if i == 0: + tmp = ops.cat(l_primary_list_0[i], -1) + l_primary_list.append(tmp) + else: + tmp = ops.cat( + (ops.cat((ops.cat(l_primary_list_left[i], + -1), ops.cat(l_primary_list_0[i], -1)), + -1), ops.cat(l_primary_list_right[i], -1)), -1) + l_primary_list.append(tmp) + + ##################### finish _l_primary ######################### + return tuple(l_primary_list) diff --git a/MindChem/applications/orb/mindchemistry/so2_conv/so3.py b/MindChem/applications/orb/mindchemistry/so2_conv/so3.py new file mode 100644 index 000000000..0ffae5a5f --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/so2_conv/so3.py @@ -0,0 +1,156 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +so3 file +""" +import mindspore as ms +from mindspore import nn, ops, vmap, jit_class +from mindspore.numpy import tensordot +from mindscience.e3nn import o3 +from mindscience.e3nn.o3 import Irreps + +from .wigner import wigner_D + + +class SO3Embedding(nn.Cell): + """ + SO3Embedding class + """ + + def __init__(self): + self.embedding = None + + def _rotate(self, so3rotation, lmax_list, max_list): + """ + SO3Embedding rotate + """ + embedding_rotate = so3rotation[0].rotate(self.embedding, lmax_list[0], + max_list[0]) + self.embedding = embedding_rotate + + def _rotate_inv(self, so3rotation): + """ + SO3Embedding rotate inverse + """ + embedding_rotate = so3rotation[0].rotate_inv(self.embedding, + self.lmax_list[0], + self.mmax_list[0]) + self.embedding = embedding_rotate + + +@jit_class +class SO3Rotation: + """ + SO3_Rotation class + """ + + def __init__(self, lmax, irreps_in, irreps_out): + self.lmax = lmax + self.irreps_in1 = Irreps(irreps_in) + self.irreps_out = Irreps(irreps_out) + self.tensordot_vmap = vmap(tensordot, (0, 0, None), 0) + + @staticmethod + def narrow(inputs, axis, start, length): + """ + SO3_Rotation narrow class + """ + begins = [0] * inputs.ndim + begins[axis] = start + + sizes = list(inputs.shape) + + sizes[axis] = length + res = ops.slice(inputs, begins, sizes) + return res + + @staticmethod + def rotation_to_wigner_d_matrix(edge_rot_mat, start_lmax, end_lmax): + """ + SO3_Rotation rotation_to_wigner_d_matrix + """ + x = edge_rot_mat @ ms.Tensor([0.0, 1.0, 0.0]) + alpha, beta = o3.xyz_to_angles(x) + rvalue = (ops.swapaxes( + o3.angles_to_matrix(alpha, beta, ops.zeros_like(alpha)), -1, -2) + @ edge_rot_mat) + gamma = ops.atan2(rvalue[..., 0, 2], rvalue[..., 0, 0]) + + block_list = [] + for lmax in range(start_lmax, end_lmax + 1): + block = wigner_D(lmax, alpha, beta, gamma).astype(ms.float32) + block_list.append(block) + return block_list + + def set_wigner(self, rot_mat3x3): + """ + SO3_Rotation set_wigner + """ + wigner = self.rotation_to_wigner_d_matrix(rot_mat3x3, 0, self.lmax) + wigner_inv = [] + length = len(wigner) + for i in range(length): + wigner_inv.append(ops.swapaxes(wigner[i], 1, 2)) + return tuple(wigner), tuple(wigner_inv) + + def rotate(self, embedding, wigner): + """ + SO3_Rotation rotate + """ + res = [] + batch_shape = embedding.shape[:-1] + for (s, l), mir in zip(self.irreps_in1.slice_tuples, + self.irreps_in1.data): + v_slice = self.narrow(embedding, -1, s, l) + if embedding.ndim == 1: + res.append((v_slice.reshape((1,) + batch_shape + + (mir.mul, mir.ir.dim)), mir.ir)) + else: + res.append( + (v_slice.reshape(batch_shape + (mir.mul, mir.ir.dim)), + mir.ir)) + rotate_data_list = [] + for data, ir in res: + self.tensordot_vmap(data.astype(ms.float16), + wigner[ir.l].astype(ms.float16), ([1], [1])) + rotate_data = self.tensordot_vmap(data.astype(ms.float16), + wigner[ir.l].astype(ms.float16), + ((1), (1))).astype(ms.float32) + rotate_data_list.append(rotate_data) + return tuple(rotate_data_list) + + def rotate_inv(self, embedding, wigner_inv): + """ + SO3_Rotation rotate_inv + """ + res = [] + batch_shape = embedding[0].shape[0:1] + index = 0 + for (_, _), mir in zip(self.irreps_out.slice_tuples, + self.irreps_out.data): + v_slice = embedding[index] + if embedding[0].ndim == 1: + res.append((v_slice, mir.ir)) + else: + res.append((v_slice, mir.ir)) + index = index + 1 + rotate_back_data_list = [] + for data, ir in res: + rotate_back_data = self.tensordot_vmap( + data.astype(ms.float16), wigner_inv[ir.l].astype(ms.float16), + ((1), (1))).astype(ms.float32) + rotate_back_data_list.append( + rotate_back_data.view(batch_shape + (-1,))) + return ops.cat(rotate_back_data_list, -1) diff --git a/MindChem/applications/orb/mindchemistry/so2_conv/wigner.py b/MindChem/applications/orb/mindchemistry/so2_conv/wigner.py new file mode 100644 index 000000000..c3e08615c --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/so2_conv/wigner.py @@ -0,0 +1,61 @@ +# Copyright 2024 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +wigner file +""" + +# pylint: disable=C0103 +import pickle +from mindspore import ops +import mindspore as ms +from mindscience.e3nn.utils.func import broadcast_args + + +def wigner_D(lv, alpha, beta, gamma): + """ + # Borrowed from e3nn @ 0.4.0: + # https://github.com/e3nn/e3nn/blob/0.4.0/e3nn/o3/_wigner.py#L10 + # jd is a list of tensors of shape (2l+1, 2l+1) + + # Borrowed from e3nn @ 0.4.0: + # https://github.com/e3nn/e3nn/blob/0.4.0/e3nn/o3/_wigner.py#L37 + # + # In 0.5.0, e3nn shifted to torch.matrix_exp which is significantly slower: + # https://github.com/e3nn/e3nn/blob/0.5.0/e3nn/o3/_wigner.py#L92 + """ + jd = None + with open("jd.pkl", "rb") as f: + jd = pickle.load(f) + if not lv < len(jd): + raise NotImplementedError( + f"wigner D maximum l implemented is {len(jd) - 1}, send us an email to ask for more" + ) + alpha, beta, gamma = broadcast_args(alpha, beta, gamma) + j = jd[lv] + xa = _z_rot_mat(alpha, lv) + xb = _z_rot_mat(beta, lv) + xc = _z_rot_mat(gamma, lv) + return xa @ j.astype(ms.float16) @ xb @ j.astype(ms.float16) @ xc + + +def _z_rot_mat(angle, lv): + shape = angle.shape + m = ops.zeros((shape[0], 2 * lv + 1, 2 * lv + 1)) + inds = ops.arange(0, 2 * lv + 1, 1) + reversed_inds = ops.arange(2 * lv, -1, -1) + frequencies = ops.arange(lv, -lv - 1, -1) + m[..., inds, reversed_inds] = ops.sin(frequencies * angle[..., None]) + m[..., inds, inds] = ops.cos(frequencies * angle[..., None]) + return m.astype(ms.float16) diff --git a/MindChem/applications/orb/mindchemistry/utils/__init__.py b/MindChem/applications/orb/mindchemistry/utils/__init__.py new file mode 100644 index 000000000..3f15063d7 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/utils/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this filepio[] except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""init""" +from .load_config import load_yaml_config + +__all__ = ['load_yaml_config'] diff --git a/MindChem/applications/orb/mindchemistry/utils/check_func.py b/MindChem/applications/orb/mindchemistry/utils/check_func.py new file mode 100644 index 000000000..711a441fe --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/utils/check_func.py @@ -0,0 +1,128 @@ +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""functions""" +from __future__ import absolute_import + +from mindspore import context + +_SPACE = " " + + +def _convert_to_tuple(params): + if params is None: + return params + if not isinstance(params, (list, tuple)): + params = (params,) + if isinstance(params, list): + params_out = tuple(params) + else: + params_out = params # ✅ 防止未定义 + return params_out + + +def check_param_type(param, param_name, data_type=None, exclude_type=None): + """Check parameter's data type""" + data_type = _convert_to_tuple(data_type) + exclude_type = _convert_to_tuple(exclude_type) + + if data_type and not isinstance(param, data_type): + raise TypeError( + f"The type of {param_name} should be instance of {data_type}, but got {param} with type {type(param)}" + ) + if exclude_type and type(param) in exclude_type: + raise TypeError( + f"The type of {param_name} should not be instance of {exclude_type},but got {param} with type {type(param)}" + ) + + +def check_param_value(param, param_name, valid_value): + """check parameter's value""" + valid_value = _convert_to_tuple(valid_value) + if param not in valid_value: + raise ValueError(f"The value of {param_name} should be in {valid_value}, but got {param}") + + +def check_param_type_value(param, param_name, valid_value, data_type=None, exclude_type=None): + """check both data type and value""" + check_param_type(param, param_name, data_type=data_type, exclude_type=exclude_type) + check_param_value(param, param_name, valid_value) + + +def check_dict_type(param_dict, param_name, key_type=None, value_type=None): + """check data type for key and value of the specified dict""" + check_param_type(param_dict, param_name, data_type=dict) + + for key in param_dict.keys(): + if key_type: + check_param_type(key, _SPACE.join(("key of", param_name)), data_type=key_type) + if value_type: + values = _convert_to_tuple(param_dict[key]) + for value in values: + check_param_type(value, _SPACE.join(("value of", param_name)), data_type=value_type) + + +def check_dict_value(param_dict, param_name, key_value=None, value_value=None): + """check values for key and value of specified dict""" + check_param_type(param_dict, param_name, data_type=dict) + + for key in param_dict.keys(): + if key_value: + check_param_value(key, _SPACE.join(("key of", param_name)), key_value) + if value_value: + values = _convert_to_tuple(param_dict[key]) + for value in values: + check_param_value(value, _SPACE.join(("value of", param_name)), value_value) + + +def check_dict_type_value(param_dict, param_name, key_type=None, value_type=None, key_value=None, value_value=None): + """check values for key and value of specified dict""" + check_dict_type(param_dict, param_name, key_type=key_type, value_type=value_type) + check_dict_value(param_dict, param_name, key_value=key_value, value_value=value_value) + + +def check_mode(api_name): + """check running mode""" + if context.get_context("mode") == context.PYNATIVE_MODE: + raise RuntimeError(f"{api_name} is only supported GRAPH_MODE now but got PYNATIVE_MODE") + + +def check_param_no_greater(param, param_name, compared_value): + """ Check whether the param less than the given compared_value""" + if param > compared_value: + raise ValueError(f"The value of {param_name} should be no greater than {compared_value}, but got {param}") + + +def check_param_odd(param, param_name): + """ Check whether the param is an odd number""" + if param % 2 == 0: + raise ValueError(f"The value of {param_name} should be an odd number, but got {param}") + + +def check_param_even(param, param_name): + """ Check whether the param is an even number""" + for value in param: + if value % 2 != 0: + raise ValueError(f"The value of {param_name} should be an even number, but got {param}") + + +def check_lr_param_type_value(param, param_name, param_type, thresh_hold=0, restrict=False, exclude=None): + if (exclude and isinstance(param, exclude)) or not isinstance(param, param_type): + raise TypeError(f"the type of {param_name} should be {param_type}, but got {type(param)}") + if restrict: + if param <= thresh_hold: + raise ValueError(f"the value of {param_name} should be > {thresh_hold}, but got: {param}") + else: + if param < thresh_hold: + raise ValueError(f"the value of {param_name} should be >= {thresh_hold}, but got: {param}") diff --git a/MindChem/applications/orb/mindchemistry/utils/load_config.py b/MindChem/applications/orb/mindchemistry/utils/load_config.py new file mode 100644 index 000000000..3ddc76e42 --- /dev/null +++ b/MindChem/applications/orb/mindchemistry/utils/load_config.py @@ -0,0 +1,85 @@ +# Copyright 2022 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +utility functions +""" +import os +import yaml + + +def _make_paths_absolute(dir_, config): + """ + Make all values for keys ending with `_path` absolute to dir_. + + Args: + dir_ (str): The path of yaml configuration file. + config (dict): The yaml for configuration file. + + Returns: + Dict. The configuration information in dict format. + """ + for key in config.keys(): + if key.endswith("_path"): + config[key] = os.path.join(dir_, config[key]) + config[key] = os.path.abspath(config[key]) + if isinstance(config[key], dict): + config[key] = _make_paths_absolute(dir_, config[key]) + return config + + +def load_yaml_config(file_path): + """ + Load a YAML configuration file. + + Args: + file_path (str): The path of yaml configuration file. + + Returns: + Dict. The configuration information in dict format. + + Supported Platforms: + ``Ascend`` ``CPU`` ``GPU`` + + Examples: + >>> from mindchemistry.utils import load_yaml_config + >>> config_file_path = 'xxx' # 'xxx' is the file_path + >>> configs = load_yaml_config(config_file_path) + """ + # Read YAML experiment definition file + with open(file_path, 'r', encoding='utf-8') as stream: + config = yaml.safe_load(stream) + config = _make_paths_absolute(os.path.join( + os.path.dirname(file_path), ".."), config) + return config + + +def load_yaml_config_from_path(file_path): + """ + Load a YAML configuration file. + + Args: + file_path (str): The path of yaml configuration file. + + Returns: + Dict. The configuration information in dict format. + + Supported Platforms: + ``Ascend`` ``CPU`` ``GPU`` + """ + # Read YAML experiment definition file + with open(file_path, 'r', encoding='utf-8') as stream: + config = yaml.safe_load(stream) + + return config diff --git a/MindChem/applications/orb/src/pretrained.py b/MindChem/applications/orb/src/pretrained.py index 7aad1c274..cb66db19f 100644 --- a/MindChem/applications/orb/src/pretrained.py +++ b/MindChem/applications/orb/src/pretrained.py @@ -20,7 +20,7 @@ from typing import Optional from mindspore import nn, load_checkpoint, load_param_into_net -from models import ( +from mindchemistry.cell import ( EnergyHead, GraphHead, Orb, diff --git a/MindSPONGE/README.md b/MindSPONGE/README.md index b328284ac..1bafafb6a 100644 --- a/MindSPONGE/README.md +++ b/MindSPONGE/README.md @@ -60,7 +60,7 @@ MindSpore SPONGE(Simulation Package tOwards Next GEneration molecular modelling) #### 端到端 -- 🔥AlphaFold3 [[Available]](./applications/AlphaFold3) +- 🔥AlphaFold3 [[Available]](https://gitee.com/mindspore/mindscience/tree/legacy-master/MindSPONGE/applications/research/AlphaFold3) - 🔥Protenix `In Progress` - 🔥RFdiffusion [[Available]](./applications/rf_diffusion) - Alphafold-Multimer [[Available]](https://gitee.com/mindspore/mindscience/blob/legacy-master/MindSPONGE/applications/model_cards/afmultimer.md) diff --git a/MindSPONGE/README_en.md b/MindSPONGE/README_en.md index 654127d17..3e3a9c7bd 100644 --- a/MindSPONGE/README_en.md +++ b/MindSPONGE/README_en.md @@ -59,7 +59,7 @@ MindSpore SPONGE (Simulation Package tOwards Next GEneration molecular modelling #### End2End -- 🔥AlphaFold3 [[Available]](./applications/AlphaFold3) +- 🔥AlphaFold3 [[Available]](https://gitee.com/mindspore/mindscience/tree/legacy-master/MindSPONGE/applications/research/AlphaFold3) - 🔥Protenix `In Progress` - 🔥RFdiffusion [[Available]](./applications/rf_diffusion) - Alphafold-Multimer [[Available]](https://gitee.com/mindspore/mindscience/blob/legacy-master/MindSPONGE/applications/model_cards/afmultimer.md) diff --git a/MindSPONGE/applications/AlphaFold3/CMakeLists.txt b/MindSPONGE/applications/AlphaFold3/CMakeLists.txt deleted file mode 100644 index 68f92c8f6..000000000 --- a/MindSPONGE/applications/AlphaFold3/CMakeLists.txt +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -cmake_minimum_required(VERSION 3.28) -project( - "${SKBUILD_PROJECT_NAME}" - LANGUAGES CXX - VERSION "${SKBUILD_PROJECT_VERSION}") - -include(FetchContent) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) -set(ABSL_PROPAGATE_CXX_STD ON) - -# Remove support for scan deps, which is only useful when using C++ modules. -unset(CMAKE_CXX_SCANDEP_SOURCE) - -FetchContent_Declare( - abseil-cpp - GIT_REPOSITORY https://github.com/abseil/abseil-cpp - GIT_TAG d7aaad83b488fd62bd51c81ecf16cd938532cc0a # 20240116.2 - EXCLUDE_FROM_ALL) - -FetchContent_Declare( - pybind11 - GIT_REPOSITORY https://github.com/pybind/pybind11 - GIT_TAG 2e0815278cb899b20870a67ca8205996ef47e70f # v2.12.0 - EXCLUDE_FROM_ALL) - -FetchContent_Declare( - pybind11_abseil - GIT_REPOSITORY https://github.com/pybind/pybind11_abseil - GIT_TAG bddf30141f9fec8e577f515313caec45f559d319 # HEAD @ 2024-08-07 - EXCLUDE_FROM_ALL) - -FetchContent_Declare( - cifpp - GIT_REPOSITORY https://github.com/pdb-redo/libcifpp - GIT_TAG ac98531a2fc8daf21131faa0c3d73766efa46180 # v7.0.3 - # Don't `EXCLUDE_FROM_ALL` as necessary for build_data. -) - -FetchContent_Declare( - dssp - GIT_REPOSITORY https://github.com/PDB-REDO/dssp - GIT_TAG 57560472b4260dc41f457706bc45fc6ef0bc0f10 # v4.4.7 - EXCLUDE_FROM_ALL) - -FetchContent_MakeAvailable(pybind11 abseil-cpp pybind11_abseil cifpp dssp) - -find_package( - Python3 - COMPONENTS Interpreter Development NumPy - REQUIRED) - -include_directories(${PYTHON_INCLUDE_DIRS}) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}) - -file(GLOB_RECURSE cpp_srcs alphafold3/*.cc) -list(FILTER cpp_srcs EXCLUDE REGEX ".*\(_test\|_main\|_benchmark\).cc$") - -add_compile_definitions(NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION) - -pybind11_add_module(cpp ${cpp_srcs}) - -target_link_libraries( - cpp - PRIVATE absl::check - absl::flat_hash_map - absl::node_hash_map - absl::strings - absl::status - absl::statusor - absl::log - pybind11_abseil::absl_casters - Python3::NumPy - dssp::dssp - cifpp::cifpp) - -target_compile_definitions(cpp PRIVATE VERSION_INFO=${PROJECT_VERSION}) -install(TARGETS cpp LIBRARY DESTINATION alphafold3) -install( - FILES LICENSE - OUTPUT_TERMS_OF_USE.md - WEIGHTS_PROHIBITED_USE_POLICY.md - WEIGHTS_TERMS_OF_USE.md - DESTINATION alphafold3) diff --git a/MindSPONGE/applications/AlphaFold3/LICENSE b/MindSPONGE/applications/AlphaFold3/LICENSE deleted file mode 100644 index bfef380bf..000000000 --- a/MindSPONGE/applications/AlphaFold3/LICENSE +++ /dev/null @@ -1,437 +0,0 @@ -Attribution-NonCommercial-ShareAlike 4.0 International - -======================================================================= - -Creative Commons Corporation ("Creative Commons") is not a law firm and -does not provide legal services or legal advice. Distribution of -Creative Commons public licenses does not create a lawyer-client or -other relationship. Creative Commons makes its licenses and related -information available on an "as-is" basis. Creative Commons gives no -warranties regarding its licenses, any material licensed under their -terms and conditions, or any related information. Creative Commons -disclaims all liability for damages resulting from their use to the -fullest extent possible. - -Using Creative Commons Public Licenses - -Creative Commons public licenses provide a standard set of terms and -conditions that creators and other rights holders may use to share -original works of authorship and other material subject to copyright -and certain other rights specified in the public license below. The -following considerations are for informational purposes only, are not -exhaustive, and do not form part of our licenses. - - Considerations for licensors: Our public licenses are - intended for use by those authorized to give the public - permission to use material in ways otherwise restricted by - copyright and certain other rights. Our licenses are - irrevocable. Licensors should read and understand the terms - and conditions of the license they choose before applying it. - Licensors should also secure all rights necessary before - applying our licenses so that the public can reuse the - material as expected. Licensors should clearly mark any - material not subject to the license. This includes other CC- - licensed material, or material used under an exception or - limitation to copyright. More considerations for licensors: - wiki.creativecommons.org/Considerations_for_licensors - - Considerations for the public: By using one of our public - licenses, a licensor grants the public permission to use the - licensed material under specified terms and conditions. If - the licensor's permission is not necessary for any reason--for - example, because of any applicable exception or limitation to - copyright--then that use is not regulated by the license. Our - licenses grant only permissions under copyright and certain - other rights that a licensor has authority to grant. Use of - the licensed material may still be restricted for other - reasons, including because others have copyright or other - rights in the material. A licensor may make special requests, - such as asking that all changes be marked or described. - Although not required by our licenses, you are encouraged to - respect those requests where reasonable. More considerations - for the public: - wiki.creativecommons.org/Considerations_for_licensees - -======================================================================= - -Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International -Public License - -By exercising the Licensed Rights (defined below), You accept and agree -to be bound by the terms and conditions of this Creative Commons -Attribution-NonCommercial-ShareAlike 4.0 International Public License -("Public License"). To the extent this Public License may be -interpreted as a contract, You are granted the Licensed Rights in -consideration of Your acceptance of these terms and conditions, and the -Licensor grants You such rights in consideration of benefits the -Licensor receives from making the Licensed Material available under -these terms and conditions. - - -Section 1 -- Definitions. - - a. Adapted Material means material subject to Copyright and Similar - Rights that is derived from or based upon the Licensed Material - and in which the Licensed Material is translated, altered, - arranged, transformed, or otherwise modified in a manner requiring - permission under the Copyright and Similar Rights held by the - Licensor. For purposes of this Public License, where the Licensed - Material is a musical work, performance, or sound recording, - Adapted Material is always produced where the Licensed Material is - synched in timed relation with a moving image. - - b. Adapter's License means the license You apply to Your Copyright - and Similar Rights in Your contributions to Adapted Material in - accordance with the terms and conditions of this Public License. - - c. BY-NC-SA Compatible License means a license listed at - creativecommons.org/compatiblelicenses, approved by Creative - Commons as essentially the equivalent of this Public License. - - d. Copyright and Similar Rights means copyright and/or similar rights - closely related to copyright including, without limitation, - performance, broadcast, sound recording, and Sui Generis Database - Rights, without regard to how the rights are labeled or - categorized. For purposes of this Public License, the rights - specified in Section 2(b)(1)-(2) are not Copyright and Similar - Rights. - - e. Effective Technological Measures means those measures that, in the - absence of proper authority, may not be circumvented under laws - fulfilling obligations under Article 11 of the WIPO Copyright - Treaty adopted on December 20, 1996, and/or similar international - agreements. - - f. Exceptions and Limitations means fair use, fair dealing, and/or - any other exception or limitation to Copyright and Similar Rights - that applies to Your use of the Licensed Material. - - g. License Elements means the license attributes listed in the name - of a Creative Commons Public License. The License Elements of this - Public License are Attribution, NonCommercial, and ShareAlike. - - h. Licensed Material means the artistic or literary work, database, - or other material to which the Licensor applied this Public - License. - - i. Licensed Rights means the rights granted to You subject to the - terms and conditions of this Public License, which are limited to - all Copyright and Similar Rights that apply to Your use of the - Licensed Material and that the Licensor has authority to license. - - j. Licensor means the individual(s) or entity(ies) granting rights - under this Public License. - - k. NonCommercial means not primarily intended for or directed towards - commercial advantage or monetary compensation. For purposes of - this Public License, the exchange of the Licensed Material for - other material subject to Copyright and Similar Rights by digital - file-sharing or similar means is NonCommercial provided there is - no payment of monetary compensation in connection with the - exchange. - - l. Share means to provide material to the public by any means or - process that requires permission under the Licensed Rights, such - as reproduction, public display, public performance, distribution, - dissemination, communication, or importation, and to make material - available to the public including in ways that members of the - public may access the material from a place and at a time - individually chosen by them. - - m. Sui Generis Database Rights means rights other than copyright - resulting from Directive 96/9/EC of the European Parliament and of - the Council of 11 March 1996 on the legal protection of databases, - as amended and/or succeeded, as well as other essentially - equivalent rights anywhere in the world. - - n. You means the individual or entity exercising the Licensed Rights - under this Public License. Your has a corresponding meaning. - - -Section 2 -- Scope. - - a. License grant. - - 1. Subject to the terms and conditions of this Public License, - the Licensor hereby grants You a worldwide, royalty-free, - non-sublicensable, non-exclusive, irrevocable license to - exercise the Licensed Rights in the Licensed Material to: - - a. reproduce and Share the Licensed Material, in whole or - in part, for NonCommercial purposes only; and - - b. produce, reproduce, and Share Adapted Material for - NonCommercial purposes only. - - 2. Exceptions and Limitations. For the avoidance of doubt, where - Exceptions and Limitations apply to Your use, this Public - License does not apply, and You do not need to comply with - its terms and conditions. - - 3. Term. The term of this Public License is specified in Section - 6(a). - - 4. Media and formats; technical modifications allowed. The - Licensor authorizes You to exercise the Licensed Rights in - all media and formats whether now known or hereafter created, - and to make technical modifications necessary to do so. The - Licensor waives and/or agrees not to assert any right or - authority to forbid You from making technical modifications - necessary to exercise the Licensed Rights, including - technical modifications necessary to circumvent Effective - Technological Measures. For purposes of this Public License, - simply making modifications authorized by this Section 2(a) - (4) never produces Adapted Material. - - 5. Downstream recipients. - - a. Offer from the Licensor -- Licensed Material. Every - recipient of the Licensed Material automatically - receives an offer from the Licensor to exercise the - Licensed Rights under the terms and conditions of this - Public License. - - b. Additional offer from the Licensor -- Adapted Material. - Every recipient of Adapted Material from You - automatically receives an offer from the Licensor to - exercise the Licensed Rights in the Adapted Material - under the conditions of the Adapter's License You apply. - - c. No downstream restrictions. You may not offer or impose - any additional or different terms or conditions on, or - apply any Effective Technological Measures to, the - Licensed Material if doing so restricts exercise of the - Licensed Rights by any recipient of the Licensed - Material. - - 6. No endorsement. Nothing in this Public License constitutes or - may be construed as permission to assert or imply that You - are, or that Your use of the Licensed Material is, connected - with, or sponsored, endorsed, or granted official status by, - the Licensor or others designated to receive attribution as - provided in Section 3(a)(1)(A)(i). - - b. Other rights. - - 1. Moral rights, such as the right of integrity, are not - licensed under this Public License, nor are publicity, - privacy, and/or other similar personality rights; however, to - the extent possible, the Licensor waives and/or agrees not to - assert any such rights held by the Licensor to the limited - extent necessary to allow You to exercise the Licensed - Rights, but not otherwise. - - 2. Patent and trademark rights are not licensed under this - Public License. - - 3. To the extent possible, the Licensor waives any right to - collect royalties from You for the exercise of the Licensed - Rights, whether directly or through a collecting society - under any voluntary or waivable statutory or compulsory - licensing scheme. In all other cases the Licensor expressly - reserves any right to collect such royalties, including when - the Licensed Material is used other than for NonCommercial - purposes. - - -Section 3 -- License Conditions. - -Your exercise of the Licensed Rights is expressly made subject to the -following conditions. - - a. Attribution. - - 1. If You Share the Licensed Material (including in modified - form), You must: - - a. retain the following if it is supplied by the Licensor - with the Licensed Material: - - i. identification of the creator(s) of the Licensed - Material and any others designated to receive - attribution, in any reasonable manner requested by - the Licensor (including by pseudonym if - designated); - - ii. a copyright notice; - - iii. a notice that refers to this Public License; - - iv. a notice that refers to the disclaimer of - warranties; - - v. a URI or hyperlink to the Licensed Material to the - extent reasonably practicable; - - b. indicate if You modified the Licensed Material and - retain an indication of any previous modifications; and - - c. indicate the Licensed Material is licensed under this - Public License, and include the text of, or the URI or - hyperlink to, this Public License. - - 2. You may satisfy the conditions in Section 3(a)(1) in any - reasonable manner based on the medium, means, and context in - which You Share the Licensed Material. For example, it may be - reasonable to satisfy the conditions by providing a URI or - hyperlink to a resource that includes the required - information. - 3. If requested by the Licensor, You must remove any of the - information required by Section 3(a)(1)(A) to the extent - reasonably practicable. - - b. ShareAlike. - - In addition to the conditions in Section 3(a), if You Share - Adapted Material You produce, the following conditions also apply. - - 1. The Adapter's License You apply must be a Creative Commons - license with the same License Elements, this version or - later, or a BY-NC-SA Compatible License. - - 2. You must include the text of, or the URI or hyperlink to, the - Adapter's License You apply. You may satisfy this condition - in any reasonable manner based on the medium, means, and - context in which You Share Adapted Material. - - 3. You may not offer or impose any additional or different terms - or conditions on, or apply any Effective Technological - Measures to, Adapted Material that restrict exercise of the - rights granted under the Adapter's License You apply. - - -Section 4 -- Sui Generis Database Rights. - -Where the Licensed Rights include Sui Generis Database Rights that -apply to Your use of the Licensed Material: - - a. for the avoidance of doubt, Section 2(a)(1) grants You the right - to extract, reuse, reproduce, and Share all or a substantial - portion of the contents of the database for NonCommercial purposes - only; - - b. if You include all or a substantial portion of the database - contents in a database in which You have Sui Generis Database - Rights, then the database in which You have Sui Generis Database - Rights (but not its individual contents) is Adapted Material, - including for purposes of Section 3(b); and - - c. You must comply with the conditions in Section 3(a) if You Share - all or a substantial portion of the contents of the database. - -For the avoidance of doubt, this Section 4 supplements and does not -replace Your obligations under this Public License where the Licensed -Rights include other Copyright and Similar Rights. - - -Section 5 -- Disclaimer of Warranties and Limitation of Liability. - - a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE - EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS - AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF - ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, - IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, - WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR - PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, - ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT - KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT - ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. - - b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE - TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, - NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, - INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, - COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR - USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN - ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR - DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR - IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. - - c. The disclaimer of warranties and limitation of liability provided - above shall be interpreted in a manner that, to the extent - possible, most closely approximates an absolute disclaimer and - waiver of all liability. - - -Section 6 -- Term and Termination. - - a. This Public License applies for the term of the Copyright and - Similar Rights licensed here. However, if You fail to comply with - this Public License, then Your rights under this Public License - terminate automatically. - - b. Where Your right to use the Licensed Material has terminated under - Section 6(a), it reinstates: - - 1. automatically as of the date the violation is cured, provided - it is cured within 30 days of Your discovery of the - violation; or - - 2. upon express reinstatement by the Licensor. - - For the avoidance of doubt, this Section 6(b) does not affect any - right the Licensor may have to seek remedies for Your violations - of this Public License. - - c. For the avoidance of doubt, the Licensor may also offer the - Licensed Material under separate terms or conditions or stop - distributing the Licensed Material at any time; however, doing so - will not terminate this Public License. - - d. Sections 1, 5, 6, 7, and 8 survive termination of this Public - License. - - -Section 7 -- Other Terms and Conditions. - - a. The Licensor shall not be bound by any additional or different - terms or conditions communicated by You unless expressly agreed. - - b. Any arrangements, understandings, or agreements regarding the - Licensed Material not stated herein are separate from and - independent of the terms and conditions of this Public License. - - -Section 8 -- Interpretation. - - a. For the avoidance of doubt, this Public License does not, and - shall not be interpreted to, reduce, limit, restrict, or impose - conditions on any use of the Licensed Material that could lawfully - be made without permission under this Public License. - - b. To the extent possible, if any provision of this Public License is - deemed unenforceable, it shall be automatically reformed to the - minimum extent necessary to make it enforceable. If the provision - cannot be reformed, it shall be severed from this Public License - without affecting the enforceability of the remaining terms and - conditions. - - c. No term or condition of this Public License will be waived and no - failure to comply consented to unless expressly agreed to by the - Licensor. - - d. Nothing in this Public License constitutes or may be interpreted - as a limitation upon, or waiver of, any privileges and immunities - that apply to the Licensor or You, including from the legal - processes of any jurisdiction or authority. - -======================================================================= - -Creative Commons is not a party to its public -licenses. Notwithstanding, Creative Commons may elect to apply one of -its public licenses to material it publishes and in those instances -will be considered the “Licensor.” The text of the Creative Commons -public licenses is dedicated to the public domain under the CC0 Public -Domain Dedication. Except for the limited purpose of indicating that -material is shared under a Creative Commons public license or as -otherwise permitted by the Creative Commons policies published at -creativecommons.org/policies, Creative Commons does not authorize the -use of the trademark "Creative Commons" or any other trademark or logo -of Creative Commons without its prior written consent including, -without limitation, in connection with any unauthorized modifications -to any of its public licenses or any other arrangements, -understandings, or agreements concerning use of licensed material. For -the avoidance of doubt, this paragraph does not form part of the -public licenses. - -Creative Commons may be contacted at creativecommons.org. \ No newline at end of file diff --git a/MindSPONGE/applications/AlphaFold3/README.md b/MindSPONGE/applications/AlphaFold3/README.md deleted file mode 100644 index 20fdfd7ee..000000000 --- a/MindSPONGE/applications/AlphaFold3/README.md +++ /dev/null @@ -1,267 +0,0 @@ -# AlphaFold3-MindSpore - -[**MindSpore版 AlphaFold3实现**] 一个基于MindSpore深度学习框架的AlphaFold3推理网络结构实现。 - -> 📖 **语言版本**: [中文](README.md) | [English](README_EN.md) - -## 📑 目录 - -- [项目简介](#项目简介) -- [安装](#安装) -- [快速开始](#快速开始) -- [详细使用说明](#详细使用说明) -- [许可证](#许可证) -- [致谢](#致谢) -- [参考文献](#参考文献) - -## 项目简介 - -**项目背景**: -AlphaFold3是DeepMind在2024年发布的革命性生物分子结构预测模型,能够预测蛋白质、DNA、RNA等生物大分子的三维结构。本项目基于Ascend NPU和MindSpore框架,实现了AlphaFold3的推理功能。 - -AlphaFold3 的模型结构如下图所示: - -![AlphaFold3 模型结构](image/af3_structure.jpg) - -- **推理流程**:首先输入的蛋白,核酸,配体等序列信息,经过模板搜索(Template Search)、多序列比对(Multiple Sequence Alignment, MSA)等预处理步骤,然后通过embeding部分对输入信息进行编码,之后通过Pairformer模块,获取序列及结构的关系,接着进入扩散模块生成三维结构,最后通过置信度模块给出预测的置信度评分 -- **生物分子结构预测**: 基于AlphaFold3算法的生物分子结构预测模型,支持包括蛋白质,DNA,RNA,小分子在内的多种输入形式;支持多链输入,预测相互作用和相对位置 -- **MindSpore支持**: 基于MindSpore对模型推理功能进行适配 - -### 硬件要求 - -- Atlas 800T A2 - -### 软件要求 - -- Python >= 3.11 -- MindSpore >= 2.5.0 -- CANN >= 8.0.0 -- cmake >= 3.28.1 - -## 安装 - -### 1. 克隆仓库 - -```bash -git clone https://gitee.com/mindspore/mindscience -cd mindsience/MindSPONGE/application/AlphaFold3 -``` - -### 2. 安装依赖 - -```bash -pip install -r requirements.txt -#`{PATH}` 为当前目录 -export PYTHONPATH={PATH}/mindscience -``` - -### 3. 安装软件包 - -[hmmer](http://eddylab.org/software/hmmer/) 在链接处下载安装包,如 `hmmer-3.4.tar.gz`,并放置在当前目录下,然后执行以下命令: - -```bash -mkdir /path/to/hmmer_build /path/to/hmmer && \ -mv ./hmmer-3.4.tar.gz /path/to/hmmer_build && \ -cd /path/to/hmmer_build && tar -zxf hmmer-3.4.tar.gz && rm hmmer-3.4.tar.gz && \ -cd /path/to/hmmer_build/hmmer-3.4 && ./configure --prefix=/path/to/hmmer && \ -make -j8 && make install && \ -cd /path/to/hmmer_build/hmmer-3.4/easel && make install && \ -rm -rf /path/to/hmmer_build -export PATH=/hmmer/bin:$PATH -which jackhmmer -``` - -如果出现`/path/to/hmmer/bin/jackhmmer`则安装成功 - -### 4. 编译 - -```bash -cd {PATH}/mindscience/MindSPONGE/applications/AlphaFold3 -mkdir build -cd build -cmake .. -make -cp ./cpp.cpython-311-aarch64-linux-gnu.so ../alphafold -cd .. -``` - -生成数据文件: - -```bash -python ./alphafold3/build_data.py -``` - -如出现报错找不到components.cif,可以去[wwpdb](https://files.wwpdb.org/pub/pdb/data/monomers/components.cif)下载components.cif文件,放置在conda环境中的`{CONDA_ENV_DIR}/lib/python3.11/site-packages/share/libcifpp`文件夹下。如不存在`share/libcifpp`文件夹,则需要手动创建。 - -下载随机数文件: - -```bash -cd ./alphafold3/model/diffusion -mkdir random -cd random -wget https://tools.mindspore.cn/dataset/workspace/mindspore_dataset/mindsponge_data/alphafold3/bias.npy -wget https://tools.mindspore.cn/dataset/workspace/mindspore_dataset/mindsponge_data/alphafold3/weight.npy -``` - -### 5. 下载数据库 - -可以从DeepMind官网下载测试用小数据库[miniature_databases](https://github.com/google-deepmind/alphafold3/tree/main/src/alphafold3/test_data/miniature_databases)(影响推理结果,仅测试使用!) -下载后放置在统一文件夹中并修改文件名如下所示(如统一放置在`/mindscience/MindSPONGE/applications/AlphaFold3/public_databases`可省略`--db_dir=/PATH/TO/DB_DIR`): - -```txt -miniature_databases - └─ mmcif_files - │ bfd-first_non_consensus_sequences.fasta - │ mgy_clusters_2022_05.fa - │ pdb_seqres_2022_09_28.fasta - │ uniprot_all_2021_04.fa - │ uniref90_2022_05.fa - │ nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta - │ rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta - │ rnacentral_active_seq_id_90_cov_80_linclust.fasta -``` - -如果想要搜索完整的数据库,请从以下链接下载数据库,放置到同一文件夹中(如统一放置在`/mindscience/MindSPONGE/applications/AlphaFold3/public_databases`可省略`--db_dir=/PATH/TO/DB_DIR`): - -- [mmcif](https://storage.googleapis.com/alphafold-databases/v3.0/pdb_2022_09_28_mmcif_files.tar.zst) -- [BFD](https://storage.googleapis.com/alphafold-databases/v3.0/bfd-first_non_consensus_sequences.fasta.zst) -- [MGnify](https://storage.googleapis.com/alphafold-databases/v3.0/mgy_clusters_2022_05.fa.zst) -- [PDB seqres](https://storage.googleapis.com/alphafold-databases/v3.0/pdb_seqres_2022_09_28.fasta.zst) -- [UniProt](https://storage.googleapis.com/alphafold-databases/v3.0/uniprot_all_2021_04.fa.zst) -- [uniref90](https://storage.googleapis.com/alphafold-databases/v3.0/uniref90_2022_05.fa.zst) -- [NT](https://storage.googleapis.com/alphafold-databases/v3.0/nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta.zst) -- [RFam](https://storage.googleapis.com/alphafold-databases/v3.0/rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta.zst) -- [RNACentral](https://storage.googleapis.com/alphafold-databases/v3.0/rnacentral_active_seq_id_90_cov_80_linclust.fasta.zst) - -请确保磁盘中有足够空间: -| DataBase | Compressed Size | Uncompressed Size| -|--------------|---------------------|------------------| -| mmcif | 233G | 233G | -| BFD | 9.2G | 16.9G | -| MGnify | 64.5G | 119G | -| PDB seqres| 25.3M | 217M | -| UniProt | 45.3G | 101G | -| uniref90 | 30.9G | 66.8G | -| NT | 15.8G | 75.4G | -| RFam | 53.9M | 217M | -| RNACentral| 3.27G | 12.9G | -| total | 402G | 534G | - -解压下载的数据文件: - -```bash -cd /PATH/TO/YOUR/DATA_DIR -tar –use-compress-program=unzstd -xf pdb_2022_09_28_mmcif_files.tar.zst -zstd -d bfd-first_non_consensus_sequences.fasta.zst -zstd -d mgy_clusters_2022_05.fa.zst -zstd -d pdb_seqres_2022_09_28.fasta.zst -zstd -d uniprot_all_2021_04.fa.zst -zstd -d uniref90_2022_05.fa.zst -zstd -d nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta.zst -zstd -d rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta.zst -zstd -d rnacentral_active_seq_id_90_cov_80_linclust.fasta.zst -``` - -如统一放置在`/mindscience/MindSPONGE/applications/AlphaFold3/public_databases`可在运行时省略`--db_dir=/PATH/TO/DB_DIR` - -## 快速开始 - -### 输入数据格式 - -示例输入JSON: - -```json -{ - "name": "5tgy", - "sequences": [ - { - "protein": { - "id": "A", - "sequence": "SEFEKLRQTGDELVQAFQRLREIFDKGDDDSLEQVLEEIEELIQKHRQLFDNRQEAADTEAAKQGDQWVQLFQRFREAIDKGDKDSLEQLLEELEQALQKIRELAEKKN" - } - } - ], - "modelSeeds": [1], - "dialect": "alphafold3", - "version": 1 -} -``` - -### 运行流程 - -使用以下命令运行模型(计算精度float32): - -```bash -source set_path.sh -python run_alphafold.py \ - --json_path=example_input.json \ - --output_dir=output \ - --run_data_pipeline=true \ - --run_inference=true \ - --db_dir=/PATH/TO/DB_DIR \ - --model_dir=/PATH/TO/MODEL_DIR\ - --buckets=256 -``` - -### 参数说明 - -- `--json_path`输入文件名称 -- `--output_dir`: 输出文件路径 -- `--run_data_pipeline`: 是否运行数据处理模块 -- `--run_inference`: 是否运行推理模块 -- `--db_dir`: 数据库存放路径, 默认 `{HOME}/public_databases` -- `--model_dir`: 模型文件路径, 默认 `{HOME}/ckpt` -- `--buckets`: 设定序列长度,如不设置会将序列长度padding到256的倍数,如传入则使用传入值作为序列长度 - -### 输入与输出文件说明 - -- **JSON格式数据输入**: 包含蛋白质核酸等的序列信息。当前支持输入种类与DeepMind版本相同,支持蛋白质,DNA,RNA及Ligand作为输入,当前推理版本为单卡版本支持序列长度不超过1000 - -- **输出文件**: 5个标准的蛋白质结构文件,及置信度信息 - -```txt -└─name_in_your_json - └─ seed-1_sample-0 # 第一个生成样本 - │ confidence.json # 第一个样本的详细置信度文件 - │ model.cif # 第一个样本的结构文件 - │ summary_confidence.json # 第一个样本的总体置信度文件 - └─ seed-1_sample-1 # 第二个生成样本 - └─ seed-1_sample-2 # 第三个生成样本 - └─ seed-1_sample-3 # 第四个生成样本 - └─ seed-1_sample-4 # 第五个生成样本 - │ {name}_confidences.json # 最优样本的详细置信度文件 - │ {name}_data.json # 数据处理后的数据文件 - │ {name}_model.cif # 最优样本的结构文件 - │ {name}_summary_confidence.json # 最优样本的总体置信度文件 - │ ranking_scores.csv # 五个样本的ranking score;ranking score越高,表明置信度越高 -``` - -### 推理完成 - -当看到如下日志,表明推理正常结束: - -```txt -=======write output to /PATH/TO/OUTPUT/DIR/name_of_your_input========== -Done processing fold input name_of_your_input. -Done processing 1 fold inputs. -``` - -## 许可证 - -详情请参阅 [LICENSE](LICENSE) 文件。 - -## 致谢 - -- `data`,`structure`,`common`,`constant`等模块使用了[DeepMind](https://deepmind.com/)实现 -- `model`,`utils`等模块基于[MindSpore](https://www.mindspore.cn/)实现 - -## 联系我们 - -如果您在使用过程中遇到任何问题或有任何建议,请通过以下方式与我们联系: - -- **Gitee仓库**:[AlphaFold3](https://gitee.com/mindspore/mindscience/tree/main/MindSPONGE/applications/AlphaFold3) -- **问题跟踪**:[问题单跟踪](https://gitee.com/mindspore/mindscience/issues) - -## 参考文献 - -- Abramson J, Adler J, Dunger J, et al. Accurate structure prediction of biomolecular interactions with AlphaFold 3[J]. Nature, 2024, 630(8016): 493-500. diff --git a/MindSPONGE/applications/AlphaFold3/README_EN.md b/MindSPONGE/applications/AlphaFold3/README_EN.md deleted file mode 100644 index 4c4f61135..000000000 --- a/MindSPONGE/applications/AlphaFold3/README_EN.md +++ /dev/null @@ -1,268 +0,0 @@ -# AlphaFold3-MindSpore - -[**MindSpore Implementation of AlphaFold3**] A MindSpore-based deep learning framework implementation of AlphaFold3 inference network architecture. - -> 📖 **Language**: [中文](README.md) | [English](README_EN.md) - -## 📑 Table of Contents - -- [Project Overview](#project-overview) -- [Installation](#installation) -- [Quick Start](#quick-start) -- [License](#license) -- [Acknowledgments](#acknowledgments) -- [Reference](#reference) - -## Project Overview - -**Project Background**: -AlphaFold3 is a revolutionary biomolecular structure prediction model released by DeepMind in 2024, capable of predicting the three-dimensional structures of proteins, DNA, RNA, and other biological macromolecules. This project implements AlphaFold3's inference functionality based on Ascend NPU and MindSpore framework. - -The model architecture is shown below: - -![AlphaFold3 Model Structure](image/af3_structure.jpg) - -- **Inference Pipeline**:The workflow begins with the provision of sequence information for proteins, DNA, RNA, and ligands. This data undergoes preprocessing steps, including template search and multiple sequence alignment, before being fed into the model. Next, an embedding module encodes the input information. Subsequently, the Pairformer cycles analyze the relationships between the sequences and their structures. Following this, a diffusion module generates the 3D structures. Finally, a confidence module assigns a confidence score to the predictions, providing a measure of their reliability. -- **Biomolecular Structure Prediction**: A biomolecular structure prediction model based on the AlphaFold3 algorithm, supporting various input forms including proteins, DNA, RNA, and small molecules; enabling multi-chain inputs and predicting interactions and relative positions. -- **MindSpore Support**: Model Inference adaptation based on MindSpore. - -### Hardware Requirements - -- Atlas 800T A2 - -### Software Requirements - -- Python >= 3.11 -- MindSpore >= 2.5.0 -- CANN >= 8.0.0 -- cmake >= 3.28.1 - -## Installation - -### 1. Clone Repository - -```bash -git clone https://gitee.com/mindspore/mindscience -cd mindsience/MindSPONGE/application/AlphaFold3 -``` - -### 2. Install Dependencies - -```bash -pip install -r requirements.txt -#`{PATH}` is the current path -export PYTHONPATH={PATH}/mindscience -``` - -### 3. Installing the Software Package - -Download the installation package from the link [hmmer](http://eddylab.org/software/hmmer/) , such as hmmer-3.4.tar.gz, and place it in the current directory. - -```bash -mkdir /path/to/hmmer_build /path/to/hmmer && \ -mv ./hmmer-3.4.tar.gz /path/to/hmmer_build && \ -cd /path/to/hmmer_build && tar -zxf hmmer-3.4.tar.gz && rm hmmer-3.4.tar.gz && \ -cd /path/to/hmmer_build/hmmer-3.4 && ./configure --prefix=/path/to/hmmer && \ -make -j8 && make install && \ -cd /path/to/hmmer_build/hmmer-3.4/easel && make install && \ -rm -rf /path/to/hmmer_build -export PATH=/hmmer/bin:$PATH -which jackhmmer -``` - -If the file `/path/to/hmmer/bin/jackhmmer` appears, the installation is successful. - -### 4. Compile - -```bash -cd {PATH}/mindscience/MindSPONGE/applications/AlphaFold3 -mkdir build -cd build -cmake .. -make -cp ./cpp.cpython-311-aarch64-linux-gnu.so ../alphafold -cd .. -``` - -Then, we need to generate data file: - -```bash -python ./alphafold3/build_data.py -``` - -if you see the error 'counld not find components.cif', download the file from [wwpdb](https://files.wwpdb.org/pub/pdb/data/monomers/components.cif),then put this file in your conda environment, `{CONDA_ENV_DIR}/lib/python3.11/site-packages/share/libcifpp`. If there is no `share/libcifpp` directory, create the directory by yourself. - -Download random number files: - -```bash -cd ./alphafold3/model/diffusion -mkdir random -cd random -wget https://tools.mindspore.cn/dataset/workspace/mindspore_dataset/mindsponge_data/alphafold3/bias.npy -wget https://tools.mindspore.cn/dataset/workspace/mindspore_dataset/mindsponge_data/alphafold3/weight.npy -cd ../../../.. -``` - -### 5. Download DataBase - -You can download a small test database from DeepMind [miniature_databases](https://github.com/google-deepmind/alphafold3/tree/main/src/alphafold3/test_data/miniature_databases)(Only for test,have influence to inference result!) -Download and put all the files in the same direction (No need to set `--db_dir=/PATH/TO/DB_DIR` if all the database are put in `/mindscience/MindSPONGE/applications/AlphaFold3/public_databases`) and rename the file like the example below: - -```txt -miniature_databases - └─ mmcif_files - │ bfd-first_non_consensus_sequences.fasta - │ mgy_clusters_2022_05.fa - │ pdb_seqres_2022_09_28.fasta - │ uniprot_all_2021_04.fa - │ uniref90_2022_05.fa - │ nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta - │ rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta - │ rnacentral_active_seq_id_90_cov_80_linclust.fasta -``` - -If you want to seearch the full database, download the following database, and put them in the same direction(No need to set `--db_dir=/PATH/TO/DB_DIR` if all the database are put in `/mindscience/MindSPONGE/applications/AlphaFold3/public_databases`): - -- [mmcif](https://storage.googleapis.com/alphafold-databases/v3.0/pdb_2022_09_28_mmcif_files.tar.zst) -- [BFD small](https://storage.googleapis.com/alphafold-databases/v3.0/bfd-first_non_consensus_sequences.fasta.zst) -- [MGnify](https://storage.googleapis.com/alphafold-databases/v3.0/mgy_clusters_2022_05.fa.zst) -- [PDB seqres](https://storage.googleapis.com/alphafold-databases/v3.0/pdb_seqres_2022_09_28.fasta.zst) -- [UniProt](https://storage.googleapis.com/alphafold-databases/v3.0/uniprot_all_2021_04.fa.zst) -- [uniref90](https://storage.googleapis.com/alphafold-databases/v3.0/uniref90_2022_05.fa.zst) -- [NT](https://storage.googleapis.com/alphafold-databases/v3.0/nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta.zst) -- [RFam](https://storage.googleapis.com/alphafold-databases/v3.0/rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta.zst) -- [RNACentral](https://storage.googleapis.com/alphafold-databases/v3.0/rnacentral_active_seq_id_90_cov_80_linclust.fasta.zst) - -Make sure having enough space on disk: - -| DataBase | Compressed Size | Uncompressed Size| -|--------------|---------------------|------------------| -| mmcif | 233G | 233G | -| BFD | 9.2G | 16.9G | -| MGnify | 64.5G | 119G | -| PDB seqres| 25.3M | 217M | -| UniProt | 45.3G | 101G | -| uniref90 | 30.9G | 66.8G | -| NT | 15.8G | 75.4G | -| RFam | 53.9M | 217M | -| RNACentral| 3.27G | 12.9G | -| total | 402G | 534G | - -Uncompressing the following database file: - -```bash -cd /PATH/TO/YOUR/DATA_DIR -tar –use-compress-program=unzstd -xf pdb_2022_09_28_mmcif_files.tar.zst -zstd -d bfd-first_non_consensus_sequences.fasta.zst -zstd -d mgy_clusters_2022_05.fa.zst -zstd -d pdb_seqres_2022_09_28.fasta.zst -zstd -d uniprot_all_2021_04.fa.zst -zstd -d uniref90_2022_05.fa.zst -zstd -d nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta.zst -zstd -d rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta.zst -zstd -d rnacentral_active_seq_id_90_cov_80_linclust.fasta.zst -``` - -If all the files are put under`/mindscience/MindSPONGE/applications/AlphaFold3/public_databases`, the setting `--db_dir=/PATH/TO/DB_DIR` can be ignored. - -## Quick Start - -### Input Structure - -Example Input JSON: - -```json -{ - "name": "5tgy", - "sequences": [ - { - "protein": { - "id": "A", - "sequence": "SEFEKLRQTGDELVQAFQRLREIFDKGDDDSLEQVLEEIEELIQKHRQLFDNRQEAADTEAAKQGDQWVQLFQRFREAIDKGDKDSLEQLLEELEQALQKIRELAEKKN" - } - } - ], - "modelSeeds": [1], - "dialect": "alphafold3", - "version": 1 -} -``` - -### Running Pipeline - -AlphaFold3 can be run with the following command(Precision: float32). - -```bash -source set_path.sh -python run_alphafold.py \ - --json_path=example_input.json \ - --output_dir=output \ - --run_data_pipeline=true \ - --run_inference=true \ - --db_dir=/PATH/TO/DB_DIR \ - --model_dir=/PATH/TO/MODEL_DIR \ - --buckets=256 -``` - -### Parameter Introduction - -- `--json_path`: Name of input json -- `--output_dir`: Output direction -- `--run_data_pipeline`: run data-pipeline or not -- `--run_inference`: run inference or not -- `--db_dir`: path to database, default `{HOME}/public_databases` -- `--model_dir`: Path to ckpt, default `{HOME}/ckpt` -- `--buckets`: Setting the sequence length,Default:padding to N * 256 - -### Input & Output - -- **JSON Input**: Contains sequence information of proteins and other molecules. Support the following types of input (same as DeepMind version): Protein, DNA, RNA, Ligand, etc. Currently, only single NPU version and the max sequence length should be smaller than 1000. - -- **CIF Output**: 5 Standard protein structure files and confidence info. - -```txt -└─name_in_your_json - └─ seed-{random_seed}_sample-0 # First Sample - │ confidence.json # Confidence of the first sample - │ model.cif # Predicted structure of the first sample - │ summary_confidence.json # Summary confidence of the first sample - └─ seed-{random_seed}_sample-1 # Second Sample - └─ seed-{random_seed}_sample-2 # Third Sample - └─ seed-{random_seed}_sample-3 # Forth Sample - └─ seed-{random_seed}_sample-4 # Fifth Sample - │ {name}_confidences.json # Confidence of the best sample - │ {name}_data.json # Data json file after data-processing - │ {name}_model.cif # Predicted structure of the best sample - │ {name}_summary_confidence.json # Summary confidence of the best sample - │ ranking_scores.csv # Ranking Score of all five samples, the higher of the ranking score, the higher of the confidence of the sample -``` - -### End of Inference - -When you see the following log,the inference finished correctly: - -```text -=======write output to /PATH/TO/OUTPUT/DIR/name_of_your_input========== -Done processing fold input name_of_your_input. -Done processing 1 fold inputs. -``` - -## License - -See the [LICENSE](LICENSE) file for details. - -## Acknowledgments - -- The implementation of Modules including: data,structure,common, constant refers to [DeepMind](https://github.com/google-deepmind/alphafold3). -- The implementation of Modules including: model,utils are based on [MindScience](https://gitee.com/mindspore/mindscience/) - -## COntact Us - -If you encounter any issues or have any suggestions during use, please contact us through the following methods: - -- **Gitee Repository**: [AlphaFold3](https://gitee.com/mindspore/mindscience/tree/main/MindSPONGE/applications/AlphaFold3) -- **Issue Tracking**: [Issue Tracking](https://gitee.com/mindspore/mindscience/issues) - -## Reference - -- Abramson J, Adler J, Dunger J, et al. Accurate structure prediction of biomolecular interactions with AlphaFold 3[J]. Nature, 2024, 630(8016): 493-500. diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/__init__.py b/MindSPONGE/applications/AlphaFold3/alphafold3/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/build_data.py b/MindSPONGE/applications/AlphaFold3/alphafold3/build_data.py deleted file mode 100644 index 58ae0c88b..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/build_data.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Script for building intermediate data.""" - -from importlib import resources -import pathlib -import site - -import alphafold3.constants.converters -from alphafold3.constants.converters import ccd_pickle_gen -from alphafold3.constants.converters import chemical_component_sets_gen - - -def build_data(): - """Builds intermediate data.""" - for site_path in site.getsitepackages(): - path = pathlib.Path(site_path) / 'share/libcifpp/components.cif' - if path.exists(): - cif_path = path - break - else: - raise ValueError('Could not find components.cif') - - out_root = resources.files(alphafold3.constants.converters) - ccd_pickle_path = out_root.joinpath('ccd.pickle') - chemical_component_sets_pickle_path = out_root.joinpath( - 'chemical_component_sets.pickle' - ) - ccd_pickle_gen.main(['', str(cif_path), str(ccd_pickle_path)]) - chemical_component_sets_gen.main( - ['', str(chemical_component_sets_pickle_path)] - ) - - -if __name__ == '__main__': - build_data() diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/common/base_config.py b/MindSPONGE/applications/AlphaFold3/alphafold3/common/base_config.py deleted file mode 100644 index 27f6eba12..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/common/base_config.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ -"""Config for the protein folding model and experiment.""" - -from collections.abc import Mapping -import copy -import dataclasses -import types -import typing -from typing import Any, ClassVar, TypeVar - - -_T = TypeVar('_T') -_ConfigT = TypeVar('_ConfigT', bound='BaseConfig') - - -def _strip_optional(t: type[Any]) -> type[Any]: - """Transforms type annotations of the form `T | None` to `T`.""" - if typing.get_origin(t) in (typing.Union, types.UnionType): - args = set(typing.get_args(t)) - {types.NoneType} - if len(args) == 1: - return args.pop() - return t - - -_NO_UPDATE = object() - - -class _Autocreate: - - def __init__(self, **defaults: Any): - self.defaults = defaults - - -def autocreate(**defaults: Any) -> Any: - """Marks a field as having a default factory derived from its type.""" - return _Autocreate(**defaults) - - -def _clone_field( - field: dataclasses.Field[_T], new_default: _T -) -> dataclasses.Field[_T]: - if new_default is _NO_UPDATE: - return copy.copy(field) - return dataclasses.field( - default=new_default, - init=True, - kw_only=True, - repr=field.repr, - hash=field.hash, - compare=field.compare, - metadata=field.metadata, - ) - - -@typing.dataclass_transform() -class ConfigMeta(type): - """Metaclass that synthesizes a __post_init__ that coerces dicts to Config subclass instances.""" - - def __new__(mcs, name, bases, classdict): - cls = super().__new__(mcs, name, bases, classdict) - - def _coercable_fields(self) -> Mapping[str, tuple[ConfigMeta, Any]]: - type_hints = typing.get_type_hints(self.__class__) - fields = dataclasses.fields(self.__class__) - field_to_type_and_default = { - field.name: (_strip_optional( - type_hints[field.name]), field.default) - for field in fields - } - coercable_fields = { - f: t - for f, t in field_to_type_and_default.items() - if issubclass(type(t[0]), ConfigMeta) - } - return coercable_fields - - cls._coercable_fields = property(_coercable_fields) - - old_post_init = getattr(cls, '__post_init__', None) - - def _post_init(self) -> None: - # Use get_type_hints instead of Field.type to ensure that forward - # references are resolved. - for field_name, ( - field_type, - field_default, - ) in self._coercable_fields.items(): # pylint: disable=protected-access - field_value = getattr(self, field_name) - if field_value is None: - continue - try: - match field_value: - case _Autocreate(): - # Construct from field defaults. - setattr(self, field_name, field_type( - **field_value.defaults)) - case Mapping(): - # Field value is not yet a `Config` instance; Assume we can create - # one by splatting keys and values. - args = {} - # Apply default args first, if present. - if isinstance(field_default, _Autocreate): - args.update(field_default.defaults) - args.update(field_value) - setattr(self, field_name, field_type(**args)) - case _: - pass - except TypeError as e: - raise TypeError( - f'Failure while coercing field {field_name!r} of' - f' {self.__class__.__qualname__}' - ) from e - if old_post_init: - old_post_init(self) - - cls.__post_init__ = _post_init - - return dataclasses.dataclass(kw_only=True)(cls) - - -class BaseConfig(metaclass=ConfigMeta): - """Config base class. - - Subclassing Config automatically makes the subclass a kw_only dataclass with - a `__post_init__` that coerces Config-subclass field values from mappings to - instances of the right type. - """ - # Provided by dataclasses.make_dataclass - __dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]] - - # Overridden by metaclass - @property - def _coercable_fields(self) -> Mapping[str, tuple[type['BaseConfig'], Any]]: - return {} - - def as_dict(self) -> Mapping[str, Any]: - result = dataclasses.asdict(self) - for field_name in self._coercable_fields: - field_value = getattr(self, field_name, None) - if isinstance(field_value, BaseConfig): - result[field_name] = field_value.as_dict() - return result diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/common/folding_input.py b/MindSPONGE/applications/AlphaFold3/alphafold3/common/folding_input.py deleted file mode 100644 index cba8d0556..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/common/folding_input.py +++ /dev/null @@ -1,1115 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Model input dataclass.""" - -from collections.abc import Collection, Mapping, Sequence -import dataclasses -import json -import logging -import pathlib -import random -import re -import string -from typing_extensions import Any, Final, Self, TypeAlias - -from alphafold3 import structure -from alphafold3.constants import chemical_components -from alphafold3.constants import mmcif_names -from alphafold3.constants import residue_names -from alphafold3.structure import mmcif as mmcif_lib -import rdkit.Chem as rd_chem - - -BondAtomId: TypeAlias = tuple[str, int, str] - -JSON_DIALECT: Final[str] = 'alphafold3' -JSON_VERSION: Final[int] = 1 - -ALPHAFOLDSERVER_JSON_DIALECT: Final[str] = 'alphafoldserver' -ALPHAFOLDSERVER_JSON_VERSION: Final[int] = 1 - - -def _validate_keys(actual: Collection[str], expected: Collection[str]): - """Validates that the JSON doesn't contain any extra unwanted keys.""" - if bad_keys := set(actual) - set(expected): - raise ValueError( - f'Unexpected JSON keys in: {", ".join(sorted(bad_keys))}') - - -class Template: - """Structural template input.""" - - __slots__ = ('_mmcif', '_query_to_template') - - def __init__(self, mmcif: str, query_to_template_map: Mapping[int, int]): - """Initializes the template. - - Args: - mmcif: The structural template in mmCIF format. The mmCIF should have only - one protein chain. - query_to_template_map: A mapping from query residue index to template - residue index. - """ - self._mmcif = mmcif - # Needed to make the Template class hashable. - self._query_to_template = tuple(query_to_template_map.items()) - - @property - def query_to_template_map(self) -> Mapping[int, int]: - return dict(self._query_to_template) - - @property - def mmcif(self) -> str: - return self._mmcif - - def __hash__(self) -> int: - return hash((self._mmcif, tuple(sorted(self._query_to_template)))) - - def __eq__(self, other: Self) -> bool: - mmcifs_equal = self._mmcif == other._mmcif - maps_equal = sorted(self._query_to_template) == sorted( - other._query_to_template - ) - return mmcifs_equal and maps_equal - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class ProteinChain: - """Protein chain input. - - Attributes: - id: Unique protein chain identifier. - sequence: The amino acid sequence of the chain. - ptms: A list of tuples containing the post-translational modification type - and the (1-based) residue index where the modification is applied. - paired_msa: Paired A3M-formatted MSA for this chain. This MSA is not - deduplicated and will be used to compute paired features. If None, this - field is unset and must be filled in by the data pipeline before - featurisation. If set to an empty string, it will be treated as a custom - MSA with no sequences. - unpaired_msa: Unpaired A3M-formatted MSA for this chain. This will be - deduplicated and used to compute unpaired features. If None, this field is - unset and must be filled in by the data pipeline before featurisation. If - set to an empty string, it will be treated as a custom MSA with no - sequences. - templates: A list of structural templates for this chain. If None, this - field is unset and must be filled in by the data pipeline before - featurisation. The list can be empty or contain up to 20 templates. - """ - - id: str - sequence: str - ptms: Sequence[tuple[str, int]] - paired_msa: str | None = None - unpaired_msa: str | None = None - templates: Sequence[Template] | None = None - - def __post_init__(self): - if not all(res.isalpha() for res in self.sequence): - raise ValueError( - f'Protein must contain only letters, got "{self.sequence}"' - ) - if any(not 0 < mod[1] <= len(self.sequence) for mod in self.ptms): - raise ValueError( - f'Invalid protein modification index: {self.ptms}') - - # Use hashable types for ptms and templates. - if self.ptms is not None: - object.__setattr__(self, 'ptms', tuple(self.ptms)) - if self.templates is not None: - object.__setattr__(self, 'templates', tuple(self.templates)) - - @classmethod - def from_alphafoldserver_dict( - cls, json_dict: Mapping[str, Any], seq_id: str - ) -> Self: - """Constructs ProteinChain from the AlphaFoldServer JSON dict.""" - _validate_keys( - json_dict.keys(), - {'sequence', 'glycans', 'modifications', 'count'}, - ) - sequence = json_dict['sequence'] - - if 'glycans' in json_dict: - raise ValueError( - f'Specifying glycans in the `{ALPHAFOLDSERVER_JSON_DIALECT}` format' - ' is not currently supported.' - ) - - ptms = [ - (mod['ptmType'].removeprefix('CCD_'), mod['ptmPosition']) - for mod in json_dict.get('modifications', []) - ] - return cls(id=seq_id, sequence=sequence, ptms=ptms) - - @classmethod - def from_dict( - cls, json_dict: Mapping[str, Any], seq_id: str | None = None - ) -> Self: - """Constructs ProteinChain from the AlphaFold JSON dict.""" - json_dict = json_dict['protein'] - _validate_keys( - json_dict.keys(), - { - 'id', - 'sequence', - 'modifications', - 'unpairedMsa', - 'pairedMsa', - 'templates', - }, - ) - - sequence = json_dict['sequence'] - ptms = [ - (mod['ptmType'], mod['ptmPosition']) - for mod in json_dict.get('modifications', []) - ] - - unpaired_msa = json_dict.get('unpairedMsa', None) - paired_msa = json_dict.get('pairedMsa', None) - - raw_templates = json_dict.get('templates', None) - - if raw_templates is None: - templates = None - else: - templates = [ - Template( - mmcif=template['mmcif'], - query_to_template_map=dict( - zip(template['queryIndices'], - template['templateIndices']) - ), - ) - for template in raw_templates - ] - - return cls( - id=seq_id or json_dict['id'], - sequence=sequence, - ptms=ptms, - paired_msa=paired_msa, - unpaired_msa=unpaired_msa, - templates=templates, - ) - - def to_dict(self) -> Mapping[str, Mapping[str, Any]]: - """Converts ProteinChain to an AlphaFold JSON dict.""" - if self.templates is None: - templates = None - else: - templates = [ - { - 'mmcif': template.mmcif, - 'queryIndices': list(template.query_to_template_map.keys()), - 'templateIndices': ( - list(template.query_to_template_map.values()) or None - ), - } - for template in self.templates - ] - contents = { - 'id': self.id, - 'sequence': self.sequence, - 'modifications': [ - {'ptmType': ptm[0], 'ptmPosition': ptm[1]} for ptm in self.ptms - ], - 'unpairedMsa': self.unpaired_msa, - 'pairedMsa': self.paired_msa, - 'templates': templates, - } - return {'protein': contents} - - def to_ccd_sequence(self) -> Sequence[str]: - """Converts to a sequence of CCD codes.""" - ccd_coded_seq = [ - residue_names.PROTEIN_COMMON_ONE_TO_THREE.get( - res, residue_names.UNK) - for res in self.sequence - ] - for ptm_code, ptm_index in self.ptms: - ccd_coded_seq[ptm_index - 1] = ptm_code - return ccd_coded_seq - - def fill_missing_fields(self) -> Self: - """Fill missing MSA and template fields with default values.""" - return dataclasses.replace( - self, - unpaired_msa=self.unpaired_msa or '', - paired_msa=self.paired_msa or '', - templates=self.templates or [], - ) - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class RnaChain: - """RNA chain input. - - Attributes: - id: Unique RNA chain identifier. - sequence: The RNA sequence of the chain. - modifications: A list of tuples containing the modification type and the - (1-based) residue index where the modification is applied. - unpaired_msa: Unpaired A3M-formatted MSA for this chain. This will be - deduplicated and used to compute unpaired features. If None, this field is - unset and must be filled in by the data pipeline before featurisation. If - set to an empty string, it will be treated as a custom MSA with no - sequences. - """ - - id: str - sequence: str - modifications: Sequence[tuple[str, int]] - unpaired_msa: str | None = None - - def __post_init__(self): - if not all(res.isalpha() for res in self.sequence): - raise ValueError( - f'RNA must contain only letters, got "{self.sequence}"') - if any(not 0 < mod[1] <= len(self.sequence) for mod in self.modifications): - raise ValueError( - f'Invalid RNA modification index: {self.modifications}') - - # Use hashable types for modifications. - object.__setattr__(self, 'modifications', tuple(self.modifications)) - - @classmethod - def from_alphafoldserver_dict( - cls, json_dict: Mapping[str, Any], seq_id: str - ) -> Self: - """Constructs RnaChain from the AlphaFoldServer JSON dict.""" - _validate_keys(json_dict.keys(), { - 'sequence', 'modifications', 'count'}) - sequence = json_dict['sequence'] - modifications = [ - (mod['modificationType'].removeprefix('CCD_'), mod['basePosition']) - for mod in json_dict.get('modifications', []) - ] - return cls(id=seq_id, sequence=sequence, modifications=modifications) - - @classmethod - def from_dict( - cls, json_dict: Mapping[str, Any], seq_id: str | None = None - ) -> Self: - """Constructs RnaChain from the AlphaFold JSON dict.""" - json_dict = json_dict['rna'] - _validate_keys( - json_dict.keys(), {'id', 'sequence', - 'unpairedMsa', 'modifications'} - ) - sequence = json_dict['sequence'] - modifications = [ - (mod['modificationType'], mod['basePosition']) - for mod in json_dict.get('modifications', []) - ] - unpaired_msa = json_dict.get('unpairedMsa', None) - return cls( - id=seq_id or json_dict['id'], - sequence=sequence, - modifications=modifications, - unpaired_msa=unpaired_msa, - ) - - def to_dict(self) -> Mapping[str, Mapping[str, Any]]: - """Converts RnaChain to an AlphaFold JSON dict.""" - contents = { - 'id': self.id, - 'sequence': self.sequence, - 'modifications': [ - {'modificationType': mod[0], 'basePosition': mod[1]} - for mod in self.modifications - ], - 'unpairedMsa': self.unpaired_msa, - } - return {'rna': contents} - - def to_ccd_sequence(self) -> Sequence[str]: - """Converts to a sequence of CCD codes.""" - mapping = { - r: r for r in residue_names.RNA_TYPES} # Same 1-letter and CCD. - ccd_coded_seq = [ - mapping.get(res, residue_names.UNK_RNA) for res in self.sequence - ] - for ccd_code, modification_index in self.modifications: - ccd_coded_seq[modification_index - 1] = ccd_code - return ccd_coded_seq - - def fill_missing_fields(self) -> Self: - """Fill missing MSA fields with default values.""" - return dataclasses.replace(self, unpaired_msa=self.unpaired_msa or '') - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class DnaChain: - """Single strand DNA chain input. - - Attributes: - id: Unique DNA chain identifier. - sequence: The DNA sequence of the chain. - modifications: A list of tuples containing the modification type and the - (1-based) residue index where the modification is applied. - """ - - id: str - sequence: str - modifications: Sequence[tuple[str, int]] - - def __post_init__(self): - if not all(res.isalpha() for res in self.sequence): - raise ValueError( - f'DNA must contain only letters, got "{self.sequence}"') - if any(not 0 < mod[1] <= len(self.sequence) for mod in self.modifications): - raise ValueError( - f'Invalid DNA modification index: {self.modifications}') - - # Use hashable types for modifications. - object.__setattr__(self, 'modifications', tuple(self.modifications)) - - @classmethod - def from_alphafoldserver_dict( - cls, json_dict: Mapping[str, Any], seq_id: str - ) -> Self: - """Constructs DnaChain from the AlphaFoldServer JSON dict.""" - _validate_keys(json_dict.keys(), { - 'sequence', 'modifications', 'count'}) - sequence = json_dict['sequence'] - modifications = [ - (mod['modificationType'].removeprefix('CCD_'), mod['basePosition']) - for mod in json_dict.get('modifications', []) - ] - return cls(id=seq_id, sequence=sequence, modifications=modifications) - - @classmethod - def from_dict( - cls, json_dict: Mapping[str, Any], seq_id: str | None = None - ) -> Self: - """Constructs DnaChain from the AlphaFold JSON dict.""" - json_dict = json_dict['dna'] - _validate_keys(json_dict.keys(), {'id', 'sequence', 'modifications'}) - sequence = json_dict['sequence'] - modifications = [ - (mod['modificationType'], mod['basePosition']) - for mod in json_dict.get('modifications', []) - ] - return cls( - id=seq_id or json_dict['id'], - sequence=sequence, - modifications=modifications, - ) - - def to_dict(self) -> Mapping[str, Mapping[str, Any]]: - """Converts DnaChain to an AlphaFold JSON dict.""" - contents = { - 'id': self.id, - 'sequence': self.sequence, - 'modifications': [ - {'modificationType': mod[0], 'basePosition': mod[1]} - for mod in self.modifications - ], - } - return {'dna': contents} - - def to_ccd_sequence(self) -> Sequence[str]: - """Converts to a sequence of CCD codes.""" - ccd_coded_seq = [ - residue_names.DNA_COMMON_ONE_TO_TWO.get(res, residue_names.UNK_DNA) - for res in self.sequence - ] - for ccd_code, modification_index in self.modifications: - ccd_coded_seq[modification_index - 1] = ccd_code - return ccd_coded_seq - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class Ligand: - """Ligand input. - - Attributes: - id: Unique ligand "chain" identifier. - ccd_ids: The Chemical Component Dictionary or user-defined CCD IDs of the - chemical components of the ligand. Typically, this is just a single ID, - but some ligands are composed of multiple components. If that is the case, - a bond linking these components should be added to the bonded_atom_pairs - Input field. - smiles: The SMILES representation of the ligand. - """ - - id: str - ccd_ids: Sequence[str] | None = None - smiles: str | None = None - - def __post_init__(self): - if (self.ccd_ids is None) == (self.smiles is None): - raise ValueError('Ligand must have one of CCD ID or SMILES set.') - - if self.smiles is not None: - mol = rd_chem.MolFromSmiles(self.smiles) - if not mol: - raise ValueError( - f'Unable to make RDKit Mol from SMILES: {self.smiles}') - - # Use hashable types for ccd_ids. - if self.ccd_ids is not None: - object.__setattr__(self, 'ccd_ids', tuple(self.ccd_ids)) - - @classmethod - def from_alphafoldserver_dict( - cls, json_dict: Mapping[str, Any], seq_id: str - ) -> Self: - """Constructs Ligand from the AlphaFoldServer JSON dict.""" - # Ligand can be specified either as a ligand, or ion (special-case). - _validate_keys(json_dict.keys(), {'ligand', 'ion', 'count'}) - if 'ligand' in json_dict: - return cls(id=seq_id, ccd_ids=[json_dict['ligand'].removeprefix('CCD_')]) - elif 'ion' in json_dict: - return cls(id=seq_id, ccd_ids=[json_dict['ion']]) - else: - raise ValueError(f'Unknown ligand type: {json_dict}') - - @classmethod - def from_dict( - cls, json_dict: Mapping[str, Any], seq_id: str | None = None - ) -> Self: - """Constructs Ligand from the AlphaFold JSON dict.""" - json_dict = json_dict['ligand'] - _validate_keys(json_dict.keys(), {'id', 'ccdCodes', 'smiles'}) - if json_dict.get('ccdCodes') and json_dict.get('smiles'): - raise ValueError( - 'Ligand cannot have both CCD code and SMILES set at the same time, ' - f'got CCD: {json_dict["ccdCodes"]} and SMILES: {json_dict["smiles"]}' - ) - - if 'ccdCodes' in json_dict: - return cls(id=seq_id or json_dict['id'], ccd_ids=json_dict['ccdCodes']) - elif 'smiles' in json_dict: - return cls(id=seq_id or json_dict['id'], smiles=json_dict['smiles']) - else: - raise ValueError(f'Unknown ligand type: {json_dict}') - - def to_dict(self) -> Mapping[str, Any]: - """Converts Ligand to an AlphaFold JSON dict.""" - contents = {'id': self.id} - if self.ccd_ids is not None: - contents['ccdCodes'] = self.ccd_ids - if self.smiles is not None: - contents['smiles'] = self.smiles - return {'ligand': contents} - - -def _sample_rng_seed() -> int: - """Sample a random seed for AlphaFoldServer job.""" - # See https://alphafoldserver.com/faq#what-are-seeds-and-how-are-they-set. - return random.randint(0, 2**32 - 1) - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class Input: - """AlphaFold input. - - Attributes: - name: The name of the target. - chains: Protein chains, RNA chains, DNA chains, or ligands. - protein_chains: Protein chains. - rna_chains: RNA chains. - dna_chains: Single strand DNA chains. - ligands: Ligand (including ion) inputs. - rng_seeds: Random number generator seeds, one for each model execution. - bonded_atom_pairs: A list of tuples of atoms that are bonded to each other. - Each atom is defined by a tuple of (chain_id, res_id, atom_name). Chain - IDs must be set if there are any bonded atoms. Residue IDs are 1-indexed. - Atoms in ligands defined by SMILES can't be bonded since SMILES doesn't - define unique atom names. - user_ccd: Optional user-defined chemical component dictionary in the CIF - format. This can be used to provide additional CCD entries that are not - present in the default CCD and thus define arbitrary new ligands. This is - more expressive than SMILES since it allows to name all atoms within the - ligand which in turn makes it possible to define bonds using those atoms. - """ - - name: str - chains: Sequence[ProteinChain | RnaChain | DnaChain | Ligand] - rng_seeds: Sequence[int] - bonded_atom_pairs: Sequence[tuple[BondAtomId, BondAtomId]] | None = None - user_ccd: str | None = None - - def __post_init__(self): - if not self.rng_seeds: - raise ValueError('Input must have at least one RNG seed.') - - if not self.name.strip() or not self.sanitised_name(): - raise ValueError( - 'Input name must be non-empty and contain at least one valid' - ' character (letters, numbers, dots, dashes, underscores).' - ) - - chain_ids = [c.id for c in self.chains] - if any(not c.id.isalpha() or c.id.islower() for c in self.chains): - raise ValueError( - f'IDs must be upper case letters, got: {chain_ids}') - if len(set(chain_ids)) != len(chain_ids): - raise ValueError( - 'Input JSON contains sequences with duplicate IDs.') - - # Use hashable types for chains, rng_seeds, and bonded_atom_pairs. - object.__setattr__(self, 'chains', tuple(self.chains)) - object.__setattr__(self, 'rng_seeds', tuple(self.rng_seeds)) - if self.bonded_atom_pairs is not None: - object.__setattr__( - self, 'bonded_atom_pairs', tuple(self.bonded_atom_pairs) - ) - - @property - def protein_chains(self) -> Sequence[ProteinChain]: - return [chain for chain in self.chains if isinstance(chain, ProteinChain)] - - @property - def rna_chains(self) -> Sequence[RnaChain]: - return [chain for chain in self.chains if isinstance(chain, RnaChain)] - - @property - def dna_chains(self) -> Sequence[DnaChain]: - return [chain for chain in self.chains if isinstance(chain, DnaChain)] - - @property - def ligands(self) -> Sequence[Ligand]: - return [chain for chain in self.chains if isinstance(chain, Ligand)] - - @classmethod - def from_alphafoldserver_fold_job(cls, fold_job: Mapping[str, Any]) -> Self: - """Constructs Input from an AlphaFoldServer fold job.""" - - # Validate the fold job has the correct format. - _validate_keys( - fold_job.keys(), - {'name', 'modelSeeds', 'sequences', 'dialect', 'version'}, - ) - if 'dialect' not in fold_job and 'version' not in fold_job: - dialect = ALPHAFOLDSERVER_JSON_DIALECT - version = ALPHAFOLDSERVER_JSON_VERSION - elif 'dialect' in fold_job and 'version' in fold_job: - dialect = fold_job['dialect'] - version = fold_job['version'] - else: - raise ValueError( - 'AlphaFold Server input JSON must either contain both `dialect` and' - ' `version` fields, or neither. If neither is specified, it is' - f' assumed that `dialect="{ALPHAFOLDSERVER_JSON_DIALECT}"` and' - f' `version="{ALPHAFOLDSERVER_JSON_VERSION}"`.' - ) - - if dialect != ALPHAFOLDSERVER_JSON_DIALECT: - raise ValueError( - f'AlphaFold Server input JSON has unsupported dialect: {dialect}, ' - f'expected {ALPHAFOLDSERVER_JSON_DIALECT}.' - ) - - # For now, there is only one AlphaFold Server JSON version. - if version != ALPHAFOLDSERVER_JSON_VERSION: - raise ValueError( - f'AlphaFold Server input JSON has unsupported version: {version}, ' - f'expected {ALPHAFOLDSERVER_JSON_VERSION}.' - ) - - # Parse the chains. - chains = [] - for sequence in fold_job['sequences']: - if 'proteinChain' in sequence: - for _ in range(sequence['proteinChain'].get('count', 1)): - chains.append( - ProteinChain.from_alphafoldserver_dict( - sequence['proteinChain'], - seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1), - ) - ) - elif 'rnaSequence' in sequence: - for _ in range(sequence['rnaSequence'].get('count', 1)): - chains.append( - RnaChain.from_alphafoldserver_dict( - sequence['rnaSequence'], - seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1), - ) - ) - elif 'dnaSequence' in sequence: - for _ in range(sequence['dnaSequence'].get('count', 1)): - chains.append( - DnaChain.from_alphafoldserver_dict( - sequence['dnaSequence'], - seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1), - ) - ) - elif 'ion' in sequence: - for _ in range(sequence['ion'].get('count', 1)): - chains.append( - Ligand.from_alphafoldserver_dict( - sequence['ion'], - seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1), - ) - ) - elif 'ligand' in sequence: - for _ in range(sequence['ligand'].get('count', 1)): - chains.append( - Ligand.from_alphafoldserver_dict( - sequence['ligand'], - seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1), - ) - ) - else: - raise ValueError(f'Unknown sequence type: {sequence}') - - if 'modelSeeds' in fold_job and fold_job['modelSeeds']: - rng_seeds = [int(seed) for seed in fold_job['modelSeeds']] - else: - rng_seeds = [_sample_rng_seed()] - - return cls(name=fold_job['name'], chains=chains, rng_seeds=rng_seeds) - - @classmethod - def from_json(cls, json_str: str) -> Self: - """Loads the input from the AlphaFold JSON string.""" - raw_json = json.loads(json_str) - - _validate_keys( - raw_json.keys(), - { - 'dialect', - 'version', - 'name', - 'modelSeeds', - 'sequences', - 'bondedAtomPairs', - 'userCCD', - }, - ) - - if 'dialect' not in raw_json or 'version' not in raw_json: - raise ValueError( - 'AlphaFold 3 input JSON must contain `dialect` and `version` fields.' - ) - - if raw_json['dialect'] != JSON_DIALECT: - raise ValueError( - 'AlphaFold 3 input JSON has unsupported dialect:' - f' {raw_json["dialect"]}, expected {JSON_DIALECT}.' - ) - - # For now, there is only one AlphaFold 3 JSON version. - if raw_json['version'] != JSON_VERSION: - raise ValueError( - 'AlphaFold 3 input JSON has unsupported version:' - f' {raw_json["version"]}, expected {JSON_VERSION}.' - ) - - if 'sequences' not in raw_json: - raise ValueError( - 'AlphaFold 3 input JSON does not contain any sequences.') - - if 'modelSeeds' not in raw_json or not raw_json['modelSeeds']: - raise ValueError( - 'AlphaFold 3 input JSON must specify at least one rng seed in' - ' `modelSeeds`.' - ) - - sequences = raw_json['sequences'] - - # Make sure sequence IDs are all set. - raw_sequence_ids = [next(iter(s.values())).get('id') - for s in sequences] - if all(raw_sequence_ids): - sequence_ids = [] - for sequence_id in raw_sequence_ids: - if isinstance(sequence_id, list): - sequence_ids.append(sequence_id) - else: - sequence_ids.append([sequence_id]) - else: - raise ValueError( - 'AlphaFold 3 input JSON contains sequences with unset IDs.' - ) - - flat_seq_ids = [] - for seq_ids in sequence_ids: - flat_seq_ids.extend(seq_ids) - - chains = [] - for seq_ids, sequence in zip(sequence_ids, sequences, strict=True): - if len(sequence) != 1: - raise ValueError(f'Chain {seq_ids} has more than 1 sequence.') - for seq_id in seq_ids: - if 'protein' in sequence: - chains.append(ProteinChain.from_dict( - sequence, seq_id=seq_id)) - elif 'rna' in sequence: - chains.append(RnaChain.from_dict(sequence, seq_id=seq_id)) - elif 'dna' in sequence: - chains.append(DnaChain.from_dict(sequence, seq_id=seq_id)) - elif 'ligand' in sequence: - chains.append(Ligand.from_dict(sequence, seq_id=seq_id)) - else: - raise ValueError(f'Unknown sequence type: {sequence}') - - ligands = [chain for chain in chains if isinstance(chain, Ligand)] - bonded_atom_pairs = None - if bonds := raw_json.get('bondedAtomPairs'): - bonded_atom_pairs = [] - for bond in bonds: - if len(bond) != 2: - raise ValueError( - f'Bond {bond} must have 2 atoms, got {len(bond)}.') - bond_beg, bond_end = bond - if ( - len(bond_beg) != 3 - or not isinstance(bond_beg[0], str) - or not isinstance(bond_beg[1], int) - or not isinstance(bond_beg[2], str) - ): - raise ValueError( - f'Atom {bond_beg} in bond {bond} must have 3 components: ' - '(chain_id: str, res_id: int, atom_name: str).' - ) - if ( - len(bond_end) != 3 - or not isinstance(bond_end[0], str) - or not isinstance(bond_end[1], int) - or not isinstance(bond_end[2], str) - ): - raise ValueError( - f'Atom {bond_end} in bond {bond} must have 3 components: ' - '(chain_id: str, res_id: int, atom_name: str).' - ) - if bond_beg[0] not in flat_seq_ids or bond_end[0] not in flat_seq_ids: - raise ValueError(f'Invalid chain ID(s) in bond {bond}') - if bond_beg[1] <= 0 or bond_end[1] <= 0: - raise ValueError(f'Invalid residue ID(s) in bond {bond}') - smiles_ligand_ids = set( - l.id for l in ligands if l.smiles is not None) - if bond_beg[0] in smiles_ligand_ids: - raise ValueError( - f'Bond {bond} involves an unsupported SMILES ligand {bond_beg[0]}' - ) - if bond_end[0] in smiles_ligand_ids: - raise ValueError( - f'Bond {bond} involves an unsupported SMILES ligand {bond_end[0]}' - ) - bonded_atom_pairs.append((tuple(bond_beg), tuple(bond_end))) - - return cls( - name=raw_json['name'], - chains=chains, - rng_seeds=[int(seed) for seed in raw_json['modelSeeds']], - bonded_atom_pairs=bonded_atom_pairs, - user_ccd=raw_json.get('userCCD'), - ) - - @classmethod - def from_mmcif(cls, mmcif_str: str, ccd: chemical_components.Ccd) -> Self: - """Loads the input from an mmCIF string. - - WARNING: Since rng seeds are not stored in mmCIFs, an rng seed is sampled - in the returned `Input`. - - Args: - mmcif_str: The mmCIF string. - ccd: The chemical components dictionary. - - Returns: - The input in an Input format. - """ - - struct = structure.from_mmcif( - mmcif_str, - include_water=False, - fix_mse_residues=True, - fix_unknown_dna=True, - include_bonds=True, - include_other=False, - ) - - # Create default bioassembly, expanding structures implied by stoichiometry. - struct = struct.generate_bioassembly(None) - - sequences = struct.chain_single_letter_sequence( - include_missing_residues=True - ) - - chains = [] - for chain_id, chain_type in zip( - struct.group_by_chain.chain_id, struct.group_by_chain.chain_type - ): - sequence = sequences[chain_id] - - if chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES: - residues = list(struct.chain_res_name_sequence()[chain_id]) - if all(ccd.get(res) is not None for res in residues): - chains.append(Ligand(id=chain_id, ccd_ids=residues)) - elif len(residues) == 1: - comp_name = residues[0] - comps = struct.chemical_components_data - if comps is None: - raise ValueError( - 'Missing mmCIF chemical components data - this is required for ' - f'a non-CCD ligand {comp_name} defined using SMILES string.' - ) - chains.append( - Ligand(id=chain_id, - smiles=comps.chem_comp[comp_name].pdbx_smiles) - ) - else: - raise ValueError( - 'Multi-component ligand must be defined using CCD IDs, defining' - ' using SMILES is supported only for single-component ligands. ' - f'Got {residues}' - ) - else: - residues = struct.chain_res_name_sequence()[chain_id] - fixed = struct.chain_res_name_sequence( - fix_non_standard_polymer_res=True - )[chain_id] - modifications = [ - (orig, i + 1) - for i, (orig, fixed) in enumerate(zip(residues, fixed, strict=True)) - if orig != fixed - ] - - if chain_type == mmcif_names.PROTEIN_CHAIN: - chains.append( - ProteinChain(id=chain_id, sequence=sequence, - ptms=modifications) - ) - elif chain_type == mmcif_names.RNA_CHAIN: - chains.append( - RnaChain( - id=chain_id, sequence=sequence, modifications=modifications - ) - ) - elif chain_type == mmcif_names.DNA_CHAIN: - chains.append( - DnaChain( - id=chain_id, sequence=sequence, modifications=modifications - ) - ) - - bonded_atom_pairs = [] - chain_ids = set(c.id for c in chains) - for atom_a, atom_b, _ in struct.iter_bonds(): - if atom_a['chain_id'] in chain_ids and atom_b['chain_id'] in chain_ids: - beg = (atom_a['chain_id'], int( - atom_a['res_id']), atom_a['atom_name']) - end = (atom_b['chain_id'], int( - atom_b['res_id']), atom_b['atom_name']) - bonded_atom_pairs.append((beg, end)) - - return cls( - name=struct.name, - chains=chains, - # mmCIFs don't store rng seeds, so we need to sample one here. - rng_seeds=[_sample_rng_seed()], - bonded_atom_pairs=bonded_atom_pairs or None, - ) - - def to_structure(self, ccd: chemical_components.Ccd) -> structure.Structure: - """Converts Input to a Structure. - - WARNING: This method does not preserve the rng seeds. - - Args: - ccd: The chemical components dictionary. - - Returns: - The input in a structure.Structure format. - """ - ids: list[str] = [] - sequences: list[str] = [] - poly_types: list[str] = [] - formats: list[structure.SequenceFormat] = [] - - for chain in self.chains: - ids.append(chain.id) - match chain: - case ProteinChain(): - sequences.append( - '(' + ')('.join(chain.to_ccd_sequence()) + ')') - poly_types.append(mmcif_names.PROTEIN_CHAIN) - formats.append(structure.SequenceFormat.CCD_CODES) - case RnaChain(): - sequences.append( - '(' + ')('.join(chain.to_ccd_sequence()) + ')') - poly_types.append(mmcif_names.RNA_CHAIN) - formats.append(structure.SequenceFormat.CCD_CODES) - case DnaChain(): - sequences.append( - '(' + ')('.join(chain.to_ccd_sequence()) + ')') - poly_types.append(mmcif_names.DNA_CHAIN) - formats.append(structure.SequenceFormat.CCD_CODES) - case Ligand(): - if chain.ccd_ids is not None: - sequences.append('(' + ')('.join(chain.ccd_ids) + ')') - if len(chain.ccd_ids) == 1: - poly_types.append(mmcif_names.NON_POLYMER_CHAIN) - else: - poly_types.append(mmcif_names.BRANCHED_CHAIN) - formats.append(structure.SequenceFormat.CCD_CODES) - elif chain.smiles is not None: - # Convert to `:` format that is expected - # by structure.from_sequences_and_bonds. - sequences.append(f'LIG_{chain.id}:{chain.smiles}') - poly_types.append(mmcif_names.NON_POLYMER_CHAIN) - formats.append(structure.SequenceFormat.LIGAND_SMILES) - else: - raise ValueError( - 'Ligand must have one of CCD ID or SMILES set.') - - # Remap bond chain IDs from chain IDs to chain indices and convert to - # 0-based residue indexing. - bonded_atom_pairs = [] - chain_indices = {cid: i for i, cid in enumerate(ids)} - if self.bonded_atom_pairs is not None: - for bond_beg, bond_end in self.bonded_atom_pairs: - bonded_atom_pairs.append(( - (chain_indices[bond_beg[0]], bond_beg[1] - 1, bond_beg[2]), - (chain_indices[bond_end[0]], bond_end[1] - 1, bond_end[2]), - )) - - struct = structure.from_sequences_and_bonds( - sequences=sequences, - chain_types=poly_types, - sequence_formats=formats, - bonded_atom_pairs=bonded_atom_pairs, - ccd=ccd, - name=self.sanitised_name(), - bond_type=mmcif_names.COVALENT_BOND, - release_date=None, - ) - # Rename chain IDs to the original ones. - return struct.rename_chain_ids(dict(zip(struct.chains, ids, strict=True))) - - def to_json(self) -> str: - """Converts Input to an AlphaFold JSON.""" - alphafold_json = json.dumps( - { - 'dialect': JSON_DIALECT, - 'version': JSON_VERSION, - 'name': self.name, - 'sequences': [chain.to_dict() for chain in self.chains], - 'modelSeeds': self.rng_seeds, - 'bondedAtomPairs': self.bonded_atom_pairs, - 'userCCD': self.user_ccd, - }, - indent=2, - ) - # Remove newlines from the query/template indices arrays. We match the - # queryIndices/templatesIndices with a non-capturing group. We then match - # the entire region between the square brackets by looking for lines - # containing only whitespace, number, or a comma. - return re.sub( - r'("(?:queryIndices|templateIndices)": \[)([\s\n\d,]+)(\],?)', - lambda mtch: mtch[1] + - re.sub(r'\n\s+', ' ', mtch[2].strip()) + mtch[3], - alphafold_json, - ) - - def fill_missing_fields(self) -> Self: - """Fill missing MSA and template fields with default values.""" - with_missing_fields = [ - c.fill_missing_fields() - if isinstance(c, (ProteinChain, RnaChain)) - else c - for c in self.chains - ] - return dataclasses.replace(self, chains=with_missing_fields) - - def sanitised_name(self) -> str: - """Returns sanitised version of the name that can be used as a filename.""" - lower_spaceless_name = self.name.lower().replace(' ', '_') - allowed_chars = set(string.ascii_lowercase + string.digits + '_-.') - return ''.join(l for l in lower_spaceless_name if l in allowed_chars) - - -def check_unique_sanitised_names(fold_inputs: Sequence[Input]) -> None: - """Checks that the names of the fold inputs are unique.""" - names = [fi.sanitised_name() for fi in fold_inputs] - if len(set(names)) != len(names): - raise ValueError( - f'Fold inputs must have unique sanitised names, got {names}.' - ) - - -def load_fold_inputs_from_path(json_path: pathlib.Path) -> Sequence[Input]: - """Loads multiple fold inputs from a JSON string.""" - with open(json_path, 'r') as f: - json_str = f.read() - - # Parse the JSON string, so we can detect its format. - raw_json = json.loads(json_str) - - fold_inputs = [] - if isinstance(raw_json, list): - # AlphaFold Server JSON. - logging.info( - 'Detected %s is an AlphaFold Server JSON since the top-level is a' - ' list.', - json_path, - ) - - logging.info('Loading %d fold jobs from %s', len(raw_json), json_path) - for fold_job_idx, fold_job in enumerate(raw_json): - try: - fold_inputs.append( - Input.from_alphafoldserver_fold_job(fold_job)) - except ValueError as e: - raise ValueError( - f'Failed to load fold job {fold_job_idx} from {json_path}. The JSON' - f' at {json_path} was detected to be an AlphaFold Server JSON since' - ' the top-level is a list.' - ) from e - else: - logging.info( - 'Detected %s is an AlphaFold 3 JSON since the top-level is not a list.', - json_path, - ) - # AlphaFold 3 JSON. - try: - fold_inputs.append(Input.from_json(json_str)) - except ValueError as e: - raise ValueError( - f'Failed to load fold input from {json_path}. The JSON at' - f' {json_path} was detected to be an AlphaFold 3 JSON since the' - ' top-level is not a list.' - ) from e - - check_unique_sanitised_names(fold_inputs) - - return fold_inputs - - -def load_fold_inputs_from_dir(input_dir: pathlib.Path) -> Sequence[Input]: - """Loads multiple fold inputs from all JSON files in a given input_dir. - - Args: - input_dir: The directory containing the JSON files. - - Returns: - The fold inputs from all JSON files in the input directory. - - Raises: - ValueError: If the fold inputs have non-unique sanitised names. - """ - fold_inputs = [] - for file_path in input_dir.glob('*.json'): - if not file_path.is_file(): - continue - - fold_inputs.extend(load_fold_inputs_from_path(file_path)) - - check_unique_sanitised_names(fold_inputs) - - return fold_inputs diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/common/resources.py b/MindSPONGE/applications/AlphaFold3/alphafold3/common/resources.py deleted file mode 100644 index 1626880cd..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/common/resources.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Load external resources, such as external tools or data resources.""" - -from collections.abc import Iterator -import os -import pathlib -import typing -from typing import BinaryIO, Final, Literal, TextIO, Union - -from importlib import resources -import alphafold3.common - - -_DATA_ROOT: Final[pathlib.Path] = ( - resources.files(alphafold3.common).joinpath('..').resolve() -) -ROOT = _DATA_ROOT - - -def filename(name: Union[str, os.PathLike[str]]) -> str: - """Returns the absolute path to an external resource. - - Note that this calls resources.GetResourceFilename under the hood and hence - causes par file unpacking, which might be unfriendly on diskless machines. - - - Args: - name: the name of the resource corresponding to its path relative to the - root of the repository. - """ - return (_DATA_ROOT / name).as_posix() - - -@typing.overload -def open_resource( - name: Union[str, os.PathLike[str]], mode: Literal['r', 'rt'] = 'rt' -) -> TextIO: - ... - - -@typing.overload -def open_resource( - name: Union[str, os.PathLike[str]], mode: Literal['rb'] -) -> BinaryIO: - ... - - -def open_resource( - name: Union[str, os.PathLike[str]], mode: str = 'rb' -) -> Union[TextIO, BinaryIO]: - """Returns an open file object for the named resource. - - Args: - name: the name of the resource corresponding to its path relative to the - root of the repository. - mode: the mode to use when opening the file. - """ - return (_DATA_ROOT / name).open(mode) - - -def get_resource_dir(path: Union[str, os.PathLike[str]]) -> os.PathLike[str]: - return _DATA_ROOT / path - - -def walk(path: str) -> Iterator[tuple[str, list[str], list[str]]]: - """Walks the directory tree of resources similar to os.walk.""" - return os.walk((_DATA_ROOT / path).as_posix()) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/common/testing/data.py b/MindSPONGE/applications/AlphaFold3/alphafold3/common/testing/data.py deleted file mode 100644 index 215905747..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/common/testing/data.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Module that provides an abstraction for accessing test data.""" - -import os -import pathlib -from typing import Literal, overload, Union, Optional - -from absl.testing import absltest - - -class Data: - """Provides an abstraction for accessing test data.""" - - def __init__(self, data_dir: Union[os.PathLike[str], str]): - """Initiailizes data wrapper, providing users with high level data access. - - Args: - data_dir: Directory containing test data. - """ - self._data_dir = pathlib.Path(data_dir) - - def path(self, data_name: Optional[Union[str, os.PathLike[str]]] = None) -> str: - """Returns the path to a given test data. - - Args: - data_name: the name of the test data file relative to data_dir. If not - set, this will return the absolute path to the data directory. - """ - data_dir_path = ( - pathlib.Path(absltest.get_default_test_srcdir()) / self._data_dir - ) - - if data_name: - return str(data_dir_path / data_name) - - return str(data_dir_path) - - @overload - def load( - self, data_name: Union[str, os.PathLike[str]], mode: Literal['rt'] = 'rt' - ) -> str: - ... - - @overload - def load( - self, data_name: Union[str, os.PathLike[str]], mode: Literal['rb'] = 'rb' - ) -> bytes: - ... - - def load( - self, data_name: Union[str, os.PathLike[str]], mode: str = 'rt' - ) -> Union[str, bytes]: - """Returns the contents of a given test data. - - Args: - data_name: the name of the test data file relative to data_dir. - mode: the mode in which to read the data file. Defaults to text ('rt'). - """ - encoding = 'utf-8' if 'b' not in mode else None - with open(self.path(data_name), mode=mode, encoding=encoding) as f: - return f.read() diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/atom_types.py b/MindSPONGE/applications/AlphaFold3/alphafold3/constants/atom_types.py deleted file mode 100644 index 8630278a1..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/atom_types.py +++ /dev/null @@ -1,262 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""List of atom types with reverse look-up.""" - -from collections.abc import Mapping, Sequence, Set -import itertools -import sys -from typing import Final -from alphafold3.constants import residue_names - -# Note: -# `sys.intern` places the values in the Python internal db for fast lookup. - -# 37 common residue atoms. -N = sys.intern('N') -CA = sys.intern('CA') -C = sys.intern('C') -CB = sys.intern('CB') -O = sys.intern('O') -CG = sys.intern('CG') -CG1 = sys.intern('CG1') -CG2 = sys.intern('CG2') -OG = sys.intern('OG') -OG1 = sys.intern('OG1') -SG = sys.intern('SG') -CD = sys.intern('CD') -CD1 = sys.intern('CD1') -CD2 = sys.intern('CD2') -ND1 = sys.intern('ND1') -ND2 = sys.intern('ND2') -OD1 = sys.intern('OD1') -OD2 = sys.intern('OD2') -SD = sys.intern('SD') -CE = sys.intern('CE') -CE1 = sys.intern('CE1') -CE2 = sys.intern('CE2') -CE3 = sys.intern('CE3') -NE = sys.intern('NE') -NE1 = sys.intern('NE1') -NE2 = sys.intern('NE2') -OE1 = sys.intern('OE1') -OE2 = sys.intern('OE2') -CH2 = sys.intern('CH2') -NH1 = sys.intern('NH1') -NH2 = sys.intern('NH2') -OH = sys.intern('OH') -CZ = sys.intern('CZ') -CZ2 = sys.intern('CZ2') -CZ3 = sys.intern('CZ3') -NZ = sys.intern('NZ') -OXT = sys.intern('OXT') - -# 29 common nucleic acid atoms. -C1PRIME = sys.intern("C1'") -C2 = sys.intern('C2') -C2PRIME = sys.intern("C2'") -C3PRIME = sys.intern("C3'") -C4 = sys.intern('C4') -C4PRIME = sys.intern("C4'") -C5 = sys.intern('C5') -C5PRIME = sys.intern("C5'") -C6 = sys.intern('C6') -C7 = sys.intern('C7') -C8 = sys.intern('C8') -N1 = sys.intern('N1') -N2 = sys.intern('N2') -N3 = sys.intern('N3') -N4 = sys.intern('N4') -N6 = sys.intern('N6') -N7 = sys.intern('N7') -N9 = sys.intern('N9') -O2 = sys.intern('O2') -O2PRIME = sys.intern("O2'") -O3PRIME = sys.intern("O3'") -O4 = sys.intern('O4') -O4PRIME = sys.intern("O4'") -O5PRIME = sys.intern("O5'") -O6 = sys.intern('O6') -OP1 = sys.intern('OP1') -OP2 = sys.intern('OP2') -OP3 = sys.intern('OP3') -P = sys.intern('P') - -# A list of atoms (excluding hydrogen) for each AA type. PDB naming convention. -RESIDUE_ATOMS: Mapping[str, tuple[str, ...]] = { - residue_names.ALA: (C, CA, CB, N, O), - residue_names.ARG: (C, CA, CB, CG, CD, CZ, N, NE, O, NH1, NH2), - residue_names.ASN: (C, CA, CB, CG, N, ND2, O, OD1), - residue_names.ASP: (C, CA, CB, CG, N, O, OD1, OD2), - residue_names.CYS: (C, CA, CB, N, O, SG), - residue_names.GLN: (C, CA, CB, CG, CD, N, NE2, O, OE1), - residue_names.GLU: (C, CA, CB, CG, CD, N, O, OE1, OE2), - residue_names.GLY: (C, CA, N, O), - residue_names.HIS: (C, CA, CB, CG, CD2, CE1, N, ND1, NE2, O), - residue_names.ILE: (C, CA, CB, CG1, CG2, CD1, N, O), - residue_names.LEU: (C, CA, CB, CG, CD1, CD2, N, O), - residue_names.LYS: (C, CA, CB, CG, CD, CE, N, NZ, O), - residue_names.MET: (C, CA, CB, CG, CE, N, O, SD), - residue_names.PHE: (C, CA, CB, CG, CD1, CD2, CE1, CE2, CZ, N, O), - residue_names.PRO: (C, CA, CB, CG, CD, N, O), - residue_names.SER: (C, CA, CB, N, O, OG), - residue_names.THR: (C, CA, CB, CG2, N, O, OG1), - residue_names.TRP: - (C, CA, CB, CG, CD1, CD2, CE2, CE3, CZ2, CZ3, CH2, N, NE1, O), - residue_names.TYR: (C, CA, CB, CG, CD1, CD2, CE1, CE2, CZ, N, O, OH), - residue_names.VAL: (C, CA, CB, CG1, CG2, N, O), -} # pyformat: disable - -# Used to identify backbone for alignment and distance calculation for sterics. -PROTEIN_BACKBONE_ATOMS: tuple[str, ...] = (N, CA, C) - -# Naming swaps for ambiguous atom names. Due to symmetries in the amino acids -# the naming of atoms is ambiguous in 4 of the 20 amino acids. (The LDDT paper -# lists 7 amino acids as ambiguous, but the naming ambiguities in LEU, VAL and -# ARG can be resolved by using the 3D constellations of the 'ambiguous' atoms -# and their neighbours) -AMBIGUOUS_ATOM_NAMES: Mapping[str, Mapping[str, str]] = { - residue_names.ASP: {OD1: OD2}, - residue_names.GLU: {OE1: OE2}, - residue_names.PHE: {CD1: CD2, CE1: CE2}, - residue_names.TYR: {CD1: CD2, CE1: CE2}, -} - -# Used when we need to store atom data in a format that requires fixed atom data -# size for every protein residue (e.g. a numpy array). -ATOM37: tuple[str, ...] = ( - N, CA, C, CB, O, CG, CG1, CG2, OG, OG1, SG, CD, CD1, CD2, ND1, ND2, OD1, - OD2, SD, CE, CE1, CE2, CE3, NE, NE1, NE2, OE1, OE2, CH2, NH1, NH2, OH, CZ, - CZ2, CZ3, NZ, OXT) # pyformat: disable -ATOM37_ORDER: Mapping[str, int] = {name: i for i, name in enumerate(ATOM37)} -ATOM37_NUM: Final[int] = len(ATOM37) # := 37. - -# Used when we need to store protein atom data in a format that requires fixed -# atom data size for any residue but takes less space than ATOM37 by having 14 -# fields, which is sufficient for storing atoms of all protein residues (e.g. a -# numpy array). -ATOM14: Mapping[str, tuple[str, ...]] = { - residue_names.ALA: (N, CA, C, O, CB), - residue_names.ARG: (N, CA, C, O, CB, CG, CD, NE, CZ, NH1, NH2), - residue_names.ASN: (N, CA, C, O, CB, CG, OD1, ND2), - residue_names.ASP: (N, CA, C, O, CB, CG, OD1, OD2), - residue_names.CYS: (N, CA, C, O, CB, SG), - residue_names.GLN: (N, CA, C, O, CB, CG, CD, OE1, NE2), - residue_names.GLU: (N, CA, C, O, CB, CG, CD, OE1, OE2), - residue_names.GLY: (N, CA, C, O), - residue_names.HIS: (N, CA, C, O, CB, CG, ND1, CD2, CE1, NE2), - residue_names.ILE: (N, CA, C, O, CB, CG1, CG2, CD1), - residue_names.LEU: (N, CA, C, O, CB, CG, CD1, CD2), - residue_names.LYS: (N, CA, C, O, CB, CG, CD, CE, NZ), - residue_names.MET: (N, CA, C, O, CB, CG, SD, CE), - residue_names.PHE: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ), - residue_names.PRO: (N, CA, C, O, CB, CG, CD), - residue_names.SER: (N, CA, C, O, CB, OG), - residue_names.THR: (N, CA, C, O, CB, OG1, CG2), - residue_names.TRP: - (N, CA, C, O, CB, CG, CD1, CD2, NE1, CE2, CE3, CZ2, CZ3, CH2), - residue_names.TYR: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ, OH), - residue_names.VAL: (N, CA, C, O, CB, CG1, CG2), - residue_names.UNK: (), -} # pyformat: disable - -# A compact atom encoding with 14 columns, padded with '' in empty slots. -ATOM14_PADDED: Mapping[str, Sequence[str]] = { - k: [v for _, v in itertools.zip_longest(range(14), values, fillvalue='')] - for k, values in ATOM14.items() -} - -ATOM14_ORDER: Mapping[str, Mapping[str, int]] = { - k: {name: i for i, name in enumerate(v)} for k, v in ATOM14.items() -} -ATOM14_NUM: Final[int] = max(len(v) for v in ATOM14.values()) - -# Used when we need to store protein and nucleic atom library. -DENSE_ATOM: Mapping[str, tuple[str, ...]] = { - # Protein. - residue_names.ALA: (N, CA, C, O, CB), - residue_names.ARG: (N, CA, C, O, CB, CG, CD, NE, CZ, NH1, NH2), - residue_names.ASN: (N, CA, C, O, CB, CG, OD1, ND2), - residue_names.ASP: (N, CA, C, O, CB, CG, OD1, OD2), - residue_names.CYS: (N, CA, C, O, CB, SG), - residue_names.GLN: (N, CA, C, O, CB, CG, CD, OE1, NE2), - residue_names.GLU: (N, CA, C, O, CB, CG, CD, OE1, OE2), - residue_names.GLY: (N, CA, C, O), - residue_names.HIS: (N, CA, C, O, CB, CG, ND1, CD2, CE1, NE2), - residue_names.ILE: (N, CA, C, O, CB, CG1, CG2, CD1), - residue_names.LEU: (N, CA, C, O, CB, CG, CD1, CD2), - residue_names.LYS: (N, CA, C, O, CB, CG, CD, CE, NZ), - residue_names.MET: (N, CA, C, O, CB, CG, SD, CE), - residue_names.PHE: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ), - residue_names.PRO: (N, CA, C, O, CB, CG, CD), - residue_names.SER: (N, CA, C, O, CB, OG), - residue_names.THR: (N, CA, C, O, CB, OG1, CG2), - residue_names.TRP: - (N, CA, C, O, CB, CG, CD1, CD2, NE1, CE2, CE3, CZ2, CZ3, CH2), - residue_names.TYR: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ, OH), - residue_names.VAL: (N, CA, C, O, CB, CG1, CG2), - residue_names.UNK: (), - # RNA. - residue_names.A: - (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, - C2PRIME, O2PRIME, C1PRIME, N9, C8, N7, C5, C6, N6, N1, C2, N3, C4), - residue_names.C: - (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, - C2PRIME, O2PRIME, C1PRIME, N1, C2, O2, N3, C4, N4, C5, C6), - residue_names.G: - (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, - C2PRIME, O2PRIME, C1PRIME, N9, C8, N7, C5, C6, O6, N1, C2, N2, N3, C4), - residue_names.U: - (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, - C2PRIME, O2PRIME, C1PRIME, N1, C2, O2, N3, C4, O4, C5, C6), - residue_names.UNK_RNA: (), - # DNA. - residue_names.DA: - (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, - C2PRIME, C1PRIME, N9, C8, N7, C5, C6, N6, N1, C2, N3, C4), - residue_names.DC: - (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, - C2PRIME, C1PRIME, N1, C2, O2, N3, C4, N4, C5, C6), - residue_names.DG: - (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, - C2PRIME, C1PRIME, N9, C8, N7, C5, C6, O6, N1, C2, N2, N3, C4), - residue_names.DT: - (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, - C2PRIME, C1PRIME, N1, C2, O2, N3, C4, O4, C5, C7, C6), - # Unknown nucleic. - residue_names.UNK_DNA: (), -} # pyformat: disable - -DENSE_ATOM_ORDER: Mapping[str, Mapping[str, int]] = { - k: {name: i for i, name in enumerate(v)} for k, v in DENSE_ATOM.items() -} -DENSE_ATOM_NUM: Final[int] = max(len(v) for v in DENSE_ATOM.values()) - -# Used when we need to store atom data in a format that requires fixed atom data -# size for every nucleic molecule (e.g. a numpy array). -ATOM29: tuple[str, ...] = ( - "C1'", 'C2', "C2'", "C3'", 'C4', "C4'", 'C5', "C5'", 'C6', 'C7', 'C8', 'N1', - 'N2', 'N3', 'N4', 'N6', 'N7', 'N9', 'OP3', 'O2', "O2'", "O3'", 'O4', "O4'", - "O5'", 'O6', 'OP1', 'OP2', 'P') # pyformat: disable -ATOM29_ORDER: Mapping[str, int] = { - atom_type: i for i, atom_type in enumerate(ATOM29) -} -ATOM29_NUM: Final[int] = len(ATOM29) # := 29 - -# Hydrogens that exist depending on the protonation state of the residue. -# Extracted from third_party/py/openmm/app/data/hydrogens.xml -PROTONATION_HYDROGENS: Mapping[str, Set[str]] = { - 'ASP': {'HD2'}, - 'CYS': {'HG'}, - 'GLU': {'HE2'}, - 'HIS': {'HD1', 'HE2'}, - 'LYS': {'HZ3'}, -} diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_component_sets.py b/MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_component_sets.py deleted file mode 100644 index d507fa2ea..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_component_sets.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Sets of chemical components.""" - -import pickle -from typing import Final - -from alphafold3.common import resources - - -_CCD_SETS_CCD_PICKLE_FILE = resources.filename( - resources.ROOT / 'constants/converters/chemical_component_sets.pickle' -) - -with open(_CCD_SETS_CCD_PICKLE_FILE, 'rb') as f: - _CCD_SET = pickle.load(f) - -# Glycan (or 'Saccharide') ligands. -# _chem_comp.type containing 'saccharide' and 'linking' (when lower-case). -GLYCAN_LINKING_LIGANDS: Final[frozenset[str]] = _CCD_SET['glycans_linking'] - -# _chem_comp.type containing 'saccharide' and not 'linking' (when lower-case). -GLYCAN_OTHER_LIGANDS: Final[frozenset[str]] = _CCD_SET['glycans_other'] - -# Each of these molecules appears in over 1k PDB structures, are used to -# facilitate crystallization conditions, but do not have biological relevance. -COMMON_CRYSTALLIZATION_AIDS: Final[frozenset[str]] = frozenset({ - 'SO4', 'GOL', 'EDO', 'PO4', 'ACT', 'PEG', 'DMS', 'TRS', 'PGE', 'PG4', 'FMT', - 'EPE', 'MPD', 'MES', 'CD', 'IOD', -}) # pyformat: disable diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_components.py b/MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_components.py deleted file mode 100644 index 6e132f9b1..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/chemical_components.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Chemical Components found in PDB (CCD) constants.""" - -from collections.abc import ItemsView, Iterator, KeysView, Mapping, Sequence, ValuesView -import dataclasses -import functools -import os -import pickle -from typing import Optional - -from alphafold3.common import resources -from alphafold3.cpp import cif_dict - - -_CCD_PICKLE_FILE = resources.filename( - resources.ROOT / 'constants/converters/ccd.pickle' -) - - -class Ccd(Mapping[str, Mapping[str, Sequence[str]]]): - """Chemical Components found in PDB (CCD) constants. - - See https://academic.oup.com/bioinformatics/article/31/8/1274/212200 for CCD - CIF format documentation. - - Wraps the dict to prevent accidental mutation. - """ - - __slots__ = ('_dict', '_ccd_pickle_path') - - def __init__( - self, - ccd_pickle_path: Optional[os.PathLike[str]] = None, - user_ccd: Optional[str] = None, - ): - """Initialises the chemical components dictionary. - - Args: - ccd_pickle_path: Path to the CCD pickle file. If None, uses the default - CCD pickle file included in the source code. - user_ccd: A string containing the user-provided CCD. This has to conform - to the same format as the CCD, see https://www.wwpdb.org/data/ccd. If - provided, takes precedence over the CCD for the the same key. This can - be used to override specific entries in the CCD if desired. - """ - self._ccd_pickle_path = ccd_pickle_path or _CCD_PICKLE_FILE - with open(self._ccd_pickle_path, 'rb') as f: - self._dict = pickle.loads(f.read()) - - if user_ccd is not None: - if not user_ccd: - raise ValueError('User CCD cannot be an empty string.') - user_ccd_cifs = { - key: {k: tuple(v) for k, v in value.items()} - for key, value in cif_dict.parse_multi_data_cif(user_ccd).items() - } - self._dict.update(user_ccd_cifs) - - def __getitem__(self, key: str) -> Mapping[str, Sequence[str]]: - return self._dict[key] - - def __contains__(self, key: str) -> bool: - return key in self._dict - - def __iter__(self) -> Iterator[str]: - return self._dict.__iter__() - - def __len__(self) -> int: - return len(self._dict) - - def __hash__(self) -> int: - return id(self) # Ok since this is immutable. - - def get( - self, key: str, default: Optional[Mapping[str, Sequence[str]]] = None - ) -> Optional[Mapping[str, Sequence[str]]]: - return self._dict.get(key, default) - - def items(self) -> ItemsView[str, Mapping[str, Sequence[str]]]: - return self._dict.items() - - def values(self) -> ValuesView[Mapping[str, Sequence[str]]]: - return self._dict.values() - - def keys(self) -> KeysView[str]: - return self._dict.keys() - - -@functools.cache -def cached_ccd(user_ccd: Optional[str] = None) -> Ccd: - return Ccd(user_ccd=user_ccd) - - -@dataclasses.dataclass(frozen=True) -class ComponentInfo: - name: str - type: str - pdbx_synonyms: str - formula: str - formula_weight: str - mon_nstd_parent_comp_id: str - mon_nstd_flag: str - pdbx_smiles: str - - -def mmcif_to_info(mmcif: Mapping[str, Sequence[str]]) -> ComponentInfo: - """Converts CCD mmCIFs to component info. Missing fields are left empty.""" - names = mmcif['_chem_comp.name'] - types = mmcif['_chem_comp.type'] - mon_nstd_parent_comp_ids = mmcif['_chem_comp.mon_nstd_parent_comp_id'] - pdbx_synonyms = mmcif['_chem_comp.pdbx_synonyms'] - formulas = mmcif['_chem_comp.formula'] - formula_weights = mmcif['_chem_comp.formula_weight'] - - def front_or_empty(values: Sequence[str]) -> str: - return values[0] if values else '' - - type_ = front_or_empty(types) - mon_nstd_parent_comp_id = front_or_empty(mon_nstd_parent_comp_ids) - if type_.lower() == 'non-polymer': - # Unset for non-polymers, e.g. water or ions. - mon_nstd_flag = '.' - elif mon_nstd_parent_comp_id == '?': - # A standard component - it doesn't have a standard parent, e.g. MET. - mon_nstd_flag = 'y' - else: - # A non-standard component, e.g. MSE. - mon_nstd_flag = 'n' - - canonical_pdbx_smiles = '' - fallback_pdbx_smiles = '' - descriptor_types = mmcif.get('_pdbx_chem_comp_descriptor.type', []) - descriptors = mmcif.get('_pdbx_chem_comp_descriptor.descriptor', []) - programs = mmcif.get('_pdbx_chem_comp_descriptor.program', []) - - for descriptor_type, descriptor in zip(descriptor_types, descriptors): - if descriptor_type == 'SMILES_CANONICAL': - if (not canonical_pdbx_smiles) or programs == 'OpenEye OEToolkits': - canonical_pdbx_smiles = descriptor - if not fallback_pdbx_smiles and descriptor_type == 'SMILES': - fallback_pdbx_smiles = descriptor - pdbx_smiles = canonical_pdbx_smiles or fallback_pdbx_smiles - - return ComponentInfo( - name=front_or_empty(names), - type=type_, - pdbx_synonyms=front_or_empty(pdbx_synonyms), - formula=front_or_empty(formulas), - formula_weight=front_or_empty(formula_weights), - mon_nstd_parent_comp_id=mon_nstd_parent_comp_id, - mon_nstd_flag=mon_nstd_flag, - pdbx_smiles=pdbx_smiles, - ) - - -@functools.lru_cache(maxsize=128) -def component_name_to_info(ccd: Ccd, res_name: str) -> Optional[ComponentInfo]: - component = ccd.get(res_name) - if component is None: - return None - return mmcif_to_info(component) - - -def type_symbol(ccd: Ccd, res_name: str, atom_name: str) -> str: - """Returns the element type for the given component name and atom name. - - Args: - ccd: The chemical components dictionary. - res_name: The component name, e.g. ARG. - atom_name: The atom name, e.g. CB, OXT, or NH1. - - Returns: - Element type, e.g. C for (ARG, CB), O for (ARG, OXT), N for (ARG, NH1). - """ - res = ccd.get(res_name) - if res is None: - return '?' - try: - return res['_chem_comp_atom.type_symbol'][ - res['_chem_comp_atom.atom_id'].index(atom_name) - ] - except (ValueError, IndexError, KeyError): - return '?' diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/converters/ccd_pickle_gen.py b/MindSPONGE/applications/AlphaFold3/alphafold3/constants/converters/ccd_pickle_gen.py deleted file mode 100644 index 876ea3fc6..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/converters/ccd_pickle_gen.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Reads Chemical Components gz file and generates a CCD pickle file.""" - -from collections.abc import Sequence -import gzip -import pickle -import sys - -from alphafold3.cpp import cif_dict -import tqdm - - -def main(argv: Sequence[str]) -> None: - if len(argv) != 3: - raise ValueError( - 'Must specify input_file components.cif and output_file') - - _, input_file, output_file = argv - - print(f'Parsing {input_file}', flush=True) - if input_file.endswith('.gz'): - opener = gzip.open - else: - opener = open - - with opener(input_file, 'rb') as f: - whole_file = f.read() - result = { - key: {k: tuple(v) for k, v in value.items()} - for key, value in tqdm.tqdm( - cif_dict.parse_multi_data_cif(whole_file).items() - ) - } - - print(f'Writing {output_file}', flush=True) - with open(output_file, 'wb') as f: - pickle.dump(result, f, protocol=pickle.HIGHEST_PROTOCOL) - print('Done', flush=True) - - -if __name__ == '__main__': - main(sys.argv) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/converters/chemical_component_sets_gen.py b/MindSPONGE/applications/AlphaFold3/alphafold3/constants/converters/chemical_component_sets_gen.py deleted file mode 100644 index 9300751f5..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/converters/chemical_component_sets_gen.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Script for updating chemical_component_sets.py.""" - -from collections.abc import Mapping, Sequence -import pathlib -import pickle -import re -import sys - -from alphafold3.common import resources -import tqdm - - -_CCD_PICKLE_FILE = resources.filename( - 'constants/converters/ccd.pickle' -) - - -def find_ions_and_glycans_in_ccd( - ccd: Mapping[str, Mapping[str, Sequence[str]]], -) -> dict[str, frozenset[str]]: - """Finds glycans and ions in all version of CCD.""" - glycans_linking = [] - glycans_other = [] - ions = [] - for name, comp in tqdm.tqdm(ccd.items()): - if name == 'UNX': - continue # Skip "unknown atom or ion". - comp_type = comp['_chem_comp.type'][0].lower() - # Glycans have the type 'saccharide'. - if re.findall(r'\bsaccharide\b', comp_type): - # Separate out linking glycans from others. - if 'linking' in comp_type: - glycans_linking.append(name) - else: - glycans_other.append(name) - - # Ions have the word 'ion' in their name. - comp_name = comp['_chem_comp.name'][0].lower() - if re.findall(r'\bion\b', comp_name): - ions.append(name) - result = { - "glycans_linking": frozenset(glycans_linking), - "glycans_other": frozenset(glycans_other), - "ions": frozenset(ions), - } - - return result - - -def main(argv: Sequence[str]) -> None: - if len(argv) != 2: - raise ValueError( - 'Directory to write to must be specified as a command-line arguments.' - ) - - print(f'Loading {_CCD_PICKLE_FILE}', flush=True) - with open(_CCD_PICKLE_FILE, 'rb') as f: - ccd: Mapping[str, Mapping[str, Sequence[str]]] = pickle.load(f) - output_path = pathlib.Path(argv[1]) - output_path.parent.mkdir(exist_ok=True) - print('Finding ions and glycans', flush=True) - result = find_ions_and_glycans_in_ccd(ccd) - print(f'writing to {output_path}', flush=True) - with output_path.open('wb') as f: - pickle.dump(result, f) - print('Done', flush=True) - - -if __name__ == '__main__': - main(sys.argv) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/mmcif_names.py b/MindSPONGE/applications/AlphaFold3/alphafold3/constants/mmcif_names.py deleted file mode 100644 index dc918cbcc..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/mmcif_names.py +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Names of things in mmCIF format. - -See https://www.iucr.org/__data/iucr/cifdic_html/2/cif_mm.dic/index.html -""" - -from collections.abc import Mapping, Sequence, Set -from typing import Final - -from alphafold3.constants import atom_types -from alphafold3.constants import residue_names - - -# The following are all possible values for the "_entity.type". -# https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Items/_entity.type.html - -BRANCHED_CHAIN: Final[str] = 'branched' -MACROLIDE_CHAIN: Final[str] = 'macrolide' -NON_POLYMER_CHAIN: Final[str] = 'non-polymer' -POLYMER_CHAIN: Final[str] = 'polymer' -WATER: Final[str] = 'water' - -CYCLIC_PSEUDO_PEPTIDE_CHAIN: Final[str] = 'cyclic-pseudo-peptide' -DNA_CHAIN: Final[str] = 'polydeoxyribonucleotide' -DNA_RNA_HYBRID_CHAIN: Final[str] = ( - 'polydeoxyribonucleotide/polyribonucleotide hybrid' -) -OTHER_CHAIN: Final[str] = 'other' -PEPTIDE_NUCLEIC_ACID_CHAIN: Final[str] = 'peptide nucleic acid' -POLYPEPTIDE_D_CHAIN: Final[str] = 'polypeptide(D)' -PROTEIN_CHAIN: Final[str] = 'polypeptide(L)' -RNA_CHAIN: Final[str] = 'polyribonucleotide' - -# Most common _entity_poly.types. -STANDARD_POLYMER_CHAIN_TYPES: Final[Set[str]] = { - PROTEIN_CHAIN, - DNA_CHAIN, - RNA_CHAIN, -} - -# Possible values for _entity.type other than polymer and water. -LIGAND_CHAIN_TYPES: Final[Set[str]] = { - BRANCHED_CHAIN, - MACROLIDE_CHAIN, - NON_POLYMER_CHAIN, -} - -# Possible values for _entity.type other than polymer. -NON_POLYMER_CHAIN_TYPES: Final[Set[str]] = { - *LIGAND_CHAIN_TYPES, - WATER, -} - -# Peptide possible values for _entity_poly.type. -PEPTIDE_CHAIN_TYPES: Final[Set[str]] = { - CYCLIC_PSEUDO_PEPTIDE_CHAIN, - POLYPEPTIDE_D_CHAIN, - PROTEIN_CHAIN, - PEPTIDE_NUCLEIC_ACID_CHAIN, -} - - -# Nucleic-acid possible values for _entity_poly.type. -NUCLEIC_ACID_CHAIN_TYPES: Final[Set[str]] = { - RNA_CHAIN, - DNA_CHAIN, - DNA_RNA_HYBRID_CHAIN, -} - -# All possible values for _entity_poly.type. -POLYMER_CHAIN_TYPES: Final[Set[str]] = { - *NUCLEIC_ACID_CHAIN_TYPES, - *PEPTIDE_CHAIN_TYPES, - OTHER_CHAIN, -} - - -TERMINAL_OXYGENS: Final[Mapping[str, str]] = { - PROTEIN_CHAIN: 'OXT', - DNA_CHAIN: 'OP3', - RNA_CHAIN: 'OP3', -} - - -# For each chain type, which atom should be used to represent each residue. -RESIDUE_REPRESENTATIVE_ATOMS: Final[Mapping[str, str]] = { - PROTEIN_CHAIN: atom_types.CA, - DNA_CHAIN: atom_types.C1PRIME, - RNA_CHAIN: atom_types.C1PRIME, -} - -# Methods involving crystallization. See the documentation at -# mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Items/_exptl.method.html -# for the full list of experimental methods. -CRYSTALLIZATION_METHODS: Final[Set[str]] = { - 'X-RAY DIFFRACTION', - 'NEUTRON DIFFRACTION', - 'ELECTRON CRYSTALLOGRAPHY', - 'POWDER CRYSTALLOGRAPHY', - 'FIBER DIFFRACTION', -} - -# Possible bond types. -COVALENT_BOND: Final[str] = 'covale' -HYDROGEN_BOND: Final[str] = 'hydrog' -METAL_COORDINATION: Final[str] = 'metalc' -DISULFIDE_BRIDGE: Final[str] = 'disulf' - - -def is_standard_polymer_type(chain_type: str) -> bool: - """Returns if chain type is a protein, DNA or RNA chain type. - - Args: - chain_type: The type of the chain. - - Returns: - A bool for if the chain_type matches protein, DNA, or RNA. - """ - return chain_type in STANDARD_POLYMER_CHAIN_TYPES - - -def guess_polymer_type(chain_residues: Sequence[str]) -> str: - """Guess the polymer type (protein/rna/dna/other) based on the residues. - - The polymer type is guessed by first checking for any of the standard - protein residues. If one is present then the chain is considered to be a - polypeptide. Otherwise we decide by counting residue types and deciding by - majority voting (e.g. mostly DNA residues -> DNA). If there is a tie between - the counts, the ordering is rna > dna > other. - - Note that we count MSE and UNK as protein residues. - - Args: - chain_residues: A sequence of full residue name (1-letter for DNA, 2-letters - for RNA, 3 for protein). The _atom_site.label_comp_id column in mmCIF. - - Returns: - The most probable chain type as set in the _entity_poly mmCIF table: - protein - polypeptide(L), rna - polyribonucleotide, - dna - polydeoxyribonucleotide or other. - """ - residue_types = { - **{r: RNA_CHAIN for r in residue_names.RNA_TYPES}, - **{r: DNA_CHAIN for r in residue_names.DNA_TYPES}, - **{r: PROTEIN_CHAIN for r in residue_names.PROTEIN_TYPES_WITH_UNKNOWN}, - residue_names.MSE: PROTEIN_CHAIN, - } - - counts = {PROTEIN_CHAIN: 0, RNA_CHAIN: 0, DNA_CHAIN: 0, OTHER_CHAIN: 0} - for residue in chain_residues: - residue_type = residue_types.get(residue, OTHER_CHAIN) - # If we ever see a protein residue we'll consider this a polypeptide(L). - if residue_type == PROTEIN_CHAIN: - return residue_type - counts[residue_type] += 1 - - # Make sure protein > rna > dna > other if there is a tie. - tie_braker = {PROTEIN_CHAIN: 3, RNA_CHAIN: 2, DNA_CHAIN: 1, OTHER_CHAIN: 0} - - def order_fn(item): - name, count = item - return count, tie_braker[name] - - most_probable_type = max(counts.items(), key=order_fn)[0] - return most_probable_type - - -def fix_non_standard_polymer_res(*, res_name: str, chain_type: str) -> str: - """Returns the res_name of the closest standard protein/RNA/DNA residue. - - Optimized for the case where a single residue needs to be converted. - - If res_name is already a standard type, it is returned unaltered. - If a match cannot be found, returns 'UNK' for protein chains and 'N' for - RNA/DNA chains. - - Args: - res_name: A residue_name (monomer code from the CCD). - chain_type: The type of the chain, must be PROTEIN_CHAIN, RNA_CHAIN or - DNA_CHAIN. - - Returns: - An element from PROTEIN_TYPES_WITH_UNKNOWN | RNA_TYPES | DNA_TYPES | {'N'}. - - Raises: - ValueError: If chain_type not in PEPTIDE_CHAIN_TYPES or - {OTHER_CHAIN, RNA_CHAIN, DNA_CHAIN, DNA_RNA_HYBRID_CHAIN}. - """ - # Map to one letter code, then back to common res_names. - one_letter_code = residue_names.letters_three_to_one(res_name, default='X') - - if chain_type in PEPTIDE_CHAIN_TYPES or chain_type == OTHER_CHAIN: - return residue_names.PROTEIN_COMMON_ONE_TO_THREE.get(one_letter_code, 'UNK') - elif chain_type == RNA_CHAIN: - # RNA's CCD monomer code is single-letter. - return ( - one_letter_code if one_letter_code in residue_names.RNA_TYPES else 'N' - ) - elif chain_type == DNA_CHAIN: - return residue_names.DNA_COMMON_ONE_TO_TWO.get(one_letter_code, 'N') - elif chain_type == DNA_RNA_HYBRID_CHAIN: - return ( - res_name - if res_name in residue_names.NUCLEIC_TYPES_WITH_UNKNOWN - else 'N' - ) - else: - raise ValueError( - f'Expected a protein/DNA/RNA chain but got {chain_type}') diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/periodic_table.py b/MindSPONGE/applications/AlphaFold3/alphafold3/constants/periodic_table.py deleted file mode 100644 index 7385245ff..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/periodic_table.py +++ /dev/null @@ -1,399 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Periodic table of elements.""" - -from collections.abc import Mapping, Sequence -import dataclasses -from typing import Final - -import numpy as np - - -@dataclasses.dataclass(frozen=True, kw_only=True) -class Element: - name: str - number: int - symbol: str - weight: float - - -# Weights taken from rdkit/Code/GraphMol/atomic_data.cpp for compatibility. -# pylint: disable=invalid-name - -# X is an unknown element that can be present in the CCD, -# https://www.rcsb.org/ligand/UNX. -X: Final[Element] = Element(name='Unknown', number=0, symbol='X', weight=0.0) -H: Final[Element] = Element( - name='Hydrogen', number=1, symbol='H', weight=1.008) -He: Final[Element] = Element( - name='Helium', number=2, symbol='He', weight=4.003) -Li: Final[Element] = Element( - name='Lithium', number=3, symbol='Li', weight=6.941 -) -Be: Final[Element] = Element( - name='Beryllium', number=4, symbol='Be', weight=9.012 -) -B: Final[Element] = Element(name='Boron', number=5, symbol='B', weight=10.812) -C: Final[Element] = Element(name='Carbon', number=6, symbol='C', weight=12.011) -N: Final[Element] = Element( - name='Nitrogen', number=7, symbol='N', weight=14.007 -) -O: Final[Element] = Element(name='Oxygen', number=8, symbol='O', weight=15.999) -F: Final[Element] = Element( - name='Fluorine', number=9, symbol='F', weight=18.998 -) -Ne: Final[Element] = Element(name='Neon', number=10, symbol='Ne', weight=20.18) -Na: Final[Element] = Element( - name='Sodium', number=11, symbol='Na', weight=22.99 -) -Mg: Final[Element] = Element( - name='Magnesium', number=12, symbol='Mg', weight=24.305 -) -Al: Final[Element] = Element( - name='Aluminium', number=13, symbol='Al', weight=26.982 -) -Si: Final[Element] = Element( - name='Silicon', number=14, symbol='Si', weight=28.086 -) -P: Final[Element] = Element( - name='Phosphorus', number=15, symbol='P', weight=30.974 -) -S: Final[Element] = Element( - name='Sulfur', number=16, symbol='S', weight=32.067) -Cl: Final[Element] = Element( - name='Chlorine', number=17, symbol='Cl', weight=35.453 -) -Ar: Final[Element] = Element( - name='Argon', number=18, symbol='Ar', weight=39.948 -) -K: Final[Element] = Element( - name='Potassium', number=19, symbol='K', weight=39.098 -) -Ca: Final[Element] = Element( - name='Calcium', number=20, symbol='Ca', weight=40.078 -) -Sc: Final[Element] = Element( - name='Scandium', number=21, symbol='Sc', weight=44.956 -) -Ti: Final[Element] = Element( - name='Titanium', number=22, symbol='Ti', weight=47.867 -) -V: Final[Element] = Element( - name='Vanadium', number=23, symbol='V', weight=50.942 -) -Cr: Final[Element] = Element( - name='Chromium', number=24, symbol='Cr', weight=51.996 -) -Mn: Final[Element] = Element( - name='Manganese', number=25, symbol='Mn', weight=54.938 -) -Fe: Final[Element] = Element( - name='Iron', number=26, symbol='Fe', weight=55.845) -Co: Final[Element] = Element( - name='Cobalt', number=27, symbol='Co', weight=58.933 -) -Ni: Final[Element] = Element( - name='Nickel', number=28, symbol='Ni', weight=58.693 -) -Cu: Final[Element] = Element( - name='Copper', number=29, symbol='Cu', weight=63.546 -) -Zn: Final[Element] = Element(name='Zinc', number=30, symbol='Zn', weight=65.39) -Ga: Final[Element] = Element( - name='Gallium', number=31, symbol='Ga', weight=69.723 -) -Ge: Final[Element] = Element( - name='Germanium', number=32, symbol='Ge', weight=72.61 -) -As: Final[Element] = Element( - name='Arsenic', number=33, symbol='As', weight=74.922 -) -Se: Final[Element] = Element( - name='Selenium', number=34, symbol='Se', weight=78.96 -) -Br: Final[Element] = Element( - name='Bromine', number=35, symbol='Br', weight=79.904 -) -Kr: Final[Element] = Element( - name='Krypton', number=36, symbol='Kr', weight=83.8 -) -Rb: Final[Element] = Element( - name='Rubidium', number=37, symbol='Rb', weight=85.468 -) -Sr: Final[Element] = Element( - name='Strontium', number=38, symbol='Sr', weight=87.62 -) -Y: Final[Element] = Element( - name='Yttrium', number=39, symbol='Y', weight=88.906 -) -Zr: Final[Element] = Element( - name='Zirconium', number=40, symbol='Zr', weight=91.224 -) -Nb: Final[Element] = Element( - name='Niobium', number=41, symbol='Nb', weight=92.906 -) -Mo: Final[Element] = Element( - name='Molybdenum', number=42, symbol='Mo', weight=95.94 -) -Tc: Final[Element] = Element( - name='Technetium', number=43, symbol='Tc', weight=98 -) -Ru: Final[Element] = Element( - name='Ruthenium', number=44, symbol='Ru', weight=101.07 -) -Rh: Final[Element] = Element( - name='Rhodium', number=45, symbol='Rh', weight=102.906 -) -Pd: Final[Element] = Element( - name='Palladium', number=46, symbol='Pd', weight=106.42 -) -Ag: Final[Element] = Element( - name='Silver', number=47, symbol='Ag', weight=107.868 -) -Cd: Final[Element] = Element( - name='Cadmium', number=48, symbol='Cd', weight=112.412 -) -In: Final[Element] = Element( - name='Indium', number=49, symbol='In', weight=114.818 -) -Sn: Final[Element] = Element( - name='Tin', number=50, symbol='Sn', weight=118.711) -Sb: Final[Element] = Element( - name='Antimony', number=51, symbol='Sb', weight=121.76 -) -Te: Final[Element] = Element( - name='Tellurium', number=52, symbol='Te', weight=127.6 -) -I: Final[Element] = Element( - name='Iodine', number=53, symbol='I', weight=126.904 -) -Xe: Final[Element] = Element( - name='Xenon', number=54, symbol='Xe', weight=131.29 -) -Cs: Final[Element] = Element( - name='Caesium', number=55, symbol='Cs', weight=132.905 -) -Ba: Final[Element] = Element( - name='Barium', number=56, symbol='Ba', weight=137.328 -) -La: Final[Element] = Element( - name='Lanthanum', number=57, symbol='La', weight=138.906 -) -Ce: Final[Element] = Element( - name='Cerium', number=58, symbol='Ce', weight=140.116 -) -Pr: Final[Element] = Element( - name='Praseodymium', number=59, symbol='Pr', weight=140.908 -) -Nd: Final[Element] = Element( - name='Neodymium', number=60, symbol='Nd', weight=144.24 -) -Pm: Final[Element] = Element( - name='Promethium', number=61, symbol='Pm', weight=145 -) -Sm: Final[Element] = Element( - name='Samarium', number=62, symbol='Sm', weight=150.36 -) -Eu: Final[Element] = Element( - name='Europium', number=63, symbol='Eu', weight=151.964 -) -Gd: Final[Element] = Element( - name='Gadolinium', number=64, symbol='Gd', weight=157.25 -) -Tb: Final[Element] = Element( - name='Terbium', number=65, symbol='Tb', weight=158.925 -) -Dy: Final[Element] = Element( - name='Dysprosium', number=66, symbol='Dy', weight=162.5 -) -Ho: Final[Element] = Element( - name='Holmium', number=67, symbol='Ho', weight=164.93 -) -Er: Final[Element] = Element( - name='Erbium', number=68, symbol='Er', weight=167.26 -) -Tm: Final[Element] = Element( - name='Thulium', number=69, symbol='Tm', weight=168.934 -) -Yb: Final[Element] = Element( - name='Ytterbium', number=70, symbol='Yb', weight=173.04 -) -Lu: Final[Element] = Element( - name='Lutetium', number=71, symbol='Lu', weight=174.967 -) -Hf: Final[Element] = Element( - name='Hafnium', number=72, symbol='Hf', weight=178.49 -) -Ta: Final[Element] = Element( - name='Tantalum', number=73, symbol='Ta', weight=180.948 -) -W: Final[Element] = Element( - name='Tungsten', number=74, symbol='W', weight=183.84 -) -Re: Final[Element] = Element( - name='Rhenium', number=75, symbol='Re', weight=186.207 -) -Os: Final[Element] = Element( - name='Osmium', number=76, symbol='Os', weight=190.23 -) -Ir: Final[Element] = Element( - name='Iridium', number=77, symbol='Ir', weight=192.217 -) -Pt: Final[Element] = Element( - name='Platinum', number=78, symbol='Pt', weight=195.078 -) -Au: Final[Element] = Element( - name='Gold', number=79, symbol='Au', weight=196.967 -) -Hg: Final[Element] = Element( - name='Mercury', number=80, symbol='Hg', weight=200.59 -) -Tl: Final[Element] = Element( - name='Thallium', number=81, symbol='Tl', weight=204.383 -) -Pb: Final[Element] = Element(name='Lead', number=82, symbol='Pb', weight=207.2) -Bi: Final[Element] = Element( - name='Bismuth', number=83, symbol='Bi', weight=208.98 -) -Po: Final[Element] = Element( - name='Polonium', number=84, symbol='Po', weight=209 -) -At: Final[Element] = Element( - name='Astatine', number=85, symbol='At', weight=210 -) -Rn: Final[Element] = Element(name='Radon', number=86, symbol='Rn', weight=222) -Fr: Final[Element] = Element( - name='Francium', number=87, symbol='Fr', weight=223 -) -Ra: Final[Element] = Element(name='Radium', number=88, symbol='Ra', weight=226) -Ac: Final[Element] = Element( - name='Actinium', number=89, symbol='Ac', weight=227 -) -Th: Final[Element] = Element( - name='Thorium', number=90, symbol='Th', weight=232.038 -) -Pa: Final[Element] = Element( - name='Protactinium', number=91, symbol='Pa', weight=231.036 -) -U: Final[Element] = Element( - name='Uranium', number=92, symbol='U', weight=238.029 -) -Np: Final[Element] = Element( - name='Neptunium', number=93, symbol='Np', weight=237 -) -Pu: Final[Element] = Element( - name='Plutonium', number=94, symbol='Pu', weight=244 -) -Am: Final[Element] = Element( - name='Americium', number=95, symbol='Am', weight=243 -) -Cm: Final[Element] = Element(name='Curium', number=96, symbol='Cm', weight=247) -Bk: Final[Element] = Element( - name='Berkelium', number=97, symbol='Bk', weight=247 -) -Cf: Final[Element] = Element( - name='Californium', number=98, symbol='Cf', weight=251 -) -Es: Final[Element] = Element( - name='Einsteinium', number=99, symbol='Es', weight=252 -) -Fm: Final[Element] = Element( - name='Fermium', number=100, symbol='Fm', weight=257 -) -Md: Final[Element] = Element( - name='Mendelevium', number=101, symbol='Md', weight=258 -) -No: Final[Element] = Element( - name='Nobelium', number=102, symbol='No', weight=259 -) -Lr: Final[Element] = Element( - name='Lawrencium', number=103, symbol='Lr', weight=262 -) -Rf: Final[Element] = Element( - name='Rutherfordium', number=104, symbol='Rf', weight=267 -) -Db: Final[Element] = Element( - name='Dubnium', number=105, symbol='Db', weight=268 -) -Sg: Final[Element] = Element( - name='Seaborgium', number=106, symbol='Sg', weight=269 -) -Bh: Final[Element] = Element( - name='Bohrium', number=107, symbol='Bh', weight=270 -) -Hs: Final[Element] = Element( - name='Hassium', number=108, symbol='Hs', weight=269 -) -Mt: Final[Element] = Element( - name='Meitnerium', number=109, symbol='Mt', weight=278 -) -Ds: Final[Element] = Element( - name='Darmstadtium', number=110, symbol='Ds', weight=281 -) -Rg: Final[Element] = Element( - name='Roentgenium', number=111, symbol='Rg', weight=281 -) -Cn: Final[Element] = Element( - name='Copernicium', number=112, symbol='Cn', weight=285 -) -Nh: Final[Element] = Element( - name='Nihonium', number=113, symbol='Nh', weight=284 -) -Fl: Final[Element] = Element( - name='Flerovium', number=114, symbol='Fl', weight=289 -) -Mc: Final[Element] = Element( - name='Moscovium', number=115, symbol='Mc', weight=288 -) -Lv: Final[Element] = Element( - name='Livermorium', number=116, symbol='Lv', weight=293 -) -Ts: Final[Element] = Element( - name='Tennessine', number=117, symbol='Ts', weight=292 -) -Og: Final[Element] = Element( - name='Oganesson', number=118, symbol='Og', weight=294 -) -# pylint: enable=invalid-name - -# fmt: off -# Lanthanides -_L: Final[Sequence[Element]] = ( - La, Ce, Pr, Nd, Pm, Sm, Eu, Gd, Tb, Dy, Ho, Er, Tm, Yb, Lu) -# Actinides -_A: Final[Sequence[Element]] = ( - Ac, Th, Pa, U, Np, Pu, Am, Cm, Bk, Cf, Es, Fm, Md, No, Lr) - -# pylint: disable=bad-whitespace -PERIODIC_TABLE: Final[Sequence[Element]] = ( - X, # Unknown - H, He, - Li, Be, B, C, N, O, F, Ne, - Na, Mg, Al, Si, P, S, Cl, Ar, - K, Ca, Sc, Ti, V, Cr, Mn, Fe, Co, Ni, Cu, Zn, Ga, Ge, As, Se, Br, Kr, - Rb, Sr, Y, Zr, Nb, Mo, Tc, Ru, Rh, Pd, Ag, Cd, In, Sn, Sb, Te, I, Xe, - Cs, Ba, *_L, Hf, Ta, W, Re, Os, Ir, Pt, Au, Hg, Tl, Pb, Bi, Po, At, Rn, - Fr, Ra, *_A, Rf, Db, Sg, Bh, Hs, Mt, Ds, Rg, Cn, Nh, Fl, Mc, Lv, Ts, Og -) -# pylint: enable=bad-whitespace -# fmt: on -ATOMIC_SYMBOL: Mapping[int, str] = {e.number: e.symbol for e in PERIODIC_TABLE} -ATOMIC_NUMBER = {e.symbol: e.number for e in PERIODIC_TABLE} -# Add Deuterium as previous table contained it. -ATOMIC_NUMBER['D'] = 1 - -ATOMIC_NUMBER: Mapping[str, int] = ATOMIC_NUMBER -ATOMIC_WEIGHT: np.ndarray = np.zeros(len(PERIODIC_TABLE), dtype=np.float64) - -for e in PERIODIC_TABLE: - ATOMIC_WEIGHT[e.number] = e.weight -ATOMIC_WEIGHT.setflags(write=False) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/residue_names.py b/MindSPONGE/applications/AlphaFold3/alphafold3/constants/residue_names.py deleted file mode 100644 index 40d42587c..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/residue_names.py +++ /dev/null @@ -1,421 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Constants associated with residue names.""" - -from collections.abc import Mapping -import functools -import sys - -# pyformat: disable -# common_typos_disable -CCD_NAME_TO_ONE_LETTER: Mapping[str, str] = { - '00C': 'C', '01W': 'X', '02K': 'A', '03Y': 'C', '07O': 'C', '08P': 'C', - '0A0': 'D', '0A1': 'Y', '0A2': 'K', '0A8': 'C', '0AA': 'V', '0AB': 'V', - '0AC': 'G', '0AD': 'G', '0AF': 'W', '0AG': 'L', '0AH': 'S', '0AK': 'D', - '0AM': 'A', '0AP': 'C', '0AU': 'U', '0AV': 'A', '0AZ': 'P', '0BN': 'F', - '0C': 'C', '0CS': 'A', '0DC': 'C', '0DG': 'G', '0DT': 'T', '0FL': 'A', - '0G': 'G', '0NC': 'A', '0SP': 'A', '0U': 'U', '10C': 'C', '125': 'U', - '126': 'U', '127': 'U', '128': 'N', '12A': 'A', '143': 'C', '193': 'X', - '1AP': 'A', '1MA': 'A', '1MG': 'G', '1PA': 'F', '1PI': 'A', '1PR': 'N', - '1SC': 'C', '1TQ': 'W', '1TY': 'Y', '1X6': 'S', '200': 'F', '23F': 'F', - '23S': 'X', '26B': 'T', '2AD': 'X', '2AG': 'A', '2AO': 'X', '2AR': 'A', - '2AS': 'X', '2AT': 'T', '2AU': 'U', '2BD': 'I', '2BT': 'T', '2BU': 'A', - '2CO': 'C', '2DA': 'A', '2DF': 'N', '2DM': 'N', '2DO': 'X', '2DT': 'T', - '2EG': 'G', '2FE': 'N', '2FI': 'N', '2FM': 'M', '2GT': 'T', '2HF': 'H', - '2LU': 'L', '2MA': 'A', '2MG': 'G', '2ML': 'L', '2MR': 'R', '2MT': 'P', - '2MU': 'U', '2NT': 'T', '2OM': 'U', '2OT': 'T', '2PI': 'X', '2PR': 'G', - '2SA': 'N', '2SI': 'X', '2ST': 'T', '2TL': 'T', '2TY': 'Y', '2VA': 'V', - '2XA': 'C', '32S': 'X', '32T': 'X', '3AH': 'H', '3AR': 'X', '3CF': 'F', - '3DA': 'A', '3DR': 'N', '3GA': 'A', '3MD': 'D', '3ME': 'U', '3NF': 'Y', - '3QN': 'K', '3TY': 'X', '3XH': 'G', '4AC': 'N', '4BF': 'Y', '4CF': 'F', - '4CY': 'M', '4DP': 'W', '4FB': 'P', '4FW': 'W', '4HT': 'W', '4IN': 'W', - '4MF': 'N', '4MM': 'X', '4OC': 'C', '4PC': 'C', '4PD': 'C', '4PE': 'C', - '4PH': 'F', '4SC': 'C', '4SU': 'U', '4TA': 'N', '4U7': 'A', '56A': 'H', - '5AA': 'A', '5AB': 'A', '5AT': 'T', '5BU': 'U', '5CG': 'G', '5CM': 'C', - '5CS': 'C', '5FA': 'A', '5FC': 'C', '5FU': 'U', '5HP': 'E', '5HT': 'T', - '5HU': 'U', '5IC': 'C', '5IT': 'T', '5IU': 'U', '5MC': 'C', '5MD': 'N', - '5MU': 'U', '5NC': 'C', '5PC': 'C', '5PY': 'T', '5SE': 'U', '64T': 'T', - '6CL': 'K', '6CT': 'T', '6CW': 'W', '6HA': 'A', '6HC': 'C', '6HG': 'G', - '6HN': 'K', '6HT': 'T', '6IA': 'A', '6MA': 'A', '6MC': 'A', '6MI': 'N', - '6MT': 'A', '6MZ': 'N', '6OG': 'G', '70U': 'U', '7DA': 'A', '7GU': 'G', - '7JA': 'I', '7MG': 'G', '8AN': 'A', '8FG': 'G', '8MG': 'G', '8OG': 'G', - '9NE': 'E', '9NF': 'F', '9NR': 'R', '9NV': 'V', 'A': 'A', 'A1P': 'N', - 'A23': 'A', 'A2L': 'A', 'A2M': 'A', 'A34': 'A', 'A35': 'A', 'A38': 'A', - 'A39': 'A', 'A3A': 'A', 'A3P': 'A', 'A40': 'A', 'A43': 'A', 'A44': 'A', - 'A47': 'A', 'A5L': 'A', 'A5M': 'C', 'A5N': 'N', 'A5O': 'A', 'A66': 'X', - 'AA3': 'A', 'AA4': 'A', 'AAR': 'R', 'AB7': 'X', 'ABA': 'A', 'ABR': 'A', - 'ABS': 'A', 'ABT': 'N', 'ACB': 'D', 'ACL': 'R', 'AD2': 'A', 'ADD': 'X', - 'ADX': 'N', 'AEA': 'X', 'AEI': 'D', 'AET': 'A', 'AFA': 'N', 'AFF': 'N', - 'AFG': 'G', 'AGM': 'R', 'AGT': 'C', 'AHB': 'N', 'AHH': 'X', 'AHO': 'A', - 'AHP': 'A', 'AHS': 'X', 'AHT': 'X', 'AIB': 'A', 'AKL': 'D', 'AKZ': 'D', - 'ALA': 'A', 'ALC': 'A', 'ALM': 'A', 'ALN': 'A', 'ALO': 'T', 'ALQ': 'X', - 'ALS': 'A', 'ALT': 'A', 'ALV': 'A', 'ALY': 'K', 'AN8': 'A', 'AP7': 'A', - 'APE': 'X', 'APH': 'A', 'API': 'K', 'APK': 'K', 'APM': 'X', 'APP': 'X', - 'AR2': 'R', 'AR4': 'E', 'AR7': 'R', 'ARG': 'R', 'ARM': 'R', 'ARO': 'R', - 'ARV': 'X', 'AS': 'A', 'AS2': 'D', 'AS9': 'X', 'ASA': 'D', 'ASB': 'D', - 'ASI': 'D', 'ASK': 'D', 'ASL': 'D', 'ASM': 'X', 'ASN': 'N', 'ASP': 'D', - 'ASQ': 'D', 'ASU': 'N', 'ASX': 'B', 'ATD': 'T', 'ATL': 'T', 'ATM': 'T', - 'AVC': 'A', 'AVN': 'X', 'AYA': 'A', 'AZK': 'K', 'AZS': 'S', 'AZY': 'Y', - 'B1F': 'F', 'B1P': 'N', 'B2A': 'A', 'B2F': 'F', 'B2I': 'I', 'B2V': 'V', - 'B3A': 'A', 'B3D': 'D', 'B3E': 'E', 'B3K': 'K', 'B3L': 'X', 'B3M': 'X', - 'B3Q': 'X', 'B3S': 'S', 'B3T': 'X', 'B3U': 'H', 'B3X': 'N', 'B3Y': 'Y', - 'BB6': 'C', 'BB7': 'C', 'BB8': 'F', 'BB9': 'C', 'BBC': 'C', 'BCS': 'C', - 'BE2': 'X', 'BFD': 'D', 'BG1': 'S', 'BGM': 'G', 'BH2': 'D', 'BHD': 'D', - 'BIF': 'F', 'BIL': 'X', 'BIU': 'I', 'BJH': 'X', 'BLE': 'L', 'BLY': 'K', - 'BMP': 'N', 'BMT': 'T', 'BNN': 'F', 'BNO': 'X', 'BOE': 'T', 'BOR': 'R', - 'BPE': 'C', 'BRU': 'U', 'BSE': 'S', 'BT5': 'N', 'BTA': 'L', 'BTC': 'C', - 'BTR': 'W', 'BUC': 'C', 'BUG': 'V', 'BVP': 'U', 'BZG': 'N', 'C': 'C', - 'C1X': 'K', 'C25': 'C', 'C2L': 'C', 'C2S': 'C', 'C31': 'C', 'C32': 'C', - 'C34': 'C', 'C36': 'C', 'C37': 'C', 'C38': 'C', 'C3Y': 'C', 'C42': 'C', - 'C43': 'C', 'C45': 'C', 'C46': 'C', 'C49': 'C', 'C4R': 'C', 'C4S': 'C', - 'C5C': 'C', 'C66': 'X', 'C6C': 'C', 'CAF': 'C', 'CAL': 'X', 'CAR': 'C', - 'CAS': 'C', 'CAV': 'X', 'CAY': 'C', 'CB2': 'C', 'CBR': 'C', 'CBV': 'C', - 'CCC': 'C', 'CCL': 'K', 'CCS': 'C', 'CDE': 'X', 'CDV': 'X', 'CDW': 'C', - 'CEA': 'C', 'CFL': 'C', 'CG1': 'G', 'CGA': 'E', 'CGU': 'E', 'CH': 'C', - 'CHF': 'X', 'CHG': 'X', 'CHP': 'G', 'CHS': 'X', 'CIR': 'R', 'CLE': 'L', - 'CLG': 'K', 'CLH': 'K', 'CM0': 'N', 'CME': 'C', 'CMH': 'C', 'CML': 'C', - 'CMR': 'C', 'CMT': 'C', 'CNU': 'U', 'CP1': 'C', 'CPC': 'X', 'CPI': 'X', - 'CR5': 'G', 'CS0': 'C', 'CS1': 'C', 'CS3': 'C', 'CS4': 'C', 'CS8': 'N', - 'CSA': 'C', 'CSB': 'C', 'CSD': 'C', 'CSE': 'C', 'CSF': 'C', 'CSI': 'G', - 'CSJ': 'C', 'CSL': 'C', 'CSO': 'C', 'CSP': 'C', 'CSR': 'C', 'CSS': 'C', - 'CSU': 'C', 'CSW': 'C', 'CSX': 'C', 'CSZ': 'C', 'CTE': 'W', 'CTG': 'T', - 'CTH': 'T', 'CUC': 'X', 'CWR': 'S', 'CXM': 'M', 'CY0': 'C', 'CY1': 'C', - 'CY3': 'C', 'CY4': 'C', 'CYA': 'C', 'CYD': 'C', 'CYF': 'C', 'CYG': 'C', - 'CYJ': 'X', 'CYM': 'C', 'CYQ': 'C', 'CYR': 'C', 'CYS': 'C', 'CZ2': 'C', - 'CZZ': 'C', 'D11': 'T', 'D1P': 'N', 'D3': 'N', 'D33': 'N', 'D3P': 'G', - 'D3T': 'T', 'D4M': 'T', 'D4P': 'X', 'DA': 'A', 'DA2': 'X', 'DAB': 'A', - 'DAH': 'F', 'DAL': 'A', 'DAR': 'R', 'DAS': 'D', 'DBB': 'T', 'DBM': 'N', - 'DBS': 'S', 'DBU': 'T', 'DBY': 'Y', 'DBZ': 'A', 'DC': 'C', 'DC2': 'C', - 'DCG': 'G', 'DCI': 'X', 'DCL': 'X', 'DCT': 'C', 'DCY': 'C', 'DDE': 'H', - 'DDG': 'G', 'DDN': 'U', 'DDX': 'N', 'DFC': 'C', 'DFG': 'G', 'DFI': 'X', - 'DFO': 'X', 'DFT': 'N', 'DG': 'G', 'DGH': 'G', 'DGI': 'G', 'DGL': 'E', - 'DGN': 'Q', 'DHA': 'S', 'DHI': 'H', 'DHL': 'X', 'DHN': 'V', 'DHP': 'X', - 'DHU': 'U', 'DHV': 'V', 'DI': 'I', 'DIL': 'I', 'DIR': 'R', 'DIV': 'V', - 'DLE': 'L', 'DLS': 'K', 'DLY': 'K', 'DM0': 'K', 'DMH': 'N', 'DMK': 'D', - 'DMT': 'X', 'DN': 'N', 'DNE': 'L', 'DNG': 'L', 'DNL': 'K', 'DNM': 'L', - 'DNP': 'A', 'DNR': 'C', 'DNS': 'K', 'DOA': 'X', 'DOC': 'C', 'DOH': 'D', - 'DON': 'L', 'DPB': 'T', 'DPH': 'F', 'DPL': 'P', 'DPP': 'A', 'DPQ': 'Y', - 'DPR': 'P', 'DPY': 'N', 'DRM': 'U', 'DRP': 'N', 'DRT': 'T', 'DRZ': 'N', - 'DSE': 'S', 'DSG': 'N', 'DSN': 'S', 'DSP': 'D', 'DT': 'T', 'DTH': 'T', - 'DTR': 'W', 'DTY': 'Y', 'DU': 'U', 'DVA': 'V', 'DXD': 'N', 'DXN': 'N', - 'DYS': 'C', 'DZM': 'A', 'E': 'A', 'E1X': 'A', 'ECC': 'Q', 'EDA': 'A', - 'EFC': 'C', 'EHP': 'F', 'EIT': 'T', 'ENP': 'N', 'ESB': 'Y', 'ESC': 'M', - 'EXB': 'X', 'EXY': 'L', 'EY5': 'N', 'EYS': 'X', 'F2F': 'F', 'FA2': 'A', - 'FA5': 'N', 'FAG': 'N', 'FAI': 'N', 'FB5': 'A', 'FB6': 'A', 'FCL': 'F', - 'FFD': 'N', 'FGA': 'E', 'FGL': 'G', 'FGP': 'S', 'FHL': 'X', 'FHO': 'K', - 'FHU': 'U', 'FLA': 'A', 'FLE': 'L', 'FLT': 'Y', 'FME': 'M', 'FMG': 'G', - 'FMU': 'N', 'FOE': 'C', 'FOX': 'G', 'FP9': 'P', 'FPA': 'F', 'FRD': 'X', - 'FT6': 'W', 'FTR': 'W', 'FTY': 'Y', 'FVA': 'V', 'FZN': 'K', 'G': 'G', - 'G25': 'G', 'G2L': 'G', 'G2S': 'G', 'G31': 'G', 'G32': 'G', 'G33': 'G', - 'G36': 'G', 'G38': 'G', 'G42': 'G', 'G46': 'G', 'G47': 'G', 'G48': 'G', - 'G49': 'G', 'G4P': 'N', 'G7M': 'G', 'GAO': 'G', 'GAU': 'E', 'GCK': 'C', - 'GCM': 'X', 'GDP': 'G', 'GDR': 'G', 'GFL': 'G', 'GGL': 'E', 'GH3': 'G', - 'GHG': 'Q', 'GHP': 'G', 'GL3': 'G', 'GLH': 'Q', 'GLJ': 'E', 'GLK': 'E', - 'GLM': 'X', 'GLN': 'Q', 'GLQ': 'E', 'GLU': 'E', 'GLX': 'Z', 'GLY': 'G', - 'GLZ': 'G', 'GMA': 'E', 'GMS': 'G', 'GMU': 'U', 'GN7': 'G', 'GND': 'X', - 'GNE': 'N', 'GOM': 'G', 'GPL': 'K', 'GS': 'G', 'GSC': 'G', 'GSR': 'G', - 'GSS': 'G', 'GSU': 'E', 'GT9': 'C', 'GTP': 'G', 'GVL': 'X', 'H2U': 'U', - 'H5M': 'P', 'HAC': 'A', 'HAR': 'R', 'HBN': 'H', 'HCS': 'X', 'HDP': 'U', - 'HEU': 'U', 'HFA': 'X', 'HGL': 'X', 'HHI': 'H', 'HIA': 'H', 'HIC': 'H', - 'HIP': 'H', 'HIQ': 'H', 'HIS': 'H', 'HL2': 'L', 'HLU': 'L', 'HMR': 'R', - 'HOL': 'N', 'HPC': 'F', 'HPE': 'F', 'HPH': 'F', 'HPQ': 'F', 'HQA': 'A', - 'HRG': 'R', 'HRP': 'W', 'HS8': 'H', 'HS9': 'H', 'HSE': 'S', 'HSL': 'S', - 'HSO': 'H', 'HTI': 'C', 'HTN': 'N', 'HTR': 'W', 'HV5': 'A', 'HVA': 'V', - 'HY3': 'P', 'HYP': 'P', 'HZP': 'P', 'I': 'I', 'I2M': 'I', 'I58': 'K', - 'I5C': 'C', 'IAM': 'A', 'IAR': 'R', 'IAS': 'D', 'IC': 'C', 'IEL': 'K', - 'IG': 'G', 'IGL': 'G', 'IGU': 'G', 'IIL': 'I', 'ILE': 'I', 'ILG': 'E', - 'ILX': 'I', 'IMC': 'C', 'IML': 'I', 'IOY': 'F', 'IPG': 'G', 'IPN': 'N', - 'IRN': 'N', 'IT1': 'K', 'IU': 'U', 'IYR': 'Y', 'IYT': 'T', 'IZO': 'M', - 'JJJ': 'C', 'JJK': 'C', 'JJL': 'C', 'JW5': 'N', 'K1R': 'C', 'KAG': 'G', - 'KCX': 'K', 'KGC': 'K', 'KNB': 'A', 'KOR': 'M', 'KPI': 'K', 'KST': 'K', - 'KYQ': 'K', 'L2A': 'X', 'LA2': 'K', 'LAA': 'D', 'LAL': 'A', 'LBY': 'K', - 'LC': 'C', 'LCA': 'A', 'LCC': 'N', 'LCG': 'G', 'LCH': 'N', 'LCK': 'K', - 'LCX': 'K', 'LDH': 'K', 'LED': 'L', 'LEF': 'L', 'LEH': 'L', 'LEI': 'V', - 'LEM': 'L', 'LEN': 'L', 'LET': 'X', 'LEU': 'L', 'LEX': 'L', 'LG': 'G', - 'LGP': 'G', 'LHC': 'X', 'LHU': 'U', 'LKC': 'N', 'LLP': 'K', 'LLY': 'K', - 'LME': 'E', 'LMF': 'K', 'LMQ': 'Q', 'LMS': 'N', 'LP6': 'K', 'LPD': 'P', - 'LPG': 'G', 'LPL': 'X', 'LPS': 'S', 'LSO': 'X', 'LTA': 'X', 'LTR': 'W', - 'LVG': 'G', 'LVN': 'V', 'LYF': 'K', 'LYK': 'K', 'LYM': 'K', 'LYN': 'K', - 'LYR': 'K', 'LYS': 'K', 'LYX': 'K', 'LYZ': 'K', 'M0H': 'C', 'M1G': 'G', - 'M2G': 'G', 'M2L': 'K', 'M2S': 'M', 'M30': 'G', 'M3L': 'K', 'M5M': 'C', - 'MA': 'A', 'MA6': 'A', 'MA7': 'A', 'MAA': 'A', 'MAD': 'A', 'MAI': 'R', - 'MBQ': 'Y', 'MBZ': 'N', 'MC1': 'S', 'MCG': 'X', 'MCL': 'K', 'MCS': 'C', - 'MCY': 'C', 'MD3': 'C', 'MD6': 'G', 'MDH': 'X', 'MDR': 'N', 'MEA': 'F', - 'MED': 'M', 'MEG': 'E', 'MEN': 'N', 'MEP': 'U', 'MEQ': 'Q', 'MET': 'M', - 'MEU': 'G', 'MF3': 'X', 'MG1': 'G', 'MGG': 'R', 'MGN': 'Q', 'MGQ': 'A', - 'MGV': 'G', 'MGY': 'G', 'MHL': 'L', 'MHO': 'M', 'MHS': 'H', 'MIA': 'A', - 'MIS': 'S', 'MK8': 'L', 'ML3': 'K', 'MLE': 'L', 'MLL': 'L', 'MLY': 'K', - 'MLZ': 'K', 'MME': 'M', 'MMO': 'R', 'MMT': 'T', 'MND': 'N', 'MNL': 'L', - 'MNU': 'U', 'MNV': 'V', 'MOD': 'X', 'MP8': 'P', 'MPH': 'X', 'MPJ': 'X', - 'MPQ': 'G', 'MRG': 'G', 'MSA': 'G', 'MSE': 'M', 'MSL': 'M', 'MSO': 'M', - 'MSP': 'X', 'MT2': 'M', 'MTR': 'T', 'MTU': 'A', 'MTY': 'Y', 'MVA': 'V', - 'N': 'N', 'N10': 'S', 'N2C': 'X', 'N5I': 'N', 'N5M': 'C', 'N6G': 'G', - 'N7P': 'P', 'NA8': 'A', 'NAL': 'A', 'NAM': 'A', 'NB8': 'N', 'NBQ': 'Y', - 'NC1': 'S', 'NCB': 'A', 'NCX': 'N', 'NCY': 'X', 'NDF': 'F', 'NDN': 'U', - 'NEM': 'H', 'NEP': 'H', 'NF2': 'N', 'NFA': 'F', 'NHL': 'E', 'NIT': 'X', - 'NIY': 'Y', 'NLE': 'L', 'NLN': 'L', 'NLO': 'L', 'NLP': 'L', 'NLQ': 'Q', - 'NMC': 'G', 'NMM': 'R', 'NMS': 'T', 'NMT': 'T', 'NNH': 'R', 'NP3': 'N', - 'NPH': 'C', 'NPI': 'A', 'NSK': 'X', 'NTY': 'Y', 'NVA': 'V', 'NYM': 'N', - 'NYS': 'C', 'NZH': 'H', 'O12': 'X', 'O2C': 'N', 'O2G': 'G', 'OAD': 'N', - 'OAS': 'S', 'OBF': 'X', 'OBS': 'X', 'OCS': 'C', 'OCY': 'C', 'ODP': 'N', - 'OHI': 'H', 'OHS': 'D', 'OIC': 'X', 'OIP': 'I', 'OLE': 'X', 'OLT': 'T', - 'OLZ': 'S', 'OMC': 'C', 'OMG': 'G', 'OMT': 'M', 'OMU': 'U', 'ONE': 'U', - 'ONH': 'A', 'ONL': 'X', 'OPR': 'R', 'ORN': 'A', 'ORQ': 'R', 'OSE': 'S', - 'OTB': 'X', 'OTH': 'T', 'OTY': 'Y', 'OXX': 'D', 'P': 'G', 'P1L': 'C', - 'P1P': 'N', 'P2T': 'T', 'P2U': 'U', 'P2Y': 'P', 'P5P': 'A', 'PAQ': 'Y', - 'PAS': 'D', 'PAT': 'W', 'PAU': 'A', 'PBB': 'C', 'PBF': 'F', 'PBT': 'N', - 'PCA': 'E', 'PCC': 'P', 'PCE': 'X', 'PCS': 'F', 'PDL': 'X', 'PDU': 'U', - 'PEC': 'C', 'PF5': 'F', 'PFF': 'F', 'PFX': 'X', 'PG1': 'S', 'PG7': 'G', - 'PG9': 'G', 'PGL': 'X', 'PGN': 'G', 'PGP': 'G', 'PGY': 'G', 'PHA': 'F', - 'PHD': 'D', 'PHE': 'F', 'PHI': 'F', 'PHL': 'F', 'PHM': 'F', 'PIV': 'X', - 'PLE': 'L', 'PM3': 'F', 'PMT': 'C', 'POM': 'P', 'PPN': 'F', 'PPU': 'A', - 'PPW': 'G', 'PQ1': 'N', 'PR3': 'C', 'PR5': 'A', 'PR9': 'P', 'PRN': 'A', - 'PRO': 'P', 'PRS': 'P', 'PSA': 'F', 'PSH': 'H', 'PST': 'T', 'PSU': 'U', - 'PSW': 'C', 'PTA': 'X', 'PTH': 'Y', 'PTM': 'Y', 'PTR': 'Y', 'PU': 'A', - 'PUY': 'N', 'PVH': 'H', 'PVL': 'X', 'PYA': 'A', 'PYO': 'U', 'PYX': 'C', - 'PYY': 'N', 'QMM': 'Q', 'QPA': 'C', 'QPH': 'F', 'QUO': 'G', 'R': 'A', - 'R1A': 'C', 'R4K': 'W', 'RE0': 'W', 'RE3': 'W', 'RIA': 'A', 'RMP': 'A', - 'RON': 'X', 'RT': 'T', 'RTP': 'N', 'S1H': 'S', 'S2C': 'C', 'S2D': 'A', - 'S2M': 'T', 'S2P': 'A', 'S4A': 'A', 'S4C': 'C', 'S4G': 'G', 'S4U': 'U', - 'S6G': 'G', 'SAC': 'S', 'SAH': 'C', 'SAR': 'G', 'SBL': 'S', 'SC': 'C', - 'SCH': 'C', 'SCS': 'C', 'SCY': 'C', 'SD2': 'X', 'SDG': 'G', 'SDP': 'S', - 'SEB': 'S', 'SEC': 'A', 'SEG': 'A', 'SEL': 'S', 'SEM': 'S', 'SEN': 'S', - 'SEP': 'S', 'SER': 'S', 'SET': 'S', 'SGB': 'S', 'SHC': 'C', 'SHP': 'G', - 'SHR': 'K', 'SIB': 'C', 'SLA': 'P', 'SLR': 'P', 'SLZ': 'K', 'SMC': 'C', - 'SME': 'M', 'SMF': 'F', 'SMP': 'A', 'SMT': 'T', 'SNC': 'C', 'SNN': 'N', - 'SOC': 'C', 'SOS': 'N', 'SOY': 'S', 'SPT': 'T', 'SRA': 'A', 'SSU': 'U', - 'STY': 'Y', 'SUB': 'X', 'SUN': 'S', 'SUR': 'U', 'SVA': 'S', 'SVV': 'S', - 'SVW': 'S', 'SVX': 'S', 'SVY': 'S', 'SVZ': 'X', 'SYS': 'C', 'T': 'T', - 'T11': 'F', 'T23': 'T', 'T2S': 'T', 'T2T': 'N', 'T31': 'U', 'T32': 'T', - 'T36': 'T', 'T37': 'T', 'T38': 'T', 'T39': 'T', 'T3P': 'T', 'T41': 'T', - 'T48': 'T', 'T49': 'T', 'T4S': 'T', 'T5O': 'U', 'T5S': 'T', 'T66': 'X', - 'T6A': 'A', 'TA3': 'T', 'TA4': 'X', 'TAF': 'T', 'TAL': 'N', 'TAV': 'D', - 'TBG': 'V', 'TBM': 'T', 'TC1': 'C', 'TCP': 'T', 'TCQ': 'Y', 'TCR': 'W', - 'TCY': 'A', 'TDD': 'L', 'TDY': 'T', 'TFE': 'T', 'TFO': 'A', 'TFQ': 'F', - 'TFT': 'T', 'TGP': 'G', 'TH6': 'T', 'THC': 'T', 'THO': 'X', 'THR': 'T', - 'THX': 'N', 'THZ': 'R', 'TIH': 'A', 'TLB': 'N', 'TLC': 'T', 'TLN': 'U', - 'TMB': 'T', 'TMD': 'T', 'TNB': 'C', 'TNR': 'S', 'TOX': 'W', 'TP1': 'T', - 'TPC': 'C', 'TPG': 'G', 'TPH': 'X', 'TPL': 'W', 'TPO': 'T', 'TPQ': 'Y', - 'TQI': 'W', 'TQQ': 'W', 'TRF': 'W', 'TRG': 'K', 'TRN': 'W', 'TRO': 'W', - 'TRP': 'W', 'TRQ': 'W', 'TRW': 'W', 'TRX': 'W', 'TS': 'N', 'TST': 'X', - 'TT': 'N', 'TTD': 'T', 'TTI': 'U', 'TTM': 'T', 'TTQ': 'W', 'TTS': 'Y', - 'TY1': 'Y', 'TY2': 'Y', 'TY3': 'Y', 'TY5': 'Y', 'TYB': 'Y', 'TYI': 'Y', - 'TYJ': 'Y', 'TYN': 'Y', 'TYO': 'Y', 'TYQ': 'Y', 'TYR': 'Y', 'TYS': 'Y', - 'TYT': 'Y', 'TYU': 'N', 'TYW': 'Y', 'TYX': 'X', 'TYY': 'Y', 'TZB': 'X', - 'TZO': 'X', 'U': 'U', 'U25': 'U', 'U2L': 'U', 'U2N': 'U', 'U2P': 'U', - 'U31': 'U', 'U33': 'U', 'U34': 'U', 'U36': 'U', 'U37': 'U', 'U8U': 'U', - 'UAR': 'U', 'UCL': 'U', 'UD5': 'U', 'UDP': 'N', 'UFP': 'N', 'UFR': 'U', - 'UFT': 'U', 'UMA': 'A', 'UMP': 'U', 'UMS': 'U', 'UN1': 'X', 'UN2': 'X', - 'UNK': 'X', 'UR3': 'U', 'URD': 'U', 'US1': 'U', 'US2': 'U', 'US3': 'T', - 'US5': 'U', 'USM': 'U', 'VAD': 'V', 'VAF': 'V', 'VAL': 'V', 'VB1': 'K', - 'VDL': 'X', 'VLL': 'X', 'VLM': 'X', 'VMS': 'X', 'VOL': 'X', 'X': 'G', - 'X2W': 'E', 'X4A': 'N', 'XAD': 'A', 'XAE': 'N', 'XAL': 'A', 'XAR': 'N', - 'XCL': 'C', 'XCN': 'C', 'XCP': 'X', 'XCR': 'C', 'XCS': 'N', 'XCT': 'C', - 'XCY': 'C', 'XGA': 'N', 'XGL': 'G', 'XGR': 'G', 'XGU': 'G', 'XPR': 'P', - 'XSN': 'N', 'XTH': 'T', 'XTL': 'T', 'XTR': 'T', 'XTS': 'G', 'XTY': 'N', - 'XUA': 'A', 'XUG': 'G', 'XX1': 'K', 'Y': 'A', 'YCM': 'C', 'YG': 'G', - 'YOF': 'Y', 'YRR': 'N', 'YYG': 'G', 'Z': 'C', 'Z01': 'A', 'ZAD': 'A', - 'ZAL': 'A', 'ZBC': 'C', 'ZBU': 'U', 'ZCL': 'F', 'ZCY': 'C', 'ZDU': 'U', - 'ZFB': 'X', 'ZGU': 'G', 'ZHP': 'N', 'ZTH': 'T', 'ZU0': 'T', 'ZZJ': 'A', -} -# common_typos_enable -# pyformat: enable - - -@functools.lru_cache(maxsize=64) -def letters_three_to_one(restype: str, *, default: str) -> str: - """Returns single letter name if one exists otherwise returns default.""" - return CCD_NAME_TO_ONE_LETTER.get(restype, default) - - -ALA = sys.intern('ALA') -ARG = sys.intern('ARG') -ASN = sys.intern('ASN') -ASP = sys.intern('ASP') -CYS = sys.intern('CYS') -GLN = sys.intern('GLN') -GLU = sys.intern('GLU') -GLY = sys.intern('GLY') -HIS = sys.intern('HIS') -ILE = sys.intern('ILE') -LEU = sys.intern('LEU') -LYS = sys.intern('LYS') -MET = sys.intern('MET') -PHE = sys.intern('PHE') -PRO = sys.intern('PRO') -SER = sys.intern('SER') -THR = sys.intern('THR') -TRP = sys.intern('TRP') -TYR = sys.intern('TYR') -VAL = sys.intern('VAL') -UNK = sys.intern('UNK') -GAP = sys.intern('-') - -# Unknown ligand. -UNL = sys.intern('UNL') - -# Non-standard version of MET (with Se instead of S), but often appears in PDB. -MSE = sys.intern('MSE') - -# 20 standard protein amino acids (no unknown). -PROTEIN_TYPES: tuple[str, ...] = ( - ALA, ARG, ASN, ASP, CYS, GLN, GLU, GLY, HIS, ILE, LEU, LYS, MET, PHE, PRO, - SER, THR, TRP, TYR, VAL, -) # pyformat: disable - -# 20 standard protein amino acids plus the unknown (UNK) amino acid. -PROTEIN_TYPES_WITH_UNKNOWN: tuple[str, ...] = PROTEIN_TYPES + (UNK,) - -# This is the standard residue order when coding AA type as a number. -# Reproduce it by taking 3-letter AA codes and sorting them alphabetically. -# For legacy reasons this only refers to protein residues. - -PROTEIN_TYPES_ONE_LETTER: tuple[str, ...] = ( - 'A', 'R', 'N', 'D', 'C', 'Q', 'E', 'G', 'H', 'I', 'L', 'K', 'M', 'F', 'P', - 'S', 'T', 'W', 'Y', 'V', -) # pyformat: disable - -PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN: tuple[str, ...] = ( - PROTEIN_TYPES_ONE_LETTER + ('X',) -) -PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP: tuple[str, ...] = ( - PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN + (GAP,) -) - -PROTEIN_TYPES_ONE_LETTER_TO_INT: Mapping[str, int] = { - r: i for i, r in enumerate(PROTEIN_TYPES_ONE_LETTER) -} -PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_TO_INT: Mapping[str, int] = { - r: i for i, r in enumerate(PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN) -} - -PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP_TO_INT: Mapping[str, int] = { - r: i for i, r in enumerate(PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP) -} - - -PROTEIN_COMMON_ONE_TO_THREE: Mapping[str, str] = { - 'A': ALA, - 'R': ARG, - 'N': ASN, - 'D': ASP, - 'C': CYS, - 'Q': GLN, - 'E': GLU, - 'G': GLY, - 'H': HIS, - 'I': ILE, - 'L': LEU, - 'K': LYS, - 'M': MET, - 'F': PHE, - 'P': PRO, - 'S': SER, - 'T': THR, - 'W': TRP, - 'Y': TYR, - 'V': VAL, -} - -PROTEIN_COMMON_THREE_TO_ONE: Mapping[str, str] = { - v: k for k, v in PROTEIN_COMMON_ONE_TO_THREE.items() -} - -A = sys.intern('A') -G = sys.intern('G') -C = sys.intern('C') -U = sys.intern('U') -T = sys.intern('T') - -DA = sys.intern('DA') -DG = sys.intern('DG') -DC = sys.intern('DC') -DT = sys.intern('DT') - -UNK_NUCLEIC_ONE_LETTER = sys.intern('N') # Unknown nucleic acid single letter. -UNK_RNA = sys.intern('N') # Unknown RNA. -UNK_DNA = sys.intern('DN') # Unknown DNA residue (differs from N). - -RNA_TYPES: tuple[str, ...] = (A, G, C, U) -DNA_TYPES: tuple[str, ...] = (DA, DG, DC, DT) - -NUCLEIC_TYPES: tuple[str, ...] = RNA_TYPES + DNA_TYPES -# Without UNK DNA. -NUCLEIC_TYPES_WITH_UNKNOWN: tuple[str, ...] = NUCLEIC_TYPES + ( - UNK_NUCLEIC_ONE_LETTER, -) -NUCLEIC_TYPES_WITH_2_UNKS: tuple[str, ...] = NUCLEIC_TYPES + ( - UNK_RNA, - UNK_DNA, -) - -RNA_TYPES_ONE_LETTER_WITH_UNKNOWN: tuple[str, ...] = RNA_TYPES + (UNK_RNA,) -RNA_TYPES_ONE_LETTER_WITH_UNKNOWN_TO_INT: Mapping[str, int] = { - r: i for i, r in enumerate(RNA_TYPES_ONE_LETTER_WITH_UNKNOWN) -} - -DNA_TYPES_WITH_UNKNOWN: tuple[str, ...] = DNA_TYPES + (UNK_DNA,) -DNA_TYPES_ONE_LETTER: tuple[str, ...] = (A, G, C, T) -DNA_TYPES_ONE_LETTER_WITH_UNKNOWN: tuple[str, ...] = DNA_TYPES_ONE_LETTER + ( - UNK_NUCLEIC_ONE_LETTER, -) -DNA_TYPES_ONE_LETTER_WITH_UNKNOWN_TO_INT: Mapping[str, int] = { - r: i for i, r in enumerate(DNA_TYPES_ONE_LETTER_WITH_UNKNOWN) -} -DNA_COMMON_ONE_TO_TWO: Mapping[str, str] = { - 'A': 'DA', - 'G': 'DG', - 'C': 'DC', - 'T': 'DT', -} - -STANDARD_POLYMER_TYPES: tuple[str, ...] = PROTEIN_TYPES + NUCLEIC_TYPES -POLYMER_TYPES: tuple[str, ...] = PROTEIN_TYPES_WITH_UNKNOWN + NUCLEIC_TYPES -POLYMER_TYPES_WITH_UNKNOWN: tuple[str, ...] = ( - PROTEIN_TYPES_WITH_UNKNOWN + NUCLEIC_TYPES_WITH_UNKNOWN -) -POLYMER_TYPES_WITH_GAP: tuple[str, ...] = PROTEIN_TYPES + \ - (GAP,) + NUCLEIC_TYPES -POLYMER_TYPES_WITH_UNKNOWN_AND_GAP: tuple[str, ...] = ( - PROTEIN_TYPES_WITH_UNKNOWN + (GAP,) + NUCLEIC_TYPES_WITH_UNKNOWN -) -POLYMER_TYPES_WITH_ALL_UNKS_AND_GAP: tuple[str, ...] = ( - PROTEIN_TYPES_WITH_UNKNOWN + (GAP,) + NUCLEIC_TYPES_WITH_2_UNKS -) - -POLYMER_TYPES_ORDER = {restype: i for i, restype in enumerate(POLYMER_TYPES)} - -POLYMER_TYPES_ORDER_WITH_UNKNOWN = { - restype: i for i, restype in enumerate(POLYMER_TYPES_WITH_UNKNOWN) -} - -POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP = { - restype: i for i, restype in enumerate(POLYMER_TYPES_WITH_UNKNOWN_AND_GAP) -} - -POLYMER_TYPES_ORDER_WITH_ALL_UNKS_AND_GAP = { - restype: i for i, restype in enumerate(POLYMER_TYPES_WITH_ALL_UNKS_AND_GAP) -} - -POLYMER_TYPES_NUM = len(POLYMER_TYPES) # := 29. -POLYMER_TYPES_NUM_WITH_UNKNOWN = len(POLYMER_TYPES_WITH_UNKNOWN) # := 30. -POLYMER_TYPES_NUM_WITH_GAP = len(POLYMER_TYPES_WITH_GAP) # := 29. -POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP = len( - POLYMER_TYPES_WITH_UNKNOWN_AND_GAP -) # := 31. -POLYMER_TYPES_NUM_ORDER_WITH_ALL_UNKS_AND_GAP = len( - POLYMER_TYPES_WITH_ALL_UNKS_AND_GAP -) # := 32. - -WATER_TYPES: tuple[str, ...] = ('HOH', 'DOD') - -UNKNOWN_TYPES: tuple[str, ...] = (UNK, UNK_RNA, UNK_DNA, UNL) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/side_chains.py b/MindSPONGE/applications/AlphaFold3/alphafold3/constants/side_chains.py deleted file mode 100644 index 0e8cd1297..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/constants/side_chains.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Constants associated with side chains.""" - -from collections.abc import Mapping, Sequence -import itertools - -# Format: The list for each AA type contains chi1, chi2, chi3, chi4 in -# this order (or a relevant subset from chi1 onwards). ALA and GLY don't have -# chi angles so their chi angle lists are empty. -CHI_ANGLES_ATOMS: Mapping[str, Sequence[tuple[str, ...]]] = { - 'ALA': [], - # Chi5 in arginine is always 0 +- 5 degrees, so ignore it. - 'ARG': [ - ('N', 'CA', 'CB', 'CG'), - ('CA', 'CB', 'CG', 'CD'), - ('CB', 'CG', 'CD', 'NE'), - ('CG', 'CD', 'NE', 'CZ'), - ], - 'ASN': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'OD1')], - 'ASP': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'OD1')], - 'CYS': [('N', 'CA', 'CB', 'SG')], - 'GLN': [ - ('N', 'CA', 'CB', 'CG'), - ('CA', 'CB', 'CG', 'CD'), - ('CB', 'CG', 'CD', 'OE1'), - ], - 'GLU': [ - ('N', 'CA', 'CB', 'CG'), - ('CA', 'CB', 'CG', 'CD'), - ('CB', 'CG', 'CD', 'OE1'), - ], - 'GLY': [], - 'HIS': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'ND1')], - 'ILE': [('N', 'CA', 'CB', 'CG1'), ('CA', 'CB', 'CG1', 'CD1')], - 'LEU': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')], - 'LYS': [ - ('N', 'CA', 'CB', 'CG'), - ('CA', 'CB', 'CG', 'CD'), - ('CB', 'CG', 'CD', 'CE'), - ('CG', 'CD', 'CE', 'NZ'), - ], - 'MET': [ - ('N', 'CA', 'CB', 'CG'), - ('CA', 'CB', 'CG', 'SD'), - ('CB', 'CG', 'SD', 'CE'), - ], - 'PHE': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')], - 'PRO': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD')], - 'SER': [('N', 'CA', 'CB', 'OG')], - 'THR': [('N', 'CA', 'CB', 'OG1')], - 'TRP': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')], - 'TYR': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')], - 'VAL': [('N', 'CA', 'CB', 'CG1')], -} - -CHI_GROUPS_FOR_ATOM = {} -for res_name, chi_angle_atoms_for_res in CHI_ANGLES_ATOMS.items(): - for chi_group_i, chi_group in enumerate(chi_angle_atoms_for_res): - for atom_i, atom in enumerate(chi_group): - CHI_GROUPS_FOR_ATOM.setdefault((res_name, atom), []).append( - (chi_group_i, atom_i) - ) - -# Mapping from (residue_name, atom_name) pairs to the atom's chi group index -# and atom index within that group. -CHI_GROUPS_FOR_ATOM: Mapping[tuple[str, str], Sequence[tuple[int, int]]] = ( - CHI_GROUPS_FOR_ATOM -) - -MAX_NUM_CHI_ANGLES: int = 4 -ATOMS_PER_CHI_ANGLE: int = 4 - -# A list of atoms for each AA type that are involved in chi angle calculations. -CHI_ATOM_SETS: Mapping[str, set[str]] = { - residue_name: set(itertools.chain(*atoms)) - for residue_name, atoms in CHI_ANGLES_ATOMS.items() -} - -# If chi angles given in fixed-length array, this matrix determines how to mask -# them for each AA type. The order is as per restype_order (see below). -CHI_ANGLES_MASK: Sequence[Sequence[float]] = ( - (0.0, 0.0, 0.0, 0.0), # ALA - (1.0, 1.0, 1.0, 1.0), # ARG - (1.0, 1.0, 0.0, 0.0), # ASN - (1.0, 1.0, 0.0, 0.0), # ASP - (1.0, 0.0, 0.0, 0.0), # CYS - (1.0, 1.0, 1.0, 0.0), # GLN - (1.0, 1.0, 1.0, 0.0), # GLU - (0.0, 0.0, 0.0, 0.0), # GLY - (1.0, 1.0, 0.0, 0.0), # HIS - (1.0, 1.0, 0.0, 0.0), # ILE - (1.0, 1.0, 0.0, 0.0), # LEU - (1.0, 1.0, 1.0, 1.0), # LYS - (1.0, 1.0, 1.0, 0.0), # MET - (1.0, 1.0, 0.0, 0.0), # PHE - (1.0, 1.0, 0.0, 0.0), # PRO - (1.0, 0.0, 0.0, 0.0), # SER - (1.0, 0.0, 0.0, 0.0), # THR - (1.0, 1.0, 0.0, 0.0), # TRP - (1.0, 1.0, 0.0, 0.0), # TYR - (1.0, 0.0, 0.0, 0.0), # VAL -) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/cpp.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/cpp.cc deleted file mode 100644 index b2286b5c3..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/cpp.cc +++ /dev/null @@ -1,48 +0,0 @@ -/* -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - */ - -#include "alphafold3/data/cpp/msa_profile_pybind.h" -#include "alphafold3/model/mkdssp_pybind.h" -#include "alphafold3/parsers/cpp/cif_dict_pybind.h" -#include "alphafold3/parsers/cpp/fasta_iterator_pybind.h" -#include "alphafold3/parsers/cpp/msa_conversion_pybind.h" -#include "alphafold3/structure/cpp/aggregation_pybind.h" -#include "alphafold3/structure/cpp/membership_pybind.h" -#include "alphafold3/structure/cpp/mmcif_atom_site_pybind.h" -#include "alphafold3/structure/cpp/mmcif_layout_pybind.h" -#include "alphafold3/structure/cpp/mmcif_struct_conn_pybind.h" -#include "alphafold3/structure/cpp/mmcif_utils_pybind.h" -#include "alphafold3/structure/cpp/string_array_pybind.h" -#include "pybind11/pybind11.h" - -namespace alphafold3 { -namespace { - -// Include all modules as submodules to simplify building. -PYBIND11_MODULE(cpp, m) { - RegisterModuleCifDict(m.def_submodule("cif_dict")); - RegisterModuleFastaIterator(m.def_submodule("fasta_iterator")); - RegisterModuleMsaConversion(m.def_submodule("msa_conversion")); - RegisterModuleMmcifLayout(m.def_submodule("mmcif_layout")); - RegisterModuleMmcifStructConn(m.def_submodule("mmcif_struct_conn")); - RegisterModuleMembership(m.def_submodule("membership")); - RegisterModuleMmcifUtils(m.def_submodule("mmcif_utils")); - RegisterModuleAggregation(m.def_submodule("aggregation")); - RegisterModuleStringArray(m.def_submodule("string_array")); - RegisterModuleMmcifAtomSite(m.def_submodule("mmcif_atom_site")); - RegisterModuleMkdssp(m.def_submodule("mkdssp")); - RegisterModuleMsaProfile(m.def_submodule("msa_profile")); -} - -} // namespace -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/cpp/msa_profile_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/data/cpp/msa_profile_pybind.cc deleted file mode 100644 index 83b86f4e2..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/cpp/msa_profile_pybind.cc +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include - -#include "absl/strings/str_cat.h" -#include "pybind11/cast.h" -#include "pybind11/numpy.h" -#include "pybind11/pybind11.h" - -namespace { - -namespace py = pybind11; - -py::array_t ComputeMsaProfile( - const py::array_t& msa, int num_residue_types) { - if (msa.size() == 0) { - throw py::value_error("The MSA must be non-empty."); - } - if (msa.ndim() != 2) { - throw py::value_error(absl::StrCat("The MSA must be rectangular, got ", - msa.ndim(), "-dimensional MSA array.")); - } - const int msa_depth = msa.shape()[0]; - const int sequence_length = msa.shape()[1]; - - py::array_t profile({sequence_length, num_residue_types}); - std::fill(profile.mutable_data(), profile.mutable_data() + profile.size(), - 0.0f); - auto profile_unchecked = profile.mutable_unchecked<2>(); - - const double normalized_count = 1.0 / msa_depth; - const int* msa_it = msa.data(); - for (int row_index = 0; row_index < msa_depth; ++row_index) { - for (int column_index = 0; column_index < sequence_length; ++column_index) { - const int residue_code = *(msa_it++); - if (residue_code < 0 || residue_code >= num_residue_types) { - throw py::value_error( - absl::StrCat("All residue codes must be positive and smaller than " - "num_residue_types ", - num_residue_types, ", got ", residue_code)); - } - profile_unchecked(column_index, residue_code) += normalized_count; - } - } - return profile; -} - -constexpr char kComputeMsaProfileDoc[] = R"( -Computes MSA profile for the given encoded MSA. - -Args: - msa: A Numpy array of shape (num_msa, num_res) with the integer coded MSA. - num_residue_types: Integer that determines the number of unique residue types. - This will determine the shape of the output profile. - -Returns: - A float Numpy array of shape (num_res, num_residue_types) with residue - frequency (residue type count normalized by MSA depth) for every column of the - MSA. -)"; - -} // namespace - -namespace alphafold3 { - -void RegisterModuleMsaProfile(pybind11::module m) { - m.def("compute_msa_profile", &ComputeMsaProfile, py::arg("msa"), - py::arg("num_residue_types"), py::doc(kComputeMsaProfileDoc + 1)); -} - -} // namespace alphafold3 \ No newline at end of file diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/cpp/msa_profile_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/data/cpp/msa_profile_pybind.h deleted file mode 100644 index 1145d331b..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/cpp/msa_profile_pybind.h +++ /dev/null @@ -1,25 +0,0 @@ -/* -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_DATA_PYTHON_MSA_PROFILE_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_DATA_PYTHON_MSA_PROFILE_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleMsaProfile(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_DATA_PYTHON_MSA_PROFILE_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/featurisation.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/featurisation.py deleted file mode 100644 index 6599654b5..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/featurisation.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""AlphaFold 3 featurisation pipeline.""" - -from collections.abc import Sequence -import datetime -import time -from typing import Optional - -from alphafold3.common import folding_input -from alphafold3.constants import chemical_components -from alphafold3.model import features -from alphafold3.model.pipeline import pipeline -import numpy as np - - -def validate_fold_input(fold_input: folding_input.Input): - """Validates the fold input contains MSA and templates for featurisation.""" - for i, chain in enumerate(fold_input.protein_chains): - if chain.unpaired_msa is None: - raise ValueError(f'Protein chain {i + 1} is missing unpaired MSA.') - if chain.paired_msa is None: - raise ValueError(f'Protein chain {i + 1} is missing paired MSA.') - if chain.templates is None: - raise ValueError(f'Protein chain {i + 1} is missing Templates.') - for i, chain in enumerate(fold_input.rna_chains): - if chain.unpaired_msa is None: - raise ValueError(f'RNA chain {i + 1} is missing unpaired MSA.') - - -def featurise_input( - fold_input: folding_input.Input, - ccd: chemical_components.Ccd, - buckets: Optional[Sequence[int]], - max_template_date: Optional[datetime.date] = None, - verbose: bool = False, -) -> Sequence[features.BatchDict]: - """Featurise the folding input. - - Args: - fold_input: The input to featurise. - ccd: The chemical components dictionary. - buckets: Bucket sizes to pad the data to, to avoid excessive re-compilation - of the model. If None, calculate the appropriate bucket size from the - number of tokens. If not None, must be a sequence of at least one integer, - in strictly increasing order. Will raise an error if the number of tokens - is more than the largest bucket size. - max_template_date: Optional max template date to prevent data leakage in - validation. - verbose: Whether to print progress messages. - - Returns: - A featurised batch for each rng_seed in the input. - """ - validate_fold_input(fold_input) - - # Set up data pipeline for single use. - data_pipeline = pipeline.WholePdbPipeline( - config=pipeline.WholePdbPipeline.Config( - buckets=buckets, max_template_date=max_template_date - ), - ) - - batches = [] - for rng_seed in fold_input.rng_seeds: - featurisation_start_time = time.time() - if verbose: - print(f'Featurising {fold_input.name} with rng_seed {rng_seed}.') - batch = data_pipeline.process_item( - fold_input=fold_input, - ccd=ccd, - random_state=np.random.RandomState(rng_seed), - random_seed=rng_seed, - ) - if verbose: - print( - f'Featurising {fold_input.name} with rng_seed {rng_seed} ' - f'took {time.time() - featurisation_start_time:.2f} seconds.' - ) - batches.append(batch) - - return batches diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa.py deleted file mode 100644 index 51fe21177..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa.py +++ /dev/null @@ -1,346 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Functions for getting MSA and calculating alignment features.""" - -from collections.abc import MutableMapping, Sequence -import string -from typing import Self - -from absl import logging -from alphafold3.constants import mmcif_names -from alphafold3.data import msa_config -from alphafold3.data import msa_features -from alphafold3.data import parsers -from alphafold3.data.tools import jackhmmer -from alphafold3.data.tools import msa_tool -from alphafold3.data.tools import nhmmer -import numpy as np - - -class Error(Exception): - """Error indicatating a problem with MSA Search.""" - - -def _featurize(seq: str, chain_poly_type: str) -> str | list[int]: - if mmcif_names.is_standard_polymer_type(chain_poly_type): - featurized_seqs, _ = msa_features.extract_msa_features( - msa_sequences=[seq], chain_poly_type=chain_poly_type - ) - return featurized_seqs[0].tolist() - # For anything else simply require an identical match. - return seq - - -def sequences_are_feature_equivalent( - sequence1: str, - sequence2: str, - chain_poly_type: str, -) -> bool: - feat1 = _featurize(sequence1, chain_poly_type) - feat2 = _featurize(sequence2, chain_poly_type) - return feat1 == feat2 - - -class Msa: - """Multiple Sequence Alignment container with methods for manipulating it.""" - - def __init__( - self, - query_sequence: str, - chain_poly_type: str, - sequences: Sequence[str], - descriptions: Sequence[str], - deduplicate: bool = True, - ): - """Raw constructor, prefer using the from_{a3m,multiple_msas} class methods. - - The first sequence must be equal (in featurised form) to the query sequence. - If sequences/descriptions are empty, they will be initialised to the query. - - Args: - query_sequence: The sequence that was used to search for MSA. - chain_poly_type: Polymer type of the query sequence, see mmcif_names. - sequences: The sequences returned by the MSA search tool. - descriptions: Metadata for the sequences returned by the MSA search tool. - deduplicate: If True, the MSA sequences will be deduplicated in the input - order. Lowercase letters (insertions) are ignored when deduplicating. - """ - if len(sequences) != len(descriptions): - raise ValueError( - 'The number of sequences and descriptions must match.') - - self.query_sequence = query_sequence - self.chain_poly_type = chain_poly_type - - if not deduplicate: - self.sequences = sequences - self.descriptions = descriptions - else: - self.sequences = [] - self.descriptions = [] - # A replacement table that removes all lowercase characters. - deletion_table = str.maketrans('', '', string.ascii_lowercase) - unique_sequences = set() - for seq, desc in zip(sequences, descriptions, strict=True): - # Using string.translate is faster than re.sub('[a-z]+', ''). - sequence_no_deletions = seq.translate(deletion_table) - if sequence_no_deletions not in unique_sequences: - unique_sequences.add(sequence_no_deletions) - self.sequences.append(seq) - self.descriptions.append(desc) - - # Make sure the MSA always has at least the query. - self.sequences = self.sequences or [query_sequence] - self.descriptions = self.descriptions or ['Original query'] - - # Check if the 1st MSA sequence matches the query sequence. Since it may be - # mutated by the search tool (jackhmmer) check using the featurized version. - if not sequences_are_feature_equivalent( - self.sequences[0], query_sequence, chain_poly_type - ): - raise ValueError( - f'First MSA sequence {self.sequences[0]} is not the {query_sequence=}' - ) - - @classmethod - def from_multiple_msas( - cls, msas: Sequence[Self], deduplicate: bool = True - ) -> Self: - """Initializes the MSA from multiple MSAs. - - Args: - msas: A sequence of Msa objects representing individual MSAs produced by - different tools/dbs. - deduplicate: If True, the MSA sequences will be deduplicated in the input - order. Lowercase letters (insertions) are ignored when deduplicating. - - Returns: - An Msa object created by merging multiple MSAs. - """ - if not msas: - raise ValueError('At least one MSA must be provided.') - - query_sequence = msas[0].query_sequence - chain_poly_type = msas[0].chain_poly_type - sequences = [] - descriptions = [] - - for msa in msas: - if msa.query_sequence != query_sequence: - raise ValueError( - f'Query sequences must match: {[m.query_sequence for m in msas]}' - ) - if msa.chain_poly_type != chain_poly_type: - raise ValueError( - f'Chain poly types must match: {[m.chain_poly_type for m in msas]}' - ) - sequences.extend(msa.sequences) - descriptions.extend(msa.descriptions) - - return cls( - query_sequence=query_sequence, - chain_poly_type=chain_poly_type, - sequences=sequences, - descriptions=descriptions, - deduplicate=deduplicate, - ) - - @classmethod - def from_multiple_a3ms( - cls, a3ms: Sequence[str], chain_poly_type: str, deduplicate: bool = True - ) -> Self: - """Initializes the MSA from multiple A3M strings. - - Args: - a3ms: A sequence of A3M strings representing individual MSAs produced by - different tools/dbs. - chain_poly_type: Polymer type of the query sequence, see mmcif_names. - deduplicate: If True, the MSA sequences will be deduplicated in the input - order. Lowercase letters (insertions) are ignored when deduplicating. - - Returns: - An Msa object created by merging multiple A3Ms. - """ - if not a3ms: - raise ValueError('At least one A3M must be provided.') - - query_sequence = None - all_sequences = [] - all_descriptions = [] - - for a3m in a3ms: - sequences, descriptions = parsers.parse_fasta(a3m) - if query_sequence is None: - query_sequence = sequences[0] - - if sequences[0] != query_sequence: - raise ValueError( - f'Query sequences must match: {sequences[0]=} != {query_sequence=}' - ) - all_sequences.extend(sequences) - all_descriptions.extend(descriptions) - - return cls( - query_sequence=query_sequence, - chain_poly_type=chain_poly_type, - sequences=all_sequences, - descriptions=all_descriptions, - deduplicate=deduplicate, - ) - - @classmethod - def from_a3m( - cls, - query_sequence: str, - chain_poly_type: str, - a3m: str, - max_depth: int | None = None, - deduplicate: bool = True, - ) -> Self: - """Parses the single A3M and builds the Msa object.""" - sequences, descriptions = parsers.parse_fasta(a3m) - - if max_depth is not None and 0 < max_depth < len(sequences): - logging.info( - 'MSA cropped from depth of %d to %d for %s.', - len(sequences), - max_depth, - query_sequence, - ) - sequences = sequences[:max_depth] - descriptions = descriptions[:max_depth] - - return cls( - query_sequence=query_sequence, - chain_poly_type=chain_poly_type, - sequences=sequences, - descriptions=descriptions, - deduplicate=deduplicate, - ) - - @classmethod - def from_empty(cls, query_sequence: str, chain_poly_type: str) -> Self: - """Creates an empty Msa containing just the query sequence.""" - return cls( - query_sequence=query_sequence, - chain_poly_type=chain_poly_type, - sequences=[], - descriptions=[], - deduplicate=False, - ) - - @property - def depth(self) -> int: - return len(self.sequences) - - def __repr__(self) -> str: - return f'Msa({self.depth} sequences, {self.chain_poly_type})' - - def to_a3m(self) -> str: - """Returns the MSA in the A3M format.""" - a3m_lines = [] - for desc, seq in zip(self.descriptions, self.sequences, strict=True): - a3m_lines.append(f'>{desc}') - a3m_lines.append(seq) - return '\n'.join(a3m_lines) + '\n' - - def featurize(self) -> MutableMapping[str, np.ndarray]: - """Featurises the MSA and returns a map of feature names to features. - - Returns: - A dictionary mapping feature names to values. - - Raises: - msa.Error: - * If the sequences in the MSA don't have the same length after deletions - (lower case letters) are removed. - * If the MSA contains an unknown amino acid code. - * If there are no sequences after aligning. - """ - try: - msa, deletion_matrix = msa_features.extract_msa_features( - msa_sequences=self.sequences, chain_poly_type=self.chain_poly_type - ) - except ValueError as e: - raise Error( - f'Error extracting MSA or deletion features: {e}') from e - - if msa.shape == (0, 0): - raise Error(f'Empty MSA feature for {self}') - - species_ids = msa_features.extract_species_ids(self.descriptions) - - return { - 'msa_species_identifiers': np.array(species_ids, dtype=object), - 'num_alignments': np.array(self.depth, dtype=np.int32), - 'msa': msa, - 'deletion_matrix_int': deletion_matrix, - } - - -def get_msa_tool( - msa_tool_config: msa_config.JackhmmerConfig | msa_config.NhmmerConfig, -) -> msa_tool.MsaTool: - """Returns the requested MSA tool.""" - - match msa_tool_config: - case msa_config.JackhmmerConfig(): - return jackhmmer.Jackhmmer( - binary_path=msa_tool_config.binary_path, - database_path=msa_tool_config.database_config.path, - n_cpu=msa_tool_config.n_cpu, - n_iter=msa_tool_config.n_iter, - e_value=msa_tool_config.e_value, - z_value=msa_tool_config.z_value, - max_sequences=msa_tool_config.max_sequences, - ) - case msa_config.NhmmerConfig(): - return nhmmer.Nhmmer( - binary_path=msa_tool_config.binary_path, - hmmalign_binary_path=msa_tool_config.hmmalign_binary_path, - hmmbuild_binary_path=msa_tool_config.hmmbuild_binary_path, - database_path=msa_tool_config.database_config.path, - n_cpu=msa_tool_config.n_cpu, - e_value=msa_tool_config.e_value, - max_sequences=msa_tool_config.max_sequences, - alphabet=msa_tool_config.alphabet, - ) - case _: - raise ValueError(f'Unknown MSA tool: {msa_tool_config}.') - - -def get_msa( - target_sequence: str, - run_config: msa_config.RunConfig, - chain_poly_type: str, - deduplicate: bool = False, -) -> Msa: - """Computes the MSA for a given query sequence. - - Args: - target_sequence: The target amino-acid sequence. - run_config: MSA run configuration. - chain_poly_type: The type of chain for which to get an MSA. - deduplicate: If True, the MSA sequences will be deduplicated in the input - order. Lowercase letters (insertions) are ignored when deduplicating. - - Returns: - Aligned MSA sequences. - """ - - return Msa.from_a3m( - query_sequence=target_sequence, - chain_poly_type=chain_poly_type, - a3m=get_msa_tool(run_config.config).query(target_sequence).a3m, - max_depth=run_config.crop_size, - deduplicate=deduplicate, - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_config.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_config.py deleted file mode 100644 index 3963f8d43..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_config.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Genetic search config settings for data pipelines.""" - -import dataclasses -import datetime -from typing import Self, Optional, Union -from alphafold3.constants import mmcif_names - - -def _validate_chain_poly_type(chain_poly_type: str) -> None: - if chain_poly_type not in mmcif_names.STANDARD_POLYMER_CHAIN_TYPES: - raise ValueError( - 'chain_poly_type must be one of' - f' {mmcif_names.STANDARD_POLYMER_CHAIN_TYPES}: {chain_poly_type}' - ) - - -@dataclasses.dataclass(frozen=True) -class DatabaseConfig: - """Configuration for a database.""" - - name: str - path: str - - -@dataclasses.dataclass(frozen=True) -class JackhmmerConfig: - """Configuration for a jackhmmer run. - - Attributes: - binary_path: Path to the binary of the msa tool. - database_config: Database configuration. - n_cpu: An integer with the number of CPUs to use. - n_iter: An integer with the number of database search iterations. - e_value: e-value for the database lookup. - z_value: The Z-value representing the number of comparisons done (i.e - correct database size) for E-value calculation. - max_sequences: Max sequences to return in MSA. - """ - - binary_path: str - database_config: DatabaseConfig - n_cpu: int - n_iter: int - e_value: float - z_value: Optional[Union[float, int]] - max_sequences: int - - -@dataclasses.dataclass(frozen=True) -class NhmmerConfig: - """Configuration for a nhmmer run. - - Attributes: - binary_path: Path to the binary of the msa tool. - hmmalign_binary_path: Path to the hmmalign binary. - hmmbuild_binary_path: Path to the hmmbuild binary. - database_config: Database configuration. - n_cpu: An integer with the number of CPUs to use. - e_value: e-value for the database lookup. - max_sequences: Max sequences to return in MSA. - alphabet: The alphabet when building a profile with hmmbuild. - """ - - binary_path: str - hmmalign_binary_path: str - hmmbuild_binary_path: str - database_config: DatabaseConfig - n_cpu: int - e_value: float - max_sequences: int - alphabet: Optional[str] - - -@dataclasses.dataclass(frozen=True) -class RunConfig: - """Configuration for an MSA run. - - Attributes: - config: MSA tool config. - chain_poly_type: The chain type for which the tools will be run. - crop_size: The maximum number of sequences to keep in the MSA. If None, all - sequences are kept. Note that the query is included in the MSA, so it - doesn't make sense to set this to less than 2. - """ - - config: Union[JackhmmerConfig, NhmmerConfig] - chain_poly_type: str - crop_size: Optional[int] - - def __post_init__(self): - if self.crop_size is not None and self.crop_size < 2: - raise ValueError( - f'crop_size must be None or >= 2: {self.crop_size}') - - _validate_chain_poly_type(self.chain_poly_type) - - -@dataclasses.dataclass(frozen=True) -class HmmsearchConfig: - """Configuration for a hmmsearch.""" - - hmmsearch_binary_path: str - hmmbuild_binary_path: str - - e_value: float - inc_e: float - dom_e: float - incdom_e: float - alphabet: str = 'amino' - filter_f1: Optional[float] = None - filter_f2: Optional[float] = None - filter_f3: Optional[float] = None - filter_max: bool = False - - -@dataclasses.dataclass(frozen=True) -class TemplateToolConfig: - """Configuration for a template tool.""" - - database_path: str - chain_poly_type: str - hmmsearch_config: HmmsearchConfig - max_a3m_query_sequences: Optional[int] = 300 - - def __post_init__(self): - _validate_chain_poly_type(self.chain_poly_type) - - -@dataclasses.dataclass(frozen=True) -class TemplateFilterConfig: - """Configuration for a template filter.""" - - max_subsequence_ratio: Optional[float] - min_align_ratio: Optional[float] - min_hit_length: Optional[int] - deduplicate_sequences: bool - max_hits: Optional[int] - max_template_date: datetime.date - - @classmethod - def no_op_filter(cls) -> Self: - """Returns a config for filter that keeps everything.""" - return cls( - max_subsequence_ratio=None, - min_align_ratio=None, - min_hit_length=None, - deduplicate_sequences=False, - max_hits=None, - # Very far in the future. - max_template_date=datetime.date(3000, 1, 1), - ) - - -@dataclasses.dataclass(frozen=True) -class TemplatesConfig: - """Configuration for the template search pipeline.""" - - template_tool_config: TemplateToolConfig - filter_config: TemplateFilterConfig diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_features.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_features.py deleted file mode 100644 index 7c6fff3f5..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_features.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Utilities for computing MSA features.""" - -from collections.abc import Sequence -import re -from alphafold3.constants import mmcif_names -import numpy as np - -_PROTEIN_TO_ID = { - 'A': 0, - 'B': 3, # Same as D. - 'C': 4, - 'D': 3, - 'E': 6, - 'F': 13, - 'G': 7, - 'H': 8, - 'I': 9, - 'J': 20, # Same as unknown (X). - 'K': 11, - 'L': 10, - 'M': 12, - 'N': 2, - 'O': 20, # Same as unknown (X). - 'P': 14, - 'Q': 5, - 'R': 1, - 'S': 15, - 'T': 16, - 'U': 4, # Same as C. - 'V': 19, - 'W': 17, - 'X': 20, - 'Y': 18, - 'Z': 6, # Same as E. - '-': 21, -} - -_RNA_TO_ID = { - # Map non-standard residues to UNK_NUCLEIC (N) -> 30 - **{chr(i): 30 for i in range(ord('A'), ord('Z') + 1)}, - # Continue the RNA indices from where Protein indices left off. - '-': 21, - 'A': 22, - 'G': 23, - 'C': 24, - 'U': 25, -} - -_DNA_TO_ID = { - # Map non-standard residues to UNK_NUCLEIC (N) -> 30 - **{chr(i): 30 for i in range(ord('A'), ord('Z') + 1)}, - # Continue the DNA indices from where DNA indices left off. - '-': 21, - 'A': 26, - 'G': 27, - 'C': 28, - 'T': 29, -} - - -def extract_msa_features( - msa_sequences: Sequence[str], chain_poly_type: str -) -> tuple[np.ndarray, np.ndarray]: - """Extracts MSA features. - - Example: - The input raw MSA is: `[["AAAAAA"], ["Ai-CiDiiiEFa"]]` - The output MSA will be: `[["AAAAAA"], ["A-CDEF"]]` - The deletions will be: `[[0, 0, 0, 0, 0, 0], [0, 1, 0, 1, 3, 0]]` - - Args: - msa_sequences: A list of strings, each string with one MSA sequence. Each - string must have the same, constant number of non-lowercase (matching) - residues. - chain_poly_type: Either 'polypeptide(L)' (protein), 'polyribonucleotide' - (RNA), or 'polydeoxyribonucleotide' (DNA). Use the appropriate string - constant from mmcif_names.py. - - Returns: - A tuple with: - * MSA array of shape (num_seq, num_res) that contains only the uppercase - characters or gaps (-) from the original MSA. - * Deletions array of shape (num_seq, num_res) that contains the number - of deletions (lowercase letters in the MSA) to the left from each - non-deleted residue (uppercase letters in the MSA). - - Raises: - ValueError if any of the preconditions are not met. - """ - - # Select the appropriate character map based on the chain type. - if chain_poly_type == mmcif_names.RNA_CHAIN: - char_map = _RNA_TO_ID - elif chain_poly_type == mmcif_names.DNA_CHAIN: - char_map = _DNA_TO_ID - elif chain_poly_type == mmcif_names.PROTEIN_CHAIN: - char_map = _PROTEIN_TO_ID - else: - raise ValueError(f'{chain_poly_type=} invalid.') - - # Handle empty MSA. - if not msa_sequences: - empty_msa = np.array([], dtype=np.int32).reshape((0, 0)) - empty_deletions = np.array([], dtype=np.int32).reshape((0, 0)) - return empty_msa, empty_deletions - - # Get the number of rows and columns in the MSA. - num_rows = len(msa_sequences) - num_cols = sum(1 for c in msa_sequences[0] if c in char_map) - - # Initialize the output arrays. - msa_arr = np.zeros((num_rows, num_cols), dtype=np.int32) - deletions_arr = np.zeros((num_rows, num_cols), dtype=np.int32) - - # Populate the output arrays. - for problem_row, msa_sequence in enumerate(msa_sequences): - deletion_count = 0 - upper_count = 0 - problem_col = 0 - problems = [] - for current in msa_sequence: - msa_id = char_map.get(current, -1) - if msa_id == -1: - if not current.islower(): - problems.append( - f'({problem_row}, {problem_col}):{current}') - deletion_count += 1 - else: - # Check the access is safe before writing to the array. - # We don't need to check problem_row since it's guaranteed to be within - # the array bounds, while upper_count is incremented in the loop. - if upper_count < deletions_arr.shape[1]: - deletions_arr[problem_row, upper_count] = deletion_count - msa_arr[problem_row, upper_count] = msa_id - deletion_count = 0 - upper_count += 1 - problem_col += 1 - if problems: - raise ValueError( - f"Unknown residues in MSA: {', '.join(problems)}. " - f'target_sequence: {msa_sequences[0]}' - ) - if upper_count != num_cols: - raise ValueError( - 'Invalid shape all strings must have the same number ' - 'of non-lowercase characters; First string has ' - f"{num_cols} non-lowercase characters but '{msa_sequence}' has " - f'{upper_count}. target_sequence: {msa_sequences[0]}' - ) - - return msa_arr, deletions_arr - - -# UniProtKB SwissProt/TrEMBL dbs have the following description format: -# `db|UniqueIdentifier|EntryName`, e.g. `sp|P0C2L1|A3X1_LOXLA` or -# `tr|A0A146SKV9|A0A146SKV9_FUNHE`. -_UNIPROT_ENTRY_NAME_REGEX = re.compile( - # UniProtKB TrEMBL or SwissProt database. - r'(?:tr|sp)\|' - # A primary accession number of the UniProtKB entry. - r'(?:[A-Z0-9]{6,10})' - # Occasionally there is an isoform suffix (e.g. _1 or _10) which we ignore. - r'(?:_\d+)?\|' - # TrEMBL: Same as AccessionId (6-10 characters). - # SwissProt: A mnemonic protein identification code (1-5 characters). - r'(?:[A-Z0-9]{1,10}_)' - # A mnemonic species identification code. - r'(?P[A-Z0-9]{1,5})' -) - - -def extract_species_ids(msa_descriptions: Sequence[str]) -> Sequence[str]: - """Extracts species ID from MSA UniProtKB sequence identifiers. - - Args: - msa_descriptions: The descriptions (the FASTA/A3M comment line) for each of - the sequences. - - Returns: - Extracted UniProtKB species IDs if there is a regex match for each - description line, blank if the regex doesn't match. - """ - species_ids = [] - for msa_description in msa_descriptions: - msa_description = msa_description.strip() - match = _UNIPROT_ENTRY_NAME_REGEX.match(msa_description) - if match: - species_ids.append(match.group('SpeciesId')) - else: - # Handle cases where the regex doesn't match - # (e.g., append None or raise an error depending on your needs) - species_ids.append('') - return species_ids diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_identifiers.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_identifiers.py deleted file mode 100644 index 110b1def6..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_identifiers.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Utilities for extracting identifiers from MSA sequence descriptions.""" - -import dataclasses -import re -from typing import Optional - - -# Sequences coming from UniProtKB database come in the -# `db|UniqueIdentifier|EntryName` format, e.g. `tr|A0A146SKV9|A0A146SKV9_FUNHE` -# or `sp|P0C2L1|A3X1_LOXLA` (for TREMBL/Swiss-Prot respectively). -_UNIPROT_PATTERN = re.compile( - r""" - ^ - # UniProtKB/TrEMBL or UniProtKB/Swiss-Prot - (?:tr|sp) - \| - # A primary accession number of the UniProtKB entry. - (?P[A-Za-z0-9]{6,10}) - # Occasionally there is a _0 or _1 isoform suffix, which we ignore. - (?:_\d)? - \| - # TREMBL repeats the accession ID here. Swiss-Prot has a mnemonic - # protein ID code. - (?:[A-Za-z0-9]+) - _ - # A mnemonic species identification code. - (?P([A-Za-z0-9]){1,5}) - # Small BFD uses a final value after an underscore, which we ignore. - (?:_\d+)? - $ - """, - re.VERBOSE, -) - - -@dataclasses.dataclass(frozen=True) -class Identifiers: - species_id: str = '' - - -def _parse_sequence_identifier(msa_sequence_identifier: str) -> Identifiers: - """Gets species from an msa sequence identifier. - - The sequence identifier has the format specified by - _UNIPROT_TREMBL_ENTRY_NAME_PATTERN or _UNIPROT_SWISSPROT_ENTRY_NAME_PATTERN. - An example of a sequence identifier: `tr|A0A146SKV9|A0A146SKV9_FUNHE` - - Args: - msa_sequence_identifier: a sequence identifier. - - Returns: - An `Identifiers` instance with species_id. These - can be empty in the case where no identifier was found. - """ - matches = re.search(_UNIPROT_PATTERN, msa_sequence_identifier.strip()) - if matches: - return Identifiers(species_id=matches.group('SpeciesIdentifier')) - return Identifiers() - - -def _extract_sequence_identifier(description: str) -> Optional[str]: - """Extracts sequence identifier from description. Returns None if no match.""" - split_description = description.split() - if split_description: - return split_description[0].partition('/')[0] - else: - return None - - -def get_identifiers(description: str) -> Identifiers: - """Computes extra MSA features from the description.""" - sequence_identifier = _extract_sequence_identifier(description) - if sequence_identifier is None: - return Identifiers() - else: - return _parse_sequence_identifier(sequence_identifier) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_store.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_store.py deleted file mode 100644 index cda58e4c9..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/msa_store.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Interface and implementations for fetching MSA data.""" - -from collections.abc import Sequence -from typing_extensions import Protocol, TypeAlias - -from alphafold3.data import msa -from alphafold3.data import msa_config - - -MsaErrors: TypeAlias = Sequence[tuple[msa_config.RunConfig, str]] - - -class MsaProvider(Protocol): - """Interface for providing Multiple Sequence Alignments.""" - - def __call__( - self, - query_sequence: str, - chain_polymer_type: str, - ) -> tuple[msa.Msa, MsaErrors]: - """Retrieve MSA for the given polymer query_sequence. - - Args: - query_sequence: The residue sequence of the polymer to search for. - chain_polymer_type: The polymer type of the query_sequence. This must - match the chain_polymer_type of the provider. - - Returns: - A tuple containing the MSA and MsaErrors. MsaErrors is a Sequence - containing a tuple for each msa_query that failed. Each tuple contains - the failing query and the associated error message. - """ - - -class EmptyMsaProvider: - """MSA provider that returns just the query sequence, useful for testing.""" - - def __init__(self, chain_polymer_type: str): - self._chain_polymer_type = chain_polymer_type - - def __call__( - self, query_sequence: str, chain_polymer_type: str - ) -> tuple[msa.Msa, MsaErrors]: - """Returns an MSA containing just the query sequence, never errors.""" - if chain_polymer_type != self._chain_polymer_type: - raise ValueError( - f'EmptyMsaProvider of type {self._chain_polymer_type} called with ' - f'sequence of {chain_polymer_type=}, {query_sequence=}.' - ) - return ( - msa.Msa.from_empty( - query_sequence=query_sequence, - chain_poly_type=self._chain_polymer_type, - ), - (), - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/parsers.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/parsers.py deleted file mode 100644 index 6d31d7deb..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/parsers.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright 2025 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ - -"""Functions for parsing various file formats.""" - -from collections.abc import Iterable, Sequence -from typing import IO, TypeAlias, Optional - -from alphafold3.cpp import fasta_iterator -from alphafold3.cpp import msa_conversion - - -DeletionMatrix: TypeAlias = Sequence[Sequence[int]] - - -def lazy_parse_fasta_string(fasta_string: str) -> Iterable[tuple[str, str]]: - """Lazily parses a FASTA/A3M string and yields (sequence, description) tuples. - - This implementation is more memory friendly than `fasta_sequence` while - offering comparable performance. The underlying implementation is in C++ and - is therefore faster than a pure Python implementation. - - Use this method when parsing FASTA files where you already have the FASTA - string, but need to control how far you iterate through its sequences. - - Arguments: - fasta_string: A string with the contents of FASTA/A3M file. - - Returns: - Iterator of (sequence, description). In the description, the leading ">" is - stripped. - - Raises: - ValueError if the FASTA/A3M file is invalid, e.g. empty. - """ - - # The lifetime of the FastaStringIterator is tied to the lifetime of - # fasta_string - fasta_string must be kept while the iterator is in use. - return fasta_iterator.FastaStringIterator(fasta_string) - - -def parse_fasta(fasta_string: str) -> tuple[Sequence[str], Sequence[str]]: - """Parses FASTA string and returns list of strings with amino-acid sequences. - - Arguments: - fasta_string: The string contents of a FASTA file. - - Returns: - A tuple of two lists: - * A list of sequences. - * A list of sequence descriptions taken from the comment lines. In the - same order as the sequences. - """ - return fasta_iterator.parse_fasta_include_descriptions(fasta_string) - - -def convert_a3m_to_stockholm(a3m: str, max_seqs: Optional[int] = None) -> str: - """Converts MSA in the A3M format to the Stockholm format.""" - sequences, descriptions = parse_fasta(a3m) - if max_seqs is not None: - sequences = sequences[:max_seqs] - descriptions = descriptions[:max_seqs] - - stockholm = ['# STOCKHOLM 1.0', ''] - - # Add the Stockholm header with the sequence metadata. - names = [] - for i, description in enumerate(descriptions): - name, _, rest = description.partition(' ') - # Ensure that the names are unique - stockholm format requires that - # the sequence names are unique. - name = f'{name}_{i}' - names.append(name) - # Avoid zero-length description due to historic hmmbuild parsing bug. - desc = rest.strip() or '' - stockholm.append(f'#=GS {name.strip()} DE {desc}') - stockholm.append('') - - # Convert insertions in a sequence into gaps in all other sequences that don't - # have an insertion in that column as well. - sequences = msa_conversion.convert_a3m_to_stockholm(sequences) - - # Add the MSA data. - max_name_width = max(len(name) for name in names) - for name, sequence in zip(names, sequences, strict=True): - # Align the names to the left and pad with spaces to the maximum length. - stockholm.append(f'{name:<{max_name_width}s} {sequence}') - - # Add the reference annotation for the query (the first sequence). - ref_annotation = ''.join('.' if c == '-' else 'x' for c in sequences[0]) - stockholm.append(f'{"#=GC RF":<{max_name_width}s} {ref_annotation}') - stockholm.append('//') - - return '\n'.join(stockholm) - - -def convert_stockholm_to_a3m( - stockholm: IO[str], - max_sequences: Optional[int] = None, - remove_first_row_gaps: bool = True, - linewidth: Optional[int] = None, -) -> str: - """Converts MSA in Stockholm format to the A3M format.""" - descriptions = {} - sequences = {} - reached_max_sequences = False - - if linewidth is not None and linewidth <= 0: - raise ValueError('linewidth must be > 0 or None') - - for line in stockholm: - reached_max_sequences = max_sequences and len( - sequences) >= max_sequences - line = line.strip() - # Ignore blank lines, markup and end symbols - remainder are alignment - # sequence parts. - if not line or line.startswith(('#', '//')): - continue - seqname, aligned_seq = line.split(maxsplit=1) - if seqname not in sequences: - if reached_max_sequences: - continue - sequences[seqname] = '' - sequences[seqname] += aligned_seq - - stockholm.seek(0) - for line in stockholm: - line = line.strip() - if line[:4] == '#=GS': - # Description row - example format is: - # #=GS UniRef90_Q9H5Z4/4-78 DE [subseq from] cDNA: FLJ22755 ... - columns = line.split(maxsplit=3) - seqname, feature = columns[1:3] - value = columns[3] if len(columns) == 4 else '' - if feature != 'DE': - continue - if reached_max_sequences and seqname not in sequences: - continue - descriptions[seqname] = value - if len(descriptions) == len(sequences): - break - - # Convert sto format to a3m line by line - a3m_sequences = {} - # query_sequence is assumed to be the first sequence - query_sequence = next(iter(sequences.values())) - for seqname, sto_sequence in sequences.items(): - if remove_first_row_gaps: - a3m_sequences[seqname] = msa_conversion.align_sequence_to_gapless_query( - sequence=sto_sequence, query_sequence=query_sequence - ).replace('.', '') - else: - a3m_sequences[seqname] = sto_sequence.replace('.', '') - - fasta_chunks = [] - - for seqname, seq in a3m_sequences.items(): - fasta_chunks.append(f'>{seqname} {descriptions.get(seqname, "")}') - - if linewidth: - fasta_chunks.extend( - seq[i: linewidth + i] for i in range(0, len(seq), linewidth) - ) - else: - fasta_chunks.append(seq) - - return '\n'.join(fasta_chunks) + '\n' # Include terminating newline. diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/pipeline.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/pipeline.py deleted file mode 100644 index 89ae3dff3..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/pipeline.py +++ /dev/null @@ -1,543 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Functions for running the MSA and template tools for the AlphaFold model.""" - -from concurrent import futures -import dataclasses -import datetime -import functools -import logging -import time - -from alphafold3.common import folding_input -from alphafold3.constants import mmcif_names -from alphafold3.data import msa -from alphafold3.data import msa_config -from alphafold3.data import structure_stores -from alphafold3.data import templates as templates_lib - - -# Cache to avoid re-running template search for the same sequence in homomers. -@functools.cache -def _get_protein_templates( - sequence: str, - input_msa_a3m: str, - run_template_search: bool, - templates_config: msa_config.TemplatesConfig, - pdb_database_path: str, -) -> templates_lib.Templates: - """Searches for templates for a single protein chain.""" - if run_template_search: - templates_start_time = time.time() - logging.info('Getting protein templates for sequence %s', sequence) - protein_templates = templates_lib.Templates.from_seq_and_a3m( - query_sequence=sequence, - msa_a3m=input_msa_a3m, - max_template_date=templates_config.filter_config.max_template_date, - database_path=templates_config.template_tool_config.database_path, - hmmsearch_config=templates_config.template_tool_config.hmmsearch_config, - max_a3m_query_sequences=None, - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - structure_store=structure_stores.StructureStore(pdb_database_path), - filter_config=templates_config.filter_config, - ) - logging.info( - 'Getting protein templates took %.2f seconds for sequence %s', - time.time() - templates_start_time, - sequence, - ) - else: - logging.info('Skipping template search for sequence %s', sequence) - protein_templates = templates_lib.Templates( - query_sequence=sequence, - hits=[], - max_template_date=templates_config.filter_config.max_template_date, - structure_store=structure_stores.StructureStore(pdb_database_path), - ) - return protein_templates - - -# Cache to avoid re-running the MSA tools for the same sequence in homomers. -@functools.cache -def _get_protein_msa_and_templates( - sequence: str, - run_template_search: bool, - uniref90_msa_config: msa_config.RunConfig, - mgnify_msa_config: msa_config.RunConfig, - small_bfd_msa_config: msa_config.RunConfig, - uniprot_msa_config: msa_config.RunConfig, - templates_config: msa_config.TemplatesConfig, - pdb_database_path: str, -) -> tuple[msa.Msa, msa.Msa, templates_lib.Templates]: - """Processes a single protein chain.""" - logging.info('Getting protein MSAs for sequence %s', sequence) - msa_start_time = time.time() - # Run various MSA tools in parallel. Use a ThreadPoolExecutor because - # they're not blocked by the GIL, as they're sub-shelled out. - with futures.ThreadPoolExecutor(max_workers=4) as executor: - uniref90_msa_future = executor.submit( - msa.get_msa, - target_sequence=sequence, - run_config=uniref90_msa_config, - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - ) - mgnify_msa_future = executor.submit( - msa.get_msa, - target_sequence=sequence, - run_config=mgnify_msa_config, - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - ) - small_bfd_msa_future = executor.submit( - msa.get_msa, - target_sequence=sequence, - run_config=small_bfd_msa_config, - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - ) - uniprot_msa_future = executor.submit( - msa.get_msa, - target_sequence=sequence, - run_config=uniprot_msa_config, - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - ) - uniref90_msa = uniref90_msa_future.result() - mgnify_msa = mgnify_msa_future.result() - small_bfd_msa = small_bfd_msa_future.result() - uniprot_msa = uniprot_msa_future.result() - logging.info( - 'Getting protein MSAs took %.2f seconds for sequence %s', - time.time() - msa_start_time, - sequence, - ) - - logging.info('Deduplicating MSAs for sequence %s', sequence) - msa_dedupe_start_time = time.time() - with futures.ThreadPoolExecutor() as executor: - unpaired_protein_msa_future = executor.submit( - msa.Msa.from_multiple_msas, - msas=[uniref90_msa, small_bfd_msa, mgnify_msa], - deduplicate=True, - ) - paired_protein_msa_future = executor.submit( - msa.Msa.from_multiple_msas, msas=[uniprot_msa], deduplicate=False - ) - unpaired_protein_msa = unpaired_protein_msa_future.result() - paired_protein_msa = paired_protein_msa_future.result() - logging.info( - 'Deduplicating MSAs took %.2f seconds for sequence %s', - time.time() - msa_dedupe_start_time, - sequence, - ) - - protein_templates = _get_protein_templates( - sequence=sequence, - input_msa_a3m=unpaired_protein_msa.to_a3m(), - run_template_search=run_template_search, - templates_config=templates_config, - pdb_database_path=pdb_database_path, - ) - - return unpaired_protein_msa, paired_protein_msa, protein_templates - - -# Cache to avoid re-running the Nhmmer for the same sequence in homomers. -@functools.cache -def _get_rna_msa( - sequence: str, - nt_rna_msa_config: msa_config.NhmmerConfig, - rfam_msa_config: msa_config.NhmmerConfig, - rnacentral_msa_config: msa_config.NhmmerConfig, -) -> msa.Msa: - """Processes a single RNA chain.""" - logging.info('Getting RNA MSAs for sequence %s', sequence) - rna_msa_start_time = time.time() - # Run various MSA tools in parallel. Use a ThreadPoolExecutor because - # they're not blocked by the GIL, as they're sub-shelled out. - with futures.ThreadPoolExecutor() as executor: - nt_rna_msa_future = executor.submit( - msa.get_msa, - target_sequence=sequence, - run_config=nt_rna_msa_config, - chain_poly_type=mmcif_names.RNA_CHAIN, - ) - rfam_msa_future = executor.submit( - msa.get_msa, - target_sequence=sequence, - run_config=rfam_msa_config, - chain_poly_type=mmcif_names.RNA_CHAIN, - ) - rnacentral_msa_future = executor.submit( - msa.get_msa, - target_sequence=sequence, - run_config=rnacentral_msa_config, - chain_poly_type=mmcif_names.RNA_CHAIN, - ) - nt_rna_msa = nt_rna_msa_future.result() - rfam_msa = rfam_msa_future.result() - rnacentral_msa = rnacentral_msa_future.result() - logging.info( - 'Getting RNA MSAs took %.2f seconds for sequence %s', - time.time() - rna_msa_start_time, - sequence, - ) - - return msa.Msa.from_multiple_msas( - msas=[rfam_msa, rnacentral_msa, nt_rna_msa], - deduplicate=True, - ) - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class DataPipelineConfig: - """The configuration for the data pipeline. - - Attributes: - jackhmmer_binary_path: Jackhmmer binary path, used for protein MSA search. - nhmmer_binary_path: Nhmmer binary path, used for RNA MSA search. - hmmalign_binary_path: Hmmalign binary path, used to align hits to the query - profile. - hmmsearch_binary_path: Hmmsearch binary path, used for template search. - hmmbuild_binary_path: Hmmbuild binary path, used to build HMM profile from - raw MSA in template search. - small_bfd_database_path: Small BFD database path, used for protein MSA - search. - mgnify_database_path: Mgnify database path, used for protein MSA search. - uniprot_cluster_annot_database_path: Uniprot database path, used for protein - paired MSA search. - uniref90_database_path: UniRef90 database path, used for MSA search, and the - MSA obtained by searching it is used to construct the profile for template - search. - ntrna_database_path: NT-RNA database path, used for RNA MSA search. - rfam_database_path: Rfam database path, used for RNA MSA search. - rna_central_database_path: RNAcentral database path, used for RNA MSA - search. - seqres_database_path: PDB sequence database path, used for template search. - pdb_database_path: PDB database directory with mmCIF files path, used for - template search. - jackhmmer_n_cpu: Number of CPUs to use for Jackhmmer. - nhmmer_n_cpu: Number of CPUs to use for Nhmmer. - max_template_date: The latest date of templates to use. - """ - - # Binary paths. - jackhmmer_binary_path: str - nhmmer_binary_path: str - hmmalign_binary_path: str - hmmsearch_binary_path: str - hmmbuild_binary_path: str - - # Jackhmmer databases. - small_bfd_database_path: str - mgnify_database_path: str - uniprot_cluster_annot_database_path: str - uniref90_database_path: str - # Nhmmer databases. - ntrna_database_path: str - rfam_database_path: str - rna_central_database_path: str - # Template search databases. - seqres_database_path: str - pdb_database_path: str - - # Optional configuration for MSA tools. - jackhmmer_n_cpu: int = 8 - nhmmer_n_cpu: int = 8 - - max_template_date: datetime.date - - -class DataPipeline: - """Runs the alignment tools and assembles the input features.""" - - def __init__(self, data_pipeline_config: DataPipelineConfig): - """Initializes the data pipeline with default configurations.""" - self._uniref90_msa_config = msa_config.RunConfig( - config=msa_config.JackhmmerConfig( - binary_path=data_pipeline_config.jackhmmer_binary_path, - database_config=msa_config.DatabaseConfig( - name='uniref90', - path=data_pipeline_config.uniref90_database_path, - ), - n_cpu=data_pipeline_config.jackhmmer_n_cpu, - n_iter=1, - e_value=1e-4, - z_value=None, - max_sequences=10_000, - ), - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - crop_size=None, - ) - self._mgnify_msa_config = msa_config.RunConfig( - config=msa_config.JackhmmerConfig( - binary_path=data_pipeline_config.jackhmmer_binary_path, - database_config=msa_config.DatabaseConfig( - name='mgnify', - path=data_pipeline_config.mgnify_database_path, - ), - n_cpu=data_pipeline_config.jackhmmer_n_cpu, - n_iter=1, - e_value=1e-4, - z_value=None, - max_sequences=5_000, - ), - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - crop_size=None, - ) - self._small_bfd_msa_config = msa_config.RunConfig( - config=msa_config.JackhmmerConfig( - binary_path=data_pipeline_config.jackhmmer_binary_path, - database_config=msa_config.DatabaseConfig( - name='small_bfd', - path=data_pipeline_config.small_bfd_database_path, - ), - n_cpu=data_pipeline_config.jackhmmer_n_cpu, - n_iter=1, - e_value=1e-4, - # Set z_value=138_515_945 to match the z_value used in the paper. - # In practice, this has minimal impact on predicted structures. - z_value=None, - max_sequences=5_000, - ), - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - crop_size=None, - ) - self._uniprot_msa_config = msa_config.RunConfig( - config=msa_config.JackhmmerConfig( - binary_path=data_pipeline_config.jackhmmer_binary_path, - database_config=msa_config.DatabaseConfig( - name='uniprot_cluster_annot', - path=data_pipeline_config.uniprot_cluster_annot_database_path, - ), - n_cpu=data_pipeline_config.jackhmmer_n_cpu, - n_iter=1, - e_value=1e-4, - z_value=None, - max_sequences=50_000, - ), - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - crop_size=None, - ) - self._nt_rna_msa_config = msa_config.RunConfig( - config=msa_config.NhmmerConfig( - binary_path=data_pipeline_config.nhmmer_binary_path, - hmmalign_binary_path=data_pipeline_config.hmmalign_binary_path, - hmmbuild_binary_path=data_pipeline_config.hmmbuild_binary_path, - database_config=msa_config.DatabaseConfig( - name='nt_rna', - path=data_pipeline_config.ntrna_database_path, - ), - n_cpu=data_pipeline_config.nhmmer_n_cpu, - e_value=1e-3, - alphabet='rna', - max_sequences=10_000, - ), - chain_poly_type=mmcif_names.RNA_CHAIN, - crop_size=None, - ) - self._rfam_msa_config = msa_config.RunConfig( - config=msa_config.NhmmerConfig( - binary_path=data_pipeline_config.nhmmer_binary_path, - hmmalign_binary_path=data_pipeline_config.hmmalign_binary_path, - hmmbuild_binary_path=data_pipeline_config.hmmbuild_binary_path, - database_config=msa_config.DatabaseConfig( - name='rfam_rna', - path=data_pipeline_config.rfam_database_path, - ), - n_cpu=data_pipeline_config.nhmmer_n_cpu, - e_value=1e-3, - alphabet='rna', - max_sequences=10_000, - ), - chain_poly_type=mmcif_names.RNA_CHAIN, - crop_size=None, - ) - self._rnacentral_msa_config = msa_config.RunConfig( - config=msa_config.NhmmerConfig( - binary_path=data_pipeline_config.nhmmer_binary_path, - hmmalign_binary_path=data_pipeline_config.hmmalign_binary_path, - hmmbuild_binary_path=data_pipeline_config.hmmbuild_binary_path, - database_config=msa_config.DatabaseConfig( - name='rna_central_rna', - path=data_pipeline_config.rna_central_database_path, - ), - n_cpu=data_pipeline_config.nhmmer_n_cpu, - e_value=1e-3, - alphabet='rna', - max_sequences=10_000, - ), - chain_poly_type=mmcif_names.RNA_CHAIN, - crop_size=None, - ) - - self._templates_config = msa_config.TemplatesConfig( - template_tool_config=msa_config.TemplateToolConfig( - database_path=data_pipeline_config.seqres_database_path, - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - hmmsearch_config=msa_config.HmmsearchConfig( - hmmsearch_binary_path=data_pipeline_config.hmmsearch_binary_path, - hmmbuild_binary_path=data_pipeline_config.hmmbuild_binary_path, - filter_f1=0.1, - filter_f2=0.1, - filter_f3=0.1, - e_value=100, - inc_e=100, - dom_e=100, - incdom_e=100, - alphabet='amino', - ), - ), - filter_config=msa_config.TemplateFilterConfig( - max_subsequence_ratio=0.95, - min_align_ratio=0.1, - min_hit_length=10, - deduplicate_sequences=True, - max_hits=4, - max_template_date=data_pipeline_config.max_template_date, - ), - ) - self._pdb_database_path = data_pipeline_config.pdb_database_path - - def process_protein_chain( - self, chain: folding_input.ProteinChain - ) -> folding_input.ProteinChain: - """Processes a single protein chain.""" - has_unpaired_msa = chain.unpaired_msa is not None - has_paired_msa = chain.paired_msa is not None - has_templates = chain.templates is not None - - if not has_unpaired_msa and not has_paired_msa and not chain.templates: - # MSA None - search. Templates either [] - don't search, or None - search. - unpaired_msa, paired_msa, template_hits = _get_protein_msa_and_templates( - sequence=chain.sequence, - # Skip template search if []. - run_template_search=not has_templates, - uniref90_msa_config=self._uniref90_msa_config, - mgnify_msa_config=self._mgnify_msa_config, - small_bfd_msa_config=self._small_bfd_msa_config, - uniprot_msa_config=self._uniprot_msa_config, - templates_config=self._templates_config, - pdb_database_path=self._pdb_database_path, - ) - unpaired_msa = unpaired_msa.to_a3m() - paired_msa = paired_msa.to_a3m() - templates = [ - folding_input.Template( - mmcif=struct.to_mmcif(), - query_to_template_map=hit.query_to_hit_mapping, - ) - for hit, struct in template_hits.get_hits_with_structures() - ] - elif has_unpaired_msa and has_paired_msa and not has_templates: - # Has MSA, but doesn't have templates. Search for templates only. - empty_msa = msa.Msa.from_empty( - query_sequence=chain.sequence, - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - ).to_a3m() - unpaired_msa = chain.unpaired_msa or empty_msa - paired_msa = chain.paired_msa or empty_msa - template_hits = _get_protein_templates( - sequence=chain.sequence, - input_msa_a3m=unpaired_msa, - run_template_search=True, - templates_config=self._templates_config, - pdb_database_path=self._pdb_database_path, - ) - templates = [ - folding_input.Template( - mmcif=struct.to_mmcif(), - query_to_template_map=hit.query_to_hit_mapping, - ) - for hit, struct in template_hits.get_hits_with_structures() - ] - else: - # Has MSA and templates, don't search for anything. - if not has_unpaired_msa or not has_paired_msa or not has_templates: - raise ValueError( - f'Protein chain {chain.id} has unpaired MSA, paired MSA, or' - ' templates set only partially. If you want to run the pipeline' - ' with custom MSA/templates, you need to set all of them. You can' - ' set MSA to empty string and templates to empty list to signify' - ' that they should not be used and searched for.' - ) - logging.info( - 'Skipping MSA and template search for protein chain %s because it ' - 'already has MSAs and templates.', - chain.id, - ) - if not chain.unpaired_msa: - logging.info( - 'Using empty unpaired MSA for protein chain %s', chain.id) - if not chain.paired_msa: - logging.info( - 'Using empty paired MSA for protein chain %s', chain.id) - if not chain.templates: - logging.info( - 'Using no templates for protein chain %s', chain.id) - empty_msa = msa.Msa.from_empty( - query_sequence=chain.sequence, - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - ).to_a3m() - unpaired_msa = chain.unpaired_msa or empty_msa - paired_msa = chain.paired_msa or empty_msa - templates = chain.templates - - return dataclasses.replace( - chain, - unpaired_msa=unpaired_msa, - paired_msa=paired_msa, - templates=templates, - ) - - def process_rna_chain( - self, chain: folding_input.RnaChain - ) -> folding_input.RnaChain: - """Processes a single RNA chain.""" - if chain.unpaired_msa is not None: - # Don't run MSA tools if the chain already has an MSA. - logging.info( - 'Skipping MSA search for RNA chain %s because it already has MSA.', - chain.id, - ) - if not chain.unpaired_msa: - logging.info( - 'Using empty unpaired MSA for RNA chain %s', chain.id) - empty_msa = msa.Msa.from_empty( - query_sequence=chain.sequence, chain_poly_type=mmcif_names.RNA_CHAIN - ).to_a3m() - unpaired_msa = chain.unpaired_msa or empty_msa - else: - unpaired_msa = _get_rna_msa( - sequence=chain.sequence, - nt_rna_msa_config=self._nt_rna_msa_config, - rfam_msa_config=self._rfam_msa_config, - rnacentral_msa_config=self._rnacentral_msa_config, - ).to_a3m() - return dataclasses.replace(chain, unpaired_msa=unpaired_msa) - - def process(self, fold_input: folding_input.Input) -> folding_input.Input: - """Runs MSA and template tools and returns a new Input with the results.""" - processed_chains = [] - for chain in fold_input.chains: - print(f'Processing chain {chain.id}') - process_chain_start_time = time.time() - match chain: - case folding_input.ProteinChain(): - processed_chains.append(self.process_protein_chain(chain)) - case folding_input.RnaChain(): - processed_chains.append(self.process_rna_chain(chain)) - case _: - processed_chains.append(chain) - print( - f'Processing chain {chain.id} took' - f' {time.time() - process_chain_start_time:.2f} seconds', - ) - - return dataclasses.replace(fold_input, chains=processed_chains) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/structure_stores.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/structure_stores.py deleted file mode 100644 index 02ed5e6fe..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/structure_stores.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Library for loading structure data from various sources.""" - -from collections.abc import Mapping, Sequence -import functools -import os -import pathlib -import tarfile -from typing import Union - - -class NotFoundError(KeyError): - """Raised when the structure store doesn't contain the requested target.""" - - -class StructureStore: - """Handles the retrieval of mmCIF files from a filesystem.""" - - def __init__( - self, - structures: Union[str, os.PathLike[str], Mapping[str, str]], - ): - """Initialises the instance. - - Args: - structures: Path of the directory where the mmCIF files are or a Mapping - from target name to mmCIF string. - """ - if isinstance(structures, Mapping): - self._structure_mapping = structures - self._structure_path = None - self._structure_tar = None - else: - self._structure_mapping = None - path_str = os.fspath(structures) - if path_str.endswith('.tar'): - self._structure_tar = tarfile.open(path_str, 'r') # pylint: disable=consider-using-with - self._structure_path = None - else: - self._structure_path = pathlib.Path(structures) - self._structure_tar = None - - @functools.cached_property - def _tar_members(self) -> Mapping[str, tarfile.TarInfo]: - return { - path.stem: tarinfo - for tarinfo in self._structure_tar.getmembers() - if tarinfo.isfile() - and (path := pathlib.Path(tarinfo.path.lower())).suffix == '.cif' - } - - def get_mmcif_str(self, target_name: str) -> str: - """Returns an mmCIF for a given `target_name`. - - Args: - target_name: Name specifying the target mmCIF. - - Raises: - NotFoundError: If the target is not found. - """ - if self._structure_mapping is not None: - try: - return self._structure_mapping[target_name] - except KeyError as e: - raise NotFoundError(f'{target_name=} not found') from e - - if self._structure_tar is not None: - try: - member = self._tar_members[target_name] - if struct_file := self._structure_tar.extractfile(member): - return struct_file.read().decode() - raise NotFoundError(f'{target_name=} not found') - except KeyError: - raise NotFoundError(f'{target_name=} not found') from None - - filepath = self._structure_path / f'{target_name}.cif' - try: - return filepath.read_text() - except FileNotFoundError as e: - raise NotFoundError( - f'{target_name=} not found at {filepath=}') from e - - def target_names(self) -> Sequence[str]: - """Returns all targets in the store.""" - if self._structure_mapping is not None: - return [*self._structure_mapping.keys()] - if self._structure_tar is not None: - return sorted(self._tar_members.keys()) - if self._structure_path is not None: - return sorted([path.stem for path in self._structure_path.glob('*.cif')]) - return () diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/template_realign.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/template_realign.py deleted file mode 100644 index 6b4d0215d..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/template_realign.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Realign sequences found in PDB seqres to the actual CIF sequences.""" - -from collections.abc import Mapping - - -class AlignmentError(Exception): - """Failed alignment between the hit sequence and the actual mmCIF sequence.""" - - -def realign_hit_to_structure( - *, - hit_sequence: str, - hit_start_index: int, - hit_end_index: int, - full_length: int, - structure_sequence: str, - query_to_hit_mapping: Mapping[int, int], -) -> Mapping[int, int]: - """Realigns the hit sequence to the Structure sequence. - - For example, for the given input: - query_sequence : ABCDEFGHIJKL - hit_sequence : ---DEFGHIJK- - struc_sequence : XDEFGHKL - the mapping is {3: 0, 4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 7}. However, the - actual Structure sequence has an extra X at the start as well as no IJ. So the - alignment from the query to the Structure sequence will be: - hit_sequence : ---DEFGHIJK- - struc_aligned : --XDEFGH--KL - and the new mapping will therefore be: {3: 1, 4: 2, 5: 3, 6: 4, 7: 5, 10: 6}. - - Args: - hit_sequence: The PDB seqres hit sequence obtained from Hmmsearch, but - without any gaps. This is not the full PDB seqres template sequence but - rather just its subsequence from hit_start_index to hit_end_index. - hit_start_index: The start index of the hit sequence in the full PDB seqres - template sequence (inclusive). - hit_end_index: The end index of the hit sequence in the full PDB seqres - template sequence (exclusive). - full_length: The length of the full PDB seqres template sequence. - structure_sequence: The actual sequence extracted from the Structure - corresponding to this template. In vast majority of cases this is the same - as the PDB seqres sequence, but this function handles the cases when not. - query_to_hit_mapping: The mapping from the query sequence to the - hit_sequence. - - Raises: - AlignmentError: if the alignment between the sequence returned by Hmmsearch - differs from the actual sequence found in the mmCIF and can't be aligned - using the simple alignment algorithm. - - Returns: - A mapping from the query sequence to the actual Structure sequence. - """ - max_num_gaps = full_length - len(structure_sequence) - if max_num_gaps < 0: - raise AlignmentError( - f'The Structure sequence ({len(structure_sequence)}) ' - f'must be shorter than the PDB seqres sequence ({full_length}):\n' - f'Structure sequence : {structure_sequence}\n' - f'PDB seqres sequence: {hit_sequence}' - ) - - if len(hit_sequence) != hit_end_index - hit_start_index: - raise AlignmentError( - f'The difference of {hit_end_index=} and {hit_start_index=} does not ' - f'equal to the length of the {hit_sequence}: {len(hit_sequence)}' - ) - - best_score = -1 - best_start = 0 - best_query_to_hit_mapping = query_to_hit_mapping - max_num_gaps_before_subseq = min(hit_start_index, max_num_gaps) - # It is possible the gaps needed to align the PDB seqres subsequence and - # the Structure subsequence need to be inserted before the match region. - # Try and pick the alignment with the best number of aligned residues. - for num_gaps_before_subseq in range(0, max_num_gaps_before_subseq + 1): - start = hit_start_index - num_gaps_before_subseq - end = hit_end_index - num_gaps_before_subseq - structure_subseq = structure_sequence[start:end] - - new_query_to_hit_mapping, score = _remap_to_struc_seq( - hit_seq=hit_sequence, - struc_seq=structure_subseq, - max_num_gaps=max_num_gaps - num_gaps_before_subseq, - mapping=query_to_hit_mapping, - ) - if score >= best_score: - # Use >= to prefer matches with larger number of gaps before. - best_score = score - best_start = start - best_query_to_hit_mapping = new_query_to_hit_mapping - - return {q: h + best_start for q, h in best_query_to_hit_mapping.items()} - - -def _remap_to_struc_seq( - *, - hit_seq: str, - struc_seq: str, - max_num_gaps: int, - mapping: Mapping[int, int], -) -> tuple[Mapping[int, int], int]: - """Remaps the query -> hit mapping to match the actual Structure sequence. - - Args: - hit_seq: The hit sequence - a subsequence of the PDB seqres sequence without - any Hmmsearch modifications like inserted gaps or lowercased residues. - struc_seq: The actual sequence obtained from the corresponding Structure. - max_num_gaps: The maximum number of gaps that can be inserted in the - Structure sequence. In practice, this is the length difference between the - PDB seqres sequence and the actual Structure sequence. - mapping: The mapping from the query residues to the hit residues. This will - be remapped to point to the actual Structure sequence using a simple - realignment algorithm. - - Returns: - A tuple of (mapping, score): - * Mapping from the query to the actual Structure sequence. - * Score which is the number of matching aligned residues. - - Raises: - ValueError if the structure sequence isn't shorter than the seqres sequence. - ValueError if the alignment fails. - """ - hit_seq_idx = 0 - struc_seq_idx = 0 - hit_to_struc_seq_mapping = {} - score = 0 - - # This while loop is guaranteed to terminate since we increase both - # struc_seq_idx and hit_seq_idx by at least 1 in each iteration. - remaining_num_gaps = max_num_gaps - while hit_seq_idx < len(hit_seq) and struc_seq_idx < len(struc_seq): - if hit_seq[hit_seq_idx] != struc_seq[struc_seq_idx]: - # Explore which alignment aligns the next residue (if present). - best_shift = 0 - for shift in range(0, remaining_num_gaps + 1): - next_hit_res = hit_seq[hit_seq_idx + - shift: hit_seq_idx + shift + 1] - next_struc_res = struc_seq[struc_seq_idx: struc_seq_idx + 1] - if next_hit_res == next_struc_res: - best_shift = shift - break - hit_seq_idx += best_shift - remaining_num_gaps -= best_shift - - hit_to_struc_seq_mapping[hit_seq_idx] = struc_seq_idx - score += hit_seq[hit_seq_idx] == struc_seq[struc_seq_idx] - hit_seq_idx += 1 - struc_seq_idx += 1 - - fixed_mapping = {} - for query_idx, original_hit_idx in mapping.items(): - fixed_hit_idx = hit_to_struc_seq_mapping.get(original_hit_idx) - if fixed_hit_idx is not None: - fixed_mapping[query_idx] = fixed_hit_idx - - return fixed_mapping, score diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/template_store.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/template_store.py deleted file mode 100644 index 17dd24579..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/template_store.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Interface and implementations for fetching templates data.""" - -from collections.abc import Mapping -import datetime -from typing import Any, Protocol, TypeAlias, Optional - - -TemplateFeatures: TypeAlias = Mapping[str, Any] - - -class TemplateFeatureProvider(Protocol): - """Interface for providing Template Features.""" - - def __call__( - self, - sequence: str, - release_date: Optional[datetime.date], - include_ligand_features: bool = True, - ) -> TemplateFeatures: - """Retrieve template features for the given sequence and release_date. - - Args: - sequence: The residue sequence of the query. - release_date: The release_date of the template query, this is used to - filter templates for training, ensuring that they do not leak structure - information from the future. - include_ligand_features: Whether to include ligand features. - - Returns: - Template features: A mapping of template feature labels to features, which - may be numpy arrays, bytes objects, or for the special case of label - `ligand_features`, a nested feature map of labels to numpy arrays. - - Raises: - TemplateRetrievalError if the template features were not found. - """ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/templates.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/templates.py deleted file mode 100644 index ded1173e5..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/templates.py +++ /dev/null @@ -1,969 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""API for retrieving and manipulating template search results.""" - -from collections.abc import Iterable, Iterator, Mapping, Sequence -import dataclasses -import datetime -import functools -import os -import re -from typing import Any, Final, Self, TypeAlias, Union, Optional -import numpy as np -from absl import logging -from alphafold3 import structure -from alphafold3.common import resources -from alphafold3.constants import atom_types -from alphafold3.constants import mmcif_names -from alphafold3.constants import residue_names -from alphafold3.data import msa_config -from alphafold3.data import parsers -from alphafold3.data import structure_stores -from alphafold3.data import template_realign -from alphafold3.data.tools import hmmsearch -from alphafold3.structure import mmcif - - -_POLYMER_FEATURES: Final[Mapping[str, Union[np.float64, np.int32, object]]] = { - 'template_aatype': np.int32, - 'template_all_atom_masks': np.float64, - 'template_all_atom_positions': np.float64, - 'template_domain_names': object, - 'template_release_date': object, - 'template_sequence': object, -} - -_LIGAND_FEATURES: Final[Mapping[str, Any]] = { - 'ligand_features': Mapping[str, Any] -} - - -TemplateFeatures: TypeAlias = Mapping[ - str, Union[np.ndarray, bytes, Mapping[str, Union[np.ndarray, bytes]]] -] -_REQUIRED_METADATA_COLUMNS: Final[Sequence[str]] = ( - 'seq_release_date', - 'seq_unresolved_res_num', - 'seq_author_chain_id', - 'seq_sequence', -) - - -@dataclasses.dataclass(frozen=True) -class _Polymer: - """Container for alphabet specific (dna, rna, protein) atom information.""" - - min_atoms: int - num_atom_types: int - atom_order: Mapping[str, int] - - -_POLYMERS = { - mmcif_names.PROTEIN_CHAIN: _Polymer( - min_atoms=5, - num_atom_types=atom_types.ATOM37_NUM, - atom_order=atom_types.ATOM37_ORDER, - ), - mmcif_names.DNA_CHAIN: _Polymer( - min_atoms=21, - num_atom_types=atom_types.ATOM29_NUM, - atom_order=atom_types.ATOM29_ORDER, - ), - mmcif_names.RNA_CHAIN: _Polymer( - min_atoms=20, - num_atom_types=atom_types.ATOM29_NUM, - atom_order=atom_types.ATOM29_ORDER, - ), -} - - -def _encode_restype( - chain_poly_type: str, - sequence: str, -) -> Sequence[int]: - """Encodes a sequence of residue names as a sequence of ints. - - Args: - chain_poly_type: Polymer chain type to determine sequence encoding. - sequence: Polymer residues. Protein encoded by single letters. RNA and DNA - encoded by multi-letter CCD codes. - - Returns: - A sequence of integers encoding amino acid types for the given chain type. - """ - if chain_poly_type == mmcif_names.PROTEIN_CHAIN: - return [ - residue_names.PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP_TO_INT[ - _STANDARDIZED_AA.get(res, res) - ] - for res in sequence - ] - - unk_nucleic = residue_names.UNK_NUCLEIC_ONE_LETTER - unk_nucleic_idx = residue_names.POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP[ - unk_nucleic - ] - if chain_poly_type == mmcif_names.RNA_CHAIN: - return [ - residue_names.POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP.get( - res, unk_nucleic_idx - ) - for res in sequence - ] - if chain_poly_type == mmcif_names.DNA_CHAIN: - # Map UNK DNA to the generic nucleic UNK (N), which happens to also be the - # same as the RNA UNK. - return [ - residue_names.POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP.get( - residue_names.DNA_COMMON_ONE_TO_TWO.get(res, unk_nucleic), - unk_nucleic_idx, - ) - for res in sequence - ] - - raise NotImplementedError(f'"{chain_poly_type}" unsupported.') - - -_DAYS_BEFORE_QUERY_DATE: Final[int] = 60 -_HIT_DESCRIPTION_REGEX = re.compile( - r'(?P[a-z0-9]{4,})_(?P\w+)/(?P\d+)-(?P\d+) ' - r'.* length:(?P\d+)\b.*' -) - -_STANDARDIZED_AA = {'B': 'D', 'J': 'X', 'O': 'X', 'U': 'C', 'Z': 'E'} - - -class Error(Exception): - """Base class for exceptions.""" - - -class HitDateError(Error): - """An error indicating that invalid release date was detected.""" - - -class InvalidTemplateError(Error): - """An error indicating that template is invalid.""" - - -@dataclasses.dataclass(frozen=True) -class Hit: - """Template hit metrics derived from the MSA for filtering and featurising. - - Attributes: - pdb_id: The PDB ID of the hit. - auth_chain_id: The author chain ID of the hit. - hmmsearch_sequence: Hit sequence as given in hmmsearch a3m output. - structure_sequence: Hit sequence as given in PDB structure. - unresolved_res_indices: Indices of unresolved residues in the structure - sequence. 0-based. - query_sequence: The query nucleotide/amino acid sequence. - start_index: The start index of the sequence relative to the full PDB seqres - sequence. Inclusive and uses 0-based indexing. - end_index: The end index of the sequence relative to the full PDB seqres - sequence. Exclusive and uses 0-based indexing. - full_length: Length of the full PDB seqres sequence. This can be different - from the length from the actual sequence we get from the mmCIF and we use - this to detect whether we need to realign or not. - release_date: The release date of the PDB corresponding to this hit. - chain_poly_type: The polymer type of the selected hit structure. - """ - - pdb_id: str - auth_chain_id: str - hmmsearch_sequence: str - structure_sequence: str - unresolved_res_indices: Optional[Sequence[int]] - query_sequence: str - start_index: int - end_index: int - full_length: int - release_date: datetime.date - chain_poly_type: str - - @functools.cached_property - def query_to_hit_mapping(self) -> Mapping[int, int]: - """0-based query index to hit index mapping.""" - query_to_hit_mapping = {} - hit_index = 0 - query_index = 0 - for residue in self.hmmsearch_sequence: - # Gap inserted in the template - if residue == '-': - query_index += 1 - # Deleted residue in the template (would be a gap in the query). - elif residue.islower(): - hit_index += 1 - # Normal aligned residue, in both query and template. Add to mapping. - elif residue.isupper(): - query_to_hit_mapping[query_index] = hit_index - query_index += 1 - hit_index += 1 - - structure_subseq = self.structure_sequence[ - self.start_index: self.end_index - ] - if self.matching_sequence != structure_subseq: - # The seqres sequence doesn't match the structure sequence. Two cases: - # 1. The sequences have the same length. The sequences are different - # because our 3->1 residue code mapping is different from the one PDB - # uses. We don't do anything in this case as both sequences have the - # same length, so the original query to hit mapping stays valid. - # 2. The sequences don't have the same length, the one in structure is - # shorter. In this case we change the mapping to match the actual - # structure sequence using a simple realignment algorithm. - # This procedure was validated on all PDB seqres (2023_01_12) sequences - # and handles all cases that can happen. - if self.full_length != len(self.structure_sequence): - return template_realign.realign_hit_to_structure( - hit_sequence=self.matching_sequence, - hit_start_index=self.start_index, - hit_end_index=self.end_index, - full_length=self.full_length, - structure_sequence=self.structure_sequence, - query_to_hit_mapping=query_to_hit_mapping, - ) - - # Hmmsearch returns a subsequence and so far indices have been relative to - # the subsequence. Add an offset to index relative to the full structure - # sequence. - return {q: h + self.start_index for q, h in query_to_hit_mapping.items()} - - @property - def matching_sequence(self) -> str: - """Returns the matching hit sequence including insertions. - - Make deleted residues uppercase and remove gaps ("-"). - """ - return self.hmmsearch_sequence.upper().replace('-', '') - - @functools.cached_property - def output_templates_sequence(self) -> str: - """Returns the final template sequence.""" - result_seq = ['-'] * len(self.query_sequence) - for query_index, template_index in self.query_to_hit_mapping.items(): - result_seq[query_index] = self.structure_sequence[template_index] - return ''.join(result_seq) - - @property - def length_ratio(self) -> float: - """Ratio of the length of the hit sequence to the query.""" - return len(self.matching_sequence) / len(self.query_sequence) - - @property - def align_ratio(self) -> float: - """Ratio of the number of aligned residues to the query length.""" - return len(self.query_to_hit_mapping) / len(self.query_sequence) - - @functools.cached_property - def is_valid(self) -> bool: - """Whether hit can be used as a template.""" - if self.unresolved_res_indices is None: - return False - - return bool( - set(self.query_to_hit_mapping.values()) - - set(self.unresolved_res_indices) - ) - - @property - def full_name(self) -> str: - """A full name of the hit.""" - return f'{self.pdb_id}_{self.auth_chain_id}' - - def __post_init__(self): - if not self.pdb_id.islower() and not self.pdb_id.isdigit(): - raise ValueError(f'pdb_id must be lowercase {self.pdb_id}') - - if not 0 <= self.start_index <= self.end_index: - raise ValueError( - 'Start must be non-negative and less than or equal to end index. ' - f'Range: {self.start_index}-{self.end_index}' - ) - - if len(self.matching_sequence) != (self.end_index - self.start_index): - raise ValueError( - 'Sequence length must be equal to end_index - start_index. ' - f'{len(self.matching_sequence)} != {self.end_index} - ' - f'{self.start_index}' - ) - - if self.full_length < 0: - raise ValueError( - f'Full length must be non-negative: {self.full_length}') - - def keep( - self, - *, - release_date_cutoff: Optional[datetime.date], - max_subsequence_ratio: Optional[float], - min_hit_length: Optional[int], - min_align_ratio: Optional[float], - ) -> bool: - """Returns whether the hit should be kept. - - In addition to filtering on all of the provided parameters, this method also - excludes hits with unresolved residues. - - Args: - release_date_cutoff: Maximum release date of the template. - max_subsequence_ratio: If set, excludes hits which are an exact - subsequence of the query sequence, and longer than this ratio. Useful to - avoid ground truth leakage. - min_hit_length: If set, excludes hits which have fewer residues than this. - min_align_ratio: If set, excludes hits where the number of residues - aligned to the query is less than this proportion of the template - length. - """ - # Exclude hits which are too recent. - if ( - release_date_cutoff is not None - and self.release_date > release_date_cutoff - ): - return False - - # Exclude hits which are large duplicates of the query_sequence. - if ( - max_subsequence_ratio is not None - and self.length_ratio > max_subsequence_ratio - ): - if self.matching_sequence in self.query_sequence: - return False - - # Exclude hits which are too short. - if ( - min_hit_length is not None - and len(self.matching_sequence) < min_hit_length - ): - return False - - # Exclude hits with unresolved residues. - if not self.is_valid: - return False - - # Exclude hits with too few alignments. - try: - if min_align_ratio is not None and self.align_ratio <= min_align_ratio: - return False - except template_realign.AlignmentError as e: - logging.warning('Failed to align %s: %s', self, str(e)) - return False - - return True - - -def _filter_hits( - hits: Iterable[Hit], - release_date_cutoff: datetime.date, - max_subsequence_ratio: Optional[float], - min_align_ratio: Optional[float], - min_hit_length: Optional[int], - deduplicate_sequences: bool, - max_hits: Optional[int], -) -> Sequence[Hit]: - """Filters hits based on the filter config.""" - filtered_hits = [] - seen_before = set() - for hit in hits: - if not hit.keep( - max_subsequence_ratio=max_subsequence_ratio, - min_align_ratio=min_align_ratio, - min_hit_length=min_hit_length, - release_date_cutoff=release_date_cutoff, - ): - continue - - # Remove duplicate templates, keeping the first. - if deduplicate_sequences: - if hit.output_templates_sequence in seen_before: - continue - seen_before.add(hit.output_templates_sequence) - - filtered_hits.append(hit) - if max_hits and len(filtered_hits) == max_hits: - break - - return filtered_hits - - -@dataclasses.dataclass(init=False) -class Templates: - """A container for templates that were found for the given query sequence. - - The structure_store is constructed from the config by default. Callers can - optionally supply a structure_store to the constructor to avoid the cost of - construction and metadata loading. - """ - - def __init__( - self, - *, - query_sequence: str, - hits: Sequence[Hit], - max_template_date: datetime.date, - structure_store: structure_stores.StructureStore, - query_release_date: Optional[datetime.date] = None, - ): - self._query_sequence = query_sequence - self._hits = tuple(hits) - self._max_template_date = max_template_date - self._query_release_date = query_release_date - self._hit_structures = {} - self._structure_store = structure_store - - if any(h.query_sequence != self._query_sequence for h in self.hits): - raise ValueError('All hits must match the query sequence.') - - if self._hits: - chain_poly_type = self._hits[0].chain_poly_type - if any(h.chain_poly_type != chain_poly_type for h in self.hits): - raise ValueError( - 'All hits must have the same chain_poly_type.') - - @classmethod - def from_seq_and_a3m( - cls, - *, - query_sequence: str, - msa_a3m: str, - max_template_date: datetime.date, - database_path: Union[os.PathLike[str], str], - hmmsearch_config: msa_config.HmmsearchConfig, - max_a3m_query_sequences: Optional[int], - structure_store: structure_stores.StructureStore, - filter_config: Optional[msa_config.TemplateFilterConfig] = None, - query_release_date: Optional[datetime.date] = None, - chain_poly_type: str = mmcif_names.PROTEIN_CHAIN, - ) -> Self: - """Creates templates from a run of hmmsearch tool against a custom a3m. - - Args: - query_sequence: The polymer sequence of the target query. - msa_a3m: An a3m of related polymers aligned to the query sequence, this is - used to create an HMM for the hmmsearch run. - max_template_date: This is used to filter templates for training, ensuring - that they do not leak ground truth information used in testing sets. - database_path: A path to the sequence database to search for templates. - hmmsearch_config: Config with Hmmsearch settings. - max_a3m_query_sequences: The maximum number of input MSA sequences to use - to construct the profile which is then used to search for templates. - structure_store: Structure store to fetch template structures from. - filter_config: Optional config that controls which and how many hits to - keep. More performant than constructing and then filtering. If not - provided, no filtering is done. - query_release_date: The release_date of the template query, this is used - to filter templates for training, ensuring that they do not leak - structure information from the future. - chain_poly_type: The polymer type of the templates. - - Returns: - Templates object containing a list of Hits initialised from the - structure_store metadata and a3m alignments. - """ - hmmsearch_a3m = run_hmmsearch_with_a3m( - database_path=database_path, - hmmsearch_config=hmmsearch_config, - max_a3m_query_sequences=max_a3m_query_sequences, - a3m=msa_a3m, - ) - return cls.from_hmmsearch_a3m( - query_sequence=query_sequence, - a3m=hmmsearch_a3m, - max_template_date=max_template_date, - query_release_date=query_release_date, - chain_poly_type=chain_poly_type, - structure_store=structure_store, - filter_config=filter_config, - ) - - @classmethod - def from_hmmsearch_a3m( - cls, - *, - query_sequence: str, - a3m: str, - max_template_date: datetime.date, - structure_store: structure_stores.StructureStore, - filter_config: Optional[msa_config.TemplateFilterConfig] = None, - query_release_date: Optional[datetime.date] = None, - chain_poly_type: str = mmcif_names.PROTEIN_CHAIN, - ) -> Self: - """Creates Templates from a Hmmsearch A3M. - - Args: - query_sequence: The polymer sequence of the target query. - a3m: Results of Hmmsearch in A3M format. This provides a list of potential - template alignments and pdb codes. - max_template_date: This is used to filter templates for training, ensuring - that they do not leak ground truth information used in testing sets. - structure_store: Structure store to fetch template structures from. - filter_config: Optional config that controls which and how many hits to - keep. More performant than constructing and then filtering. If not - provided, no filtering is done. - query_release_date: The release_date of the template query, this is used - to filter templates for training, ensuring that they do not leak - structure information from the future. - chain_poly_type: The polymer type of the templates. - - Returns: - Templates object containing a list of Hits initialised from the - structure_store metadata and a3m alignments. - """ - - def hit_generator(a3m: str): - for hit_seq, hit_desc in parsers.lazy_parse_fasta_string(a3m): - pdb_id, auth_chain_id, start, end, full_length = _parse_hit_description( - hit_desc - ) - - release_date, sequence, unresolved_res_ids = _parse_hit_metadata( - structure_store, pdb_id, auth_chain_id - ) - if unresolved_res_ids is None: - continue - - # seq_unresolved_res_num are 1-based, setting to 0-based indices. - unresolved_indices = [i - 1 for i in unresolved_res_ids] - - yield Hit( - pdb_id=pdb_id, - auth_chain_id=auth_chain_id, - hmmsearch_sequence=hit_seq, - structure_sequence=sequence, - query_sequence=query_sequence, - unresolved_res_indices=unresolved_indices, - # Raw value is residue number, not index. - start_index=start - 1, - end_index=end, - full_length=full_length, - release_date=datetime.date.fromisoformat(release_date), - chain_poly_type=chain_poly_type, - ) - - if filter_config is None: - hits = tuple(hit_generator(a3m)) - else: - hits = _filter_hits( - hit_generator(a3m), - release_date_cutoff=filter_config.max_template_date, - max_subsequence_ratio=filter_config.max_subsequence_ratio, - min_align_ratio=filter_config.min_align_ratio, - min_hit_length=filter_config.min_hit_length, - deduplicate_sequences=filter_config.deduplicate_sequences, - max_hits=filter_config.max_hits, - ) - - return Templates( - query_sequence=query_sequence, - query_release_date=query_release_date, - hits=hits, - max_template_date=max_template_date, - structure_store=structure_store, - ) - - @property - def query_sequence(self) -> str: - return self._query_sequence - - @property - def hits(self) -> tuple[Hit, ...]: - return self._hits - - @property - def query_release_date(self) -> Optional[datetime.date]: - return self._query_release_date - - @property - def num_hits(self) -> int: - return len(self._hits) - - @functools.cached_property - def release_date_cutoff(self) -> datetime.date: - if self.query_release_date is None: - return self._max_template_date - return min( - self._max_template_date, - self.query_release_date - - datetime.timedelta(days=_DAYS_BEFORE_QUERY_DATE), - ) - - def __repr__(self) -> str: - return f'Templates({self.num_hits} hits)' - - def filter( - self, - *, - max_subsequence_ratio: Optional[float], - min_align_ratio: Optional[float], - min_hit_length: Optional[int], - deduplicate_sequences: bool, - max_hits: Optional[int], - ) -> Self: - """Returns a new Templates object with only the hits that pass all filters. - - This also filters on query_release_date and max_template_date. - - Args: - max_subsequence_ratio: If set, excludes hits which are an exact - subsequence of the query sequence, and longer than this ratio. Useful to - avoid ground truth leakage. - min_align_ratio: If set, excludes hits where the number of residues - aligned to the query is less than this proportion of the template - length. - min_hit_length: If set, excludes hits which have fewer residues than this. - deduplicate_sequences: Whether to exclude duplicate template sequences, - keeping only the first. This can be useful in increasing the diversity - of hits especially in the case of homomer hits. - max_hits: If set, excludes any hits which exceed this count. - """ - filtered_hits = _filter_hits( - hits=self._hits, - release_date_cutoff=self.release_date_cutoff, - max_subsequence_ratio=max_subsequence_ratio, - min_align_ratio=min_align_ratio, - min_hit_length=min_hit_length, - deduplicate_sequences=deduplicate_sequences, - max_hits=max_hits, - ) - return Templates( - query_sequence=self.query_sequence, - query_release_date=self.query_release_date, - hits=filtered_hits, - max_template_date=self._max_template_date, - structure_store=self._structure_store, - ) - - def get_hits_with_structures( - self, - ) -> Sequence[tuple[Hit, structure.Structure]]: - """Returns hits + Structures, Structures filtered to the hit's chain.""" - results = [] - structures = {struct.name.lower(): struct for struct in self.structures} - for hit in self.hits: - if not hit.is_valid: - raise InvalidTemplateError( - 'Hits must be filtered before calling get_hits_with_structures.' - ) - struct = structures[hit.pdb_id] - label_chain_id = struct.polymer_auth_asym_id_to_label_asym_id().get( - hit.auth_chain_id - ) - results.append((hit, struct.filter(chain_id=label_chain_id))) - return results - - def featurize( - self, - include_ligand_features: bool = True, - ) -> TemplateFeatures: - """Featurises the templates and returns a map of feature names to features. - - NB: If you don't do any prefiltering, this method might be slow to run - as it has to fetch many CIFs and featurize them all. - - Args: - include_ligand_features: Whether to compute ligand features. - - Returns: - Template features: A mapping of template feature labels to features, which - may be numpy arrays, bytes objects, or for the special case of label - `ligand_features` (if `include_ligand_features` is True), a nested - feature map of labels to numpy arrays. - - Raises: - InvalidTemplateError: If hits haven't been filtered before featurization. - """ - hits_by_pdb_id = {} - for idx, hit in enumerate(self.hits): - if not hit.is_valid: - raise InvalidTemplateError( - f'Hits must be filtered before featurizing, got unprocessed {hit=}' - ) - hits_by_pdb_id.setdefault(hit.pdb_id, []).append((idx, hit)) - - unsorted_features = [] - for struct in self.structures: - pdb_id = str(struct.name).lower() - for idx, hit in hits_by_pdb_id[pdb_id]: - try: - label_chain_id = struct.polymer_auth_asym_id_to_label_asym_id()[ - hit.auth_chain_id - ] - hit_features = { - **get_polymer_features( - chain=struct.filter(chain_id=label_chain_id), - chain_poly_type=hit.chain_poly_type, - query_sequence_length=len(hit.query_sequence), - query_to_hit_mapping=hit.query_to_hit_mapping, - ), - } - if include_ligand_features: - hit_features['ligand_features'] = _get_ligand_features( - struct) - unsorted_features.append((idx, hit_features)) - except Error as e: - raise type(e)(f'Failed to featurise {hit=}') from e - - sorted_features = sorted(unsorted_features, key=lambda x: x[0]) - sorted_features = [feat for _, feat in sorted_features] - return package_template_features( - hit_features=sorted_features, - include_ligand_features=include_ligand_features, - ) - - @property - def structures(self) -> Iterator[structure.Structure]: - """Yields template structures for each unique PDB ID among hits. - - If there are multiple hits in the same Structure, the Structure will be - included only once by this method. - - Yields: - A Structure object for each unique PDB ID among hits. - - Raises: - HitDateError: If template's release date exceeds max cutoff date. - """ - - for hit in self.hits: - if hit.release_date > self.release_date_cutoff: # pylint: disable=comparison-with-callable - raise HitDateError( - f'Invalid release date for hit {hit.pdb_id=}, when release date ' - f'cutoff is {self.release_date_cutoff}.' - ) - - # Get the set of pdbs to load. In particular, remove duplicate PDB IDs. - targets_to_load = tuple({hit.pdb_id for hit in self.hits}) - - for target_name in targets_to_load: - yield structure.from_mmcif( - mmcif_string=self._structure_store.get_mmcif_str(target_name), - fix_mse_residues=True, - fix_arginines=True, - include_water=False, - include_bonds=False, - include_other=True, # For non-standard polymer chains. - ) - - -def _parse_hit_description(description: str) -> tuple[str, str, int, int, int]: - """Parses the hmmsearch A3M sequence description line.""" - # Example lines (protein, nucleic, no description): - # >4pqx_A/2-217 [subseq from] mol:protein length:217 Free text - # >4pqx_A/2-217 [subseq from] mol:na length:217 Free text - # >5g3r_A/1-55 [subseq from] mol:protein length:352 - if match := re.fullmatch(_HIT_DESCRIPTION_REGEX, description): - return ( - match['pdb_id'], - match['chain_id'], - int(match['start']), - int(match['end']), - int(match['length']), - ) - raise ValueError(f'Could not parse description "{description}"') - - -def _parse_hit_metadata( - structure_store: structure_stores.StructureStore, - pdb_id: str, - auth_chain_id: str, -) -> tuple[Any, Optional[str], Optional[Sequence[int]]]: - """Parse hit metadata by parsing mmCIF from structure store.""" - try: - cif = mmcif.from_string(structure_store.get_mmcif_str(pdb_id)) - except structure_stores.NotFoundError: - logging.warning('Failed to get mmCIF for %s.', pdb_id) - return None, None, None - release_date = mmcif.get_release_date(cif) - - try: - struct = structure.from_parsed_mmcif( - cif, - model_id=structure.ModelID.ALL, - include_water=True, - include_other=True, - include_bonds=False, - ) - except ValueError: - struct = structure.from_parsed_mmcif( - cif, - model_id=structure.ModelID.FIRST, - include_water=True, - include_other=True, - include_bonds=False, - ) - - sequence = struct.polymer_author_chain_single_letter_sequence( - include_missing_residues=True, - protein=True, - dna=True, - rna=True, - other=True, - )[auth_chain_id] - - unresolved_res_ids = struct.filter( - chain_auth_asym_id=auth_chain_id - ).unresolved_residues.id - - return release_date, sequence, unresolved_res_ids - - -def get_polymer_features( - *, - chain: structure.Structure, - chain_poly_type: str, - query_sequence_length: int, - query_to_hit_mapping: Mapping[int, int], -) -> Mapping[str, Any]: - """Returns features for this polymer chain. - - Args: - chain: Structure object representing the template. Must be already filtered - to a single chain. - chain_poly_type: The chain polymer type (protein, DNA, RNA). - query_sequence_length: The length of the query sequence. - query_to_hit_mapping: 0-based query index to hit index mapping. - - Returns: - A dictionary with polymer features for template_chain_id in the struct. - - Raises: - ValueError: If the input structure contains more than just a single chain. - """ - if len(chain.polymer_auth_asym_id_to_label_asym_id()) != 1: - raise ValueError('The structure must be filtered to a single chain.') - - if chain.name is None: - raise ValueError('The structure must have a name.') - - if chain.release_date is None: - raise ValueError('The structure must have a release date.') - - auth_chain_id, label_chain_id = next( - iter(chain.polymer_auth_asym_id_to_label_asym_id().items()) - ) - chain_sequence = chain.chain_single_letter_sequence()[label_chain_id] - - polymer = _POLYMERS[chain_poly_type] - positions, positions_mask = chain.to_res_arrays( - include_missing_residues=True, atom_order=polymer.atom_order - ) - template_all_atom_positions = np.zeros( - (query_sequence_length, polymer.num_atom_types, 3), dtype=np.float64 - ) - template_all_atom_masks = np.zeros( - (query_sequence_length, polymer.num_atom_types), dtype=np.int64 - ) - - template_sequence = ['-'] * query_sequence_length - for query_index, template_index in query_to_hit_mapping.items(): - template_all_atom_positions[query_index] = positions[template_index] - template_all_atom_masks[query_index] = positions_mask[template_index] - template_sequence[query_index] = chain_sequence[template_index] - - template_sequence = ''.join(template_sequence) - template_aatype = _encode_restype(chain_poly_type, template_sequence) - template_name = f'{chain.name.lower()}_{auth_chain_id}' - release_date = chain.release_date.strftime('%Y-%m-%d') - return { - 'template_all_atom_positions': template_all_atom_positions, - 'template_all_atom_masks': template_all_atom_masks, - 'template_sequence': template_sequence.encode(), - 'template_aatype': np.array(template_aatype, dtype=np.int32), - 'template_domain_names': np.array(template_name.encode(), dtype=object), - 'template_release_date': np.array(release_date.encode(), dtype=object), - } - - -def _get_ligand_features( - struct: structure.Structure, -) -> Mapping[str, Mapping[str, Union[np.ndarray, bytes]]]: - """Returns features for the ligands in this structure.""" - ligand_struct = struct.filter_to_entity_type(ligand=True) - - ligand_features = {} - for ligand_chain_id in ligand_struct.chains: - idxs = np.where(ligand_struct.chain_id == ligand_chain_id)[0] - if idxs.shape[0]: - ligand_features[ligand_chain_id] = { - 'ligand_atom_positions': ligand_struct.coords[idxs, :].astype( - np.float32 - ), - 'ligand_atom_names': ligand_struct.atom_name[idxs].astype(object), - 'ligand_atom_occupancies': ligand_struct.atom_occupancy[idxs].astype( - np.float32 - ), - 'ccd_id': ligand_struct.res_name[idxs][0].encode(), - } - return ligand_features - - -def package_template_features( - *, - hit_features: Sequence[Mapping[str, Any]], - include_ligand_features: bool, -) -> Mapping[str, Any]: - """Stacks polymer features, adds empty and keeps ligand features unstacked.""" - - features_to_include = set(_POLYMER_FEATURES) - if include_ligand_features: - features_to_include.update(_LIGAND_FEATURES) - - features = { - feat: [single_hit_features[feat] - for single_hit_features in hit_features] - for feat in features_to_include - } - - stacked_features = {} - for k, v in features.items(): - if k in _POLYMER_FEATURES: - v = np.stack(v, axis=0) if v else np.array( - [], dtype=_POLYMER_FEATURES[k]) - stacked_features[k] = v - - return stacked_features - - -def _resolve_path(path: Union[os.PathLike[str], str]) -> str: - """Resolves path for data dep paths, stringifies otherwise.""" - # Data dependency paths: db baked into the binary. - resolved_path = resources.filename(path) - if os.path.exists(resolved_path): - return resolved_path - # Other paths, e.g. local. - return str(path) - - -def run_hmmsearch_with_a3m( - *, - database_path: Union[os.PathLike[str], str], - hmmsearch_config: msa_config.HmmsearchConfig, - max_a3m_query_sequences: Optional[int], - a3m: Optional[str], -) -> str: - """Runs Hmmsearch to get a3m string of hits.""" - searcher = hmmsearch.Hmmsearch( - binary_path=hmmsearch_config.hmmsearch_binary_path, - hmmbuild_binary_path=hmmsearch_config.hmmbuild_binary_path, - database_path=_resolve_path(database_path), - e_value=hmmsearch_config.e_value, - inc_e=hmmsearch_config.inc_e, - dom_e=hmmsearch_config.dom_e, - incdom_e=hmmsearch_config.incdom_e, - alphabet=hmmsearch_config.alphabet, - filter_f1=hmmsearch_config.filter_f1, - filter_f2=hmmsearch_config.filter_f2, - filter_f3=hmmsearch_config.filter_f3, - filter_max=hmmsearch_config.filter_max, - ) - # STO enables us to annotate query non-gap columns as reference columns. - sto = parsers.convert_a3m_to_stockholm(a3m, max_a3m_query_sequences) - return searcher.query_with_sto(sto, model_construction='hand') diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmalign.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmalign.py deleted file mode 100644 index e6e010a96..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmalign.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""A Python wrapper for hmmalign from the HMMER Suite.""" - -from collections.abc import Mapping, Sequence -import os -import tempfile -from typing import Optional - -from alphafold3.data import parsers -from alphafold3.data.tools import subprocess_utils - - -def _to_a3m(sequences: Sequence[str], name_prefix: str = 'sequence') -> str: - a3m = '' - for i, sequence in enumerate(sequences, 1): - a3m += f'> {name_prefix} {i}\n{sequence}\n' - return a3m - - -class Hmmalign: - """Python wrapper of the hmmalign binary.""" - - def __init__(self, binary_path: str): - """Initializes the Python hmmalign wrapper. - - Args: - binary_path: Path to the hmmalign binary. - - Raises: - RuntimeError: If hmmalign binary not found within the path. - """ - self.binary_path = binary_path - - subprocess_utils.check_binary_exists( - path=self.binary_path, name='hmmalign') - - def align_sequences( - self, - sequences: Sequence[str], - profile: str, - extra_flags: Optional[Mapping[str, str]] = None, - ) -> str: - """Aligns sequence list to the profile and returns the alignment in A3M.""" - return self.align( - a3m_str=_to_a3m(sequences, name_prefix='query'), - profile=profile, - extra_flags=extra_flags, - ) - - def align( - self, - a3m_str: str, - profile: str, - extra_flags: Optional[Mapping[str, str]] = None, - ) -> str: - """Aligns sequences in A3M to the profile and returns the alignment in A3M. - - Args: - a3m_str: A list of sequence strings. - profile: A hmm file with the hmm profile to align the sequences to. - extra_flags: Dictionary with extra flags, flag_name: flag_value, that are - added to hmmalign. - - Returns: - An A3M string with the aligned sequences. - - Raises: - RuntimeError: If hmmalign fails. - """ - with tempfile.TemporaryDirectory() as query_tmp_dir: - input_profile = os.path.join(query_tmp_dir, 'profile.hmm') - input_sequences = os.path.join(query_tmp_dir, 'sequences.a3m') - output_a3m_path = os.path.join(query_tmp_dir, 'output.a3m') - - with open(input_profile, 'w', encoding='utf-8') as f: - f.write(profile) - - with open(input_sequences, 'w', encoding='utf-8') as f: - f.write(a3m_str) - - cmd = [ - self.binary_path, - *('-o', output_a3m_path), - *('--outformat', 'A2M'), # A2M is A3M in the HMMER suite. - ] - if extra_flags: - for flag_name, flag_value in extra_flags.items(): - cmd.extend([flag_name, flag_value]) - cmd.extend([input_profile, input_sequences]) - - subprocess_utils.run( - cmd=cmd, - cmd_name='hmmalign', - log_stdout=False, - log_stderr=True, - log_on_process_error=True, - ) - - with open(output_a3m_path, encoding='utf-8') as f: - a3m = f.read() - - return a3m - - def align_sequences_to_profile(self, profile: str, sequences_a3m: str) -> str: - """Aligns the sequences to profile and returns the alignment in A3M string. - - Uses hmmalign to align the sequences to the profile, then outputs the - sequence concatenated at the beginning of the sequences in the A3M format. - As the sequences are represented by an alignment with possible gaps ('-') - and insertions (lowercase characters), the method first removes the gaps, - then uppercases the insertions to prepare the sequences for realignment. - Sequences with gaps cannot be aligned, as '-'s are not a valid symbol to - align; lowercase characters must be uppercased to preserve the original - sequences before realignment. - - Args: - profile: The Hmmbuild profile to align the sequences to. - sequences_a3m: Sequences in A3M format to align to the profile. - - Returns: - An A3M string with the aligned sequences. - - Raises: - RuntimeError: If hmmalign fails. - """ - deletion_table = str.maketrans('', '', '-') - sequences_no_gaps_a3m = [] - for seq, desc in parsers.lazy_parse_fasta_string(sequences_a3m): - sequences_no_gaps_a3m.append(f'>{desc}') - sequences_no_gaps_a3m.append(seq.translate(deletion_table)) - sequences_no_gaps_a3m = '\n'.join(sequences_no_gaps_a3m) - - aligned_sequences = self.align(sequences_no_gaps_a3m, profile) - - return aligned_sequences diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmbuild.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmbuild.py deleted file mode 100644 index 450e7ab8f..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmbuild.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""A Python wrapper for hmmbuild - construct HMM profiles from MSA.""" - -import os -import re -import tempfile -from typing import Literal, Optional - -from alphafold3.data import parsers -from alphafold3.data.tools import subprocess_utils - - -class Hmmbuild: - """Python wrapper of the hmmbuild binary.""" - - def __init__( - self, - *, - binary_path: str, - singlemx: bool = False, - alphabet: Optional[str] = None, - ): - """Initializes the Python hmmbuild wrapper. - - Args: - binary_path: The path to the hmmbuild executable. - singlemx: Whether to use --singlemx flag. If True, it forces HMMBuild to - just use a common substitution score matrix. - alphabet: The alphabet to assert when building a profile. Useful when - hmmbuild cannot guess the alphabet. If None, no alphabet is asserted. - - Raises: - RuntimeError: If hmmbuild binary not found within the path. - """ - self.binary_path = binary_path - self.singlemx = singlemx - self.alphabet = alphabet - - subprocess_utils.check_binary_exists( - path=self.binary_path, name='hmmbuild') - - def build_profile_from_sto(self, sto: str, model_construction='fast') -> str: - """Builds a HHM for the aligned sequences given as an A3M string. - - Args: - sto: A string with the aligned sequences in the Stockholm format. - model_construction: Whether to use reference annotation in the msa to - determine consensus columns ('hand') or default ('fast'). - - Returns: - A string with the profile in the HMM format. - - Raises: - RuntimeError: If hmmbuild fails. - """ - return self._build_profile( - sto, informat='stockholm', model_construction=model_construction - ) - - def build_profile_from_a3m(self, a3m: str) -> str: - """Builds a HHM for the aligned sequences given as an A3M string. - - Args: - a3m: A string with the aligned sequences in the A3M format. - - Returns: - A string with the profile in the HMM format. - - Raises: - RuntimeError: If hmmbuild fails. - """ - lines = [] - for sequence, description in parsers.lazy_parse_fasta_string(a3m): - # Remove inserted residues. - sequence = re.sub('[a-z]+', '', sequence) - lines.append(f'>{description}\n{sequence}\n') - msa = ''.join(lines) - return self._build_profile(msa, informat='afa') - - def _build_profile( - self, - msa: str, - informat: Literal['afa', 'stockholm'], - model_construction: str = 'fast', - ) -> str: - """Builds a HMM for the aligned sequences given as an MSA string. - - Args: - msa: A string with the aligned sequences, in A3M or STO format. - informat: One of 'afa' (aligned FASTA) or 'sto' (Stockholm). - model_construction: Whether to use reference annotation in the msa to - determine consensus columns ('hand') or default ('fast'). - - Returns: - A string with the profile in the HMM format. - - Raises: - RuntimeError: If hmmbuild fails. - ValueError: If unspecified arguments are provided. - """ - if model_construction not in {'hand', 'fast'}: - raise ValueError( - f'Bad {model_construction=}. Only hand or fast allowed.') - - with tempfile.TemporaryDirectory() as query_tmp_dir: - input_msa_path = os.path.join(query_tmp_dir, 'query.msa') - output_hmm_path = os.path.join(query_tmp_dir, 'output.hmm') - - with open(input_msa_path, 'w', encoding='utf-8') as f: - f.write(msa) - - # Specify the format as we don't specify the input file extension. See - # https://github.com/EddyRivasLab/hmmer/issues/321 for more details. - cmd_flags = ['--informat', informat] - # If adding flags, we have to do so before the output and input: - if model_construction == 'hand': - cmd_flags.append(f'--{model_construction}') - if self.singlemx: - cmd_flags.append('--singlemx') - if self.alphabet: - cmd_flags.append(f'--{self.alphabet}') - - cmd_flags.extend([output_hmm_path, input_msa_path]) - - cmd = [self.binary_path, *cmd_flags] - - subprocess_utils.run( - cmd=cmd, - cmd_name='Hmmbuild', - log_stdout=False, - log_stderr=True, - log_on_process_error=True, - ) - - with open(output_hmm_path, encoding='utf-8') as f: - hmm = f.read() - - return hmm diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmsearch.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmsearch.py deleted file mode 100644 index 5a893abec..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/hmmsearch.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""A Python wrapper for hmmsearch - search profile against a sequence db.""" - -import os -import tempfile -from typing import Optional - -from absl import logging -from alphafold3.data import parsers -from alphafold3.data.tools import hmmbuild -from alphafold3.data.tools import subprocess_utils - - -class Hmmsearch: - """Python wrapper of the hmmsearch binary.""" - - def __init__( - self, - *, - binary_path: str, - hmmbuild_binary_path: str, - database_path: str, - alphabet: str = 'amino', - filter_f1: Optional[float] = None, - filter_f2: Optional[float] = None, - filter_f3: Optional[float] = None, - e_value: Optional[float] = None, - inc_e: Optional[float] = None, - dom_e: Optional[float] = None, - incdom_e: Optional[float] = None, - filter_max: bool = False, - ): - """Initializes the Python hmmsearch wrapper. - - Args: - binary_path: The path to the hmmsearch executable. - hmmbuild_binary_path: The path to the hmmbuild executable. Used to build - an hmm from an input a3m. - database_path: The path to the hmmsearch database (FASTA format). - alphabet: Chain type e.g. amino, rna, dna. - filter_f1: MSV and biased composition pre-filter, set to >1.0 to turn off. - filter_f2: Viterbi pre-filter, set to >1.0 to turn off. - filter_f3: Forward pre-filter, set to >1.0 to turn off. - e_value: E-value criteria for inclusion in tblout. - inc_e: E-value criteria for inclusion in MSA/next round. - dom_e: Domain e-value criteria for inclusion in tblout. - incdom_e: Domain e-value criteria for inclusion of domains in MSA/next - round. - filter_max: Remove all filters, will ignore all filter_f* settings. - - Raises: - RuntimeError: If hmmsearch binary not found within the path. - """ - self.binary_path = binary_path - self.hmmbuild_runner = hmmbuild.Hmmbuild( - alphabet=alphabet, binary_path=hmmbuild_binary_path - ) - self.database_path = database_path - flags = [] - if filter_max: - flags.append('--max') - else: - if filter_f1 is not None: - flags.extend(('--F1', filter_f1)) - if filter_f2 is not None: - flags.extend(('--F2', filter_f2)) - if filter_f3 is not None: - flags.extend(('--F3', filter_f3)) - - if e_value is not None: - flags.extend(('-E', e_value)) - if inc_e is not None: - flags.extend(('--incE', inc_e)) - if dom_e is not None: - flags.extend(('--domE', dom_e)) - if incdom_e is not None: - flags.extend(('--incdomE', incdom_e)) - - self.flags = tuple(map(str, flags)) - - subprocess_utils.check_binary_exists( - path=self.binary_path, name='hmmsearch' - ) - - if not os.path.exists(self.database_path): - logging.error( - 'Could not find hmmsearch database %s', database_path) - raise ValueError( - f'Could not find hmmsearch database {database_path}') - - def query_with_hmm(self, hmm: str) -> str: - """Queries the database using hmmsearch using a given hmm.""" - with tempfile.TemporaryDirectory() as query_tmp_dir: - hmm_input_path = os.path.join(query_tmp_dir, 'query.hmm') - sto_out_path = os.path.join(query_tmp_dir, 'output.sto') - with open(hmm_input_path, 'w', encoding='utf-8') as f: - f.write(hmm) - - cmd = [ - self.binary_path, - '--noali', # Don't include the alignment in stdout. - *('--cpu', '8'), - ] - # If adding flags, we have to do so before the output and input: - if self.flags: - cmd.extend(self.flags) - cmd.extend([ - *('-A', sto_out_path), - hmm_input_path, - self.database_path, - ]) - - subprocess_utils.run( - cmd=cmd, - cmd_name='Hmmsearch', - log_stdout=False, - log_stderr=True, - log_on_process_error=True, - ) - - with open(sto_out_path, encoding='utf-8') as f: - a3m_out = parsers.convert_stockholm_to_a3m( - f, remove_first_row_gaps=False, linewidth=60 - ) - - return a3m_out - - def query_with_a3m(self, a3m_in: str) -> str: - """Query the database using hmmsearch using a given a3m.""" - - # Only the "fast" model construction makes sense with A3M, as it doesn't - # have any way to annotate reference columns. - hmm = self.hmmbuild_runner.build_profile_from_a3m(a3m_in) - return self.query_with_hmm(hmm) - - def query_with_sto( - self, msa_sto: str, model_construction: str = 'fast' - ) -> str: - """Queries the database using hmmsearch using a given stockholm msa.""" - hmm = self.hmmbuild_runner.build_profile_from_sto( - msa_sto, model_construction=model_construction - ) - return self.query_with_hmm(hmm) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/jackhmmer.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/jackhmmer.py deleted file mode 100644 index c7a2b22ab..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/jackhmmer.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Library to run Jackhmmer from Python.""" - -import os -import tempfile -from typing import Optional, Union - -from absl import logging -from alphafold3.data import parsers -from alphafold3.data.tools import msa_tool -from alphafold3.data.tools import subprocess_utils - - -class Jackhmmer(msa_tool.MsaTool): - """Python wrapper of the Jackhmmer binary.""" - - def __init__( - self, - *, - binary_path: str, - database_path: str, - n_cpu: int = 8, - n_iter: int = 3, - e_value: Optional[float] = 1e-3, - z_value: Optional[Union[float, int]] = None, - max_sequences: int = 5000, - filter_f1: float = 5e-4, - filter_f2: float = 5e-5, - filter_f3: float = 5e-7, - ): - """Initializes the Python Jackhmmer wrapper. - - Args: - binary_path: The path to the jackhmmer executable. - database_path: The path to the jackhmmer database (FASTA format). - n_cpu: The number of CPUs to give Jackhmmer. - n_iter: The number of Jackhmmer iterations. - e_value: The E-value, see Jackhmmer docs for more details. - z_value: The Z-value representing the number of comparisons done (i.e - correct database size) for E-value calculation. - max_sequences: Maximum number of sequences to return in the MSA. - filter_f1: MSV and biased composition pre-filter, set to >1.0 to turn off. - filter_f2: Viterbi pre-filter, set to >1.0 to turn off. - filter_f3: Forward pre-filter, set to >1.0 to turn off. - - Raises: - RuntimeError: If Jackhmmer binary not found within the path. - """ - self.binary_path = binary_path - self.database_path = database_path - - subprocess_utils.check_binary_exists( - path=self.binary_path, name='Jackhmmer' - ) - - if not os.path.exists(self.database_path): - raise ValueError( - f'Could not find Jackhmmer database {database_path}') - - self.n_cpu = n_cpu - self.n_iter = n_iter - self.e_value = e_value - self.z_value = z_value - self.max_sequences = max_sequences - self.filter_f1 = filter_f1 - self.filter_f2 = filter_f2 - self.filter_f3 = filter_f3 - - def query(self, target_sequence: str) -> msa_tool.MsaToolResult: - """Queries the database using Jackhmmer.""" - logging.info('Query sequence: %s', target_sequence) - with tempfile.TemporaryDirectory() as query_tmp_dir: - input_fasta_path = os.path.join(query_tmp_dir, 'query.fasta') - subprocess_utils.create_query_fasta_file( - sequence=target_sequence, path=input_fasta_path - ) - - output_sto_path = os.path.join(query_tmp_dir, 'output.sto') - - # The F1/F2/F3 are the expected proportion to pass each of the filtering - # stages (which get progressively more expensive), reducing these - # speeds up the pipeline at the expensive of sensitivity. They are - # currently set very low to make querying Mgnify run in a reasonable - # amount of time. - cmd_flags = [ - # Don't pollute stdout with Jackhmmer output. - *('-o', '/dev/null'), - *('-A', output_sto_path), - '--noali', - *('--F1', str(self.filter_f1)), - *('--F2', str(self.filter_f2)), - *('--F3', str(self.filter_f3)), - *('--cpu', str(self.n_cpu)), - *('-N', str(self.n_iter)), - ] - - # Report only sequences with E-values <= x in per-sequence output. - if self.e_value is not None: - cmd_flags.extend(['-E', str(self.e_value)]) - - # Use the same value as the reporting e-value (`-E` flag). - cmd_flags.extend(['--incE', str(self.e_value)]) - - if self.z_value is not None: - cmd_flags.extend(['-Z', str(self.z_value)]) - - cmd = ( - [self.binary_path] - + cmd_flags - + [input_fasta_path, self.database_path] - ) - - subprocess_utils.run( - cmd=cmd, - cmd_name='Jackhmmer', - log_stdout=False, - log_stderr=True, - log_on_process_error=True, - ) - - with open(output_sto_path, encoding='utf-8') as f: - a3m = parsers.convert_stockholm_to_a3m( - f, max_sequences=self.max_sequences - ) - - return msa_tool.MsaToolResult( - target_sequence=target_sequence, a3m=a3m, e_value=self.e_value - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/msa_tool.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/msa_tool.py deleted file mode 100644 index 0c8bd1894..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/msa_tool.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Defines protocol for MSA tools.""" - -import dataclasses -from typing import Protocol - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class MsaToolResult: - """The result of a MSA tool query.""" - - target_sequence: str - e_value: float - a3m: str - - -class MsaTool(Protocol): - """Interface for MSA tools.""" - - def query(self, target_sequence: str) -> MsaToolResult: - """Runs the MSA tool on the target sequence.""" diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/nhmmer.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/nhmmer.py deleted file mode 100644 index a205e895e..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/nhmmer.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Library to run Nhmmer from Python.""" - -import os -import pathlib -import tempfile -from typing import Final, Optional - -from absl import logging -from alphafold3.data import parsers -from alphafold3.data.tools import hmmalign -from alphafold3.data.tools import hmmbuild -from alphafold3.data.tools import msa_tool -from alphafold3.data.tools import subprocess_utils - -_SHORT_SEQUENCE_CUTOFF: Final[int] = 50 - - -class Nhmmer(msa_tool.MsaTool): - """Python wrapper of the Nhmmer binary.""" - - def __init__( - self, - binary_path: str, - hmmalign_binary_path: str, - hmmbuild_binary_path: str, - database_path: str, - n_cpu: int = 8, - e_value: float = 1e-3, - max_sequences: int = 5000, - filter_f3: float = 1e-5, - alphabet: Optional[str] = None, - strand: Optional[str] = None, - ): - """Initializes the Python Nhmmer wrapper. - - Args: - binary_path: Path to the Nhmmer binary. - hmmalign_binary_path: Path to the Hmmalign binary. - hmmbuild_binary_path: Path to the Hmmbuild binary. - database_path: MSA database path to search against. This can be either a - FASTA (slow) or HMMERDB produced from the FASTA using the makehmmerdb - binary. The HMMERDB is ~10x faster but experimental. - n_cpu: The number of CPUs to give Nhmmer. - e_value: The E-value, see Nhmmer docs for more details. Will be - overwritten if bit_score is set. - max_sequences: Maximum number of sequences to return in the MSA. - filter_f3: Forward pre-filter, set to >1.0 to turn off. - alphabet: The alphabet to assert when building a profile with hmmbuild. - This must be 'rna', 'dna', or None. - strand: "watson" searches query sequence, "crick" searches - reverse-compliment and default is None which means searching for both. - - Raises: - RuntimeError: If Nhmmer binary not found within the path. - """ - self._binary_path = binary_path - self._hmmalign_binary_path = hmmalign_binary_path - self._hmmbuild_binary_path = hmmbuild_binary_path - self._db_path = database_path - - subprocess_utils.check_binary_exists( - path=self._binary_path, name='Nhmmer') - - if strand and strand not in {'watson', 'crick'}: - raise ValueError( - f'Invalid {strand=}. only "watson" or "crick" supported') - - if alphabet and alphabet not in {'rna', 'dna'}: - raise ValueError( - f'Invalid {alphabet=}, only "rna" or "dna" supported') - - self._e_value = e_value - self._n_cpu = n_cpu - self._max_sequences = max_sequences - self._filter_f3 = filter_f3 - self._alphabet = alphabet - self._strand = strand - - def query(self, target_sequence: str) -> msa_tool.MsaToolResult: - """Query the database using Nhmmer.""" - logging.info('Query sequence: %s', target_sequence) - - with tempfile.TemporaryDirectory() as query_tmp_dir: - input_a3m_path = os.path.join(query_tmp_dir, 'query.a3m') - output_sto_path = os.path.join(query_tmp_dir, 'output.sto') - pathlib.Path(output_sto_path).touch() - subprocess_utils.create_query_fasta_file( - sequence=target_sequence, path=input_a3m_path - ) - - cmd_flags = [ - # Don't pollute stdout with nhmmer output. - *('-o', '/dev/null'), - '--noali', # Don't include the alignment in stdout. - *('--cpu', str(self._n_cpu)), - ] - - cmd_flags.extend(['-E', str(self._e_value)]) - - if self._alphabet: - cmd_flags.extend([f'--{self._alphabet}']) - - if self._strand is not None: - cmd_flags.extend([f'--{self._strand}']) - - cmd_flags.extend(['-A', output_sto_path]) - # As recommend by RNAcentral for short sequences. - if ( - self._alphabet == 'rna' - and len(target_sequence) < _SHORT_SEQUENCE_CUTOFF - ): - cmd_flags.extend(['--F3', str(0.02)]) - else: - cmd_flags.extend(['--F3', str(self._filter_f3)]) - - # The input A3M and the db are the last two arguments. - cmd_flags.extend((input_a3m_path, self._db_path)) - - cmd = [self._binary_path, *cmd_flags] - - subprocess_utils.run( - cmd=cmd, - cmd_name='Nhmmer', - log_stdout=False, - log_stderr=True, - log_on_process_error=True, - ) - - if os.path.getsize(output_sto_path) > 0: - with open(output_sto_path, encoding='utf-8') as f: - a3m_out = parsers.convert_stockholm_to_a3m( - # Query not included. - f, max_sequences=self._max_sequences - 1 - ) - # Nhmmer hits are generally shorter than the query sequence. To get MSA - # of width equal to the query sequence, align hits to the query profile. - logging.info( - 'Aligning output a3m of size %d bytes', len(a3m_out)) - - aligner = hmmalign.Hmmalign(self._hmmalign_binary_path) - target_sequence_fasta = f'>query\n{target_sequence}\n' - profile_builder = hmmbuild.Hmmbuild( - binary_path=self._hmmbuild_binary_path, alphabet=self._alphabet - ) - profile = profile_builder.build_profile_from_a3m( - target_sequence_fasta) - a3m_out = aligner.align_sequences_to_profile( - profile=profile, sequences_a3m=a3m_out - ) - a3m_out = ''.join([target_sequence_fasta, a3m_out]) - - # Parse the output a3m to remove line breaks. - a3m = '\n'.join( - [f'>{n}\n{s}' for s, - n in parsers.lazy_parse_fasta_string(a3m_out)] - ) - else: - # Nhmmer returns an empty file if there are no hits. - # In this case return only the query sequence. - a3m = f'>query\n{target_sequence}' - - return msa_tool.MsaToolResult( - target_sequence=target_sequence, e_value=self._e_value, a3m=a3m - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/rdkit_utils.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/rdkit_utils.py deleted file mode 100644 index 793b65ce4..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/rdkit_utils.py +++ /dev/null @@ -1,524 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Tools for calculating features for ligands.""" - -import collections -from collections.abc import Mapping, Sequence -from typing import Union, Optional - -from absl import logging -from alphafold3.cpp import cif_dict -import numpy as np -import rdkit.Chem as rd_chem - - -_RDKIT_MMCIF_TO_BOND_TYPE: Mapping[str, rd_chem.BondType] = { - 'SING': rd_chem.BondType.SINGLE, - 'DOUB': rd_chem.BondType.DOUBLE, - 'TRIP': rd_chem.BondType.TRIPLE, -} - -_RDKIT_BOND_TYPE_TO_MMCIF: Mapping[rd_chem.BondType, str] = { - v: k for k, v in _RDKIT_MMCIF_TO_BOND_TYPE.items() -} - -_RDKIT_BOND_STEREO_TO_MMCIF: Mapping[rd_chem.BondStereo, str] = { - rd_chem.BondStereo.STEREONONE: 'N', - rd_chem.BondStereo.STEREOE: 'E', - rd_chem.BondStereo.STEREOZ: 'Z', - rd_chem.BondStereo.STEREOCIS: 'Z', - rd_chem.BondStereo.STEREOTRANS: 'E', -} - - -class MolFromMmcifError(Exception): - """Raised when conversion from mmCIF to RDKit Mol fails.""" - - -class UnsupportedMolBondError(Exception): - """Raised when we try to handle unsupported RDKit bonds.""" - - -def _populate_atoms_in_mol( - mol: rd_chem.Mol, - atom_names: Sequence[str], - atom_types: Sequence[str], - atom_charges: Sequence[int], - implicit_hydrogens: bool, - ligand_name: str, - atom_leaving_flags: Sequence[str], -): - """Populate the atoms of a Mol given atom features. - - Args: - mol: Mol object. - atom_names: Names of the atoms. - atom_types: Types of the atoms. - atom_charges: Charges of the atoms. - implicit_hydrogens: Whether to mark the atoms to allow implicit Hs. - ligand_name: Name of the ligand which the atoms are in. - atom_leaving_flags: Whether the atom is possibly a leaving atom. Values from - the CCD column `_chem_comp_atom.pdbx_leaving_atom_flag`. The expected - values are 'Y' (yes), 'N' (no), '?' (unknown/unset, interpreted as no). - - Raises: - ValueError: If atom type is invalid. - """ - # Map atom names to the position they will take in the rdkit molecule. - - for atom_name, atom_type, atom_charge, atom_leaving_flag in zip( - atom_names, atom_types, atom_charges, atom_leaving_flags, strict=True - ): - try: - if atom_type == 'X': - atom_type = '*' - atom = rd_chem.Atom(atom_type) - except RuntimeError as e: - raise ValueError(f'Failed to use atom type: {str(e)}') from e - - if not implicit_hydrogens: - atom.SetNoImplicit(True) - - atom.SetProp('atom_name', atom_name) - atom.SetProp('atom_leaving_flag', atom_leaving_flag) - atom.SetFormalCharge(atom_charge) - residue_info = rd_chem.AtomPDBResidueInfo() - residue_info.SetName(_format_atom_name(atom_name, atom_type)) - residue_info.SetIsHeteroAtom(True) - residue_info.SetResidueName(ligand_name) - residue_info.SetResidueNumber(1) - atom.SetPDBResidueInfo(residue_info) - mol.AddAtom(atom) - - -def _populate_bonds_in_mol( - mol: rd_chem.Mol, - atom_names: Sequence[str], - bond_begins: Sequence[str], - bond_ends: Sequence[str], - bond_orders: Sequence[str], - bond_is_aromatics: Sequence[bool], -): - """Populate the bonds of a Mol given bond features. - - Args: - mol: Mol object. - atom_names: Names of atoms in the molecule. - bond_begins: Names of atoms at the beginning of the bond. - bond_ends: Names of atoms at the end of the bond. - bond_orders: What order the bonds are. - bond_is_aromatics: Whether the bonds are aromatic. - """ - atom_name_to_idx = {name: i for i, name in enumerate(atom_names)} - for begin, end, bond_type, is_aromatic in zip( - bond_begins, bond_ends, bond_orders, bond_is_aromatics, strict=True - ): - begin_name, end_name = atom_name_to_idx[begin], atom_name_to_idx[end] - bond_idx = mol.AddBond(begin_name, end_name, bond_type) - mol.GetBondWithIdx(bond_idx - 1).SetIsAromatic(is_aromatic) - - -def _sanitize_mol(mol, sort_alphabetically, remove_hydrogens) -> rd_chem.Mol: - # https://www.rdkit.org/docs/source/rdkit.Chem.rdmolops.html#rdkit.Chem.rdmolops.SanitizeMol - # Kekulize, check valencies, set aromaticity, conjugation and hybridization. - # This can repair e.g. incorrect aromatic flags. - rd_chem.SanitizeMol(mol) - if sort_alphabetically: - mol = sort_atoms_by_name(mol) - if remove_hydrogens: - mol = rd_chem.RemoveHs(mol) - return mol - - -def _add_conformer_to_mol(mol, conformer, force_parse) -> rd_chem.Mol: - # Create conformer and use it to assign stereochemistry. - if conformer is not None: - try: - mol.AddConformer(conformer) - rd_chem.AssignStereochemistryFrom3D(mol) - except ValueError as e: - logging.warning('Failed to parse conformer: %s', e) - if not force_parse: - raise - - -def mol_from_ccd_cif( - mol_cif: cif_dict.CifDict, - *, - force_parse: bool = False, - sort_alphabetically: bool = True, - remove_hydrogens: bool = True, - implicit_hydrogens: bool = False, -) -> rd_chem.Mol: - """Creates an rdkit Mol object from a CCD mmcif data block. - - The atoms are renumbered so that their names are in alphabetical order and - these names are placed on the atoms under property 'atom_name'. - Only hydrogens which are not required to define the molecule are removed. - For example, hydrogens that define stereochemistry around a double bond are - retained. - See this link for more details. - https://www.rdkit.org/docs/source/rdkit.Chem.rdmolops.html#rdkit.Chem.rdmolops.RemoveHs - - Args: - mol_cif: An mmcif object representing a molecule. - force_parse: If True, assumes missing aromatic flags are false, substitutes - deuterium for hydrogen, assumes missing charges are 0 and ignores missing - conformer / stereochemistry information. - sort_alphabetically: True: sort atom alphabetically; False: keep CCD order - remove_hydrogens: if True, remove non-important hydrogens - implicit_hydrogens: Sets a marker on the atom that allows implicit Hs. - - Returns: - An rdkit molecule, with the atoms sorted by name. - - Raises: - MolToMmcifError: If conversion from mmcif to rdkit Mol fails. More detailed - error is available as this error's cause. - """ - # Read data fields. - try: - atom_names, atom_types, atom_charges, atom_leaving_flags = parse_atom_data( - mol_cif, force_parse - ) - bond_begins, bond_ends, bond_orders, bond_is_aromatics = parse_bond_data( - mol_cif, force_parse - ) - lig_name = mol_cif['_chem_comp.id'][0].rjust(3) - except (KeyError, ValueError) as e: - raise MolFromMmcifError from e - - # Build Rdkit molecule. - mol = rd_chem.RWMol() - - # Per atom features. - try: - _populate_atoms_in_mol( - mol=mol, - atom_names=atom_names, - atom_types=atom_types, - atom_charges=atom_charges, - implicit_hydrogens=implicit_hydrogens, - ligand_name=lig_name, - atom_leaving_flags=atom_leaving_flags, - ) - except (ValueError, RuntimeError) as e: - raise MolFromMmcifError from e - - _populate_bonds_in_mol( - mol, atom_names, bond_begins, bond_ends, bond_orders, bond_is_aromatics - ) - - try: - conformer = _parse_ideal_conformer(mol_cif) - except (KeyError, ValueError) as e: - logging.warning('Failed to parse ideal conformer: %s', e) - if not force_parse: - raise MolFromMmcifError from e - conformer = None - - mol.UpdatePropertyCache(strict=False) - - try: - _add_conformer_to_mol(mol, conformer, force_parse) - mol = _sanitize_mol(mol, sort_alphabetically, remove_hydrogens) - except ( - ValueError, - rd_chem.KekulizeException, - rd_chem.AtomValenceException, - ) as e: - raise MolFromMmcifError from e - - return mol - - -def mol_to_ccd_cif( - mol: rd_chem.Mol, - component_id: str, - pdbx_smiles: Optional[str] = None, - include_hydrogens: bool = True, -) -> cif_dict.CifDict: - """Creates a CCD-like mmcif data block from an rdkit Mol object. - - Only a subset of associated mmcif fields is populated, but that is - sufficient for further usage, e.g. in featurization code. - - Atom names can be specified via `atom_name` property. For atoms with - unspecified value of that property, the name is assigned based on element type - and the order in the Mol object. - - If the Mol object has associated conformers, atom positions from the first of - them will be populated in the resulting mmcif file. - - Args: - mol: An rdkit molecule. - component_id: Name of the molecule to use in the resulting mmcif. That is - equivalent to CCD code. - pdbx_smiles: If specified, the value will be used to populate - `_chem_comp.pdbx_smiles`. - include_hydrogens: Whether to include atom and bond data involving - hydrogens. - - Returns: - An mmcif data block corresponding for the given rdkit molecule. - - Raises: - UnsupportedMolBond: When a molecule contains a bond that can't be - represented with mmcif. - """ - mol = rd_chem.Mol(mol) - if include_hydrogens: - mol = rd_chem.AddHs(mol) - rd_chem.Kekulize(mol) - - if mol.GetNumConformers() > 0: - ideal_conformer = mol.GetConformer(0).GetPositions() - ideal_conformer = np.vectorize(lambda x: f'{x:.3f}')(ideal_conformer) - else: - # No data will be populated in the resulting mmcif if the molecule doesn't - # have any conformers attached to it. - ideal_conformer = None - - mol_cif = collections.defaultdict(list) - mol_cif['data_'] = [component_id] - mol_cif['_chem_comp.id'] = [component_id] - if pdbx_smiles: - mol_cif['_chem_comp.pdbx_smiles'] = [pdbx_smiles] - - mol = assign_atom_names_from_graph(mol, keep_existing_names=True) - - for atom_idx, atom in enumerate(mol.GetAtoms()): - element = atom.GetSymbol() - if not include_hydrogens and element in ('H', 'D'): - continue - - mol_cif['_chem_comp_atom.comp_id'].append(component_id) - mol_cif['_chem_comp_atom.atom_id'].append(atom.GetProp('atom_name')) - mol_cif['_chem_comp_atom.type_symbol'].append(atom.GetSymbol().upper()) - mol_cif['_chem_comp_atom.charge'].append(str(atom.GetFormalCharge())) - if ideal_conformer is not None: - coords = ideal_conformer[atom_idx] - mol_cif['_chem_comp_atom.pdbx_model_Cartn_x_ideal'].append( - coords[0]) - mol_cif['_chem_comp_atom.pdbx_model_Cartn_y_ideal'].append( - coords[1]) - mol_cif['_chem_comp_atom.pdbx_model_Cartn_z_ideal'].append( - coords[2]) - - for bond in mol.GetBonds(): - atom1 = bond.GetBeginAtom() - atom2 = bond.GetEndAtom() - if not include_hydrogens and ( - atom1.GetSymbol() in ('H', 'D') or atom2.GetSymbol() in ('H', 'D') - ): - continue - mol_cif['_chem_comp_bond.comp_id'].append(component_id) - mol_cif['_chem_comp_bond.atom_id_1'].append( - bond.GetBeginAtom().GetProp('atom_name') - ) - mol_cif['_chem_comp_bond.atom_id_2'].append( - bond.GetEndAtom().GetProp('atom_name') - ) - try: - bond_type = bond.GetBondType() - # Older versions of RDKit did not have a DATIVE bond type. Convert it to - # SINGLE to match the AF3 training setup. - if bond_type == rd_chem.BondType.DATIVE: - bond_type = rd_chem.BondType.SINGLE - mol_cif['_chem_comp_bond.value_order'].append( - _RDKIT_BOND_TYPE_TO_MMCIF[bond_type] - ) - mol_cif['_chem_comp_bond.pdbx_stereo_config'].append( - _RDKIT_BOND_STEREO_TO_MMCIF[bond.GetStereo()] - ) - except KeyError as e: - raise UnsupportedMolBondError from e - mol_cif['_chem_comp_bond.pdbx_aromatic_flag'].append( - 'Y' if bond.GetIsAromatic() else 'N' - ) - - return cif_dict.CifDict(mol_cif) - - -def _format_atom_name(atom_name: str, atom_type: str) -> str: - """Formats an atom name to fit in the four characters specified in PDB. - - See for example the following note on atom name formatting in PDB files: - https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/tutorials/pdbintro.html#note1 - - Args: - atom_name: The unformatted atom name. - atom_type: The atom element symbol. - - Returns: - formatted_atom_name: The formatted 4-character atom name. - """ - atom_name = atom_name.strip() - atom_type = atom_type.strip().upper() - if len(atom_name) == 1: - return atom_name.rjust(2).ljust(4) - if len(atom_name) == 2: - if atom_name == atom_type: - return atom_name.ljust(4) - return atom_name.center(4) - if len(atom_name) == 3: - if atom_name[:2] == atom_type: - return atom_name.ljust(4) - return atom_name.rjust(4) - if len(atom_name) == 4: - return atom_name - raise ValueError( - f'Atom name `{atom_name}` has more than four characters ' - 'or is an empty string.' - ) - - -def parse_atom_data( - mol_cif: Union[cif_dict.CifDict, Mapping[str, Sequence[str]]], force_parse: bool -) -> tuple[Sequence[str], Sequence[str], Sequence[int], Sequence[str]]: - """Parses atoms. If force_parse is True, fix deuterium and missing charge.""" - atom_types = [t.capitalize() - for t in mol_cif['_chem_comp_atom.type_symbol']] - atom_names = mol_cif['_chem_comp_atom.atom_id'] - atom_charges = mol_cif['_chem_comp_atom.charge'] - atom_leaving_flags = ['?'] * len(atom_names) - if '_chem_comp_atom.pdbx_leaving_atom_flag' in mol_cif: - atom_leaving_flags = mol_cif['_chem_comp_atom.pdbx_leaving_atom_flag'] - - if force_parse: - # Replace missing charges with 0. - atom_charges = [charge if charge != - '?' else '0' for charge in atom_charges] - # Deuterium for hydrogen. - atom_types = [type_ if type_ != 'D' else 'H' for type_ in atom_types] - - atom_charges = [int(atom_charge) for atom_charge in atom_charges] - return atom_names, atom_types, atom_charges, atom_leaving_flags - - -def parse_bond_data( - mol_cif: Union[cif_dict.CifDict, Mapping[str, Sequence[str]]], force_parse: bool -) -> tuple[ - Sequence[str], Sequence[str], Sequence[rd_chem.BondType], Sequence[bool] -]: - """Parses bond data. If force_parse is True, ignore missing aromatic flags.""" - # The bond table isn't present if there are no bonds. Use [] in that case. - begin_atoms = mol_cif.get('_chem_comp_bond.atom_id_1', []) - end_atoms = mol_cif.get('_chem_comp_bond.atom_id_2', []) - orders = mol_cif.get('_chem_comp_bond.value_order', []) - bond_types = [_RDKIT_MMCIF_TO_BOND_TYPE[order] for order in orders] - - try: - aromatic_flags = mol_cif.get('_chem_comp_bond.pdbx_aromatic_flag', []) - is_aromatic = [{'Y': True, 'N': False}[flag] - for flag in aromatic_flags] - except KeyError: - if force_parse: - # Set them all to not aromatic. - is_aromatic = [False for _ in begin_atoms] - else: - raise - - return begin_atoms, end_atoms, bond_types, is_aromatic - - -def _parse_ideal_conformer(mol_cif: cif_dict.CifDict) -> rd_chem.Conformer: - """Builds a conformer containing the ideal coordinates from the CCD. - - Args: - mol_cif: An mmcif object representing a molecule. - - Returns: - An rdkit conformer filled with the ideal positions from the mmcif. - - Raises: - ValueError: if the positions can't be interpreted. - """ - atom_x = [ - float(x) for x in mol_cif['_chem_comp_atom.pdbx_model_Cartn_x_ideal'] - ] - atom_y = [ - float(y) for y in mol_cif['_chem_comp_atom.pdbx_model_Cartn_y_ideal'] - ] - atom_z = [ - float(z) for z in mol_cif['_chem_comp_atom.pdbx_model_Cartn_z_ideal'] - ] - atom_positions = zip(atom_x, atom_y, atom_z, strict=True) - - conformer = rd_chem.Conformer(len(atom_x)) - for atom_index, atom_position in enumerate(atom_positions): - conformer.SetAtomPosition(atom_index, atom_position) - - return conformer - - -def sort_atoms_by_name(mol: rd_chem.Mol) -> rd_chem.Mol: - """Sorts the atoms in the molecule by their names.""" - atom_names = { - atom.GetProp('atom_name'): atom.GetIdx() for atom in mol.GetAtoms() - } - - # Sort the name, int tuples by the names. - sorted_atom_names = sorted(atom_names.items()) - - # Zip these tuples back together to the sorted indices. - _, new_order = zip(*sorted_atom_names, strict=True) - - # Reorder the molecule. - # new_order is effectively an argsort of the names. - return rd_chem.RenumberAtoms(mol, new_order) - - -def assign_atom_names_from_graph( - mol: rd_chem.Mol, - keep_existing_names: bool = False, -) -> rd_chem.Mol: - """Assigns atom names from the molecular graph. - - The atom name is stored as an atom property 'atom_name', accessible - with atom.GetProp('atom_name'). If the property is already specified, and - keep_existing_names is True we keep the original name. - - We traverse the graph in the order of the rdkit atom index and give each atom - a name equal to '{ELEMENT_TYPE}_{INDEX}'. E.g. C5 is the name for the fifth - unnamed carbon encountered. - - NOTE: A new mol is returned, the original is not changed in place. - - Args: - mol: - keep_existing_names: If True, atoms that already have the atom_name property - will keep their assigned names. - - Returns: - A new mol, with potentially new 'atom_name' properties. - """ - mol = rd_chem.Mol(mol) - - specified_atom_names = { - atom.GetProp('atom_name') - for atom in mol.GetAtoms() - if atom.HasProp('atom_name') and keep_existing_names - } - - element_counts = collections.Counter() - for atom in mol.GetAtoms(): - if not atom.HasProp('atom_name') or not keep_existing_names: - element = atom.GetSymbol() - while True: - element_counts[element] += 1 - new_name = f'{element}{element_counts[element]}' - if new_name not in specified_atom_names: - break - atom.SetProp('atom_name', new_name) - - return mol diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/subprocess_utils.py b/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/subprocess_utils.py deleted file mode 100644 index 97accc187..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/data/tools/subprocess_utils.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Helper functions for launching external tools.""" - -from collections.abc import Sequence -import os -import subprocess -import time -from typing import Any, Optional - -from absl import logging - - -def create_query_fasta_file(sequence: str, path: str, linewidth: int = 80): - """Creates a fasta file with the sequence with line width limit.""" - with open(path, 'w', encoding='utf-8') as f: - f.write('>query\n') - - i = 0 - while i < len(sequence): - f.write(f'{sequence[i:(i + linewidth)]}\n') - i += linewidth - - -def check_binary_exists(path: str, name: str) -> None: - """Checks if a binary exists on the given path and raises otherwise.""" - if not os.path.exists(path): - raise RuntimeError(f'{name} binary not found at {path}') - - -def run( - cmd: Sequence[str], - cmd_name: str, - log_on_process_error: bool = False, - log_stderr: bool = False, - log_stdout: bool = False, - max_out_streams_len: Optional[int] = 500_000, - **run_kwargs, -) -> subprocess.CompletedProcess[Any]: - """Launches a subprocess, times it, and checks for errors. - - Args: - cmd: Command to launch. - cmd_name: Human-readable command name to be used in logs. - log_on_process_error: Whether to use `logging.error` to log the process' - stderr on failure. - log_stderr: Whether to log the stderr of the command. - log_stdout: Whether to log the stdout of the command. - max_out_streams_len: Max length of prefix of stdout and stderr included in - the exception message. Set to `None` to disable truncation. - **run_kwargs: Any other kwargs for `subprocess.run`. - - Returns: - The completed process object. - - Raises: - RuntimeError: if the process completes with a non-zero return code. - """ - - logging.info('Launching subprocess "%s"', ' '.join(cmd)) - - start_time = time.time() - try: - completed_process = subprocess.run( - cmd, - check=True, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - text=True, - **run_kwargs, - ) - except subprocess.CalledProcessError as e: - if log_on_process_error: - # Logs have a 15k character limit, so log the error line by line. - logging.error('%s failed. %s stderr begin:', cmd_name, cmd_name) - for error_line in e.stderr.splitlines(): - if stripped_error_line := error_line.strip(): - logging.error(stripped_error_line) - logging.error('%s stderr end.', cmd_name) - - error_msg = ( - f'{cmd_name} failed' - f'\nstdout:\n{e.stdout[:max_out_streams_len]}\n' - f'\nstderr:\n{e.stderr[:max_out_streams_len]}' - ) - raise RuntimeError(error_msg) from e - end_time = time.time() - - logging.info('Finished %s in %.3f seconds', - cmd_name, end_time - start_time) - stdout, stderr = completed_process.stdout, completed_process.stderr - - if log_stdout and stdout: - logging.info('%s stdout:\n%s', cmd_name, stdout) - - if log_stderr and stderr: - logging.info('%s stderr:\n%s', cmd_name, stderr) - - return completed_process diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/atom_layout/atom_layout.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/atom_layout/atom_layout.py deleted file mode 100644 index f75712690..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/atom_layout/atom_layout.py +++ /dev/null @@ -1,1191 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Helper functions for different atom layouts and conversion between them.""" - -import collections -from collections.abc import Mapping, Sequence -import math -import dataclasses -import types -from typing import Any, TypeAlias, Optional, Union - -import numpy as np -import mindspore as ms -from mindspore import ops -from rdkit import Chem - -from alphafold3 import structure -from alphafold3.constants import atom_types -from alphafold3.constants import chemical_component_sets -from alphafold3.constants import chemical_components -from alphafold3.constants import mmcif_names -from alphafold3.constants import residue_names -from alphafold3.structure import chemical_components as struc_chem_comps - - -xnp_ndarray: TypeAlias = np.ndarray # pylint: disable=invalid-name -NumpyIndex: TypeAlias = Any - - -def _assign_atom_names_from_graph(mol: Chem.Mol) -> Chem.Mol: - """Assigns atom names from the molecular graph. - - The atom name is stored as an atom property 'atom_name', accessible with - atom.GetProp('atom_name'). If the property is already specified, we keep the - original name. - - We traverse the graph in the order of the rdkit atom index and give each atom - a name equal to '{ELEMENT_TYPE}_{INDEX}'. E.g. C5 is the name for the fifth - unnamed carbon encountered. - - NOTE: A new mol is returned, the original is not changed in place. - - Args: - mol: RDKit molecule. - - Returns: - A new mol, with potentially new 'atom_name' properties. - """ - mol = Chem.Mol(mol) - - specified_atom_names = { - a.GetProp('atom_name') for a in mol.GetAtoms() if a.HasProp('atom_name') - } - - element_counts = collections.Counter() - for atom in mol.GetAtoms(): - if not atom.HasProp('atom_name'): - element = atom.GetSymbol() - while True: - element_counts[element] += 1 - new_name = f'{element}{element_counts[element]}' - if new_name not in specified_atom_names: - break - atom.SetProp('atom_name', new_name) - - return mol - - -@dataclasses.dataclass(frozen=True) -class AtomLayout: - """Atom layout in a fixed shape (usually 1-dim or 2-dim). - - Examples for atom layouts are atom37, atom14, and similar. - All members are np.ndarrays with the same shape, e.g. - - [num_atoms] - - [num_residues, max_atoms_per_residue] - - [num_fragments, max_fragments_per_residue] - All string arrays should have dtype=object to avoid pitfalls with Numpy's - fixed-size strings - - Attributes: - atom_name: np.ndarray of str: atom names (e.g. 'CA', 'NE2'), padding - elements have an empty string (''), None or any other value, that maps to - False for .astype(bool). mmCIF field: _atom_site.label_atom_id. - res_id: np.ndarray of int: residue index (usually starting from 1) padding - elements can have an arbitrary value. mmCIF field: - _atom_site.label_seq_id. - chain_id: np.ndarray of str: chain names (e.g. 'A', 'B') padding elements - can have an arbitrary value. mmCIF field: _atom_site.label_seq_id. - atom_element: np.ndarray of str: atom elements (e.g. 'C', 'N', 'O'), padding - elements have an empty string (''), None or any other value, that maps to - False for .astype(bool). mmCIF field: _atom_site.type_symbol. - res_name: np.ndarray of str: residue names (e.g. 'ARG', 'TRP') padding - elements can have an arbitrary value. mmCIF field: - _atom_site.label_comp_id. - chain_type: np.ndarray of str: chain types (e.g. 'polypeptide(L)'). padding - elements can have an arbitrary value. mmCIF field: _entity_poly.type OR - _entity.type (for non-polymers). - shape: shape of the layout (just returns atom_name.shape) - """ - - atom_name: np.ndarray - res_id: np.ndarray - chain_id: np.ndarray - atom_element: Optional[np.ndarray] = None - res_name: Optional[np.ndarray] = None - chain_type: Optional[np.ndarray] = None - - def __post_init__(self): - """Assert all arrays have the same shape.""" - attribute_names = ( - 'atom_name', - 'atom_element', - 'res_name', - 'res_id', - 'chain_id', - 'chain_type', - ) - _assert_all_arrays_have_same_shape( - obj=self, - expected_shape=self.atom_name.shape, - attribute_names=attribute_names, - ) - # atom_name must have dtype object, such that we can convert it to bool to - # obtain the mask - if self.atom_name.dtype != object: - raise ValueError( - 'atom_name must have dtype object, such that it can ' - 'be converted converted to bool to obtain the mask' - ) - - def __getitem__(self, key: NumpyIndex) -> 'AtomLayout': - return AtomLayout( - atom_name=self.atom_name[key], - res_id=self.res_id[key], - chain_id=self.chain_id[key], - atom_element=( - self.atom_element[key] if self.atom_element is not None else None - ), - res_name=(self.res_name[key] - if self.res_name is not None else None), - chain_type=( - self.chain_type[key] if self.chain_type is not None else None - ), - ) - - def __eq__(self, other: 'AtomLayout') -> bool: - if not np.array_equal(self.atom_name, other.atom_name): - return False - - mask = self.atom_name.astype(bool) - # Check essential fields. - for field in ('res_id', 'chain_id'): - my_arr = getattr(self, field) - other_arr = getattr(other, field) - if not np.array_equal(my_arr[mask], other_arr[mask]): - return False - - # Check optional fields. - for field in ('atom_element', 'res_name', 'chain_type'): - my_arr = getattr(self, field) - other_arr = getattr(other, field) - if ( - my_arr is not None - and other_arr is not None - and not np.array_equal(my_arr[mask], other_arr[mask]) - ): - return False - - return True - - def copy_and_pad_to(self, shape: tuple[int, ...]) -> 'AtomLayout': - """Copies and pads the layout to the requested shape. - - Args: - shape: new shape for the atom layout - - Returns: - a copy of the atom layout padded to the requested shape - - Raises: - ValueError: incompatible shapes. - """ - if len(shape) != len(self.atom_name.shape): - raise ValueError( - f'Incompatible shape {shape}. Current layout has shape {self.shape}.' - ) - if any(new < old for old, new in zip(self.atom_name.shape, shape)): - raise ValueError( - "Can't pad to a smaller shape. Current layout has shape " - f'{self.shape} and you requested shape {shape}.' - ) - pad_width = [ - (0, new - old) for old, new in zip(self.atom_name.shape, shape) - ] - pad_val = np.array('', dtype=object) - return AtomLayout( - atom_name=np.pad(self.atom_name, pad_width, - constant_values=pad_val), - res_id=np.pad(self.res_id, pad_width, constant_values=0), - chain_id=np.pad(self.chain_id, pad_width, constant_values=pad_val), - atom_element=( - np.pad(self.atom_element, pad_width, constant_values=pad_val) - if self.atom_element is not None - else None - ), - res_name=( - np.pad(self.res_name, pad_width, constant_values=pad_val) - if self.res_name is not None - else None - ), - chain_type=( - np.pad(self.chain_type, pad_width, constant_values=pad_val) - if self.chain_type is not None - else None - ), - ) - - def to_array(self) -> np.ndarray: - """Stacks the fields to a numpy array with shape (6, ). - - Creates a pure numpy array of type `object` by stacking the 6 fields of the - AtomLayout, i.e. (atom_name, atom_element, res_name, res_id, chain_id, - chain_type). This method together with from_array() provides an easy way to - apply pure numpy methods like np.concatenate() to `AtomLayout`s. - - Returns: - np.ndarray of object with shape (6, ), e.g. - array([['N', 'CA', 'C', ..., 'CB', 'CG', 'CD'], - ['N', 'C', 'C', ..., 'C', 'C', 'C'], - ['LEU', 'LEU', 'LEU', ..., 'PRO', 'PRO', 'PRO'], - [1, 1, 1, ..., 403, 403, 403], - ['A', 'A', 'A', ..., 'D', 'D', 'D'], - ['polypeptide(L)', 'polypeptide(L)', ..., 'polypeptide(L)']], - dtype=object) - """ - if ( - self.atom_element is None - or self.res_name is None - or self.chain_type is None - ): - raise ValueError('All optional fields need to be present.') - - return np.stack(dataclasses.astuple(self), axis=0) - - @classmethod - def from_array(cls, arr: np.ndarray) -> 'AtomLayout': - """Creates an AtomLayout object from a numpy array with shape (6, ...). - - see also to_array() - Args: - arr: np.ndarray of object with shape (6, ) - - Returns: - AtomLayout object with shape () - """ - if arr.shape[0] != 6: - raise ValueError( - 'Given array must have shape (6, ...) to match the 6 fields of ' - 'AtomLayout (atom_name, atom_element, res_name, res_id, chain_id, ' - f'chain_type). Your array has {arr.shape=}' - ) - return cls(*arr) - - @property - def shape(self) -> tuple[int, ...]: - return self.atom_name.shape - - -@dataclasses.dataclass(frozen=True) -class Residues: - """List of residues with meta data. - - Attributes: - res_name: np.ndarray of str [num_res], e.g. 'ARG', 'TRP' - res_id: np.ndarray of int [num_res] - chain_id: np.ndarray of str [num_res], e.g. 'A', 'B' - chain_type: np.ndarray of str [num_res], e.g. 'polypeptide(L)' - is_start_terminus: np.ndarray of bool [num_res] - is_end_terminus: np.ndarray of bool [num_res] - deprotonation: (optional) np.ndarray of set() [num_res], e.g. {'HD1', 'HE2'} - smiles_string: (optional) np.ndarray of str [num_res], e.g. 'Cc1ccccc1' - shape: shape of the layout (just returns res_name.shape) - """ - - res_name: np.ndarray - res_id: np.ndarray - chain_id: np.ndarray - chain_type: np.ndarray - is_start_terminus: np.ndarray - is_end_terminus: np.ndarray - deprotonation: Optional[np.ndarray] = None - smiles_string: Optional[np.ndarray] = None - - def __post_init__(self): - """Assert all arrays are 1D have the same shape.""" - attribute_names = ( - 'res_name', - 'res_id', - 'chain_id', - 'chain_type', - 'is_start_terminus', - 'is_end_terminus', - 'deprotonation', - 'smiles_string', - ) - _assert_all_arrays_have_same_shape( - obj=self, - expected_shape=(self.res_name.shape[0],), - attribute_names=attribute_names, - ) - - def __getitem__(self, key: NumpyIndex) -> 'Residues': - return Residues( - res_name=self.res_name[key], - res_id=self.res_id[key], - chain_id=self.chain_id[key], - chain_type=self.chain_type[key], - is_start_terminus=self.is_start_terminus[key], - is_end_terminus=self.is_end_terminus[key], - deprotonation=( - self.deprotonation[key] if self.deprotonation is not None else None - ), - smiles_string=( - self.smiles_string[key] if self.smiles_string is not None else None - ), - ) - - def __eq__(self, other: 'Residues') -> bool: - return all( - np.array_equal(getattr(self, field.name), - getattr(other, field.name)) - for field in dataclasses.fields(self) - ) - - @property - def shape(self) -> tuple[int, ...]: - return self.res_name.shape - - -@dataclasses.dataclass # (frozen=True) -class GatherInfo: - """Gather indices to translate from one atom layout to another. - - All members are np or jnp ndarray (usually 1-dim or 2-dim) with the same - shape, e.g. - - [num_atoms] - - [num_residues, max_atoms_per_residue] - - [num_fragments, max_fragments_per_residue] - - Attributes: - gather_idxs: np or jnp ndarray of int: gather indices into a flattened array - gather_mask: np or jnp ndarray of bool: mask for resulting array - input_shape: np or jnp ndarray of int: the shape of the unflattened input - array - shape: output shape. Just returns gather_idxs.shape - """ - - gather_idxs: ms.Tensor - gather_mask: ms.Tensor - input_shape: ms.Tensor - - def __post_init__(self): - if self.gather_mask.shape != self.gather_idxs.shape: - raise ValueError( - 'All arrays must have the same shape. Got\n' - f'gather_idxs.shape = {self.gather_idxs.shape}\n' - f'gather_mask.shape = {self.gather_mask.shape}\n' - ) - - def __getitem__(self, key: NumpyIndex) -> 'GatherInfo': - return GatherInfo( - gather_idxs=self.gather_idxs[key], - gather_mask=self.gather_mask[key], - input_shape=self.input_shape, - ) - - @property - def shape(self) -> tuple[int, ...]: - return self.gather_idxs.shape - - def as_np_or_jnp(self, xnp: types.ModuleType) -> 'GatherInfo': - return GatherInfo( - gather_idxs=xnp.array(self.gather_idxs), - gather_mask=xnp.array(self.gather_mask), - input_shape=xnp.array(self.input_shape), - ) - - def as_dict( - self, - key_prefix: Optional[str] = None, - ) -> dict[str, xnp_ndarray]: - prefix = f'{key_prefix}:' if key_prefix else '' - return { - prefix + 'gather_idxs': self.gather_idxs, - prefix + 'gather_mask': self.gather_mask, - prefix + 'input_shape': self.input_shape, - } - - @classmethod - def from_dict( - cls, - d: Mapping[str, xnp_ndarray], - key_prefix: Optional[str] = None, - ) -> 'GatherInfo': - """Creates GatherInfo from a given dictionary.""" - prefix = f'{key_prefix}:' if key_prefix else '' - return cls( - gather_idxs=d[prefix + 'gather_idxs'], - gather_mask=d[prefix + 'gather_mask'], - input_shape=d[prefix + 'input_shape'], - ) - - -def fill_in_optional_fields( - minimal_atom_layout: AtomLayout, - reference_atoms: AtomLayout, -) -> AtomLayout: - """Fill in the optional fields (atom_element, res_name, chain_type). - - Extracts the optional fields (atom_element, res_name, chain_type) from a - flat reference layout and fills them into the fields from this layout. - - Args: - minimal_atom_layout: An AtomLayout that only contains the essential fields - (atom_name, res_id, chain_id). - reference_atoms: A flat layout that contains all fields for all atoms. - - Returns: - An AtomLayout that contains all fields. - - Raises: - ValueError: Reference atoms layout is not flat. - ValueError: Missing atoms in reference. - """ - if len(reference_atoms.shape) > 1: - raise ValueError('Only flat layouts are supported as reference.') - ref_to_self = compute_gather_idxs( - source_layout=reference_atoms, target_layout=minimal_atom_layout - ) - atom_mask = minimal_atom_layout.atom_name.astype(bool) - missing_atoms_mask = atom_mask & ~ref_to_self.gather_mask - if np.any(missing_atoms_mask): - raise ValueError( - f'{np.sum(missing_atoms_mask)} missing atoms in reference: ' - f'{minimal_atom_layout[missing_atoms_mask]}' - ) - - def _convert_str_array(gather: GatherInfo, arr: np.ndarray): - output = arr[gather.gather_idxs] - output[~gather.gather_mask] = '' - return output - - return dataclasses.replace( - minimal_atom_layout, - atom_element=_convert_str_array( - ref_to_self, reference_atoms.atom_element - ), - res_name=_convert_str_array(ref_to_self, reference_atoms.res_name), - chain_type=_convert_str_array(ref_to_self, reference_atoms.chain_type), - ) - - -def guess_deprotonation(residues: Residues) -> Residues: - """Convenience function to create a plausible deprotonation field. - - Assumes a pH of 7 and always prefers HE2 over HD1 for HIS. - Args: - residues: a Residues object without a depronotation field - - Returns: - a Residues object with a depronotation field - """ - num_residues = residues.res_name.shape[0] - deprotonation = np.empty(num_residues, dtype=object) - deprotonation_at_ph7 = { - 'ASP': 'HD2', - 'GLU': 'HE2', - 'HIS': 'HD1', - } - for idx, res_name in enumerate(residues.res_name): - deprotonation[idx] = set() - if res_name in deprotonation_at_ph7: - deprotonation[idx].add(deprotonation_at_ph7[res_name]) - if residues.is_end_terminus[idx]: - deprotonation[idx].add('HXT') - - return dataclasses.replace(residues, deprotonation=deprotonation) - - -def atom_layout_from_structure( - struct: structure.Structure, - *, - fix_non_standard_polymer_res: bool = False, -) -> AtomLayout: - """Extract AtomLayout from a Structure.""" - - if not fix_non_standard_polymer_res: - return AtomLayout( - atom_name=np.array(struct.atom_name, dtype=object), - atom_element=np.array(struct.atom_element, dtype=object), - res_name=np.array(struct.res_name, dtype=object), - res_id=np.array(struct.res_id, dtype=int), - chain_id=np.array(struct.chain_id, dtype=object), - chain_type=np.array(struct.chain_type, dtype=object), - ) - - # Target lists. - target_atom_names = [] - target_atom_elements = [] - target_res_ids = [] - target_res_names = [] - target_chain_ids = [] - target_chain_types = [] - - for atom in struct.iter_atoms(): - target_atom_names.append(atom['atom_name']) - target_atom_elements.append(atom['atom_element']) - target_res_ids.append(atom['res_id']) - target_chain_ids.append(atom['chain_id']) - target_chain_types.append(atom['chain_type']) - if mmcif_names.is_standard_polymer_type(atom['chain_type']): - fixed_res_name = mmcif_names.fix_non_standard_polymer_res( - res_name=atom['res_name'], chain_type=atom['chain_type'] - ) - target_res_names.append(fixed_res_name) - else: - target_res_names.append(atom['res_name']) - - return AtomLayout( - atom_name=np.array(target_atom_names, dtype=object), - atom_element=np.array(target_atom_elements, dtype=object), - res_name=np.array(target_res_names, dtype=object), - res_id=np.array(target_res_ids, dtype=int), - chain_id=np.array(target_chain_ids, dtype=object), - chain_type=np.array(target_chain_types, dtype=object), - ) - - -def residues_from_structure( - struct: structure.Structure, - *, - include_missing_residues: bool = True, - fix_non_standard_polymer_res: bool = False, -) -> Residues: - """Create a Residues object from a Structure object.""" - - def _get_smiles(res_name): - """Get SMILES string from chemical components.""" - smiles = None - if ( - struct.chemical_components_data is not None - and struct.chemical_components_data.chem_comp is not None - and struct.chemical_components_data.chem_comp.get(res_name) - ): - smiles = struct.chemical_components_data.chem_comp[res_name].pdbx_smiles - return smiles - - res_names_per_chain = struct.chain_res_name_sequence( - include_missing_residues=include_missing_residues, - fix_non_standard_polymer_res=fix_non_standard_polymer_res, - ) - res_name = [] - res_id = [] - chain_id = [] - chain_type = [] - smiles = [] - is_start_terminus = [] - for c in struct.iter_chains(): - if include_missing_residues: - this_res_ids = [ - id for (_, id) in struct.all_residues[c['chain_id']]] - else: - this_res_ids = [ - r['res_id'] - for r in struct.iter_residues() - if r['chain_id'] == c['chain_id'] - ] - fixed_res_names = res_names_per_chain[c['chain_id']] - this_start_res_id = min(1, *this_res_ids) - this_is_start_terminus = [r == this_start_res_id for r in this_res_ids] - smiles.extend([_get_smiles(res_name) for res_name in fixed_res_names]) - num_res = len(fixed_res_names) - res_name.extend(fixed_res_names) - res_id.extend(this_res_ids) - chain_id.extend([c['chain_id']] * num_res) - chain_type.extend([c['chain_type']] * num_res) - is_start_terminus.extend(this_is_start_terminus) - res_name = np.array(res_name, dtype=object) - res_id = np.array(res_id, dtype=int) - chain_id = np.array(chain_id, dtype=object) - chain_type = np.array(chain_type, dtype=object) - smiles = np.array(smiles, dtype=object) - is_start_terminus = np.array(is_start_terminus, dtype=bool) - - res_uid_to_idx = { - uid: idx for idx, uid in enumerate(zip(chain_id, res_id, strict=True)) - } - - # Start terminus indicates whether residue index is 1 and chain is polymer. - is_polymer = np.isin(chain_type, tuple(mmcif_names.POLYMER_CHAIN_TYPES)) - is_start_terminus = is_start_terminus & is_polymer - - # Start also indicates whether amino acid is attached to H2 or proline to H. - start_terminus_atom_index = np.nonzero( - (struct.chain_type == mmcif_names.PROTEIN_CHAIN) - & ( - (struct.atom_name == 'H2') - | ((struct.atom_name == 'H') & (struct.res_name == 'PRO')) - ) - )[0] - - # Translate atom idx to residue idx to assign start terminus. - for atom_idx in start_terminus_atom_index: - res_uid = (struct.chain_id[atom_idx], struct.res_id[atom_idx]) - res_idx = res_uid_to_idx[res_uid] - is_start_terminus[res_idx] = True - - # Infer end terminus: Check for OXT, or in case of - # include_missing_residues==True for the last residue of the chain. - num_all_residues = res_name.shape[0] - is_end_terminus = np.zeros(num_all_residues, dtype=bool) - end_term_atom_idxs = np.nonzero(struct.atom_name == 'OXT')[0] - for atom_idx in end_term_atom_idxs: - res_uid = (struct.chain_id[atom_idx], struct.res_id[atom_idx]) - res_idx = res_uid_to_idx[res_uid] - is_end_terminus[res_idx] = True - - if include_missing_residues: - for idx in range(num_all_residues - 1): - if is_polymer[idx] and chain_id[idx] != chain_id[idx + 1]: - is_end_terminus[idx] = True - if (num_all_residues > 0) and is_polymer[-1]: - is_end_terminus[-1] = True - - # Infer (de-)protonation: Only if hydrogens are given. - num_hydrogens = np.sum( - (struct.atom_element == 'H') & (struct.chain_type == 'polypeptide(L)') - ) - if num_hydrogens > 0: - deprotonation = np.empty(num_all_residues, dtype=object) - all_atom_uids = set( - zip(struct.chain_id, struct.res_id, struct.atom_name, strict=True) - ) - for idx in range(num_all_residues): - deprotonation[idx] = set() - check_hydrogens = set() - if is_end_terminus[idx]: - check_hydrogens.add('HXT') - if res_name[idx] in atom_types.PROTONATION_HYDROGENS: - check_hydrogens.update( - atom_types.PROTONATION_HYDROGENS[res_name[idx]]) - for hydrogen in check_hydrogens: - if (chain_id[idx], res_id[idx], hydrogen) not in all_atom_uids: - deprotonation[idx].add(hydrogen) - else: - deprotonation = None - - return Residues( - res_name=res_name, - res_id=res_id, - chain_id=chain_id, - chain_type=chain_type, - is_start_terminus=is_start_terminus.astype(bool), - is_end_terminus=is_end_terminus, - deprotonation=deprotonation, - smiles_string=smiles, - ) - - -def get_link_drop_atoms( - res_name: str, - chain_type: str, - *, - is_start_terminus: bool, - is_end_terminus: bool, - bonded_atoms: set[str], - drop_ligand_leaving_atoms: bool = False, -) -> set[str]: - """Returns set of atoms that are dropped when this res_name gets linked. - - Args: - res_name: residue name, e.g. 'ARG' - chain_type: chain_type, e.g. 'polypeptide(L)' - is_start_terminus: whether the residue is the n-terminus - is_end_terminus: whether the residue is the c-terminus - bonded_atoms: Names of atoms coming off this residue. - drop_ligand_leaving_atoms: Flag to switch on/off leaving atoms for ligands. - - Returns: - Set of atoms that are dropped when this amino acid gets linked. - """ - drop_atoms = set() - if chain_type == mmcif_names.PROTEIN_CHAIN: - if res_name == 'PRO': - if not is_start_terminus: - drop_atoms.update({'H', 'H2', 'H3'}) - if not is_end_terminus: - drop_atoms.update({'OXT', 'HXT'}) - else: - if not is_start_terminus: - drop_atoms.update({'H2', 'H3'}) - if not is_end_terminus: - drop_atoms.update({'OXT', 'HXT'}) - elif chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES: - if not is_start_terminus: - drop_atoms.update({'OP3'}) - elif ( - drop_ligand_leaving_atoms and chain_type in mmcif_names.LIGAND_CHAIN_TYPES - ): - if res_name in { - *chemical_component_sets.GLYCAN_OTHER_LIGANDS, - *chemical_component_sets.GLYCAN_LINKING_LIGANDS, - }: - if 'O1' not in bonded_atoms: - drop_atoms.update({'O1'}) - return drop_atoms - - -def get_bonded_atoms( - polymer_ligand_bonds: AtomLayout, - ligand_ligand_bonds: AtomLayout, - res_id: int, - chain_id: str, -) -> set[str]: - """Finds the res_name on the opposite end of the bond, if a bond exists. - - Args: - polymer_ligand_bonds: Bond information for polymer-ligand pairs. - ligand_ligand_bonds: Bond information for ligand-ligand pairs. - res_id: residue id in question. - chain_id: chain id of residue in question. - - Returns: - res_name of bonded atom. - """ - bonded_atoms = set() - if polymer_ligand_bonds: - # Filter before searching to speed this up. - bond_idx = np.logical_and( - polymer_ligand_bonds.res_id == res_id, - polymer_ligand_bonds.chain_id == chain_id, - ).any(axis=1) - relevant_polymer_bonds = polymer_ligand_bonds[bond_idx] - for atom_names, res_ids, chain_ids in zip( - relevant_polymer_bonds.atom_name, - relevant_polymer_bonds.res_id, - relevant_polymer_bonds.chain_id, - ): - if (res_ids[0], chain_ids[0]) == (res_id, chain_id): - bonded_atoms.add(atom_names[0]) - elif (res_ids[1], chain_ids[1]) == (res_id, chain_id): - bonded_atoms.add(atom_names[1]) - if ligand_ligand_bonds: - bond_idx = np.logical_and( - ligand_ligand_bonds.res_id == res_id, - ligand_ligand_bonds.chain_id == chain_id, - ).any(axis=1) - relevant_ligand_bonds = ligand_ligand_bonds[bond_idx] - for atom_names, res_ids, chain_ids in zip( - relevant_ligand_bonds.atom_name, - relevant_ligand_bonds.res_id, - relevant_ligand_bonds.chain_id, - ): - if (res_ids[0], chain_ids[0]) == (res_id, chain_id): - bonded_atoms.add(atom_names[0]) - elif (res_ids[1], chain_ids[1]) == (res_id, chain_id): - bonded_atoms.add(atom_names[1]) - return bonded_atoms - - -def make_flat_atom_layout( - residues: Residues, - ccd: chemical_components.Ccd, - polymer_ligand_bonds: Optional[AtomLayout] = None, - ligand_ligand_bonds: Optional[AtomLayout] = None, - *, - with_hydrogens: bool = False, - skip_unk_residues: bool = True, - drop_ligand_leaving_atoms: bool = False, -) -> AtomLayout: - """Make a flat atom layout for given residues. - - Create a flat layout from a `Residues` object. The required atoms for each - amino acid type are taken from the CCD, hydrogens and oxygens are dropped to - make the linked residues. Terminal OXT's and protonation state for the - hydrogens come from the `Residues` object. - - Args: - residues: a `Residues` object. - ccd: The chemical components dictionary. - polymer_ligand_bonds: Bond information for polymer-ligand pairs. - ligand_ligand_bonds: Bond information for ligand-ligand pairs. - with_hydrogens: whether to create hydrogens - skip_unk_residues: whether to skip 'UNK' resides -- default is True to be - compatible with the rest of AlphaFold that does not predict atoms for - unknown residues - drop_ligand_leaving_atoms: Flag to switch on/ off leaving atoms for ligands. - - Returns: - an `AtomLayout` object - """ - num_res = residues.res_name.shape[0] - - # Target lists. - target_atom_names = [] - target_atom_elements = [] - target_res_ids = [] - target_res_names = [] - target_chain_ids = [] - target_chain_types = [] - - for idx in range(num_res): - # skip 'UNK' residues if requested - if ( - skip_unk_residues - and residues.res_name[idx] in residue_names.UNKNOWN_TYPES - ): - continue - - # Get the atoms for this residue type from CCD. - if ccd.get(residues.res_name[idx]): - res_atoms = struc_chem_comps.get_all_atoms_in_entry( - ccd=ccd, res_name=residues.res_name[idx] - ) - atom_names_elements = list( - zip( - res_atoms['_chem_comp_atom.atom_id'], - res_atoms['_chem_comp_atom.type_symbol'], - strict=True, - ) - ) - elif residues.smiles_string[idx]: - # Get atoms from RDKit via SMILES. - mol = Chem.MolFromSmiles(residues.smiles_string[idx]) - mol = _assign_atom_names_from_graph(mol) - atom_names_elements = [ - (a.GetProp('atom_name'), a.GetSymbol()) for a in mol.GetAtoms() - ] - else: - raise ValueError( - f'{residues.res_name[idx]} not found in CCD and no SMILES string' - ) - - # Remove hydrogens if requested. - if not with_hydrogens: - atom_names_elements = [ - (n, e) for n, e in atom_names_elements if e not in ('H', 'D') - ] - bonded_atoms = get_bonded_atoms( - polymer_ligand_bonds, - ligand_ligand_bonds, - residues.res_id[idx], - residues.chain_id[idx], - ) - # Connect the amino-acids, i.e. remove OXT, HXT and H2. - drop_atoms = get_link_drop_atoms( - res_name=residues.res_name[idx], - chain_type=residues.chain_type[idx], - is_start_terminus=residues.is_start_terminus[idx], - is_end_terminus=residues.is_end_terminus[idx], - bonded_atoms=bonded_atoms, - drop_ligand_leaving_atoms=drop_ligand_leaving_atoms, - ) - - # If deprotonation info is available, remove the specific atoms. - if residues.deprotonation is not None: - drop_atoms.update(residues.deprotonation[idx]) - - atom_names_elements = [ - (n, e) for n, e in atom_names_elements if n not in drop_atoms - ] - - # Append the found atoms to the target lists. - target_atom_names.extend([n for n, _ in atom_names_elements]) - target_atom_elements.extend([e for _, e in atom_names_elements]) - num_atoms = len(atom_names_elements) - target_res_names.extend([residues.res_name[idx]] * num_atoms) - target_res_ids.extend([residues.res_id[idx]] * num_atoms) - target_chain_ids.extend([residues.chain_id[idx]] * num_atoms) - target_chain_types.extend([residues.chain_type[idx]] * num_atoms) - - return AtomLayout( - atom_name=np.array(target_atom_names, dtype=object), - atom_element=np.array(target_atom_elements, dtype=object), - res_name=np.array(target_res_names, dtype=object), - res_id=np.array(target_res_ids, dtype=int), - chain_id=np.array(target_chain_ids, dtype=object), - chain_type=np.array(target_chain_types, dtype=object), - ) - - -def compute_gather_idxs( - *, - source_layout: AtomLayout, - target_layout: AtomLayout, - fill_value: int = 0, -) -> GatherInfo: - """Produce gather indices and mask to convert from source layout to target.""" - source_uid_to_idx = { - uid: idx - for idx, uid in enumerate( - zip( - source_layout.chain_id.ravel(), - source_layout.res_id.ravel(), - source_layout.atom_name.ravel(), - strict=True, - ) - ) - } - gather_idxs = [] - gather_mask = [] - for uid in zip( - target_layout.chain_id.ravel(), - target_layout.res_id.ravel(), - target_layout.atom_name.ravel(), - strict=True, - ): - if uid in source_uid_to_idx: - gather_idxs.append(source_uid_to_idx[uid]) - gather_mask.append(True) - else: - gather_idxs.append(fill_value) - gather_mask.append(False) - target_shape = target_layout.atom_name.shape - return GatherInfo( - gather_idxs=np.array(gather_idxs, dtype=int).reshape(target_shape), - gather_mask=np.array(gather_mask, dtype=bool).reshape(target_shape), - input_shape=np.array(source_layout.atom_name.shape), - ) - - -def convert( - gather_info: GatherInfo, - arr: xnp_ndarray, - *, - layout_axes: tuple[int, ...] = (0,), -) -> xnp_ndarray: - """Convert an array from one atom layout to another.""" - # Translate negative indices to the corresponding positives. - layout_axes = tuple(i if i >= 0 else i + arr.ndim for i in layout_axes) - - # Ensure that layout_axes are continuous. - layout_axes_begin = layout_axes[0] - layout_axes_end = layout_axes[-1] + 1 - - if layout_axes != tuple(range(layout_axes_begin, layout_axes_end)): - raise ValueError(f'layout_axes must be continuous. Got {layout_axes}.') - layout_shape = arr.shape[layout_axes_begin:layout_axes_end] - - # Ensure that the layout shape is compatible - # with the gather_info. I.e. the first axis size must be equal or greater - # than the gather_info.input_shape, and all subsequent axes sizes must match. - if (len(layout_shape) != gather_info.input_shape.size) or ( - isinstance(gather_info.input_shape, np.ndarray) - and ( - (layout_shape[0] < gather_info.input_shape[0]) - or (np.any(layout_shape[1:] != gather_info.input_shape[1:])) - ) - ): - raise ValueError( - 'Input array layout axes are incompatible. You specified layout ' - f'axes {layout_axes} with an input array of shape {arr.shape}, but ' - f'the gather info expects shape {gather_info.input_shape}. ' - 'Your first axis size must be equal or greater than the ' - 'gather_info.input_shape, and all subsequent axes sizes must ' - 'match.' - ) - - # Compute the shape of the input array with flattened layout. - batch_shape = arr.shape[:layout_axes_begin] - features_shape = arr.shape[layout_axes_end:] - arr_flattened_shape = batch_shape + \ - (int(np.prod(layout_shape)),) + features_shape - - # Flatten input array and perform the gather. - arr_flattened = arr.reshape(arr_flattened_shape) - if layout_axes_begin == 0: - out_arr = arr_flattened[gather_info.gather_idxs, ...] - elif layout_axes_begin == 1: - out_arr = arr_flattened[:, gather_info.gather_idxs, ...] - elif layout_axes_begin == 2: - out_arr = arr_flattened[:, :, gather_info.gather_idxs, ...] - elif layout_axes_begin == 3: - out_arr = arr_flattened[:, :, :, gather_info.gather_idxs, ...] - elif layout_axes_begin == 4: - out_arr = arr_flattened[:, :, :, :, gather_info.gather_idxs, ...] - else: - raise ValueError( - 'Only 4 batch axes supported. If you need more, the code ' - 'is easy to extend.' - ) - - # Broadcast the mask and apply it. - broadcasted_mask_shape = ( - (1,) * len(batch_shape) - + gather_info.gather_mask.shape - + (1,) * len(features_shape) - ) - out_arr *= gather_info.gather_mask.reshape(broadcasted_mask_shape) - return out_arr - - -def convert_ms( - gather_info: GatherInfo, - arr: ms.Tensor, - *, - layout_axes: tuple[int, ...] = (0,), -) -> ms.Tensor: - """Convert an array from one atom layout to another.""" - # Translate negative indices to the corresponding positives. - layout_axes = tuple(i if i >= 0 else i + arr.ndim for i in layout_axes) - - # Ensure that layout_axes are continuous. - layout_axes_begin = layout_axes[0] - layout_axes_end = layout_axes[-1] + 1 - - if layout_axes != tuple(range(layout_axes_begin, layout_axes_end)): - raise ValueError(f'layout_axes must be continuous. Got {layout_axes}.') - layout_shape = arr.shape[layout_axes_begin:layout_axes_end] - - # Ensure that the layout shape is compatible - # with the gather_info. I.e. the first axis size must be equal or greater - # than the gather_info.input_shape, and all subsequent axes sizes must match. - # if (len(layout_shape) != gather_info.input_shape.size) or ( - # isinstance(gather_info.input_shape, np.ndarray) - # and ( - # (layout_shape[0] < gather_info.input_shape[0]) - # or (np.any(layout_shape[1:] != gather_info.input_shape[1:])) - # ) - # ): - # raise ValueError( - # 'Input array layout axes are incompatible. You specified layout ' - # f'axes {layout_axes} with an input array of shape {arr.shape}, but ' - # f'the gather info expects shape {gather_info.input_shape}. ' - # 'Your first axis size must be equal or greater than the ' - # 'gather_info.input_shape, and all subsequent axes sizes must ' - # 'match.' - # ) - - # Compute the shape of the input array with flattened layout. - batch_shape = arr.shape[:layout_axes_begin] - features_shape = arr.shape[layout_axes_end:] - arr_flattened_shape = batch_shape + \ - (int(math.prod(layout_shape)),) + features_shape - - # Flatten input array and perform the gather. - arr_flattened = arr.reshape(arr_flattened_shape) - out_arr = ops.gather(arr_flattened, gather_info.gather_idxs, axis=layout_axes_begin) - - # Broadcast the mask and apply it. - broadcasted_mask_shape = ( - (1,) * len(batch_shape) - + gather_info.gather_mask.shape - + (1,) * len(features_shape) - ) - out_arr *= ms.Tensor(gather_info.gather_mask.reshape(broadcasted_mask_shape)) - return out_arr.astype(ms.float32) - - -def make_structure( - flat_layout: AtomLayout, - atom_coords: np.ndarray, - name: str, - *, - atom_b_factors: Optional[np.ndarray] = None, - all_physical_residues: Optional[Residues] = None, -) -> structure.Structure: - """Returns a Structure from a flat layout and atom coordinates. - - The provided flat_layout must be 1-dim and must not contain any padding - elements. The flat_layout.atom_name must conform to the OpenMM/CCD standard - and must not contain deuterium. - - Args: - flat_layout: flat 1-dim AtomLayout without padding elements - atom_coords: np.ndarray of float, shape (num_atoms, 3) - name: str: the name (usually PDB id), e.g. '1uao' - atom_b_factors: np.ndarray of float, shape (num_atoms,) or None. If None, - they will be set to all zeros. - all_physical_residues: a Residues object that contains all physically - existing residues, i.e. also those residues that have no resolved atoms. - This is common in experimental structures, but also appears in predicted - structures for 'UNK' or other non-standard residue types, where the model - does not predict coordinates. This will be used to create the - `all_residues` field of the structure object. - """ - - if flat_layout.atom_name.ndim != 1 or not np.all( - flat_layout.atom_name.astype(bool) - ): - raise ValueError( - 'flat_layout must be 1-dim and must not contain anypadding element' - ) - if ( - flat_layout.atom_element is None - or flat_layout.res_name is None - or flat_layout.chain_type is None - ): - raise ValueError('All optional fields must be present.') - - if atom_b_factors is None: - atom_b_factors = np.zeros(atom_coords.shape[:-1]) - - if all_physical_residues is not None: - # Create the all_residues field from a Residues object - # (unfortunately there is no central place to keep the chain_types in - # the structure class, so we drop it here) - all_residues = collections.defaultdict(list) - for chain_id, res_id, res_name in zip( - all_physical_residues.chain_id, - all_physical_residues.res_id, - all_physical_residues.res_name, - strict=True, - ): - all_residues[chain_id].append((res_name, res_id)) - else: - # Create the all_residues field from the flat_layout - all_residues = collections.defaultdict(list) - if flat_layout.chain_id.shape[0] > 0: - all_residues[flat_layout.chain_id[0]].append( - (flat_layout.res_name[0], flat_layout.res_id[0]) - ) - for i in range(1, flat_layout.shape[0]): - if ( - flat_layout.chain_id[i] != flat_layout.chain_id[i - 1] - or flat_layout.res_name[i] != flat_layout.res_name[i - 1] - or flat_layout.res_id[i] != flat_layout.res_id[i - 1] - ): - all_residues[flat_layout.chain_id[i]].append( - (flat_layout.res_name[i], flat_layout.res_id[i]) - ) - - return structure.from_atom_arrays( - name=name, - all_residues=dict(all_residues), - chain_id=flat_layout.chain_id, - chain_type=flat_layout.chain_type, - res_id=flat_layout.res_id.astype(np.int32), - res_name=flat_layout.res_name, - atom_name=flat_layout.atom_name, - atom_element=flat_layout.atom_element, - atom_x=atom_coords[..., 0], - atom_y=atom_coords[..., 1], - atom_z=atom_coords[..., 2], - atom_b_factor=atom_b_factors, - ) - - -def _assert_all_arrays_have_same_shape( - *, - obj: Union[AtomLayout, Residues, GatherInfo], - expected_shape: tuple[int, ...], - attribute_names: Sequence[str], -) -> None: - """Checks that given attributes of the object have the expected shape.""" - attribute_shapes_description = [] - all_shapes_are_valid = True - - for attribute_name in attribute_names: - attribute = getattr(obj, attribute_name) - - if attribute is None: - attribute_shape = None - else: - attribute_shape = attribute.shape - - if attribute_shape is not None and expected_shape != attribute_shape: - all_shapes_are_valid = False - - attribute_shape_name = attribute_name + '.shape' - attribute_shapes_description.append( - f'{attribute_shape_name:25} = {attribute_shape}' - ) - - if not all_shapes_are_valid: - raise ValueError( - f'All arrays must have the same shape ({expected_shape=}). Got\n' - + '\n'.join(attribute_shapes_description) - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/base_config.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/base_config.py deleted file mode 100644 index 0d3a08b62..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/base_config.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright 2025 Huawei Technologies Co., Ltd -# -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Config for the protein folding model and experiment.""" - -from collections.abc import Mapping -import copy -import dataclasses -import types -import typing -from typing import Any, ClassVar, TypeVar - - -_T = TypeVar('_T') -_ConfigT = TypeVar('_ConfigT', bound='BaseConfig') - - -def _strip_optional(t: type[Any]) -> type[Any]: - """Transforms type annotations of the form `T | None` to `T`.""" - if typing.get_origin(t) in (typing.Union, types.UnionType): - args = set(typing.get_args(t)) - {types.NoneType} - if len(args) == 1: - return args.pop() - return t - - -_NO_UPDATE = object() - - -class _Autocreate: - - def __init__(self, **defaults: Any): - self.defaults = defaults - - -def autocreate(**defaults: Any) -> Any: - """Marks a field as having a default factory derived from its type.""" - return _Autocreate(**defaults) - - -def _clone_field( - field: dataclasses.Field[_T], new_default: _T -) -> dataclasses.Field[_T]: - if new_default is _NO_UPDATE: - return copy.copy(field) - return dataclasses.field( - default=new_default, - init=True, - kw_only=True, - repr=field.repr, - hash=field.hash, - compare=field.compare, - metadata=field.metadata, - ) - - -@typing.dataclass_transform() -class ConfigMeta(type): - """Metaclass that synthesizes a __post_init__ that coerces dicts to Config subclass instances.""" - - def __new__(mcs, name, bases, classdict): - cls = super().__new__(mcs, name, bases, classdict) - - def _coercable_fields(self) -> Mapping[str, tuple[ConfigMeta, Any]]: - type_hints = typing.get_type_hints(self.__class__) - fields = dataclasses.fields(self.__class__) - field_to_type_and_default = { - field.name: (_strip_optional( - type_hints[field.name]), field.default) - for field in fields - } - coercable_fields = { - f: t - for f, t in field_to_type_and_default.items() - if issubclass(type(t[0]), ConfigMeta) - } - return coercable_fields - - cls._coercable_fields = property(_coercable_fields) - - old_post_init = getattr(cls, '__post_init__', None) - - def _post_init(self) -> None: - # Use get_type_hints instead of Field.type to ensure that forward - # references are resolved. - for field_name, ( - field_type, - field_default, - ) in self._coercable_fields.items(): # pylint: disable=protected-access - field_value = getattr(self, field_name) - if field_value is None: - continue - try: - match field_value: - case _Autocreate(): - # Construct from field defaults. - setattr(self, field_name, field_type( - **field_value.defaults)) - case Mapping(): - # Field value is not yet a `Config` instance; Assume we can create - # one by splatting keys and values. - args = {} - # Apply default args first, if present. - if isinstance(field_default, _Autocreate): - args.update(field_default.defaults) - args.update(field_value) - setattr(self, field_name, field_type(**args)) - case _: - pass - except TypeError as e: - raise TypeError( - f'Failure while coercing field {field_name!r} of' - f' {self.__class__.__qualname__}' - ) from e - if old_post_init: - old_post_init(self) - - cls.__post_init__ = _post_init - - return dataclasses.dataclass(kw_only=True)(cls) - - -class BaseConfig(metaclass=ConfigMeta): - """Config base class. - - Subclassing Config automatically makes the subclass a kw_only dataclass with - a `__post_init__` that coerces Config-subclass field values from mappings to - instances of the right type. - """ - # Provided by dataclasses.make_dataclass - __dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]] - - # Overridden by metaclass - @property - def _coercable_fields(self) -> Mapping[str, tuple[type['BaseConfig'], Any]]: - return {} - - def as_dict(self) -> Mapping[str, Any]: - result = dataclasses.asdict(self) - for field_name in self._coercable_fields: - field_value = getattr(self, field_name, None) - if isinstance(field_value, BaseConfig): - result[field_name] = field_value.as_dict() - return result diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_model.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_model.py deleted file mode 100644 index 7e2f0382a..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_model.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Defines interface of a BaseModel.""" - -from collections.abc import Mapping -import dataclasses -from typing import Any, TypeAlias, Union, Optional -from alphafold3 import structure -import numpy as np -import mindspore as ms - -ModelResult: TypeAlias = Mapping[str, Any] -ScalarNumberOrArray: TypeAlias = Mapping[str, Union[float, int, np.ndarray]] - -# Eval result will contain scalars (e.g. metrics or losses), selected from the -# forward pass outputs or computed in the online evaluation; np.ndarrays or -# jax.Arrays generated from the forward pass outputs (e.g. distogram expected -# distances) or batch inputs; protein structures (predicted and ground-truth). -EvalResultValue: TypeAlias = Union[ - float, int, np.ndarray, ms.Tensor, structure.Structure -] -# Eval result may be None for some metrics if they are not computable. -EvalResults: TypeAlias = Mapping[str, Optional[EvalResultValue]] -# Interface metrics are all floats or None. -InterfaceMetrics: TypeAlias = Mapping[str, Optional[float]] -# Interface results are a mapping from interface name to mappings from score -# type to metric value. -InterfaceResults: TypeAlias = Mapping[str, Mapping[str, InterfaceMetrics]] -# Eval output consists of full eval results and a dict of interface metrics. -EvalOutput: TypeAlias = tuple[EvalResults, InterfaceResults] - -# Signature for `apply` method of hk.transform_with_state called on a BaseModel. -# ForwardFn: TypeAlias = Callable[ -# [hk.Params, hk.State, jax.Array, features.BatchDict], -# tuple[ModelResult, hk.State], -# ] - - -@dataclasses.dataclass(frozen=True) -class InferenceResult: - """Postprocessed model result.""" - - # Predicted protein structure. - predicted_structure: structure.Structure = dataclasses.field() - # Useful numerical data (scalars or arrays) to be saved at inference time. - numerical_data: ScalarNumberOrArray = dataclasses.field( - default_factory=dict) - # Smaller numerical data (usually scalar) to be saved as inference metadata. - metadata: ScalarNumberOrArray = dataclasses.field(default_factory=dict) - # Additional dict for debugging, e.g. raw outputs of a model forward pass. - debug_outputs: Optional[ModelResult] = dataclasses.field(default_factory=dict) - # Model identifier. - model_id: bytes = b'' diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_modules.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_modules.py deleted file mode 100644 index 478c0da8d..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/base_modules.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Common modules.""" - -from collections.abc import Sequence -import contextlib -import numbers -from typing import TypeAlias - -import numpy as np -import mindspore as ms -from mindspore import nn, ops -from mindspore.common import initializer -from mindscience.e3nn.utils import Ncon - -# Useful for mocking in tests. -DEFAULT_PRECISION = None - -# Constant from scipy.stats.truncnorm.std(a=-2, b=2, loc=0., scale=1.) -TRUNCATED_NORMAL_STDDEV_FACTOR = np.asarray( - 0.87962566103423978, dtype=np.float32 -) - - -class LayerNorm(nn.Cell): - """LayerNorm module. - - Equivalent to ms.nn.LayerNorm. In most cases, it can be replaced by ms.nn.LayerNorm. - Here, gamma is scale, beta is shift or offset - Args: - normalized_shape (tuple | list): The shape of Tensor which need to LayerNorm. - name (str): Name of this layer. - begin_norm_axis(int): From which axis norm begin - begin_params_axis(int): From which axis params begin - gamma_init('str'): Initializer of gamma - beta_init('str'): Initializer of beta - epsilon(float): epsilon value - dtype(ms.type): Type of output - create_beta(bool): whether to create a trainable beta parameter - create_gamma(bool): whether to create a trainable gamma parameter - Inputs: - - **x** (Tensor) - Tensor of any shape - Outputs: - The shape of tensor is the same as x. - Supported Platforms: - ``Ascend`` - """ - - def __init__(self, normalized_shape, name=None, begin_norm_axis=-1, - begin_params_axis=-1, gamma_init='ones', - beta_init='zeros', epsilon=1e-5, dtype=ms.float32, - create_beta=True, create_gamma=True): - super().__init__() - if not create_beta: - beta_init = 'zeros' - if not create_gamma: - gamma_init = 'ones' - self.layernorm = nn.LayerNorm(normalized_shape[begin_norm_axis:], begin_norm_axis=begin_norm_axis, - begin_params_axis=begin_params_axis, gamma_init=gamma_init, - beta_init=beta_init, epsilon=epsilon, dtype=dtype) - if create_beta is False: - self.layernorm.beta.requires_grad = False - if create_gamma is False: - self.layernorm.gamma.requires_grad = False - self.dtype = dtype - - def construct(self, x): - out = self.layernorm(x.astype(ms.float32)).astype(x.dtype) - return out - - -class CustomDense(nn.Cell): - """ - Custom Linear Module. It can be apply to a high dimension Tensor, and can be used on more than 1D Matmul. - In Alphafold, they use Einsum to replace Matmul, here we use Ncon to replace Matmul. if in_shape and out_shape - are both int, this layer is equivalence to nn.Dense. - Args: - in_shape (Union(int, List, Tuple)): input shape, that need to be multiplied. - out_shape (Union(int, List, Tuple)): output shape, that need to be multiplied. - Inputs: - - **x** (Tensor) - Outputs: - - Supported Platforms: - ``Ascend`` - """ - - def __init__(self, in_shape, out_shape, weight_init="zeros", use_bias=False, \ - bias_init="zeros", ndim=None, dtype=ms.float32): - super().__init__() - if isinstance(in_shape, int): - in_shape = (in_shape,) - if isinstance(out_shape, int): - out_shape = (out_shape,) - self.num_output_dims = len(out_shape) - self.num_input_dims = len(in_shape) - if ndim is None: - ndim = len(in_shape) + 1 - if weight_init in ["relu", "linear"]: - self.weight = custom_initializer( - weight_init, in_shape + out_shape, dtype=dtype) - else: - self.weight = ms.Parameter(initializer.initializer( - weight_init, in_shape + out_shape, dtype=dtype)) - self.use_bias = use_bias - if self.use_bias: - self.bias = ms.Parameter( - initializer.initializer(bias_init, out_shape, dtype=dtype)) - ncon_list1 = [-i-1 for i in range(ndim - self.num_input_dims)] + [ - i+1 for i in range(len(in_shape))] - ncon_list2 = (ncon_list1[ndim - self.num_input_dims:]) + \ - [-i-ndim+self.num_input_dims-1 for i in range(len(out_shape))] - self.ncon = Ncon([ncon_list1, ncon_list2]) - - in_letters = 'abcde'[: self.num_input_dims] - out_letters = 'hijkl'[: self.num_output_dims] - self.equation = f'...{in_letters}, {in_letters}{out_letters}->...{out_letters}' - - def construct(self, x): - if self.use_bias: - output = self.ncon([x, self.weight]) + self.bias - else: - output = self.ncon([x, self.weight]) - return output - - -def custom_initializer(initializer_name, input_shape, dtype=ms.float32): - """custom initializer""" - noise_scale = ms.Tensor(1.0) - for channel_dim in input_shape: - noise_scale /= channel_dim - if initializer_name == 'relu': - noise_scale *= 2 - stddev = ops.sqrt(noise_scale) - stddev = stddev / ms.Tensor(TRUNCATED_NORMAL_STDDEV_FACTOR) - param = ms.Parameter(initializer.initializer( - initializer.TruncatedNormal(stddev, 0), input_shape, dtype)) - return param diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py deleted file mode 100644 index 361a65bbb..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/mapping.py +++ /dev/null @@ -1,355 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Specialized mapping functions.""" - -from collections.abc import Callable, Sequence -import functools -from typing import Any, Union, Optional -import mindspore as ms - - -Pytree = Any -PytreeJaxArray = Any - -partial = functools.partial -PROXY = object() - - -def _maybe_slice(array, i, slice_size, axis): - "modified to mindspore" - if axis is PROXY: - return array - start = [0]*array.ndim - start[axis] = i - size = list(array.shape) - size[axis] = slice_size - return ms.ops.slice(array, start, size) - - -def _maybe_get_size(array, axis): - "modified to mindspore" - if axis == PROXY: - return -1 - return array.shape[axis] - - -def tree_flatten(tree): - """tree flatten""" - if isinstance(tree, (list, tuple)): - flat, structure = [], [] - for item in tree: - sub_flat, sub_struct = tree_flatten(item) - flat.extend(sub_flat) - structure.append(sub_struct) - return flat, structure - elif isinstance(tree, dict): - flat, structure = [], {} - for key, value in tree.items(): - sub_flat, sub_struct = tree_flatten(value) - flat.extend(sub_flat) - structure[key] = sub_struct - return flat, structure - else: - return [tree], None - - -def tree_unflatten(flat, structure): - """tree unflatten""" - if isinstance(structure, list): - result, idx = [], 0 - for sub_struct in structure: - sub_tree, idx = tree_unflatten(flat[idx:], sub_struct) - result.append(sub_tree) - return result, idx - elif isinstance(structure, dict): - result, idx = {}, 0 - for key, sub_struct in structure.items(): - sub_tree, idx = tree_unflatten(flat[idx:], sub_struct) - result[key] = sub_tree - return result, idx - else: - return flat[0], 1 - - -def _expand_axes(axes, values, name="sharded_apply"): - values_tree_def = tree_flatten(values)[1] - # flat_axes = tree_flatten(axes)[0] - flat_axes = [PROXY if axes is None else axes for _ in values_tree_def] - expanded_axes, _ = tree_unflatten(flat_axes, values_tree_def) - return expanded_axes - - -def tree_map(fn, *trees): - "Mindspore do not have the same function like Jax.tree.map, so try to write a mindspore version." - tree_types = {type(tree) for tree in trees} - tree_type = tree_types.pop() - if tree_type in (list,): - return tree_type(tree_map(fn, *subtrees) for subtrees in zip(*trees)) - if tree_type is dict: - keys = trees[0].keys() - if not all(tree.keys() == keys for tree in trees): - raise ValueError("All input dictionaries must have the same keys") - return {key: tree_map(fn, *(tree[key] for tree in trees)) for key in keys} - return fn(*trees) - - -def tree_leaves(tree): - "same as tree_map" - if isinstance(tree, (list, tuple)): - leaves = [] - for item in tree: - leaves.extend(tree_leaves(item)) - return leaves - if isinstance(tree, dict): - leaves = [] - for key in tree: - leaves.extend(tree_leaves(tree[key])) - return leaves - return [tree] - - -def eval_shape(fun, *args, **kwargs): - fake_inputs = [ms.ops.zeros(arg.shape, dtype=arg.dtype) if isinstance( - arg, ms.Tensor) else arg for arg in args] - output = fun(*fake_inputs, **kwargs) - return output - - -def sharded_apply( - fun: Callable[..., PytreeJaxArray], - shard_size: Optional[int] = 1, - in_axes: Union[int, Pytree] = 0, - out_axes: Union[int, Pytree] = 0, - new_out_axes: bool = False, -) -> Callable[..., PytreeJaxArray]: - """Sharded apply. - - Applies `fun` over shards to axes, in a way similar to vmap, - but does so in shards of `shard_size`. Shards are stacked after. - This allows a smooth trade-off between - memory usage (as in a plain map) vs higher throughput (as in a vmap). - - Args: - fun: Function to apply smap transform to. - shard_size: Integer denoting shard size. - in_axes: Either integer or pytree describing which axis to map over for each - input to `fun`, None denotes broadcasting. - out_axes: Integer or pytree denoting to what axis in the output the mapped - over axis maps. - new_out_axes: Whether to stack outputs on new axes. This assumes that the - output sizes for each shard (including the possible remainder shard) are - the same. - - Returns: - Function with smap applied. - """ - docstr = ( - "Mapped version of {fun}. Takes similar arguments to {fun} " - "but with additional array axes over which {fun} is mapped." - ) - if new_out_axes: - raise NotImplementedError("New output axes not yet implemented.") - - # shard size None denotes no sharding - if shard_size is None: - return fun - - def mapped_fn(*args, **kwargs): - # Expand in axes and determine loop range. - in_axes_ = _expand_axes(ms.Tensor(in_axes), args) - - in_sizes = tree_map(_maybe_get_size, list(args), in_axes_) - in_size = max(tree_leaves(in_sizes)) - - num_extra_shards = (in_size - 1) // shard_size - - # Fix if necessary. - last_shard_size = in_size % shard_size - last_shard_size = shard_size if last_shard_size == 0 else last_shard_size - - def apply_fun_to_slice(slice_start, slice_size, args, in_axes_): - input_slice = tree_map( - lambda array, axis: _maybe_slice( - array, slice_start, slice_size, axis - ), - args, - in_axes_, - ) - return fun(input_slice, **kwargs) - - remainder_shape_dtype = eval_shape( - lambda array, axis: apply_fun_to_slice( - 0, last_shard_size, array, axis), - args, in_axes_ - ) - - out_shapes = tree_map(lambda x: x.shape, remainder_shape_dtype) - out_dtypes = tree_map(lambda x: x.dtype, remainder_shape_dtype) - out_axes_ = _expand_axes(out_axes, out_shapes) - - if num_extra_shards > 0: - regular_shard_shape_dtype = eval_shape( - lambda array, axis: apply_fun_to_slice( - 0, shard_size, array, axis), - args, in_axes_ - ) - shard_shapes = tree_map( - lambda x: x.shape, regular_shard_shape_dtype) - - def make_output_shape(axis, shard_shape, remainder_shape): - axis = axis if isinstance(axis, int) else int(axis[0]) - shard_shape = tuple(shard_shape) - remainder_shape = tuple(remainder_shape) - return ms.ops.stack( - shard_shape[:axis] - + (shard_shape[axis] * num_extra_shards + - remainder_shape[axis],) - + shard_shape[axis + 1:] - ) - - out_shapes = tree_map( - make_output_shape, out_axes_[0], ms.Tensor( - shard_shapes), ms.Tensor(out_shapes) - ) - - # Calls dynamic Update slice with different argument order. - # This is here since tree_map only works with positional arguments. - def dynamic_update_slice_in_dim(array, slice_size, axis, i): - start = [0]*array.ndim - start[axis] = int(i) - size = list(array.shape) - size[axis] = slice_size.shape[axis] - # return ms.ops.slice(array, start, size) - end = [x + y for x, y in zip(start, size)] - array[start[0]: end[0]] = slice_size - return array - - def compute_shard(outputs, slice_start, slice_size): - def slice_op(array, axis): - return apply_fun_to_slice(int(slice_start), shard_size, array, axis) - slice_out = slice_op(args, in_axes_) - update_slice = partial(dynamic_update_slice_in_dim, i=slice_start) - # slice_out = (slice_out,) if not isinstance(slice, (int, float)) else [int(x) for x in slice_out] - return tree_map(update_slice, outputs, slice_out, out_axes_[0]) - - def scan_iteration(outputs, i): - new_outputs = compute_shard(outputs, i, shard_size) - return new_outputs - - slice_starts = ms.ops.arange(0, in_size - shard_size + 1, shard_size) - - def allocate_buffer(dtype, shape): - return ms.ops.zeros(shape, dtype=dtype) - - outputs = tree_map(allocate_buffer, out_dtypes, out_shapes) - - if slice_starts.shape[0] > 0: - for slice_start in slice_starts: - outputs = scan_iteration(outputs, slice_start) - # scan_op = ms.ops.Scan() - # outputs, _ = scan_op(scan_iteration, outputs, slice_starts) - - if last_shard_size != shard_size: - remainder_start = in_size - last_shard_size - outputs = compute_shard(outputs, remainder_start, last_shard_size) - - return outputs - - return mapped_fn - - -def sharded_map(fun, shard_size=1, in_axes=0, out_axes=0): - vmapped_fun = ms.vmap(fun, int(in_axes), int(out_axes)) - return sharded_apply(vmapped_fun, shard_size, in_axes, out_axes) - - -def reshape_partitioned_inputs(batched_args, partitioned_dim, subbatch_size): - """Reshapes so subbatching doesn't happen on the partitioned dim.""" - subbatched_args = [] - for arg in batched_args: - shape = arg.shape - new_shape = ( - shape[:partitioned_dim] - + (subbatch_size, shape[partitioned_dim] // subbatch_size) - + shape[partitioned_dim + 1:] - ) - subbatched_args.append(arg.reshape(new_shape)) - return subbatched_args - - -def reshape_partitioned_output(output, output_subbatch_dim): - """Reshapes outputs as if reshape_partitioned_inputs were never applied.""" - out_shape = ( - output.shape[: output_subbatch_dim - 1] - + (-1,) - + output.shape[output_subbatch_dim + 1:] - ) - return output.reshape(out_shape) - - -def inference_subbatch(module, subbatch_size, batched_args, - nonbatched_args, input_subbatch_dim=0, output_subbatch_dim=None, - input_subbatch_dim_is_partitioned=False): - """Run through subbatches (like batch apply but with split and concat).""" - if output_subbatch_dim is None: - output_subbatch_dim = input_subbatch_dim - if input_subbatch_dim_is_partitioned: - # Subbatching along the partitioned axis would induce an all-gather that - # undoes the partitioning. So instead we reshape such that - # [..., partitioned_input_size, ...] becomes [..., subbatch_size, - # partitioned_input_size // subbatch_size, ...] and then actually subbatch - # along the partitioned_input_size // subbatch_size axis in slices of - # size 1. Partitioning is then preserved on the partitioned axis, except - # that dimension is now of size subbatch_size instead of - # partitioned_input_size. Note that the module itself still sees inputs of - # size [..., subbatch_size, ...], just as it would if this reshaping were - # not applied. - batched_args = reshape_partitioned_inputs( - batched_args, input_subbatch_dim, subbatch_size - ) - input_subbatch_dim += 1 - output_subbatch_dim += 1 - subbatch_size = 1 - - def run_module(*batched_args): - if input_subbatch_dim_is_partitioned: - # Squeeze off the singleton dimension (otherwise the module would see - # [..., subbatch_size, 1, ...]). - batched_args = [b.squeeze(axis=input_subbatch_dim) - for b in batched_args] - args = list(batched_args)[0] + list(nonbatched_args) - res = module(*args) - if input_subbatch_dim_is_partitioned: - # Add back in the singleton dimension so the outputs are stacked on the - # axis we are actually subbatching over (i.e stacked back to - # [..., subbatch_size, partitioned_input_size // subbatch_size, ...]), - # rather than on the partitioned axis, which would again induce an - # all-gather that breaks partitioning. - res = ms.ops.expand_dims(res, axis=output_subbatch_dim) - return res - sharded_module = sharded_apply( - run_module, - shard_size=subbatch_size, - in_axes=input_subbatch_dim, - out_axes=output_subbatch_dim, - ) - output = sharded_module(*batched_args) - if input_subbatch_dim_is_partitioned: - # The is of the same shape as the inputs [..., subbatch_size, - # partitioned_input_size // subbatch_size, ...]. Reshape to - # [..., partitioned_input_size, ...] as if the reshaping due to partitioning - # had never been applied. - output = reshape_partitioned_output(output, output_subbatch_dim) - - return output diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/utils.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/utils.py deleted file mode 100644 index 59dd90e87..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/components/utils.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Utils for components""" -import numbers - -import numpy as np -import mindspore as ms - -VALID_DTYPES = [np.float32, np.float64, np.int8, np.int32, np.int32, bool] - - -def remove_invalidly_typed_feats(batch): - """Remove features of types we don't want to send to the TPU e.g. strings.""" - return { - k: v - for k, v in batch.items() - if hasattr(v, 'dtype') and v.dtype in VALID_DTYPES - } - - -def mask_mean(mask, value, axis=None, keepdims=False, eps=1e-10): - """Masked mean.""" - - mask_shape = mask.shape - value_shape = value.shape - - if isinstance(axis, numbers.Integral): - axis = [axis] - elif axis is None: - axis = list(range(len(mask_shape))) - - broadcast_factor = 1.0 - for axis_ in axis: - value_size = value_shape[axis_] - mask_size = mask_shape[axis_] - if mask_size == 1: - broadcast_factor *= value_size - else: - error = f'Shapes are not compatible, shapes: {mask_shape}, {value_shape}' - if mask_size != value_size: - raise ValueError(error) - - return ms.ops.sum(mask * value, keepdim=keepdims, dim=axis) / ( - ms.ops.maximum( - ms.ops.sum(mask, keepdim=keepdims, dim=axis) * - broadcast_factor, eps - ) - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/confidence_types.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/confidence_types.py deleted file mode 100644 index 3ceb926d6..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/confidence_types.py +++ /dev/null @@ -1,310 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Confidence categories for predictions.""" - -import dataclasses -import enum -import json -from typing import Any, Self - -from absl import logging -import numpy as np -import mindspore as ms -from alphafold3.model.components import base_model -from alphafold3.model.components.mapping import tree_map - - -class StructureConfidenceFullEncoder(json.JSONEncoder): - """JSON encoder for serializing confidence types.""" - - def __init__(self, **kwargs): - super().__init__(**(kwargs | dict(separators=(',', ':')))) - - def encode(self, o: 'StructureConfidenceFull'): - # Cast to np.float64 before rounding, since casting to Python float will - # cast to a 64 bit float, potentially undoing np.float32 rounding. - atom_plddts = np.round( - np.clip(np.asarray(o.atom_plddts, dtype=np.float64), 0.0, 99.99), 2 - ).astype(float) - contact_probs = np.round( - np.clip(np.asarray(o.contact_probs, dtype=np.float64), 0.0, 1.0), 2 - ).astype(float) - pae = np.round( - np.clip(np.asarray(o.pae, dtype=np.float64), 0.0, 99.9), 1 - ).astype(float) - return """\ -{ - "atom_chain_ids": %s, - "atom_plddts": %s, - "contact_probs": %s, - "pae": %s, - "token_chain_ids": %s, - "token_res_ids": %s -}""" % ( - super().encode(o.atom_chain_ids), - super().encode(list(atom_plddts)).replace('NaN', 'null'), - super().encode([list(x) - for x in contact_probs]).replace('NaN', 'null'), - super().encode([list(x) for x in pae]).replace('NaN', 'null'), - super().encode(o.token_chain_ids), - super().encode(o.token_res_ids), - ) - - -def _dump_json(data: Any, indent: int | None = None) -> str: - """Dumps a json string with JSON compatible NaN representation.""" - json_str = json.dumps( - data, - sort_keys=True, - indent=indent, - separators=(',', ': '), - ) - return json_str.replace('NaN', 'null') - - -@enum.unique -class ConfidenceCategory(enum.Enum): - """Confidence categories for AlphaFold predictions.""" - - HIGH = 0 - MEDIUM = 1 - LOW = 2 - DISORDERED = 3 - - @classmethod - def from_char(cls, char: str) -> Self: - match char: - case 'H': - return cls.HIGH - case 'M': - return cls.MEDIUM - case 'L': - return cls.LOW - case 'D': - return cls.DISORDERED - case _: - raise ValueError( - f'Unknown character. Expected one of H, M, L or D; got: {char}' - ) - - def to_char(self) -> str: - match self: - case self.HIGH: - return 'H' - case self.MEDIUM: - return 'M' - case self.LOW: - return 'L' - case self.DISORDERED: - return 'D' - - @classmethod - def from_confidence_score(cls, confidence: float) -> Self: - if 90 <= confidence <= 100: - return cls.HIGH - if 70 <= confidence < 90: - return cls.MEDIUM - if 50 <= confidence < 70: - return cls.LOW - if 0 <= confidence < 50: - return cls.DISORDERED - raise ValueError( - f'Confidence score out of range [0, 100]: {confidence}') - - -@dataclasses.dataclass() -class AtomConfidence: - """Dataclass for 1D per-atom confidences from AlphaFold.""" - - chain_id: list[str] - atom_number: list[int] - confidence: list[float] - confidence_category: list[ConfidenceCategory] - - def __post_init__(self): - num_res = len(self.atom_number) - if not all( - len(v) == num_res - for v in [self.chain_id, self.confidence, self.confidence_category] - ): - raise ValueError( - 'All confidence fields must have the same length.') - - @classmethod - def from_inference_result( - cls, inference_result: base_model.InferenceResult - ) -> Self: - """Instantiates an AtomConfidence from a structure. - - Args: - inference_result: Inference result from AlphaFold. - - Returns: - Scores in AtomConfidence dataclass. - """ - struct = inference_result.predicted_structure - as_dict = { - 'chain_id': [], - 'atom_number': [], - 'confidence': [], - 'confidence_category': [], - } - for atom_number, atom in enumerate(struct.iter_atoms()): - this_confidence = float(struct.atom_b_factor[atom_number]) - as_dict['chain_id'].append(atom['chain_id']) - as_dict['atom_number'].append(atom_number) - as_dict['confidence'].append(round(this_confidence, 2)) - as_dict['confidence_category'].append( - ConfidenceCategory.from_confidence_score(this_confidence) - ) - return cls(**as_dict) - - @classmethod - def from_json(cls, json_string: str) -> Self: - """Instantiates a AtomConfidence from a json string.""" - input_dict = json.loads(json_string) - input_dict['confidence_category'] = [ - ConfidenceCategory.from_char(k) - for k in input_dict['confidence_category'] - ] - return cls(**input_dict) - - def to_json(self) -> str: - output = dataclasses.asdict(self) - output['confidence_category'] = [ - k.to_char() for k in output['confidence_category'] - ] - output['atom_number'] = [int(k) for k in output['atom_number']] - return _dump_json(output) - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class StructureConfidenceSummary: - """Dataclass for the summary of structure scores from AlphaFold. - - Attributes: - ptm: Predicted TM global score. - iptm: Interface predicted TM global score. - ranking_score: Ranking score extracted from CIF metadata. - fraction_disordered: Fraction disordered, measured with RASA. - has_clash: Has significant clashing. - chain_pair_pae_min: [num_chains, num_chains] Minimum cross chain PAE. - chain_pair_iptm: [num_chains, num_chains] Chain pair ipTM. - chain_ptm: [num_chains] Chain pTM. - chain_iptm: [num_chains] Mean cross chain ipTM for a chain. - """ - - ptm: float - iptm: float - ranking_score: float - fraction_disordered: float - has_clash: float - chain_pair_pae_min: np.ndarray - chain_pair_iptm: np.ndarray - chain_ptm: np.ndarray - chain_iptm: np.ndarray - - @classmethod - def from_inference_result( - cls, inference_result: base_model.InferenceResult - ) -> Self: - """Returns a new instance based on a given inference result.""" - return cls( - ptm=float(inference_result.metadata['ptm']), - iptm=float(inference_result.metadata['iptm']), - ranking_score=float(inference_result.metadata['ranking_score']), - fraction_disordered=float( - inference_result.metadata['fraction_disordered'] - ), - has_clash=float(inference_result.metadata['has_clash']), - chain_pair_pae_min=inference_result.metadata['chain_pair_pae_min'], - chain_pair_iptm=inference_result.metadata['chain_pair_iptm'], - chain_ptm=inference_result.metadata['iptm_ichain'], - chain_iptm=inference_result.metadata['iptm_xchain'], - ) - - @classmethod - def from_json(cls, json_string: str) -> Self: - """Returns a new instance from a given json string.""" - return cls(**json.loads(json_string)) - - def to_json(self) -> str: - def convert(data): - if isinstance(data, np.ndarray): - # Cast to np.float64 before rounding, since casting to Python float will - # cast to a 64 bit float, potentially undoing np.float32 rounding. - rounded_data = np.round(data.astype( - np.float64), decimals=2).tolist() - else: - rounded_data = np.round(data, decimals=2) - return rounded_data - - return _dump_json(tree_map(convert, dataclasses.asdict(self)), indent=1) - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class StructureConfidenceFull: - """Dataclass for full structure data from AlphaFold.""" - - pae: np.ndarray - token_chain_ids: list[str] - token_res_ids: list[int] - atom_plddts: list[float] - atom_chain_ids: list[str] - contact_probs: np.ndarray # [num_tokens, num_tokens] - - @classmethod - def from_inference_result( - cls, inference_result: base_model.InferenceResult - ) -> Self: - """Returns a new instance based on a given inference result.""" - - pae = inference_result.numerical_data['full_pae'] - if isinstance(pae, ms.Tensor): - pae = pae.asnumpy() - if not isinstance(pae, np.ndarray): - logging.info('%s', type(pae)) - raise TypeError('pae should be a numpy array.') - - contact_probs = inference_result.numerical_data['contact_probs'] - if isinstance(contact_probs, ms.Tensor): - contact_probs = contact_probs.asnumpy() - if not isinstance(contact_probs, np.ndarray): - logging.info('%s', type(contact_probs)) - raise TypeError('contact_probs should be a numpy array.') - - struct = inference_result.predicted_structure - chain_ids = struct.chain_id.tolist() - atom_plddts = struct.atom_b_factor.tolist() - token_chain_ids = [ - str(token_id) - for token_id in inference_result.metadata['token_chain_ids'] - ] - token_res_ids = [ - int(token_id) for token_id in inference_result.metadata['token_res_ids'] - ] - return cls( - pae=pae, - token_chain_ids=token_chain_ids, - token_res_ids=token_res_ids, - atom_plddts=atom_plddts, - atom_chain_ids=chain_ids, - contact_probs=contact_probs, - ) - - @classmethod - def from_json(cls, json_string: str) -> Self: - """Returns a new instance from a given json string.""" - return cls(**json.loads(json_string)) - - def to_json(self) -> str: - """Converts StructureConfidenceFull to json string.""" - return json.dumps(self, cls=StructureConfidenceFullEncoder) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/confidences.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/confidences.py deleted file mode 100644 index f519f858a..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/confidences.py +++ /dev/null @@ -1,660 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Functions for extracting and processing confidences from model outputs.""" -import warnings -from typing import Optional -import numpy as np -from absl import logging -from alphafold3 import structure -from alphafold3.constants import residue_names -from alphafold3.cpp import mkdssp -from scipy import spatial - - -# From Sander & Rost 1994 https://doi.org/10.1002/prot.340200303 - -MAX_ACCESSIBLE_SURFACE_AREA = { - 'ALA': 106.0, - 'ARG': 248.0, - 'ASN': 157.0, - 'ASP': 163.0, - 'CYS': 135.0, - 'GLN': 198.0, - 'GLU': 194.0, - 'GLY': 84.0, - 'HIS': 184.0, - 'ILE': 169.0, - 'LEU': 164.0, - 'LYS': 205.0, - 'MET': 188.0, - 'PHE': 197.0, - 'PRO': 136.0, - 'SER': 130.0, - 'THR': 142.0, - 'TRP': 227.0, - 'TYR': 222.0, - 'VAL': 142.0, -} - -# Weights for ranking confidence. -_IPTM_WEIGHT = 0.8 -_FRACTION_DISORDERED_WEIGHT = 0.5 -_CLASH_PENALIZATION_WEIGHT = 100.0 - - -def windowed_solvent_accessible_area(cif: str, window: int = 25) -> np.ndarray: - """Implementation of AlphaFold_RSA. - - AlphaFold_RSA defined in - https://www.ncbi.nlm.nih.gov/pmc/articles/PMC9601767/. - - Args: - cif: raw cif string. - window: The window over which to average accessible surface area - - Returns: - An array of size num_res that predicts disorder by using windowed solvent - accessible surface area. - """ - result = mkdssp.get_dssp(cif, calculate_surface_accessibility=True) - parse_row = False - rasa = [] - for row in result.splitlines(): - if parse_row: - aa = row[13:14] - if aa == '!': - continue - aa3 = residue_names.PROTEIN_COMMON_ONE_TO_THREE.get(aa, 'ALA') - max_acc = MAX_ACCESSIBLE_SURFACE_AREA[aa3] - acc = int(row[34:38]) - norm_acc = acc / max_acc - norm_acc = min(norm_acc, 1.0) - rasa.append(norm_acc) - if row.startswith(' # RESIDUE'): - parse_row = True - - half_w = (window - 1) // 2 - pad_rasa = np.pad(rasa, (half_w, half_w), 'reflect') - rasa = np.convolve(pad_rasa, np.ones(window), 'valid') / window - return rasa - - -def fraction_disordered( - struct: structure.Structure, rasa_disorder_cutoff: float = 0.581 -) -> float: - """Compute fraction of protein residues that are disordered. - - Args: - struct: A structure to compute rASA metrics on. - rasa_disorder_cutoff: The threshold at which residues are considered - disordered. Default value taken from - https://www.ncbi.nlm.nih.gov/pmc/articles/PMC9601767/. - - Returns: - The fraction of protein residues that are disordered - (rasa > rasa_disorder_cutoff). - """ - struct = struct.filter_to_entity_type(protein=True) - rasa = [] - seq_rasa = {} - for chain_id, chain_seq in struct.chain_single_letter_sequence().items(): - if chain_seq in seq_rasa: - # We assume that identical sequences have approximately similar rasa - # values to speed up the computation. - rasa.extend(seq_rasa[chain_seq]) - continue - chain_struct = struct.filter(chain_id=chain_id) - try: - rasa_per_residue = windowed_solvent_accessible_area( - chain_struct.to_mmcif() - ) - seq_rasa[chain_seq] = rasa_per_residue - rasa.extend(rasa_per_residue) - except (ValueError, RuntimeError): - logging.warning('%s: rasa calculation failed', struct.name) - - if not rasa: - return 0.0 - return np.mean(np.array(rasa) > rasa_disorder_cutoff) - - -def has_clash( - struct: structure.Structure, - cutoff_radius: float = 1.1, - min_clashes_for_overlap: int = 100, - min_fraction_for_overlap: float = 0.5, -) -> bool: - """Determine whether the structure has at least one clashing chain. - - A clashing chain is defined as having greater than 100 polymer atoms within - 1.1A of another polymer atom, or having more than 50% of the chain with - clashing atoms. - - Args: - struct: A structure to get clash metrics for. - cutoff_radius: atom distances under this threshold are considered a clash. - min_clashes_for_overlap: The minimum number of atom-atom clashes for a chain - to be considered overlapping. - min_fraction_for_overlap: The minimum fraction of atoms within a chain that - are clashing for the chain to be considered overlapping. - - Returns: - True if the structure has at least one clashing chain. - """ - struct = struct.filter_to_entity_type(protein=True, rna=True, dna=True) - if not struct.chains: - return False - coords = struct.coords - coord_kdtree = spatial.cKDTree(coords) - clashes_per_atom = coord_kdtree.query_ball_point( - coords, p=2.0, r=cutoff_radius - ) - per_atom_has_clash = np.zeros(len(coords), dtype=np.int32) - for atom_idx, clashing_indices in enumerate(clashes_per_atom): - for clashing_idx in clashing_indices: - if np.abs(struct.res_id[atom_idx] - struct.res_id[clashing_idx]) > 1 or ( - struct.chain_id[atom_idx] != struct.chain_id[clashing_idx] - ): - per_atom_has_clash[atom_idx] = True - break - for chain_id in struct.chains: - mask = struct.chain_id == chain_id - num_atoms = np.sum(mask) - if num_atoms == 0: - continue - num_clashes = np.sum(per_atom_has_clash * mask) - frac_clashes = num_clashes / num_atoms - if ( - num_clashes > min_clashes_for_overlap - or frac_clashes > min_fraction_for_overlap - ): - return True - return False - - -def get_ranking_score( - ptm: float, iptm: float, fraction_disordered_: float, has_clash_: bool -) -> float: - # ipTM is NaN for single chain structures. Use pTM for such cases. - if np.isnan(iptm): - ptm_iptm_average = ptm - else: - ptm_iptm_average = _IPTM_WEIGHT * iptm + (1.0 - _IPTM_WEIGHT) * ptm - return ( - ptm_iptm_average - + _FRACTION_DISORDERED_WEIGHT * fraction_disordered_ - - _CLASH_PENALIZATION_WEIGHT * has_clash_ - ) - - -def rank_metric( - full_pde: np.ndarray, contact_probs: np.ndarray -) -> np.ndarray: - """Compute the metric that will be used to rank predictions, higher is better. - - Args: - full_pde: A [num_samples, num_tokens,num_tokens] matrix of predicted - distance errors between pairs of tokens. - contact_probs: A [num_tokens, num_tokens] matrix consisting of the - probability of contact (<8A) that is returned from the distogram head. - - Returns: - A scalar that can be used to rank (higher is better). - """ - if not isinstance(full_pde, type(contact_probs)): - raise ValueError( - 'full_pde and contact_probs must be of the same type.') - - if isinstance(full_pde, np.ndarray): - sum_fn = np.sum - else: - raise ValueError('full_pde must be a numpy array or a jax array.') - # It was found that taking the contact_map weighted average was better than - # just the predicted distance error on its own. - return -sum_fn(full_pde * contact_probs[None, :, :], axis=(-2, -1)) / ( - sum_fn(contact_probs) + 1e-6 - ) - - -def weighted_mean(mask, value, axis): - return np.mean(mask * value, axis=axis) / (1e-8 + np.mean(mask, axis=axis)) - - -def pde_single( - num_tokens: int, - asym_ids: np.ndarray, - full_pde: np.ndarray, - contact_probs: np.ndarray, -) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Compute 1D PDE summaries. - - Args: - num_tokens: The number of tokens (not including padding). - asym_ids: The asym_ids (array of shape num_tokens). - full_pde: A [num_samples, num_tokens, num_tokens] matrix of predicted - distance errors. - contact_probs: A [num_tokens, num_tokens] matrix consisting of the - probability of contact (<8A) that is returned from the distogram head. - - Returns: - A tuple (ichain, xchain, full_chain) where: - `ichain` is a [num_samples, num_chains] matrix where the - value assigned to each chain is an average of the full PDE matrix over all - its within-chain interactions, weighted by `contact_probs`. - `xchain` is a [num_samples, num_chains] matrix where the - value assigned to each chain is an average of the full PDE matrix over all - its cross-chain interactions, weighted by `contact_probs`. - `full_chain` is a [num_samples, num_tokens] matrix where the - value assigned to each token is an average of it PDE against all tokens, - weighted by `contact_probs`. - """ - - full_pde = full_pde[:, :num_tokens, :num_tokens] - contact_probs = contact_probs[:num_tokens, :num_tokens] - asym_ids = asym_ids[:num_tokens] - unique_asym_ids = np.unique(asym_ids) - num_chains = len(unique_asym_ids) - num_samples = full_pde.shape[0] - - asym_ids = asym_ids[None] - contact_probs = contact_probs[None] - - ichain = np.zeros((num_samples, num_chains)) - xchain = np.zeros((num_samples, num_chains)) - - for idx, asym_id in enumerate(unique_asym_ids): - my_asym_id = asym_ids == asym_id - imask = my_asym_id[:, :, None] * my_asym_id[:, None, :] - xmask = my_asym_id[:, :, None] * ~my_asym_id[:, None, :] - imask = imask * contact_probs - xmask = xmask * contact_probs - ichain[:, idx] = weighted_mean( - mask=imask, value=full_pde, axis=(-2, -1)) - xchain[:, idx] = weighted_mean( - mask=xmask, value=full_pde, axis=(-2, -1)) - - full_chain = weighted_mean(mask=contact_probs, value=full_pde, axis=(-1,)) - - return ichain, xchain, full_chain - - -def chain_pair_pde( - num_tokens: int, asym_ids: np.ndarray, full_pde: np.ndarray -) -> tuple[np.ndarray, np.ndarray]: - """Compute predicted distance errors for all pairs of chains. - - Args: - num_tokens: The number of tokens (not including padding). - asym_ids: The asym_ids (array of shape num_tokens). - full_pde: A [num_samples, num_tokens, num_tokens] matrix of predicted - distance errors. - - Returns: - chain_pair_pred_err_mean - a [num_chains, num_chains] matrix with average - per chain-pair predicted distance error. - chain_pair_pred_err_min - a [num_chains, num_chains] matrix with min - per chain-pair predicted distance error. - """ - full_pde = full_pde[:, :num_tokens, :num_tokens] - asym_ids = asym_ids[:num_tokens] - unique_asym_ids = np.unique(asym_ids) - num_chains = len(unique_asym_ids) - num_samples = full_pde.shape[0] - chain_pair_pred_err_mean = np.zeros((num_samples, num_chains, num_chains)) - chain_pair_pred_err_min = np.zeros((num_samples, num_chains, num_chains)) - - for idx1, asym_id_1 in enumerate(unique_asym_ids): - subset = full_pde[:, asym_ids == asym_id_1, :] - for idx2, asym_id_2 in enumerate(unique_asym_ids): - subsubset = subset[:, :, asym_ids == asym_id_2] - chain_pair_pred_err_mean[:, idx1, idx2] = np.mean( - subsubset, axis=(1, 2)) - chain_pair_pred_err_min[:, idx1, idx2] = np.min( - subsubset, axis=(1, 2)) - return chain_pair_pred_err_mean, chain_pair_pred_err_min - - -def weighted_nanmean( - value: np.ndarray, mask: np.ndarray, axis: int -) -> np.ndarray: - """Nan-mean with weighting -- empty slices return NaN.""" - - nan_idxs = np.where(np.isnan(value)) - # Need to NaN the mask to get the correct denominator weighting. - mask_with_nan = mask.copy() - mask_with_nan[nan_idxs] = np.nan - with warnings.catch_warnings(): - # Mean of empty slice is ok and should return a NaN. - warnings.filterwarnings(action='ignore', message='Mean of empty slice') - return np.nanmean(value * mask_with_nan, axis=axis) / np.nanmean( - mask_with_nan, axis=axis - ) - - -def chain_pair_pae( - *, - num_tokens: int, - asym_ids: np.ndarray, - full_pae: np.ndarray, - mask: Optional[np.ndarray] = None, - contact_probs: Optional[np.ndarray] = None, -) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Compute predicted errors for all pairs of chains. - - Args: - num_tokens: The number of tokens (not including padding). - asym_ids: The asym_ids (array of shape num_tokens). - full_pae: A [num_samples, num_tokens, num_tokens] matrix of predicted - errors. - mask: A [num_tokens, num_tokens] mask matrix. - contact_probs: A [num_tokens, num_tokens] matrix consisting of the - probability of contact (<8A) that is returned from the distogram head. - - Returns: - chain_pair_pred_err_mean - a [num_chains, num_chains] matrix with average - per chain-pair predicted error. - """ - if mask is None: - mask = np.ones(shape=full_pae.shape[1:], dtype=bool) - if contact_probs is None: - contact_probs = np.ones(shape=full_pae.shape[1:], dtype=float) - - full_pae = full_pae[:, :num_tokens, :num_tokens] - mask = mask[:num_tokens, :num_tokens] - asym_ids = asym_ids[:num_tokens] - contact_probs = contact_probs[:num_tokens, :num_tokens] - unique_asym_ids = np.unique(asym_ids) - num_chains = len(unique_asym_ids) - num_samples = full_pae.shape[0] - chain_pair_pred_err_mean = np.zeros((num_samples, num_chains, num_chains)) - chain_pair_pred_err_min = np.zeros((num_samples, num_chains, num_chains)) - - for idx1, asym_id_1 in enumerate(unique_asym_ids): - subset = full_pae[:, asym_ids == asym_id_1, :] - subset_mask = mask[asym_ids == asym_id_1, :] - subset_contact_probs = contact_probs[asym_ids == asym_id_1, :] - for idx2, asym_id_2 in enumerate(unique_asym_ids): - subsubset = subset[:, :, asym_ids == asym_id_2] - subsubset_mask = subset_mask[:, asym_ids == asym_id_2] - subsubset_contact_probs = subset_contact_probs[:, - asym_ids == asym_id_2] - (flat_mask_idxs,) = np.where(subsubset_mask.flatten() > 0) - flat_subsubset = subsubset.reshape([num_samples, -1]) - flat_contact_probs = subsubset_contact_probs.flatten() - # A ligand chain will have no valid frames if it contains fewer than - # three non-colinear atoms (e.g. a sodium ion). - if not flat_mask_idxs.size: - chain_pair_pred_err_mean[:, idx1, idx2] = np.nan - chain_pair_pred_err_min[:, idx1, idx2] = np.nan - else: - chain_pair_pred_err_min[:, idx1, idx2] = np.min( - flat_subsubset[:, flat_mask_idxs], axis=1 - ) - chain_pair_pred_err_mean[:, idx1, idx2] = weighted_mean( - mask=flat_contact_probs[flat_mask_idxs], - value=flat_subsubset[:, flat_mask_idxs], - axis=-1, - ) - return chain_pair_pred_err_mean, chain_pair_pred_err_min, unique_asym_ids - - -def reduce_chain_pair( - *, - chain_pair_met: np.ndarray, - num_chain_tokens: np.ndarray, - agg_over_col: bool, - agg_type: str, - weight_method: str, -) -> tuple[np.ndarray, np.ndarray]: - """Compute 1D summaries from a chain-pair summary. - - Args: - chain_pair_met: A [num_samples, num_chains, num_chains] aggregate matrix. - num_chain_tokens: A [num_chains] array of number of tokens for each chain. - Used for 'per_token' weighting. - agg_over_col: Whether to aggregate the PAE over rows (i.e. average error - when aligned to me) or columns (i.e. my average error when aligned to all - others.) - agg_type: The type of aggregation to use, 'mean' or 'min'. - weight_method: The method to use for weighting the PAE, 'per_token' or - 'per_chain'. - - Returns: - A tuple (ichain, xchain) where: - `ichain` is a [num_samples, num_chains] matrix where the - value assigned to each chain is an average of the full PAE matrix over all - its within-chain interactions, weighted by `contact_probs`. - `xchain` is a [num_samples, num_chains] matrix where the - value assigned to each chain is an average of the full PAE matrix over all - its cross-chain interactions, weighted by `contact_probs`. - """ - num_samples, num_chains, _ = chain_pair_met.shape - - ichain = chain_pair_met.diagonal(axis1=-2, axis2=-1) - - if weight_method == 'per_chain': - chain_weight = np.ones((num_chains,), dtype=float) - elif weight_method == 'per_token': - chain_weight = num_chain_tokens - else: - raise ValueError(f'Unknown weight method: {weight_method}') - - if agg_over_col: - agg_axis = -1 - else: - agg_axis = -2 - - if agg_type == 'mean': - weight = np.ones((num_samples, num_chains, num_chains), dtype=float) - weight -= np.eye(num_chains, dtype=float) - weight *= chain_weight[None] * chain_weight[:, None] - xchain = weighted_nanmean(chain_pair_met, mask=weight, axis=agg_axis) - elif agg_type == 'min': - is_self = np.eye(num_chains) - with warnings.catch_warnings(): - # Min over empty slice is ok and should return a NaN. - warnings.filterwarnings( - 'ignore', message='All-NaN slice encountered') - xchain = np.nanmin(chain_pair_met + 1e8 * is_self, axis=agg_axis) - else: - raise ValueError(f'Unknown aggregation method: {agg_type}') - - return ichain, xchain - - -def pae_metrics( - num_tokens: int, - asym_ids: np.ndarray, - full_pae: np.ndarray, - mask: np.ndarray, - contact_probs: np.ndarray, - tm_adjusted_pae: np.ndarray, -): - """PAE aggregate metrics.""" - - chain_pair_contact_weighted, _, unique_asym_ids = chain_pair_pae( - num_tokens=num_tokens, - asym_ids=asym_ids, - full_pae=full_pae, - mask=mask, - contact_probs=contact_probs, - ) - - ret = {} - ret['chain_pair_pae_mean'], ret['chain_pair_pae_min'], _ = chain_pair_pae( - num_tokens=num_tokens, - asym_ids=asym_ids, - full_pae=full_pae, - mask=mask, - ) - chain_pair_iptm = np.stack( - [ - chain_pairwise_predicted_tm_scores( - tm_adjusted_pae=sample_tm_adjusted_pae[:num_tokens], - asym_id=asym_ids[:num_tokens], - pair_mask=mask[:num_tokens, :num_tokens], - ) - for sample_tm_adjusted_pae in tm_adjusted_pae - ], - axis=0, - ) - - num_chain_tokens = np.array( - [sum(asym_ids == asym_id) for asym_id in unique_asym_ids] - ) - - def reduce_chain_pair_fn(chain_pair: np.ndarray): - def inner(agg_over_col): - ichain_pae, xchain_pae = reduce_chain_pair( - num_chain_tokens=num_chain_tokens, - chain_pair_met=chain_pair, - agg_over_col=agg_over_col, - agg_type='mean', - weight_method='per_chain', - ) - return ichain_pae, xchain_pae - - ichain, xchain_row_agg = inner(False) - _, xchain_col_agg = inner(True) - with warnings.catch_warnings(): - # Mean of empty slice is ok and should return a NaN. - warnings.filterwarnings( - action='ignore', message='Mean of empty slice') - xchain = np.nanmean( - np.stack([xchain_row_agg, xchain_col_agg], axis=0), axis=0 - ) - return ichain, xchain - - pae_ichain, pae_xchain = reduce_chain_pair_fn(chain_pair_contact_weighted) - iptm_ichain, iptm_xchain = reduce_chain_pair_fn(chain_pair_iptm) - - ret.update({ - 'chain_pair_iptm': chain_pair_iptm, - 'iptm_ichain': iptm_ichain, - 'iptm_xchain': iptm_xchain, - 'pae_ichain': pae_ichain, - 'pae_xchain': pae_xchain, - }) - - return ret - - -def get_iptm_xchain(chain_pair_iptm: np.ndarray) -> np.ndarray: - """Cross chain aggregate ipTM.""" - num_samples, num_chains, _ = chain_pair_iptm.shape - weight = np.ones((num_samples, num_chains, num_chains), dtype=float) - weight -= np.eye(num_chains, dtype=float) - xchain_row_agg = weighted_nanmean(chain_pair_iptm, mask=weight, axis=-2) - xchain_col_agg = weighted_nanmean(chain_pair_iptm, mask=weight, axis=-1) - with warnings.catch_warnings(): - # Mean of empty slice is ok and should return a NaN. - warnings.filterwarnings(action='ignore', message='Mean of empty slice') - iptm_xchain = np.nanmean( - np.stack([xchain_row_agg, xchain_col_agg], axis=0), axis=0 - ) - return iptm_xchain - - -def predicted_tm_score( - tm_adjusted_pae: np.ndarray, - pair_mask: np.ndarray, - asym_id: np.ndarray, - interface: bool = False, -) -> float: - """Computes predicted TM alignment or predicted interface TM alignment score. - - Args: - tm_adjusted_pae: [num_res, num_res] Relevant tensor for computing TMScore - values. - pair_mask: A [num_res, num_res] mask. The TM score will only aggregate over - masked-on entries. - asym_id: [num_res] asymmetric unit ID (the chain ID). Only needed for ipTM - calculation, i.e. when interface=True. - interface: If True, the interface predicted TM score is computed. If False, - the predicted TM score without any residue pair restrictions is computed. - - Returns: - score: pTM or ipTM score. - """ - num_tokens, _ = tm_adjusted_pae.shape - if tm_adjusted_pae.shape != (num_tokens, num_tokens): - raise ValueError( - f'Bad tm_adjusted_pae shape, expected ({num_tokens, num_tokens}), got ' - f'{tm_adjusted_pae.shape}.' - ) - - if pair_mask.shape != (num_tokens, num_tokens): - raise ValueError( - f'Bad pair_mask shape, expected ({num_tokens, num_tokens}), got ' - f'{pair_mask.shape}.' - ) - if pair_mask.dtype != bool: - raise TypeError( - f'Bad pair mask type, expected bool, got {pair_mask.dtype}') - if asym_id.shape[0] != num_tokens: - raise ValueError( - f'Bad asym_id shape, expected ({num_tokens},), got {asym_id.shape}.' - ) - - # Create pair mask. - if interface: - pair_mask = pair_mask * (asym_id[:, None] != asym_id[None, :]) - - # Ions and other ligands with colinear atoms have ill-defined frames. - if pair_mask.sum() == 0: - return np.nan - - normed_residue_mask = pair_mask / ( - 1e-8 + np.sum(pair_mask, axis=-1, keepdims=True) - ) - per_alignment = np.sum(tm_adjusted_pae * normed_residue_mask, axis=-1) - return per_alignment.max() - - -def chain_pairwise_predicted_tm_scores( - tm_adjusted_pae: np.ndarray, - pair_mask: np.ndarray, - asym_id: np.ndarray, -) -> np.ndarray: - """Compute predicted TM (pTM) between each pair of chains independently. - - Args: - tm_adjusted_pae: [num_res, num_res] Relevant tensor for computing TMScore - values. - pair_mask: A [num_res, num_res] mask specifying which frames are valid. - Invalid frames can be the result of chains with not enough atoms (e.g. - ions). - asym_id: [num_res] asymmetric unit ID (the chain ID). - - Returns: - A [num_chains, num_chains] matrix, where row i, column j indicates the - predicted TM-score for the interface between chain i and chain j. - """ - unique_chains = list(np.unique(asym_id)) - num_chains = len(unique_chains) - all_pairs_iptms = np.zeros((num_chains, num_chains)) - for i, chain_i in enumerate(unique_chains): - chain_i_mask = asym_id == chain_i - for j, chain_j in enumerate(unique_chains[i:]): - chain_j_mask = asym_id == chain_j - mask = chain_i_mask | chain_j_mask - (indices,) = np.where(mask) - is_interface = chain_i != chain_j - indices = np.ix_(indices, indices) - iptm = predicted_tm_score( - tm_adjusted_pae=tm_adjusted_pae[indices], - pair_mask=pair_mask[indices], - asym_id=asym_id[mask], - interface=is_interface, - ) - all_pairs_iptms[i, i + j] = iptm - all_pairs_iptms[i + j, i] = iptm - return all_pairs_iptms diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/data3.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/data3.py deleted file mode 100644 index 26c799387..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/data3.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Protein features that are computed from parsed mmCIF objects.""" - -from collections.abc import Mapping, MutableMapping -import datetime -from typing import TypeAlias - -from alphafold3.constants import residue_names -from alphafold3.cpp import msa_profile -from alphafold3.model import protein_data_processing -import numpy as np - - -FeatureDict: TypeAlias = Mapping[str, np.ndarray] -MutableFeatureDict: TypeAlias = MutableMapping[str, np.ndarray] - - -def fix_features(msa_features: MutableFeatureDict) -> MutableFeatureDict: - """Renames the deletion_matrix feature.""" - msa_features['deletion_matrix'] = msa_features.pop('deletion_matrix_int') - return msa_features - - -def get_profile_features( - msa: np.ndarray, deletion_matrix: np.ndarray -) -> FeatureDict: - """Returns the MSA profile and deletion_mean features.""" - num_restypes = residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP - profile = msa_profile.compute_msa_profile( - msa=msa, num_residue_types=num_restypes - ) - - return { - 'profile': profile.astype(np.float32), - 'deletion_mean': np.mean(deletion_matrix, axis=0), - } - - -def fix_template_features( - sequence: str, - template_features: FeatureDict, -) -> FeatureDict: - """Convert template features to AlphaFold 3 format. - - Args: - sequence: amino acid sequence of the protein. - template_features: Template features for the protein. - - Returns: - Updated template_features for the chain. - """ - num_res = len(sequence) - if not template_features['template_aatype'].shape[0]: - template_features = empty_template_features(num_res) - else: - template_release_timestamp = [ - _get_timestamp(x.decode('utf-8')) - for x in template_features['template_release_date'] - ] - - # Convert from atom37 to dense atom - dense_atom_indices = np.take( - protein_data_processing.PROTEIN_AATYPE_DENSE_ATOM_TO_ATOM37, - template_features['template_aatype'], - axis=0, - ) - - atom_mask = np.take_along_axis( - template_features['template_all_atom_masks'], dense_atom_indices, axis=2 - ) - atom_positions = np.take_along_axis( - template_features['template_all_atom_positions'], - dense_atom_indices[..., None], - axis=2, - ) - atom_positions *= atom_mask[..., None] - - template_features = { - 'template_aatype': template_features['template_aatype'], - 'template_atom_mask': atom_mask.astype(np.int32), - 'template_atom_positions': atom_positions.astype(np.float32), - 'template_domain_names': np.array( - template_features['template_domain_names'], dtype=object - ), - 'template_release_timestamp': np.array( - template_release_timestamp, dtype=np.float32 - ), - } - return template_features - - -def empty_template_features(num_res: int) -> FeatureDict: - """Creates a fully masked out template features to allow padding to work. - - Args: - num_res: The length of the target chain. - - Returns: - Empty template features for the chain. - """ - template_features = { - 'template_aatype': np.zeros(num_res, dtype=np.int32)[None, ...], - 'template_atom_mask': np.zeros( - (num_res, protein_data_processing.NUM_DENSE), dtype=np.int32 - )[None, ...], - 'template_atom_positions': np.zeros( - (num_res, protein_data_processing.NUM_DENSE, 3), dtype=np.float32 - )[None, ...], - 'template_domain_names': np.array([b''], dtype=object), - 'template_release_timestamp': np.array([0.0], dtype=np.float32), - } - return template_features - - -def _get_timestamp(date_str: str): - dt = datetime.datetime.fromisoformat(date_str) - dt = dt.replace(tzinfo=datetime.timezone.utc) - return dt.timestamp() diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/data_constants.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/data_constants.py deleted file mode 100644 index eabdcfda9..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/data_constants.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Constants shared across modules in the AlphaFold data pipeline.""" - -from alphafold3.constants import residue_names - -MSA_GAP_IDX = residue_names.PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP.index( - '-' -) - -# Feature groups. -NUM_SEQ_NUM_RES_MSA_FEATURES = ('msa', 'msa_mask', 'deletion_matrix') -NUM_SEQ_MSA_FEATURES = ('msa_species_identifiers',) -TEMPLATE_FEATURES = ( - 'template_aatype', - 'template_atom_positions', - 'template_atom_mask', -) -MSA_PAD_VALUES = {'msa': MSA_GAP_IDX, 'msa_mask': 1, 'deletion_matrix': 0} diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/atom_cross_attention.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/atom_cross_attention.py deleted file mode 100644 index aa6d4f15b..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/atom_cross_attention.py +++ /dev/null @@ -1,469 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""atom_cross_attention""" - -from dataclasses import dataclass -import mindspore as ms -from mindspore import nn, ops, Tensor - -from alphafold3.model import base_config -from alphafold3.model.atom_layout import atom_layout -from alphafold3.model.components import base_modules as bm -from alphafold3.model.components import utils -from alphafold3.model.diffusion import diffusion_transformer - -@dataclass -class AtomCrossAttEncoderConfig(base_config.BaseConfig): - per_token_channels: int = 768 - per_atom_channels: int = 128 - atom_transformer: diffusion_transformer.CrossAttTransformer.Config = ( - base_config.autocreate(num_intermediate_factor=2, num_blocks=3) - ) - per_atom_pair_channels: int = 16 - - -class _PerAtomConditioning(nn.Cell): - """ - A class to compute per-atom and pairwise conditioning information for structural data. - - Args: - config: Configuration object containing model parameters. - - Inputs: - - **batch** (dict) - A dictionary containing structural information: - - **ref_structure.positions** (Tensor) - Tensor of atomic positions. - - **ref_structure.mask** (Tensor) - Tensor of masks indicating valid atoms. - - **ref_structure.element** (Tensor) - Tensor of atomic elements. - - **ref_structure.charge** (Tensor) - Tensor of atomic charges. - - **ref_structure.atom_name_chars** (Tensor) - Tensor of atomic name characters. - - Outputs: - - **act** (Tensor) - Per-atom conditioning information. - - **pair_act** (Tensor) - Pairwise conditioning information. - """ - - def __init__(self, config): - super().__init__() - self.c = config - self.linear1 = nn.Dense(3, self.c.per_atom_channels, has_bias=False) - self.linear2 = nn.Dense(1, self.c.per_atom_channels, has_bias=False) - self.linear3 = nn.Dense(128, self.c.per_atom_channels, has_bias=False) - self.linear4 = nn.Dense(1, self.c.per_atom_channels, has_bias=False) - self.linear5 = nn.Dense(256, self.c.per_atom_channels, has_bias=False) - self.linear_row_act = nn.Dense( - self.c.per_atom_channels, self.c.per_atom_pair_channels, has_bias=False) - self.linear_col_act = nn.Dense( - self.c.per_atom_channels, self.c.per_atom_pair_channels, has_bias=False) - self.linear_pair_act1 = nn.Dense( - 3, self.c.per_atom_pair_channels, has_bias=False) - self.linear_pair_act2 = nn.Dense( - 1, self.c.per_atom_pair_channels, has_bias=False) - - def construct(self, batch): - """construct for _PerAtomConditioning - Compute per-atom single conditioning - Shape (num_tokens, num_dense, channels)""" - act = self.linear1(batch.ref_structure.positions) - act += self.linear2(batch.ref_structure.mask[:, :, None]) - # Element is encoded as atomic number if the periodic table, so - # 128 should be fine. - act += self.linear3( - ops.one_hot(batch.ref_structure.element, 128, - Tensor(1.0, ms.float32), Tensor(0.0, ms.float32)) - .astype(act.dtype)) - act += self.linear4(ops.arcsinh(batch.ref_structure.charge) - [:, :, None]) - # Characters are encoded as ASCII code minus 32, so we need 64 classes, - # to encode all standard ASCII characters between 32 and 96. - atom_name_chars_1hot = ops.one_hot(batch.ref_structure.atom_name_chars, 64, - Tensor(1.0, ms.float32), Tensor(0.0, ms.float32)).astype(act.dtype) - num_token, num_dense, _ = act.shape - act += self.linear5(atom_name_chars_1hot.reshape(num_token, num_dense, -1)) - act *= batch.ref_structure.mask[:, :, None] - - # Compute pair conditioning - # shape (num_tokens, num_dense, num_dense, channels) - # Embed single features - row_act = self.linear_row_act(ops.relu(act)) - col_act = self.linear_col_act(ops.relu(act)) - pair_act = row_act[:, :, None, :] + col_act[:, None, :, :] - - # Embed pairwise offsets - pair_act += self.linear_pair_act1(batch.ref_structure.positions[:, :, None, :] - - batch.ref_structure.positions[:, None, :, :]) - # Embed pairwise inverse squared distances - sq_dists = ops.sum(ops.square(batch.ref_structure.positions[:, :, None, :] - - batch.ref_structure.positions[:, None, :, :]), dim=-1) - pair_act += self.linear_pair_act2(1.0 / (1 + sq_dists[:, :, :, None])) - return act, pair_act - -@dataclass -class AtomCrossAttEncoderOutput: - """Output class for AtomCrossAttEncoder.""" - def __init__( - self, - token_act, - skip_connection, - queries_mask, - queries_single_cond, - keys_mask, - keys_single_cond, - pair_cond, - ): - self.token_act = token_act - self.skip_connection = skip_connection - self.queries_mask = queries_mask - self.queries_single_cond = queries_single_cond - self.keys_mask = keys_mask - self.keys_single_cond = keys_single_cond - self.pair_cond = pair_cond - - -class AtomCrossAttEncoder(nn.Cell): - """Cross-attention on flat atom subsets and mapping to per-token features. - - Args: - config: Configuration object containing model parameters. - global_config: Global configuration object with initialization settings. - name (str): Name of the module. - cond_channels (int): Number of conditioning channels. Default: ``384``. - with_cond (bool): Whether to include conditioning layers. Default: ``True``. - - Inputs: - - **token_atoms_act** (ms.Tensor): Tensor representing token atom activations. - - **trunk_single_cond** (ms.Tensor): Tensor representing single token conditioning. - - **trunk_pair_cond** (ms.Tensor): Tensor representing pair token conditioning. - - **batch** (feat_batch.Batch) : Batch of input data. - - Outputs: - - **token_act** (ms.Tensor): Activations for tokens after processing. - - **skip_connection** (ms.Tensor): Skip connection tensor for token queries. - - **queries_mask** (ms.Tensor): Mask for token queries. - - **queries_single_cond** (ms.Tensor): Single conditioning for token queries. - - **keys_mask** (ms.Tensor): Mask for token keys. - - **keys_single_cond** (ms.Tensor): Single conditioning for token keys. - - **pair_cond** (ms.Tensor): Pair conditioning tensor. - """ - - def __init__(self, config, global_config, name, cond_channels=384, with_cond=True, dtype=ms.float32): - super().__init__() - self.c = config - self.with_cond = with_cond - self.dtype = dtype - self._per_atom_conditioning = _PerAtomConditioning(config) - if self.with_cond: - self._embed_trunk_single_cond = nn.Dense(cond_channels, self.c.per_atom_channels, - weight_init=global_config.final_init, has_bias=False, dtype=dtype) - self._lnorm_trunk_single_cond = bm.LayerNorm((cond_channels,), - create_beta=False, gamma_init="ones", dtype=dtype) - self._atom_positions_to_features = nn.Dense(3, self.c.per_atom_channels, has_bias=False, dtype=dtype) - self._embed_trunk_pair_cond = nn.Dense(self.c.per_atom_channels, self.c.per_atom_pair_channels, - weight_init=global_config.final_init, has_bias=False, dtype=dtype) - self._lnorm_trunk_pair_cond = bm.LayerNorm((self.c.per_atom_channels,), create_beta=False, - gamma_init="ones", dtype=dtype) - - self._single_to_pair_cond_row = nn.Dense( - self.c.per_atom_channels, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) - self._single_to_pair_cond_col = nn.Dense( - self.c.per_atom_channels, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) - - self._embed_pair_offsets = nn.Dense( - 3, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) - self._embed_pair_distances = nn.Dense( - 1, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) - self._embed_pair_offsets_valid = nn.Dense( - 1, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) - - self._pair_mlp_1 = nn.Dense( - self.c.per_atom_pair_channels, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) - self._pair_mlp_2 = nn.Dense( - self.c.per_atom_pair_channels, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) - self._pair_mlp_3 = nn.Dense(self.c.per_atom_pair_channels, self.c.per_atom_pair_channels, - weight_init=global_config.final_init, has_bias=False, dtype=dtype) - self.relu = nn.ReLU() - self._project_atom_features_for_aggr = nn.Dense( - self.c.per_atom_channels, self.c.per_token_channels, has_bias=False, dtype=dtype) - - self._atom_transformer_encoder = diffusion_transformer.CrossAttTransformer( - self.c.atom_transformer, global_config, in_shape=[ - self.c.per_atom_channels, self.c.per_atom_pair_channels], dtype=dtype - ) - - def construct( - self, - token_atoms_act, - trunk_single_cond, - trunk_pair_cond, - batch, - ): - """Compute single conditioning from atom meta data and convert to queries - layout""" - token_atoms_single_cond, _ = self._per_atom_conditioning( - batch) - token_atoms_mask = batch.predicted_structure_info.atom_mask - queries_single_cond = atom_layout.convert_ms( - batch.atom_cross_att.token_atoms_to_queries, - token_atoms_single_cond, - layout_axes=(-3, -2), - ) - queries_mask = atom_layout.convert_ms( - batch.atom_cross_att.token_atoms_to_queries, - token_atoms_mask, - layout_axes=(-2, -1), - ) - - # If provided, broadcast single conditioning from trunk to all queries - if trunk_single_cond is not None: - trunk_single_cond = self._embed_trunk_single_cond( - self._lnorm_trunk_single_cond( - trunk_single_cond) - ) - queries_single_cond += atom_layout.convert_ms( - batch.atom_cross_att.tokens_to_queries, - trunk_single_cond, - layout_axes=(-2,), - ) - - if token_atoms_act is None: - # if no token_atoms_act is given (e.g. begin of evoformer), we use the - # static conditioning only - queries_act = queries_single_cond - else: - # Convert token_atoms_act to queries layout and map to per_atom_channels - queries_act = atom_layout.convert_ms( - batch.atom_cross_att.token_atoms_to_queries, - token_atoms_act, - layout_axes=(-3, -2), - ) - queries_act = self._atom_positions_to_features( - queries_act) - queries_act *= queries_mask[..., None] - queries_act += queries_single_cond - - # Gather the keys from the queries. - keys_single_cond = atom_layout.convert_ms( - batch.atom_cross_att.queries_to_keys, queries_single_cond, layout_axes=( - -3, -2), - ) - keys_mask = atom_layout.convert_ms( - batch.atom_cross_att.queries_to_keys, queries_mask, layout_axes=( - -2, -1) - ) - - # Embed single features into the pair conditioning. - row_act = self._single_to_pair_cond_row( - self.relu(queries_single_cond)) - pair_cond_keys_input = atom_layout.convert_ms( - batch.atom_cross_att.queries_to_keys, queries_single_cond, layout_axes=( - -3, -2), - ) - col_act = self._single_to_pair_cond_col( - self.relu(pair_cond_keys_input)) - pair_act = row_act[:, :, None, :] + col_act[:, None, :, :] - - if trunk_pair_cond is not None: - # If provided, broadcast the pair conditioning for the trunk (evoformer - # pairs) to the atom pair activations. This should boost ligands, but also - # help for cross attention within proteins, because we always have atoms - # from multiple residues in a subset. - # Map trunk pair conditioning to per_atom_pair_channels - trunk_pair_cond = self._embed_trunk_pair_cond( - self._lnorm_trunk_pair_cond( - trunk_pair_cond) - ) - - # Create the GatherInfo into a flattened trunk_pair_cond from the - # queries and keys gather infos. - num_tokens = trunk_pair_cond.shape[0] - tokens_to_queries = batch.atom_cross_att.tokens_to_queries - tokens_to_keys = batch.atom_cross_att.tokens_to_keys - - # Gather the conditioning and add it to the atom-pair activations. - gather_idxs = Tensor(num_tokens * tokens_to_queries.gather_idxs[:, :, None] + - tokens_to_keys.gather_idxs[:, None, :]) - gather_mask = ops.logical_and(tokens_to_queries.gather_mask[:, :, None], - tokens_to_keys.gather_mask[:, None, :]) - input_shape = Tensor((num_tokens, num_tokens)) - trunk_pair_to_atom_pair = atom_layout.GatherInfo(gather_idxs=gather_idxs, - gather_mask=gather_mask, - input_shape=input_shape) - pair_act += atom_layout.convert_ms( - trunk_pair_to_atom_pair, trunk_pair_cond, layout_axes=(-3, -2) - ) - - # Embed pairwise offsets - queries_ref_pos = atom_layout.convert_ms( - batch.atom_cross_att.token_atoms_to_queries, - batch.ref_structure.positions, - layout_axes=(-3, -2), - ) - queries_ref_space_uid = atom_layout.convert_ms( - batch.atom_cross_att.token_atoms_to_queries, - batch.ref_structure.ref_space_uid, - layout_axes=(-2, -1), - ) - keys_ref_pos = atom_layout.convert_ms( - batch.atom_cross_att.queries_to_keys, - queries_ref_pos, - layout_axes=(-3, -2), - ) - keys_ref_space_uid = atom_layout.convert_ms( - batch.atom_cross_att.queries_to_keys, - batch.ref_structure.ref_space_uid, - layout_axes=(-2, -1), - ) - - offsets_valid = ( - queries_ref_space_uid[:, :, None] == keys_ref_space_uid[:, None, :] - ) - offsets = queries_ref_pos[:, :, None, :] - keys_ref_pos[:, None, :, :] - pair_act += (self._embed_pair_offsets(offsets) - * offsets_valid[:, :, :, None]) - - # Embed pairwise inverse squared distances - sq_dists = ops.sum(ops.square(offsets), dim=-1) - pair_act += ( - self._embed_pair_distances(1.0 / (1 + sq_dists[:, :, :, None])) - * offsets_valid[:, :, :, None] - ) - - # Embed offsets valid mask - pair_act += self._embed_pair_offsets_valid( - offsets_valid[:, :, :, None].astype(ms.float32)) - - # Run a small MLP on the pair acitvations - pair_act2 = self._pair_mlp_1(self.relu(pair_act)) - pair_act2 = self._pair_mlp_2(self.relu(pair_act2)) - pair_act += self._pair_mlp_3(self.relu(pair_act2)) - - # Run the atom cross attention transformer. - queries_act = self._atom_transformer_encoder( - queries_act=queries_act, - queries_mask=queries_mask, - queries_to_keys=batch.atom_cross_att.queries_to_keys, - keys_mask=keys_mask, - queries_single_cond=queries_single_cond, - keys_single_cond=keys_single_cond, - pair_cond=pair_act, - ) - queries_act *= queries_mask[..., None] - skip_connection = queries_act - - # convert back to token-atom layout and aggregate to tokens - queries_act = self._project_atom_features_for_aggr(queries_act) - token_atoms_act = atom_layout.convert_ms( - batch.atom_cross_att.queries_to_token_atoms, - queries_act, - layout_axes=(-3, -2), - ) - token_act = utils.mask_mean( - token_atoms_mask[..., None], self.relu(token_atoms_act), axis=-2 - ) - - return AtomCrossAttEncoderOutput( - token_act=token_act, - skip_connection=skip_connection, - queries_mask=queries_mask, - queries_single_cond=queries_single_cond, - keys_mask=keys_mask, - keys_single_cond=keys_single_cond, - pair_cond=pair_act, - ) - -@dataclass -class AtomCrossAttDecoderConfig(base_config.BaseConfig): - per_token_channels: int = 768 - per_atom_channels: int = 128 - per_atom_pair_channels: int = 16 - atom_transformer: diffusion_transformer.CrossAttTransformer.Config = ( - base_config.autocreate(num_intermediate_factor=2, num_blocks=3) - ) - - -class AtomCrossAttDecoder(nn.Cell): - """Mapping to per-atom features and self-attention on subsets. - - Args: - config: Configuration object containing model parameters. - global_config: Global configuration object with additional parameters. - name (str): Name of the decoder. Default: ``None``. - - Inputs: - - **token_act** (Tensor) - Tensor representing token activations. - - **enc** (AtomCrossAttEncoderOutput) - Output from the encoder containing necessary features and masks. - - **batch** (feat_batch.Batch) - Batch containing atom cross attention features. - - Outputs: - - **position_update** (Tensor) - Tensor representing the updated positions after processing. - """ - - def __init__(self, config, global_config, name, dtype=ms.float32): - super().__init__() - self.c = config - self._project_token_features_for_broadcast = nn.Dense( - self.c.per_token_channels, self.c.per_atom_channels, has_bias=False, dtype=dtype) - self._atom_features_layer_norm = bm.LayerNorm( - (self.c.per_atom_channels,), create_beta=False, gamma_init="ones", dtype=dtype) - self._atom_features_to_position_update = nn.Dense( - self.c.per_atom_channels, 3, weight_init=global_config.final_init, has_bias=False, dtype=dtype) - self._atom_transformer_decoder = diffusion_transformer.CrossAttTransformer( - self.c.atom_transformer, global_config, in_shape=[ - self.c.per_atom_channels, self.c.per_atom_pair_channels], dtype=dtype - ) - - def construct( - self, - token_act, - enc, - batch, - ): - """map per-token act down to per_atom channels""" - token_act = self._project_token_features_for_broadcast(token_act) - # Broadcast to token-atoms layout and convert to queries layout. - num_token, max_atoms_per_token = ( - batch.atom_cross_att.queries_to_token_atoms.shape - ) - token_atom_act = ops.broadcast_to( - token_act[:, None, :], - (num_token, max_atoms_per_token, self.c.per_atom_channels), - ) - queries_act = atom_layout.convert_ms( - batch.atom_cross_att.token_atoms_to_queries, - token_atom_act, - layout_axes=(-3, -2), - ) - queries_act += enc.skip_connection - queries_act *= enc.queries_mask[..., None] - - # Run the atom cross attention transformer. - queries_act = self._atom_transformer_decoder( - queries_act=queries_act, - queries_mask=enc.queries_mask, - queries_to_keys=batch.atom_cross_att.queries_to_keys, - keys_mask=enc.keys_mask, - queries_single_cond=enc.queries_single_cond, - keys_single_cond=enc.keys_single_cond, - pair_cond=enc.pair_cond, - ) - - queries_act *= enc.queries_mask[..., None] - queries_position_update = self._atom_features_to_position_update( - self._atom_features_layer_norm(queries_act) - ) - position_update = atom_layout.convert_ms( - batch.atom_cross_att.queries_to_token_atoms, - queries_position_update, - layout_axes=(-3, -2), - ) - return position_update diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/confidence_head.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/confidence_head.py deleted file mode 100644 index e30ec0adb..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/confidence_head.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Confidence Head.""" -from dataclasses import dataclass -import mindspore as ms -from mindspore import nn, ops -from alphafold3.model import base_config -from alphafold3.model.atom_layout import atom_layout -from alphafold3.model.components import base_modules as bm -from alphafold3.model.diffusion import modules -from alphafold3.model.diffusion import template_modules - - -def _safe_norm(x, keepdims, axis, eps=1e-8): - """Compute safe norm.""" - return ops.sqrt(eps + ops.sum(ops.square(x), dim=axis, keepdims=keepdims)) - - -class ConfidenceHead(nn.Cell): - """Head to predict the distance errors in a prediction. - - Args: - config (ConfidenceHead.Config): Configuration for the ConfidenceHead module. - global_config (base_config.BaseConfig): Global configuration for the model. - pair_shape (tuple): Shape of the pair features. - single_shape (tuple): Shape of the single features. - atom_shape (tuple): Shape of the atom features. - feat_in_channel (int): Number of input channels for feature projections. - out_channel (int): Number of output channels for feature projections. - - Inputs: - - **dense_atom_positions** (Tensor): [N_res, N_atom, 3] array of atom positions. - - **embeddings** (dict): Dictionary containing pair, single, and target features. - - **seq_mask** (Tensor): Sequence mask indicating valid residues. - - **token_atoms_to_pseudo_beta** (Tensor): Pseudo beta information for atom tokens. - - **asym_id** (Tensor): Asym ID token features. - - Outputs: - - **predicted_lddt** (Tensor): Predicted LDDT scores for each residue. - - **predicted_experimentally_resolved** (Tensor): Predicted experimental resolution scores. - - **full_pde** (Tensor): Full predicted distance errors. - - **average_pde** (Tensor): Average predicted distance errors. - - **pae_outputs** (dict): Additional outputs from PAE (Predicted Alignment Error) calculations. - """ - @dataclass - class PAEConfig(base_config.BaseConfig): - max_error_bin: float = 31.0 - num_bins: int = 64 - - @dataclass - class Config(base_config.BaseConfig): - """Configuration for ConfidenceHead.""" - - pairformer: modules.PairFormerIteration.Config = base_config.autocreate( - single_attention=base_config.autocreate(), - single_transition=base_config.autocreate(), - num_layer=4, - ) - max_error_bin: float = 31.0 - num_plddt_bins: int = 50 - num_bins: int = 64 - no_embedding_prob: float = 0.2 - pae: 'ConfidenceHead.PAEConfig' = base_config.autocreate() - dgram_features: template_modules.DistogramFeaturesConfig = ( - base_config.autocreate() - ) - - def __init__(self, config, global_config, pair_shape, single_shape, atom_shape, - feat_in_channel, out_channel, dtype=ms.float32): - super().__init__() - self.dtype = dtype - self.config = config - self.global_config = global_config - self.left_target_feat_project = nn.Dense( - feat_in_channel, out_channel, has_bias=False, dtype=dtype) - self.right_target_feat_project = nn.Dense( - feat_in_channel, out_channel, has_bias=False, dtype=dtype) - self.distogram_feat_project = nn.Dense( - template_modules.DistogramFeaturesConfig.num_bins, out_channel, has_bias=False, dtype=dtype) - self.pairformer_block = ms.nn.CellList( - [ - modules.PairFormerIteration( - self.config.pairformer, global_config, pair_shape, single_shape, with_single=True, dtype=dtype - ) - for _ in range(self.config.pairformer.num_layer) - ] - ) - self.left_half_distance_logits = nn.Dense( - pair_shape[-1], self.config.num_bins, has_bias=False, dtype=ms.float32) - self.logits_ln = bm.LayerNorm(pair_shape, dtype=ms.float32) - self.pae_logits = nn.Dense( - pair_shape[-1], self.config.pae.num_bins, has_bias=False, dtype=ms.float32) - self.pae_logits_ln = bm.LayerNorm(pair_shape, dtype=ms.float32) - self.plddt_logits = bm.CustomDense( - single_shape[-1], (atom_shape[-2], self.config.num_plddt_bins), ndim=2, dtype=ms.float32) - self.plddt_logits_ln = bm.LayerNorm(single_shape, dtype=ms.float32) - self.experimentally_resolved_logits = bm.CustomDense( - single_shape[-1], (atom_shape[-2], 2), ndim=2, dtype=ms.float32) - self.experimentally_resolved_ln = bm.LayerNorm(single_shape, dtype=ms.float32) - - def _embed_features(self, dense_atom_positions, token_atoms_to_pseude_beta, - pair_mask, target_feat): - """Embed features for confidence head.""" - out = self.left_target_feat_project(target_feat) - out2 = self.right_target_feat_project(target_feat)[:, None] - out = out + out2 - positions = atom_layout.convert_ms( - token_atoms_to_pseude_beta, - dense_atom_positions, - layout_axes=(-3, -2), - ) - dgram = template_modules.dgram_from_positions( - positions, self.config.dgram_features, dtype=ms.float32 - ) - dgram *= pair_mask[..., None] - out += self.distogram_feat_project(dgram) - return out - - def construct(self, dense_atom_positions, embeddings, seq_mask, - token_atoms_to_pseudo_beta, asym_id): - """confidence head""" - seq_mask_cast = seq_mask - pair_mask = seq_mask_cast[:, None] * seq_mask_cast[None, :] - pair_act = embeddings['pair'] - single_act = embeddings['single'] - target_feat = embeddings['target_feat'] - pair_act += self._embed_features( - dense_atom_positions, - token_atoms_to_pseudo_beta, - pair_mask, - target_feat, - ) - - for i in range(self.config.pairformer.num_layer): - pair_act, single_act = self.pairformer_block[i]( - pair_act, pair_mask, single_act, seq_mask) - pair_act = pair_act.astype(ms.float32) - - # Produce logits to predict a distogram of pairwise distance errors - # between the input prediction and the ground truth. - # Shape (num_res, num_res, num_bins) - left_distance_logits = self.left_half_distance_logits( - self.logits_ln(pair_act)) - right_distance_logits = left_distance_logits - distance_logits = left_distance_logits + ops.swapaxes( # Symmetrize. - right_distance_logits, -2, -3 - ) - # Shape (num_bins,) - distance_breaks = ops.linspace( - 0.0, self.config.max_error_bin, self.config.num_bins - 1 - ) - - step = distance_breaks[1] - distance_breaks[0] - - # Add half-step to get the center - bin_centers = distance_breaks + step / 2 - # Add a catch-all bin at the end. - bin_centers = ops.concat( - [bin_centers, bin_centers[-1:] + step], axis=0 - ) - - distance_probs = ops.softmax(distance_logits, axis=-1) - - pred_distance_error = ( - ops.sum(distance_probs * bin_centers, dim=-1) * pair_mask - ) - average_pred_distance_error = ops.sum( - pred_distance_error, dim=[-2, -1] - ) / ops.sum(pair_mask, dim=[-2, -1]) - - # Predicted aligned error - pae_outputs = {} - # Shape (num_res, num_res, num_bins) - pae_logits = self.pae_logits(self.pae_logits_ln(pair_act)) - # Shape (num_bins,) - pae_breaks = ops.linspace( - 0.0, self.config.pae.max_error_bin, self.config.pae.num_bins - 1 - ) - step = pae_breaks[1] - pae_breaks[0] - # Add half-step to get the center - bin_centers = pae_breaks + step / 2 - # Add a catch-all bin at the end. - bin_centers = ops.concat( - [bin_centers, bin_centers[-1:] + step], axis=0 - ) - pae_probs = ops.softmax(pae_logits, axis=-1) - - seq_mask_bool = seq_mask.astype(bool) - pair_mask_bool = seq_mask_bool[:, None] * seq_mask_bool[None, :] - pae = ops.sum(pae_probs * bin_centers, dim=-1) * pair_mask_bool - pae_outputs.update({ - 'full_pae': pae, - }) - - # The pTM is computed outside of bfloat16 context. - tmscore_adjusted_pae_global, tmscore_adjusted_pae_interface = ( - self._get_tmscore_adjusted_pae( - asym_id=asym_id, - seq_mask=seq_mask, - pair_mask=pair_mask_bool, - bin_centers=bin_centers, - pae_probs=pae_probs, - ) - ) - pae_outputs.update({ - 'tmscore_adjusted_pae_global': tmscore_adjusted_pae_global, - 'tmscore_adjusted_pae_interface': tmscore_adjusted_pae_interface, - }) - - # pLDDT - # Shape (num_res, num_atom, num_bins) - plddt_logits = self.plddt_logits(self.plddt_logits_ln(single_act)) - - bin_width = 1.0 / self.config.num_plddt_bins - bin_centers = ops.arange(0.5 * bin_width, 1.0, bin_width) - predicted_lddt = ops.sum( - ops.softmax(plddt_logits, axis=-1) * bin_centers, dim=-1 - ) - predicted_lddt = predicted_lddt * 100.0 - - # Experimentally resolved - # Shape (num_res, num_atom, 2) - experimentally_resolved_logits = self.experimentally_resolved_logits( - self.experimentally_resolved_ln(single_act) - ) - - predicted_experimentally_resolved = ops.softmax( - experimentally_resolved_logits, axis=-1 - )[..., 1] - - return { - 'predicted_lddt': predicted_lddt, - 'predicted_experimentally_resolved': predicted_experimentally_resolved, - 'full_pde': pred_distance_error, - 'average_pde': average_pred_distance_error, - **pae_outputs, - } - - def _get_tmscore_adjusted_pae( - self, asym_id, seq_mask, pair_mask, bin_centers, pae_probs, - ): - """get tmscore adjusted pae""" - def get_tmscore_adjusted_pae(num_interface_tokens, bin_centers, pae_probs): - # Clip to avoid negative/undefined d0. - clipped_num_res = ops.maximum(num_interface_tokens, 19) - - # Compute d_0(num_res) as defined by TM-score, eqn. (5) in - # http://zhanglab.ccmb.med.umich.edu/papers/2004_3.pdf - # Yang & Skolnick "Scoring function for automated - # assessment of protein structure template quality" 2004. - d0 = 1.24 * (clipped_num_res - 15) ** (1.0 / 3) - 1.8 - - # Make compatible with [num_tokens, num_tokens, num_bins] - d0 = d0[:, :, None] - bin_centers = bin_centers[None, None, :] - - # TM-Score term for every bin. - tm_per_bin = 1.0 / (1 + ops.square(bin_centers) / ops.square(d0)) - # E_distances tm(distance). - predicted_tm_term = ops.sum(pae_probs * tm_per_bin, dim=-1) - return predicted_tm_term - - # Interface version - x = asym_id[None, :] == asym_id[:, None] - num_chain_tokens = ops.sum(x * pair_mask, dim=-1) - num_interface_tokens = num_chain_tokens[None, - :] + num_chain_tokens[:, None] - # Don't double-count within a single chain - num_interface_tokens -= x * (num_interface_tokens // 2) - num_interface_tokens = num_interface_tokens * pair_mask - - num_global_tokens = ops.full( - size=pair_mask.shape, fill_value=seq_mask.sum() - ).astype(ms.int32) - - global_apae = get_tmscore_adjusted_pae( - num_global_tokens, bin_centers, pae_probs - ) - interface_apae = get_tmscore_adjusted_pae( - num_interface_tokens, bin_centers, pae_probs - ) - return global_apae, interface_apae diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_head.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_head.py deleted file mode 100644 index 1b524ee25..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_head.py +++ /dev/null @@ -1,336 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Diffusion Head.""" - -from dataclasses import dataclass -from collections.abc import Callable -import math -import numpy as np -import mindspore as ms -from mindspore import mint, nn -from alphafold3.constants import residue_names -from alphafold3.model import base_config -from alphafold3.model.components import base_modules as bm -from alphafold3.model.components import utils -from alphafold3.model.diffusion import atom_cross_attention -from alphafold3.model.diffusion import diffusion_transformer -from alphafold3.model.diffusion import featurization - - -# Carefully measured by averaging multimer training set. -SIGMA_DATA = 16.0 -WEIGHT = ms.Tensor(np.load("./alphafold3/model/diffusion/random/weight.npy"), dtype=ms.float32) -BIAS = ms.Tensor(np.load("./alphafold3/model/diffusion/random/bias.npy"), dtype=ms.float32) - -def fourier_embeddings(x): - """Compute Fourier embeddings.""" - return mint.cos(2 * math.pi * (x[..., None] * WEIGHT + BIAS)) - -def random_rotation(key): - """Generate random rotation matrix.""" - # Create a random rotation (Gram-Schmidt orthogonalization of two - # random normal vectors) - np.random.seed(key) - v0, v1 = ms.Tensor(np.random.normal(0, 1, (2, 3)), dtype=ms.float32) - e0 = v0 / mint.maximum(1e-10, mint.norm(v0)) - v1 = v1 - e0 * mint.matmul(v1, e0) - e1 = v1 / mint.maximum(1e-10, mint.norm(v1)) - e2 = mint.cross(e0, e1) - return mint.stack([e0, e1, e2]) - -def random_augmentation(rng_key, positions, mask): - """Apply random rigid augmentation. - Args: - rng_key: random key - positions: atom positions of shape (, 3) - mask: per-atom mask of shape (,) - Returns: - Transformed positions with the same shape as input positions. - """ - center = utils.mask_mean( - mask.unsqueeze(-1), positions, axis=(-2, -3), keepdims=True, eps=1e-6 - ).astype(ms.float32) - rot = random_rotation(rng_key) - np.random.seed(rng_key) - translation = ms.Tensor(np.random.normal(0, 1, (3,)), dtype=ms.float32) - - augmented_positions = ( - mint.einsum( - '...i,ij->...j', - (positions - center).astype(ms.float32), - rot, - ) - + translation - ) - return augmented_positions * mask[..., None] - -def noise_schedule(t, smin=0.0004, smax=160.0, p=7): - """Compute noise schedule.""" - return ( - SIGMA_DATA - * (smax ** (1 / p) + t * (smin ** (1 / p) - smax ** (1 / p))) ** p - ) - -@dataclass -class ConditioningConfig(base_config.BaseConfig): - pair_channel: int - seq_channel: int - prob: float - -@dataclass -class SampleConfig(base_config.BaseConfig): - steps: int - gamma_0: float = 0.8 - gamma_min: float = 1.0 - noise_scale: float = 1.003 - step_scale: float = 1.5 - num_samples: int = 1 - -class DiffusionHead(nn.Cell): - """Denoising Diffusion Head. - - Args: - config (Config): Configuration object containing parameters for the diffusion head. - global_config (GlobalConfig): Global configuration object containing shared parameters. - in_shape (tuple): Input shape for the module. - max_relative_chain (int): Maximum number of relative chains for positional encoding. Default: ``2``. - max_relative_idx (int): Maximum relative index for positional encoding. Default: ``32``. - - Inputs: - - **positions_noisy** (Tensor) - Noisy atomic positions tensor. - - **noise_level** (Tensor) - Tensor representing the noise level. - - **batch** (Batch) - Batch of input data containing token features and structure information. - - **embeddings** (dict) - Dictionary of embeddings for single and pair features. - - **use_conditioning** (bool) - Flag to enable or disable conditioning. - - Outputs: - - **position_update** (Tensor) - Refined atomic positions tensor. - """ - - class Config( - atom_cross_attention.AtomCrossAttEncoderConfig, - atom_cross_attention.AtomCrossAttDecoderConfig, - ): - """Configuration for DiffusionHead.""" - eval_batch_size: int = 5 - eval_batch_dim_shard_size: int = 5 - conditioning: ConditioningConfig = base_config.autocreate( - prob=0.8, pair_channel=128, seq_channel=384 - ) - eval: SampleConfig = base_config.autocreate( - num_samples=5, - steps=200, - ) - transformer: diffusion_transformer.Transformer.Config = ( - base_config.autocreate() - ) - - def __init__(self, config, global_config, in_shape, max_relative_chain=2, max_relative_idx=32, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.dtype = dtype - in_channel = in_shape[-1] - self.max_relative_chain = max_relative_chain - self.max_relative_idx = max_relative_idx - - # _conditioning modules - in_channel_pair = in_channel + 4 * self.max_relative_idx + 4 + 2 * self.max_relative_chain + 2 + 1 - self.pair_cond_initial_norm = bm.LayerNorm( - in_shape[:-1] + (in_channel_pair,), - create_beta=False, gamma_init="ones", - name='pair_cond_initial_norm', dtype=dtype) - self.pair_cond_initial_projection = nn.Dense(in_channel_pair, self.config.conditioning.pair_channel, - has_bias=False, dtype=ms.float32) - self.transition_block1 = diffusion_transformer.TransitionBlock( - in_channel, 2, with_single_cond=False, - dtype=dtype - ) - self.transition_block2 = diffusion_transformer.TransitionBlock( - in_channel, 2, with_single_cond=False, - dtype=dtype - ) - in_channel_single = self.config.conditioning.seq_channel * 2 \ - + residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP * 2 + 1 - self.single_cond_initial_norm = bm.LayerNorm( - in_shape[:-1] + (in_channel_single,), - create_beta=False, gamma_init="ones", - name='single_cond_initial_norm', dtype=dtype) - self.single_cond_initial_projection = nn.Dense(in_channel_single, self.config.conditioning.seq_channel, - has_bias=False, dtype=dtype) - self.num_noise_embedding = 256 - self.layer_norm_noise = bm.LayerNorm( - in_shape[:-1]+(self.num_noise_embedding,), - create_beta=False, gamma_init="ones", - name='noise_embedding_initial_norm', dtype=dtype) - self.linear_noise = nn.Dense(self.num_noise_embedding, self.config.conditioning.seq_channel, - has_bias=False, dtype=dtype) - self.single_transition1 = diffusion_transformer.TransitionBlock( - self.config.conditioning.seq_channel, 2, - ndim=2, with_single_cond=False, - dtype=dtype - ) - self.single_transition2 = diffusion_transformer.TransitionBlock( - self.config.conditioning.seq_channel, 2, - ndim=2, with_single_cond=False, - dtype=dtype - ) - - # modules - self.layer_norm_act = bm.LayerNorm( - (in_channel,)+(self.config.conditioning.seq_channel,), - create_beta=False, gamma_init="ones", - name='single_cond_embedding_norm', dtype=dtype) - self.linear_act = nn.Dense(self.config.conditioning.seq_channel, - self.config.per_token_channels, has_bias=False, dtype=dtype) - self.layer_norm_out = bm.LayerNorm( - in_shape[:-1]+(self.config.per_token_channels,), - create_beta=False, gamma_init="ones", - name='output_norm', dtype=dtype) - self.atom_cross_att_encoder = atom_cross_attention.AtomCrossAttEncoder( - self.config, self.global_config, "", dtype=dtype - ) - self.transformer = diffusion_transformer.Transformer( - self.config.transformer, self.global_config, in_shape[:-1] + (self.config.conditioning.seq_channel * 2,), - in_shape, using_pair_act=True, dtype=dtype - ) - self.atom_cross_att_decoder = atom_cross_attention.AtomCrossAttDecoder( - self.config, self.global_config, '', dtype=dtype - ) - - def _conditioning(self, batch, embeddings, noise_level, use_conditioning): - """conditioning""" - single_embedding = use_conditioning * embeddings['single'] - pair_embedding = use_conditioning * embeddings['pair'] - rel_features = featurization.create_relative_encoding( - batch.token_features, max_relative_idx=self.max_relative_idx, max_relative_chain=self.max_relative_chain - ).astype(pair_embedding.dtype) - features_2d = mint.concat([pair_embedding, rel_features], dim=-1) - pair_cond = self.pair_cond_initial_projection( - self.pair_cond_initial_norm(features_2d) - ) - pair_cond += self.transition_block1(pair_cond) - pair_cond += self.transition_block2(pair_cond) - - target_feat = embeddings['target_feat'] - features_1d = mint.concat([single_embedding, target_feat], dim=-1) - single_cond = self.single_cond_initial_norm(features_1d) - single_cond = self.single_cond_initial_projection(single_cond) - noise_embedding = fourier_embeddings( - (1 / 4) * mint.log(noise_level / SIGMA_DATA) - ) - single_cond += self.linear_noise(self.layer_norm_noise(noise_embedding)) - single_cond += self.single_transition1(single_cond) - single_cond += self.single_transition2(single_cond) - - return single_cond, pair_cond - - def construct(self, positions_noisy, noise_level, batch, embeddings, use_conditioning): - """diffusion head""" - trunk_single_cond, trunk_pair_cond = self._conditioning( - batch=batch, - embeddings=embeddings, - noise_level=noise_level, - use_conditioning=use_conditioning, - ) - - # Extract features - sequence_mask = batch.token_features.mask - atom_mask = batch.predicted_structure_info.atom_mask - # Position features - act = positions_noisy * atom_mask[..., None] - act = act / mint.sqrt(noise_level**2 + SIGMA_DATA**2) - enc = self.atom_cross_att_encoder(act, embeddings["single"], trunk_pair_cond, batch) - - act = enc.token_act - act += self.linear_act(self.layer_norm_act(trunk_single_cond)) - act = self.transformer(act, trunk_single_cond, sequence_mask, trunk_pair_cond) - act = self.layer_norm_out(act) - position_update = self.atom_cross_att_decoder(act, enc, batch) - skip_scaling = SIGMA_DATA**2 / (noise_level**2 + SIGMA_DATA**2) - out_scaling = ( - noise_level * SIGMA_DATA / mint.sqrt(noise_level**2 + SIGMA_DATA**2) - ) - return ( - skip_scaling * positions_noisy + out_scaling * position_update - ) * atom_mask[..., None] - -def sample(denoising_step, batch, key, config, init_positions=None): - """Sample using denoiser on batch. - - Args: - denoising_step: the denoising function. - batch: the batch - key: random key - config: config for the sampling process (e.g. number of denoising steps, - etc.) - - Returns: - a dict - { - 'atom_positions': ms.Tensor # shape (, 3) - 'mask': ms.Tensor # shape (,) - } - where the are - (num_samples, num_tokens, max_atoms_per_token) - """ - - mask = batch.predicted_structure_info.atom_mask - # get weight and bias from Jax, this two values cannot be randomly generated - - def apply_denoising_step(carry, noise_level): - key, positions, noise_level_prev = carry - - positions = random_augmentation( - rng_key=key, positions=positions, mask=mask, - ) - gamma = config.gamma_0 * (noise_level > config.gamma_min) - t_hat = noise_level_prev * (1 + gamma) - - noise_scale = config.noise_scale * mint.sqrt(t_hat**2 - noise_level_prev**2) - np.random.seed(key) - noise = noise_scale * ms.Tensor(np.random.normal(0, 1, positions.shape), dtype=ms.float32) - positions_noisy = positions + noise - - positions_denoised = denoising_step(positions_noisy, t_hat) - grad = (positions_noisy - positions_denoised) / t_hat - - d_t = noise_level - t_hat - positions_out = positions_noisy + config.step_scale * d_t * grad - - return (key, positions_out, noise_level), positions_out - - num_samples = config.num_samples - - noise_levels = noise_schedule(mint.linspace(0, 1, config.steps + 1)) - - noise_key, key = key, key + 1 - np.random.seed(noise_key) - if init_positions is None: - init_positions = ms.Tensor(np.random.normal(0, 1, (num_samples,) + mask.shape + (3,)), dtype=ms.float32) - init_positions *= noise_levels[0] - init = (ms.Tensor([key + i for i in range(num_samples)]).reshape((-1, 1)), - init_positions, - mint.tile(noise_levels[None, 0], (num_samples,)).reshape((-1, 1))) - count = 0 - for noise_level in noise_levels[1:]: - for i in range(num_samples): - temp, _ = apply_denoising_step((count * 10 + i, init[1][i], init[2][i]), noise_level) - init[0][i], init[1][i], init[2][i] = temp - count += 1 - _, positions_out, _ = init - - final_dense_atom_mask = mint.tile(mask[None], (num_samples, 1, 1)) - - return {'atom_positions': positions_out, 'mask': final_dense_atom_mask} diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_transformer.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_transformer.py deleted file mode 100644 index 015f4e734..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/diffusion_transformer.py +++ /dev/null @@ -1,520 +0,0 @@ -# Copyright 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Diffusion transformer model.""" - -from dataclasses import dataclass -from typing import Optional -from alphafold3.model import base_config -from alphafold3.utils.gated_linear_unit import gated_linear_unit -from alphafold3.model.atom_layout import atom_layout -from alphafold3.model.components import base_modules as bm - -from mindspore import mint -import mindspore as ms -from mindspore import nn, ops -from mindscience.e3nn.utils import Ncon - - -class AdaptiveLayernorm(nn.Cell): - """ - If single condition is None, this layer is the same as layernorm. - If single condition is given, the layer is modified from Scalable Diffusion Models with Transformers - https://arxiv.org/abs/2212.09748 - - Args: - num_channels (int): Number of channels in the input tensor. - single_channel (int, optional): Number of channels in the single condition tensor. Required if - `with_single_cond` is True. Default: ``None``. - ndim (int, optional): Number of dimensions for the dense layers. Default: ``3``. - with_single_cond (bool, optional): Whether to include the single condition adaptation. Default: ``True``. - - Inputs: - - **x** (Tensor) - Input tensor to be normalized. - - **single_cond** (Tensor, optional) - Optional single condition tensor used to adapt the normalization - parameters. Required if `with_single_cond` is True. - - Outputs: - - **output** (Tensor) - The normalized output tensor. - """ - - def __init__(self, num_channels, single_channel=None, ndim=3, with_single_cond=True, dtype=ms.float32): - super().__init__() - self.with_single_cond = with_single_cond - if self.with_single_cond: - self.layernorm = bm.LayerNorm([num_channels], name='layer_norm', - create_gamma=False, create_beta=False, - gamma_init='ones', beta_init='zeros', dtype=ms.float32) - self.single_cond_layer_norm = bm.LayerNorm([single_channel], name='single_cond_layer_norm', - create_beta=False, gamma_init='ones', beta_init='zeros', - dtype=ms.float32) - self.single_cond_scale = bm.CustomDense(single_channel, num_channels, weight_init='zeros', - use_bias=True, bias_init='ones', ndim=ndim, dtype=dtype) - self.single_cond_bias = bm.CustomDense( - single_channel, num_channels, weight_init='zeros', ndim=ndim, dtype=dtype) - else: - self.layernorm = bm.LayerNorm([num_channels], dtype=ms.float32) - - def construct(self, x, single_cond=None): - if not self.with_single_cond: - x = self.layernorm(x) - else: - x = self.layernorm(x) - single_cond = self.single_cond_layer_norm(single_cond) - single_scale = self.single_cond_scale(single_cond) - single_bias = self.single_cond_bias(single_cond) - x = mint.add(mint.mul(mint.sigmoid(single_scale), x), single_bias) - return x - - -class AdaptiveZeroInit(nn.Cell): - """ - An adaptive initialization layer that combines two conditional linear transformations. - - Args: - global_config: Configuration object containing initialization settings. - in_channels (int): Number of input channels. - out_channels (int): Number of output channels. - single_channels (int, optional): Number of single conditional channels. Default: ``None``. - ndim (int, optional): Number of dimensions for the dense layer input. Default: ``3``. - with_single_cond (bool, optional): Whether to use single conditional transformation. Default: ``True``. - - Inputs: - - **x** (Tensor) - Input tensor to the layer. - - **single_cond** (Tensor, optional) - Single conditional tensor. Required if `with_single_cond` is True. - - Outputs: - - **output** (Tensor) - Output tensor after applying the adaptive initialization. - """ - - def __init__(self, in_channels, out_channels, single_channels=None, ndim=3, with_single_cond=True, - dtype=ms.float32): - super().__init__() - self.with_single_cond = with_single_cond - self.cond_linear1 = bm.CustomDense( - in_channels, out_channels, weight_init='zeros', ndim=ndim, dtype=dtype) - if self.with_single_cond: - if single_channels is None: - single_channels = in_channels - self.cond_linear2 = bm.CustomDense(single_channels, out_channels, weight_init='zeros', - use_bias=True, bias_init='zeros', ndim=ndim, dtype=dtype) - self.cond_linear2.bias = ms.Parameter( - self.cond_linear2.bias * (-2)) - - def construct(self, x, single_cond=None): - if not self.with_single_cond: - output = self.cond_linear1(x) - else: - output = self.cond_linear1(x) - cond = self.cond_linear2(single_cond) - output = mint.mul(mint.sigmoid(cond), output) - return output - - -class TransitionBlock(nn.Cell): - """ - A neural network layer that combines adaptive layer normalization, a gated linear unit (GLU), and - adaptive zero initialization to process input data with optional conditional inputs. - - Args: - global_config: Configuration object containing initialization settings. - in_channels (int): Number of input channels. - num_intermediate_factor (int): Factor to determine the number of intermediate channels. - single_channels (int, optional): Number of single conditional channels. Default: ``None``. - ndim (int, optional): Number of dimensions for input tensor. Default: ``3``. - with_single_cond (bool, optional): Whether to use single conditional processing. Default: ``True``. - use_glu_kernel (bool, optional): Whether to use GLU. Default: ``True``. - name (str, optional): Name of the layer. Default: ``''``. - - Inputs: - - **x** (Tensor) - Input tensor to the layer. - - **single_cond** (Tensor, optional) - Single conditional tensor. Required if `with_single_cond` is True. - - Outputs: - - **output** (Tensor) - Output tensor after processing through the TransitionBlock. - """ - - def __init__(self, in_channels, num_intermediate_factor, single_channels=None, ndim=3, - with_single_cond=True, use_glu_kernel=True, dtype=ms.float32): - super().__init__() - self.num_intermediate = num_intermediate_factor * in_channels - if single_channels is None: - single_channels = in_channels - self.adaptive_layernorm = AdaptiveLayernorm( - in_channels, single_channels, ndim=ndim, with_single_cond=with_single_cond, dtype=dtype) - self.use_glu_kernel = use_glu_kernel - if self.use_glu_kernel: - self.weights = bm.custom_initializer( - 'relu', [in_channels, self.num_intermediate * 2], dtype=dtype) - self.weights = ms.Parameter(ms.Tensor(self.weights).reshape( - in_channels, 2, self.num_intermediate)) - else: - self.linear = bm.CustomDense( - in_channels, self.num_intermediate * 2, weight_init='zeros', ndim=3, dtype=dtype) - self.adaptive_zero_init = AdaptiveZeroInit( - self.num_intermediate, in_channels, - single_channels, ndim=ndim, - with_single_cond=with_single_cond, dtype=dtype) - - def construct(self, x, single_cond=None): - """Construct the TransitionBlock.""" - x = self.adaptive_layernorm(x, single_cond) - if self.use_glu_kernel: - c = gated_linear_unit( - x=x, weight=self.weights.astype(x.dtype), - activation=mint.nn.functional.silu - ).astype(x.dtype) - else: - x = self.linear(x) - x0, x1 = ops.split(x, int(x.shape[-1]/2), axis=-1) - c = ops.silu(x0) * x1 - output = self.adaptive_zero_init(c, single_cond) - return output - - -@dataclass -class SelfAttentionConfig(base_config.BaseConfig): - num_head: int = 16 - key_dim: Optional[int] = None - value_dim: Optional[int] = None - - -class SelfAttention(nn.Cell): - """ - A self-attention mechanism implementation with adaptive layer normalization and adaptive zero initialization. - - This class implements the self-attention mechanism commonly used in transformer models. It includes adaptive layer - normalization for input processing and adaptive zero initialization for the final output. The mechanism computes - attention scores using query, key, and value transformations, applies masking, and optionally incorporates - pair-wise logits. - - Args: - config: Configuration object containing parameters such as key dimension, value dimension, and number of - attention heads. - global_config: Global configuration object for additional settings. - num_channels (int): Number of channels in the input tensor. - in_shape (tuple): Shape of the input tensor. - ndim (int, optional): Number of dimensions for the dense layers. Default: ``3``. - with_single_cond (bool, optional): Whether to include single condition adaptation. Default: ``True``. - - Inputs: - - **x** (Tensor) - Input tensor to the self-attention layer. - - **mask** (Tensor) - Attention mask to apply. - - **single_cond** (Tensor, optional) - Single condition tensor for adaptation. - - **pair_logits** (Tensor, optional) - Additional logits to incorporate into attention scores. - - Outputs: - - **output** (Tensor) - The output tensor after self-attention and adaptive zero initialization. - - Notes: - - The class uses adaptive layer normalization and adaptive zero initialization for processing inputs and - outputs. - - The attention mechanism supports optional single condition adaptation and pair-wise logits. - """ - - def __init__(self, config, global_config, num_channels, ndim=3, with_single_cond=True, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.adaptive_layernorm = AdaptiveLayernorm(num_channels, int( - num_channels//2), ndim=ndim, with_single_cond=with_single_cond, dtype=dtype) - key_dim = self.config.key_dim if self.config.key_dim is not None else num_channels - value_dim = self.config.value_dim if self.config.value_dim is not None else num_channels - num_head = self.config.num_head - key_dim = key_dim // num_head - self.key_dim = key_dim - value_dim = value_dim // num_head - qk_shape = (num_head, key_dim) - v_shape = (num_head, value_dim) - self.q_linear = bm.CustomDense( - num_channels, qk_shape, use_bias=True, dtype=dtype) - self.k_linear = bm.CustomDense( - num_channels, qk_shape, use_bias=False, dtype=dtype) - self.v_linear = bm.CustomDense( - num_channels, v_shape, use_bias=False, dtype=dtype) - self.linear = bm.CustomDense( - num_channels, num_head * value_dim, weight_init='zeros', dtype=dtype) - self.adaptive_zero_init = AdaptiveZeroInit(num_channels, num_channels, int( - num_channels//2), 2, with_single_cond=with_single_cond, dtype=dtype) - self.ncon1 = Ncon([[-2, -1, 1], [-3, -1, 1]]) - self.ncon2 = Ncon([[-2, -1, 2], [2, -2, -3]]) - - def construct(self, x, mask, single_cond, pair_logits): - """Construct the SelfAttention layer.""" - bias = (1e9 * (mask - 1.0))[..., None, None, :].astype(x.dtype) - x = self.adaptive_layernorm(x, single_cond) - q = self.q_linear(x) - k = self.k_linear(x) - logits = mint.einsum('...qhc,...khc->...hqk', q * - self.key_dim ** (-0.5), k) + bias - if pair_logits is not None: - logits += pair_logits - weights = mint.softmax(logits, dim=-1) - weights = weights.astype(q.dtype) - v = self.v_linear(x) - weighted_avg = mint.einsum('...hqk,...khc->...qhc', weights, v) - weighted_avg = weighted_avg.reshape(weighted_avg.shape[:-2] + (-1,)) - gate_logits = self.linear(x) - weighted_avg *= mint.sigmoid(gate_logits) - output = self.adaptive_zero_init(weighted_avg, single_cond) - return output - - -class Transformer(nn.Cell): - """Transformer model.""" - @dataclass - class Config(base_config.BaseConfig): - attention: SelfAttentionConfig = base_config.autocreate() - num_blocks: int = 24 - block_remat: bool = False - super_block_size: int = 4 - num_intermediate_factor: int = 2 - - def __init__(self, config, global_config, in_shape, pair_shape, - using_pair_act=False, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.using_pair_act = using_pair_act - self.act = [] - if using_pair_act: - self.pair_layernorm = bm.LayerNorm( - pair_shape, create_beta=False, dtype=ms.float32) - else: - self.pair_layernorm = None - self.num_super_blocks = self.config.num_blocks // self.config.super_block_size - self.super_blocks = ms.nn.CellList( - [ - SuperBlock( - config, global_config, self.config.num_blocks, - using_pair_act, in_shape, pair_shape, dtype=dtype - ) - for _ in range(self.num_super_blocks) - ] - ) - - @ms.jit - def construct(self, act, single_cond, mask, pair_cond=None): - if pair_cond is None: - pair_act = None - else: - pair_act = self.pair_layernorm(pair_cond) - for i in range(self.num_super_blocks): - act = self.super_blocks[i](act, mask, single_cond, pair_act) - return act - - -class Block(nn.Cell): - """Single transformer block.""" - - def __init__(self, config, global_config, in_shape, dtype=ms.float32): - super().__init__() - self.self_attention = SelfAttention( - config.attention, global_config, in_shape[-1], ndim=2, dtype=dtype) - self.transition_block = TransitionBlock(in_shape[-1], - config.num_intermediate_factor, - int(in_shape[-1]//2), ndim=2, dtype=dtype) - - def construct(self, act, mask, single_cond, pair_logits): - act += self.self_attention(act, mask, single_cond, pair_logits) - act += self.transition_block(act, single_cond) - return act - - -class SuperBlock(nn.Cell): - """Super block of transformer layers.""" - - def __init__(self, config, global_config, num_blocks, using_pair_act, in_shape, pair_shape=None, - dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.num_blocks = num_blocks - self.using_pair_act = using_pair_act - self.blocks = ms.nn.CellList( - [ - Block( - config, global_config, in_shape, dtype=dtype - ) - for _ in range(self.config.super_block_size) - ] - ) - if self.using_pair_act: - self.pair_linear = bm.CustomDense( - pair_shape[-1], (self.config.super_block_size, self.config.attention.num_head), ndim=3, dtype=dtype) - else: - self.pair_linear = None - - def construct(self, act, mask, single_cond, pair_act): - if pair_act is None: - pair_logits = None - else: - pair_logits = self.pair_linear(pair_act).transpose([2, 3, 0, 1]) - for j in range(self.config.super_block_size): - act = self.blocks[j](act, mask, single_cond, pair_logits[j]) - return act - - -@dataclass -class CrossAttentionConfig(base_config.BaseConfig): - num_head: int = 4 - key_dim: int = 128 - value_dim: int = 128 - - -class CrossAttention(nn.Cell): - """ - A CrossAttention class implementing multi-head cross-attention mechanism for processing sequential data. - - Args: - config (Config): Configuration object containing attention settings. - global_config (GlobalConfig): Global configuration object. - in_channel (int): Input dimension for the attention mechanism. - - Inputs: - - **x_q** (Tensor) - Query tensor. - - **x_k** (Tensor) - Key tensor. - - **mask_q** (Tensor) - Query mask tensor. - - **mask_k** (Tensor) - Key mask tensor. - - **pair_logits** (Tensor, optional) - Optional pair logits tensor. Default: ``None``. - - **single_cond_q** (Tensor) - Single condition tensor for queries. - - **single_cond_k** (Tensor) - Single condition tensor for keys. - - Outputs: - - **output** (Tensor) - Output tensor after cross-attention processing. - """ - - def __init__(self, config, global_config, in_channel, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.adaptive_layernorm_q = AdaptiveLayernorm( - in_channel, in_channel, dtype=dtype) - self.adaptive_layernorm_k = AdaptiveLayernorm( - in_channel, in_channel, dtype=dtype) - self.key_dim = config.key_dim // config.num_head - self.value_dim = config.value_dim // config.num_head - self.linear_q = bm.CustomDense( - in_channel, (self.config.num_head, self.key_dim), use_bias=True, ndim=3, dtype=dtype) - self.linear_k = bm.CustomDense( - in_channel, (self.config.num_head, self.key_dim), use_bias=False, ndim=3, dtype=dtype) - self.linear_v = bm.CustomDense( - in_channel, (self.config.num_head, self.value_dim), use_bias=False, ndim=3, dtype=dtype) - self.ncon1 = Ncon([[-1, -3, -2, 1], [-1, -4, -2, 1]]) - self.ncon2 = Ncon([[-1, -3, -2, 1], [-1, 1, -3, -4]]) - self.gating_query = bm.CustomDense( - in_channel, self.config.num_head * self.value_dim, use_bias=False, - weight_init='zeros', bias_init='ones', ndim=3, dtype=dtype) - self.adaptive_zero_init = AdaptiveZeroInit( - in_channel, in_channel, in_channel, dtype=dtype) - - def construct(self, x_q, x_k, mask_q, mask_k, pair_logits, single_cond_q, single_cond_k): - """Multihead self-attention.""" - bias = ( - 1e9 - * (mask_q - 1.0)[..., None, :, None] - * (mask_k - 1.0)[..., None, None, :] - ) - x_q = self.adaptive_layernorm_q(x_q, single_cond_q) - x_k = self.adaptive_layernorm_k(x_k, single_cond_k) - q = self.linear_q(x_q) - k = self.linear_k(x_k) - logits = mint.einsum('...qhc,...khc->...hqk', q * - self.key_dim ** (-0.5), k) + bias - if pair_logits is not None: - logits += pair_logits - weights = ops.softmax(logits, axis=-1) - v = self.linear_v(x_k) - weighted_avg = mint.einsum('...hqk,...khc->...qhc', weights, v) - weighted_avg = ops.reshape( - weighted_avg, weighted_avg.shape[:-2] + (-1,)) - - gate_logits = self.gating_query(x_q) - weighted_avg *= ops.sigmoid(gate_logits) - - output = self.adaptive_zero_init(weighted_avg, single_cond_q,) - return output - - -class CrossAttTransformer(nn.Cell): - """ - A CrossAttTransformer class implementing a transformer that applies cross attention between two sets of subsets. - - Args: - config (Config): Configuration object containing settings for the transformer. - global_config (GlobalConfig): Global configuration object. - in_shape (tuple): Input shape for the transformer. - - Inputs: - - **queries_act** (Tensor) - Query activations tensor. - - **queries_mask** (Tensor) - Mask tensor for queries. - - **queries_to_keys** (Tensor) - Tensor mapping queries to keys. - - **keys_mask** (Tensor) - Mask tensor for keys. - - **queries_single_cond** (Tensor) - Single condition tensor for queries. - - **keys_single_cond** (Tensor) - Single condition tensor for keys. - - **pair_cond** (Tensor) - Pair condition tensor. - - Outputs: - - **queries_act** (Tensor) - Processed query activations tensor after cross attention. - """ - @dataclass - class Config(base_config.BaseConfig): - num_intermediate_factor: int - num_blocks: int - attention: CrossAttentionConfig = base_config.autocreate() - - def __init__(self, config, global_config, in_shape, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.pair_input_layer_norm = bm.LayerNorm( - in_shape, create_beta=False, dtype=ms.float32) - self.pair_logits_projection = bm.CustomDense( - in_shape[-1], (self.config.num_blocks, self.config.attention.num_head), ndim=4, dtype=dtype) - self.block = ms.nn.CellList( - [ - CrossAttTransformerBlock( - config, global_config, in_shape[-2], dtype=dtype - ) - for _ in range(self.config.num_blocks) - ] - ) - - def construct(self, queries_act, queries_mask, queries_to_keys, - keys_mask, queries_single_cond, keys_single_cond, - pair_cond): - pair_act = self.pair_input_layer_norm(pair_cond) - pair_logits = self.pair_logits_projection(pair_act) - pair_logits = ops.transpose(pair_logits, (3, 0, 4, 1, 2)) - for i in range(self.config.num_blocks): - queries_act = self.block[i](queries_act, queries_mask, queries_to_keys, keys_mask, pair_logits[i], - queries_single_cond, keys_single_cond) - return queries_act - - -class CrossAttTransformerBlock(nn.Cell): - """Block for CrossAttTransformer.""" - - def __init__(self, config, global_config, in_channel, dtype=ms.float32): - super().__init__() - self.cross_attention = CrossAttention( - config.attention, global_config, in_channel, dtype=dtype) - self.transition = TransitionBlock( - in_channel, config.num_intermediate_factor, dtype=dtype) - - def construct(self, queries_act, queries_mask, queries_to_keys, keys_mask, pair_logits, - queries_single_cond, keys_single_cond): - keys_act = atom_layout.convert_ms( - queries_to_keys, queries_act, layout_axes=(-3, -2) - ) - queries_act += self.cross_attention(queries_act, keys_act, queries_mask, keys_mask, - pair_logits, queries_single_cond, keys_single_cond) - queries_act += self.transition(queries_act, queries_single_cond) - return queries_act diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/distogram_head.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/distogram_head.py deleted file mode 100644 index 126d208d0..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/distogram_head.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Distogram head.""" - -from typing import Final -from dataclasses import dataclass -import mindspore as ms -from mindspore import nn, ops -from alphafold3.model import base_config -from alphafold3.model.components import base_modules as bm -from mindscience.e3nn.utils import Ncon - - -_CONTACT_THRESHOLD: Final[float] = 8.0 -_CONTACT_EPSILON: Final[float] = 1e-3 - - -class DistogramHead(nn.Cell): - """ - A DistogramHead class that computes a distogram from pair embeddings, predicting distances between residues. - - Args: - config (Config): Configuration object containing parameters for the distogram head. - global_config (GlobalConfig): Global configuration object. - in_channel (int): Number of input channels for the linear layer. - - Inputs: - - **batch** (dict) - Dictionary containing batch features. - - **embeddings** (dict) - Dictionary containing pair embeddings. - - Outputs: - - **bin_edges** (Tensor) - Tensor of bin edges for distance predictions. - - **contact_probs** (Tensor) - Tensor of contact probabilities. - - Notes: - - The distogram head computes distance probabilities using a linear transformation and softmax. - - The Ncon class is used for tensor contraction operations. - """ - @dataclass - class Config(base_config.BaseConfig): - first_break: float = 2.3125 - last_break: float = 21.6875 - num_bins: int = 64 - - def __init__( - self, config, global_config, in_channel, dtype=ms.float32 - ): - """Initialize DistogramHead.""" - super().__init__() - self.config = config - self.global_config = global_config - self.linear = bm.CustomDense( - in_channel, self.config.num_bins, weight_init=self.global_config.final_init, ndim=3, dtype=dtype) - self.ncon = Ncon([[-1, -2, 1], [1]]) - - def construct(self, batch, embeddings): - """Compute distogram predictions.""" - pair_act = embeddings["pair"] - seq_mask = batch.token_features.mask.astype(ms.bool_) - pair_mask = seq_mask[:, None] * seq_mask[None, :] - left_half_logits = self.linear(pair_act) - right_half_logits = left_half_logits - logits = left_half_logits + ops.swapaxes(right_half_logits, -2, -3) - probs = ops.softmax(logits, axis=-1) - breaks = ops.linspace( - self.config.first_break, - self.config.last_break, - self.config.num_bins - 1, - ) - bin_tops = ops.concat( - (breaks, (breaks[-1] + breaks[-1] - breaks[-2]).reshape(-1))) - threshold = _CONTACT_THRESHOLD + _CONTACT_EPSILON - is_contact_bin = 1.0 * (bin_tops <= threshold) - contact_probs = self.ncon([probs.astype(ms.float32), is_contact_bin.astype(ms.float32)]) - contact_probs = pair_mask * contact_probs - return { - 'bin_edges': breaks, - 'contact_probs': contact_probs, - } diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/featurization.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/featurization.py deleted file mode 100644 index d8ce9a9ef..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/featurization.py +++ /dev/null @@ -1,218 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Model-side of the input features processing.""" -import math -import numpy as np -import mindspore as ms -from mindspore import ops -from alphafold3.constants import residue_names -from alphafold3.model.components import utils - - -def _grid_keys(key, shape): - """Generate a grid of rng keys that is consistent with different padding. - - Generate random keys such that the keys will be identical, regardless of - how much padding is added to any dimension. - - Args: - key: A PRNG key. - shape: The shape of the output array of keys that will be generated. - - Returns: - An array of shape `shape` consisting of random keys. - """ - if not shape: - return key - - def partial_bitwise_xor(other): - return ops.bitwise_xor(key, other) - - def _partial_grid_keys(key): - return _grid_keys(key, shape[1:]) - new_keys = ms.vmap(partial_bitwise_xor)( - ops.arange(shape[0]) - ) - return ms.vmap(_partial_grid_keys)(new_keys) - - -def _padding_consistent_rng(f): - """Decorator to make RNG consistent with padding.""" - def inner(key, shape, **kwargs): - keys = _grid_keys(key, shape) - out = keys.flatten() - count = 0 - for key in keys.flatten(): - out[count] = f((), key) - count += 1 - return out.reshape(keys.shape) - return inner - - -def gumbel_sample(shape): - """Sample from Gumbel distribution.""" - uniform_samples = ms.Tensor(np.random.uniform(0.0, 1.0, shape)) - gumbel_samples = -ops.log(-ops.log(uniform_samples)) - return gumbel_samples - - -def gumbel_argsort_sample_idx(key, logits): - """Sample indices using Gumbel-Max trick.""" - gumbel = _padding_consistent_rng(gumbel_sample) - z = gumbel(key, logits.shape) - perm = ops.argsort(logits + z, axis=-1, descending=False) - return perm[::-1] - - -def create_msa_feat(msa): - """Create MSA features.""" - msa_1hot = ops.one_hot(msa.rows.astype( - ms.int64), residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP + 1) - deletion_matrix = msa.deletion_matrix - has_deletion = ops.clip(deletion_matrix, 0.0, 1.0)[..., None] - deletion_value = (ops.arctan(deletion_matrix / 3.0) - * (2.0 / math.pi))[..., None] - msa_feat = [msa_1hot.astype(deletion_value.dtype), has_deletion, deletion_value] - return ops.concat(msa_feat, axis=-1) - - -def truncate_msa_batch(msa, num_msa): - """Truncate MSA batch.""" - indices = ops.arange(num_msa) - return msa.index_msa_rows(indices) - - -def create_target_feat(batch, append_per_atom_features, dtype=ms.float32): - """Create target features.""" - token_features = batch.token_features - target_features = [] - target_features.append(ops.one_hot( - token_features.aatype.astype(ms.int64), - residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP).astype(dtype)) - target_features.append(batch.msa.profile) - target_features.append(batch.msa.deletion_mean[..., None]) - - if append_per_atom_features: - ref_mask = batch.ref_structure.mask - element_feat = ops.one_hot(batch.ref_structure.element, 128) - element_feat = utils.mask_mean( - mask=ref_mask[..., None], value=element_feat, axis=-2, eps=1e-6) - target_features.append(element_feat) - pos_feat = batch.ref_structure.positions - pos_feat = pos_feat.reshape([pos_feat.shape[0], -1]) - target_features.append(pos_feat) - target_features.append(ref_mask) - return ops.concat(target_features, axis=-1) - - -def create_relative_encoding( - seq_features, - max_relative_idx, - max_relative_chain, -): - """Add relative position encodings.""" - rel_feats = [] - token_index = seq_features.token_index - residue_index = seq_features.residue_index - asym_id = seq_features.asym_id - entity_id = seq_features.entity_id - sym_id = seq_features.sym_id - - left_asym_id = asym_id[:, None] - right_asym_id = asym_id[None, :] - - left_residue_index = residue_index[:, None] - right_residue_index = residue_index[None, :] - - left_token_index = token_index[:, None] - right_token_index = token_index[None, :] - - left_entity_id = entity_id[:, None] - right_entity_id = entity_id[None, :] - left_sym_id = sym_id[:, None] - right_sym_id = sym_id[None, :] - - # Embed relative positions using a one-hot embedding of distance along chain - offset = left_residue_index - right_residue_index - clipped_offset = ops.clip( - offset + max_relative_idx, min=0, max=2 * max_relative_idx - ) - asym_id_same = left_asym_id == right_asym_id - final_offset = ops.where( - asym_id_same, - clipped_offset, - (2 * max_relative_idx + 1) * ops.ones_like(clipped_offset), - ) - rel_pos = ops.one_hot(final_offset.astype( - ms.int64), 2 * max_relative_idx + 2) - rel_feats.append(rel_pos) - - # Embed relative token index as a one-hot embedding of distance along residue - token_offset = left_token_index - right_token_index - clipped_token_offset = ops.clip( - token_offset + max_relative_idx, min=0, max=2 * max_relative_idx - ) - residue_same = ops.logical_and((left_asym_id == right_asym_id), ( - left_residue_index == right_residue_index - )) - final_token_offset = ops.where( - residue_same, - clipped_token_offset, - (2 * max_relative_idx + 1) * ops.ones_like(clipped_token_offset), - ) - rel_token = ops.one_hot(final_token_offset.astype( - ms.int64), 2 * max_relative_idx + 2) - rel_feats.append(rel_token) - - # Embed same entity ID - entity_id_same = left_entity_id == right_entity_id - rel_feats.append(entity_id_same.astype(rel_pos.dtype)[..., None]) - - # Embed relative chain ID inside each symmetry class - rel_sym_id = left_sym_id - right_sym_id - - max_rel_chain = max_relative_chain - - clipped_rel_chain = ops.clip( - rel_sym_id + max_rel_chain, min=0, max=2 * max_rel_chain - ) - - final_rel_chain = ops.where( - entity_id_same, - clipped_rel_chain, - (2 * max_rel_chain + 1) * ops.ones_like(clipped_rel_chain), - ) - rel_chain = ops.one_hot(final_rel_chain.astype( - ms.int64), 2 * max_relative_chain + 2) - - rel_feats.append(rel_chain) - - return ops.concat(rel_feats, axis=-1) - - -def shuffle_msa(key, msa): - """Shuffle MSA randomly, return batch with shuffled MSA. - - Args: - key: rng key for random number generation. - msa: MSA object to sample msa from. - - Returns: - Protein with sampled msa. - """ - key, sample_key = key + 1, key - # Sample uniformly among sequences with at least one non-masked position. - logits = (ops.clip(ops.sum(msa.mask, dim=-1), 0.0, 1.0) - 1.0) * 1e6 - index_order = gumbel_argsort_sample_idx(sample_key, logits) - return msa.index_msa_rows(index_order), sample_key diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/load_ckpt.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/load_ckpt.py deleted file mode 100644 index f61994f71..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/load_ckpt.py +++ /dev/null @@ -1,611 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""load ckpt""" - -import pathlib -import mindspore as ms -from mindspore import ops -from alphafold3.model.params import get_model_af3_params - - -def np_slice(arr, i, j, dtype=ms.bfloat16): - """Slice a numpy array and convert to MindSpore Parameter.""" - if i is not None and j is not None: - return ms.Parameter(ms.Tensor(arr[i, j], dtype)) - if i is not None and j is None: - return ms.Parameter(ms.Tensor(arr[i], dtype)) - if i is None and j is not None: - return ms.Parameter(ms.Tensor(arr[j], dtype)) - return ms.Parameter(ms.Tensor(arr, dtype)) - - - -def load_adaptive_layernorm(adaptive_layernorm, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load adaptive layer normalization parameters.""" - if not ckpt.get(f'{path}single_cond_layer_norm'): - adaptive_layernorm.layernorm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}layer_norm']['scale'], i, j, dtype=ms.float32)) - adaptive_layernorm.layernorm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}layer_norm']['offset'], i, j, dtype=ms.float32)) - else: - adaptive_layernorm.single_cond_layer_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}single_cond_layer_norm']['scale'], i, j, dtype=ms.float32)) - adaptive_layernorm.single_cond_scale.weight.set_data( - np_slice(ckpt[f'{path}single_cond_scale']['weights'], i, j, dtype=dtype)) - adaptive_layernorm.single_cond_scale.bias.set_data( - np_slice(ckpt[f'{path}single_cond_scale']['bias'], i, j, dtype=dtype)) - adaptive_layernorm.single_cond_bias.weight.set_data( - np_slice(ckpt[f'{path}single_cond_bias']['weights'], i, j, dtype=dtype)) - - -def load_adaptive_zero_init(adaptive_zero_init, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load adaptive zero initialization parameters.""" - adaptive_zero_init.cond_linear1.weight.set_data( - np_slice(ckpt[f'{path}transition2']['weights'], i, j, dtype=dtype)) - if ckpt.get(f'{path}adaptive_zero_cond'): - adaptive_zero_init.cond_linear2.weight.set_data( - np_slice(ckpt[f'{path}adaptive_zero_cond']['weights'], i, j, dtype=dtype)) - adaptive_zero_init.cond_linear2.bias.set_data( - np_slice(ckpt[f'{path}adaptive_zero_cond']['bias'], i, j, dtype=dtype)) - - -def load_transition(transition_block, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load transition block parameters.""" - load_adaptive_layernorm( - transition_block.adaptive_layernorm, f'{path}ffw_', ckpt, i, j, dtype=dtype) - transition_block.weights.set_data( - np_slice(ckpt[f'{path}ffw_transition1']['weights'], i, j, dtype=dtype).reshape( - (transition_block.weights.shape[0], 2, transition_block.num_intermediate))) - load_adaptive_zero_init( - transition_block.adaptive_zero_init, f'{path}ffw_', ckpt, i, j, dtype=dtype) - - -def load_self_attention(self_attention, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load self attention parameters.""" - load_adaptive_layernorm( - self_attention.adaptive_layernorm, path, ckpt, i, j) - self_attention.q_linear.weight.set_data( - np_slice(ckpt[f'{path}q_projection']['weights'], i, j, dtype=dtype)) - self_attention.q_linear.bias.set_data( - np_slice(ckpt[f'{path}q_projection']['bias'], i, j, dtype=dtype)) - self_attention.k_linear.weight.set_data( - np_slice(ckpt[f'{path}k_projection']['weights'], i, j, dtype=dtype)) - self_attention.v_linear.weight.set_data( - np_slice(ckpt[f'{path}v_projection']['weights'], i, j, dtype=dtype)) - self_attention.linear.weight.set_data( - np_slice(ckpt[f'{path}gating_query']['weights'], i, j, dtype=dtype)) - load_adaptive_zero_init( - self_attention.adaptive_zero_init, path, ckpt, i, j, dtype=dtype) - - -def load_transformer(transformer, path, ckpt, dtype=ms.bfloat16): - """Load transformer parameters.""" - for i in range(6): - for j in range(4): - transformer_path = (path + - '/__layer_stack_with_per_layer/__layer_stack_with_per_layer/transformer') - load_self_attention(transformer.super_blocks[i].blocks[j].self_attention, - transformer_path, ckpt, i, j, dtype=dtype) - load_transition(transformer.super_blocks[i].blocks[j].transition_block, - transformer_path, ckpt, i, j, dtype=dtype) - if transformer.using_pair_act is True: - pair_projection_path = f'{path}/__layer_stack_with_per_layer/pair_logits_projection' - transformer.super_blocks[i].pair_linear.weight.set_data( - np_slice(ckpt[pair_projection_path]['weights'], i, None, dtype=dtype)) - if transformer.using_pair_act is True: - pair_norm_path = f'{path}/pair_input_layer_norm' - transformer.pair_layernorm.layernorm.gamma.set_data( - np_slice(ckpt[pair_norm_path]['scale'].T, dtype=ms.float32)) - - -def load_transition_block(transition_block, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load transition block parameters.""" - transition_block.glu_weight.set_data( - np_slice(ckpt[f'{path}/transition1']['weights'], i, j, dtype=dtype).reshape( - (-1, 2, transition_block.num_intermediate))) - transition_block.out_linear.weight.set_data( - np_slice(ckpt[f'{path}/transition2']['weights'], i, j, dtype=dtype)) - transition_block.layernorm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/input_layer_norm']['scale'], i, j, dtype=ms.float32)) - transition_block.layernorm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/input_layer_norm']['offset'], i, j, dtype=ms.float32)) - - -def load_grid_self_attention(grid_self_attention, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load grid self attention parameters.""" - grid_self_attention.q_projection.weight.set_data( - np_slice(ckpt[f'{path}/q_projection']['weights'], i, j, dtype=dtype).transpose(2, 0, 1)) - grid_self_attention.k_projection.weight.set_data( - np_slice(ckpt[f'{path}/k_projection']['weights'], i, j, dtype=dtype).transpose(2, 0, 1)) - grid_self_attention.v_projection.weight.set_data( - np_slice(ckpt[f'{path}/v_projection']['weights'], i, j, dtype=dtype)) - grid_self_attention.gating_query.weight.set_data( - np_slice(ckpt[f'{path}/gating_query']['weights'], i, j, dtype=dtype).T) - grid_self_attention.output_projection.weight.set_data( - np_slice(ckpt[f'{path}/output_projection']['weights'], i, j, dtype=dtype)) - grid_self_attention.pair_bias_projection.weight.set_data( - np_slice(ckpt[f'{path}/pair_bias_projection']['weights'], i, j, dtype=dtype)) - grid_self_attention.act_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/act_norm']['scale'], i, j, dtype=ms.float32)) - grid_self_attention.act_norm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/act_norm']['offset'], i, j, dtype=ms.float32)) - - -def load_outer_product_mean(outer_product_mean, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load outer product mean parameters.""" - outer_product_mean.outer_product_mean.o_biases.set_data( - np_slice(ckpt[path]['output_b'], i, j, dtype=dtype)) - outer_product_mean.outer_product_mean.linear_output_weight.set_data( - np_slice(ckpt[path]['output_w'], i, j, dtype=dtype)) - outer_product_mean.outer_product_mean.left_projection_weight.set_data( - np_slice(ckpt[f'{path}/left_projection']['weights'], i, j, dtype=dtype).T) - outer_product_mean.outer_product_mean.right_projection_weight.set_data( - np_slice(ckpt[f'{path}/right_projection']['weights'], i, j, dtype=dtype).T) - outer_product_mean.outer_product_mean.layer_norm_input_gamma.set_data( - np_slice(ckpt[f'{path}/layer_norm_input']['scale'], i, j, dtype=ms.float32)) - outer_product_mean.outer_product_mean.layer_norm_input_beta.set_data( - np_slice(ckpt[f'{path}/layer_norm_input']['offset'], i, j, dtype=ms.float32)) - - -def load_msa_attention(msa_attention, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load msa attention parameters.""" - msa_attention.actnorm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/act_norm']['scale'], i, j, dtype=ms.float32)) - msa_attention.actnorm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/act_norm']['offset'], i, j, dtype=ms.float32)) - msa_attention.pairnorm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/pair_norm']['scale'], i, j, dtype=ms.float32)) - msa_attention.pairnorm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/pair_norm']['offset'], i, j, dtype=ms.float32)) - msa_attention.pair_logits.weight.set_data( - np_slice(ckpt[f'{path}/pair_logits']['weights'], i, j, dtype=dtype)) - msa_attention.v_projection.weight.set_data( - np_slice(ckpt[f'{path}/v_projection']['weights'], i, j, dtype=dtype)) - msa_attention.gating_query.weight.set_data( - np_slice(ckpt[f'{path}/gating_query']['weights'], i, j, dtype=dtype)) - msa_attention.output_projection.weight.set_data( - np_slice(ckpt[f'{path}/output_projection']['weights'], i, j, dtype=dtype)) - - -def load_triangle_multiplication(triangle_multiplication, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load triangle multiplication parameters.""" - triangle_multiplication.triangle_multi.gate.weight.set_data( - np_slice(ckpt[f'{path}/gate']['weights'], i, j, dtype=dtype).T) - triangle_multiplication.triangle_multi.projection.weight.set_data( - np_slice(ckpt[f'{path}/projection']['weights'], i, j, dtype=dtype).T) - triangle_multiplication.triangle_multi.weight_glu = ops.stack( - [triangle_multiplication.triangle_multi.gate.weight, - triangle_multiplication.triangle_multi.projection.weight], axis=1) - triangle_multiplication.triangle_multi.output_projection.weight.set_data( - np_slice(ckpt[f'{path}/output_projection']['weights'], i, j, dtype=dtype)) - triangle_multiplication.triangle_multi.gating_linear.weight.set_data( - np_slice(ckpt[f'{path}/gating_linear']['weights'], i, j, dtype=dtype)) - triangle_multiplication.triangle_multi.left_norm_input.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/left_norm_input']['scale'], i, j, dtype=ms.float32)) - triangle_multiplication.triangle_multi.left_norm_input.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/left_norm_input']['offset'], i, j, dtype=ms.float32)) - triangle_multiplication.triangle_multi.center_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/center_norm']['scale'], i, j, dtype=ms.float32)) - triangle_multiplication.triangle_multi.center_norm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/center_norm']['offset'], i, j, dtype=ms.float32)) - - -def load_pair_former(pair_former, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load pair former parameters.""" - load_grid_self_attention(pair_former.grid_self_attention1, f'{path}/pair_attention1', - ckpt, i, j, dtype=dtype) - load_grid_self_attention(pair_former.grid_self_attention2, f'{path}/pair_attention2', - ckpt, i, j, dtype=dtype) - load_triangle_multiplication(pair_former.triangle_multiplication1, - f'{path}/triangle_multiplication_outgoing', ckpt, i, j, dtype=dtype) - load_triangle_multiplication(pair_former.triangle_multiplication2, - f'{path}/triangle_multiplication_incoming', ckpt, i, j, dtype=dtype) - load_transition_block(pair_former.transition_block, f'{path}/pair_transition', - ckpt, i, j, dtype=dtype) - if pair_former.with_single: - pair_former.single_pair_logits_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/single_pair_logits_norm']['scale'], i, j, dtype=ms.float32)) - pair_former.single_pair_logits_norm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/single_pair_logits_norm']['offset'], i, j, dtype=ms.float32)) - pair_former.single_pair_logits_projection.weight.set_data( - np_slice(ckpt[f'{path}/single_pair_logits_projection']['weights'], i, j, dtype=dtype)) - load_self_attention(pair_former.single_attention, f'{path}/single_attention_', - ckpt, i, j, dtype=dtype) - load_transition_block(pair_former.single_transition, f'{path}/single_transition', - ckpt, i, j, dtype=dtype) - - -def load_evo_former(evo_former, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load evo former parameters.""" - load_outer_product_mean(evo_former.outer_product_mean, f'{path}/outer_product_mean', - ckpt, i, j, dtype=dtype) - load_msa_attention(evo_former.msa_attention, f'{path}/msa_attention1', - ckpt, i, j, dtype=dtype) - load_transition_block(evo_former.msa_transition, f'{path}/msa_transition', - ckpt, i, j, dtype=dtype) - load_triangle_multiplication(evo_former.triangle_multiplication1, - f'{path}/triangle_multiplication_outgoing', ckpt, i, j, dtype=dtype) - load_triangle_multiplication(evo_former.triangle_multiplication2, - f'{path}/triangle_multiplication_incoming', ckpt, i, j, dtype=dtype) - load_grid_self_attention(evo_former.pair_attention1, f'{path}/pair_attention1', - ckpt, i, j, dtype=dtype) - load_grid_self_attention(evo_former.pair_attention2, f'{path}/pair_attention2', - ckpt, i, j, dtype=dtype) - load_transition_block(evo_former.transition_block, f'{path}/pair_transition', - ckpt, i, j, dtype=dtype) - - -def load_single_template_embedding(single_template_embedding, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load single template embedding parameters.""" - num_layer = single_template_embedding.config.template_stack.num_layer - for ii in range(num_layer): - template_path = f'{path}/__layer_stack_no_per_layer/template_embedding_iteration' - load_pair_former(single_template_embedding.template_stack[ii], template_path, - ckpt, ii, dtype=dtype) - for jj in range(9): - template_pair_path = f'{path}/template_pair_embedding_{jj}' - single_template_embedding.template_pair_embedding[jj].weight.set_data( - np_slice(ckpt[template_pair_path]['weights'], None, None, dtype=dtype)) - single_template_embedding.output_layer_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/output_layer_norm']['scale'], i, j, dtype=ms.float32)) - single_template_embedding.output_layer_norm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/output_layer_norm']['offset'], i, j, dtype=ms.float32)) - single_template_embedding.query_embedding_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/query_embedding_norm']['scale'], i, j, dtype=ms.float32)) - single_template_embedding.query_embedding_norm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/query_embedding_norm']['offset'], i, j, dtype=ms.float32)) - - -def load_template_embedding(template_embedding, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load template embedding parameters.""" - template_embedding.output_linear.weight.set_data( - np_slice(ckpt[f'{path}/output_linear']['weights'], i, j, dtype=dtype)) - load_single_template_embedding(template_embedding.template_embedder, - f'{path}/single_template_embedding', ckpt, i, j, dtype=dtype) - - -def load_distogram_head(distogram_head, path, ckpt, i=None, j=None, dtype=ms.float32): - """Load distogram head parameters.""" - distogram_head.linear.weight.set_data( - np_slice(ckpt[f'{path}/half_logits']['weights'], i, j, dtype=dtype)) - - -def load_evoformer(evoformer, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load evoformer parameters.""" - relative_encoding_path = f'{path}/~_relative_encoding/position_activations' - evoformer.position_activations.weight.set_data( - np_slice(ckpt[relative_encoding_path]['weights'], i, j, dtype=dtype)) - evoformer.left_single.weight.set_data( - np_slice(ckpt[f'{path}/left_single']['weights'], i, j, dtype=dtype)) - evoformer.right_single.weight.set_data( - np_slice(ckpt[f'{path}/right_single']['weights'], i, j, dtype=dtype)) - evoformer.bond_embedding.weight.set_data( - np_slice(ckpt[f'{path}/bond_embedding']['weights'], i, j, dtype=dtype)) - evoformer.msa_activations.weight.set_data( - np_slice(ckpt[f'{path}/msa_activations']['weights'], i, j, dtype=dtype)) - evoformer.extra_msa_target_feat.weight.set_data( - np_slice(ckpt[f'{path}/extra_msa_target_feat']['weights'], i, j, dtype=dtype)) - evoformer.prev_embedding.weight.set_data( - np_slice(ckpt[f'{path}/prev_embedding']['weights'], i, j, dtype=dtype)) - evoformer.prev_embedding_layer_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/prev_embedding_layer_norm']['scale'], i, j, dtype=ms.float32)) - evoformer.prev_embedding_layer_norm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/prev_embedding_layer_norm']['offset'], i, j, dtype=ms.float32)) - evoformer.single_activations.weight.set_data( - np_slice(ckpt[f'{path}/single_activations']['weights'], i, j, dtype=dtype)) - evoformer.prev_single_embedding.weight.set_data( - np_slice(ckpt[f'{path}/prev_single_embedding']['weights'], i, j, dtype=dtype)) - evoformer.prev_single_embedding_layer_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/prev_single_embedding_layer_norm']['scale'], i, j, dtype=ms.float32)) - evoformer.prev_single_embedding_layer_norm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/prev_single_embedding_layer_norm']['offset'], i, j, dtype=ms.float32)) - load_template_embedding(evoformer.template_module, f'{path}/template_embedding', - ckpt, i, j, dtype=dtype) - for ii in range(evoformer.config.pairformer.num_layer): - pairformer_path = path+'/__layer_stack_no_per_layer_1/trunk_pairformer' - load_pair_former( - evoformer.pairformer_stack[ii], pairformer_path, ckpt, ii, dtype=dtype) - for jj in range(evoformer.config.msa_stack.num_layer): - msa_stack_path = path+'/__layer_stack_no_per_layer/msa_stack' - load_evo_former( - evoformer.evoformer_stack[jj], msa_stack_path, ckpt, jj, dtype=dtype) - - -def load_adaptive_layernorm_ms(adaptive_layernorm, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load adaptive layer normalization parameters for MS.""" - if not ckpt.get(f'{path}single_cond_layer_norm'): - adaptive_layernorm.layernorm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}layer_norm']['scale'], i, j, dtype=dtype)) - adaptive_layernorm.layernorm.layernorm.beta.set_data( - np_slice(ckpt[f'{path}layer_norm']['offset'], i, j, dtype=dtype)) - else: - adaptive_layernorm.single_cond_layer_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}single_cond_layer_norm']['scale'], i, j, dtype=dtype)) - adaptive_layernorm.single_cond_scale.weight.set_data( - np_slice(ckpt[f'{path}single_cond_scale']['weights'], i, j, dtype=dtype)) - adaptive_layernorm.single_cond_scale.bias.set_data( - np_slice(ckpt[f'{path}single_cond_scale']['bias'], i, j, dtype=dtype)) - adaptive_layernorm.single_cond_bias.weight.set_data( - np_slice(ckpt[f'{path}single_cond_bias']['weights'], i, j, dtype=dtype)) - - -def load_adaptive_zero_init_ms(adaptive_zero_init, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load adaptive zero initialization parameters for MS.""" - adaptive_zero_init.cond_linear1.weight.set_data( - np_slice(ckpt[f'{path}transition2']['weights'], i, j, dtype=dtype)) - if ckpt.get(f'{path}adaptive_zero_cond'): - adaptive_zero_init.cond_linear2.weight.set_data( - np_slice(ckpt[f'{path}adaptive_zero_cond']['weights'], i, j, dtype=dtype)) - adaptive_zero_init.cond_linear2.bias.set_data( - np_slice(ckpt[f'{path}adaptive_zero_cond']['bias'], i, j, dtype=dtype)) - - -def load_transition_ms(transition_block, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load transition block parameters for MS.""" - load_adaptive_layernorm_ms( - transition_block.adaptive_layernorm, f'{path}ffw_', ckpt, i, j, dtype=dtype) - transition_block.weights.set_data( - np_slice(ckpt[f'{path}ffw_transition1']['weights'], i, j, dtype=dtype).reshape( - (transition_block.weights.shape[0], 2, transition_block.num_intermediate))) - load_adaptive_zero_init_ms( - transition_block.adaptive_zero_init, f'{path}ffw_', ckpt, i, j, dtype=dtype) - - -def load_self_attention_ms(self_attention, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load self attention parameters for MS.""" - load_adaptive_layernorm_ms( - self_attention.adaptive_layernorm, path, ckpt, i, j, dtype=dtype) - self_attention.q_linear.weight.set_data( - np_slice(ckpt[f'{path}q_projection']['weights'], i, j, dtype=dtype)) - self_attention.q_linear.bias.set_data( - np_slice(ckpt[f'{path}q_projection']['bias'], i, j, dtype=dtype)) - self_attention.k_linear.weight.set_data( - np_slice(ckpt[f'{path}k_projection']['weights'], i, j, dtype=dtype)) - self_attention.v_linear.weight.set_data( - np_slice(ckpt[f'{path}v_projection']['weights'], i, j, dtype=dtype)) - self_attention.linear.weight.set_data( - np_slice(ckpt[f'{path}gating_query']['weights'], i, j, dtype=dtype)) - load_adaptive_zero_init_ms( - self_attention.adaptive_zero_init, path, ckpt, i, j, dtype=dtype) - - -def load_transformer_ms(transformer, path, ckpt, dtype=ms.float16): - """Load transformer parameters for MS.""" - for i in range(6): - for j in range(4): - transformer_path = (path + - '/__layer_stack_with_per_layer/__layer_stack_with_per_layer/transformer') - load_self_attention_ms(transformer.super_blocks[i].blocks[j].self_attention, - transformer_path, ckpt, i, j, dtype=dtype) - load_transition_ms(transformer.super_blocks[i].blocks[j].transition_block, - transformer_path, ckpt, i, j, dtype=dtype) - if transformer.using_pair_act: - pair_projection_path = path + '/__layer_stack_with_per_layer/pair_logits_projection' - transformer.super_blocks[i].pair_linear.weight.set_data( - np_slice(ckpt[pair_projection_path]['weights'], i, None, dtype=dtype)) - if transformer.using_pair_act: - pair_norm_path = f'{path}/pair_input_layer_norm' - transformer.pair_layernorm.layernorm.gamma.set_data( - np_slice(ckpt[pair_norm_path]['scale'].T, None, None, dtype=ms.float32)) - - -def load_cross_attention(cross_attention, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load cross attention parameters.""" - load_adaptive_layernorm_ms( - cross_attention.adaptive_layernorm_q, f'{path}q', ckpt, i, j, dtype=dtype) - load_adaptive_layernorm_ms( - cross_attention.adaptive_layernorm_k, f'{path}k', ckpt, i, j, dtype=dtype) - cross_attention.linear_q.weight.set_data( - np_slice(ckpt[f'{path}q_projection']['weights'], i, j, dtype=dtype)) - cross_attention.linear_q.bias.set_data( - np_slice(ckpt[f'{path}q_projection']['bias'], i, j, dtype=dtype)) - cross_attention.linear_k.weight.set_data( - np_slice(ckpt[f'{path}k_projection']['weights'], i, j, dtype=dtype)) - cross_attention.linear_v.weight.set_data( - np_slice(ckpt[f'{path}v_projection']['weights'], i, j, dtype=dtype)) - cross_attention.gating_query.weight.set_data( - np_slice(ckpt[f'{path}gating_query']['weights'], i, j, dtype=dtype)) - load_adaptive_zero_init_ms( - cross_attention.adaptive_zero_init, path, ckpt, i, j, dtype=dtype) - - -def load_cross_att_transformer_block(cross_att_transformer_block, path, ckpt, i=None, dtype=ms.bfloat16): - """Load cross attention transformer block parameters.""" - load_cross_attention( - cross_att_transformer_block.cross_attention, path, ckpt, i, dtype=dtype) - load_transition_ms(cross_att_transformer_block.transition, - path, ckpt, i, dtype=dtype) - - -def load_cross_attention_transformer(cross_attention_transformer, path, ckpt, last_name, i, j, dtype=ms.bfloat16): - """Load cross attention transformer parameters.""" - cross_attention_transformer.pair_input_layer_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/pair_input_layer_norm']['scale'], i, j, dtype=dtype)) - cross_attention_transformer.pair_logits_projection.weight.set_data( - np_slice(ckpt[f'{path}/pair_logits_projection']['weights'], i, j, dtype=dtype)) - for ii in range(cross_attention_transformer.config.num_blocks): - block_path = path + f'/__layer_stack_with_per_layer/{last_name}' - load_cross_att_transformer_block(cross_attention_transformer.block[ii], block_path, - ckpt, ii, dtype=dtype) - - -def load_per_atom_conditioning(per_atom_conditioning, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load per atom conditioning parameters.""" - per_atom_conditioning.linear1.weight.set_data( - np_slice(ckpt[f'{path}_embed_ref_pos']['weights'].T, i, j, dtype=dtype)) - per_atom_conditioning.linear2.weight.set_data( - np_slice(ckpt[f'{path}_embed_ref_mask']['weights'].T, i, j, dtype=dtype)) - per_atom_conditioning.linear3.weight.set_data( - np_slice(ckpt[f'{path}_embed_ref_element']['weights'].T, i, j, dtype=dtype)) - per_atom_conditioning.linear4.weight.set_data( - np_slice(ckpt[f'{path}_embed_ref_charge']['weights'].T, i, j, dtype=dtype)) - per_atom_conditioning.linear5.weight.set_data( - np_slice(ckpt[f'{path}_embed_ref_atom_name']['weights'].T, i, j, dtype=dtype)) - per_atom_conditioning.linear_row_act.weight.set_data( - np_slice(ckpt[f'{path}_single_to_pair_cond_row']['weights'].T, i, j, dtype=dtype)) - per_atom_conditioning.linear_col_act.weight.set_data( - np_slice(ckpt[f'{path}_single_to_pair_cond_col']['weights'].T, i, j, dtype=dtype)) - per_atom_conditioning.linear_pair_act1.weight.set_data( - np_slice(ckpt[f'{path}_embed_pair_offsets']['weights'].T, i, j, dtype=dtype)) - per_atom_conditioning.linear_pair_act2.weight.set_data( - np_slice(ckpt[f'{path}_embed_pair_distances']['weights'].T, i, j, dtype=dtype)) - - -def load_atom_cross_encoder(atom_cross_att_encoder, path, ckpt, last_name, i=None, j=None, dtype=ms.bfloat16): - """Load atom cross encoder parameters.""" - load_per_atom_conditioning( - atom_cross_att_encoder._per_atom_conditioning, path, ckpt, dtype=dtype) - if atom_cross_att_encoder.with_cond: - atom_cross_att_encoder._embed_trunk_single_cond.weight.set_data( - np_slice(ckpt[f'{path}_embed_trunk_single_cond']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._lnorm_trunk_single_cond.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}_lnorm_trunk_single_cond']['scale'], i, j, dtype=ms.float32)) - atom_cross_att_encoder._atom_positions_to_features.weight.set_data( - np_slice(ckpt[f'{path}_atom_positions_to_features']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._embed_trunk_pair_cond.weight.set_data( - np_slice(ckpt[f'{path}_embed_trunk_pair_cond']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._lnorm_trunk_pair_cond.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}_lnorm_trunk_pair_cond']['scale'], i, j, dtype=ms.float32)) - atom_cross_att_encoder._single_to_pair_cond_row.weight.set_data( - np_slice(ckpt[f'{path}_single_to_pair_cond_row_1']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._single_to_pair_cond_col.weight.set_data( - np_slice(ckpt[f'{path}_single_to_pair_cond_col_1']['weights'].T, i, j, dtype=dtype)) - if atom_cross_att_encoder.with_cond: - atom_cross_att_encoder._embed_pair_offsets.weight.set_data( - np_slice(ckpt[f'{path}_embed_pair_offsets_1']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._embed_pair_distances.weight.set_data( - np_slice(ckpt[f'{path}_embed_pair_distances_1']['weights'].T, i, j, dtype=dtype)) - else: - atom_cross_att_encoder._embed_pair_offsets.weight.set_data( - np_slice(ckpt[f'{path}_embed_pair_offsets']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._embed_pair_distances.weight.set_data( - np_slice(ckpt[f'{path}_embed_pair_distances']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._embed_pair_offsets_valid.weight.set_data( - np_slice(ckpt[f'{path}_embed_pair_offsets_valid']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._pair_mlp_1.weight.set_data( - np_slice(ckpt[f'{path}_pair_mlp_1']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._pair_mlp_2.weight.set_data( - np_slice(ckpt[f'{path}_pair_mlp_2']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._pair_mlp_3.weight.set_data( - np_slice(ckpt[f'{path}_pair_mlp_3']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_encoder._project_atom_features_for_aggr.weight.set_data( - np_slice(ckpt[f'{path}_project_atom_features_for_aggr']['weights'].T, i, j, dtype=dtype)) - load_cross_attention_transformer(atom_cross_att_encoder._atom_transformer_encoder, - f'{path}_atom_transformer_encoder', ckpt, - f"{last_name}_atom_transformer_encoder", i, j, dtype=dtype) - - -def load_atom_cross_decoder(atom_cross_att_decoder, path, ckpt, i=None, j=None, dtype=ms.bfloat16): - """Load atom cross decoder parameters.""" - atom_cross_att_decoder._project_token_features_for_broadcast.weight.set_data( - np_slice(ckpt[f'{path}_project_token_features_for_broadcast']['weights'].T, i, j, dtype=dtype)) - atom_cross_att_decoder._atom_features_layer_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}_atom_features_layer_norm']['scale'], i, j, dtype=ms.float32)) - atom_cross_att_decoder._atom_features_to_position_update.weight.set_data( - np_slice(ckpt[f'{path}_atom_features_to_position_update']['weights'].T, i, j, dtype=dtype)) - load_cross_attention_transformer(atom_cross_att_decoder._atom_transformer_decoder, - f'{path}_atom_transformer_decoder', ckpt, - last_name='diffusion_atom_transformer_decoder', i=i, j=j, dtype=dtype) - - -def load_diffusion_head(diffusion_head, path, ckpt, i=None, j=None, dtype=ms.float32): - """Load diffusion head parameters.""" - diffusion_head.pair_cond_initial_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/pair_cond_initial_norm']['scale'], i, j, dtype=ms.float32)) - diffusion_head.pair_cond_initial_projection.weight.set_data( - np_slice(ckpt[f'{path}/pair_cond_initial_projection']['weights'].T, i, j, dtype=ms.float32)) - load_transition_ms(diffusion_head.transition_block1, - f'{path}/pair_transition_0', ckpt, dtype=dtype) - load_transition_ms(diffusion_head.transition_block2, - f'{path}/pair_transition_1', ckpt, dtype=dtype) - diffusion_head.single_cond_initial_norm.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/single_cond_initial_norm']['scale'], i, j, dtype=ms.float32)) - diffusion_head.single_cond_initial_projection.weight.set_data( - np_slice(ckpt[f'{path}/single_cond_initial_projection']['weights'].T, i, j, dtype=dtype)) - diffusion_head.layer_norm_noise.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/noise_embedding_initial_norm']['scale'], i, j, dtype=ms.float32)) - diffusion_head.linear_noise.weight.set_data( - np_slice(ckpt[f'{path}/noise_embedding_initial_projection']['weights'].T, i, j, dtype=dtype)) - load_transition_ms(diffusion_head.single_transition1, - f'{path}/single_transition_0', ckpt, dtype=dtype) - load_transition_ms(diffusion_head.single_transition2, - f'{path}/single_transition_1', ckpt, dtype=dtype) - diffusion_head.layer_norm_act.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/single_cond_embedding_norm']['scale'], i, j, dtype=ms.float32)) - diffusion_head.linear_act.weight.set_data( - np_slice(ckpt[f'{path}/single_cond_embedding_projection']['weights'].T, i, j, dtype=dtype)) - diffusion_head.layer_norm_out.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/output_norm']['scale'], i, j, dtype=ms.float32)) - load_atom_cross_encoder(diffusion_head.atom_cross_att_encoder, f'{path}/diffusion', ckpt, - last_name="diffusion", dtype=dtype) - load_transformer_ms(diffusion_head.transformer, path + - '/transformer', ckpt, dtype=dtype) - load_atom_cross_decoder( - diffusion_head.atom_cross_att_decoder, f'{path}/diffusion', ckpt, dtype=dtype) - - -def load_confidence_head(confidence_head, path, ckpt, i=None, j=None, dtype=ms.float32): - """Load confidence head parameters.""" - confidence_head.left_target_feat_project.weight.set_data( - np_slice(ckpt[f'{path}/~_embed_features/left_target_feat_project']['weights'].T, i, j, dtype=dtype)) - confidence_head.right_target_feat_project.weight.set_data( - np_slice(ckpt[f'{path}/~_embed_features/right_target_feat_project']['weights'].T, i, j, dtype=dtype)) - confidence_head.distogram_feat_project.weight.set_data( - np_slice(ckpt[f'{path}/~_embed_features/distogram_feat_project']['weights'].T, i, j, dtype=dtype)) - for ii in range(confidence_head.config.pairformer.num_layer): - confidence_pairformer_path = path + \ - '/__layer_stack_no_per_layer/confidence_pairformer' - load_pair_former(confidence_head.pairformer_block[ii], confidence_pairformer_path, - ckpt, ii, dtype=dtype) - confidence_head.left_half_distance_logits.weight.set_data( - np_slice(ckpt[f'{path}/left_half_distance_logits']['weights'].T, i, j, dtype=ms.float32)) - confidence_head.logits_ln.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/logits_ln']['scale'], i, j, dtype=ms.float32)) - confidence_head.logits_ln.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/logits_ln']['offset'], i, j, dtype=ms.float32)) - confidence_head.pae_logits.weight.set_data( - np_slice(ckpt[f'{path}/pae_logits']['weights'].T, i, j, dtype=ms.float32)) - confidence_head.pae_logits_ln.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/pae_logits_ln']['scale'], i, j, dtype=ms.float32)) - confidence_head.pae_logits_ln.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/pae_logits_ln']['offset'], i, j, dtype=ms.float32)) - confidence_head.plddt_logits.weight.set_data( - np_slice(ckpt[f'{path}/plddt_logits']['weights'], i, j, dtype=ms.float32)) - confidence_head.plddt_logits_ln.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/plddt_logits_ln']['scale'], i, j, dtype=ms.float32)) - confidence_head.plddt_logits_ln.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/plddt_logits_ln']['offset'], i, j, dtype=ms.float32)) - confidence_head.experimentally_resolved_logits.weight.set_data( - np_slice(ckpt[f'{path}/experimentally_resolved_logits']['weights'], i, j, dtype=ms.float32)) - confidence_head.experimentally_resolved_ln.layernorm.gamma.set_data( - np_slice(ckpt[f'{path}/experimentally_resolved_ln']['scale'], i, j, dtype=ms.float32)) - confidence_head.experimentally_resolved_ln.layernorm.beta.set_data( - np_slice(ckpt[f'{path}/experimentally_resolved_ln']['offset'], i, j, dtype=ms.float32)) - - -def load_diffuser(diffuser, ckpt_dir, dtype=ms.bfloat16): - """Load diffuser parameters from checkpoint directory.""" - path = 'diffuser' - ckpt = get_model_af3_params(pathlib.Path(ckpt_dir)) - load_evoformer(diffuser.embedding_module, path + - '/evoformer', ckpt, dtype=dtype) - load_distogram_head(diffuser.distogram_head, path + - '/distogram_head', ckpt, dtype=ms.float32) - load_atom_cross_encoder(diffuser.create_target_feat_embedding.atom_cross_att_encoder, - f'{path}/evoformer_conditioning', ckpt, - last_name='evoformer_conditioning', dtype=ms.float32) - load_diffusion_head(diffuser.diffusion_module, path + - '/~/diffusion_head', ckpt, dtype=ms.float32) - load_confidence_head(diffuser.confidence_head, path + - '/confidence_head', ckpt, dtype=ms.float32) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/model.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/model.py deleted file mode 100644 index 1826ac83f..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/model.py +++ /dev/null @@ -1,762 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -""" -Model for AlphaFold3 -""" -from dataclasses import dataclass -import random -import concurrent -import functools -from absl import logging -import numpy as np -import mindspore as ms -from mindspore import ops, nn -from alphafold3.constants import residue_names -from alphafold3.model import base_config -from alphafold3.model import confidences -from alphafold3.model import model_config -from alphafold3.model.atom_layout import atom_layout -from alphafold3.model.components import base_model -from alphafold3.model.components import base_modules as bm -from alphafold3.model.diffusion import atom_cross_attention -from alphafold3.model.diffusion import confidence_head -from alphafold3.model.diffusion import diffusion_head -from alphafold3.model.diffusion import distogram_head -from alphafold3.model.diffusion import featurization -from alphafold3.model.diffusion import modules -from alphafold3.model.diffusion import template_modules -from alphafold3.structure import mmcif - - -def get_predicted_structure(result, batch): - """Creates the predicted structure and ion preditions. - - Args: - result: model output in a model specific layout - batch: model input batch - - Returns: - Predicted structure. - """ - model_output_coords = result['diffusion_samples']['atom_positions'] - - # Rearrange model output coordinates to the flat output layout. - model_output_to_flat = atom_layout.compute_gather_idxs( - source_layout=batch.convert_model_output.token_atoms_layout, - target_layout=batch.convert_model_output.flat_output_layout, - ) - pred_flat_atom_coords = atom_layout.convert( - gather_info=model_output_to_flat, - arr=model_output_coords.asnumpy(), - layout_axes=(-3, -2), - ) - - predicted_lddt = result.get('predicted_lddt') - - if predicted_lddt is not None: - pred_flat_b_factors = atom_layout.convert( - gather_info=model_output_to_flat, - arr=predicted_lddt.asnumpy(), - layout_axes=(-2, -1), - ) - else: - # Handle models which don't have predicted_lddt outputs. - pred_flat_b_factors = np.zeros(pred_flat_atom_coords.shape[:-1]) - - (missing_atoms_indices,) = np.nonzero( - model_output_to_flat.gather_mask == 0) - if missing_atoms_indices.shape[0] > 0: - missing_atoms_flat_layout = batch.convert_model_output.flat_output_layout[ - missing_atoms_indices - ] - missing_atoms_uids = list( - zip( - missing_atoms_flat_layout.chain_id, - missing_atoms_flat_layout.res_id, - missing_atoms_flat_layout.res_name, - missing_atoms_flat_layout.atom_name, - ) - ) - logging.warning( - 'Target %s: warning: %s atoms were not predicted by the ' - 'model, setting their coordinates to (0, 0, 0). ' - 'Missing atoms: %s', - batch.convert_model_output.empty_output_struc.name, - missing_atoms_indices.shape[0], - missing_atoms_uids, - ) - - # Put them into a structure - pred_struc = batch.convert_model_output.empty_output_struc - pred_struc = pred_struc.copy_and_update_atoms( - atom_x=pred_flat_atom_coords[..., 0], - atom_y=pred_flat_atom_coords[..., 1], - atom_z=pred_flat_atom_coords[..., 2], - atom_b_factor=pred_flat_b_factors, - # Always 1.0. - atom_occupancy=np.ones(pred_flat_atom_coords.shape[:-1]), - ) - # Set manually/differently when adding metadata. - pred_struc = pred_struc.copy_and_update_globals(release_date=None) - return pred_struc - - -class CreateTargetFeatEmbedding(nn.Cell): - """ - A class that creates target feature embeddings by combining raw features with cross-attention encoded features. - - Args: - config (Config): Configuration object containing parameters for the target feature embedding. - global_config (GlobalConfig): Global configuration object. - - Inputs: - - **batch** (dict) - Dictionary containing batch features. - - Outputs: - - **target_feat** (Tensor) - Tensor of target feature embeddings. - """ - - def __init__(self, config, global_config, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.dtype = dtype - self.atom_cross_att_encoder = atom_cross_attention.AtomCrossAttEncoder( - self.config.per_atom_conditioning, self.global_config, '', with_cond=False, dtype=dtype - ) - - def construct(self, batch): - """Create target feature embedding.""" - target_feat = featurization.create_target_feat( - batch, - append_per_atom_features=False, - dtype=ms.float32 - ).astype(self.dtype) - enc = self.atom_cross_att_encoder( - token_atoms_act=None, - trunk_single_cond=None, - trunk_pair_cond=None, - batch=batch, - ) - target_feat = ops.concat( - [target_feat, enc.token_act.astype(self.dtype)], axis=-1) - return target_feat - - -def _compute_ptm(result, num_tokens, asym_id, pae_single_mask, interface): - """Computes the pTM metrics from PAE.""" - return np.stack( - [ - confidences.predicted_tm_score( - tm_adjusted_pae=tm_adjusted_pae[:num_tokens, :num_tokens].asnumpy( - ), - asym_id=asym_id.asnumpy(), - pair_mask=pae_single_mask[:num_tokens, :num_tokens], - interface=interface, - ) - for tm_adjusted_pae in result['tmscore_adjusted_pae_global'] - ], - axis=0, - ) - - -def _compute_chain_pair_iptm( - num_tokens, - asym_ids, - mask, - tm_adjusted_pae): - """Computes the chain pair ipTM metrics from PAE.""" - return np.stack( - [ - confidences.chain_pairwise_predicted_tm_scores( - tm_adjusted_pae=sample_tm_adjusted_pae[:num_tokens], - asym_id=asym_ids[:num_tokens], - pair_mask=mask[:num_tokens, :num_tokens], - ) - for sample_tm_adjusted_pae in tm_adjusted_pae - ], - axis=0, - ) - - -class Diffuser(nn.Cell): - """ - Diffuser class for processing and generating diffusion samples, confidence scores, and distanceograms. - - Args: - config (Diffuser.Config): Configuration object containing parameters for the diffuser. - in_channel (int): Number of input channels. - feat_shape (tuple): Shape of the feature tensor. - act_shape (tuple): Shape of the activation tensor. - pair_shape (tuple): Shape of the pair tensor. - single_shape (tuple): Shape of the single tensor. - atom_shape (tuple): Shape of the atom tensor. - out_channel (int): Number of output channels. - num_templates (int): Number of templates. - - Inputs: - - **batch** (dict): Dictionary containing batch data. - - **key** (int): Random key generator. - - Outputs: - - **result** (dict): Dictionary containing diffusion samples, distanceogram, and confidence outputs. - """ - @dataclass - class HeadsConfig(base_config.BaseConfig): - diffusion: diffusion_head.DiffusionHead.Config = base_config.autocreate() - confidence: confidence_head.ConfidenceHead.Config = base_config.autocreate() - distogram: distogram_head.DistogramHead.Config = base_config.autocreate() - - @dataclass - class Config(base_config.BaseConfig): - evoformer: 'Evoformer.Config' = base_config.autocreate() - global_config: model_config.GlobalConfig = base_config.autocreate() - heads: 'Diffuser.HeadsConfig' = base_config.autocreate() - num_recycles: int = 10 - return_embeddings: bool = False - - def __init__(self, config, in_channel, feat_shape, act_shape, pair_shape, single_shape, atom_shape, - out_channel, num_templates, dtype=ms.float32, name="model"): - super().__init__(auto_prefix=True) - self.config = config - self.global_config = config.global_config - self.dtype = dtype - self.diffusion_module = diffusion_head.DiffusionHead( - self.config.heads.diffusion, self.global_config, pair_shape, dtype=ms.float32 - ) - self.embedding_module = Evoformer(self.config.evoformer, self.global_config, - feat_shape, act_shape, pair_shape, single_shape, num_templates, dtype=dtype) - self.create_target_feat_embedding = CreateTargetFeatEmbedding( - self.embedding_module.config, self.global_config, dtype=ms.float32) - self.confidence_head = confidence_head.ConfidenceHead( - self.config.heads.confidence, self.global_config, - pair_shape, single_shape, atom_shape, feat_shape[-1], out_channel, dtype=dtype - ) - self.distogram_head = distogram_head.DistogramHead( - self.config.heads.distogram, self.global_config, pair_shape[-1], dtype=ms.float32 - ) - - def _sample_diffusion(self, batch, embeddings, sample_config, key, init_positions=None): - """Sample diffusion.""" - denoising_step = functools.partial( - self.diffusion_module, - batch=batch, - embeddings=embeddings, - use_conditioning=True, - ) - sample = diffusion_head.sample( - denoising_step=denoising_step, - batch=batch, - key=key+1, - config=sample_config, - init_positions=init_positions, - ) - return sample - - def construct(self, batch, key): - """Construct diffusion model.""" - if key is None: - # generate a random number - key = int(np.random.randint(100)) - # batch = feat_batch.Batch.from_data_dict(batch) - target_feat = self.create_target_feat_embedding( - batch) - - def recycle_body(prev, key): - key, subkey = random.randint(0, 1e6), key - embeddings = self.embedding_module( - batch=batch, - prev=prev, - target_feat=target_feat, - key=subkey, - ) - embeddings['pair'] = embeddings['pair'] - embeddings['single'] = embeddings['single'] - return embeddings, key - - num_res = batch.num_res - embeddings = { - 'pair': ops.zeros( - [num_res, num_res, self.config.evoformer.pair_channel], - dtype=ms.float32, - ), - 'single': ops.zeros( - [num_res, self.config.evoformer.seq_channel], dtype=ms.float32 - ), - 'target_feat': target_feat, - } - num_iter = self.config.num_recycles + 1 - for _ in range(num_iter): - embeddings, _ = recycle_body(embeddings, key) - - samples = self._sample_diffusion( - batch, - embeddings, - sample_config=self.config.heads.diffusion.eval, - key=key - ) - confidence_output = [] - for i in range(samples['atom_positions'].shape[0]): - confidence_output.append(self.confidence_head( - dense_atom_positions=samples['atom_positions'][i], - embeddings=embeddings, - seq_mask=batch.token_features.mask, - token_atoms_to_pseudo_beta=batch.pseudo_beta_info.token_atoms_to_pseudo_beta, - asym_id=batch.token_features.asym_id, - )) - for key in confidence_output[0].keys(): - confidence_output[0][key] = ops.stack( - [value[key] for value in confidence_output]) - confidence_output = confidence_output[0] - distogram = self.distogram_head(batch, embeddings) - output = { - 'diffusion_samples': samples, - 'distogram': distogram, - **confidence_output, - } - if self.config.return_embeddings: - output['single_embeddings'] = embeddings['single'] - output['pair_embeddings'] = embeddings['pair'] - return output - - @classmethod - def get_inference_result(cls, batch, result, target_name,): - """Get the predicted structure, scalars, and arrays for inference. - - This function also computes any inference-time quantities, which are not a - part of the forward-pass, e.g. additional confidence scores. Note that this - function is not serialized, so it should be slim if possible. - - Args: - batch: data batch used for model inference, incl. TPU invalid types. - result: output dict from the model's forward pass. - target_name: target name to be saved within structure. - - Yields: - inference_result: dataclass object that contains a predicted structure, - important inference-time scalars and arrays, as well as a slightly trimmed - dictionary of raw model result from the forward pass (for debugging). - """ - del target_name - # Retrieve structure and construct a predicted structure. - pred_structure = get_predicted_structure(result=result, batch=batch) - num_tokens = batch.token_features.seq_length.item() - pae_single_mask = np.tile( - batch.frames.mask[:, None], - [1, batch.frames.mask.shape[0]], - ) - ptm = _compute_ptm( - result=result, - num_tokens=num_tokens, - asym_id=batch.token_features.asym_id[:num_tokens], - pae_single_mask=pae_single_mask, - interface=False, - ) - iptm = _compute_ptm( - result=result, - num_tokens=num_tokens, - asym_id=batch.token_features.asym_id[:num_tokens], - pae_single_mask=pae_single_mask, - interface=True, - ) - ptm_iptm_average = 0.8 * iptm + 0.2 * ptm - - asym_ids = batch.token_features.asym_id[:num_tokens].asnumpy() - chain_ids = [mmcif.int_id_to_str_id(asym_id) for asym_id in asym_ids] - res_ids = batch.token_features.residue_index[:num_tokens] - - if len(np.unique(asym_ids)) > 1: - # There is more than one chain, hence interface pTM (i.e. ipTM) defined, - # so use it. - ranking_confidence = ptm_iptm_average - else: - # There is only one chain, hence ipTM=NaN, so use just pTM. - ranking_confidence = ptm - - contact_probs = result['distogram']['contact_probs'].astype(ms.float32) - # Compute PAE related summaries. - _, chain_pair_pae_min, _ = confidences.chain_pair_pae( - num_tokens=num_tokens, - asym_ids=batch.token_features.asym_id.asnumpy(), - full_pae=result['full_pae'].asnumpy(), - mask=pae_single_mask, - ) - chain_pair_pde_mean, chain_pair_pde_min = confidences.chain_pair_pde( - num_tokens=num_tokens, - asym_ids=batch.token_features.asym_id.asnumpy(), - full_pde=result['full_pde'].asnumpy(), - ) - intra_chain_single_pde, cross_chain_single_pde, _ = confidences.pde_single( - num_tokens, - batch.token_features.asym_id.asnumpy(), - result['full_pde'].asnumpy(), - contact_probs.asnumpy(), - ) - pae_metrics = confidences.pae_metrics( - num_tokens=num_tokens, - asym_ids=batch.token_features.asym_id.asnumpy(), - full_pae=result['full_pae'].asnumpy(), - mask=pae_single_mask, - contact_probs=contact_probs.asnumpy(), - tm_adjusted_pae=result['tmscore_adjusted_pae_interface'].asnumpy(), - ) - ranking_confidence_pae = confidences.rank_metric( - result['full_pae'].asnumpy(), - contact_probs.asnumpy() * batch.frames.mask[:, None].astype(float), - ) - chain_pair_iptm = _compute_chain_pair_iptm( - num_tokens=num_tokens, - asym_ids=batch.token_features.asym_id.asnumpy(), - mask=pae_single_mask, - tm_adjusted_pae=result['tmscore_adjusted_pae_interface'].asnumpy(), - ) - # iptm_ichain is a vector of per-chain ptm values. iptm_ichain[0], - # for example, is just the zeroth diagonal entry of the chain pair iptm - # matrix: - # [[x, , ], - # [ , , ], - # [ , , ]]] - iptm_ichain = chain_pair_iptm.diagonal(axis1=-2, axis2=-1) - # iptm_xchain is a vector of cross-chain interactions for each chain. - # iptm_xchain[0], for example, is an average of chain 0's interactions with - # other chains: - # [[ ,x,x], - # [x, , ], - # [x, , ]]] - iptm_xchain = confidences.get_iptm_xchain(chain_pair_iptm) - - predicted_distance_errors = result['average_pde'] - - # Computing solvent accessible area with dssp can be slow for large - # structures with lots of chains, so we parallelize the call. - pred_structures = pred_structure.unstack() - num_workers = len(pred_structures) - with concurrent.futures.ThreadPoolExecutor( - max_workers=num_workers - ) as executor: - has_clash = list(executor.map( - confidences.has_clash, pred_structures)) - fraction_disordered = list( - executor.map(confidences.fraction_disordered, pred_structures) - ) - for idx, pred_structure in enumerate(pred_structures): - ranking_score = confidences.get_ranking_score( - ptm=ptm[idx], - iptm=iptm[idx], - fraction_disordered_=fraction_disordered[idx], - has_clash_=has_clash[idx], - ) - print(f"####### result {idx} ######") - print(f"####### ranking_score {ranking_score} ######") - print(f"####### predicted_tm_score {ptm[idx]} ######") - print(f"####### interface_predicted_tm_score {iptm[idx]} ######") - yield base_model.InferenceResult( - predicted_structure=pred_structure, - numerical_data={ - 'full_pde': result['full_pde'][idx, :num_tokens, :num_tokens], - 'full_pae': result['full_pae'][idx, :num_tokens, :num_tokens], - 'contact_probs': contact_probs[:num_tokens, :num_tokens], - }, - metadata={ - 'predicted_distance_error': predicted_distance_errors[idx], - 'ranking_score': ranking_score, - 'fraction_disordered': fraction_disordered[idx], - 'has_clash': has_clash[idx], - 'predicted_tm_score': ptm[idx], - 'interface_predicted_tm_score': iptm[idx], - 'chain_pair_pde_mean': chain_pair_pde_mean[idx], - 'chain_pair_pde_min': chain_pair_pde_min[idx], - 'chain_pair_pae_min': chain_pair_pae_min[idx], - 'ptm': ptm[idx], - 'iptm': iptm[idx], - 'ptm_iptm_average': ptm_iptm_average[idx], - 'intra_chain_single_pde': intra_chain_single_pde[idx], - 'cross_chain_single_pde': cross_chain_single_pde[idx], - 'pae_ichain': pae_metrics['pae_ichain'][idx], - 'pae_xchain': pae_metrics['pae_xchain'][idx], - 'ranking_confidence': ranking_confidence[idx], - 'ranking_confidence_pae': ranking_confidence_pae[idx], - 'chain_pair_iptm': chain_pair_iptm[idx], - 'iptm_ichain': iptm_ichain[idx], - 'iptm_xchain': iptm_xchain[idx], - 'token_chain_ids': chain_ids, - 'token_res_ids': res_ids, - }, - ) - - -class Evoformer(nn.Cell): - """ - Evoformer class for generating 'single' and 'pair' embeddings in protein structure prediction. - - Args: - config (Evoformer.Config): Configuration object defining the parameters for the Evoformer module. - global_config (base_config.BaseConfig): Global configuration object containing general settings. - feat_shape (tuple): Shape of the feature tensor. - act_shape (tuple): Shape of the activation tensor. - pair_shape (tuple): Shape of the pair tensor. - single_shape (tuple): Shape of the single tensor. - num_templates (int): Number of templates used in the model. - - Inputs: - - **batch** (dict): Dictionary containing batch data including token features, MSA, and other - relevant information. - - **prev** (dict): Dictionary containing previous embeddings for 'single' and 'pair' activations. - - **target_feat** (Tensor): Target feature tensor used for generating embeddings. - - **key** (int): Random key for reproducibility. - - Outputs: - - **output** (dict): Dictionary containing the generated embeddings: - - **single** (Tensor): Single residue embeddings. - - **pair** (Tensor): Pairwise residue embeddings. - - **target_feat** (Tensor): Target feature tensor. - - Notes: - - The class processes input data through multiple modules including position encoding, bond embedding, - template embedding, MSA processing, and Pairformer iterations. - - The `construct` method iteratively processes the input data to generate rich embeddings for - downstream tasks in protein structure prediction. - """ - @dataclass - # pytype: disable=invalid-function-definition - class PairformerConfig(modules.PairFormerIteration.Config): - block_remat: bool = False - remat_block_size: int = 8 - - @dataclass - class Config(base_config.BaseConfig): - """Configuration for Evoformer.""" - - max_relative_chain: int = 2 - msa_channel: int = 64 - seq_channel: int = 384 - max_relative_idx: int = 32 - num_msa: int = 1024 - pair_channel: int = 128 - pairformer: 'Evoformer.PairformerConfig' = base_config.autocreate( - single_transition=base_config.autocreate(), - single_attention=base_config.autocreate(), - num_layer=48, - ) - per_atom_conditioning: atom_cross_attention.AtomCrossAttEncoderConfig = ( - base_config.autocreate( - per_token_channels=384, - per_atom_channels=128, - atom_transformer=base_config.autocreate( - num_intermediate_factor=2, - num_blocks=3, - ), - per_atom_pair_channels=16, - ) - ) - template: template_modules.TemplateEmbedding.Config = ( - base_config.autocreate() - ) - msa_stack: modules.EvoformerIteration.Config = base_config.autocreate() - - def __init__(self, config, global_config, feat_shape, act_shape, pair_shape, single_shape, - num_templates, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - in_channel = feat_shape[-1] - position_activations_in = 4 * self.config.max_relative_idx + \ - 4 + 2 * self.config.max_relative_chain + 2 + 1 - self.position_activations = bm.CustomDense( - position_activations_in, self.config.pair_channel, ndim=3, dtype=dtype) - self.left_single = bm.CustomDense( - in_channel, self.config.pair_channel, ndim=2, dtype=dtype) - self.right_single = bm.CustomDense( - in_channel, self.config.pair_channel, ndim=2, dtype=dtype) - self.bond_embedding = bm.CustomDense( - 1, self.config.pair_channel, ndim=3, dtype=dtype) - self.template_module = template_modules.TemplateEmbedding( - self.config.template, self.global_config, num_templates, act_shape, dtype=dtype - ) - self.msa_activations = bm.CustomDense( - residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP + 3, self.config.msa_channel, ndim=3, dtype=dtype) - self.extra_msa_target_feat = bm.CustomDense( - in_channel, self.config.msa_channel, ndim=2, dtype=dtype) - evofromer_act_shape = (self.config.num_msa, - act_shape[1], self.config.msa_channel) - self.evoformer_stack = nn.CellList( - [ - modules.EvoformerIteration( - self.config.msa_stack, self.global_config, evofromer_act_shape, pair_shape, dtype=dtype - ) for _ in range(self.config.msa_stack.num_layer) - ] - ) - self.prev_embedding = bm.CustomDense( - pair_shape[-1], pair_shape[-1], ndim=3, dtype=dtype) - self.prev_embedding_layer_norm = bm.LayerNorm( - pair_shape, dtype=ms.float32) - self.single_activations = bm.CustomDense( - in_channel, self.config.seq_channel, ndim=2, dtype=dtype) - self.prev_single_embedding = bm.CustomDense( - self.config.seq_channel, self.config.seq_channel, ndim=2, dtype=dtype) - self.prev_single_embedding_layer_norm = bm.LayerNorm(act_shape[:-1] + - (self.config.seq_channel,), dtype=ms.float32) - self.pairformer_stack = nn.CellList( - [ - modules.PairFormerIteration( - self.config.pairformer, self.global_config, pair_shape, single_shape, with_single=True, dtype=dtype - ) for _ in range(self.config.pairformer.num_layer) - ] - ) - - def _relative_encoding(self, batch, pair_activations): - rel_feat = featurization.create_relative_encoding( - batch.token_features, - self.config.max_relative_idx, - self.config.max_relative_chain, - ) - rel_feat = rel_feat.astype(pair_activations.dtype) - pair_activations += self.position_activations(rel_feat) - return pair_activations - - def _seq_pair_embedding(self, token_features, target_feat): - left_single = self.left_single(target_feat)[:, None] - right_single = self.right_single(target_feat)[None] - dtype = left_single.dtype - pair_activations = left_single + right_single - mask = token_features.mask - pair_mask = (mask[:, None] * mask[None, :]).astype(dtype) - return pair_activations, pair_mask - - def _embed_bonds(self, batch, pair_activations): - """Embeds bond features and merges into pair activations.""" - # Construct contact matrix. - num_tokens = batch.token_features.token_index.shape[0] - contact_matrix = ops.zeros((num_tokens, num_tokens)) - - tokens_to_polymer_ligand_bonds = ( - batch.polymer_ligand_bond_info.tokens_to_polymer_ligand_bonds - ) - gather_idxs_polymer_ligand = tokens_to_polymer_ligand_bonds.gather_idxs - gather_mask_polymer_ligand = ( - tokens_to_polymer_ligand_bonds.gather_mask.prod(dim=1).astype( - gather_idxs_polymer_ligand.dtype - )[:, None] - ) - # If valid mask then it will be all 1's, so idxs should be unchanged. - gather_idxs_polymer_ligand = ( - gather_idxs_polymer_ligand * gather_mask_polymer_ligand - ) - tokens_to_ligand_ligand_bonds = ( - batch.ligand_ligand_bond_info.tokens_to_ligand_ligand_bonds - ) - gather_idxs_ligand_ligand = tokens_to_ligand_ligand_bonds.gather_idxs - gather_mask_ligand_ligand = tokens_to_ligand_ligand_bonds.gather_mask.prod( - dim=1 - ).astype(gather_idxs_ligand_ligand.dtype)[:, None] - gather_idxs_ligand_ligand = ( - gather_idxs_ligand_ligand * gather_mask_ligand_ligand - ) - gather_idxs = ops.concat( - [gather_idxs_polymer_ligand, gather_idxs_ligand_ligand] - ) - contact_matrix[gather_idxs[:, 0], gather_idxs[:, 1]] = 1.0 - contact_matrix[0, 0] = 0.0 - - bonds_act = self.bond_embedding( - contact_matrix[:, :, None].astype(pair_activations.dtype) - ) - return pair_activations + bonds_act - - def _embed_template_pair(self, batch, pair_activations, pair_mask, key): - """Embeds Templates and merges into pair activations.""" - dtype = pair_activations.dtype - key, subkey = key + 1, key - - templates = batch.templates - asym_id = batch.token_features.asym_id - # Construct a mask such that only intra-chain template features are - # computed, since all templates are for each chain individually. - multichain_mask = (asym_id[:, None] == asym_id[None, :]).astype(dtype) - template_fn = functools.partial(self.template_module, key=subkey) - template_act = template_fn( - query_embedding=pair_activations, - templates=templates, - multichain_mask_2d=multichain_mask, - padding_mask_2d=pair_mask, - ) - return pair_activations + template_act, key - - def _embed_process_msa(self, msa_batch, pair_activations, pair_mask, key, target_feat): - """Processes MSA and returns updated pair activations.""" - dtype = pair_activations.dtype - msa_batch = featurization.truncate_msa_batch( - msa_batch, self.config.num_msa) - msa_feat = featurization.create_msa_feat(msa_batch).astype(dtype) - - msa_activations = self.msa_activations(msa_feat) - msa_activations += self.extra_msa_target_feat(target_feat)[None] - msa_mask = msa_batch.mask.astype(dtype) - # Evoformer MSA stack. - evoformer_input = {'msa': msa_activations, 'pair': pair_activations} - mask = {'msa': msa_mask, 'pair': pair_mask} - for i in range(self.config.msa_stack.num_layer): - evoformer_input = self.evoformer_stack[i](evoformer_input, mask) - - return evoformer_input['pair'], key - - def construct(self, batch, prev, target_feat, key): - """evoformer""" - dtype = (ms.bfloat16 if self.global_config.bfloat16 == - 'all' else ms.float32) - pair_activations, pair_mask = self._seq_pair_embedding( - batch.token_features, target_feat - ) - pair_activations += self.prev_embedding( - self.prev_embedding_layer_norm( - prev['pair'] - ).astype(pair_activations.dtype) - ) - pair_activations = self._relative_encoding(batch, pair_activations) - pair_activations = self._embed_bonds( - batch=batch, pair_activations=pair_activations - ) - pair_activations, key = self._embed_template_pair( - batch=batch, - pair_activations=pair_activations, - pair_mask=pair_mask, - key=key, - ) - pair_activations, key = self._embed_process_msa( - msa_batch=batch.msa, - pair_activations=pair_activations, - pair_mask=pair_mask, - key=key, - target_feat=target_feat, - ) - del key # Unused after this point. - single_activations = self.single_activations(target_feat) - - single_activations += self.prev_single_embedding( - self.prev_single_embedding_layer_norm( - prev['single'].astype(single_activations.dtype) - ) - ) - for i in range(self.config.pairformer.num_layer): - pair_activations, single_activations = self.pairformer_stack[i]( - pair_activations, pair_mask, single_act=single_activations, - seq_mask=batch.token_features.mask.astype(dtype) - ) - output = { - 'single': single_activations, - 'pair': pair_activations, - 'target_feat': target_feat, - } - - return output diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/modules.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/modules.py deleted file mode 100644 index 895618e7f..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/modules.py +++ /dev/null @@ -1,560 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""modules for the Diffuser model.""" - -from dataclasses import dataclass -from typing import Literal, Optional - -import mindspore as ms -from mindspore import nn, ops, Tensor, mint -from alphafold3.model import base_config -from alphafold3.utils.attention import attention -from alphafold3.utils.gated_linear_unit import gated_linear_unit -from alphafold3.model.components import base_modules as bm -from alphafold3.model.components import mapping -from alphafold3.model.diffusion import diffusion_transformer -from alphafold3.model.diffusion.triangle import TriangleMultiplication as Triangle -from alphafold3.model.diffusion.triangle import OuterProductMean as ProductMean -from mindscience.e3nn.utils import Ncon - -def get_shard_size(num_residues, shard_spec): - shard_size = shard_spec[0][-1] - for num_residues_upper_bound, num_residues_shard_size in shard_spec: - shard_size = num_residues_shard_size - if ( - num_residues_upper_bound is None - or num_residues <= num_residues_upper_bound - ): - break - return shard_size - - -class TransitionBlock(nn.Cell): - """ - A transition block for transformer networks, implementing either a GLU-based or linear-based transformation. - - Args: - config (Config): Configuration object containing parameters for the transition block. - global_config (GlobalConfig): Global configuration object. - normalized_shape (tuple): Shape of the input tensor for normalization. - ndim (int): Number of dimensions of the input tensor. Default: ``3``. - - Inputs: - - **act** (Tensor) - Input activation tensor to be processed. - - Outputs: - - **output** (Tensor) - Output tensor after processing through the transition block. - """ - @dataclass - class Config(base_config.BaseConfig): - num_intermediate_factor: int = 4 - use_glu_kernel: bool = True - - def __init__( - self, config, global_config, normalized_shape, ndim=3, dtype=ms.float32 - ): - super().__init__() - self.config = config - self.global_config = global_config - num_channels = normalized_shape[-1] - self.num_intermediate = int( - num_channels * self.config.num_intermediate_factor) - self.layernorm = bm.LayerNorm( - normalized_shape, name='input_layer_norm', dtype=ms.float32) - if self.config.use_glu_kernel: - self.glu_weight = bm.custom_initializer( - 'relu', (num_channels, 2 * self.num_intermediate), dtype=dtype) - self.glu_weight = ms.Parameter(Tensor(self.glu_weight).reshape( - num_channels, 2, self.num_intermediate)) - else: - self.linear = bm.CustomDense(num_channels, self.num_intermediate * 2, - weight_init='zeros', ndim=ndim, dtype=dtype) - self.linear.weight = bm.custom_initializer( - 'zeros', self.linear.weight.shape, dtype=dtype) - self.out_linear = bm.CustomDense(self.num_intermediate, num_channels, - weight_init=self.global_config.final_init, ndim=ndim, dtype=dtype) - - def construct(self, act, broadcast_dim=0): - """Transition Block""" - act = self.layernorm(act) - if self.config.use_glu_kernel: - c = gated_linear_unit( - x=act, - weight=self.glu_weight, - activation=mint.nn.functional.silu, - ) - else: - act = self.linear(act) - a, b = mint.split(act, act.shape[-1]//2, axis=-1) - c = mint.nn.functional.silu(a) * b - return self.out_linear(c) - - -class MSAAttention(nn.Cell): - """ - Multi-Head Self-Attention (MSA) attention mechanism for processing sequence and pair data. - - Args: - config (Config): Configuration object containing parameters for the attention mechanism. - global_config (GlobalConfig): Global configuration object. - act_shape (tuple): Shape of the activation tensor. - pair_shape (tuple): Shape of the pair tensor. - - Inputs: - - **act** (Tensor) - Input activation tensor. - - **mask** (Tensor) - Mask tensor to prevent attention weights from focusing on invalid positions. - - **pair_act** (Tensor) - Pair activation tensor. - - Outputs: - - **output** (Tensor) - Output tensor after processing through the attention mechanism. - """ - @dataclass - class Config(base_config.BaseConfig): - num_head: int = 8 - - def __init__(self, config, global_config, act_shape, pair_shape, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.actnorm = bm.LayerNorm(act_shape, dtype=ms.float32) - self.pairnorm = bm.LayerNorm(pair_shape, dtype=ms.float32) - num_channel = act_shape[-1] - value_dim = num_channel // self.config.num_head - self.pair_logits = bm.CustomDense(pair_shape[-1], self.config.num_head, use_bias=False, - weight_init='zeros', ndim=3, dtype=dtype) - self.v_projection = bm.CustomDense(num_channel, (self.config.num_head, value_dim), - use_bias=False, ndim=len(act_shape), dtype=dtype) - ncon_list1 = [-3, -2, 1] - ncon_list2 = [-1, 1, -3, -4] - self.ncon = Ncon([ncon_list1, ncon_list2]) - self.gating_query = bm.CustomDense( - num_channel, self.config.num_head * value_dim, weight_init='zeros', use_bias=False, ndim=3, dtype=dtype) - self.output_projection = bm.CustomDense(self.config.num_head * value_dim, num_channel, - weight_init=self.global_config.final_init, - use_bias=False, ndim=3, dtype=dtype) - - def construct(self, act, mask, pair_act): - """MSA Attention""" - act = self.actnorm(act) - pair_act = self.pairnorm(pair_act) - logits = self.pair_logits(pair_act).transpose([2, 0, 1]) - logits += 1e9 * (mint.max(mask, dim=0)[0] - 1.0) - weights = mint.softmax(logits, dim=-1) - v = self.v_projection(act) - v_avg = self.ncon([weights, v]) - v_avg = v_avg.reshape(v_avg.shape[:-2]+(-1,)) - gate_value = self.gating_query(act) - v_avg *= mint.sigmoid(gate_value) - out = self.output_projection(v_avg) - return out - - -class GridSelfAttention(nn.Cell): - """ - Self-attention mechanism that operates either per-sequence or per-residue. - - Args: - config (Config): Configuration object containing parameters for the attention mechanism. - global_config (GlobalConfig): Global configuration object. - transpose (bool): Whether to transpose the activation tensor during processing. - normalized_shape (tuple): Shape of the input tensor for normalization. - - Inputs: - - **act** (Tensor) - Input activation tensor. - - **pair_mask** (Tensor) - Mask tensor indicating valid regions in the input. - - Outputs: - - **output** (Tensor) - Output tensor after processing through the self-attention mechanism. - """ - @dataclass - class Config(base_config.BaseConfig): - num_head: int = 4 - - def __init__( - self, config, global_config, transpose, normalized_shape, dtype=ms.float32 - ): - super().__init__() - self.config = config - self.global_config = global_config - self.transpose = transpose - num_channels = normalized_shape[-1] - in_shape = normalized_shape[-1] - qkv_dim = max(num_channels // self.config.num_head, 16) - qkv_shape = (self.config.num_head, qkv_dim) - self.q_projection = bm.CustomDense( - in_shape, qkv_shape, use_bias=False, ndim=3, dtype=dtype) - self.k_projection = bm.CustomDense( - in_shape, qkv_shape, use_bias=False, ndim=3, dtype=dtype) - self.v_projection = bm.CustomDense( - in_shape, qkv_shape, use_bias=False, ndim=3, dtype=dtype) - self.gating_query = bm.CustomDense( - num_channels, self.config.num_head * qkv_dim, weight_init='zeros', use_bias=False, ndim=3, dtype=dtype) - self.output_projection = bm.CustomDense(self.config.num_head * qkv_dim, num_channels, - weight_init=self.global_config.final_init, ndim=3, dtype=dtype) - self.act_norm = bm.LayerNorm(normalized_shape, dtype=ms.float32) - self.pair_bias_projection = bm.CustomDense( - num_channels, self.config.num_head, use_bias=False, weight_init='linear', ndim=3, dtype=dtype) - num_residues = normalized_shape[0] - self.chunk_size = get_shard_size( - num_residues, self.global_config.pair_attention_chunk_size - ) - - def _attention(self, act, mask, bias): - """attention""" - q = self.q_projection(act) - k = self.k_projection(act) - v = self.v_projection(act) - bias = ops.expand_dims(bias, 0) - weighted_avg = attention( - q, - k, - v, - mask=mask, - bias=bias, - logits_scale=1/ms.ops.sqrt(ms.Tensor(q.shape[-1])) - ) - weighted_avg = weighted_avg.reshape(weighted_avg.shape[:-2] + (-1,)) - gate_value = self.gating_query(act) - weighted_avg *= mint.sigmoid(gate_value) - return self.output_projection(weighted_avg) - - def construct(self, act, pair_mask): - """Builds a module. - - Arguments: - act: [num_seq, num_res, channels] activations tensor - pair_mask: [num_seq, num_res] mask of non-padded regions in the tensor. - Only used in inducing points attention currently. - - Returns: - Result of the self-attention operation. - """ - pair_mask = mint.swapaxes(pair_mask, -1, -2) - act = self.act_norm(act) - - non_batched_bias = self.pair_bias_projection(act) - non_batched_bias = non_batched_bias.transpose(2, 0, 1) - if self.transpose: - act = mint.swapaxes(act, -2, -3) - pair_mask = pair_mask[:, None, None, :].astype(ms.bool_) - act = self._attention(act, pair_mask, non_batched_bias) - if self.transpose: - act = mint.swapaxes(act, -2, -3) - return act - - -class TriangleMultiplication(nn.Cell): - """ - Implements triangle multiplication for tensor operations. - - Args: - config (Config): Configuration object specifying the equation and whether to use a GLU kernel. - global_config (GlobalConfig): Global configuration object. - in_channel (int): Number of input channels. - normalized_shape (tuple): Shape of the input tensor for normalization. - batch_size (int, optional): Batch size for processing. Default: ``None``. - - Inputs: - - **act** (Tensor) - Input activation tensor. - - **mask** (Tensor) - Mask tensor indicating valid regions in the input. - - Outputs: - - **out** (Tensor) - Output tensor after triangle multiplication. - """ - @dataclass - class Config(base_config.BaseConfig): - equation: Literal['ikc,jkc->ijc', 'kjc,kic->ijc'] - use_glu_kernel: bool = True - - def __init__(self, config, global_config, in_channel, normalized_shape, batch_size=None, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.triangle_multi = Triangle( - self.config, - self.global_config, - num_intermediate_channel=in_channel, - equation=self.config.equation, - normalized_shape=normalized_shape, - batch_size=batch_size, - dtype=dtype) - - def construct(self, act, mask): - out = self.triangle_multi(act, mask) - return out - - -class OuterProductMean(nn.Cell): - """ - Implements the OuterProductMean operation for tensor computations. - - Args: - config (Config): Configuration object containing parameters for the operation. - global_config (GlobalConfig): Global configuration object. - num_output_channel (int): Number of output channels. - in_channel (int): Number of input channels. - - Inputs: - - **act** (Tensor) - Input activation tensor. - - **mask** (Tensor) - Mask tensor indicating valid regions in the input. - - Outputs: - - **out** (Tensor) - Output tensor after applying the outer product mean operation. - """ - @dataclass - class Config(base_config.BaseConfig): - chunk_size: int = 128 - num_outer_channel: int = 32 - - def __init__(self, config, global_config, num_output_channel, in_channel, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.num_output_channel = num_output_channel - self.outer_product_mean = ProductMean(self.config.num_outer_channel, - in_channel, - self.num_output_channel, - dtype=dtype) - - def construct(self, act, mask): - mask_norm = ops.expand_dims(mint.matmul(mask.T, mask), -1) - out = self.outer_product_mean(act, mask, mask_norm) - return out - - -class PairFormerIteration(nn.Cell): - """ - Single Iteration of PairFormer, which processes pairwise and single activations in a single iteration. - - Args: - config (PairFormerIteration.Config): Configuration for the PairFormerIteration module. - global_config: Global configuration for the model. - normalized_shape (tuple): Shape of the input tensor for normalization. - single_shape (tuple | None): Shape of the single activation tensor. Default: ``None``. - with_single (bool): Whether to include single activation processing. Default: ``False``. - - Inputs: - - **act** (Tensor) - Pairwise activations tensor. - - **pair_mask** (Tensor) - Padding mask for pairwise activations. - - **single_act** (Tensor | None) - Single activations tensor, optional. - - **seq_mask** (Tensor | None) - Sequence mask, optional. - - Outputs: - - **act** (Tensor) - Processed pairwise activations tensor. - - **single_act** (Tensor) - Processed single activations tensor (if `with_single` is True). - """ - @dataclass - class Config(base_config.BaseConfig): - """Config for PairFormerIteration.""" - num_layer: int = 1 - pair_attention: GridSelfAttention.Config = base_config.autocreate() - pair_transition: TransitionBlock.Config = base_config.autocreate() - single_attention: Optional[diffusion_transformer.SelfAttentionConfig] = base_config.autocreate() - single_transition: Optional[TransitionBlock.Config] = base_config.autocreate() - triangle_multiplication_incoming: TriangleMultiplication.Config = ( - base_config.autocreate(equation='kjc,kic->ijc') - ) - triangle_multiplication_outgoing: TriangleMultiplication.Config = ( - base_config.autocreate(equation='ikc,jkc->ijc') - ) - shard_transition_blocks: bool = True - - def __init__(self, config, global_config, normalized_shape, single_shape=None, with_single=False, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.with_single = with_single - num_channel = normalized_shape[-1] - self.triangle_multiplication1 = TriangleMultiplication( - self.config.triangle_multiplication_outgoing, - self.global_config, - num_channel, - normalized_shape, - dtype=dtype - ) - self.triangle_multiplication2 = TriangleMultiplication( - self.config.triangle_multiplication_incoming, - self.global_config, - num_channel, - normalized_shape, - dtype=dtype - ) - self.grid_self_attention1 = GridSelfAttention( - self.config.pair_attention, - self.global_config, - False, - normalized_shape, - dtype=dtype - ) - self.grid_self_attention2 = GridSelfAttention( - self.config.pair_attention, - self.global_config, - True, - normalized_shape, - dtype=dtype - ) - self.transition_block = TransitionBlock( - self.config.pair_transition, self.global_config, normalized_shape, dtype=dtype - ) - num_residues = normalized_shape[0] - if self.config.shard_transition_blocks: - self.transition_block = mapping.sharded_apply( - self.transition_block, - get_shard_size( - num_residues, self.global_config.pair_transition_shard_spec - ) - ) - if self.with_single: - self.single_pair_logits_projection = bm.CustomDense( - num_channel, self.config.single_attention.num_head, ndim=3, dtype=dtype - ) - self.single_pair_logits_norm = bm.LayerNorm(normalized_shape, dtype=ms.float32) - self.single_attention = diffusion_transformer.SelfAttention( - self.config.single_attention, self.global_config, - single_shape[-1], with_single_cond=False, dtype=dtype) - self.single_transition = TransitionBlock( - self.config.single_transition, - self.global_config, - single_shape, - 2, - dtype=dtype - ) - - def construct(self, act, pair_mask, single_act=None, seq_mask=None): - """pairformer iteration""" - act += self.triangle_multiplication1(act, pair_mask) - act += self.triangle_multiplication2(act, pair_mask) - act += self.grid_self_attention1(act, pair_mask) - act += self.grid_self_attention2(act, pair_mask) - act += self.transition_block(act) - if self.with_single: - norm_act = self.single_pair_logits_norm(act) - pair_logits = self.single_pair_logits_projection(norm_act) - pair_logits = pair_logits.transpose((2, 0, 1)) - single_act += self.single_attention( - single_act, seq_mask, None, pair_logits - ) - single_act += self.single_transition(single_act, - broadcast_dim=None) - return act, single_act - return act - - -class EvoformerIteration(nn.Cell): - """ - EvoformerIteration is a single iteration of the Evoformer main stack, which processes - activations and masks through a series of attention and transformation layers to - update the MSA (Multiple Sequence Alignment) and pair representations. - - Args: - config (EvoformerIteration.Config): Configuration for the EvoformerIteration. - global_config (base_config.BaseConfig): Global configuration for the model. - act_shape (tuple): Shape of the activation tensor. - pair_shape (tuple): Shape of the pair tensor. - - Inputs: - - **activations** (dict): A dictionary containing the MSA and pair activations. - - **masks** (dict): A dictionary containing the MSA and pair masks. - - Outputs: - - **activations** (dict): A dictionary containing the updated MSA and pair activations. - """ - @dataclass - class Config(base_config.BaseConfig): - """Configuration for EvoformerIteration.""" - - num_layer: int = 4 - msa_attention: MSAAttention.Config = base_config.autocreate() - outer_product_mean: OuterProductMean.Config = base_config.autocreate() - msa_transition: TransitionBlock.Config = base_config.autocreate() - pair_attention: GridSelfAttention.Config = base_config.autocreate() - pair_transition: TransitionBlock.Config = base_config.autocreate() - triangle_multiplication_incoming: TriangleMultiplication.Config = ( - base_config.autocreate(equation='kjc,kic->ijc') - ) - triangle_multiplication_outgoing: TriangleMultiplication.Config = ( - base_config.autocreate(equation='ikc,jkc->ijc') - ) - shard_transition_blocks: bool = False - - def __init__(self, config, global_config, act_shape, pair_shape, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - num_channel = pair_shape[-1] - self.outer_product_mean = OuterProductMean( - config=self.config.outer_product_mean, - global_config=self.global_config, - num_output_channel=num_channel, - in_channel=act_shape[-1], - dtype=dtype - ) - self.msa_attention = MSAAttention(self.config.msa_attention, - self.global_config, act_shape, pair_shape, dtype=dtype) - self.msa_transition = TransitionBlock( - self.config.msa_transition, self.global_config, act_shape, dtype=dtype - ) - self.triangle_multiplication1 = TriangleMultiplication( - self.config.triangle_multiplication_outgoing, - self.global_config, - num_channel, - pair_shape, - dtype=dtype - ) - self.triangle_multiplication2 = TriangleMultiplication( - self.config.triangle_multiplication_incoming, - self.global_config, - num_channel, - pair_shape, - dtype=dtype - ) - self.pair_attention1 = GridSelfAttention( - self.config.pair_attention, - self.global_config, - False, - pair_shape, - dtype=dtype - ) - self.pair_attention2 = GridSelfAttention( - self.config.pair_attention, - self.global_config, - True, - pair_shape, - dtype=dtype - ) - self.transition_block = TransitionBlock( - self.config.msa_transition, self.global_config, pair_shape, dtype=dtype - ) - num_residues = act_shape[0] - if self.config.shard_transition_blocks: - self.transition_block = mapping.sharded_apply( - self.transition_block, - get_shard_size( - num_residues, self.global_config.pair_transition_shard_spec - ) - ) - - def construct(self, activations, masks): - """evoformer iteration""" - msa_act, pair_act = activations["msa"], activations["pair"] - msa_mask, pair_mask = masks['msa'], masks['pair'] - pair_act += self.outer_product_mean(msa_act, msa_mask) - msa_act += self.msa_attention(msa_act, msa_mask, pair_act) - msa_act += self.msa_transition(msa_act) - pair_act += self.triangle_multiplication1(pair_act, pair_mask) - pair_act += self.triangle_multiplication2(pair_act, pair_mask) - pair_act += self.pair_attention1(pair_act, pair_mask) - pair_act += self.pair_attention2(pair_act, pair_mask) - pair_act += self.transition_block(pair_act) - return {"msa": msa_act, "pair": pair_act} diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/template_modules.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/template_modules.py deleted file mode 100644 index 3d01ea589..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/template_modules.py +++ /dev/null @@ -1,327 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""template modules""" -from dataclasses import dataclass -import mindspore as ms -from mindspore import nn, ops, Tensor, mint - -from alphafold3.model import base_config -from alphafold3.constants import residue_names -from alphafold3.utils import geometry -from alphafold3.model import protein_data_processing -from alphafold3.model.components import base_modules as bm -from alphafold3.model.diffusion import modules -from alphafold3.model.scoring import scoring - - -@dataclass -class DistogramFeaturesConfig(base_config.BaseConfig): - # The left edge of the first bin. - min_bin: float = 3.25 - # The left edge of the final bin. The final bin catches everything larger than - # `max_bin`. - max_bin: float = 50.75 - # The number of bins in the distogram. - num_bins: int = 39 - - -def dgram_from_positions(positions, config, dtype=ms.float32): - """Compute distogram from amino acid positions. - - Args: - positions: (num_res, 3) Position coordinates. - config: Distogram bin configuration. - - Returns: - Distogram with the specified number of bins. - """ - lower_breaks = mint.linspace( - config.min_bin, config.max_bin, config.num_bins) - lower_breaks = mint.square(lower_breaks) - upper_breaks = mint.concat( - [lower_breaks[1:], Tensor([1e8], dtype=ms.float32)], dim=-1) - dist2 = mint.sum(mint.square(ops.expand_dims(positions, axis=-2) - - ops.expand_dims(positions, axis=-3)), dim=-1, keepdim=True) - dgram = (dist2 > lower_breaks).astype(ms.float32) * \ - (dist2 < upper_breaks).astype(ms.float32) - return dgram - - -def slice_index(x, idx): - """Slice index.""" - return ops.gather_d(x, 1, idx.reshape(-1, 1)).squeeze() - - -def make_backbone_rigid(positions, mask, group_indices,): - """Make backbone Rigid3Array and mask. - - Args: - positions: (num_res, num_atoms) of atom positions as Vec3Array. - mask: (num_res, num_atoms) for atom mask. - group_indices: (num_res, num_group, 3) for atom indices forming groups. - - Returns: - tuple of backbone Rigid3Array and mask (num_res,). - """ - backbone_indices = group_indices[:, 0] - - # main backbone frames differ in sidechain frame convention. - # for sidechain it's (C, CA, N), for backbone it's (N, CA, C) - # Hence using c, b, a, each of shape (num_res,). - c, b, a = [backbone_indices[..., i] for i in range(3)] - - rigid_mask = slice_index(mask, a) * \ - slice_index(mask, b) * slice_index(mask, c) - frame_positions = [] - for indices in [a, b, c]: - frame_positions.append(geometry.vector.tree_map( - lambda x, idx=indices: slice_index(x, idx), positions - )) - rotation = geometry.Rot3Array.from_two_vectors( - frame_positions[2] - frame_positions[1], - frame_positions[0] - frame_positions[1], - ) - rigid = geometry.Rigid3Array(rotation, frame_positions[1]) - return rigid, rigid_mask - - -class TemplateEmbedding(nn.Cell): - """ - Embed a set of templates. - - Args: - config (TemplateEmbedding.Config): Configuration for the template embedding. - global_config (base_config.BaseConfig): Global configuration for the model. - num_templates (int): Number of templates to process. - normalized_shape (tuple): Shape of the normalized input tensor. - num_atoms (int): Number of atoms per residue. Default: ``24``. - - Inputs: - - **query_embedding** (Tensor) - Query tensor of shape [num_res, num_res, num_channel]. - - **templates** (Templates) - Object containing template data. - - **padding_mask_2d** (Tensor) - Pair mask for attention operations of shape [num_res, num_res]. - - **multichain_mask_2d** (Tensor) - Pair mask for multichain operations of shape [num_res, num_res]. - - **key** (int) - Random key generator. - - Outputs: - - **embedding** (Tensor) - Output embedding tensor of shape [num_res, num_res, num_channels]. - """ - @dataclass - class Config(base_config.BaseConfig): - num_channels: int = 64 - template_stack: modules.PairFormerIteration.Config = base_config.autocreate( - num_layer=2, - pair_transition=base_config.autocreate(num_intermediate_factor=2), - ) - dgram_features: DistogramFeaturesConfig = base_config.autocreate() - - def __init__(self, config, global_config, num_templates, normalized_shape, num_atoms=24, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.num_residues = normalized_shape[0] - self.num_templates = num_templates - self.query_num_channels = normalized_shape[2] - self.num_atoms = num_atoms - self.template_embedder = SingleTemplateEmbedding( - self.config, self.global_config, normalized_shape, dtype=dtype) - self.output_linear = bm.CustomDense( - self.config.num_channels, self.query_num_channels, ndim=3, dtype=dtype) - self.output_linear.weight = bm.custom_initializer( - 'relu', (self.config.num_channels, self.query_num_channels), dtype=dtype) - - def construct(self, query_embedding, templates, padding_mask_2d, - multichain_mask_2d, key): - """Generate an embedding for a set of templates. - - Args: - query_embedding: [num_res, num_res, num_channel] a query tensor that will - be used to attend over the templates to remove the num_templates - dimension. - templates: A 'Templates' object. - padding_mask_2d: [num_res, num_res] Pair mask for attention operations. - multichain_mask_2d: [num_res, num_res] Pair mask for multichain. - key: random key generator. - - Returns: - An embedding of size [num_res, num_res, num_channels] - """ - subkeys = mint.arange(key, key + self.num_templates, 1) - summed_template_embeddings = mint.zeros( - (self.num_residues, self.num_residues, - self.config.num_channels), dtype=query_embedding.dtype - ) - - def scan_fn(carry, x): - templates, key = x - embedding = self.template_embedder( - query_embedding, - templates, - padding_mask_2d, - multichain_mask_2d, - key, - ) - return carry + embedding - for i, subkey in enumerate(subkeys): - summed_template_embeddings = scan_fn( - summed_template_embeddings, (templates[i], subkey)) - embedding = summed_template_embeddings / (1e-7 + self.num_templates) - embedding = mint.nn.functional.relu(embedding) - embedding = self.output_linear(embedding) - return embedding - - -class SingleTemplateEmbedding(nn.Cell): - """ - Embed a single template. - - Args: - config: Configuration object containing model parameters. - global_config: Global configuration object. - normalized_shape (tuple): Shape for normalization layers. - - Inputs: - - **query_embedding** (Tensor) - Query embedding tensor of shape (num_res, num_res, num_channels). - - **templates** (Templates object) - Object containing single template data. - - **padding_mask_2d** (Tensor) - Padding mask tensor. - - **multichain_mask_2d** (Tensor) - Mask indicating intra-chain residue pairs. - - **key** (random.KeyArray) - Random key generator. - - Outputs: - - **output** (Tensor) - Template embedding tensor of shape (num_res, num_res, num_channels). - """ - - def __init__( - self, - config, - global_config, - normalized_shape, - dtype=ms.float32 - ): - super().__init__() - self.config = config - self.global_config = global_config - num_channels = self.config.num_channels - self.query_embedding_norm = bm.LayerNorm( - normalized_shape, dtype=ms.float32) - - # to be determined the shape of input, output and number of layers - num_layers = 9 - in_shape_list = [39, (), 31, 31, (), (), (), (), 128] - ndim_list = [3, 2, 3, 3, 2, 2, 2, 2, 3] - self.template_pair_embedding = ms.nn.CellList( - [ - bm.CustomDense( - in_shape_list[i], num_channels, weight_init="relu", ndim=ndim_list[i], dtype=dtype - ) - for i in range(num_layers) - ] - ) - self.template_stack = ms.nn.CellList( - [ - modules.PairFormerIteration( - self.config.template_stack, self.global_config, normalized_shape[:-1] + ( - num_channels,), dtype=dtype - ) - for _ in range(self.config.template_stack.num_layer) - ] - ) - self.output_layer_norm = bm.LayerNorm( - normalized_shape[:-1] + (num_channels,), dtype=ms.float32) - - def construct(self, query_embedding, templates, padding_mask_2d, multichain_mask_2d, key): - act = self.construct_input( - query_embedding, templates, multichain_mask_2d) - if self.config.template_stack.num_layer: - for i in range(self.config.template_stack.num_layer): - act = self.template_stack[i](act, padding_mask_2d) - act = self.output_layer_norm(act) - return act - - def construct_input(self, query_embedding, templates, multichain_mask_2d): - """Construct input for template embedding.""" - # Compute distogram feature for the template. - dtype = multichain_mask_2d.dtype - aatype = templates.aatype - dense_atom_mask = templates.atom_mask - dense_atom_positions = templates.atom_positions - dense_atom_positions *= dense_atom_mask[..., None] - pseudo_beta_positions, pseudo_beta_mask = [ms.Tensor(x) for x in scoring.pseudo_beta_fn( - templates.aatype, dense_atom_positions, dense_atom_mask - )] - pseudo_beta_mask_2d = ( - pseudo_beta_mask[:, None] * pseudo_beta_mask[None, :] - ) - pseudo_beta_mask_2d *= multichain_mask_2d - dgram = dgram_from_positions( - pseudo_beta_positions, self.config.dgram_features - ) - dgram *= pseudo_beta_mask_2d[..., None] - pseudo_beta_mask_2d = pseudo_beta_mask_2d.astype(dtype) - to_concat = [(dgram, 1), (pseudo_beta_mask_2d, 0)] - aatype = mint.nn.functional.one_hot( - aatype.astype(ms.int64), - residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP, - ).astype(dtype) - to_concat.append((aatype[None, :, :], 1)) - to_concat.append((aatype[:, None, :], 1)) - template_group_indices = mint.index_select( - ms.Tensor(protein_data_processing.RESTYPE_RIGIDGROUP_DENSE_ATOM_IDX), - 0, - templates.aatype, - ) - rigid, backbone_mask = make_backbone_rigid( - geometry.Vec3Array.from_array(dense_atom_positions), - dense_atom_mask, - template_group_indices, - ) - points = rigid.translation - x = rigid.translation.x.unsqueeze(-1) - y = rigid.translation.y.unsqueeze(-1) - z = rigid.translation.z.unsqueeze(-1) - xx = rigid.rotation.xx.unsqueeze(-1) - xy = rigid.rotation.xy.unsqueeze(-1) - xz = rigid.rotation.xz.unsqueeze(-1) - yx = rigid.rotation.yx.unsqueeze(-1) - yy = rigid.rotation.yy.unsqueeze(-1) - yz = rigid.rotation.yz.unsqueeze(-1) - zx = rigid.rotation.zx.unsqueeze(-1) - zy = rigid.rotation.zy.unsqueeze(-1) - zz = rigid.rotation.zz.unsqueeze(-1) - rigid = geometry.Rigid3Array(geometry.Rot3Array( - xx, xy, xz, yx, yy, yz, zx, zy, zz), geometry.Vec3Array(x, y, z)) - rigid_vec = rigid.inverse().apply_to_point(points) - - unit_vector = rigid_vec.normalized() - unit_vector = [unit_vector.x, unit_vector.y, unit_vector.z] - unit_vector = list(unit_vector) - - backbone_mask_2d = (backbone_mask[:, None] * backbone_mask[None, :]).astype(dtype) - backbone_mask_2d *= multichain_mask_2d - unit_vector = [x * backbone_mask_2d for x in unit_vector] - - # Note that the backbone_mask takes into account C, CA and N (unlike - # pseudo beta mask which just needs CB) so we add both masks as features. - to_concat.extend([(x, 0) for x in unit_vector]) - to_concat.append((backbone_mask_2d, 0)) - query_embedding = self.query_embedding_norm(query_embedding) - # Allow the template embedder to see the query embedding. Note this - # contains the position relative feature, so this is how the network knows - # which residues are next to each other. - to_concat.append((query_embedding, 1)) - - act = 0 - for i, (x, _) in enumerate(to_concat): - act += self.template_pair_embedding[i](x) - return act diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/triangle.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/triangle.py deleted file mode 100644 index ed60525db..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/diffusion/triangle.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Triangle""" -import numpy as np -import mindspore as ms -from mindspore import nn, ops -import mindspore.common.dtype as mstype -from mindspore import Parameter, mint -from mindspore.common.tensor import Tensor -from mindspore.ops import operations as P -from mindspore.common.initializer import initializer -from alphafold3.utils.gated_linear_unit import gated_linear_unit -from alphafold3.model.components.base_modules import LayerNorm, CustomDense -from mindscience.common.memory_reduce import _memory_reduce -from mindscience.common.initializer import lecun_init -from mindscience.models.layers.mask import MaskedLayerNorm -from mindscience.e3nn.utils import Ncon - - -class TriangleMultiplication(nn.Cell): - r""" - Triangle multiplication layer. for the detailed implementation process, refer to - `TriangleMultiplication `_. - - The information between the amino acid pair is integrated through the information of three edges ij, ik, jk, and - the result of the dot product between ik and jk is added to the edge of ij. - - Args: - num_intermediate_channel (float): The number of intermediate channel. - equation (str): The equation used in triangle multiplication layer. edge update forms - corresponding to 'incoming' and 'outgoing', - :math:`(ikc,jkc->ijc, kjc,kic->ijc)`. - layer_norm_dim (int): The last dimension length of the layer norm. - batch_size (int): The batch size of parameters in triangle multiplication. Default: ``None``. - - Inputs: - - **pair_act** (Tensor) - Tensor of pair_act. shape :math:`(N{res}, N{res}, layer\_norm\_dim)`. - - **pair_mask** (Tensor) - The mask for TriangleAttention matrix with shape. shape :math:`(N{res}, N{res})`. - - **index** (Tensor) - The index of while loop, only used in case of while control - flow. - - Outputs: - Tensor, the float tensor of the pair_act of the layer with shape :math:`(N{res}, N{res}, layer\_norm\_dim)`. - - Supported Platforms: - ``Ascend`` - """ - - def __init__(self, config, global_config, num_intermediate_channel, equation, normalized_shape, - batch_size=None, dtype=ms.float32): - super().__init__() - self.config = config - self.global_config = global_config - self.num_intermediate_channel = num_intermediate_channel - self.left_norm_input = LayerNorm(normalized_shape, dtype=ms.float32) - self.center_norm = LayerNorm(normalized_shape, dtype=ms.float32) - self.projection = nn.Dense( - normalized_shape[-1], num_intermediate_channel * 2, has_bias=False, dtype=dtype) - self.gate = nn.Dense(normalized_shape[-1], num_intermediate_channel * 2, - weight_init=self.global_config.final_init, has_bias=False, dtype=dtype) - self.output_projection = CustomDense( - normalized_shape[-1], num_intermediate_channel, weight_init=self.global_config.final_init, - ndim=3, dtype=dtype) - self.gating_linear = CustomDense( - num_intermediate_channel, num_intermediate_channel, weight_init=self.global_config.final_init, - ndim=3, dtype=dtype) - self.weight_glu = mint.stack( - [self.gate.weight.T, self.projection.weight.T], dim=1) - if self.config.equation == "ikc,jkc->ijc": - ncon_list = [[-1, -2, 1], [-1, -3, 1]] - elif self.config.equation == "kjc,kic->ijc": - ncon_list = [[-1, 1, -3], [-1, 1, -2]] - else: - raise ValueError("Not support this equation.") - self.ncon = Ncon(ncon_list) - - def construct(self, act, mask, use_glu=True): - r""" - Builds triangle multiplication module. - - Args: - act(Tensor): Pair activations. Data type is float. - mask(Tensor): Pair mask. Data type is float. - - Returns: - act(Tensor), the shape is same as act_shape[:-1]. - """ - self.weight_glu = mint.stack( - [self.gate.weight.T, self.projection.weight.T], dim=1) - - mask = mask[None, ...] - act = self.left_norm_input(act) - input_act = act - - if use_glu is True: - projection = gated_linear_unit( - x=act, - weight=self.weight_glu, - activation=ms.mint.sigmoid, - ) - projection = ops.transpose(projection, (2, 0, 1)) - projection *= mask - else: - projection = self.projection(act) - projection = ops.transpose(projection, (2, 0, 1)) - projection *= mask - gate = self.gate(act) - gate = ops.transpose(gate, (2, 0, 1)) - projection *= ms.mint.sigmoid(gate) - projection = projection.reshape( - self.num_intermediate_channel, 2, *projection.shape[1:]) - a, b = projection[:, 0], projection[:, 1] - act = self.ncon([a, b]) - act = self.center_norm(act.transpose((1, 2, 0))) - act = self.output_projection(act) - gate_out = self.gating_linear(input_act) - act *= mint.sigmoid(gate_out) - return act - - -class OuterProductMean(nn.Cell): - r""" - Computing the correlation of the input tensor along its second dimension, the computed correlation - could be used to update the correlation features(e.g. the Pair representation). - - .. math:: - OuterProductMean(\mathbf{act}) = Linear(flatten(mean(\mathbf{act}\otimes\mathbf{act}))) - - Args: - num_outer_channel (float): The last dimension size of intermediate layer in OuterProductMean. - act_dim (int): The last dimension size of the input act. - num_output_channel (int): The last dimension size of output. - batch_size(int): The batch size of parameters in OuterProductMean, - used in while control flow. Default: "None". - slice_num (int): The slice num used in OuterProductMean layer - when the memory is overflow. Default: 0. - - Inputs: - - **act** (Tensor) - The input tensor with shape :math:`(dim_1, dim_2, act\_dim)`. - - **mask** (Tensor) - The mask for OuterProductMean with shape :math:`(dim_1, dim_2)`. - - **mask_norm** (Tensor) - Squared L2-norm along the first dimension of **mask**, - pre-computed to avoid re-computing, its shape is :math:`(dim_2, dim_2, 1)`. - - **index** (Tensor) - The index of while loop, only used in case of while control - flow. Default: "None". - - Outputs: - Tensor, the float tensor of the output of OuterProductMean layer with - shape :math:`(dim_2, dim_2, num\_output\_channel)`. - - Supported Platforms: - ``Ascend`` - """ - - def __init__(self, num_outer_channel, act_dim, num_output_channel, batch_size=None, slice_num=0, dtype=ms.float32): - super().__init__() - self.dtype = dtype - self.num_output_channel = num_output_channel - self.num_outer_channel = num_outer_channel - self.layer_norm_input = MaskedLayerNorm() - self.matmul_trans_b = P.MatMul(transpose_b=True) - self.matmul = P.MatMul() - self.batch_matmul_trans_b = P.BatchMatMul(transpose_b=True) - self.act_dim = act_dim - self.batch_size = batch_size - self.slice_num = slice_num - self.idx = Tensor(0, mstype.int32) - self._init_parameter() - - def construct(self, act, mask, mask_norm, index=None): - """Compute outer product mean.""" - mask = P.ExpandDims()(mask, -1) - act = self.layer_norm_input( - act, self.layer_norm_input_gamma, self.layer_norm_input_beta) - act_shape = P.Shape()(act) - if len(act_shape) != 2: - act = P.Reshape()(act, (-1, act_shape[-1])) - out_shape = act_shape[:-1] + (-1,) - left_act = mask * P.Reshape()( - P.BiasAdd()(self.matmul_trans_b(act, self.left_projection_weight), self.left_projection_bias), out_shape) - right_act = mask * P.Reshape()( - P.BiasAdd()(self.matmul_trans_b(act, self.right_projection_weight), self.right_projection_bias), out_shape) - _, d, e = right_act.shape - batched_inputs = (left_act,) - nonbatched_inputs = (right_act, self.linear_output_weight, - self.o_biases, d, e) - act = _memory_reduce(self._compute, batched_inputs, - nonbatched_inputs, self.slice_num, 1) - epsilon = 1e-3 - act = P.RealDiv()(act, epsilon + mask_norm) - return act - - def _init_parameter(self): - '''init parameter''' - self.layer_norm_input_gamma = Parameter( - Tensor(np.ones((self.act_dim)), self.dtype)) - self.layer_norm_input_beta = Parameter( - Tensor(np.zeros((self.act_dim)), self.dtype)) - self.left_projection_weight = Parameter( - initializer(lecun_init(self.act_dim), [self.num_outer_channel, self.act_dim], self.dtype)) - self.left_projection_bias = Tensor( - np.zeros((self.num_outer_channel)), self.dtype) - self.right_projection_weight = Parameter( - initializer(lecun_init(self.act_dim), [self.num_outer_channel, self.act_dim], self.dtype)) - self.right_projection_bias = Tensor( - np.zeros((self.num_outer_channel)), self.dtype) - self.linear_output_weight = Parameter( - Tensor(np.zeros((self.num_outer_channel, self.num_outer_channel, self.num_output_channel)), - self.dtype)) - self.o_biases = Parameter( - Tensor(np.zeros((self.num_output_channel)), self.dtype)) - - def _compute(self, left_act, right_act, linear_output_weight, linear_output_bias, d, e): - '''compute outer product mean''' - - left_act = left_act.transpose((0, 2, 1)) - act = Ncon([[1, -2, -4], [1, -1, -3]])([left_act, right_act]) - act = Ncon([[-1, 1, 2, -2], [1, 2, -3]] - )([act, linear_output_weight]) + linear_output_bias - act = P.Transpose()(act, (1, 0, 2)) - return act diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/feat_batch.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/feat_batch.py deleted file mode 100644 index a2bf31edf..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/feat_batch.py +++ /dev/null @@ -1,184 +0,0 @@ -# Copyright 2025 Huawei Technologies Co., Ltd -# -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Batch dataclass.""" - -import dataclasses -from typing import Self -import mindspore as ms -from mindspore import Tensor -from alphafold3.model import features - - -@dataclasses.dataclass -class Batch: - """Dataclass containing batch.""" - - msa: features.MSA - templates: features.Templates - token_features: features.TokenFeatures - ref_structure: features.RefStructure - predicted_structure_info: features.PredictedStructureInfo - polymer_ligand_bond_info: features.PolymerLigandBondInfo - ligand_ligand_bond_info: features.LigandLigandBondInfo - pseudo_beta_info: features.PseudoBetaInfo - atom_cross_att: features.AtomCrossAtt - convert_model_output: features.ConvertModelOutput - frames: features.Frames - - @property - def num_res(self) -> int: - """Number of residues.""" - return self.token_features.aatype.shape[-1] - - @staticmethod - def gather_to_tensor(input_feat): - """Convert gather indices to tensor.""" - input_feat.gather_idxs = Tensor(input_feat.gather_idxs) - input_feat.gather_mask = Tensor(input_feat.gather_mask) - input_feat.input_shape = Tensor(input_feat.input_shape) - - @classmethod - def from_data_dict(cls, batch: features.BatchDict) -> Self: - """Construct batch object from dictionary.""" - return cls( - msa=features.MSA.from_data_dict(batch), - templates=features.Templates.from_data_dict(batch), - token_features=features.TokenFeatures.from_data_dict(batch), - ref_structure=features.RefStructure.from_data_dict(batch), - predicted_structure_info=features.PredictedStructureInfo.from_data_dict( - batch - ), - polymer_ligand_bond_info=features.PolymerLigandBondInfo.from_data_dict( - batch - ), - ligand_ligand_bond_info=features.LigandLigandBondInfo.from_data_dict( - batch - ), - pseudo_beta_info=features.PseudoBetaInfo.from_data_dict(batch), - atom_cross_att=features.AtomCrossAtt.from_data_dict(batch), - convert_model_output=features.ConvertModelOutput.from_data_dict( - batch), - frames=features.Frames.from_data_dict(batch), - ) - - def as_data_dict(self) -> features.BatchDict: - """Converts batch object to dictionary.""" - output = { - **self.msa.as_data_dict(), - **self.templates.as_data_dict(), - **self.token_features.as_data_dict(), - **self.ref_structure.as_data_dict(), - **self.predicted_structure_info.as_data_dict(), - **self.polymer_ligand_bond_info.as_data_dict(), - **self.ligand_ligand_bond_info.as_data_dict(), - **self.pseudo_beta_info.as_data_dict(), - **self.atom_cross_att.as_data_dict(), - **self.convert_model_output.as_data_dict(), - **self.frames.as_data_dict(), - } - return output - - def convert_to_tensor(self, dtype=ms.float32): - """Convert all fields to tensor.""" - # msa: features.MSA - self.msa.rows = Tensor(self.msa.rows, dtype=ms.int32) - self.msa.mask = Tensor(self.msa.mask, dtype=ms.int32) - self.msa.deletion_matrix = Tensor( - self.msa.deletion_matrix, dtype=dtype) - self.msa.deletion_mean = Tensor(self.msa.deletion_mean, dtype=dtype) - self.msa.profile = Tensor(self.msa.profile, dtype=dtype) - self.msa.num_alignments = Tensor( - self.msa.num_alignments, dtype=ms.int32) - # templates: features.Templates - self.templates.aatype = Tensor(self.templates.aatype, dtype=ms.int32) - self.templates.atom_mask = Tensor( - self.templates.atom_mask, dtype=ms.int32) - self.templates.atom_positions = Tensor( - self.templates.atom_positions, dtype=dtype) - # token_features: features.TokenFeatures - self.token_features.mask = Tensor( - self.token_features.mask, dtype=ms.int32) - self.token_features.token_index = Tensor( - self.token_features.mask, dtype=ms.int32) - self.token_features.asym_id = Tensor( - self.token_features.asym_id, dtype=ms.int32) - self.token_features.aatype = Tensor( - self.token_features.aatype, dtype=ms.int32) - self.token_features.residue_index = Tensor( - self.token_features.residue_index, dtype=ms.int32) - self.token_features.entity_id = Tensor( - self.token_features.entity_id, dtype=ms.int32) - self.token_features.sym_id = Tensor( - self.token_features.sym_id, dtype=ms.int32) - # ref_structure: features.RefStructure - self.ref_structure.positions = Tensor( - self.ref_structure.positions, dtype=dtype) - self.ref_structure.mask = Tensor(self.ref_structure.mask, dtype=dtype) - self.ref_structure.element = Tensor( - self.ref_structure.element, dtype=ms.int32) - self.ref_structure.charge = Tensor( - self.ref_structure.charge, dtype=dtype) - self.ref_structure.atom_name_chars = Tensor( - self.ref_structure.atom_name_chars, dtype=ms.int32) - self.ref_structure.ref_space_uid = Tensor( - self.ref_structure.ref_space_uid, dtype=dtype) - - # predicted_structure_info: features.PredictedStructureInfo - self.predicted_structure_info.atom_mask = Tensor( - self.predicted_structure_info.atom_mask, dtype=dtype) - - # polymer_ligand_bond_info: features.PolymerLigandBondInfo - self.polymer_ligand_bond_info.tokens_to_polymer_ligand_bonds.gather_idxs = Tensor( - self.polymer_ligand_bond_info.tokens_to_polymer_ligand_bonds.gather_idxs, dtype=ms.int32 - ) - self.polymer_ligand_bond_info.tokens_to_polymer_ligand_bonds.gather_mask = Tensor( - self.polymer_ligand_bond_info.tokens_to_polymer_ligand_bonds.gather_mask, dtype=ms.int32 - ) - # ligand_ligand_bond_info: features.LigandLigandBondInfo - self.ligand_ligand_bond_info.tokens_to_ligand_ligand_bonds.gather_idxs = Tensor( - self.ligand_ligand_bond_info.tokens_to_ligand_ligand_bonds.gather_idxs, dtype=ms.int32 - ) - self.ligand_ligand_bond_info.tokens_to_ligand_ligand_bonds.gather_mask = Tensor( - self.ligand_ligand_bond_info.tokens_to_ligand_ligand_bonds.gather_mask, dtype=ms.int32 - ) - - self.gather_to_tensor(self.pseudo_beta_info.token_atoms_to_pseudo_beta) - self.gather_to_tensor(self.atom_cross_att.queries_to_keys) - self.gather_to_tensor(self.atom_cross_att.tokens_to_queries) - self.gather_to_tensor(self.atom_cross_att.tokens_to_keys) - self.gather_to_tensor(self.atom_cross_att.token_atoms_to_queries) - self.gather_to_tensor(self.atom_cross_att.queries_to_token_atoms) - - # frames: features.Frames - - def astype(self, dtype=ms.float32): - """Change dtype of float.""" - # change dtype of float - # msa: features.MSA - self.msa.deletion_matrix = self.msa.deletion_matrix.astype(dtype) - self.msa.deletion_mean = self.msa.deletion_mean.astype(dtype) - self.msa.profile = self.msa.profile.astype(dtype) - # templates: features.Templates - self.templates.atom_positions = self.templates.atom_positions.astype( - dtype) - # ref_structure: features.RefStructure - self.ref_structure.positions = self.ref_structure.positions.astype( - dtype) - self.ref_structure.mask = self.ref_structure.mask.astype(dtype) - self.ref_structure.charge = self.ref_structure.charge.astype(dtype) - self.ref_structure.ref_space_uid = self.ref_structure.ref_space_uid.astype( - dtype) - - # predicted_structure_info: features.PredictedStructureInfo - self.predicted_structure_info.atom_mask = self.predicted_structure_info.atom_mask.astype( - dtype) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/features.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/features.py deleted file mode 100644 index 65ac814e2..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/features.py +++ /dev/null @@ -1,2081 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Data-side of the input features processing.""" - -import dataclasses -import datetime -import itertools -from typing import Optional - -import numpy as np -from typing_extensions import Any, Self, TypeAlias -from rdkit import Chem -from rdkit.Chem import AllChem -from absl import logging -from alphafold3 import structure -from alphafold3.common import folding_input -from alphafold3.constants import chemical_components -from alphafold3.constants import mmcif_names -from alphafold3.constants import periodic_table -from alphafold3.constants import residue_names -from alphafold3.data import msa as msa_module -from alphafold3.data import templates -from alphafold3.data.tools import rdkit_utils -from alphafold3.model import data3 -from alphafold3.model import data_constants -from alphafold3.model import merging_features -from alphafold3.model import msa_pairing -from alphafold3.model.atom_layout import atom_layout -from alphafold3.structure import chemical_components as struc_chem_comps - - -xnp_ndarray: TypeAlias = np.ndarray # pylint: disable=invalid-name -BatchDict: TypeAlias = dict[str, xnp_ndarray] - -_STANDARD_RESIDUES = frozenset({ - *residue_names.PROTEIN_TYPES_WITH_UNKNOWN, - *residue_names.NUCLEIC_TYPES_WITH_2_UNKS, -}) - - -@dataclasses.dataclass -class PaddingShapes: - num_tokens: int - msa_size: int - num_chains: int - num_templates: int - num_atoms: int - - -def _pad_to( - arr: np.ndarray, shape: tuple[Optional[int], ...], **kwargs -) -> np.ndarray: - """Pads an array to a given shape. Wrapper around np.pad(). - - Args: - arr: numpy array to pad - shape: target shape, use None for axes that should stay the same - **kwargs: additional args for np.pad, e.g. constant_values=-1 - - Returns: - the padded array - - Raises: - ValueError if arr and shape have a different number of axes. - """ - if arr.ndim != len(shape): - raise ValueError( - f'arr and shape have different number of axes. {arr.shape=}, {shape=}' - ) - - num_pad = [] - for axis, width in enumerate(shape): - if width is None: - num_pad.append((0, 0)) - else: - if width >= arr.shape[axis]: - num_pad.append((0, width - arr.shape[axis])) - else: - raise ValueError( - f'Can not pad to a smaller shape. {arr.shape=}, {shape=}' - ) - padded_arr = np.pad(arr, pad_width=num_pad, **kwargs) - return padded_arr - - -def _unwrap(obj): - """Unwrap an object from a zero-dim np.ndarray.""" - if isinstance(obj, np.ndarray) and obj.ndim == 0: - return obj.item() - return obj - - -@dataclasses.dataclass -class Chains: - chain_id: np.ndarray - asym_id: np.ndarray - entity_id: np.ndarray - sym_id: np.ndarray - - -def _compute_asym_entity_and_sym_id( - all_tokens: atom_layout.AtomLayout, -) -> Chains: - """Compute asym_id, entity_id and sym_id. - - Args: - all_tokens: atom layout containing a representative atom for each token. - - Returns: - A Chains object - """ - - # Find identical sequences and assign entity_id and sym_id to every chain. - seq_to_entity_id_sym_id = {} - seen_chain_ids = set() - chain_ids = [] - asym_ids = [] - entity_ids = [] - sym_ids = [] - for chain_id in all_tokens.chain_id: - if chain_id not in seen_chain_ids: - asym_id = len(seen_chain_ids) + 1 - seen_chain_ids.add(chain_id) - seq = ','.join( - all_tokens.res_name[all_tokens.chain_id == chain_id]) - entity_id, sym_id = seq_to_entity_id_sym_id.get(seq, (len(seq_to_entity_id_sym_id) + 1, 0)) - sym_id += 1 - seq_to_entity_id_sym_id[seq] = (entity_id, sym_id) - - chain_ids.append(chain_id) - asym_ids.append(asym_id) - entity_ids.append(entity_id) - sym_ids.append(sym_id) - - return Chains( - chain_id=np.array(chain_ids), - asym_id=np.array(asym_ids), - entity_id=np.array(entity_ids), - sym_id=np.array(sym_ids), - ) - - -def tokenizer( - flat_output_layout: atom_layout.AtomLayout, - ccd: chemical_components.Ccd, - max_atoms_per_token: int, - flatten_non_standard_residues: bool, - logging_name: str, -) -> tuple[atom_layout.AtomLayout, atom_layout.AtomLayout, np.ndarray]: - """Maps a flat atom layout to tokens for evoformer. - - Creates the evoformer tokens as one token per polymer residue and one token - per ligand atom. The tokens are represented as AtomLayouts all_tokens - (1 representative atom per token) atoms per residue, and - all_token_atoms_layout (num_tokens, max_atoms_per_token). The atoms in a - residue token use the layout of the corresponding CCD entry - - Args: - flat_output_layout: flat AtomLayout containing all atoms that the model - wants to predict. - ccd: The chemical components dictionary. - max_atoms_per_token: number of slots per token. - flatten_non_standard_residues: whether to flatten non-standard residues, - i.e. whether to use one token per atom for non-standard residues. - logging_name: logging name for debugging (usually the mmcif_id). - - Returns: - A tuple (all_tokens, all_tokens_atoms_layout) with - all_tokens: AtomLayout shape (num_tokens,) containing one representative - atom per token. - all_token_atoms_layout: AtomLayout with shape - (num_tokens, max_atoms_per_token) containing all atoms per token. - standard_token_idxs: The token index that each token would have if not - flattening non standard resiudes. - """ - # Select the representative atom for each token. - token_idxs = [] - single_atom_token = [] - standard_token_idxs = [] - current_standard_token_id = 0 - # Iterate over residues, and provide a group_iter over the atoms of each - # residue. - for key, group_iter in itertools.groupby( - zip( - flat_output_layout.chain_type, - flat_output_layout.chain_id, - flat_output_layout.res_id, - flat_output_layout.res_name, - flat_output_layout.atom_name, - np.arange(flat_output_layout.shape[0]), - ), - key=lambda x: x[:3], - ): - - # Get chain type and chain id of this residue - chain_type, chain_id, _ = key - - # Get names and global idxs for all atoms of this residue - _, _, _, res_names, atom_names, idxs = zip(*group_iter) - - # As of March 2023, all OTHER CHAINs in pdb are artificial nucleics. - is_nucleic_backbone = ( - chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES - or chain_type == mmcif_names.OTHER_CHAIN - ) - if chain_type in mmcif_names.PEPTIDE_CHAIN_TYPES: - res_name = res_names[0] - if ( - flatten_non_standard_residues - and res_name not in residue_names.PROTEIN_TYPES_WITH_UNKNOWN - and res_name != residue_names.MSE - ): - # For non-standard protein residues take all atoms. - # NOTE: This may get very large if we include hydrogens. - token_idxs.extend(idxs) - single_atom_token += [True] * len(idxs) - standard_token_idxs.extend( - [current_standard_token_id] * len(idxs)) - else: - # For standard protein residues take 'CA' if it exists, else first atom. - if 'CA' in atom_names: - token_idxs.append(idxs[atom_names.index('CA')]) - else: - token_idxs.append(idxs[0]) - single_atom_token += [False] - standard_token_idxs.append(current_standard_token_id) - current_standard_token_id += 1 - elif is_nucleic_backbone: - res_name = res_names[0] - if ( - flatten_non_standard_residues - and res_name not in residue_names.NUCLEIC_TYPES_WITH_2_UNKS - ): - # For non-standard nucleic residues take all atoms. - token_idxs.extend(idxs) - single_atom_token += [True] * len(idxs) - standard_token_idxs.extend( - [current_standard_token_id] * len(idxs)) - else: - # For standard nucleic residues take C1' if it exists, else first atom. - if "C1'" in atom_names: - token_idxs.append(idxs[atom_names.index("C1'")]) - else: - token_idxs.append(idxs[0]) - single_atom_token += [False] - standard_token_idxs.append(current_standard_token_id) - current_standard_token_id += 1 - elif chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES: - # For non-polymers take all atoms - token_idxs.extend(idxs) - single_atom_token += [True] * len(idxs) - standard_token_idxs.extend([current_standard_token_id] * len(idxs)) - current_standard_token_id += len(idxs) - else: - # Chain type that we don't handle yet. - logging.warning( - '%s: ignoring chain %s with chain type %s.', - logging_name, - chain_id, - chain_type, - ) - - standard_token_idxs = np.array(standard_token_idxs, dtype=np.int32) - - # Create the list of all tokens, represented as a flat AtomLayout with 1 - # representative atom per token. - all_tokens = flat_output_layout[token_idxs] - - # Create the 2D atoms_per_token layout - num_tokens = all_tokens.shape[0] - - # Target lists. - target_atom_names = [] - target_atom_elements = [] - target_res_ids = [] - target_res_names = [] - target_chain_ids = [] - target_chain_types = [] - - # uids of all atoms in the flat layout, to check whether the dense atoms - # exist -- This is necessary for terminal atoms (e.g. 'OP3' or 'OXT') - all_atoms_uids = set( - zip( - flat_output_layout.chain_id, - flat_output_layout.res_id, - flat_output_layout.atom_name, - ) - ) - - for idx, single_atom in enumerate(single_atom_token): - if not single_atom: - # Standard protein and nucleic residues have many atoms per token - chain_id = all_tokens.chain_id[idx] - res_id = all_tokens.res_id[idx] - res_name = all_tokens.res_name[idx] - atom_names = [] - atom_elements = [] - - res_atoms = struc_chem_comps.get_all_atoms_in_entry( - ccd=ccd, res_name=res_name - ) - atom_names_elements = list( - zip( - res_atoms['_chem_comp_atom.atom_id'], - res_atoms['_chem_comp_atom.type_symbol'], - strict=True, - ) - ) - - for atom_name, atom_element in atom_names_elements: - # Remove hydrogens if they are not in flat layout. - if atom_element in ['H', 'D'] and ( - (chain_id, res_id, atom_name) not in all_atoms_uids - ): - continue - if (chain_id, res_id, atom_name) in all_atoms_uids: - atom_names.append(atom_name) - atom_elements.append(atom_element) - # Leave spaces for OXT etc. - else: - atom_names.append('') - atom_elements.append('') - - if len(atom_names) > max_atoms_per_token: - logging.warning( - 'Atom list for chain %s ' - 'residue %s %s is too long and will be truncated: ' - '%s to the max atoms limit %s. Dropped atoms: %s', - chain_id, - res_id, - res_name, - len(atom_names), - max_atoms_per_token, - list( - zip( - atom_names[max_atoms_per_token:], - atom_elements[max_atoms_per_token:], - strict=True, - ) - ), - ) - atom_names = atom_names[:max_atoms_per_token] - atom_elements = atom_elements[:max_atoms_per_token] - - num_pad = max_atoms_per_token - len(atom_names) - atom_names.extend([''] * num_pad) - atom_elements.extend([''] * num_pad) - - else: - # ligands have only 1 atom per token - padding = [''] * (max_atoms_per_token - 1) - atom_names = [all_tokens.atom_name[idx]] + padding - atom_elements = [all_tokens.atom_element[idx]] + padding - - # Append the atoms to the target lists. - target_atom_names.append(atom_names) - target_atom_elements.append(atom_elements) - target_res_names.append( - [all_tokens.res_name[idx]] * max_atoms_per_token) - target_res_ids.append([all_tokens.res_id[idx]] * max_atoms_per_token) - target_chain_ids.append( - [all_tokens.chain_id[idx]] * max_atoms_per_token) - target_chain_types.append( - [all_tokens.chain_type[idx]] * max_atoms_per_token - ) - - # Make sure to get the right shape also for 0 tokens - trg_shape = (num_tokens, max_atoms_per_token) - all_token_atoms_layout = atom_layout.AtomLayout( - atom_name=np.array(target_atom_names, dtype=object).reshape(trg_shape), - atom_element=np.array(target_atom_elements, dtype=object).reshape( - trg_shape - ), - res_name=np.array(target_res_names, dtype=object).reshape(trg_shape), - res_id=np.array(target_res_ids, dtype=int).reshape(trg_shape), - chain_id=np.array(target_chain_ids, dtype=object).reshape(trg_shape), - chain_type=np.array(target_chain_types, - dtype=object).reshape(trg_shape), - ) - - return all_tokens, all_token_atoms_layout, standard_token_idxs - - -@dataclasses.dataclass -class MSA: - """Dataclass containing MSA.""" - - rows: xnp_ndarray - mask: xnp_ndarray - deletion_matrix: xnp_ndarray - # Occurrence of each residue type along the sequence, averaged over MSA rows. - profile: xnp_ndarray - # Occurrence of deletions along the sequence, averaged over MSA rows. - deletion_mean: xnp_ndarray - # Number of MSA alignments. - num_alignments: xnp_ndarray - - @classmethod - def compute_features( - cls, - *, - all_tokens: atom_layout.AtomLayout, - standard_token_idxs: np.ndarray, - padding_shapes: PaddingShapes, - fold_input: folding_input.Input, - logging_name: str, - max_paired_sequence_per_species: int, - ) -> Self: - """Compute the msa features.""" - seen_entities = {} - - substruct = atom_layout.make_structure( - flat_layout=all_tokens, - atom_coords=np.zeros(all_tokens.shape + (3,)), - name=logging_name, - ) - prot = substruct.filter_to_entity_type(protein=True) - num_unique_chains = len( - set(prot.chain_single_letter_sequence().values())) - need_msa_pairing = num_unique_chains > 1 - - np_chains_list = [] - input_chains_by_id = {chain.id: chain for chain in fold_input.chains} - nonempty_chain_ids = set(all_tokens.chain_id) - for asym_id, chain_info in enumerate(substruct.iter_chains(), start=1): - b_chain_id = chain_info['chain_id'] - chain_type = chain_info['chain_type'] - chain = input_chains_by_id[b_chain_id] - - # Generalised "sequence" for ligands (can't trust residue name) - chain_tokens = all_tokens[all_tokens.chain_id == b_chain_id] - three_letter_sequence = ','.join(chain_tokens.res_name.tolist()) - chain_num_tokens = len(chain_tokens.atom_name) - if chain_type in mmcif_names.POLYMER_CHAIN_TYPES: - sequence = substruct.chain_single_letter_sequence()[b_chain_id] - if chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES: - # Only allow nucleic residue types for nucleic chains (can have some - # protein residues in e.g. tRNA, but that causes MSA search failures). - # Replace non nucleic residue types by UNK_NUCLEIC. - nucleic_types_one_letter = ( - residue_names.DNA_TYPES_ONE_LETTER - + residue_names.RNA_TYPES_ONE_LETTER_WITH_UNKNOWN - ) - sequence = ''.join([ - base - if base in nucleic_types_one_letter - else residue_names.UNK_NUCLEIC_ONE_LETTER - for base in sequence - ]) - else: - sequence = 'X' * chain_num_tokens - - skip_chain = ( - chain_type not in mmcif_names.STANDARD_POLYMER_CHAIN_TYPES - or len(sequence) <= 4 - or b_chain_id not in nonempty_chain_ids - ) - - entity_id = seen_entities.get(three_letter_sequence, len(seen_entities) + 1) - - if chain_type in mmcif_names.STANDARD_POLYMER_CHAIN_TYPES: - unpaired_a3m = '' - paired_a3m = '' - if not skip_chain: - if need_msa_pairing and isinstance(chain, folding_input.ProteinChain): - paired_a3m = chain.paired_msa - if isinstance( - chain, folding_input.RnaChain | folding_input.ProteinChain - ): - unpaired_a3m = chain.unpaired_msa - unpaired_msa = msa_module.Msa.from_a3m( - query_sequence=sequence, - chain_poly_type=chain_type, - a3m=unpaired_a3m, - deduplicate=True, - ) - - paired_msa = msa_module.Msa.from_a3m( - query_sequence=sequence, - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - a3m=paired_a3m, - deduplicate=False, - ) - else: - unpaired_msa = msa_module.Msa.from_empty( - query_sequence='-' * len(sequence), - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - ) - paired_msa = msa_module.Msa.from_empty( - query_sequence='-' * len(sequence), - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - ) - - msa_features = unpaired_msa.featurize() - all_seqs_msa_features = paired_msa.featurize() - - msa_features = data3.fix_features(msa_features) - all_seqs_msa_features = data3.fix_features(all_seqs_msa_features) - - msa_features = msa_features | { - f'{k}_all_seq': v for k, v in all_seqs_msa_features.items() - } - feats = msa_features - feats['chain_id'] = b_chain_id - feats['asym_id'] = np.full(chain_num_tokens, asym_id) - feats['entity_id'] = entity_id - np_chains_list.append(feats) - - # Add profile features to each chain. - for chain in np_chains_list: - chain.update( - data3.get_profile_features( - chain['msa'], chain['deletion_matrix']) - ) - - # Allow 50% of the MSA to come from MSA pairing. - max_paired_sequences = padding_shapes.msa_size // 2 - if need_msa_pairing: - np_chains_list = list(map(dict, np_chains_list)) - np_chains_list = msa_pairing.create_paired_features( - np_chains_list, - max_paired_sequences=max_paired_sequences, - nonempty_chain_ids=nonempty_chain_ids, - max_hits_per_species=max_paired_sequence_per_species, - ) - np_chains_list = msa_pairing.deduplicate_unpaired_sequences( - np_chains_list - ) - - # Remove all gapped rows from all seqs. - nonempty_asym_ids = [] - for chain in np_chains_list: - if chain['chain_id'] in nonempty_chain_ids: - nonempty_asym_ids.append(chain['asym_id'][0]) - if 'msa_all_seq' in np_chains_list[0]: - np_chains_list = msa_pairing.remove_all_gapped_rows_from_all_seqs( - np_chains_list, asym_ids=nonempty_asym_ids - ) - - # Crop MSA rows. - cropped_chains_list = [] - for chain in np_chains_list: - unpaired_msa_size, paired_msa_size = ( - msa_pairing.choose_paired_unpaired_msa_crop_sizes( - unpaired_msa=chain['msa'], - paired_msa=chain.get('msa_all_seq'), - total_msa_crop_size=padding_shapes.msa_size, - max_paired_sequences=max_paired_sequences, - ) - ) - cropped_chain = { - 'asym_id': chain['asym_id'], - 'chain_id': chain['chain_id'], - 'profile': chain['profile'], - 'deletion_mean': chain['deletion_mean'], - } - for feat in data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES: - if feat in chain: - cropped_chain[feat] = chain[feat][:unpaired_msa_size] - if feat + '_all_seq' in chain: - cropped_chain[feat + '_all_seq'] = chain[feat + '_all_seq'][ - :paired_msa_size - ] - cropped_chains_list.append(cropped_chain) - - # Merge Chains. - np_example = { - 'asym_id': np.concatenate( - [c['asym_id'] for c in cropped_chains_list], axis=0 - ), - } - for feature in data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES: - for feat in [feature, feature + '_all_seq']: - if feat in cropped_chains_list[0]: - np_example[feat] = merging_features.merge_msa_features( - feat, cropped_chains_list - ) - for feature in ['profile', 'deletion_mean']: - feature_list = [c[feature] for c in cropped_chains_list] - np_example[feature] = np.concatenate(feature_list, axis=0) - - # Crop MSA rows to maximum size given by chains participating in the crop. - max_allowed_unpaired = max( - len(chain['msa']) - for chain in cropped_chains_list - if chain['asym_id'][0] in nonempty_asym_ids - ) - np_example['msa'] = np_example['msa'][:max_allowed_unpaired] - if 'msa_all_seq' in np_example: - max_allowed_paired = max( - len(chain['msa_all_seq']) - for chain in cropped_chains_list - if chain['asym_id'][0] in nonempty_asym_ids - ) - np_example['msa_all_seq'] = np_example['msa_all_seq'][:max_allowed_paired] - - np_example = merging_features.merge_paired_and_unpaired_msa(np_example) - - # Crop MSA residues. Need to use the standard token indices, since msa does - # not expand non-standard residues. This means that for expanded residues, - # we get repeated msa columns. - new_cropping_idxs = standard_token_idxs - for feature in data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES: - if feature in np_example: - np_example[feature] = np_example[feature][:, - new_cropping_idxs].copy() - for feature in ['profile', 'deletion_mean']: - np_example[feature] = np_example[feature][new_cropping_idxs] - - # Make MSA mask. - np_example['msa_mask'] = np.ones_like( - np_example['msa'], dtype=np.float32) - - # Count MSA size before padding. - num_alignments = np_example['msa'].shape[0] - - # Pad: - msa_size, num_tokens = padding_shapes.msa_size, padding_shapes.num_tokens - - def safe_cast_int8(x): - return np.clip(x, np.iinfo(np.int8).min, np.iinfo(np.int8).max).astype( - np.int8 - ) - - return MSA( - rows=_pad_to(safe_cast_int8( - np_example['msa']), (msa_size, num_tokens)), - mask=_pad_to( - np_example['msa_mask'].astype(bool), (msa_size, num_tokens) - ), - # deletion_matrix may be out of int8 range, but we mostly care about - # small values since we arctan it in the model. - deletion_matrix=_pad_to( - safe_cast_int8(np_example['deletion_matrix']), - (msa_size, num_tokens), - ), - profile=_pad_to(np_example['profile'], (num_tokens, None)), - deletion_mean=_pad_to(np_example['deletion_mean'], (num_tokens,)), - num_alignments=np.array(num_alignments, dtype=np.int32), - ) - - def index_msa_rows(self, indices: xnp_ndarray) -> Self: - return MSA( - rows=self.rows[indices, :], - mask=self.mask[indices, :], - deletion_matrix=self.deletion_matrix[indices, :], - profile=self.profile, - deletion_mean=self.deletion_mean, - num_alignments=self.num_alignments, - ) - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - """from data dict""" - output = cls( - rows=batch['msa'], - mask=batch['msa_mask'], - deletion_matrix=batch['deletion_matrix'], - profile=batch['profile'], - deletion_mean=batch['deletion_mean'], - num_alignments=batch['num_alignments'], - ) - return output - - def as_data_dict(self) -> BatchDict: - return { - 'msa': self.rows, - 'msa_mask': self.mask, - 'deletion_matrix': self.deletion_matrix, - 'profile': self.profile, - 'deletion_mean': self.deletion_mean, - 'num_alignments': self.num_alignments, - } - - -@dataclasses.dataclass -class Templates: - """Dataclass containing templates.""" - - # aatype of templates, int32 w shape [num_templates, num_res] - aatype: xnp_ndarray - # atom positions of templates, float32 w shape [num_templates, num_res, 24, 3] - atom_positions: xnp_ndarray - # atom mask of templates, bool w shape [num_templates, num_res, 24] - atom_mask: xnp_ndarray - def __getitem__(self, idx): - return Templates(self.aatype[idx], self.atom_positions[idx], self.atom_mask[idx]) - @classmethod - def compute_features( - cls, - all_tokens: atom_layout.AtomLayout, - standard_token_idxs: np.ndarray, - padding_shapes: PaddingShapes, - fold_input: folding_input.Input, - max_templates: int, - logging_name: str, - ) -> Self: - """Compute the template features.""" - - seen_entities = {} - polymer_entity_features = {True: {}, False: {}} - - substruct = atom_layout.make_structure( - flat_layout=all_tokens, - atom_coords=np.zeros(all_tokens.shape + (3,)), - name=logging_name, - ) - np_chains_list = [] - - input_chains_by_id = {chain.id: chain for chain in fold_input.chains} - - nonempty_chain_ids = set(all_tokens.chain_id) - for chain_info in substruct.iter_chains(): - chain_id = chain_info['chain_id'] - chain_type = chain_info['chain_type'] - chain = input_chains_by_id[chain_id] - - # Generalised "sequence" for ligands (can't trust residue name) - chain_tokens = all_tokens[all_tokens.chain_id == chain_id] - three_letter_sequence = ','.join(chain_tokens.res_name.tolist()) - chain_num_tokens = len(chain_tokens.atom_name) - - # Don't compute features for chains not included in the crop, or ligands. - skip_chain = ( - chain_type != mmcif_names.PROTEIN_CHAIN - or chain_num_tokens <= 4 # not cache filled - or chain_id not in nonempty_chain_ids - ) - - entity_id = seen_entities.get(three_letter_sequence, len(seen_entities) + 1) - - if entity_id not in polymer_entity_features[skip_chain]: - if skip_chain: - template_features = data3.empty_template_features( - chain_num_tokens) - else: - sorted_features = [] - for template in chain.templates: - struct = structure.from_mmcif( - template.mmcif, - fix_mse_residues=True, - fix_arginines=True, - include_bonds=False, - include_water=False, - # For non-standard polymer chains. - include_other=True, - ) - hit_features = templates.get_polymer_features( - chain=struct, - chain_poly_type=mmcif_names.PROTEIN_CHAIN, - query_sequence_length=len(chain.sequence), - query_to_hit_mapping=dict( - template.query_to_template_map), - ) - sorted_features.append(hit_features) - - template_features = templates.package_template_features( - hit_features=sorted_features, - include_ligand_features=False, - ) - - template_features = data3.fix_template_features( - sequence=chain.sequence, - template_features=template_features, - ) - - template_features = _reduce_template_features( - template_features, max_templates - ) - polymer_entity_features[skip_chain][entity_id] = template_features - - seen_entities[three_letter_sequence] = entity_id - feats = polymer_entity_features[skip_chain][entity_id].copy() - feats['chain_id'] = chain_id - np_chains_list.append(feats) - - # We pad the num_templates dimension before merging, so that different - # chains can be concatenated on the num_res dimension. Masking will be - # applied so that each chains templates can't see each other. - for chain in np_chains_list: - chain['template_aatype'] = _pad_to( - chain['template_aatype'], (max_templates, None) - ) - chain['template_atom_positions'] = _pad_to( - chain['template_atom_positions'], ( - max_templates, None, None, None) - ) - chain['template_atom_mask'] = _pad_to( - chain['template_atom_mask'], (max_templates, None, None) - ) - - # Merge on token dimension. - np_example = { - ft: np.concatenate([c[ft] for c in np_chains_list], axis=1) - for ft in np_chains_list[0] - if ft in data_constants.TEMPLATE_FEATURES - } - - # Crop template data. Need to use the standard token indices, since msa does - # not expand non-standard residues. This means that for expanded residues, - # we get repeated template information. - for feature_name, v in np_example.items(): - np_example[feature_name] = v[:max_templates, - standard_token_idxs, ...] - - # Pad along the token dimension. - templates_features = Templates( - aatype=_pad_to( - np_example['template_aatype'], (None, - padding_shapes.num_tokens) - ), - atom_positions=_pad_to( - np_example['template_atom_positions'], - (None, padding_shapes.num_tokens, None, None), - ), - atom_mask=_pad_to( - np_example['template_atom_mask'].astype(bool), - (None, padding_shapes.num_tokens, None), - ), - ) - return templates_features - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - """Make Template from batch dictionary.""" - return cls( - aatype=batch['template_aatype'], - atom_positions=batch['template_atom_positions'], - atom_mask=batch['template_atom_mask'], - ) - - def as_data_dict(self) -> BatchDict: - return { - 'template_aatype': self.aatype, - 'template_atom_positions': self.atom_positions, - 'template_atom_mask': self.atom_mask, - } - - -def _reduce_template_features( - template_features: data3.FeatureDict, - max_templates: int, -) -> data3.FeatureDict: - """Reduces template features to max num templates and defined feature set.""" - num_templates = template_features['template_aatype'].shape[0] - template_keep_mask = np.arange(num_templates) < max_templates - template_fields = data_constants.TEMPLATE_FEATURES + ( - 'template_release_timestamp', - ) - template_features = { - k: v[template_keep_mask] - for k, v in template_features.items() - if k in template_fields - } - return template_features - - -@dataclasses.dataclass -class TokenFeatures: - """Dataclass containing features for tokens.""" - - residue_index: xnp_ndarray - token_index: xnp_ndarray - aatype: xnp_ndarray - mask: xnp_ndarray - seq_length: xnp_ndarray - - # Chain symmetry identifiers - # for an A3B2 stoichiometry the meaning of these features is as follows: - # asym_id: 1 2 3 4 5 - # entity_id: 1 1 1 2 2 - # sym_id: 1 2 3 1 2 - asym_id: xnp_ndarray - entity_id: xnp_ndarray - sym_id: xnp_ndarray - - # token type features - is_protein: xnp_ndarray - is_rna: xnp_ndarray - is_dna: xnp_ndarray - is_ligand: xnp_ndarray - is_nonstandard_polymer_chain: xnp_ndarray - is_water: xnp_ndarray - - @classmethod - def compute_features( - cls, - all_tokens: atom_layout.AtomLayout, - padding_shapes: PaddingShapes, - ) -> Self: - """Compute the per-token features.""" - - residue_index = all_tokens.res_id.astype(np.int32) - - token_index = np.arange( - 1, len(all_tokens.atom_name) + 1).astype(np.int32) - - aatype = [] - for res_name, chain_type in zip(all_tokens.res_name, all_tokens.chain_type): - if chain_type in mmcif_names.POLYMER_CHAIN_TYPES: - res_name = mmcif_names.fix_non_standard_polymer_res( - res_name=res_name, chain_type=chain_type - ) - if ( - chain_type == mmcif_names.DNA_CHAIN - and res_name == residue_names.UNK_DNA - ): - res_name = residue_names.UNK_NUCLEIC_ONE_LETTER - elif chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES: - res_name = residue_names.UNK - else: - raise ValueError( - f'Chain type {chain_type} not polymer or ligand.') - aa = residue_names.POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP[res_name] - aatype.append(aa) - aatype = np.array(aatype, dtype=np.int32) - - mask = np.ones(all_tokens.shape[0], dtype=bool) - chains = _compute_asym_entity_and_sym_id(all_tokens) - m = dict(zip(chains.chain_id, chains.asym_id)) - asym_id = np.array([m[c] for c in all_tokens.chain_id], dtype=np.int32) - - m = dict(zip(chains.chain_id, chains.entity_id)) - entity_id = np.array([m[c] - for c in all_tokens.chain_id], dtype=np.int32) - - m = dict(zip(chains.chain_id, chains.sym_id)) - sym_id = np.array([m[c] for c in all_tokens.chain_id], dtype=np.int32) - - seq_length = np.array(all_tokens.shape[0], dtype=np.int32) - - is_protein = all_tokens.chain_type == mmcif_names.PROTEIN_CHAIN - is_rna = all_tokens.chain_type == mmcif_names.RNA_CHAIN - is_dna = all_tokens.chain_type == mmcif_names.DNA_CHAIN - is_ligand = np.isin( - all_tokens.chain_type, list(mmcif_names.LIGAND_CHAIN_TYPES) - ) - standard_polymer_chain = list(mmcif_names.NON_POLYMER_CHAIN_TYPES) + list( - mmcif_names.STANDARD_POLYMER_CHAIN_TYPES - ) - is_nonstandard_polymer_chain = np.isin( - all_tokens.chain_type, standard_polymer_chain, invert=True - ) - is_water = all_tokens.chain_type == mmcif_names.WATER - - return TokenFeatures( - residue_index=_pad_to(residue_index, (padding_shapes.num_tokens,)), - token_index=_pad_to(token_index, (padding_shapes.num_tokens,)), - aatype=_pad_to(aatype, (padding_shapes.num_tokens,)), - mask=_pad_to(mask, (padding_shapes.num_tokens,)), - asym_id=_pad_to(asym_id, (padding_shapes.num_tokens,)), - entity_id=_pad_to(entity_id, (padding_shapes.num_tokens,)), - sym_id=_pad_to(sym_id, (padding_shapes.num_tokens,)), - seq_length=seq_length, - is_protein=_pad_to(is_protein, (padding_shapes.num_tokens,)), - is_rna=_pad_to(is_rna, (padding_shapes.num_tokens,)), - is_dna=_pad_to(is_dna, (padding_shapes.num_tokens,)), - is_ligand=_pad_to(is_ligand, (padding_shapes.num_tokens,)), - is_nonstandard_polymer_chain=_pad_to( - is_nonstandard_polymer_chain, (padding_shapes.num_tokens,) - ), - is_water=_pad_to(is_water, (padding_shapes.num_tokens,)), - ) - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - return cls( - residue_index=batch['residue_index'], - token_index=batch['token_index'], - aatype=batch['aatype'], - mask=batch['seq_mask'], - entity_id=batch['entity_id'], - asym_id=batch['asym_id'], - sym_id=batch['sym_id'], - seq_length=batch['seq_length'], - is_protein=batch['is_protein'], - is_rna=batch['is_rna'], - is_dna=batch['is_dna'], - is_ligand=batch['is_ligand'], - is_nonstandard_polymer_chain=batch['is_nonstandard_polymer_chain'], - is_water=batch['is_water'], - ) - - def as_data_dict(self) -> BatchDict: - return { - 'residue_index': self.residue_index, - 'token_index': self.token_index, - 'aatype': self.aatype, - 'seq_mask': self.mask, - 'entity_id': self.entity_id, - 'asym_id': self.asym_id, - 'sym_id': self.sym_id, - 'seq_length': self.seq_length, - 'is_protein': self.is_protein, - 'is_rna': self.is_rna, - 'is_dna': self.is_dna, - 'is_ligand': self.is_ligand, - 'is_nonstandard_polymer_chain': self.is_nonstandard_polymer_chain, - 'is_water': self.is_water, - } - - -@dataclasses.dataclass -class PredictedStructureInfo: - """Contains information necessary to work with predicted structure.""" - - atom_mask: xnp_ndarray - residue_center_index: xnp_ndarray - - @classmethod - def compute_features( - cls, - all_tokens: atom_layout.AtomLayout, - all_token_atoms_layout: atom_layout.AtomLayout, - padding_shapes: PaddingShapes, - ) -> Self: - """Compute the PredictedStructureInfo features. - - Args: - all_tokens: flat AtomLayout with 1 representative atom per token, shape - (num_tokens,) - all_token_atoms_layout: AtomLayout for all atoms per token, shape - (num_tokens, max_atoms_per_token) - padding_shapes: padding shapes. - - Returns: - A PredictedStructureInfo object. - """ - atom_mask = _pad_to( - all_token_atoms_layout.atom_name.astype(bool), - (padding_shapes.num_tokens, None), - ) - residue_center_index = np.zeros( - padding_shapes.num_tokens, dtype=np.int32) - for idx in range(all_tokens.shape[0]): - repr_atom = all_tokens.atom_name[idx] - atoms = list(all_token_atoms_layout.atom_name[idx, :]) - if repr_atom in atoms: - residue_center_index[idx] = atoms.index(repr_atom) - else: - # Representative atoms can be missing if cropping the number of atoms - # per residue. - logging.warning( - 'The representative atom in all_tokens (%s) is not in ' - 'all_token_atoms_layout (%s)', - all_tokens[idx: idx + 1], - all_token_atoms_layout[idx, :], - ) - residue_center_index[idx] = 0 - return cls(atom_mask=atom_mask, residue_center_index=residue_center_index) - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - return cls( - atom_mask=batch['pred_dense_atom_mask'], - residue_center_index=batch['residue_center_index'], - ) - - def as_data_dict(self) -> BatchDict: - return { - 'pred_dense_atom_mask': self.atom_mask, - 'residue_center_index': self.residue_center_index, - } - - -@dataclasses.dataclass -class PolymerLigandBondInfo: - """Contains information about polymer-ligand bonds.""" - - tokens_to_polymer_ligand_bonds: atom_layout.GatherInfo - # Gather indices to convert from cropped dense atom layout to bonds layout - # (num_tokens, 2) - token_atoms_to_bonds: atom_layout.GatherInfo - - @classmethod - def compute_features( - cls, - all_tokens: atom_layout.AtomLayout, - all_token_atoms_layout: atom_layout.AtomLayout, - bond_layout: Optional[atom_layout.AtomLayout], - padding_shapes: PaddingShapes, - ) -> Self: - """Computes the InterChainBondInfo features. - - Args: - all_tokens: AtomLayout for tokens; shape (num_tokens,). - all_token_atoms_layout: Atom Layout for all atoms (num_tokens, - max_atoms_per_token) - bond_layout: Bond layout for polymer-ligand bonds. - padding_shapes: Padding shapes. - - Returns: - A PolymerLigandBondInfo object. - """ - - if bond_layout is not None: - # Must convert to list before calling np.isin, will not work raw. - peptide_types = list(mmcif_names.PEPTIDE_CHAIN_TYPES) - nucleic_types = list(mmcif_names.NUCLEIC_ACID_CHAIN_TYPES) + [ - mmcif_names.OTHER_CHAIN - ] - # These atom renames are so that we can use the atom layout code with - # all_tokens, which only has a single atom per token. - atom_names = bond_layout.atom_name.copy() - atom_names[np.isin(bond_layout.chain_type, peptide_types)] = 'CA' - atom_names[np.isin(bond_layout.chain_type, nucleic_types)] = "C1'" - adjusted_bond_layout = atom_layout.AtomLayout( - atom_name=atom_names, - res_id=bond_layout.res_id, - chain_id=bond_layout.chain_id, - chain_type=bond_layout.chain_type, - ) - # Remove bonds that are not in the crop. - cropped_tokens_to_bonds = atom_layout.compute_gather_idxs( - source_layout=all_tokens, target_layout=adjusted_bond_layout - ) - bond_is_in_crop = np.all( - cropped_tokens_to_bonds.gather_mask, axis=1 - ).astype(bool) - adjusted_bond_layout = adjusted_bond_layout[bond_is_in_crop, :] - else: - # Create layout with correct shape when bond_layout is None. - s = (0, 2) - adjusted_bond_layout = atom_layout.AtomLayout( - atom_name=np.array([], dtype=object).reshape(s), - res_id=np.array([], dtype=int).reshape(s), - chain_id=np.array([], dtype=object).reshape(s), - ) - adjusted_bond_layout = adjusted_bond_layout.copy_and_pad_to( - (padding_shapes.num_tokens, 2) - ) - tokens_to_polymer_ligand_bonds = atom_layout.compute_gather_idxs( - source_layout=all_tokens, target_layout=adjusted_bond_layout - ) - - # Stuff for computing the bond loss. - if bond_layout is not None: - # Pad to num_tokens (hoping that there are never more bonds than tokens). - padded_bond_layout = bond_layout.copy_and_pad_to( - (padding_shapes.num_tokens, 2) - ) - token_atoms_to_bonds = atom_layout.compute_gather_idxs( - source_layout=all_token_atoms_layout, target_layout=padded_bond_layout - ) - else: - token_atoms_to_bonds = atom_layout.GatherInfo( - gather_idxs=np.zeros( - (padding_shapes.num_tokens, 2), dtype=int), - gather_mask=np.zeros( - (padding_shapes.num_tokens, 2), dtype=bool), - input_shape=np.array(( - padding_shapes.num_tokens, - all_token_atoms_layout.shape[1], - )), - ) - - return cls( - tokens_to_polymer_ligand_bonds=tokens_to_polymer_ligand_bonds, - token_atoms_to_bonds=token_atoms_to_bonds, - ) - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - return cls( - tokens_to_polymer_ligand_bonds=atom_layout.GatherInfo.from_dict( - batch, key_prefix='tokens_to_polymer_ligand_bonds' - ), - token_atoms_to_bonds=atom_layout.GatherInfo.from_dict( - batch, key_prefix='token_atoms_to_polymer_ligand_bonds' - ), - ) - - def as_data_dict(self) -> BatchDict: - return { - **self.tokens_to_polymer_ligand_bonds.as_dict( - key_prefix='tokens_to_polymer_ligand_bonds' - ), - **self.token_atoms_to_bonds.as_dict( - key_prefix='token_atoms_to_polymer_ligand_bonds' - ), - } - - -@dataclasses.dataclass -class LigandLigandBondInfo: - """Contains information about the location of ligand-ligand bonds.""" - - tokens_to_ligand_ligand_bonds: atom_layout.GatherInfo - - @classmethod - def compute_features( - cls, - all_tokens: atom_layout.AtomLayout, - bond_layout: Optional[atom_layout.AtomLayout], - padding_shapes: PaddingShapes, - ) -> Self: - """Computes the InterChainBondInfo features. - - Args: - all_tokens: AtomLayout for tokens; shape (num_tokens,). - bond_layout: Bond layout for ligand-ligand bonds. - padding_shapes: Padding shapes. - - Returns: - A LigandLigandBondInfo object. - """ - - if bond_layout is not None: - # Discard any bonds that do not join to an existing atom. - keep_mask = [] - all_atom_ids = set( - zip( - all_tokens.chain_id, - all_tokens.res_id, - all_tokens.atom_name, - strict=True, - ) - ) - for chain_id, res_id, atom_name in zip( - bond_layout.chain_id, - bond_layout.res_id, - bond_layout.atom_name, - strict=True, - ): - atom_a = (chain_id[0], res_id[0], atom_name[0]) - atom_b = (chain_id[1], res_id[1], atom_name[1]) - if atom_a in all_atom_ids and atom_b in all_atom_ids: - keep_mask.append(True) - else: - keep_mask.append(False) - keep_mask = np.array(keep_mask).astype(bool) - bond_layout = bond_layout[keep_mask] - # Remove any bonds to Hydrogen atoms. - bond_layout = bond_layout[ - ~np.char.startswith(bond_layout.atom_name.astype(str), 'H').any( - axis=1 - ) - ] - atom_names = bond_layout.atom_name - adjusted_bond_layout = atom_layout.AtomLayout( - atom_name=atom_names, - res_id=bond_layout.res_id, - chain_id=bond_layout.chain_id, - chain_type=bond_layout.chain_type, - ) - else: - # Create layout with correct shape when bond_layout is None. - s = (0, 2) - adjusted_bond_layout = atom_layout.AtomLayout( - atom_name=np.array([], dtype=object).reshape(s), - res_id=np.array([], dtype=int).reshape(s), - chain_id=np.array([], dtype=object).reshape(s), - ) - # 10 x num_tokens as max_inter_bonds_ratio + max_intra_bonds_ration = 2.061. - adjusted_bond_layout = adjusted_bond_layout.copy_and_pad_to( - (padding_shapes.num_tokens * 10, 2) - ) - gather_idx = atom_layout.compute_gather_idxs( - source_layout=all_tokens, target_layout=adjusted_bond_layout - ) - return cls(tokens_to_ligand_ligand_bonds=gather_idx) - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - return cls( - tokens_to_ligand_ligand_bonds=atom_layout.GatherInfo.from_dict( - batch, key_prefix='tokens_to_ligand_ligand_bonds' - ) - ) - - def as_data_dict(self) -> BatchDict: - return { - **self.tokens_to_ligand_ligand_bonds.as_dict( - key_prefix='tokens_to_ligand_ligand_bonds' - ) - } - - -@dataclasses.dataclass -class PseudoBetaInfo: - """Contains information for extracting pseudo-beta and equivalent atoms.""" - - token_atoms_to_pseudo_beta: atom_layout.GatherInfo - - @classmethod - def compute_features( - cls, - all_token_atoms_layout: atom_layout.AtomLayout, - ccd: chemical_components.Ccd, - padding_shapes: PaddingShapes, - logging_name: str, - ) -> Self: - """Compute the PseudoBetaInfo features. - - Args: - all_token_atoms_layout: AtomLayout for all atoms per token, shape - (num_tokens, max_atoms_per_token) - ccd: The chemical components dictionary. - padding_shapes: padding shapes. - logging_name: logging name for debugging (usually the mmcif_id) - - Returns: - A PseudoBetaInfo object. - """ - token_idxs = [] - atom_idxs = [] - for token_idx in range(all_token_atoms_layout.shape[0]): - chain_type = all_token_atoms_layout.chain_type[token_idx, 0] - atom_names = list(all_token_atoms_layout.atom_name[token_idx, :]) - atom_idx = None - is_nucleic_backbone = ( - chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES - or chain_type == mmcif_names.OTHER_CHAIN - ) - if chain_type == mmcif_names.PROTEIN_CHAIN: - # Protein chains - if 'CB' in atom_names: - atom_idx = atom_names.index('CB') - elif 'CA' in atom_names: - atom_idx = atom_names.index('CA') - elif is_nucleic_backbone: - # RNA / DNA chains - res_name = all_token_atoms_layout.res_name[token_idx, 0] - cifdict = ccd.get(res_name) - if cifdict: - parent = cifdict['_chem_comp.mon_nstd_parent_comp_id'][0] - if parent != '?': - res_name = parent - if res_name in {'A', 'G', 'DA', 'DG'}: - if 'C4' in atom_names: - atom_idx = atom_names.index('C4') - else: - if 'C2' in atom_names: - atom_idx = atom_names.index('C2') - elif chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES: - # Ligands: there is only one atom per token - atom_idx = 0 - else: - logging.warning( - '%s: Unknown chain type for token %i. (%s)', - logging_name, - token_idx, - all_token_atoms_layout[token_idx: token_idx + 1], - ) - atom_idx = 0 - if atom_idx is None: - (valid_atom_idxs,) = np.nonzero( - all_token_atoms_layout.atom_name[token_idx, :] - ) - if valid_atom_idxs.shape[0] > 0: - atom_idx = valid_atom_idxs[0] - else: - atom_idx = 0 - logging.warning( - '%s token %i (%s), does not contain a pseudo-beta atom.' - 'Using first valid atom (%s) instead.', - logging_name, - token_idx, - all_token_atoms_layout[token_idx: token_idx + 1], - all_token_atoms_layout.atom_name[token_idx, atom_idx], - ) - - token_idxs.append(token_idx) - atom_idxs.append(atom_idx) - - pseudo_beta_layout = all_token_atoms_layout[token_idxs, atom_idxs] - pseudo_beta_layout = pseudo_beta_layout.copy_and_pad_to(( - padding_shapes.num_tokens, - )) - token_atoms_to_pseudo_beta = atom_layout.compute_gather_idxs( - source_layout=all_token_atoms_layout, target_layout=pseudo_beta_layout - ) - - return cls( - token_atoms_to_pseudo_beta=token_atoms_to_pseudo_beta, - ) - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - return cls( - token_atoms_to_pseudo_beta=atom_layout.GatherInfo.from_dict( - batch, key_prefix='token_atoms_to_pseudo_beta' - ), - ) - - def as_data_dict(self) -> BatchDict: - return { - **self.token_atoms_to_pseudo_beta.as_dict( - key_prefix='token_atoms_to_pseudo_beta' - ), - } - - -_DEFAULT_BLANK_REF = { - 'positions': np.zeros(3), - 'mask': 0, - 'element': 0, - 'charge': 0, - 'atom_name_chars': np.zeros(4), -} - - -def random_rotation(random_state: np.random.RandomState) -> np.ndarray: - # Create a random rotation (Gram-Schmidt orthogonalization of two - # random normal vectors) - v0, v1 = random_state.normal(size=(2, 3)) - e0 = v0 / np.maximum(1e-10, np.linalg.norm(v0)) - v1 = v1 - e0 * np.dot(v1, e0) - e1 = v1 / np.maximum(1e-10, np.linalg.norm(v1)) - e2 = np.cross(e0, e1) - return np.stack([e0, e1, e2]) - - -def random_augmentation( - positions: np.ndarray, - random_state: np.random.RandomState, -) -> np.ndarray: - """Center then apply random translation and rotation.""" - - center = np.mean(positions, axis=0) - rot = random_rotation(random_state) - positions_target = np.einsum('ij,kj->ki', rot, positions - center) - - translation = random_state.normal(size=(3,)) - positions_target = positions_target + translation - return positions_target - - -def get_reference( - res_name: str, - chemical_components_data: struc_chem_comps.ChemicalComponentsData, - ccd: chemical_components.Ccd, - random_state: np.random.RandomState, - ref_max_modified_date: datetime.date, - intra_ligand_ptm_bonds: bool, -) -> tuple[dict[str, Any], Any, Any]: - """Reference structure for residue from CCD or SMILES. - - Args: - res_name: ccd code of the residue. - chemical_components_data: ChemicalComponentsData for making ref structure. - ccd: The chemical components dictionary. - random_state: Numpy RandomState - ref_max_modified_date: date beyond which reference structures must not be - modefied. - intra_ligand_ptm_bonds: Whether to return intra ligand/ ptm bonds. - - Returns: - Mapping from atom names to features, from_atoms, dest_atoms. - """ - ccd_cif = ccd.get(res_name) - non_ccd_with_smiles = False - if not ccd_cif: - # If res name is non-CCD try to get SMILES from chem comp dict. - has_smiles = ( - chemical_components_data.chem_comp - and res_name in chemical_components_data.chem_comp - and chemical_components_data.chem_comp[res_name].pdbx_smiles - ) - if has_smiles: - non_ccd_with_smiles = True - else: - # If no SMILES or CCD, return empty dictionary. - return {}, None, None - - pos = [] - elements = [] - charges = [] - atom_names = [] - - mol_from_smiles = None # useless init to make pylint happy - if non_ccd_with_smiles: - smiles_string = chemical_components_data.chem_comp[res_name].pdbx_smiles - mol_from_smiles = Chem.MolFromSmiles(smiles_string) - if mol_from_smiles is None: - logging.warning( - 'Fail to construct RDKit Mol from the SMILES string: %s', - smiles_string, - ) - return {}, None, None - # Note this does not contain ideal coordinates, just bonds. - ccd_cif = rdkit_utils.mol_to_ccd_cif( - mol_from_smiles, component_id='fake_cif' - ) - - # RDKit for non-CCD structure and if ref should be a random RDKit conformer. - try: - if non_ccd_with_smiles: - m = mol_from_smiles - m = Chem.AddHs(m) - m = rdkit_utils.assign_atom_names_from_graph( - m, keep_existing_names=True) - logging.info( - 'Success constructing SMILES reference structure for: %s', res_name - ) - else: - m = rdkit_utils.mol_from_ccd_cif(ccd_cif, remove_hydrogens=False) - # Stochastic conformer search method. - # V3 is the latest and supports macrocycles . - params = AllChem.ETKDGv3() - params.randomSeed = int(random_state.randint(1, 1 << 31)) - AllChem.EmbedMolecule(m, params) - conformer = m.GetConformer() - for i, atom in enumerate(m.GetAtoms()): - elements.append(atom.GetAtomicNum()) - charges.append(atom.GetFormalCharge()) - name = atom.GetProp('atom_name') - atom_names.append(name) - coords = conformer.GetAtomPosition(i) - pos.append([coords.x, coords.y, coords.z]) - pos = np.array(pos, dtype=np.float32) - except (rdkit_utils.MolFromMmcifError, ValueError): - logging.warning( - 'Failed to construct RDKit reference structure for: %s', res_name - ) - - if not atom_names: - # Get CCD ideal coordinates if RDKit fails. - atom_names = ccd_cif['_chem_comp_atom.atom_id'] - # If mol_from_smiles then it won't have ideal coordinates by default. - if '_chem_comp_atom.pdbx_model_Cartn_x_ideal' in ccd_cif: - atom_x = ccd_cif['_chem_comp_atom.pdbx_model_Cartn_x_ideal'] - atom_y = ccd_cif['_chem_comp_atom.pdbx_model_Cartn_y_ideal'] - atom_z = ccd_cif['_chem_comp_atom.pdbx_model_Cartn_z_ideal'] - else: - atom_x = np.array(['?'] * len(atom_names)) - atom_y = np.array(['?'] * len(atom_names)) - atom_z = np.array(['?'] * len(atom_names)) - type_symbols = ccd_cif['_chem_comp_atom.type_symbol'] - charges = ccd_cif['_chem_comp_atom.charge'] - elements = [ - periodic_table.ATOMIC_NUMBER.get(elem_type.capitalize(), 0) - for elem_type in type_symbols - ] - pos = np.array([[x, y, z] for x, y, z in zip(atom_x, atom_y, atom_z)]) - # Unknown reference coordinates are specified by '?' in chem comp dict. - # Replace unknown reference coords with 0. - if '?' in pos and '_chem_comp.pdbx_modified_date' in ccd_cif: - # Use reference coordinates if modified date is before cutoff. - modified_dates = [ - datetime.date.fromisoformat(date) - for date in ccd_cif['_chem_comp.pdbx_modified_date'] - ] - max_modified_date = max(modified_dates) - if max_modified_date < ref_max_modified_date: - atom_x = ccd_cif['_chem_comp_atom.model_Cartn_x'] - atom_y = ccd_cif['_chem_comp_atom.model_Cartn_y'] - atom_z = ccd_cif['_chem_comp_atom.model_Cartn_z'] - pos = np.array([[x, y, z] - for x, y, z in zip(atom_x, atom_y, atom_z)]) - if '?' in pos: - if np.all(pos == '?'): - logging.warning('All ref positions unknown for: %s', res_name) - else: - logging.warning('Some ref positions unknown for: %s', res_name) - pos[pos == '?'] = 0 - pos = np.array(pos, dtype=np.float32) - - pos = random_augmentation(pos, random_state) - - if intra_ligand_ptm_bonds: - from_atom = ccd_cif.get('_chem_comp_bond.atom_id_1', None) - dest_atom = ccd_cif.get('_chem_comp_bond.atom_id_2', None) - else: - from_atom = None - dest_atom = None - - features = {} - for atom_name in atom_names: - features[atom_name] = {} - idx = atom_names.index(atom_name) - charge = 0 if charges[idx] == '?' else int(charges[idx]) - atom_name_chars = np.array([ord(c) - 32 for c in atom_name], dtype=int) - atom_name_chars = _pad_to(atom_name_chars, (4,)) - features[atom_name]['positions'] = pos[idx] - features[atom_name]['mask'] = 1 - features[atom_name]['element'] = elements[idx] - features[atom_name]['charge'] = charge - features[atom_name]['atom_name_chars'] = atom_name_chars - return features, from_atom, dest_atom - - -@dataclasses.dataclass -class RefStructure: - """Contains ref structure information.""" - - # Array with positions, float32, shape [num_res, max_atoms_per_token, 3] - positions: xnp_ndarray - # Array with masks, bool, shape [num_res, max_atoms_per_token] - mask: xnp_ndarray - # Array with elements, int32, shape [num_res, max_atoms_per_token] - element: xnp_ndarray - # Array with charges, float32, shape [num_res, max_atoms_per_token] - charge: xnp_ndarray - # Array with atom name characters, int32, [num_res, max_atoms_per_token, 4] - atom_name_chars: xnp_ndarray - # Array with reference space uids, int32, [num_res, max_atoms_per_token] - ref_space_uid: xnp_ndarray - - @classmethod - def compute_features( - cls, - all_token_atoms_layout: atom_layout.AtomLayout, - ccd: chemical_components.Ccd, - padding_shapes: PaddingShapes, - chemical_components_data: struc_chem_comps.ChemicalComponentsData, - random_state: np.random.RandomState, - ref_max_modified_date: datetime.date, - intra_ligand_ptm_bonds: bool, - ligand_ligand_bonds: Optional[atom_layout.AtomLayout] = None, - ) -> tuple[Self, Any]: - """Reference structure information for each residue.""" - - # Get features per atom - padded_shape = (padding_shapes.num_tokens, - all_token_atoms_layout.shape[1]) - result = { - 'positions': np.zeros((*padded_shape, 3), 'float32'), - 'mask': np.zeros(padded_shape, 'bool'), - 'element': np.zeros(padded_shape, 'int32'), - 'charge': np.zeros(padded_shape, 'float32'), - 'atom_name_chars': np.zeros((*padded_shape, 4), 'int32'), - 'ref_space_uid': np.zeros((*padded_shape,), 'int32'), - } - - atom_names_all = [] - chain_ids_all = [] - res_ids_all = [] - - # Cache reference conformations for each residue. - conformations = {} - ref_space_uids = {} - for idx in np.ndindex(all_token_atoms_layout.shape): - chain_id = all_token_atoms_layout.chain_id[idx] - res_id = all_token_atoms_layout.res_id[idx] - res_name = all_token_atoms_layout.res_name[idx] - is_non_standard = res_name not in _STANDARD_RESIDUES - atom_name = all_token_atoms_layout.atom_name[idx] - if not atom_name: - ref = _DEFAULT_BLANK_REF - else: - if (chain_id, res_id) not in conformations: - conf, from_atom, dest_atom = get_reference( - res_name=res_name, - chemical_components_data=chemical_components_data, - ccd=ccd, - random_state=random_state, - ref_max_modified_date=ref_max_modified_date, - intra_ligand_ptm_bonds=intra_ligand_ptm_bonds, - ) - conformations[(chain_id, res_id)] = conf - - if ( - is_non_standard - and (from_atom is not None) - and (dest_atom is not None) - ): - # Add intra-ligand bond graph - atom_names_ligand = np.stack( - [from_atom, dest_atom], axis=1, dtype=object - ) - atom_names_all.append(atom_names_ligand) - res_ids_all.append( - np.full_like(atom_names_ligand, res_id, dtype=int) - ) - chain_ids_all.append( - np.full_like(atom_names_ligand, - chain_id, dtype=object) - ) - - conformation = conformations.get( - (chain_id, res_id), {atom_name: _DEFAULT_BLANK_REF} - ) - if atom_name not in conformation: - logging.warning( - 'Missing atom "%s" for CCD "%s"', - atom_name, - all_token_atoms_layout.res_name[idx], - ) - ref = conformation.get(atom_name, _DEFAULT_BLANK_REF) - for k in ref: - result[k][idx] = ref[k] - - # Assign a unique reference space id to each component, to determine which - # reference positions live in the same reference space. - space_str_id = ( - all_token_atoms_layout.chain_id[idx], - all_token_atoms_layout.res_id[idx], - ) - if space_str_id not in ref_space_uids: - ref_space_uids[space_str_id] = len(ref_space_uids) - result['ref_space_uid'][idx] = ref_space_uids[space_str_id] - - if atom_names_all: - atom_names_all = np.concatenate(atom_names_all, axis=0) - res_ids_all = np.concatenate(res_ids_all, axis=0) - chain_ids_all = np.concatenate(chain_ids_all, axis=0) - if ligand_ligand_bonds is not None: - adjusted_ligand_ligand_bonds = atom_layout.AtomLayout( - atom_name=np.concatenate( - [ligand_ligand_bonds.atom_name, atom_names_all], axis=0 - ), - chain_id=np.concatenate( - [ligand_ligand_bonds.chain_id, chain_ids_all], axis=0 - ), - res_id=np.concatenate( - [ligand_ligand_bonds.res_id, res_ids_all], axis=0 - ), - ) - else: - adjusted_ligand_ligand_bonds = atom_layout.AtomLayout( - atom_name=atom_names_all, - chain_id=chain_ids_all, - res_id=res_ids_all, - ) - else: - adjusted_ligand_ligand_bonds = ligand_ligand_bonds - - return cls(**result), adjusted_ligand_ligand_bonds - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - return cls( - positions=batch['ref_pos'], - mask=batch['ref_mask'], - element=batch['ref_element'], - charge=batch['ref_charge'], - atom_name_chars=batch['ref_atom_name_chars'], - ref_space_uid=batch['ref_space_uid'], - ) - - def as_data_dict(self) -> BatchDict: - return { - 'ref_pos': self.positions, - 'ref_mask': self.mask, - 'ref_element': self.element, - 'ref_charge': self.charge, - 'ref_atom_name_chars': self.atom_name_chars, - 'ref_space_uid': self.ref_space_uid, - } - - -@dataclasses.dataclass -class ConvertModelOutput: - """Contains atom layout info.""" - - cleaned_struc: structure.Structure - token_atoms_layout: atom_layout.AtomLayout - flat_output_layout: atom_layout.AtomLayout - empty_output_struc: structure.Structure - polymer_ligand_bonds: atom_layout.AtomLayout - ligand_ligand_bonds: atom_layout.AtomLayout - - @classmethod - def compute_features( - cls, - all_token_atoms_layout: atom_layout.AtomLayout, - padding_shapes: PaddingShapes, - cleaned_struc: structure.Structure, - flat_output_layout: atom_layout.AtomLayout, - empty_output_struc: structure.Structure, - polymer_ligand_bonds: atom_layout.AtomLayout, - ligand_ligand_bonds: atom_layout.AtomLayout, - ) -> Self: - """Pads the all_token_atoms_layout and stores other data.""" - # Crop and pad the all_token_atoms_layout. - token_atoms_layout = all_token_atoms_layout.copy_and_pad_to( - (padding_shapes.num_tokens, all_token_atoms_layout.shape[1]) - ) - - return cls( - cleaned_struc=cleaned_struc, - token_atoms_layout=token_atoms_layout, - flat_output_layout=flat_output_layout, - empty_output_struc=empty_output_struc, - polymer_ligand_bonds=polymer_ligand_bonds, - ligand_ligand_bonds=ligand_ligand_bonds, - ) - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - """Construct atom layout object from dictionary.""" - - return cls( - cleaned_struc=_unwrap(batch.get('cleaned_struc', None)), - token_atoms_layout=_unwrap(batch.get('token_atoms_layout', None)), - flat_output_layout=_unwrap(batch.get('flat_output_layout', None)), - empty_output_struc=_unwrap(batch.get('empty_output_struc', None)), - polymer_ligand_bonds=_unwrap( - batch.get('polymer_ligand_bonds', None)), - ligand_ligand_bonds=_unwrap( - batch.get('ligand_ligand_bonds', None)), - ) - - def as_data_dict(self) -> BatchDict: - return { - 'cleaned_struc': np.array(self.cleaned_struc, object), - 'token_atoms_layout': np.array(self.token_atoms_layout, object), - 'flat_output_layout': np.array(self.flat_output_layout, object), - 'empty_output_struc': np.array(self.empty_output_struc, object), - 'polymer_ligand_bonds': np.array(self.polymer_ligand_bonds, object), - 'ligand_ligand_bonds': np.array(self.ligand_ligand_bonds, object), - } - - -@dataclasses.dataclass -class AtomCrossAtt: - """Operate on flat atoms.""" - - token_atoms_to_queries: atom_layout.GatherInfo - tokens_to_queries: atom_layout.GatherInfo - tokens_to_keys: atom_layout.GatherInfo - queries_to_keys: atom_layout.GatherInfo - queries_to_token_atoms: atom_layout.GatherInfo - - @classmethod - def compute_features( - cls, - # (num_tokens, num_dense) - all_token_atoms_layout: atom_layout.AtomLayout, - queries_subset_size: int, - keys_subset_size: int, - padding_shapes: PaddingShapes, - ) -> Self: - """Computes gather indices and meta data to work with a flat atom list.""" - - token_atoms_layout = all_token_atoms_layout.copy_and_pad_to( - (padding_shapes.num_tokens, all_token_atoms_layout.shape[1]) - ) - token_atoms_mask = token_atoms_layout.atom_name.astype(bool) - flat_layout = token_atoms_layout[token_atoms_mask] - num_atoms = flat_layout.shape[0] - - padded_flat_layout = flat_layout.copy_and_pad_to(( - padding_shapes.num_atoms, - )) - - # Create the layout for queries - num_subsets = padding_shapes.num_atoms // queries_subset_size - lay_arr = padded_flat_layout.to_array() - queries_layout = atom_layout.AtomLayout.from_array( - lay_arr.reshape((6, num_subsets, queries_subset_size)) - ) - - # Create the layout for the keys (the key subsets are centered around the - # query subsets) - # Create initial gather indices (contain out-of-bound indices) - subset_centers = np.arange( - queries_subset_size / 2, padding_shapes.num_atoms, queries_subset_size - ) - flat_to_key_gathers = ( - subset_centers[:, None] - + np.arange(-keys_subset_size / 2, keys_subset_size / 2)[None, :] - ) - flat_to_key_gathers = flat_to_key_gathers.astype(int) - # Shift subsets with out-of-bound indices, such that they are fully within - # the bounds. - for row in range(flat_to_key_gathers.shape[0]): - if flat_to_key_gathers[row, 0] < 0: - flat_to_key_gathers[row, :] -= flat_to_key_gathers[row, 0] - elif flat_to_key_gathers[row, -1] > num_atoms - 1: - overflow = flat_to_key_gathers[row, -1] - (num_atoms - 1) - flat_to_key_gathers[row, :] -= overflow - # Create the keys layout. - keys_layout = padded_flat_layout[flat_to_key_gathers] - - # Create gather indices for conversion between token atoms layout, - # queries layout and keys layout. - token_atoms_to_queries = atom_layout.compute_gather_idxs( - source_layout=token_atoms_layout, target_layout=queries_layout - ) - - token_atoms_to_keys = atom_layout.compute_gather_idxs( - source_layout=token_atoms_layout, target_layout=keys_layout - ) - - queries_to_keys = atom_layout.compute_gather_idxs( - source_layout=queries_layout, target_layout=keys_layout - ) - - queries_to_token_atoms = atom_layout.compute_gather_idxs( - source_layout=queries_layout, target_layout=token_atoms_layout - ) - - # Create gather indices for conversion of tokens layout to - # queries and keys layout - token_idxs = np.arange(padding_shapes.num_tokens).astype(np.int64) - token_idxs = np.broadcast_to( - token_idxs[:, None], token_atoms_layout.shape) - tokens_to_queries = atom_layout.GatherInfo( - gather_idxs=atom_layout.convert( - token_atoms_to_queries, token_idxs, layout_axes=(0, 1) - ), - gather_mask=atom_layout.convert( - token_atoms_to_queries, token_atoms_mask, layout_axes=(0, 1) - ), - input_shape=np.array((padding_shapes.num_tokens,)), - ) - - tokens_to_keys = atom_layout.GatherInfo( - gather_idxs=atom_layout.convert( - token_atoms_to_keys, token_idxs, layout_axes=(0, 1) - ), - gather_mask=atom_layout.convert( - token_atoms_to_keys, token_atoms_mask, layout_axes=(0, 1) - ), - input_shape=np.array((padding_shapes.num_tokens,)), - ) - - return cls( - token_atoms_to_queries=token_atoms_to_queries, - tokens_to_queries=tokens_to_queries, - tokens_to_keys=tokens_to_keys, - queries_to_keys=queries_to_keys, - queries_to_token_atoms=queries_to_token_atoms, - ) - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - return cls( - token_atoms_to_queries=atom_layout.GatherInfo.from_dict( - batch, key_prefix='token_atoms_to_queries' - ), - tokens_to_queries=atom_layout.GatherInfo.from_dict( - batch, key_prefix='tokens_to_queries' - ), - tokens_to_keys=atom_layout.GatherInfo.from_dict( - batch, key_prefix='tokens_to_keys' - ), - queries_to_keys=atom_layout.GatherInfo.from_dict( - batch, key_prefix='queries_to_keys' - ), - queries_to_token_atoms=atom_layout.GatherInfo.from_dict( - batch, key_prefix='queries_to_token_atoms' - ), - ) - - def as_data_dict(self) -> BatchDict: - return { - **self.token_atoms_to_queries.as_dict( - key_prefix='token_atoms_to_queries' - ), - **self.tokens_to_queries.as_dict(key_prefix='tokens_to_queries'), - **self.tokens_to_keys.as_dict(key_prefix='tokens_to_keys'), - **self.queries_to_keys.as_dict(key_prefix='queries_to_keys'), - **self.queries_to_token_atoms.as_dict( - key_prefix='queries_to_token_atoms' - ), - } - - -@dataclasses.dataclass -class Frames: - """Features for backbone frames.""" - - mask: xnp_ndarray - - @classmethod - def compute_features( - cls, - all_tokens: atom_layout.AtomLayout, - all_token_atoms_layout: atom_layout.AtomLayout, - ref_structure: RefStructure, - padding_shapes: PaddingShapes, - ) -> Self: - """Computes features for backbone frames.""" - num_tokens = padding_shapes.num_tokens - all_token_atoms_layout = all_token_atoms_layout.copy_and_pad_to( - (num_tokens, all_token_atoms_layout.shape[1]) - ) - - all_token_atoms_to_all_tokens = atom_layout.compute_gather_idxs( - source_layout=all_token_atoms_layout, target_layout=all_tokens - ) - ref_coordinates = atom_layout.convert( - all_token_atoms_to_all_tokens, - ref_structure.positions.astype(np.float32), - layout_axes=(0, 1), - ) - ref_mask = atom_layout.convert( - all_token_atoms_to_all_tokens, - ref_structure.mask.astype(bool), - layout_axes=(0, 1), - ) - ref_mask = ref_mask & all_token_atoms_to_all_tokens.gather_mask.astype( - bool) - - all_frame_mask = [] - - # Iterate over tokens - for idx, args in enumerate( - zip(all_tokens.chain_type, all_tokens.chain_id, all_tokens.res_id) - ): - - chain_type, chain_id, res_id = args - - if chain_type in list(mmcif_names.PEPTIDE_CHAIN_TYPES): - frame_mask = True - elif chain_type in list(mmcif_names.NUCLEIC_ACID_CHAIN_TYPES): - frame_mask = True - elif chain_type in list(mmcif_names.NON_POLYMER_CHAIN_TYPES): - # For ligands, build frames from closest atoms from the same molecule. - (local_token_idxs,) = np.where( - (all_tokens.chain_type == chain_type) - & (all_tokens.chain_id == chain_id) - & (all_tokens.res_id == res_id) - ) - - if len(local_token_idxs) < 3: - frame_mask = False - - else: - # [local_tokens] - local_dist = np.linalg.norm( - ref_coordinates[idx] - ref_coordinates[local_token_idxs], axis=-1 - ) - local_mask = ref_mask[local_token_idxs] - cost = local_dist + 1e8 * ~local_mask - cost = cost + 1e8 * (idx == local_token_idxs) - # [local_tokens] - closest_idxs = np.argsort(cost, axis=0) - - # The closest indices index an array of local tokens. Convert this - # to indices of the full (num_tokens,) array. - global_closest_idxs = local_token_idxs[closest_idxs] - - # Construct frame by placing the current token at the origin and two - # nearest atoms on either side. - global_frame_idxs = np.array( - (global_closest_idxs[0], idx, global_closest_idxs[1]) - ) - - # Check that the frame atoms are not colinear. - a, b, c = ref_coordinates[global_frame_idxs] - vec1 = a - b - vec2 = c - b - # Reference coordinates can be all zeros, in which case we have - # to explicitly set colinearity. - if np.isclose(np.linalg.norm(vec1, axis=-1), 0) or np.isclose( - np.linalg.norm(vec2, axis=-1), 0 - ): - is_colinear = True - logging.info( - 'Found identical coordinates: Assigning as colinear.') - else: - vec1 = vec1 / np.linalg.norm(vec1, axis=-1) - vec2 = vec2 / np.linalg.norm(vec2, axis=-1) - cos_angle = np.einsum('...k,...k->...', vec1, vec2) - # <25 degree deviation is considered colinear. - is_colinear = 1 - np.abs(cos_angle) < 0.0937 - - frame_mask = not is_colinear - else: - # No frame for other chain types. - frame_mask = False - - all_frame_mask.append(frame_mask) - - all_frame_mask = np.array(all_frame_mask, dtype=bool) - - mask = _pad_to(all_frame_mask, (padding_shapes.num_tokens,)) - - return cls(mask=mask) - - @classmethod - def from_data_dict(cls, batch: BatchDict) -> Self: - return cls(mask=batch['frames_mask']) - - def as_data_dict(self) -> BatchDict: - return {'frames_mask': self.mask} diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/load_batch.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/load_batch.py deleted file mode 100644 index 41cf2bf48..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/load_batch.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -"""load data 'batch' used in test""" -import pickle -import mindspore as ms -from alphafold3.model.feat_batch import Batch - - -def load_batch(dtype=ms.float32): - """Load batch data for test""" - with open('/data/zmmVol2/AF3/test/unit_tests/model/diffusion/example_np.pkl', 'rb') as f: - data = pickle.load(f) - batch = Batch.from_data_dict(data) - batch.convert_to_tensor(dtype=dtype) - return batch diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/merging_features.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/merging_features.py deleted file mode 100644 index 3c1fab899..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/merging_features.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Methods for merging existing features to create a new example. - -Covers: -- Merging features across chains. -- Merging the paired and unpaired parts of the MSA. -""" - -from typing import TypeAlias - -from alphafold3.model import data_constants -import numpy as np - -NUM_SEQ_NUM_RES_MSA_FEATURES = data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES -NUM_SEQ_MSA_FEATURES = data_constants.NUM_SEQ_MSA_FEATURES -MSA_PAD_VALUES = data_constants.MSA_PAD_VALUES - - -xnp_ndarray: TypeAlias = np.ndarray # pylint: disable=invalid-name -BatchDict: TypeAlias = dict[str, xnp_ndarray] - - -def _pad_features_to_max(feat_name: str, chains: list[BatchDict], axis: int): - """Pad a set of features to the maximum size amongst all chains. - - Args: - feat_name: The feature name to pad. - chains: A list of chains with associated features. - axis: Which axis to pad to the max. - - Returns: - A list of features, all with the same size on the given axis. - """ - max_num_seq = np.max([chain[feat_name].shape[axis] for chain in chains]) - - padded_feats = [] - for chain in chains: - feat = chain[feat_name] - - padding = np.zeros_like(feat.shape) # pytype: disable=attribute-error - # pytype: disable=attribute-error - padding[axis] = max_num_seq - feat.shape[axis] - padding = [(0, p) for p in padding] - padded_feats.append( - np.pad( - feat, - padding, - mode='constant', - constant_values=MSA_PAD_VALUES[feat_name], - ) - ) - return padded_feats - - -def merge_msa_features(feat_name: str, chains: list[BatchDict]) -> np.ndarray: - """Merges MSA features with shape (NUM_SEQ, NUM_RES) across chains.""" - expected_dtype = chains[0][feat_name].dtype - if '_all_seq' in feat_name: - return np.concatenate( - [c.get(feat_name, np.array([], expected_dtype)) for c in chains], axis=1 - ) - else: - # Since each MSA can be of different lengths, we first need to pad them - # all to the size of the largest MSA before concatenating. - padded_feats = _pad_features_to_max(feat_name, chains, axis=0) - return np.concatenate(padded_feats, axis=1) - - -def merge_paired_and_unpaired_msa(example: BatchDict) -> BatchDict: - """Concatenates the paired (all_seq) MSA features with the unpaired ones.""" - new_example = dict(example) - - for feature_name in NUM_SEQ_NUM_RES_MSA_FEATURES + NUM_SEQ_MSA_FEATURES: - if feature_name in example and feature_name + '_all_seq' in example: - feat = example[feature_name] - feat_all_seq = example[feature_name + '_all_seq'] - merged_feat = np.concatenate([feat_all_seq, feat], axis=0) - new_example[feature_name] = merged_feat - - new_example['num_alignments'] = np.array( - new_example['msa'].shape[0], dtype=np.int32 - ) - return new_example diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.cc deleted file mode 100644 index 663e7f303..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.cc +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include "alphafold3/model/mkdssp_pybind.h" - -#include - -#include -#include -#include -#include - -#include "absl/strings/string_view.h" -#include "pybind11/pybind11.h" -#include "pybind11/pytypes.h" - -namespace alphafold3 { -namespace py = pybind11; - -void RegisterModuleMkdssp(pybind11::module m) { - py::module site = py::module::import("site"); - py::list paths = py::cast(site.attr("getsitepackages")()); - // Find the first path that contains the libcifpp components.cif file. - bool found = false; - for (const auto& py_path : paths) { - auto path_str = - std::filesystem::path(py::cast(py_path)) / - "share/libcifpp/components.cif"; - if (std::filesystem::exists(path_str)) { - setenv("LIBCIFPP_DATA_DIR", path_str.parent_path().c_str(), 0); - found = true; - break; - } - } - if (!found) { - throw py::type_error("Could not find the libcifpp components.cif file."); - } - m.def( - "get_dssp", - [](absl::string_view mmcif, int model_no, - int min_poly_proline_stretch_length, - bool calculate_surface_accessibility) { - cif::file cif_file(mmcif.data(), mmcif.size()); - dssp result(cif_file.front(), model_no, min_poly_proline_stretch_length, - calculate_surface_accessibility); - std::stringstream sstream; - result.write_legacy_output(sstream); - return sstream.str(); - }, - py::arg("mmcif"), py::arg("model_no") = 1, - py::arg("min_poly_proline_stretch_length") = 3, - py::arg("calculate_surface_accessibility") = false, - py::doc("Gets secondary structure from an mmCIF file.")); -} - -} // namespace alphafold3 \ No newline at end of file diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.h deleted file mode 100644 index a1e4832b8..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/mkdssp_pybind.h +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_MODEL_MKDSSP_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_MODEL_MKDSSP_PYBIND_H_ - - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleMkdssp(pybind11::module m); - -} - - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_MODEL_MKDSSP_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/mmcif_metadata.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/mmcif_metadata.py deleted file mode 100644 index 4c3f1a9a0..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/mmcif_metadata.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Adds mmCIF metadata (to be ModelCIF-conformant) and author and legal info.""" - -from typing import Final - -from alphafold3.structure import mmcif -import numpy as np - -_LICENSE_URL: Final[str] = ( - 'https://github.com/google-deepmind/alphafold3/blob/main/OUTPUT_TERMS_OF_USE.md' -) - -_LICENSE: Final[str] = f"""\ -Non-commercial use only, by using this file you agree to the terms of use found -at {_LICENSE_URL}. -To request access to the AlphaFold 3 model parameters, follow the process set -out at https://github.com/google-deepmind/alphafold3. You may only use these if -received directly from Google. Use is subject to terms of use available at -https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md. -""" - -_DISCLAIMER: Final[str] = """\ -AlphaFold 3 and its output are not intended for, have not been validated for, -and are not approved for clinical use. They are provided "as-is" without any -warranty of any kind, whether expressed or implied. No warranty is given that -use shall not infringe the rights of any third party. -""" - -_MMCIF_PAPER_AUTHORS: Final[tuple[str, ...]] = ( - 'Google DeepMind', - 'Isomorphic Labs', -) - -# Authors of the mmCIF - we set them to be equal to the authors of the paper. -_MMCIF_AUTHORS: Final[tuple[str, ...]] = _MMCIF_PAPER_AUTHORS - - -def add_metadata_to_mmcif( - old_cif: mmcif.Mmcif, model_id: bytes -) -> mmcif.Mmcif: - """Adds metadata to a mmCIF to make it ModelCIF-conformant.""" - cif = {} - - # ModelCIF conformation dictionary. - cif['_audit_conform.dict_name'] = ['mmcif_ma.dic'] -# cif['_audit_conform.dict_version'] = ['1.4.5'] - cif['_audit_conform.dict_location'] = [ - 'https://raw.githubusercontent.com/ihmwg/ModelCIF/master/dist/mmcif_ma.dic' - ] - - cif['_pdbx_data_usage.id'] = ['1', '2'] - cif['_pdbx_data_usage.type'] = ['license', 'disclaimer'] - cif['_pdbx_data_usage.details'] = [_LICENSE, _DISCLAIMER] - cif['_pdbx_data_usage.url'] = [_LICENSE_URL, '?'] - - # Structure author details. - cif['_audit_author.name'] = [] - cif['_audit_author.pdbx_ordinal'] = [] - for author_index, author_name in enumerate(_MMCIF_AUTHORS, start=1): - cif['_audit_author.name'].append(author_name) - cif['_audit_author.pdbx_ordinal'].append(str(author_index)) - - # Paper author details. - cif['_citation_author.citation_id'] = [] - cif['_citation_author.name'] = [] - cif['_citation_author.ordinal'] = [] - for author_index, author_name in enumerate(_MMCIF_PAPER_AUTHORS, start=1): - cif['_citation_author.citation_id'].append('primary') - cif['_citation_author.name'].append(author_name) - cif['_citation_author.ordinal'].append(str(author_index)) - - # Paper citation details. - cif['_citation.id'] = ['primary'] - cif['_citation.title'] = [ - 'Accurate structure prediction of biomolecular interactions with' - ' AlphaFold 3' - ] - cif['_citation.journal_full'] = ['Nature'] - cif['_citation.journal_volume'] = ['630'] - cif['_citation.page_first'] = ['493'] - cif['_citation.page_last'] = ['500'] - cif['_citation.year'] = ['2024'] - cif['_citation.journal_id_ASTM'] = ['NATUAS'] - cif['_citation.country'] = ['UK'] - cif['_citation.journal_id_ISSN'] = ['0028-0836'] - cif['_citation.journal_id_CSD'] = ['0006'] - cif['_citation.book_publisher'] = ['?'] - cif['_citation.pdbx_database_id_PubMed'] = ['38718835'] - cif['_citation.pdbx_database_id_DOI'] = ['10.1038/s41586-024-07487-w'] - - # Type of data in the dataset including data used in the model generation. - cif['_ma_data.id'] = ['1'] - cif['_ma_data.name'] = ['Model'] - cif['_ma_data.content_type'] = ['model coordinates'] - - # Description of number of instances for each entity. - cif['_ma_target_entity_instance.asym_id'] = old_cif['_struct_asym.id'] - cif['_ma_target_entity_instance.entity_id'] = old_cif[ - '_struct_asym.entity_id' - ] - cif['_ma_target_entity_instance.details'] = ['.'] * len( - cif['_ma_target_entity_instance.entity_id'] - ) - - # Details about the target entities. - cif['_ma_target_entity.entity_id'] = cif[ - '_ma_target_entity_instance.entity_id' - ] - cif['_ma_target_entity.data_id'] = ['1'] * len( - cif['_ma_target_entity.entity_id'] - ) - cif['_ma_target_entity.origin'] = ['.'] * len( - cif['_ma_target_entity.entity_id'] - ) - - # Details of the models being deposited. - cif['_ma_model_list.ordinal_id'] = ['1'] - cif['_ma_model_list.model_id'] = ['1'] - cif['_ma_model_list.model_group_id'] = ['1'] - cif['_ma_model_list.model_name'] = ['Top ranked model'] - - cif['_ma_model_list.model_group_name'] = [ - 'AlphaFold-beta-20231127' - ] - cif['_ma_model_list.data_id'] = ['1'] - cif['_ma_model_list.model_type'] = ['Ab initio model'] - - # Software used. - cif['_software.pdbx_ordinal'] = ['1'] - cif['_software.name'] = ['AlphaFold'] -# cif['_software.version'] = [ -# f'AlphaFold-beta-20231127 ({model_id.decode("ascii")})' -# ] - cif['_software.type'] = ['package'] - cif['_software.description'] = ['Structure prediction'] - cif['_software.classification'] = ['other'] - cif['_software.date'] = ['?'] - - # Collection of software into groups. - cif['_ma_software_group.ordinal_id'] = ['1'] - cif['_ma_software_group.group_id'] = ['1'] - cif['_ma_software_group.software_id'] = ['1'] - - # Method description to conform with ModelCIF. - cif['_ma_protocol_step.ordinal_id'] = ['1', '2', '3'] - cif['_ma_protocol_step.protocol_id'] = ['1', '1', '1'] - cif['_ma_protocol_step.step_id'] = ['1', '2', '3'] - cif['_ma_protocol_step.method_type'] = [ - 'coevolution MSA', - 'template search', - 'modeling', - ] - - # Details of the metrics use to assess model confidence. - cif['_ma_qa_metric.id'] = ['1', '2'] - cif['_ma_qa_metric.name'] = ['pLDDT', 'pLDDT'] - # Accepted values are distance, energy, normalised score, other, zscore. - cif['_ma_qa_metric.type'] = ['pLDDT', 'pLDDT'] - cif['_ma_qa_metric.mode'] = ['global', 'local'] - cif['_ma_qa_metric.software_group_id'] = ['1', '1'] - - # Global model confidence metric value. - cif['_ma_qa_metric_global.ordinal_id'] = ['1'] - cif['_ma_qa_metric_global.model_id'] = ['1'] - cif['_ma_qa_metric_global.metric_id'] = ['1'] - global_plddt = np.mean( - [float(v) for v in old_cif['_atom_site.B_iso_or_equiv']] - ) - cif['_ma_qa_metric_global.metric_value'] = [f'{global_plddt:.2f}'] - - cif['_atom_type.symbol'] = sorted(set(old_cif['_atom_site.type_symbol'])) - - return old_cif.copy_and_update(cif) - - -def add_legal_comment(cif: str) -> str: - """Adds legal comment at the top of the mmCIF.""" - # fmt: off - # pylint: disable=line-too-long - comment = ( - '# By using this file you agree to the legally binding terms of use found at\n' - f'# {_LICENSE_URL}.\n' - '# To request access to the AlphaFold 3 model parameters, follow the process set\n' - '# out at https://github.com/google-deepmind/alphafold3. You may only use these if\n' - '# received directly from Google. Use is subject to terms of use available at\n' - '# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md.' - ) - # pylint: enable=line-too-long - # fmt: on - return f'{comment}\n{cif}' diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/model_config.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/model_config.py deleted file mode 100644 index 79ba8cab0..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/model_config.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Config for the protein folding model and experiment.""" - -from collections.abc import Sequence -from typing import Literal, TypeAlias, Optional, Tuple - -from alphafold3.model import base_config - - -_Shape2DType: TypeAlias = Tuple[Optional[int], Optional[int]] - - -class GlobalConfig(base_config.BaseConfig): - bfloat16: Literal['all', 'none', 'intermediate'] = 'none' - final_init: Literal['zeros', 'linear'] = 'zeros' - pair_attention_chunk_size: Sequence[_Shape2DType] = ( - (1536, 128), (None, 32)) - pair_transition_shard_spec: Sequence[_Shape2DType] = ( - (2048, None), - (None, 1024), - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/msa_pairing.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/msa_pairing.py deleted file mode 100644 index a1736a359..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/msa_pairing.py +++ /dev/null @@ -1,314 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Functions for producing "paired" and "unpaired" MSA features for each chain. - -The paired MSA: -- Is made from the result of the all_seqs MSA query. -- Is ordered such that you can concatenate features across chains and related - sequences will end up on the same row. Related here means "from the same - species". Gaps are added to facilitate this whenever a sequence has no - suitable pair. - -The unpaired MSA: -- Is made from the results of the remaining MSA queries. -- Has no special ordering properties. -- Is deduplicated such that it doesn't contain any sequences in the paired MSA. -""" - -from typing import Mapping, MutableMapping, Sequence, Optional -from alphafold3.model import data_constants -import numpy as np - - -def _align_species( - all_species: Sequence[bytes], - chains_species_to_rows: Sequence[Mapping[bytes, np.ndarray]], - min_hits_per_species: Mapping[bytes, int], -) -> np.ndarray: - """Aligns MSA row indices based on species. - - Within a species, MSAs are aligned based on their original order (the first - sequence for a species in the first chain's MSA is aligned to the first - sequence for the same species in the second chain's MSA). - - Args: - all_species: A list of all unique species identifiers. - chains_species_to_rows: A dictionary for each chain, that maps species to - the set of MSA row indices from that species in that chain. - min_hits_per_species: A mapping from species id, to the minimum MSA size - across chains for that species (ignoring chains with zero hits). - - Returns: - A matrix of size [num_msa_rows, num_chains], where the i,j element is an - index into the jth chains MSA. Each row consists of sequences from each - chain for the same species (or -1 if that chain has no sequences for that - species). - """ - # Each species block is of size [num_seqs x num_chains] and consists of - # indices into the respective MSAs that have been aligned and are all for the - # same species. - species_blocks = [] - for species in all_species: - chain_row_indices = [] - for species_to_rows in chains_species_to_rows: - min_msa_size = min_hits_per_species[species] - if species not in species_to_rows: - # If a given chain has no hits for a species then we pad it with -1's, - # later on these values are used to make sure each feature is padded - # with its appropriate pad value. - row_indices = np.full( - min_msa_size, fill_value=-1, dtype=np.int32) - else: - # We crop down to the smallest MSA for a given species across chains. - row_indices = species_to_rows[species][:min_msa_size] - chain_row_indices.append(row_indices) - species_block = np.stack(chain_row_indices, axis=1) - species_blocks.append(species_block) - aligned_matrix = np.concatenate(species_blocks, axis=0) - return aligned_matrix - - -def create_paired_features( - chains: Sequence[MutableMapping[str, np.ndarray]], - max_paired_sequences: int, - nonempty_chain_ids: set[str], - max_hits_per_species: int, -) -> Sequence[MutableMapping[str, np.ndarray]]: - """Creates per-chain MSA features where the MSAs have been aligned. - - Args: - chains: A list of feature dicts, one for each chain. - max_paired_sequences: No more than this many paired sequences will be - returned from this function. - nonempty_chain_ids: A set of chain ids (str) that are included in the crop - there is no reason to process chains not in this list. - max_hits_per_species: No more than this number of sequences will be returned - for a given species. - - Returns: - An updated feature dictionary for each chain, where the {}_all_seq features - have been aligned so that the nth row in chain 1 is aligned to the nth row - in chain 2's features. - """ - # The number of chains that the given species appears in - we rank hits - # across more chains higher. - species_num_chains = {} - - # For each chain we keep a mapping from species to the row indices in the - # original MSA for that chain. - chains_species_to_rows = [] - - # Keep track of the minimum number of hits across chains for a given species. - min_hits_per_species = {} - - for chain in chains: - species_ids = chain['msa_species_identifiers_all_seq'] - - # The query gets an empty species_id, so no pairing happens for this row. - if ( - species_ids.size == 0 - or (species_ids.size == 1 and not species_ids[0]) - or chain['chain_id'] not in nonempty_chain_ids - ): - chains_species_to_rows.append({}) - continue - - # For each species keep track of which row indices in the original MSA are - # from this species. - row_indices = np.arange(len(species_ids)) - # The grouping np.split code requires that the input is already clustered - # by species id. - sort_idxs = species_ids.argsort() - species_ids = species_ids[sort_idxs] - row_indices = row_indices[sort_idxs] - - species, unique_row_indices = np.unique(species_ids, return_index=True) - grouped_row_indices = np.split(row_indices, unique_row_indices[1:]) - species_to_rows = dict(zip(species, grouped_row_indices, strict=True)) - chains_species_to_rows.append(species_to_rows) - - for s in species: - species_num_chains[s] = species_num_chains.get(s, 0) + 1 - - for species, row_indices in species_to_rows.items(): - min_hits_per_species[species] = min( - min_hits_per_species.get(species, max_hits_per_species), - len(row_indices), - ) - - # Construct a mapping from the number of chains a species appears in to - # the list of species with that count. - num_chains_to_species = {} - for species, num_chains in species_num_chains.items(): - if not species or num_chains <= 1: - continue - if num_chains not in num_chains_to_species: - num_chains_to_species[num_chains] = [] - num_chains_to_species[num_chains].append(species) - - num_rows_seen = 0 - # We always keep the first row as it is the query sequence. - all_rows = [np.array([[0] * len(chains)], dtype=np.int32)] - - # We prioritize species that have hits across more chains. - for num_chains in sorted(num_chains_to_species, reverse=True): - all_species = num_chains_to_species[num_chains] - - # Align all the per-chain row indices by species, so every paired row is - # for a single species. - rows = _align_species( - all_species, chains_species_to_rows, min_hits_per_species - ) - # Sort rows by the product of the original indices in the respective chain - # MSAS, so as to rank hits that appear higher in the original MSAs higher. - rank_metric = np.abs(np.prod(rows.astype(np.float32), axis=1)) - sorted_rows = rows[np.argsort(rank_metric), :] - all_rows.append(sorted_rows) - num_rows_seen += rows.shape[0] - if num_rows_seen >= max_paired_sequences: - break - - all_rows = np.concatenate(all_rows, axis=0) - all_rows = all_rows[:max_paired_sequences, :] - - # Now we just have to select the relevant rows from the original msa and - # deletion matrix features - paired_chains = [] - for chain_idx, chain in enumerate(chains): - out_chain = {k: v for k, v in chain.items() if 'all_seq' not in k} - selected_row_indices = all_rows[:, chain_idx] - for feat_name in ['msa', 'deletion_matrix']: - all_seq_name = f'{feat_name}_all_seq' - feat_value = chain[all_seq_name] - - # The selected row indices are padded to be the same shape for each chain, - # they are padded with -1's, so we add a single row onto the feature with - # the appropriate pad value. This has the effect that we correctly pad - # each feature since all padded indices will select this padding row. - pad_value = data_constants.MSA_PAD_VALUES[feat_name] - feat_value = np.concatenate([ - feat_value, - np.full((1, feat_value.shape[1]), pad_value, feat_value.dtype), - ]) - - feat_value = feat_value[selected_row_indices, :] - out_chain[all_seq_name] = feat_value - out_chain['num_alignments_all_seq'] = np.array( - out_chain['msa_all_seq'].shape[0] - ) - paired_chains.append(out_chain) - return paired_chains - - -def deduplicate_unpaired_sequences( - np_chains: Sequence[MutableMapping[str, np.ndarray]], -) -> Sequence[MutableMapping[str, np.ndarray]]: - """Deduplicates unpaired sequences based on paired sequences.""" - - feature_names = np_chains[0].keys() - msa_features = ( - data_constants.NUM_SEQ_MSA_FEATURES - + data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES - ) - - for chain in np_chains: - sequence_set = set( - hash(s.data.tobytes()) for s in chain['msa_all_seq'].astype(np.int8) - ) - keep_rows = [] - # Go through unpaired MSA seqs and remove any rows that correspond to the - # sequences that are already present in the paired MSA. - for row_num, seq in enumerate(chain['msa'].astype(np.int8)): - if hash(seq.data.tobytes()) not in sequence_set: - keep_rows.append(row_num) - for feature_name in feature_names: - if feature_name in msa_features: - chain[feature_name] = chain[feature_name][keep_rows] - chain['num_alignments'] = np.array( - chain['msa'].shape[0], dtype=np.int32) - return np_chains - - -def choose_paired_unpaired_msa_crop_sizes( - unpaired_msa: np.ndarray, - paired_msa: Optional[np.ndarray], - total_msa_crop_size: int, - max_paired_sequences: int, -) -> tuple[int, Optional[int]]: - """Returns the sizes of the MSA crop and MSA_all_seq crop. - - NOTE: Unpaired + paired MSA sizes can exceed total_msa_size when - there are lots of gapped rows. Through the pairing logic another chain(s) - will have fewer than total_msa_size. - - Args: - unpaired_msa: The unpaired MSA array (not all_seq). - paired_msa: The paired MSA array (all_seq). - total_msa_crop_size: The maximum total number of sequences to crop to. - max_paired_sequences: The maximum number of sequences that can come from - MSA pairing. - - Returns: - A tuple of: - The size of the reduced MSA crop (not all_seq features). - The size of the unreduced MSA crop (for all_seq features) or None, if - paired_msa is None. - """ - if paired_msa is not None: - paired_crop_size = np.minimum( - paired_msa.shape[0], max_paired_sequences) - - # We reduce the number of un-paired sequences, by the number of times a - # sequence from this chains MSA is included in the paired MSA. This keeps - # the MSA size for each chain roughly constant. - cropped_all_seq_msa = paired_msa[:max_paired_sequences] - num_non_gapped_pairs = cropped_all_seq_msa.shape[0] - - unpaired_crop_size = np.minimum( - unpaired_msa.shape[0], total_msa_crop_size - num_non_gapped_pairs - ) - else: - unpaired_crop_size = np.minimum( - unpaired_msa.shape[0], total_msa_crop_size) - paired_crop_size = None - return unpaired_crop_size, paired_crop_size - - -def remove_all_gapped_rows_from_all_seqs( - chains_list: Sequence[dict[str, np.ndarray]], asym_ids: Sequence[float] -) -> Sequence[dict[str, np.ndarray]]: - """Removes all gapped rows from all_seq feat based on selected asym_ids.""" - - merged_msa_all_seq = np.concatenate( - [ - chain['msa_all_seq'] - for chain in chains_list - if chain['asym_id'][0] in asym_ids - ], - axis=1, - ) - - non_gapped_keep_rows = np.any( - merged_msa_all_seq != data_constants.MSA_GAP_IDX, axis=1 - ) - for chain in chains_list: - for feat_name in list(chains_list)[0]: - if '_all_seq' in feat_name: - feat_name_split = feat_name.split('_all_seq')[0] - if feat_name_split in ( - data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES - + data_constants.NUM_SEQ_MSA_FEATURES - ): - # For consistency we do this for all chains even though the - # gapped rows are based on a selected set asym_ids. - chain[feat_name] = chain[feat_name][non_gapped_keep_rows] - chain['num_alignments_all_seq'] = np.sum(non_gapped_keep_rows) - return chains_list diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/params.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/params.py deleted file mode 100644 index 3c1d22df6..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/params.py +++ /dev/null @@ -1,218 +0,0 @@ -# Copyright 2025 Huawei Technologies Co., Ltd -# -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Model param loading.""" - -import bisect -import collections -from collections.abc import Iterator -import contextlib -import io -import os -import pathlib -import re -import struct -import sys -from typing import IO -import numpy as np - - -class RecordError(Exception): - """Error reading a record.""" - - -def encode_record(scope: str, name: str, arr: np.ndarray) -> bytes: - """Encodes a single haiku param as bytes, preserving non-numpy dtypes.""" - scope = scope.encode('utf-8') - name = name.encode('utf-8') - shape = arr.shape - dtype = str(arr.dtype).encode('utf-8') - arr = np.ascontiguousarray(arr) - if sys.byteorder == 'big': - arr = arr.byteswap() - arr_buffer = arr.tobytes('C') - header = struct.pack( - '<5i', len(scope), len(name), len(dtype), len(shape), len(arr_buffer) - ) - return header + b''.join( - (scope, name, dtype, struct.pack(f'{len(shape)}i', *shape), arr_buffer) - ) - - -def _read_record(stream: IO[bytes]) -> tuple[str, str, np.ndarray] | None: - """Reads a record encoded by `_encode_record` from a byte stream.""" - header_size = struct.calcsize('<5i') - header = stream.read(header_size) - if not header: - return None - if len(header) < header_size: - raise RecordError( - f'Incomplete header: {len(header)=} < {header_size=}') - (scope_len, name_len, dtype_len, shape_len, arr_buffer_len) = struct.unpack( - '<5i', header - ) - fmt = f'<{scope_len}s{name_len}s{dtype_len}s{shape_len}i' - payload_size = struct.calcsize(fmt) + arr_buffer_len - payload = stream.read(payload_size) - if len(payload) < payload_size: - raise RecordError( - f'Incomplete payload: {len(payload)=} < {payload_size=}') - scope, name, dtype, *shape = struct.unpack_from(fmt, payload) - scope = scope.decode('utf-8') - name = name.decode('utf-8') - dtype = dtype.decode('utf-8') - if dtype == 'bfloat16': - buffer = payload[-arr_buffer_len:] - if sys.byteorder == 'big': - buffer = buffer[::-1] - arr_uint16 = np.frombuffer(buffer, dtype=np.uint16) - arr_bf16 = arr_uint16.view('bfloat16') - arr = arr_bf16.astype(np.float32) - else: - arr = np.frombuffer(payload[-arr_buffer_len:], dtype=dtype) - if sys.byteorder == 'big': - arr = arr.byteswap() - arr = np.reshape(arr, shape) - if sys.byteorder == 'big': - arr = arr.byteswap() - return scope, name, arr - - -def read_records(stream: IO[bytes]) -> Iterator[tuple[str, str, np.ndarray]]: - """Fully reads the contents of a byte stream.""" - while record := _read_record(stream): - yield record - - -class _MultiFileIO(io.RawIOBase): - """A file-like object that presents a concatenated view of multiple files.""" - - def __init__(self, files: list[pathlib.Path]): - self._files = files - self._stack = contextlib.ExitStack() - self._handles = [ - self._stack.enter_context(file.open('rb')) for file in files - ] - self._sizes = [] - for handle in self._handles: - handle.seek(0, os.SEEK_END) - self._sizes.append(handle.tell()) - self._length = sum(self._sizes) - self._offsets = [0] - for s in self._sizes[:-1]: - self._offsets.append(self._offsets[-1] + s) - self._abspos = 0 - self._relpos = (0, 0) - - def _abs_to_rel(self, pos: int) -> tuple[int, int]: - idx = bisect.bisect_right(self._offsets, pos) - 1 - return idx, pos - self._offsets[idx] - - def close(self): - self._stack.close() - - def closed(self) -> bool: - return all(handle.closed for handle in self._handles) - - def fileno(self) -> int: - return -1 - - def readable(self) -> bool: - return True - - def tell(self) -> int: - return self._abspos - - def seek(self, pos: int, whence: int = os.SEEK_SET, /): - match whence: - case os.SEEK_SET: - pass - case os.SEEK_CUR: - pos += self._abspos - case os.SEEK_END: - pos = self._length - pos - case _: - raise ValueError(f'Invalid whence: {whence}') - self._abspos = pos - self._relpos = self._abs_to_rel(pos) - - def readinto(self, b: bytearray | memoryview) -> int: - result = 0 - mem = memoryview(b) - while mem: - self._handles[self._relpos[0]].seek(self._relpos[1]) - count = self._handles[self._relpos[0]].readinto(mem) - result += count - self._abspos += count - self._relpos = self._abs_to_rel(self._abspos) - mem = mem[count:] - if self._abspos == self._length: - break - return result - - -@contextlib.contextmanager -def open_for_reading(model_files: list[pathlib.Path], is_compressed: bool): - with contextlib.closing(_MultiFileIO(model_files)) as f: - yield f - - -def _match_model( - paths: list[pathlib.Path], pattern: re.Pattern[str] -) -> dict[str, list[pathlib.Path]]: - """Match files in a directory with a pattern, and group by model name.""" - models = collections.defaultdict(list) - for path in paths: - match = pattern.fullmatch(path.name) - if match: - models[match.group('model_name')].append(path) - return {k: sorted(v) for k, v in models.items()} - - -def select_model_files( - model_dir: pathlib.Path, model_name: str | None = None -) -> tuple[list[pathlib.Path], bool]: - """Select the model files from a model directory.""" - files = [file for file in model_dir.iterdir() if file.is_file()] - - for pattern, is_compressed in ( - (r'(?P.*)\.[0-9]+\.bin\.zst$', True), - (r'(?P.*)\.bin\.zst\.[0-9]+$', True), - (r'(?P.*)\.[0-9]+\.bin$', False), - (r'(?P.*)\.bin]\.[0-9]+$', False), - (r'(?P.*)\.bin\.zst$', True), - (r'(?P.*)\.bin$', False), - ): - models = _match_model(files, re.compile(pattern)) - if model_name is not None: - if model_name in models: - return models[model_name], is_compressed - else: - if models: - if len(models) > 1: - raise RuntimeError( - f'Multiple models matched in {model_dir}') - _, model_files = models.popitem() - return model_files, is_compressed - raise FileNotFoundError(f'No models matched in {model_dir}') - - -def get_model_af3_params(model_dir: pathlib.Path): - """Get the Haiku parameters from a model name.""" - params: dict[str, dict[str, np.array]] = {} - model_files, is_compressed = select_model_files(model_dir) - with open_for_reading(model_files, is_compressed) as stream: - for scope, name, arr in read_records(stream): - params.setdefault(scope, {})[name] = np.array(arr) - if not params: - raise FileNotFoundError(f'Model missing from "{model_dir}"') - return params diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/inter_chain_bonds.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/inter_chain_bonds.py deleted file mode 100644 index d6a7e0917..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/inter_chain_bonds.py +++ /dev/null @@ -1,348 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Functions for handling inter-chain bonds.""" - -from collections.abc import Collection -import functools -from typing import Final, NamedTuple, Optional -import numpy as np -from alphafold3 import structure -from alphafold3.constants import chemical_component_sets -from alphafold3.constants import mmcif_names -from alphafold3.model.atom_layout import atom_layout - - - -BOND_THRESHOLD_GLYCANS_ANGSTROM: Final[float] = 1.7 -# See https://pubs.acs.org/doi/10.1021/ja010331r for P-P atom bond distances. -BOND_THRESHOLD_ALL_ANGSTROM: Final[float] = 2.4 - - -class BondAtomArrays(NamedTuple): - chain_id: np.ndarray - chain_type: np.ndarray - res_id: np.ndarray - res_name: np.ndarray - atom_name: np.ndarray - coords: np.ndarray - - -def _get_bond_atom_arrays( - struct: structure.Structure, bond_atom_indices: np.ndarray -) -> BondAtomArrays: - return BondAtomArrays( - chain_id=struct.chain_id[bond_atom_indices], - chain_type=struct.chain_type[bond_atom_indices], - res_id=struct.res_id[bond_atom_indices], - res_name=struct.res_name[bond_atom_indices], - atom_name=struct.atom_name[bond_atom_indices], - coords=struct.coords[..., bond_atom_indices, :], - ) - - -@functools.lru_cache(maxsize=1) -def get_polymer_ligand_and_ligand_ligand_bonds( - struct: structure.Structure, - only_glycan_ligands: bool, - allow_multiple_bonds_per_atom: bool, -) -> tuple[atom_layout.AtomLayout, atom_layout.AtomLayout]: - """Return polymer-ligand & ligand-ligand inter-residue bonds. - - Args: - struct: Structure object to extract bonds from. - only_glycan_ligands: Whether to only include glycans in ligand category. - allow_multiple_bonds_per_atom: If not allowed, we greedily choose the first - bond seen per atom and discard the remaining on each atom.. - - Returns: - polymer_ligand, ligand_ligand_bonds: Each object is an AtomLayout object - [num_bonds, 2] for the bond-defining atoms. - """ - if only_glycan_ligands: - allowed_res_names = list({ - *chemical_component_sets.GLYCAN_OTHER_LIGANDS, - *chemical_component_sets.GLYCAN_LINKING_LIGANDS, - }) - else: - allowed_res_names = None - all_bonds = get_bond_layout( - bond_threshold=BOND_THRESHOLD_GLYCANS_ANGSTROM - if only_glycan_ligands - else BOND_THRESHOLD_ALL_ANGSTROM, - struct=struct, - allowed_chain_types1=list({ - *mmcif_names.LIGAND_CHAIN_TYPES, - *mmcif_names.POLYMER_CHAIN_TYPES, - }), - allowed_chain_types2=list(mmcif_names.LIGAND_CHAIN_TYPES), - allowed_res_names=allowed_res_names, - allow_multiple_bonds_per_atom=allow_multiple_bonds_per_atom, - ) - ligand_ligand_bonds_mask = np.isin( - all_bonds.chain_type, list(mmcif_names.LIGAND_CHAIN_TYPES) - ) - polymer_ligand_bonds_mask = np.isin( - all_bonds.chain_type, list(mmcif_names.POLYMER_CHAIN_TYPES) - ) - polymer_ligand_bonds_mask = np.logical_and( - ligand_ligand_bonds_mask.any(axis=1), - polymer_ligand_bonds_mask.any(axis=1), - ) - ligand_ligand_bonds = all_bonds[ligand_ligand_bonds_mask.all(axis=1)] - polymer_ligand_bonds = all_bonds[polymer_ligand_bonds_mask] - return polymer_ligand_bonds, ligand_ligand_bonds - - -def _remove_multi_bonds( - bond_layout: atom_layout.AtomLayout, -) -> atom_layout.AtomLayout: - """Remove instances greedily.""" - uids = {} - keep_indx = [] - for chain_id, res_id, atom_name in zip( - bond_layout.chain_id, - bond_layout.res_id, - bond_layout.atom_name, - strict=True, - ): - key1 = (chain_id[0], res_id[0], atom_name[0]) - key2 = (chain_id[1], res_id[1], atom_name[1]) - keep_indx.append(bool(key1 not in uids) and bool(key2 not in uids)) - if key1 not in uids: - uids[key1] = None - if key2 not in uids: - uids[key2] = None - return bond_layout[np.array(keep_indx, dtype=bool)] - - -@functools.lru_cache(maxsize=1) -def get_ligand_ligand_bonds( - struct: structure.Structure, - only_glycan_ligands: bool, - allow_multiple_bonds_per_atom: bool = False, -) -> atom_layout.AtomLayout: - """Return ligand-ligand inter-residue bonds. - - Args: - struct: Structure object to extract bonds from. - only_glycan_ligands: Whether to only include glycans in ligand category. - allow_multiple_bonds_per_atom: If not allowed, we greedily choose the first - bond seen per atom and discard the remaining on each atom. - - Returns: - bond_layout: AtomLayout object [num_bonds, 2] for the bond-defining atoms. - """ - if only_glycan_ligands: - allowed_res_names = list({ - *chemical_component_sets.GLYCAN_OTHER_LIGANDS, - *chemical_component_sets.GLYCAN_LINKING_LIGANDS, - }) - else: - allowed_res_names = None - return get_bond_layout( - bond_threshold=BOND_THRESHOLD_GLYCANS_ANGSTROM - if only_glycan_ligands - else BOND_THRESHOLD_ALL_ANGSTROM, - struct=struct, - allowed_chain_types1=list(mmcif_names.LIGAND_CHAIN_TYPES), - allowed_chain_types2=list(mmcif_names.LIGAND_CHAIN_TYPES), - allowed_res_names=allowed_res_names, - allow_multiple_bonds_per_atom=allow_multiple_bonds_per_atom, - ) - - -@functools.lru_cache(maxsize=1) -def get_polymer_ligand_bonds( - struct: structure.Structure, - only_glycan_ligands: bool, - allow_multiple_bonds_per_atom: bool = False, - bond_threshold: Optional[float] = None, -) -> atom_layout.AtomLayout: - """Return polymer-ligand interchain bonds. - - Args: - struct: Structure object to extract bonds from. - only_glycan_ligands: Whether to only include glycans in ligand category. - allow_multiple_bonds_per_atom: If not allowed, we greedily choose the first - bond seen per atom and discard the remaining on each atom. - bond_threshold: Euclidean distance of max allowed bond. - - Returns: - bond_layout: AtomLayout object [num_bonds, 2] for the bond-defining atoms. - """ - if only_glycan_ligands: - allowed_res_names = list({ - *chemical_component_sets.GLYCAN_OTHER_LIGANDS, - *chemical_component_sets.GLYCAN_LINKING_LIGANDS, - }) - else: - allowed_res_names = None - if bond_threshold is None: - if only_glycan_ligands: - bond_threshold = BOND_THRESHOLD_GLYCANS_ANGSTROM - else: - bond_threshold = BOND_THRESHOLD_ALL_ANGSTROM - return get_bond_layout( - bond_threshold=bond_threshold, - struct=struct, - allowed_chain_types1=list(mmcif_names.POLYMER_CHAIN_TYPES), - allowed_chain_types2=list(mmcif_names.LIGAND_CHAIN_TYPES), - allowed_res_names=allowed_res_names, - allow_multiple_bonds_per_atom=allow_multiple_bonds_per_atom, - ) - - -def get_bond_layout( - bond_threshold: float = BOND_THRESHOLD_ALL_ANGSTROM, - *, - struct: structure.Structure, - allowed_chain_types1: Collection[str], - allowed_chain_types2: Collection[str], - include_bond_types: Collection[str] = ('covale',), - allowed_res_names: Optional[Collection[str]] = None, - allow_multiple_bonds_per_atom: bool, -) -> atom_layout.AtomLayout: - """Get bond_layout for all bonds between two sets of chain types. - - There is a mask (all_mask) that runs through this script, and each bond pair - needs to maintain a True across all conditions in order to be preserved at the - end, otherwise the bond pair has invalidated a condition with a False and is - removed entirely. Note, we remove oxygen atom bonds as they are an edge case - that causes issues with scoring, due to multiple waters bonding with single - residues. - - Args: - bond_threshold: Maximum bond distance in Angstrom. - struct: Structure object to extract bonds from. - allowed_chain_types1: One end of the bonds must be an atom with one of these - chain types. - allowed_chain_types2: The other end of the bond must be an atom with one of - these chain types. - include_bond_types: Only include bonds with specified type e.g. hydrog, - metalc, covale, disulf. - allowed_res_names: Further restricts from chain_types. Either end of the - bonds must be an atom part of these res_names. If none all will be - accepted after chain and bond type filtering. - allow_multiple_bonds_per_atom: If not allowed, we greedily choose the first - bond seen per atom and discard the remaining on each atom. - - Returns: - bond_layout: AtomLayout object [num_bonds, 2] for the bond-defining atoms. - """ - if not struct.bonds: - return atom_layout.AtomLayout( - atom_name=np.empty((0, 2), dtype=object), - res_id=np.empty((0, 2), dtype=int), - res_name=np.empty((0, 2), dtype=object), - chain_id=np.empty((0, 2), dtype=object), - chain_type=np.empty((0, 2), dtype=object), - atom_element=np.empty((0, 2), dtype=object), - ) - from_atom_idxs, dest_atom_idxs = struct.bonds.get_atom_indices( - struct.atom_key - ) - from_atoms = _get_bond_atom_arrays(struct, from_atom_idxs) - dest_atoms = _get_bond_atom_arrays(struct, dest_atom_idxs) - # Chain type - chain_mask = np.logical_or( - np.logical_and( - np.isin( - from_atoms.chain_type, - allowed_chain_types1, - ), - np.isin( - dest_atoms.chain_type, - allowed_chain_types2, - ), - ), - np.logical_and( - np.isin( - from_atoms.chain_type, - allowed_chain_types2, - ), - np.isin( - dest_atoms.chain_type, - allowed_chain_types1, - ), - ), - ) - if allowed_res_names: - # Res type - res_mask = np.logical_or( - np.isin(from_atoms.res_name, allowed_res_names), - np.isin(dest_atoms.res_name, allowed_res_names), - ) - # All mask - all_mask = np.logical_and(chain_mask, res_mask) - else: - all_mask = chain_mask - # Bond type mask - type_mask = np.isin(struct.bonds.type, list(include_bond_types)) - np.logical_and(all_mask, type_mask, out=all_mask) - # Bond length check. Work in square length to avoid taking many square roots. - bond_length_squared = np.square(from_atoms.coords - dest_atoms.coords).sum( - axis=1 - ) - bond_threshold_squared = bond_threshold * bond_threshold - np.logical_and( - all_mask, bond_length_squared < bond_threshold_squared, out=all_mask - ) - # Inter-chain and inter-residue bonds for ligands - ligand_types = list(mmcif_names.LIGAND_CHAIN_TYPES) - is_ligand = np.logical_or( - np.isin( - from_atoms.chain_type, - ligand_types, - ), - np.isin( - dest_atoms.chain_type, - ligand_types, - ), - ) - res_id_differs = from_atoms.res_id != dest_atoms.res_id - chain_id_differs = from_atoms.chain_id != dest_atoms.chain_id - is_inter_res = np.logical_or(res_id_differs, chain_id_differs) - is_inter_ligand_res = np.logical_and(is_inter_res, is_ligand) - is_inter_chain_not_ligand = np.logical_and(chain_id_differs, ~is_ligand) - # If ligand then inter-res & inter-chain bonds, otherwise inter-chain only. - combined_allowed_bonds = np.logical_or( - is_inter_chain_not_ligand, is_inter_ligand_res - ) - np.logical_and(all_mask, combined_allowed_bonds, out=all_mask) - bond_layout = atom_layout.AtomLayout( - atom_name=np.stack( - [ - from_atoms.atom_name[all_mask], - dest_atoms.atom_name[all_mask], - ], - axis=1, - dtype=object, - ), - res_id=np.stack( - [from_atoms.res_id[all_mask], dest_atoms.res_id[all_mask]], - axis=1, - dtype=int, - ), - chain_id=np.stack( - [ - from_atoms.chain_id[all_mask], - dest_atoms.chain_id[all_mask], - ], - axis=1, - dtype=object, - ), - ) - if not allow_multiple_bonds_per_atom: - bond_layout = _remove_multi_bonds(bond_layout) - return atom_layout.fill_in_optional_fields( - bond_layout, - reference_atoms=atom_layout.atom_layout_from_structure(struct), - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/pipeline.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/pipeline.py deleted file mode 100644 index 3d2165f20..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/pipeline.py +++ /dev/null @@ -1,446 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""The main featurizer.""" - -import bisect -from collections.abc import Sequence -import datetime -import itertools -from typing import Optional - -from absl import logging -from alphafold3.common import base_config -from alphafold3.common import folding_input -from alphafold3.constants import chemical_components -from alphafold3.model import feat_batch -from alphafold3.model import features -from alphafold3.model.pipeline import inter_chain_bonds -from alphafold3.model.pipeline import structure_cleaning -from alphafold3.structure import chemical_components as struc_chem_comps -import numpy as np - - -_DETERMINISTIC_FRAMES_RANDOM_SEED = 12312837 - - -def calculate_bucket_size( - num_tokens: int, buckets: Optional[Sequence[int]] -) -> int: - """Calculates the bucket size to pad the data to.""" - if buckets is None: - return num_tokens - - if not buckets: - raise ValueError('Buckets must be non-empty.') - - if not all(prev < curr for prev, curr in itertools.pairwise(buckets)): - raise ValueError( - f'Buckets must be in strictly increasing order. Got {buckets=}.' - ) - - bucket_idx = bisect.bisect_left(buckets, num_tokens) - - if bucket_idx == len(buckets): - logging.warning( - 'Creating a new bucket of size %d since the input has more tokens than' - ' the largest bucket size %d. This may trigger a re-compilation of the' - ' model. Consider additional large bucket sizes to avoid excessive' - ' re-compilation.', - num_tokens, - buckets[-1], - ) - return num_tokens - - return buckets[bucket_idx] - - -class NanDataError(Exception): - """Raised if the data pipeline produces data containing nans.""" - - -class TotalNumResOutOfRangeError(Exception): - """Raised if total number of residues for all chains outside allowed range.""" - - -class MmcifNumChainsError(Exception): - """Raised if the mmcif file contains too many / too few chains.""" - - -class WholePdbPipeline: - """Processes an entire mmcif entity and merges the content.""" - - class Config(base_config.BaseConfig): - """Configuration object for `WholePdbPipeline`. - - Properties: - max_atoms_per_token: number of atom slots in one token (was called - num_dense, and semi-hardcoded to 24 before) - pad_num_chains: Size to pad NUM_CHAINS feature dimensions to, only for - protein chains. - buckets: Bucket sizes to pad the data to, to avoid excessive - re-compilation of the model. If None, calculate the appropriate bucket - size from the number of tokens. If not None, must be a sequence of at - least one integer, in strictly increasing order. Will raise an error if - the number of tokens is more than the largest bucket size. - max_total_residues: Any mmCIF with more total residues will be rejected. - If none, then no limit is applied. - min_total_residues: Any mmCIF with less total residues will be rejected. - msa_crop_size: Maximum size of MSA to take across all chains. - max_template_date: Optional max template date to prevent data leakage in - validation. - max_templates: The maximum number of templates to send through the network - set to 0 to switch off templates. - filter_clashes: If true then will remove clashing chains. - filter_crystal_aids: If true ligands in the cryal aid list are removed. - max_paired_sequence_per_species: The maximum number of sequences per - species that will be used for MSA pairing. - drop_ligand_leaving_atoms: Flag for handling leaving atoms for ligands. - intra_ligand_ptm_bonds: Whether to embed intra ligand covalent bond graph. - average_num_atoms_per_token: Target average number of atoms per token to - compute the padding size for flat atoms. - atom_cross_att_queries_subset_size: queries subset size in atom cross - attention - atom_cross_att_keys_subset_size: keys subset size in atom cross attention - flatten_non_standard_residues: Whether to expand non-standard polymer - residues into flat-atom format. - remove_nonsymmetric_bonds: Whether to remove nonsymmetric bonds from - symmetric polymer chains. - deterministic_frames: Whether to use fixed-seed reference positions to - construct deterministic frames. - """ - - max_atoms_per_token: int = 24 - pad_num_chains: int = 1000 - buckets: Optional[list[int]] = None - max_total_residues: Optional[int] = None - min_total_residues: Optional[int] = None - msa_crop_size: int = 16384 - max_template_date: Optional[datetime.date] = None - max_templates: int = 4 - filter_clashes: bool = False - filter_crystal_aids: bool = False - max_paired_sequence_per_species: int = 600 - drop_ligand_leaving_atoms: bool = True - intra_ligand_ptm_bonds: bool = True - average_num_atoms_per_token: int = 24 - atom_cross_att_queries_subset_size: int = 32 - atom_cross_att_keys_subset_size: int = 128 - flatten_non_standard_residues: bool = True - remove_nonsymmetric_bonds: bool = False - deterministic_frames: bool = True - - def __init__( - self, - *, - config: Config, - ): - """Init WholePdb. - - Args: - config: Pipeline configuration. - """ - self._config = config - - def process_item( - self, - fold_input: folding_input.Input, - random_state: np.random.RandomState, - ccd: chemical_components.Ccd, - random_seed: Optional[int] = None, - ) -> features.BatchDict: - """Takes requests from in_queue, adds (key, serialized ex) to out_queue.""" - if random_seed is None: - random_seed = random_state.randint(2**31) - - random_state = np.random.RandomState(seed=random_seed) - - logging_name = f'{fold_input.name}, random_seed={random_seed}' - logging.info('processing %s', logging_name) - struct = fold_input.to_structure(ccd=ccd) - - # Clean structure. - cleaned_struc, cleaning_metadata = structure_cleaning.clean_structure( - struct, - ccd=ccd, - drop_non_standard_atoms=True, - drop_missing_sequence=True, - filter_clashes=self._config.filter_clashes, - filter_crystal_aids=self._config.filter_crystal_aids, - filter_waters=True, - filter_hydrogens=True, - filter_leaving_atoms=self._config.drop_ligand_leaving_atoms, - only_glycan_ligands_for_leaving_atoms=True, - covalent_bonds_only=True, - remove_polymer_polymer_bonds=True, - remove_bad_bonds=True, - remove_nonsymmetric_bonds=self._config.remove_nonsymmetric_bonds, - ) - - num_clashing_chains_removed = cleaning_metadata[ - 'num_clashing_chains_removed' - ] - - if num_clashing_chains_removed: - logging.info( - 'Removed %d clashing chains from %s', - num_clashing_chains_removed, - logging_name, - ) - - # No chains after fixes - # if cleaned_struc.num_chains == 0: - # raise MmcifNumChainsError(f'{logging_name}: No chains in structure!') - - polymer_ligand_bonds, ligand_ligand_bonds = ( - inter_chain_bonds.get_polymer_ligand_and_ligand_ligand_bonds( - cleaned_struc, - only_glycan_ligands=False, - allow_multiple_bonds_per_atom=True, - ) - ) - - # If empty replace with None as this causes errors downstream. - if ligand_ligand_bonds and not ligand_ligand_bonds.atom_name.size: - ligand_ligand_bonds = None - if polymer_ligand_bonds and not polymer_ligand_bonds.atom_name.size: - polymer_ligand_bonds = None - - # Create the flat output AtomLayout - empty_output_struc, flat_output_layout = ( - structure_cleaning.create_empty_output_struct_and_layout( - struct=cleaned_struc, - ccd=ccd, - polymer_ligand_bonds=polymer_ligand_bonds, - ligand_ligand_bonds=ligand_ligand_bonds, - drop_ligand_leaving_atoms=self._config.drop_ligand_leaving_atoms, - ) - ) - - # Select the tokens for Evoformer. - # Each token (e.g. a residue) is encoded as one representative atom. This - # is flexible enough to allow the 1-token-per-atom ligand representation - # in the future. - all_tokens, all_token_atoms_layout, standard_token_idxs = ( - features.tokenizer( - flat_output_layout, - ccd=ccd, - max_atoms_per_token=self._config.max_atoms_per_token, - flatten_non_standard_residues=self._config.flatten_non_standard_residues, - logging_name=logging_name, - ) - ) - total_tokens = len(all_tokens.atom_name) - if ( - self._config.max_total_residues - and total_tokens > self._config.max_total_residues - ): - raise TotalNumResOutOfRangeError( - 'Total Number of Residues > max_total_residues: ' - f'({total_tokens} > {self._config.max_total_residues})' - ) - - if ( - self._config.min_total_residues - and total_tokens < self._config.min_total_residues - ): - raise TotalNumResOutOfRangeError( - 'Total Number of Residues < min_total_residues: ' - f'({total_tokens} < {self._config.min_total_residues})' - ) - - logging.info( - 'Calculating bucket size for input with %d tokens.', total_tokens - ) - padded_token_length = calculate_bucket_size( - total_tokens, self._config.buckets - ) - logging.info( - 'Got bucket size %d for input with %d tokens, resulting in %d padded' - ' tokens.', - padded_token_length, - total_tokens, - padded_token_length - total_tokens, - ) - - # Padding shapes for all features. - num_atoms = padded_token_length * self._config.average_num_atoms_per_token - # Round up to next multiple of subset size. - num_atoms = int( - np.ceil(num_atoms / self._config.atom_cross_att_queries_subset_size) - * self._config.atom_cross_att_queries_subset_size - ) - padding_shapes = features.PaddingShapes( - num_tokens=padded_token_length, - msa_size=self._config.msa_crop_size, - num_chains=self._config.pad_num_chains, - num_templates=self._config.max_templates, - num_atoms=num_atoms, - ) - - # Create the atom layouts for flat atom cross attention - batch_atom_cross_att = features.AtomCrossAtt.compute_features( - all_token_atoms_layout=all_token_atoms_layout, - queries_subset_size=self._config.atom_cross_att_queries_subset_size, - keys_subset_size=self._config.atom_cross_att_keys_subset_size, - padding_shapes=padding_shapes, - ) - - # Extract per-token features - batch_token_features = features.TokenFeatures.compute_features( - all_tokens=all_tokens, - padding_shapes=padding_shapes, - ) - - # Create reference structure features - chemical_components_data = struc_chem_comps.populate_missing_ccd_data( - ccd=ccd, - chemical_components_data=cleaned_struc.chemical_components_data, - populate_pdbx_smiles=True, - ) - - # Add smiles info to empty_output_struc. - empty_output_struc = empty_output_struc.copy_and_update_globals( - chemical_components_data=chemical_components_data - ) - # Create layouts and store structures for model output conversion. - batch_convert_model_output = features.ConvertModelOutput.compute_features( - all_token_atoms_layout=all_token_atoms_layout, - padding_shapes=padding_shapes, - cleaned_struc=cleaned_struc, - flat_output_layout=flat_output_layout, - empty_output_struc=empty_output_struc, - polymer_ligand_bonds=polymer_ligand_bonds, - ligand_ligand_bonds=ligand_ligand_bonds, - ) - - # Create the PredictedStructureInfo - batch_predicted_structure_info = ( - features.PredictedStructureInfo.compute_features( - all_tokens=all_tokens, - all_token_atoms_layout=all_token_atoms_layout, - padding_shapes=padding_shapes, - ) - ) - - # Create MSA features - batch_msa = features.MSA.compute_features( - all_tokens=all_tokens, - standard_token_idxs=standard_token_idxs, - padding_shapes=padding_shapes, - fold_input=fold_input, - logging_name=logging_name, - max_paired_sequence_per_species=self._config.max_paired_sequence_per_species, - ) - - # Create template features - batch_templates = features.Templates.compute_features( - all_tokens=all_tokens, - standard_token_idxs=standard_token_idxs, - padding_shapes=padding_shapes, - fold_input=fold_input, - max_templates=self._config.max_templates, - logging_name=logging_name, - ) - - ref_max_modified_date = self._config.max_template_date - batch_ref_structure, ligand_ligand_bonds = ( - features.RefStructure.compute_features( - all_token_atoms_layout=all_token_atoms_layout, - ccd=ccd, - padding_shapes=padding_shapes, - chemical_components_data=chemical_components_data, - random_state=random_state, - ref_max_modified_date=ref_max_modified_date, - intra_ligand_ptm_bonds=self._config.intra_ligand_ptm_bonds, - ligand_ligand_bonds=ligand_ligand_bonds, - ) - ) - deterministic_ref_structure = None - if self._config.deterministic_frames: - deterministic_ref_structure, _ = features.RefStructure.compute_features( - all_token_atoms_layout=all_token_atoms_layout, - ccd=ccd, - padding_shapes=padding_shapes, - chemical_components_data=chemical_components_data, - random_state=( - np.random.RandomState(_DETERMINISTIC_FRAMES_RANDOM_SEED) - ), - ref_max_modified_date=ref_max_modified_date, - intra_ligand_ptm_bonds=self._config.intra_ligand_ptm_bonds, - ligand_ligand_bonds=ligand_ligand_bonds, - ) - - # Create ligand-polymer bond features. - polymer_ligand_bond_info = features.PolymerLigandBondInfo.compute_features( - all_tokens=all_tokens, - all_token_atoms_layout=all_token_atoms_layout, - bond_layout=polymer_ligand_bonds, - padding_shapes=padding_shapes, - ) - # Create ligand-ligand bond features. - ligand_ligand_bond_info = features.LigandLigandBondInfo.compute_features( - all_tokens, - ligand_ligand_bonds, - padding_shapes, - ) - - # Create the Pseudo-beta layout for distogram head and distance error head. - batch_pseudo_beta_info = features.PseudoBetaInfo.compute_features( - all_token_atoms_layout=all_token_atoms_layout, - ccd=ccd, - padding_shapes=padding_shapes, - logging_name=logging_name, - ) - - # Frame construction. - batch_frames = features.Frames.compute_features( - all_tokens=all_tokens, - all_token_atoms_layout=all_token_atoms_layout, - ref_structure=( - deterministic_ref_structure - if self._config.deterministic_frames - else batch_ref_structure - ), - padding_shapes=padding_shapes, - ) - - # Assemble the Batch object. - batch = feat_batch.Batch( - msa=batch_msa, - templates=batch_templates, - token_features=batch_token_features, - ref_structure=batch_ref_structure, - predicted_structure_info=batch_predicted_structure_info, - polymer_ligand_bond_info=polymer_ligand_bond_info, - ligand_ligand_bond_info=ligand_ligand_bond_info, - pseudo_beta_info=batch_pseudo_beta_info, - atom_cross_att=batch_atom_cross_att, - convert_model_output=batch_convert_model_output, - frames=batch_frames, - ) - - np_example = batch.as_data_dict() - if 'num_iter_recycling' in np_example: - del np_example['num_iter_recycling'] # that does not belong here - - for name, value in np_example.items(): - if ( - value.dtype.kind not in {'U', 'S'} - and value.dtype.name != 'object' - and np.isnan(np.sum(value)) - ): - raise NanDataError( - 'The output of the data pipeline contained nans. ' - f'nan feature: {name}, fold input name: {fold_input.name}, ' - f'random_seed {random_seed}' - ) - - return np_example diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/structure_cleaning.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/structure_cleaning.py deleted file mode 100644 index ead97cbcb..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/pipeline/structure_cleaning.py +++ /dev/null @@ -1,370 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Prepare PDB structure for training or inference.""" - -from typing import Any, Optional -import numpy as np -from absl import logging -from alphafold3 import structure -from alphafold3.constants import chemical_component_sets -from alphafold3.constants import chemical_components -from alphafold3.constants import mmcif_names -from alphafold3.model.atom_layout import atom_layout -from alphafold3.model.pipeline import inter_chain_bonds -from alphafold3.model.scoring import covalent_bond_cleaning -from alphafold3.structure import sterics - - -def _get_leaving_atom_mask( - struct: structure.Structure, - polymer_ligand_bonds: Optional[atom_layout.AtomLayout], - ligand_ligand_bonds: Optional[atom_layout.AtomLayout], - chain_id: str, - chain_type: str, - res_id: int, - res_name: str, -) -> np.ndarray: - """Updates a drop_leaving_atoms mask with new leaving atom locations.""" - bonded_atoms = atom_layout.get_bonded_atoms( - polymer_ligand_bonds, - ligand_ligand_bonds, - res_id, - chain_id, - ) - # Connect the amino-acids, i.e. remove OXT, HXT and H2. - drop_atoms = atom_layout.get_link_drop_atoms( - res_name=res_name, - chain_type=chain_type, - is_start_terminus=False, - is_end_terminus=False, - bonded_atoms=bonded_atoms, - drop_ligand_leaving_atoms=True, - ) - # Default mask where everything is false, which equates to being kept. - drop_atom_filter_atoms = struct.chain_id != struct.chain_id - for drop_atom in drop_atoms: - drop_atom_filter_atom = np.logical_and( - np.logical_and( - struct.atom_name == drop_atom, - struct.chain_id == chain_id, - ), - struct.res_id == res_id, - ) - drop_atom_filter_atoms = np.logical_or( - drop_atom_filter_atoms, drop_atom_filter_atom - ) - return drop_atom_filter_atoms - - -def clean_structure( - struct: structure.Structure, - ccd: chemical_components.Ccd, - *, - drop_missing_sequence: bool, - filter_clashes: bool, - drop_non_standard_atoms: bool, - filter_crystal_aids: bool, - filter_waters: bool, - filter_hydrogens: bool, - filter_leaving_atoms: bool, - only_glycan_ligands_for_leaving_atoms: bool, - covalent_bonds_only: bool, - remove_polymer_polymer_bonds: bool, - remove_bad_bonds: bool, - remove_nonsymmetric_bonds: bool, -) -> tuple[structure.Structure, dict[str, Any]]: - """Cleans structure. - - Args: - struct: Structure to clean. - ccd: The chemical components dictionary. - drop_missing_sequence: Whether to drop chains without specified sequences. - filter_clashes: Whether to drop clashing chains. - drop_non_standard_atoms: Whether to drop non CCD standard atoms. - filter_crystal_aids: Whether to drop ligands in the crystal aid set. - filter_waters: Whether to drop water chains. - filter_hydrogens: Whether to drop hyrdogen atoms. - filter_leaving_atoms: Whether to drop leaving atoms based on heuristics. - only_glycan_ligands_for_leaving_atoms: Whether to only include glycan - ligands when filtering leaving atoms. - covalent_bonds_only: Only include covalent bonds. - remove_polymer_polymer_bonds: Remove polymer-polymer bonds. - remove_bad_bonds: Whether to remove badly bonded ligands. - remove_nonsymmetric_bonds: Whether to remove nonsymmetric polymer-ligand - bonds from symmetric polymer chains. - - Returns: - Tuple of structure and metadata dict. The metadata dict has - information about what was cleaned from the original. - """ - - metadata = {} - # Crop crystallization aids. - if ( - filter_crystal_aids - and struct.structure_method in mmcif_names.CRYSTALLIZATION_METHODS - ): - struct = struct.filter_out( - res_name=chemical_component_sets.COMMON_CRYSTALLIZATION_AIDS - ) - - # Drop chains without specified sequences. - if drop_missing_sequence: - chains_with_unk_sequence = struct.find_chains_with_unknown_sequence() - num_with_unk_sequence = len(chains_with_unk_sequence) - if chains_with_unk_sequence: - struct = struct.filter_out(chain_id=chains_with_unk_sequence) - else: - num_with_unk_sequence = 0 - metadata['num_with_unk_sequence'] = num_with_unk_sequence - - # Remove intersecting chains. - if filter_clashes and struct.num_chains > 1: - clashing_chains = sterics.find_clashing_chains(struct) - if clashing_chains: - struct = struct.filter_out(chain_id=clashing_chains) - else: - clashing_chains = [] - metadata['num_clashing_chains_removed'] = len(clashing_chains) - metadata['chains_removed'] = clashing_chains - - # Drop non-standard atoms - if drop_non_standard_atoms: - struct = struct.drop_non_standard_atoms( - ccd=ccd, drop_unk=False, drop_non_ccd=False - ) - - # Sort chains in "reverse-spreadsheet" order. - struct = struct.with_sorted_chains - - if filter_hydrogens: - struct = struct.without_hydrogen() - - if filter_waters: - struct = struct.filter_out(chain_type=mmcif_names.WATER) - - if filter_leaving_atoms: - drop_leaving_atoms_all = struct.chain_id != struct.chain_id - polymer_ligand_bonds = inter_chain_bonds.get_polymer_ligand_bonds( - struct, - only_glycan_ligands=only_glycan_ligands_for_leaving_atoms, - ) - ligand_ligand_bonds = inter_chain_bonds.get_ligand_ligand_bonds( - struct, - only_glycan_ligands=only_glycan_ligands_for_leaving_atoms, - ) - all_glycans = { - *chemical_component_sets.GLYCAN_OTHER_LIGANDS, - *chemical_component_sets.GLYCAN_LINKING_LIGANDS, - } - # If only glycan ligands and no O1 atoms, we can do parallel drop. - if ( - only_glycan_ligands_for_leaving_atoms - and (not (ligand_ligand_bonds.atom_name == 'O1').any()) - and (not (polymer_ligand_bonds.atom_name == 'O1').any()) - ): - drop_leaving_atoms_all = np.logical_and( - np.isin(struct.atom_name, 'O1'), - np.isin(struct.res_name, list(all_glycans)), - ) - else: - substruct = struct.group_by_residue - glycan_mask = np.isin(substruct.res_name, list(all_glycans)) - substruct = substruct.filter(glycan_mask) - # We need to iterate over all glycan residues for this. - for res in substruct.iter_residues(): - # Only need to do drop leaving atoms for glycans depending on bonds. - if (res_name := res['res_name']) in all_glycans: - drop_atom_filter = _get_leaving_atom_mask( - struct=struct, - polymer_ligand_bonds=polymer_ligand_bonds, - ligand_ligand_bonds=ligand_ligand_bonds, - chain_id=res['chain_id'], - chain_type=res['chain_type'], - res_id=res['res_id'], - res_name=res_name, - ) - drop_leaving_atoms_all = np.logical_or( - drop_leaving_atoms_all, drop_atom_filter - ) - - num_atoms_before = struct.num_atoms - struct = struct.filter_out(drop_leaving_atoms_all) - num_atoms_after = struct.num_atoms - - if num_atoms_before > num_atoms_after: - logging.error( - 'Dropped %s atoms from GT struct: chain_id %s res_id %s res_name %s', - num_atoms_before - num_atoms_after, - struct.chain_id, - struct.res_id, - struct.res_name, - ) - - # Can filter by bond type without having to iterate over bonds. - if struct.bonds and covalent_bonds_only: - is_covalent = np.isin(struct.bonds.type, ['covale']) - if sum(is_covalent) > 0: - new_bonds = struct.bonds[is_covalent] - else: - new_bonds = structture.Bonds.make_empty() - struct = struct.copy_and_update(bonds=new_bonds) - - # Other bond filters require iterating over individual bonds. - if struct.bonds and (remove_bad_bonds or remove_polymer_polymer_bonds): - include_bond = [] - num_pp_bonds = 0 - num_bad_bonds = 0 - for bond in struct.iter_bonds(): - dest_atom = bond.dest_atom - from_atom = bond.from_atom - if remove_polymer_polymer_bonds: - if ( - from_atom['chain_type'] in mmcif_names.POLYMER_CHAIN_TYPES - and dest_atom['chain_type'] in mmcif_names.POLYMER_CHAIN_TYPES - ): - num_pp_bonds += 1 - include_bond.append(False) - continue - if remove_bad_bonds: - dest_coords = np.array( - [dest_atom['atom_x'], dest_atom['atom_y'], dest_atom['atom_z']] - ) - from_coords = np.array( - [from_atom['atom_x'], from_atom['atom_y'], from_atom['atom_z']] - ) - squared_dist = np.sum(np.square(dest_coords - from_coords)) - squared_threshold = 2.4 * 2.4 - if squared_dist > squared_threshold: - num_bad_bonds += 1 - include_bond.append(False) - continue - include_bond.append(True) - if sum(include_bond) < len(struct.bonds): - logging.info( - 'Reducing number of bonds for %s from %s to %s, of which %s are' - ' polymer-polymer bonds and %s are bad bonds.', - struct.name, - len(struct.bonds), - sum(include_bond), - num_pp_bonds, - num_bad_bonds, - ) - if sum(include_bond) > 0: - # Need to index bonds with bond keys or arrays of bools with same length - # as num bonds. In this case, we use array of bools (as elsewhere in the - # cleaning code). - new_bonds = struct.bonds[np.array(include_bond, dtype=bool)] - else: - new_bonds = structure.Bonds.make_empty() - struct = struct.copy_and_update(bonds=new_bonds) - - if struct.bonds and remove_nonsymmetric_bonds: - # Check for asymmetric polymer-ligand bonds and remove if these exist. - polymer_ligand_bonds = inter_chain_bonds.get_polymer_ligand_bonds( - struct, - only_glycan_ligands=False, - ) - if polymer_ligand_bonds: - if covalent_bond_cleaning.has_nonsymmetric_bonds_on_symmetric_polymer_chains( - struct, polymer_ligand_bonds - ): - from_atom_idxs, dest_atom_idxs = struct.bonds.get_atom_indices( - struct.atom_key - ) - poly_chain_types = list(mmcif_names.POLYMER_CHAIN_TYPES) - is_polymer_bond = np.logical_or( - np.isin( - struct.chain_type[from_atom_idxs], poly_chain_types), - np.isin( - struct.chain_type[dest_atom_idxs], poly_chain_types), - ) - struct = struct.copy_and_update( - bonds=struct.bonds[~is_polymer_bond]) - - return struct, metadata - - -def create_empty_output_struct_and_layout( - struct: structure.Structure, - ccd: chemical_components.Ccd, - *, - with_hydrogens: bool = False, - skip_unk: bool = False, - polymer_ligand_bonds: Optional[atom_layout.AtomLayout] = None, - ligand_ligand_bonds: Optional[atom_layout.AtomLayout] = None, - drop_ligand_leaving_atoms: bool = False, -) -> tuple[structure.Structure, atom_layout.AtomLayout]: - """Make zero-coordinate structure from all physical residues. - - Args: - struct: Structure object. - ccd: The chemical components dictionary. - with_hydrogens: Whether to keep hydrogen atoms in structure. - skip_unk: Whether to remove unknown residues from structure. - polymer_ligand_bonds: Bond information for polymer-ligand pairs. - ligand_ligand_bonds: Bond information for ligand-ligand pairs. - drop_ligand_leaving_atoms: Flag for handling leaving atoms for ligands. - - Returns: - Tuple of structure with all bonds, physical residues and coordinates set to - 0 and a flat atom layout of empty structure. - """ - bonded_atom_pairs = [] - if polymer_ligand_bonds: - for chain_ids, res_ids, atom_names in zip( - polymer_ligand_bonds.chain_id, - polymer_ligand_bonds.res_id, - polymer_ligand_bonds.atom_name, - strict=True, - ): - bonded_atom_pairs.append(( - (chain_ids[0], res_ids[0], atom_names[0]), - (chain_ids[1], res_ids[1], atom_names[1]), - )) - if ligand_ligand_bonds: - for chain_ids, res_ids, atom_names in zip( - ligand_ligand_bonds.chain_id, - ligand_ligand_bonds.res_id, - ligand_ligand_bonds.atom_name, - strict=True, - ): - bonded_atom_pairs.append(( - (chain_ids[0], res_ids[0], atom_names[0]), - (chain_ids[1], res_ids[1], atom_names[1]), - )) - residues = atom_layout.residues_from_structure( - struct, include_missing_residues=True - ) - - flat_output_layout = atom_layout.make_flat_atom_layout( - residues, - ccd=ccd, - with_hydrogens=with_hydrogens, - skip_unk_residues=skip_unk, - polymer_ligand_bonds=polymer_ligand_bonds, - ligand_ligand_bonds=ligand_ligand_bonds, - drop_ligand_leaving_atoms=drop_ligand_leaving_atoms, - ) - - empty_output_struct = atom_layout.make_structure( - flat_layout=flat_output_layout, - atom_coords=np.zeros((flat_output_layout.shape[0], 3)), - name=struct.name, - atom_b_factors=None, - all_physical_residues=residues, - ) - if bonded_atom_pairs: - empty_output_struct = empty_output_struct.add_bonds( - bonded_atom_pairs, bond_type=mmcif_names.COVALENT_BOND - ) - - return empty_output_struct, flat_output_layout diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/post_processing.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/post_processing.py deleted file mode 100644 index b4e011913..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/post_processing.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Post-processing utilities for AlphaFold inference results.""" - -import dataclasses -import datetime -import os -from typing import Union, Optional - -# from alphafold3 import version -from alphafold3.model import confidence_types -from alphafold3.model import mmcif_metadata -from alphafold3.model.components import base_model -import numpy as np - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class ProcessedInferenceResult: - """Stores attributes of a processed inference result. - - Attributes: - cif: CIF file containing an inference result. - mean_confidence_1d: Mean 1D confidence calculated from confidence_1d. - ranking_score: Ranking score extracted from CIF metadata. - structure_confidence_summary_json: Content of JSON file with structure - confidences summary calculated from CIF file. - structure_full_data_json: Content of JSON file with structure full - confidences calculated from CIF file. - model_id: Identifier of the model that produced the inference result. - """ - - cif: bytes - mean_confidence_1d: float - ranking_score: float - structure_confidence_summary_json: bytes - structure_full_data_json: bytes - model_id: bytes - - -def post_process_inference_result( - inference_result: base_model.InferenceResult, -) -> ProcessedInferenceResult: - """Returns cif, confidence_1d_json, confidence_2d_json, mean_confidence_1d, and ranking confidence.""" - - # Add mmCIF metadata fields. - timestamp = datetime.datetime.now().isoformat(sep=' ', timespec='seconds') - cif_with_metadata = mmcif_metadata.add_metadata_to_mmcif( - old_cif=inference_result.predicted_structure.to_mmcif_dict(), - # version=f'{version.__version__} @ {timestamp}', - # version=None, - model_id=inference_result.model_id, - ) - cif = mmcif_metadata.add_legal_comment(cif_with_metadata.to_string()) - cif = cif.encode('utf-8') - confidence_1d = confidence_types.AtomConfidence.from_inference_result( - inference_result - ) - mean_confidence_1d = np.mean(confidence_1d.confidence) - structure_confidence_summary_json = ( - confidence_types.StructureConfidenceSummary.from_inference_result( - inference_result - ) - .to_json() - .encode('utf-8') - ) - structure_full_data_json = ( - confidence_types.StructureConfidenceFull.from_inference_result( - inference_result - ) - .to_json() - .encode('utf-8') - ) - return ProcessedInferenceResult( - cif=cif, - mean_confidence_1d=mean_confidence_1d, - ranking_score=float(inference_result.metadata['ranking_score']), - structure_confidence_summary_json=structure_confidence_summary_json, - structure_full_data_json=structure_full_data_json, - model_id=inference_result.model_id, - ) - - -def write_output( - inference_result: base_model.InferenceResult, - output_dir: Union[os.PathLike[str], str], - terms_of_use: Optional[str] = None, - name: Optional[str] = None, -) -> None: - """Writes processed inference result to a directory.""" - processed_result = post_process_inference_result(inference_result) - - prefix = f'{name}_' if name is not None else '' - - with open(os.path.join(output_dir, f'{prefix}model.cif'), 'wb') as f: - f.write(processed_result.cif) - - with open( - os.path.join(output_dir, f'{prefix}summary_confidences.json'), 'wb' - ) as f: - f.write(processed_result.structure_confidence_summary_json) - - with open(os.path.join(output_dir, f'{prefix}confidences.json'), 'wb') as f: - f.write(processed_result.structure_full_data_json) - - if terms_of_use is not None: - with open(os.path.join(output_dir, 'TERMS_OF_USE.md'), 'wt', encoding='utf-8') as f: - f.write(terms_of_use) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/protein_data_processing.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/protein_data_processing.py deleted file mode 100644 index 195db4c27..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/protein_data_processing.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Process Structure Data.""" - -from alphafold3.constants import atom_types -from alphafold3.constants import residue_names -from alphafold3.constants import side_chains -import numpy as np - - -NUM_DENSE = atom_types.DENSE_ATOM_NUM -NUM_AA = len(residue_names.PROTEIN_TYPES) -NUM_AA_WITH_UNK_AND_GAP = len( - residue_names.PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP -) -NUM_RESTYPES_WITH_UNK_AND_GAP = ( - residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP -) - - -def _make_restype_rigidgroup_dense_atom_idx(): - """Create Mapping from rigid_groups to dense_atom indices.""" - # Create an array with the atom names. - # shape (num_restypes, num_rigidgroups, 3_atoms): - # (31, 8, 3) - base_atom_indices = np.zeros( - (NUM_RESTYPES_WITH_UNK_AND_GAP, 8, 3), dtype=np.int32 - ) - - # 4,5,6,7: 'chi1,2,3,4-group' - for restype, restype_letter in enumerate( - residue_names.PROTEIN_TYPES_ONE_LETTER - ): - resname = residue_names.PROTEIN_COMMON_ONE_TO_THREE[restype_letter] - - dense_atom_names = atom_types.ATOM14[resname] - # 0: backbone frame - base_atom_indices[restype, 0, :] = [ - dense_atom_names.index(atom) for atom in ['C', 'CA', 'N'] - ] - - # 3: 'psi-group' - base_atom_indices[restype, 3, :] = [ - dense_atom_names.index(atom) for atom in ['CA', 'C', 'O'] - ] - for chi_idx in range(4): - if side_chains.CHI_ANGLES_MASK[restype][chi_idx]: - atom_names = side_chains.CHI_ANGLES_ATOMS[resname][chi_idx] - base_atom_indices[restype, chi_idx + 4, :] = [ - dense_atom_names.index(atom) for atom in atom_names[1:] - ] - dense_atom_names = atom_types.DENSE_ATOM['A'] - nucleic_rigid_atoms = [ - dense_atom_names.index(atom) for atom in ["C1'", "C3'", "C4'"] - ] - for nanum, _ in enumerate(residue_names.NUCLEIC_TYPES): - # 0: backbone frame only. - # we have aa + unk + gap, so we want to start after those - resnum = nanum + NUM_AA_WITH_UNK_AND_GAP - base_atom_indices[resnum, 0, :] = nucleic_rigid_atoms - - return base_atom_indices - - -RESTYPE_RIGIDGROUP_DENSE_ATOM_IDX = _make_restype_rigidgroup_dense_atom_idx() - - -def _make_restype_pseudobeta_idx(): - """Returns indices of residue's pseudo-beta.""" - restype_pseudobeta_index = np.zeros( - (NUM_RESTYPES_WITH_UNK_AND_GAP,), dtype=np.int32 - ) - for restype, restype_letter in enumerate( - residue_names.PROTEIN_TYPES_ONE_LETTER - ): - restype_name = residue_names.PROTEIN_COMMON_ONE_TO_THREE[restype_letter] - atom_names = list(atom_types.ATOM14[restype_name]) - if restype_name in {'GLY'}: - restype_pseudobeta_index[restype] = atom_names.index('CA') - else: - restype_pseudobeta_index[restype] = atom_names.index('CB') - for nanum, resname in enumerate(residue_names.NUCLEIC_TYPES): - atom_names = list(atom_types.DENSE_ATOM[resname]) - # 0: backbone frame only. - # we have aa + unk , so we want to start after those - restype = nanum + NUM_AA_WITH_UNK_AND_GAP - if resname in {'A', 'G', 'DA', 'DG'}: - restype_pseudobeta_index[restype] = atom_names.index('C4') - else: - restype_pseudobeta_index[restype] = atom_names.index('C2') - return restype_pseudobeta_index - - -RESTYPE_PSEUDOBETA_INDEX = _make_restype_pseudobeta_idx() - - -def _make_aatype_dense_atom_to_atom37(): - """Map from dense_atom to atom37 per residue type.""" - restype_dense_atom_to_atom37 = [ - ] # mapping (restype, dense_atom) --> atom37 - for rt in residue_names.PROTEIN_TYPES_ONE_LETTER: - atom_names = list( - atom_types.ATOM14_PADDED[residue_names.PROTEIN_COMMON_ONE_TO_THREE[rt]] - ) - atom_names.extend([''] * (NUM_DENSE - len(atom_names))) - restype_dense_atom_to_atom37.append( - [(atom_types.ATOM37_ORDER[name] if name else 0) - for name in atom_names] - ) - # Add dummy mapping for restype 'UNK', '-' (gap), and nucleics [but not DN]. - for _ in range(2 + len(residue_names.NUCLEIC_TYPES_WITH_UNKNOWN)): - restype_dense_atom_to_atom37.append([0] * NUM_DENSE) - - restype_dense_atom_to_atom37 = np.array( - restype_dense_atom_to_atom37, dtype=np.int32 - ) - return restype_dense_atom_to_atom37 - - -PROTEIN_AATYPE_DENSE_ATOM_TO_ATOM37 = _make_aatype_dense_atom_to_atom37() diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/alignment.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/alignment.py deleted file mode 100644 index 4f10ee738..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/alignment.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Alignment based metrics.""" - -from typing import Optional, Union -import numpy as np - - -def transform_ls( - x: np.ndarray, - b: np.ndarray, - *, - allow_reflection: bool = False, -) -> np.ndarray: - """Find the least squares best fit rotation between two sets of N points. - - Solve Ax = b for A. Where A is the transform rotating x^T into b^T. - - Args: - x: NxD numpy array of coordinates. Usually dimension D is 3. - b: NxD numpy array of coordinates. Usually dimension D is 3. - allow_reflection: Whether the returned transformation can reflect as well as - rotate. - - Returns: - Matrix A transforming x into b, i.e. s.t. Ax^T = b^T. - """ - # First postmultiply by x.; - # Axx^t = b x^t - bxt = np.dot(b.transpose(), x) / b.shape[0] - - u, _, v = np.linalg.svd(bxt) - - r = np.dot(u, v) - if not allow_reflection: - flip = np.ones((v.shape[1], 1)) - flip[v.shape[1] - 1, 0] = np.sign(np.linalg.det(r)) - r = np.dot(u, v * flip) - return r - - -def align( - *, - x: np.ndarray, - y: np.ndarray, - x_indices: np.ndarray, - y_indices: np.ndarray, -) -> np.ndarray: - """Align x to y considering only included_idxs. - - Args: - x: NxD np array of coordinates. - y: NxD np array of coordinates. - x_indices: An np array of indices for `x` that will be used in the - alignment. Must be of the same length as `y_included_idxs`. - y_indices: An np array of indices for `y` that will be used in the - alignment. Must be of the same length as `x_included_idxs`. - - Returns: - NxD np array of points obtained by applying a rigid transformation to x. - These points are aligned to y and the alignment is the optimal alignment - over the points in included_idxs. - - Raises: - ValueError: If the number of included indices is not the same for both - input arrays. - """ - if len(x_indices) != len(y_indices): - raise ValueError( - 'Number of included indices must be the same for both input arrays,' - f' but got for x: {len(x_indices)}, and for y: {len(y_indices)}.' - ) - - x_mean = np.mean(x[x_indices, :], axis=0) - y_mean = np.mean(y[y_indices, :], axis=0) - - centered_x = x - x_mean - centered_y = y - y_mean - t = transform_ls(centered_x[x_indices, :], centered_y[y_indices, :]) - transformed_x = np.dot(centered_x, t.transpose()) + y_mean - - return transformed_x - - -def deviations_from_coords( - decoy_coords: np.ndarray, - gt_coords: np.ndarray, - align_idxs: Optional[np.ndarray] = None, - include_idxs: Optional[np.ndarray] = None, -) -> np.ndarray: - """Returns the raw per-atom deviations used in RMSD computation.""" - if decoy_coords.shape != gt_coords.shape: - raise ValueError( - f'decoy_coords.shape and gt_coords.shape must match.Found: {decoy_coords.shape} and {gt_coords.shape}.' - ) - # Include and align all residues unless specified otherwise. - if include_idxs is None: - include_idxs = np.arange(decoy_coords.shape[0]) - if align_idxs is None: - align_idxs = include_idxs - aligned_decoy_coords = align( - x=decoy_coords, - y=gt_coords, - x_indices=align_idxs, - y_indices=align_idxs, - ) - deviations = np.linalg.norm( - aligned_decoy_coords[include_idxs] - gt_coords[include_idxs], axis=1 - ) - return deviations - - -def rmsd_from_coords( - decoy_coords: Union[np.ndarray, str], - gt_coords: Union[np.ndarray, str], - align_idxs: Optional[np.ndarray] = None, - include_idxs: Optional[np.ndarray] = None, -) -> float: - """Computes the *aligned* RMSD of two Mx3 np arrays of coordinates. - - Args: - decoy_coords: [M, 3] np array of decoy atom coordinates. - gt_coords: [M, 3] np array of gt atom coordinates. - align_idxs: [M] np array of indices specifying coordinates to align on. - Defaults to None, in which case all the include_idx (see after) are used. - include_idxs: [M] np array of indices specifying coordinates to score. - Defaults to None, in which case all indices are used for scoring. - - Returns: - rmsd value of the aligned decoy and gt coordinates. - """ - deviations = deviations_from_coords( - decoy_coords, gt_coords, align_idxs, include_idxs - ) - return np.sqrt(np.mean(np.square(deviations))) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/covalent_bond_cleaning.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/covalent_bond_cleaning.py deleted file mode 100644 index 9202d7c9d..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/covalent_bond_cleaning.py +++ /dev/null @@ -1,264 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Some methods to compute metrics for PTMs.""" - -import collections -from collections.abc import Mapping -import dataclasses -import numpy as np -from alphafold3 import structure -from alphafold3.constants import mmcif_names -from alphafold3.model.atom_layout import atom_layout - - -@dataclasses.dataclass(frozen=True) -class ResIdMapping: - old_res_ids: np.ndarray - new_res_ids: np.ndarray - - -def _count_symmetric_chains(struct: structure.Structure) -> Mapping[str, int]: - """Returns a dict with each chain ID and count.""" - chain_res_name_sequence_from_chain_id = struct.chain_res_name_sequence( - include_missing_residues=True, fix_non_standard_polymer_res=False - ) - counts_for_chain_res_name_sequence = collections.Counter( - chain_res_name_sequence_from_chain_id.values() - ) - chain_symmetric_count = {} - for chain_id, chain_res_name in chain_res_name_sequence_from_chain_id.items(): - chain_symmetric_count[chain_id] = counts_for_chain_res_name_sequence[ - chain_res_name - ] - return chain_symmetric_count - - -def has_nonsymmetric_bonds_on_symmetric_polymer_chains( - struct: structure.Structure, polymer_ligand_bonds: atom_layout.AtomLayout -) -> bool: - """Returns true if nonsymmetric bonds found on polymer chains.""" - try: - _get_polymer_dim(polymer_ligand_bonds) - except ValueError: - return True - if _has_non_polymer_ligand_ptm_bonds(polymer_ligand_bonds): - return True - if _has_multiple_polymers_bonded_to_one_ligand(polymer_ligand_bonds): - return True - combined_struct, _ = _combine_polymer_ligand_ptm_chains( - struct, polymer_ligand_bonds - ) - struct = struct.filter(chain_type=mmcif_names.POLYMER_CHAIN_TYPES) - combined_struct = combined_struct.filter( - chain_type=mmcif_names.POLYMER_CHAIN_TYPES - ) - return _count_symmetric_chains(struct) != _count_symmetric_chains( - combined_struc - ) - - -def _has_non_polymer_ligand_ptm_bonds( - polymer_ligand_bonds: atom_layout.AtomLayout, -): - """Checks if all bonds are between a polymer chain and a ligand chain type.""" - for start_chain_type, end_chain_type in polymer_ligand_bonds.chain_type: - if ( - start_chain_type in mmcif_names.POLYMER_CHAIN_TYPES - and end_chain_type in mmcif_names.LIGAND_CHAIN_TYPES - ): - continue - if ( - start_chain_type in mmcif_names.LIGAND_CHAIN_TYPES - and end_chain_type in mmcif_names.POLYMER_CHAIN_TYPES - ): - continue - return True - return False - - -def _combine_polymer_ligand_ptm_chains( - struct: structure.Structure, - polymer_ligand_bonds: atom_layout.AtomLayout, -) -> tuple[structure.Structure, dict[tuple[str, str], ResIdMapping]]: - """Combines the ptm polymer-ligand chains together. - - This will prevent them from being permuted away from each other when chains - are matched to the ground truth. This function also returns the res_id mapping - from the separate ligand res_ids to their res_ids in the combined - polymer-ligand chain; this information is needed to later separate the - combined polymer-ligand chain. - - Args: - struct: Structure to be modified. - polymer_ligand_bonds: AtomLayout with polymer-ligand bond info. - - Returns: - A tuple of a Structure with each ptm polymer-ligand chain relabelled as one - chain and a dict from bond chain pair to the res_id mapping. - """ - if not _has_only_single_bond_from_each_chain(polymer_ligand_bonds): - if _has_multiple_ligands_bonded_to_one_polymer(polymer_ligand_bonds): - # For structures where a polymer chain is connected to multiple ligands, - # we need to sort the multiple bonds from the same chain by res_id to - # ensure that the combined polymer-ligand chain will always be the same - # when you have repeated symmetric polymer-ligand chains. - polymer_ligand_bonds = ( - _sort_polymer_ligand_bonds_by_polymer_chain_and_res_id( - polymer_ligand_bonds - ) - ) - else: - raise ValueError( - 'Code cannot handle multiple bonds from one chain unless' - ' its several ligands bonded to a polymer.' - ) - res_id_mappings_for_bond_chain_pair = {} - for (start_chain_id, end_chain_id), (start_chain_type, end_chain_type) in zip( - polymer_ligand_bonds.chain_id, polymer_ligand_bonds.chain_type - ): - poly_info, ligand_info = _get_polymer_and_ligand_chain_ids_and_types( - start_chain_id, end_chain_id, start_chain_type, end_chain_type - ) - polymer_chain_id, polymer_chain_type = poly_info - ligand_chain_id, _ = ligand_info - - # Join the ligand chain to the polymer chain. - ligand_res_ids = struct.filter(chain_id=ligand_chain_id).res_id - new_res_ids = ligand_res_ids + \ - len(struct.all_residues[polymer_chain_id]) - res_id_mappings_for_bond_chain_pair[(polymer_chain_id, ligand_chain_id)] = ( - ResIdMapping(old_res_ids=ligand_res_ids, new_res_ids=new_res_ids) - ) - chain_groups = [] - chain_group_ids = [] - chain_group_types = [] - for chain_id, chain_type in zip( - struct.chains_table.id, struct.chains_table.type - ): - if chain_id == ligand_chain_id: - continue - if chain_id == polymer_chain_id: - chain_groups.append([polymer_chain_id, ligand_chain_id]) - chain_group_ids.append(polymer_chain_id) - chain_group_types.append(polymer_chain_type) - else: - chain_groups.append([chain_id]) - chain_group_ids.append(chain_id) - chain_group_types.append(chain_type) - - struct = struct.merge_chains( - chain_groups=chain_groups, - chain_group_ids=chain_group_ids, - chain_group_types=chain_group_types, - ) - - return struct, res_id_mappings_for_bond_chain_pair - - -def _has_only_single_bond_from_each_chain( - polymer_ligand_bonds: atom_layout.AtomLayout, -) -> bool: - """Checks that there is at most one bond from each chain.""" - chain_ids = [] - for chains in polymer_ligand_bonds.chain_id: - chain_ids.extend(chains) - if len(chain_ids) != len(set(chain_ids)): - return False - return True - - -def _get_polymer_and_ligand_chain_ids_and_types( - start_chain_id: str, - end_chain_id: str, - start_chain_type: str, - end_chain_type: str, -) -> tuple[tuple[str, str], tuple[str, str]]: - """Finds polymer and ligand chain ids from chain types.""" - if ( - start_chain_type in mmcif_names.POLYMER_CHAIN_TYPES - and end_chain_type in mmcif_names.LIGAND_CHAIN_TYPES - ): - return (start_chain_id, start_chain_type), (end_chain_id, end_chain_type) - elif ( - start_chain_type in mmcif_names.LIGAND_CHAIN_TYPES - and end_chain_type in mmcif_names.POLYMER_CHAIN_TYPES - ): - return (end_chain_id, end_chain_type), (start_chain_id, start_chain_type) - else: - raise ValueError( - 'This code only handles PTM-bonds from polymer chain to ligands.' - ) - - -def _get_polymer_dim(polymer_ligand_bonds: atom_layout.AtomLayout) -> int: - """Gets polymer dimension from the polymer-ligand bond layout.""" - start_chain_types = [] - end_chain_types = [] - for start_chain_type, end_chain_type in polymer_ligand_bonds.chain_type: - start_chain_types.append(start_chain_type) - end_chain_types.append(end_chain_type) - if set(start_chain_types).issubset( - set(mmcif_names.POLYMER_CHAIN_TYPES) - ) and set(end_chain_types).issubset(set(mmcif_names.LIGAND_CHAIN_TYPES)): - return 0 - elif set(start_chain_types).issubset(mmcif_names.LIGAND_CHAIN_TYPES) and set( - end_chain_types - ).issubset(set(mmcif_names.POLYMER_CHAIN_TYPES)): - return 1 - else: - raise ValueError( - 'Polymer and ligand dimensions are not consistent within the structure.' - ) - - -def _has_multiple_ligands_bonded_to_one_polymer(polymer_ligand_bonds): - """Checks if there are multiple ligands bonded to one polymer.""" - polymer_dim = _get_polymer_dim(polymer_ligand_bonds) - polymer_chain_ids = [ - chains[polymer_dim] for chains in polymer_ligand_bonds.chain_id - ] - if len(polymer_chain_ids) != len(set(polymer_chain_ids)): - return True - return False - - -def _has_multiple_polymers_bonded_to_one_ligand(polymer_ligand_bonds): - """Checks if there are multiple polymer chains bonded to one ligand.""" - polymer_dim = _get_polymer_dim(polymer_ligand_bonds) - ligand_dim = 1 - polymer_dim - ligand_chain_ids = [ - chains[ligand_dim] for chains in polymer_ligand_bonds.chain_id - ] - if len(ligand_chain_ids) != len(set(ligand_chain_ids)): - return True - return False - - -def _sort_polymer_ligand_bonds_by_polymer_chain_and_res_id( - polymer_ligand_bonds, -): - """Sorts bonds by res_id (for when a polymer chain has multiple bonded ligands).""" - - polymer_dim = _get_polymer_dim(polymer_ligand_bonds) - - polymer_chain_ids = [ - chains[polymer_dim] for chains in polymer_ligand_bonds.chain_id - ] - polymer_res_ids = [res[polymer_dim] for res in polymer_ligand_bonds.res_id] - - polymer_chain_and_res_id = zip(polymer_chain_ids, polymer_res_ids) - sorted_indices = [ - idx - for idx, _ in sorted( - enumerate(polymer_chain_and_res_id), key=lambda x: x[1] - ) - ] - return polymer_ligand_bonds[sorted_indices] diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/scoring.py b/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/scoring.py deleted file mode 100644 index 0194faa18..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/model/scoring/scoring.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Library of scoring methods of the model outputs.""" - -from typing import Optional, Union -from alphafold3.model import protein_data_processing -import numpy as np - - -Array = np.ndarray - - -def pseudo_beta_fn( - aatype: Array, - dense_atom_positions: Array, - dense_atom_masks: Array, - is_ligand: Optional[Array] = None, - use_jax: Optional[bool] = True, -) -> Union[tuple[Array, Array], Array]: - """Create pseudo beta atom positions and optionally mask. - - Args: - aatype: [num_res] amino acid types. - dense_atom_positions: [num_res, NUM_DENSE, 3] vector of all atom positions. - dense_atom_masks: [num_res, NUM_DENSE] mask. - is_ligand: [num_res] flag if something is a ligand. - use_jax: whether to use jax for the computations. - - Returns: - Pseudo beta dense atom positions and the corresponding mask. - """ - - if is_ligand is None: - is_ligand = np.zeros_like(aatype) - - pseudobeta_index_polymer = np.take( - protein_data_processing.RESTYPE_PSEUDOBETA_INDEX, aatype, axis=0 - ).astype(np.int32) - - pseudobeta_index = np.where( - is_ligand, - np.zeros_like(pseudobeta_index_polymer), - pseudobeta_index_polymer, - ) - - if not isinstance(dense_atom_positions, Array): - dense_atom_positions = dense_atom_positions.asnumpy() - if not isinstance(dense_atom_masks, Array): - dense_atom_masks = dense_atom_masks.asnumpy() - pseudo_beta = np.take_along_axis( - dense_atom_positions, pseudobeta_index[..., None, None], axis=-2 - ) - pseudo_beta = np.squeeze(pseudo_beta, axis=-2) - - pseudo_beta_mask = np.take_along_axis( - dense_atom_masks, pseudobeta_index[..., None], axis=-1 - ).astype(np.float32) - pseudo_beta_mask = np.squeeze(pseudo_beta_mask, axis=-1) - - return pseudo_beta, pseudo_beta_mask diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict.pyi b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict.pyi deleted file mode 100644 index 09d915c84..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict.pyi +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -from typing import Any, ClassVar, Iterable, Iterator, TypeVar, overload - -import numpy as np - -_T = TypeVar('_T') - -class CifDict: - class ItemView: - def __iter__(self) -> Iterator[tuple[str, list[str]]]: ... - def __len__(self) -> int: ... - - class KeyView: - @overload - def __contains__(self, key: str) -> bool: ... - @overload - def __contains__(self, key: object) -> bool: ... - def __iter__(self) -> Iterator[str]: ... - def __len__(self) -> int: ... - - class ValueView: - def __iter__(self) -> Iterator[list[str]]: ... - def __len__(self) -> int: ... - - def __init__(self, d: dict[str, Iterable[str]]) -> None: ... - def copy_and_update(self, d: dict[str, Iterable[str]]) -> CifDict: ... - def extract_loop_as_dict(self, prefix: str, index: str) -> dict: - """Extracts loop associated with a prefix from mmCIF data as a dict. - - For instance for an mmCIF with these fields: - '_a.ix': ['1', '2', '3'] - '_a.1': ['a.1.1', 'a.1.2', 'a.1.3'] - '_a.2': ['a.2.1', 'a.2.2', 'a.2.3'] - - this function called with prefix='_a.', index='_a.ix' extracts: - {'1': {'a.ix': '1', 'a.1': 'a.1.1', 'a.2': 'a.2.1'} - '2': {'a.ix': '2', 'a.1': 'a.1.2', 'a.2': 'a.2.2'} - '3': {'a.ix': '3', 'a.1': 'a.1.3', 'a.2': 'a.2.3'}} - - Args: - prefix: Prefix shared by each of the data items in the loop. The prefix - should include the trailing period. - index: Which item of loop data should serve as the key. - - Returns: - Dict of dicts; each dict represents 1 entry from an mmCIF loop, - indexed by the index column. - """ - - def extract_loop_as_list(self, prefix: str) -> list: - """Extracts loop associated with a prefix from mmCIF data as a list. - - Reference for loop_ in mmCIF: - http://mmcif.wwpdb.org/docs/tutorials/mechanics/pdbx-mmcif-syntax.html - - For instance for an mmCIF with these fields: - '_a.1': ['a.1.1', 'a.1.2', 'a.1.3'] - '_a.2': ['a.2.1', 'a.2.2', 'a.2.3'] - - this function called with prefix='_a.' extracts: - [{'_a.1': 'a.1.1', '_a.2': 'a.2.1'} - {'_a.1': 'a.1.2', '_a.2': 'a.2.2'} - {'_a.1': 'a.1.3', '_a.2': 'a.2.3'}] - - Args: - prefix: Prefix shared by each of the data items in the loop. The prefix - should include the trailing period. - - Returns: - A list of dicts; each dict represents 1 entry from an mmCIF loop. - """ - - def get(self, key: str, default_value: _T = ...) -> list[str] | _T: ... - def get_array( - self, key: str, dtype: object = ..., gather: object = ... - ) -> np.ndarray: - """Returns values looked up in dict converted to a NumPy array. - - Args: - key: Key in dictionary. - dtype: Optional (default `object`) Specifies output dtype of array. One of - [object, np.{int,uint}{8,16,32,64} np.float{32,64}]. As with NumPy use - `object` to return a NumPy array of strings. - gather: Optional one of [slice, np.{int,uint}{32,64}] non-intermediate - version of get_array(key, dtype)[gather]. - - Returns: - A NumPy array of given dtype. An optimised equivalent to - np.array(cif[key]).astype(dtype). With support of '.' being treated - as np.nan if dtype is one of np.float{32,64}. - Identical strings will all reference the same object to save space. - - Raises: - KeyError - if key is not found. - TypeError - if dtype is not valid or supported. - ValueError - if string cannot convert to dtype. - """ - - def get_data_name(self) -> str: ... - def items(self) -> CifDict.ItemView: ... - def keys(self) -> CifDict.KeyView: ... - def to_string(self) -> str: ... - def value_length(self, key: str) -> int: ... - def values(self) -> CifDict.ValueView: ... - def __bool__(self) -> bool: ... - def __contains__(self, key: str) -> bool: ... - def __getitem__(self, key: str) -> list[str]: ... - def __getstate__(self) -> tuple: ... - def __iter__(self) -> Iterator[str]: ... - def __len__(self) -> int: ... - def __setstate__(self, state: tuple) -> None: ... - -def tokenize(cif_string: str) -> list[str]: ... -def split_line(line: str) -> list[str]: ... -def from_string(mmcif_string: str | bytes) -> CifDict: ... -def parse_multi_data_cif(cif_string: str | bytes) -> dict[str, CifDict]: ... diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.cc deleted file mode 100644 index 2d2675c75..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.cc +++ /dev/null @@ -1,648 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include "alphafold3/parsers/cpp/cif_dict_lib.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "absl/algorithm/container.h" -#include "absl/container/btree_map.h" -#include "absl/container/flat_hash_map.h" -#include "absl/container/flat_hash_set.h" -#include "absl/container/node_hash_map.h" -#include "absl/log/check.h" -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/ascii.h" -#include "absl/strings/match.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" -#include "absl/strings/str_join.h" -#include "absl/strings/str_split.h" -#include "absl/strings/string_view.h" -#include "absl/strings/strip.h" - -namespace alphafold3 { -namespace { - -bool IsQuote(const char symbol) { return symbol == '\'' || symbol == '"'; } -bool IsWhitespace(const char symbol) { return symbol == ' ' || symbol == '\t'; } - -// Splits line into tokens, returns whether successful. -bool SplitLineInline(absl::string_view line, - std::vector* tokens) { - // See https://www.iucr.org/resources/cif/spec/version1.1/cifsyntax - for (int i = 0, line_length = line.length(); i < line_length;) { - // Skip whitespace (spaces or tabs). - while (IsWhitespace(line[i])) { - if (++i == line_length) { - break; - } - } - if (i == line_length) { - break; - } - - // Skip comments (from # until the end of the line). If # is a non-comment - // character, it must be inside a quoted token. - if (line[i] == '#') { - break; - } - - int start_index; - int end_index; - if (IsQuote(line[i])) { - // Token in single or double quotes. CIF v1.1 specification considers a - // quote to be an opening quote only if it is at the beginning of a token. - // So e.g. A' B has tokens A' and B. Also, ""A" is a token "A. - const char quote_char = line[i++]; - start_index = i; - - // Find matching quote. The double loop is not strictly necessary, but - // optimises a bit better. - while (true) { - while (i < line_length && line[i] != quote_char) { - ++i; - } - if (i == line_length) { - // Reached the end of the line while still being inside a token. - return false; - } - if (i + 1 == line_length || IsWhitespace(line[i + 1])) { - break; - } - ++i; - } - end_index = i++; - } else { - // Non-quoted token. Read until reaching whitespace. - start_index = i++; - while (i < line_length && !IsWhitespace(line[i])) { - ++i; - } - end_index = i; - } - - tokens->push_back(line.substr(start_index, end_index - start_index)); - } - - return true; -} - -using HeapStrings = std::vector>; - -// The majority of strings can be viewed on original cif_string. -// heap_strings store multi-line tokens that have internal white-space stripped. -absl::StatusOr> TokenizeInternal( - absl::string_view cif_string, HeapStrings* heap_strings) { - const std::vector lines = absl::StrSplit(cif_string, '\n'); - std::vector tokens; - // Heuristic: Most lines in an mmCIF are _atom_site lines with 21 tokens. - tokens.reserve(lines.size() * 21); - int line_num = 0; - while (line_num < lines.size()) { - auto line = lines[line_num]; - line_num++; - - if (line.empty() || line[0] == '#') { - // Skip empty lines or lines that contain only comments. - continue; - } else if (line[0] == ';') { - // Leading whitespace on each line must be preserved while trailing - // whitespace may be stripped. - std::vector multiline_tokens; - // Strip the leading ";". - multiline_tokens.push_back( - absl::StripTrailingAsciiWhitespace(line.substr(1))); - while (line_num < lines.size()) { - auto multiline = absl::StripTrailingAsciiWhitespace(lines[line_num]); - line_num++; - if (!multiline.empty() && multiline[0] == ';') { - break; - } - multiline_tokens.push_back(multiline); - } - heap_strings->push_back( - std::make_unique(absl::StrJoin(multiline_tokens, "\n"))); - tokens.emplace_back(*heap_strings->back()); - } else { - if (!SplitLineInline(line, &tokens)) { - return absl::InvalidArgumentError( - absl::StrCat("Line ended with quote open: ", line)); - } - } - } - return tokens; -} - -absl::string_view GetEscapeQuote(const absl::string_view value) { - // Empty values should not happen, but if so, they should be quoted. - if (value.empty()) { - return "\""; - } - - // Shortcut for the most common cases where no quoting needed. - if (std::all_of(value.begin(), value.end(), [](char c) { - return absl::ascii_isalnum(c) || c == '.' || c == '?' || c == '-'; - })) { - return ""; - } - - // The value must not start with one of these CIF keywords. - if (absl::StartsWithIgnoreCase(value, "data_") || - absl::StartsWithIgnoreCase(value, "loop_") || - absl::StartsWithIgnoreCase(value, "save_") || - absl::StartsWithIgnoreCase(value, "stop_") || - absl::StartsWithIgnoreCase(value, "global_")) { - return "\""; - } - - // The first character must not be a special character. - const char first = value.front(); - if (first == '_' || first == '#' || first == '$' || first == '[' || - first == ']' || first == ';') { - return "\""; - } - - // No quotes or whitespace allowed inside. - for (const char c : value) { - if (c == '"') { - return "'"; - } else if (c == '\'' || c == ' ' || c == '\t') { - return "\""; - } - } - return ""; -} - -int RecordIndex(absl::string_view record) { - if (record == "_entry") { - return 0; // _entry is always first. - } - if (record == "_atom_site") { - return 2; // _atom_site is always last. - } - return 1; // other records are between _entry and _atom_site. -} - -struct RecordOrder { - using is_transparent = void; // Enable heterogeneous lookup. - bool operator()(absl::string_view lhs, absl::string_view rhs) const { - std::size_t lhs_index = RecordIndex(lhs); - std::size_t rhs_index = RecordIndex(rhs); - return std::tie(lhs_index, lhs) < std::tie(rhs_index, rhs); - } -}; - -// Make sure the _atom_site loop columns are sorted in the PDB-standard way. -constexpr absl::string_view kAtomSiteSortOrder[] = { - "_atom_site.group_PDB", - "_atom_site.id", - "_atom_site.type_symbol", - "_atom_site.label_atom_id", - "_atom_site.label_alt_id", - "_atom_site.label_comp_id", - "_atom_site.label_asym_id", - "_atom_site.label_entity_id", - "_atom_site.label_seq_id", - "_atom_site.pdbx_PDB_ins_code", - "_atom_site.Cartn_x", - "_atom_site.Cartn_y", - "_atom_site.Cartn_z", - "_atom_site.occupancy", - "_atom_site.B_iso_or_equiv", - "_atom_site.pdbx_formal_charge", - "_atom_site.auth_seq_id", - "_atom_site.auth_comp_id", - "_atom_site.auth_asym_id", - "_atom_site.auth_atom_id", - "_atom_site.pdbx_PDB_model_num", -}; - -size_t AtomSiteIndex(absl::string_view atom_site) { - return std::distance(std::begin(kAtomSiteSortOrder), - absl::c_find(kAtomSiteSortOrder, atom_site)); -} - -struct AtomSiteOrder { - bool operator()(absl::string_view lhs, absl::string_view rhs) const { - auto lhs_index = AtomSiteIndex(lhs); - auto rhs_index = AtomSiteIndex(rhs); - return std::tie(lhs_index, lhs) < std::tie(rhs_index, rhs); - } -}; - -class Column { - public: - Column(absl::string_view key, const std::vector* values) - : key_(key), values_(values) { - int max_value_length = 0; - for (size_t i = 0; i < values->size(); ++i) { - absl::string_view value = (*values)[i]; - if (absl::StrContains(value, '\n')) { - values_with_newlines_.insert(i); - } else { - absl::string_view quote = GetEscapeQuote(value); - if (!quote.empty()) { - values_with_quotes_[i] = quote; - } - max_value_length = - std::max(max_value_length, value.size() + quote.size() * 2); - } - } - max_value_length_ = max_value_length; - } - - absl::string_view key() const { return key_; } - - const std::vector* values() const { return values_; } - - int max_value_length() const { return max_value_length_; } - - bool has_newlines(size_t index) const { - return values_with_newlines_.contains(index); - } - - absl::string_view quote(size_t index) const { - if (auto it = values_with_quotes_.find(index); - it != values_with_quotes_.end()) { - return it->second; - } - return ""; - } - - private: - absl::string_view key_; - const std::vector* values_; - int max_value_length_; - // Values with newlines or quotes are very rare in a typical CIF file. - absl::flat_hash_set values_with_newlines_; - absl::flat_hash_map values_with_quotes_; -}; - -struct GroupedKeys { - std::vector grouped_columns; - int max_key_length; - int value_size; -}; - -} // namespace - -absl::StatusOr CifDict::FromString(absl::string_view cif_string) { - CifDict::Dict cif; - - bool loop_flag = false; - absl::string_view key; - - HeapStrings heap_strings; - auto tokens = TokenizeInternal(cif_string, &heap_strings); - if (!tokens.ok()) { - return tokens.status(); - } - - if (tokens->empty()) { - return absl::InvalidArgumentError("The CIF file must not be empty."); - } - - // The first token should be data_XXX. Split into key = data, value = XXX. - absl::string_view first_token = tokens->front(); - if (!absl::ConsumePrefix(&first_token, "data_")) { - return absl::InvalidArgumentError( - "The CIF file does not start with the data_ field."); - } - cif["data_"].emplace_back(first_token); - - // Counters for CIF loop_ regions. - int loop_token_index = 0; - int num_loop_keys = 0; - // Loops have usually O(10) columns but could have up to O(10^6) rows. It is - // therefore wasteful to look up the cif vector where to add a loop value - // since that means doing `columns * rows` map lookups. If we save pointers to - // these loop column fields instead, we need only 1 cif lookup per column. - std::vector*> loop_column_values; - - // Skip the first element since we already processed it above. - for (auto token_itr = tokens->begin() + 1; token_itr != tokens->end(); - ++token_itr) { - auto token = *token_itr; - if (absl::EqualsIgnoreCase(token, "loop_")) { - // A new loop started, get rid of old loop's data. - loop_flag = true; - loop_column_values.clear(); - loop_token_index = 0; - num_loop_keys = 0; - continue; - } else if (loop_flag) { - // The second condition checks we are in the first column. Some mmCIF - // files (e.g. 4q9r) have values in later columns starting with an - // underscore and we don't want to read these as keys. - int token_column_index = - num_loop_keys == 0 ? 0 : loop_token_index % num_loop_keys; - if (token_column_index == 0 && !token.empty() && token[0] == '_') { - if (loop_token_index > 0) { - // We are out of the loop. - loop_flag = false; - } else { - // We are in the keys (column names) section of the loop. - auto& columns = cif[token]; - columns.clear(); - - // Heuristic: _atom_site is typically the largest table in an mmCIF - // with ~16 columns. Make sure we reserve enough space for its values. - if (absl::StartsWith(token, "_atom_site.")) { - columns.reserve(tokens->size() / 20); - } - - // Save the pointer to the loop column values. - loop_column_values.push_back(&columns); - num_loop_keys += 1; - continue; - } - } else { - // We are in the values section of the loop. We have a pointer to the - // loops' values, add the new token in there. - if (token_column_index >= loop_column_values.size()) { - return absl::InvalidArgumentError( - absl::StrCat("Too many columns at: '", token, - "' at column index: ", token_column_index, - " expected at most: ", loop_column_values.size())); - } - loop_column_values[token_column_index]->emplace_back(token); - loop_token_index++; - continue; - } - } - if (key.empty()) { - key = token; - } else { - cif[key].emplace_back(token); - key = ""; - } - } - return CifDict(std::move(cif)); -} - -absl::StatusOr CifDict::ToString() const { - std::string output; - - absl::string_view data_name; - // Check that the data_ field exists. - if (auto name_it = (*dict_).find("data_"); - name_it == (*dict_).end() || name_it->second.empty()) { - return absl::InvalidArgumentError( - "The CIF must contain a valid name for this data block in the special " - "data_ field."); - } else { - data_name = name_it->second.front(); - } - - if (absl::c_any_of(data_name, - [](char i) { return absl::ascii_isspace(i); })) { - return absl::InvalidArgumentError(absl::StrFormat( - "The CIF data block name must not contain any whitespace characters, " - "got '%s'.", - data_name)); - } - absl::StrAppend(&output, "data_", data_name, "\n#\n"); - - // Group keys by their prefix. Use btree_map to iterate in alphabetical order, - // but with some keys being placed at the end (e.g. _atom_site). - absl::btree_map grouped_keys; - for (const auto& [key, values] : *dict_) { - if (key == "data_") { - continue; // Skip the special data_ key, we are already done with it. - } - const std::pair key_parts = - absl::StrSplit(key, absl::MaxSplits('.', 1)); - const absl::string_view key_prefix = key_parts.first; - auto [it, inserted] = grouped_keys.emplace(key_prefix, GroupedKeys{}); - GroupedKeys& grouped_key = it->second; - grouped_key.grouped_columns.push_back(Column(key, &values)); - if (inserted) { - grouped_key.max_key_length = key.length(); - grouped_key.value_size = values.size(); - } else { - grouped_key.max_key_length = - std::max(key.length(), grouped_key.max_key_length); - if (grouped_key.value_size != values.size()) { - return absl::InvalidArgumentError( - absl::StrFormat("Values for key %s have different length (%d) than " - "the other values with the same key prefix (%d).", - key, values.size(), grouped_key.value_size)); - } - } - } - - for (auto& [key_prefix, group_info] : grouped_keys) { - if (key_prefix == "_atom_site") { - // Make sure we sort the _atom_site loop in the standard way. - absl::c_sort(group_info.grouped_columns, - [](const Column& lhs, const Column& rhs) { - return AtomSiteOrder{}(lhs.key(), rhs.key()); - }); - } else { - // Make the key ordering within a key group deterministic. - absl::c_sort(group_info.grouped_columns, - [](const Column& lhs, const Column& rhs) { - return lhs.key() < rhs.key(); - }); - } - - // Force `_atom_site` field to always be a loop. This resolves issues with - // third party mmCIF parsers such as OpenBabel which always expect a loop - // even when there is only a single atom present. - if (group_info.value_size == 1 && key_prefix != "_atom_site") { - // Plain key-value pairs, output them as they are. - for (const Column& grouped_column : group_info.grouped_columns) { - int width = group_info.max_key_length + 1; - size_t start_pos = output.size(); - output.append(width, ' '); - auto out_it = output.begin() + start_pos; - absl::c_copy(grouped_column.key(), out_it); - // Append the value, handle multi-line/quoting. - absl::string_view value = grouped_column.values()->front(); - if (grouped_column.has_newlines(0)) { - absl::StrAppend(&output, "\n;", value, "\n;\n"); // Multi-line value. - } else { - const absl::string_view quote_char = grouped_column.quote(0); - absl::StrAppend(&output, quote_char, value, quote_char, "\n"); - } - } - } else { - // CIF loop. Output the column names, then the rows with data. - absl::StrAppend(&output, "loop_\n"); - for (Column& grouped_column : group_info.grouped_columns) { - absl::StrAppend(&output, grouped_column.key(), "\n"); - } - // Write the loop values, line by line. This is the most expensive part - // since this path is taken to write the entire atom site table which has - // about 20 columns, but thousands of rows. - for (int i = 0; i < group_info.value_size; i++) { - for (int column_index = 0; - column_index < group_info.grouped_columns.size(); ++column_index) { - const Column& grouped_column = - group_info.grouped_columns[column_index]; - const absl::string_view value = (*grouped_column.values())[i]; - if (grouped_column.has_newlines(i)) { - // Multi-line. This is very rarely taken path. - if (column_index == 0) { - // No extra newline before leading ;, already inserted. - absl::StrAppend(&output, ";", value, "\n;\n"); - } else if (column_index == group_info.grouped_columns.size() - 1) { - // No extra newline after trailing ;, will be inserted. - absl::StrAppend(&output, "\n;", value, "\n;"); - } else { - absl::StrAppend(&output, "\n;", value, "\n;\n"); - } - } else { - size_t start_pos = output.size(); - output.append(grouped_column.max_value_length() + 1, ' '); - auto out_it = output.begin() + start_pos; - absl::string_view quote = grouped_column.quote(i); - if (!quote.empty()) { - out_it = absl::c_copy(quote, out_it); - out_it = absl::c_copy(value, out_it); - absl::c_copy(quote, out_it); - } else { - absl::c_copy(value, out_it); - } - } - } - absl::StrAppend(&output, "\n"); - } - } - absl::StrAppend(&output, "#\n"); // Comment token after every key group. - } - return output; -} - -absl::StatusOr< - std::vector>> -CifDict::ExtractLoopAsList(absl::string_view prefix) const { - std::vector column_names; - std::vector> column_data; - - for (const auto& element : *dict_) { - if (absl::StartsWith(element.first, prefix)) { - column_names.emplace_back(element.first); - auto& cells = column_data.emplace_back(); - cells.insert(cells.begin(), element.second.begin(), element.second.end()); - } - } - // Make sure all columns have the same number of rows. - const std::size_t num_rows = column_data.empty() ? 0 : column_data[0].size(); - for (const auto& column : column_data) { - if (column.size() != num_rows) { - return absl::InvalidArgumentError(absl::StrCat( - GetDataName(), - ": Columns do not have the same number of rows for prefix: '", prefix, - "'. One possible reason could be not including the trailing dot, " - "e.g. '_atom_site.'.")); - } - } - - std::vector> result; - result.reserve(num_rows); - CHECK_EQ(column_names.size(), column_data.size()); - for (std::size_t row_index = 0; row_index < num_rows; ++row_index) { - auto& row_dict = result.emplace_back(); - row_dict.reserve(column_names.size()); - for (int col_index = 0; col_index < column_names.size(); ++col_index) { - row_dict[column_names[col_index]] = column_data[col_index][row_index]; - } - } - return result; -} - -absl::StatusOr>> -CifDict::ExtractLoopAsDict(absl::string_view prefix, - absl::string_view index) const { - if (!absl::StartsWith(index, prefix)) { - return absl::InvalidArgumentError( - absl::StrCat(GetDataName(), ": The loop index '", index, - "' must start with the loop prefix '", prefix, "'.")); - } - absl::flat_hash_map> - result; - auto loop_as_list = ExtractLoopAsList(prefix); - if (!loop_as_list.ok()) { - return loop_as_list.status(); - } - result.reserve(loop_as_list->size()); - for (auto& entry : *loop_as_list) { - if (const auto it = entry.find(index); it != entry.end()) { - result[it->second] = entry; - } else { - return absl::InvalidArgumentError(absl::StrCat( - GetDataName(), ": The index column '", index, - "' could not be found in the loop with prefix '", prefix, "'.")); - } - } - return result; -} - -absl::StatusOr> Tokenize( - absl::string_view cif_string) { - HeapStrings heap_strings; - auto tokens = TokenizeInternal(cif_string, &heap_strings); - if (!tokens.ok()) { - return tokens.status(); - } - return std::vector(tokens->begin(), tokens->end()); -} - -absl::StatusOr> SplitLine( - absl::string_view line) { - std::vector tokens; - if (!SplitLineInline(line, &tokens)) { - return absl::InvalidArgumentError( - absl::StrCat("Line ended with quote open: ", line)); - } - return tokens; -} - -absl::StatusOr> ParseMultiDataCifDict( - absl::string_view cif_string) { - absl::flat_hash_map mapping; - constexpr absl::string_view delimiter = "data_"; - // Check cif_string starts with correct offset. - if (!cif_string.empty() && !absl::StartsWith(cif_string, delimiter)) { - return absl::InvalidArgumentError( - "Invalid format. MultiDataCifDict must start with 'data_'"); - } - for (absl::string_view data_block : - absl::StrSplit(cif_string, delimiter, absl::SkipEmpty())) { - absl::string_view block_with_delimitor( - data_block.data() - delimiter.size(), - data_block.size() + delimiter.size()); - absl::StatusOr parsed_block = - CifDict::FromString(block_with_delimitor); - if (!parsed_block.ok()) { - return parsed_block.status(); - } - absl::string_view data_name = parsed_block->GetDataName(); - mapping[data_name] = *std::move(parsed_block); - } - - return mapping; -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.h b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.h deleted file mode 100644 index 5c16eaa87..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_lib.h +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -// A C++ implementation of a CIF parser. For the format specification see -// https://www.iucr.org/resources/cif/spec/version1.1/cifsyntax -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_LIB_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_LIB_H_ - -#include -#include -#include -#include -#include - -#include "absl/container/flat_hash_map.h" -#include "absl/container/node_hash_map.h" -#include "absl/status/statusor.h" -#include "absl/strings/string_view.h" -#include "absl/types/span.h" - -namespace alphafold3 { - -class CifDict { - public: - // Use absl::node_hash_map since it guarantees pointer stability. - using Dict = absl::node_hash_map>; - - CifDict() = default; - - explicit CifDict(Dict dict) - : dict_(std::make_shared(std::move(dict))) {} - - // Converts a CIF string into a dictionary mapping each CIF field to a list of - // values that field contains. - static absl::StatusOr FromString(absl::string_view cif_string); - - // Converts the CIF into into a string that is a valid CIF file. - absl::StatusOr ToString() const; - - // Extracts loop associated with a prefix from mmCIF data as a list. - // Reference for loop_ in mmCIF: - // http://mmcif.wwpdb.org/docs/tutorials/mechanics/pdbx-mmcif-syntax.html - // Args: - // prefix: Prefix shared by each of the data items in the loop. - // e.g. '_entity_poly_seq.', where the data items are _entity_poly_seq.num, - // _entity_poly_seq.mon_id. Should include the trailing period. - // - // Returns a list of dicts; each dict represents 1 entry from an mmCIF loop. - // Lifetime of string_views tied to this. - absl::StatusOr< - std::vector>> - ExtractLoopAsList(absl::string_view prefix) const; - - // Extracts loop associated with a prefix from mmCIF data as a dictionary. - // Args: - // prefix: Prefix shared by each of the data items in the loop. - // e.g. '_entity_poly_seq.', where the data items are _entity_poly_seq.num, - // _entity_poly_seq.mon_id. Should include the trailing period. - // index: Which item of loop data should serve as the key. - // - // Returns a dict of dicts; each dict represents 1 entry from an mmCIF loop, - // indexed by the index column. - // Lifetime of string_views tied to this. - absl::StatusOr>> - ExtractLoopAsDict(absl::string_view prefix, absl::string_view index) const; - - // Returns value at key if present or an empty list. - absl::Span operator[](absl::string_view key) const { - auto it = dict_->find(key); - if (it != dict_->end()) { - return it->second; - } - return {}; - } - - // Returns boolean of whether dict contains key. - bool Contains(absl::string_view key) const { return dict_->contains(key); } - - // Returns number of values for the given key if present, 0 otherwise. - size_t ValueLength(absl::string_view key) const { - return (*this)[key].size(); - } - - // Returns the size of the underlying dictionary. - std::size_t Length() { return dict_->size(); } - - // Creates a copy of this CifDict object that will contain the original values - // but only if not updated by the given dictionary. - // E.g. if the CifDict = {a: [a1, a2], b: [b1]} and other = {a: [x], c: [z]}, - // you will get {a: [x], b: [b1], c: [z]}. - CifDict CopyAndUpdate(Dict other) const { - other.insert(dict_->begin(), dict_->end()); - return CifDict(std::move(other)); - } - - // Returns the value of the special CIF data_ field. - absl::string_view GetDataName() const { - // The data_ element has to be present by construction. - if (auto it = dict_->find("data_"); - it != dict_->end() && !it->second.empty()) { - return it->second.front(); - } else { - return ""; - } - } - - const std::shared_ptr& dict() const { return dict_; } - - private: - std::shared_ptr dict_; -}; - -// Tokenizes a CIF string into a list of string tokens. This is more involved -// than just a simple split on whitespace as CIF allows comments and quoting. -absl::StatusOr> Tokenize(absl::string_view cif_string); - -// Tokenizes a single line of a CIF string. -absl::StatusOr> SplitLine( - absl::string_view line); - -// Parses a CIF string with multiple data records and returns a mapping from -// record names to CifDict objects. For instance, the following CIF string: -// -// data_001 -// _foo bar -// -// data_002 -// _foo baz -// -// will be parsed as: -// {'001': CifDict({'_foo': ['bar']}), -// '002': CifDict({'_foo': ['baz']})} -absl::StatusOr> ParseMultiDataCifDict( - absl::string_view cif_string); - -} // namespace alphafold3 - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_LIB_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.cc deleted file mode 100644 index 130a8215a..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.cc +++ /dev/null @@ -1,652 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "numpy/ndarrayobject.h" -#include "numpy/ndarraytypes.h" -#include "numpy/npy_common.h" -#include "absl/base/no_destructor.h" -#include "absl/container/flat_hash_map.h" -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/numbers.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/string_view.h" -#include "absl/types/span.h" -#include "alphafold3/parsers/cpp/cif_dict_lib.h" -#include "pybind11/attr.h" -#include "pybind11/cast.h" -#include "pybind11/gil.h" -#include "pybind11/pybind11.h" -#include "pybind11/pytypes.h" -#include "pybind11/stl.h" - -namespace alphafold3 { -namespace { -namespace py = pybind11; - -template -bool GatherArray(size_t num_dims, npy_intp* shape_array, npy_intp* stride_array, - const char* data, absl::Span values, - ForEach&& for_each_cb) { - if (num_dims == 1) { - const npy_intp shape = shape_array[0]; - const npy_intp stride = stride_array[0]; - for (size_t i = 0; i < shape; ++i) { - Item index; - std::memcpy(&index, data + stride * i, sizeof(Item)); - if (index < 0 || index >= values.size()) { - PyErr_SetString(PyExc_IndexError, - absl::StrCat("index ", index, - " is out of bounds for column with size ", - values.size()) - .c_str()); - return false; - } - if (!for_each_cb(values[index])) { - return false; - } - } - } else if (num_dims == 0) { - Item index; - std::memcpy(&index, data, sizeof(Item)); - if (index < 0 || index >= values.size()) { - PyErr_SetString( - PyExc_IndexError, - absl::StrCat("index ", index, - " is out of bounds for column with size ", values.size()) - .c_str()); - return false; - } - if (!for_each_cb(values[index])) { - return false; - } - } else { - const npy_intp shape = shape_array[0]; - const npy_intp stride = stride_array[0]; - for (size_t i = 0; i < shape; ++i) { - if (!GatherArray(num_dims - 1, shape_array + 1, stride_array + 1, - data + stride * i, values, for_each_cb)) { - return false; - } - } - } - return true; -} - -template -bool Gather(PyObject* gather, absl::Span values, - Size&& size_cb, ForEach&& for_each_cb) { - if (gather == Py_None) { - npy_intp dim = static_cast(values.size()); - if (!size_cb(absl::MakeSpan(&dim, 1))) { - return false; - } - for (const std::string& v : values) { - if (!for_each_cb(v)) { - return false; - } - } - return true; - } - if (PySlice_Check(gather)) { - Py_ssize_t start, stop, step, slice_length; - if (PySlice_GetIndicesEx(gather, values.size(), &start, &stop, &step, - &slice_length) != 0) { - return false; - } - npy_intp dim = static_cast(slice_length); - if (!size_cb(absl::MakeSpan(&dim, 1))) { - return false; - } - for (size_t i = 0; i < slice_length; ++i) { - if (!for_each_cb(values[start + i * step])) { - return false; - } - } - return true; - } - if (PyArray_Check(gather)) { - PyArrayObject* gather_array = reinterpret_cast(gather); - auto shape = - absl::MakeSpan(PyArray_DIMS(gather_array), PyArray_NDIM(gather_array)); - switch (PyArray_TYPE(gather_array)) { - case NPY_INT16: - if (!size_cb(shape)) { - return false; - } - return GatherArray(shape.size(), shape.data(), - PyArray_STRIDES(gather_array), - PyArray_BYTES(gather_array), values, - std::forward(for_each_cb)); - case NPY_UINT16: - if (!size_cb(shape)) { - return false; - } - return GatherArray(shape.size(), shape.data(), - PyArray_STRIDES(gather_array), - PyArray_BYTES(gather_array), values, - std::forward(for_each_cb)); - case NPY_INT32: - if (!size_cb(shape)) { - return false; - } - return GatherArray(shape.size(), shape.data(), - PyArray_STRIDES(gather_array), - PyArray_BYTES(gather_array), values, - std::forward(for_each_cb)); - case NPY_UINT32: - if (!size_cb(shape)) { - return false; - } - return GatherArray(shape.size(), shape.data(), - PyArray_STRIDES(gather_array), - PyArray_BYTES(gather_array), values, - std::forward(for_each_cb)); - case NPY_INT64: - if (!size_cb(shape)) { - return false; - } - return GatherArray(shape.size(), shape.data(), - PyArray_STRIDES(gather_array), - PyArray_BYTES(gather_array), values, - std::forward(for_each_cb)); - case NPY_UINT64: - if (!size_cb(shape)) { - return false; - } - return GatherArray(shape.size(), shape.data(), - PyArray_STRIDES(gather_array), - PyArray_BYTES(gather_array), values, - std::forward(for_each_cb)); - default: - PyErr_SetString(PyExc_TypeError, "Unsupported NumPy array type."); - return false; - } - } - - PyErr_Format(PyExc_TypeError, "Invalid gather %R", gather); - return false; -} - -// Creates a NumPy array of objects of given strings. Reusing duplicates where -// possible. -PyObject* ConvertStrings(PyObject* gather, PyArray_Descr* type, - absl::Span values) { - absl::flat_hash_map existing; - - PyObject* ret = nullptr; - PyObject** dst; - if (Gather( - gather, values, - [&dst, &ret, type](absl::Span size) { - ret = PyArray_NewFromDescr( - /*subtype=*/&PyArray_Type, - /*type=*/type, - /*nd=*/size.size(), - /*dims=*/size.data(), - /*strides=*/nullptr, - /*data=*/nullptr, - /*flags=*/0, - /*obj=*/nullptr); - dst = static_cast( - PyArray_DATA(reinterpret_cast(ret))); - return true; - }, - [&dst, &existing](absl::string_view value) { - auto [it, inserted] = existing.emplace(value, nullptr); - if (inserted) { - it->second = - PyUnicode_FromStringAndSize(value.data(), value.size()); - PyUnicode_InternInPlace(&it->second); - } else { - Py_INCREF(it->second); - } - *dst++ = it->second; - return true; - })) { - return ret; - } else { - Py_XDECREF(ret); - return nullptr; - } -} - -// Creates NumPy array with given dtype given specified converter. -// `converter` shall have the following signature: -// bool converter(const std::string& value, T* result); -// It must return whether conversion is successful and store conversion in -// result. -template -inline PyObject* Convert(PyObject* gather, PyArray_Descr* type, - absl::Span values, C&& converter) { - py::object ret; - T* dst; - if (Gather( - gather, values, - [&dst, &ret, type](absl::Span size) { - // Construct uninitialised NumPy array of type T. - ret = py::reinterpret_steal(PyArray_NewFromDescr( - /*subtype=*/&PyArray_Type, - /*type=*/type, - /*nd=*/size.size(), - /*dims=*/size.data(), - /*strides=*/nullptr, - /*data=*/nullptr, - /*flags=*/0, - /*obj=*/nullptr)); - - dst = static_cast( - PyArray_DATA(reinterpret_cast(ret.ptr()))); - return true; - }, - [&dst, &converter](const std::string& value) { - if (!converter(value, dst++)) { - PyErr_SetString(PyExc_ValueError, value.c_str()); - return false; - } - return true; - })) { - return ret.release().ptr(); - } - return nullptr; -} - -PyObject* CifDictGetArray(const CifDict& self, absl::string_view key, - PyObject* dtype, PyObject* gather) { - import_array(); - PyArray_Descr* type = nullptr; - if (dtype == Py_None) { - type = PyArray_DescrFromType(NPY_OBJECT); - } else if (PyArray_DescrConverter(dtype, &type) == NPY_FAIL || !type) { - PyErr_Format(PyExc_TypeError, "Invalid dtype %R", dtype); - Py_XDECREF(type); - return nullptr; - } - auto entry = self.dict()->find(key); - if (entry == self.dict()->end()) { - Py_DECREF(type); - PyErr_SetObject(PyExc_KeyError, - PyUnicode_FromStringAndSize(key.data(), key.size())); - return nullptr; - } - - auto int_convert = [](absl::string_view str, auto* value) { - return absl::SimpleAtoi(str, value); - }; - - auto int_convert_bounded = [](absl::string_view str, auto* value) { - int64_t v; - if (absl::SimpleAtoi(str, &v)) { - using limits = - std::numeric_limits>; - if (limits::min() <= v && v <= limits::max()) { - *value = v; - return true; - } - } - return false; - }; - - absl::Span values = entry->second; - - switch (type->type_num) { - case NPY_DOUBLE: - return Convert( - gather, type, values, [](absl::string_view str, double* value) { - if (str == ".") { - *value = std::numeric_limits::quiet_NaN(); - return true; - } - return absl::SimpleAtod(str, value); - }); - case NPY_FLOAT: - return Convert( - gather, type, values, [](absl::string_view str, float* value) { - if (str == ".") { - *value = std::numeric_limits::quiet_NaN(); - return true; - } - return absl::SimpleAtof(str, value); - }); - case NPY_INT8: - return Convert(gather, type, values, int_convert_bounded); - case NPY_INT16: - return Convert(gather, type, values, int_convert_bounded); - case NPY_INT32: - return Convert(gather, type, values, int_convert); - case NPY_INT64: - return Convert(gather, type, values, int_convert); - case NPY_UINT8: - return Convert(gather, type, values, int_convert_bounded); - case NPY_UINT16: - return Convert(gather, type, values, int_convert_bounded); - case NPY_UINT32: - return Convert(gather, type, values, int_convert); - case NPY_UINT64: - return Convert(gather, type, values, int_convert); - case NPY_BOOL: - return Convert(gather, type, values, - [](absl::string_view str, bool* value) { - if (str == "n" || str == "no") { - *value = false; - return true; - } - if (str == "y" || str == "yes") { - *value = true; - return true; - } - return false; - }); - case NPY_OBJECT: - return ConvertStrings(gather, type, values); - default: { - PyErr_Format(PyExc_TypeError, "Unsupported dtype %R", dtype); - Py_XDECREF(type); - return nullptr; - } - } -} - -} // namespace - -void RegisterModuleCifDict(pybind11::module m) { - using Value = std::vector; - static absl::NoDestructor> empty_values; - - m.def( - "from_string", - [](absl::string_view s) { - absl::StatusOr dict = CifDict::FromString(s); - if (!dict.ok()) { - throw py::value_error(dict.status().ToString()); - } - return *dict; - }, - py::call_guard()); - - m.def( - "tokenize", - [](absl::string_view cif_string) { - absl::StatusOr> tokens = Tokenize(cif_string); - if (!tokens.ok()) { - throw py::value_error(tokens.status().ToString()); - } - return *std::move(tokens); - }, - py::arg("cif_string")); - - m.def("split_line", [](absl::string_view line) { - absl::StatusOr> tokens = SplitLine(line); - if (!tokens.ok()) { - throw py::value_error(tokens.status().ToString()); - } - return *std::move(tokens); - }); - - m.def( - "parse_multi_data_cif", - [](absl::string_view cif_string) { - auto result = ParseMultiDataCifDict(cif_string); - if (!result.ok()) { - throw py::value_error(result.status().ToString()); - } - py::dict dict; - for (auto& [key, value] : *result) { - dict[py::cast(key)] = py::cast(value); - } - return dict; - }, - py::arg("cif_string")); - - auto cif_dict = - py::class_(m, "CifDict") - .def(py::init<>([](py::dict dict) { - CifDict::Dict result; - for (const auto& [key, value] : dict) { - result.emplace(py::cast(key), - py::cast>(value)); - } - return CifDict(std::move(result)); - }), - "Initialise with a map") - .def("copy_and_update", - [](const CifDict& self, py::dict dict) { - CifDict::Dict result; - for (const auto& [key, value] : dict) { - result.emplace(py::cast(key), - py::cast>(value)); - } - { - py::gil_scoped_release gil_release; - return self.CopyAndUpdate(std::move(result)); - } - }) - .def( - "__str__", - [](const CifDict& self) { - absl::StatusOr result = self.ToString(); - if (!result.ok()) { - throw py::value_error(result.status().ToString()); - } - return *result; - }, - "Serialize to a string", py::call_guard()) - .def( - "to_string", - [](const CifDict& self) { - absl::StatusOr result = self.ToString(); - if (!result.ok()) { - throw py::value_error(result.status().ToString()); - } - return *result; - }, - "Serialize to a string", py::call_guard()) - .def("value_length", &CifDict::ValueLength, py::arg("key"), - "Num elements in value") - .def("__len__", - [](const CifDict& self) { return self.dict()->size(); }) - .def( - "__bool__", - [](const CifDict& self) { return !self.dict()->empty(); }, - "Check whether the map is nonempty") - .def( - "__contains__", - [](const CifDict& self, absl::string_view k) { - return self.dict()->find(k) != self.dict()->end(); - }, - py::arg("key"), py::call_guard()) - .def("get_data_name", &CifDict::GetDataName) - .def( - "get", - [](const CifDict& self, absl::string_view k, - py::object default_value) -> py::object { - auto it = self.dict()->find(k); - if (it == self.dict()->end()) return default_value; - py::list result(it->second.size()); - size_t index = 0; - for (const std::string& v : it->second) { - result[index++] = py::cast(v); - } - return result; - }, - py::arg("key"), py::arg("default_value") = py::none()) - .def( - "get_array", - [](const CifDict& self, absl::string_view key, py::handle dtype, - py::handle gather) -> py::object { - PyObject* obj = - CifDictGetArray(self, key, dtype.ptr(), gather.ptr()); - if (obj == nullptr) { - throw py::error_already_set(); - } - return py::reinterpret_steal(obj); - }, - py::arg("key"), py::arg("dtype") = py::none(), - py::arg("gather") = py::none()) - .def( - "__getitem__", - [](const CifDict& self, absl::string_view k) -> const Value& { - auto it = self.dict()->find(k); - if (it == self.dict()->end()) { - throw py::key_error(std::string(k).c_str()); - } - return it->second; - }, - py::arg("key"), py::call_guard()) - .def( - "extract_loop_as_dict", - [](const CifDict& self, absl::string_view prefix, - absl::string_view index) { - absl::StatusOr>> - dict; - { - py::gil_scoped_release gil_release; - dict = self.ExtractLoopAsDict(prefix, index); - if (!dict.ok()) { - throw py::value_error(dict.status().ToString()); - } - } - py::dict key_value_dict; - for (const auto& [key, value] : *dict) { - py::dict value_dict; - for (const auto& [key2, value2] : value) { - value_dict[py::cast(key2)] = py::cast(value2); - } - key_value_dict[py::cast(key)] = std::move(value_dict); - } - return key_value_dict; - }, - py::arg("prefix"), py::arg("index")) - .def( - "extract_loop_as_list", - [](const CifDict& self, absl::string_view prefix) { - absl::StatusOr>> - list_dict; - { - py::gil_scoped_release gil_release; - list_dict = self.ExtractLoopAsList(prefix); - if (!list_dict.ok()) { - throw py::value_error(list_dict.status().ToString()); - } - } - py::list list_obj(list_dict->size()); - size_t index = 0; - for (const auto& value : *list_dict) { - py::dict value_dict; - for (const auto& [key, value] : value) { - value_dict[py::cast(key)] = py::cast(value); - } - list_obj[index++] = std::move(value_dict); - } - return list_obj; - }, - py::arg("prefix")) - .def(py::pickle( - [](const CifDict& self) { // __getstate__. - py::tuple result_tuple(1); - py::dict result; - for (const auto& [key, value] : *self.dict()) { - result[py::cast(key)] = py::cast(value); - } - result_tuple[0] = std::move(result); - return result_tuple; - }, - [](py::tuple t) { // __setstate__. - py::dict dict = t[0].cast(); - CifDict::Dict result; - for (const auto& [key, value] : dict) { - result.emplace(py::cast(key), - py::cast>(value)); - } - return CifDict(std::move(result)); - })); - - // Item, value, and key views - struct KeyView { - CifDict map; - }; - - struct ValueView { - CifDict map; - }; - struct ItemView { - CifDict map; - }; - - py::class_(cif_dict, "ItemView") - .def("__len__", [](const ItemView& v) { return v.map.dict()->size(); }) - .def( - "__iter__", - [](const ItemView& v) { - return py::make_iterator(v.map.dict()->begin(), - v.map.dict()->end()); - }, - py::keep_alive<0, 1>()); - - py::class_(cif_dict, "KeyView") - .def("__contains__", - [](const KeyView& v, absl::string_view k) { - return v.map.dict()->find(k) != v.map.dict()->end(); - }) - .def("__contains__", [](const KeyView&, py::handle) { return false; }) - .def("__len__", [](const KeyView& v) { return v.map.dict()->size(); }) - .def( - "__iter__", - [](const KeyView& v) { - return py::make_key_iterator(v.map.dict()->begin(), - v.map.dict()->end()); - }, - py::keep_alive<0, 1>()); - - py::class_(cif_dict, "ValueView") - .def("__len__", [](const ValueView& v) { return v.map.dict()->size(); }) - .def( - "__iter__", - [](const ValueView& v) { - return py::make_value_iterator(v.map.dict()->begin(), - v.map.dict()->end()); - }, - py::keep_alive<0, 1>()); - - cif_dict - .def( - "__iter__", - [](CifDict& self) { - return py::make_key_iterator(self.dict()->begin(), - self.dict()->end()); - }, - py::keep_alive<0, 1>()) - .def( - "keys", [](CifDict& self) { return KeyView{self}; }, - "Returns an iterable view of the map's keys.") - .def( - "values", [](CifDict& self) { return ValueView{self}; }, - "Returns an iterable view of the map's values.") - .def( - "items", [](CifDict& self) { return ItemView{self}; }, - "Returns an iterable view of the map's items."); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.h deleted file mode 100644 index ca4f94702..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/cif_dict_pybind.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleCifDict(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator.pyi b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator.pyi deleted file mode 100644 index d5da60ec8..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator.pyi +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -class FastaFileIterator: - def __init__(self, fasta_path: str) -> None: ... - def __iter__(self) -> FastaFileIterator: ... - def __next__(self) -> tuple[str,str]: ... - -class FastaStringIterator: - def __init__(self, fasta_string: str | bytes) -> None: ... - def __iter__(self) -> FastaStringIterator: ... - def __next__(self) -> tuple[str,str]: ... - -def parse_fasta(fasta_string: str | bytes) -> list[str]: ... -def parse_fasta_include_descriptions(fasta_string: str | bytes) -> tuple[list[str],list[str]]: ... diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_lib.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_lib.cc deleted file mode 100644 index 82cac9343..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_lib.cc +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include "alphafold3/parsers/cpp/fasta_iterator_lib.h" - -#include -#include -#include -#include -#include - -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/ascii.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_split.h" -#include "absl/strings/string_view.h" -#include "absl/strings/strip.h" - -namespace alphafold3 { - -// Parse FASTA string and return list of strings with amino acid sequences. -// Returns a list of amino acid sequences only. -std::vector ParseFasta(absl::string_view fasta_string) { - std::vector sequences; - std::string* sequence = nullptr; - for (absl::string_view line_raw : absl::StrSplit(fasta_string, '\n')) { - absl::string_view line = absl::StripAsciiWhitespace(line_raw); - if (absl::ConsumePrefix(&line, ">")) { - sequence = &sequences.emplace_back(); - } else if (!line.empty() && sequence != nullptr) { - absl::StrAppend(sequence, line); - } - } - return sequences; -} - -// Parse FASTA string and return list of strings with amino acid sequences. -// Returns two lists: The first one with amino acid sequences, the second with -// the descriptions associated with each sequence. -std::pair, std::vector> -ParseFastaIncludeDescriptions(absl::string_view fasta_string) { - std::pair, std::vector> result; - auto& [sequences, descriptions] = result; - std::string* sequence = nullptr; - for (absl::string_view line_raw : absl::StrSplit(fasta_string, '\n')) { - absl::string_view line = absl::StripAsciiWhitespace(line_raw); - if (absl::ConsumePrefix(&line, ">")) { - descriptions.emplace_back(line); - sequence = &sequences.emplace_back(); - } else if (!line.empty() && sequence != nullptr) { - absl::StrAppend(sequence, line); - } - } - return result; -} - -absl::StatusOr> FastaFileIterator::Next() { - std::string line_str; - while (std::getline(reader_, line_str)) { - absl::string_view line = line_str; - line = absl::StripAsciiWhitespace(line); - if (absl::ConsumePrefix(&line, ">")) { - if (!description_.has_value()) { - description_ = line; - } else { - std::pair output(sequence_, *description_); - description_ = line; - sequence_ = ""; - return output; - } - } else if (description_.has_value()) { - absl::StrAppend(&sequence_, line); - } - } - has_next_ = false; - reader_.close(); - if (description_.has_value()) { - return std::pair(sequence_, *description_); - } else { - return absl::InvalidArgumentError( - absl::StrCat("Invalid FASTA file: ", filename_)); - } -} - -absl::StatusOr> -FastaStringIterator::Next() { - size_t consumed = 0; - for (absl::string_view line_raw : absl::StrSplit(fasta_string_, '\n')) { - consumed += line_raw.size() + 1; // +1 for the newline character. - absl::string_view line = absl::StripAsciiWhitespace(line_raw); - if (absl::ConsumePrefix(&line, ">")) { - if (!description_.has_value()) { - description_ = line; - } else { - std::pair output(sequence_, *description_); - description_ = line; - sequence_ = ""; - fasta_string_.remove_prefix(consumed); - return output; - } - } else if (description_.has_value()) { - absl::StrAppend(&sequence_, line); - } - } - has_next_ = false; - if (description_.has_value()) { - return std::pair(sequence_, *description_); - } else { - return absl::InvalidArgumentError("Invalid FASTA string"); - } -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_lib.h b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_lib.h deleted file mode 100644 index 486d05f20..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_lib.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -// A C++ implementation of a FASTA parser. -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_LIB_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_LIB_H_ - -#include -#include -#include -#include -#include -#include - -#include "absl/status/statusor.h" -#include "absl/strings/string_view.h" - -namespace alphafold3 { - -// Parse FASTA string and return list of strings with amino acid sequences. -// Returns a list of amino acid sequences only. -std::vector ParseFasta(absl::string_view fasta_string); - -// Parse FASTA string and return list of strings with amino acid sequences. -// Returns two lists: The first one with amino acid sequences, the second with -// the descriptions associated with each sequence. -std::pair, std::vector> -ParseFastaIncludeDescriptions(absl::string_view fasta_string); - -// Lazy FASTA parser for memory efficient FASTA parsing from a path. -class FastaFileIterator { - public: - // Initialise FastaFileIterator with filename of fasta. If you initialize - // reader_ with an invalid path or empty file, it won't fail, only - // riegeli::ReadLine within the Next method will then return false. That will - // then trigger the "Invalid FASTA file" error. - explicit FastaFileIterator(absl::string_view fasta_path) - : filename_(fasta_path), - reader_(filename_, std::ios::in), - has_next_(true) {} - - // Returns whether there are more sequences. Returns true before first call to - // next even if the file is empty. - bool HasNext() const { return has_next_; } - - // Fetches the next (sequence, description) from the file. - absl::StatusOr> Next(); - - private: - // Use riegeli::FileReader instead of FileLineIterator for about 2x speedup. - std::string filename_; - std::fstream reader_; - std::optional description_; - std::string sequence_; - bool has_next_; -}; - -// Lazy FASTA parser for memory efficient FASTA parsing from a string. -class FastaStringIterator { - public: - // Initialise FastaStringIterator with a string_view of a FASTA. If you - // initialize it with an invalid FASTA string, it won't fail, the Next method - // will then return false. That will then trigger the "Invalid FASTA" error. - // WARNING: The object backing the fasta_string string_view must not be - // deleted while this Iterator is alive. - explicit FastaStringIterator(absl::string_view fasta_string) - : fasta_string_(fasta_string), has_next_(true) {} - - // Returns whether there are more sequences. Returns true before first call to - // next even if the string is empty. - bool HasNext() const { return has_next_; } - - // Fetches the next (sequence, description) from the string. - absl::StatusOr> Next(); - - private: - absl::string_view fasta_string_; - bool has_next_; - std::optional description_; - std::string sequence_; -}; - -} // namespace alphafold3 - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_LIB_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_pybind.cc deleted file mode 100644 index 0b47933d4..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_pybind.cc +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include - -#include "absl/status/statusor.h" -#include "absl/strings/string_view.h" -#include "alphafold3/parsers/cpp/fasta_iterator_lib.h" -#include "pybind11/attr.h" -#include "pybind11/pybind11.h" -#include "pybind11/pytypes.h" -#include "pybind11/stl.h" - -namespace alphafold3 { -namespace { - -namespace py = pybind11; - -template -T ValueOrThrowValueError(absl::StatusOr value) { - if (!value.ok()) throw py::value_error(value.status().ToString()); - return *std::move(value); -} - -constexpr char kFastaFileIteratorDoc[] = R"( -Lazy FASTA parser for memory efficient FASTA parsing from a path.)"; - -constexpr char kFastaStringIteratorDoc[] = R"( -Lazy FASTA parser for memory efficient FASTA parsing from a string. - -WARNING: The object backing the fasta_string string_view must not be -deleted while the FastaStringIterator is alive. E.g. this will break: - -``` -# Make sure the fasta_string is not interned. -fasta_string = '\n'.join(['>d\nS' for _ in range(10)]) -iterator = fasta_iterator.FastaStringIterator(fasta_string) -del fasta_string -iterator.next() # Heap use-after-free. -``` -)"; - -constexpr char kParseFastaDoc[] = R"( -Parses a FASTA string and returns a list of amino-acid sequences. - -Args: - fasta_string: The contents of a FASTA file. - -Returns: - List of sequences in the FASTA file. Descriptions are ignored. -)"; - -constexpr char kParseFastaIncludeDescriptionsDoc[] = R"( -Parses a FASTA string, returns amino-acid sequences with descriptions. - -Args: - fasta_string: The contents of a FASTA file. - -Returns: - A tuple with two lists (sequences, descriptions): - * A list of sequences. - * A list of sequence descriptions taken from the comment lines. In the - same order as the sequences. -)"; - -class PythonFastaStringIterator : public FastaStringIterator { - public: - explicit PythonFastaStringIterator(py::object fasta_string) - : FastaStringIterator(py::cast(fasta_string)), - fasta_string_(std::move(fasta_string)) {} - - private: - py::object fasta_string_; -}; - -} // namespace - -void RegisterModuleFastaIterator(pybind11::module m) { - py::class_(m, "FastaFileIterator", kFastaFileIteratorDoc) - .def(py::init(), py::arg("fasta_path")) - .def("__iter__", - [](FastaFileIterator& iterator) -> FastaFileIterator& { - return iterator; - }) - .def( - "__next__", - [](FastaFileIterator& iterator) { - if (iterator.HasNext()) { - return ValueOrThrowValueError(iterator.Next()); - } else { - throw py::stop_iteration(); - } - }, - py::call_guard()); - - py::class_(m, "FastaStringIterator", - kFastaStringIteratorDoc) - .def(py::init(), py::arg("fasta_string")) - .def("__iter__", - [](PythonFastaStringIterator& iterator) - -> PythonFastaStringIterator& { return iterator; }) - .def( - "__next__", - [](PythonFastaStringIterator& iterator) { - if (iterator.HasNext()) { - return ValueOrThrowValueError(iterator.Next()); - } else { - throw py::stop_iteration(); - } - }, - py::call_guard()); - - m.def("parse_fasta", &ParseFasta, py::arg("fasta_string"), - py::call_guard(), py::doc(kParseFastaDoc + 1)); - m.def("parse_fasta_include_descriptions", &ParseFastaIncludeDescriptions, - py::arg("fasta_string"), py::call_guard(), - py::doc(kParseFastaIncludeDescriptionsDoc + 1)); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_pybind.h deleted file mode 100644 index 091ea3fa2..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/fasta_iterator_pybind.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleFastaIterator(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion.pyi b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion.pyi deleted file mode 100644 index 3602032b9..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion.pyi +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Type annotations for Python bindings for `msa_conversion`. - -The type annotations in this file were modified from the automatically generated -stubgen output. -""" - -from collections.abc import Iterable - - -def align_sequence_to_gapless_query( - sequence: str | bytes, - query_sequence: str | bytes, -) -> str: ... - - -def convert_a3m_to_stockholm(a3m_sequences: Iterable[str]) -> list[str]: ... diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion_pybind.cc deleted file mode 100644 index c192052f0..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion_pybind.cc +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include -#include -#include -#include -#include - -#include "absl/strings/ascii.h" -#include "absl/strings/str_format.h" -#include "absl/strings/string_view.h" -#include "pybind11/pybind11.h" -#include "pybind11/stl.h" - -namespace { - -namespace py = pybind11; - -std::vector ConvertA3MToStockholm( - std::vector a3m_sequences) { - std::vector stockholm_sequences(a3m_sequences.size()); - auto max_length_element = - std::max_element(a3m_sequences.begin(), a3m_sequences.end(), - [](absl::string_view lhs, absl::string_view rhs) { - return lhs.size() < rhs.size(); - }); - - for (auto& out : stockholm_sequences) { - out.reserve(max_length_element->size()); - } - - // While any sequence has remaining columns. - while (std::any_of(a3m_sequences.begin(), a3m_sequences.end(), - [](absl::string_view in) { return !in.empty(); })) { - if (std::any_of(a3m_sequences.begin(), a3m_sequences.end(), - [](absl::string_view in) { - return !in.empty() && absl::ascii_islower(in.front()); - })) { - // Insertion(s) found at column. - for (std::size_t i = 0; i < a3m_sequences.size(); ++i) { - absl::string_view& in = a3m_sequences[i]; - std::string& out = stockholm_sequences[i]; - if (!in.empty() && absl::ascii_islower(in.front())) { - // Consume insertion. - out.push_back(absl::ascii_toupper(in.front())); - in.remove_prefix(1); - } else { - // Row requires padding. - out.push_back('-'); - } - } - } else { - // No insertions found. - for (std::size_t i = 0; i < a3m_sequences.size(); ++i) { - absl::string_view& in = a3m_sequences[i]; - std::string& out = stockholm_sequences[i]; - if (!in.empty()) { - // Consume entire column. - out.push_back(in.front()); - in.remove_prefix(1); - } else { - // One alignment is shorter than the others. Should not happen with - // valid A3M input. - throw std::invalid_argument(absl::StrFormat( - "a3m rows have inconsistent lengths; row %d has no columns left " - "but not all rows are exhausted", - i)); - } - } - } - } - return stockholm_sequences; -} - -std::string AlignSequenceToGaplessQuery(absl::string_view sequence, - absl::string_view query_sequence) { - if (sequence.size() != query_sequence.size()) { - throw py::value_error( - absl::StrFormat("The sequence (%d) and the query sequence (%d) don't " - "have the same length.", - sequence.size(), query_sequence.size())); - } - std::string output; - for (std::size_t residue_index = 0, sequence_length = sequence.size(); - residue_index < sequence_length; ++residue_index) { - const char query_residue = query_sequence[residue_index]; - const char residue = sequence[residue_index]; - if (query_residue != '-') { - // No gap in the query, so the residue is aligned. - output += residue; - } else if (residue == '-') { - // Gap in both sequence and query, simply skip. - continue; - } else { - // Gap only in the query, so this must be an inserted residue. - output += absl::ascii_tolower(residue); - } - } - return output; -} - -constexpr char kConvertA3mToStockholm[] = R"( -Converts a list of sequences in a3m format to stockholm format sequences. - -As an example if the input is: -abCD -CgD -fCDa - -Then the output will be: -ABC-D- ---CGD- -F-C-DA - -Args: - a3m_sequences: A list of strings in a3m format. - -Returns - A list of strings converted to stockholm format. -)"; - -constexpr char kAlignSequenceToGaplessQuery[] = R"( -Aligns a sequence to a gapless query sequence. - -This is useful when converting Stockholm MSA to A3M MSA. Example: -Seq : AB--E -Query: A--DE -Output: Ab-E. - -Args: - sequence: A string containing to be aligned. - query_sequence: A string containing the reference sequence to align to. - -Returns - The input sequence with gaps dropped where both the `sequence` and - `query_sequence` have gaps, and sequence elements non-capitalized where the - `query_sequence` has a gap, but the `sequence` does not. -)"; - -} // namespace - -namespace alphafold3 { - -void RegisterModuleMsaConversion(pybind11::module m) { - m.def("convert_a3m_to_stockholm", &ConvertA3MToStockholm, - py::arg("a3m_sequences"), py::call_guard(), - py::doc(kConvertA3mToStockholm + 1)); - m.def("align_sequence_to_gapless_query", &AlignSequenceToGaplessQuery, - py::arg("sequence"), py::arg("query_sequence"), - py::call_guard(), - py::doc(kAlignSequenceToGaplessQuery + 1)); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion_pybind.h deleted file mode 100644 index 65f5fe99e..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/parsers/cpp/msa_conversion_pybind.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_MSA_CONVERSION_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_MSA_CONVERSION_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleMsaConversion(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_MSA_CONVERSION_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/__init__.py b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/__init__.py deleted file mode 100644 index 17f44cd06..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Structure module initialization.""" - -# pylint: disable=g-importing-member -from alphafold3.structure.bioassemblies import BioassemblyData -from alphafold3.structure.bonds import Bonds -from alphafold3.structure.chemical_components import ChemCompEntry -from alphafold3.structure.chemical_components import ChemicalComponentsData -from alphafold3.structure.chemical_components import get_data_for_ccd_components -from alphafold3.structure.chemical_components import populate_missing_ccd_data -from alphafold3.structure.mmcif import BondParsingError -from alphafold3.structure.parsing import BondAtomId -from alphafold3.structure.parsing import from_atom_arrays -from alphafold3.structure.parsing import from_mmcif -from alphafold3.structure.parsing import from_parsed_mmcif -from alphafold3.structure.parsing import from_res_arrays -from alphafold3.structure.parsing import from_sequences_and_bonds -from alphafold3.structure.parsing import ModelID -from alphafold3.structure.parsing import SequenceFormat -from alphafold3.structure.structure import ARRAY_FIELDS -from alphafold3.structure.structure import AuthorNamingScheme -from alphafold3.structure.structure import Bond -from alphafold3.structure.structure import CascadeDelete -from alphafold3.structure.structure import concat -from alphafold3.structure.structure import enumerate_residues -from alphafold3.structure.structure import fix_non_standard_polymer_residues -from alphafold3.structure.structure import GLOBAL_FIELDS -from alphafold3.structure.structure import make_empty_structure -from alphafold3.structure.structure import MissingAtomError -from alphafold3.structure.structure import MissingAuthorResidueIdError -from alphafold3.structure.structure import multichain_residue_index -from alphafold3.structure.structure import stack -from alphafold3.structure.structure import Structure -from alphafold3.structure.structure_tables import Atoms -from alphafold3.structure.structure_tables import Chains -from alphafold3.structure.structure_tables import Residues diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/bioassemblies.py b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/bioassemblies.py deleted file mode 100644 index 6c1d8e3cc..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/bioassemblies.py +++ /dev/null @@ -1,333 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Utilities for parsing and manipulating bioassembly data.""" - -from collections.abc import Mapping, Sequence -import copy -import dataclasses -from typing_extensions import Self - -from alphafold3.structure import mmcif -import numpy as np - - -@dataclasses.dataclass(frozen=True) -class Operation: - """A rigid transformation operation.""" - - trans: np.ndarray # shape: (3,) - rot: np.ndarray # shape: (3, 3) - - def apply_to_coords(self, coords: np.ndarray) -> np.ndarray: - """Applies the rotation followed by the translation to `coords`.""" - return np.dot(coords, self.rot.T) + self.trans[np.newaxis, :] - - -@dataclasses.dataclass(frozen=True) -class Transform: - """A rigid transformation composed of a sequence of `Operation`s.""" - - # The sequence of operations that form the transform. These will be applied - # right-to-left (last-to-first). - operations: Sequence[Operation] - - # The chain IDs that this transform should be applied to. These are - # label_asym_ids in the mmCIF spec. - chain_ids: Sequence[str] - - # A mapping from chain IDs (of chains that participate in this transform) - # to their new values in the bioassembly. - chain_id_rename_map: Mapping[str, str] - - def apply_to_coords(self, coords: np.ndarray) -> np.ndarray: - """Applies the `operations` in right-to-left order.""" - for operation in reversed(self.operations): - coords = operation.apply_to_coords(coords) - return coords - - -def _get_operation(oper_data: Mapping[str, str]) -> Operation: - """Parses an `Operation` from a mmCIF _pdbx_struct_oper_list row.""" - trans = np.zeros((3,), dtype=np.float32) - rot = np.zeros((3, 3), dtype=np.float32) - for i in range(3): - trans[i] = float(oper_data[f'_pdbx_struct_oper_list.vector[{i + 1}]']) - for i in range(3): - for j in range(3): - rot[i][j] = float( - oper_data[f'_pdbx_struct_oper_list.matrix[{i + 1}][{j + 1}]'] - ) - return Operation(trans=trans, rot=rot) - - -class MissingBioassemblyDataError(Exception): - """Raised when bioassembly data is missing from an mmCIF.""" - - -class BioassemblyData: - """Stores and processes bioassembly data from mmCIF tables.""" - - # Not all of these columns are required for internal operations, but all - # should be present whenever bioassemblies are defined in an mmCIF to stay - # consistent with external mmCIFs. - _REQUIRED_COLUMNS = ( - '_pdbx_struct_assembly.id', - '_pdbx_struct_assembly.details', - '_pdbx_struct_assembly.method_details', - '_pdbx_struct_assembly.oligomeric_details', - '_pdbx_struct_assembly.oligomeric_count', - '_pdbx_struct_assembly_gen.assembly_id', - '_pdbx_struct_assembly_gen.oper_expression', - '_pdbx_struct_assembly_gen.asym_id_list', - '_pdbx_struct_oper_list.id', - '_pdbx_struct_oper_list.type', - '_pdbx_struct_oper_list.name', - '_pdbx_struct_oper_list.symmetry_operation', - '_pdbx_struct_oper_list.matrix[1][1]', - '_pdbx_struct_oper_list.matrix[1][2]', - '_pdbx_struct_oper_list.matrix[1][3]', - '_pdbx_struct_oper_list.vector[1]', - '_pdbx_struct_oper_list.matrix[2][1]', - '_pdbx_struct_oper_list.matrix[2][2]', - '_pdbx_struct_oper_list.matrix[2][3]', - '_pdbx_struct_oper_list.vector[2]', - '_pdbx_struct_oper_list.matrix[3][1]', - '_pdbx_struct_oper_list.matrix[3][2]', - '_pdbx_struct_oper_list.matrix[3][3]', - '_pdbx_struct_oper_list.vector[3]', - ) - - def __init__( - self, - *, - pdbx_struct_assembly: Mapping[str, Mapping[str, str]], - pdbx_struct_assembly_gen: Mapping[str, Sequence[Mapping[str, str]]], - pdbx_struct_oper_list: Mapping[str, Mapping[str, str]], - assembly_ids: Sequence[str], - oper_ids: Sequence[str], - ): - for assembly_id in assembly_ids: - for table, table_name in ( - (pdbx_struct_assembly, '_pdbx_struct_assembly'), - (pdbx_struct_assembly_gen, '_pdbx_struct_assembly_gen'), - ): - if assembly_id not in table: - raise ValueError( - f'Assembly ID "{assembly_id}" missing from {table_name} ' - f'with keys: {table.keys()}' - ) - for oper_id in oper_ids: - if oper_id not in pdbx_struct_oper_list: - raise ValueError( - f'Oper ID "{oper_id}" missing from _pdbx_struct_oper_list ' - f'with keys: {pdbx_struct_oper_list.keys()}' - ) - - self._pdbx_struct_assembly = pdbx_struct_assembly - self._pdbx_struct_assembly_gen = pdbx_struct_assembly_gen - self._pdbx_struct_oper_list = pdbx_struct_oper_list - self._operations = { - oper_id: _get_operation(oper_data) - for oper_id, oper_data in self._pdbx_struct_oper_list.items() - } - self._assembly_ids = assembly_ids - self._oper_ids = oper_ids - - @classmethod - def from_mmcif(cls, cif: mmcif.Mmcif) -> Self: - """Constructs an instance of `BioassemblyData` from an `Mmcif` object.""" - for col in cls._REQUIRED_COLUMNS: - if col not in cif: - raise MissingBioassemblyDataError(col) - - pdbx_struct_assembly = cif.extract_loop_as_dict( - prefix='_pdbx_struct_assembly.', index='_pdbx_struct_assembly.id' - ) - pdbx_struct_oper_list = cif.extract_loop_as_dict( - prefix='_pdbx_struct_oper_list.', index='_pdbx_struct_oper_list.id' - ) - - # _pdbx_struct_assembly_gen is unlike the other two tables because it can - # have multiple rows share the same assembly ID. This can happen when an - # assembly is constructed by applying different sets of transforms to - # different sets of chain IDs. Each of these would have its own row. - # Here we group rows by their assembly_id. - pdbx_struct_assembly_gen = {} - for assembly_id, oper_expression, asym_id_list in zip( - cif['_pdbx_struct_assembly_gen.assembly_id'], - cif['_pdbx_struct_assembly_gen.oper_expression'], - cif['_pdbx_struct_assembly_gen.asym_id_list'], - ): - pdbx_struct_assembly_gen.setdefault(assembly_id, []).append({ - '_pdbx_struct_assembly_gen.assembly_id': assembly_id, - '_pdbx_struct_assembly_gen.oper_expression': oper_expression, - '_pdbx_struct_assembly_gen.asym_id_list': asym_id_list, - }) - - # We provide these separately to keep track of the original order that they - # appear in the mmCIF. - assembly_ids = cif['_pdbx_struct_assembly.id'] - oper_ids = cif['_pdbx_struct_oper_list.id'] - return cls( - pdbx_struct_assembly=pdbx_struct_assembly, - pdbx_struct_assembly_gen=pdbx_struct_assembly_gen, - pdbx_struct_oper_list=pdbx_struct_oper_list, - assembly_ids=assembly_ids, - oper_ids=oper_ids, - ) - - @property - def assembly_ids(self) -> Sequence[str]: - return self._assembly_ids - - def asym_id_by_assembly_chain_id(self, assembly_id: str) -> Mapping[str, str]: - asym_id_by_assembly_chain_id = {} - for transform in self.get_transforms(assembly_id): - for asym_id, assembly_chain_id in transform.chain_id_rename_map.items(): - asym_id_by_assembly_chain_id[assembly_chain_id] = asym_id - return asym_id_by_assembly_chain_id - - def assembly_chain_ids_by_asym_id( - self, assembly_id: str - ) -> Mapping[str, set[str]]: - assembly_chain_ids_by_asym_id = {} - for transform in self.get_transforms(assembly_id): - for asym_id, assembly_chain_id in transform.chain_id_rename_map.items(): - assembly_chain_ids_by_asym_id.setdefault(asym_id, set()).add( - assembly_chain_id - ) - return assembly_chain_ids_by_asym_id - - def get_default_assembly_id(self) -> str: - """Gets a default assembly ID.""" - # The first assembly is usually (though not always) the best choice. - # If we find a better heuristic for picking bioassemblies then this - # method should be updated. - return min(self._assembly_ids) - - def get_assembly_info(self, assembly_id: str) -> Mapping[str, str]: - return { - k.replace('_pdbx_struct_assembly.', ''): v - for k, v in self._pdbx_struct_assembly[assembly_id].items() - } - - def get_transforms(self, assembly_id: str) -> Sequence[Transform]: - """Returns the transforms required to generate the given assembly.""" - partial_transforms = [] - all_chain_ids = set() - for row in self._pdbx_struct_assembly_gen[assembly_id]: - oper_expression = row['_pdbx_struct_assembly_gen.oper_expression'] - parsed_oper_id_seqs = mmcif.parse_oper_expr(oper_expression) - label_asym_ids = row['_pdbx_struct_assembly_gen.asym_id_list'].split( - ',') - all_chain_ids |= set(label_asym_ids) - for parsed_oper_id_seq in parsed_oper_id_seqs: - partial_transforms.append((parsed_oper_id_seq, label_asym_ids)) - - # We start assigning new chain IDs by finding the largest chain ID in - # the original structure that is involved in this bioassembly, and then - # starting from the next one. - max_int_chain_id = max(mmcif.str_id_to_int_id(c) - for c in all_chain_ids) - next_int_chain_id = max_int_chain_id + 1 - - transforms = [] - has_been_renamed = set() - for parsed_oper_id_seq, label_asym_ids in partial_transforms: - chain_id_rename_map = {} - for label_asym_id in label_asym_ids: - if label_asym_id not in has_been_renamed: - # The first time we see a label_asym_id we don't need to rename it. - # This isn't strictly necessary since we don't provide any - # guarantees about chain naming after bioassembly extraction but - # can make it a bit easier to inspect and compare structures - # pre and post bioassembly extraction. - chain_id_rename_map[label_asym_id] = label_asym_id - has_been_renamed.add(label_asym_id) - else: - chain_id_rename_map[label_asym_id] = mmcif.int_id_to_str_id( - next_int_chain_id - ) - next_int_chain_id += 1 - transforms.append( - Transform( - operations=[ - self._operations[oper_id] for oper_id in parsed_oper_id_seq - ], - chain_ids=label_asym_ids, - chain_id_rename_map=chain_id_rename_map, - ) - ) - return transforms - - def to_mmcif_dict(self) -> Mapping[str, Sequence[str]]: - """Returns the bioassembly data as a dict suitable for `mmcif.Mmcif`.""" - mmcif_dict = {} - for assembly_id in self._assembly_ids: - for column, val in self._pdbx_struct_assembly[assembly_id].items(): - mmcif_dict.setdefault(column, []).append(val) - for row in self._pdbx_struct_assembly_gen[assembly_id]: - for column, val in row.items(): - mmcif_dict.setdefault(column, []).append(val) - for oper_id in self._oper_ids: - for column, val in self._pdbx_struct_oper_list[oper_id].items(): - mmcif_dict.setdefault(column, []).append(val) - return mmcif_dict - - def rename_label_asym_ids( - self, - mapping: Mapping[str, str], - present_chains: set[str], - ) -> Self: - """Returns a new BioassemblyData with renamed label_asym_ids. - - Args: - mapping: A mapping from original label_asym_ids to their new values. Any - label_asym_ids in this BioassemblyData that are not in this mapping will - remain unchanged. - present_chains: A set of label_asym_ids that are actually present in the - atom site list. All label_asym_ids that are in the BioassemblyData but - not in present_chains won't be included in the output BioassemblyData. - - Returns: - A new BioassemblyData with renamed label_asym_ids. - - Raises: - ValueError: If any two previously distinct chains do not have unique names - anymore after the rename. - """ - new_pdbx_struct_assembly_gen = copy.deepcopy( - self._pdbx_struct_assembly_gen) - for rows in new_pdbx_struct_assembly_gen.values(): - for row in rows: - old_asym_ids = row['_pdbx_struct_assembly_gen.asym_id_list'].split( - ',') - new_asym_ids = [ - mapping.get(label_asym_id, label_asym_id) - for label_asym_id in old_asym_ids - if label_asym_id in present_chains - ] - if len(set(old_asym_ids) & present_chains) != len(set(new_asym_ids)): - raise ValueError( - 'Can not rename chains, the new names are not unique: ' - f'{sorted(new_asym_ids)}.' - ) - row['_pdbx_struct_assembly_gen.asym_id_list'] = ','.join( - new_asym_ids) # pytype: disable=unsupported-operands - - return BioassemblyData( - pdbx_struct_assembly=copy.deepcopy(self._pdbx_struct_assembly), - pdbx_struct_assembly_gen=new_pdbx_struct_assembly_gen, - pdbx_struct_oper_list=copy.deepcopy(self._pdbx_struct_oper_list), - assembly_ids=copy.deepcopy(self._assembly_ids), - oper_ids=copy.deepcopy(self._oper_ids), - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/bonds.py b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/bonds.py deleted file mode 100644 index dc89d3401..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/bonds.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Bond representation for structure module.""" - -import collections -from collections.abc import Mapping, Sequence -import dataclasses -import typing -from typing import Optional, Union -from typing_extensions import Self - -from alphafold3.structure import table -import numpy as np - - -@dataclasses.dataclass(frozen=True) -class Bonds(table.Table): - """Table of atomic bonds.""" - - # mmCIF column: _struct_conn.conn_type_id - # mmCIF desc: This data item is a pointer to _struct_conn_type.id in the - # STRUCT_CONN_TYPE category. - # E.g.: "covale", "disulf", "hydrog", "metalc". - type: np.ndarray - - # mmCIF column: _struct_conn.pdbx_role - # mmCIF desc: The chemical or structural role of the interaction. - # E.g.: "N-Glycosylation", "O-Glycosylation". - role: np.ndarray - - # mmCIF columns: _struct_conn.ptnr1_* - from_atom_key: np.ndarray - - # mmCIF columns: _struct_conn.ptnr2_* - dest_atom_key: np.ndarray - key: np.ndarray - - @classmethod - def make_empty(cls) -> Self: - return cls( - key=np.empty((0,), dtype=np.int64), # pylint: disable=unexpected-keyword-arg - from_atom_key=np.empty((0,), dtype=np.int64), - dest_atom_key=np.empty((0,), dtype=np.int64), - type=np.empty((0,), dtype=object), - role=np.empty((0,), dtype=object), - ) - - def get_atom_indices( - self, - atom_key: np.ndarray, - ) -> tuple[np.ndarray, np.ndarray]: - """Returns the indices of the from/dest atoms in the atom_key array.""" - from_atom_missing = ~np.isin(self.from_atom_key, atom_key) - dest_atom_missing = ~np.isin(self.dest_atom_key, atom_key) - if np.any(from_atom_missing): - raise ValueError( - f'No atoms for from_atom_key {self.from_atom_key[from_atom_missing]}' - ) - if np.any(dest_atom_missing): - raise ValueError( - f'No atoms for dest_atom_key {self.dest_atom_key[dest_atom_missing]}' - ) - sort_indices = np.argsort(atom_key) - from_indices_sorted = np.searchsorted( - atom_key, self.from_atom_key, sorter=sort_indices - ) - dest_indices_sorted = np.searchsorted( - atom_key, self.dest_atom_key, sorter=sort_indices - ) - from_indices = sort_indices[from_indices_sorted] - dest_indices = sort_indices[dest_indices_sorted] - return from_indices, dest_indices - - def restrict_to_atoms(self, atom_key: np.ndarray) -> Self: - if not self.size: # Early-out for empty table. - return self - from_atom_mask = np.isin(self.from_atom_key, atom_key) - dest_atom_mask = np.isin(self.dest_atom_key, atom_key) - mask = np.logical_and(from_atom_mask, dest_atom_mask) - return typing.cast(Bonds, self.filter(mask=mask)) - - def to_mmcif_dict_from_atom_arrays( - self, - atom_key: np.ndarray, - chain_id: np.ndarray, - res_id: np.ndarray, - res_name: np.ndarray, - atom_name: np.ndarray, - auth_asym_id: np.ndarray, - auth_seq_id: np.ndarray, - insertion_code: np.ndarray, - ) -> Mapping[str, Union[Sequence[str], np.ndarray]]: - """Returns a dict suitable for building a CifDict, representing bonds. - - Args: - atom_key: A (num_atom,) integer array of atom_keys. - chain_id: A (num_atom,) array of label_asym_id strings. - res_id: A (num_atom,) array of label_seq_id strings. - res_name: A (num_atom,) array of label_comp_id strings. - atom_name: A (num_atom,) array of label_atom_id strings. - auth_asym_id: A (num_atom,) array of auth_asym_id strings. - auth_seq_id: A (num_atom,) array of auth_seq_id strings. - insertion_code: A (num_atom,) array of insertion code strings. - """ - mmcif_dict = collections.defaultdict(list) - ptnr1_indices, ptnr2_indices = self.get_atom_indices(atom_key) - - mmcif_dict['_struct_conn.ptnr1_label_asym_id'] = chain_id[ptnr1_indices] - mmcif_dict['_struct_conn.ptnr2_label_asym_id'] = chain_id[ptnr2_indices] - mmcif_dict['_struct_conn.ptnr1_label_comp_id'] = res_name[ptnr1_indices] - mmcif_dict['_struct_conn.ptnr2_label_comp_id'] = res_name[ptnr2_indices] - mmcif_dict['_struct_conn.ptnr1_label_seq_id'] = res_id[ptnr1_indices] - mmcif_dict['_struct_conn.ptnr2_label_seq_id'] = res_id[ptnr2_indices] - mmcif_dict['_struct_conn.ptnr1_label_atom_id'] = atom_name[ptnr1_indices] - mmcif_dict['_struct_conn.ptnr2_label_atom_id'] = atom_name[ptnr2_indices] - - mmcif_dict['_struct_conn.ptnr1_auth_asym_id'] = auth_asym_id[ptnr1_indices] - mmcif_dict['_struct_conn.ptnr2_auth_asym_id'] = auth_asym_id[ptnr2_indices] - mmcif_dict['_struct_conn.ptnr1_auth_seq_id'] = auth_seq_id[ptnr1_indices] - mmcif_dict['_struct_conn.ptnr2_auth_seq_id'] = auth_seq_id[ptnr2_indices] - mmcif_dict['_struct_conn.pdbx_ptnr1_PDB_ins_code'] = insertion_code[ - ptnr1_indices - ] - mmcif_dict['_struct_conn.pdbx_ptnr2_PDB_ins_code'] = insertion_code[ - ptnr2_indices - ] - - label_alt_id = ['?'] * self.size - mmcif_dict['_struct_conn.pdbx_ptnr1_label_alt_id'] = label_alt_id - mmcif_dict['_struct_conn.pdbx_ptnr2_label_alt_id'] = label_alt_id - - # We need to set this to make visualisation work in NGL/PyMOL. - mmcif_dict['_struct_conn.pdbx_value_order'] = ['?'] * self.size - - # We use a symmetry of 1_555 which is the no-op transformation. Other - # values are used when bonds involve atoms that only exist after expanding - # the bioassembly, but we don't support this kind of bond at the moment. - symmetry = ['1_555'] * self.size - mmcif_dict['_struct_conn.ptnr1_symmetry'] = symmetry - mmcif_dict['_struct_conn.ptnr2_symmetry'] = symmetry - bond_type_counter = collections.Counter() - for bond_row in self.iterrows(): - bond_type = bond_row['type'] - bond_type_counter[bond_type] += 1 - mmcif_dict['_struct_conn.id'].append( - f'{bond_type}{bond_type_counter[bond_type]}' - ) - mmcif_dict['_struct_conn.pdbx_role'].append(bond_row['role']) - mmcif_dict['_struct_conn.conn_type_id'].append(bond_type) - - bond_types = np.unique(self.type) - mmcif_dict['_struct_conn_type.id'] = bond_types - unknown = ['?'] * len(bond_types) - mmcif_dict['_struct_conn_type.criteria'] = unknown - mmcif_dict['_struct_conn_type.reference'] = unknown - - return dict(mmcif_dict) - - -def concat_with_atom_keys( - bonds_tables: Sequence[Optional[Bonds]], - atom_key_arrays: Sequence[np.ndarray], -) -> tuple[Optional[Bonds], np.ndarray]: - """Concatenates bonds tables and atom keys simultaneously. - - Args: - bonds_tables: A sequence of `Bonds` instances to concatenate. If any are - None then these are skipped. - atom_key_arrays: A sequence of integer `atom_key` arrays, where the n-th - bonds_table refers to the atoms in the n-th atom_key array. These must - all be non-None. - - Returns: - A pair of (bonds, atom_key) where atom_key is a unique atom_key array with - length equal to the sum of the input atom array sizes, and the bonds table - contains all the bonds from the individual bonds table inputs. - """ - if not bonds_tables or not atom_key_arrays: - if bonds_tables or atom_key_arrays: - raise ValueError( - 'bonds_tables and atom_keys must have same length but got' - f' {len(bonds_tables)=} and {len(atom_key_arrays)=}' - ) - return None, np.array([], dtype=np.int64) - max_key = -1 - atom_keys_to_concat = [] - types_to_concat = [] - roles_to_concat = [] - from_atom_keys_to_concat = [] - dest_atom_keys_to_concat = [] - for bonds, atom_key in zip(bonds_tables, atom_key_arrays, strict=True): - if not atom_key.size: - continue - # Should always be non-negative! - offset = max_key + 1 - offset_atom_key = atom_key + offset - atom_keys_to_concat.append(offset_atom_key) - max_key = np.max(offset_atom_key) - if bonds is not None: - types_to_concat.append(bonds.type) - roles_to_concat.append(bonds.role) - from_atom_keys_to_concat.append(bonds.from_atom_key + offset) - dest_atom_keys_to_concat.append(bonds.dest_atom_key + offset) - - if atom_keys_to_concat: - concatted_atom_keys = np.concatenate(atom_keys_to_concat, axis=0) - else: - concatted_atom_keys = np.array([], dtype=np.int64) - - if types_to_concat: - num_bonds = sum(b.size for b in bonds_tables if b is not None) - concatted_bonds = Bonds( - key=np.arange(num_bonds, dtype=np.int64), # pylint: disable=unexpected-keyword-arg - type=np.concatenate(types_to_concat, axis=0), - role=np.concatenate(roles_to_concat, axis=0), - from_atom_key=np.concatenate(from_atom_keys_to_concat, axis=0), - dest_atom_key=np.concatenate(dest_atom_keys_to_concat, axis=0), - ) - else: - concatted_bonds = None - - return concatted_bonds, concatted_atom_keys diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/chemical_components.py b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/chemical_components.py deleted file mode 100644 index 195fed31d..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/chemical_components.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Utilities for manipulating chemical components data.""" - -from collections.abc import Iterable, Mapping, Sequence -import dataclasses -import functools -from typing import Optional -from typing_extensions import Self - -from alphafold3.constants import chemical_components -from alphafold3.constants import residue_names -from alphafold3.structure import mmcif -import rdkit.Chem as rd_chem - - -@dataclasses.dataclass(frozen=True) -class ChemCompEntry: - """Items of _chem_comp category. - - For the full list of items and their semantics see - http://mmcif.rcsb.org/dictionaries/mmcif_pdbx_v50.dic/Categories/chem_comp.html - """ - - type: str - name: str = '?' - pdbx_synonyms: str = '?' - formula: str = '?' - formula_weight: str = '?' - mon_nstd_flag: str = '?' - pdbx_smiles: Optional[str] = None - - def __post_init__(self): - for field, value in vars(self).items(): - if not value and value is not None: - raise ValueError(f"{field} value can't be an empty string.") - - def extends(self, other: Self) -> bool: - """Checks whether this ChemCompEntry extends another one.""" - for field, value in vars(self).items(): - other_value = getattr(other, field) - if _value_is_missing(other_value): - continue - if value != other_value: - return False - return True - - @property - def rdkit_mol(self) -> rd_chem.Mol: - """Returns an RDKit Mol, created via RDKit from entry SMILES string.""" - if not self.pdbx_smiles: - raise ValueError( - 'Cannot construct RDKit Mol with empty pdbx_smiles') - return rd_chem.MolFromSmiles(self.pdbx_smiles) - - -_REQUIRED_MMCIF_COLUMNS = ('_chem_comp.id', '_chem_comp.type') - - -class MissingChemicalComponentsDataError(Exception): - """Raised when chemical components data is missing from an mmCIF.""" - - -@dataclasses.dataclass(frozen=True) -class ChemicalComponentsData: - """Extra information for chemical components occurring in mmCIF. - - Fields: - chem_comp: A mapping from _chem_comp.id to associated items in the - chem_comp category. - """ - - chem_comp: Mapping[str, ChemCompEntry] - - @classmethod - def from_mmcif( - cls, cif: mmcif.Mmcif, fix_mse: bool, fix_unknown_dna: bool - ) -> Self: - """Constructs an instance of ChemicalComponentsData from an Mmcif object.""" - for col in _REQUIRED_MMCIF_COLUMNS: - if col not in cif: - raise MissingChemicalComponentsDataError(col) - - id_ = cif['_chem_comp.id'] # Guaranteed to be present. - type_ = cif['_chem_comp.type'] # Guaranteed to be present. - name = cif.get('_chem_comp.name', ['?'] * len(id_)) - synonyms = cif.get('_chem_comp.pdbx_synonyms', ['?'] * len(id_)) - formula = cif.get('_chem_comp.formula', ['?'] * len(id_)) - weight = cif.get('_chem_comp.formula_weight', ['?'] * len(id_)) - mon_nstd_flag = cif.get('_chem_comp.mon_nstd_flag', ['?'] * len(id_)) - smiles = cif.get('_chem_comp.pdbx_smiles', ['?'] * len(id_)) - smiles = [None if s == '?' else s for s in smiles] - - chem_comp = { - component_name: ChemCompEntry(*entry) - for component_name, *entry in zip( - id_, type_, name, synonyms, formula, weight, mon_nstd_flag, smiles - ) - } - - if fix_mse and 'MSE' in chem_comp: - if 'MET' not in chem_comp: - chem_comp['MET'] = ChemCompEntry( - type='L-PEPTIDE LINKING', - name='METHIONINE', - pdbx_synonyms='?', - formula='C5 H11 N O2 S', - formula_weight='149.211', - mon_nstd_flag='y', - pdbx_smiles=None, - ) - - if fix_unknown_dna and 'N' in chem_comp: - # Do not delete 'N' as it may be needed for RNA in the system. - if 'DN' not in chem_comp: - chem_comp['DN'] = ChemCompEntry( - type='DNA LINKING', - name="UNKNOWN 2'-DEOXYNUCLEOTIDE", - pdbx_synonyms='?', - formula='C5 H11 O6 P', - formula_weight='198.111', - mon_nstd_flag='y', - pdbx_smiles=None, - ) - - return ChemicalComponentsData(chem_comp) - - def to_mmcif_dict(self) -> Mapping[str, Sequence[str]]: - """Returns chemical components data as a dict suitable for `mmcif.Mmcif`.""" - mmcif_dict = {} - - mmcif_fields = set() - for entry in self.chem_comp.values(): - for field, value in vars(entry).items(): - if value: - mmcif_fields.add(field) - chem_comp_ids = [] - for component_id in sorted(self.chem_comp): - entry = self.chem_comp[component_id] - chem_comp_ids.append(component_id) - for field in mmcif_fields: - mmcif_dict.setdefault(f'_chem_comp.{field}', []).append( - getattr(entry, field) or '?' - ) - if chem_comp_ids: - mmcif_dict['_chem_comp.id'] = chem_comp_ids - return mmcif_dict - - -def _value_is_missing(value: str) -> bool: - return not value or value in ('.', '?') - - -def get_data_for_ccd_components( - ccd: chemical_components.Ccd, - chemical_component_ids: Iterable[str], - populate_pdbx_smiles: bool = False, -) -> ChemicalComponentsData: - """Returns `ChemicalComponentsData` for chemical components known by PDB.""" - chem_comp = {} - for chemical_component_id in chemical_component_ids: - chem_data = chemical_components.component_name_to_info( - ccd=ccd, res_name=chemical_component_id - ) - if not chem_data: - continue - chem_comp[chemical_component_id] = ChemCompEntry( - type=chem_data.type, - name=chem_data.name, - pdbx_synonyms=chem_data.pdbx_synonyms, - formula=chem_data.formula, - formula_weight=chem_data.formula_weight, - mon_nstd_flag=chem_data.mon_nstd_flag, - pdbx_smiles=( - chem_data.pdbx_smiles or None if populate_pdbx_smiles else None - ), - ) - return ChemicalComponentsData(chem_comp=chem_comp) - - -def populate_missing_ccd_data( - ccd: chemical_components.Ccd, - chemical_components_data: ChemicalComponentsData, - chemical_component_ids: Optional[Iterable[str]] = None, - populate_pdbx_smiles: bool = False, -) -> ChemicalComponentsData: - """Populates missing data for the chemical components from CCD. - - Args: - ccd: The chemical components database. - chemical_components_data: ChemicalComponentsData to populate missing values - for. This function doesn't modify the object, extended version is provided - as a return value. - chemical_component_ids: chemical components to populate missing values for. - If not specified, the function will consider all chemical components which - are already present in `chemical_components_data`. - populate_pdbx_smiles: whether to populate `pdbx_smiles` field using SMILES - descriptors from _pdbx_chem_comp_descriptor CCD table. If CCD provides - multiple SMILES strings, any of them could be used. - - Returns: - New instance of ChemicalComponentsData without missing values for CCD - entries. - """ - if chemical_component_ids is None: - chemical_component_ids = chemical_components_data.chem_comp.keys() - - ccd_data = get_data_for_ccd_components( - ccd, chemical_component_ids, populate_pdbx_smiles - ) - chem_comp = dict(chemical_components_data.chem_comp) - for component_id, ccd_entry in ccd_data.chem_comp.items(): - if component_id not in chem_comp: - chem_comp[component_id] = ccd_entry - else: - already_specified_fields = { - field: value - for field, value in vars(chem_comp[component_id]).items() - if not _value_is_missing(value) - } - chem_comp[component_id] = ChemCompEntry( - **{**vars(ccd_entry), **already_specified_fields} - ) - return ChemicalComponentsData(chem_comp=chem_comp) - - -def get_all_atoms_in_entry( - ccd: chemical_components.Ccd, res_name: str -) -> Mapping[str, Sequence[str]]: - """Get all possible atoms and bonds for this residue in a standard order. - - Args: - ccd: The chemical components dictionary. - res_name: Full CCD name. - - Returns: - A dictionary table of the atoms and bonds for this residue in this residue - type. - """ - # The CCD version of 'UNK' is weird. It has a CB and a CG atom. We just want - # the minimal amino-acid here which is GLY. - if res_name == 'UNK': - res_name = 'GLY' - ccd_data = ccd.get(res_name) - if not ccd_data: - raise ValueError(f'Unknown residue type {res_name}') - - keys = ( - '_chem_comp_atom.atom_id', - '_chem_comp_atom.type_symbol', - '_chem_comp_bond.atom_id_1', - '_chem_comp_bond.atom_id_2', - ) - - # Add terminal hydrogens for protonation of the N-terminal - if res_name == 'PRO': - res_atoms = {key: [*ccd_data.get(key, [])] for key in keys} - res_atoms['_chem_comp_atom.atom_id'].extend(['H2', 'H3']) - res_atoms['_chem_comp_atom.type_symbol'].extend(['H', 'H']) - res_atoms['_chem_comp_bond.atom_id_1'].extend(['N', 'N']) - res_atoms['_chem_comp_bond.atom_id_2'].extend(['H2', 'H3']) - elif res_name in residue_names.PROTEIN_TYPES_WITH_UNKNOWN: - res_atoms = {key: [*ccd_data.get(key, [])] for key in keys} - res_atoms['_chem_comp_atom.atom_id'].append('H3') - res_atoms['_chem_comp_atom.type_symbol'].append('H') - res_atoms['_chem_comp_bond.atom_id_1'].append('N') - res_atoms['_chem_comp_bond.atom_id_2'].append('H3') - else: - res_atoms = {key: ccd_data.get(key, []) for key in keys} - - return res_atoms - - -@functools.lru_cache(maxsize=128) -def get_res_atom_names(ccd: chemical_components.Ccd, res_name: str) -> set[str]: - """Gets the names of the atoms in a given CCD residue.""" - atoms = get_all_atoms_in_entry(ccd, res_name)['_chem_comp_atom.atom_id'] - return set(atoms) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation.pyi b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation.pyi deleted file mode 100644 index 8f4a8b375..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation.pyi +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -from collections.abc import Sequence - -def indices_grouped_by_value(values: Sequence[int]) -> dict[int, list[int]]: ... diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation_pybind.cc deleted file mode 100644 index 5ac46d62c..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation_pybind.cc +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include -#include - -#include "absl/container/flat_hash_map.h" -#include "absl/types/span.h" -#include "pybind11/cast.h" -#include "pybind11/numpy.h" -#include "pybind11/pybind11.h" -#include "pybind11_abseil/absl_casters.h" - -namespace { - -namespace py = pybind11; - -absl::flat_hash_map> IndicesGroupedByValue( - absl::Span values) { - absl::flat_hash_map> group_indices; - for (int64_t i = 0, e = values.size(); i < e; ++i) { - group_indices[values[i]].push_back(i); - } - return group_indices; -} - -constexpr char kIndicesGroupedByValue[] = R"( -Returns a map from value to a list of indices this value occupies. - -E.g. indices_grouped_by_value([1, 1, 2, 3, 3, 1, 1]) returns: -{1: [0, 1, 5, 6], 2: [2], 3: [3, 4]} - -Args: - values: a list of values to group. -)"; - -} // namespace - -namespace alphafold3 { - -void RegisterModuleAggregation(py::module m) { - m.def("indices_grouped_by_value", &IndicesGroupedByValue, py::arg("values"), - py::doc(kIndicesGroupedByValue + 1), - py::call_guard()); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation_pybind.h deleted file mode 100644 index 9547b9448..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/aggregation_pybind.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_AGGREGATION_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_AGGREGATION_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleAggregation(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_AGGREGATION_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership.pyi b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership.pyi deleted file mode 100644 index 305f36600..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership.pyi +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -import numpy - - -def isin( - array: numpy.ndarray[numpy.int64], - test_elements: set[int], - invert: bool = ..., -) -> numpy.ndarray[bool]: ... diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership_pybind.cc deleted file mode 100644 index 2b3faf8a2..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership_pybind.cc +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include -#include -#include -#include - -#include "absl/container/flat_hash_set.h" -#include "pybind11/cast.h" -#include "pybind11/numpy.h" -#include "pybind11/pybind11.h" -#include "pybind11_abseil/absl_casters.h" - -namespace { - -namespace py = pybind11; - -py::array_t IsIn(const py::array_t& array, - const absl::flat_hash_set& test_elements, - bool invert) { - const size_t num_elements = array.size(); - - py::array_t output(num_elements); - std::fill(output.mutable_data(), output.mutable_data() + output.size(), - invert); - - // Shortcut: The output will be trivially always false if test_elements empty. - if (test_elements.empty()) { - return output; - } - - for (size_t i = 0; i < num_elements; ++i) { - if (test_elements.contains(array.data()[i])) { - output.mutable_data()[i] = !invert; - } - } - if (array.ndim() > 1) { - auto shape = - std::vector(array.shape(), array.shape() + array.ndim()); - return output.reshape(shape); - } - return output; -} - -constexpr char kIsInDoc[] = R"( -Computes whether each element is in test_elements. - -Same use as np.isin, but much faster. If len(array) = n, len(test_elements) = m: -* This function has complexity O(n). -* np.isin with kind='sort' has complexity O(m*log(m) + n * log(m)). - -Args: - array: Input NumPy array with dtype=np.int64. - test_elements: The values against which to test each value of array. - invert: If True, the values in the returned array are inverted, as if - calculating `element not in test_elements`. Default is False. - `isin(a, b, invert=True)` is equivalent to but faster than `~isin(a, b)`. - -Returns - A boolean array of the same shape as the input array. Each value `val` is: - * `val in test_elements` if `invert=False`, - * `val not in test_elements` if `invert=True`. -)"; - -} // namespace - -namespace alphafold3 { - -void RegisterModuleMembership(pybind11::module m) { - m.def("isin", &IsIn, py::arg("array"), py::arg("test_elements"), - py::kw_only(), py::arg("invert") = false, py::doc(kIsInDoc + 1)); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership_pybind.h deleted file mode 100644 index d224fb1f6..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/membership_pybind.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MEMBERSHIP_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MEMBERSHIP_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleMembership(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MEMBERSHIP_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.cc deleted file mode 100644 index cea9a1b1c..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.cc +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include "alphafold3/structure/cpp/mmcif_altlocs.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "absl/algorithm/container.h" -#include "absl/log/log.h" -#include "absl/strings/numbers.h" -#include "absl/strings/string_view.h" -#include "absl/types/span.h" -#include "alphafold3/structure/cpp/mmcif_layout.h" - -namespace alphafold3 { -namespace { - -float OccupancyToFloat(absl::string_view occupancy) { - float result = 0.0f; - LOG_IF(ERROR, !absl::SimpleAtof(occupancy, &result)) - << "Invalid Occupancy: " << occupancy; - return result; -} - -// Deuterium is the same atom as Hydrogen so keep equivalent for grouping. -bool AtomEquiv(absl::string_view lhs, absl::string_view rhs) { - if (lhs == rhs) return true; - if (lhs.empty() != rhs.empty()) return false; - // Both lhs and rhs are guaranteed to be non-empty after this. - char first_lhs = lhs.front(); - char second_rhs = rhs.front(); - if ((first_lhs == 'H' && second_rhs == 'D') || - (first_lhs == 'D' && second_rhs == 'H')) { - lhs.remove_prefix(1); - rhs.remove_prefix(1); - return lhs == rhs; - } - return false; -} - -// Calls group_callback with that start index and count for each group of -// equivalent values in `values`, starting at `start` and ending at `count`. -// Example: -// GroupBy({"B", "B", "B", "C", "C"}, 0, 5, [](size_t start, size_t count) { -// absl::Printf("start=%d, count=%d\n", start, count); -// }); -// Would print: -// start=0, count=3 -// start=3, count=2 -template > -void GroupBy(absl::Span values, std::size_t start, - std::size_t count, GroupCallback&& group_callback, - IsEqual&& is_equal = std::equal_to{}) { - std::size_t span_start = start; - if (count > 0) { - for (std::size_t i = start + 1; i < start + count; ++i) { - if (!is_equal(values[i], values[span_start])) { - group_callback(span_start, i - span_start); - span_start = i; - } - } - group_callback(span_start, start + count - span_start); - } -} - -void ProcessAltLocGroupsWhole(std::size_t alt_loc_start, - std::size_t alt_loc_count, - absl::Span comp_ids, - absl::Span atom_ids, - absl::Span alt_ids, - absl::Span occupancies, - std::vector& in_out_keep_indices) { - std::pair best_split = {alt_loc_start, - alt_loc_count}; - std::vector alt_loc_groups; - float best_occupancy = -std::numeric_limits::infinity(); - char best_group = alt_ids[alt_loc_start].front(); - std::vector> occupancy_stats; - - // Group by residue type. - GroupBy(comp_ids, alt_loc_start, alt_loc_count, - [&](std::size_t start, std::size_t count) { - // This callback selects the best residue group and the best - // Alt-loc char within that group. - alt_loc_groups.clear(); - occupancy_stats.clear(); - // Calculate total occupancy for residue type. - for (std::size_t i = 0; i < count; ++i) { - char alt_loc_id = alt_ids[start + i].front(); - float occupancy = OccupancyToFloat(occupancies[start + i]); - if (auto loc = absl::c_find(alt_loc_groups, alt_loc_id); - loc == alt_loc_groups.end()) { - occupancy_stats.emplace_back(1, occupancy); - alt_loc_groups.push_back(alt_loc_id); - } else { - auto& stat = - occupancy_stats[std::distance(alt_loc_groups.begin(), loc)]; - ++stat.first; - stat.second += occupancy; - } - } - float total_occupancy = 0.0; - for (auto& stat : occupancy_stats) { - total_occupancy += stat.second / stat.first; - } - char group = *absl::c_min_element(alt_loc_groups); - // Compares occupancy of residue to best seen so far. - // Tie breaks alphabetic. - if (total_occupancy > best_occupancy || - (total_occupancy == best_occupancy && group < best_group)) { - // Selects the best sub group. - best_group = alt_loc_groups.front(); - float best_amount = occupancy_stats.front().second / - occupancy_stats.front().first; - for (std::size_t i = 1; i < occupancy_stats.size(); ++i) { - float amount = - occupancy_stats[i].second / occupancy_stats[i].first; - char group = alt_loc_groups[i]; - if (amount > best_amount || - (amount == best_amount && group < best_group)) { - best_amount = amount; - best_group = group; - } - } - best_occupancy = total_occupancy; - best_split = {start, count}; - } - }); - - // Now that the best residue type has been selected and the best alt-loc - // within that has been selected add indices of indices to keep to the keep - // list. - auto [split_start, split_count] = best_split; - GroupBy( - atom_ids, split_start, split_count, - [&in_out_keep_indices, &alt_ids, best_group](std::size_t start, - std::size_t count) { - // This makes sure we select an atom for each atom id even if it does - // not have our selected alt-loc char. - std::size_t best_index = start; - for (std::size_t i = 1; i < count; ++i) { - if (alt_ids[start + i].front() == best_group) { - best_index = start + i; - break; - } - } - in_out_keep_indices.push_back(best_index); - }, - AtomEquiv); -} - -// Finds the alt-loc group with the highest score and pushes the indices on to -// the back of in_out_keep_indices. -void ProcessAltLocGroupPartial( - std::size_t alt_loc_start, std::size_t alt_loc_count, - absl::Span atom_ids, - absl::Span alt_ids, - absl::Span occupancies, - std::vector& in_out_keep_indices) { - GroupBy( - atom_ids, alt_loc_start, alt_loc_count, - [&](std::size_t start, std::size_t count) { - if (count == 1) { - in_out_keep_indices.push_back(start); - } else { - float best_occ = OccupancyToFloat(occupancies[start]); - std::size_t best_index = start; - char best_group = alt_ids[start].front(); - for (std::size_t i = 0; i < count; ++i) { - float occ = OccupancyToFloat(occupancies[start + i]); - char group = alt_ids[start + i].front(); - if (occ > best_occ || (occ == best_occ && group < best_group)) { - best_group = group; - best_index = start + i; - best_occ = occ; - } - } - in_out_keep_indices.push_back(best_index); - } - }, - AtomEquiv); -} - -} // namespace - -// Resolves alt-locs returning the atom indices that will be left. -std::vector ResolveMmcifAltLocs( - const MmcifLayout& layout, absl::Span comp_ids, - absl::Span atom_ids, - absl::Span alt_ids, - absl::Span occupancies, - absl::Span chain_indices) { - std::vector keep_indices; - keep_indices.reserve(layout.num_atoms()); - std::size_t alt_loc_start = 0; - for (std::size_t chain_index : chain_indices) { - auto [residues_start, residues_end] = layout.residue_range(chain_index); - for (std::size_t residue = residues_start; residue < residues_end; - ++residue) { - std::size_t alt_loc_count = 0; - auto [atom_start, atom_end] = layout.atom_range(residue); - for (std::size_t i = atom_start; i < atom_end; ++i) { - char alt_loc_id = alt_ids[i].front(); - if (alt_loc_id == '.' || alt_loc_id == '?') { - if (alt_loc_count > 0) { - ProcessAltLocGroupPartial(alt_loc_start, alt_loc_count, atom_ids, - alt_ids, occupancies, keep_indices); - alt_loc_count = 0; - } - keep_indices.push_back(i); - } else { - if (alt_loc_count == 0) { - alt_loc_start = i; - } - ++alt_loc_count; - } - } - if (alt_loc_count > 0) { - if (atom_end - atom_start == alt_loc_count) { - ProcessAltLocGroupsWhole(alt_loc_start, alt_loc_count, comp_ids, - atom_ids, alt_ids, occupancies, - keep_indices); - } else { - ProcessAltLocGroupPartial(alt_loc_start, alt_loc_count, atom_ids, - alt_ids, occupancies, keep_indices); - } - } - } - } - - return keep_indices; -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.h b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.h deleted file mode 100644 index fab57817c..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_altlocs.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ALTLOCS_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ALTLOCS_H_ - -#include -#include -#include -#include - -#include "absl/types/span.h" -#include "alphafold3/structure/cpp/mmcif_layout.h" - -namespace alphafold3 { - -// Returns the list of indices that should be kept after resolving alt-locs. -// 1) Partial Residue. Each cycle of alt-locs are resolved separately with the -// highest occupancy alt-loc. Tie-breaks are resolved alphabetically. See -// tests for examples. -// 2) Whole Residue. These are resolved in two passes. -// a) The residue with the highest occupancy is chosen. -// b) The locations for a given residue are resolved. -// All tie-breaks are resolved alphabetically. See tests for examples. -// -// Preconditions: layout and comp_ids, alt_ids, occupancies are all from same -// mmCIF file and chain_indices are monotonically increasing and less than -// layout.num_chains(). -// -// comp_ids from '_atom_site.label_comp_id'. -// alt_ids from '_atom_site.label_alt_id'. -// occupancies from '_atom_site.occupancy'. -std::vector ResolveMmcifAltLocs( - const MmcifLayout& layout, absl::Span comp_ids, - absl::Span atom_ids, - absl::Span alt_ids, - absl::Span occupancies, - absl::Span chain_indices); - -} // namespace alphafold3 - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ALTLOCS_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site.pyi b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site.pyi deleted file mode 100644 index 5f0ba34b0..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site.pyi +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -from collections.abc import Callable -from alphafold3.cpp import cif_dict - - -def get_internal_to_author_chain_id_map( - mmcif: cif_dict.CifDict -) -> dict[str,str]: ... - - -def get_or_infer_type_symbol( - mmcif: cif_dict.CifDict, - atom_id_to_type_symbol: Callable[[str, str], str], -) -> list[str]: ... diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site_pybind.cc deleted file mode 100644 index 6037fe08b..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site_pybind.cc +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include - -#include "absl/container/flat_hash_map.h" -#include "absl/log/check.h" -#include "absl/strings/string_view.h" -#include "absl/types/span.h" -#include "alphafold3/parsers/cpp/cif_dict_lib.h" -#include "pybind11/gil.h" -#include "pybind11/pybind11.h" -#include "pybind11/pytypes.h" -#include "pybind11/stl.h" -#include "pybind11_abseil/absl_casters.h" - -namespace alphafold3 { -namespace { -namespace py = pybind11; - -// If present, returns the _atom_site.type_symbol. If not, infers it using -// _atom_site.label_comp_id (residue name), _atom_site.label_atom_id (atom name) -// and the CCD. -py::list GetOrInferTypeSymbol(const CifDict& mmcif, - const py::object& atom_id_to_type_symbol) { - const auto& type_symbol = mmcif["_atom_site.type_symbol"]; - const int num_atom = mmcif["_atom_site.id"].size(); - py::list patched_type_symbol(num_atom); - if (type_symbol.empty()) { - const auto& label_comp_id = mmcif["_atom_site.label_comp_id"]; - const auto& label_atom_id = mmcif["_atom_site.label_atom_id"]; - CHECK_EQ(label_comp_id.size(), num_atom); - CHECK_EQ(label_atom_id.size(), num_atom); - for (int i = 0; i < num_atom; i++) { - patched_type_symbol[i] = - atom_id_to_type_symbol(label_comp_id[i], label_atom_id[i]); - } - } else { - for (int i = 0; i < num_atom; i++) { - patched_type_symbol[i] = type_symbol[i]; - } - } - return patched_type_symbol; -} - -absl::flat_hash_map -GetInternalToAuthorChainIdMap(const CifDict& mmcif) { - const auto& label_asym_ids = mmcif["_atom_site.label_asym_id"]; - const auto& auth_asym_ids = mmcif["_atom_site.auth_asym_id"]; - CHECK_EQ(label_asym_ids.size(), auth_asym_ids.size()); - - absl::flat_hash_map mapping; - for (size_t i = 0, num_rows = label_asym_ids.size(); i < num_rows; ++i) { - // Use only the first internal_chain_id occurrence to generate the mapping. - // It should not matter as there should not be a case where a single - // internal chain ID would map to more than one author chain IDs (i.e. the - // mapping should be injective). Since we need this method to be fast, we - // choose not to check it. - mapping.emplace(label_asym_ids[i], auth_asym_ids[i]); - } - return mapping; -} - -} // namespace - -namespace py = pybind11; - -void RegisterModuleMmcifAtomSite(pybind11::module m) { - m.def("get_or_infer_type_symbol", &GetOrInferTypeSymbol, py::arg("mmcif"), - py::arg("atom_id_to_type_symbol")); - - m.def("get_internal_to_author_chain_id_map", &GetInternalToAuthorChainIdMap, - py::arg("mmcif"), py::call_guard()); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site_pybind.h deleted file mode 100644 index 1f2104ecf..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_atom_site_pybind.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ATOM_SITE_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ATOM_SITE_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleMmcifAtomSite(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ATOM_SITE_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout.h b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout.h deleted file mode 100644 index 51c67c528..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout.h +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_H_ - -#include -#include -#include -#include -#include - -#include "absl/status/statusor.h" -#include "absl/strings/string_view.h" -#include "absl/types/span.h" -#include "alphafold3/parsers/cpp/cif_dict_lib.h" - -namespace alphafold3 { - -// Holds the layout of a parsed mmCIF file. -class MmcifLayout { - public: - MmcifLayout(std::vector chain_ends, - std::vector residues, std::size_t model_offset, - std::size_t num_models) - : chain_ends_(std::move(chain_ends)), - residue_ends_(std::move(residues)), - model_offset_(model_offset), - num_models_(num_models) {} - - // Reads a layout from a valid parsed mmCIF. If a valid model_id is provided - // the offsets will select that model from the mmCIF. - // If no model_id is specified, we calculate the layout of the first model - // only. Therefore it is a requirement that each model has identical atom - // layouts. An error is returned if the atom counts do not between models. - static absl::StatusOr Create(const CifDict& mmcif, - absl::string_view model_id = ""); - - std::string ToDebugString() const; - - // Returns the start index and one past the last residue index of a given - // chain. A chain_index of n refers to the n-th chain in the mmCIF. The - // returned residue indices are 0-based enumerations of residues in the - // _atom_site records, and therefore do not include missing residues. - std::pair residue_range( - std::size_t chain_index) const { - if (chain_index > 0) { - return {chain_ends_[chain_index - 1], chain_ends_[chain_index]}; - } else { - return {0, chain_ends_[0]}; - } - } - - // Returns the start index and one past the last index of a given residue. - // A residue_index of n refers to the n-th residue in the mmCIF, not - // including residues that are unresolved (i.e. only using _atom_site). - std::pair atom_range( - std::size_t residue_index) const { - if (residue_index > 0) { - return {residue_ends_[residue_index - 1], residue_ends_[residue_index]}; - } else { - return {model_offset_, residue_ends_[residue_index]}; - } - } - - // If model_id was provided during construction then this is 1, otherwise - // it is the number of models present in the mmCIF. - std::size_t num_models() const { return num_models_; } - // The number of atoms in the chosen model. - std::size_t num_atoms() const { - return residue_ends_.empty() ? 0 : residue_ends_.back() - model_offset_; - } - // The number of chains in the chosen model. - std::size_t num_chains() const { return chain_ends_.size(); } - // The number of residues in the chosen model, not counting unresolved - // residues. - std::size_t num_residues() const { return residue_ends_.size(); } - - // Returns the first atom index that is part of the specified chain. - // The chain is specified using chain_index, which is a 0-based - // enumeration of the chains in the _atom_site table. - std::size_t atom_site_from_chain_index(std::size_t chain_index) const { - if (chain_index == 0) { - return model_offset_; - } - return atom_site_from_residue_index(chain_ends_[chain_index - 1]); - } - - // Returns the first atom index that is part of the specified residue. - // The residue is specified using residue_index, which is a 0-based - // enumeration of the residues in the _atom_site table. - std::size_t atom_site_from_residue_index(std::size_t residues_index) const { - if (residues_index == 0) { - return model_offset_; - } - return residue_ends_[residues_index - 1]; - } - - // One past last residue index of each chain. The residue index does not - // include unresolved residues and is a simple 0-based enumeration of the - // residues in _atom_site table. - const std::vector& chains() const { return chain_ends_; } - - // Indices of the first atom of each chain. Note that this returns atom - // indices (like residue_starts()), not residue indices (like chains()). - std::vector chain_starts() const; - - // One past last atom index of each residue. - const std::vector& residues() const { return residue_ends_; } - - // Indices of the first atom of each residue. - std::vector residue_starts() const { - std::vector residue_starts; - if (!residue_ends_.empty()) { - residue_starts.reserve(residue_ends_.size()); - residue_starts.push_back(model_offset_); - residue_starts.insert(residue_starts.end(), residue_ends_.begin(), - residue_ends_.end() - 1); - } - return residue_starts; - } - - // The first atom index that is part of the specified model. - std::size_t model_offset() const { return model_offset_; } - - void Filter(absl::Span keep_indices); - - private: - std::vector chain_ends_; - std::vector residue_ends_; - std::size_t model_offset_; - std::size_t num_models_; -}; - -} // namespace alphafold3 - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout.pyi b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout.pyi deleted file mode 100644 index add1b05ea..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout.pyi +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -from alphafold3.cpp import cif_dict - -class MmcifLayout: - def atom_range(self, residue_index: int) -> tuple[int, int]: ... - def chain_starts(self) -> list[int]: ... - def chains(self) -> list[int]: ... - def model_offset(self) -> int: ... - def num_atoms(self) -> int: ... - def num_chains(self) -> int: ... - def num_models(self) -> int: ... - def num_residues(self) -> int: ... - def residue_range(self, chain_index: int) -> tuple[int, int]: ... - def residue_starts(self) -> list[int]: ... - def residues(self) -> list[int]: ... - -def from_mmcif(mmcif: cif_dict.CifDict, model_id: str = ...) -> MmcifLayout: ... diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_lib.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_lib.cc deleted file mode 100644 index 91ad70c0b..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_lib.cc +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "absl/algorithm/container.h" -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" -#include "absl/strings/string_view.h" -#include "absl/types/span.h" -#include "alphafold3/parsers/cpp/cif_dict_lib.h" -#include "alphafold3/structure/cpp/mmcif_layout.h" - -namespace alphafold3 { - -std::string MmcifLayout::ToDebugString() const { - return absl::StrFormat( - "MmcifLayout(models=%d, chains=%d, num_residues=%d, atoms=%d)", - num_models(), num_chains(), num_residues(), num_atoms()); -} - -// Changes layout to match keep_indices removing empty chains/residues. -void MmcifLayout::Filter(absl::Span keep_indices) { - if (num_chains() == 0) { - return; - } - // Update residue indices. - auto keep_it = absl::c_lower_bound(keep_indices, residue_ends_.front()); - for (auto& residue : residue_ends_) { - while (keep_it != keep_indices.end() && *keep_it < residue) { - ++keep_it; - } - residue = std::distance(keep_indices.begin(), keep_it); - } - // Unique residue_ends_ with updating chains. - auto first = residue_ends_.begin(); - auto tail = first; - std::size_t num_skipped = 0; - std::size_t current = 0; - for (std::size_t& chain_end : chain_ends_) { - for (auto e = residue_ends_.begin() + chain_end; first != e; ++first) { - std::size_t next = *first; - *tail = next; - if (current != next) { - current = next; - ++tail; - } else { - ++num_skipped; - } - } - chain_end -= num_skipped; - } - residue_ends_.erase(tail, residue_ends_.end()); - - current = 0; - chain_ends_.erase(std::remove_if(chain_ends_.begin(), chain_ends_.end(), - [¤t](std::size_t next) { - bool result = current == next; - current = next; - return result; - }), - chain_ends_.end()); - model_offset_ = 0; -} - -absl::StatusOr MmcifLayout::Create(const CifDict& mmcif, - absl::string_view model_id) { - auto model_ids = mmcif["_atom_site.pdbx_PDB_model_num"]; - auto chain_ids = mmcif["_atom_site.label_asym_id"]; // chain ID. - auto label_seq_ids = mmcif["_atom_site.label_seq_id"]; // residue ID. - auto auth_seq_ids = mmcif["_atom_site.auth_seq_id"]; // author residue ID. - auto insertion_codes = mmcif["_atom_site.pdbx_PDB_ins_code"]; - - if (model_ids.size() != chain_ids.size() || - model_ids.size() != label_seq_ids.size() || - (model_ids.size() != auth_seq_ids.size() && !auth_seq_ids.empty()) || - (model_ids.size() != insertion_codes.size() && - !insertion_codes.empty())) { - return absl::InvalidArgumentError(absl::StrCat( - "Invalid _atom_site table.", // - " len(_atom_site.pdbx_PDB_model_num): ", model_ids.size(), - " len(_atom_site.label_asym_id): ", chain_ids.size(), - " len(_atom_site.label_seq_id): ", label_seq_ids.size(), - " len(_atom_site.auth_seq_id): ", auth_seq_ids.size(), - " len(_atom_site.pdbx_PDB_ins_code): ", insertion_codes.size())); - } - std::size_t num_atoms = model_ids.size(); - if (num_atoms == 0) { - return MmcifLayout({}, {}, 0, 0); - } - std::size_t model_offset = 0; - std::size_t num_models; - std::size_t num_atoms_per_model; - if (model_id.empty()) { - absl::string_view first_model_id = model_ids.front(); - - // Binary search for where the first model ends. - num_atoms_per_model = std::distance( - model_ids.begin(), - absl::c_upper_bound(model_ids, first_model_id, std::not_equal_to<>{})); - if (num_atoms % num_atoms_per_model != 0) { - return absl::InvalidArgumentError(absl::StrCat( - "Each model must have the same number of atoms: (", num_atoms, " % ", - num_atoms_per_model, " == ", num_atoms % num_atoms_per_model, ").")); - } - num_models = num_atoms / num_atoms_per_model; - // Test boundary conditions for each model hold. - for (std::size_t i = 1; i < num_models; ++i) { - if ((model_ids[i * num_atoms_per_model] != - model_ids[(i + 1) * num_atoms_per_model - 1]) || - (model_ids[i * num_atoms_per_model - 1] == - model_ids[i * num_atoms_per_model])) { - return absl::InvalidArgumentError( - absl::StrCat("Each model must have the same number of atoms: (", - num_atoms, " % ", num_atoms_per_model, - " == ", num_atoms % num_atoms_per_model, ").")); - } - } - } else { - num_models = 1; - model_offset = - std::distance(model_ids.begin(), absl::c_find(model_ids, model_id)); - if (model_offset == model_ids.size()) { - return absl::InvalidArgumentError( - absl::StrCat("Unknown model_id: ", model_id)); - } - model_ids.remove_prefix(model_offset); - chain_ids.remove_prefix(model_offset); - label_seq_ids.remove_prefix(model_offset); - if (!auth_seq_ids.empty()) auth_seq_ids.remove_prefix(model_offset); - if (!insertion_codes.empty()) insertion_codes.remove_prefix(model_offset); - - num_atoms_per_model = std::distance( - model_ids.begin(), std::upper_bound(model_ids.begin(), model_ids.end(), - model_id, std::not_equal_to<>{})); - num_atoms = num_atoms_per_model; - } - std::vector residues; - std::vector chains; - absl::string_view chain_id = chain_ids.front(); - if (!auth_seq_ids.empty() && !insertion_codes.empty()) { - // If author residue IDs are present then these are preferred to - // label residue IDs because they work for multi-residue ligands (which - // are given constant "." label residue IDs). - // NB: Author residue IDs require both the auth_seq_id and the insertion - // code to be unique. - absl::string_view auth_seq_id = auth_seq_ids.front(); - absl::string_view insertion_code = insertion_codes.front(); - for (std::size_t i = 1; i < num_atoms_per_model; ++i) { - if (absl::string_view current_chain_id = chain_ids[i]; - current_chain_id != chain_id) { - residues.push_back(i + model_offset); - chains.push_back(residues.size()); - chain_id = current_chain_id; - auth_seq_id = auth_seq_ids[i]; - insertion_code = insertion_codes[i]; - } else if (absl::string_view current_seq_id = auth_seq_ids[i], - current_insertion_code = insertion_codes[i]; - insertion_code != current_insertion_code || - auth_seq_id != current_seq_id) { - residues.push_back(i + model_offset); - auth_seq_id = current_seq_id; - insertion_code = current_insertion_code; - } - } - } else { - absl::string_view label_seq_id = label_seq_ids.front(); - for (std::size_t i = 1; i < num_atoms_per_model; ++i) { - if (absl::string_view current_chain_id = chain_ids[i]; - current_chain_id != chain_id) { - residues.push_back(i + model_offset); - chains.push_back(residues.size()); - chain_id = current_chain_id; - label_seq_id = label_seq_ids[i]; - } else if (absl::string_view current_seq_id = label_seq_ids[i]; - label_seq_id != current_seq_id) { - residues.push_back(i + model_offset); - label_seq_id = current_seq_id; - } - } - } - residues.push_back(num_atoms_per_model + model_offset); - chains.push_back(residues.size()); - return MmcifLayout(std::move(chains), std::move(residues), model_offset, - num_models); -} - -std::vector MmcifLayout::chain_starts() const { - std::vector chain_starts; - chain_starts.reserve(chain_ends_.size()); - for (std::size_t index = 0; index < chain_ends_.size(); ++index) { - chain_starts.push_back(atom_site_from_chain_index(index)); - } - return chain_starts; -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_pybind.cc deleted file mode 100644 index 8eb69befc..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_pybind.cc +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include "alphafold3/structure/cpp/mmcif_layout.h" -#include "pybind11/pybind11.h" -#include "pybind11/pytypes.h" -#include "pybind11/stl.h" - -namespace alphafold3 { - -namespace py = pybind11; - -void RegisterModuleMmcifLayout(pybind11::module m) { - py::class_(m, "MmcifLayout") - .def("__str__", &MmcifLayout::ToDebugString) - .def("num_models", &MmcifLayout::num_models) - .def("num_chains", &MmcifLayout::num_chains) - .def("num_residues", &MmcifLayout::num_residues) - .def("num_atoms", &MmcifLayout::num_atoms) - .def("residue_range", &MmcifLayout::residue_range, py::arg("chain_index")) - .def("atom_range", &MmcifLayout::atom_range, py::arg("residue_index")) - .def("chains", &MmcifLayout::chains, - py::doc("Returns a list of indices one past the last residue of " - "each chain.")) - .def( - "chain_starts", &MmcifLayout::chain_starts, - py::doc("Returns a list of indices of the first atom of each chain.")) - .def("residues", &MmcifLayout::residues, - py::doc("Returns a list of indices one past the last atom of each " - "residue.")) - .def("residue_starts", &MmcifLayout::residue_starts, - py::doc( - "Returns a list of indices of the first atom of each residue.")) - .def("model_offset", &MmcifLayout::model_offset, - py::doc("Returns the first atom index that is part of the specified " - "model.")); - - m.def("from_mmcif", &MmcifLayout::Create, py::arg("mmcif"), - py::arg("model_id") = ""); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_pybind.h deleted file mode 100644 index c79b2dd50..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_layout_pybind.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleMmcifLayout(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn.h b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn.h deleted file mode 100644 index 821be658d..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_H_ - -#include -#include - -#include "absl/status/statusor.h" -#include "absl/strings/string_view.h" -#include "alphafold3/parsers/cpp/cif_dict_lib.h" - -namespace alphafold3 { - -// Returns a pair of atom indices for each row in the bonds table (aka -// _struct_conn). The indices are simple 0-based indexes into the columns of -// the _atom_site table in the input mmCIF, and do not necessarily correspond -// to the values in _atom_site.id, or any other column. -absl::StatusOr, std::vector>> -GetBondAtomIndices(const CifDict& mmcif, absl::string_view model_id); - -} // namespace alphafold3 - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn.pyi b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn.pyi deleted file mode 100644 index d293e666a..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn.pyi +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -from alphafold3.cpp import cif_dict - -def get_bond_atom_indices(mmcif_dict: cif_dict.CifDict, model_id: str) -> tuple[list[int],list[int]]: ... diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc deleted file mode 100644 index afb930fab..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include -#include -#include -#include -#include -#include - -#include "absl/algorithm/container.h" -#include "absl/container/flat_hash_map.h" -#include "absl/container/flat_hash_set.h" -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/string_view.h" -#include "absl/types/span.h" -#include "alphafold3/parsers/cpp/cif_dict_lib.h" -#include "alphafold3/structure/cpp/mmcif_struct_conn.h" - -namespace alphafold3 { - -namespace { - -struct AtomId { - absl::string_view chain_id; - absl::string_view res_id_1; - absl::string_view res_id_2; - absl::string_view atom_name; - absl::string_view alt_id; - - friend bool operator==(const AtomId&, const AtomId&) = default; - template - friend H AbslHashValue(H h, const AtomId& m) { - return H::combine(std::move(h), m.chain_id, m.res_id_1, m.res_id_2, - m.atom_name, m.alt_id); - } -}; - -using StringArrayRef = absl::Span; -using BondIndexByAtom = absl::flat_hash_map>; -using BondAtomIndices = std::vector; - -// Returns whether each container is the same size. -template -bool AreSameSize(const C& c, const Cs&... cs) { - return ((c.size() == cs.size()) && ...); -} - -struct ColumnSpec { - absl::string_view chain_id_col; - absl::string_view res_id_1_col; - absl::string_view res_id_2_col; - absl::string_view atom_name_col; - std::optional alt_id_col; // Not used by OpenMM. -}; - -class AtomColumns { - public: - static absl::StatusOr Create(const CifDict& mmcif, - const ColumnSpec& column_spec) { - StringArrayRef chain_id = mmcif[column_spec.chain_id_col]; - StringArrayRef res_id_1 = mmcif[column_spec.res_id_1_col]; - StringArrayRef res_id_2 = mmcif[column_spec.res_id_2_col]; - StringArrayRef atom_name = mmcif[column_spec.atom_name_col]; - if (!AreSameSize(chain_id, res_id_1, res_id_2, atom_name)) { - return absl::InvalidArgumentError(absl::StrCat( - "Atom columns are not the same size. ", // - "len(", column_spec.chain_id_col, ")=", chain_id.size(), // - ", len(", column_spec.res_id_1_col, ")=", res_id_1.size(), // - ", len(", column_spec.res_id_2_col, ")=", res_id_2.size(), // - ", len(", column_spec.atom_name_col, ")=", atom_name.size(), // - ".")); - } - if (column_spec.alt_id_col.has_value()) { - StringArrayRef alt_id = mmcif[*column_spec.alt_id_col]; - if (!AreSameSize(alt_id, chain_id)) { - return absl::InvalidArgumentError(absl::StrCat( - "Atom columns are not the same size. ", // - "len(", column_spec.chain_id_col, ")=", chain_id.size(), // - ", len(", *column_spec.alt_id_col, ")=", alt_id.size(), // - ".")); - } - return AtomColumns(chain_id, res_id_1, res_id_2, atom_name, alt_id, - column_spec); - } else { - return AtomColumns(chain_id, res_id_1, res_id_2, atom_name, std::nullopt, - column_spec); - } - } - - inline std::size_t size() const { return size_; } - - absl::string_view GetNormalizedAltId(const std::size_t index) const { - constexpr absl::string_view kFullStop = "."; - if (alt_id_.has_value()) { - absl::string_view alt_id = (*alt_id_)[index]; - return alt_id == "?" ? kFullStop : alt_id; - } else { - return kFullStop; - } - } - - AtomId GetAtom(const std::size_t index) const { - return {.chain_id = chain_id_[index], - .res_id_1 = res_id_1_[index], - .res_id_2 = res_id_2_[index], - .atom_name = atom_name_[index], - .alt_id = GetNormalizedAltId(index)}; - } - - std::string GetAtomString(const std::size_t index) const { - std::string alt_id_col; - if (column_spec_.alt_id_col.has_value()) { - alt_id_col = *column_spec_.alt_id_col; - } else { - alt_id_col = "default label_alt_id"; - } - return absl::StrCat( - column_spec_.chain_id_col, "=", chain_id_[index], ", ", // - column_spec_.res_id_1_col, "=", res_id_1_[index], ", ", // - column_spec_.res_id_2_col, "=", res_id_2_[index], ", ", // - column_spec_.atom_name_col, "=", atom_name_[index], ", ", // - alt_id_col, "=", GetNormalizedAltId(index)); // - } - - private: - AtomColumns(StringArrayRef chain_id, StringArrayRef res_id_1, - StringArrayRef res_id_2, StringArrayRef atom_name, - std::optional alt_id, - const ColumnSpec& column_spec) - : chain_id_(chain_id), - res_id_1_(res_id_1), - res_id_2_(res_id_2), - atom_name_(atom_name), - alt_id_(alt_id), - column_spec_(column_spec), - size_(chain_id.size()) {} - StringArrayRef chain_id_; - StringArrayRef res_id_1_; - StringArrayRef res_id_2_; - StringArrayRef atom_name_; - std::optional alt_id_; - ColumnSpec column_spec_; - std::size_t size_; -}; - -// Adds the atom index to any rows in the bond table involving that atom. -absl::Status FillInBondsForAtom(const BondIndexByAtom& bond_index_by_atom, - const AtomId& atom, - const std::size_t atom_index, - BondAtomIndices& bond_atom_indices) { - if (auto bond_index_it = bond_index_by_atom.find(atom); - bond_index_it != bond_index_by_atom.end()) { - for (std::size_t bond_index : bond_index_it->second) { - if (bond_index < 0 || bond_index >= bond_atom_indices.size()) { - return absl::OutOfRangeError( - absl::StrCat("Bond index out of range: ", bond_index)); - } - bond_atom_indices[bond_index] = atom_index; - } - } - return absl::OkStatus(); -} - -// Checks that the CifDict has all of the columns in the column spec. -bool HasAllColumns(const CifDict& mmcif, const ColumnSpec& columns) { - return mmcif.Contains(columns.chain_id_col) && - mmcif.Contains(columns.res_id_1_col) && - mmcif.Contains(columns.res_id_2_col) && - mmcif.Contains(columns.atom_name_col) && - (!columns.alt_id_col.has_value() || - mmcif.Contains(*columns.alt_id_col)); -} - -// Fully specified ptnr1 atom. -constexpr ColumnSpec kStructConnPtnr1ColumnsFull{ - .chain_id_col = "_struct_conn.ptnr1_label_asym_id", - .res_id_1_col = "_struct_conn.ptnr1_auth_seq_id", - .res_id_2_col = "_struct_conn.pdbx_ptnr1_PDB_ins_code", - .atom_name_col = "_struct_conn.ptnr1_label_atom_id", - .alt_id_col = "_struct_conn.pdbx_ptnr1_label_alt_id", -}; - -// Fully specified ptnr2 atom. -constexpr ColumnSpec kStructConnPtnr2ColumnsFull{ - .chain_id_col = "_struct_conn.ptnr2_label_asym_id", - .res_id_1_col = "_struct_conn.ptnr2_auth_seq_id", - .res_id_2_col = "_struct_conn.pdbx_ptnr2_PDB_ins_code", - .atom_name_col = "_struct_conn.ptnr2_label_atom_id", - .alt_id_col = "_struct_conn.pdbx_ptnr2_label_alt_id", -}; - -// Columns used by OpenMM for ptnr1 atoms. -constexpr ColumnSpec kStructConnPtnr1OpenMM{ - .chain_id_col = "_struct_conn.ptnr1_label_asym_id", - .res_id_1_col = "_struct_conn.ptnr1_label_seq_id", - .res_id_2_col = "_struct_conn.ptnr1_label_comp_id", - .atom_name_col = "_struct_conn.ptnr1_label_atom_id", - .alt_id_col = std::nullopt, -}; - -// Columns used by OpenMM for ptnr2 atoms. -constexpr ColumnSpec kStructConnPtnr2OpenMM{ - .chain_id_col = "_struct_conn.ptnr2_label_asym_id", - .res_id_1_col = "_struct_conn.ptnr2_label_seq_id", - .res_id_2_col = "_struct_conn.ptnr2_label_comp_id", - .atom_name_col = "_struct_conn.ptnr2_label_atom_id", - .alt_id_col = std::nullopt, -}; - -// Fully specified atom sites. -constexpr ColumnSpec kAtomSiteColumnsFull{ - .chain_id_col = "_atom_site.label_asym_id", - .res_id_1_col = "_atom_site.auth_seq_id", - .res_id_2_col = "_atom_site.pdbx_PDB_ins_code", - .atom_name_col = "_atom_site.label_atom_id", - .alt_id_col = "_atom_site.label_alt_id", -}; - -// Atom site columns used to match OpenMM _struct_conn tables. -constexpr ColumnSpec kAtomSiteColumnsOpenMM{ - .chain_id_col = "_atom_site.label_asym_id", - .res_id_1_col = "_atom_site.label_seq_id", - .res_id_2_col = "_atom_site.label_comp_id", - .atom_name_col = "_atom_site.label_atom_id", - .alt_id_col = "_atom_site.label_alt_id", -}; - -} // namespace - -absl::StatusOr> GetBondAtomIndices( - const CifDict& mmcif, absl::string_view model_id) { - ColumnSpec ptnr1_columns, ptnr2_columns, atom_site_columns; - - if (HasAllColumns(mmcif, kStructConnPtnr1ColumnsFull) && - HasAllColumns(mmcif, kStructConnPtnr2ColumnsFull)) { - ptnr1_columns = kStructConnPtnr1ColumnsFull; - ptnr2_columns = kStructConnPtnr2ColumnsFull; - atom_site_columns = kAtomSiteColumnsFull; - } else { - ptnr1_columns = kStructConnPtnr1OpenMM; - ptnr2_columns = kStructConnPtnr2OpenMM; - atom_site_columns = kAtomSiteColumnsOpenMM; - } - - absl::StatusOr ptnr1_atoms = - AtomColumns::Create(mmcif, ptnr1_columns); - if (!ptnr1_atoms.ok()) { - return ptnr1_atoms.status(); - } - absl::StatusOr ptnr2_atoms = - AtomColumns::Create(mmcif, ptnr2_columns); - if (!ptnr2_atoms.ok()) { - return ptnr2_atoms.status(); - } - StringArrayRef struct_conn_id = mmcif["_struct_conn.id"]; - if (!AreSameSize(struct_conn_id, *ptnr1_atoms, *ptnr2_atoms)) { - return absl::InvalidArgumentError(absl::StrCat( - "Invalid '_struct_conn.' loop. ", // - "len(id) = ", struct_conn_id.size(), ", ", // - "len(ptnr1_atoms) = ", ptnr1_atoms->size(), ", ", // - "len(ptnr2_atoms) = ", ptnr2_atoms->size(), "." // - )); - } - - absl::StatusOr atoms = - AtomColumns::Create(mmcif, atom_site_columns); - if (!atoms.ok()) { - return atoms.status(); - } - StringArrayRef atom_site_id = mmcif["_atom_site.id"]; - StringArrayRef atom_site_model_id = mmcif["_atom_site.pdbx_PDB_model_num"]; - if (!AreSameSize(atom_site_id, atom_site_model_id, *atoms)) { - return absl::InvalidArgumentError(absl::StrCat( - "Invalid '_atom_site.' loop. ", // - "len(id)= ", atom_site_id.size(), ", ", // - "len(pdbx_PDB_model_num)= ", atom_site_model_id.size(), ", ", // - "len(atoms)= ", atoms->size(), ".")); // - } - - // Build maps from atom ID tuples to the rows in _struct_conn where that - // atom appears (NB could be multiple). - const std::size_t struct_conn_size = struct_conn_id.size(); - BondIndexByAtom ptnr1_rows_by_atom(struct_conn_size); - BondIndexByAtom ptnr2_rows_by_atom(struct_conn_size); - for (std::size_t i = 0; i < struct_conn_size; ++i) { - ptnr1_rows_by_atom[ptnr1_atoms->GetAtom(i)].push_back(i); - ptnr2_rows_by_atom[ptnr2_atoms->GetAtom(i)].push_back(i); - } - - // Allocate two output arrays with one element per row in struct_conn, where - // each element will be the index of that atom in the atom_site table. - // Fill the arrays with atom_site_size, which is an invalid value, so that - // we can check at the end that each atom has been found. - const std::size_t atom_site_size = atom_site_id.size(); - BondAtomIndices ptnr1_atom_indices(struct_conn_size, atom_site_size); - BondAtomIndices ptnr2_atom_indices(struct_conn_size, atom_site_size); - - bool model_id_ecountered = false; - absl::flat_hash_set seen_alt_ids; - for (std::size_t atom_i = 0; atom_i < atom_site_size; ++atom_i) { - if (atom_site_model_id[atom_i] != model_id) { - if (!model_id_ecountered) { - continue; - } else { - // Models are contiguous so once we see a different model ID after - // encountering our model ID then we can exit early. - break; - } - } else { - model_id_ecountered = true; - } - AtomId atom = atoms->GetAtom(atom_i); - seen_alt_ids.insert(atom.alt_id); - - if (auto fill_in_bonds_status1 = FillInBondsForAtom( - ptnr1_rows_by_atom, atom, atom_i, ptnr1_atom_indices); - !fill_in_bonds_status1.ok()) { - return fill_in_bonds_status1; - } - if (auto fill_in_bonds_status2 = FillInBondsForAtom( - ptnr2_rows_by_atom, atom, atom_i, ptnr2_atom_indices); - !fill_in_bonds_status2.ok()) { - return fill_in_bonds_status2; - } - } - // The seen_alt_ids check is a workaround for a known PDB issue: some mmCIFs - // (2evw, 2g0v, 2g0x, 2g0z, 2g10, 2g11, 2g12, 2g14, 2grz, 2ntw as of 2024) - // have multiple models and they set different whole-chain altloc in each - // model. The bond table however doesn't distinguish between models, so there - // are bonds that are valid only for some models. E.g. 2grz has model 1 with - // chain A with altloc A, and model 2 with chain A with altloc B. The bonds - // table lists a bond for each of these. - - // Check that a ptnr1 atom was found for every bond. - if (auto row_it = absl::c_find(ptnr1_atom_indices, atom_site_size); - row_it != ptnr1_atom_indices.end()) { - if (seen_alt_ids.size() > 1 || seen_alt_ids.contains(".") || - seen_alt_ids.contains("?")) { - std::size_t i = std::distance(ptnr1_atom_indices.begin(), row_it); - return absl::InvalidArgumentError( - absl::StrCat("Error parsing \"", mmcif.GetDataName(), "\". ", - "Cannot find atom for bond ID ", struct_conn_id[i], ": ", - ptnr1_atoms->GetAtomString(i))); - } - } - - // Check that a ptnr2 atom was found for every bond. - if (auto row_it = absl::c_find(ptnr2_atom_indices, atom_site_size); - row_it != ptnr2_atom_indices.end()) { - if (seen_alt_ids.size() > 1 || seen_alt_ids.contains(".") || - seen_alt_ids.contains("?")) { - std::size_t i = std::distance(ptnr2_atom_indices.begin(), row_it); - return absl::InvalidArgumentError( - absl::StrCat("Error parsing \"", mmcif.GetDataName(), "\". ", - "Cannot find atom for bond ID ", struct_conn_id[i], ": ", - ptnr2_atoms->GetAtomString(i))); - } - } - - if (!model_id_ecountered) { - return absl::InvalidArgumentError(absl::StrCat( - "Error parsing \"", mmcif.GetDataName(), "\". model_id \"", model_id, - "\" not found in _atom_site.pdbx_PDB_model_num.")); - } - - return std::make_pair(std::move(ptnr1_atom_indices), - std::move(ptnr2_atom_indices)); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_pybind.cc deleted file mode 100644 index 111715ab5..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_pybind.cc +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include - -#include "absl/strings/string_view.h" -#include "alphafold3/parsers/cpp/cif_dict_lib.h" -#include "alphafold3/structure/cpp/mmcif_struct_conn.h" -#include "pybind11/gil.h" -#include "pybind11/pybind11.h" -#include "pybind11/pytypes.h" -#include "pybind11/stl.h" - -namespace alphafold3 { - -namespace py = pybind11; - -constexpr char kGetBondAtomIndices[] = R"( -Extracts the indices of the atoms that participate in bonds. - -This function has a workaround for a known PDB issue: some mmCIFs have -(2evw, 2g0v, 2g0x, 2g0z, 2g10, 2g11, 2g12, 2g14, 2grz, 2ntw as of 2024) -multiple models and they set different whole-chain altloc in each model. -The bond table however doesn't distinguish between models, so there are -bonds that are valid only for some models. E.g. 2grz has model 1 with -chain A with altloc A, and model 2 with chain A with altloc B. The bonds -table lists a bond for each of these. This case is rather rare (10 cases -in PDB as of 2024). For the offending bonds, the returned atom index is -set to the size of the atom_site table, i.e. it is an invalid index. - -Args: - mmcif: The mmCIF object to process. - model_id: The ID of the model that the returned atoms will belong to. This - should be a value in the mmCIF's _atom_site.pdbx_PDB_model_num column. - -Returns: - Two lists of atom indices, `from_atoms` and `to_atoms`, each one having - length num_bonds (as defined by _struct_conn, the bonds table). The bond - i, defined by the i'th row in _struct_conn, is a bond from atom at index - from_atoms[i], to the atom at index to_atoms[i]. The indices are simple - 0-based indexes into the columns of the _atom_site table in the input - mmCIF, and do not necessarily correspond to the values in _atom_site.id, - or any other column. -)"; - -void RegisterModuleMmcifStructConn(pybind11::module m) { - m.def( - "get_bond_atom_indices", - [](const CifDict& mmcif, absl::string_view model_id) { - auto result = GetBondAtomIndices(mmcif, model_id); - if (result.ok()) { - return *result; - } - throw py::value_error(std::string(result.status().message())); - }, - py::arg("mmcif_dict"), py::arg("model_id"), - py::doc(kGetBondAtomIndices + 1), - py::call_guard()); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_pybind.h deleted file mode 100644 index acdbf7b77..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_struct_conn_pybind.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleMmcifStructConn(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils.pyi b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils.pyi deleted file mode 100644 index aa2dc23e9..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils.pyi +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -from collections.abc import Sequence - -import numpy as np - -from alphafold3.cpp import cif_dict -from alphafold3.structure.python import mmcif_layout - - -def filter( - mmcif: cif_dict.CifDict, - include_nucleotides: bool, - include_ligands: bool = ..., - include_water: bool = ..., - include_other: bool = ..., - model_id: str = ..., -) -> tuple[np.ndarray[int], mmcif_layout.MmcifLayout]: ... - - -def fix_residues( - layout: mmcif_layout.MmcifLayout, - comp_id: Sequence[str], - atom_id: Sequence[str], - atom_x: Sequence[float], - atom_y: Sequence[float], - atom_z: Sequence[float], - fix_arg: bool = ..., -) -> None: ... - - -def read_layout( - mmcif: cif_dict.CifDict, model_id: str = ... -) -> mmcif_layout.MmcifLayout: ... - - -def selected_ligand_residue_mask( - layout: mmcif_layout.MmcifLayout, - atom_site_label_asym_ids: list[str], - atom_site_label_seq_ids: list[str], - atom_site_auth_seq_ids: list[str], - atom_site_label_comp_ids: list[str], - atom_site_pdbx_pdb_ins_codes: list[str], - nonpoly_asym_ids: list[str], - nonpoly_auth_seq_ids: list[str], - nonpoly_pdb_ins_codes: list[str], - nonpoly_mon_ids: list[str], - branch_asym_ids: list[str], - branch_auth_seq_ids: list[str], - branch_pdb_ins_codes: list[str], - branch_mon_ids: list[str], -) -> tuple[list[bool], list[bool]]: ... - - -def selected_polymer_residue_mask( - layout: mmcif_layout.MmcifLayout, - atom_site_label_asym_ids: list[str], - atom_site_label_seq_ids: list[str], - atom_site_label_comp_ids: list[str], - poly_seq_asym_ids: list[str], - poly_seq_seq_ids: list[str], - poly_seq_mon_ids: list[str], -) -> list[bool]: ... diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.cc deleted file mode 100644 index 52bd039b2..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.cc +++ /dev/null @@ -1,787 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "numpy/ndarrayobject.h" -#include "numpy/ndarraytypes.h" -#include "numpy/npy_common.h" -#include "absl/container/flat_hash_map.h" -#include "absl/container/flat_hash_set.h" -#include "absl/memory/memory.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/string_view.h" -#include "absl/types/span.h" -#include "alphafold3/parsers/cpp/cif_dict_lib.h" -#include "alphafold3/structure/cpp/mmcif_altlocs.h" -#include "alphafold3/structure/cpp/mmcif_layout.h" -#include "pybind11/cast.h" -#include "pybind11/gil.h" -#include "pybind11/numpy.h" -#include "pybind11/pybind11.h" -#include "pybind11/pytypes.h" -#include "pybind11/stl.h" -#include "pybind11_abseil/absl_casters.h" - -namespace alphafold3 { -namespace { -namespace py = pybind11; - -struct PyObjectDeleter { - inline void operator()(PyObject* obj) const { Py_CLEAR(obj); } -}; - -using ScopedPyObject = std::unique_ptr; - -using StringArrayRef = absl::Span; -using Indexer = absl::flat_hash_map; - -// Returns the reverse look-up map of name to index. -Indexer MakeIndex(StringArrayRef col) { - Indexer index; - index.reserve(col.size()); - for (std::size_t i = 0; i < col.size(); ++i) { - index[col[i]] = i; - } - return index; -} - -// Returns whether each container is the same size. -template -bool AreSameSize(C c, const Cs&... cs) { - return ((c.size() == cs.size()) && ...); -} - -// Stores references to columns in `_atom_site` ensuring they all exist and -// are the same size. -struct AtomSiteLoop { - explicit AtomSiteLoop(const CifDict& cif_dict) - : id(cif_dict["_atom_site.id"]), - model_id(cif_dict["_atom_site.pdbx_PDB_model_num"]), - chain_id(cif_dict["_atom_site.label_asym_id"]), - seq_id(cif_dict["_atom_site.label_seq_id"]), - - comp_id(cif_dict["_atom_site.label_comp_id"]), - atom_id(cif_dict["_atom_site.label_atom_id"]), - - alt_id(cif_dict["_atom_site.label_alt_id"]), - occupancy(cif_dict["_atom_site.occupancy"]) - - { - if (!AreSameSize(id, model_id, chain_id, seq_id, comp_id, atom_id, alt_id, - occupancy)) { - throw py::value_error( - absl::StrCat("Invalid '_atom_site.' loop. ", // - "len(id)=", id.size(), ", ", // - "len(pdbx_PDB_model_num)=", model_id.size(), ", ", // - "len(label_asym_id)=", chain_id.size(), ", ", // - "len(label_seq_id)=", seq_id.size(), ", ", // - "len(label_comp_id)=", comp_id.size(), ", ", // - "len(atom_id)=", atom_id.size(), ", ", // - "len(label_alt_id)=", alt_id.size(), ", ", // - "len(occupancy)=", occupancy.size())); - } - } - StringArrayRef id; - StringArrayRef model_id; - StringArrayRef chain_id; - StringArrayRef seq_id; - StringArrayRef comp_id; - StringArrayRef atom_id; - StringArrayRef alt_id; - StringArrayRef occupancy; -}; - -// Stores references to columns in `_entity` ensuring they all exist and are the -// same size. -struct EntityLoop { - explicit EntityLoop(const CifDict& cif_dict) - : id(cif_dict["_entity.id"]), type(cif_dict["_entity.type"]) { - if (!AreSameSize(id, type)) { - throw py::value_error(absl::StrCat("Invalid '_entity.' loop. ", // - "len(id)=", id.size(), ", ", // - "len(type)=", type.size())); - } - } - StringArrayRef id; - StringArrayRef type; -}; - -// Stores references to columns in `_entity_poly` ensuring they all exist and -// are the same size. -struct EntityPolyLoop { - explicit EntityPolyLoop(const CifDict& cif_dict) - : entity_id(cif_dict["_entity_poly.entity_id"]), - type(cif_dict["_entity_poly.type"]) { - if (!AreSameSize(entity_id, type)) { - throw py::value_error(absl::StrCat("Invalid '_entity_poly.' loop. ", // - "len(entity_id)=", entity_id.size(), - ", ", // - "len(type)=", type.size())); - } - } - StringArrayRef entity_id; - StringArrayRef type; -}; - -// Returns a set of entity names removing ones not included by the flags -// specified. -absl::flat_hash_set SelectChains(const CifDict& mmcif, - bool include_nucleotides, - bool include_ligands, - bool include_water, - bool include_other) { - EntityLoop entity_loop(mmcif); - EntityPolyLoop entity_poly(mmcif); - absl::flat_hash_set permitted_polymers{"polypeptide(L)"}; - absl::flat_hash_set forbidden_polymers; - for (absl::string_view type : - {"polydeoxyribonucleotide", "polyribonucleotide", - "polydeoxyribonucleotide/polyribonucleotide hybrid"}) { - if (include_nucleotides) { - permitted_polymers.emplace(type); - } else { - forbidden_polymers.emplace(type); - } - } - - absl::flat_hash_set permitted_nonpoly_entity_types; - absl::flat_hash_set forbidden_nonpoly_entity_types; - for (absl::string_view type : {"non-polymer", "branched"}) { - if (include_ligands) { - permitted_nonpoly_entity_types.emplace(type); - } else { - forbidden_nonpoly_entity_types.emplace(type); - } - } - absl::string_view water_type = "water"; - if (include_water) { - permitted_nonpoly_entity_types.emplace(water_type); - } else { - forbidden_nonpoly_entity_types.emplace(water_type); - } - - StringArrayRef chain_ids = mmcif["_struct_asym.id"]; - StringArrayRef entity_ids = mmcif["_struct_asym.entity_id"]; - Indexer chain_index = MakeIndex(chain_ids); - Indexer entity_poly_index = MakeIndex(entity_poly.entity_id); - Indexer entity_id_to_index = MakeIndex(entity_loop.id); - - absl::flat_hash_set keep_chain_id; - for (std::size_t i = 0; i < chain_ids.size(); ++i) { - absl::string_view chain_id = chain_ids[i]; - absl::string_view entity_id = entity_ids[i]; - if (entity_id_to_index.empty() || - entity_loop.type[entity_id_to_index[entity_id]] == "polymer") { - if (auto it = entity_poly_index.find(entity_id); - it != entity_poly_index.end()) { - absl::string_view poly_type = entity_poly.type[it->second]; - if (include_other) { - if (!forbidden_polymers.contains(poly_type)) { - keep_chain_id.insert(chain_id); - } - } else { - if (permitted_polymers.contains(poly_type)) { - keep_chain_id.insert(chain_id); - } - } - } - } else { - absl::string_view entity_type = - entity_loop.type[entity_id_to_index[entity_id]]; - if (include_other) { - if (!forbidden_nonpoly_entity_types.contains(entity_type)) { - keep_chain_id.insert(chain_id); - continue; - } - } else { - if (permitted_nonpoly_entity_types.contains(entity_type)) { - keep_chain_id.insert(chain_id); - continue; - } - } - } - } - return keep_chain_id; -} - -class ProcessResidue { - public: - explicit ProcessResidue(const char* residue) - : residue_(PyUnicode_InternFromString(residue)) {} - bool IsResidue(PyObject* residue) { - return ArePyObjectsEqual(residue_.get(), residue); - } - - static bool ArePyObjectsEqual(PyObject* lhs, PyObject* rhs) { - switch (PyObject_RichCompareBool(lhs, rhs, Py_EQ)) { - case -1: - PyErr_Clear(); - return false; - case 0: - return false; - default: - return true; - } - } - - private: - ScopedPyObject residue_; -}; - -struct Position3 { - float x; - float y; - float z; -}; - -float DistanceSquared(Position3 v1, Position3 v2) { - float dx = v1.x - v2.x; - float dy = v1.y - v2.y; - float dz = v1.z - v2.z; - return dx * dx + dy * dy + dz * dz; -} - -class FixArginine : public ProcessResidue { - public: - FixArginine() - : ProcessResidue("ARG"), - cd_(PyUnicode_InternFromString("CD")), - nh1_(PyUnicode_InternFromString("NH1")), - nh2_(PyUnicode_InternFromString("NH2")), - hh11_(PyUnicode_InternFromString("HH11")), - hh21_(PyUnicode_InternFromString("HH21")), - hh12_(PyUnicode_InternFromString("HH12")), - hh22_(PyUnicode_InternFromString("HH22")) {} - void Fix(absl::Span atom_ids, absl::Span atom_x, - absl::Span atom_y, absl::Span atom_z) { - std::ptrdiff_t cd_index = -1; - std::ptrdiff_t nh1_index = -1; - std::ptrdiff_t nh2_index = -1; - std::ptrdiff_t hh11_index = -1; - std::ptrdiff_t hh21_index = -1; - std::ptrdiff_t hh12_index = -1; - std::ptrdiff_t hh22_index = -1; - for (std::ptrdiff_t index = 0; index < atom_ids.size(); ++index) { - PyObject* atom_id = atom_ids[index]; - if (cd_index == -1 && ArePyObjectsEqual(atom_id, cd_.get())) { - cd_index = index; - } else if (nh1_index == -1 && ArePyObjectsEqual(atom_id, nh1_.get())) { - nh1_index = index; - } else if (nh2_index == -1 && ArePyObjectsEqual(atom_id, nh2_.get())) { - nh2_index = index; - } else if (hh11_index == -1 && ArePyObjectsEqual(atom_id, hh11_.get())) { - hh11_index = index; - } else if (hh21_index == -1 && ArePyObjectsEqual(atom_id, hh21_.get())) { - hh21_index = index; - } else if (hh12_index == -1 && ArePyObjectsEqual(atom_id, hh12_.get())) { - hh12_index = index; - } else if (hh22_index == -1 && ArePyObjectsEqual(atom_id, hh22_.get())) { - hh22_index = index; - } - } - if (cd_index < 0 || nh1_index < 0 || nh2_index < 0) { - return; - } - Position3 cd_pos(atom_x[cd_index], atom_y[cd_index], atom_z[cd_index]); - Position3 nh1_pos(atom_x[nh1_index], atom_y[nh1_index], atom_z[nh1_index]); - Position3 nh2_pos(atom_x[nh2_index], atom_y[nh2_index], atom_z[nh2_index]); - if (DistanceSquared(nh1_pos, cd_pos) <= DistanceSquared(nh2_pos, cd_pos)) { - return; - } - std::swap(atom_ids[nh1_index], atom_ids[nh2_index]); - if (hh11_index >= 0 && hh21_index >= 0) { - std::swap(atom_ids[hh11_index], atom_ids[hh21_index]); - } else if (hh11_index >= 0) { - Py_DECREF(atom_ids[hh11_index]); - Py_INCREF(hh21_.get()); - atom_ids[hh11_index] = hh21_.get(); - } else if (hh21_index >= 0) { - Py_DECREF(atom_ids[hh21_index]); - Py_INCREF(hh11_.get()); - atom_ids[hh21_index] = hh11_.get(); - } - if (hh12_index >= 0 && hh22_index >= 0) { - std::swap(atom_ids[hh12_index], atom_ids[hh22_index]); - } else if (hh12_index >= 0) { - Py_DECREF(atom_ids[hh12_index]); - Py_INCREF(hh22_.get()); - atom_ids[hh12_index] = hh22_.get(); - } else if (hh22_index >= 0) { - Py_DECREF(atom_ids[hh22_index]); - Py_INCREF(hh21_.get()); - atom_ids[hh22_index] = hh21_.get(); - } - } - - private: - ScopedPyObject cd_; - ScopedPyObject nh1_; - ScopedPyObject nh2_; - ScopedPyObject hh11_; - ScopedPyObject hh21_; - ScopedPyObject hh12_; - ScopedPyObject hh22_; -}; - -// Returns the layout of the mmCIF `_atom_site` table. -inline MmcifLayout ReadMmcifLayout(const CifDict& mmcif, - absl::string_view model_id = "") { - py::gil_scoped_release release; - auto mmcif_layout = MmcifLayout::Create(mmcif, model_id); - if (mmcif_layout.ok()) { - return *mmcif_layout; - } - - throw py::value_error(std::string(mmcif_layout.status().message())); -} - -std::pair MmcifFilter( // - const CifDict& mmcif, // - bool include_nucleotides, // - bool include_ligands, // - bool include_water, // - bool include_other, // - absl::string_view model_id) { - if (_import_array() < 0) { - throw py::import_error("Failed to import NumPy."); - } - auto layout = ReadMmcifLayout(mmcif, model_id); - std::unique_ptr> keep_indices; - size_t new_num_atoms; - - { - py::gil_scoped_release release; - - AtomSiteLoop atom_site(mmcif); - - auto keep_chain_ids = - SelectChains(mmcif, include_nucleotides, include_ligands, include_water, - include_other); - - std::vector chain_indices; - chain_indices.reserve(keep_chain_ids.size()); - for (std::size_t i = 0; i < layout.num_chains(); ++i) { - if (keep_chain_ids.contains( - atom_site.chain_id[layout.atom_site_from_chain_index(i)])) { - chain_indices.push_back(i); - } - } - - keep_indices = - absl::WrapUnique(new std::vector(ResolveMmcifAltLocs( - layout, atom_site.comp_id, atom_site.atom_id, atom_site.alt_id, - atom_site.occupancy, chain_indices))); - new_num_atoms = keep_indices->size(); - - if (layout.num_models() > 1) { - keep_indices->reserve(layout.num_models() * new_num_atoms); - std::uint64_t* start = &(*keep_indices->begin()); - std::size_t num_atom = keep_indices->size(); - // Copy first model indices into all model indices offsetting each copy. - for (std::size_t i = 1; i < layout.num_models(); ++i) { - std::size_t offset = i * layout.num_atoms(); - std::transform(start, start + num_atom, - std::back_inserter(*keep_indices), - [offset](std::size_t v) { return v + offset; }); - } - } - } - - layout.Filter(*keep_indices); - - npy_intp shape[] = {static_cast(layout.num_models()), - static_cast(new_num_atoms)}; - PyObject* arr = - PyArray_SimpleNewFromData(2, shape, NPY_INT64, keep_indices->data()); - // Create a capsule to hold the memory of the buffer so NumPy knows how to - // delete it when done with it. - PyObject* capsule = PyCapsule_New( - keep_indices.release(), nullptr, +[](PyObject* capsule_cleanup) { - void* memory = PyCapsule_GetPointer(capsule_cleanup, nullptr); - delete static_cast*>(memory); - }); - PyArray_SetBaseObject(reinterpret_cast(arr), capsule); - - return std::make_pair(py::reinterpret_steal(arr), - std::move(layout)); -} - -void MmcifFixResidues( // - const MmcifLayout& layout, // - absl::Span comp_id, // - absl::Span atom_id, // - absl::Span atom_x, // - absl::Span atom_y, // - absl::Span atom_z, // - bool fix_arginine // -) { - std::optional arginine; - std::size_t num_atoms = layout.num_atoms(); - if (comp_id.size() != num_atoms || atom_id.size() != num_atoms || - atom_x.size() != num_atoms || atom_y.size() != num_atoms || - atom_z.size() != num_atoms) { - throw py::value_error( - absl::StrCat("Sizes must match. ", // - "num_atoms=", num_atoms, ", ", // - "len(comp_id)=", comp_id.size(), ", ", // - "len(atom_id)=", atom_id.size(), ", ", // - "len(atom_x)=", atom_x.size(), ", ", // - "len(atom_y)=", atom_y.size(), ", ", // - "len(atom_z)=", atom_z.size())); - } - - if (fix_arginine) { - arginine.emplace(); - } - if (!arginine.has_value()) { - return; - } - - for (std::size_t res_index = 0; res_index < layout.num_residues(); - ++res_index) { - auto [atom_start, atom_end] = layout.atom_range(res_index); - std::size_t atom_count = atom_end - atom_start; - PyObject* resname = comp_id[atom_start]; - if (arginine.has_value() && arginine->IsResidue(resname)) { - arginine->Fix(atom_id.subspan(atom_start, atom_count), - atom_x.subspan(atom_start, atom_count), - atom_y.subspan(atom_start, atom_count), - atom_z.subspan(atom_start, atom_count)); - } - } -} - -std::vector SelectedPolymerResidueMask( - const MmcifLayout& layout, - const std::vector& atom_site_label_asym_ids, // - const std::vector& atom_site_label_seq_ids, // - const std::vector& atom_site_label_comp_ids, // - const std::vector& poly_seq_asym_ids, // - const std::vector& poly_seq_seq_ids, // - const std::vector& poly_seq_mon_ids // -) { - absl::flat_hash_map, - absl::string_view> - selected; - selected.reserve(layout.num_residues()); - // layout.residues() is O(1) while layout.residue_starts() is O(num_res). - const std::vector& residue_starts = layout.residue_starts(); - for (int i = 0; i < layout.residues().size(); ++i) { - std::size_t res_start = residue_starts[i]; - std::size_t res_end = layout.residues()[i]; - if (res_start == res_end) { - continue; // Skip empty residues (containing no atoms). - } - - absl::string_view label_seq_id = atom_site_label_seq_ids[i]; - if (label_seq_id == ".") { - continue; // Skip non-polymers. - } - - absl::string_view label_asym_id = atom_site_label_asym_ids[i]; - absl::string_view label_comp_id = atom_site_label_comp_ids[i]; - selected[std::make_pair(label_asym_id, label_seq_id)] = label_comp_id; - } - - std::vector mask; - mask.reserve(poly_seq_mon_ids.size()); - for (int i = 0; i < poly_seq_mon_ids.size(); ++i) { - absl::string_view poly_seq_asym_id = poly_seq_asym_ids[i]; - absl::string_view poly_seq_seq_id = poly_seq_seq_ids[i]; - absl::string_view poly_seq_mon_id = poly_seq_mon_ids[i]; - - auto it = selected.find(std::make_pair(poly_seq_asym_id, poly_seq_seq_id)); - if (it != selected.end()) { - mask.push_back(it->second == poly_seq_mon_id); - } else { - mask.push_back(true); // Missing residues are not heterogeneous. - } - } - return mask; -} - -std::pair, std::vector> SelectedLigandResidueMask( - const MmcifLayout& layout, // - const std::vector& atom_site_label_asym_ids, // - const std::vector& atom_site_label_seq_ids, // - const std::vector& atom_site_auth_seq_ids, // - const std::vector& atom_site_label_comp_ids, // - const std::vector& atom_site_pdbx_pdb_ins_codes, // - const std::vector& nonpoly_asym_ids, // - const std::vector& nonpoly_auth_seq_ids, // - const std::vector& nonpoly_pdb_ins_codes, // - const std::vector& nonpoly_mon_ids, // - const std::vector& branch_asym_ids, // - const std::vector& branch_auth_seq_ids, // - const std::vector& branch_pdb_ins_codes, // - const std::vector& branch_mon_ids) { - absl::flat_hash_map< - std::tuple, - absl::string_view> - selected; - selected.reserve(layout.num_residues()); - // layout.residues() is O(1) while layout.residue_starts() is O(num_res). - const std::vector& residue_starts = layout.residue_starts(); - for (int i = 0; i < layout.residues().size(); ++i) { - std::size_t res_start = residue_starts[i]; - std::size_t res_end = layout.residues()[i]; - if (res_start == res_end) { - continue; // Skip empty residues (containing no atoms). - } - - absl::string_view label_seq_id = atom_site_label_seq_ids[i]; - if (label_seq_id != ".") { - continue; // Skip polymers. - } - - absl::string_view label_asym_id = atom_site_label_asym_ids[i]; - absl::string_view auth_seq_id = atom_site_auth_seq_ids[i]; - absl::string_view ins_code = atom_site_pdbx_pdb_ins_codes[i]; - ins_code = ins_code == "?" ? "." : ins_code; // Remap unknown to unset. - absl::string_view label_comp_id = atom_site_label_comp_ids[i]; - selected[std::make_tuple(label_asym_id, auth_seq_id, ins_code)] = - label_comp_id; - } - - std::vector nonpoly_mask; - nonpoly_mask.reserve(nonpoly_asym_ids.size()); - for (int i = 0; i < nonpoly_asym_ids.size(); ++i) { - absl::string_view nonpoly_asym_id = nonpoly_asym_ids[i]; - absl::string_view nonpoly_auth_seq_id = nonpoly_auth_seq_ids[i]; - absl::string_view nonpoly_ins_code = nonpoly_pdb_ins_codes[i]; - // Remap unknown to unset. - nonpoly_ins_code = nonpoly_ins_code == "?" ? "." : nonpoly_ins_code; - absl::string_view nonpoly_mon_id = nonpoly_mon_ids[i]; - - auto it = selected.find(std::make_tuple( - nonpoly_asym_id, nonpoly_auth_seq_id, nonpoly_ins_code)); - if (it != selected.end()) { - nonpoly_mask.push_back(it->second == nonpoly_mon_id); - } else { - nonpoly_mask.push_back(true); // Missing residues are not heterogeneous. - } - } - - std::vector branch_mask; - branch_mask.reserve(branch_asym_ids.size()); - for (int i = 0; i < branch_asym_ids.size(); ++i) { - absl::string_view branch_asym_id = branch_asym_ids[i]; - absl::string_view branch_auth_seq_id = branch_auth_seq_ids[i]; - - // Insertion codes in _pdbx_branch_scheme are not required and can be - // missing. Default to unset ('.') in such case. - absl::string_view branch_ins_code; - if (i < branch_pdb_ins_codes.size()) { - branch_ins_code = branch_pdb_ins_codes[i]; - // Remap unknown to unset. - branch_ins_code = branch_ins_code == "?" ? "." : branch_ins_code; - } else { - branch_ins_code = "."; - } - - absl::string_view branch_mon_id = branch_mon_ids[i]; - - auto it = selected.find( - std::make_tuple(branch_asym_id, branch_auth_seq_id, branch_ins_code)); - if (it != selected.end()) { - branch_mask.push_back(it->second == branch_mon_id); - } else { - branch_mask.push_back(true); // Missing residues are not heterogeneous. - } - } - - return std::make_pair(nonpoly_mask, branch_mask); -} - -constexpr char kReadMmcifLayout[] = R"( -Returns the layout of the cif_dict. - -Args: - mmcif: mmCIF to calculate the layout for. - model_id: If non-empty the layout of the given model is returned - otherwise the layout of all models are returned. -Raises: - ValueError: if the mmCIF is malformed or the number of atoms in each - model are inconsistent. -)"; - -constexpr char kMmcifFilter[] = R"( -Returns NumpyArray of selected rows in `_atom_site` and new layout. - -Args: - mmcif: mmCIF to filter. - include_nucleotides: Whether to include polymer entities of type: - "polypeptide(L)\", "polydeoxyribonucleotide", "polyribonucleotide". - Otherwise only "polypeptide(L)\". ("polypeptide(D)\" is never included.) - include_ligands: Whether to include non-polymer entities of type: - "non-polymer", "branched". - include_water: Whether to include entities of type water. - include_other: Whether to include other (non-standard) entity types - that are not covered by any of the above parameters. - model_id: If non-empty the model with given name is selected otherwise - all models are selected. - -Returns: - A tuple containing a numpy array with a shape (num_models, num_atoms) - with the atom_site indices selected and the new layout. - -Raises: - ValueError error if mmCIF dict does not have all required fields. -)"; - -constexpr char kMmcifFixResidues[] = R"( -Fixes residue columns in-place. - -Args: - layout: layout from filter command. - comp_id: '_atom_site.label_comp_id' of first model. - group: '_atom_site.group_PDB' of first model. - atom_id: '_atom_site.label_atom_id' of first model. - type_symbol: '_atom_site.type_symbol' of first model. - atom_x: '_atom_site.Cartn_x' of first model. - atom_y: '_atom_site.Cartn_y' of first model. - atom_z: '_atom_site.Cartn_z' of first model. - fix_mse: Whether to convert MSE residues into MET residues. - fix_arg: Whether to ensure the atoms in ARG are in the correct order. - fix_unknown_dna: Whether to convert DNA residues from N to DN. - dna_mask: Which atoms are from DNA chains. - -Raises: - ValueError: If shapes are invalid. -)"; - -constexpr char kSelectedPolymerResidueMask[] = R"( -Returns a _pdbx_poly_seq_scheme mask for selected hetero residues. - -Should be called after filtering the layout using mmcif_utils.filter. - -Args: - layout: Layout defining the _atom_site residue selection. - atom_site_label_asym_ids: Internal (label) chain ID, per selected residue. - atom_site_label_seq_ids: Internal (label) residue ID, per selected residue. - atom_site_label_comp_ids: Residue name, per selected residue. - poly_seq_asym_ids: Internal (label) chain ID, per residue. - poly_seq_seq_ids: Internal (label) residue ID, per residue. - poly_seq_mon_ids: Residue name, per residue. - -Returns: - A mask for the _pdbx_poly_seq_scheme table. If residues are selected - using this mask, they will have consistent heterogeneous residue - selection with the _atom_site table. -)"; - -constexpr char kSelectedLigandResidueMask[] = R"( -Returns masks for selected ligand hetero residues. - -Should be called after filtering the layout using mmcif_utils.filter. - -Args: - layout: Layout defining the _atom_site residue selection. - atom_site_label_asym_ids: Internal (label) chain ID, per selected residue. - atom_site_label_seq_ids: Internal (author) residue ID, per selected residue. - atom_site_auth_seq_ids: External (author) residue ID, per selected residue. - atom_site_label_comp_ids: Residue name, per selected residue. - atom_site_pdbx_pdb_ins_codes: Insertion code, per selected residue. - nonpoly_asym_ids: Internal (label) chain ID, per residue from - _pdbx_nonpoly_scheme. - nonpoly_auth_seq_ids: External (author) residue ID, per residue from - _pdbx_nonpoly_scheme. - nonpoly_pdb_ins_codes: Residue name, per residue from - _pdbx_nonpoly_scheme. - nonpoly_mon_ids: Insertion code, per residue from _pdbx_nonpoly_scheme. - branch_asym_ids: Internal (label) chain ID, per residue from - _pdbx_branch_scheme. - branch_auth_seq_ids: External (author) residue ID, per residue from - _pdbx_branch_scheme. - branch_pdb_ins_codes: Residue name, per residue from _pdbx_branch_scheme. - branch_mon_ids: Insertion code, per residue from _pdbx_branch_scheme. - -Returns: - A tuple with masks for _pdbx_nonpoly_scheme and _pdbx_branch_scheme. If - residues are selected using these masks, they will have consistent - heterogeneous residue selection with the _atom_site table. -)"; - -} // namespace - -void RegisterModuleMmcifUtils(pybind11::module m) { - m.def("read_layout", ReadMmcifLayout, - py::arg("mmcif"), // - py::arg("model_id") = "", // - py::doc(kReadMmcifLayout + 1) // - ); - - m.def("filter", MmcifFilter, // - py::arg("mmcif"), // - py::arg("include_nucleotides"), // - py::arg("include_ligands") = false, // - py::arg("include_water") = false, // - py::arg("include_other") = false, // - py::arg("model_id") = "", // - py::doc(kMmcifFilter + 1) // - ); - - m.def("fix_residues", MmcifFixResidues, - py::arg("layout"), // - py::arg("comp_id"), // - py::arg("atom_id"), // - py::arg("atom_x"), // - py::arg("atom_y"), // - py::arg("atom_z"), // - py::arg("fix_arg") = false, // - py::doc(kMmcifFixResidues + 1) // - ); - - m.def("selected_polymer_residue_mask", SelectedPolymerResidueMask, - py::arg("layout"), // - py::arg("atom_site_label_asym_ids"), // - py::arg("atom_site_label_seq_ids"), // - py::arg("atom_site_label_comp_ids"), // - py::arg("poly_seq_asym_ids"), // - py::arg("poly_seq_seq_ids"), // - py::arg("poly_seq_mon_ids"), // - py::call_guard(), // - py::doc(kSelectedPolymerResidueMask + 1) // - ); - - m.def("selected_ligand_residue_mask", SelectedLigandResidueMask, - py::arg("layout"), // - py::arg("atom_site_label_asym_ids"), // - py::arg("atom_site_label_seq_ids"), // - py::arg("atom_site_auth_seq_ids"), // - py::arg("atom_site_label_comp_ids"), // - py::arg("atom_site_pdbx_pdb_ins_codes"), // - py::arg("nonpoly_asym_ids"), // - py::arg("nonpoly_auth_seq_ids"), // - py::arg("nonpoly_pdb_ins_codes"), // - py::arg("nonpoly_mon_ids"), // - py::arg("branch_asym_ids"), // - py::arg("branch_auth_seq_ids"), // - py::arg("branch_pdb_ins_codes"), // - py::arg("branch_mon_ids"), // - py::call_guard(), // - py::doc(kSelectedLigandResidueMask + 1) // - ); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.h deleted file mode 100644 index 7ba19420b..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/mmcif_utils_pybind.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_UTILS_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_UTILS_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleMmcifUtils(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_UTILS_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array.pyi b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array.pyi deleted file mode 100644 index b4b76c27f..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array.pyi +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -from collections.abc import Sequence -from typing import Any, overload - -import numpy as np - - -def format_float_array( - values: Sequence[float], num_decimal_places: int -) -> list[str]: ... - - -def isin( - array: np.ndarray[object], - test_elements: set[str | bytes], - *, - invert: bool = ..., -) -> np.ndarray[bool]: ... - - -@overload -def remap( - array: np.ndarray[object], - mapping: dict[str, str], - default_value: str, - inplace: bool = ..., -) -> np.ndarray[object]: ... - - -@overload -def remap( - array: np.ndarray[object], - mapping: dict[str, str], - inplace: bool = ..., -) -> np.ndarray[object]: ... - - -def remap_multiple( - arrays: Sequence[np.ndarray[object]], - mapping: dict[tuple[Any], int], -) -> np.ndarray[int]: ... diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.cc b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.cc deleted file mode 100644 index 29fac727a..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.cc +++ /dev/null @@ -1,329 +0,0 @@ -// Copyright 2024 DeepMind Technologies Limited -// -// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// To request access to the AlphaFold 3 model parameters, follow the process set -// out at https://github.com/google-deepmind/alphafold3. You may only use these -// if received directly from Google. Use is subject to terms of use available at -// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "numpy/arrayobject.h" -#include "numpy/ndarrayobject.h" -#include "numpy/ndarraytypes.h" -#include "numpy/npy_common.h" -#include "absl/algorithm/container.h" -#include "absl/container/flat_hash_set.h" -#include "absl/strings/str_format.h" -#include "absl/strings/string_view.h" -#include "absl/types/span.h" -#include "pybind11/cast.h" -#include "pybind11/numpy.h" -#include "pybind11/pybind11.h" -#include "pybind11/pytypes.h" -#include "pybind11_abseil/absl_casters.h" - -namespace { - -namespace py = pybind11; - -PyObject* RemapNumpyArrayObjects(PyObject* array, PyObject* mapping, - bool inplace, PyObject* default_value) { - import_array(); - if (!PyArray_Check(array)) { - PyErr_SetString(PyExc_TypeError, "'array' must be a np.ndarray."); - return nullptr; - } - if (!PyDict_Check(mapping)) { - PyErr_SetString(PyExc_TypeError, "'mapping' must be a Python dict."); - return nullptr; - } - - PyArrayObject* array_obj = reinterpret_cast(array); - if (PyArray_TYPE(array_obj) != NPY_OBJECT) { - PyErr_SetString(PyExc_TypeError, "`array` must be an array of objects."); - return nullptr; - } - - if (inplace) { - // We are returning original array so we need to increase the ref count. - Py_INCREF(array); - } else { - // We are returning a fresh copy. - array = PyArray_NewCopy(array_obj, NPY_CORDER); - if (array == nullptr) { - PyErr_SetString(PyExc_MemoryError, "Out of memory!"); - return nullptr; - } - array_obj = reinterpret_cast(array); - } - - if (PyArray_SIZE(array_obj) == 0) { - return array; - } - - if (default_value == nullptr && PyDict_Size(mapping) == 0) { - return array; - } - - NpyIter* iter = NpyIter_New( - array_obj, NPY_ITER_READWRITE | NPY_ITER_EXTERNAL_LOOP | NPY_ITER_REFS_OK, - NPY_KEEPORDER, NPY_NO_CASTING, nullptr); - if (iter == nullptr) { - PyErr_SetString(PyExc_MemoryError, "Out of memory!"); - Py_XDECREF(array); - return nullptr; - } - - NpyIter_IterNextFunc* iter_next = NpyIter_GetIterNext(iter, nullptr); - if (iter_next == nullptr) { - NpyIter_Deallocate(iter); - Py_XDECREF(array); - PyErr_SetString(PyExc_MemoryError, "Out of memory!"); - return nullptr; - } - - // Iterating arrays taken from: - // https://numpy.org/doc/stable/reference/c-api/iterator.html - char** data_pointer = NpyIter_GetDataPtrArray(iter); - npy_intp* stride_pointer = NpyIter_GetInnerStrideArray(iter); - npy_intp* inner_size_pointer = NpyIter_GetInnerLoopSizePtr(iter); - do { - char* data = *data_pointer; - npy_intp stride = *stride_pointer; - npy_intp count = *inner_size_pointer; - for (size_t i = 0; i < count; ++i) { - PyObject* entry; - std::memcpy(&entry, data, sizeof(PyObject*)); - PyObject* result = PyDict_GetItem(mapping, entry); - if (result != nullptr) { - // Replace entry. - Py_INCREF(result); - Py_XDECREF(entry); - std::memcpy(data, &result, sizeof(PyObject*)); - } else if (default_value != nullptr) { - // Replace entry with a default value. - Py_INCREF(default_value); - Py_XDECREF(entry); - std::memcpy(data, &default_value, sizeof(PyObject*)); - } - data += stride; - } - } while (iter_next(iter)); - - NpyIter_Deallocate(iter); - return array; -} - -// Convert 1D Numpy float array to a list of strings where each string has fixed -// number of decimal points. This is faster than Python list comprehension. -std::vector FormatFloatArray(absl::Span values, - int num_decimal_places) { - std::vector output; - output.reserve(values.size()); - - absl::c_transform(values, std::back_inserter(output), - [num_decimal_places](float value) { - return absl::StrFormat("%.*f", num_decimal_places, value); - }); - return output; -} - -py::array_t IsIn( - const py::array_t& array, - const absl::flat_hash_set& test_elements, bool invert) { - const size_t num_elements = array.size(); - py::array_t output(num_elements); - std::fill(output.mutable_data(), output.mutable_data() + output.size(), - invert); - - // Shortcut: The output will be trivially always false if test_elements empty. - if (test_elements.empty()) { - return output; - } - - for (size_t i = 0; i < num_elements; ++i) { - // Compare the string values instead of comparing just object pointers. - py::handle handle = array.data()[i]; - if (!PyUnicode_Check(handle.ptr()) && !PyBytes_Check(handle.ptr())) { - continue; - } - if (test_elements.contains(py::cast(handle))) { - output.mutable_data()[i] = !invert; - } - } - if (array.ndim() > 1) { - auto shape = - std::vector(array.shape(), array.shape() + array.ndim()); - return output.reshape(shape); - } - return output; -} - -py::array RemapMultipleArrays( - const std::vector>& arrays, - const py::dict& mapping) { - size_t array_size = arrays[0].size(); - for (const auto& array : arrays) { - if (array.size() != array_size) { - throw py::value_error("All arrays must have the same length."); - } - } - - // Create a result buffer. - auto result = py::array_t(array_size); - absl::Span result_buffer(result.mutable_data(), array_size); - PyObject* entry = PyTuple_New(arrays.size()); - if (entry == nullptr) { - throw py::error_already_set(); - } - std::vector> array_spans; - array_spans.reserve(arrays.size()); - for (const auto& array : arrays) { - array_spans.emplace_back(array.data(), array.size()); - } - - // Iterate over arrays and look up elements in the `py_dict`. - bool fail = false; - for (size_t i = 0; i < array_size; ++i) { - for (size_t j = 0; j < array_spans.size(); ++j) { - PyTuple_SET_ITEM(entry, j, array_spans[j][i]); - } - PyObject* result = PyDict_GetItem(mapping.ptr(), entry); - if (result != nullptr) { - int64_t result_value = PyLong_AsLongLong(result); - if (result_value == -1 && PyErr_Occurred()) { - fail = true; - break; - } - if (result_value > std::numeric_limits::max() || - result_value < std::numeric_limits::lowest()) { - PyErr_SetString(PyExc_OverflowError, "Result value too large."); - fail = true; - break; - } - result_buffer[i] = result_value; - } else { - PyErr_Format(PyExc_KeyError, "%R", entry); - fail = true; - break; - } - } - - for (size_t j = 0; j < array_spans.size(); ++j) { - PyTuple_SET_ITEM(entry, j, nullptr); - } - Py_XDECREF(entry); - if (fail) { - throw py::error_already_set(); - } - return result; -} - -constexpr char kRemapNumpyArrayObjects[] = R"( -Replace objects in NumPy array of objects using mapping. - -Args: - array: NumPy array with dtype=object. - mapping: Dict mapping old values to new values. - inplace: Bool (default False) whether to replace values inplace or to - create a new array. - default_value: If given, what value to map to if the mapping is missing - for that particular item. If not given, such items are left unchanged. - -Returns - NumPy array of dtype object with values replaced according to mapping. - If inplace is True the original array is modified inplace otherwise a - new array is returned. -)"; - -constexpr char kFormatFloatArrayDoc[] = R"( -Converts float -> string array with given number of decimal places. -)"; - -constexpr char kIsInDoc[] = R"( -Computes whether each element is in test_elements. - -Same use as np.isin, but much faster. If len(array) = n, len(test_elements) = m: -* This function has complexity O(n). -* np.isin with arrays of objects has complexity O(m*log(m) + n * log(m)). - -Args: - array: Input NumPy array with dtype=object. - test_elements: The values against which to test each value of array. - invert: If True, the values in the returned array are inverted, as if - calculating `element not in test_elements`. Default is False. - `isin(a, b, invert=True)` is equivalent to but faster than `~isin(a, b)`. - -Returns - A boolean array of the same shape as the input array. Each value `val` is: - * `val in test_elements` if `invert=False`, - * `val not in test_elements` if `invert=True`. -)"; - -constexpr char kRemapMultipleDoc[] = R"( -Maps keys from multiple aligned arrays to a single array. - -Args: - arrays: Numpy arrays of the same length. The tuple of aligned entries is used - as key for the mapping. - mapping: Dict mapping from tuples to integer values. - -Returns - NumPy array of dtype `int` with values looked up in mapping according to the - tuple of aligned array entries as keys. -)"; - -} // namespace - -namespace alphafold3 { - -void RegisterModuleStringArray(pybind11::module m) { - m.def( - "remap", - [](py::object array, py::object mapping, bool inplace, - py::object default_value) -> py::object { - PyObject* result = RemapNumpyArrayObjects(array.ptr(), mapping.ptr(), - inplace, default_value.ptr()); - if (result == nullptr) { - throw py::error_already_set(); - } - return py::reinterpret_steal(result); - }, - py::return_value_policy::take_ownership, py::arg("array"), - py::arg("mapping"), py::arg("inplace") = false, py::arg("default_value"), - py::doc(kRemapNumpyArrayObjects + 1)); - m.def( - "remap", - [](py::object array, py::object mapping, bool inplace) -> py::object { - PyObject* result = RemapNumpyArrayObjects(array.ptr(), mapping.ptr(), - inplace, nullptr); - if (result == nullptr) { - throw py::error_already_set(); - } - return py::reinterpret_steal(result); - }, - py::return_value_policy::take_ownership, py::arg("array"), - py::arg("mapping"), py::arg("inplace") = false, - py::doc(kRemapNumpyArrayObjects + 1)); - m.def("format_float_array", &FormatFloatArray, py::arg("values"), - py::arg("num_decimal_places"), py::doc(kFormatFloatArrayDoc + 1), - py::call_guard()); - m.def("isin", &IsIn, py::arg("array"), py::arg("test_elements"), - py::kw_only(), py::arg("invert") = false, py::doc(kIsInDoc + 1)); - m.def("remap_multiple", &RemapMultipleArrays, py::arg("arrays"), - py::arg("mapping"), py::doc(kRemapMultipleDoc + 1)); -} - -} // namespace alphafold3 diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.h b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.h deleted file mode 100644 index 85790ddd8..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/cpp/string_array_pybind.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 DeepMind Technologies Limited - * - * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of - * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ - * - * To request access to the AlphaFold 3 model parameters, follow the process set - * out at https://github.com/google-deepmind/alphafold3. You may only use these - * if received directly from Google. Use is subject to terms of use available at - * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - */ - -#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_STRING_ARRAY_PYBIND_H_ -#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_STRING_ARRAY_PYBIND_H_ - -#include "pybind11/pybind11.h" - -namespace alphafold3 { - -void RegisterModuleStringArray(pybind11::module m); - -} - -#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_STRING_ARRAY_PYBIND_H_ diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/mmcif.py b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/mmcif.py deleted file mode 100644 index 5d373293f..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/mmcif.py +++ /dev/null @@ -1,333 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Low level mmCIF parsing operations and wrappers for nicer C++/Py errors. - -Note that the cif_dict.CifDict class has many useful methods to help with data -extraction which are not shown in this file. You can find them in cif_dict.clif -together with docstrings. The cif_dict.CifDict class behaves like an immutable -Python dictionary (some methods are not implemented though). -""" -from collections.abc import Callable, Mapping, Sequence -import functools -import itertools -import re -from typing import ParamSpec, TypeAlias, TypeVar, Union, Optional - -from alphafold3.constants import chemical_components -from alphafold3.cpp import cif_dict -from alphafold3.cpp import mmcif_atom_site -from alphafold3.cpp import mmcif_struct_conn -from alphafold3.cpp import string_array -import numpy as np - -Mmcif = cif_dict.CifDict - - -_P = ParamSpec('_P') -_T = TypeVar('_T') -_WappedFn: TypeAlias = Callable[_P, _T] - - -@functools.lru_cache(maxsize=256) -def int_id_to_str_id(num: int) -> str: - """Encodes a number as a string, using reverse spreadsheet style naming. - - Args: - num: A positive integer. - - Returns: - A string that encodes the positive integer using reverse spreadsheet style, - naming e.g. 1 = A, 2 = B, ..., 27 = AA, 28 = BA, 29 = CA, ... This is the - usual way to encode chain IDs in mmCIF files. - """ - if num <= 0: - raise ValueError(f'Only positive integers allowed, got {num}.') - - num = num - 1 # 1-based indexing. - output = [] - while num >= 0: - output.append(chr(num % 26 + ord('A'))) - num = num // 26 - 1 - return ''.join(output) - - -@functools.lru_cache(maxsize=256) -def str_id_to_int_id(str_id: str) -> int: - """Encodes an mmCIF-style string chain ID as an integer. - - The integer IDs are one based so this function is the inverse of - int_id_to_str_id. - - Args: - str_id: A string chain ID consisting only of upper case letters A-Z. - - Returns: - An integer that can be used to order mmCIF chain IDs in the standard - (reverse spreadsheet style) ordering. - """ - if not re.match('^[A-Z]+$', str_id): - raise ValueError( - f'String ID must be upper case letters, got {str_id}.') - - offset = ord('A') - 1 - output = 0 - for i, c in enumerate(str_id): - output += (ord(c) - offset) * int(26**i) - return output - - -def from_string(mmcif_string: Union[str, bytes]) -> Mmcif: - return cif_dict.from_string(mmcif_string) - - -def parse_multi_data_cif(cif_string: str) -> dict[str, Mmcif]: - """Parses a CIF string with multiple data records. - - For instance, the CIF string: - - ``` - data_001 - _foo bar - # - data_002 - _foo baz - ``` - - is parsed as: - - ``` - {'001': Mmcif({'_foo': ['bar']}), '002': Mmcif({'_foo': ['baz']})} - ``` - - Args: - cif_string: The multi-data CIF string to be parsed. - - Returns: - A dictionary mapping record names to Mmcif objects with data. - """ - return cif_dict.parse_multi_data_cif(cif_string) - - -def tokenize(mmcif_string: str) -> list[str]: - return cif_dict.tokenize(mmcif_string) - - -def split_line(line: str) -> list[str]: - return cif_dict.split_line(line) - - -class BondParsingError(Exception): - """Exception raised by errors when getting bond atom indices.""" - - -def get_bond_atom_indices( - mmcif: Mmcif, - model_id: str = '1', -) -> tuple[Sequence[int], Sequence[int]]: - """Extracts the indices of the atoms that participate in bonds. - - Args: - mmcif: The mmCIF object to process. - model_id: The ID of the model that the returned atoms will belong to. This - should be a value in the mmCIF's _atom_site.pdbx_PDB_model_num column. - - Returns: - Two lists of atom indices, `from_atoms` and `to_atoms`, each one having - length num_bonds (as defined by _struct_conn, the bonds table). The bond - i, defined by the i'th row in _struct_conn, is a bond from atom at index - from_atoms[i], to the atom at index to_atoms[i]. The indices are simple - 0-based indexes into the columns of the _atom_site table in the input - mmCIF, and do not necessarily correspond to the values in _atom_site.id, - or any other column. - - Raises: - BondParsingError: If any of the required tables or columns are not present - in - the mmCIF, or if the _struct_conn table refers to atoms that cannot - be found in the _atom_site table. - """ - try: - return mmcif_struct_conn.get_bond_atom_indices(mmcif, model_id) - except ValueError as e: - raise BondParsingError(str(e)) from e - - -def get_or_infer_type_symbol( - mmcif: Mmcif, ccd: Optional[chemical_components.Ccd] = None -) -> Sequence[str]: - """Returns the type symbol (element) for all of the atoms. - - Args: - mmcif: A parsed mmCIF file in the Mmcif format. - ccd: The chemical component dictionary. If not provided, defaults to the - cached CCD. - - If present, returns the _atom_site.type_symbol. If not, infers it using - _atom_site.label_comp_id (residue name), _atom_site.label_atom_id (atom name) - and the CCD. - """ - ccd = ccd or chemical_components.cached_ccd() - - def type_symbol_fn(res_name, atom_name): return chemical_components.type_symbol( - ccd, res_name, atom_name - ) - return mmcif_atom_site.get_or_infer_type_symbol(mmcif, type_symbol_fn) - - -def get_chain_type_by_entity_id(mmcif: Mmcif) -> Mapping[str, str]: - """Returns mapping from entity ID to its type or polymer type if available. - - If the entity is in the _entity_poly table, returns its polymer chain type. - If not, returns the type as specified in the _entity table. - - Args: - mmcif: CifDict holding the mmCIF. - """ - poly_entity_id = mmcif.get('_entity_poly.entity_id', []) - poly_type = mmcif.get('_entity_poly.type', []) - poly_type_by_entity_id = dict(zip(poly_entity_id, poly_type, strict=True)) - - chain_type_by_entity_id = {} - for entity_id, entity_type in zip( - mmcif.get('_entity.id', []), mmcif.get('_entity.type', []), strict=True - ): - chain_type = poly_type_by_entity_id.get(entity_id) or entity_type - chain_type_by_entity_id[entity_id] = chain_type - - return chain_type_by_entity_id - - -def get_internal_to_author_chain_id_map(mmcif: Mmcif) -> Mapping[str, str]: - """Returns a mapping from internal chain ID to the author chain ID. - - Note that this is not a bijection. One author chain ID can map to multiple - internal chain IDs. For example, a protein chain and a ligand bound to it will - share the same author chain ID, but they will each have a unique internal - chain ID). - - Args: - mmcif: CifDict holding the mmCIF. - """ - return mmcif_atom_site.get_internal_to_author_chain_id_map(mmcif) - - -def get_experimental_method(mmcif: Mmcif) -> Optional[str]: - field = '_exptl.method' - return ','.join(mmcif[field]).lower() if field in mmcif else None - - -def get_release_date(mmcif: Mmcif) -> Optional[str]: - """Returns the oldest revision date.""" - if '_pdbx_audit_revision_history.revision_date' not in mmcif: - return None - - # Release dates are ISO-8601, hence sort well. - return min(mmcif['_pdbx_audit_revision_history.revision_date']) - - -def get_resolution(mmcif: Mmcif) -> Optional[float]: - """Returns the resolution of the structure. - - More than one resolution can be reported in an mmCIF. This function returns - the first one (in the order _refine.ls_d_res_high, - _em_3d_reconstruction.resolution, _reflns.d_resolution_high) that appears - in the mmCIF as is parseable as a float. - - Args: - mmcif: An `Mmcif` object. - - Returns: - The resolution as reported in the mmCIF. - """ - for res_key in ('_refine.ls_d_res_high', - '_em_3d_reconstruction.resolution', - '_reflns.d_resolution_high'): - if res_key in mmcif: - try: - raw_resolution = mmcif[res_key][0] - return float(raw_resolution) - except ValueError: - continue - return None - - -def parse_oper_expr(oper_expression: str) -> list[tuple[str, ...]]: - """Determines which transforms to apply based on an MMCIF oper_expression str. - - Args: - oper_expression: the field oper_expression from MMCIF format data. - Transform ids may be either numbers or single letters. Hyphens are used to - denote a numeric range of transforms to apply, and commas are used to - delimit a sequence of transforms. Where two sets of parentheses are - adjacent without a comma, the two sets of transforms should be combined as - a cartesian product, i.e. all possible pairs. - example 1,2,3 -> generate 3 copies of each chain by applying 1, 2 or 3. - example (1-3) -> generate 3 copies of each chain by applying 1, 2 or 3. - example (1-3)(4-6) -> generate 9 copies of each chain by applying one of - [(1,4), (1,5), (1,6), - (2,4), (2,5), (2,6), - (3,4), (3,5), (3,6)] - example (P) -> apply transform with id P. - - Raises: - ValueError: Failure to parse oper_expression. - - Returns: - A list with one element for each chain copy that should be generated. - Each element is a list of transform ids to apply. - """ - # Expand ranges, e.g. 1-4 -> 1,2,3,4. - def range_expander(match): - return ','.join( - [str(i) for i in range(int(match.group(1)), - int(match.group(2)) + 1)]) - - ranges_expanded = re.sub(r'\b(\d+)-(\d+)', range_expander, oper_expression) - - if re.fullmatch(r'(\w+,)*\w+', ranges_expanded): - # No brackets, just a single range, e.g. "1,2,3". - return [(t,) for t in ranges_expanded.split(',')] - elif re.fullmatch(r'\((\w+,)*\w+\)', ranges_expanded): - # Single range in brackets, e.g. "(1,2,3)". - return [(t,) for t in ranges_expanded[1:-1].split(',')] - elif re.fullmatch(r'\((\w+,)*\w+\)\((\w+,)*\w+\)', ranges_expanded): - # Cartesian product of two ranges, e.g. "(1,2,3)(4,5)". - part1, part2 = ranges_expanded[1:-1].split(')(') - return list(itertools.product(part1.split(','), part2.split(','))) - else: - raise ValueError( - f'Unsupported oper_expression format: {oper_expression}') - - -def format_float_array( - values: np.ndarray, num_decimal_places: int) -> Sequence[str]: - """Converts 1D array to a list of strings with the given number of decimals. - - This function is faster than converting via Python list comprehension, e.g.: - atoms_x = ['%.3f' % x for x in atoms_x] - - Args: - values: A numpy array with values to convert. This array is casted to - float32 before doing the conversion. - num_decimal_places: The number of decimal points to keep, including trailing - zeros. E.g. for 1.07 and num_decimal_places=1: 1.1, - num_decimal_places=2: 1.07, num_decimal_places=3: 1.070. - - Returns: - A list of formatted strings. - """ - if values.ndim != 1: - raise ValueError(f'The given array must be 1D, got {values.ndim}D') - - return string_array.format_float_array( - values=values.astype(np.float32), num_decimal_places=num_decimal_places - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/parsing.py b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/parsing.py deleted file mode 100644 index 77b894a39..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/parsing.py +++ /dev/null @@ -1,1802 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Module for parsing various data sources and producing Structures.""" - -from collections.abc import Collection, Mapping, MutableMapping, Sequence -import dataclasses -import datetime -import enum -import functools -import itertools -from typing import TypeAlias -import numpy as np -from alphafold3.constants import chemical_components -from alphafold3.constants import mmcif_names -from alphafold3.constants import residue_names -from alphafold3.cpp import mmcif_utils -from alphafold3.cpp import string_array -from alphafold3.structure import bioassemblies -from alphafold3.structure import bonds -from alphafold3.structure import chemical_components as struc_chem_comps -from alphafold3.structure import mmcif -from alphafold3.structure import structure -from alphafold3.structure import structure_tables - - -ChainIndex: TypeAlias = int -ResIndex: TypeAlias = int -AtomName: TypeAlias = str -BondAtomId: TypeAlias = tuple[ChainIndex, ResIndex, AtomName] - -_INSERTION_CODE_REMAP: Mapping[str, str] = {'.': '?'} - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class BondIndices: - from_indices: list[int] - dest_indices: list[int] - - -@enum.unique -class ModelID(enum.Enum): - """Values for specifying model IDs when parsing.""" - - FIRST = 1 # The first model in the file. - ALL = 2 # All models in the file. - - -@enum.unique -class SequenceFormat(enum.Enum): - """The possible formats for an input sequence.""" - - FASTA = 'fasta' # One-letter code used in FASTA. - # Multiple-letter chemical components dictionary ids. - CCD_CODES = 'ccd_codes' - LIGAND_SMILES = 'ligand_smiles' # SMILES string defining a molecule. - - -def _create_bond_lookup( - bonded_atom_pairs: Sequence[tuple[BondAtomId, BondAtomId]], -) -> Mapping[tuple[ChainIndex, ResIndex], Mapping[AtomName, BondIndices]]: - """Creates maps to help find bonds during a loop over residues.""" - bond_lookup = {} - for bond_i, (from_atom_id, dest_atom_id) in enumerate(bonded_atom_pairs): - from_chain_i, from_res_i, from_atom_name = from_atom_id - dest_chain_i, dest_res_i, dest_atom_name = dest_atom_id - bonds_by_from_atom_name = bond_lookup.setdefault( - (from_chain_i, from_res_i), {} - ) - bonds_by_dest_atom_name = bond_lookup.setdefault( - (dest_chain_i, dest_res_i), {} - ) - bonds_by_from_atom_name.setdefault( - from_atom_name, BondIndices(from_indices=[], dest_indices=[]) - ).from_indices.append(bond_i) - bonds_by_dest_atom_name.setdefault( - dest_atom_name, BondIndices(from_indices=[], dest_indices=[]) - ).dest_indices.append(bond_i) - return bond_lookup - - -def _get_atom_element( - ccd: chemical_components.Ccd, res_name: str, atom_name: str -) -> str: - return ( - chemical_components.type_symbol( - ccd=ccd, res_name=res_name, atom_name=atom_name - ) - or '?' - ) - - -def _get_representative_atom( - ccd: chemical_components.Ccd, - res_name: str, - chain_type: str, - sequence_format: SequenceFormat, -) -> tuple[str, str]: - match sequence_format: - case SequenceFormat.CCD_CODES: - atom_name = _get_first_non_leaving_atom(ccd=ccd, res_name=res_name) - atom_element = _get_atom_element( - ccd=ccd, res_name=res_name, atom_name=atom_name - ) - return atom_name, atom_element - case SequenceFormat.LIGAND_SMILES: - return '', '?' - case SequenceFormat.FASTA: - if chain_type in mmcif_names.PEPTIDE_CHAIN_TYPES: - return 'CA', 'C' - if chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES: - return "C1'", 'C' - else: - raise ValueError(chain_type) - case _: - raise ValueError(sequence_format) - - -@functools.lru_cache(maxsize=128) -def _get_first_non_leaving_atom( - ccd: chemical_components.Ccd, res_name: str -) -> str: - """Returns first definitely non-leaving atom if exists, as a stand-in.""" - all_atoms = struc_chem_comps.get_all_atoms_in_entry(ccd, res_name=res_name)[ - '_chem_comp_atom.atom_id' - ] - representative_atom = all_atoms[0] - if representative_atom == 'O1' and len(all_atoms) > 1: - representative_atom = all_atoms[1] - return representative_atom - - -def _add_ligand_to_chem_comp( - chem_comp: MutableMapping[str, struc_chem_comps.ChemCompEntry], - ligand_id: str, - ligand_smiles: str, -): - """Adds a ligand to chemical components. Raises ValueError on mismatch.""" - new_entry = struc_chem_comps.ChemCompEntry( - type='non-polymer', pdbx_smiles=ligand_smiles - ) - - existing_entry = chem_comp.get(ligand_id) - if existing_entry is None: - chem_comp[ligand_id] = new_entry - elif existing_entry != new_entry: - raise ValueError( - f'Mismatching data for ligand {ligand_id}: ' - f'{new_entry} != {existing_entry}' - ) - - -def _get_first_model_id(cif: mmcif.Mmcif) -> str: - """Returns cheaply the first model ID from the mmCIF.""" - return cif.get_array( - '_atom_site.pdbx_PDB_model_num', dtype=object, gather=slice(1) - )[0] - - -def _get_str_model_id( - cif: mmcif.Mmcif, - model_id: ModelID | int, -) -> str: - """Converts a user-specified model_id argument into a string.""" - match model_id: - case int(): - str_model_id = str(model_id) - case enum.Enum(): - # We compare the enum's value attribute since regular enum comparison - # breaks when adhoc importing. - match model_id.value: - case ModelID.FIRST.value: - str_model_id = _get_first_model_id(cif) - case ModelID.ALL.value: - str_model_id = '' - case _: - raise ValueError( - f'Model ID {model_id} with value {model_id.value} not recognized.' - ) - case _: - raise ValueError( - f'Model ID {model_id} with type {type(model_id)} not recognized.' - ) - return str_model_id - - -def _parse_bonds( - cif: mmcif.Mmcif, - atom_key: np.ndarray, - model_id: str, -) -> bonds.Bonds: - """Returns the bonds table extracted from the mmCIF. - - Args: - cif: The raw mmCIF to extract the bond information from. - atom_key: A numpy array defining atom key for each atom in _atom_site. Note - that the atom key must be computed before resolving alt-locs since this - function operates on the raw mmCIF! - model_id: The ID of the model to get bonds for. - """ - if '_struct_conn.id' not in cif: - # This is the category key item for the _struct_conn table, therefore - # we use it to determine whether to parse bond info. - return bonds.Bonds.make_empty() - from_atom, dest_atom = mmcif.get_bond_atom_indices(cif, model_id) - from_atom = np.array(from_atom, dtype=np.int64) - dest_atom = np.array(dest_atom, dtype=np.int64) - num_bonds = from_atom.shape[0] - bond_key = np.arange(num_bonds, dtype=np.int64) - bond_type = cif.get_array('_struct_conn.conn_type_id', dtype=object) - if '_struct_conn.pdbx_role' in cif: # This column isn't always present. - bond_role = cif.get_array('_struct_conn.pdbx_role', dtype=object) - else: - bond_role = np.full((num_bonds,), '?', dtype=object) - - bonds_mask = np.ones((num_bonds,), dtype=bool) - # Symmetries other than 1_555 imply the atom is not part of the asymmetric - # unit, and therefore this is a bond that only exists in the expanded - # bioassembly. - # We do not currently support parsing these types of bonds. - if '_struct_conn.ptnr1_symmetry' in cif: - ptnr1_symmetry = cif.get_array( - '_struct_conn.ptnr1_symmetry', dtype=object) - np.logical_and(bonds_mask, ptnr1_symmetry == '1_555', out=bonds_mask) - if '_struct_conn.ptnr2_symmetry' in cif: - ptnr2_symmetry = cif.get_array( - '_struct_conn.ptnr2_symmetry', dtype=object) - np.logical_and(bonds_mask, ptnr2_symmetry == '1_555', out=bonds_mask) - # Remove bonds that involve atoms that are not part of the structure, - # e.g. waters if include_water=False. In a rare case this also removes invalid - # bonds that are indicated by a key that is set to _atom_site size. - np.logical_and(bonds_mask, np.isin(from_atom, atom_key), out=bonds_mask) - np.logical_and(bonds_mask, np.isin(dest_atom, atom_key), out=bonds_mask) - return bonds.Bonds( - key=bond_key[bonds_mask], - type=bond_type[bonds_mask], - role=bond_role[bonds_mask], - from_atom_key=from_atom[bonds_mask], - dest_atom_key=dest_atom[bonds_mask], - ) - - -@dataclasses.dataclass(frozen=True, slots=True) -class _MmcifHeader: - name: str - resolution: float | None - release_date: datetime.date | None - structure_method: str | None - bioassembly_data: bioassemblies.BioassemblyData | None - chemical_components_data: struc_chem_comps.ChemicalComponentsData | None - - -def _get_mmcif_header( - cif: mmcif.Mmcif, - fix_mse: bool, - fix_unknown_dna: bool, -) -> _MmcifHeader: - """Extract header fields from an mmCIF object.""" - name = cif.get_data_name() - resolution = mmcif.get_resolution(cif) - - release_date = mmcif.get_release_date(cif) - if release_date is not None: - release_date = datetime.date.fromisoformat(release_date) - - experiments = cif.get('_exptl.method') - structure_method = ','.join(experiments) if experiments else None - - try: - bioassembly_data = bioassemblies.BioassemblyData.from_mmcif(cif) - except bioassemblies.MissingBioassemblyDataError: - bioassembly_data = None - - try: - chemical_components_data = ( - struc_chem_comps.ChemicalComponentsData.from_mmcif( - cif, fix_mse=fix_mse, fix_unknown_dna=fix_unknown_dna - ) - ) - except struc_chem_comps.MissingChemicalComponentsDataError: - chemical_components_data = None - - return _MmcifHeader( - name=name, - resolution=resolution, - release_date=release_date, - structure_method=structure_method, - bioassembly_data=bioassembly_data, - chemical_components_data=chemical_components_data, - ) - - -def from_parsed_mmcif( - mmcif_object: mmcif.Mmcif, - *, - name: str | None = None, - fix_mse_residues: bool = False, - fix_arginines: bool = False, - fix_unknown_dna: bool = False, - include_water: bool = False, - include_other: bool = False, - include_bonds: bool = False, - model_id: int | ModelID = ModelID.FIRST, -) -> structure.Structure: - """Construct a Structure from a parsed mmCIF object. - - This function is called by `from_mmcif` but can be useful when an mmCIF has - already been parsed e.g. to extract extra information from the header before - then converting to Structure for further manipulation. - - Args: - mmcif_object: A parsed mmcif.Mmcif object. - name: Optional name for the structure. If not provided, the name will be - taken from the mmCIF data_ field. - fix_mse_residues: If True, selenium atom sites (SE) in selenomethionine - (MSE) residues will be changed to sulphur atom sites (SD). This is because - methionine (MET) residues are often replaced with MSE to aid X-Ray - crystallography. If False, the SE MSE atom sites won't be modified. - fix_arginines: If True, NH1 and NH2 in arginine will be swapped if needed so - that NH1 is always closer to CD than NH2. If False, no atom sites in - arginine will be touched. Note that HH11, HH12, HH21, HH22 are fixed too. - fix_unknown_dna: If True, residues with name N in DNA chains will have their - res_name replaced with DN. Atoms are not changed. - include_water: If True, water (HOH) molecules will be parsed. Water - molecules may be grouped into chains, where number of residues > 1. Water - molecules are usually grouped into chains but do not necessarily all share - the same chain ID. - include_other: If True, all other atoms that are not included by any of the - above parameters will be included. This covers e.g. "polypeptide(D)" and - "macrolide" entities, as well as all other non-standard types. - include_bonds: If True, bond information will be parsed from the mmCIF and - stored in the Structure. - model_id: Either the integer model ID to parse, or one of ModelID.FIRST to - parse the first model, or ModelID.ALL to parse all models. - - Returns: - A Structure representation of the mmCIF object. - """ - str_model_id = _get_str_model_id(cif=mmcif_object, model_id=model_id) - header = _get_mmcif_header( - mmcif_object, fix_mse=fix_mse_residues, fix_unknown_dna=fix_unknown_dna - ) - - chains, residues, atoms = get_tables( - cif=mmcif_object, - fix_mse_residues=fix_mse_residues, - fix_arginines=fix_arginines, - fix_unknown_dna=fix_unknown_dna, - include_water=include_water, - include_other=include_other, - model_id=str_model_id, - ) - - if include_bonds: - # NB: parsing the atom table before the bonds table allows for a more - # informative error message when dealing with bad multi-model mmCIFs. - # We also ensure that we always use a specific model ID, even when parsing - # all models. - if str_model_id == '': # pylint: disable=g-explicit-bool-comparison - bonds_model_id = _get_first_model_id(mmcif_object) - else: - bonds_model_id = str_model_id - - bonds_table = _parse_bonds( - mmcif_object, - atom_key=atoms.key, - model_id=bonds_model_id, - ) - else: - bonds_table = bonds.Bonds.make_empty() - - return structure.Structure( - name=name if name is not None else header.name, - resolution=header.resolution, - release_date=header.release_date, - structure_method=header.structure_method, - bioassembly_data=header.bioassembly_data, - chemical_components_data=header.chemical_components_data, - bonds=bonds_table, - chains=chains, - residues=residues, - atoms=atoms, - ) - - -def from_mmcif( - mmcif_string: str | bytes, - *, - name: str | None = None, - fix_mse_residues: bool = False, - fix_arginines: bool = False, - fix_unknown_dna: bool = False, - include_water: bool = False, - include_other: bool = False, - include_bonds: bool = False, - model_id: int | ModelID = ModelID.FIRST, -) -> structure.Structure: - """Construct a Structure from a mmCIF string. - - Args: - mmcif_string: The string contents of an mmCIF file. - name: Optional name for the structure. If not provided, the name will be - taken from the mmCIF data_ field. - fix_mse_residues: If True, selenium atom sites (SE) in selenomethionine - (MSE) residues will be changed to sulphur atom sites (SD). This is because - methionine (MET) residues are often replaced with MSE to aid X-Ray - crystallography. If False, the SE MSE atom sites won't be modified. - fix_arginines: If True, NH1 and NH2 in arginine will be swapped if needed so - that NH1 is always closer to CD than NH2. If False, no atom sites in - arginine will be touched. Note that HH11, HH12, HH21, HH22 are fixed too. - fix_unknown_dna: If True, residues with name N in DNA chains will have their - res_name replaced with DN. Atoms are not changed. - include_water: If True, water (HOH) molecules will be parsed. Water - molecules may be grouped into chains, where number of residues > 1. Water - molecules are usually grouped into chains but do not necessarily all share - the same chain ID. - include_other: If True, all other atoms that are not included by any of the - above parameters will be included. This covers e.g. "polypeptide(D)" and - "macrolide" entities, as well as all other non-standard types. - include_bonds: If True, bond information will be parsed from the mmCIF and - stored in the Structure. - model_id: Either the integer model ID to parse, or one of ModelID.FIRST to - parse the first model, or ModelID.ALL to parse all models. - - Returns: - A Structure representation of the mmCIF string. - """ - mmcif_object = mmcif.from_string(mmcif_string) - - return from_parsed_mmcif( - mmcif_object, - name=name, - fix_mse_residues=fix_mse_residues, - fix_arginines=fix_arginines, - fix_unknown_dna=fix_unknown_dna, - include_water=include_water, - include_other=include_other, - include_bonds=include_bonds, - model_id=model_id, - ) - - -def from_res_arrays(atom_mask: np.ndarray, **kwargs) -> structure.Structure: - """Returns Structure created from from arrays with a residue dimension. - - All unset fields are filled with defaults (e.g. 1.0 for occupancy) or - unset/unknown values (e.g. UNK for residue type, or '.' for atom element). - - Args: - atom_mask: A array with shape (num_res, num_atom). This is used to decide - which atoms in the atom dimension are present in a given residue. Present - atoms should have a nonzero value, e.g. 1.0 or True. - **kwargs: A mapping from field name to values. For all array-valued fields - these arrays must have a dimension of length num_res. Chain and residue - fields should have this as their only dimension and atom fields should be - shaped (num_res, num_atom). Coordinate fields may also have arbitrary - leading dimensions (they must be the same across all coordinate fields). - See structure.{CHAIN,RESIDUE,ATOM}_FIELDS for a list of allowed fields. - """ - num_res, num_atom = atom_mask.shape - included_indices = np.flatnonzero(atom_mask) - - array_fields = ( - structure.CHAIN_FIELDS.keys() - | structure.RESIDUE_FIELDS.keys() - | structure.ATOM_FIELDS.keys() - ) - initializer_kwargs = {} - fields = {} - for k, val in kwargs.items(): - if k not in array_fields: - # The kwarg key isn't an array field name. Such kwargs are forwarded as-is - # to the constructor. They are expected to be global fields (e.g. name). - # Other values will raise an error when the constructor is called. - if k in structure.TABLE_FIELDS: - raise ValueError(f'Table fields must not be set. Got {k}.') - initializer_kwargs[k] = val - continue - elif val is None: - raise ValueError(f'{k} must be non-None.') - - if not isinstance(val, np.ndarray): - raise TypeError( - f'Value for {k} must be a NumPy array. Got {type(val)}.') - if k in structure.CHAIN_FIELDS or k in structure.RESIDUE_FIELDS: - if val.shape != (num_res,): - raise ValueError( - f'{k} must have shape ({num_res=},). Got {val.shape=}.' - ) - # Do not reshape the chain/residue arrays, they have the shape we need. - fields[k] = val - else: - if val.shape[-2:] != (num_res, num_atom): - raise ValueError( - f'{k} must have final two dimensions of length ' - f'{(num_res, num_atom)=}. Got {val.shape=}.' - ) - leading_dims = val.shape[:-2] - flat_val = val.reshape(leading_dims + (-1,), order='C') - masked_val = flat_val[..., included_indices] - fields[k] = masked_val - - # Get chain IDs or assume this is a single-chain structure. - chain_id = kwargs.get('chain_id', np.array(['A'] * num_res, dtype=object)) - # Find chain starts in res-sized arrays, use these to make chain-sized arrays. - chain_start = np.concatenate( - ([0], np.where(chain_id[1:] != chain_id[:-1])[0] + 1) - ) - if len(set(chain_id)) != len(chain_start): - raise ValueError(f'Chain IDs must be contiguous, but got {chain_id}') - - chain_lengths = np.diff(chain_start, append=len(chain_id)) - chain_key = np.repeat(np.arange(len(chain_start)), chain_lengths) - - chain_entity_id = fields.get('chain_entity_id') - if chain_entity_id is not None: - entity_id = chain_entity_id[chain_entity_id] - else: - entity_id = np.array( - [str(mmcif.str_id_to_int_id(cid)) - for cid in chain_id[chain_start]], - dtype=object, - ) - chain_str_empty = np.full((num_res,), '.', dtype=object) - chains_table = structure_tables.Chains( - key=chain_key[chain_start], - id=chain_id[chain_start], - type=fields.get('chain_type', chain_str_empty)[chain_start], - auth_asym_id=fields.get('chain_auth_asym_id', chain_id)[chain_start], - entity_id=entity_id, - entity_desc=fields.get('chain_entity_desc', chain_str_empty)[ - chain_start], - ) - - # Since all arrays are residue-shaped, we can use them directly. - res_key = np.arange(num_res, dtype=np.int64) - res_id = fields.get('res_id', res_key + 1).astype(np.int32) - residues_table = structure_tables.Residues( - key=res_key, - chain_key=chain_key, - id=res_id, - name=fields.get('res_name', np.full(num_res, 'UNK', dtype=object)), - auth_seq_id=fields.get( - 'res_auth_seq_id', np.char.mod('%d', res_id).astype(object) - ), - insertion_code=fields.get( - 'res_insertion_code', np.full(num_res, '?', dtype=object) - ), - ) - - # The atom-sized arrays have already been masked and reshaped. - num_atoms_per_res = np.sum(atom_mask, axis=1, dtype=np.int32) - num_atoms_total = np.sum(num_atoms_per_res, dtype=np.int32) - # Structure is immutable, so use the same array multiple times to save RAM. - atom_str_empty = np.full(num_atoms_total, '.', dtype=object) - atom_float32_zeros = np.zeros(num_atoms_total, dtype=np.float32) - atom_float32_ones = np.ones(num_atoms_total, dtype=np.float32) - atoms_table = structure_tables.Atoms( - key=np.arange(num_atoms_total, dtype=np.int64), - chain_key=np.repeat(chain_key, num_atoms_per_res), - res_key=np.repeat(res_key, num_atoms_per_res), - name=fields.get('atom_name', atom_str_empty), - element=fields.get('atom_element', atom_str_empty), - x=fields.get('atom_x', atom_float32_zeros), - y=fields.get('atom_y', atom_float32_zeros), - z=fields.get('atom_z', atom_float32_zeros), - b_factor=fields.get('atom_b_factor', atom_float32_zeros), - occupancy=fields.get('atom_occupancy', atom_float32_ones), - ) - - return structure.Structure( - chains=chains_table, - residues=residues_table, - atoms=atoms_table, - bonds=structure_tables.Bonds.make_empty(), # Currently not set. - **initializer_kwargs, - ) - - -def expand_sequence( - sequence: str, chain_type: str, sequence_format: SequenceFormat -) -> Sequence[str]: - """Returns full residue names based on a sequence string. - - Args: - sequence: A string representing the sequence. - chain_type: The chain type of the sequence. - sequence_format: The format of the sequence argument. - """ - match sequence_format: - case SequenceFormat.FASTA: - if not all(c.isalpha() for c in sequence): - raise ValueError( - f'Sequence "{sequence}" has non-alphabetic characters') - match chain_type: - case mmcif_names.PROTEIN_CHAIN: - res_name_map = residue_names.PROTEIN_COMMON_ONE_TO_THREE - default_res_name = residue_names.UNK - case mmcif_names.RNA_CHAIN: - res_name_map = {r: r for r in residue_names.RNA_TYPES} - default_res_name = residue_names.UNK_RNA - case mmcif_names.DNA_CHAIN: - res_name_map = residue_names.DNA_COMMON_ONE_TO_TWO - default_res_name = residue_names.UNK_DNA - case _: - raise ValueError( - f'{chain_type=} not supported for FASTA format.') - return [ - res_name_map.get(one_letter_res, default_res_name) - for one_letter_res in sequence - ] - case SequenceFormat.CCD_CODES: - return sequence.strip('()').split(')(') - case SequenceFormat.LIGAND_SMILES: - ligand_id, _ = sequence.split(':', maxsplit=1) - return [ligand_id] - - -def from_sequences_and_bonds( - sequences: Sequence[str], - chain_types: Sequence[str], - sequence_formats: Sequence[SequenceFormat], - bonded_atom_pairs: Sequence[tuple[BondAtomId, BondAtomId]] | None, - ccd: chemical_components.Ccd, - name: str = 'from_sequences_and_bonds', - bond_type: str | None = None, - **constructor_args, -) -> structure.Structure: - """Returns a minimal structure for the input sequences and bonds. - - The returned structure will have at least one atom per residue. If the - residue has any bonded atoms, according to `bonded_atom_pairs`, then - all (and only) those atoms will be present for that residue. If the residue - is not involved in any bond then an arbitrary atom will be created. - - Args: - sequences: A sequence of strings, each one representing a single chain. - chain_types: The types of each chain, e.g. polypeptide(L). The n-th element - describes the n-th sequence in `sequences`. - sequence_formats: The format of each sequence. The n-th element describes - the n-th sequence in `sequences`. - bonded_atom_pairs: A sequence of bonded atom pairs. Each atom is described - as a tuple of (chain_index, res_index, atom_name), where the first two - values are 0-based indices. The chain_index is the index of the chain in - the `sequences` argument, and the res_index is the index of the residue in - that sequence. The atom_name is the name of the atom in the residue, e.g. - CA. If the atom is not found in the standard atoms for that residue - (according to the CCD) then an error is raised. - ccd: The chemical components dictionary. - name: A name for the returned structure. - bond_type: This type will be used for all bonds in the structure, where type - follows PDB scheme, e.g. unknown (?), hydrog, metalc, covale, disulf. - **constructor_args: These arguments are passed directly to the - structure.Structure constructor. - """ - chain_id = [] - chain_type = [] - chain_res_count = [] - res_id = [] - res_name = [] - res_atom_count = [] - atom_name = [] - atom_element = [] - chem_comp = {} - - num_bonds = len(bonded_atom_pairs or ()) - from_atom_key = np.full((num_bonds,), -1, dtype=np.int64) - dest_atom_key = np.full((num_bonds,), -1, dtype=np.int64) - - # Create map (chain_i, res_i) -> {atom_name -> (from_idxs dest_idxs)}. - # This allows quick lookup of whether a residue has any bonded atoms, and - # which bonds those atoms participate in. - bond_lookup = _create_bond_lookup(bonded_atom_pairs or ()) - - current_atom_key = 0 - for chain_i, (sequence, curr_chain_type, sequence_format) in enumerate( - zip(sequences, chain_types, sequence_formats, strict=True) - ): - current_chain_id = mmcif.int_id_to_str_id(chain_i + 1) - num_chain_residues = 0 - for res_i, full_res_name in enumerate( - expand_sequence(sequence, curr_chain_type, sequence_format) - ): - current_res_id = res_i + 1 - num_res_atoms = 0 - - # Look for bonded atoms in the bond lookup and if any are found, add - # their atom keys to the bond atom_key columns. - if bond_indices_by_atom_name := bond_lookup.get((chain_i, res_i)): - for bond_atom_name, bond_indices in bond_indices_by_atom_name.items(): - atom_name.append(bond_atom_name) - atom_element.append( - _get_atom_element( - ccd=ccd, res_name=full_res_name, atom_name=bond_atom_name - ) - ) - for from_bond_i in bond_indices.from_indices: - from_atom_key[from_bond_i] = current_atom_key - for dest_bond_i in bond_indices.dest_indices: - dest_atom_key[dest_bond_i] = current_atom_key - current_atom_key += 1 - num_res_atoms += 1 - else: - # If this residue has no bonded atoms then we need to add one atom - # like in from_sequences. - rep_atom_name, rep_atom_element = _get_representative_atom( - ccd=ccd, - res_name=full_res_name, - chain_type=curr_chain_type, - sequence_format=sequence_format, - ) - atom_name.append(rep_atom_name) - atom_element.append(rep_atom_element) - num_res_atoms += 1 - current_atom_key += 1 - - if sequence_format == SequenceFormat.LIGAND_SMILES: - # Sequence expect to be in the format :, - # which always corresponds to a single-residue chain. - ligand_id, ligand_smiles = sequence.split(':', maxsplit=1) - if ccd.get(ligand_id) is not None: - raise ValueError( - f'Ligand name {ligand_id} is in CCD - it is not supported to give' - ' ligands created from SMILES the same name as CCD components.' - ) - # We need to provide additional chemical components metadata for - # ligands specified via SMILES strings since they might not be in CCD. - _add_ligand_to_chem_comp(chem_comp, ligand_id, ligand_smiles) - - res_atom_count.append(num_res_atoms) - num_chain_residues += 1 - res_id.append(current_res_id) - res_name.append(full_res_name) - - chain_id.append(current_chain_id) - chain_type.append(curr_chain_type) - chain_res_count.append(num_chain_residues) - - chem_comp_data = struc_chem_comps.ChemicalComponentsData(chem_comp) - chem_comp_data = struc_chem_comps.populate_missing_ccd_data( - ccd=ccd, - chemical_components_data=chem_comp_data, - chemical_component_ids=set(res_name), - ) - - if bonded_atom_pairs is not None: - unknown_bond_col = np.full((num_bonds,), '?', dtype=object) - if bond_type is None: - bond_type_col = unknown_bond_col - else: - bond_type_col = np.full((num_bonds,), bond_type, dtype=object) - bonds_table = bonds.Bonds( - key=np.arange(num_bonds, dtype=np.int64), - type=bond_type_col, - role=unknown_bond_col, - from_atom_key=from_atom_key, - dest_atom_key=dest_atom_key, - ) - else: - bonds_table = structure_tables.Bonds.make_empty() - - # 1 chain per sequence. - chain_key = np.arange(len(sequences), dtype=np.int64) - chain_id = np.array(chain_id, dtype=object) - chains_table = structure_tables.Chains( - key=chain_key, - id=chain_id, - type=np.array(chain_type, dtype=object), - auth_asym_id=chain_id, - entity_id=np.char.mod('%d', chain_key + 1).astype(object), - entity_desc=np.array(['.'] * len(chain_key), dtype=object), - ) - - res_key = np.arange(len(res_name), dtype=np.int64) - res_chain_key = np.repeat(chain_key, chain_res_count) - residues_table = structure_tables.Residues( - key=res_key, - chain_key=res_chain_key, - id=np.array(res_id, dtype=np.int32), - name=np.array(res_name, dtype=object), - auth_seq_id=np.char.mod('%d', res_id).astype(object), - insertion_code=np.full(len(res_name), '?', dtype=object), - ) - - num_atoms = current_atom_key - atom_float32_zeros = np.zeros(num_atoms, dtype=np.float32) - atoms_table = structure_tables.Atoms( - key=np.arange(num_atoms, dtype=np.int64), - chain_key=np.repeat(res_chain_key, res_atom_count), - res_key=np.repeat(res_key, res_atom_count), - name=np.array(atom_name, dtype=object), - element=np.array(atom_element, dtype=object), - x=atom_float32_zeros, - y=atom_float32_zeros, - z=atom_float32_zeros, - b_factor=atom_float32_zeros, - occupancy=np.ones(num_atoms, np.float32), - ) - - return structure.Structure( - name=name, - atoms=atoms_table, - residues=residues_table, - chains=chains_table, - bonds=bonds_table, - chemical_components_data=chem_comp_data, - **constructor_args, - ) - - -class _ChainResBuilder: - """Class for incrementally building chain and residue tables.""" - - def __init__( - self, - *, - chain_key_by_chain_id: Mapping[str, int], - entity_id_by_chain_id: Mapping[str, str], - chain_type_by_entity_id: Mapping[str, str], - entity_desc_by_entity_id: Mapping[str, str], - fix_mse_residues: bool, - fix_unknown_dna: bool, - ): - # Len: num_chains. - self.chain_key = [] - self.chain_id = [] - self.chain_type = [] - self.chain_auth_asym_id = [] - self.chain_entity_id = [] - self.chain_entity_desc = [] - - # Len: num_residues. - self.res_key = [] - self.res_chain_key = [] - self.res_id = [] - self.res_name = [] - self.res_auth_seq_id = [] - self.res_insertion_code = [] - - self.chain_key_by_chain_id = chain_key_by_chain_id - self.entity_id_by_chain_id = entity_id_by_chain_id - self.chain_type_by_entity_id = chain_type_by_entity_id - self.entity_desc_by_entity_id = entity_desc_by_entity_id - self.key_for_res: dict[tuple[str, str, str, str], int] = {} - - self._fix_mse_residues = fix_mse_residues - self._fix_unknown_dna = fix_unknown_dna - - def add_residues( - self, - *, - chain_ids: np.ndarray, - chain_auth_asym_ids: np.ndarray, - res_ids: np.ndarray, - res_names: np.ndarray, - res_auth_seq_ids: np.ndarray, - res_ins_codes: np.ndarray, - ): - """Adds a residue (and its chain) to the tables.""" - # Create chain table data. - if chain_ids.size == 0: - return - - chain_ids_with_prev = np.concatenate( - (([self.chain_id[-1] if self.chain_id else None], chain_ids)) - ) - chain_change_mask = chain_ids_with_prev[:-1] != chain_ids_with_prev[1:] - chain_change_ids = chain_ids[chain_change_mask] - chain_keys = string_array.remap( - chain_change_ids, self.chain_key_by_chain_id, inplace=False - ) - self.chain_key.extend(chain_keys) - self.chain_id.extend(chain_change_ids) - self.chain_auth_asym_id.extend(chain_auth_asym_ids[chain_change_mask]) - chain_entity_id = string_array.remap( - chain_change_ids, self.entity_id_by_chain_id, inplace=False - ) - self.chain_entity_id.extend(chain_entity_id) - chain_type = string_array.remap( - chain_entity_id, self.chain_type_by_entity_id, inplace=False - ) - self.chain_type.extend(chain_type) - chain_entity_desc = string_array.remap( - chain_entity_id, self.entity_desc_by_entity_id, inplace=False - ) - self.chain_entity_desc.extend(chain_entity_desc) - - # Create residue table data. - num_prev_res = len(self.res_id) - res_keys = np.arange(num_prev_res, num_prev_res + len(res_ids)) - res_iter = zip( - chain_ids, - res_auth_seq_ids, - res_names, - res_ins_codes, - strict=True, - ) - key_for_res_update = { - res_unique_id: res_key - for res_key, res_unique_id in enumerate(res_iter, num_prev_res) - } - self.key_for_res.update(key_for_res_update) - self.res_key.extend(res_keys) - self.res_chain_key.extend( - string_array.remap( - chain_ids, self.chain_key_by_chain_id, inplace=False) - ) - self.res_id.extend(res_ids) - self.res_name.extend(res_names) - self.res_auth_seq_id.extend(res_auth_seq_ids) - self.res_insertion_code.extend(res_ins_codes) - - def make_chains_table(self) -> structure_tables.Chains: - """Returns the Structure chains table.""" - chain_key = np.array(self.chain_key, dtype=np.int64) - if not np.all(chain_key[:-1] <= chain_key[1:]): - # If the order is inconsistent with the atoms table, sort so that it is. - order = np.argsort(self.chain_key, kind='stable') - return structure_tables.Chains( - key=chain_key[order], - id=np.array(self.chain_id, dtype=object)[order], - type=np.array(self.chain_type, dtype=object)[order], - auth_asym_id=np.array( - self.chain_auth_asym_id, dtype=object)[order], - entity_id=np.array(self.chain_entity_id, dtype=object)[order], - entity_desc=np.array( - self.chain_entity_desc, dtype=object)[order], - ) - return structure_tables.Chains( - key=chain_key, - id=np.array(self.chain_id, dtype=object), - type=np.array(self.chain_type, dtype=object), - auth_asym_id=np.array(self.chain_auth_asym_id, dtype=object), - entity_id=np.array(self.chain_entity_id, dtype=object), - entity_desc=np.array(self.chain_entity_desc, dtype=object), - ) - - def make_residues_table(self) -> structure_tables.Residues: - """Returns the Structure residues table.""" - res_name = np.array(self.res_name, dtype=object) - res_chain_key = np.array(self.res_chain_key, dtype=np.int64) - - if self._fix_mse_residues: - string_array.remap(res_name, mapping={'MSE': 'MET'}, inplace=True) - - if self._fix_unknown_dna: - # Remap residues from N -> DN in DNA chains only. - dna_chain_mask = ( - np.array(self.chain_type, dtype=object) == mmcif_names.DNA_CHAIN - ) - dna_chain_key = np.array(self.chain_key, dtype=object)[ - dna_chain_mask] - res_name[(res_name == 'N') & np.isin( - res_chain_key, dna_chain_key)] = 'DN' - - if not np.all(res_chain_key[:-1] <= res_chain_key[1:]): - # If the order is inconsistent with the atoms table, sort so that it is. - order = np.argsort(res_chain_key, kind='stable') - return structure_tables.Residues( - key=np.array(self.res_key, dtype=np.int64)[order], - chain_key=res_chain_key[order], - id=np.array(self.res_id, dtype=np.int32)[order], - name=res_name[order], - auth_seq_id=np.array(self.res_auth_seq_id, - dtype=object)[order], - insertion_code=np.array( - self.res_insertion_code, dtype=object)[order], - ) - return structure_tables.Residues( - key=np.array(self.res_key, dtype=np.int64), - chain_key=res_chain_key, - id=np.array(self.res_id, dtype=np.int32), - name=res_name, - auth_seq_id=np.array(self.res_auth_seq_id, dtype=object), - insertion_code=np.array(self.res_insertion_code, dtype=object), - ) - - -def _get_string_array_default(cif: mmcif.Mmcif, key: str, default: list[str]): - try: - return cif.get_array(key, dtype=object) - except KeyError: - return default - - -def _generate_required_tables_if_missing( - cif: mmcif.Mmcif, -) -> Mapping[str, Sequence[str]]: - """Generates all required tables and columns if missing.""" - update = {} - - atom_site_entities = _get_string_array_default( - cif, '_atom_site.label_entity_id', [] - ) - - # OpenMM produces files that don't have any of the tables and also have - # _atom_site.label_entity_id set to '?' for all atoms. We infer the entities - # based on the _atom_site.label_asym_id column. We start with cheaper O(1) - # checks to prevent running the expensive O(n) check on most files. - if ( - len(atom_site_entities) > 0 # pylint: disable=g-explicit-length-test - and '_entity.id' not in cif # Ignore if the _entity table exists. - and atom_site_entities[0] == '?' # Cheap check. - and set(atom_site_entities) == {'?'} # Expensive check. - ): - label_asym_ids = cif.get_array( - '_atom_site.label_asym_id', dtype=object) - atom_site_entities = [ - str(mmcif.str_id_to_int_id(cid)) for cid in label_asym_ids - ] - # Update _atom_site.label_entity_id to be consistent with the new tables. - update['_atom_site.label_entity_id'] = atom_site_entities - - # Check table existence by checking the presence of its primary key. - if '_struct_asym.id' not in cif: - # Infer the _struct_asym table using the _atom_site table. - asym_ids = _get_string_array_default( - cif, '_atom_site.label_asym_id', []) - - if len(atom_site_entities) == 0 or len(asym_ids) == 0: # pylint: disable=g-explicit-length-test - raise ValueError( - 'Could not parse an mmCIF with no _struct_asym table and also no ' - '_atom_site.label_entity_id or _atom_site.label_asym_id columns.' - ) - - # Deduplicate, but keep the order intact - dict.fromkeys maintains order. - entity_id_chain_id_pairs = list( - dict.fromkeys(zip(atom_site_entities, asym_ids, strict=True)) - ) - update['_struct_asym.entity_id'] = [ - e for e, _ in entity_id_chain_id_pairs] - update['_struct_asym.id'] = [c for _, c in entity_id_chain_id_pairs] - - if '_entity.id' not in cif: - # Infer the _entity_poly and _entity tables using the _atom_site table. - residues = _get_string_array_default( - cif, '_atom_site.label_comp_id', []) - group_pdb = _get_string_array_default(cif, '_atom_site.group_PDB', []) - if '_atom_site.label_entity_id' in cif: - entities = atom_site_entities - else: - # If _atom_site.label_entity_id not set, use the asym_id -> entity_id map. - asym_to_entity = dict( - zip( - cif['_struct_asym.id'], cif['_struct_asym.entity_id'], strict=True - ) - ) - entities = string_array.remap( - cif.get_array('_atom_site.label_asym_id', dtype=object), - mapping=asym_to_entity, - ) - - entity_ids = [] - entity_types = [] - entity_poly_entity_ids = [] - entity_poly_types = [] - entity_poly_table_missing = '_entity_poly.entity_id' not in cif - for entity_id, group in itertools.groupby( - zip(entities, residues, group_pdb, strict=True), key=lambda e: e[0] - ): - _, entity_residues, entity_group_pdb = zip(*group, strict=True) - entity_type = _guess_entity_type( - chain_residues=entity_residues, atom_types=entity_group_pdb - ) - entity_ids.append(entity_id) - entity_types.append(entity_type) - - if entity_poly_table_missing and entity_type == mmcif_names.POLYMER_CHAIN: - polymer_type = mmcif_names.guess_polymer_type(entity_residues) - entity_poly_entity_ids.append(entity_id) - entity_poly_types.append(polymer_type) - - update['_entity.id'] = entity_ids - update['_entity.type'] = entity_types - if entity_poly_table_missing: - update['_entity_poly.entity_id'] = entity_poly_entity_ids - update['_entity_poly.type'] = entity_poly_types - - if '_atom_site.type_symbol' not in cif: - update['_atom_site.type_symbol'] = mmcif.get_or_infer_type_symbol(cif) - - return update - - -def _maybe_add_missing_scheme_tables( - cif: mmcif.Mmcif, - res_starts: Sequence[int], - label_asym_ids: np.ndarray, - label_seq_ids: np.ndarray, - label_comp_ids: np.ndarray, - auth_seq_ids: np.ndarray, - pdb_ins_codes: np.ndarray, -) -> Mapping[str, Sequence[str]]: - """If missing, infers the scheme tables from the _atom_site table.""" - update = {} - - required_poly_seq_scheme_cols = ( - '_pdbx_poly_seq_scheme.asym_id', - '_pdbx_poly_seq_scheme.pdb_seq_num', - '_pdbx_poly_seq_scheme.pdb_ins_code', - '_pdbx_poly_seq_scheme.seq_id', - '_pdbx_poly_seq_scheme.mon_id', - '_pdbx_poly_seq_scheme.pdb_strand_id', - ) - if not all(col in cif for col in required_poly_seq_scheme_cols): - # Create a mask for atoms where each polymer residue start. - entity_id_by_chain_id = dict( - zip(cif['_struct_asym.id'], - cif['_struct_asym.entity_id'], strict=True) - ) - chain_type_by_entity_id = dict( - zip(cif['_entity.id'], cif['_entity.type'], strict=True) - ) - # Remap asym ID -> entity ID. - chain_type = string_array.remap( - label_asym_ids, mapping=entity_id_by_chain_id, inplace=False - ) - # Remap entity ID -> chain type. - string_array.remap( - chain_type, mapping=chain_type_by_entity_id, inplace=True - ) - res_mask = np.zeros_like(label_seq_ids, dtype=bool) - res_mask[res_starts] = True - res_mask &= chain_type == mmcif_names.POLYMER_CHAIN - - entity_poly_seq_cols = ( - '_entity_poly_seq.entity_id', - '_entity_poly_seq.num', - '_entity_poly_seq.mon_id', - ) - if all(col in cif for col in entity_poly_seq_cols): - # Use _entity_poly_seq if available. - poly_seq_num = cif.get_array('_entity_poly_seq.num', dtype=object) - poly_seq_mon_id = cif.get_array( - '_entity_poly_seq.mon_id', dtype=object) - poly_seq_entity_id = cif.get_array( - '_entity_poly_seq.entity_id', dtype=object - ) - label_seq_id_to_auth_seq_id = dict( - zip(label_seq_ids[res_mask], - auth_seq_ids[res_mask], strict=True) - ) - scheme_pdb_seq_num = string_array.remap( - poly_seq_num, mapping=label_seq_id_to_auth_seq_id, default_value='.' - ) - label_seq_id_to_ins_code = dict( - zip(label_seq_ids[res_mask], - pdb_ins_codes[res_mask], strict=True) - ) - scheme_pdb_ins_code = string_array.remap( - poly_seq_num, mapping=label_seq_id_to_ins_code, default_value='.' - ) - - # The _entity_poly_seq table is entity-based, while _pdbx_poly_seq_scheme - # is chain-based. A single entity could mean multiple chains (asym_ids), - # we therefore need to replicate each entity for all of the chains. - scheme_asym_id = [] - select = [] - indices = np.arange(len(poly_seq_entity_id), dtype=np.int32) - for asym_id, entity_id in zip( - cif['_struct_asym.id'], cif['_struct_asym.entity_id'], strict=True - ): - entity_mask = poly_seq_entity_id == entity_id - select.extend(indices[entity_mask]) - scheme_asym_id.extend([asym_id] * sum(entity_mask)) - - scheme_pdb_strand_id = string_array.remap( - np.array(scheme_asym_id, dtype=object), - mapping=mmcif.get_internal_to_author_chain_id_map(cif), - inplace=False, - ) - - update['_pdbx_poly_seq_scheme.asym_id'] = scheme_asym_id - update['_pdbx_poly_seq_scheme.pdb_strand_id'] = scheme_pdb_strand_id - update['_pdbx_poly_seq_scheme.pdb_seq_num'] = scheme_pdb_seq_num[select] - update['_pdbx_poly_seq_scheme.pdb_ins_code'] = scheme_pdb_ins_code[select] - update['_pdbx_poly_seq_scheme.seq_id'] = poly_seq_num[select] - update['_pdbx_poly_seq_scheme.mon_id'] = poly_seq_mon_id[select] - else: - # _entity_poly_seq not available, fallback to _atom_site. - res_asym_ids = label_asym_ids[res_mask] - res_strand_ids = string_array.remap( - array=res_asym_ids, - mapping=mmcif.get_internal_to_author_chain_id_map(cif), - inplace=False, - ) - update['_pdbx_poly_seq_scheme.asym_id'] = res_asym_ids - update['_pdbx_poly_seq_scheme.pdb_seq_num'] = auth_seq_ids[res_mask] - update['_pdbx_poly_seq_scheme.pdb_ins_code'] = pdb_ins_codes[res_mask] - update['_pdbx_poly_seq_scheme.seq_id'] = label_seq_ids[res_mask] - update['_pdbx_poly_seq_scheme.mon_id'] = label_comp_ids[res_mask] - update['_pdbx_poly_seq_scheme.pdb_strand_id'] = res_strand_ids - - required_nonpoly_scheme_cols = ( - '_pdbx_nonpoly_scheme.mon_id', - '_pdbx_nonpoly_scheme.asym_id', - '_pdbx_nonpoly_scheme.pdb_seq_num', - '_pdbx_nonpoly_scheme.pdb_ins_code', - ) - required_branch_scheme_cols = ( - '_pdbx_branch_scheme.mon_id', - '_pdbx_branch_scheme.asym_id', - '_pdbx_branch_scheme.pdb_seq_num', - ) - - # Generate _pdbx_nonpoly_scheme only if both tables are missing. - if not ( - all(col in cif for col in required_nonpoly_scheme_cols) - or all(col in cif for col in required_branch_scheme_cols) - ): - # To be strictly semantically correct, multi-residue ligands should be - # written in _pdbx_branch_scheme. However, Structure parsing handles - # correctly multi-residue ligands in _pdbx_nonpoly_scheme and the tables - # constructed here live only while parsing, hence this is unnecessary. - entity_id_by_chain_id = dict( - zip(cif['_struct_asym.id'], - cif['_struct_asym.entity_id'], strict=True) - ) - chain_type_by_entity_id = dict( - zip(cif['_entity.id'], cif['_entity.type'], strict=True) - ) - # Remap asym ID -> entity ID. - chain_type = string_array.remap( - label_asym_ids, mapping=entity_id_by_chain_id, inplace=False - ) - # Remap entity ID -> chain type. - string_array.remap( - chain_type, mapping=chain_type_by_entity_id, inplace=True - ) - res_mask = np.zeros_like(label_seq_ids, dtype=bool) - res_mask[res_starts] = True - res_mask &= chain_type != mmcif_names.POLYMER_CHAIN - - if not np.any(res_mask): - return update # Shortcut: no non-polymer residues. - - ins_codes = string_array.remap( - pdb_ins_codes[res_mask], mapping={'?': '.'}, inplace=False - ) - - update['_pdbx_nonpoly_scheme.asym_id'] = label_asym_ids[res_mask] - update['_pdbx_nonpoly_scheme.pdb_seq_num'] = auth_seq_ids[res_mask] - update['_pdbx_nonpoly_scheme.pdb_ins_code'] = ins_codes - update['_pdbx_nonpoly_scheme.mon_id'] = label_comp_ids[res_mask] - - return update - - -def _get_chain_key_by_chain_id( - resolved_chain_ids: np.ndarray, struct_asym_chain_ids: np.ndarray -) -> Mapping[str, int]: - """Returns chain key for each chain ID respecting resolved chain ordering.""" - # Check that all chain IDs found in the (potentially filtered) _atom_site - # table are present in the _struct_asym table. - unique_resolved_chain_ids = set(resolved_chain_ids) - if not unique_resolved_chain_ids.issubset(set(struct_asym_chain_ids)): - unique_resolved_chain_ids = sorted(unique_resolved_chain_ids) - unique_struct_asym_chain_ids = sorted(set(struct_asym_chain_ids)) - raise ValueError( - 'Bad mmCIF: chain IDs in _atom_site.label_asym_id ' - f'{unique_resolved_chain_ids} is not a subset of chain IDs in ' - f'_struct_asym.id {unique_struct_asym_chain_ids}.' - ) - - resolved_mask = string_array.isin( - struct_asym_chain_ids, unique_resolved_chain_ids - ) - # For all resolved chains, use the _atom_site order they appear in. E.g. - # resolved_chain_ids = [B A E D F] - # struct_asym_chain_ids = [A B C D E F] - # consistent_chain_order = [B A C E D F] - # chain_keys = [0 1 2 3 4 5] - consistent_chain_order = struct_asym_chain_ids.copy() - consistent_chain_order[resolved_mask] = resolved_chain_ids - return dict(zip(consistent_chain_order, range(len(struct_asym_chain_ids)))) - - -def get_tables( - cif: mmcif.Mmcif, - fix_mse_residues: bool, - fix_arginines: bool, - fix_unknown_dna: bool, - include_water: bool, - include_other: bool, - model_id: str, -) -> tuple[ - structure_tables.Chains, structure_tables.Residues, structure_tables.Atoms -]: - """Returns chain, residue, and atom tables from a parsed mmcif. - - Args: - cif: A parsed mmcif.Mmcif. - fix_mse_residues: See from_mmcif. - fix_arginines: See from_mmcif. - fix_unknown_dna: See from_mmcif. - include_water: See from_mmcif. - include_other: See from_mmcif. - model_id: A string defining which model ID to use. If set, only coordinates, - b-factors and occupancies for the given model are returned. If empty, - coordinates, b-factors and occupanciesall for models are returned with a - leading dimension of num_models. Note that the model_id argument in - from_mmcif is an integer and has slightly different use (see from_mmcif). - """ - # Add any missing tables and columns we require for parsing. - if cif_update := _generate_required_tables_if_missing(cif): - cif = cif.copy_and_update(cif_update) - - # Resolve alt-locs, selecting only a single option for each residue. Also - # computes the layout, which defines where chain and residue boundaries are. - atom_site_all_models, layout = mmcif_utils.filter( - cif, - include_nucleotides=True, - include_ligands=True, - include_water=include_water, - include_other=include_other, - model_id=model_id, - ) - atom_site_first_model = atom_site_all_models[0] - - # Get atom information from the _atom_site table. - def _first_model_string_array(col: str) -> np.ndarray: - return cif.get_array(col, dtype=object, gather=atom_site_first_model) - - def _requested_models_float_array(col: str) -> np.ndarray: - if not model_id: - # Return data for all models with a leading dimension of num_models. - return cif.get_array(col, dtype=np.float32, gather=atom_site_all_models) - else: - # Return data only for the single requested model. - return cif.get_array(col, dtype=np.float32, gather=atom_site_first_model) - - # These columns are the same for all models, fetch them just for the 1st one. - label_comp_ids = _first_model_string_array('_atom_site.label_comp_id') - label_asym_ids = _first_model_string_array('_atom_site.label_asym_id') - label_seq_ids = _first_model_string_array('_atom_site.label_seq_id') - label_atom_ids = _first_model_string_array('_atom_site.label_atom_id') - if '_atom_site.auth_seq_id' in cif: - auth_seq_ids = _first_model_string_array('_atom_site.auth_seq_id') - else: - # auth_seq_id unset, fallback to label_seq_id. - auth_seq_ids = label_seq_ids - type_symbols = _first_model_string_array('_atom_site.type_symbol') - pdbx_pdb_ins_codes = _first_model_string_array( - '_atom_site.pdbx_PDB_ins_code') - - # These columns are different for all models, fetch them as requested. - atom_x = _requested_models_float_array('_atom_site.Cartn_x') - atom_y = _requested_models_float_array('_atom_site.Cartn_y') - atom_z = _requested_models_float_array('_atom_site.Cartn_z') - atom_b_factor = _requested_models_float_array('_atom_site.B_iso_or_equiv') - atom_occupancy = _requested_models_float_array('_atom_site.occupancy') - - # Make sure the scheme (residue) tables exist in case they are not present. - if cif_update := _maybe_add_missing_scheme_tables( - cif, - res_starts=layout.residue_starts(), - label_asym_ids=label_asym_ids, - label_seq_ids=label_seq_ids, - label_comp_ids=label_comp_ids, - auth_seq_ids=auth_seq_ids, - pdb_ins_codes=pdbx_pdb_ins_codes, - ): - cif = cif.copy_and_update(cif_update) - - # Fix common issues found in mmCIF files, like swapped arginine NH atoms. - mmcif_utils.fix_residues( - layout, - comp_id=label_comp_ids, - atom_id=label_atom_ids, - atom_x=atom_x[0] if not model_id else atom_x, - atom_y=atom_y[0] if not model_id else atom_y, - atom_z=atom_z[0] if not model_id else atom_z, - fix_arg=fix_arginines, - ) - - # Get keys for chains in the order they appear in _atom_site while also - # dealing with empty chains. - resolved_chain_ids = label_asym_ids[layout.chain_starts()] - struct_asym_chain_ids = cif.get_array('_struct_asym.id', dtype=object) - - chain_key_by_chain_id = _get_chain_key_by_chain_id( - resolved_chain_ids=resolved_chain_ids, - struct_asym_chain_ids=struct_asym_chain_ids, - ) - entity_id_by_chain_id = dict( - zip(struct_asym_chain_ids, cif['_struct_asym.entity_id'], strict=True) - ) - entity_description = cif.get( - '_entity.pdbx_description', ['?'] * len(cif['_entity.id']) - ) - entity_desc_by_entity_id = dict( - zip(cif['_entity.id'], entity_description, strict=True) - ) - chain_type_by_entity_id = mmcif.get_chain_type_by_entity_id(cif) - auth_asym_id_by_chain_id = mmcif.get_internal_to_author_chain_id_map(cif) - - chain_res_builder = _ChainResBuilder( - chain_key_by_chain_id=chain_key_by_chain_id, - entity_id_by_chain_id=entity_id_by_chain_id, - chain_type_by_entity_id=chain_type_by_entity_id, - entity_desc_by_entity_id=entity_desc_by_entity_id, - fix_mse_residues=fix_mse_residues, - fix_unknown_dna=fix_unknown_dna, - ) - - # Collect data for polymer chain and residue tables. _pdbx_poly_seq_scheme is - # guaranteed to be present thanks to _maybe_add_missing_scheme_tables. - def _get_poly_seq_scheme_col(col: str) -> np.ndarray: - return cif.get_array(key=f'_pdbx_poly_seq_scheme.{col}', dtype=object) - - poly_seq_asym_ids = _get_poly_seq_scheme_col('asym_id') - poly_seq_pdb_seq_nums = _get_poly_seq_scheme_col('pdb_seq_num') - poly_seq_seq_ids = _get_poly_seq_scheme_col('seq_id') - poly_seq_mon_ids = _get_poly_seq_scheme_col('mon_id') - poly_seq_pdb_strand_ids = _get_poly_seq_scheme_col('pdb_strand_id') - poly_seq_pdb_ins_codes = _get_poly_seq_scheme_col('pdb_ins_code') - string_array.remap( - poly_seq_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True - ) - - # We resolved alt-locs earlier for the atoms table. In cases of heterogeneous - # residues (a residue with an alt-loc that is of different residue type), we - # need to also do the same resolution in the residues table. Compute a mask - # for the residues that were selected in the atoms table. - poly_seq_mask = mmcif_utils.selected_polymer_residue_mask( - layout=layout, - atom_site_label_asym_ids=label_asym_ids[layout.residue_starts()], - atom_site_label_seq_ids=label_seq_ids[layout.residue_starts()], - atom_site_label_comp_ids=label_comp_ids[layout.residue_starts()], - poly_seq_asym_ids=poly_seq_asym_ids, - poly_seq_seq_ids=poly_seq_seq_ids, - poly_seq_mon_ids=poly_seq_mon_ids, - ) - - if not include_other and poly_seq_mask: - # Mask filtered-out residues so that they are not treated as missing. - # Instead, we don't want them included in the chains/residues tables at all. - keep_mask = string_array.remap( - poly_seq_asym_ids, - mapping={cid: True for cid in resolved_chain_ids}, - default_value=False, - inplace=False, - ).astype(bool) - poly_seq_mask &= keep_mask - - chain_res_builder.add_residues( - chain_ids=poly_seq_asym_ids[poly_seq_mask], - chain_auth_asym_ids=poly_seq_pdb_strand_ids[poly_seq_mask], - res_ids=poly_seq_seq_ids[poly_seq_mask].astype(np.int32), - res_names=poly_seq_mon_ids[poly_seq_mask], - res_auth_seq_ids=poly_seq_pdb_seq_nums[poly_seq_mask], - res_ins_codes=poly_seq_pdb_ins_codes[poly_seq_mask], - ) - - # Collect data for ligand chain and residue tables. _pdbx_nonpoly_scheme - # could be empty/unset if there are only branched ligands. - def _get_nonpoly_scheme_col(col: str) -> np.ndarray: - key = f'_pdbx_nonpoly_scheme.{col}' - if f'_pdbx_nonpoly_scheme.{col}' in cif: - return cif.get_array(key=key, dtype=object) - else: - return np.array([], dtype=object) - - nonpoly_asym_ids = _get_nonpoly_scheme_col('asym_id') - nonpoly_auth_seq_ids = _get_nonpoly_scheme_col('pdb_seq_num') - nonpoly_pdb_ins_codes = _get_nonpoly_scheme_col('pdb_ins_code') - nonpoly_mon_ids = _get_nonpoly_scheme_col('mon_id') - nonpoly_auth_asym_id = string_array.remap( - nonpoly_asym_ids, mapping=auth_asym_id_by_chain_id, inplace=False - ) - - def _get_branch_scheme_col(col: str) -> np.ndarray: - key = f'_pdbx_branch_scheme.{col}' - if f'_pdbx_branch_scheme.{col}' in cif: - return cif.get_array(key=key, dtype=object) - else: - return np.array([], dtype=object) - - branch_asym_ids = _get_branch_scheme_col('asym_id') - branch_auth_seq_ids = _get_branch_scheme_col('pdb_seq_num') - branch_pdb_ins_codes = _get_branch_scheme_col('pdb_ins_code') - branch_mon_ids = _get_branch_scheme_col('mon_id') - branch_auth_asym_id = string_array.remap( - branch_asym_ids, mapping=auth_asym_id_by_chain_id, inplace=False - ) - - if branch_asym_ids.size > 0 and branch_pdb_ins_codes.size == 0: - branch_pdb_ins_codes = np.array( - ['.'] * branch_asym_ids.size, dtype=object) - - # Compute the heterogeneous residue masks as above, this time for ligands. - nonpoly_mask, branch_mask = mmcif_utils.selected_ligand_residue_mask( - layout=layout, - atom_site_label_asym_ids=label_asym_ids[layout.residue_starts()], - atom_site_label_seq_ids=label_seq_ids[layout.residue_starts()], - atom_site_auth_seq_ids=auth_seq_ids[layout.residue_starts()], - atom_site_label_comp_ids=label_comp_ids[layout.residue_starts()], - atom_site_pdbx_pdb_ins_codes=pdbx_pdb_ins_codes[layout.residue_starts( - )], - nonpoly_asym_ids=nonpoly_asym_ids, - nonpoly_auth_seq_ids=nonpoly_auth_seq_ids, - nonpoly_pdb_ins_codes=nonpoly_pdb_ins_codes, - nonpoly_mon_ids=nonpoly_mon_ids, - branch_asym_ids=branch_asym_ids, - branch_auth_seq_ids=branch_auth_seq_ids, - branch_pdb_ins_codes=branch_pdb_ins_codes, - branch_mon_ids=branch_mon_ids, - ) - - if not include_water: - if nonpoly_mask: - nonpoly_mask &= (nonpoly_mon_ids != 'HOH') & ( - nonpoly_mon_ids != 'DOD') - if branch_mask: - # Fix for bad mmCIFs that have water in the branch scheme table. - branch_mask &= (branch_mon_ids != 'HOH') & ( - branch_mon_ids != 'DOD') - - string_array.remap( - pdbx_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True - ) - string_array.remap( - nonpoly_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True - ) - string_array.remap( - branch_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True - ) - - def _ligand_residue_ids(chain_ids: np.ndarray) -> np.ndarray: - """Computes internal residue ID for ligand residues that don't have it.""" - - # E.g. chain_ids=[A, A, A, B, C, C, D, D, D] -> [1, 2, 3, 1, 1, 2, 1, 2, 3]. - indices = np.arange(chain_ids.size, dtype=np.int32) - return (indices + 1) - np.maximum.accumulate( - indices * (chain_ids != np.roll(chain_ids, 1)) - ) - - branch_residue_ids = _ligand_residue_ids(branch_asym_ids[branch_mask]) - nonpoly_residue_ids = _ligand_residue_ids(nonpoly_asym_ids[nonpoly_mask]) - - chain_res_builder.add_residues( - chain_ids=branch_asym_ids[branch_mask], - chain_auth_asym_ids=branch_auth_asym_id[branch_mask], - res_ids=branch_residue_ids, - res_names=branch_mon_ids[branch_mask], - res_auth_seq_ids=branch_auth_seq_ids[branch_mask], - res_ins_codes=branch_pdb_ins_codes[branch_mask], - ) - - chain_res_builder.add_residues( - chain_ids=nonpoly_asym_ids[nonpoly_mask], - chain_auth_asym_ids=nonpoly_auth_asym_id[nonpoly_mask], - res_ids=nonpoly_residue_ids, - res_names=nonpoly_mon_ids[nonpoly_mask], - res_auth_seq_ids=nonpoly_auth_seq_ids[nonpoly_mask], - res_ins_codes=nonpoly_pdb_ins_codes[nonpoly_mask], - ) - - chains = chain_res_builder.make_chains_table() - residues = chain_res_builder.make_residues_table() - - # Construct foreign residue keys for the atoms table. - res_ends = np.array(layout.residues(), dtype=np.int32) - res_starts = np.array(layout.residue_starts(), dtype=np.int32) - res_lengths = res_ends - res_starts - - # Check just for HOH, DOD can be part e.g. of hydroxycysteine. - if include_water: - res_chain_types = chains.apply_array_to_column( - column_name='type', arr=residues.chain_key - ) - water_mask = res_chain_types != mmcif_names.WATER - if 'HOH' in set(residues.name[water_mask]): - raise ValueError( - 'Bad mmCIF file: non-water entity has water molecules.') - else: - # Include resolved and unresolved residues. - if 'HOH' in set(residues.name) | set(label_comp_ids[res_starts]): - raise ValueError( - 'Bad mmCIF file: non-water entity has water molecules.') - - atom_chain_key = string_array.remap( - label_asym_ids, mapping=chain_res_builder.chain_key_by_chain_id - ).astype(int) - - # If any of the residue lookups failed, the mmCIF is corrupted. - try: - atom_res_key_per_res = string_array.remap_multiple( - ( - label_asym_ids[res_starts], - auth_seq_ids[res_starts], - label_comp_ids[res_starts], - pdbx_pdb_ins_codes[res_starts], - ), - mapping=chain_res_builder.key_for_res, - ) - except KeyError as e: - raise ValueError( - 'Lookup for the following atom from the _atom_site table failed: ' - f'(atom_id, auth_seq_id, res_name, ins_code)={e}. This is ' - 'likely due to a known issue with some multi-model mmCIFs that only ' - 'match the first model in _atom_site table to the _pdbx_poly_scheme, ' - '_pdbx_nonpoly_scheme, or _pdbx_branch_scheme tables.' - ) from e - - # The residue ID will be shared for all atoms within that residue. - atom_res_key = np.repeat(atom_res_key_per_res, repeats=res_lengths) - - if fix_mse_residues: - met_residues_mask = (residues.name == 'MET')[atom_res_key] - unfixed_mse_selenium_mask = met_residues_mask & ( - label_atom_ids == 'SE') - label_atom_ids[unfixed_mse_selenium_mask] = 'SD' - type_symbols[unfixed_mse_selenium_mask] = 'S' - - atoms = structure_tables.Atoms( - key=atom_site_first_model, - chain_key=atom_chain_key, - res_key=atom_res_key, - name=label_atom_ids, - element=type_symbols, - x=atom_x, - y=atom_y, - z=atom_z, - b_factor=atom_b_factor, - occupancy=atom_occupancy, - ) - - return chains, residues, atoms - - -def from_atom_arrays( - *, - res_id: np.ndarray, - name: str = 'unset', - release_date: datetime.date | None = None, - resolution: float | None = None, - structure_method: str | None = None, - all_residues: Mapping[str, Sequence[tuple[str, int]]] | None = None, - bioassembly_data: bioassemblies.BioassemblyData | None = None, - chemical_components_data: ( - struc_chem_comps.ChemicalComponentsData | None - ) = None, - bond_table: structure_tables.Bonds | None = None, - chain_id: np.ndarray | None = None, - chain_type: np.ndarray | None = None, - res_name: np.ndarray | None = None, - atom_key: np.ndarray | None = None, - atom_name: np.ndarray | None = None, - atom_element: np.ndarray | None = None, - atom_x: np.ndarray | None = None, - atom_y: np.ndarray | None = None, - atom_z: np.ndarray | None = None, - atom_b_factor: np.ndarray | None = None, - atom_occupancy: np.ndarray | None = None, -) -> structure.Structure: - """Returns a Structure constructed from atom array level data. - - All fields except name and, res_id are optional, all array fields consist of a - value for each atom in the structure - so residue and chain values should hold - the same value for each atom in the chain or residue. Fields which are not - defined are filled with default values. - - Validation is performed by the Structure constructor where possible - but - author_naming scheme and all_residues must be checked in this function. - - It is not possible to construct structures with chains that do not contain - any resolved residues using this function. If this is necessary, use the - structure.Structure constructor directly. - - Args: - res_id: Integer array of shape [num_atom]. The unique residue identifier for - each residue. mmCIF field - _atom_site.label_seq_id. - name: The name of the structure. E.g. a PDB ID. - release_date: The release date of the structure as a `datetime.date`. - resolution: The resolution of the structure in Angstroms. - structure_method: The method used to solve this structure's coordinates. - all_residues: An optional mapping from each chain ID (i.e. label_asym_id) to - a sequence of (label_comp_id, label_seq_id) tuples, one per residue. This - can contain residues that aren't present in the atom arrays. This is - common in experimental data where some residues are not resolved but are - known to be present. - bioassembly_data: An optional instance of bioassembly.BioassemblyData. If - present then a new Structure representing a specific bioassembly can be - extracted using `Structure.generate_bioassembly(assembly_id)`. - chemical_components_data: An optional instance of ChemicalComponentsData. - Its content will be used for providing metadata about chemical components - in this Structure instance. If not specified information will be retrieved - from the standard chemical component dictionary (CCD, for more details see - https://www.wwpdb.org/data/ccd). - bond_table: A table representing manually-specified bonds. This corresponds - to the _struct_conn table in an mmCIF. Atoms are identified by their key, - as specified by the atom_key column. If this table is provided then the - atom_key column must also be defined. - chain_id: String array of shape [num_atom] of unique chain identifiers. - mmCIF field - _atom_site.label_asym_id. - chain_type: String array of shape [num_atom]. The molecular type of the - current chain (e.g. polyribonucleotide). mmCIF field - _entity_poly.type - OR _entity.type (for non-polymers). - res_name: String array of shape [num_atom].. The name of each residue, - typically a 3 letter string for polypeptides or 1-2 letter strings for - polynucleotides. mmCIF field - _atom_site.label_comp_id. - atom_key: A unique sorted integer array, used only by the bonds table to - identify the atoms participating in each bond. If the bonds table is - specified then this column must be non-None. - atom_name: String array of shape [num_atom]. The name of each atom (e.g CA, - O2', etc.). mmCIF field - _atom_site.label_atom_id. - atom_element: String array of shape [num_atom]. The element type of each - atom (e.g. C, O, N, etc.). mmCIF field - _atom_site.type_symbol. - atom_x: Float array of shape [..., num_atom] of atom x coordinates. May have - arbitrary leading dimensions, provided that these are consistent across - all coordinate fields. - atom_y: Float array of shape [..., num_atom] of atom y coordinates. May have - arbitrary leading dimensions, provided that these are consistent across - all coordinate fields. - atom_z: Float array of shape [..., num_atom] of atom z coordinates. May have - arbitrary leading dimensions, provided that these are consistent across - all coordinate fields. - atom_b_factor: Float array of shape [..., num_atom] or [num_atom] of atom - b-factors or equivalent. If there are no extra leading dimensions then - these values are assumed to apply to all coordinates for a given atom. If - there are leading dimensions then these must match those used by the - coordinate fields. - atom_occupancy: Float array of shape [..., num_atom] or [num_atom] of atom - occupancies or equivalent. If there are no extra leading dimensions then - these values are assumed to apply to all coordinates for a given atom. If - there are leading dimensions then these must match those used by the - coordinate fields. - """ - - atoms, residues, chains = structure_tables.tables_from_atom_arrays( - res_id=res_id, - all_residues=all_residues, - chain_id=chain_id, - chain_type=chain_type, - res_name=res_name, - atom_key=atom_key, - atom_name=atom_name, - atom_element=atom_element, - atom_x=atom_x, - atom_y=atom_y, - atom_z=atom_z, - atom_b_factor=atom_b_factor, - atom_occupancy=atom_occupancy, - ) - - return structure.Structure( - name=name, - release_date=release_date, - resolution=resolution, - structure_method=structure_method, - bioassembly_data=bioassembly_data, - chemical_components_data=chemical_components_data, - atoms=atoms, - chains=chains, - residues=residues, - bonds=bond_table or structure_tables.Bonds.make_empty(), - ) - - -def _guess_entity_type( - chain_residues: Collection[str], atom_types: Collection[str] -) -> str: - """Guess the entity type (polymer/non-polymer/water) based on residues/atoms. - - We treat both arguments as unordered collections since we care only whether - all elements satisfy come conditions. The chain_residues can be either - grouped by residue (length num_res), or it can be raw (length num_atoms). - Atom type is unique for each atom in a residue, so don't group atom_types. - - Args: - chain_residues: A sequence of full residue name (1-letter for DNA, 2-letters - for RNA, 3 for protein). The _atom_site.label_comp_id column in mmCIF. - atom_types: Atom type: ATOM or HETATM. The _atom_site.group_PDB column in - mmCIF. - - Returns: - One of polymer/non-polymer/water based on the following criteria: - * If all atoms are HETATMs and all residues are water -> water. - * If all atoms are HETATMs and not all residues are water -> non-polymer. - * Otherwise -> polymer. - """ - if not chain_residues or not atom_types: - raise ValueError( - f'chain_residues (len {len(chain_residues)}) and atom_types (len ' - f'{len(atom_types)}) must be both non-empty. Got: {chain_residues=} ' - f'and {atom_types=}' - ) - - if all(a == 'HETATM' for a in atom_types): - if all(c in residue_names.WATER_TYPES for c in chain_residues): - return mmcif_names.WATER - return mmcif_names.NON_POLYMER_CHAIN - return mmcif_names.POLYMER_CHAIN diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/sterics.py b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/sterics.py deleted file mode 100644 index 55c7c1783..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/sterics.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Functions relating to spatial locations of atoms within a structure.""" - -from collections.abc import Collection, Sequence - -from alphafold3 import structure -from alphafold3.structure import mmcif -import numpy as np -import scipy - - -def _make_atom_has_clash_mask( - kd_query_result: np.ndarray, - struct: structure.Structure, - ignore_chains: Collection[str], -) -> np.ndarray: - """Returns a boolean NumPy array representing whether each atom has a clash. - - Args: - kd_query_result: NumPy array containing N-atoms arrays, each array - containing indices to atoms that clash with the N'th atom. - struct: Structure over which clashes were detected. - ignore_chains: Collection of chains that should not be considered clashing. - A boolean NumPy array of length N atoms. - """ - atom_is_clashing = np.zeros((struct.num_atoms,), dtype=bool) - for atom_index, clashes in enumerate(kd_query_result): - chain_i = struct.chain_id[atom_index] - if chain_i in ignore_chains: - continue - islig_i = struct.is_ligand_mask[atom_index] - for clashing_atom_index in clashes: - chain_c = struct.chain_id[clashing_atom_index] - if chain_c in ignore_chains: - continue - islig_c = struct.is_ligand_mask[clashing_atom_index] - if ( - clashing_atom_index == atom_index - or chain_i == chain_c - or islig_i != islig_c - ): - # Ignore clashes within chain or between ligand and polymer. - continue - atom_is_clashing[atom_index] = True - return atom_is_clashing - - -def find_clashing_chains( - struct: structure.Structure, - clash_thresh_angstrom: float = 1.7, - clash_thresh_fraction: float = 0.3, -) -> Sequence[str]: - """Finds chains that clash with others. - - Clashes are defined by polymer backbone atoms and all ligand atoms. - Ligand-polymer clashes are not dropped. - - Will not find clashes if all coordinates are 0. Coordinates are all 0s if - the structure is generated from sequences only, as done for inference in - dendro for example. - - Args: - struct: The structure defining the chains and atom positions. - clash_thresh_angstrom: Below this distance, atoms are considered clashing. - clash_thresh_fraction: Chains with more than this fraction of their atoms - considered clashing will be dropped. This value should be in the range (0, - 1]. - - Returns: - A sequence of chain ids for chains that clash. - - Raises: - ValueError: If `clash_thresh_fraction` is not in range (0,1]. - """ - if not 0 < clash_thresh_fraction <= 1: - raise ValueError('clash_thresh_fraction must be in range (0,1]') - - struct_backbone = struct.filter_polymers_to_single_atom_per_res() - if struct_backbone.num_chains == 0: - return [] - - # If the coordinates are all 0, do not search for clashes. - if not np.any(struct_backbone.coords): - return [] - - coord_kdtree = scipy.spatial.cKDTree(struct_backbone.coords) - - # For each atom coordinate, find all atoms within the clash thresh radius. - clashing_per_atom = coord_kdtree.query_ball_point( - struct_backbone.coords, r=clash_thresh_angstrom - ) - chain_ids = struct_backbone.chains - if struct_backbone.atom_occupancy is not None: - chain_occupancy = np.array([ - np.mean(struct_backbone.atom_occupancy[start:end]) - for start, end in struct_backbone.iter_chain_ranges() - ]) - else: - chain_occupancy = None - - # Remove chains until no more significant clashing. - chains_to_remove = set() - for _ in range(len(chain_ids)): - # Calculate maximally clashing. - atom_has_clash = _make_atom_has_clash_mask( - clashing_per_atom, struct_backbone, chains_to_remove - ) - clashes_per_chain = np.array([ - atom_has_clash[start:end].mean() - for start, end in struct_backbone.iter_chain_ranges() - ]) - max_clash = np.max(clashes_per_chain) - if max_clash <= clash_thresh_fraction: - # None of the remaining chains exceed the clash fraction threshold, so - # we can exit. - break - - # Greedily remove worst with the lowest occupancy. - most_clashes = np.nonzero(clashes_per_chain == max_clash)[0] - if chain_occupancy is not None: - occupancy_clashing = chain_occupancy[most_clashes] - last_lowest_occupancy = ( - len(occupancy_clashing) - - np.argmin(occupancy_clashing[::-1]) - 1 - ) - worst_and_last = most_clashes[last_lowest_occupancy] - else: - worst_and_last = most_clashes[-1] - - chains_to_remove.add(chain_ids[worst_and_last]) - - return sorted(chains_to_remove, key=mmcif.str_id_to_int_id) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure.py b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure.py deleted file mode 100644 index d0dff798c..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure.py +++ /dev/null @@ -1,3179 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Structure class for representing and processing molecular structures.""" - -import collections -from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, Sequence, Set -import dataclasses -import datetime -import enum -import functools -import itertools -import typing -from typing_extensions import Any, ClassVar, Final, Literal, NamedTuple, Self, TypeAlias, TypeVar -import numpy as np -from alphafold3.constants import atom_types -from alphafold3.constants import chemical_components -from alphafold3.constants import mmcif_names -from alphafold3.constants import residue_names -from alphafold3.cpp import membership -from alphafold3.cpp import string_array -from alphafold3.structure import bioassemblies -from alphafold3.structure import chemical_components as struct_chem_comps -from alphafold3.structure import mmcif -from alphafold3.structure import structure_tables -from alphafold3.structure import table - -# Controls the default number of decimal places for coordinates when writing to -# mmCIF. -_COORDS_DECIMAL_PLACES: Final[int] = 3 - - -@enum.unique -class CascadeDelete(enum.Enum): - NONE = 0 - FULL = 1 - CHAINS = 2 - - -# See www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions -class _UnsetSentinel(enum.Enum): - UNSET = object() - - -_UNSET = _UnsetSentinel.UNSET - - -class Bond(NamedTuple): - """Describes a bond between two atoms.""" - - from_atom: Mapping[str, str | int | float | np.ndarray] - dest_atom: Mapping[str, str | int | float | np.ndarray] - bond_info: Mapping[str, str | int] - - -class MissingAtomError(Exception): - """Error raised when an atom is missing during alignment.""" - - -class MissingAuthorResidueIdError(Exception): - """Raised when author naming data is missing for a residue. - - This can occur in certain edge cases where missing residue data is provided - without also providing author IDs for those missing residues. - """ - - -# AllResidues is a mapping from label_asym_id to a sequence of (label_comp_id, -# label_seq_id) pairs. These represent the full sequence including residues -# that might be missing (e.g. unresolved residues in X-ray data). -AllResidues: TypeAlias = Mapping[str, Sequence[tuple[str, int]]] -AuthorNamingScheme: TypeAlias = structure_tables.AuthorNamingScheme - - -# External residue ID given to missing residues that don't have an ID -# already provided. In mmCIFs this data is found in _pdbx_poly_seq_scheme. -MISSING_AUTH_SEQ_ID: Final[str] = '.' - - -# Maps from structure fields to column names in the relevant table. -CHAIN_FIELDS: Final[Mapping[str, str]] = { - 'chain_id': 'id', - 'chain_type': 'type', - 'chain_auth_asym_id': 'auth_asym_id', - 'chain_entity_id': 'entity_id', - 'chain_entity_desc': 'entity_desc', -} - - -RESIDUE_FIELDS: Final[Mapping[str, str]] = { - 'res_id': 'id', - 'res_name': 'name', - 'res_auth_seq_id': 'auth_seq_id', - 'res_insertion_code': 'insertion_code', -} - -ATOM_FIELDS: Final[Mapping[str, str]] = { - 'atom_name': 'name', - 'atom_element': 'element', - 'atom_x': 'x', - 'atom_y': 'y', - 'atom_z': 'z', - 'atom_b_factor': 'b_factor', - 'atom_occupancy': 'occupancy', - 'atom_key': 'key', -} - -# Fields in structure. -ARRAY_FIELDS = frozenset({ - 'atom_b_factor', - 'atom_element', - 'atom_key', - 'atom_name', - 'atom_occupancy', - 'atom_x', - 'atom_y', - 'atom_z', - 'chain_id', - 'chain_type', - 'res_id', - 'res_name', -}) - -GLOBAL_FIELDS = frozenset({ - 'name', - 'release_date', - 'resolution', - 'structure_method', - 'bioassembly_data', - 'chemical_components_data', -}) - -# Fields which can be updated in copy_and_update. -_UPDATEABLE_FIELDS: Final[Set[str]] = frozenset({ - 'all_residues', - 'atom_b_factor', - 'atom_element', - 'atom_key', - 'atom_name', - 'atom_occupancy', - 'atom_x', - 'atom_y', - 'atom_z', - 'bioassembly_data', - 'bonds', - 'chain_id', - 'chain_type', - 'chemical_components_data', - 'name', - 'release_date', - 'res_id', - 'res_name', - 'resolution', - 'structure_method', -}) - - -def fix_non_standard_polymer_residues( - res_names: np.ndarray, chain_type: str -) -> np.ndarray: - """Remaps residue names to the closest standard protein/RNA/DNA residue. - - If residue name is already a standard type, it is not altered. - If a match cannot be found, returns 'UNK' for protein chainresidues and 'N' - for RNA/DNA chain residue. - - Args: - res_names: A numpy array of string residue names (CCD monomer codes). E.g. - 'ARG' (protein), 'DT' (DNA), 'N' (RNA). - chain_type: The type of the chain, must be PROTEIN_CHAIN, RNA_CHAIN or - DNA_CHAIN. - - Returns: - An array remapped so that its elements are all from - PROTEIN_TYPES_WITH_UNKNOWN | RNA_TYPES | DNA_TYPES | {'N'}. - - Raises: - ValueError: If chain_type not in PEPTIDE_CHAIN_TYPES or - {OTHER_CHAIN, RNA_CHAIN, DNA_CHAIN, DNA_RNA_HYBRID_CHAIN}. - """ - # Map to one letter code, then back to common res_names. - one_letter_codes = string_array.remap( - res_names, mapping=residue_names.CCD_NAME_TO_ONE_LETTER, default_value='X' - ) - - if ( - chain_type in mmcif_names.PEPTIDE_CHAIN_TYPES - or chain_type == mmcif_names.OTHER_CHAIN - ): - mapping = residue_names.PROTEIN_COMMON_ONE_TO_THREE - default_value = 'UNK' - elif chain_type == mmcif_names.RNA_CHAIN: - # RNA has single-letter CCD monomer codes. - mapping = {r: r for r in residue_names.RNA_TYPES} - default_value = 'N' - elif chain_type == mmcif_names.DNA_CHAIN: - mapping = residue_names.DNA_COMMON_ONE_TO_TWO - default_value = 'N' - elif chain_type == mmcif_names.DNA_RNA_HYBRID_CHAIN: - mapping = {r: r for r in residue_names.NUCLEIC_TYPES_WITH_UNKNOWN} - default_value = 'N' - else: - raise ValueError( - f'Expected a protein/DNA/RNA chain but got {chain_type}') - - return string_array.remap( - one_letter_codes, mapping=mapping, default_value=default_value - ) - - -def _get_change_indices(arr: np.ndarray) -> np.ndarray: - if arr.size == 0: - return np.array([], dtype=np.int32) - else: - changing_idxs = np.where(arr[1:] != arr[:-1])[0] + 1 - return np.concatenate(([0], changing_idxs), axis=0) - - -def _unpack_filter_predicates( - predicate_by_field_name: Mapping[str, table.FilterPredicate], -) -> tuple[ - Mapping[str, table.FilterPredicate], - Mapping[str, table.FilterPredicate], - Mapping[str, table.FilterPredicate], -]: - """Unpacks filter kwargs into predicates for each table.""" - chain_predicates = {} - res_predicates = {} - atom_predicates = {} - for k, pred in predicate_by_field_name.items(): - if col := CHAIN_FIELDS.get(k): - chain_predicates[col] = pred - elif col := RESIDUE_FIELDS.get(k): - res_predicates[col] = pred - elif col := ATOM_FIELDS.get(k): - atom_predicates[col] = pred - else: - raise ValueError(k) - return chain_predicates, res_predicates, atom_predicates - - -_T = TypeVar('_T') - - -SCALAR_FIELDS: Final[Collection[str]] = frozenset({ - 'name', - 'release_date', - 'resolution', - 'structure_method', - 'bioassembly_data', - 'chemical_components_data', -}) - - -TABLE_FIELDS: Final[Collection[str]] = frozenset( - {'chains', 'residues', 'atoms', 'bonds'} -) - - -V2_FIELDS: Final[Collection[str]] = frozenset({*SCALAR_FIELDS, *TABLE_FIELDS}) - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class StructureTables: - chains: structure_tables.Chains - residues: structure_tables.Residues - atoms: structure_tables.Atoms - bonds: structure_tables.Bonds - - -class Structure(table.Database): - """Structure class for representing and processing molecular structures.""" - - tables: ClassVar[Collection[str]] = TABLE_FIELDS - - foreign_keys: ClassVar[Mapping[str, Collection[tuple[str, str]]]] = { - 'residues': (('chain_key', 'chains'),), - 'atoms': (('chain_key', 'chains'), ('res_key', 'residues')), - 'bonds': (('from_atom_key', 'atoms'), ('dest_atom_key', 'atoms')), - } - - def __init__( - self, - *, - name: str = 'unset', - release_date: datetime.date | None = None, - resolution: float | None = None, - structure_method: str | None = None, - bioassembly_data: bioassemblies.BioassemblyData | None = None, - chemical_components_data: ( - struct_chem_comps.ChemicalComponentsData | None - ) = None, - chains: structure_tables.Chains, - residues: structure_tables.Residues, - atoms: structure_tables.Atoms, - bonds: structure_tables.Bonds, - skip_validation: bool = False, - ): - # Version number is written to mmCIF and should be incremented when changes - # are made to mmCIF writing or internals that affect this. - # b/345221494 Rename this variable when structure_v1 compatibility code - # is removed. - self._VERSION = '2.0.0' # pylint: disable=invalid-name - self._name = name - self._release_date = release_date - self._resolution = resolution - self._structure_method = structure_method - self._bioassembly_data = bioassembly_data - self._chemical_components_data = chemical_components_data - - self._chains = chains - self._residues = residues - self._atoms = atoms - self._bonds = bonds - - if not skip_validation: - self._validate_table_foreign_keys() - self._validate_consistent_table_ordering() - - def _validate_table_foreign_keys(self): - """Validates that all foreign keys are present in the referred tables.""" - residue_keys = set(self._residues.key) - chain_keys = set(self._chains.key) - if np.any(membership.isin(self._atoms.res_key, residue_keys, invert=True)): - raise ValueError( - 'Atom residue keys not in the residues table: ' - f'{set(self._atoms.res_key).difference(self._residues.key)}' - ) - if np.any(membership.isin(self._atoms.chain_key, chain_keys, invert=True)): - raise ValueError( - 'Atom chain keys not in the chains table: ' - f'{set(self._atoms.chain_key).difference(self._chains.key)}' - ) - if np.any( - membership.isin(self._residues.chain_key, chain_keys, invert=True) - ): - raise ValueError( - 'Residue chain keys not in the chains table: ' - f'{set(self._residues.chain_key).difference(self._chains.key)}' - ) - - def _validate_consistent_table_ordering(self): - """Validates that all tables have the same ordering.""" - atom_chain_keys = self._atoms.chain_key[self.chain_boundaries] - atom_res_keys = self._atoms.res_key[self.res_boundaries] - - if not np.array_equal(self.present_chains.key, atom_chain_keys): - raise ValueError( - f'Atom table chain order\n{atom_chain_keys}\ndoes not match the ' - f'chain table order\n{self._chains.key}' - ) - if not np.array_equal(self.present_residues.key, atom_res_keys): - raise ValueError( - f'Atom table residue order\n{atom_res_keys}\ndoes not match the ' - f'present residue table order\n{self.present_residues.key}' - ) - - def get_table(self, table_name: str) -> table.Table: - match table_name: - case 'chains': - return self.chains_table - case 'residues': - return self.residues_table - case 'atoms': - return self.atoms_table - case 'bonds': - return self.bonds_table - case _: - raise ValueError(table_name) - - @property - def chains_table(self) -> structure_tables.Chains: - """Chains table.""" - return self._chains - - @property - def residues_table(self) -> structure_tables.Residues: - """Residues table.""" - return self._residues - - @property - def atoms_table(self) -> structure_tables.Atoms: - """Atoms table.""" - return self._atoms - - @property - def bonds_table(self) -> structure_tables.Bonds: - """Bonds table.""" - return self._bonds - - @property - def name(self) -> str: - return self._name - - @property - def release_date(self) -> datetime.date | None: - return self._release_date - - @property - def resolution(self) -> float | None: - return self._resolution - - @property - def structure_method(self) -> str | None: - return self._structure_method - - @property - def bioassembly_data(self) -> bioassemblies.BioassemblyData | None: - return self._bioassembly_data - - @property - def chemical_components_data( - self, - ) -> struct_chem_comps.ChemicalComponentsData | None: - return self._chemical_components_data - - @property - def bonds(self) -> structure_tables.Bonds: - return self._bonds - - @functools.cached_property - def author_naming_scheme(self) -> AuthorNamingScheme: - auth_asym_id = {} - entity_id = {} - entity_desc = {} - auth_seq_id = collections.defaultdict(dict) - insertion_code = collections.defaultdict(dict) - - for chain_i in range(self._chains.size): - chain_id = self._chains.id[chain_i] - auth_asym_id[chain_id] = self._chains.auth_asym_id[chain_i] - chain_entity_id = self._chains.entity_id[chain_i] - entity_id[chain_id] = chain_entity_id - entity_desc[chain_entity_id] = self._chains.entity_desc[chain_i] - - chain_index_by_key = self._chains.index_by_key - for res_i in range(self._residues.size): - chain_key = self._residues.chain_key[res_i] - chain_id = self._chains.id[chain_index_by_key[chain_key]] - res_id = self._residues.id[res_i] - res_auth_seq_id = self._residues.auth_seq_id[res_i] - if res_auth_seq_id == MISSING_AUTH_SEQ_ID: - continue - auth_seq_id[chain_id][res_id] = res_auth_seq_id - ins_code = self._residues.insertion_code[res_i] - # Compatibility with Structure v1 which used None to represent . or ?. - insertion_code[chain_id][res_id] = ( - ins_code if ins_code not in {'.', '?'} else None - ) - - return AuthorNamingScheme( - auth_asym_id=auth_asym_id, - entity_id=entity_id, - entity_desc=entity_desc, - auth_seq_id=dict(auth_seq_id), - insertion_code=dict(insertion_code), - ) - - @functools.cached_property - def all_residues(self) -> AllResidues: - chain_id_by_key = dict(zip(self._chains.key, self._chains.id)) - residue_chain_boundaries = _get_change_indices( - self._residues.chain_key) - boundaries = self._iter_residue_ranges( - residue_chain_boundaries, count_unresolved=True - ) - return { - chain_id_by_key[self._residues.chain_key[start]]: list( - zip(self._residues.name[start:end], - self._residues.id[start:end]) - ) - for start, end in boundaries - } - - @functools.cached_property - def label_asym_id_to_entity_id(self) -> Mapping[str, str]: - return dict(zip(self._chains.id, self._chains.entity_id)) - - @functools.cached_property - def chain_entity_id(self) -> np.ndarray: - """Returns the entity ID for each atom in the structure.""" - return self.chains_table.apply_array_to_column( - 'entity_id', self._atoms.chain_key - ) - - @functools.cached_property - def chain_entity_desc(self) -> np.ndarray: - """Returns the entity description for each atom in the structure.""" - return self.chains_table.apply_array_to_column( - 'entity_desc', self._atoms.chain_key - ) - - @functools.cached_property - def chain_auth_asym_id(self) -> np.ndarray: - """Returns the chain auth asym ID for each atom in the structure.""" - return self.chains_table.apply_array_to_column( - 'auth_asym_id', self._atoms.chain_key - ) - - @functools.cached_property - def chain_id(self) -> np.ndarray: - chain_index_by_key = self._chains.index_by_key - return self._chains.id[chain_index_by_key[self._atoms.chain_key]] - - @functools.cached_property - def chain_type(self) -> np.ndarray: - chain_index_by_key = self._chains.index_by_key - return self._chains.type[chain_index_by_key[self._atoms.chain_key]] - - @functools.cached_property - def res_id(self) -> np.ndarray: - return self._residues['id', self._atoms.res_key] - - @functools.cached_property - def res_name(self) -> np.ndarray: - return self._residues['name', self._atoms.res_key] - - @functools.cached_property - def res_auth_seq_id(self) -> np.ndarray: - """Returns the residue auth seq ID for each atom in the structure.""" - return self.residues_table.apply_array_to_column( - 'auth_seq_id', self._atoms.res_key - ) - - @functools.cached_property - def res_insertion_code(self) -> np.ndarray: - """Returns the residue insertion code for each atom in the structure.""" - return self.residues_table.apply_array_to_column( - 'insertion_code', self._atoms.res_key - ) - - @property - def atom_key(self) -> np.ndarray: - return self._atoms.key - - @property - def atom_name(self) -> np.ndarray: - return self._atoms.name - - @property - def atom_element(self) -> np.ndarray: - return self._atoms.element - - @property - def atom_x(self) -> np.ndarray: - return self._atoms.x - - @property - def atom_y(self) -> np.ndarray: - return self._atoms.y - - @property - def atom_z(self) -> np.ndarray: - return self._atoms.z - - @property - def atom_b_factor(self) -> np.ndarray: - return self._atoms.b_factor - - @property - def atom_occupancy(self) -> np.ndarray: - return self._atoms.occupancy - - @functools.cached_property - def chain_boundaries(self) -> np.ndarray: - """The indices in the atom fields where each chain begins.""" - return _get_change_indices(self._atoms.chain_key) - - @functools.cached_property - def res_boundaries(self) -> np.ndarray: - """The indices in the atom fields where each residue begins.""" - return _get_change_indices(self._atoms.res_key) - - @functools.cached_property - def present_chains(self) -> structure_tables.Chains: - """Returns table of chains which have at least 1 resolved atom.""" - is_present_mask = np.isin(self._chains.key, self._atoms.chain_key) - return typing.cast(structure_tables.Chains, self._chains[is_present_mask]) - - @functools.cached_property - def present_residues(self) -> structure_tables.Residues: - """Returns table of residues which have at least 1 resolved atom.""" - is_present_mask = np.isin(self._residues.key, self._atoms.res_key) - return typing.cast( - structure_tables.Residues, self._residues[is_present_mask] - ) - - @functools.cached_property - def unresolved_residues(self) -> structure_tables.Residues: - """Returns table of residues which have at least 1 resolved atom.""" - is_unresolved_mask = np.isin( - self._residues.key, self._atoms.res_key, invert=True - ) - return typing.cast( - structure_tables.Residues, self._residues[is_unresolved_mask] - ) - - def __getitem__(self, field: str) -> Any: - """Gets raw field data using field name as a string.""" - if field in TABLE_FIELDS: - return self.get_table(field) - else: - return getattr(self, field) - - def __getstate__(self) -> dict[str, Any]: - """Pickle calls this on dump. - - Returns: - Members with cached properties removed. - """ - cached_props = { - k - for k, v in self.__class__.__dict__.items() - if isinstance(v, functools.cached_property) - } - return {k: v for k, v in self.__dict__.items() if k not in cached_props} - - def __repr__(self): - return ( - f'Structure({self._name}: {self.num_chains} chains, ' - f'{self.num_residues(count_unresolved=False)} residues, ' - f'{self.num_atoms} atoms)' - ) - - @property - def num_atoms(self) -> int: - return self._atoms.size - - def num_residues(self, *, count_unresolved: bool) -> int: - """Returns the number of residues in this Structure. - - Args: - count_unresolved: Whether to include unresolved (empty) residues. - - Returns: - Number of residues in the Structure. - """ - if count_unresolved: - return self._residues.size - else: - return self.present_residues.size - - @property - def num_chains(self) -> int: - return self._chains.size - - @property - def num_models(self) -> int: - """The number of models of this Structure.""" - return self._atoms.num_models - - def _atom_mask(self, entities: Set[str]) -> np.ndarray: - """Boolean label indicating if each atom is from entities or not.""" - mask = np.zeros(self.num_atoms, dtype=bool) - chain_index_by_key = self._chains.index_by_key - for start, end in self.iter_chain_ranges(): - chain_index = chain_index_by_key[self._atoms.chain_key[start]] - chain_type = self._chains.type[chain_index] - mask[start:end] = chain_type in entities - return mask - - @functools.cached_property - def is_protein_mask(self) -> np.ndarray: - """Boolean label indicating if each atom is from protein or not.""" - return self._atom_mask(entities={mmcif_names.PROTEIN_CHAIN}) - - @functools.cached_property - def is_dna_mask(self) -> np.ndarray: - """Boolean label indicating if each atom is from DNA or not.""" - return self._atom_mask(entities={mmcif_names.DNA_CHAIN}) - - @functools.cached_property - def is_rna_mask(self) -> np.ndarray: - """Boolean label indicating if each atom is from RNA or not.""" - return self._atom_mask(entities={mmcif_names.RNA_CHAIN}) - - @functools.cached_property - def is_nucleic_mask(self) -> np.ndarray: - """Boolean label indicating if each atom is a nucleic acid or not.""" - return self._atom_mask(entities=mmcif_names.NUCLEIC_ACID_CHAIN_TYPES) - - @functools.cached_property - def is_ligand_mask(self) -> np.ndarray: - """Boolean label indicating if each atom is a ligand or not.""" - return self._atom_mask(entities=mmcif_names.LIGAND_CHAIN_TYPES) - - @functools.cached_property - def is_water_mask(self) -> np.ndarray: - """Boolean label indicating if each atom is from water or not.""" - return self._atom_mask(entities={mmcif_names.WATER}) - - def iter_atoms(self) -> Iterator[Mapping[str, Any]]: - """Iterates over the atoms in the structure.""" - if self._atoms.size == 0: - return - - current_chain = self._chains.get_row_by_key( - column_name_map=CHAIN_FIELDS, key=self._atoms.chain_key[0] - ) - current_chain_key = self._atoms.chain_key[0] - current_res = self._residues.get_row_by_key( - column_name_map=RESIDUE_FIELDS, key=self._atoms.res_key[0] - ) - current_res_key = self._atoms.res_key[0] - for atom_i in range(self._atoms.size): - atom_chain_key = self._atoms.chain_key[atom_i] - atom_res_key = self._atoms.res_key[atom_i] - - if atom_chain_key != current_chain_key: - chain_index = self._chains.index_by_key[atom_chain_key] - current_chain = { - 'chain_id': self._chains.id[chain_index], - 'chain_type': self._chains.type[chain_index], - 'chain_auth_asym_id': self._chains.auth_asym_id[chain_index], - 'chain_entity_id': self._chains.entity_id[chain_index], - 'chain_entity_desc': self._chains.entity_desc[chain_index], - } - current_chain_key = atom_chain_key - if atom_res_key != current_res_key: - res_index = self._residues.index_by_key[atom_res_key] - current_res = { - 'res_id': self._residues.id[res_index], - 'res_name': self._residues.name[res_index], - 'res_auth_seq_id': self._residues.auth_seq_id[res_index], - 'res_insertion_code': self._residues.insertion_code[res_index], - } - current_res_key = atom_res_key - - yield { - 'atom_name': self._atoms.name[atom_i], - 'atom_element': self._atoms.element[atom_i], - 'atom_x': self._atoms.x[..., atom_i], - 'atom_y': self._atoms.y[..., atom_i], - 'atom_z': self._atoms.z[..., atom_i], - 'atom_b_factor': self._atoms.b_factor[..., atom_i], - 'atom_occupancy': self._atoms.occupancy[..., atom_i], - 'atom_key': self._atoms.key[atom_i], - **current_res, - **current_chain, - } - - def iter_residues( - self, - include_unresolved: bool = False, - ) -> Iterator[Mapping[str, Any]]: - """Iterates over the residues in the structure.""" - res_table = self._residues if include_unresolved else self.present_residues - if res_table.size == 0: - return - - current_chain = self._chains.get_row_by_key( - column_name_map=CHAIN_FIELDS, key=res_table.chain_key[0] - ) - current_chain_key = res_table.chain_key[0] - for res_i in range(res_table.size): - res_chain_key = res_table.chain_key[res_i] - - if res_chain_key != current_chain_key: - current_chain = self._chains.get_row_by_key( - column_name_map=CHAIN_FIELDS, key=res_table.chain_key[res_i] - ) - current_chain_key = res_chain_key - - row = { - 'res_id': res_table.id[res_i], - 'res_name': res_table.name[res_i], - 'res_auth_seq_id': res_table.auth_seq_id[res_i], - 'res_insertion_code': res_table.insertion_code[res_i], - } - yield row | current_chain - - def _iter_atom_ranges( - self, boundaries: Sequence[int] - ) -> Iterator[tuple[int, int]]: - """Iterator for (start, end) pairs from an array of start indices.""" - yield from itertools.pairwise(boundaries) - # Use explicit length test as boundaries can be a NumPy array. - if len(boundaries) > 0: # pylint: disable=g-explicit-length-test - yield boundaries[-1], self.num_atoms - - def _iter_residue_ranges( - self, - boundaries: Sequence[int], - *, - count_unresolved: bool, - ) -> Iterator[tuple[int, int]]: - """Iterator for (start, end) pairs from an array of start indices.""" - yield from itertools.pairwise(boundaries) - # Use explicit length test as boundaries can be a NumPy array. - if len(boundaries) > 0: # pylint: disable=g-explicit-length-test - yield boundaries[-1], self.num_residues(count_unresolved=count_unresolved) - - def iter_chain_ranges(self) -> Iterator[tuple[int, int]]: - """Iterates pairs of (chain_start, chain_end) indices. - - Yields: - Pairs of (start, end) indices for each chain, where end is not inclusive. - i.e. struct.chain_id[start:end] would be a constant array with length - equal to the number of atoms in the chain. - """ - yield from self._iter_atom_ranges(self.chain_boundaries) - - def iter_residue_ranges(self) -> Iterator[tuple[int, int]]: - """Iterates pairs of (residue_start, residue_end) indices. - - Yields: - Pairs of (start, end) indices for each residue, where end is not - inclusive. i.e. struct.res_id[start:end] would be a constant array with - length equal to the number of atoms in the residue. - """ - yield from self._iter_atom_ranges(self.res_boundaries) - - def iter_chains(self) -> Iterator[Mapping[str, Any]]: - """Iterates over the chains in the structure.""" - for chain_i in range(self.present_chains.size): - yield { - 'chain_id': self.present_chains.id[chain_i], - 'chain_type': self.present_chains.type[chain_i], - 'chain_auth_asym_id': self.present_chains.auth_asym_id[chain_i], - 'chain_entity_id': self.present_chains.entity_id[chain_i], - 'chain_entity_desc': self.present_chains.entity_desc[chain_i], - } - - def iter_bonds(self) -> Iterator[Bond]: - """Iterates over the atoms and bond information. - - Example usage: - - ``` - for from_atom, dest_atom, bond_info in struct.iter_bonds(): - print( - f'From atom: name={from_atom["atom_name"]}, ' - f'chain={from_atom["chain_id"]}, ...' - ) - # Same for dest_atom - print(f'Bond info: type={bond_info["type"]}, role={bond_info["role"]}') - ``` - - Yields: - A `Bond` NamedTuple for each bond in the bonds table. - These have fields `from_atom`, `dest_atom`, `bond_info` where each - is a dictionary. The first two have the same keys as the atom dicts - returned by self.iter_atoms() -- i.e. one key per non-None field. - The final dict has the same keys as self.bonds.iterrows() -- i.e. one - key per column in the bonds table. - """ - from_atom_iter = self._atoms.iterrows( - row_keys=self._bonds.from_atom_key, - column_name_map=ATOM_FIELDS, - chain_key=self._chains.with_column_names(CHAIN_FIELDS), - res_key=self._residues.with_column_names(RESIDUE_FIELDS), - ) - dest_atom_iter = self._atoms.iterrows( - row_keys=self._bonds.dest_atom_key, - column_name_map=ATOM_FIELDS, - chain_key=self._chains.with_column_names(CHAIN_FIELDS), - res_key=self._residues.with_column_names(RESIDUE_FIELDS), - ) - - for from_atom, dest_atom, bond_info in zip( - from_atom_iter, dest_atom_iter, self._bonds.iterrows(), strict=True - ): - yield Bond(from_atom=from_atom, dest_atom=dest_atom, bond_info=bond_info) - - def _apply_atom_index_array( - self, - index_arr: np.ndarray, - chain_boundaries: np.ndarray | None = None, - res_boundaries: np.ndarray | None = None, - skip_validation: bool = False, - ) -> Self: - """Applies index_arr to the atom table using NumPy-style array indexing. - - Args: - index_arr: A 1D NumPy array that will be used to index into the atoms - table. This can either be a boolean array to act as a mask, or an - integer array to perform a gather operation. - chain_boundaries: Unused in structure v2. - res_boundaries: Unused in structure v2. - skip_validation: Whether to skip the validation step that checks internal - consistency after applying atom index array. Do not set to True unless - you are certain the transform is safe, e.g. when the order of atoms is - guaranteed to not change. - - Returns: - A new Structure with an updated atoms table. - """ - del chain_boundaries, res_boundaries - - if index_arr.ndim != 1: - raise ValueError( - f'index_arr must be a 1D NumPy array, but has shape {index_arr.shape}' - ) - - if index_arr.dtype == bool and np.all(index_arr): - # Shortcut: The operation is a no-op, so just return itself. - return self - - atoms = structure_tables.Atoms( - **{col: self._atoms[col][..., index_arr] for col in self._atoms.columns} - ) - updated_tables = self._cascade_delete(atoms=atoms) - return self.copy_and_update( - atoms=updated_tables.atoms, - bonds=updated_tables.bonds, - skip_validation=skip_validation, - ) - - @property - def group_by_residue(self) -> Self: - """Returns a Structure with one atom per residue. - - e.g. restypes = struct.group_by_residue['res_id'] - - Returns: - A new Structure with one atom per residue such that per-atom arrays - such as res_name (i.e. Structure v1 fields) have one element per residue. - """ - # This use of _apply_atom_index_array is safe because the chain/residue/atom - # ordering won't change (essentially applying a residue start mask). - return self._apply_atom_index_array( - self.res_boundaries, skip_validation=True - ) - - @property - def group_by_chain(self) -> Self: - """Returns a Structure where all fields are per-chain. - - e.g. chains = struct.group_by_chain['chain_id'] - - Returns: - A new Structure with one atom per chain such that per-atom arrays - such as res_name (i.e. Structure v1 fields) have one element per chain. - """ - # This use of _apply_atom_index_array is safe because the chain/residue/atom - # ordering won't change (essentially applying a chain start mask). - return self._apply_atom_index_array( - self.chain_boundaries, skip_validation=True - ) - - @property - def with_sorted_chains(self) -> Self: - """Returns a new structure with the chains are in reverse spreadsheet style. - - This is the usual order to write chains in an mmCIF: - (A < B < ... < AA < BA < CA < ... < AB < BB < CB ...) - - NB: this method will fail if chains do not conform to this mmCIF naming - convention. - - Only to be used for third party metrics that rely on the chain order. - Elsewhere chains should be identified by name and code should be agnostic to - the order. - """ - sorted_chains = sorted(self.chains, key=mmcif.str_id_to_int_id) - return self.reorder_chains(new_order=sorted_chains) - - @functools.cached_property - def atom_ids(self) -> Sequence[tuple[str, str, None, str]]: - """Gets a list of atom ID tuples from Structure class arrays. - - Returns: - A list of tuples of (chain_id, res_id, insertion_code, atom_name) where - insertion code is always None. There is one element per atom, and the - list is ordered according to the order of atoms in the input arrays. - """ - # Convert to Numpy strings, then to Python strings (dtype=object). - res_ids = self.residues_table.id.astype(str).astype(object) - res_ids = res_ids[ - self.residues_table.index_by_key[self.atoms_table.res_key] - ] - ins_codes = [None] * self.num_atoms - return list( - zip(self.chain_id, res_ids, ins_codes, self.atom_name, strict=True) - ) - - def order_and_drop_atoms_to_match( - self, - other: 'Structure', - *, - allow_missing_atoms: bool = False, - ) -> Self: - """Returns a new structure with atoms ordered & dropped to match another's. - - This performs two operations simultaneously: - * Ordering the atoms in this structure to match the order in the other. - * Dropping atoms in this structure that do not appear in the other. - - Example: - Consider a prediction and ground truth with the following atoms, described - using tuples of `(chain_id, res_id, atom_name)`: - * `prediction: [(A, 1, CA), (A, 1, N), (A, 2, CA), (B, 1, CA)]` - * `ground_truth: [(B, 1, CA), (A, 1, N), (A, 1, CA)]` - Note how the ground truth is missing the `(A, 2, CA)` atom and also - has the atoms in a different order. This method returns a modified - prediction that has reordered atoms and without any atoms not in the ground - truth so that its atom list looks the same as the ground truth atom list. - This means `prediction.coords` and `ground_truth.coords` now have the - same shape and can be compared across the atom dimension. - - Note that matching residues with no atoms and matching chains with no - residues will also be kept. E.g. in the example above, if prediction and - ground truth both had an unresolved residue (A, 3), the output structure - will also have an unresolved residue (A, 3). - - Args: - other: Another `Structure`. This provides the reference ordering that is - used to sort this structure's atom arrays. - allow_missing_atoms: Whether to skip atoms present in `other` but not this - structure and return a structure containing a subset of the atoms in the - other structure. - - Returns: - A new `Structure`, based on this structure, which, if - `allow_missing_atoms` is False, contains exactly the same atoms as in - the `other` structure and which matches the `other` structure in terms - of the order of the atoms in the field arrays. Otherwise, if missing - atoms are allowed then the resulting structure contains a subset of - those atoms in the other structure. - - Raises: - MissingAtomError: If there are atoms present in the other structure that - cannot be found in this structure. - """ - atom_index_map = {atom_id: i for i, - atom_id in enumerate(self.atom_ids)} - try: - if allow_missing_atoms: - # Only include atoms that were found in the other structure. - atom_indices = [ - atom_index - for atom_id in other.atom_ids - if (atom_index := atom_index_map.get(atom_id)) is not None - ] - else: - atom_indices = [ - atom_index_map[atom_id] # Hard fail on missing. - for atom_id in other.atom_ids - ] - except KeyError as e: - if len(e.args[0]) == 4: - chain_id, res_id, ins_code, atom_name = e.args[0] - raise MissingAtomError( - f'No atom in this structure (name: {self._name}) matches atom in ' - f'other structure (name: {other.name}) with internal (label) chain ' - f'ID {chain_id}, residue ID {res_id}, insertion code {ins_code} ' - f'and atom name {atom_name}.' - ) from e - else: - raise - - def _iter_residues(struct: Self) -> Iterable[tuple[str, str]]: - yield from zip( - struct.chains_table['id', struct.residues_table.chain_key], - struct.residues_table.id, - strict=True, - ) - - chain_index_map = { - chain_id: i for i, chain_id in enumerate(self._chains.id) - } - chain_indices = [ - chain_index - for chain_id in other.chains_table.id - if (chain_index := chain_index_map.get(chain_id)) is not None - ] - residue_index_map = { - res_id: i for i, res_id in enumerate(_iter_residues(self)) - } - res_indices = [ - residue_index - for res_id in _iter_residues(other) - if (residue_index := residue_index_map.get(res_id)) is not None - ] - - # Reorder all tables. - chains = self._chains.apply_index( - np.array(chain_indices, dtype=np.int64)) - residues = self._residues.apply_index( - np.array(res_indices, dtype=np.int64)) - atoms = self._atoms.apply_index(np.array(atom_indices, dtype=np.int64)) - - # Get chain keys in the order they appear in the atoms table. - new_chain_boundaries = _get_change_indices(atoms.chain_key) - new_chain_key_order = atoms.chain_key[new_chain_boundaries] - if len(new_chain_key_order) != len(set(new_chain_key_order)): - raise ValueError( - f'Chain keys not contiguous after reordering: {new_chain_key_order}' - ) - - # Get residue keys in the order they appear in the atoms table. - new_res_boundaries = _get_change_indices(atoms.res_key) - new_res_key_order = atoms.res_key[new_res_boundaries] - if len(new_res_key_order) != len(set(new_res_key_order)): - raise ValueError( - f'Residue keys not contiguous after reordering: {new_res_key_order}' - ) - - # If any atoms were deleted, propagate that into the bonds table. - updated_tables = self._cascade_delete( - chains=chains, - residues=residues, - atoms=atoms, - ) - return self.copy_and_update( - chains=chains, - residues=residues, - atoms=updated_tables.atoms, - bonds=updated_tables.bonds, - ) - - def copy_and_update( - self, - *, - name: str | Literal[_UNSET] = _UNSET, - release_date: datetime.date | None | Literal[_UNSET] = _UNSET, - resolution: float | None | Literal[_UNSET] = _UNSET, - structure_method: str | None | Literal[_UNSET] = _UNSET, - bioassembly_data: ( - bioassemblies.BioassemblyData | None | Literal[_UNSET] - ) = _UNSET, - chemical_components_data: ( - struct_chem_comps.ChemicalComponentsData | None | Literal[_UNSET] - ) = _UNSET, - chains: structure_tables.Chains | None | Literal[_UNSET] = _UNSET, - residues: structure_tables.Residues | None | Literal[_UNSET] = _UNSET, - atoms: structure_tables.Atoms | None | Literal[_UNSET] = _UNSET, - bonds: structure_tables.Bonds | None | Literal[_UNSET] = _UNSET, - skip_validation: bool = False, - ) -> Self: - """Performs a shallow copy but with specified fields updated.""" - - def all_unset(fields): - return all(field == _UNSET for field in fields) - - if all_unset((chains, residues, atoms, bonds)): - if all_unset(( - name, - release_date, - resolution, - structure_method, - bioassembly_data, - chemical_components_data, - )): - raise ValueError( - 'Unnecessary call to copy_and_update with no changes. As Structure' - ' and its component tables are immutable, there is no need to copy' - ' it. Any subsequent operation that modifies structure will return' - ' a new object.' - ) - else: - raise ValueError( - 'When only changing global fields, prefer to use the specialised ' - 'copy_and_update_globals.' - ) - - def select(field, default): - return field if field != _UNSET else default - - return Structure( - name=select(name, self.name), - release_date=select(release_date, self.release_date), - resolution=select(resolution, self.resolution), - structure_method=select(structure_method, self.structure_method), - bioassembly_data=select(bioassembly_data, self.bioassembly_data), - chemical_components_data=select( - chemical_components_data, self.chemical_components_data - ), - chains=select(chains, self._chains), - residues=select(residues, self._residues), - atoms=select(atoms, self._atoms), - bonds=select(bonds, self._bonds), - skip_validation=skip_validation, - ) - - def _copy_and_update( - self, skip_validation: bool = False, **changes: Any - ) -> Self: - """Performs a shallow copy but with specified fields updated.""" - if not changes: - raise ValueError( - 'Unnecessary call to copy_and_update with no changes. As Structure ' - 'and its component tables are immutable, there is no need to copy ' - 'it. Any subsequent operation that modifies structure will return a ' - 'new object.' - ) - - if 'author_naming_scheme' in changes: - raise ValueError( - 'Updating using author_naming_scheme is not supported. Update ' - 'auth_asym_id, entity_id, entity_desc fields directly in the chains ' - 'table and auth_seq_id, insertion_code in the residues table.' - ) - - if all(k in GLOBAL_FIELDS for k in changes): - raise ValueError( - 'When only changing global fields, prefer to use the specialised ' - 'copy_and_update_globals.' - ) - - if all(k in V2_FIELDS for k in changes): - constructor_kwargs = {field: self[field] for field in V2_FIELDS} - constructor_kwargs.update(changes) - elif any(k in ('atoms', 'residues', 'chains') for k in changes): - raise ValueError( - 'Cannot specify atoms/chains/residues table changes with non-v2' - f' constructor params: {changes.keys()}' - ) - elif all(k in ATOM_FIELDS for k in changes): - if 'atom_key' not in changes: - raise ValueError( - 'When only changing atom fields, prefer to use the specialised ' - 'copy_and_update_atoms.' - ) - # Only atom fields are being updated, do that directly on the atoms table. - updated_atoms = self._atoms.copy_and_update( - **{ATOM_FIELDS[k]: v for k, v in changes.items()} - ) - constructor_kwargs = { - field: self[field] for field in V2_FIELDS if field != 'atoms' - } - constructor_kwargs['atoms'] = updated_atoms - else: - constructor_kwargs = {field: self[field] - for field in _UPDATEABLE_FIELDS} - constructor_kwargs.update(changes) - return Structure(skip_validation=skip_validation, **constructor_kwargs) - - def copy_and_update_coords(self, coords: np.ndarray) -> Self: - """Performs a shallow copy but with coordinates updated.""" - if coords.shape[-2:] != (self.num_atoms, 3): - raise ValueError( - f'{coords.shape=} does not have last dimensions ({self.num_atoms}, 3)' - ) - updated_atoms = self._atoms.copy_and_update_coords(coords) - return self.copy_and_update(atoms=updated_atoms, skip_validation=True) - - def copy_and_update_from_res_arrays(self, **changes: np.ndarray) -> Self: - """Like copy_and_update but changes are arrays of length num_residues. - - These changes are first scattered into arrays of length num_atoms such - that each value is repeated across the residue at that index, then they - are used as the new values of these fields. - - E.g. - * This structure's res_id: 1, 1, 1, 2, 3, 3 (3 res, 6 atoms) - * new atom_b_factor: 7, 8, 9 - * Returned structure's atom_b_factor: 7, 7, 7, 8, 9, 9 - - Args: - **changes: kwargs corresponding to atom array fields, e.g. atom_x or - atom_b_factor, but with length num_residues rather than num_atoms. Note - that changing atom_key this way is is not supported. - - Returns: - A new `Structure` with all fields other than those specified as kwargs - shallow copied from this structure. The values of the kwargs are - scattered across the atom arrays and then used to overwrite these - fields for the returned structure. - """ - # We create scatter indices by (1) starting from zeros, then (2) setting - # the position where each residue starts to 1 and then (3) doing a - # cumulative sum. Finally, since self.res_boundaries always starts with 0 - # the result of the cumulative sum will start from 1, so (4) we subtract - # 1 to get the final array of zero-based indices. - # Example, 6 atoms, 3 residues at indices 0, 2 and 5. - # (1) 0 0 0 0 0 0 - # (2) 1 0 1 0 0 1 - # (3) 1 1 2 2 2 3 - # (4) 0 0 1 1 1 2 - if not all(c in set(ATOM_FIELDS) - {'atom_key'} for c in changes): - raise ValueError( - 'Changes must only be to atom fields, got changes to' - f' {changes.keys()}' - ) - scatter_idxs = np.zeros((self.num_atoms,), dtype=int) - scatter_idxs[self.res_boundaries] = 1 - scatter_idxs = scatter_idxs.cumsum() - 1 - atom_array_changes = { - ATOM_FIELDS[field]: new_val[scatter_idxs] - for field, new_val in changes.items() - } - updated_atoms = self._atoms.copy_and_update(**atom_array_changes) - return self.copy_and_update(atoms=updated_atoms, skip_validation=True) - - def copy_and_update_globals( - self, - *, - name: str | Literal[_UNSET] = _UNSET, - release_date: datetime.date | Literal[_UNSET] | None = _UNSET, - resolution: float | Literal[_UNSET] | None = _UNSET, - structure_method: str | Literal[_UNSET] | None = _UNSET, - bioassembly_data: ( - bioassemblies.BioassemblyData | Literal[_UNSET] | None - ) = _UNSET, - chemical_components_data: ( - struct_chem_comps.ChemicalComponentsData | Literal[_UNSET] | None - ) = _UNSET, - ) -> Self: - """Returns a shallow copy with the global columns updated.""" - - def select(field, default): - return field if field != _UNSET else default - - name = select(name, self.name) - release_date = select(release_date, self.release_date) - resolution = select(resolution, self.resolution) - structure_method = select(structure_method, self.structure_method) - bioassembly_data = select(bioassembly_data, self.bioassembly_data) - chem_data = select(chemical_components_data, - self.chemical_components_data) - - return Structure( - name=name, - release_date=release_date, - resolution=resolution, - structure_method=structure_method, - bioassembly_data=bioassembly_data, - chemical_components_data=chem_data, - atoms=self._atoms, - residues=self._residues, - chains=self._chains, - bonds=self._bonds, - ) - - def copy_and_update_atoms( - self, - *, - atom_name: np.ndarray | None = None, - atom_element: np.ndarray | None = None, - atom_x: np.ndarray | None = None, - atom_y: np.ndarray | None = None, - atom_z: np.ndarray | None = None, - atom_b_factor: np.ndarray | None = None, - atom_occupancy: np.ndarray | None = None, - ) -> Self: - """Returns a shallow copy with the atoms table updated.""" - new_atoms = structure_tables.Atoms( - key=self._atoms.key, - res_key=self._atoms.res_key, - chain_key=self._atoms.chain_key, - name=atom_name if atom_name is not None else self.atom_name, - element=atom_element if atom_element is not None else self.atom_element, - x=atom_x if atom_x is not None else self.atom_x, - y=atom_y if atom_y is not None else self.atom_y, - z=atom_z if atom_z is not None else self.atom_z, - b_factor=( - atom_b_factor if atom_b_factor is not None else self.atom_b_factor - ), - occupancy=( - atom_occupancy - if atom_occupancy is not None - else self.atom_occupancy - ), - ) - return self.copy_and_update(atoms=new_atoms) - - def _cascade_delete( - self, - *, - chains: structure_tables.Chains | None = None, - residues: structure_tables.Residues | None = None, - atoms: structure_tables.Atoms | None = None, - bonds: structure_tables.Bonds | None = None, - ) -> StructureTables: - """Performs a cascade delete operation on the structure's tables. - - Cascade delete ensures all the tables are consistent after any table fields - are being updated by cascading any deletions down the hierarchy of tables: - chains > residues > atoms > bonds. - - E.g.: if a row from residues table is removed then all the atoms in that - residue will also be removed from the atoms table. In turn this cascades - also to the bond table, by removing any bond row which involves any of those - removed atoms. However the chains table will not be modified, even if - that was the only residue in its chain, because the chains table is above - the residues table in the hierarchy. - - Args: - chains: An optional new chains table. - residues: An optional new residues table. - atoms: An optional new atoms table. - bonds: An optional new bonds table. - - Returns: - A StructureTables object with the updated tables. - """ - if chains_unchanged := chains is None: - chains = self._chains - if residues_unchanged := residues is None: - residues = self._residues - if atoms_unchanged := atoms is None: - atoms = self._atoms - if bonds is None: - bonds = self._bonds - - if not chains_unchanged: - residues_mask = membership.isin(residues.chain_key, set( - chains.key)) # pylint:disable=attribute-error - if not np.all(residues_mask): # Only apply if this is not a no-op. - residues = residues[residues_mask] - residues_unchanged = False - if not residues_unchanged: - atoms_mask = membership.isin(atoms.res_key, set( - residues.key)) # pylint:disable=attribute-error - if not np.all(atoms_mask): # Only apply if this is not a no-op. - atoms = atoms[atoms_mask] - atoms_unchanged = False - if not atoms_unchanged: - bonds = bonds.restrict_to_atoms(atoms.key) - return StructureTables( - chains=chains, residues=residues, atoms=atoms, bonds=bonds - ) - - def filter( - self, - mask: np.ndarray | None = None, - *, - apply_per_element: bool = False, - invert: bool = False, - cascade_delete: CascadeDelete = CascadeDelete.CHAINS, - **predicate_by_field_name: table.FilterPredicate, - ) -> Self: - """Filters the structure by field values and returns a new structure. - - Predicates are specified as keyword arguments, with names following the - pattern: _, where table_name := (chain|res|atom). - For instance the auth_seq_id column in the residues table can be filtered - by passing `res_auth_seq_id=pred_value`. The full list of valid options - are defined in the `col_by_field_name` fields on the different Table - dataclasses. - - Predicate values can be either: - 1. A constant value, e.g. 'CA'. In this case then only rows that match - this value for the given field are retained. - 2. A (non-string) iterable e.g. ('A', 'B'). In this - case then rows are retained if they match any of the provided values for - the given field. - 3. A boolean function e.g. lambda b_fac: b_fac < 100.0. - In this case then only rows that evaluate to True are retained. By - default this function's parameter is expected to be an array, unless - apply_per_element=True. - - Example usage: - # Filter to backbone atoms in residues up to 100 in chain B. - filtered_struct = struct.filter( - chain_id='B', - atom_name=('N', 'CA', 'C'), - res_id=lambda res_id: res_id < 100) - - Example usage where predicate must be applied per-element: - # Filter to residues with IDs in either [1, 100) or [300, 400). - ranges = ((1, 100), (300, 400)) - filtered_struct = struct.filter( - res_id=lambda i: np.any([start <= i < end for start, end in ranges]), - apply_per_element=True) - - Example usage of providing a raw mask: - filtered_struct = struct.filter(struct.atom_b_factor < 10.0) - - Args: - mask: An optional boolean NumPy array with length equal to num_atoms. If - provided then this will be combined with the other predicates so that an - atom is included if it is masked-in *and* matches all the predicates. - apply_per_element: Whether apply predicates to each element individually, - or to pass the whole column array to the predicate. - invert: Whether to remove, rather than retain, the entities which match - the specified predicates. - cascade_delete: Whether to remove residues and chains which are left - unresolved in a cascade. filter operates on the atoms table, removing - atoms which match the predicate. If all atoms in a residue are removed, - the residue is "unresolved". The value of this argument then determines - whether such residues and their parent chains should be deleted. FULL - implies that all unresolved residues should be deleted, and any chains - which are left with no resolved residues should be deleted. CHAINS is - the default behaviour - only chains with no resolved residues, and their - child residues are deleted. Unresolved residues in partially resolved - chains remain. NONE implies that no unresolved residues or chains should - be deleted. - **predicate_by_field_name: A mapping from field name to a predicate. - Filtered columns must be 1D arrays. If multiple fields are provided as - keyword arguments then each predicate is applied and the results are - combined using a boolean AND operation, so an atom is only retained if - it passes all predicates. - - Returns: - A new structure representing a filtered version of the current structure. - - Raises: - ValueError: If mask is provided and is not a bool array with shape - (num_atoms,). - """ - chain_predicates, res_predicates, atom_predicates = ( - _unpack_filter_predicates(predicate_by_field_name) - ) - # Get boolean masks for each table. These are None if none of the filter - # parameters affect the table in question. - chain_mask = self._chains.make_filter_mask( - **chain_predicates, apply_per_element=apply_per_element - ) - res_mask = self._residues.make_filter_mask( - **res_predicates, apply_per_element=apply_per_element - ) - atom_mask = self._atoms.make_filter_mask( - mask, **atom_predicates, apply_per_element=apply_per_element - ) - if atom_mask is None: - atom_mask = np.ones((self._atoms.size,), dtype=bool) - - # Remove atoms that belong to filtered out chains. - if chain_mask is not None: - atom_chain_mask = membership.isin( - self._atoms.chain_key, set(self._chains.key[chain_mask]) - ) - np.logical_and(atom_mask, atom_chain_mask, out=atom_mask) - - # Remove atoms that belong to filtered out residues. - if res_mask is not None: - atom_res_mask = membership.isin( - self._atoms.res_key, set(self._residues.key[res_mask]) - ) - np.logical_and(atom_mask, atom_res_mask, out=atom_mask) - - final_atom_mask = ~atom_mask if invert else atom_mask - - if cascade_delete == CascadeDelete.NONE and np.all(final_atom_mask): - # Shortcut: The filter is a no-op, so just return itself. - return self - - filtered_atoms = typing.cast( - structure_tables.Atoms, self._atoms[final_atom_mask] - ) - - match cascade_delete: - case CascadeDelete.FULL: - nonempty_residues_mask = np.isin( - self._residues.key, filtered_atoms.res_key - ) - filtered_residues = self._residues[nonempty_residues_mask] - nonempty_chain_mask = np.isin( - self._chains.key, filtered_atoms.chain_key - ) - filtered_chains = self._chains[nonempty_chain_mask] - updated_tables = self._cascade_delete( - chains=filtered_chains, - residues=filtered_residues, - atoms=filtered_atoms, - ) - case CascadeDelete.CHAINS: - # To match v1 behavior we remove chains that have no atoms remaining, - # and we remove residues in those chains. - # NB we do not remove empty residues. - nonempty_chain_mask = membership.isin( - self._chains.key, set(filtered_atoms.chain_key) - ) - filtered_chains = self._chains[nonempty_chain_mask] - updated_tables = self._cascade_delete( - chains=filtered_chains, atoms=filtered_atoms - ) - case CascadeDelete.NONE: - updated_tables = self._cascade_delete(atoms=filtered_atoms) - case _: - raise ValueError( - f'Unknown cascade_delete behaviour: {cascade_delete}') - return self.copy_and_update( - chains=updated_tables.chains, - residues=updated_tables.residues, - atoms=updated_tables.atoms, - bonds=updated_tables.bonds, - skip_validation=True, - ) - - def filter_out(self, *args, **kwargs) -> Self: - """Returns a new structure with the specified elements removed.""" - return self.filter(*args, invert=True, **kwargs) - - def filter_to_entity_type( - self, - *, - protein: bool = False, - rna: bool = False, - dna: bool = False, - dna_rna_hybrid: bool = False, - ligand: bool = False, - water: bool = False, - ) -> Self: - """Filters the structure to only include the selected entity types. - - This convenience method abstracts away the specifics of mmCIF entity - type names which, especially for ligands, are non-trivial. - - Args: - protein: Whether to include protein (polypeptide(L)) chains. - rna: Whether to include RNA chains. - dna: Whether to include DNA chains. - dna_rna_hybrid: Whether to include DNA RNA hybrid chains. - ligand: Whether to include ligand (i.e. not polymer) chains. - water: Whether to include water chains. - - Returns: - The filtered structure. - """ - include_types = [] - if protein: - include_types.append(mmcif_names.PROTEIN_CHAIN) - if rna: - include_types.append(mmcif_names.RNA_CHAIN) - if dna: - include_types.append(mmcif_names.DNA_CHAIN) - if dna_rna_hybrid: - include_types.append(mmcif_names.DNA_RNA_HYBRID_CHAIN) - if ligand: - include_types.extend(mmcif_names.LIGAND_CHAIN_TYPES) - if water: - include_types.append(mmcif_names.WATER) - return self.filter(chain_type=include_types) - - def get_stoichiometry( - self, *, fix_non_standard_polymer_res: bool = False - ) -> Sequence[int]: - """Returns the structure's stoichiometry using chain_res_name_sequence. - - Note that everything is considered (protein, RNA, DNA, ligands) except for - water molecules. If you are interested only in a certain type of entities, - filter them out before calling this method. - - Args: - fix_non_standard_polymer_res: If True, maps non standard residues in - protein / RNA / DNA chains to standard residues (e.g. MSE -> MET) or UNK - / N if a match is not found. - - Returns: - A list of integers, one for each unique chain in the structure, - determining the number of that chain appearing in the structure. The - numbers are sorted highest to lowest. E.g. for an A3B2 protein this method - will return [3, 2]. - """ - filtered = self.filter_to_entity_type( - protein=True, - rna=True, - dna=True, - dna_rna_hybrid=True, - ligand=True, - water=False, - ) - seqs = filtered.chain_res_name_sequence( - include_missing_residues=True, - fix_non_standard_polymer_res=fix_non_standard_polymer_res, - ) - - unique_seq_counts = collections.Counter(seqs.values()) - return sorted(unique_seq_counts.values(), reverse=True) - - def without_hydrogen(self) -> Self: - """Returns the structure without hydrogen atoms.""" - return self.filter( - np.logical_and(self._atoms.element != 'H', - self._atoms.element != 'D') - ) - - def without_terminal_oxygens(self) -> Self: - """Returns the structure without terminal oxygen atoms.""" - terminal_oxygen_filter = np.zeros(self.num_atoms, dtype=bool) - for chain_type, atom_name in mmcif_names.TERMINAL_OXYGENS.items(): - chain_keys = self._chains.key[self._chains.type == chain_type] - chain_atom_filter = np.logical_and( - self._atoms.name == atom_name, - np.isin(self._atoms.chain_key, chain_keys), - ) - np.logical_or( - terminal_oxygen_filter, chain_atom_filter, out=terminal_oxygen_filter - ) - return self.filter_out(terminal_oxygen_filter) - - def reset_author_naming_scheme(self) -> Self: - """Remove author chain/residue ids, entity info and use internal ids.""" - new_chains = structure_tables.Chains( - key=self._chains.key, - id=self._chains.id, - type=self._chains.type, - auth_asym_id=self._chains.id, - entity_id=np.arange(1, self.num_chains + - 1).astype(str).astype(object), - entity_desc=np.full(self.num_chains, '.', dtype=object), - ) - new_residues = structure_tables.Residues( - key=self._residues.key, - chain_key=self._residues.chain_key, - id=self._residues.id, - name=self._residues.name, - auth_seq_id=self._residues.id.astype(str).astype(object), - insertion_code=np.full( - self.num_residues(count_unresolved=True), '?', dtype=object - ), - ) - return self.copy_and_update( - chains=new_chains, residues=new_residues, skip_validation=True - ) - - def filter_residues(self, res_mask: np.ndarray) -> Self: - """Filter resolved residues using a boolean mask.""" - required_shape = (self.num_residues(count_unresolved=False),) - if res_mask.shape != required_shape: - raise ValueError( - f'res_mask must have shape {required_shape}. Got: {res_mask.shape}.' - ) - if res_mask.dtype != bool: - raise ValueError( - f'res_mask must have dtype bool. Got: {res_mask.dtype}.') - - filtered_residues = self.present_residues.filter(res_mask) - atom_mask = np.isin(self._atoms.res_key, filtered_residues.key) - return self.filter(atom_mask) - - def filter_coords( - self, coord_predicate: Callable[[np.ndarray], bool] - ) -> Self: - """Filter a structure's atoms by a function of their coordinates. - - Args: - coord_predicate: A boolean function of coordinate vectors (shape (3,)). - - Returns: - A Structure filtered so that only atoms with coords passing the predicate - function are present. - - Raises: - ValueError: If the coords are not shaped (num_atom, 3). - """ - coords = self.coords - if coords.ndim != 2 or coords.shape[-1] != 3: - raise ValueError( - f'coords should have shape (num_atom, 3). Got {coords.shape}.' - ) - mask = np.vectorize(coord_predicate, signature='(n)->()')(coords) - # This use of _apply_atom_index_array is safe because a boolean mask is - # used, which means the chain/residue/atom ordering will stay unchanged. - return self._apply_atom_index_array(mask, skip_validation=True) - - def filter_polymers_to_single_atom_per_res( - self, - representative_atom_by_chain_type: Mapping[ - str, str - ] = mmcif_names.RESIDUE_REPRESENTATIVE_ATOMS, - ) -> Self: - """Filter to one representative atom per polymer residue, ligands unchanged. - - Args: - representative_atom_by_chain_type: Chain type str to atom name, only atoms - with this name will be kept for this chain type. Chains types from the - structure not found in this mapping will keep all their atoms. - - Returns: - A Structure filtered so that per chain types, only specified atoms are - present. - """ - polymer_chain_keys = self._chains.key[ - string_array.isin( - self._chains.type, set(representative_atom_by_chain_type) - ) - ] - polymer_atoms_mask = np.isin(self._atoms.chain_key, polymer_chain_keys) - - wanted_atom_by_chain_key = { - chain_key: representative_atom_by_chain_type.get(chain_type, None) - for chain_key, chain_type in zip(self._chains.key, self._chains.type) - } - wanted_atoms = string_array.remap( - self._atoms.chain_key.astype(object), mapping=wanted_atom_by_chain_key - ) - - representative_polymer_atoms_mask = polymer_atoms_mask & ( - wanted_atoms == self._atoms.name - ) - - return self.filter(representative_polymer_atoms_mask | ~polymer_atoms_mask) - - def drop_non_standard_protein_atoms(self, *, drop_oxt: bool = True) -> Self: - """Drops non-standard atom names from protein chains. - - Args: - drop_oxt: If True, also drop terminal oxygens (OXT). - - Returns: - A new Structure object where the protein chains have been filtered to - only contain atoms with names listed in `atom_types` - (including OXT unless `drop_oxt` is `True`). Non-protein chains are - unaltered. - """ - allowed_names = set(atom_types.ATOM37) - if drop_oxt: - allowed_names = {n for n in allowed_names if n != atom_types.OXT} - - return self.filter_out( - chain_type=mmcif_names.PROTEIN_CHAIN, - atom_name=lambda n: string_array.isin( - n, allowed_names, invert=True), - ) - - def drop_non_standard_atoms( - self, - *, - ccd: chemical_components.Ccd, - drop_unk: bool, - drop_non_ccd: bool, - drop_terminal_oxygens: bool = False, - ) -> Self: - """Drops atoms that are not in the CCD for the given residue type.""" - - # We don't remove any atoms in UNL, as it has no standard atoms. - def _keep(atom_index: int) -> bool: - atom_name = self._atoms.name[atom_index] - res_name = self._residues.name[ - self._residues.index_by_key[self._atoms.res_key[atom_index]] - ] - if drop_unk and res_name in residue_names.UNKNOWN_TYPES: - return False - else: - return ( - (not drop_non_ccd and not ccd.get(res_name)) - or atom_name in struct_chem_comps.get_res_atom_names(ccd, res_name) - or res_name == residue_names.UNL - ) - - standard_atom_mask = np.array( - [_keep(atom_i) for atom_i in range(self.num_atoms)], dtype=bool - ) - standard_atoms = self.filter(mask=standard_atom_mask) - if drop_terminal_oxygens: - standard_atoms = standard_atoms.without_terminal_oxygens() - return standard_atoms - - def find_chains_with_unknown_sequence(self) -> Sequence[str]: - """Returns a sequence of chain IDs that contain only unknown residues.""" - unknown_sequences = [] - for start, end in self.iter_chain_ranges(): - try: - unknown_id = residue_names.UNKNOWN_TYPES.index( - self.res_name[start]) - if start + 1 == end or np.all( - self.res_name[start + 1: end] - == residue_names.UNKNOWN_TYPES[unknown_id] - ): - unknown_sequences.append(self.chain_id[start]) - except ValueError: - pass - return unknown_sequences - - def add_bonds( - self, - bonded_atom_pairs: Sequence[ - tuple[tuple[str, int, str], tuple[str, int, str]], - ], - bond_type: str | None = None, - ) -> Self: - """Returns a structure with new bonds added. - - Args: - bonded_atom_pairs: A sequence of pairs of atoms, with one pair per bond. - Each element of the pair is a tuple of (chain_id, res_id, atom_name), - matching values from the respective fields of this structure. The first - element is the start atom, and the second atom is the end atom of the - bond. - bond_type: This type will be used for all bonds in the structure, where - type follows PDB scheme, e.g. unknown (?), hydrog, metalc, covale, - disulf. - - Returns: - A copy of this structure with the new bonds added. If this structure has - bonds already then the new bonds are concatenated onto the end of the - old bonds. NB: bonds are not deduplicated. - """ - atom_key_lookup: dict[tuple[str, str, None, str], int] = dict( - zip(self.atom_ids, self._atoms.key, strict=True) - ) - - # iter_atoms returns a 4-tuple (chain_id, res_id, ins_code, atom_name) but - # the insertion code is always None. It also uses string residue IDs. - def _to_internal_res_id( - bonded_atom_id: tuple[str, int, str], - ) -> tuple[str, str, None, str]: - return bonded_atom_id[0], str(bonded_atom_id[1]), None, bonded_atom_id[2] - - from_atom_key = [] - dest_atom_key = [] - for from_atom, dest_atom in bonded_atom_pairs: - from_atom_key.append( - atom_key_lookup[_to_internal_res_id(from_atom)]) - dest_atom_key.append( - atom_key_lookup[_to_internal_res_id(dest_atom)]) - num_bonds = len(bonded_atom_pairs) - bonds_key = np.arange(num_bonds, dtype=np.int64) - from_atom_key = np.array(from_atom_key, dtype=np.int64) - dest_atom_key = np.array(dest_atom_key, dtype=np.int64) - all_unk_col = np.array(['?'] * num_bonds, dtype=object) - if bond_type is None: - bond_type_col = all_unk_col - else: - bond_type_col = np.full((num_bonds,), bond_type, dtype=object) - - max_key = -1 if not self._bonds.size else np.max(self._bonds.key) - new_bonds = structure_tables.Bonds( - key=np.concatenate([self._bonds.key, bonds_key + max_key + 1]), - from_atom_key=np.concatenate( - [self._bonds.from_atom_key, from_atom_key] - ), - dest_atom_key=np.concatenate( - [self._bonds.dest_atom_key, dest_atom_key] - ), - type=np.concatenate([self._bonds.type, bond_type_col]), - role=np.concatenate([self._bonds.role, all_unk_col]), - ) - return self.copy_and_update(bonds=new_bonds) - - @property - def coords(self) -> np.ndarray: - """A [..., num_atom, 3] shaped array of atom coordinates.""" - return np.stack([self._atoms.x, self._atoms.y, self._atoms.z], axis=-1) - - def chain_single_letter_sequence( - self, include_missing_residues: bool = True - ) -> Mapping[str, str]: - """Returns a mapping from chain ID to a single letter residue sequence. - - Args: - include_missing_residues: Whether to include residues that have no atoms. - """ - res_table = ( - self._residues if include_missing_residues else self.present_residues - ) - residue_chain_boundaries = _get_change_indices(res_table.chain_key) - boundaries = self._iter_residue_ranges( - residue_chain_boundaries, - count_unresolved=include_missing_residues, - ) - chain_keys = res_table.chain_key[residue_chain_boundaries] - chain_ids = self._chains.apply_array_to_column('id', chain_keys) - chain_types = self._chains.apply_array_to_column('type', chain_keys) - chain_seqs = {} - for idx, (start, end) in enumerate(boundaries): - chain_id = chain_ids[idx] - chain_type = chain_types[idx] - chain_res = res_table.name[start:end] - if chain_type in mmcif_names.PEPTIDE_CHAIN_TYPES: - unknown_default = 'X' - elif chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES: - unknown_default = 'N' - else: - chain_seqs[chain_id] = 'X' * chain_res.size - continue - - chain_res = string_array.remap( - chain_res, - mapping=residue_names.CCD_NAME_TO_ONE_LETTER, - inplace=False, - default_value=unknown_default, - ) - chain_seqs[chain_id] = ''.join(chain_res.tolist()) - - return chain_seqs - - def polymer_auth_asym_id_to_label_asym_id( - self, - *, - protein: bool = True, - rna: bool = True, - dna: bool = True, - other: bool = True, - ) -> Mapping[str, str]: - """Mapping from author chain ID to internal chain ID, polymers only. - - This mapping is well defined only for polymers (protein, DNA, RNA), but not - for ligands or water. - - E.g. if a structure had the following internal chain IDs (label_asym_id): - A (protein), B (DNA), C (ligand bound to A), D (ligand bound to A), - E (ligand bound to B). - - Such structure would have this internal chain ID (label_asym_id) -> author - chain ID (auth_asym_id) mapping: - A -> A, B -> B, C -> A, D -> A, E -> B - - This is a bijection only for polymers (A, B), but not for ligands. - - Args: - protein: Whether to include protein (polypeptide(L)) chains. - rna: Whether to include RNA chains. - dna: Whether to include DNA chains. - other: Whether to include other polymer chains, e.g. RNA/DNA hybrid or - polypeptide(D). Note that include_other=True must be set in from_mmcif. - - Returns: - A mapping from author chain ID to the internal (label) chain ID for the - given polymer types in the Structure, ligands/water are ignored. - - Raises: - ValueError: If the mapping from internal chain IDs to author chain IDs is - not a bijection for polymer chains. - """ - allowed_types = set() - if protein: - allowed_types.add(mmcif_names.PROTEIN_CHAIN) - if rna: - allowed_types.add(mmcif_names.RNA_CHAIN) - if dna: - allowed_types.add(mmcif_names.DNA_CHAIN) - if other: - non_standard_chain_types = ( - mmcif_names.POLYMER_CHAIN_TYPES - - mmcif_names.STANDARD_POLYMER_CHAIN_TYPES - ) - allowed_types |= non_standard_chain_types - - auth_asym_id_to_label_asym_id = {} - for chain in self.iter_chains(): - if chain['chain_type'] not in allowed_types: - continue - label_asym_id = chain['chain_id'] - auth_asym_id = chain['chain_auth_asym_id'] - # The mapping from author chain id to label chain id is only one-to-one if - # we restrict our attention to polymers. But check nevertheless. - if auth_asym_id in auth_asym_id_to_label_asym_id: - raise ValueError( - f'Author chain ID "{auth_asym_id}" does not have a unique mapping ' - f'to internal chain ID "{label_asym_id}", it is already mapped to ' - f'"{auth_asym_id_to_label_asym_id[auth_asym_id]}".' - ) - auth_asym_id_to_label_asym_id[auth_asym_id] = label_asym_id - - return auth_asym_id_to_label_asym_id - - def polymer_author_chain_single_letter_sequence( - self, - *, - include_missing_residues: bool = True, - protein: bool = True, - rna: bool = True, - dna: bool = True, - other: bool = True, - ) -> Mapping[str, str]: - """Mapping from author chain ID to single letter aa sequence, polymers only. - - This mapping is well defined only for polymers (protein, DNA, RNA), but not - for ligands or water. - - Args: - include_missing_residues: If True then all residues will be returned for - each polymer chain present in the structure. This uses the all_residues - field and will include residues missing due to filtering operations as - well as e.g. unresolved residues specified in an mmCIF header. - protein: Whether to include protein (polypeptide(L)) chains. - rna: Whether to include RNA chains. - dna: Whether to include DNA chains. - other: Whether to include other polymer chains, e.g. RNA/DNA hybrid or - polypeptide(D). Note that include_other=True must be set in from_mmcif. - - Returns: - A mapping from (author) chain IDs to their single-letter sequences for all - polymers in the Structure, ligands/water are ignored. - - Raises: - ValueError: If the mapping from internal chain IDs to author chain IDs is - not a bijection for polymer chains. - """ - label_chain_id_to_seq = self.chain_single_letter_sequence( - include_missing_residues=include_missing_residues - ) - auth_to_label = self.polymer_auth_asym_id_to_label_asym_id( - protein=protein, rna=rna, dna=dna, other=other - ) - return { - auth: label_chain_id_to_seq[label] - for auth, label in auth_to_label.items() - } - - def chain_res_name_sequence( - self, - *, - include_missing_residues: bool = True, - fix_non_standard_polymer_res: bool = False, - ) -> Mapping[str, Sequence[str]]: - """A mapping from internal chain ID to a sequence of residue names. - - The residue names are the full residue names rather than single letter - codes. For instance, for proteins these are the 3 letter CCD codes. - - Args: - include_missing_residues: Whether to include residues with no atoms in the - returned sequences. - fix_non_standard_polymer_res: Whether to map non standard residues in - protein / RNA / DNA chains to standard residues (e.g. MSE -> MET) or UNK - / N if a match is not found. - - Returns: - A mapping from (internal) chain IDs to a sequence of residue names. - """ - res_table = ( - self._residues if include_missing_residues else self.present_residues - ) - residue_chain_boundaries = _get_change_indices(res_table.chain_key) - boundaries = self._iter_residue_ranges( - residue_chain_boundaries, count_unresolved=include_missing_residues - ) - chain_keys = res_table.chain_key[residue_chain_boundaries] - chain_ids = self._chains.apply_array_to_column('id', chain_keys) - chain_types = self._chains.apply_array_to_column('type', chain_keys) - chain_seqs = {} - for idx, (start, end) in enumerate(boundaries): - chain_id = chain_ids[idx] - chain_type = chain_types[idx] - chain_res = res_table.name[start:end] - if ( - fix_non_standard_polymer_res - and chain_type in mmcif_names.POLYMER_CHAIN_TYPES - ): - chain_seqs[chain_id] = tuple( - fix_non_standard_polymer_residues( - res_names=chain_res, chain_type=chain_type - ) - ) - else: - chain_seqs[chain_id] = tuple(chain_res) - - return chain_seqs - - def fix_non_standard_polymer_res( - self, - res_mapper: Callable[ - [np.ndarray, str], np.ndarray - ] = fix_non_standard_polymer_residues, - ) -> Self: - """Replaces non-standard polymer residues with standard alternatives or UNK. - - e.g. maps 'ACE' -> 'UNK', 'MSE' -> 'MET'. - - NB: Only fixes the residue names, but does not fix the atom names. - E.g., 'MSE' will be renamed to 'MET' but its 'SE' atom will not be renamed - to 'S'. Fixing MSE should be done during conversion from mmcif with the - `fix_mse_residues` flag. - - Args: - res_mapper: An optional function that accepts a numpy array of residue - names and chain_type, and returns an array with fixed res_names. This - defaults to fix_non_standard_polymer_residues. - - Returns: - A Structure containing only standard residue types (or 'UNK') in its - polymer chains. - """ - fixed_res_name = self._residues.name.copy() - chain_change_indices = _get_change_indices(self._residues.chain_key) - for start, end in self._iter_atom_ranges(chain_change_indices): - chain_key = self._residues.chain_key[start] - chain_type = self._chains.type[self._chains.index_by_key[chain_key]] - if chain_type not in mmcif_names.POLYMER_CHAIN_TYPES: - continue # We don't need to change anything for non-polymers. - fixed_res_name[start:end] = res_mapper( - fixed_res_name[start:end], chain_type - ) - fixed_residues = self._residues.copy_and_update(name=fixed_res_name) - return self.copy_and_update(residues=fixed_residues, skip_validation=True) - - @property - def slice_leading_dims(self) -> '_LeadingDimSlice': - """Used to create a new Structure by slicing into the leading dimensions. - - Example usage 1: - - ``` - final_state = multi_state_struct.slice_leading_dims[-1] - ``` - - Example usage 2: - - ``` - # Structure has leading batch and time dimensions. - # Get final 3 time frames from first two batch elements. - sliced_strucs = batched_trajectories.slice_leading_dims[:2, -3:] - ``` - """ - return _LeadingDimSlice(self) - - def unstack(self, axis: int = 0) -> Sequence[Self]: - """Unstacks a multi-model structure into a list of Structures. - - This method is the inverse of `stack`. - - Example usage: - ``` - structs = multi_dim_struct.unstack(axis=0) - ``` - - Args: - axis: The axis to unstack over. The structures in the returned list won't - have this axis in their coordinate of b-factor fields. - - Returns: - A list of `Structure`s with length equal to the size of the specified - axis in the coordinate field arrays. - - Raises: - IndexError: If axis does not refer to one of the leading dimensions of - `self.atoms_table.size`. - """ - ndim = self._atoms.ndim - if not (-ndim <= axis < ndim): - raise IndexError( - f'{axis=} is out of range for atom coordinate fields with {ndim=}.' - ) - elif axis < 0: - axis += ndim - if axis == ndim - 1: - raise IndexError( - 'axis must refer to one of the leading dimensions, not the final ' - f'dimension. The atom fields have {ndim=} and {axis=} was specified.' - ) - unstacked = [] - leading_dim_slice = self.slice_leading_dims # Compute once here. - for i in range(self._atoms.shape[axis]): - slice_i = (slice(None),) * axis + (i,) - unstacked.append(leading_dim_slice[slice_i]) - return unstacked - - def split_by_chain(self) -> Sequence[Self]: - """Splits a Structure into single-chain Structures, one for each chain. - - The obtained structures can be merged back together into the original - structure using the `concat` function. - - Returns: - A list of `Structure`s, one for each chain. The order is the same as the - chain order in the original Structure. - """ - return [self.filter(chain_id=chain_id) for chain_id in self.chains] - - def transform_states_to_chains(self) -> Self: - """Transforms states to chains. - - A multi-state protein structure will be transformed to a multi-chain - single-state protein structure. Useful for visualising multiples states to - examine diversity. This structure's coordinate fields must have shape - `(num_states, num_atoms)`. - - Returns: - A new `Structure`, based on this structure, but with the multiple states - now represented as `num_states * num_chains` chains in a - single-state protein. - - Raises: - ValueError: If this structure's array fields don't have shape - `(num_states, num_atoms)`. - """ - if self._atoms.ndim != 2: - raise ValueError( - 'Coordinate field tensor must have 2 dimensions: ' - f'(num_states, num_atoms), got {self._atoms.ndim}.' - ) - return concat(self.unstack(axis=0)) - - def merge_chains( - self, - *, - chain_groups: Sequence[Sequence[str]], - chain_group_ids: Sequence[str] | None = None, - chain_group_types: Sequence[str] | None = None, - ) -> Self: - """Merges chains in each group into a single chain. - - If a Structure has chains A, B, C, D, E, and - `merge_chains([[A, C], [B, D], [E]])` is called, the new Structure will have - 3 chains A, B, C, the first being concatenation of A+C, the second B+D, the - third just the original chain E. - - Args: - chain_groups: Each group defines what chains should be merged into a - single chain. The output structure will therefore have len(chain_groups) - chains. Residue IDs are renumbered to preserve uniqueness within new - chains. Order of chain groups and within each group matters. - chain_group_ids: Optional sequence of new chain IDs for each group. If not - given, the new internal chain IDs (label_asym_id) are assigned in the - standard mmCIF order (i.e. A, B, ..., Z, AA, BA, CA, ...). Author chain - names (auth_asym_id) are set to be equal to the new internal chain IDs. - chain_group_types: Optional sequence of new chain types for each group. If - not given, only chains with the same type can be merged. - - Returns: - A new `Structure` with chains merged together into a single chain within - each chain group. - - Raises: - ValueError: If chain_group_ids or chain_group_types are given but don't - match the length of chain_groups. - ValueError: If the chain IDs in the flattened chain_groups don't match the - chain IDs in the Structure. - ValueError: If chains in any of the groups don't have the same chain type. - """ - if chain_group_ids and len(chain_group_ids) != len(chain_groups): - raise ValueError( - 'chain_group_ids must the same length as chain_groups: ' - f'{len(chain_group_ids)=} != {len(chain_groups)=}' - ) - if chain_group_types and len(chain_group_types) != len(chain_groups): - raise ValueError( - 'chain_group_types must the same length as chain_groups: ' - f'{len(chain_group_types)=} != {len(chain_groups)=}' - ) - flattened = sorted(itertools.chain.from_iterable(chain_groups)) - if flattened != sorted(self.chains): - raise ValueError( - 'IDs in chain groups do not match Structure chain IDs: ' - f'{chain_groups=}, chains={self.chains}' - ) - - new_chain_key_by_chain_id = {} - for new_chain_key, group_chain_ids in enumerate(chain_groups): - for chain_id in group_chain_ids: - new_chain_key_by_chain_id[chain_id] = new_chain_key - - chain_key_remap = {} - new_chain_type_by_chain_key = {} - for old_chain_key, old_chain_id, old_chain_type in zip( - self._chains.key, self._chains.id, self._chains.type - ): - new_chain_key = new_chain_key_by_chain_id[old_chain_id] - chain_key_remap[old_chain_key] = new_chain_key - - if new_chain_key not in new_chain_type_by_chain_key: - new_chain_type_by_chain_key[new_chain_key] = old_chain_type - elif not chain_group_types: - if new_chain_type_by_chain_key[new_chain_key] != old_chain_type: - bad_types = [ - f'{cid}: {self._chains.type[np.where(self._chains.id == cid)][0]}' - for cid in chain_groups[new_chain_key] - ] - raise ValueError( - 'Inconsistent chain types within group:\n' + - '\n'.join(bad_types) - ) - - new_chain_key = np.arange(len(chain_groups), dtype=np.int64) - if chain_group_ids: - new_chain_id = np.array(chain_group_ids, dtype=object) - else: - new_chain_id = np.array( - [mmcif.int_id_to_str_id(k) for k in new_chain_key + 1], dtype=object - ) - if chain_group_types: - new_chain_type = np.array(chain_group_types, dtype=object) - else: - new_chain_type = np.array( - [new_chain_type_by_chain_key[k] for k in new_chain_key], dtype=object - ) - new_chains = structure_tables.Chains( - key=new_chain_key, - id=new_chain_id, - type=new_chain_type, - auth_asym_id=new_chain_id, - entity_id=np.char.mod('%d', new_chain_key + 1).astype(object), - entity_desc=np.full(len(chain_groups), - fill_value='.', dtype=object), - ) - - # Remap chain keys and sort residues to match the chain table order. - new_residues = self._residues.copy_and_remap(chain_key=chain_key_remap) - new_residues = new_residues.apply_index( - np.argsort(new_residues.chain_key, kind='stable') - ) - # Renumber uniquely residues in each chain. - indices = np.arange(new_residues.chain_key.size, dtype=np.int32) - new_res_ids = (indices + 1) - np.maximum.accumulate( - indices * (new_residues.chain_key != - np.roll(new_residues.chain_key, 1)) - ) - new_residues = new_residues.copy_and_update(id=new_res_ids) - - # Remap chain keys and sort atoms to match the chain table order. - new_atoms = self._atoms.copy_and_remap(chain_key=chain_key_remap) - new_atoms = new_atoms.apply_index( - np.argsort(new_atoms.chain_key, kind='stable') - ) - - return self.copy_and_update( - chains=new_chains, - residues=new_residues, - atoms=new_atoms, - bonds=self._bonds, - ) - - def to_res_arrays( - self, - *, - include_missing_residues: bool, - atom_order: Mapping[str, int] = atom_types.ATOM37_ORDER, - ) -> tuple[np.ndarray, np.ndarray]: - """Returns an atom position and atom mask array with a num_res dimension. - - NB: All residues in the structure will appear in the residue - dimension but atoms will only have a True (1.0) mask value if - they are defined in `atom_order`. - - Args: - include_missing_residues: If True then the res arrays will include rows - for missing residues where all atoms will be masked out. Otherwise these - will simply be skipped. - atom_order: Atom order mapping atom names to their index in the atom - dimension of the returned arrays. Default is atom_order for proteins, - choose atom_types.ATOM29_ORDER for nucleics. - - Returns: - A pair of arrays: - * atom_positions: [num_res, atom_type_num, 3] float32 array of coords. - * atom_mask: [num_res, atom_type_num] float32 atom mask denoting - which atoms are present in this Structure. - """ - num_res = self.num_residues(count_unresolved=include_missing_residues) - atom_type_num = len(atom_order) - atom_positions = np.zeros( - (num_res, atom_type_num, 3), dtype=np.float32) - atom_mask = np.zeros((num_res, atom_type_num), dtype=np.float32) - - all_residues = None if not include_missing_residues else self.all_residues - for i, atom in enumerate_residues(self.iter_atoms(), all_residues): - atom_idx = atom_order.get(atom['atom_name']) - if atom_idx is not None: - atom_positions[i, atom_idx, 0] = atom['atom_x'] - atom_positions[i, atom_idx, 1] = atom['atom_y'] - atom_positions[i, atom_idx, 2] = atom['atom_z'] - atom_mask[i, atom_idx] = 1.0 - - return atom_positions, atom_mask - - def to_res_atom_lists( - self, *, include_missing_residues: bool - ) -> Sequence[Sequence[Mapping[str, Any]]]: - """Returns list of atom dictionaries grouped by residue. - - If this is a multi-model structure, each atom will store its fields - atom_x, atom_y, atom_z, and atom_b_factor as Numpy arrays of shape of the - leading dimension(s). If this is a single-mode structure, these fields will - just be scalars. - - Args: - include_missing_residues: If True, then the output list will contain an - empty list of atoms for missing residues. Otherwise missing residues - will simply be skipped. - - Returns: - A list of size `num_res`. Each element in the list represents atoms of one - residue. If a residue is present is present, the list will contain an atom - dictionary for every atom present in that residue. If a residue is missing - and `include_missing_residues=True`, the list for that missing residue - will be empty. - """ - num_res = self.num_residues(count_unresolved=include_missing_residues) - residue_atoms = [[] for _ in range(num_res)] - all_residues = None if not include_missing_residues else self.all_residues - - # We could yield directly in this loop but the code would be more complex. - # Let's optimise if memory usage is an issue. - for res_index, atom in enumerate_residues(self.iter_atoms(), all_residues): - residue_atoms[res_index].append(atom) - - return residue_atoms - - def reorder_chains(self, new_order: Sequence[str]) -> Self: - """Reorders tables so that the label_asym_ids are in the given order. - - This method changes the order of the chains, residues, and atoms tables so - that they are all consistent with each other. Moreover, it remaps chain keys - so that they stay monotonically increasing in chains/residues/atoms tables. - - Args: - new_order: The order in which the chain IDs (label_asym_id) should be. - This must be a permutation of the current chain IDs. - - Returns: - A structure with chains reordered. - """ - if len(new_order) != len(self.chains): - raise ValueError( - f'The new number of chains ({len(new_order)}) does not match the ' - f'current number of chains ({len(self.chains)}).' - ) - new_chain_set = set(new_order) - if len(new_chain_set) != len(new_order): - raise ValueError( - f'The new order {new_order} contains non-unique IDs.') - if new_chain_set.symmetric_difference(set(self.chains)): - raise ValueError( - f'New chain IDs {new_order} do not match the old {set(self.chains)}' - ) - - if self.chains == tuple(new_order): - # Shortcut: the new order is the same as the current one. - return self - - desired_chain_id_pos = {chain_id: i for i, - chain_id in enumerate(new_order)} - - current_chain_index_order = np.empty(self.num_chains, dtype=np.int64) - for index, old_chain_id in enumerate(self._chains.id): - current_chain_index_order[index] = desired_chain_id_pos[old_chain_id] - chain_reorder = np.argsort(current_chain_index_order, kind='stable') - chain_key_map = dict( - zip(self._chains.key[chain_reorder], range(self.num_chains)) - ) - chains = self._chains.apply_index(chain_reorder) - chains = chains.copy_and_remap(key=chain_key_map) - - # The stable sort keeps the original residue ordering within each chain. - residues = self._residues.copy_and_remap(chain_key=chain_key_map) - residue_reorder = np.argsort(residues.chain_key, kind='stable') - residues = residues.apply_index(residue_reorder) - - # The stable sort keeps the original atom ordering within each chain. - atoms = self._atoms.copy_and_remap(chain_key=chain_key_map) - atoms_reorder = np.argsort(atoms.chain_key, kind='stable') - atoms = atoms.apply_index(atoms_reorder) - - # Bonds unchanged - each references 2 atom keys, hence ordering not defined. - return self.copy_and_update(chains=chains, residues=residues, atoms=atoms) - - def rename_auth_asym_ids(self, new_id_by_old_id: Mapping[str, str]) -> Self: - """Returns a new structure with renamed auth_asym_ids. - - Args: - new_id_by_old_id: A mapping from original auth_asym_ids to their new - values. Any auth_asym_ids in this structure that are not in the mapping - will remain unchanged. - - Raises: - ValueError: If any two previously distinct polymer chains do not have - unique names anymore after the rename. - """ - mapped_chains = self._chains.copy_and_remap( - auth_asym_id=new_id_by_old_id) - mapped_polymer_ids = mapped_chains.filter( - type=mmcif_names.POLYMER_CHAIN_TYPES - ).auth_asym_id - if len(mapped_polymer_ids) != len(set(mapped_polymer_ids)): - raise ValueError( - 'The new polymer auth_asym_ids are not unique:' - f' {sorted(mapped_polymer_ids)}.' - ) - return self.copy_and_update(chains=mapped_chains, skip_validation=True) - - def rename_chain_ids(self, new_id_by_old_id: Mapping[str, str]) -> Self: - """Returns a new structure with renamed chain IDs (label_asym_ids). - - The chains' auth_asym_ids will be updated to be identical to the chain ID - since there isn't one unambiguous way to maintain the auth_asym_ids after - renaming the chain IDs (depending on whether you view the auth_asym_id as - more strongly associated with a given physical chain, or with a given - chain ID). - - The residues' auth_seq_id will be updated to be identical to the residue ID - since they are strongly tied to the original author chain naming and keeping - them would be misleading. - - Args: - new_id_by_old_id: A mapping from original chain ID to their new values. - Any chain IDs in this structure that are not in this mapping will remain - unchanged. - - Returns: - A new structure with renamed chains (and bioassembly data if it is - present). - - Raises: - ValueError: If any two previously distinct chains do not have unique names - anymore after the rename. - """ - new_chain_id = string_array.remap(self._chains.id, new_id_by_old_id) - if len(new_chain_id) != len(set(new_chain_id)): - raise ValueError( - f"New chain names aren't unique: {sorted(new_chain_id)}") - - # Map label_asym_ids in the bioassembly data. - if self._bioassembly_data is None: - new_bioassembly_data = None - else: - new_bioassembly_data = self._bioassembly_data.rename_label_asym_ids( - new_id_by_old_id, present_chains=set(self.present_chains.id) - ) - - # Set author residue IDs to be the string version of internal residue IDs. - new_residues = self._residues.copy_and_update( - auth_seq_id=self._residues.id.astype(str).astype(object) - ) - - new_chains = self._chains.copy_and_update( - id=new_chain_id, auth_asym_id=new_chain_id - ) - - return self.copy_and_update( - bioassembly_data=new_bioassembly_data, - chains=new_chains, - residues=new_residues, - skip_validation=True, - ) - - @functools.cached_property - def chains(self) -> tuple[str, ...]: - """Ordered internal chain IDs (label_asym_id) present in the Structure.""" - return tuple(self._chains.id) - - def rename_res_name( - self, - res_name_map: Mapping[str, str], - fail_if_not_found: bool = True, - ) -> Self: - """Returns a copy of this structure with residues renamed. - - Residue names in chemical components data will also be renamed. - - Args: - res_name_map: A mapping from old residue names to new residue names. Any - residues that are not in this mapping will be left unchanged. - fail_if_not_found: Whether to fail if keys in the res_name_map mapping are - not found in this structure's residues' `name` column. - - Raises: - ValueError: If `fail_if_not_found=True` and a residue name isn't found in - the residues table's `name` field. - """ - res_name_set = set(self._residues.name) - if fail_if_not_found: - for res_name in res_name_map: - if res_name not in res_name_set: - raise ValueError( - f'"{res_name}" not found in this structure.') - new_residues = self._residues.copy_and_remap(name=res_name_map) - - if self._chemical_components_data is not None: - chem_comp = { - res_name_map.get(res_name, res_name): data - for res_name, data in self._chemical_components_data.chem_comp.items() - } - new_chem_comp = struct_chem_comps.ChemicalComponentsData(chem_comp) - else: - new_chem_comp = None - - return self.copy_and_update( - residues=new_residues, - chemical_components_data=new_chem_comp, - skip_validation=True, - ) - - def rename_chains_to_match( - self, - other: 'Structure', - *, - fuzzy_match_non_standard_res: bool = True, - ) -> Self: - """Returns a new structure with renamed chains to match another's. - - Example: - This structure has chains: {'A': 'DEEP', 'B': 'MIND', 'C': 'MIND'} - Other structure has chains: {'X': 'DEEP', 'Z': 'MIND', 'Y': 'MIND'} - - After calling this method, you will get a structure that has chains named: - {'X': 'DEEP', 'Z': 'MIND', Y: 'MIND'} - - Args: - other: Another `Structure`. This provides the reference chain names that - is used to rename this structure's chains. - fuzzy_match_non_standard_res: If True, protein/RNA/DNA chains with the - same one letter sequence will be matched. e.g. "MET-MET-UNK1" will match - "MET-MSE-UNK2", since both will be mapped to "MMX". If False, we require - the full res_names to match. - - Returns: - A new `Structure`, based on this structure, which has chains renamed to - match the other structure. - """ - sequences = self.chain_res_name_sequence( - include_missing_residues=True, - fix_non_standard_polymer_res=fuzzy_match_non_standard_res, - ) - - other_sequences = other.chain_res_name_sequence( - include_missing_residues=True, - fix_non_standard_polymer_res=fuzzy_match_non_standard_res, - ) - - # Check that the sequences are the same. - sequence_counts = collections.Counter(sequences.values()) - other_sequence_counts = collections.Counter(other_sequences.values()) - if other_sequence_counts != sequence_counts: - raise ValueError( - 'The other structure does not have the same sequences\n' - f' other: {other_sequence_counts}\n self: {sequence_counts}' - ) - - new_decoy_id_by_old_id = {} - used_chain_ids = set() - # Sort self keys and take min over other to make matching deterministic. - # The matching is arbitrary but this helps debugging. - for self_chain_id, self_seq in sorted(sequences.items()): - # Find corresponding chains in the other structure. - other_chain_id = min( - k - for k, v in other_sequences.items() - if v == self_seq and k not in used_chain_ids - ) - - new_decoy_id_by_old_id[self_chain_id] = other_chain_id - used_chain_ids.add(other_chain_id) - - return self.rename_chain_ids(new_decoy_id_by_old_id) - - def _apply_bioassembly_transform( - self, transform: bioassemblies.Transform - ) -> Self: - """Applies a bioassembly transform to this structure.""" - base_struct = self.filter(chain_id=transform.chain_ids) - transformed_atoms = base_struct.atoms_table.copy_and_update_coords( - transform.apply_to_coords(base_struct.coords) - ) - transformed_chains = base_struct.chains_table.copy_and_remap( - id=transform.chain_id_rename_map - ) - # Set the transformed author chain ID to match the label chain ID. - transformed_chains = transformed_chains.copy_and_update( - auth_asym_id=transformed_chains.id - ) - return base_struct.copy_and_update( - chains=transformed_chains, - atoms=transformed_atoms, - skip_validation=True, - ) - - def generate_bioassembly(self, assembly_id: str | None = None) -> Self: - """Generates a biological assembly as a new `Structure`. - - When no assembly ID is provided this method produces a default assembly. - If this structure has no `bioassembly_data` then this returns itself - unchanged. Otherwise a default assembly ID is picked with - `BioassemblyData.get_default_assembly_id()`. - - Args: - assembly_id: The assembly ID to generate, or None to generate a default - bioassembly. - - Returns: - A new `Structure`, based on this one, representing the specified - bioassembly. Note that if the bioassembly contains copies of chains - in the original structure then they will be given new unique chain IDs. - - Raises: - ValueError: If this structure's `bioassembly_data` is `None` and - `assembly_id` is not `None`. - """ - if self._bioassembly_data is None: - if assembly_id is None: - return self - else: - raise ValueError( - f'Unset bioassembly_data, cannot generate assembly {assembly_id}' - ) - - if assembly_id is None: - assembly_id = self._bioassembly_data.get_default_assembly_id() - - transformed_structs = [ - self._apply_bioassembly_transform(transform) - for transform in self._bioassembly_data.get_transforms(assembly_id) - ] - - # We don't need to assign unique chain IDs because the bioassembly - # transform takes care of remapping chain IDs to be unique. - concatenated = concat(transformed_structs, - assign_unique_chain_ids=False) - - # Copy over all scalar fields (e.g. name, release date, etc.) other than - # bioassembly_data because it relates only to the pre-transformed structure. - return concatenated.copy_and_update_globals( - name=self.name, - release_date=self.release_date, - resolution=self.resolution, - structure_method=self.structure_method, - bioassembly_data=None, - chemical_components_data=self.chemical_components_data, - ) - - def _to_mmcif_header(self) -> Mapping[str, Sequence[str]]: - raw_mmcif = collections.defaultdict(list) - raw_mmcif['data_'] = [self._name] - raw_mmcif['_entry.id'] = [self._name] - - if self._release_date is not None: - date = [datetime.datetime.strftime(self._release_date, '%Y-%m-%d')] - raw_mmcif['_pdbx_audit_revision_history.revision_date'] = date - raw_mmcif['_pdbx_database_status.recvd_initial_deposition_date'] = date - - if self._resolution is not None: - raw_mmcif['_refine.ls_d_res_high'] = ['%.2f' % self._resolution] - - if self._structure_method is not None: - for method in self._structure_method.split(','): - raw_mmcif['_exptl.method'].append(method) - - if self._bioassembly_data is not None: - raw_mmcif.update(self._bioassembly_data.to_mmcif_dict()) - - # Populate chemical components data for all residues of this Structure. - if self._chemical_components_data: - raw_mmcif.update(self._chemical_components_data.to_mmcif_dict()) - - # Add _software table to store version number used to generate mmCIF. - # Only required data items are used (+ _software.version). - raw_mmcif['_software.pdbx_ordinal'] = ['1'] - raw_mmcif['_software.name'] = ['DeepMind Structure Class'] - raw_mmcif['_software.version'] = [self._VERSION] - raw_mmcif['_software.classification'] = ['other'] # Required. - - return raw_mmcif - - def to_mmcif_dict( - self, - *, - coords_decimal_places: int = _COORDS_DECIMAL_PLACES, - ) -> mmcif.Mmcif: - """Returns an Mmcif representing the structure.""" - header = self._to_mmcif_header() - sequence_tables = structure_tables.to_mmcif_sequence_and_entity_tables( - self._chains, self._residues, self._atoms.res_key - ) - atom_and_bond_tables = structure_tables.to_mmcif_atom_site_and_bonds_table( - chains=self._chains, - residues=self._residues, - atoms=self._atoms, - bonds=self._bonds, - coords_decimal_places=coords_decimal_places, - ) - return mmcif.Mmcif({**header, **sequence_tables, **atom_and_bond_tables}) - - def to_mmcif( - self, *, coords_decimal_places: int = _COORDS_DECIMAL_PLACES - ) -> str: - """Returns an mmCIF string representing the structure. - - Args: - coords_decimal_places: The number of decimal places to keep for atom - coordinates, including trailing zeros. - """ - return self.to_mmcif_dict( - coords_decimal_places=coords_decimal_places - ).to_string() - - -class _LeadingDimSlice: - """Helper class for slicing the leading dimensions of a `Structure`. - - Wraps a `Structure` instance and applies a slice operation to the coordinate - fields and other fields that may have leading dimensions (e.g. b_factor). - - Example usage: - t0_struct = multi_state_struct.slice_leading_dims[0] - """ - - def __init__(self, struct: Structure): - self._struct = struct - - def __getitem__(self, *args, **kwargs) -> Structure: - sliced_atom_cols = {} - for col_name in structure_tables.Atoms.multimodel_cols: - if (col := self._struct.atoms_table.get_column(col_name)).ndim > 1: - sliced_col = col.__getitem__(*args, **kwargs) - if ( - not sliced_col.shape - or sliced_col.shape[-1] != self._struct.num_atoms - ): - raise ValueError( - 'Coordinate slice cannot change final (atom) dimension.' - ) - sliced_atom_cols[col_name] = sliced_col - sliced_atoms = self._struct.atoms_table.copy_and_update( - **sliced_atom_cols) - return self._struct.copy_and_update(atoms=sliced_atoms, skip_validation=True) - - -def stack(structs: Sequence[Structure], axis: int = 0) -> Structure: - """Stacks multiple structures into a single multi-model Structure. - - This function is the inverse of `Structure.unstack()`. - - NB: this function assumes that every structure in `structs` is identical - other than the coordinates and b-factors. Under this assumption we can safely - copy all these identical fields from the first element of structs w.l.o.g. - However this is not checked in full detail as full comparison is expensive. - Instead this only checks that the `atom_name` field is identical, and that - the coordinates have the same shape. - - Usage example: - ``` - multi_model_struct = structure.stack(structs, axis=0) - ``` - - Args: - structs: A sequence of structures, each with the same atoms, but they may - have different coordinates and b-factors. If any b-factors are not None - then they must have the same shape as each of the coordinate fields. - axis: The axis in the returned structure that represents the different - structures in `structs` and will have size `len(structs)`. This cannot be - the final dimension as this is reserved for `num_atoms`. - - Returns: - A `Structure` with the same atoms as the structures in `structs` but with - all of their coordinates stacked into a new leading axis. - - Raises: - ValueError: If `structs` is empty. - ValueError: If `structs` do not all have the same `atom_name` field. - """ - if not structs: - raise ValueError('Need at least one Structure to stack.') - struct_0, *other_structs = structs - for i, struct in enumerate(other_structs, start=1): - # Check that every structure has the same atom name column. - # This check is intended to catch cases where the input structures might - # contain the same atoms, but in different orders. This won't catch every - # such case, e.g. if these are carbon-alpha-only structures, but should - # catch most cases. - if np.any(struct.atoms_table.name != struct_0.atoms_table.name): - raise ValueError( - f'structs[0] and structs[{i}] have mismatching atom name columns.' - ) - - stacked_atoms = struct_0.atoms_table.copy_and_update( - x=np.stack([s.atoms_table.x for s in structs], axis=axis), - y=np.stack([s.atoms_table.y for s in structs], axis=axis), - z=np.stack([s.atoms_table.z for s in structs], axis=axis), - b_factor=np.stack([s.atoms_table.b_factor for s in structs], axis=axis), - occupancy=np.stack( - [s.atoms_table.occupancy for s in structs], axis=axis), - ) - return struct_0.copy_and_update(atoms=stacked_atoms, skip_validation=True) - - -def _assign_unique_chain_ids( - structs: Iterable[Structure], -) -> Sequence[Structure]: - """Creates a sequence of `Structure` objects with unique chain IDs. - - Let e.g. [A, B] denote a structure of two chains A and B, then this function - performs the following kind of renaming operation: - - e.g.: [Z], [C], [B, C] -> [A], [B], [C, D] - - NB: This function uses Structure.rename_chain_ids which will define each - structure's chains.auth_asym_id to be identical to its chains.id columns. - - Args: - structs: Structures whose chains ids are to be uniquified. - - Returns: - A sequence with the same number of elements as `structs` but where each - element has had its chains renamed so that they aren't shared with any - other `Structure` in the sequence. - """ - # Start counting at 1 because mmcif.int_id_to_str_id expects integers >= 1. - chain_counter = 1 - structs_with_new_chain_ids = [] - for struct in structs: - rename_map = {} - for chain_id in struct.chains: - rename_map[chain_id] = mmcif.int_id_to_str_id(chain_counter) - chain_counter += 1 - renamed = struct.rename_chain_ids(rename_map) - structs_with_new_chain_ids.append(renamed) - return structs_with_new_chain_ids - - -def concat( - structs: Sequence[Structure], - *, - name: str | None = None, - assign_unique_chain_ids: bool = True, -) -> Structure: - """Concatenates structures along the atom dimension. - - NB: By default this function will first assign unique chain IDs to all chains - in `structs` so that the resulting structure does not contain duplicate chain - IDs. This will also fix entity IDs and author chain IDs. If this is disabled - via `assign_unique_chain_ids=False` the user must ensure that there are no - duplicate chains (label_asym_id). However, duplicate entity IDs and author - chain IDs are allowed as that might be the desired behavior. - - If `assign_unique_chain_ids=True`, note also that the chain_ids may be - overwritten even if they are already unique. - - Let e.g. [A, B] denote a structure of two chains A and B, then this function - performs the following kind of concatenation operation: - - assign_unique_chain_ids=True: - label chain IDS : [Z], [C], [B, C] -> [A, B, C, D] - author chain IDS: [U], [V], [V, C] -> [A, B, C, D] - entity IDs : [1], [1], [3, 3] -> [1, 2, 3, 4] - assign_unique_chain_ids=False: - label chain IDS : [D], [B], [C, A] -> [D, B, C, A] (inputs must be unique) - author chain IDS: [U], [V], [V, A] -> [U, V, V, A] - entity IDs : [1], [1], [3, 3] -> [1, 1, 3, 3] - - NB: This operation loses some information from the elements of `structs`, - namely the `name`, `resolution`, `release_date` and `bioassembly_data` fields. - - Args: - structs: The `Structure` instances to concatenate. These should all have the - same number and shape of leading dimensions (i.e. if any are multi-model - structures then they should all have the same number of models). - name: Optional name to give to the concatenated structure. If None, the name - will be concatenation of names of all concatenated structures. - assign_unique_chain_ids: Whether this function will first assign new unique - chain IDs, entity IDs and author chain IDs to every chain in `structs`. If - `False` then users must ensure chain IDs are already unique, otherwise an - exception is raised. See `_assign_unique_chain_ids` for more information - on how this is performed. - - Returns: - A new concatenated `Structure` with all of the chains in `structs` combined - into one new structure. The new structure will be named by joining the - names of `structs` with underscores. - - Raises: - ValueError: If `structs` is empty. - ValueError: If `assign_unique_chain_ids=False` and not all chains in - `structs` have unique chain IDs. - """ - if not structs: - raise ValueError('Need at least one Structure to concatenate.') - if assign_unique_chain_ids: - structs = _assign_unique_chain_ids(structs) - - chemical_components_data = {} - seen_label_chain_ids = set() - for i, struct in enumerate(structs): - if not assign_unique_chain_ids: - if seen_cid := seen_label_chain_ids.intersection(struct.chains): - raise ValueError( - f'Chain IDs {seen_cid} from structs[{i}] also exist in other' - ' members of structs. All given structures must have unique chain' - ' IDs. Consider setting assign_unique_chain_ids=True.' - ) - seen_label_chain_ids.update(struct.chains) - - if struct.chemical_components_data is not None: - # pytype: disable=attribute-error # always-use-property-annotation - chemical_components_data.update( - struct.chemical_components_data.chem_comp) - - concatted_struct = table.concat_databases(structs) - name = name if name is not None else '_'.join(s.name for s in structs) - # Chain IDs (label and author) are fixed at this point, fix also entity IDs. - if assign_unique_chain_ids: - entity_id = np.char.mod('%d', np.arange( - 1, concatted_struct.num_chains + 1)) - chains = concatted_struct.chains_table.copy_and_update( - entity_id=entity_id) - else: - chains = concatted_struct.chains_table - return concatted_struct.copy_and_update( - name=name, - release_date=None, - resolution=None, - structure_method=None, - bioassembly_data=None, - chemical_components_data=( - struct_chem_comps.ChemicalComponentsData(chemical_components_data) - if chemical_components_data - else None - ), - chains=chains, - skip_validation=True, # Already validated by table.concat_databases. - ) - - -def multichain_residue_index( - struct: Structure, chain_offset: int = 9000, between_chain_buffer: int = 1000 -) -> np.ndarray: - """Compute a residue index array that is monotonic across all chains. - - Lots of metrics (lddt, l1_long, etc) require computing a - distance-along-chain between two residues. For multimers we want to ensure - that any residues on different chains have a high along-chain distance - (i.e. they should always count as long-range contacts for example). To - do this we add 10000 to the residue indices of each chain, and enforce that - the residue index is monotonically increasing across the whole complex. - - Note: This returns the same as struct.res_id for monomers. - - Args: - struct: The structure to make a multichain residue index for. - chain_offset: The start of each chain is offset by at least this amount. - This must be larger than the absolute range of standard residue IDs. - between_chain_buffer: The final residue in one chain will have at least this - much of a buffer before the first residue in the next chain. - - Returns: - A monotonically increasing residue index, with at least - `between_chain_buffer` residues in between each chain. - """ - if struct.num_atoms: - res_id_range = np.max(struct.res_id) - np.min(struct.res_id) - chain_id_int = struct.chain_id - monotonic_chain_id_int = np.concatenate( - ([0], np.cumsum(chain_id_int[1:] != chain_id_int[:-1])) - ) - return struct.res_id + monotonic_chain_id_int * ( - chain_offset + between_chain_buffer - ) - - -def make_empty_structure() -> Structure: - """Returns a new structure consisting of empty array fields.""" - return Structure( - chains=structure_tables.Chains.make_empty(), - residues=structure_tables.Residues.make_empty(), - atoms=structure_tables.Atoms.make_empty(), - bonds=structure_tables.Bonds.make_empty(), - ) - - -def enumerate_residues( - atom_iter: Iterable[Mapping[str, Any]], - all_residues: AllResidues | None = None, -) -> Iterator[tuple[int, Mapping[str, Any]]]: - """Provides a zero-indexed enumeration of residues in an atom iterable. - - Args: - atom_iter: An iterable of atom dicts as returned by Structure.iter_atoms(). - all_residues: (Optional) A structure's all_residues field. If present then - this will be used to count missing residues by adding appropriate gaps in - the residue enumeration. - - Yields: - (res_i, atom) pairs where atom is the unmodified atom dict and res_i is a - zero-based index for the residue that the atom belongs to. - """ - if all_residues is None: - prev_res = None - res_i = -1 - for atom in atom_iter: - res = (atom['chain_id'], atom['res_id']) - if res != prev_res: - prev_res = res - res_i += 1 - yield res_i, atom - else: - all_res_seq = [] # Sequence of (chain_id, res_id) for all chains. - prev_chain = None - res_i = 0 - for atom in atom_iter: - chain_id = atom['chain_id'] - if chain_id not in all_residues: - raise ValueError( - f'Atom {atom} does not belong to any residue in all_residues.' - ) - if chain_id != prev_chain: - prev_chain = chain_id - all_res_seq.extend( - (chain_id, res_id) for (_, res_id) in all_residues[chain_id] - ) - res = (chain_id, atom['res_id']) - while res_i < len(all_res_seq) and res != all_res_seq[res_i]: - res_i += 1 - if res_i == len(all_res_seq): - raise ValueError( - f'Atom {atom} does not belong to a residue in all_residues.' - ) - yield res_i, atom diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure_tables.py b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure_tables.py deleted file mode 100644 index f1b1c7923..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/structure_tables.py +++ /dev/null @@ -1,841 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Table implementations for the Structure class.""" - -import collections -from collections.abc import Mapping, Sequence -import dataclasses -import functools -import itertools -import typing -from typing_extensions import Any, ClassVar, Self -import numpy as np -from alphafold3.constants import mmcif_names -from alphafold3.constants import residue_names -from alphafold3.cpp import aggregation -from alphafold3.cpp import string_array -from alphafold3.structure import bonds as bonds_module -from alphafold3.structure import mmcif -from alphafold3.structure import table - - -Bonds = bonds_module.Bonds - - -def _residue_name_to_record_name( - residue_name: np.ndarray, - polymer_mask: np.ndarray, -) -> np.ndarray: - """Returns record names (ATOM/HETATM) given residue names and polymer mask.""" - record_name = np.array(['HETATM'] * len(residue_name), dtype=object) - record_name[polymer_mask] = string_array.remap( - residue_name[polymer_mask], - mapping={r: 'ATOM' for r in residue_names.STANDARD_POLYMER_TYPES}, - default_value='HETATM', - ) - return record_name - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class AuthorNamingScheme: - """A mapping from internal values to author values in a mmCIF. - - Fields: - auth_asym_id: A mapping from label_asym_id to auth_asym_id. - auth_seq_id: A mapping from label_asym_id to a mapping from - label_seq_id to auth_seq_id. - insertion_code: A mapping from label_asym_id to a mapping from - label_seq_id to insertion codes. - entity_id: A mapping from label_asym_id to _entity.id. - entity_desc: A mapping from _entity.id to _entity.pdbx_description. - """ - - auth_asym_id: Mapping[str, str] - auth_seq_id: Mapping[str, Mapping[int, str]] - insertion_code: Mapping[str, Mapping[int, str | None]] - entity_id: Mapping[str, str] - entity_desc: Mapping[str, str] - - -def _default( - candidate_value: np.ndarray | None, default_value: Sequence[Any], dtype: Any -) -> np.ndarray: - if candidate_value is None: - return np.array(default_value, dtype=dtype) - return np.array(candidate_value, dtype=dtype) - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class Atoms(table.Table): - """Table of atoms in a Structure.""" - - chain_key: np.ndarray - res_key: np.ndarray - name: np.ndarray - element: np.ndarray - x: np.ndarray - y: np.ndarray - z: np.ndarray - b_factor: np.ndarray - occupancy: np.ndarray - multimodel_cols: ClassVar[tuple[str, ...]] = ( - 'x', - 'y', - 'z', - 'b_factor', - 'occupancy', - ) - - def __post_init__(self): - # Validates that the atom coordinates, b-factors and occupancies are finite. - for column_name in ('x', 'y', 'z', 'b_factor', 'occupancy'): - column = self.get_column(column_name) - if not np.isfinite(column).all(): - raise ValueError( - f'Column {column_name} must not contain NaN/inf values.' - ) - # super().__post_init__() can't be used as that causes the following error: - # TypeError: super(type, obj): obj must be an instance or subtype of type - super(Atoms, self).__post_init__() - - @classmethod - def make_empty(cls) -> Self: - return cls( - key=np.array([], dtype=np.int64), - chain_key=np.array([], dtype=np.int64), - res_key=np.array([], dtype=np.int64), - name=np.array([], dtype=object), - element=np.array([], dtype=object), - x=np.array([], dtype=np.float32), - y=np.array([], dtype=np.float32), - z=np.array([], dtype=np.float32), - b_factor=np.array([], dtype=np.float32), - occupancy=np.array([], dtype=np.float32), - ) - - @classmethod - def from_defaults( - cls, - *, - chain_key: np.ndarray, - res_key: np.ndarray, - key: np.ndarray | None = None, - name: np.ndarray | None = None, - element: np.ndarray | None = None, - x: np.ndarray | None = None, - y: np.ndarray | None = None, - z: np.ndarray | None = None, - b_factor: np.ndarray | None = None, - occupancy: np.ndarray | None = None, - ) -> Self: - """Create an Atoms table with minimal user inputs.""" - num_atoms = len(chain_key) - if not num_atoms: - return cls.make_empty() - return Atoms( - chain_key=chain_key, - res_key=res_key, - key=_default(key, np.arange(num_atoms), np.int64), - name=_default(name, ['?'] * num_atoms, object), - element=_default(element, ['?'] * num_atoms, object), - x=_default(x, [0.0] * num_atoms, np.float32), - y=_default(y, [0.0] * num_atoms, np.float32), - z=_default(z, [0.0] * num_atoms, np.float32), - b_factor=_default(b_factor, [0.0] * num_atoms, np.float32), - occupancy=_default(occupancy, [1.0] * num_atoms, np.float32), - ) - - def get_value_by_index( - self, column_name: str, index: int - ) -> table.TableEntry | np.ndarray: - if column_name in self.multimodel_cols: - return self.get_column(column_name)[..., index] - else: - return self.get_column(column_name)[index] - - def copy_and_update_coords(self, coords: np.ndarray) -> Self: - """Returns a copy with the x, y and z columns updated.""" - if coords.shape[-1] != 3: - raise ValueError( - f'Expecting 3-dimensional coordinates, got {coords.shape}' - ) - return typing.cast( - Atoms, - self.copy_and_update( - x=coords[..., 0], y=coords[..., 1], z=coords[..., 2] - ), - ) - - @property - def shape(self) -> tuple[int, ...]: - return self.x.shape - - @property - def ndim(self) -> int: - return len(self.shape) - - @functools.cached_property - def num_models(self) -> int: - """The number of models of this Structure.""" - leading_dims = self.shape[:-1] - match leading_dims: - case(): - return 1 - case(single_leading_dim_size,): - return single_leading_dim_size - case _: - raise ValueError( - 'num_models not defined for atom tables with more than one ' - 'leading dimension.' - ) - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class Residues(table.Table): - """Table of residues in a Structure.""" - - chain_key: np.ndarray - id: np.ndarray - name: np.ndarray - auth_seq_id: np.ndarray - insertion_code: np.ndarray - - @classmethod - def make_empty(cls) -> Self: - return cls( - key=np.array([], dtype=np.int64), - chain_key=np.array([], dtype=np.int64), - id=np.array([], dtype=np.int32), - name=np.array([], dtype=object), - auth_seq_id=np.array([], dtype=object), - insertion_code=np.array([], dtype=object), - ) - - @classmethod - def from_defaults( - cls, - *, - id: np.ndarray, # pylint:disable=redefined-builtin - chain_key: np.ndarray, - key: np.ndarray | None = None, - name: np.ndarray | None = None, - auth_seq_id: np.ndarray | None = None, - insertion_code: np.ndarray | None = None, - ) -> Self: - """Create a Residues table with minimal user inputs.""" - num_res = len(id) - if not num_res: - return cls.make_empty() - return Residues( - key=_default(key, np.arange(num_res), np.int64), - id=id, - chain_key=chain_key, - name=_default(name, ['UNK'] * num_res, object), - auth_seq_id=_default(auth_seq_id, id.astype(str), object), - insertion_code=_default(insertion_code, ['?'] * num_res, object), - ) - - -@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class Chains(table.Table): - """Table of chains in a Structure.""" - - id: np.ndarray - type: np.ndarray - auth_asym_id: np.ndarray - entity_id: np.ndarray - entity_desc: np.ndarray - - @classmethod - def make_empty(cls) -> Self: - return cls( - key=np.array([], dtype=np.int64), - id=np.array([], dtype=object), - type=np.array([], dtype=object), - auth_asym_id=np.array([], dtype=object), - entity_id=np.array([], dtype=object), - entity_desc=np.array([], dtype=object), - ) - - @classmethod - def from_defaults( - cls, - *, - id: np.ndarray, # pylint:disable=redefined-builtin - key: np.ndarray | None = None, - type: np.ndarray | None = None, # pylint:disable=redefined-builtin - auth_asym_id: np.ndarray | None = None, - entity_id: np.ndarray | None = None, - entity_desc: np.ndarray | None = None, - ) -> Self: - """Create a Chains table with minimal user inputs.""" - num_chains = len(id) - if not num_chains: - return cls.make_empty() - - return Chains( - key=_default(key, np.arange(num_chains), np.int64), - id=id, - type=_default(type, [mmcif_names.PROTEIN_CHAIN] - * num_chains, object), - auth_asym_id=_default(auth_asym_id, id, object), - entity_id=_default( - entity_id, np.arange(1, num_chains + 1).astype(str), object - ), - entity_desc=_default(entity_desc, ['.'] * num_chains, object), - ) - - -def to_mmcif_sequence_and_entity_tables( - chains: Chains, - residues: Residues, - atom_res_key: np.ndarray, -) -> Mapping[str, Sequence[str]]: - """Returns raw sequence and entity mmCIF tables.""" - raw_mmcif = collections.defaultdict(list) - chains_by_entity_id = {} - written_entity_poly_seq_ids = set() - present_res_keys = set(atom_res_key) - - # Performance optimisation: Find residue indices for each chain in advance, so - # that we don't have to do redundant masking work for each chain. - res_indices_for_chain = aggregation.indices_grouped_by_value( - residues.chain_key - ) - - for chain in chains.iterrows(): - # Add all chain information to the _struct_asym table. - chain_id = chain['id'] # Saves multiple dict lookups. - auth_asym_id = chain['auth_asym_id'] - entity_id = chain['entity_id'] - chains_by_entity_id.setdefault(entity_id, []).append(chain) - raw_mmcif['_struct_asym.id'].append(chain_id) - raw_mmcif['_struct_asym.entity_id'].append(entity_id) - - res_chain_indices = res_indices_for_chain[chain['key']] - chain_type = chain['type'] - is_polymer = chain_type in mmcif_names.POLYMER_CHAIN_TYPES - is_water = chain_type == mmcif_names.WATER - is_branched = len( - res_chain_indices) > 1 and not is_polymer and not is_water - write_entity_poly_seq = entity_id not in written_entity_poly_seq_ids - - # Iterate over the individual masked residue table columns, as that doesn't - # create a copy (only a view), while residues[res_chain_indices] does. - for res_key, res_name, res_id, pdb_seq_num, res_ins_code in zip( - residues.key[res_chain_indices], - residues.name[res_chain_indices], - residues.id[res_chain_indices], - residues.auth_seq_id[res_chain_indices], - residues.insertion_code[res_chain_indices], - strict=True, - ): - is_missing = res_key not in present_res_keys - str_res_id = str(res_id) - # While atom_site uses "?" for insertion codes, scheme tables use ".". - ins_code = (res_ins_code or '.').replace('?', '.') - auth_seq_num = '?' if is_missing else pdb_seq_num - - if is_polymer: - raw_mmcif['_pdbx_poly_seq_scheme.asym_id'].append(chain_id) - raw_mmcif['_pdbx_poly_seq_scheme.entity_id'].append(entity_id) - raw_mmcif['_pdbx_poly_seq_scheme.seq_id'].append(str_res_id) - raw_mmcif['_pdbx_poly_seq_scheme.mon_id'].append(res_name) - raw_mmcif['_pdbx_poly_seq_scheme.pdb_seq_num'].append( - pdb_seq_num) - raw_mmcif['_pdbx_poly_seq_scheme.auth_seq_num'].append( - auth_seq_num) - raw_mmcif['_pdbx_poly_seq_scheme.pdb_strand_id'].append( - auth_asym_id) - raw_mmcif['_pdbx_poly_seq_scheme.pdb_ins_code'].append( - ins_code) - # Structure doesn't support heterogeneous sequences. - raw_mmcif['_pdbx_poly_seq_scheme.hetero'].append('n') - if write_entity_poly_seq: - raw_mmcif['_entity_poly_seq.entity_id'].append(entity_id) - raw_mmcif['_entity_poly_seq.num'].append(str_res_id) - raw_mmcif['_entity_poly_seq.mon_id'].append(res_name) - # Structure doesn't support heterogeneous sequences. - raw_mmcif['_entity_poly_seq.hetero'].append('n') - written_entity_poly_seq_ids.add(entity_id) - elif is_branched: - raw_mmcif['_pdbx_branch_scheme.asym_id'].append(chain_id) - raw_mmcif['_pdbx_branch_scheme.entity_id'].append(entity_id) - raw_mmcif['_pdbx_branch_scheme.mon_id'].append(res_name) - raw_mmcif['_pdbx_branch_scheme.num'].append(str_res_id) - raw_mmcif['_pdbx_branch_scheme.pdb_asym_id'].append( - auth_asym_id) - raw_mmcif['_pdbx_branch_scheme.pdb_seq_num'].append( - pdb_seq_num) - raw_mmcif['_pdbx_branch_scheme.auth_asym_id'].append( - auth_asym_id) - raw_mmcif['_pdbx_branch_scheme.auth_seq_num'].append( - auth_seq_num) - raw_mmcif['_pdbx_branch_scheme.pdb_ins_code'].append(ins_code) - # Structure doesn't support heterogeneous sequences. - raw_mmcif['_pdbx_branch_scheme.hetero'].append('n') - else: - raw_mmcif['_pdbx_nonpoly_scheme.asym_id'].append(chain_id) - raw_mmcif['_pdbx_nonpoly_scheme.entity_id'].append(entity_id) - raw_mmcif['_pdbx_nonpoly_scheme.mon_id'].append(res_name) - raw_mmcif['_pdbx_nonpoly_scheme.pdb_seq_num'].append( - pdb_seq_num) - raw_mmcif['_pdbx_nonpoly_scheme.auth_seq_num'].append( - auth_seq_num) - raw_mmcif['_pdbx_nonpoly_scheme.pdb_strand_id'].append( - auth_asym_id) - raw_mmcif['_pdbx_nonpoly_scheme.pdb_ins_code'].append(ins_code) - - # Add _entity and _entity_poly tables. - for entity_id, chains in chains_by_entity_id.items(): - # chains should always be a non-empty list because of how we constructed - # chains_by_entity_id. - # All chains for a given entity should have the same type and sequence - # so we can pick the first one without losing information. - key_chain = chains[0] - raw_mmcif['_entity.id'].append(entity_id) - raw_mmcif['_entity.pdbx_description'].append(key_chain['entity_desc']) - entity_type = key_chain['type'] - if entity_type not in mmcif_names.POLYMER_CHAIN_TYPES: - raw_mmcif['_entity.type'].append(entity_type) - else: - raw_mmcif['_entity.type'].append('polymer') - raw_mmcif['_entity_poly.entity_id'].append(entity_id) - raw_mmcif['_entity_poly.type'].append(entity_type) - - # _entity_poly.pdbx_strand_id is a comma-separated list of - # auth_asym_ids that are part of the entity. - raw_mmcif['_entity_poly.pdbx_strand_id'].append( - ','.join(chain['auth_asym_id'] for chain in chains) - ) - return raw_mmcif - - -def to_mmcif_atom_site_and_bonds_table( - *, - chains: Chains, - residues: Residues, - atoms: Atoms, - bonds: Bonds, - coords_decimal_places: int, -) -> Mapping[str, Sequence[str]]: - """Returns raw _atom_site and _struct_conn mmCIF tables.""" - raw_mmcif = collections.defaultdict(list) - # Use [value] * num wherever possible since it is about 10x faster than list - # comprehension in such cases. Also use f-strings instead of str() - faster. - total_atoms = atoms.size * atoms.num_models - raw_mmcif['_atom_site.id'] = [f'{i}' for i in range(1, total_atoms + 1)] - raw_mmcif['_atom_site.label_alt_id'] = ['.'] * total_atoms - # Use format_float_array instead of list comprehension for performance. - raw_mmcif['_atom_site.Cartn_x'] = mmcif.format_float_array( - values=atoms.x.ravel(), num_decimal_places=coords_decimal_places - ) - raw_mmcif['_atom_site.Cartn_y'] = mmcif.format_float_array( - values=atoms.y.ravel(), num_decimal_places=coords_decimal_places - ) - raw_mmcif['_atom_site.Cartn_z'] = mmcif.format_float_array( - values=atoms.z.ravel(), num_decimal_places=coords_decimal_places - ) - - # atoms.b_factor or atoms.occupancy can be flat even when the coordinates have - # leading dimensions. In this case we tile it to match. - if atoms.b_factor.ndim == 1: - atom_b_factor = np.tile(atoms.b_factor, atoms.num_models) - else: - atom_b_factor = atoms.b_factor.ravel() - raw_mmcif['_atom_site.B_iso_or_equiv'] = mmcif.format_float_array( - values=atom_b_factor, num_decimal_places=2 - ) - - if atoms.occupancy.ndim == 1: - atom_occupancy = np.tile(atoms.occupancy, atoms.num_models) - else: - atom_occupancy = atoms.occupancy.ravel() - raw_mmcif['_atom_site.occupancy'] = mmcif.format_float_array( - values=atom_occupancy.ravel(), num_decimal_places=2 - ) - - label_atom_id = atoms.name - type_symbol = atoms.element - label_comp_id = residues.apply_array_to_column('name', atoms.res_key) - label_asym_id = chains.apply_array_to_column('id', atoms.chain_key) - label_entity_id = chains.apply_array_to_column( - 'entity_id', atoms.chain_key) - # Performance optimisation: Do the int->str conversion on num_residue-sized, - # array, then select instead of selecting and then converting. - label_seq_id = residues.id.astype('str').astype(object)[ - ..., residues.index_by_key[atoms.res_key] - ] - - # _atom_site.label_seq_id is '.' for non-polymers. - non_polymer_chain_mask = string_array.isin( - chains.type, mmcif_names.POLYMER_CHAIN_TYPES, invert=True - ) - non_polymer_chain_keys = chains.key[non_polymer_chain_mask] - non_polymer_atom_mask = np.isin(atoms.chain_key, non_polymer_chain_keys) - label_seq_id[non_polymer_atom_mask] = '.' - - auth_asym_id = chains.apply_array_to_column( - 'auth_asym_id', atoms.chain_key) - auth_seq_id = residues.apply_array_to_column('auth_seq_id', atoms.res_key) - pdbx_pdb_ins_code = residues.apply_array_to_column( - 'insertion_code', atoms.res_key - ) - string_array.remap(pdbx_pdb_ins_code, mapping={None: '?'}, inplace=True) - - group_pdb = _residue_name_to_record_name( - residue_name=label_comp_id, polymer_mask=~non_polymer_atom_mask - ) - - def tile_for_models(arr: np.ndarray) -> list[str]: - if atoms.num_models == 1: - # Memory optimisation: np.tile(arr, 1) does a copy. - return arr.tolist() - return np.tile(arr, atoms.num_models).tolist() - - raw_mmcif['_atom_site.group_PDB'] = tile_for_models(group_pdb) - raw_mmcif['_atom_site.label_atom_id'] = tile_for_models(label_atom_id) - raw_mmcif['_atom_site.type_symbol'] = tile_for_models(type_symbol) - raw_mmcif['_atom_site.label_comp_id'] = tile_for_models(label_comp_id) - raw_mmcif['_atom_site.label_asym_id'] = tile_for_models(label_asym_id) - raw_mmcif['_atom_site.label_entity_id'] = tile_for_models(label_entity_id) - raw_mmcif['_atom_site.label_seq_id'] = tile_for_models(label_seq_id) - raw_mmcif['_atom_site.auth_asym_id'] = tile_for_models(auth_asym_id) - raw_mmcif['_atom_site.auth_seq_id'] = tile_for_models(auth_seq_id) - raw_mmcif['_atom_site.pdbx_PDB_ins_code'] = tile_for_models( - pdbx_pdb_ins_code) - model_id = np.array( - [str(i + 1) for i in range(atoms.num_models)], dtype=object - ) - raw_mmcif['_atom_site.pdbx_PDB_model_num'] = np.repeat( - model_id, [atoms.size] * atoms.num_models - ).tolist() - - if bonds.key.size > 0: - raw_mmcif.update( - bonds.to_mmcif_dict_from_atom_arrays( - atom_key=atoms.key, - chain_id=label_asym_id, - res_id=label_seq_id, - res_name=label_comp_id, - atom_name=label_atom_id, - auth_asym_id=auth_asym_id, - auth_seq_id=auth_seq_id, - insertion_code=np.array(pdbx_pdb_ins_code), - ) - ) - return raw_mmcif - - -def _flatten_author_naming_scheme_table( - res_table: Mapping[str, Mapping[int, str]], - chain_ids: np.ndarray, - res_chain_ids: np.ndarray, - res_ids: np.ndarray, - default_if_missing: str, - table_name: str, -) -> np.ndarray: - """Flattens an author naming scheme table consistently with res_ids.""" - if not set(chain_ids).issubset(res_table): - raise ValueError( - f'Chain IDs in the chain_id array must be a subset of {table_name} in ' - 'author naming scheme:\n' - f'chain_ids: {sorted(chain_ids)}\n' - f'{table_name} keys: {sorted(res_table.keys())}' - ) - - chain_change_mask = res_chain_ids[1:] != res_chain_ids[:-1] - res_chain_boundaries = np.concatenate( - ([0], np.where(chain_change_mask)[0] + 1, [len(res_chain_ids)]) - ) - - flat_vals = np.empty(len(res_ids), dtype=object) - for chain_start, chain_end in itertools.pairwise(res_chain_boundaries): - chain_id = res_chain_ids[chain_start] - chain_res_ids = res_ids[chain_start:chain_end] - chain_mapping = res_table[chain_id] - flat_vals[chain_start:chain_end] = [ - chain_mapping.get(r, default_if_missing) for r in chain_res_ids - ] - - return flat_vals - - -def tables_from_atom_arrays( - *, - res_id: np.ndarray, - author_naming_scheme: AuthorNamingScheme | None = None, - all_residues: Mapping[str, Sequence[tuple[str, int]]] | None = None, - chain_id: np.ndarray | None = None, - chain_type: np.ndarray | None = None, - res_name: np.ndarray | None = None, - atom_key: np.ndarray | None = None, - atom_name: np.ndarray | None = None, - atom_element: np.ndarray | None = None, - atom_x: np.ndarray | None = None, - atom_y: np.ndarray | None = None, - atom_z: np.ndarray | None = None, - atom_b_factor: np.ndarray | None = None, - atom_occupancy: np.ndarray | None = None, -) -> tuple[Atoms, Residues, Chains]: - """Returns Structure tables constructed from atom array level data. - - All fields except name and, res_id are optional, all array fields consist of a - value for each atom in the structure - so residue and chain values should hold - the same value for each atom in the chain or residue. Fields which are not - defined are filled with default values. - - Validation is performed by the Structure constructor where possible - but - author_naming scheme and all_residues must be checked in this function. - - It is not possible to construct structures with chains that do not contain - any resolved residues using this function. If this is necessary, use the - structure.Structure constructor directly. - - Args: - res_id: Integer array of shape [num_atom]. The unique residue identifier for - each residue. mmCIF field - _atom_site.label_seq_id. - author_naming_scheme: An optional instance of AuthorNamingScheme to use when - converting this structure to mmCIF. - all_residues: An optional mapping from each chain ID (i.e. label_asym_id) to - a sequence of (label_comp_id, label_seq_id) tuples, one per residue. This - can contain residues that aren't present in the atom arrays. This is - common in experimental data where some residues are not resolved but are - known to be present. - chain_id: String array of shape [num_atom] of unique chain identifiers. - mmCIF field - _atom_site.label_asym_id. - chain_type: String array of shape [num_atom]. The molecular type of the - current chain (e.g. polyribonucleotide). mmCIF field - _entity_poly.type - OR _entity.type (for non-polymers). - res_name: String array of shape [num_atom].. The name of each residue, - typically a 3 letter string for polypeptides or 1-2 letter strings for - polynucleotides. mmCIF field - _atom_site.label_comp_id. - atom_key: A unique sorted integer array, used only by the bonds table to - identify the atoms participating in each bond. If the bonds table is - specified then this column must be non-None. - atom_name: String array of shape [num_atom]. The name of each atom (e.g CA, - O2', etc.). mmCIF field - _atom_site.label_atom_id. - atom_element: String array of shape [num_atom]. The element type of each - atom (e.g. C, O, N, etc.). mmCIF field - _atom_site.type_symbol. - atom_x: Float array of shape [..., num_atom] of atom x coordinates. May have - arbitrary leading dimensions, provided that these are consistent across - all coordinate fields. - atom_y: Float array of shape [..., num_atom] of atom y coordinates. May have - arbitrary leading dimensions, provided that these are consistent across - all coordinate fields. - atom_z: Float array of shape [..., num_atom] of atom z coordinates. May have - arbitrary leading dimensions, provided that these are consistent across - all coordinate fields. - atom_b_factor: Float array of shape [..., num_atom] or [num_atom] of atom - b-factors or equivalent. If there are no extra leading dimensions then - these values are assumed to apply to all coordinates for a given atom. If - there are leading dimensions then these must match those used by the - coordinate fields. - atom_occupancy: Float array of shape [..., num_atom] or [num_atom] of atom - occupancies or equivalent. If there are no extra leading dimensions then - these values are assumed to apply to all coordinates for a given atom. If - there are leading dimensions then these must match those used by the - coordinate fields. - """ - num_atoms = len(res_id) - - for arr_name, array, dtype in ( - ('chain_id', chain_id, object), - ('chain_type', chain_type, object), - ('res_id', res_id, np.int32), - ('res_name', res_name, object), - ('atom_key', atom_key, np.int64), - ('atom_name', atom_name, object), - ('atom_element', atom_element, object), - ): - if array is not None and array.shape != (num_atoms,): - raise ValueError( - f'{arr_name} shape {array.shape} != ({num_atoms},)') - if array is not None and array.dtype != dtype: - raise ValueError(f'{arr_name} dtype {array.dtype} != {dtype}') - - for arr_name, array in ( - ('atom_x', atom_x), - ('atom_y', atom_y), - ('atom_z', atom_z), - ('atom_b_factor', atom_b_factor), - ('atom_occupancy', atom_occupancy), - ): - if array is not None and array.shape[-1] != num_atoms: - raise ValueError( - f'{arr_name} last dim {array.shape[-1]} != {num_atoms=}') - if ( - array is not None - and array.dtype != np.float32 - and array.dtype != np.float64 - ): - raise ValueError( - f'{arr_name} must be np.float32 or np.float64, got {array.dtype=}' - ) - - if all_residues is not None and (res_name is None or res_id is None): - raise ValueError( - 'If all_residues != None, res_name and res_id must not be None either.' - ) - - if num_atoms == 0: - return Atoms.make_empty(), Residues.make_empty(), Chains.make_empty() - - if chain_id is None: - chain_id = np.full(shape=num_atoms, fill_value='A', dtype=object) - if res_name is None: - res_name = np.full(shape=num_atoms, fill_value='UNK', dtype=object) - - chain_change_mask = chain_id[1:] != chain_id[:-1] - chain_start = np.concatenate(([0], np.where(chain_change_mask)[0] + 1)) - res_start = np.concatenate( - ([0], np.where((res_id[1:] != res_id[:-1]) | chain_change_mask)[0] + 1) - ) - - if len(set(chain_id)) != len(chain_start): - raise ValueError(f'Chain IDs must be contiguous, but got {chain_id}') - - # We do not support chains with unresolved residues-only in this function. - chain_ids = chain_id[chain_start] - if all_residues and set(all_residues.keys()) != set(chain_ids): - raise ValueError( - 'all_residues must contain the same set of chain IDs as the chain_id ' - f'array:\nall_residues keys: {sorted(all_residues.keys())}\n' - f'chain_ids: {sorted(chain_ids)}.' - ) - # Make sure all_residue ordering is consistent with chain_id. - if all_residues and np.any(list(all_residues.keys()) != chain_ids): - all_residues = {cid: all_residues[cid] for cid in chain_ids} - - # Create the chains table. - num_chains = len(chain_ids) - chain_keys = np.arange(num_chains, dtype=np.int64) - chain_key_by_chain_id = dict(zip(chain_ids, chain_keys, strict=True)) - - if chain_type is not None: - chain_types = chain_type[chain_start] - else: - chain_types = np.full( - num_chains, mmcif_names.PROTEIN_CHAIN, dtype=object) - - if author_naming_scheme is not None: - auth_asym_id = string_array.remap( - chain_ids, author_naming_scheme.auth_asym_id - ) - entity_id = string_array.remap( - chain_ids, author_naming_scheme.entity_id, default_value='.' - ) - entity_desc = string_array.remap( - entity_id, author_naming_scheme.entity_desc, default_value='.' - ) - else: - auth_asym_id = chain_ids - entity_id = (chain_keys + 1).astype(str).astype(object) - entity_desc = np.full(num_chains, '.', dtype=object) - - chains = Chains( - key=chain_keys, - id=chain_ids, - type=chain_types, - auth_asym_id=auth_asym_id, - entity_id=entity_id, - entity_desc=entity_desc, - ) - - # Create the residues table. - if all_residues is not None: - residue_order = [] - for cid, residues in all_residues.items(): - residue_order.extend((cid, rname, int(rid)) - for (rname, rid) in residues) - res_chain_ids, res_names, res_ids = zip(*residue_order) - res_chain_ids = np.array(res_chain_ids, dtype=object) - res_ids = np.array(res_ids, dtype=np.int32) - res_names = np.array(res_names, dtype=object) - else: - res_chain_ids = chain_id[res_start] - res_ids = res_id[res_start] - res_names = res_name[res_start] - residue_order = list(zip(res_chain_ids, res_names, res_ids)) - - if author_naming_scheme is not None and author_naming_scheme.auth_seq_id: - auth_seq_id = _flatten_author_naming_scheme_table( - author_naming_scheme.auth_seq_id, - chain_ids=chain_ids, - res_chain_ids=res_chain_ids, - res_ids=res_ids, - default_if_missing='.', - table_name='auth_seq_id', - ) - else: - auth_seq_id = res_ids.astype(str).astype(object) - - if author_naming_scheme is not None and author_naming_scheme.insertion_code: - insertion_code = _flatten_author_naming_scheme_table( - author_naming_scheme.insertion_code, - chain_ids=chain_ids, - res_chain_ids=res_chain_ids, - res_ids=res_ids, - default_if_missing='?', - table_name='insertion_code', - ) - # Make sure insertion code of None is mapped to '.'. - insertion_code = string_array.remap(insertion_code, {None: '?'}) - else: - insertion_code = np.full( - shape=len(res_ids), fill_value='?', dtype=object) - - res_key_by_res = {res: i for i, res in enumerate(residue_order)} - res_keys = np.arange(len(residue_order), dtype=np.int64) - res_chain_keys = string_array.remap( - res_chain_ids, chain_key_by_chain_id - ).astype(np.int64) - residues = Residues( - chain_key=res_chain_keys, - key=res_keys, - id=res_ids, - name=res_names, - auth_seq_id=auth_seq_id, - insertion_code=insertion_code, - ) - - if atom_key is None: - atom_key = np.arange(num_atoms, dtype=np.int64) - - atom_chain_keys = string_array.remap(chain_id, chain_key_by_chain_id).astype( - np.int64 - ) - - try: - atom_res_keys = [res_key_by_res[r] - for r in zip(chain_id, res_name, res_id)] - except KeyError as e: - missing_chain_id, missing_res_name, missing_res_id = e.args[0] - raise ValueError( - 'Inconsistent res_name, res_id and all_residues. Could not find ' - f'residue with chain_id={missing_chain_id}, ' - f'res_name={missing_res_name}, res_id={missing_res_id} in all_residues.' - ) from e - - atoms = Atoms( - key=atom_key, - chain_key=atom_chain_keys, - res_key=np.array(atom_res_keys, dtype=np.int64), - name=_default(atom_name, ['?'] * num_atoms, object), - element=_default(atom_element, ['?'] * num_atoms, object), - x=_default(atom_x, [0.0] * num_atoms, np.float32), - y=_default(atom_y, [0.0] * num_atoms, np.float32), - z=_default(atom_z, [0.0] * num_atoms, np.float32), - b_factor=_default(atom_b_factor, [0.0] * num_atoms, np.float32), - occupancy=_default(atom_occupancy, [1.0] * num_atoms, np.float32), - ) - return atoms, residues, chains diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/table.py b/MindSPONGE/applications/AlphaFold3/alphafold3/structure/table.py deleted file mode 100644 index 050f8958d..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/structure/table.py +++ /dev/null @@ -1,565 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# ============================================================================ - -"""Table module for atom/residue/chain tables in Structure. - -Tables are intended to be lightweight collections of columns, loosely based -on a pandas dataframe, for use in the Structure class. -""" - -import abc -from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, Sequence -import dataclasses -import functools -import graphlib -import typing -from typing_extensions import Any, Protocol, Self, TypeAlias, TypeVar, overload - -from alphafold3.cpp import string_array -import numpy as np - - -TableEntry: TypeAlias = str | int | float | None -FilterPredicate: TypeAlias = ( - TableEntry - | Iterable[Any] # Workaround for b/326384670. Tighten once fixed. - | Callable[[Any], bool] # Workaround for b/326384670. Tighten once fixed. - | Callable[[np.ndarray], bool] -) - - -class RowLookup(Protocol): - - def get_row_by_key( - self, - key: int, - column_name_map: Mapping[str, str] | None = None, - ) -> Mapping[str, Any]: - ... - - -@dataclasses.dataclass(frozen=True) -class Table: - """Parent class for structure tables. - - A table is a collection of columns of equal length, where one column is the - key. The key uniquely identifies each row in the table. - - A table can refer to other tables by including a foreign key column, whose - values are key values from the other table's key column. These column can have - arbitrary names and are treated like any other integer-valued column. - - See the `Database` class in this module for utilities for handing sets of - tables that are related via foreign keys. - - NB: This does not correspond to an mmCIF table. - """ - - key: np.ndarray - - def __post_init__(self): - for col_name in self.columns: - if (col_len := self.get_column(col_name).shape[-1]) != self.size: - raise ValueError( - f'All columns should have length {self.size} but got "{col_name}"' - f' with length {col_len}.' - ) - # Make col immutable. - self.get_column(col_name).flags.writeable = False - if self.key.size and self.key.min() < 0: - raise ValueError( - 'Key values must be non-negative. Got negative values:' - f' {set(self.key[self.key < 0])}' - ) - self.key.flags.writeable = False # Make key immutable. - - def __getstate__(self) -> dict[str, Any]: - """Returns members with cached properties removed for pickling.""" - cached_props = { - k - for k, v in self.__class__.__dict__.items() - if isinstance(v, functools.cached_property) - } - return {k: v for k, v in self.__dict__.items() if k not in cached_props} - - @functools.cached_property - def index_by_key(self) -> np.ndarray: - """Mapping from key values to their index in the column arrays. - - i.e.: self.key[index_by_key[k]] == k - """ - if not self.key.size: - return np.array([], dtype=np.int64) - else: - index_by_key = np.zeros(np.max(self.key) + 1, dtype=np.int64) - index_by_key[self.key] = np.arange(self.size) - return index_by_key - - @functools.cached_property - def columns(self) -> tuple[str, ...]: - """The names of the columns in the table, including the key column.""" - return tuple(field.name for field in dataclasses.fields(self)) - - @functools.cached_property - def items(self) -> Mapping[str, np.ndarray]: - """Returns the mapping from column names to column values.""" - return {col: getattr(self, col) for col in self.columns} - - @functools.cached_property - def size(self) -> int: - """The number of rows in the table.""" - return self.key.shape[-1] - - def __len__(self) -> int: - return self.size - - def get_column(self, column_name: str) -> np.ndarray: - """Gets a column by name.""" - # Performance optimisation: use the cached columns, instead of getattr. - return self.items[column_name] - - def apply_array(self, arr: np.ndarray) -> Self: - """Returns a sliced table using a key (!= index) array or a boolean mask.""" - if arr.dtype == bool and np.all(arr): - return self # Shortcut: No-op, so just return. - - return self.copy_and_update(**{ - column_name: self.apply_array_to_column(column_name, arr) - for column_name in self.columns - }) - - def apply_index(self, index_arr: np.ndarray) -> Self: - """Returns a sliced table using an index (!= key) array.""" - if index_arr.dtype == bool: - raise ValueError('The index array must not be a boolean mask.') - - return self.copy_and_update( - **{col: self.get_column(col)[..., index_arr] for col in self.columns} - ) - - def apply_array_to_column( - self, - column_name: str, - arr: np.ndarray, - ) -> np.ndarray: - """Returns a sliced column array using a key array or a boolean mask.""" - if arr.dtype == bool: - return self.get_column(column_name)[..., arr] - else: - return self.get_column(column_name)[..., self.index_by_key[arr]] - - def get_value_by_index(self, column_name: str, index: int) -> Any: - return self.get_column(column_name)[index] - - def get_value_by_key( - self, - column_name: str, - key: int | np.integer, - ) -> TableEntry: - """Gets the value of a column at the row with specified key value.""" - return self.get_value_by_index(column_name, self.index_by_key[key]) - - @overload - def __getitem__(self, key: str) -> np.ndarray: - ... - - @overload - def __getitem__(self, key: np.ndarray) -> 'Table': - ... - - @overload - def __getitem__(self, key: tuple[str, int | np.integer]) -> TableEntry: - ... - - @overload - def __getitem__(self, key: tuple[str, np.ndarray]) -> np.ndarray: - ... - - def __getitem__(self, key): - match key: - case str(): - return self.get_column(key) - case np.ndarray() as key_arr_or_mask: - return self.apply_array(key_arr_or_mask) - case str() as col, int() | np.integer() as key_val: - return self.get_value_by_key(col, key_val) - case str() as col, np.ndarray() as key_arr_or_mask: - return self.apply_array_to_column(col, key_arr_or_mask) - case _: - if isinstance(key, tuple): - err_msg = f'{key}, type: tuple({[type(v) for v in key]})' - else: - err_msg = f'{key}, type: {type(key)}' - raise KeyError(err_msg) - - def get_row_by_key( - self, - key: int, - column_name_map: Mapping[str, str] | None = None, - ) -> dict[str, Any]: - """Gets the row with specified key value.""" - return self.get_row_by_index( - self.index_by_key[key], column_name_map=column_name_map - ) - - def get_row_by_index( - self, - index: int, - column_name_map: Mapping[str, str] | None = None, - ) -> dict[str, Any]: - """Gets the row at the specified index.""" - if column_name_map is not None: - return { - renamed_col: self.get_value_by_index(col, index) - for renamed_col, col in column_name_map.items() - } - else: - return {col: self.get_value_by_index(col, index) for col in self.columns} - - def iterrows( - self, - *, - row_keys: np.ndarray | None = None, - column_name_map: Mapping[str, str] | None = None, - **table_by_foreign_key_col: RowLookup, - ) -> Iterator[Mapping[str, Any]]: - """Yields rows from the table. - - Args: - row_keys: An optional array of keys of rows to yield. If None, all rows - will be yielded. - column_name_map: An optional mapping from desired keys in the row dicts to - the names of the columns they correspond to. - **table_by_foreign_key_col: An optional mapping from column names in this - table, which are expected to be columns of foreign keys, to the table - that the foreign keys point into. If provided, then the yielded rows - will include data from the foreign tables at the appropriate key. - """ - if row_keys is not None: - row_indices = self.index_by_key[row_keys] - else: - row_indices = range(self.size) - for i in row_indices: - row = self.get_row_by_index(i, column_name_map=column_name_map) - for key_col, table in table_by_foreign_key_col.items(): - foreign_key = self[key_col][i] - foreign_row = table.get_row_by_key(foreign_key) - row.update(foreign_row) - yield row - - def with_column_names( - self, column_name_map: Mapping[str, str] - ) -> 'RenamedTableView': - """Returns a view of this table with mapped column names.""" - return RenamedTableView(self, column_name_map=column_name_map) - - def make_filter_mask( - self, - mask: np.ndarray | None = None, - *, - apply_per_element: bool = False, - **predicate_by_col: FilterPredicate, - ) -> np.ndarray | None: - """Returns a boolean array of rows to keep, or None if all can be kept. - - Args: - mask: See `Table.filter`. - apply_per_element: See `Table.filter`. - **predicate_by_col: See `Table.filter`. - - Returns: - Either a boolean NumPy array of length `(self.size,)` denoting which rows - should be kept according to the input mask and predicates, or None. None - implies there is no filtering required, and is used where possible - instead of an all-True array to save time and space. - """ - if mask is None: - if not predicate_by_col: - return None - else: - mask = np.ones((self.size,), dtype=bool) - else: - if mask.shape != (self.size,): - raise ValueError( - f'mask must have shape ({self.size},). Got: {mask.shape}.' - ) - if mask.dtype != bool: - raise ValueError( - f'mask must have dtype bool. Got: {mask.dtype}.') - - for col, predicate in predicate_by_col.items(): - if self[col].ndim > 1: - raise ValueError( - f'Cannot filter by column {col} with more than 1 dimension.' - ) - - callable_predicates = [] - if not callable(predicate): - if isinstance(predicate, Iterable) and not isinstance(predicate, str): - target_vals = predicate - else: - target_vals = [predicate] - for target_val in target_vals: - callable_predicates.append( - lambda x, target=target_val: x == target) - else: - callable_predicates.append(predicate) - - field_mask = np.zeros_like(mask) - for callable_predicate in callable_predicates: - if not apply_per_element: - callable_predicate = typing.cast( - Callable[[np.ndarray], bool], callable_predicate - ) - predicate_result = callable_predicate(self.get_column(col)) - else: - predicate_result = np.array( - [callable_predicate(elem) - for elem in self.get_column(col)] - ) - np.logical_or(field_mask, predicate_result, out=field_mask) - np.logical_and(mask, field_mask, out=mask) # Update in-place. - return mask - - def filter( - self, - mask: np.ndarray | None = None, - *, - apply_per_element: bool = False, - invert: bool = False, - **predicate_by_col: FilterPredicate, - ) -> Self: - """Filters the table using mask and/or predicates and returns a new table. - - Predicates can be either: - 1. A constant value, e.g. `'CA'`. In this case then only rows that match - this value for the given column are retained. - 2. A (non-string) iterable e.g. `('A', 'B')`. In this - case then rows are retained if they match any of the provided values for - the given column. - 3. A boolean function e.g. `lambda b_fac: b_fac < 100.0`. - In this case then only rows that evaluate to `True` are retained. By - default this function's parameter is expected to be an array, unless - `apply_per_element=True`. - - Args: - mask: An optional boolean NumPy array with length equal to the table size. - If provided then this will be combined with the other predicates so that - a row is included if it is masked-in *and* matches all the predicates. - apply_per_element: Whether apply predicates to each element in the column - individually, or to pass the whole column array to the predicate. - invert: If True then the returned table will contain exactly those rows - that would be removed if this was `False`. - **predicate_by_col: A mapping from column name to a predicate. Filtered - columns must be 1D arrays. If multiple columns are provided as keyword - arguments then each predicate is applied and the results are combined - using a boolean AND operation, so an atom is only retained if it passes - all predicates. - - Returns: - A new table with the desired rows retained (or filtered out if - `invert=True`). - - Raises: - ValueError: If mask is provided and is not a bool array with shape - `(num_atoms,)`. - """ - filter_mask = self.make_filter_mask( - mask, apply_per_element=apply_per_element, **predicate_by_col - ) - if filter_mask is None: - # No mask or predicate was specified, so we can return early. - if not invert: - return self - else: - return self[np.array((), dtype=np.int64)] - else: - return self[~filter_mask if invert else filter_mask] - - def _validate_keys_are_column_names(self, keys: Collection[str]) -> None: - """Raises an error if any of the keys are not column names.""" - if mismatches := set(keys) - set(self.columns): - raise ValueError(f'Invalid column names: {sorted(mismatches)}.') - - def copy_and_update(self, **new_column_by_column_name: np.ndarray) -> Self: - """Returns a copy of this table with the specified changes applied. - - Args: - **new_column_by_column_name: New values for the specified columns. - - Raises: - ValueError: If a specified column name is not a column in this table. - """ - self._validate_keys_are_column_names(new_column_by_column_name) - return dataclasses.replace(self, **new_column_by_column_name) - - def copy_and_remap( - self, **mapping_by_col: Mapping[TableEntry, TableEntry] - ) -> Self: - """Returns a copy of the table with the specified columns remapped. - - Args: - **mapping_by_col: Each kwarg key should be the name of one of this table's - columns, and each value should be a mapping. The values in the column - will be looked up in the mapping and replaced with the result if one is - found. - - Raises: - ValueError: If a specified column name is not a column in this table. - """ - self._validate_keys_are_column_names(mapping_by_col) - if not self.size: - return self - remapped_cols = {} - for column_name, mapping in mapping_by_col.items(): - col_arr = self.get_column(column_name) - if col_arr.dtype == object: - remapped = string_array.remap(col_arr, mapping) - else: - remapped = np.vectorize(lambda x: mapping.get(x, x))( - col_arr) # pylint: disable=cell-var-from-loop - remapped_cols[column_name] = remapped - return self.copy_and_update(**remapped_cols) - - -class RenamedTableView: - """View of a table with renamed column names.""" - - def __init__(self, table: Table, column_name_map: Mapping[str, str]): - self._table = table - self._column_name_map = column_name_map - - def get_row_by_key( - self, - key: int, - column_name_map: Mapping[str, str] | None = None, - ) -> Mapping[str, Any]: - del column_name_map - return self._table.get_row_by_key( - key, column_name_map=self._column_name_map - ) - - -_DatabaseT = TypeVar('_DatabaseT', bound='Database') - - -class Database(abc.ABC): - """Relational database base class.""" - - @property - @abc.abstractmethod - def tables(self) -> Collection[str]: - """The names of the tables in this database.""" - - @abc.abstractmethod - def get_table(self, table_name: str) -> Table: - """Gets the table with the given name.""" - - @property - @abc.abstractmethod - def foreign_keys(self) -> Mapping[str, Collection[tuple[str, str]]]: - """Describes the relationship between keys in the database. - - Returns: - A map from table names to pairs of `(column_name, foreign_table_name)` - where `column_name` is a column containing foreign keys in the table named - by the key, and the `foreign_table_name` is the name of the table that - those foreign keys refer to. - """ - - @abc.abstractmethod - def copy_and_update( - self: _DatabaseT, - **new_field_by_field_name: ..., - ) -> _DatabaseT: - """Returns a copy of this database with the specified changes applied.""" - - -def table_dependency_order(db: Database) -> Iterable[str]: - """Yields the names of the tables in the database in dependency order. - - This order guarantees that a table appears after all other tables that - it refers to using foreign keys. Specifically A < B implies that A contains - no column that refers to B.key as a foreign key. - - Args: - db: The database that defines the table names and foreign keys. - """ - connections: dict[str, set[str]] = {} - for table_name in db.tables: - connection_set = set() - for _, foreign_table in db.foreign_keys.get(table_name, ()): - connection_set.add(foreign_table) - connections[table_name] = connection_set - yield from graphlib.TopologicalSorter(connections).static_order() - - -def concat_databases(dbs: Sequence[_DatabaseT]) -> _DatabaseT: - """Concatenates the tables across a sequence of databases. - - Args: - dbs: A non-empty sequence of database instances of the same type. - - Returns: - A new database containing the concatenated tables from the input databases. - - Raises: - ValueError: If `dbs` is empty or `dbs` contains different Database - types. - """ - if not dbs: - raise ValueError('Need at least one value to concatenate.') - distinct_db_types = {type(db) for db in dbs} - if len(distinct_db_types) > 1: - raise ValueError( - f'All `dbs` must be of the same type, got: {distinct_db_types}' - ) - - first_db, *other_dbs = dbs - concatted_tables: dict[str, Table] = {} - key_offsets: dict[str, list[int]] = {} - for table_name in table_dependency_order(first_db): - first_table = first_db.get_table(table_name) - columns: dict[str, list[np.ndarray]] = { - column_name: [first_table.get_column(column_name)] - for column_name in first_table.columns - } - key_offsets[table_name] = [ - first_table.key.max() + 1 if first_table.size else 0 - ] - - for prev_index, db in enumerate(other_dbs): - table = db.get_table(table_name) - for col_name in table.columns: - columns[col_name].append(table.get_column(col_name)) - key_offset = key_offsets[table_name][prev_index] - offset_key = table.key + key_offset - columns['key'][-1] = offset_key - if table.size: - key_offsets[table_name].append(offset_key.max() + 1) - else: - key_offsets[table_name].append( - key_offsets[table_name][prev_index]) - for fkey_col_name, foreign_table_name in first_db.foreign_keys.get( - table_name, [] - ): - fkey_columns = columns[fkey_col_name] - fkey_columns[-1] = ( - fkey_columns[-1] + - key_offsets[foreign_table_name][prev_index] - ) - - concatted_columns = { - column_name: np.concatenate(values, axis=-1) - for column_name, values in columns.items() - } - concatted_tables[table_name] = (type(first_table))(**concatted_columns) - return first_db.copy_and_update(**concatted_tables) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/attention.py b/MindSPONGE/applications/AlphaFold3/alphafold3/utils/attention.py deleted file mode 100644 index ba5ebdb52..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/attention.py +++ /dev/null @@ -1,189 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""MindSpore implementation of dot product attention.""" -import dataclasses -import functools -import mindspore as ms -from mindspore import ops - - -def _softmax(x): - """Computes softmax.""" - dtype = ms.float32 - x_max, _ = ops.max(x.astype(dtype), axis=-1, keepdims=True) - unnormalized = ops.exp(x - x_max) - denom = ops.sum(unnormalized, dim=-1, keepdim=True) - return (unnormalized / denom).astype(x.dtype) - - -def cal_logits(q, k, use_bf16=False): - """Calculate logits.""" - # ...qhd,...khd->...hqk - dtype = q.dtype - if use_bf16: - q = q.astype(ms.bfloat16) - k = k.astype(ms.bfloat16) - q_trans = ops.transpose(q, (0, 2, 1, 3)) # ...qhd -> ...hqd - k_trans = ops.transpose(k, (0, 2, 3, 1)) # ...khd -> ...hdk - logits = ops.matmul(q_trans, k_trans) - if use_bf16: - logits = logits.astype(dtype) - return logits - - -def cal_out(weights, v, use_bf16=False): - """Calculate output.""" - # ...hqk,...khd->...qhd - if use_bf16: - weights = weights.astype(ms.bfloat16) - v = v.astype(ms.bfloat16) - v_trans = ops.transpose(v, (0, 2, 1, 3)) # ...khd -> ...hkd - out_temp = ops.matmul(weights, v_trans) # ...hqk,...hkd->...hqd - out = ops.transpose(out_temp, (0, 2, 1, 3)) - return out - - -def attention( - q, k, v, *, logits_scale, - bias, mask -): - """Compute attention.""" - logits = cal_logits(q, k) - - logits *= logits_scale - - if bias is not None: - logits += bias - - if mask is not None: - if not isinstance(mask, Mask): - mask = Mask(mask) - mask = mask.as_array(q.shape[-3], k.shape[-3]) - mask_value = -3.4028235e+37 # a small value close to min of bfloat16 - logits = ops.where(mask.bool(), logits, mask_value) - - weights = _softmax(logits) - - out = cal_out(weights, v) - - return out - -@dataclasses.dataclass(frozen=True) -class Mask: - """An attention mask. - - `k_start` (inclusive) and `k_end` (exclusive) define range of enabled - k-sequence values for each row of logits. - - For example, a local attention mask could be defined as follows: - ``` - seq_len_q = seq_len_k = 4 - window_size = 2 - k_start = Tensor(np.maximum(0, np.arange(seq_len_q) + 1 - window_size)) - mask = Mask(k_start=k_start, is_causal=True) - assert mask.as_array(seq_len_q, seq_len_k) == Tensor(np.array( - [[1, 0, 0, 0], - [1, 1, 0, 0], - [0, 1, 1, 0], - [0, 0, 1, 1]], dtype=bool)) - ``` - """ - bool_mask: ms.Tensor = None - _: dataclasses.KW_ONLY - q_start: ms.Tensor = None - q_end: ms.Tensor = None - k_start: ms.Tensor = None - k_end: ms.Tensor = None - is_causal: bool = False - - def tree_flatten(self): - return ( - self.bool_mask, - self.q_start, - self.q_end, - self.k_start, - self.k_end, - ), (self.is_causal,) - - @classmethod - def tree_unflatten(cls, aux, children): - (is_causal,) = aux - bool_mask, q_start, q_end, k_start, k_end = children - return cls( - bool_mask, - q_start=q_start, - q_end=q_end, - k_start=k_start, - k_end=k_end, - is_causal=is_causal, - ) - - def as_array(self, q_len_or_indices, k_len_or_indices): - """Returns the mask as a boolean array.""" - q_indices = ops.arange(q_len_or_indices) if isinstance( - q_len_or_indices, int) else q_len_or_indices - q_indices = q_indices[..., None] - - k_indices = ops.arange(k_len_or_indices) if isinstance( - k_len_or_indices, int) else k_len_or_indices - k_indices = k_indices[..., None, :] - - mask = [] - if self.bool_mask is not None: - mask.append(self.bool_mask) - - if self.q_start is not None: - mask.append(q_indices >= self.q_start[..., None, :]) - - if self.q_end is not None: - mask.append(q_indices < self.q_end[..., None, :]) - - if self.k_start is not None: - mask.append(k_indices >= self.k_start[..., None]) - - if self.k_end is not None: - mask.append(k_indices < self.k_end[..., None]) - - if self.is_causal: - mask.append(q_indices >= k_indices) - - logical_and = functools.partial(functools.reduce, ops.logical_and) - - if mask: - return logical_and(mask) - return None - - def take(self, *attrs): - """Returns a mask with attrs removed and the removed attrs.""" - default_mask = type(self)() - replacements = {attr: getattr(default_mask, attr) for attr in attrs} - values = (getattr(self, attr) for attr in attrs) - return dataclasses.replace(self, **replacements), *values - - def __and__(self, other): - """Returns the intersection of two masks.""" - if not isinstance(other, Mask): - other = Mask(other) - - def combine(op): - return lambda a, b: b if a is None else a if b is None else op(a, b) - - return Mask( - bool_mask=combine(ops.logical_and)( - self.bool_mask, other.bool_mask), - q_end=combine(ops.minimum)(self.q_end, other.q_end), - k_start=combine(ops.maximum)(self.k_start, other.k_start), - k_end=combine(ops.minimum)(self.k_end, other.k_end), - is_causal=self.is_causal or other.is_causal, - ) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/common/precision.py b/MindSPONGE/applications/AlphaFold3/alphafold3/utils/common/precision.py deleted file mode 100644 index b4b299dcd..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/common/precision.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Precision classes and utilities.""" - -import enum -import mindspore as ms - - -@enum.unique -class DotPrecision(enum.Enum): - """Precision for `dot` operation. - - Naming scheme: {OPERAND_DTYPE}_{ACCUMULATOR_DTYPE}[_{NUM_PASSES}x] - """ - - BF16_F32 = "bf16_f32" - - # NPU only precisions. - F32_F32 = "f32_f32" # Full f32 precision (doesn't use TensorCores). - F16_F16 = "f16_f16" - F16_F32 = "f16_f32" - - @property - def operand_dtype(self) -> ms.dtype: - match self: - case DotPrecision.BF16_F32: - return ms.bfloat16 - case DotPrecision.F16_F16 | DotPrecision.F16_F32: - return ms.float16 - case _: - return ms.float32 - - @property - def accumulator_dtype(self) -> ms.dtype: - return ms.float16 if (self == DotPrecision.F16_F16) else ms.float32 - - -_MS_NPU_PRECISION_MAP = { - (ms.float16, "DEFAULT"): DotPrecision.F16_F32, - (ms.bfloat16, "DEFAULT"): DotPrecision.BF16_F32, - (ms.float32, "DEFAULT"): DotPrecision.F32_F32, - (ms.float32, "HIGH"): DotPrecision.F32_F32, - (ms.float32, "HIGHEST"): DotPrecision.F32_F32, -} - -_MS_CPU_PRECISION_MAP = { - (ms.float16, "DEFAULT"): DotPrecision.F16_F32, - (ms.bfloat16, "DEFAULT"): DotPrecision.F32_F32, - (ms.float32, "DEFAULT"): DotPrecision.F32_F32, - (ms.float32, "HIGH"): DotPrecision.F32_F32, - (ms.float32, "HIGHEST"): DotPrecision.F32_F32, -} - - -def _create_ms_precision_map(): - precision_map = {} - for (dtype, ms_precision), dot_precision in _MS_NPU_PRECISION_MAP.items(): - precision_map[("ascend", dtype, ms_precision)] = dot_precision - for (dtype, ms_precision), dot_precision in _MS_CPU_PRECISION_MAP.items(): - precision_map[("cpu", dtype, ms_precision)] = dot_precision - return precision_map - - -_MS_PRECISION_MAP = _create_ms_precision_map() - - -def get_equivalent_dot_precision( - a_dtype: ms.dtype, b_dtype: ms.dtype, ms_precision: str -) -> DotPrecision: - """Returns `DotPrecision` replicating default behaviour.""" - if a_dtype != b_dtype: - raise ValueError("Cannot infer precision if operand types differ.") - - backend = ms.context.get_context("device_target").lower() - if (ms_precision != "DEFAULT") and (a_dtype != ms.float32): - raise ValueError( - "`Precision` values other than `DEFAULT` only have an effect if" - " the operand type is `float32`." - ) - return _MS_PRECISION_MAP[(backend, a_dtype, ms_precision)] diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/gated_linear_unit.py b/MindSPONGE/applications/AlphaFold3/alphafold3/utils/gated_linear_unit.py deleted file mode 100644 index d4dde0521..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/gated_linear_unit.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Common types for gated linear unit kernels.""" -import mindspore as ms -from mindspore import mint - -def gated_linear_unit(x, weight, *, activation): - """Applies a gated linear unit (https://arxiv.org/abs/1612.08083). - - Computes `activation(x @ weight[:, 0]) * x @ weight[:, 1]`. - - This is SwiGLU when `activation=swish`, GEGLU when - `activation=gelu`, REGLU when `activation=relu`, and GLU when - `activation=sigmoid` (https://arxiv.org/abs/2002.05202). - - Args: - x: the input array. - weight: the combined weight array. - activation: optional activation function. - precision: specifies the matrix multiplication precision. Either `None` - (default), which means the default precision for the backend, or an - enum of "DEFAULT/HIGH/...". - - Returns: - The output array. - """ - - weight_reshaped = mint.reshape( - weight, (-1, weight.shape[-2] * weight.shape[-1])) - y1 = mint.matmul(x, weight_reshaped) - y = y1.astype(ms.float32) - a, b = y.split(y.shape[-1] // 2, axis=-1) - out = mint.mul(a, b) if activation is None else mint.mul(activation(a), b) - out = out.astype(x.dtype) - - return out diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/__init__.py b/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/__init__.py deleted file mode 100644 index 93ec6c1da..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2025 Huawei Technologies Co., Ltd -# -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""geometry""" - -from alphafold3.utils.geometry import rigid_matrix_vector -from alphafold3.utils.geometry import rotation_matrix -from alphafold3.utils.geometry import struct_of_array -from alphafold3.utils.geometry import vector - -Rot3Array = rotation_matrix.Rot3Array -Rigid3Array = rigid_matrix_vector.Rigid3Array - -StructOfArray = struct_of_array.StructOfArray - -Vec3Array = vector.Vec3Array -square_euclidean_distance = vector.square_euclidean_distance -euclidean_distance = vector.euclidean_distance -dihedral_angle = vector.dihedral_angle -dot = vector.dot -cross = vector.cross diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/rigid_matrix_vector.py b/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/rigid_matrix_vector.py deleted file mode 100644 index 89193a1f0..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/rigid_matrix_vector.py +++ /dev/null @@ -1,194 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Rigid3Array Transformations represented by a Matrix and a Vector.""" - -from typing import Any, Final, TypeAlias, Union, Optional -import mindspore as ms -import mindspore.numpy as mnp -from mindspore import Tensor -from mindspore.ops import operations as P - -from alphafold3.utils.geometry import rotation_matrix, struct_of_array, utils, vector - -Float: TypeAlias = Union[float, Tensor] - -VERSION: Final[str] = '0.1' - - -def _compute_covariance_matrix( - row_values: vector.Vec3Array, - col_values: vector.Vec3Array, - weights: Tensor, - epsilon=1e-6, -) -> Tensor: - """Compute covariance matrix.""" - weights = mnp.asarray(weights) - - weights = mnp.broadcast_to(weights, row_values.shape) - - normalized_weights = weights / \ - (mnp.sum(weights, axis=-1, keepdims=True) + epsilon) - - def weighted_average(x): - return mnp.sum(normalized_weights * x, axis=-1) - - out = [ - mnp.stack( - ( - weighted_average(row_values.x * col_values.x), - weighted_average(row_values.x * col_values.y), - weighted_average(row_values.x * col_values.z), - ), - axis=-1, - ) - ] - - out.append( - mnp.stack( - ( - weighted_average(row_values.y * col_values.x), - weighted_average(row_values.y * col_values.y), - weighted_average(row_values.y * col_values.z), - ), - axis=-1, - ) - ) - - out.append( - mnp.stack( - ( - weighted_average(row_values.z * col_values.x), - weighted_average(row_values.z * col_values.y), - weighted_average(row_values.z * col_values.z), - ), - axis=-1, - ) - ) - - return mnp.stack(out, axis=-2) - - -@struct_of_array.StructOfArray(same_dtype=True) -class Rigid3Array: - """Rigid Transformation, i.e. element of special euclidean group.""" - - rotation: rotation_matrix.Rot3Array - translation: vector.Vec3Array - - def __matmul__(self, other: 'Rigid3Array') -> 'Rigid3Array': - new_rotation = self.rotation @ other.rotation - new_translation = self.apply_to_point(other.translation) - return Rigid3Array(new_rotation, new_translation) - - def inverse(self) -> 'Rigid3Array': - """Return Rigid3Array corresponding to inverse transform.""" - inv_rotation = self.rotation.inverse() - inv_translation = inv_rotation.apply_to_point(-self.translation) - return Rigid3Array(inv_rotation, inv_translation) - - def apply_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array: - """Apply Rigid3Array transform to point.""" - return self.rotation.apply_to_point(point) + self.translation - - def apply_inverse_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array: - """Apply inverse Rigid3Array transform to point.""" - new_point = point - self.translation - return self.rotation.apply_inverse_to_point(new_point) - - def compose_rotation(self, other_rotation: rotation_matrix.Rot3Array) -> 'Rigid3Array': - rot = self.rotation @ other_rotation - trans = P.BroadcastTo(rot.shape)(self.translation) - return Rigid3Array(rot, trans) - - @classmethod - def identity(cls, shape: Any, dtype: ms.dtype = ms.float32) -> 'Rigid3Array': - """Return identity Rigid3Array of given shape.""" - - return cls( - rotation_matrix.Rot3Array.identity(shape, dtype=dtype), - vector.Vec3Array.zeros(shape, dtype=dtype), - ) - - def scale_translation(self, factor: Float) -> 'Rigid3Array': - """Scale translation in Rigid3Array by 'factor'.""" - return Rigid3Array(self.rotation, self.translation * factor) - - def to_array(self): - rot_array = self.rotation.to_array() - vec_array = self.translation.to_array() - return mnp.concatenate([rot_array, vec_array[..., None]], axis=-1) - - @classmethod - def from_array(cls, array): - rot = rotation_matrix.Rot3Array.from_array(array[..., :3]) - vec = vector.Vec3Array.from_array(array[..., -1]) - return cls(rot, vec) - - @classmethod - def from_array4x4(cls, array: Tensor) -> 'Rigid3Array': - """Construct Rigid3Array from homogeneous 4x4 array.""" - if array.shape[-2:] != (4, 4): - raise ValueError(f'array.shape({array.shape}) must be [..., 4, 4]') - rotation = rotation_matrix.Rot3Array( - *(array[..., 0, 0], array[..., 0, 1], array[..., 0, 2]), - *(array[..., 1, 0], array[..., 1, 1], array[..., 1, 2]), - *(array[..., 2, 0], array[..., 2, 1], array[..., 2, 2]), - ) - translation = vector.Vec3Array( - array[..., 0, 3], array[..., 1, 3], array[..., 2, 3] - ) - return cls(rotation, translation) - - @classmethod - def from_point_alignment( - cls, - points_to: vector.Vec3Array, - points_from: vector.Vec3Array, - weights: Optional[Float] = None, - epsilon: float = 1e-6, - ) -> 'Rigid3Array': - """Constructs Rigid3Array by finding transform aligning points.""" - if weights is None: - weights = 1.0 - - def compute_center(value): - return utils.weighted_mean(value=value, weights=weights, axis=-1) - - points_to_center = P.Map()(compute_center, points_to) - points_from_center = P.Map()(compute_center, points_from) - centered_points_to = points_to - points_to_center[..., None] - centered_points_from = points_from - points_from_center[..., None] - cov_mat = _compute_covariance_matrix( - centered_points_to, - centered_points_from, - weights=weights, - epsilon=epsilon, - ) - rots = rotation_matrix.Rot3Array.from_svd( - mnp.reshape(cov_mat, cov_mat.shape[:-2] + (9,)) - ) - - translations = points_to_center - \ - rots.apply_to_point(points_from_center) - - return cls(rots, translations) - - def __getstate__(self): - return (VERSION, (self.rotation, self.translation)) - - def __setstate__(self, state): - version, (rot, trans) = state - del version - object.__setattr__(self, 'rotation', rot) - object.__setattr__(self, 'translation', trans) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/rotation_matrix.py b/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/rotation_matrix.py deleted file mode 100644 index 19ec01c70..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/rotation_matrix.py +++ /dev/null @@ -1,255 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Rot3Array Matrix Class.""" - -from typing import Any, Final -import numpy as np -import mindspore as ms -import mindspore.numpy as mnp -from mindspore import ops, mint -from mindspore import Tensor -from alphafold3.utils.geometry import struct_of_array, utils, vector - -COMPONENTS: Final[tuple[str, ...]] = ( - *('xx', 'xy', 'xz'), - *('yx', 'yy', 'yz'), - *('zx', 'zy', 'zz'), -) -VERSION: Final[str] = '0.1' - - -def make_matrix_svd_factors() -> Tensor: - """Generates factors for converting 3x3 matrix to symmetric 4x4 matrix.""" - factors = mnp.zeros((16, 9), dtype=ms.float32) - - indices = [(0, [0, 4, 8]), ([1, 4], 5), ([1, 4], 7), ([2, 8], 6), ([2, 8], 2), - ([3, 12], 1), ([3, 12], 3), (5, 0), (5, [4, 8]), - ([6, 9], 1), ([6, 9], 3), ([7, 13], 2), ([7, 13], 6), - (10, 4), (10, [0, 8]), ([11, 14], 5), ([11, 14], 7), (15, 8), (15, [0, 4])] - - values = [[1.0], [1.0, -1.0], [1.0, -1.0], [1.0, -1.0], [1.0, -1.0], - [1.0, -1.0], [1.0, 1.0], [1.0, -1.0], [-1.0, -1.0], - [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], - [1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [1.0, 1.0]] - - for idx, val in zip(indices, values): - if isinstance(idx[1], list): - for i in idx[1]: - factors[idx[0], i] = val[i % len(val)] - else: - factors[idx[0], idx[1]] = val[0] - - return factors - - -def largest_evec(m): - _, eigvecs = np.linalg.eigh(m.asnumpy()) - - return Tensor(eigvecs[..., -1]) - - -MATRIX_SVD_QUAT_FACTORS = make_matrix_svd_factors() - - -@struct_of_array.StructOfArray(same_dtype=True) -class Rot3Array: - """Rot3Array Matrix in 3 dimensional Space implemented as struct of arrays.""" - - xx: Tensor - xy: Tensor - xz: Tensor - yx: Tensor - yy: Tensor - yz: Tensor - zx: Tensor - zy: Tensor - zz: Tensor - - __array_ufunc__ = None - - def inverse(self): - """Returns inverse of Rot3Array.""" - return Rot3Array( - *(self.xx, self.yx, self.zx), - *(self.xy, self.yy, self.zy), - *(self.xz, self.yz, self.zz), - ) - - def apply_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array: - """Applies Rot3Array to point.""" - x = self.xx * point.x + self.xy * point.y + self.xz * point.z - y = self.yx * point.x + self.yy * point.y + self.yz * point.z - z = self.zx * point.x + self.zy * point.y + self.zz * point.z - return vector.Vec3Array(x, y, z) - - def apply_inverse_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array: - """Applies inverse Rot3Array to point.""" - return self.inverse().apply_to_point(point) - - def __matmul__(self, other): - """Composes two Rot3Arrays.""" - c0 = self.apply_to_point( - vector.Vec3Array(other.xx, other.yx, other.zx)) - c1 = self.apply_to_point( - vector.Vec3Array(other.xy, other.yy, other.zy)) - c2 = self.apply_to_point( - vector.Vec3Array(other.xz, other.yz, other.zz)) - return Rot3Array(c0.x, c1.x, c2.x, c0.y, c1.y, c2.y, c0.z, c1.z, c2.z) - - @classmethod - def identity(cls, shape: Any, dtype: ms.dtype = ms.float32): - """Returns identity of given shape.""" - ones = mint.ones(shape, dtype=dtype) - zeros = mint.zeros(shape, dtype=dtype) - - temp = cls(ones, zeros, zeros, zeros, ones, zeros, zeros, zeros, ones) - return temp - - @classmethod - def from_two_vectors(cls, e0: vector.Vec3Array, e1: vector.Vec3Array): - """Construct Rot3Array from two Vectors. - - Rot3Array is constructed such that in the corresponding frame 'e0' lies on - the positive x-Axis and 'e1' lies in the xy plane with positive sign of y. - - Args: - e0: Vector - e1: Vector - - Returns: - Rot3Array - """ - # Normalize the unit vector for the x-axis, e0. - e0 = e0.normalized() - # Make e1 perpendicular to e0. - c = e1.dot(e0) - e1 = (e1 - e0 * c).normalized() - # Compute e2 as cross product of e0 and e1. - e2 = e0.cross(e1) - return cls(e0.x, e1.x, e2.x, e0.y, e1.y, e2.y, e0.z, e1.z, e2.z) - - @classmethod - def from_array(cls, array: Tensor): - """Construct Rot3Array Matrix from array of shape [..., 3, 3].""" - unstacked = utils.unstack(array, axis=-2) - unstacked = sum([utils.unstack(x, axis=-1) for x in unstacked], []) - return cls(*unstacked) - - def to_array(self) -> Tensor: - """Convert Rot3Array to array of shape [..., 3, 3].""" - return ops.stack( - [ - ops.stack([self.xx, self.xy, self.xz], axis=-1), - ops.stack([self.yx, self.yy, self.yz], axis=-1), - ops.stack([self.zx, self.zy, self.zz], axis=-1), - ], - axis=-2, - ) - - @classmethod - def from_quaternion( - cls, - w: Tensor, - x: Tensor, - y: Tensor, - z: Tensor, - normalize: bool = True, - epsilon: float = 1e-6, - ): - """Construct Rot3Array from components of quaternion.""" - if normalize: - inv_norm = ops.rsqrt(ops.maximum( - epsilon, w**2 + x**2 + y**2 + z**2)) - w *= inv_norm - x *= inv_norm - y *= inv_norm - z *= inv_norm - xx = 1 - 2 * (y**2 + z**2) - xy = 2 * (x * y - w * z) - xz = 2 * (x * z + w * y) - yx = 2 * (x * y + w * z) - yy = 1 - 2 * (x**2 + z**2) - yz = 2 * (y * z - w * x) - zx = 2 * (x * z - w * y) - zy = 2 * (y * z + w * x) - zz = 1 - 2 * (x**2 + y**2) - return cls(xx, xy, xz, yx, yy, yz, zx, zy, zz) - - @classmethod - def from_svd(cls, mat: Tensor, use_quat_formula: bool = True): - """Constructs Rot3Array from arbitrary array of shape [3 * 3] using SVD. - - The case when 'use_quat_formula' is False rephrases the problem of - projecting the matrix to a rotation matrix as a problem of finding the - largest eigenvector of a certain 4x4 matrix. This has the advantage of - having fewer numerical issues. - This approach follows: - https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.65.971&rep=rep1&type=pdf - - In the other case we construct it via svd following: - https://arxiv.org/pdf/2006.14616.pdf - - In that case [∂L/∂M] is large if the two smallest singular values are close - to each other, or if they are close to 0. - - Args: - mat: Array of shape [..., 3 * 3] - use_quat_formula: Whether to construct matrix via 4x4 eigenvalue problem. - - Returns: - Rot3Array of shape [...] - """ - if use_quat_formula: - symmetric_4by4 = ops.einsum( - 'ji, ...i -> ...j', - MATRIX_SVD_QUAT_FACTORS, - mat, - ) - symmetric_4by4 = ops.reshape( - symmetric_4by4, mat.shape[:-1] + (4, 4)) - largest_eigvec = largest_evec(symmetric_4by4) - return cls.from_quaternion( - *utils.unstack(largest_eigvec, axis=-1) - ).inverse() - - mat = ops.reshape(mat, mat.shape[:-1] + (3, 3)) - u, _, v_t = np.linalg.svd(mat.asnumpy(), full_matrices=False) - u = Tensor(u) - v_t = Tensor(v_t) - det_uv_t = ops.det(ops.matmul(u, v_t)) - ones = ops.ones_like(det_uv_t) - diag_array = ops.stack([ones, ones, det_uv_t], axis=-1) - # This is equivalent to making diag_array into a diagonal array and matrix - # multiplying - diag_times_v_t = diag_array[..., None] * v_t - out = ops.matmul(u, diag_times_v_t) - return cls.from_array(out) - - @classmethod - def random_uniform(cls, key, shape, dtype=ms.float32): - """Samples uniform random Rot3Array according to Haar Measure.""" - stdnormal = ops.StandardNormal(seed=key) - quat_array = stdnormal(shape + (4,)).astype(dtype) - # quat_array = ops.StandardNormal()(shape=(tuple(shape) + (4,)), seed=key) - quats = utils.unstack(quat_array) - return cls.from_quaternion(*quats) - - def __getstate__(self): - return (VERSION, [getattr(self, field) for field in COMPONENTS]) - - def __setstate__(self, state): - version, state = state - del version - for i, field in enumerate(COMPONENTS): - object.__setattr__(self, field, state[i]) diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/struct_of_array.py b/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/struct_of_array.py deleted file mode 100644 index 67ee1dc1e..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/struct_of_array.py +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Class decorator to represent (nested) struct of arrays.""" - -import dataclasses -import mindspore as ms - -def get_item(instance, key): - """get item""" - sliced = {} - for field in get_array_fields(instance): - num_trailing_dims = field.metadata.get('num_trailing_dims', 0) - this_key = key - if isinstance(key, tuple) and Ellipsis in this_key: - this_key += (slice(None),) * num_trailing_dims - - def apply_slice(x): - if isinstance(x, ms.Tensor): - return x[this_key] - elif isinstance(x, dict): - return {k: apply_slice(v) for k, v in x.items()} - elif isinstance(x, list): - return [apply_slice(item) for item in x] - else: - return x - - sliced[field.name] = apply_slice(getattr(instance, field.name)) - - return dataclasses.replace(instance, **sliced) - - -@property -def get_shape(instance): - """Returns Shape for given instance of dataclass.""" - first_field = dataclasses.fields(instance)[0] - num_trailing_dims = first_field.metadata.get('num_trailing_dims', None) - value = getattr(instance, first_field.name) - if num_trailing_dims: - return value.shape[:-num_trailing_dims] - else: - return value.shape - - -def get_len(instance): - """Returns length for given instance of dataclass.""" - shape = instance.shape - if shape: - return shape[0] - else: - # Match utils.numpy behavior. - raise TypeError('len() of unsized object') - - -@property -def get_dtype(instance): - """Returns Dtype for given instance of dataclass.""" - fields = dataclasses.fields(instance) - sets_dtype = [ - field.name for field in fields if field.metadata.get('sets_dtype', False) - ] - if sets_dtype: - field_value = getattr(instance, sets_dtype[0]) - elif instance.same_dtype: - field_value = getattr(instance, fields[0].name) - else: - raise AttributeError( - 'Trying to access Dtype on Struct of Array without' - 'either "same_dtype" or field setting dtype' - ) - - if hasattr(field_value, 'dtype'): - return field_value.dtype - else: - raise AttributeError(f'field_value {field_value} does not have dtype') - - -def replace(instance, **kwargs): - return dataclasses.replace(instance, **kwargs) - -def flatten(instance): - """Flatten Struct Of Array instance.""" - array_likes = get_array_fields(instance, return_values=True).values() - flat_array_likes = [] - inner_treedefs = [] - num_arrays = [] - for array_like in array_likes: - flat_array_like, inner_treedef = tree_flatten(array_like) - inner_treedefs.append(inner_treedef) - flat_array_likes += flat_array_like - num_arrays.append(len(flat_array_like)) - metadata = get_metadata_fields(instance, return_values=True) - metadata = type(instance).metadata_cls(**metadata) - return flat_array_likes, (inner_treedefs, metadata, num_arrays) - -def make_metadata_class(cls): - """make metadata class""" - metadata_fields = get_fields( - cls, lambda x: x.metadata.get('is_metadata', False) - ) - metadata_cls = dataclasses.make_dataclass( - cls_name='Meta' + cls.__name__, - fields=[(field.name, field.type, field) for field in metadata_fields], - frozen=True, - eq=True, - ) - return metadata_cls - - -def get_fields(cls_or_instance, filterfn, return_values=False): - fields = dataclasses.fields(cls_or_instance) - fields = [field for field in fields if filterfn(field)] - if return_values: - return { - field.name: getattr(cls_or_instance, field.name) for field in fields - } - else: - return fields - - -def get_array_fields(cls, return_values=False): - return get_fields( - cls, - lambda x: not x.metadata.get('is_metadata', False), - return_values=return_values, - ) - - -def get_metadata_fields(cls, return_values=False): - return get_fields( - cls, - lambda x: x.metadata.get('is_metadata', False), - return_values=return_values, - ) - - -def tree_flatten(pytree): - """Custom tree flattening function for MindSpore tensors.""" - if isinstance(pytree, ms.Tensor): - return [pytree], None - elif isinstance(pytree, dict): - keys, values = zip(*pytree.items()) - flat_values, treedefs = zip(*(tree_flatten(v) for v in values)) - return sum(flat_values, []), {'keys': keys, 'treedefs': treedefs} - elif isinstance(pytree, list): - flat_items, treedefs = zip(*(tree_flatten(item) for item in pytree)) - return sum(flat_items, []), {'treedefs': treedefs} - else: - return [], None - - -def tree_unflatten(treedef, leaves): - """Custom tree unflattening function for MindSpore tensors.""" - if treedef is None: - return leaves[0] - elif isinstance(treedef, dict): - if 'keys' in treedef: - keys = treedef['keys'] - treedefs = treedef['treedefs'] - items = [tree_unflatten(td, leaves[i:i+1]) - for i, td in enumerate(treedefs)] - return dict(zip(keys, items)) - else: - treedefs = treedef['treedefs'] - start = 0 - items = [] - for td in treedefs: - size = len(tree_flatten(tree_unflatten( - td, leaves[start:start+1]))[0]) - items.append(tree_unflatten(td, leaves[start:start+size])) - start += size - return items - else: - return [] - - -class StructOfArray: - """Class Decorator for Struct Of Arrays.""" - - def __init__(self, same_dtype=True): - self.same_dtype = same_dtype - - def __call__(self, cls): - cls.__array_ufunc__ = None - cls.replace = replace - cls.same_dtype = self.same_dtype - cls.dtype = get_dtype - cls.shape = get_shape - cls.__len__ = get_len - cls.__getitem__ = get_item - new_cls = dataclasses.dataclass(cls, frozen=True, eq=False) - # pytree claims to require metadata to be hashable, not sure why, - # But making derived dataclass that can just hold metadata - new_cls.metadata_cls = make_metadata_class(new_cls) - - def unflatten(cls, params): - aux, data = params - inner_treedefs, metadata, num_arrays = aux - array_fields = [field.name for field in get_array_fields(new_cls)] - value_dict = {} - array_start = 0 - for num_array, inner_treedef, array_field in zip( - num_arrays, inner_treedefs, array_fields, strict=True - ): - value_dict[array_field] = tree_unflatten( - inner_treedef, data[array_start: array_start + num_array] - ) - array_start += num_array - metadata_fields = get_metadata_fields(new_cls) - for field in metadata_fields: - value_dict[field.name] = getattr(metadata, field.name) - - return new_cls(**value_dict) - - # Override __flatten__ and __unflatten__ methods - new_cls.__flatten__ = flatten - new_cls.__unflatten__ = unflatten - - return new_cls diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/utils.py b/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/utils.py deleted file mode 100644 index 24850a314..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/utils.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright 2025 Huawei Technologies Co., Ltd -# -# Copyright 2024 DeepMind Technologies Limited -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md - -"""Utils for geometry library.""" - -from collections.abc import Iterable -import numbers -from typing import Optional, Union - -import mindspore as ms -from mindspore import ops -import mindspore.numpy as mnp - - -def safe_select(condition, true_fn, false_fn): - """Safe version of selection (i.e. `where`). - - This applies the double-where trick. - Like jnp.where, this function will still execute both branches and is - expected to be more lightweight than lax.cond. Other than NaN-semantics, - safe_select(condition, true_fn, false_fn) is equivalent to - - utils.tree.map(lambda x, y: jnp.where(condition, x, y), - true_fn(), - false_fn()), - - Compared to the naive implementation above, safe_select provides the - following guarantee: in either the forward or backward pass, a NaN produced - *during the execution of true_fn()* will not propagate to the rest of the - computation and similarly for false_fn. It is very important to note that - while true_fn and false_fn will typically close over other tensors (i.e. they - use values computed prior to the safe_select function), there is no NaN-safety - for the backward pass of closed over values. It is important than any NaN's - are produced within the branch functions and not before them. For example, - - safe_select(x < eps, lambda: 0., lambda: jnp.sqrt(x)) - - will not produce NaN on the backward pass even if x == 0. since sqrt happens - within the false_fn, but the very similar - - y = jnp.sqrt(x) - safe_select(x < eps, lambda: 0., lambda: y) - - will produce a NaN on the backward pass if x == 0 because the sqrt happens - prior to the false_fn. - - Args: - condition: Boolean array to use in where - true_fn: Zero-argument function to construct the values used in the True - condition. Tensors that this function closes over will be extracted - automatically to implement the double-where trick to suppress spurious NaN - propagation. - false_fn: False branch equivalent of true_fn - - Returns: - Resulting PyTree equivalent to tree_map line above. - """ - true_result = true_fn() - false_result = false_fn() - - # Apply the double-where trick - true_part = ops.select(condition, true_result, - ops.stop_gradient(true_result)) - false_part = ops.select( - condition, ops.stop_gradient(false_result), false_result) - - return ops.select(condition, true_part, false_part) - - -def unstack(value: ms.Tensor, axis: int = -1) -> list[ms.Tensor]: - """unstack""" - split_tensors = [] - if len(value.shape) == 3: - if axis == -1: - split_tensors = [value[:, :, i] for i in range(value.shape[axis])] - elif axis == -2: - split_tensors = [value[:, i, :] for i in range(value.shape[axis])] - else: - split_tensors = [value[i, :, :] for i in range(value.shape[axis])] - elif len(value.shape) == 2: - if axis == -1: - split_tensors = [value[:, i] for i in range(value.shape[axis])] - else: - split_tensors = [value[i, :] for i in range(value.shape[axis])] - return split_tensors - - -def angdiff(alpha: ms.Tensor, beta: ms.Tensor) -> ms.Tensor: - """Compute absolute difference between two angles.""" - d = alpha - beta - d = (d + mnp.pi) % (2 * mnp.pi) - mnp.pi - return d - - -def safe_arctan2( - x1: ms.Tensor, x2: ms.Tensor, eps: float = 1e-8 -) -> ms.Tensor: - """Safe version of arctan2 that avoids NaN gradients when x1=x2=0.""" - - return safe_select( - ops.abs(x1) + ops.abs(x2) < eps, - lambda: ops.zeros_like(ops.atan2(x1, x2)), - lambda: ops.atan2(x1, x2), - ) - - -def weighted_mean( - *, - weights: ms.Tensor, - value: ms.Tensor, - axis: Optional[Union[int, Iterable[int]]] = None, - eps: float = 1e-10, -) -> ms.Tensor: - """Computes weighted mean in a safe way that avoids NaNs. - - This is equivalent to jnp.average for the case eps=0.0, but adds a small - constant to the denominator of the weighted average to avoid NaNs. - 'weights' should be broadcastable to the shape of value. - - Args: - weights: Weights to weight value by. - value: Values to average - axis: Axes to average over. - eps: Epsilon to add to the denominator. - - Returns: - Weighted average. - """ - - weights = ops.cast(weights, value.dtype) - weights = ops.broadcast_to(weights, value.shape) - - weights_shape = weights.shape - - if isinstance(axis, numbers.Integral): - axis = [axis] - elif axis is None: - axis = list(range(len(weights_shape))) - - numerator = ops.reduce_sum(weights * value, axis=tuple(axis)) - denominator = ops.reduce_sum(weights, axis=tuple(axis)) + eps - - return numerator / denominator diff --git a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/vector.py b/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/vector.py deleted file mode 100644 index 85100bd55..000000000 --- a/MindSPONGE/applications/AlphaFold3/alphafold3/utils/geometry/vector.py +++ /dev/null @@ -1,255 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""Vec3Array Class.""" - -import dataclasses -from typing import Final, TypeVar, TypeAlias, Union - -import mindspore as ms -from mindspore import ops, mint -from alphafold3.utils.geometry import struct_of_array - -Self = TypeVar('Self', bound='Vec3Array') -Float = TypeAlias = Union[float, ms.Tensor] -VERSION: Final[str] = '0.1' - - -def tree_map(func, *trees): - """ - Recursively applies a function to each leaf of the input trees. - - Args: - func: A function to apply to each leaf. - *trees: One or more tree structures (nested lists/tuples/dicts). - - Returns: - A new tree with the same structure where `func` has been applied to each leaf. - """ - if isinstance(trees[0], Vec3Array): - return Vec3Array( - x=tree_map(func, *(t.x for t in trees)), - y=tree_map(func, *(t.y for t in trees)), - z=tree_map(func, *(t.z for t in trees)) - ) - if isinstance(trees[0], dict): - return {key: tree_map(func, *(t[key] for t in trees)) for key in trees[0]} - if isinstance(trees[0], (list, tuple)): - return type(trees[0])(tree_map(func, *args) for args in zip(*trees)) - return func(*trees) - - -@struct_of_array.StructOfArray(same_dtype=True) -class Vec3Array: - """Vec3Array in 3 dimensional Space implemented as struct of arrays. - This is done in order to improve performance and precision. - """ - - x: ms.Tensor = dataclasses.field(metadata={'dtype': ms.float32}) # pylint: disable=invalid-field-call - y: ms.Tensor - z: ms.Tensor - - def __post_init__(self): - if hasattr(self.x, 'dtype'): - if not self.x.dtype == self.y.dtype == self.z.dtype: - raise ValueError( - f'Type mismatch: {self.x.dtype}, {self.y.dtype}, {self.z.dtype}' - ) - if not self.x.shape == self.y.shape == self.z.shape: - raise ValueError( - f'Shape mismatch: {self.x.shape}, {self.y.shape}, {self.z.shape}' - ) - - @property - def shape(self): - """Return the shape of the Vec3Array.""" - return self.x.shape - - def __add__(self, other: Self) -> Self: - return tree_map(ops.add, self, other) - - def __sub__(self, other: Self) -> Self: - return tree_map(ops.sub, self, other) - - def __mul__(self, other: Union[Float, ms.Tensor]) -> Self: - if isinstance(other, float): - return tree_map(lambda x: ops.mul(x, other), self) - x = ops.mul(self.x, other) - y = ops.mul(self.y, other) - z = ops.mul(self.z, other) - return Vec3Array(x, y, z) - - def __rmul__(self, other: Union[Float, ms.Tensor]) -> Self: - if isinstance(other, float): - return self * other - x = ops.mul(self.x, other) - y = ops.mul(self.y, other) - z = ops.mul(self.z, other) - return Vec3Array(x, y, z) - - def __truediv__(self, other: Float) -> Self: - return tree_map(lambda x: ops.div(x, other), self) - - def __neg__(self) -> Self: - return tree_map(lambda x: -x, self) - - def __pos__(self) -> Self: - return tree_map(lambda x: x, self) - - def cross(self, other: Self) -> Self: - """Compute cross product between 'self' and 'other'.""" - new_x = ops.sub(ops.mul(self.y, other.z), ops.mul(self.z, other.y)) - new_y = ops.sub(ops.mul(self.z, other.x), ops.mul(self.x, other.z)) - new_z = ops.sub(ops.mul(self.x, other.y), ops.mul(self.y, other.x)) - return Vec3Array(new_x, new_y, new_z) - - def dot(self, other: Self) -> ms.Tensor: - """Compute dot product between 'self' and 'other'.""" - return ops.add(ops.add(ops.mul(self.x, other.x), ops.mul(self.y, other.y)), ops.mul(self.z, other.z)) - - def norm(self, epsilon: float = 1e-6) -> ms.Tensor: - """Compute Norm of Vec3Array, clipped to epsilon.""" - # To avoid NaN on the backward pass, we must use maximum before the sqrt - norm2 = self.dot(self) - if epsilon: - norm2 = ops.maximum(norm2, epsilon**2) - return ops.sqrt(norm2) - - def norm2(self) -> ms.Tensor: - return self.dot(self) - - def normalized(self, epsilon: float = 1e-6) -> Self: - """Return unit vector with optional clipping.""" - return self / self.norm(epsilon) - - @classmethod - def zeros(cls, shape, dtype=ms.float32): - """Return Vec3Array corresponding to zeros of given shape.""" - return cls( - mint.zeros(shape, dtype=dtype), - mint.zeros(shape, dtype=dtype), - mint.zeros(shape, dtype=dtype), - ) - - def to_array(self) -> ms.Tensor: - return ops.stack([self.x, self.y, self.z], axis=-1) - - @classmethod - def from_array(cls, array): - unstacked = ops.unstack(array, axis=-1) - return cls(unstacked[0], unstacked[1], unstacked[2]) - - def __getstate__(self): - return ( - VERSION, - [self.x.asnumpy(), self.y.asnumpy(), self.z.asnumpy()], - ) - - def __setstate__(self, state): - version, state = state - del version - for i, letter in enumerate('xyz'): - object.__setattr__(self, letter, ms.Tensor(state[i])) - - -def square_euclidean_distance( - vec1: Vec3Array, vec2: Vec3Array, epsilon: float = 1e-6 -) -> Float: - """Computes square of euclidean distance between 'vec1' and 'vec2'. - - Args: - vec1: Vec3Array to compute distance to - vec2: Vec3Array to compute distance from, should be broadcast compatible - with 'vec1' - epsilon: distance is clipped from below to be at least epsilon - - Returns: - Array of square euclidean distances; - shape will be result of broadcasting 'vec1' and 'vec2' - """ - difference = vec1 - vec2 - distance = difference.dot(difference) - if epsilon: - distance = ops.maximum(distance, epsilon) - return distance - - -def dot(vector1: Vec3Array, vector2: Vec3Array) -> Float: - return vector1.dot(vector2) - - -def cross(vector1: Vec3Array, vector2: Vec3Array) -> Float: - return vector1.cross(vector2) - - -def norm(vector: Vec3Array, epsilon: float = 1e-6) -> Float: - return vector.norm(epsilon) - - -def normalized(vector: Vec3Array, epsilon: float = 1e-6) -> Vec3Array: - return vector.normalized(epsilon) - - -def euclidean_distance( - vec1: Vec3Array, vec2: Vec3Array, epsilon: float = 1e-6 -) -> Float: - """Computes euclidean distance between 'vec1' and 'vec2'. - - Args: - vec1: Vec3Array to compute euclidean distance to - vec2: Vec3Array to compute euclidean distance from, should be broadcast - compatible with 'vec1' - epsilon: distance is clipped from below to be at least epsilon - - Returns: - Array of euclidean distances; - shape will be result of broadcasting 'vec1' and 'vec2' - """ - distance_sq = square_euclidean_distance(vec1, vec2, epsilon**2) - distance = ops.sqrt(distance_sq) - return distance - - -def dihedral_angle( - a: Vec3Array, b: Vec3Array, c: Vec3Array, d: Vec3Array -) -> Float: - """Computes torsion angle for a quadruple of points. - - For points (a, b, c, d), this is the angle between the planes defined by - points (a, b, c) and (b, c, d). It is also known as the dihedral angle. - - Arguments: - a: A Vec3Array of coordinates. - b: A Vec3Array of coordinates. - c: A Vec3Array of coordinates. - d: A Vec3Array of coordinates. - - Returns: - A tensor of angles in radians: [-pi, pi]. - """ - v1 = a - b - v2 = b - c - v3 = d - c - - c1 = v1.cross(v2) - c2 = v3.cross(v2) - c3 = c2.cross(c1) - - v2_mag = v2.norm() - return ops.atan2(c3.dot(v2), v2_mag * c1.dot(c2)) - - -def random_gaussian_vector(shape, key=None, dtype=ms.float32) -> Vec3Array: - stdnormal = ops.StandardNormal(seed=key) - vec_array = stdnormal(shape + (3,)).astype(dtype) - return Vec3Array.from_array(vec_array) diff --git a/MindSPONGE/applications/AlphaFold3/example_input.json b/MindSPONGE/applications/AlphaFold3/example_input.json deleted file mode 100644 index f2d8f326b..000000000 --- a/MindSPONGE/applications/AlphaFold3/example_input.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "5tgy", - "sequences": [ - { - "protein": { - "id": "AA", - "sequence": "SEFEKLRQTGDELVQAFQRLREIFDKGDDDSLEQVLEEIEELIQKHRQLFDNRQEAADTEAAKQGDQWVQLFQRFREAIDKGDKDSLEQLLEELEQALQKIRELAEKKN" - } - } - ], - "modelSeeds": [1], - "dialect": "alphafold3", - "version": 1 - } \ No newline at end of file diff --git a/MindSPONGE/applications/AlphaFold3/image/af3_structure.jpg b/MindSPONGE/applications/AlphaFold3/image/af3_structure.jpg deleted file mode 100644 index a8e383f913b2ed2b3cec8f11c30203e65d8b197b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1669512 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�RbEP}unJ|$36JZgQalMf+!Nn+=UNKG35T4) z6Gg^oQWTSbh41a_lAo+ECV!nmKIh<$QdY@)>i+zr{6{KZ?vjfZ1~;4Q19i!!vV`{q z9@LI2|H!$carY~!^iL&kCz($3s4%=F;c=Ac7rLSSX5hF{wA5W4JF1*o>buU5C#d#B z(L(p~gr1K(qUxo2PxEQocS>4g=>&+my89c$Gt2#ByawpGMpY z%4w&3o8pX(a{CZA%sAT}A$x5^IJWyr8`UkJrb8}Qu%VPmvhpz&xBJ7B$XG%iEIF(% zDrKK`B(CxNEd0>3RJi673mXcX*DvWr1IBQ<4-- zIyAVQoR((^IspMbV&%}_D?(&Edt*El{{`FKMktc$Z;Zc@J*x_RWg7|ncG_{%uYPNe z@hH_n;BRs6<7!|Df}IUE>!&Z8)Sd^MXq}59ItIj#DE16KW^IM z;mE?o658d3w=(6@-oq)gV@`Vp0x$93KTW#?8@oSH}`m5 z2ItrV*L!HOY63qK6(3f%!0Q2VX76wI;=eB_CNg|eWP1FXQX%$MLBNIX0;C1B_~OI=d;QEt2So% zP5S>IYU}^~cO35A5)6QmtQ6QAAdnXFrHFj z)40BUWl8SuGJo`4&cA*l8Q@_ul6W%nOb=v!niwg62!--90lLp0ec^tb8c4A8J7Iqp zer}V3w6^T6)8F3&#h<3}y#J9I`+v{CzitPV`1+mCRF*um|EF$@g1QI+is4(NDnHuL zRrXrGsyQlA(RXSmNK%l`{PEp9om50g@%a|1@~^Mu4`pQ$p^LAG^e9W7{+(&~-Bff9 ziwVWqgngofIF0zT&G&=AGJo&=cLP(UsY2A^R_aUfe||G@=lEII)5-5lzhoEs0SB*S zV&(jksgjAa5C1d+RTG0dZ4%WSm9ss5GqHXQsVhBY?TM2-J!kk=Q@+F{hw)^bGF0xq zfAkwJ|M@BZyH-v~=d4?Noa&?Zmoq2GTEsES?Wb25{M#$~Bg~wh*%lXY_Bh1z`>gXP z<1k#jWF4o(Ljwt2ek9}Y#wP52X7QEG?)%k$IqU)PH2kdUpFPC2-^`Q$JSjgJhsF$} zNhOg=x0O==;@!kt!^fxo+0A9*{gJH0mOc2YJWhv8Rm=3|{z;9iFXIdqaK{s2@}C`h zG1Fficl`gj(@fbCr^Ar*PBDW*l`0!&iof`0=Z#H*(kc}9IL;*5ZBZnB4=Ox>{3`eV zsHgmsi-#LKH3sJ@P+0!N%kLr-85->NLOsg&FXJ?e%1_1-!T&`(4kE zz>S=I;ZGJK&Ie*EQ4-$${vxOU6u$Y7o8na8AG*O$%I@@ZT)V1y>PyEz4}AOW`^ny0 z?=Srptoku*@}CDzV5b#i@BY_zR@93YYMe>nPWnsi>_@@8)Aqsl@L&4c%yxQe;Q2qQ z7Lvq3@5I3$GZ1%H<9iIUBKE(}%DVAn8s{!h@#3WI^G7d;2#zdbJN`K&^jqxn$9O@d z-j?BlS$6{ooN+qkPu>K5ff7amr&4fHIIpzBUxLiSKZNtC!WY~(e2a2I>MsXgM2UX) zvt_%s|J!iaew{@B$D`l>nzo<$J}C|u;Ra5HTNe6r)YH9H?TO>=&*lol;BQKi&feAelL`7V3Y@Jzstx8}T>i|J$%>b~h03n} zqt|&lrkaHU5BC!c#vITt?`~BaEJteU82M!DoO%F>M z2AWVP|ApA2G!e2ta72sGw?)*VMEQZ(_fYw%fBq;yKs^bP__aTI9zO@;{_}3ocZWIS zXFbg&7+a=oO`)XtUBCP;POcg){@qAC_iMEFhgy9*U7p_*D$nxgg$cZ@0L_bbIE*YA zU;R1-ssE;6{CK!}wITX?Xy}smJd)J6##x*7g1)aT^;TPxV{bDgt61cV zl$=lFZ7;&WB`<=HydNl~xMUQ&wc>IsZ9>_jOxAd(?Beu6 zXI52cUD&!07js9dbNc(-B&OriRNfP3#V;M+p6-xt*d>cCU_djdRM1ieZr$)?XF1oW zTO`#7?Q3X{kIeTpo3stQBF0vx7S_nSf6sj_%BPO_tu!TuBq2x6*n$4TN9pUyRiu<_ z-Xt|VdmRkXnV*;{^GO^G_=DQtf!&i@7WcrHrK+>hC7xY2sVQX@a@iUW1*K>x`CX+a zGLy1#%IWOyl+!QE1}dh(Ur#$UJ;CNR4R}!(x@CoVe0dg^iEU{r9jEHOk)%bKP&TW* z2W@8Lsl?n(xzQ_3UHmN*+FLg{`^KZoc$PPRiN?e8ov7&v9=>!7)1`tcWyjf!r={~3 zmUSOS@V&mW`e~#mES<|O6NOTJZBd}fjLVp~p2=>LVfHLFJeuDk!RWNka7jfc(plVw z5&2yH>l#w)bo$fV1)$80mKP?~xDrN&irI+Ay<5*cxyb0vNe?s;Oysf6F&`B*JTuiB zq6(vP48{W5U0tSD?#kY%Jet}ns2}d6V>wMg#I9jI>YR><%hS&=bE_updlu)#L_k2x zT^5^BWC7N`l}~#ApPggi{P!#CfII+s01N;O01N;O01N;O01N;O01W(C45%Ab*{R=l zOpt$|so(?afh()i2z<;kVi)I!a7sG5)dq;s-!Y3Hc>#V+G+B^8Ul-+^Y5Cld#%bVZ&YMm=z+VJL(B#@K788ZLw|_=EM+B% zbyD$ySe^q#c+TLBi@XNPSX1bu671^xVL$E?xa;7dZLxl5(){qojuhsyX6{m(mF37t zMm~tbm?-UZruH2+=7Vv5=Vh*&oGyP#Y(Wj8%BZT zCfcBn6_?nqvcl~+nsRy{nb<#fIvrrjC_Mb!!92sS5Xa;sIMXQmnpG_t}tTg09td*tcTXW;=o_CnRL zx}AIz)|;bc!YSu^17E9$iwa8&L@2+u5r`;N?+{{RGyh#?DK0blR7E;x%gvZQ2>0{^h4hr?6`6M*R*2xb*Oz1@sUdKBQ9<>j z*3A>V;Mz;Rxp$+?_spn`GHX2JZMlp4c8$jo%kh!%QHN!h1ubaaFe%EmBg*Qh1{mK( zuN<=SePN9?Vvye0!ntw+!xqu}G^2KUUS^?4=Z6D#d6pn@7iASZL4zJFQ?4aCCeBJG zq8K%g3sZc(s{?3tg4m}JljP4e5T@WA*if%K^L7nm8?%34BT9dB@WN#W8A(bsljC2m z?EbRC3mkvHK@N}yAP;~6fB}F3fB}F3fB}F3fB}GkAC>_@>gGCs?SYB4wH<-%~6 zY)Y7lBx`izHX$6(y(0u|ppzfy-XA|%1;RFqpI2--mpib$x!kBR!X>9;ZDhYugtn%_GdcW>KO-9*W%ZpG)O6&T#Q2J!H2r zp>ja9_2KJw`SZo}BhM&RJpKk9*;=OPwZ2Tjkk|CZPQO#EQ#jeKK?^dF>uH+1tI_1E z6i6mTVJAre`RrD_-54%A^1TWS^&b<(f$QHV&I9rQmnJPv)6CY)XHs*YB1Cn+2MqA%Z)n%d;d{srO(=sfXY_jf0FFFb_etz)Af z#26+);l}cT*Uqp}Pn%~TUc;696C%J~byJ3~QfQTQY;enTMlL`40xd8;Ac+8A0AK)M z0AK)M0AK)M0AK)M0AS$XVn7~x$2T@CF^7`))_anPx_uv}g?u}ou=r&MeM4i{olVY9 zRs>fWpF?zxjwxXF7XBn=a z;h@MD2cLKoizQmlF+Y!Xvp1d&T9T!Sm!#eI7m%E==GpVAJ(izFCrM-w23XHjctKLx z4Uc)ao45Ze@eZ8A^>f6c{<@*X4&6gvPf$Y#$i7KMdg58qd9^%_#{)q|_V&%;fuPy0 z{y)__`$N_3)4=s_{Q&@30I~oW02lxm02lxm02lxm02lxm_^)7qAm95EHc*u4vX+Am zrO&iHn{7!PeQ-Iw#+D$9vcpQ}MaSF*V!`e)M(^B1iK_C^(k`UK@CO8cP~IWUoCWvc z&EjElTjp!jf*FXiEb%D*wX}_31!2m7h@D=mGKQ%IiqnsnZ3k-JVQ-@);0ug8jE41- zsCKfq1?l@J_#r8VYS1YLIpza$JcPpTq~h*Lc*<0FHHzPF6(IOU8V_fL|s=-KJccO1&W>c%LTa(0nh0j9!J#>N+W?tW= zvXQsH*y1cI0H1!7L!AChqkSU^wa%88==^HZCu8iqqH&Y+MHeqM8`hlkoE&W@1HO(s z<+7Rr0xBAhaUN^y${#xb0M~zLsrttjtHAZ|Th0RV0OSEM05AYB05AYB05AYB05I?) zG9UwQ>fS9%P zlV|6ZtdcYurWdAz-g(BXBr#ViVW-C~Fg#a`r6<2hTj5@oHOgPz$d&AVcTA6xxr30r zD^0lvP8*aaJ)+@q`dbehUwpbzGiA!YHC@pZBRVGC zTf9#%2K+c682|#{^#C-%zo;YcDxSI@R_vDO6OV_^&KkYyhko>t?NjFO+kyRaYcSv( zkPW~9zyQDizyQDizyQDizyQF&e=P%q*DykTG8ujh(AJn)baid>zzLpt&q(OYDh0#R zseu{Ra z%#zTXKkshAIMbHh37#Hp&fkg?|LH+yHEW&sH$nrrTS>AbDh+%4_`!wk_auJ*f5~A< z#kKlYWj)Bu$Swf)wH+_MCuOcV!F&69LOU?ymy^E_vi*n=O<+vG^8y$E7yuXm7yuXm z7yuXm7yuafml$A&UdrG1sQbw2%JR*(kCnj(@g70toGj-3(Ex0D{{#=JOS%){$*8my z4(6M_mA)ufLCH>kk1rC^%@qIcZb-JrGAxS-m(M(2d{J3RV@koWd|jxo6yW_-;Q;Rf8qE7 z&OH%7GRIHB)#3eP!;T$7w6e z4uWm-cvo6#2TmV7PF{=0`zE};cTRpgJ6GrDWA?xH<^NBG2K*i%Uw{FC0e}I30e}I3 z0e}I30f2%31Ounaz?f-pX0HKwX+!~TR|bo(c~OaTiX2by*5dbeRdA4x6CS{Iu zxH@Pk@Jdvae<~Gx>L2U9_*yT&wcwA(+FW0rd(X5^^zHVrH~P#3$uZrirSqDKQ|Fy$ z?vQy;#A?nov2>@8wo?8hjW&cZd1#jJ@wHiFURnWGOaJAIHCKDBC!WR(ER82U$@H$5 z%Q(UNDn4<1K#9dX$1t~O&mQQ77&%__TAPMk#n^_X>v~(spWu1YVptj}n7{4~VH9XZ zNe4lmmi3s@2A7Ne-MX}e^Q?zk2A#FH4#_dX%}Hc6R4Pm4x}>Z0hhH_3-?h7X zxa2+Xn5BA}ziIGB_2w416Lzo!rgffr@!*U*YQEU3NB>sTgR=@X+e;^S7r~Z6?IX+b zJo~4cC0>33$%@GM87y7TnJ^=xni?{616KaASYXjT-PvrcK7|rwE|eN!qNh6 zbF%SIdy(0Ab^Qa6*|E^Nub+oQGcKw~iOsp?*egS<;$$TCA1`NjReSW_TP>Sh-vr_c0%wM4N=Km+)P|J=;J$?f-O) zXA6E!m_0am2$(5BLBw02lxm02lxm02lxm02lxm02ugxi~(!Z z;j63@JV#VlWz3;(f%iE77UiFBX^DLU-VVVr?gQzCP(EcwO}; zcoX8q$Sy_;d#xgl6xcwG#&KW5v6txyUQJGsr7BPIR)D-K(-8NX*Q?_)^<&l1OpJPv zI9`}Tc~VKVdq>Ut*=twhH#Dj$z~8>2Ka%>!TyD?1<2)*)L0L4|lRobWlRnkBZ5Nwu zI7{ciSm25df5$q`H@rdmwf;|wk+ z7z_ENf8zx2Wl$GO-&czrt>BIPynR&ovH_AXxJurRHNuXMjnyh1lQ)NT7q#&?W)-V9 z+#fySMTy-o7rJPJqQWp#t7}4%Et!yzSF7+$^Z9Yi&X~bF?_ov`)J+Q;BpU-$ZZv;h zhO5|AY}`{0x@=AZ{s45S!BpBb#>1rT@K&;s^ns-XuU?6{{}cYZ83+2>{Bd`4A4YAS zO`iOCYs15*vXV~mg0}Suo(di!4&J0W=B6zMCTlJ~a?{#D)GG#QBwyk~veb`}$X}OA~HiZuM_oN;w)=YV^ zXC<8A6>Ie59x4puJlv2WL>XA>=XGMXTli=g_Etekp2QS^--OFhMeC{oDVAI=a9ZA{ z3dxgH1sSg`JL)~bt7p7MWRy>$c=$pm>zhau8vPDF&ybC|IUU%=5t^qYb@w|RhARvDr<02lCgi73_w0Fnb{+Iv z40^Pz)pgD*q&`pEU%r)JNKLD*THSLcIuO(=`V6-Oj}O-Bf8Xu6h~T2;P%e-`d}bP! zYjQCl6T7KjXY6u)gpirY710eIw$+peP3fn5tg}neJ9J4H#4(Z;--}P*RgjBz*!1GZ zR7nImzRzh&KGmeTXt>y%$dJIhb%N)|UBa&Kc43><#eE^)QC?ah#Hg(5oiW3m=8p*s z{Lcj7)LZ?G+k3%<#R`kHRfqO&A)2M=iScLAr9vGUJ!8^t;Ml6*!h#xbAfrH8!1YkH zUeJUSv?Z&+UEQ~kF)mvx!_|Y=6e`%*i?!r+!M*Y&sGu#7lb@+6{y=rwr&{D(vdgTF z<)?tyY1Y8K)e_hAgSPmNnhp5J2^|qTYtM%Gtq+HCK`AWo?&T@hS|&fsZ*$TM>;6@e zP_(=#*D8szW&g4ITAn;%94}= zKewTx)ZW==5F3yS@+WJcZs%SAb_ENHxbUiUiL|8E9q<$gPW>4cy$uE=~?{T_MYBs16khTwFag zOdFHB0aNmQ8~my=K2JeMhLW~#wj``r(oDzom15P~8~5y5$6xdxHPFDxRR;U{IsKJo z%9Z1df6J4Q{hmHSflIBwIVFk5Ns-|DJ*4;VbnI3!{T>jM{QI70Y7nNSr#LO%hoo%T zj{}eKotkl(_;x8_Sy`>@=i}cd_Wu%71diXI5byzH05AYB05AYB05AYB05AYB@ZZG% zzD?~61Nb1b#hST8~gLq%?$2+NQyJ35^G#-3jPx+K3J!$sd{*qG;G2X~J7Vp|F z5cp3D=Uknx0cvyK1;3=`yVGiq^MLZcl^sReGr0(~wvi0(Xrl8zP^;!*(idWt;iw z@}+OV@m4m;>AAl3feTh!8|CO^3yWZll1r<~%Lc+4C2`;S^C}3cvvuvXl^R-`q={g% z)kLA7mnv4@vTn9t8p+S`qv^24LYm9Q@heqH)~_bVI_I0a)}1a}*&1{TGGKYZ(LWTU z_D}7Ef-}Ep&=TB%B4Ctcbnc7=V69Lf3yahTdnmtP=9tgJEFiD^eo8Q??5YE)rDHC)Rmwqk4W7Bj zop*H-f)>o=M{`Gb-UYwo&96fj_Vag}TbbAL8I?^b>rVI^*axGOiBN3=&JkkFWa)86 z)cfkK#@W}kKN6uX#?8w<+-MX)Bd3ZtrzahA@1QFciF4)dgjCWxUd`=O_LLx&*T{aq zpM=~{Y`RidcNBXmz*}|)EW5EzIQroIa#*g;Sq|?3qw3OeLk`P^@(ypwr!k>Km%Pm+ z_CNZ&rN#7S-7X5qP7U0JWf4h8S(c{Qb6T0j2n@dNsg7zKiyFe5;H~yg^O7W0vdg`? zdXNj9={luz6#exY{k#Y&!>fCE6Wfa>pZXF&w^|_9`9-Vsy`4d*nLuqmn#C;j>mlhI z?slrccS%7K#l2H>AO!*T=x}@~kg34Fg8`X}c5-6BHFx1ie|i~XJsa%IkJ7anQVBUx10(%3$J#6=DuJxtmZk)~k@mJy2s zFT`AqijQx!Pr`h6H%8=Ub#I`aRy&Sm!y7JZ1T8>iKtm$2Xp6yzB^_P^lyOV@O0ib-K0vcfff{F zT7viv?9M=Vsax>p-~zay)Pp7+Ig-H+!=n^!@Wr8yqbz-He!#3c4MrEn=V+6R00h$`>5|qYovEXEg4WjXn zpUrV{*pg^)FGo4*=*&`+b3v6L{2Sk{k-m1Ep=WOfWgteg<@++u*JK}uHK;k6i`(Wt zcA`+$BYcEYbf<`$`ZNmpB4i7%2S*$G=sKaANiS-5eyzzkZz3RIXCUM?Za`IVcx|ec z!|{0o@s}5V!*&MA`_`ynei`B~mHCZrT@(D1#=M$_tZjXJ!3`oAFO!RrLkE)Yu_MwL z+QOphQ1NQ1DIKuYtK;V_xP&R9L+=fLn27OY8RwM@W}DS=l#Cr_qy<7fl=Hai1kV+9 z#?>zbDV@N`-k>ufy|mw2z*9i|oYrN^AkmBL=!1y#)Zmk{%FDVu(;O}QtMQ7f6~-*V zrgK9cy{N$TtaneAY(b1sjMqP@A2^GBIJEHA{^ou5xSNp{BU)n3GV1)TcB7DTaX`qo zG=r$an!TOQo+uH8kRy{Vs$y{%PYX!X^IC#%;_}|UI{N+^ACLUL@8|nQpT|94|6~Hl z#Pe!0@?bLKCOxmrEB6(soJ$oSG=6EJ$Ek_1?hsy;B^JZK(L-S1pk9(proDXT4U-+^ zUItFfkL(>vV3b!lXSo+JZL`5lk*I;%VzXc_5%q+qH%Czw5QgRX*;tMJ6?5@>{tmMy z@kWfs^qfL2{tgREw^D0f?|=BaolO6z$&h3D^WGnG{MPl_nQQk3Qgdv7KK^0P(jWIQ z0oSMhVkrf<2XFx}05AYB05AYB05AYB05I^QGjMjyyH&`0SKi|w$GcT~{%G3Zf!-hQ zNJgwPZ+h`y%@146Qv>&TH?OAtV}n*f%ar1wZ}PIPlQWpedxql#PkXBQ1n*<(&Nsz9 z8s%+K8^tP%l0`90CaM7r8y}yNq)@%|OZ4oQr+LV_cFVEeE7~f?Hrtq6@R>j_s#k_ zDYN-QEqBLhrOoQ=O(i4A0ud$j@!PkFm%0tEuPjwVPdP?~Un1J3bdf7OZZlG{TQ!I? zx|w&I7qg-^tp}}GD9z81hMP<47R2P7{lpr-FqQD^Y>ihsy?$1^dM0|MgL+Usdt$uU zI6h0On%KU?PWEPry$2GRvczOkPLGM7olYLlmvln<5K^SDsfI_2CuT@lLQ|H?AOW-w zo)ePW%%DDE!P+p*?k8b%TvT&&HLAq}vGq!c1p){~EFF6K;-*&T&?X7haIMi))f#1d zgQBz7ZoP=4A{oo8q;=zoTj{B_GGJYDf-BS3r5F%xLOXajc8vlOE zfo2l2&cg(Af1~S_?bA`p^pF>V1-pWa2B71?B(4!dl7utmR#O!S5O1R=JhNJ5N-95sDF^c zC0D5ouH(B8NoM{MmPWJ-KOz~k(XpWHl3=wOnD<|6yeAXklVtf}U?(rO_Z&Kp%QFra zks=jis$)b_?im%}qM$xW&^pH>%T*`gM=Rfy3lkybi_KESu#STn2ZOI}q{X2$;>i;G zFh9@8XOGtl}>QRCh+xfl)zSj0h;vqTDpqgu+IggDm#@cp%sN%JlP300MQY62FAq+u+kh*8zhV2a` z`7;hhJ%^yNhtj2qvvb~}bFqkg^&FPbrF^)A6i8*fD^O32X)LhMk1-Yzk)u3{4UA%F zH7v?29CAcw`Ej>UIt%25s*{ST(`eIOwQDlB7=B_cI~WE_R1Y$D{s0exQkhBz+ozLh z_0A>s2Tyefg_SuhD-v9VH8{&9P7zq|??cEoO3`!HG?zW5Ao%>F-o)PWPo8G^z4|7- zL!!eEIWH?$bof1KYSjI~qY1~J9>&O1dx!%k?YKjwID4r>W2Eb`Z^m3F$3>6Snw+E3 zV_ISPv$;(6+#Mb#cvB980^SqoE!PH5Bfz?o>) zVu6ex&n~bSRYh-Km$k$7O8e8LpylMD+l}$n$FT#uoqAM0<#Sb%j$bh!@Us!>So|Eh z46F|aYK?oIa_y;BWX|+ry^2SX7|d_eqWjBYy{bo1Ps;$G%Iz@<FZw$=5&A^}c*7 zZ3E$3;*DjChUMwru*k|D9pqmf_Fi?Gl7sVr0$4+C7=B)(htM`FMU-(j7h+9UyNo9Y zIG^FtB>SgqFKY(9?wbr6iiM~eqCG(xDg!YaorHQJdUiSIJ-Fs|p{h9>ZM+(RmaPRT zk!ytI86D=9oD{w<()fPP3RAJ6l+YdE+~VXy@6Xmt5UmLcyeCnerfM8v@)aYjM%kLE zeS^%XxW^yb9=!)!A*eiz1`zq^@-V*CCZ%gLunc#q)wTmQgTqG{7L?!CVn8qD=O~TI z#A!;{0p)RK_eojfd<4na}c|13U-NV;6 z_xsd2e?F@HZN;Ns=3C(S_o;z^YyjB+3;+xO3;+xO3;+xO3;+!Ls0@rC_6QY^7ZJ%$ zxA!jSp5RqM7eIf$RpdG2W!r2(%oK;)&SxOy_4F6df^zVX4Oh{p_y`*MYZy;pV%Lpv zT;>FinO!Uk-nJWZ_ouCcSf|2+kx^Xgn$b@sV*aTh3*A3ePK9>fz;(S~!gakkX8^}@ zoc~?OiXhtMj@%Hh_6F=Tv&@;qq4baTJDdHDdO5FuXqa zo)>7wH99_;G@)3GDrk}IiQoZmt2O7tupE35^@SCQi)0Eth9`K^`g6!>|M)|Fb%w6_ zihdVCy4I&XYxu!Y9gac@nPp@Jr2G!5Jp;$h4`8t*pcfzdZuD%Q^|Q(z^U-eNcqr0c zqS9EGQ7Ov8x|tcKu?KM~;qzwTLYWf*jvbC zhMJO{v1W}L<5zp9lFs9YSG|Cmi4wZ+-P>`)H14~+2~BVjyc6LQVW64+Qj$cF@GW%z zHhYWoo%;I9v9$f#8(&i5pX8vgZzGelM*@7SKYpHYqKGgcIPPV-q}n{rp+oJa<2=&j z>51`g{_4tFx9g(vL8bMx;0~fQfzA&@7>Gb#IVcjmdd$D6s~o)JNO`VW>zjyK=#Hj$ zp5#(zmz;lFnIvNr9NK4E3{NC}uMx@Y48r6jclDDfwrC4W1hpP8_UklMmc1j&5!r`$ zdS(^mWXW70ah|JEk@MR)uDHR&koOS};zhT+wa2)ywNcun;yAvr)mMp=>rzFY^&@%e zC$4!HJl8cVr7ovgclz3jE=hi5M0RoR9;ja*gK2jS(HxNH7WM^}g5VpZ0cjhxZ{Rbm z0d$Fw=#eG5r}P6Q_Zm6_r;ZI7iS>vg>d@Ep8E7=MBib=)7{257z9YQu?MFx`waqm_ zX(dZsFnSaGQtVC%hR`o9gx)P|(t2G#T9m+B+7QYiN!_ofd<1>f*h1y+Ykrij*3VB} zl0Z{n`XqpMCZfsfxyKXCBTNgdK$*Ja)}agK7HU-))E-_qYA2mM;foW)(*Q$Q&y!)j;nuN4(iDUIJRvB-B{rR#nf3S}`I>T4UU43wx z^TJgoSI8`hvkUmL6GyK|twQXOr9!ZG;Ku}=knUIUlOoFJdz{vE1T7a!K6dd?3a;u$ znQu19IEh+Gin+2CWXVy~!=>*jKuPFe?ns6%1!!j^=}onc)&f$vg{Gd7*(_9WKB^u1|xvtmoo8#PSwjm)?PBq2$8)O{JkG3%{7+{Pdl> z^@@Y2;Ecj8ruyaOy^L_dm+2RJ$E)w{K{~Xl-XP5|1c@3tAvIps$gNo5nVP1o7 z8c`FS^H*R?UtX3#pFBagWv7N$H!L)zv*N{rg!OUpDN(NH6{FX$M}%A4O)Y2~4*V#_ zcT_CK%-`Q9ByL|=dWYDx)^AmOq}jWwfrE@)U47$ZCV=3^B(Gk>J^J(Y z^9QnY0pz)PL_cjIeG~Oa6%tXnG^(oo5GI^R+FZkmxpzpx-Z?O*^PG5GP&uyV(Jf#0xgx-R{EbvZ^{iHoEz|nb##3ketk7{>GepaYXVFM8 zW)D$+rQXX>*B9Yp4%!A6`t&SL@J!zxVlF!9OR-Yt^{5-B=_HZdW1^QQ(5L0EkVPpD z@MS;LKo$G$xG#!`VtAXqhy%hq(H1W5g#}3rH;k!sz=bWl(AH23?Sx&^E|MfW{fJ%D z&e~74rKAB{<|@Q8a@Ug_3?8}??i6iI1YQ9>&+$lz7U;AyxGFyt%XMmk5h|v5Hb_fk zgrniMl7s#@T>yr{F)6G23ATmOADXP@m26SjlnJ+r7*Qxqn)k^08ZSCwNXDmxpUyguaMn$w+}IO2t&) z@G7qC6OmyvQbi6XlRt^7qPrSziL{PC8dqlKy|~SPK)2EINdrt;#cG_XJ<>uhO$~Xf z`xN!&7`NC+q}sqprKf@>j!_Ir4}~sv&FZ;X)2OCqfdk&{NinKrv-M+Rm)LlvmUc)T zwZ9*KE-A;x`+a-yZ&eKlzMuaisw$_f>zxyE)O(*%+VceO+W8tJrQ9zke_O)v7sCoT z;#Ly=#`pv71F``a02lxm02lxm02lxm02ugD8MwQA`~X*Js}edtCU-230P=yy`ZI<+{7%cC-|X>Q6*a#*-gkT4FB@2pX@GQpjQ^j3qC_^Am1<))D9I=7bC(I&pFKK>+^)rD2*?|>1TzFf5cr&ZRfJG&4qd$j1FRF83yX$N@I4J2Hv|Tl4 z7}3G>#W`@v%9Q6$T|v=+oXoWYmMN8+)WiLuj0z!+DAj}u%H56iEngUEFc04HU4Dn4 zY3?1U^zEz0#X9S;V;g0f8w!Y{3PbY~yrJNc(C%hX($_g zHY^(sCg7S#lt>oSy=5&$;tuljlY42#3$-tf1#L;N^&scSATujt5y;^94NQi=-xgIr zSC-`Kn^V#BsJYBn=u@`O8S2~oRY)GdVL>N&)JR(@^PPD4>UIuxJi{c=N|El^aQPuiZP3Ie7A$-^j@2i2xEb4m)b35y(Nw3(1O;w%JHLX3}?!FUsM{niovEp2<^{r0K-`D+6ASqveW&RuWC$akpg^$`f->Mo*%lNS4YW8$q#An#nnHcjeiqErbK{!3uw6 z+Tt_sRcbjW>8vJ4QOi*miBM>4;D&pDHj>o6S*_DTS;%}12G@GI48@P1Gwvg#@53yK z3g)vsnvEhatg15YX?=mV+IWuY!PWmu%HV8%{b5w5Qk{XC;KYzftH=*o0*H|emU9(yDMvIamWjF6B?oh_4_zLIOWXCvZ_eU> zj^ec!tX@`?uM}<(C@>O6xK|syls?!luz0W*s@3aUDJqSts&^@-z7n1;e`P1^1tI0O zSdiyYMplDeu&ojleZ=@&y&qm+leXzmbdGYn*0)O8&~6G_p#2aTpyoO4(^IQIIck|F z?r2h_bx`KN_Y&ckBd(}foRhh!O9IEV*A0t|s|`2R98S*~YKn0jWmH|8>R><^*ZR$b zI)jLgh}3gh+?aA@s|D6?xR1Lb&K|)%EEs(!&#zujFzI`-K`IJ*TXt0CA^5EE90e)j z1W&)l7Q2UOs=0-Yds=!%i*$+}0qh05w2$9CQ1tKw$?Yf7rx0H zE^`R2+L(#MgIuj_gqjyhUtvG?MN636WE4TosHX`wm3oou^Rw4&+PFJ%e#(1`e7d+R zzBDSgtj!Is_Bc%SsD89;EY8Ee#o1GAWD{j0m>2(OCR~V^;g#-t(enlK2T-+cQ=hF8 zu2DzIxqZ*(wo`=F?#Vuf53DbCR}T=q1TA8{sw~i#qj$yHeKD8P0)tI5hE_DZZU@P1 zBx*-j@Qg52c(zb(DmG7yoZvNxkp;?eo}Sd5U~8UwrMr7bk=Lk=gw-do@zF(>$q;rb zO}{dTOFE0AA31v1)7AJ<11S}L zPDcbmIj3YkoK(mx=a&6=w(DB#K#c!ZIlNz-i@>S0RF=D$!0UO?7j`vkS?Sr>l^&#p zn49@i3o$CcAv}}Ij1@M}jG`Vx7rX=Nef%^kP)Ewm>8&jTwP;!V_O;-)%IB>YNdODkqMiF9%EFCW=nte z-uPxUaAt%Wdtb?Q-689{@4zh9Z(;-e@R4SwFh?L2X$ zwXTBu&<-ov8($Bps%p}FN2btmqHf1LVa=Olxv4qoExHifEDyP_+mAFf3;ND3hJ&W4 zw}iddl5H%H-~|Y}Tcsb(ry1}Z4AkX91P#_L7Ce!pmlE~zRJw7xym-fd!~5kZSDMGA zWJjqU+%9%gkjnU}Boms>ee5_&yU!7t5oHqLXvRFp zzZJ`IG`)Brt1MP_8|`N5cm$``at3?By>2W}X)N+slC2tsMN{(E!b*hmUI&VbUtaB< zr^Gf*P6X+*qR?`#+b%))r(96BS&xMFioNW|%ZC?XbdX1yb4PP*sTdAmX5bauN6Sva3YV(5~xJa^5?!s2n-I zdJU|p`<6pKG1Oj7x=dw&D{h*YSwqPhk?OHC3VL$K=0WQ=LY6nUAHDXRJJAwUH|q4g zd}bcUuPs!7Rwh76?*GS1a{sZQ5^xQ)A_5oy7yuXm7yuXm7yuafYX<%=TABQm1ia!! zfh$z0b&PEJwF)M^?LigH7T4V)EQ(2WVCJAXK_OzD*4jg8@Mv`n7v<;vH8lQ@v>W1o zDtL*UzwFto51XvrJ9B&Bg%IsOB^m$p|8R*rzcl#t9NsJ2DemKpk-Nl8$`^df;1R!v z+<|e29WDtiDpzWz@zLG`L}J&{bCHWxU6E+Zns&~FHMg$Ubuh*Gfl2}Giq$J@m;0Zk zYxD`D!qXv>Ti+42N+8Af8iX2U+Z@Ayy`M}hqKB5U8XL|Q61f5NISZdIj z+m0R-3Xq=H;<)jVS{k3L<$Xc*OsR_((gsWcn&h2vp`4*&y8h6!)8mUV=@o0rW8nFSL2 z+PU&5*Yd(dGE0ot@{(4~;LJ}T4>HI2zHm3Wjt61H+m%dCUoG(Vt2U?u<+5@4EerKVz*vp<+^_eYw{8-OZt~;fayOySQwcNbjrAn0l)!321{k*zk;tw095tLEEAllp)P14%?`7qyEq9DqUiBz!> zf7iQ8xrK4C+yEFl*>_(tuCz}Sm^5s#()@PM zD(~Ag|A7@1<@^N2@j0w5x->$x6mCdgT=X_Ptd)Ovu<9bW5;DJg-&vR|D>$ni%I77t zIL_mDe8wrlf;o-0rsgWs#1XWwM)>$s4aR1OHJ&k?dABBnf=$KR? zmU=)B9_p}xnfD&57ru;Xa=ao0M-FL(`7Q13dFaqS!98y6VeioCkVqQ;d0iyW+;%{N z4ydM3EVQUk{RPn|MAFz*V6IBKzDBIHQYnN7=7D-|QQ0E{T7OJNnnb+C5+EsI5@_O2J ztI60t9|LArw4eypblbESGl$)xfg*0V_>xQTwO*@nDdQJQ#H!fpgX>H2^Dm!bgp27~ zdpo#y5e2RGqmtV8F`O}RySluQMtR-uq!qqyiiovnQC(twl^BeHr1kB4^iS6?VZw_0 zxcELce={kwe$Pnk)LwA$G+kg%0f|zC1|QrfO0cARJTs#=KF;Y{3mE!Y>(J#DY#0QyY;?Whv`LHfHKWl*`8Oe~o=Mp03_F*zhF{l7+0Lh= znOc%6xfdiD&6s&5-;SchA~~mq3L{wnpL*^o{Y9A?=1}VX1k#*bf@-M{G0X+~@hzV7 z)$}tFf$&U}@UQzFQgQ{@d|AC7b&*s7)b4T52o(d%9zEQ>%q@s0?%=Jp_6=qe)!v1D zbtt&LgT2+0x-z^Q+EBl5xww<_F-|yHd&zqEgY^yNkQQreZ5cAWy;qcbPCBJS*d}P{ z2>;b3kV=P_N*1;}-`q)#@=4~@gZ%;IyV;V)a~UM#fzX%A(K{l;Vd4F0O6U4{yHB zrXE`lhR9nRo#nw*TTlYEC9x|yyE<`)dz;rGH-@>yl%P^z;>JN?H+Z@AQJ&QJL>6TO z$3vKqYtAn3it$~$*DD}#w(2FhZ9`Sg1*mxXdYLa&f-Gv7t6ze*mD+svPz(M6RVqdsdaEgK*<)Mj z4x{f;2<$HlqHaT6{$K39XFyZywk{ki ziXs+zRRIMeAYhOl5di@asew=;B2pq9B($I?Afc)B8tFAer1#!?m)?6zfI#S9);fFb zz0bP$+ZEfKQmlS$tCVO6s-@fFMTVjH@9fGHfq*2(P!;WQ@+aO5z$y-|DDv~G_wYmL5R^D z3@Q%h3j}@*TDxj-J+V$(68AK_CBliF9(|VYGulG%q4^q3q(rX ztx<|szu`>+M@~+dxk3vmyx`rd*qoMVzW=!~N7Nqt1uh~= zXv98h-*tK*o^L*X=i%p19YS)>`Rkq;rMC6%4Z6BhW@#0vi+fT|A8XU3nEP`$uYk(Q;i^M^4?b}#kt%UsE@eR}lvT;pTXTLA&N${#ty`n^_aYV11%CstF zNq%f!2hH}Viu)}b`%YlEUZlgAb6K%C@F50){&ghxY8k2@22mRyM)T`*D6&Ax!xxJD zX`k$2*&2II^i}*?ms952X}gYCP-gQ1T=Jv-LKo8EO&+FbW|irn%bXCtlBe$(rQm}; z`N)M&b&ZVjpu}8ol8tDQ@eh2R#;|N!0%}#F=C@{tKUB4o|H4W8otmS9v@Pw^1QeD0 z#}DuSyzPv9oopuc02*xBoSDSfwtC@ zNgdO0^lpIrx&PfoI@PkEc4F&P;?}JM6mYS#?XR8Fe@HRe_WzaO!iV{IgJYx_JZqxR zM<^z-(e@^hz-ic(0=x)s}gkyR!{G=MjKr$fr`rfA6tU!6dNIQ+zahZWQ^V)%-xIa~2Vq zV(r~x?y4`l=P2#fhvY_hC>L*ig;pdyoFj@ zL3r9`M$BmiC{cFDH@Y6liBBrL|HPy)V6#+S9@xQwuMCvr$!MqeWknNT9UKGEnnDj> zckg6Nv@m^XBSYKn*A~Z*{@KuDYp8HXLIkmF*mysgTY#g5AmG2h;k`aS0Eg7MBca1M z_J#mRgUiohxientPqQcL4DaANeRp%0cypg#(X%u%gF=ZO&fO00!pv2#_{AW#wir-4 zVhX{R6kDDVR}EM$&#dA!^;4RWH{G81OHx)F`zF6Ny&$ZwdiY?yoWsFl~r zEAI5EU_DgAQ}{i=i|aOJ)dqbT9DwBpe#~fQ2_MelVsRgC}c)))LF^9xcLj@w}uz^ug=Z|^}Ur-VvQ06HH_C39!!S7)Yz{KEpHVoMYx z_$TgjV?Cw0J*p)RmE%{Q#W7NY;w4|&!ZJ9PiYyf4V8)M z)3{E<92`lh=%UBe8rxxOnsV`}bzzwM){-)6DS*-(+Nf!hmw+*zTVelosVa>A>*$@; zcy|8oc2}Q|$8?L*mvW!pTb+ za_G~Hn3*E=pAFvx5&`fIB!?RShpzMU;61LPvEI1%s$9U5PZ<@q7%8Xh;Kq!y`p2sBekE!?b%9$p)XunW)>iwDR>= zHJsVsL@zx|7c=usZn3Ql6~jMzs%krn*92ja z(W}6xAAQLHY6}Lt(H}?=m-0Wa5iUXW4A%mIgOcEbyJ`JpBh5OpOHuo1OP2X!p4Xz3 z>hn8J1kp5HhpxQ-amPm6%pn)9Q63jV8HVHZPh^g?HvsL#UP(-CH|0HZ*)J!YL*yt11|7OdwoVb9ENjKuxA$xrI0=WVBD0`Eue)T2x1S~ zEr!kJ{U^UAQ6AyV>c0N`Ij4FS;^X$}=-VMHUa7;G`8kS8uDY)3O8(6B&tb>%#kljr z+^L^;6BEH4e3Clhl#H)05m6--w^>J#j;kJw;K{ggZX7a|v*cNjR4z+e&{vok!mnyv z5T`OTJht@koCa|xYHvBxA+bbSJ6nU-s-5*4{QI``K;At!?Si(4U>7{mF?xw>bNr51 zuK)ddhkfm6sb$Z0ecJm!Ja>bH3Mj7{?yZ1v(YkqJc8rZmw}@Y*D!Na{R`fTUS1@l0 zmRV)zs^H4p+#;}SN!&8Jap#moEtE#W7x3%pZke;*=kxL=XyAN|+GATHFe;wr{d~7XxEmH>ulV( zFm@ z7ImE-lfi_uz~+DUnwfN3ah0HV|-m!5uOaL;@^ELbl~+2888 z=VjNTKr;&sYg`jizkM_y8o0*x8a;22Kdm;D1+A+Q z^qLqhm#oN3l-D%r+OaFy3}n-Bc5u1TnG&J<#xSzoVVtdKG~;4wqw&IsC5NP~?!e-( z!7uG5#*C2ithc-Tj#7F0Y>OLfHdSo-$&}k!Dt8v2CPt0;Z)2|~IOS~RYdo(fzGt-< zwbWfENZPp$-uqh5WiRtOeHsm??9e4q8GW(UDT-OYtC74MrHh9CI;u~yjg(VpriHq7BxsX?(( zukEqpDmxR%d`(jLAeC3~b{V|7|L)D3%@SS{bEw20J}x zzf+tW^oouX)lrhOQt!$(#FJ0;b;JD?Z{mv~7S@57`xE?#tO=?`&yl)oZ5=PHmSP~* zwyA{a;xS{zxRn?A2f}-TJwH>rpeDsWAxdL$KO7U8)hv(JrD(5U(-3J`&64! z73fqj4##-1wg6``?soJ@Qzp2r3knZ(4u=c{4;L8c^9`pMNOj7nsE2qyFy%nQ*)1^c zDSKh-f_)I}VYtV;gkY7IsT?R|gBdonPrh)KfGzODUOK<07R`Xo>i=l;VTKXw`vB9kP5}a1K+kM zd?7840J8ep=|6?hyMw#UD+5B!!e)02vDA4u8rqd#|bkSy$(~hyH2%afNrs9}GL75xwxKfyNDMn*}VV zQLAn|IFH)n8$Pc^Y@dz)MT?M~TX$c*f*FENW;bdY>`jLvAW*5Bl*C9 zM7BAQ6gA+?QFzyD6yA~Rbh|af0v5CJb)kNAL3A?@AifV;yr2vE+aa|5-WPvS|8#+H^2)T^J)3+mT=W8 zJ0PDQk9nNi274KD{xo2_8HZ4R92kjZ!G(pHd{Cs_omeAGO1P7x2sLZxm6>qAmQX~W zDnUj|q^&12?cTbhfQeQT5>lt`0F-!Zy|n*S-`0RD;IqE%^W6NAK%hlWKZFtMh0)F3 zg7)u~cqK3UB~JoW}edW5J;?`CBjE z?SlIChZ~(Pe^vT0W9vy0bgHj>bHkn8%PqR#&65}OptX@w)2QRn+7=LZgYui8mC<{N z+Ml>sqZH<_YuyK2%i>q^!KgzCE0tWqZ*l?dE3zlI@%`)D{-dVt=4EHS;rHx44>} zO$Osp02c2Gg0cp!s3A&stnnKk-dtn8j`zUXoSq#m zF;$>eI&a+(s#NnY|!xf6CazqZ23FV)vDuxjpLF+=t@=N${_XKOu=PW9oGKp#w_X$ z>W-}Joa27`GQswH9IGzbu^AUJu@@&%^GqF}zQ}}Gy7`|Ej$CJfrNe~}Xtz@Dxx<5p z#Ce}hIT*4QcE@T36mS?yXhO%m`T3ruN!#j)*dW)Cp^0a2;uKmGlr{|@^!Bx(<{IIG z3ne1o2R1Jxtt=VD#!iMRNDc~4lNgLTO~pzJH44S{STu>t!rC^zS+%6v-rT!X*GJh1Xc!^WBkbTDw^hIL|@zUFb(Pvtjpo88GeAk(giAXbCFkeEQs%i}e<@ z849+Hp0KDpEZyhC1xk!m(O1U9pUp~hA-+u*;!w12l}B$Fx=-!`ndS>CcoE_&tCi-x z%vTAU1I1v9u~2_G?jhb7mccHg8S2dAHG*{J#S88lyQ=AnU)p=HBkdddtEBw~7Kabx z2#vfXRt!_c%i{z)p7Is6_ev`0-6c#TE$RB%rPz1jciXPOtu?#{_8XgvZAyz+2E`K5 z!Ytd#15?q^ibCagzpifSJ52W*T%oi@4tH||DX56tK}o9x+Upi-vm&y`Ex{%wJnZ*r zgB?w((*qRR)=in(6;q4r40XKJT_8Le=Hh`r>mAIZ^t`rdN8v+q7q5q|kqZe&)Y&^5WJ5jLmUUHC3VCH^pVE<0s_%Y>D8;a7KfvTLS5 zDyRDcyCTiNMsN54hiZi)Bl1N}MHCT|(U^vKGo}%cQ1)X< zcESo&@;dqh$+Wq$lf=$*v0C2?%m}1RzS0Qn@_Se9zam9M(x{ZV6E&2~-9YAU_$T>5 zY~KD0sfO<7n4HD+tDF-k9Dx9A17XkJE^ATnNP=aDT(qvK}_dK1XT~)CeF03OS zBc>Thonie7)Y^qrLBfoYQ#O}&d;7MfD%Yqx0)BfdGqh%d6y`4_P$Z^-<;%{#&)uhcxe|g^G z3E)#DT$yqJ>Xia)|Pa)|ZUNS160 zvMIi==rO1k-Y8ov7iX86Ico`>=wp%w<5wgF<7TC6r z$meP+xK3&WH~QbU8}ZbnznRpQ#!P3b+*0J{|QJ^^fv9y?-pR16oW_ov8M7a5%+1C?aVmJDB$k$f^FMXv9V!3Q${qYw3=8LOiHq}Ge-h~=U)`9`XJeTtHM?IP-|IdKhU|SjvL$J*b{4oc{D%F>|Mq4`O%mNelGIJ5Iu{fU zx%U4JSsMa*M1Tib$d4u)gzO>YAw!-+94E<>DtVRpz3N0>8_B@}IeH?8lfPA-$?-L* zE>B(?$r%_qZzLzuWMtugGSm|7zEWp-Xq&DOuo+Aos1K_1efiR)mBg!Sf>?D_o6e$A zcoUL@kf2Bcr5Wdi7@yW4=2ET0Fi)lCFDv1n=Pdn}7tsfkfH`_=6Nmq)c@O zQ(l})w3g5_edk>73ImboUh?Mitt}@b*SPdcujJPTpmS@V8w5+PHY6s0N(Xfd&jg*4 zkGomklUTgRxg~bEvTa=Bq7Rk+XnGdrAfLL8?X>lJ&&AI7 zCf-$wz6Y~7=D-IuqFvKfg8LOga21^2#ssIg?1b%#fmGl((LKaYHmXsH#TG^ZV-^Fu zD9lG>)z~AqBR?2V>3M|jQc}At=A8hjYQDeXI?S}{eBK~LVuY&V7sX!B=0c0<(M_ml z`%N_=ZRZ3=r7{;xy90gj2P#FEEoUB{;k1o|uNd6<#X;Rt2HAO!3m+m&1w6s%oQ>> z;A1t6%$3RwNPaB|S=mK>FD&V@<|uCb)*Z931Pk%F!BLn)%ynW-XH$qkZamM&jVh|x zHMpyGh!&u11LTUkhZ{uBp!y08LnBjS@sxw4ANDL2(uw&7!vcCA>dhV}r0acv-p49U zMoYH%ms*tSKx9~djz50Ct)X*uhmieIDZ+-zM%AHH+%?6W8YfUEVwsvBaf{1PaK*5; z-VOfU;s(*at|0ts@zrmaCI<(_*)VjuDfRZ(E;C%7uKZr)!IXpG&d%vZ_qhQh6j;M$ zU^3GPyEEs@7V6n^tv|Y{rGaMmU-R7BmNERn z^UWjkEt~C&>9|GT#{v$>*T`w)Ajj1GdhW5{@nGNUA-bXajU?zqpUro2xHASxTnzMJr$gR)Z3Ad-Ok5cpso03kYHLi_%|MgKP2gYIW9o*gc z_O)i(g|E40`(M3&o&wEkgy;JJM{()OBRwQ()1E0S3Xl~e1;%J;*VC-J11>M-<=pdO zw(nzcI!ovqvGSEW5~v@AjM?T-=0m77qZvTC$rH3DfGz!+QObOCr@0?lCg)?jJGrF> z7L)N?wd}YEqBumRL`OrKfRn*Lh~kg8HFx;NqcD7tc3-+s8C05ue(?5!(Tb)e_%-aJ zo8gb5Z{ysP6>no^C_uZ14|R?8f8-)Je;t88<**N5(Gd?CSE$;l;8uQ5S#i#+ewunG zv+r2sxSEZR=p;O99%s^b7RMnrUEwg`2A;;AZ=ao5f?PW8mW3H;2!uG89YhuDe_!oh zERep*XM@7@?GNT5z`I=yW9@F($?`YwH4D+C&*>ELv;^2ENNd5zB^WTOS5qqp)v=gy zO>NnQHAOOKI}`ujR#*F{k(}8`xtnA!SS&3v;b!0_g|{4q%?xO!;(ph9*~4_kM2%U& zPTCb*rL2YplSx74n}Oc%oPk$((_XP&T*(EJNboJ9}DD^HdRLh2y;gYpwIOsxtHx7{>KRi2PpWQ&Bzn_?wqJM>PT% z)xsaR1Tr{xg+CDA797-szq|PGeBR6c?vne5OM|AWmCX{e`-9-SKJ#0b8l~xfv^#Rw zD35DPXd0=x?z6rXFY!Pi9Wf~rz7a16&lo4R&r^#}@*YGjKNx#FsLZjODRHHyctuUo zHI>gz@>O|n?}LM&J0l3NAOmfm8wj^HpX_Ms_|D6Cxq>iXaiy04V@7x~Bm{iVzbxeX zs-m*Xfg$gR?UELt1lu;AQcyLANq_EI9`N%pV_EBN>94H-g=ai*;t*gKccE9z&Wa&+ z8cv(<<;yZ%)_EXeeboXt^f|N6Rm#1`LRAuzkAGdX;0#285-zjPJb13nCnxvof zkqTD$no+&=M`o<+hW-9SuT7mfQPFfPQtt%tb+0)K;q9(EE!Sq}4N!VCM!-Jn9Wka> z>N)yeaU+%jdOXmbHPkHeS&&o3F)j~vHETv^8+HYYwhSKN}JC36F@_aQxj}* z4`kbdt&#aV4ELm%fV}g>f0v(T5&OX-Z<0C&%2i;rDX$BV|p97%p4_LNc3Ft zVBOzW=6^AIE}rfnq1UP#8s;QE5&RzYzqi{p*E*K@5N?5%+ZfJm*CeCUiyVM%8sg9Tty99Y>`#!TDbhW=LUu28ef3VOycHMmb7BSfB!`7 z1aRrq!9i#Peo28X|QN9!?N{q8O|iC{O!{itOIv7H#CBu)DK(eX%| z$HIz)08x*AVl?shyMO^RJ%Li(`(ey`XAZ0dNlZRjtCdr4VIFotzu%SpFi1&sJ8@aZ zdX{>vtnnC``|~f)1^<3m(YO@%Oixg{QRNpf`LB~LPxdv9)Co=-D}5pa_^6eZWw3>Pam+VQnFuXcfpO7 z^%d-1A8M|k3A*#@8S6R z#{VhWVH+Eb^@(HC4r^T6LmJBprgkbjc0sulid8lLqt@@gzYYY}MQyvVM!I<(hbBAP z6^Lv|#L<=MH834UHGDtN+kHe*AXGT#?YKY^rF5P*`#DS()Ofq1bQ+GaGY&UVeMZQ# zDet(rukJNc&*vT>DjzlH38uwBQ*K*1n%Ih_r$i42nO?shKT%JrhM3lirt78sZB0!1;Q*!qq4))|F|!f{hq4+{VWx zc6Pc<5=o^b-4oSs<-T!cK_gMiD6<9cs!w3zLEOC7)dOi|&-!2F+n7ChoJ7?IP&3Gz z#4k%<7;anXeMAih@MuSUdd_0S&%ksYqYdIihG#a0?JM6Gcwu=zn!O27}q~RRBhbg^tH5)x#1tRHT91kF@lk7Jxt*BY^w0#VO|6GF6 zLPOHp8x+{4Z*xh@PHG5 ziS6+T=>RP+Rg4Hw5(shcHuPg}XF^-79}i%D;lO=|>|43B6GSbS-J{bU_!Gc!J}n_m zDlPR01Nm_R=x1Ty5}sn>PN+ya3<2g4HBJDxO!o&_4T5UWdzZiiMYfgen^I-L$MTx) zs(Qg2OqSxRW#;*~731B)A`%W>_eXQdA4d_@5_tC^NrdgD*ZyFP;;xDd*z*K1d$ke@ zSTy@P4G~o(Ja$;`k>^3x?!)J{x37SCchn434xScF_&C924d1=Qeo9~L1cD${{jf39 zPFPOuJtSulO1)%jpI&grvc=tzX_;$PlxhZvsrJ9dcSW)OWTod zYRc%GZ5$w_`##yB@W#8*LOG56{;Z8)$MQCNfsFegGw~lyVl)O)pOWI*9BCpGA|H1v zs(gFxE#k~@TYxKKk`kaF1#FSNbKJvrgJ^yU)PDHzdfFU^E$p7#;}d}A=aDh}3)`Yf zw+S3kGZ<8&3=$thi|@OC0$>TMFfBn12yHgyd}rheuoH6A$CW`;tQ?=#p={(8*fB_R zMCMKgzR^F@E4J@T-Keo`Hrq}Pmhtl#?bO8y?@9}S(}+aqace>U?E^K@ks?U80(_T(wH8V~gP6*$TI>N3=IwlP-x{Ov^&%F2$Sq$0{ zbu%tWh1hTW*!wuIxl*pX<49BY_4T6`yQJ4l@1_ik*Zt%`Nty<2)sVivSDR;gUY`Ie zyZ}hM2FdO738-1`2?n9}!*6TBz0#IUZ)zaJ$ z=iLo~^OgLay{QI!Xn*_@0~X~iaaRczW7J4)?oz$bz(g`!ui<(HWzx`Is0Ah%I&z>L zbAPQK;+@@S=oW1nu2x{CbC@NaA3L)_Rgaa0bAGq<`p|Ar@b*5Yq0r!Q!_#Quprw6{ zRjf|CgDIC-iB_VTgK0N7H!j)4cJkNS8q6sKuwqzjNOb@*w;@`q#tkK(SEN2&K3$nC zwBKE4WZoB~>0xN1^ZC^e&E?t**|Y@Ic970*S=r<<$e`5(x1kkA_iKf*nq3waToPJW z$abfwB+{?xwImvY%s#X`0epjJ&K#h-2ItzxBX?R$z16lZUOa+NBEBgJd+nIcv*<}f zLwa3=GjT^%VYCaN?wUsp@XkY!%d3dGQA^S6_3=mDAEOROuCeKgAQe$5Ga?o;(5r>d z;ru*+dc8oUt=B=fZ9yEt<9YYi%L-TquIp>A*cP(ocXI2`hDszB>ILONLy!3zd&+w! zeFC!vy3Dpzj=z-)!ROl_st?zKN~cCPRxwD{$0EZc-YV|&)bpLdx-?DXYM$wUO@-4v zG$Vr+ayHL&q&jz<$5sbPGq{$Ixjn9vs>p6OO0$b;IocG`K!q6kAIGMO_Mu`7{m(%v zqE~n9TaWsT_qF^{^T?YBl)z=8;pV2+Xo3jCA$Lfzu}w_ztD(Ja{6@Xy^v35wI%4Cu z6Tm=h3jc@$o1gx>%yhYF>dz&iqq3)xz#XL$uux4`$P{C;0_2*QGrDcBA7+V)DQkxt zGMKIp-ojHn74%4HC_jMcS-EHh$f_E^o_#ydc+1|9OFXV6AzXIBT-(7S-N7`5XsWZt z@uCs^Y*3H2$%-(?_n?A(FzLgf#hDLL>krnR>OV=9g7?;`9oP=VH2|+3sUDQp#$)0SF5Wc7sOb4Yx~mQ*C||6{*DXHm@A5Sl`i!P z`C(|x{P=F~t5|J`2u?=k_n-#E#^l{kZmw^;$CutM-C|#%6EqaV-JJfrRPXJD+!&WH_ zQ~z*a)1j*`JcF1L|5H5LS==7_BHsww-W%0l7#x_7xA)zfdm4RT#A5Xxgh4c`_1zLw zyV2o2(|eD_z>ey6;bmJ|CF zV>Y^`lKQPQpT#W@(3UF+#xDp{t=+bmws7Uj*^%V)#`tHh8fTbR53>gvm%a!bVPUyC zs=Z}V^Q6gRpuBSD>yej$r3qRW7jNPzh~@#;R0tFgf;!JH3+Y%C2-VIO)T}4HsmP=a z?(q9{@a&DO2KC((pRDFgk9@7y{S??0eMxD-{IVdMSegA^PW)w^&Zr_%0ryA27#p;@ z*($I_1#*;;&W{9^D(x7~Gg}BK5!6!CZE2*B=@nb4k7Y4-ol5(g@bAEZNu#-rw> zB}6lI)sFg{)i&4|;0BDwf#Y03*7#HHEzK=E#Q4Ve^Z<` zwz?w2f!cpw(;y|?Bg(<7CJ$Wy$x^!VD>XvfaG9vK52G>x59<_0P1BcD(CCij0i;9N z%;v)?)xmFgM}Ji#1SNS$1;)2$G)b~Cj6L3}JB~RgfPz=~f(p{1l4)^T#o;G_3k{(s zfJZAYJys&H-@x!7uY!W`g5j+D8S+mh9!*!Y*1Ihnyne=kOU8Db+CGHWcidupX>q57 zU8;^|=HAbrg^#U=B1SqBAD;z=zM!VvK9&0s8_a&R37kIx42gJ1C>gp!dPsrX)Kv5d z!1VD#WcBWGb86e|G#M`j5(tCMkpA@tP{#`lA_#euSU~^MkptM2*x0+ELP*Qd`?q7- zKNP@7=R@i>MQoo>Om5z10uX?fuevQ~&g-LHKjZv3PxyW%I|6^t&xhGd2A(GS?tNyqyRdfJ zneahL4%U{)TT|sg-?ATZ)h#V4dQw~V|CUGhzy0+&SoHb#!h~|g2eDG~#vQ;~0!EPlUkHu13$DsOnAT2cseD86 z(eTZC((rBN=GItnMSr!GHxmC9lv#g}GYW-rW8V@<(%Ap<@W(%NKKTdQ<5Z&H&k~#r z7E1J(HclJ46bg$k93cZ=goJUo)qdB7W0#nP<)!NUbBlR)|Y*WvOWvXk@6M(NG#Jw|dg z+6rcW-(uLB?p2;y%GN&>$K8ik@8`N~mw~_<;sZK|9Kx(3X{UCtGxEnCskpOgsKC|G zk@$C9;|yk%Y9V=QuqVj3e*iP&Ts$7p{IR=?so5dZ0WbSi8R5yL8u%^;OEiaVeE z5i9;jN5--|b17h>^sQwT=L|sBJFEp^WoL7HZ=?(2xuW5o_#$K>9%~OwNq#8ZScP|V zdj5$!)#_E82&ssM+7I4*{|}s`|GO?e|FG}%TSW~dDd6UCq|0tQtg5sN6szdB_(o`V z!+m3z_fKzZH$0f?(UUFYTtMx$4O2?y8mRUVrAx!}zK{E<6KK?6;>eJH)iFcu^u^f_(uPtub~zfzV(Qilh%qkp_;Dv8TfCIr3%p6^ zfw7tK7@KudW&`#Oe6{%%UmL0MT6I) zwmr_B0Ge(g^G`Wm_JNL$J zwz|(cSMu$2Lh5_W-NP(==X=ry(cT6&ew3+Y%DBn|2KioB?*r-H8i&5?%9exk!vm}wK? zfAfDs5Usp%JTSalbtJrMYarF$SZZ%TUnM}~X-IU&$eB!Y?~FX1a4&__o&av-D+e(m z8l3glKiXEU=?X4#@;cx2)_1^`Rc}q<@C(s)V3pRR6nZLlbCHGOBWfajLy1RF8Q3~w zsmJ$asD1U40Xy_?jZ_YCaI=n`Ql%V!+LkjVKNI&0+f8fK?k>;v6RV=gZu+V2o*Wk^ zdo4^7ImFILT?_UpwAE|=xrsQ|W_5vd5pWZSG%j@p=I=fQ$9jhGCMGMkJJ5+_CO+0M zpzbVXZue*$44zna@$d1rdn|DsnA77XXifRP8;FKtCS7k9a2l68X^Th(0yo>Vd9^r4 zBZ=7$8O4LGo&~9NCk8xvT5|#@HQ@7(LaeAS?Rz{2#^>f<3Ir~|A=26vTq|iM+Ob>w z3NB9dDtt*esb2vPS$+1j2hdjrSRiuFsVVVOBN4H(CaScv?nb?tjPFdn2;tAmEZ&(N z&i8EZ2JX))MJw9ZJ0UNX6ywmo_wwOlmv7Tw9&z1*N(8J zHz6~hv|baWrvn|8L}{U_8Y#ry19vzyELA#os>!!70AGI2hRI{ieI|$#cu|-*7(?24 z=clTlvHot(&t*3|ZbFfQ;^m>2TOfxhR?I+xV@go>_;ymX`cHA{Ysel$AU-?%DB6|9 z`$l!UF~U%W1*PZRVR01Zvmlh_Z@;A4??BNtbmy*_lyE&l)cT1?36;o8N&-ZLSnTi`X8QUFRG{YpcsgxR9v_JZ1wTtT5YR_1-qf}k;Swb7R6NAUGQ2o8Pz$n{gyS~71DUzZ3_O;8;);zx5)YR^-mOLKtxPhn7 zlhqzZuQP$ZM=Lp);l@Zz7GG9S>Ek>ad@>3rNMCOchN-6!7LlD*#jCF?bC=6q*yYro z>7GfuNnoT|tEtogThO7DxX(0um{Ze|(#omvc$#*fP~cW!&8)=@1Eup?V>z&w$PvTa zx%piUXn`9~%OFkTX9^Gad@15Nw_xG6QdSl%tU(Q;EtMj-WE>P8W?1)t&=IuSppnJ> z_ffP9590W7`xa5Is!YcOdAJwuIEO2sacJS3j)=6yp7K2v2~0Ph=bi!0z#@Trem7Im z28b@KxGsMIWMnP1(jF?@9D2w4(tBPRJ1O{CgL(8$rAy~f!~w^5@9l=#sL|ZMvc;B# zn3qgb(qkL#I=TIa1uS@-PU`l8dD{D%MBq%ihgQKEQ zH~wW)?M>9-s?Q5Zltk==B!YvM0I50w_|u8k`$G-dqVGNfv>`zIu*;@UcjfB@Jz>H) z)ik@)0ho~wQZuwJP1xF4bHwv@)*#PCbrn6lep=+1cB4mM;WCw=(OlZ7p5&`ap~|-> zfX`jT+G(DiHO>hpX=&~0iV@*D*UJ)SN6cdN_e8F%>*aPFva=8KvmN5+zriaCqeZjX zAHja;G}5O+7uT%HSMboK78P_SLqu_NbF4~M93c}fsIiIwwL!S+> z%~`Jf?*>F-yz9Xk>9;FV+X|%Asbuc-VXQc;`4V3$(yon59wklpB5yRO)Xim=VFfs_ zN_%%Y>mnu#s0Z?mOLF_XTN8_}b;SV-&wQ>k2{dd!z{W3{{L0c+y}N2K!`EGJ#+=Xh z%2M0TZHOTljd2PqOyr1KAgEAf!P@iY1=cZI`TaZhA$!!t$&jP?V$2XId_XUgEj1o$ zK(b=A>xV|r!j4M~ek-V>@E$>hIP3x4%yS3st zKfF?)N-@srY_&SD_85e0LDCX2sqM#JRG1N-I#f|J);_6m24>7HZ_%SL@jS)? zsHlJV3GZ}xW2HeH;+yLC45fRokTvOyKSa#_fZk26uuR!Hr$y~x4Rj#Y4oc9rtUo24 z1VP`c_!PV9Y}bO@bjl%~0Fpea!H00y^e5pjTE67Weu4K(%A5esR$V2Uu4#u~9y}19 zTPR0{TPCsx=-X%}b`3B(Uf>I=#xf(@!B?eOmxX2dBZWuerwZaiYYO7imehp|beq{a zDsM4h7wHXg?}DezS}a7=^w0GD+%nXd#{N2(+VJJ$zj1I!)Fc=zdb-%cq&uu_Iy0of zRT!_t&LYS0Qi2+z?^EaLQF3??bwp1jMImR~dh(?}y|O@S3HI_Y!1+qwN}S3GfC(YZ z5LE*~I~hcRYVYNzCP)s=38_9o1O$EFDYz8$Jbwvx`Ow0Wa5kuoLCQH>qmDgiqmG@@ z{LTK;I}xX1Gp@itce_2Dalfl6dlcZj$%d523ebj$e+hYuC>o9IPvy2lJDcrynau<{ zQH+~7!VXEX;mikUcmqOKC8HXOTzu~at55K{-#ltgG{axEY6NINeEh8a2JE$g{ z_CSjFe*C%TQ9l*7St%djw)=UNnn@fZ+6_}A%GcbJFG_r=s@HOq82^>2a{R>L)yvArv2`;m1k}sk_CS$i7T>=eHUBv~kps#_AE-T|%OZ%ann1KUT_Dy8#GexR zmn1B$H4NP!&*|BST3Y~icrn zV+i{?3i{MzriFS}jt}YDkM7PPLVjaE`ajAd@IW>wm9!3OK43Z@5UY<%c(>*b63QuN zDE1dKJHyve2l|yq<9DarWJwHN;^Ru)2d_u%?aUm5t-#};ZD;8ou+WSEDyU6~)QyJBCyJ1E)TW=sYF;GKN2|s$QYit#wqk2QGE0W zf^^d_CZP6$36IEiwEq#!FK6H+KF=>9uyxe-XOBK?Puax3KY!0ja)dLJ`y{$8IdGKc zALSI={=f2r`Y<1FaEvsAXH69P2*uL)l-{I+hG^K80EK@5hnB4mAWReVlKfdMjyk8<*HEK9 zhmd6WedmJ0uBOTjl$ZMTp~!|TuDq`qI>yOv-3PMD$tqZ{i1gxy|Bt=*4rpr4^Z!v) zR0O0+2bHFTBGPL_1*8iIq30@{&}*o%Tv})%0@6i#jg-(K5}JUaL+HKN&;tbg<<89A z-`(%d?3A6|nc4NPa}r44oIH8Xd0wCQ=l#9}S(byCmG|w{CROuB=yM4Sp@8zdgePDY z{HRj-i_Lc%j8cf0Srg^5FOmuq^;DsA*Is1Asb2ST?b+WN=|{VA&|_SK4}V&UjNL~U z)A<8hVAjA(6U#OCP}Pzr4=qT|ux6=h!BBe&NfGSKIREDa2RK9PaA)a5+3V|lpFLYO@v_q0G(L!y}~45V-6`HnHOwJW2@ z+?M%;(U>x`;KZko+mGYUZsUt`SH+tSwK!qYE9)CRp!tNjTLVRaGF;)@jn!Fhug_}$ z`IG|3q3#?y!_mE>T4U`Nzk1c2wxPmKX@L;i7*3Q}@b;iZFj}Ya3ZNQN-Luh_oMyBb zAa3i5$=M{MeuZ(FSq0ORAIh!^nAzJ))ricQ0@2xu)*eTl>@LPWPC*ab4`Hes>wD#v zk|g2B`hmr9Hxyd@H`MJblvf7bt{7(81$291<T_{cY^AJ!?+pG2YdWWu(T>3p$ApD?iZ|czs9>lhh zqN=g(2EtE%eTNaW&Azx0IupaCYmtuVokXzc0JT z6;<}_?uokSY%RwJGtx>5n=kuh^vmMnNiNYQ%C8&CtEyjXGFfukB9zOcvI2zQF}gNq z-mR+INulYsl)~^~WQdF5K|vq#=4_qxJ9*E-VjH94A^IVkbv93S1^V`f!$qg;)5^E8 zDWK|k{rY04ax0LE8x=;;I+5$WV#sV2$IUnG44IGzKwU>qo}8FxuaFt-Y|sp_y;AfS zW7QJ7;2+6B-yInBPO_UrS*$pjJ!B#-CnbeeOf?it)P%j3eP`CAnj=R}M6Q ze;MZw=+WaC9I7$d=hW9CXYd!aF&vJREUJ~c{_PSUxnvFKQRY}Em(h;hfOV)*nNgCF zcnn2Kt;skEHArlCs|5_-%7?zS9WoSF8vcsNLCW#lr7|S;N$e~Pdw0tLW*K^ox@Rc{ zXv7a9x`7csO!6$FN(TA0?sDp3m-1Voqf=Lh4-i*ZhlBVlx!la&Si3AKQFxo&pcx%& z6E%M+Rb1AT*ponS*x;nP*rKEB*DJa0PPZkQQs<_ZyLeb=ut~-`m{oPYF7yE|OMN)< zh0MT;yzrVqyW5&h(R87y2$}lI-s$6XCb4X+kdxcRbqeIHOr#8!nr%jjb!aH1;4V6* zam`S9^!X~^z(nG`C?tANZ@E^5!<@j0k7fujcaOAsRug`)Q8pFOb6Nvh;t28BqJ>(m zh_RjbjO3EC)FpPU1fwd-3?ETsmq&1ak#yA(JJAO<8~AB3=6Ch4!X8=F83wjKpV<#} z?an(u4JB}f^rn@DCoTkV5ET0ZZrVb;EpJ}ff9PG%AU%}P<0q7B`gJwwxjl{VY(s+> z=N-3}2@Wsjo>-tQgS#L72?UA;QbT7)`*^>T=zG4U%JM!73Wyn}Q6Vh4Vm@qUW$W<7Q{onbf zAcsdg`;gN%#UE{~&qY@uQgU>j@nU+8j6AY}@-`U8p@<-YcC9^4vNTRX7Hd$M9)EMO z`a4Nz8s(;_cbZQBxt-(E+{Nk$veq!(O+l7UXO|#ZZ1ws~NO1Wlf98y9CiHe3E-PyO zTWY82TP9xyNd=$H>%x#^hXrZFtSHcqbkxSVH4TSHY*b>MY`UlUp-7@H>*oc2;fZ1V zo6aM{0x#8V(NWWR3H^5;#;%y_P-r2xG~8mj5A7~o)}2ac&v7g>u6>9nBzG< z?I62`==tnoO)>@T`EYnh?2Sh2dtrUBA%gpIDp8~CI!#_fwQFEn1>w`(O|zryRc6Pu z0>7aa&X}S4ghfVp`5>!OrbrTuZxiSUjacZp&yH z)#aqPSf~E@y=h$G(YRVTnBT)%J12i+?5NL!eh8{HV~cND9Gw`YDwzvqf{4s0H(`ut zmC76JshrH2j^12557^Kmhw8VXeJniVTrQ(uRYfi~bOuX$>ZA>AyHr~E6B7b)nM>N{ zB_*ZHQD484jFL@Lg`8c)e}W`2vYQGd-OQhVQN_mFHgpKT9Sc_{yLpV=kcjZRmojs5FPBL{V*;%N>rPFeFFAiIg98@44 zAb9l)6QF&5aQ{^rK90!e{SpgDTT7BD?)!Zwse*vl&n!z&2Vm*pV}Q!`s`Ad0vEF(O zNV$(@O>OfUYw*VlpjWnMq}yFN7D5hto$0i$B)Vo{xTynMeC%AVkiL+VsHoI6%m=1g7n{nDf?_Wo9X~Fr(p0o(KXMv5|7HUec+2Q>%%f_ z|6;+zwS~_JJ6u|mKLh&uZ9}8{&u9D~9WtaprEwF~o-WoXbq}k%x)VTyw(Qf(FZM1j zeBIhgA%8JwQVcG}$`^jMq%T|>47Mm&p2&;~A)20iK>vo$UC3RmgwJ?(GJkCd7t!f` zz=Sork>*Pc_r1d%l|A$Ta<_p6YEr_hP}=^TB>Q2F7CPQR>#M%frL<`WGL@>`^}QsB zH>{rZC;jEQEJ3+Et-dhsQX07NFZ}1z>p%RRYfki#XuQ{>nueIo=AW_iQxP7*bEwGp zm|I`py&x;W1Y{7vm62;-YT!>!7@jZ54<4oCrW)To!}sXWR^M>-;wbV9rq(#QqXV{0 zBWuw!&bUgxA+jfP;DH-&V8h8$t9$=+8KKu=0X6!vQbUndEd^j)*(-VlRtd~mw|U4G zy^rUUZ=$;D|F&FP(IX}O*IPnu6Ifo%X}`_@h+~!5M(N%|7Ci;~`p~IJQRffae<+AO z$F!7A--GvRjL_;0~{G$Y9TF3N;hpoBi3JjXT^@6N;ybNX(%z z%BD(#AE;7%KuH?pW8ZpF7eFQEvLMxF-XLLj;?UwrTH}lQ8&fgUXF8{tjV1Z8|x?@nA zNl+b^fQ@5ewS?|AXyus_fN;m3*z(i3O=Qadj^v#C@%W$jd_0+Q@9UG85WTYQ1N*f^ z;0(J=tT}?Sykc?qt;2-0S__UHfvjcXxNzZk@aIYv3i+_Fx0g=)!Sds|BXI6u?sN{U9J9f>%PxXnCNvWZ(aTX?|6x-4eOXPbFTpx( zP?LxNqK5gU!`bUPvNL1Fvwh#gAfiQ8418=KT6F1 zH-g2)%hQgoD4s1No&=w&o&qp?zkPV;Jh3|XQl5(Vz2DP4%KdM*Y|6~8YiBMKanM`U zIJd|KnRgm{G+UAPakwJT!symb;%#s;^8qY`qns?bj*=^4^M?0(cl|9eE5R6_;IIwW zu}y$dwDmRUN@AS%Y&sI->rOc6)2t2b<=LN^%U}-VolJ#VjRMhM6I0saZ=D?>obg^o z&=~Yk3HtbbpEc7_enskyp^Kq<&HyKMbFPy^{p)XPDnnma8N$%G#wqZ_B}PP< zA7I*nqsbFjADisb&=^Mo>c@4_FFuV`S3i1hcwoZv#=D%`m_b4iR3=_tFD}Q2W-$nE zXMV{yRV0JUWI_xM=%@%ppZLJxD)z5xupa{Y?xJSoi$$X1kD2!fpoDA4NpFwK85r;fJRX? zt(6;0gq05}C$^jqyX0dL5DwLDBz+#KIsMFlHD(~XHe<$0tH^?2RZ>-ln@h5sXfvwL z!IUa4Rw9>;b#dj}%2D?a^eviZU3FGnS6$sp?j>#dHKh;N>2q^))OGs|ydEf^arP?IdgUEMCqxi_TIz`XwzdYYN+Qy z>*H%&4@nMN9It8)9+#Q4X9rB&$>U`hAi(iiGsaOQ1fA-N=xfeQS6F}YD1M{$R_$tO zDuqAqi4);Ab5Nw;BTGt0P~0eB9&u?kX+vl7(a8Yo8*tPTR3f-|qYQXymY)*T(bOho33ss)ye*O`Kap1Wgh z#kNIGynKq@B>hTJMV)jRc1sHgoP*7qV-KV(QCh5PfLU@Ca z_r0l0io4aE_~=L@`MOFzD$J5ITq8@A27Ob?;hGZvh9A~H@BjnR9X^_$WSHDn_WzP#(G8V}*9`zavbR+t}v1tHL z9!*yJhqkO!hHW(L3pC^`y!8SJA8h%6iBc}4N^ir@r{D}Z_BN1V`cCH2+sK4sK+7Zp zvUjd(@P-&)zVR!)_5CP;F9PsM2c0Q{GKfqmGt(ssx7Os`?gaZTo4z=9zfQ-TNIb*A zED8^v)hZno5xqR4deGu;OAH7^e8z|3?Cv@&86X5XS$ef{5upvj0bLbi^Mmd$y^r8| zYbKyqbAv?|H@pJu##)_8+3x}N3I*@KLyu7iCW-8?Q_BB*4}{hjDbke-Mqg*-PF#Ex z_dc$;V`!RosH&4?c_!lN{YZda7v2Z^74*i{A&o2~dx2NnQ@<<{pffq`0-twqQf3`G zNL-wG$%>89NthYT$?ED?3zAc%b#i_EWY`PAg^xIq7+(eTP1%r&cmRYug+VAl0sjJM9cw=f;7Bg4M6FA4d1gX#QY^v z#a{0E`zoAD)uSSb+fo4}1cj5w(f*N33-<0}EvNB1G8x@xtp#keCP~eJZp`tJM_cGS z;9TSJfZJI&?E160br91r^S;o9?<9@DhMUk%C5D#FFgf}%<2HeK^G{6bC+wP&qSw#+ zjYk!U3xz!$OXYdKhwvgg7{0c~ADHS~NYy`)4{-81@O8p-6DVx2#|9@0S1s0uHkgwEY&o4H{%Kjlm5h}a0{Xp< zTy2sh*kY)}0T?IrYWCUZe%d$5pXrE(iK`-gnvG}qJ$c);oU>CT1g`CP_~b-Rf9C5+ z&72EYkNanq*0DJ@728^{P|G<_{@XxARHbY&)CfLO|){CZX z`kfA*k)4s;Z96c;Nt~W1LXL)uz6`(op0DHq=LoU?W#lxr?wZfaQ`pq`jPE2tS~wou z)W|)LvuT7)CgJXhq$id2#h!(7RFFJl?>cAs8)YQ*%`e|xRJea9p;{T$iMBSUs3M|s zn~SzO0rZ|~7Dhxko=2m6s0UiUJDl|?;r~@A{JC{489ZRfD7Gsemwlk754iPpj_8*f zfZ3eF#TZR=$B4$bPDS{aoIHE9L=dhqtA{L2jqo;l`XF)d7NjC}fYNx@?V5xjcKB-d zH_4d9V(Y+`KS0PvA7P$ZV z*ZcNQ#3Rz>J;~0>Ed~y>pRIhP;%p!izMrRz;90-!{KGYk9M>>T_oAV<>p7cz(5pAn zRZeFQ5TZp|l&hCow>6!aN*=IhJT)^any6Kp6XUw?-3y2eO2dktn@>zBFq(Xl>5^Dr z8XU1Vm0+o`-{Nw#_75!v{8nfAV_*A!*JmNm@duTC<^ijha@4=ka#QPH3HqS{aQaaX zfmD(R*8-iM5|=rjcYe@~e_)?MA<++j=|9TsksPy4e_CeW`$J~`e}h&0r%dX7?qSfA;OTVR()$v~!OP4$OBW!x5H zhV-yOwZC34nj1{`GXRUE*yy5~i|OB1?XmhEIb?(p0+ zGR$3&QpMSaUROtaQ8;8h*!rydcV!{&fZUvbLm!;DkpOM%UqGi z*}eX_#R)-kCO0g7l({|kGseXlJxITfjQ*{T#J6GQDbmxgVm@PgI6f~W9mr{$Tt7D_ zHoI%ke!GQEnf-Yq{Q=qf;dGU(&`b%Ra;agEN{fHm<%fF0x&^(QTX>q})j2!;<><9? zZN-3^fc2lC-A(#u0Nm+BU_GL~7A>+R2T;Z`V<`$TrJ zcCd)ozLAxcjk15md~tl34YDLsNYnDvRqdHy8nxwj5|bt=Mc-a|r}AnVrD7`WMAS8O z-ELz8m+^_`!zSU1NhG$-hYm#UXO;dPHgszmDOis zx!j-HvFnm1F7MSlJ6y)JsqP{weFrLzt3!h{p34Hoa~JXgmp~!`f_`_;dfu*ybI87V zhg_6)*lj&~JfiY!ItY>Oj-olAEp^#u>@|fmm|C7)*_+c&8cZ?x9#j_C;+r4nn8bcS z&bC=7&zapOX6@0RxCg$fF8_5wlLHAoE#DdWdOJl`n%Kr$&-V{DV{dEsriaJ$2c=ovTTDK;)+IR$~IK~{1F9OzX6dtD%O zAEBQVgKlZaZ$oNy7)Vx*wtY0fpu_iy#>k7pFRz#c#(wEG8ZWD21GE&VAFU2_z^==Z zafGa6CK@hwgcJSDSEbVeH!<}D`{=0I+YRBp6R8hKzRD>Saj^Qy_3n35V#GQMb@?o{ zTy1BzSHqt1737o{*!MmkZPEv+ZPoey72Mn^-srQt;<3QgIk%n9V8)}!`ULP~dc3@((DhJ`-&YY4L2K1+pu)rm zcxtNfY4v4k{=>F<=;KMzony7*AJ!o{d?^UXMFtR}v`QzAg63h_A>4$UL??CBXJu=4Th$n-#wuj+y9Up~=%{E#bLH$+})!lAGA7dJYH^&jTQ!~iu*MAbZyioEqgH1Yh(=f5= zZWQ64fz=QJRElQ+Ov@jIPmJgWV^ipUAZnJ(ZKcZPJh@HPBbW{ zC^r?oIo4xXJ&O%PE|eflPO}Rgg&J7L3z_(-?2hK9{jmD?4V&ME%p2YtKfx|}&$*YG zh=EFuv}ja%yFSOJwvFd2&>?j-K4@5yUK;ldPeT`hmTf0YPW5C8!W8Lr8GjW&J^O>; zcmVl(KNBKz^qShap0uX@tn&BUf4s8i;k#0RkDDUhxfOQ$fxv~-@tseV1sFfxTo&WO zFj+Al4HE4k0sYlhMb2awGeZwY2WyKqpMy(JI~0~7-w1~G&j(ppuk~D#`{CyCcOR`k z_%Cq=_`yv7<@oOpX8NCU!5=m#f4XG&gPH!r+2lVfNBzM}e;A_tO@=T(nCTB@`p1~* zIorEjPJ+B|cW4tAtnev03eLFL&ReYzJ9j(1Gv?~Hkk%QT=fUnm^>U3C;pb1p?iNr+ zQyQ^UGc65kWxGGQBX!l9#HwZwfBMOR39dfZGlbG@hTUp9MH1uEx(9sHW)!`h2%?ul zzmv$$6LK?FJ&3L0!3RU&?<7F&U3zYhDcnm4aCa5B6{Br}XVD9iMOKlXj-F^aXjS8b z2V8)=>kDhYX3`FELN)i5R9W4Nb7mSsU9wlhcL5~+kT53wA+~%*PSo{0B!-iH zL|i{DlI4-r#-Ufm{XgB)@O5S#;)sxyqJq}}y>q_ZFI9SQJX>0zHLB279q1D<^gexp z<7zEk& z^B_|y|J1!y*LH0g^xm-F7}Hmto@7fV)@3~IUc~4q4GNY_lhH|-SU!B7zSHeuhQtXO zopo$K_T14b@lsluni%X|;{KcPmTxJaQq6{hkw5SQSp%Ka%0Nz%$yWeI^ z?e}0Jdsi%I?pR9ceibEPnQ#SW{=R5&w~m=iK)2X%5Stw2n1)90c4kf5z}{lMqNa@S zp0=W4`aG=MtLbt87nE|7ll>%w(@G1C5-3MiK$V;0N6kJ(P)9z@Wqg6M;R;t{QfAU; zyxnl+v>Ihyo&Et@s}yD-oYKcfiEl?FbUAYv=4{+XDz~{@(l2qZFVxS)ecgOTvRK^a zv1R5>RIX9z3~;8D=8ZnvFOHMhsMiQEmXipSxlXcc;~t1FN7S4+b-0$Rb4yeFB!n^x+DD<}I$H@>MT>A~&ZW7`LM~H8{$P zl65EgH*1aLkd?_+s*UU_tarVhob=tIHZV3wkB(EQC6xtDoY7XE2nE}7-Ysc9 z5pS7x{`AuZ60&5-lAn$!pno`h?&ZY*6#B@gar%8yZS)E1k|jjwi(93axP08ZaO_Ch z#|TF$LcxO8Sn%xS9zV&%d-D^5R27gFFdhCK7F{FImEpO@s(nk!oal*S?B|EC2DZXN zXbJ_+BJAfWp5-t4lY{i8~-wv5r1!>d@WZz-6lF%o`u71_5^T=67B&@HcR7f9OcP>)Nf78eTcPr7F}` z%@xRL($X&uJ!fQ+YRf?29KXq~AwHHjp;XI0eRoKm`q}qG!|_;EPaKoZ_r+rgk|QCk3|1 zR+l^P6&w*Vq?KNz$(|S2_(IsIeQ(_hhqu;hC_j;FR^7QjlN`CfFe!ltc(G#S_w|TZOwZRY{%ltmfoPEcLG}jXVsYeqDpJc8~7Av zz&FOzg$=&CEC)5zBP&a*K8U_e=Q;+tWHsd&tn2_s=Gr;N>HGPQ`tA0*+P6hA2Ui^Z zE;`M~`RwS!XK#cCzo0Oz#2e?Em|l2$&twO(GDWR$u{DpYV*Ae0=iKK=fn8BMlGWkj zc5B*7Q5~7Av}cu~FW$*go=n>Yjqr%DHBS?RRS!&0gWOTHDWr)|Z64_a$%W?f1XS@mqC?0EVLZ+R@-n8Zz z4#hvxQH~o%TgTDWdbbt8z}N4VH2y70jXhTAK*?8sUA$hhAbRA@ zXce2Mw`+4*f&&ZZIlD_Zb%~LQ%jQ$JYsUn5yBse*Cp_EsPisn}RC8`oU$48itE-7r zMRKa9t|#iP>gO?wdf3kv3uWNJk`2T*uk~)G6ZkNK=q_v3J!Y-E9XoGyBg^xXvnQZr z5yfBke9jco#+=;O3Ghz|9j(r5xYXj2L(uF`<$Yh7d~Vc#ZJ!lE4!UTonYGn(8qeK` z?d(flo%l{7^yrwXvPvyi$0o_oUIIWln?=kZmyXWD$%l8RU>q}-irixlS0*@eTPAn} z=-%tm3D70bJw4z1o2Zpv+3nxD)zRsDN+z;R{=@--uJp{UO^JVn2ep{dA`_e1c1Jqz zzUg?o-Cys&xoXHl$`u*ZK!MNuw90-3U*MKIzTn_!Ap?`fck!m*oyjl*7>Mj{3Zpxq zPiB8YzLD?2vG!up+ZH+(H@@(S^_>E~Hjv}AcupC8u=&H|{+|Xp@>;xa%obHS zL;45+mnB>Hm7`oz$0`(y;l}B{XKe6nr?yKDyA<8I#jw7(D51`d3^zL;=c%v{{Yf|XCVFRmzfa#YnRCX=It*w*_ppsU}RTvr!M_!k^KVt2OA79X~-G> zsn)U|>a;H&|5RI%zRTygN=-6>ZJDzNx9c1^_FvS;%z6=CDniGBt1bSHdS_d^K(^Vb zBSr)+ow(lgC2Rc`xxAWJ@IP}S^@S*d{IjkQ|CCVt2ax_f zkiJ0tbV&b;6U>KS#{O&KL_!=^I)4P#{f=P0{96>W|BSEsL8N~W=|3{a@0_ayZ_hZx zM#Of9a%d`wT%VkAf*(%6zDmOmHT+KUU|)@vi1s&%F?{TouhHPnkG8yJ5};XSEqf_(>XnxMb9!z*A~MO6XF+1@sC}h zD1H-Lid>@c?>CE@Mjc%c8hR%Qst3L`c1n660-StGt2CyA&#=|BrT-o`E1hL z9mXYSi*2y(*tdA+eb`ORby7Dj$1Yb*WpjT&OKpW_4aDq?_G z&~BDASu56#jXlYixapO)RHa^0+wzrX)p0N!ZbsBR;J> zxGBl$HOKX0^O6tJOjKWUUSWQ+CHgXp=kY~XMJmd)G~B7ix6CHh8|zqEMt4mNwyah# zq|#SSUAL;-qd!Uo49@~oboGI(t*2nxki?wyWtUablc^#DQ?(poXr26>$Zds*&&SLS ziIopofDzvB(DhM#P$_}lv9^4Nn{SMdwL&jVGgYw_d}9E8CxPte=^E{P7UamQRY^Ue?xN7gFPAH>&h~_Q2-| z3h_~v3Neu@vCR}<6Bsn<9oTg`U8Tl%CVL*|~M z_Pe$$Xj>Df;&uiwL8U0zV&fTE~yVD$Elq}V{S8b1<6wA?gx(A{!Z;{dGeSINU z07^{^J(RVPhY@v0Rg!IxTGWu^&6R5>Jexzs6Y)*x8?fhL-A?;E^Ceq}^9=d}vf70z z1*=h9@%M%8CB(2Rm4eNFbv_`p)rW)^O93H-PN|k|Hl0)I@s(ZH*U0(>h5U!aL`2~y zTPDSc-1dGEM4=ds_&?dYxCyfSTtdJv~|r=Dsct7#|fP%F%GyM@>rABR+(Ym1wpZiv{wbT1STgK2EA zMri^?F5dA;d94fEPBuOKRSY;WjR9u;orIdeu`M<(p&z4W$wDYRck^n^2aXp=kYKUV zGN403$`tr)$RjL@hsRQX6R_em(tq%9P+rjVM*YAjV;e6QwyS|{TMU^T@-+Y6>J|F^ zob~<_g|sWH&IQti%0Z1S(0;_ePj7NM)I&a?n0K`*r0ZFK&cN~7OwRhFnY0T{jCFT& zpJcLqL+(+&nbF{F@?4=zvu3Nw{W2Cbc>|QnS zLWpYe>TJVi_;QV%`-W9OtWq}fvshJqZE21hHlOit6lzOS9<^~fH14+qJn(yreA5JD z5>vZ;FTYfr&L6Vb9gZGzUxi)d66h=;VEi^+ zbRutGO$OKbWyAD?_!2Y7yxEf=Pvvy_izbi{8yq;WaY_+q~yN8eTq>g;VCxIiO59shQyIh{Eo)mYE~_<|KtD zPHeYjj&js8y1l{xm%o*D?o3~sE%)QdFl*11tIM}c)ZK~3_>1ARrqyeBKGsxZzG`x% z4;EWlNO17$yq>`^Q#7(}aiqj4*__*^3h`7q5h5_eH%dQ84bV#HxZand-%|!g+-}W% zrRPjWqBDzfYU&R!DKI}YLJz9H_LY|&bc=tM-E@i{U08PzC9 zIby|*eP>gG73#(tL7lQ`Spj-}3Xj2I*g!_*$}5 zJ)E6nezDN}3eCmyMY@Ut{_V$4h}q4#cKS7@)v2qNLBa%e7d^j6vn)c_R@|nhzP)X{ zx+xPW0#4*Wf;aq&p#5g?O1Q$}IF}NQuaGEOcur8nd^eCGBI`|*m{P#|m4$R{(V@Doe_NQR6T7okz_|^ z>*%t^er~?;@3*;s>~8RX`nhu~S)ONCl1l?OOjL>szU;sJQL* z6hzsP>=>t4EPUC|eunL=IDQ~!$NV>Ga{d`tWk3F>f1Y00yTstsLb?t=;Mzsr&vP}h zzO(U!V6vfV={5Pgu&>VkkQhWw<9Cwt?7`DaZG>=u4-rY32EYC9;Y2Fx7e6Ic`zx{p z7u_=PpqpqKif9`8@NauBt5)wyP#H<@Nqu}ltR(&glp+RFNuBO?_BF$y$;(23E>&ZS zaI8WMG3Yyz0Pn8xp6=M1QpJHg&~PpD#;oj{M4I+j&IJ!+B#x^RwGI_YVtj7dWbQZ; z*MxmqCyP;n^Va@#@Qi9*#hxR}`VZyWiWi7y?l&m+u6W1P>L**^1RVv>!40?;&(@hg z7jgM$v*Q?~X8fdJts=I^wcY)f18wFO;K4dltEDL@$9R1wkuQZ!i5ADrVci2i#kALc zCrL8|@<}Z>L40&;e%cx|AO;fr4jccmHRS*3bH5s3g9@0n2tf|0$=o~~>X99=B{-kZ z{qZ@m9sdsT@XO!+nxIKIEC6O?;1Cb~9M#l8OwzqCI1D5;PPN^BbLy_Q)Oo1rL)A*P z-wR3;nxH?ZhMm-Dq4B|IN*?kLiP4D59Mb_SbjwnARopc{9PQ`Nk{u7-V>c9v^V&tlxf+|u+g{a9?NZ5mjKma|)t;ETrV3O3NKi8}-- zIiX7q_G(koM)0+6zT&RW^iFk~4vqMeto6xO45yc}924VmXWv_r*%m1hRd4@I)4|_x zuz$-5`U_q|&wAQ`ec6^T{z9|F%4llN*F`YP6Fzo2#mTlR`-@T|()*2L(ch3_Pr z?@?$XU@dm*`7NrIa{7(0$a)$rYW_T+m3ZH$4_I?W9KsUtTQ`Vr+5!vw#c_35{@y>u zH8OL4k2sUfA>QJTb!sjoS3K<17;zzvS&p3y%QgZI))AJzHtfO8(;0O%O_W@+SZ$G0 zp~F66Kd;`ciaGO^SWw0g)GF7s{6hyFrQwd#KCJz}+<@H;Cm!7cI$gtx{bGbN9-O~c z@o90KgMTkKpU|sC zWmHj~;THR;7KYb3UOLoJ@WzE1mA(Ad(elh<+wjB(_|09mmN9lh++|NUJA7b}vt8l8tL<(pZZ*(+kXLd>8K`fUyEUt<6NvJtvQljN3GC9_3A~6F_%_BBSjMj)?*MThX5E7lYCs#+L@+pbbun+Kgm7J1MC-jg=DbJ`_S!- zJAi{?WDnq5f&rJ-Dp1fH1OTlL-Zy_mJN_Nd^YuG=D_T~32?2tsW!b&m+D|(9`SqV9 z2(R8DlLMsN6-NObT8QKGIr(9 zZ89}VN2Q0Gl;Yk@174!&i%xPf0~Fw+XSAnJRUFzLfzBB5Y<`x`+bGGb0J{TxqkN}UeZ{Tz2eJrxs{OAy((WeP1!EqV4Z40@VY0 zl|-Q42P#ZY%?<7;5+g|)EKZ;P$MN&evsU`?zx{na3kNLW4Y7FOj_t0ACj=J5h@sT< z|M4k9`5mCdoKR5IH)4=1A_I8;moI`mt}u<7xLyb?vTNVw_@%P8d=@La`1=x?-N4i; z*-2}D&!4)G{pVS=m*7Xc2UU0$vKFd~<0fNcSHu_@_=qW_SGwtj6mFM(?udR3;Cnx( zBS^DqshLc|x`vK-8*BECZ{$aiKK^z|i45tXz53{CV}ouFc66$OVcpAi4s+ih z*3@&^L$cB9vXm@#i8X!{_1fb~Ip#&51vKd=D>J|@A;6GN#6f_nQ-nX&z`I(}#^=3J ze7$DPTefX8n8WN7o(E`4j`hVt3UA}+o4^RD7R9{Z7+T6*PDpcGq4MU9;xx5wDY!*d z5LnQ%ahytQW@_xw$@sv~8Nk<7-VMGOcd;Uif-BM}0h0VaBg0htSOn*F5EpJdx{~j zUqobsV4~!bO?mFrd08B~1~&Sr`HUaG9dLtX$oS%Ne%qzGj3;;qEQ1gIgqP8Lvesn; z!?!)3A;+|A<-O0UE|+p;MC6fTDF57(IYu@Z^QuGPPlIzi)*91Mvve4iFJ6?Sq8-9x zVxu-jvYtDIW{=c_^24!BL(Pi7iPdgQ3FC*ULn9PtyPp;I8 z^y9YZaOQ2TP%utI&QXf?j2b;AgGI^~L(G@+Ee1JDXtOb(<&8x!FB^rUA~CgN7#}EB^Y!t|G;@Zs%6b9 zx<8d5BSYF&!s}QHIvhQCqBgYJ-LPV{#QXOPL-@B9=5*Oq@VWy8$A#}y$Sipi_s4+4F*Ey-S#0ahDvLRsH(x~U+hsQJeZ0V<1jS# z9%JEGhh%w!D^a|h1~_?Yo>h(Zv}*)89DEXAEK|FJ@<3hU5j$84+DoDk(hM>9@;2k9 zl=e`A+BRy0z`MHX5tP+o={Up7m+a?lh<&K2dO#W*;LwOBJj1+VszpxT4r_h7kf5od8zwezdk*ldap zRyJrh3&0Z8&`VCRU8DxiWmD;i-k9GC# zftbXkmQ*U~>^O^1&kwDhuguyq@j}gBAf+jt(in{HYtK}OnG;1zM#<})4kd~K2Si)} zzZJWk-re$Uc#&+t4Lc888HDb9JykP>t%#8cP3f&K7>@&=O0A^*qVFW(Y1e*2Ryu&x zN}`Rop6k~^l(ruoO+H@keUif==JS5Q#r-ssKFTgJNID+*v(dg>5LTz_-Z<8BrXFG! zE`b@Xv+6eJlDAtGWwRj`qd`d_=t&B8C83L$OHoxYtH6E_0Xt5=PVO$HFM|itxr$da zY}f$1%G2@i?<8e}=hpY$=Y4IH1uYYY8j>_`MgQ<(sj2G|MA;0y1PW3edT)5lK=y)@ zpCzL#OpLg$6j#v^<6o!ujroEo2P()O&JCrD$K#p|sw!WU^84L8W+oK$AXPk^s3%Ou zOT5)HElwd@QKL-4T*qvLMIH_G-FQDS+tJA9v|WxS<9+8VpO|CFZ18?|R1&l`VM$-wv1n#2Fio|_*EHv4xT0?xI&E|ejI-z+ zU45-qRdq3`kLc3A?%^~;^ykMv);aP#GEc=9!*T$`7X|IyN)F?jw4#j2 zngLL>373Qb;Q}_Nz|xgcITz7B;nv*iN8gp<1l9LAs<_nRFMPszHg<#Kh7j#TRIL7< zU%ml$g-O$WyBGOn?Axg|=Pbhq^x#j}v`6c!chg^y^cSpARsU{5a92dh=A6x6*{FW^ z4u^z9jmNEF3>%HBhIR zr%{9tTGS|g*V|UL*Z`5}zGLZ9Hsnve!?Z*_Q)=Lf^2r$G9W>)Q&Hbt0L-BRrxXA;p zDf&LB*W5u;-b0JpvGXStfCV8cJcG%0iOt@JOzPaPR+p;M=%BFHRO*4HN>rp9gk$m! zh2MKOznz>D)r`#pd3L5P>cQ_Mb_OuFl4FG#7>jMHz|ZyJYq!{xAik@Th%Q7RikYR! zp5o^PQ%!aWN65O<&r?kNb>4Qj-YG_ID9c3Yg0*9<%gd-H^bYL=dJnvLX?ih0H$7K9 zW5kZU|0%*xm?vqr)8AFH6g>cxRdSdZ&ZOkGZ?)xd%=?lVhLU70k%}1TRJZBIo{>F#iADh+D2B=Ow6z zVBCM)kAU;K(tS<$Q4 zvE*W))X?F(0JApG)lX{mst%XFGr>@ICoXMZCvGD$T4fJ1tp}@Sj_@5=m=bp9(@0eM zVLdF%a3*9Y-lA|mUP}n*&$C5255T~m-ud?u&xnUakcD%BeXC&BmC_ZnVEb-(5BU_V z{VQyu)L7f@ZAtBtrKVO}pPGrdALxkUTvRkrp}c(PF+lCwRQJeRtX&-&l?v{tRe`)!pCqnU%+EsY_W zCnjoX!X`IvO&(FyL;2)hx|@IUNiVVZyk!wo(0?s-OZI;Ct@ZC+%=7~C&b!dVX>G`O z`s)nf?*5sh5#1Cw(HOV`>n*Ol{obUh+}nu>0g77#<@#u>%Em)1^rLix5jeZGpm!M$9cZ4sdcuIzB2 z*Vs)E&y0X?5N@FtL}#kRIC{O7;=+O4KJJvYxYZLnj&&LpBDxK{A#OSy)39l1`7%D2 z^R^4YUCJe2BZdG?DsWb#hf4^(yXkuuM`4Yc+F1YKepon- z?q2$X<|7KWr?5I6+nawBt}FM}^uaMJ)LS0TsYsvTpwq*+!nyA^9+O5shuftqd9y;?1dg4Z2|s zfsVlHJo|nWR&5bB4($M(;NcJHEA?e$Z%nN@QkB^>hp$7}8}xa`!2f#b$yDX0)ij?Y ziVM!F&&Sqds1T=PNje-oIB1_M!p1i(*shw*~{ zI6~v8Ra#iL0%_sk5p=T$CI!l{s298WEDH7|PnIleWEJ~q2_7hla*W^&Q+lJ5MS3AXBsgPPkLGZhD_>sC ze093JY1u$G)&!WS^j0_jF=kHtxP1AWu*)&qO3Tsu=4H$8U+F4)eDC*SbO;JxYR1{H z$sd2p2`cvUNDXkPxyt|@v%W1L_;LM+y6XGq5Dt}ia=HcM9Gns*aZUW z9fhbE@^4$a?izVsLfaN+a__P5Y z*NuN-iXL-|9-sWjB*i=+FZ=g6NB_5=oBzK3|2MYWJ{uiurdndkIGMneyq_db^vO}n z87&{ZX=NN2w)d(DyulK9h=o2Uz9K)ULajl%=i=g799H~8n`*pu;}3J3Ef~o3rIElQ z|9wvrnB^En0D$zI1TQApNqpou=;b|B^MvZdNk0y$r|_MS)4-6#m$*6a?Pzqsp}qn= zYe`fm^RXtcMlu0dOW_fP-vwx;eBV!vg9YuKZJ%<9z$P&#oflAi-WJkdmjtJQ9Hy_h zU)+0nj-0TZ=4MUAZ025M&TQWwK+_vaZL;)r+!4F`Lg}af4dMycoHoOukAA|YjzymK zT0Ju^E~nU|h=^)+n$voMC@d?unMYq(a1z(=Ni%{Ni*%03Q-!Jl2HG$buEm$Buaz_ zNFgmljeS{lozISvvL)w`kCR1jk9h-P?lk^F3rG-tM*Njp=+zRhdkOdY^Wy2ZW$S}o z=w1vJs#L`5)}>*~V0B<#Ja~j!7ZSzy1u%uX*zWa4FY8tm$d)Yllz+M78+NJb%gfO1 z+Bu6rL|{CRj@llZ5onBV-{D!&YukZs>lyoJB^p#j59h0Pp^lfkCB%4Qu83T@T9m4*Ns7nQhi~2&9iGw9 zm%T%H#CEOsd3{n}7!l2zkZ+$})fUApl^01 zY^@FBSXgH?Y?TOJZ)T5WzXfUiLMSka-ud}zgXD-2qq->`YVSx7D!)U%YUebP&J>zk^ZezqXFI}o^+C`Q( znD>R-t=t6#1v*HT?}Otd4fs0;cpesjxqtt6&izm4{9nYB|5q{Te|-OcDHbH=mFkVh zZN@3)G3C{#O1y!7qUpgLhB5BUEr%VO!L0Td2D0TYy}XZ?l=`sL5y_tP+MmkUGbp>Z z;fJO>bBN)tA0?I?mu-||K6sB6_AYF}(b1wjh&}XVR>TSI=Z-Ttq%2Y-*Saz0KoA;> z{d~uCb#^S6SyBMwpvjB)TqyTi@@uJ23!79gNjhgt{rr};vD!TPkQM4cDY>L(eNo0P zxlQ6epB1CD=xj=_HCzPl@>&6su3A~E#aXSyhHF^ro*oPo<-JeuYBCpzqY`*IZdH|F z%r-oi=Vq@ps9C1t@-Ee|{4zk3Tmh+!^ z{FY6G?<}(FsxHTmXtcdn>h;)MZ8yNdG5GMu@c&z${4p5gL(^Zpn&T<^^hSx$#f3im zk(SFX_3x{g!+tm=EJMpv-!%;6c|7$+Dk`{|^ZH1s`$ek?G#|P$T+p!Ueu)oxDb<+^qyC0eouO!tK$H>f8TIPRN zUb`j^urLQ;M3oBE-a+&xB8H?qU7~F~kZ;TRtcb`mXid7sFu4f`GfC}Q#4mbu2Nbq; zh}zL@DZL^bQ9KRNBAj-1B0uYPO6c+bxp&}>Hmkry7#pj4j^^j)B6?W1;q-}qm6e0-V2=q$q(>GXvJv|2u4;Dz=WKW>@&<`B09LRC} z8*#?J1&=@bBdho)KleAD{2vDJPd@Fj|9Wir{>OaS6X_cxLc8$E28knz8_ci_#0h{w z*aK@cJ;b8+C`J#@cru~BfwZ^SsL3+WM^GS+oYNTicO3GJe{K68?0ESXlk#7F4rl)l z?)pQMzsqL#7dd9L1JT{S=;n>AFz!?97k!*UxB6B8yb1jn*`X7 zCO)Z^%7A9btfJmwPS(=*8t3gw-8^EKD5V|l z;Tlw1p{K|1*Mx#ILhPd-){jr?#jg0Bj^^5jrY29S;>QO= zA|c$Fn&US9XYIxPx$vXrM-&>dSILXTZ3FJXOxI#zlebYW4VDMuo_uY+l*qmqjRRi3 zbm-71cBud~JWbxC?>vl0)V6Hs`Y{m;4-q9wy(15gC{BpolI6o>zh^i4UN3o&hq8@c zvBv|bg&#m7gO#owWdRCt8RKtO-;1AL3uDt|vb{lWjY!Ce*#ny8fUi2P#M-N|&rX@e zY+T<4o${2x35Bna2V|qA9f4*`KmAJ#%V9g2em2fG-x zd!FhXQJ9%o65t<^*S~g7M|KDLvyt1$>;%pRhF5)!na&Nxj}f`PCk{k`E+aNG_2<3V zr`I8s{ALeM$VER?pKH9p$tL*l@JwYpWDV!QT_PpVGM-FWmQQw zVUe>hb2%sWi(HSYf^bZQOZiEiD^gc@#)t$IOrr*#APVuBdPd@iJ z9D8Y|=(oK2V-5Ti-HTRwv}HZ(tHh9KZ|U8JDw&ZHsS>1sO^l{aQJ8u%Zv~(P}h0Vfkxw zmw`IR$mqEB4>P;|c7l2(;=Gf8DiJ3k)%xm8WV9&>U{I?Xs|)aRYD6mGF)Ltl_XwT;FF1<$<3>o&52?ktvd zm>*_EDh>He_Zzz-n{+)t>O_oB%omJI_zaHcVj5pa`R5|<-Yb36b&xOCY8W~{nBg%9 zx$$|f-pIcBQW6K(MTS6AGYhXxA+b>eBPRX%yoI?CTtVg%>o%q1L0TGgSUvQ z{5bt-=tHP`%yX?mqh-&awUU^9rHlA}IAni^$ERqlpgec+^P;$>(Sv|1Vie{jJK6YL zM1QZc0jQGG@;Rq3?SUH+XSAwgqxs~;C2`1@Dc3-7NapxNEJKIdu-Kjf;Z=(kaT(XL z>(r)TaBhk)h;){2Ik+eq=3PEh6UVUa%N*wYh30~7R<~-<D9Z0uNUp> zmDKq_rYmoa))MJ$tJXR%kA~G#7i{HJKZLB}HW@EMPUm!sxLOo%9Z`r8A_mgwma35F z@ucd__a)TPb&7fowZ$unagxqT?o^IG56=yRu8g>3GoKjAIMJ`04*309%S1|9L z13uR#e=bh|&x<>Yy6?5RW72YF70{%xdRIw9O?uWhTNab>_BCrhLQIa8wIp+d@+U@Y zi)#fu^+~`LH-H*_%5sN8Y=7dKW*$~C?cHG?rPkdPZmk|rG4gy};%3vnZ+WbxO@PDT zVgdiER>z>ExiS026MB4W*U$@PJ$#|6%E=NJEYG9u038t!ove(f5mv{i(x-*yu+=Pi zjzn`{;K_kzP<6KVEUS0ppQew4pI#XbFHsjt_-?#2!ke0guq@!Vw=c+}%xgPaGFLG+ zi0e^KHaw1U|8a|qzuLxZjA{Wf8F>mdV<6@2^aJM~Xc#V?zaZiRX@gaUMCD_omCUjX z?lbD{v@@ia+6S_R=}Sw)RWs-2B-^L%lp_S+Z8oakBeQ!ZPq#8`FuW1J&*79dFyJdj zR4yWE5!j7Q6$eKiQP=7QP=qxCRGM?*xp4gj{HYm9Eh38#X2_K3ml=L7+S&4=-5arQ zqZ3G~Tb*eZrUp?$CT>qi%^@%IKbc!5Py4O9J+Th)<;wT8>f+`v(Gn=4`x0?ga~Vn1 zEB)|d&%{tOPl89Hi zXH9+ZXk6cS7rGs7zqFmJ2kre+m_=k`=7?vwYFg8dFlR$&Wjtmcw#sxMv;?eM@c=Y! z{qJ4H!~_&v$axyOY7}0qyQH_b_xp7Pet*^Zb4lQjAoBP3>bk6>&V#9CFeLX$DPFbk zLxN%}nV4~Ip-^0Cx0PA@0QZ_BE4>non;G5ng71Rz`uU$U-$BO4CFVHKWqQakxy%mS|F^4 zK{r@HhBM2>M4)7O^V@c*I&ztkbj};}+8VTRr(_8*7anYX%B22%tMH%Hzy?$hUrnAx z%~%C4@dY^7*&~T!K!W5hNYi*IeX4zH1GU@XpFb$S7WTZ?OHTZt@P&1Wh`5N#`f>Wxru0|plxO;pXBx>~bSYGJ-l(}6@L zo(#;(nXV;v@qvHvU{W}=AFY znRyC&0Q`FbHS-L-4oQJzlg=Jd#JoPDXf*zSn0mbZC4G0vKfix@SE;wW0RY?IudYl9<2Pm8W0e|5PA@lob42jb0s-Up27{UN*9JU*&_H^_R1Ek;j z2lh{p@Na+u$5TzU@gX*w!*v^dGTnK5fExS>$S$PO>}ipv*V{U`=}d$t+ix9&ESe4+ z+&kRkYh{e)Kja$$Aj;tFSjgG?hZ9P_o&!@WU`u$_7(ia2X0-dk%c*XZm9AufSP`S~ z^oXL=ozRa81!?aTLk}7?6nzcB(`(a*n_4;+WZ5nd=ncXV}KTAPN*@DD8- z+}R4sh=vDeWhc1~w+*aiYIF7g<%UoGx>q8M>LGU{Ro?5pO*U05_w0nec!-Ll4BXUv z9xKxltgJroq?`vl+++U!t4hgMB}+4voTXcNquT%jx#-66)B2mS8-In;@U#G3Pa|^1 z5*h#7#Z&KTx`}y|zTf9THJlP)KC#C0lKZc~yq{|3slM-IPy!QSaghqt6z4Fx`3=P@ zczp+?AR#EbLBHSohDP0H^Mp;#J=U|ff5-?N@BiCHn5X_z=%~L_-T9Bd_b*9@W2kFy z`jfarBR}q|L%RSLCY}7S8aChPlTLsH`~LtY2sNO|A+tPoMDY&OY6V)q0)B~|+`HDh zB`j_6iZeaL*(w4%AVTd62Y6EQ2AlKH3goHj5{J1|dHz*y^L(B{TuDhOY`9lf{b+*QlVH?rRdo|R=-9zx8+yas`K zVfP?-eikJar?+o=$1NsN#TNv_igQ5O}?5h zC3LUna|JZt|K|3}MgE|B>Bqa`O}z!LG5Q{z%XO>Tvh7NGQ>kVyaIyGE05{it9?Vy$)L{pg4G>OAy2QHrMIFWg~eRT$ezs{`sGMjF+J=}x&sYZ~$>to-0o%ms(>tMFv(_Z3CRh4J*X^*!czm?#s(W>M~a5m-@s$&^Ef zaD9Vw-?fc7JOSW>?O{u*4Br6q^rD2`tB|hHq$R#U22CHPm*wN%7u2SoFw%pI*UEz?165Y-V)<> z5pulfz!Ub=2ma<++YJUA2QLVseG$@zAnC~L)}S8TUv0$(11f&d~oWZ zS1&|TQ@J-H9cyw#p+aG(QfZMhv83e~b)FJy^pqqyuy#&2G}y0*^|6wNGoW>V_;#)9 zW!Tc1EZwqRXk$z?)&S#wp?@WNf^ZL{5~LS5oCGKafat}|dz5dvwzWuKs@bNqPI*;Q zJv3*FRy@$N9pdGbo5n@8q@X12 zlUyfujbN#wLeueBnzmK{bDv(qdexvXMO>1`(&3d+?vWUt%lXTPNf*(M=LTxHX|x4O~Uc1S*3be z2w-&IhXIScTLC13y!Elk*f(gZ>R+03w?271RB#?4$qYE3{37>NqEdDjJAueQ zBz_>%Csl^H!k64vf36I7^=O+cR(nu~-gr-=#Y^$6POmV(=w6DkNiqN*Io2Bjuw=?L z^RqMkJKnzy_>4HP%JFz-QXb)*xeHOCY)& zV|ER2{% zkG^15Fon{XetC2Aec-Cf1S?Yad+-C<1Qx=Y{feD)N5@^Q_PlE&)6@&Lj#J5NW-OZJ z^LahVaJcbLg=)*15x(qlqBDGQBjyfBrLd5A|7kDX$7#z`Bbhqk9#0k%8EEOdaH|Uz zJ&$D_0!9)m?#cZ(Ryj`ov%mGfO-ufI8Z8h7&w$o19B?(KZ=3;~`j<#?d^9QK2H=Z9 zhGkiR%>wxc37a3Qhry~-heg-LHoMW_%XWa76%aJMxnjRy0TuTJrd9Me<9iO;#c>d& z#+LE~>8maYALo{*H7>7$7B^s%Iz*B^?sSaN>IMs}(tv6gP)mGOoG#t>z*2{+& z_v8mFT=Yn-{Q0&;@@R0!!|PG{CKgRsgu6Ess1{Z5aa z>PJve*QJ!P=k!-c+vYhH9;*vOZuU3}zL%0)$CVd9qC^!&0cr~~)Hja%psG%75y;|UjZIXn6dws&8zDpUvazh3GIG@&UhhMnh7 z!gii9t9<*6)`+?nc{00U6rB0FXEyW9Omqs^2A=KW7MfC&V&32-`LGp$u%KmbFRg3+ z(5#Qxp`YW!j*`JHT(eNiz<3JO^@{r^-m_ma~E<|#twva?T6QuQKKYePeAeSbh#%8gqDf zxnVePnm50&x8Lz9Aa^qC+6^=oSC@R@CJwfs${?M zt$Ox1Pxf(jRN&ycR(ywICs9Ww=O#fkE0L|z8QWT(oK+7ZYA3Jn8f_FwfT*{9jMqY= zqyXI{JCmQYuUngOd4RhSyVSANp%v}S1zPEUFdz7>Bu>L3Ow+}~29Txu0QeJ{*Yz%B z^ZQ&lRr2=twcP)K?4u8|{SqP01Il+EQ|uJ%ygy!wV0_$~>TWh4f*CoYY}4hgvoVLutz(1#z^%S0A|6-{w;dtw>mqp<hF(P~IpMwI5HtqDDQp+K^`2bA)kiV6Rmu=wX#Ie$5Z zw*F2A_2d~FAsnms#GU#$X#(__r;k#Ie4klWkX(>v3MAaW+muCDWc*3(TIG6RFH%FK zcwRfkAsSNyr6@2XmvI&cMnFE042TJ!U`NwI$|H)$E)h`!3sOI)`nEDxXB6E%^Q_O{)hTUm#jM$6!34wj zTls3k`{|ZMj<#!L{@&?i2{tEL%hEU*&sN`Zyha-p_NVgTlKdxS*WGr7dQGB{9V(`U(^&URWy^D!ngtL>qmQE zvnNuWPw#4-!-am5xx-iWi%hj#*Y^F5NSoK9fqF3y>B(Yd5aOLjHggJBGLJOYblv&@ zj2cF#$oi9XCh-{%QW$Rk@X~x$jSm~NQ!QzqX`C<-n%t{8@wO=*0CiTZ`%Ng^7zM1BPg(Fnf)WqC8d&9#k5{@x<_*z;!THh9(iOL<*=HYcZN8r@=7MD6Y3)|^|HDk`h7 zpo>3opAfg%P%2unVfDNT_Xd;m)lAAyS9wDk$z2|BP}-C`(S?4>b$|?V)oi!lrk}t1n-$W<1OJ@0uPCbPFFQ?pj*3@l9!1*fMn+M<}S>QTk6QECt^ac9E zoi84;3)dkVEx1iF;A@GhQIRu?My!jwR5K>3w98DRCQ1P$8kcFFG)d8fWs}%~ zLiEQp-Rtyx8pGNUYtD^c5^{S5gzp}S&R;Z0yFnE7%N%A70dX%U&qawuq{hkc%+!py zyiAaZ7RyR}VDWK!EuSu7$n%>5-JnGZZ>(RziT>srtoE7sBZ}rjNGs*x_1ntS!^AK?_ORsFr5F&+!`WoF}JU$$||Rh zxu4riBnmpIrb(E_97qG%O5AoEG4R`dbTH?z>s3%wJ$QIBol(X%G&YgVNpPRh(rsQ< zB=)R$o0#ooxMq}HD zm6ZS1DzTBuln5%Zt)Gijebg{@VM=h-`tyg@yHDP_^YfLa+PTYK^BhWH_2oOdcD+wSPRXaP)ZgJ+c}ijfu(nXY(Wye#bf^6GcAvNC7k%em;0|)c00Zx z%al6FH_4w}R5gt&@jXB2fc3J_(!KYUeqKV)b+Nocw56lnC6T^Qy;7>KHBFD>%)2Jk zq(WxArh=185S^CQ>uYhjQjAu0E-ti7pGW_3*SX?+!&FW$Q8-Ny zBFwkKe0ZMxEc*xj`W{X0>Ze&l_7RfA=i(yUaQI1u2@Zsdjk&avs{W7KqGm)@!wSs*ExJ;6s&H zdJ2zkkLf1pD^0vJ%OMYXielu&y?qGR@l%IwzXFj$0?$(B3U~)M;u-V;T}Kh^XJ4H0 z8Z6A3uyJK{Vt=8e*XrJfP=W87=EsG8#l5H-42sbTQ*Y$4cRh9p&H}Zke+bn3f3Z!J zY@|Tm_Sp>v5Pzqfx9F$9e*;fFB|+p^@(=umLcezbPFYx`JAARwg@po>6QiDEH(`iB#{huF?Q9rDK z>~Y+A)o>sV2PP84_IeI^y?*>I>^4w{>ZjR_)cuFZfwSNL3W2&AD!YhK!b3@K7?vNf z0>}7|%NxJ#6YrEkk*sbbjiDt_;_~9A?(0ct7k-E+3N+9uyr4P%>5B+lkItf7bC?w= zQ-JuuaadZyQR~99C%UYFUX9G-W*3q0Q}F!KeTNp-BKy)wRh!ZAO+*&+$d@`IUIzIicH=IKM+S(>R${MWK%q-AXbTM*+qPZkAH(}Pfx z{5{AwHtCoO=TMAwz&AVO^P#KczD-8F1M}*@m8kT>y6SUHO_=DWqn7B5PfToNZa(q;@5LEe9sE?!Xskl za3cGR?`pm~`3u{S??`R2kD8Q;i{@Wkt0_55w&$4@X*UGwLd>_(gA?e?6)zj9*!+O4ke62-pqe*4C44VcjPg6cS zaLc@*c{(X*?QY@Kr?k^toMljB&-5vV&<4S0s*hE#EeDkf;m|7w(_jWqu|a05$t%zC zFYV_Rp1EF>y)E5eA*)KAWc9(}x$&Z`$;XZSknI40Vrvg07L0*Kp*0I8p|WH>z&-SC zqvrK{13RQ<`hmrfG?z^X{9W^K`A94F-t&QI<-T>KNzpU9$ zZ^~eyhEBMXzph#GUWz3>;~0_D6vBTm8VR$EEvQKczDLbu$sMcb##_~ufMy4_xj*;uH&NKoHC_%Q?*mVAkl+$*r{I8 z{zvhc<}}7p-~yOd`av2qex)q?1b=wIGL+B6Hg-wks`1#%m6ixhgRkB|No9R&Ww-~i z4nrj#UMC#%l_h!q;+fC=m`%|ok=hvNh9A9&5*=b5j!@1lV?0S z@{6sWBw1EJm~NnQwcLif2cB=sb;p``7ez(7d8O+^eT;6jQ|0bGe$Ft&DqH&@M~H1V z)4QEsqULIRMSSJ{&Hc|416ZIv*ebm(-dR4W!!mI%{&mJ?@^&}{vvDCS6!R-8&ZW+RSsP98Uy>XIu zzu+w+4~r^xG-L50xLGPcWcXHJY4Ul7OubfbzqA1Q;)k?y(PnzEGI`iJ#E)6*beI35yhzLBSKd5+0yTp;&%)H87JXS z&4ujm+nJa6sGj2GuU#4Kz}L(;NFp@Or|V`C?q?5wC5SnQD%Y2ymv@rjBldc5S8FL_ zxpAgT3dYpXWH2SlX8j6DENxPw-ixy@;r0D!yMuZxs?CuSClLSAV4VF2gg@e|E7U9C z=N)3}+I`WjAN5krRg#5lh$&gUj;po`X_Sw7g3k(e%aKiwGMQ?Rgec- zW#NT(`c1isp>TBovJq+=4^y{a?jE_0lk{gB4@v5ve$Onq3S}bQ=`$-IWU{^&HA|=` zAYS7}NEK!w;e$@-S(0n0OLJfI!(|DJWmo_kbA==LK!aUbCy3g#bE_1LO$u4vqPf({ zTGpYw2{8-XA49%rHp^7=kX`0$F1~#!mCvE_*b3%&H~Z*P+bdLHwib&K=2yP4uUb0U zE5@-6<<7JWY+290TID;CT!K=&+xjYCYd)?FS64iiL{N|?DhZ;ut_K{*Y*r0KM6h61 zu9akuEGLQ>OvshX-}t7-grFeMtf)?{8nwK)zg!q>GwDBe$qNzlL*$%!k&mHJLSuK( zPf61>5kz%!XG~+4&yEsN;4nI0sdxfiC3hksW)N9YBqbPkEj)L0_(6b0&`FvmggG*V zGAQa&gbg}#`=0*=#*5N6N*PPsS@qG6k5fngiQYN+iuKh#+P3nJl&q~3zV-*E{+M0Z z$?Z?}%#RHr@pw2{Z1V%4N0>qkK&}P?fS3ae4zm(7H5v6PW{mh+=p$1{^4wh%k*m8| z;P%)<7Fv<5*@qc45isl=nH_)-oh3zr@S89|3L?;75!s3XTY%ppnJAXdJDzj;)PJEm zm)wFnCkEG)Y5W;LnKf&33J2)2d7{A5b*B$c*pbsKVC) z1rct5w!R@WvbiIJ=W1RLl<-C-omTaD_xR?644N~}M4pM-9*5*OGMftyw1>Cb1P?*5 zvY`ZR^wnW|VgC1pjANoBC}kA(jX$%OaQYW?EVvHb^jek^pn|7(h5!_RjqKmQOK|Q$ z4joYhQxTm3%v9MC#pEdxaBSfH3(o_7*ls&;9u!1iXJ?$y9-yY5L}v3U+5O@QRU-d_ zl?yeY0+FXs$eQZ#Rca<--!~{>SvDdgNu8K~E!vWHLs0PC)9kFZuNgEL@G#f3cqQSJ zhKUw=7+3M$(jdqW#2)lMptr_3V3Ot;(F{M)te~%VPm_T^DMu8R z>4DxMXjB;OcXz7JlO10gg4ECd^v4i2n(uf@G+&caE2d%?{>3v?(@l3G19gj5(d+rs zoL=oJ2`~?-U}N+kehV6cUO{KvYZwK7cztlGKvj4=s#t{W|_|!v;ULbj& zJ_g}GN~Y|+IesbkV#xv{WWF_>bQOV{=?nUJQF;8!aOpePH!+fT&zx%QFf#A8Yv)&Z7KfSDjQBxniTM}Ue;Y4|!ZMs$aog4V- zrmSe(o}7}<_~e7@$HVQ-XVWEK!pOo}_;6K#Dc3a3=yDs|4O{>A4I}5pO~@}8%@M_O zSfDI_zs0-p2fB5FifTR+F@OVxB|`FXxB(7#vzUYonvO*wR>Av!%&2m3oDRf1J6YLj{f}8? z^uK_%-*UmV`wP-k0Ll`;`$ql-xS^K)$hBX!f1P4O4G@@%JyZia%jA=SIQNqg|D|gii-;4oSKxFQtM#$l_oE?@|O3}VRUGe?YK8DN0x>acJ5>3fV_g#JP zl>9H!DMkb?VWQ9h9RJ30_65 zSt3Nq;#u?~?Nh6Q(pptyLC*3+nNLc+*Cx+y4GUyh>3%ofDyRendb@dueBmBr;0W7_ z6ssN@O+(!v>(oWbHjBgR>~Y9SNb(qZ4LYnZp-u|7z@g~%sfF}>Y}))CGawdI;J|8s zzEPPr^N`ZV%O+|`&4D-?mb;so2fRpcNWHff;$jDX_#Y1Hx9i!t-}%b-vfeKHsR z5cTfv@UxGngtaR$dd#d^@kyd}(kY!Gcw&*0PC}lq+8lMGS7}lc+ZiOmvA5`v9tTOk zM2YAs>N$EKVc}pangZ!Fy^3!IRNZPaqQoGl5)tL_J?+DeGk|gQcqK&JR|KJ3oC_2=ue-;@!Ok zvwMD^@h0AR>LL{18$KmPn{py~-{Yh7c^76S_OpsCfe@d+xQr_aVVDouyHIimRD4y_o;8#0Ou_=*r1qxN&Z zUQ|KXL!6MH;-~g{`o41E?_#PAaHs%|d_XD3KYz(j9p&c7cfSR;ai#&l?OIhp)}nKt zV$4JrU(pIdw9fcSAA_n5*WzPWe9c^d%<9DLy5MoD#P?B@jo!O&$P^zrRO{zPH1OA# z$;>3taK(l?Wc@qoAA82p-dn8lhULBedmBX-Rrcd8)44+9y|SnHvHQXrS0k1tLhf7R z%N472sZSzhinh_s<5z5|{G!BmTXo}bPT?%0(qwxJHG*O~+~wkuMlC`_t3|-bday4T zD)jOK|Fqcpn>s5vt^R|zhkR&XNBql!=KDF-+9D@nAN=%#)JIjG87PLluh7bHGkbHc zq+q(J*SkJDgteLY#2Csuo8*f16=~OO8>Bpa-Ef;G5=s--3_wTi%C*0!tKWwt@pSoM z278L(57oJ*hkkHAq4v@&5@4&)WYKWB>!Ns`-=)>Th4aTs&>JRaPFlI#u`MdoB!DLN zMgnQgt#M(UC#P$+>1%JRK^<xuh#07 zRM-{U7!XbsANhk49Gm!l-8^vH+U-~|=cO74^U0vbJLlVew&qmp> zuv6N$Wbxi@j(2NxAXjzK7PN_S8i>Ed+mSE*F7W`%y8gCjCcx0-@7DWHj(Nx4R2+L*PmJi#vzzQ|1`Y~vP?a!X-nxbznWzpj_!}{k}C(I^@ z@=kPtFO1=QIq+vy7kcNmNnbR^5n6rzR~D~%EJa^vf3O_3CKohCaYkG@mj1S)j^7P5faTtg*1_Y4Fqc9EPGy}m{PeXkvxbZ^DZC}&f8 zW)>Q4sF(50qDFK3?zFTDMyp7()6VqMZ0%-sv@G?6rzuzR5K|meJAM6u%-CHCM`r4h z6k4d9vAlE9aD%l&S>5zt4J##foy6h2Ic5}bE3%K3uX?$(gh?A-ySdJ;|HMsL(m80& z`{7?x44n7%DE~w^@S;2B7pxFqYnxjD*}(?LP}fKA{#4eCCLYuPY>)ng{gVL0AQjY{ z$|mCjHS&^OvtQsQQ!NHD@Ye1_02z3|KPZ!}-2MSxia&sKS?T7hXFauEOc4!e#@*@C z<#^@#Q%IKDwrr&1o1)x&uJ168&&&*AJbGhK^E2OSsytcR`iSDi5d|-?x(y1*X#%Ug z-9}wGoX52>%9Qt8PqW;oTxOGx?)LI;O_mcsON@JbE2{KqB6+3(t8~aF!b82sfp}fq z-8@|{I;WU?$+p~}X(gLev^RV`(A&Y|W&EiZ#!-B~Dm=#XjM8_`r~(3xY_anIene!JJHBfjpB1NxW-#HK!7? zlDXkIUE))fo z>XRQ8|1+g@JHjLziLDXee7CB|D!PzY@+7i|GAwwl>;9N&I?;Y0UyQYg`MG?e8P-;|^77ziEPE@3e2uLqcks7MB0D;)( zO+Y}pbPz&BI)s4qj?@6sk|4b#)Bqu#@3;1>eP+(AeP;Hx_sn1G4QilIkxmjSF<)1K;j;Im0jaqdaNa7Nd7lpcj`mnBQ8fa^ESL zkm(w>KJ1M?#})hBjr_WK9KgArP~9_28qg{yv#(X2!2|$)KazRRkwyDPVgvx@h(qYK z9UDP;^v6V_{SZZM#JRaN>CFC^xowiFZcEeV<4rL(c}tn!+@8Bu>%y+srJKp+#qEgMYueCM;a_bh7jwkzz)Eo zs2aDCL6BOpW)bXKXrlxk)r-nWI0!?xqxW~?==*n3IjH{fjJ-}wz#{DrgJn-_So9Ge z{VyszPFoQm(6TvZY%npF>~h@pqQD*?nW~4xwr5KFnSi=+jT!&uxin+0*9>Dta}J{L z9PWlygb}+$MtRm+tzvP;g>Tf3E{CN>e_%H})0YH*Z!vU(E~F;eGkzw>nf;u#21AE1 zq^2s#`FhkNSwjxCM8g$#mz$0f9O|t|58=Mc+GV?6shU!Y)dgrHD?(9`O=&HH>uZHX ztxhPXLNY9@BvkK0X*;MFKc%0jz!u#IAM|kON?B}*`lJW-{npHdumR^Bp+OJTa`2FvN8BAj4fOZsA{5K9cnss zh#6IVmd-m!RCTw%$^bx?C3crcpnobrB#I+ zx&6w$T5|5P1&8@9A=%EwsPhoalT^OoyjYjl^~OWxztT(zFDf6-rMe2sYne!$tDcRx zWKIe@@FwUq%iQHrlCS&RY<{Ps0|NtvMR8tPMtT{<5BDq=U;>@{lmW5*F||a{?=8aT zxlGLSA=>2}`_ixNw!F{$R}JTnQw|KY^$jZrolN~gkKZ^*-WUG-wc>Zp0sV@|kKfzd zVc(bA)p94=2Ty_nHFv*tRNDAjZxo!PnC@M03{HUTd zVk@;awZ>;)Rl=@P|2FTmgi?GUnkG9a1^0U35F<;!29a!@xms@V+VkzJ%{i+mtdTc& z=#3!N$^4HGimlfQ6g}kakF@Rt*VyyTyM%yGu6uy|>O^|dyxtCCOXG>JV+CHOlZv~H>!K!jO7f@;^sXk2R zh|xY-)j1J~ig%H9d_Mu|xIn?}NFu}!e;D%-K3R~O3Gq_843M(OugCBDN8z}J?ui|6 z`zs3fmc-eKBJnB21l-pG2_*MNa7WnFueg$8S~ta{fs4;9WTS_0o&V$PBk1jhNSDoknA{A*5)2vOPAa4jhC!?}U}iYZWPln1Ab2 zFZsjZEVBriFE#m)voEw7kP=6plG8OP)m+5` zDoQ5o?x7Wj-|G6=U^kvocXE_#ochf0QVS2$d?#A(`Kve@GM-u=wwD~fSsrBFteWF5 zj7{c==z5ee@NEw?$DRSs+-mUKleEAD(M9d1ZzR2pviQy`t6EGTZvK^pv~;7LHsBB8D;PMi`Y2+&w_k05Fs6itE{*8Oz0dXlS-iOAQ} zbl8Ntw!B!9yQC?+yNXO9M4#;^Mzd+aIl%&`={0~0XhaxIBCN3KI>%~78HX(>ou_Jh zd@jjP7SZhg*y3)1D<$^It8`SSJ>Yp;LbJKyY1^rtAr))=83_Ux8)KDh*}K^@6-@IHGc@m1u$k-v*;;?v@oZ?9f_RPh-R8p%2O zZMEpSL;jq5n2^+r7$VrDV8%P=8o%}Uh3!YTT3)FkmlWPAFYtQ*15@dP5upyux?zmf zq_9@~Q@y?j?pK*g8uhH7`@d@5C6_Zap+Oqd z8Rr2}sHWex4`8SAgU{NzmE8!PdfjV?eLzuDA+NRxEt~>GtWw6s+UAcY1y^n3tr6yx zVd#kOaqAufONJlcTsnDmsa0gc3Eh9tVSvlRuJ@Dr0`CS|R2~?4XHnnv!zxVHY?yOA zdVR`rO*86m_7^r2zacWbKE-WHdiEc)m+MXaSW#}bD)St&01KI6nkOTp6@2W@&hBE0 z>LDlErg?QPE=o>(f9KW)&psEP#!*z4-QBzCd$mi#Ei$LFl;V4jknCweIh`5fVvhYsVKrM0Yr2RbQMI(dz+7-u zIH(3QVaM*|=6UVWiBa6cJmHcXhL4Y;&&JPgPMC?xeQ)u#C`Pmhiw}i)B;4D2Jpcjc zA`%)X0E=biRltM6Li>jDlZWjSO6w@iU&}H<7^|3taS!WGe??ZOS~4yXncr6yxwQ=G z!d;`WzT3G~JJhHOn*}WfT%_~{hl`GU`RMa`&ZgG=ot)uCv>B^t-$6ByG^o17pYaSS z_0`^+Q;PI!^15Dr_1B7RwRY%nHJ_I*Yd6jbayab&`aISaK$el(p_)Hz(brQWCFQ|H z>MpKMPca%|-dkxP)tEY@uRC6V3zyUQ$M9eHN(g4ps3r?P>378{`b)M^EK8AjjRD;p zU7PBKQew>Zk=Az(Z~C<78k0NwA{}-19CkJnDn6aB#sq$v&v&-kThz@J7TOf81M?vA zRbyA$g8D`zim8pDZN}!Az8!N-8h{<*%_H?@iC9B;Wql};MWvVRd#d+a`(M*~{w4IX;r+FJK$Y>3GS<74YU+MonN5{x+EC3z6RY+o_>OM z5|o)}95uN1^+9WzvFTv*XrQ`t(%!A&8M~^e2YtVL)w?)c{qTyj7Fvf|58fVreQUvk zWFwOJqZj2sAP3##Wn}%~eLG^iWh4m}zb`c}wJqJ=D|%a0QXZqj= zotCmyxy>QE8#+-k^qa)I#A6oPIT(b+2e8<9)-Xz%Tz(?lzHa zvGzNPN^Bo&!((G>ONW4MP4cg84Hb{Tw;$aeY-(k}Lb$vqAk#(74R#nR#gyVI8HeF2 zTQ6IFMtYkPp%)O`Z*`;xP@xzH*Tky!e-?jWC4vZN&PKXfW|JF;ESL}d8~u)+n7lSs z_Z$*SfA@FOayueP?%>gZ4Au5AbaRP=CVW(n^@<50-Vc#y+6-;5W4SG!K4ZG=!n`s- zjnfR<1y>(I<{SplPoxXVp)Z@P?jS;|2@@_Iqf@O0wL?yrAb~(&(1b9kox@s@x?yMX zmR303KIPgudl4ae*^F|11NWc=&LIY3AG8ZQL#m9lwk3$)ytl-)(WW9yLk*6-x(FDI z2qB-9Qfz`M6KMC`E%U&vmQq$i0#@X4{657s}j=!6KpvA6-2HZY9+T9 zt8unuvN=7KVOt$>y0`7XerFOh730nH-47qN?PdE*RZ~~nQLlDjbt6U6EGKv*P#NeR zN0TN7p4fdIEO3?nN+0a7#s=m&dVPEMK>&I8nSOU;x-8Bh#|Y zn!cqUsvtIkhcyb#iZ&&HG#7Ei!BAW2!-V{hqm?&64Z@jo0!M)91VtVBl<^T4N3wlimc zO`28aOW3F=xLKC)SAENyN2Z&+L(rbtp`+e6qE0z(BS^{nhDL3g$=is#T<1K0fr{<7 zL7DBjK7jlJ6B>iK2NvRz{6@qnr&kK+#tk3Ejm>T*a}R3V^bU7ogIDO@F96KQ&U9! zv+-71XT#Ds3hJM3MP#!?d|5bUFKQ4S^4Et1%a56MK2$u{@YHc3_!%GsaCy$Eqx5*z?Z^<*iX{vWK`gWD4XudS*1E%xwk@PLX*lQ70Aq!m4 z>7-eIuy!cByiB(k_Gzo245@#QAqMWL<96mA zf{Gfv^;hABKb~J{uuD(-oxgX=DDH7Ziu@P-bgWUhSo_YF>|sqwMl+y@E028g=$-8^ zK5kp7@^3(o6s76Y2QxLtE-vrr9ADy$ugf-7&20!_qD1o`&%$rUZno@atcD#hP0vF* z4AoQRs;<4)Qx97S;^5t3b2G40>=jQ^W#wUciJ1a3P5#n;;reqjQ;qTJjTG2@njIC+ z(_6Oe6&hrzS%AsV#5!8V1#!Q`HL!*lhdHRz+$Fk~tt;wND;GuW5e9MeOIk8>T=A*Q zzN1d&@E!lYA_-jT#4mI4JQ4fUK(jW!szsjyM4IixbEp!&U2vByd!C2@l zzl=@Vy$e)%RTr!bGXM$C7s`oc?{jf=e0$U1P7cO}xt>wuwZVkNAG`ca6sZfA>mC1tb{&`&2?fMY8#R*}rfmY&KB77*69FL%sKx(3l5iY54U7S4Rw^3LGALZb@uYi})4ZP|Dp7j%?!TOo~q46q1@L{h+y@x#AMb+ZE&esk*@ z8i@C?EU_s8&)0bWZ*)dO1o_$QphikiPre&lxvrD5e4UBn>92u{;!m=|PF|NenI2z5 z05eWeoA$52W&NZh8rC_qIyYQ##_8uJo^Nh2Q<@~%pdrx1xKdSN{K~DOzBXb4FQ>CV zyH>oGBzl8(20hylDCZI^s^1@KlTu=qynek(3(=&S`OHgK&q$WI!DWoln@XR5(7m}4jRfK0H4lcs(*g<)B3Y@dG;yyoEP>SD}W~Ed_ zhl`A=-{iHCHOJpV@|Y`qSByCA`Jo4POB$1uK@tn{bqOt=(qjvAA9&LSpGR>b_*Z`H z^|FObTOu3|#eRW0km}?J-dh7UqE{ueertO?r^IGUamZ))^?jE!u6>dVvd6USGjI=% zMC$9rsW&7{KUIcUC1YUb$qtJ&FO44+q64UAXTDLiv$;p6L+ZCQj2EkXH|6cM^YP`bhzK=YA5+r2MFOg< zIp1pe;0b(T<}@v!HkNSNKmg9|Zn>7T{1MJ>?|#CXvj+0w>x&tjpE2_g%V)^Xg!+gX zHiS}pOTzb?WTB5y-1yFE8S*gh>S+Fl5=ekHrSO|DtMZKIp#?MKz<*gyomi(&?EB7R`>Dy2W5fz9lo;N?)W2ZL4K+P*AbSc` z^KGu4LJ&&t6zOLxjdM&0Vm<)_!7o_VuMBk0l?FzdPXVG!R_WK6QRir*K{l_CV?e_@ zA~XNJubEHw0@MnI0zOr=(KIQd_@KMdO85=@$!p76F}(B}<*wUPCaY0h-)Y(zt6aVH z$THYn>9tctamj*3#pQGTQPbCOm57@J?h9~yl!=Lkd&Lr8#53Dg`=o5hc4F+0QUEF7b?8rgRm2|#uNd<9`I=AQq-`;j1BotB z3OldWZ#7W|b|5JU0y5n?v7h)3V4N6wc~_+QXf!`_Q}kcbLecERIW#a`1Dcrk9&4v31PO1I^C>T@WMB@C zu_AUBQO6^aDLP>pS5q`%inkY-j{v8;5n)d^{qMF*)M7&}q0C;K4U?ntf`R6D0HLw&?lKMg{R4ORhv*jYr zeQXQ%cu7vNB--)gua*Lk+h$nL_o`Czlkmiex2hUT$p9Y0fefe)T2|l2g}GVg9O0BK z=R5Z0&Yf!B)-O5QtJ%Jg4cKFZlkg$2{tGv~LCZ6&0e6)#5~kCY0#;EKk>^L80&I&Vif&&$&T3_8OHn4E)6qG-o=#_?2&WMnmY<8i3xAut81tx9WI#e@ zGUxew^JiZ*BTACih~!VDEes?y+@fP)au~?11QyNC3w7?T{CFebHJfkpD{25oB@lKSCyVhX$BN_X~ovuZXj zAq2E3r&3G19n-DOgF)eBJdr!#PN_kB`-EQv-aC2An)9=dnT@};EMbe232t0QiUuQu z7nlm={(x7W{Fx==Au5gUS$4F{!yF2YGF5+*}E1$U*Gir z>2&LvacYVhAtB31dA8xS1)}zdFB0XQZUzdqwmFEOyVG{|R_R-q8B&TISrxP`!=!q0 z@U}+#-O}p#;GmTFnBOiEK%sJx2bd`a9k>KJTIpQ3PPMw-RJ&skd0+Ppql0(7Pkt*n zOdMf3l39tI^dMCriwzz>TdRXw6e`>^QcfyT;eGE}_{$WqluNr5J7ZX0-55pSPEWwtS0?(pF@ovnD5T*>695e>zx-jZwSMl z$VbHXJ+iy-M2$6U4#!3_hFummnO3^(7Ui3~<2>@aOUkvZY#;-sTEZoj*%MFfkD$5F z)QPD|9AfUQwaZ0A^$(@7=|5wWW@&8x;Zs{g4CACNg#55lt9@(A9%=*t+O&37arCppTar<${wH|%la6&H>ufnCepu_yCbq5BkDhUOabn5@ zXe1oDa?DAd)&c#n^UhKk7jAB~>9wA@M#S}0RGP^n2#@1+-&*!5ClsrI?L%{FEZcv& z_B|~%cCn8D4mzk{!=sN1$}dVfD)E=NtmmGm8i^Jo;6%0e8!Yh=S6Deyfu#09FBNCa z_baV>!_czT*;wM*HFXk)=nb?V2;qH7@p-C0$Hpk(F)nxNE@?5q%@`?9nTf&#^cGG$ zzVL_PA|q%U+}J<<0xRz&wu6~=`V9*74B$V);CK*PPZI_;bn_!fb4;=rW(tbNCc$N) z8>ju)Y$1XfugHuMYwS;VN!qTf=??Dvo?81At!F-DW|84JG2Uv+)XKD~GJ8x9zwn2F zrtnY)BLBqA!lkK!D##vJ;()RqO?g?`=t#u9lMUiE6$0xs51oUkJU%o`sBSnzGbL|C z-Zb7mT%1tlVCs>yNLTR{2PP7>g&eAy6mRx0O`%VJ8NvQTXyukD+c8VHv9tJm+xLq{ z1dZLR$h?S0byuIi5pGv1PqDY-S`TnT{dmw|V`xDa`ne_z;+njH{~^a3%47WFHM9Hm ziDzW^0R!bqup>=3r#~OoRA$ywXM6|qrUA}6cD_&Xw!h1dZ^V^b;~<}2M0F2qIrd9* zT93qrlg|$aB+LqJ#V7-sp~6}o+-KObrk@(-(iuhPMm>}1W@efJYShpe^E)2{ zJSm@xxjvaY2B4z4C$mUa+Z(%5BZ-|c{I%1)O(pP^H{-nl7W5tz+cokuhI#LD%++fE zHH)~lJ4@zIlpbH_e<2sW;YKT(>~Q-S$rx0yCYYhoEJIahCRgOzo?jSn>XU7Cn^vTv zzaXKd2~H0WZYEv1p*WdLZ6~&`trYZnkf~;F5!*L#eiYUS1vQG^k+Ww}%~SSHi+0dd zL|*|RYgj5~ah_W_xJl(u(c~9>2v%{jflW$ai0~md$Cha-1fxhszJq{2?W~wsLdlEe zLsovCCsieSqa3Qnf0kj zmBM`*=W(xd`R0}fZB1zRC2p0Uv9y(GlPy%>$8&zdi&o@*v;~4lHyXhoYS;5g zDkEaEFJ7WkhZsc%V=v)-qq68=U{WV#ewl^~9mX?KC$R%(C(xR-)*< zioX@-nV*8I;@es0JgNfuA|GZ&FbFkz5*wU;JSV)%Drn9;x|}#G^m`b@WWfkz_rR-- z$IBWyO16fSU`G|W>kYw3Nl;`C)#t5|ffU92!*<$NJZrbs)@93#LvuvTPCvI)vbu#X zrE0K-<@`)%S;+Z1l>p_ItNBJ3i0L6TzA*79oUkq~MojIMOp*9lSS+X)76>QZx)*>F zD%Z7udOH{nCJsRZ9o4{^6)fMpc;llh?B+dlFUQuB%Z&!&JvtSI)6k>&WNhTai8b^K z{Zz(QUr$^Lx7UWk=i}n({t!~b1f1A9HJ{0!_(S)U2!WW51whyNGm zDVk>i33unA4I);Is$!%Ww9-{8npT6kN;UdYQtFb^d^g30S;7zh;Lu3fsf2bvN+yqA z1?mc3vS3|Z$yz!X&192wld1vjd$}RKJk^*hqw-0zhd$942;^|P<^CcC`8+@Xyu|*~ zd8&dD8tA~gJw_Vm6kN<^TlK^Y4SrWYiiO1Alnc`|&H8Ji`|sxLSwO?94@6ny7L-7T zJLyYbSov_kZuViuG%WY;7yfaS6B326#owK9>YX!nPIGs6ZIT(h@hc^l0m%ZWZq=wj zbRgBEPMep4o*FwX=n9|D7{9kiM74r>O(x9X-tV>b6q@?nsD>_Uy%aBS24o^`G6UFTa5WvrJhSq{C9Lv0Ty-|w+*eqrs69+M{W$CEeOjigP{ zO~2aIe^^B|H9STUzF|+ywLvS^4p0aqQvMIvdtbnA!Pkt1UoFZ&Azo@*jVA9+o| z0XA=xYN&Y1l}1##RH~f7Zj1bH`O6F3qlt~tV{i?sR@+@#Lc9;2WhC7ph?JhZlj9Iv1^hPHU@wx9un! zNha3C*v2T=u0V;eTLX954 zcLd;Bf4>M>h^Bh^@6l+Tz-y~?NDp}Rg*6j^?g*9_feBs+;M{Kfhha9;@?y;HQI^;s z4xl;*+s>f+d*b_%0E+a^X9jVlQ7#DQ42;%Q^`^Iv?brsPT%7n?)A^%S%T(fX9Bq{< zws%w9c8~6zYp9#Q4W~eMg-WGoI7MH^bdDJ{)Cc@payxU^57DloODi}I_^!TTA_YJ* zsHK)S0nME#t>}A+u*BdA=)T%SGp?xux`=udpoyOjPF<^pm@otL0 z#MRv47aAbdXE94VnC6XfpLA%%73YkVD?aAF>r&&?g?1Ro1?yKhu@vrmO|8jFrj;R} zBO~NMl9}fT-yeqKY%^42UoqGk@B)d$BA^p9oPQXW_I7gUqf|VQ#|g+&83OvnqPOqG zXgTO!&d`OONBww)ePQoINRwGgT;Yp&mCdxz;pq~IydZ`$-Es*}zeZ`@cKb!morkmy z@Y|gjMZK&SFs1~zoHJRk2)$Ms_~v>BI-tRxeuGSp&TqDvNOkV`!al7CA!7)N(NUxd zcmuPFcm%KM9aOHbBpk}cJ$F=|K?N}cbfKE82^$AzfHl^iNr(Cvs|fh{p83PDQ3{KM z{mTOfR596yW+d2=(jNxW@UcT+B!4zKxMA@+_-I$y40h3;Gj6R6t6?@^%t^BWp5v%H zfMjBs_b@(3Av9~!uZ<9QQ^XtdePQ3u)TfL~PHYXEZIUx=pZ;O@VDw-Xw$H@5u=0g0 zMhj$N@RgF#*>T#wd+;h_`PZV|iB2h@-gG1SzRvJLhyQ?f0#xE2 z%+&QT>pko=>i)JuehccP)6SYMhVwp0q%Gh&sszAW{RWs zt;W75dO^h$4Ar(9w!d7C`ujDlClyhq(yXx2qLqrnxTD}VbmjStP}o@lIqIE@Rh%2+ zX>0)d7GJNw8s!j74y;mG$0VRp!)X88Fg*joEgip%&u;9T370ummmm=HgmMK@AiPTy z*ca>!{p}Dix~i7WUJ(v;PRTcz@?tr>677y8%|Ebf_AJ$ z3J=;EhE%w1bYgP4$)#?)aU;MZ;)NR+9H1GyLG_VhR1GOFr}?F-gZVlaz|j%p$pbMA zn%Fcbk!5|9baQ0Xly?s|L7p2a7S)4E8_HQ#Lor~R)<%E zXI^?kn`$sYQ2%j@)QlEZ?gz7LvT4y9A^R%GdLo!-8FvlmgWWYAJDS96Q|elJOW;a+ zrRO->Abp9^4hO5}IqxnNN_q9Sxv!3vKrSskG>q^ng;-A3IoqEPsPNOK7#8MC-YlKG z!{lpIW_Rs^imx-1>0M;G!?V2oOaYYsvd#w}q8NBrAj~r&9d|So;e*wfC>+Pi_eDDY z+lwI)>F||9`M}zWD1*gjsZH4+@Ypcc%XSqj|5=MCs6r=9K#{`OOj(%*S4Vu22;A33 zq6(M3+Z}wU++2_^TDN)HWf1g$h|{D_<%jEVYJ79jZqN0yHh@Xy^|OY5bK7-mRrAbj zHu^#Xk=u9iA@TXzKMVV`eI|Pkv0(@BS*5V+KS#+0lg((~vE{z_D^C5f9j@UVQ4y8{ zBk6@MOuw39zBCoIDi^}q^TxZ~Ag2sC=F#|Mn#ahiAsRpNhH}*&oV563q#%3pD22n~ zIeD<4mahdMsHisJD?-1#GgJqCI}Hzv^XN^KRdkT~9SfVj>uA2Ik-zS;F8v$k`6N{+ z_i8p-M3uipIJ;2q$!djtk>%2D-U+=AyluERsx)b1ZRKP|O`Lz@x;C;kflh!1dP7fn z3W*>sz|0qtNo@m>rrNn*6^o;JjLwCBaNf5J#&a7*(Y(knXYAJs+rD(w!^dvCxarWG z2}&VqfrjsuAw=t9H6>4&C?^LpMpiireRn-F?li|0X4VAL^w zTwsuOb>K});4^~>%=|oIq2`)@S~4B7!uml;(B%@siKCBn^*GK&qNy!o<9g|VE%W?P z@ib>$-J*9B&@=wez&eXJ8|J81)cz%xKMYeCE>@cHL59<3RIS0bZM)&WXyHXw{w9up zl2rM22h#veJK%j!c4^Wkin^ITJDRk;76UW<^{{f6($e6mdderUaf(;w?#}y6@gP^|w2iD-pPR+FGrNT4sNFtYuvb_=wn^7(-6X}hq4uRsQtHBJ z$WG+LnWM+6!7B!fn5^H1716y%xmsK9@(1Y{ay#i%TZ%_Q;>LUt_whE|$Ff?j732IT z25kfw(-wE3pp2k<@8(Q2)uLCYM|Xqpe;6-zgu*aBAEj(@R5@e{uP{^aPm zPAgw@1S(i-xTh$cw}ZlxxlwH(R|60T_OCiYr$O6#RFi$Q7*TBmY)f@qA3)dicR(~% zE(z)Xj4%WYwdPoy=(A|coqZaeAN2^h*?X>C2{F#0<^)NL?_UD}jK!TANeMeN#>LL) z#Am9=oYM9Mx4T?UQ4}xcbIJ{t!KCmp7QJ@wHX5*|8VDY0EC<-PBo#09FMsy#N|=vB zq!`CkDGssk^g2>)iAw0qnbw>5^OW};z3yzCiNfO;=Vvc&IqMTL!!Vkft|^E8W!THa z4w^}J?gTTpy9F=&M}zH@gZk{@b?9}BFNk9NRWs7)@}5FAJLR+bvxFlZDeCx_W`9B< zhC?ms>PpzY;ps^jWJYMbNPubiR90PogMpTu2L z4Oet)T76x9urj8>jkgwbVkJMSV{RvvmQCVEGEUtsuOd%+A7ilPf#-uw2D9^4w`Baz za@$A=Iq~Q`3KM5`c=KGaG0(zWG_9_OCVbn$d8ZNil5+hg@?Pr`<~L#coKHP%faNqA zWGSP0>FaKoLI%;hjSH>0sU^?2nIQS@0p?DMVMA?a^2-IK3R4ZG0go+St<*vMuYt@b zpPuPK8Cyw3B;ne4{;6G+zkR|_bBwc&)@u>GTMr|ZmPDl(w?M#JfK7-o>r9q?`y*l@ z4DvMJRN?!B5A8-jMhvb?=PR$9xu4HumOl1?SweVx$v)apbOWwc8D{06j0-=TmC zYYn);%6kz!2x5V1xza!{QU##FDnI>Oeq=20JLPB#W{Xq>POANnlfdHfx05QLQ0Fc4 z=dmy76rG>30N>1mdf`7gLiryyd;hs{3quj74lT&3VH$`v)&Rh*-m1e95a9UZq(=R= z154Uw2BJY253>?t!PL$7fCd7VzGJ)X5dc~Qs`V59zxB4kVN+~;0Qn2ty;tcDsw51% zAFzmdK>b0l72f)Nn1#cO{bBG9rNS-%U)#R?4gfN!Kd}JR@&6u&`>#ItH-k6*rN82y zbC3J+0&XUfz+<|%LEFQ))A=kp{`H|SwYd3+sJ3n{4=h&`mLK#R6Q7p97JTK*l-#Ps z_%ldn*Tcde;y@&b0!D^12Fq|(`sGN-P5;c-GAriab`$n)`+?{f&oy*FA`ie%`wP#x z4+LRD35-AgFf8)0{s!KJvpB$53wmxJSnB?MaWtc0OBOYTs-pt)v@@#zax1b}l&MO3 zVv%Ec$U{XoBOEK2y>|P=l`YJ1Dxv7ta&qO0_jyJ5K%+2j+}*Ohhr&7%kaHAkj$iaK zJ(`G}Agr#>CPWwXuxzQZ1tj@(vHWvu5!|FetrW z>E^AxiX^glV6Q)eqMuPSamkMp4aUwHEN;tq!BS0n zNzB33X~~=Le;sU;9y{b;bQ!wfTHFxm7pK3zYL9VwPp|#8}~q! zUSttPo_K4$?yI&7bI&)(^Ah-EfCN0&4##DqTwZ+xryJ9Q*VFX5?G89o1Z&I1s`a`V zzL4pBQCR2AcW=43I-<&aa32!w8yj|@$l8k>`>4U6aV>dr)vZHn8~g(t@_PABIHXg$ zbHJMyzR=tc`bm=?o*P0MICiOX1;N8XDu#L6BfQi4QR%Fp)4ADGU>k~wrQo2E=|G9- z=U3ln4W^XlEXG@}(Qg%T`Qlsh2#}KC_3shzq2GRYky^xFF#Y0@Gh9biDxtf^)d#c$ z<}Qz647*u#f+4QerAO8INm z4SPNZlEmn;{D~47x}=`Z8P?;UQ0*gt3q%I&3&fc2x!jg2JnM;hNo6E}hMl!nSLm3S zR>b1GUE*PH*g^L!J?btHi`*{wl=&SN+fE)EDzR+(IHj^(bPx>&5 zE^SxPdwTy`*uZk}z&mGA4f(tNLS$I`AUr|K-jjd-NCRVv`5163`}{i+}}ss5%QcK-CKMG)dVg25BR~`Uhvt z67{1fGvT6GPIolM&lFw}$0 zUB4oGGkP38{LEf6r9%N3{``tsq^w8on2Aq?oX)KN&xEII^A52*^Nig*KH}F1f@b=J0)> zMbt58W+f3>1)CbmaJ;5y#aBgz;4Atpksn?j;WKxf?PFEHWBM|KGxKeKpVa4n`K`{E z8Q-$v&pMfgE}RE*(2&!hq0#B_%I<6J-4i(525j}nf_+I!kBy?X7xao$r<7BGUCzy7 zr#FyLKy$U``V1tjxndZ=lWw&T(b^@wr+Z~w*Y`a9C!LkrwV_5i>82%nC@ym@dG@Q* zJu8<&vpX{K?V8JIZG-pWYBBFIBBF@lU{(KCxNDW+lJLu;q5)7f4LfsR3dh5|~IB{ciNyFVYR_>g$M(cQVz)yCREsiz9S!ynvjv-^J zo3xT6OsGmOI^!e<6>ljTUv4Fu$o1@mP_+%N^XK;BHu~OGKJ!z$$@|T-{IOD^=A99% zj$sErMh` z9Kw;Pg8hZ>mT5)0728YiC0BHRPNgky>&6d#$oYNyZL4wEw_K*X8yjBYh8M7VxpvK- z97TT^#-wMq+H|_2SGhL!NkHnKPCxI$@ANjzW|=KvA*@6}y2LC?_F~7F3pNjkqaN}K-+q|GlLiB|q*&CE)e^0sJe zn7+%p#1G)kfDSja_>#k)M?nUr=)hI4Qlzw*KmaUvJ;l<%b18*4NVT{l@oyr5I zeRdKRI*lp8okw+KIBkQ{?P;K8mnP^&+_rozd1FF=u3%W-hLzt91++~d%;jS=M21fb zc>S_cK=6@qX13wVBj|Fg5^k6#?r2c^R-v@r4F*GAeTri&JCO&OGsEmyEkjl??_ryK z6aJ9aUg*1eok{|DZ`_w>3Zb@k<}G?3w7x@tmS3Uy(WQvbp;6IwMDX6(45+emx-l`= zctnR=m-V1j2UGcz1_a|}8_o^Acnd8TMUZ+xqBo(N7s`R&4H#JD)X({&XQve;^8v@I z>t^64INy(%O%Y(axzk}-bC}nll+o6|*aIbBUu@u;7mFS!a!8HZ`3-SSQ3I_&!O%G& zDn1@F1r(yt*+(=6+8dES43i) zol_JW#ZrLotf~pH^%a;M&p-UFqifH#o@}amm{%Juvco#QM_1O~!Q|HkfVNmy4}p~^ z?*KnE)xfmR-A`RuMT*jifO3Kz)zRPwAUM590|3pr4)5RV-@;TNO@LPEXyeFTiej)w z3&cQYpnY}UBXbbkFKC*|*L80V+xd#O`oqXxee$sLf)UZ5kHhEYT|_o`)HDygXL)mX zH(0)jc9A0Rs&*;49zK&0!FGNMzmNLuFy=yL)jTbu7m)1w|p$Nb>GX8@tFa z=-~Z*ZG;SGSEPXu_>1jl;m@nwg@e8sd_r7^C7mjFxNF(<{ow^E{0i)>R6WmBvV;|5 z1k+pz+S&1Q&PSzy_}zkqT2ylUDX1|P3vZ^m5B=0VfV6oq!st?o6RQ2wO#R}z}uvUADSY(@`FO}II$ynO$P zUIPX(CI|Qt-uTUie{*D#LFos~>5rP{t*IIG`&F=YPHKbY|5VPdv0etMH1?IFX4sXL znaw*E!0v!b^Rkv#r&KrlTB|Nu^fy7zrTMj7n7sH&l>^jN#pi<2JYe1R9%!wI7^>a} z&{j%3kFbPo*IczhSi7pkxPzYx7IIREGeS7(Q`KcrtQcRaa71$A(r67VWFpR<&Q<%0 zki>$tmZ^=?T?eQTS7&>>q; z5fM?5Di9kWAR;0JX|Yj56_6GZ6#=D1L_lc~5Rn>@UL#$l*GTWZCDe59WS@QZ>F>So z@8119U!D)hTGM3`*P5AgjPWm{nvED;Afa{YC!2iIhqRK!T>LQ~9&?|`&c6mvFt)$M zPT5mcUu|C%gbD=))IIvDGI&B)#^spne35Tb6G?j<(p~n-zW8B`RUNP>JMDb26*;!V zVHl;;^Da)Zu=q(N!kTh(4v&J_$|vN1(7{JH&-$rI*E^#NYkpA2rFEYT4eD+6+GA{w z9OTM+Dl-XvAui^`TNitTOUrb;s$sec#Y{a{SZJDi3zMy)AmjuWuBa3u92&b1-@Ejx zSL#I%VLJr=X3L^vx~-<VTL0vDlUAL4QYiyrZ^B;deDhg2l0;_2J&W?&2=jB6qr_gkAkJ^O4sEM+|)2|>u z*M8J|AV_sq5yC?>2O2B=tt>BUy-JA5W`w?Q+mN}3#tYch-6_ICAEkFKPj@VLj`uZN zo2wbwzhc%}mG|6EoiHBnjtPgFF(k=G-tiZ?C2cg>gjZy~5NzXXMQ!$9=M}`GX5uJe z)(Y42_3m$>Vh@s)kL#NM7-gOLk<%Dx8!PlfX7UXNyMTDWxE7`}N}rO<1RN2{Ow;Oz zmyZUKL*L&IDV9Tb)wdhgU((CmR=O6UPX8y)?|;?!O{Jl81>sxzphSZs1+&!BXmB43 zXbmV##m-qyp)}4jr)`(}I2fV!zJNMvq22y(38a5VEZq+%48H)~k_WM^7)qwQvB>y$ zg$C5!7C+g_kTU~V3KJCby@?%@^s9`u0JF!et8vMr;tvwvup!`DNP3y!qZ_gm^ z_sZTI_CvQGzG8dNk&faZ(dKCjYTu&6CT*%x!cm2sdmviu-o!7mXMQb`f6~NLxHw&P z{&ajI62K7_O2M~IpYgwqxKU<)Ud87A<4k?j3$lED6eZpw4yebjQ+bf;OjZJV3(3B6ce0qgGYTN0ygdYX`R+vy@<;GKf}Oo5M= zGGOC^hlfr0)rioWw{`DS=x}hocfZ~JG=oD(GK<^H)VCvp11Z0J{(N*rVZ;Mqx7c#+ zyO?g|OD&lg3etmU1u1%TzH%Xx2Cu$_J<`93xp;Tp&*2Qt164?FsvWsYJ`#KUbnvl? zp6g>tJ?lWJcrW1ETHc!Y###Z#nGpI(?(4p192rLH$)XynZV_!ar=!4jr{}WOxCR`h zL1--jbf*s?RUIQiDT}$kmHcLuF z6z&HuS!r>`7G_6jl&jV2?S1-sqI4pbPtVWuWLKV-jg+UX0(x{K9X?Qd7T>p0*Xa;*v%ge145)sVM?8qR=vE@aruzY6#VurwOuG4wo;piG=ci(y z8fcfUzg@Dx)rBGoyd6dXGNtdC=!%5acf3$jLjTe#ajfc6SgC_1F8}`FrPFIx*C;72 zF|<+NGifOktV>=2%zRDs<04ohuCAfjfyS{3=w)!gGDD+#)_E7ntXxDgUg->LmqvUr zZ1V1|0bo1>Y?a`QP{828gL*jU8a^DcD#zMWWxkJLhs&NOEPJZ+Mk81nM`)cLQVf*N zMoQHp3xhNiuFTp5SlUYf7GJhH(X)ie$1t8r(S%1EG38%rCxcm9=dj6ykdl!Q#OkSU z>k54sh#LVgMl7RdkrO0Pu6aQhzsp)MYNsAtV8H-8)Hel~fIxoNfS!SSJg?gBv}!Q( z4Rvh`+c#6|rsqIhJ@w#ngQj2JI$nVSwQ*8Y^QI3H)7IiB366sc=htDLUuQD~1&BBPMpoAXg8d;Y9N^FX}YB(Vc3p4%^`F3J74_5*SnI70a&2fxp0 zof{mU`Qm1B!uZUI5Z^1FvfC@|g`zFFbjzd7DAhPx(-`zc1YzuJMMHB|bqgx`SK!dy z4?r8kfg6hSDunK8zOb%`MOo^CG7@q^u>LyjDov#K6T=!P_|Idv^qqbG=r)UwN++jo zFyJJqS(HmI=Ig^5=7h0Gve^9aU!|Kyu!WbtMKUX?q*Ur~dZV1t0LIpm{S;TRyp%NX z=zLcdJx$oX$lrCDCpAV4*-33d*{G&wSXMPI-{uj!@uk|+R*t;SLRc<%Swz|P5^-OGXun==((v4yN-Z|zWGz$SdOp; z6T>u-dU5?}USBO&?|NS%1cgm`mXt$+to-P2Y=Nf%&C@7h4){NmM z`SPeEy!TVlY-FVtaweQ(G$`xCg2~o-^wqCYAvFGe#fae}>c$tC0DKhc%1}7&3DCzl z!32f9Ms-^Kv$mA@YuH;IxpzIeUn_r|Lx@n~b({oDOP;Tvx_~cwk8Rd6>IjmV;tlVW zXsjE6QzDS4%+zUDW$$91L_wmSadNeSFGsZ;RaAmT#1-qV^FBqaDr;`w{U8i!Unp)0 zvr@n`+;~h+iFo=dYouk{HmS`ZDQr3?y>{fihmh)T=GXU-`7DpieSNT>g3ZJM^>^$Iz9 zP1$6W3E1A16^>ULZhF-Y-7ds&^$)^^t*6V()^JZ_gAiPr{O$!@@Kj-n=oKG_Z_suh zPi^zP*IoQ1YcY1Ah*K@r>I+{EPoGB4!TQgh0boO&3&T8Xx*W41vyRVdN^1$$o=nh# z+P*ngmNFdcK#WVvl>4<_r&qbl{>)S4ltEC@)0Mgd(lba-P$o6FDE9ie zD*Mg~M!(C@lXcYLi^p5@%|1VJ%{2ccgpaF&dB5G@zUY*Uc_sf;W|rO{W3PDrFf0w_ zG_mlZgnabJ{g=5&+{q8=hTqoJd1UHzV<1Kb<0>0BQbTUaUr57e!6fp2prpv!42iO> zL>ctV2bp2mn7e(vmo-L}YA)*PY7g_lZiHL;J3>@fXCl{4xz_k%J2%g5Xm^w~HqiHF z+2_>x@Vtk&#Z@$UWf`c3$(;Gs*g|P5;|TRu!kXC_iB!uFyMC?qY|fi=^XuxnMk7L* z8RKbda1V!v@(8%LV35KU5o0Yj{9Yz*eSNpZ9cR4vOb%+;+VFc}ndCZ4ur^cq=&797 zo;erneI4_QNf`>{#t4PC3iyK-6=Ax|qi|x9OqX>}!3(Fcu^;y;#8*Z>a`mF3}rv zWgLsrPQcxL6GZic>hDqQ+rv8tSJ_5=JE;M~mA0%y;u*i;M;GUlf>-hG3$Oif9L&%N zK>KxEYvWkpPF`pq)T%o;YE#un+L-G;heQT^^ZbTSB94k@Mrq9xJV1;4C!{LevDoAR zbGOEE3l#H>FMVA9@5~Q%Y zCkO<*4N+i{R5WZ+n`NS6tt~uQ+1h%%HuH^vz0*B!yqR<)aM(RWqfL-U|5K3jCynpG z{Zn&b06)ZZU@EPcV+1PO0F&KYh)@kRS{LVLsUxl+KqH-LgPxuLuFmv7WB>KA?4Nkg z_Y7gas$#YcZZa@m&A>*8xM=y$Bbrewl56O*6g$ROI}rBEul)*U>8^PK#Uc*zkue{U zPxdk>DSU8AWeXpt(GDxa$X{LCBglqrsBl>B@-g*B&r&<5^Qp;U$DB)R(!J|VaOIo*obwnFe!YA4eLWNP+dOk$!zL{1>7t+ni zel84|r8_w4=*V{hccFAFi(j$07(1e5kmD^>ICxOI**3gP_xL*ZUeuf<0tF|a&X9g| z<8!hbxep8CcamPLoq9=gN4TJy#rX=z{K-zfKX`YD!e?%#Ze&}lo$t$E^|?VC)`$^W z98T{+a8*b`15rvyXUUxws1{4Cufs>n%sm{sI_VL^im$Zx3Ww#NnM2^a(uHr_i1^B! zk5=yZkWqbGmXns0?(*KW$v#ZP+Vl#!At$;g-+)9FymYUM4#y?z9CveVT z99{ZTSZlXqt7(Voj*W+raVU6;FMrTsLpL}2z*`lYyRFD6c}HR&YdWf%;<%=|Dfh$B z_syd!-jYh2lI|&FXt2sjbE)CNWRiNQTnn#4ui1~~C9{`5cy%e9B^f$H`D-VB^c8!_ zB<%C%X%bw8>1Mf&3f?=sG%@h&n_*_9doOI}jyuAO)dEB~_=ptC^H!b(rB=Ii8jv$0ZNYI#X=&d+ zn)aWI`B?fZ-ho_-vQF(swm2R7$#$>%4oWQD9-sDQ+O%0c{kRg};@QCoXkejEmovxW zQqH14dv@*=rk6#P)8?R`jp1yzW7_|4T(#qclH!Ju|tD9I)JY=`#BY)+-^)j-gtIDZ@e8Y|<)m3pd!|CII zn(&*2QnwZlMV3(9$c)izb(t}lUN>h5Tm}Z{6O&Pao=p;}r(ll}=J7 zuR17HascA0z3;78_}6{4T=%N{DV1Y%+&=G*!v~7?IL?Tgy!ABgPWY%)^#N2dZzX(M zTFF$+vecVyMLM)n+**8cqjF^Hq$NI8Z{@yfy4kAXFqdy{)Ei@l z@#QYdd_U<49hOrVwimcWF|SS9$4PR#Rq7-;?N;D(FmgS85l`~n%ddZvdgbH9Ve>dz zL!*9i*PYdz6w~{xd14akmrmk!l7H#$gWq`}bA|NlhL@sGo;-S8(a8aGdD7eL(CJ%K zx{42V^15%{Bo_JqlDH5@c`|;Bgb&2KjIf>94?8i(j#{DHd8at>jb)PuN)9RmSTql! z9xZ)svc6~YoVC$sjYDMq0@f)?)JLm4S&^%*yuD@R#;ZNI9tq~|`r;P!|86PLcs)s2Hrz*fhf5X$@`9kT-caHa5}_k5?>TH zgzb4XZtpE-U`CUifIdG;*2T^QzCqYZRA$2!($PcF3| zG5pBJn`WuLurYtDNBLvwmczN8y?bW}w7KX~QD) zx>r+}KvOvTn8Corm*K+|rs6Iq&osE3s&(?9||&#{=SdFaNDfA?c(fyn^91#fo4q4N(w<*uD0as>1Bl1q-}&EJ3j8`*eC ziz`c&GB$<{Bvr&sW(gX0kKW+$6DwV9{*3yBKiT*WAAxa(_7CWi5P_#W#14|J33qNR zIwrlrpJJwBM&I-2+ZX%V2Bh(P-Wm9sSdV5W@q*HY-HMouQ_^hP@v~R^e%TL_?@Y$z!yVe%Aiylv3iHR5#cBoy=ZcTMb zn88n2EzwO-9we_iTwlY|Vxg%Sc>-3#CEwLqA-@S4=RHMMmg~5WMmNWYvI|L6>63#O zd&o-FXDU>m2}m5MHjXS}-P*B;A?b@mBFPf7`Rqbs=%?FQL$So_eJ0Ig6aPhS7bTzp zv8Paq(B)(kp`{jmnJLqO1)@}iThfmNw_Hf?cn1(mvp?Bp#$dXzV&J=n1Fh=FQY?}9 zaoeqdH5XFq-wwgGI%;|@&d zE*xZjMi^=j9NmEM_p;^~ANxm!V9{)P+n4zemoz;kS#4;E-Q+2yRs4nD{tk>@NM{Lc z)k*C}&z1Y#;FyeE1jNIm-(g$9tY^s0vNqO55|d*UUl0A^#nO9?S>a{=LR|91F#^VD zyi*r=wiBy=6ke9Aj)S=?4MDSezvK&;EC%}DMnoTvLhM9mYFO&kQ7q*EzRzE73=zO; zgp6!jAnaZD!D3We&|lg*?esb?(+e0SJoNKLns%l*5&b{*QW>4@P* zDNQzG^6yiPV6Knw-(ZyE8fhQ&C!VC+cbqu)@V-NYH$FR-#c4UlUR-LSXr*YlAwf_? z%cHIuJjX$DH%7C2!;hax?^0{$nA>9TVaFn(q1BbN#E@@lFUs9{kN|n5n6QLF)!PHJ z^#%VJy@NHB#7D6medAFo`5r#c4biT@1VLsB-~&A6ARw)^hcjZFDJ-j>FWBaLvYQ&DYc@ zJM$iWI$lEPYa6eoG>!Qv7$uf`#T&e1Utd4;`ac+4c!;xu0%vkAx{P-xfTM1-bEFy_ z0k>}CvcU%(Rt}h|YEieq74Hc~#`6sspzf@L{*cxFZ&%BIw}Sr9*uURd{^!2@>wVeo zLj1vK>?fP4w0f{u$Hd5A#h17Ls>k&2<)!Rjhq1MS!oGPofX5cq{tR?%2jC*CRB>az zws3UAcLFTKpM+#-*=eMd=E15X=Rkq=HPawFb~z-%Yjy0=x=@C|a>#Z|Vo(P6AlAsX z&2o@%8kB(v_T^CgHzyIXcfCpsIT`TnR(J@Se!a%jSSszx$i43XE{FHutm%wa{+3Ew zv_XbNNB|3!YwKYBu!4iml@72m9|YJivYHj~Tbd$pdP*TJ2Rfw!DncxIL0GWuy%LMap<~9jcp?r86~0Jn3EM#K`;}!O@w|xU{!p`OCso z9L1Y)l{uYSsK~&ERp@&-GcMJE(+CJHzzF9>(4Fo|${`<=?}NXFdlViWzXyPAiL zg<>nzrDAh0F}FDBBGs$Zez9`QXPF6oA-(y?9Yj}rp4y&pXwcFVAcfK8CW#DxHbzz_M8jm} zEIE9?_oDP{lTR&VC~{ImP>iN1_l=X6(oQ&;P_7H1f0@f<+krWOpxpFzNnsgRXh_d$ z;|e$t6f-j@gTcA(|3R*q3aYYG(4o_?F`c=pP^xhybWAn^>s@@niHkQ4L_js2fO|MI zX$U$O>4Du57fJ0d9gnmBaLdm6^sCoC&Jjh2_BUh^MU9*fsqvhLNUrA^vG`+xfpS42 z643s=VIZ1+=qFrEkY4cj+lO?=S5W=N#Y&6G;nUt!>{_;9iju(T%(XSZsy>Psm{0mO z$AS`jVYZ~ikgdKJk9&CxXdr1R zM%6JVgkqMv7Mnp|=2t8DwIXt@ap{21Oj#qdpKSM&ttVuIfDLIv>4@4?M5AXFHoe78 zlVJzi9t_`AtOo_(64LW&u)EgwR|PT?a_fA}s}~UJO{{EUDXYybU;1um#iDN|hx?rJ z!|N)~%ohD9f2Zin{Q`%Q5v(imKiRgBldeD6>aiJgp1Z6$I74n#L$#QWXOYu&gx*GL zZv`rjEEDJrx%Tti>vw}i0{!V77A)V8avhvGN~)Nj#hWi!Eg83%6-V(+M#rJC0 zjBw-zKKCy0rN!w|MgHmBqKopJiF`&FIy$fU@h4m3>z{1PxG4m`cN0siscoypf4jru zzl=J)X53V!G*Ub&uYLcU@b3C=^c)d`tw8@nfS!Z=jh@q2B?1B?g<|^CLVu)8EoOBO zSUg=#@Hwd6Kz^rrgLTONz#4Rk8-*D=cJ;|brO&-EcnS(dt*`(lLrL)d(VmYN7?R3S zNg7%HHF1oLbig=K_~FBm$=F*VlcO!*NbK@lo%uqb3Z~LRXrYCupa(goOl)Ahx0oLQ z?IYMU#L752tIqd+QqXY&%UYhA8=wL#wb2RNgM>+GIeJ6+YT|p;Pc{LD$9F5kl2_+M z#cHbRDF~NM49D$Lr`-^T?HBV#w9G$@A?`C^)=@P@*1k@eEx6_Juo4ZKftHQDj{6}1 z$nHNos}c8d6SAzZ23k|@6*VWC`(EWp-EvkOP<=#xqS@n*O;6)(Rh~>M+=~6AfM#7;5@* zv;xZWzbQ5R$z#8Hv7~qXmJjIe-c5LP#U zs1|Q79pW0@)An+FPFG>>VY%hmtm|I;Io5wLKu^=~*iW{{p5M+P4h*m>-y9_V z==blmx_$Y?{_5Az&MKkX;n_`RN#B83IDfQ?z|0$aUKjpWU}qKPT&j2Gb0+VzyoCH1 zVFQ1|!FeE^cF0ST{OTzIIQ5aKm^+jp)6J#9Nn)SXP}U5-e^;#|v4#m}sl>-#8N1OsbwpN2G+ja$=qfKyUCO1P z4W!JTG(t~j8oSq$sr}v3toDr9niji-n~KZ+;iGd^9q-JGQ6@`wy%pu8YB#_8%Xr06 zIB_DdWD;@nG~oeijZ8azN+_=tSuY7XAL1t#zZ2Qd6JIoLvHQnxP!7Njwh=~V?zU{{ z`_6pfr7#pGo&Gp9Z5zEB7R6+7boyuQYQ(Ibn1^jN_kzdxg*?ItDZ_4agFP^y+xg}o zf?mNC+TOXbS=g->jW@7~`3dHl0lwX&ajn=y0W-1M?OTE`VDKcPWYZ@q?hxxk>!|2fF{p`IoR3jE1}jr{yZ)hZ3qE%pUjXBcs0M48ym@uLh?6hBm=`AfH+M8sDCqkF2;RdM*Y=<^_= zxx;fE8FiASvgoEwk8NI!rl$g~ZZ*)6^GeZsYUKu@y36Qo`YBLg;em4ur}mVO3w;P? zw4e;@PN%1&bjLU4mAG7yTF(G0lo6Dkl7)2CihiWcSaq2~a8^lfQOj?-e5I^4#$H-L zl)#1@sE-eTL0MzS%t&j@ z(uf^|9hiwW5?+3rtCb#v8R=LooPmnuh8G9wh<(Ff#h<2-!G?w#M^2GKHc~3udfXrN z?n$-8#_scpFx@zocr5~lc!b^w8AL(AQu)yz8Js&Yif4rDZR9!{kly?HAm~$aMhm7O zEW>xERNNIyXvUObqIICOLYlFYIjpZ33i!^(WN1}OKQ~U0(mFcmok<639%`>Kf^5JD zR~lJ~OGfzXU6rKvR}R@14+Vk=u}MrolZ-O5KseG%*l5S7_AK5P>)6CSbqDN~A{^kO z*E?@!!**l7W~E~QA3rf`Z5}5IzEKr&zcKJ<5dAVN4ZZ~{5YU_iY_v_xKc3S;?N9{-Jt5+BO0NQEUhxA+V8ti8p8)wavqIz9H{;UnSJVX0gh zqaoj1IOjA&MU+d9 zBL=T0$OH{^p{q0E=;&k@TkeE=$~Po{uwsEFd=RLb329JLdo1u#cux|1A*+YoKlRcp z0w+0JfrgCzWGjFKzsXQkl{2dcPB;No>r9y#a(dkWmjCk9_tUDJb2Xz`hMDi%loVS@ z-pwa7F3!82N#c=#*I-t~gPCKD1zETq%&eP)V+4bZ!W*MXQ(>HBv~D{K`A3f7s5=HQ z~Vt2UceqMcU-+&T1wFRebX{w2W;~xn1Gr`kowR;R>{5jWJ;c z)%m&+9~4v9fSY7)YSZuh)c*`U{*#N-|Fi2O00sWlw(2NuFvbt}1T$$FI%n?r0+^-= z+@qNcp?D-I`5Un0Rq%{-5n~qtb6REg?|I0g#dRxGwh(d&h}ky{UW}~nfJ}hQAq%om z3a1mA8D627$$NyF;0{g5B2@7IGi>?iFy>D>cK~jKmtSHkfawDe=J0nhzf%TkV5_15 zVDoxFac5m9!Y#vFZAKn2>EL|?fp`-3RUBhU6H{E%9k21~wq|$#I@y{aD@4rU^ytmO z-Sj@ywHU|!Y2yacOMyMHq#!>8BG~`#R(AERSFUXMCsET;w(Ca){Z1GLKkx=vv$tgU z{WinSDnINpF2@(!Sm{Tw?Kk;e;x zU{6hPgb_SroR`-mUYF?APx26AJ4r@i%GE=n+!=7UG8!j zAd$TzOpK86kN!9US|<;G;{Km(CD%yGVCMLrg>Kx#Aso=6MVwbLlZQdNZpr>P2~u#L z-ne%EFgI9)ZE9JioPEwgc@-cxl4uDAwB*l2+-QGTIIhDRGkUoLt=)|Ko8)Nz?~XT1 zV54=v%a01(FM>F;0Jegixivo>0K#uO%m}o#%$Ahjl(ubaV@-jxF7RWPIeS@rw>7}> z|B@XC@-V_P;+M7w+jULe(Z&ww2^84ZU#=6(*6Kk7!8f4`As#v67PnRi>>x(=F$8e| z)1l1I=^wXQ{<5I4m8JG;-{3#gMic2{2zgd((X{`)n;e|TxezMXw)Ut(mNi;=4fp>; zd$bz<96z!1n?H(A?hqO;=|`Sg_KQFcWn5Q~m^gxhaA{tX z)ijCkfjZl1{8~s{oJblrcB(ykanj`NOeM;G4Ex+H{n>k{ZRA^j8KRmznWh4&C3cLe z!qvjn{?`MpJ<#kahJH{EM0j`+>rhe{G_U?SrHtOYCxb-S>P|hpIAH>-W53nOlAiTV zJyGv^)=zkTv?OF~*OjsAbrrlyX17Mzt!-rYXj_{1YW+|cL-9M;m13|MrALj2CywbQL zx}KKhI~{v0IHSp4ZkQviN0;_~B@=f1we|Ju2ZzMN=|KMdwHIBup`C`p|NTjfEyI0- z#Rsx#>RYrFc$Tt6dQg=dkuZ6u94z{vpZ6TqxMXDr`d?^JU> zJ^a>yW&Z0HJ_EgX;Ejjc%tw3-VX6RKx;@Y~W9PjN>3TczBMHGdun@bdE;kJNcWQP7 zU$o{}k@eN|DQ?J;cT0|8&EgYk%m=h)=580V(ryl6qsjC>1NtqRGsfrF^>g#|eA2d2 zieRf|ogl0MtXe@^;$F`dso#w2QI41ipy;djk0cF`EVB4M|73G=x8iqVDC7L9x4c7Q z7CqG$8en#_J?o($Nl!xd(TSm~j}ItB_E#07J9&_yZbT3Gw?Pch0B-0P18>YO{sjU z7T`0TL%0qPOd#|MDvwMX9rAuF|ATDhZuNxR#?ms}z{5aESLQ|tjJ+k^dp>-N(-G&< zmGOD|dlBO&@Z~JJ7esLWs4)^R3&zn~g@us&u;f0vHWyeYQe!C~c92M%p%dDPjeMBK zI@}5zKyn8OlSUQ5c!efR8F!&jqL}j=XK`aq-Bc?%mYVv`KhI}o8;Sfs{Q+%4?YHHJ zAj5@?@iKh$)^k@&V9!8eFV|WAor3bBH@6La=B4s?u7i!e1pDrPx4r+`1DibH&YHvH zoM9826We^hf7Sk^e%p+sVuctU=zE|~-n#b8jz6P{@tv~ZBlccL=1n3B zxZ^Em^Y)hxw4i#=3SzGpZhA$WGu_D9GbQx!`?GDoz|Tc_^b~ z6Q(#`A)gK$V8Q{z5VC~hIb^8Q`IzfTZ{VFF6#(?Ids{Q$=wCIZNV(;8m|Yv@%8aHf zN7=r8$+aJs?!p~ON>A37t0Sf@Wn7wFQ;k3eEWkHp4{mAs7w<1=EW8mlZA|N(*EJVM zr5@L~Eh9euk86?qy8gOBKgm+sLmO&4|YhBG^tOD+5j*h_4wSKmR;p|I972}i8X0eT=ejF$UIo?z5RZIv zIqR~)M(#;GrLnLcv23!YHU`3@_lPoSf2V*VE2FYlZ{m>OnHvF%IqbOtY^Vm zHIBql_YPx-mo)`ot9)Q7$PEf%vZJKd;wf*4ONa}sHt75mO_)lh!`or`jkHp9P{m>` zg^Ok-3SRITFJPklq1UMtV92yqDFD?kMVRU+M@~WK$Ngk0?OxX~iP{$LbX>ua$x^)` zU`ggE3AgH@D}jCG5+)fBaoEK75m=Y_nB1=S2}6_qJYZdv6xMDZskd;cm!k?ipb0Pj zosNf0!Hi#FVow8QQpPNZo!lwgZ2zgTRJ%jFig#8( z{D4xYfE}NR6n!(xj#{oPih8)je*fAR+ zjJ-6d$O3Lv6x80mdtg-WQ09E;@5&qi@lT~dFC+Y+fzUPx>;*`h4W2!~)WG;*<(5l6 zh%eW^Ae9Hp;N8P*9Pu>jyrZ<_{PgxtwA^u=oJkWZ!XJa#I*!s2Xd`d+{kKZgU z>EJ6`Me4Nd7Q@E^_zNfk7enn3!QzlY?#A%xsvi~9?3t}_LNw~`1QPL?61P5)6<3iy z9vLi{zBmVy7YD242iR#&X=x>+{qqqn8#cHS->wJLn3w9<6b-ifi;}&SS!pOb9X8Lg zbcts;di?-Cq_IA8k=YphA^mY+&^L*O@;9T#m?aSx>M}0zE`>I3j^WimawJ%+9#!o_ z=7R@qOsts)EEsSHcn5lXYmxtG?S3P{#eISBe>AT~w&xY>k9lPV=GBAkd6h$$DFgFr zg>f?Huk-5ngUws?-{w{N4_Lp+Z!?w*>({+%-Q^Ug#5F{=x(yIa88`^%a;Uw|h2!yU z`%}`o^4T{Ox`LXfE@j*FO--Y8^h5%q*+mo|Xu%#4bp3xF-JxeiCjFNN03a~f1)C~B ztiTE0F02cRzzE)I*H1Rt6p)ox(gB*aqnO4?lE*yNNS{>`Vfa=(CxF$c8JGF@<%Rz_ z{*w`y{|?j#yeBtEs9t)20!kcp>n1oXY@;_+J^gosp;E)z(+q1;2lXYWApzAis6qXO z*<*>V!BO|YdQ0XP(TBiBe4ABR8xB%|$fZf#>e?`}5~#a@WyrWe8M1>R0H_;*w04fme|?|-&2NtL6D=ZizZ$c|m*=m& zRDuPfMF6q09Hl`?%e>O*ljPV!i(}3tojG?_Ye-C&b&qfuHkx9!qlg5rvs-@m%^v5b zyoHxM9jtIZ=@`-=DV#3KQA@40x|Gf`6NS%7Ef$h!`f~dE7mwC_YVBv#P+WWnH}YpF zf#?!e^Qr*JxO8EalagH($LhfD#cvy#b$F1TiL^QN07@J0(se59(J8--x3VD3e6JOeV1`2;S)6JArsP{MG znwM=mLfWXHO|8=!Qcz9B{?+kG8-r%GrTHOET~58nQ#A$IxQLpz(Cad*YMMGarFxIh zkdW+oo$m%F7KH)r@XM*QD;~A`>Th*AhS!BQiXn?58tlgO%poK0Qu_+rt;Eb;y^dCv z^TDZ@Wlt$+y{dSB_Ir=XgZx4R#jyxQyxAu29#)f*+1kLoSwi{!`{myq7SKBxdz}=F z%zC>##|}rE!aM8*!!Kn98jtQke0Y$c#HD5;DX^lyzKE`^tvJecQJk3TEOkb|VmmSPAs*@a9bTd$$m3$k_-ULP0=*)huODE%iwq z-R`{RGPb`QG(c*8a_-v!*s$X*ympgJevP;y-UtQ)6=nWb@`kFcd(lXN zZJk0z(EXG3H)u)7?ilyrQ1!QNd-zW^F8Y45<`VgJyT%IX zt60+3=IX2Qt43B>8(*?{tzu2fKwP7y+8Vnd^zN((Q-VKS@f`w4i-ki4{uuVzlXPg-Ul@nn z;!3Z&^}fX7G$hbpf!;{7f7t#}n@&W={bU_c{?B`pcP|gj z^%8XID^QUKZD*xF-@YjEx#G;rT8=5mnBf>oO!#}YiwCM+d|wl3qgO(p47Nkpf+A%S zrPb}FttKTJGJjIR`Or)N!jUzA$WtaCai`#FNCzIh`z9f=)=N15*3)GlLg#*#x&EQZ z#Q3*YAxTO*=ZZfB`2<O;8#s=t{|pRv`sj_PJM<)TCv~H+4(N0ZNjoKu;L0N zT8~w@`lea(wGdg5!Ih?;1xCn!JJ7JchXI#1u2 z*;z#jXMF7mJ{$xzS+x|bQ$Uy%xt%E=NJf}8O$bzY9C_cuEs;KahhPNm#=eQ-APw<> zW*4~|&8g2!MB`{G*U$A#F8W){YsF2+=glg?UHj@fTO=9<=bPOSiG*S);NGmJFCLXX zyf^3d(w&_8o0JT?B?Z?ACn^eSt#_5y%0brlr2BjNjJq9OZ#miAceE5A-Yjotax?h~ zg{w<7{YBWPZdFfpnVLvu?6q*W0U%B*d{}lX`R)aE8Q)eHUq{8t0FT?RgOSz7!l~%UxlQs3Kt@>7t3HBNXTp$kZrZs&fk))Acg4RzqTf#Gw5$+1< zou8%4A}>mb$uz-ip?$1x75IaL8O)jH%$lm~2!(LSA=lKM1gEbL=kCtYeF>t3T{O9% zvv2R(F>MHqa$jDB=qhp2)|P|v+2j$jtX^25W^ApP_{7z&qwt=iBA2CSk-e<*48tM{ zVzfMd4&j&^#+M)-P1PRXw?GKdI9t|(e_$^;QtIVCT){;=ZE9+E9w(P*6S~VF2F9o2 zuiIn+GLM#wh1i4FKC9fxt|J@aYfG}bMJ{xFRpH|HwXBMXY&dXhM@Xso8<9`=mihZL zQq7ZS9k_gkii(QpDJ&iUBH2NWH)GC)G25rBUQ^`ht`12;tJ!Lou*zfC!t{L%Hm|C~ z6->5jVzxS>&t&3O?#In0Cz!ljPV?}*TM}>}$SW)t+eHY537&&JFY_?!8`3&@mxcFI z7=lj{12$T`#&TBM(%K|56P9Km8<-kIKV`(i{0HTkkF(ZL&!#s>&3T^}IFw&ya-13u zJ9Vr*oBF`tP49M@2EjVF|7iXDrW2>zzWd|=vQg&Qu-J2!9h)7mZSVpfpwHs18-G=n z_LJKBv+6flP?CAyce?o0RkJNs%GPFoPr;JmdK2$?ry9jGINvpLPi2sB+VyWqHr9yw zAF9!Q$GizOLvl-R^AnZczjEdvF#VWr-b441$B{ui|}N5g=*!WwSLl76m*oL}pnlh7c2>EP+XINGs< zY;B?O$FN_w>)O@o483s!5%ri4M?zZr4cj(~AB0#{R;t zNUN|6r@cemWmjfmtObvi0Dh}k{cyl&X!(Nt+s?B!k5#F1Ig|IStcS2>F8v^R`3xc% zQ8i1tE9!DS*_augpLeF&@YaJkkXUoSAp5Z~;}$rWLGO6`et2FzEOjRvu z!6j{o;%TcH{~cB?N*soAqPZE8low;(3VJy1f&PjYsk!2l9-K?in`I-%3woi7@04~hc1kkj{F(X@`bQgG> z8H+yf4P<+}{P$x5(KiNJgCl$=&6`T)_bw}{43?Zxz?AQ`uD9r=_0_`-k*$_N7DgYO$)ZjG;X@zkfH_pr0z-^?m#M;OlEZ;T|Jr5#C zBT2k~X8Q(nM*DC76kbapEn}X;)_O6$Sc(^{4oY2sEnI-Q{p~}z5yUV~Bb=1-QC5Dc zxrLT(N%-RidC-{s?>A+{4cg$RXx z($6j?6de5&CSD5L>OuGFi(G!@QgO%@h>_k9M$`6dOD#@3)SX;{nK8zg5gL2!M}!%M z?)K>N82Kl0%){GaILxk`h6(3>6{ZI0fOm{R8AKuU4(+_+!>CMt;pkkCev5*Tujft3?d(Gee|ix2 zz~nGChYba|V0=b3i=WX`XyZ0BiqvK{MI@aeK@NLla^KOK?#{BeJa*O&1E zym0Utm**W1qUf!F^UX&0V(s0!7l)}aeaf{b6!hT4Ct>ciQICMH*v41P37YVKraa85 z0WHDd%NESvB}DIdwf>?Q!d;7En)`FJ`5emt7i`OgdcuuZmtHaT;s3Hu?)(#77yjq9 z{~MYc{C7N0951cZ4@{P9>uFh_5xDm@zts8{G634l{o z{1M;jw6h%^mn3fAY#KA_FwXH1!c z%NT@vhT#j^z2cvl;|NV-8XA;T08V&}7+KVNj2wEiEf!L5aTf{B@!x=sX}7a{8|_0f z=Ic|UTsKl(H*z8l0}Z#jUCYig{sJ|*ZFD%D-h=y_=I1TyB%mxOZ}(e+Qg?5&qPTAZ zTi2&fgrS^0__i6cJna~klsbz_BzLj2`8IMnfyM{7s*KBVHQrK2j9zYM@X3P`ch)=v zLZroj7zn>1YpLh{J*Kf6lMi;^7fl_*c6{-}LX|v42Q;%&0D-wu~@Crl$YZ z!c`R<8T-Q<&iqDutMOZLy{)rK8Q3rde*lOE@dxW)$83x?u_9p_)ro&}CG)BGg?(|M z3i8r>7RCurBqu)Ow&v04-3x$;Y3zCBp#9da-1~QF#%Z$zUF5sS+Xk1;Ah-&=V1BQC zQaZ@c7dckMIHkX2>RT0 zGfF!(@$vopNB!@K7iZ$E1j3re_j&9(@(B-;)4jzVOYLEz@a7SAhV-%0T+K&@MOxa1 z2xE!G8=0rszTn@qWJ)RQMrYJ0-!@7;vGQ>ZA&2s=8X7kVP_s?*SMu|SdGz_(3@4Cr z0E_LSk7dNrG|-=pOVc{K`a53fetM|P@w*{M$=};>7LPJdB z2x+i))cWxec+=R2jNVW`Xy)|mH1oYT4-7XpKH4%WxU&LF)Y*JNsKTf|O19~@N>xae zM#$#1Xd~S1`Zr-aQ6y*Cyb-&2`pjDDk~{{*^62->DJUiy)^g_aJKo`x6|t%o_c?#8 zfmw7t=`LdqaCUL@=}H&Yp$A0aL-75)>%kTikQvpjr-cUGUu%|+eOZS*J9Eh4{4rnU zFoIBUNJ3ZsnI`#%gK_=|Beqq6WvKB~i()J3TtY=yL^HR?98&z=|6=bwqnhgWePJw! zqKFEp)Tp2+C`F`5NmK*`M5IP)M4AwaG--jT2nbRH1O$PIG^r7#mqdDzA~i_wJ&*u_ z03p7Ur|swLv(Fy)!#(G|=8Y^Iv|&F%Fv1Z^{_kMymANhlwm+XrmuR zB0qWf%IveK2mU3(quDi)IZ?0=D_dDbClg{VJXnbk&tIZf+OCh|GOm0|Q;*Hxgt3`Tsc_BMpZ`36F%_%3c{Yl5=rcA2 zW-X^(CJ$e?M>d_SiWICc7uXi>!ge+=!#IVzmbjGiZ4HOw{8tZO?`d_&@2) zeC?L|QAJyh@DKLS=Q&{X<@kruS6t06hLg~Y`Rjq(X$WPM+iwP6+NlAY5rDYC!GRw_ zZ-mM^JO#H9eP*?eXyx;oR5WVr0sS!}$k7|tGlc+s{?d+*HRpTk&_3zN1 zH7^MD$vT^av+sP&m6}6}@p=>50VFJ&W1Xpv^+@<}>QmkNyS5sr*Y1Z6M${de;ooH@ z;hj^_?4MRBD%0{>fr*!`3yVToJ_f5%+Cxw5+PY%hci!8p8?_6I-mLSmtPu6|-Y=eP z-|F`qVMMz^Dx9<9e^oM^DlBOZ`w_b9tl(AFCcn^@jtr?76#nC!_z zH;k-Yn%|{~=*N=z=Y5M`gTiv=Z2M~!n$I8_XR8+SyI)5t_Qy#;u-AWzH~^YwNJOrW z3+q<)EYe|wQ+3K-c(=>kN=;L>m zZ%-7izB5c}&ar6klCYDgdpM)vXg+b#Whyu_qxbTKPB(+GNGUy?dL{Uv0&*!z!vykN zO(Y>f;L6o|TF)jY*cZbM3qQ`eEXbxFtKt?LYT6sn2{$`k7C9NTH1gFqi9GkU_1x%s zt2deqmm5X1){PCFIGAFRqwA3OnjcCb^<^dXW=Fz&nxC{Y`T1XRG>@l(v2rc|B!|We z6X24VNN-Q$TK~nt%E1(>GX(~lVBHAzefd^P$vfkl|9?k6=0PWVQo^22f&b}^^(m8t zSG0xI1!mFOKbQe(!bR8+d%>Gswi;w9*pRl74NFNOVQ+wD$>~3fDgWwG8#~PY7O;)> z870~L#V+z2mWb51b z#qbkDzgdi)sr1H>;Q_vY&AY-<4zk__kb(u{gaIzYBVs%DD7fK@Vw2mc8+0WFMS5vG z_4Ds{J+6}s7LH)e`v7fXg%dtHVq3D)wX@n^iAouZAXk{tyi8vf=(rctR?*F s{Z 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 D%h}+So3xMJ@XRxOU7*+tE#+`8 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 N3sW4n}nU>2*dN)PbC(5X)AKSeye*t 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 z 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
NwBB4c!^<%Dp5xSc$5-EQL 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 z ziynco?V=+>OU4ca%d$G8%5|wY&L$nhEpT&D(A}2}Sdj8|?FlErXiLV&)9`9qY(~+6 ziM+LRvo4!Oru%Of!3I8$7h40&Gkcz1XvNnX%SwjFx?j7-K_^m;pg%hvZ<%myD#WsT zS}Qf99!Le3Kc9NuV`aZtu(!Ywi31g^RbH~U^m_W|gYGUEl#0NLWN$GQdXFo!r9I!q>8+iOPnmH@puwIeqq?|IgK zqRH~QR<9$DO@Ah@SmDz}akLBo7V$q@PE_b$G+bzvhew6$UpypSIJuUfpCqB>ZDdzd zPhX_#sfna-UT@KNJ`m=sZ5IFB$9q6%zEWX=seJ-Nsx<-D1-vwuusT4o$*Ep4vubP-><)0>9-hT_c+T`9QP)&sX-r{o93D-(vx^FT zwDnsfzW?8U@&5$Y&z>UKGA(ka6DB(gTRu?&_%QHo(7)Q$es}vlc_^ro1tQGComd$} zeh)y7sJ5$tlnel9eIAp=Rf`~w!dH^Ev{znXyXZE8b+WV&>EI=ntiOxg|6kCj(}d`0 zy7Wsh3JX?gX=+^Iu+Q{hB-Bm5*?LOwAIcD?f`*g&!AC7NYPLD}H#BbFrCpmDV#lDa zE3}Qk`Fl$)~_d9In2dWb66pC?*|Fi?x<2i zalnX`yCMgzYxqe1Jh+R<_vs@E|2;`;Y>4>=K$IHNvdxCib^)(n&Gl(6?YWE7L8qeT zM+=@R95FcyuCCxUG$}&8gy!tbSh)1Ts3#<}p>LC8S7e1r`0c$G#;1y+Hm-y=mFS$k zD20E=agm@ND9bn(EgBP?TtCS5?m16A--Z|gEkk-mP_MxP5+F?~0|(`5KU|5mJ=JqL zPb+mz=7N`!U){A2qL9;<=~(0(vQ{2>VFqUU`-=@_oM6m|+cNI&oSB$P8AI)9ReY`~ z)_F{EU=brma;rPKVntEst3C?B3Cr@qUqRxx>#9VYaNB`<)r7i_j)(`KN3T4CT|J7Z2_xu{UtlyIiaL*5-1wgMfYqB^OAJQIaFb8R;u%E1L8vUf+AXz0$VKew<+sPb0O= zTsEOx>dv!izJ<5N{nRUT9MH+y%T7If{`I^v%Bs&x)-x8MRx~Ln(E)wPCv;jr*U0mY z%vebN&|_1gb1kz7*GbV-DUl2_~2P@X8Y*=N@(k!vDBcaFiJ}RUsu)lbF z*H*{1j&1d zVpX?>zYAA4OY;SSU^OGg=_2f$7aoq-d12O>+1zSnwGm=Og+onT#yu9(WTUa^1?`-L z4u^de4_Oe8JdP{DeF2qf-`rO$f%j?i=fGyj7c4`bDA79CwjQEoT({+t@vOsdbG5qG{R_61DGnD~_ zg&{qlz_WVo@_W&si$d~z<6N3LA`y_p++JmIMQ)8iMIcgo)9DwUgnuPz>qpUT6%9i~ zM#N6%6%GY}a`5z_N(@Nl>&oAS0sUXP1QlF?Pb7LZ=Q==xXyo}9Ep9gwd$I@Y7~(kc zO@W4-n_r{PztSte$2nX?fnAoy=sCq7<6rHoIo?2i<#UH2 zsB}vss(0jK7vXKoh{JP`KCkmGD~}dBD#G-Xm}UIY@wTbkV*V_5>gy?(g+YdH`HYnM z&G83DdZlhWNllGJwpNFGA6G&H3&#v@}WW~-Q6qQWJUb+v-MLXuxbQ5yo9QjSe{l`SY$vh1Pt2m!6Zi|+&BZ4yqJ;<}H5`v5z zXUa`**WVVMaLTK#0Qgfqw0lDPlM1g#o;3%}lm@>m%#XVEz^BFIGu5b!5=KV#RfrQ7 z{Mz-UqCM^vQYQ|tZILN7>+bwyjGp9$=Xg;m4Do;UC+u}rT=v1LV3*zn%QK^Y&Wcj)#6rv)Y_@b~z=CMAtz zSK6!GJ#fkJ-nzGInt4lbh|GRZ`Ys)n;EO$TvUhoheBkOQxo0?|8Nxd=KW3C&ni7aj zI?Y(_gtqFD1slNjP3)mx2TjGFBAjatsY^1NnKN1?3TBLZOmgzo-sxU&u)nPNFk7ei zOjKzo*qZrfZfw8_ky&*{UEI^l^_4w$#6OtFypq-X*b^zHnS8r69y4qtsArLP&u$83 zNYo+ab(l-k2onwLqjIS>aeJ#k{hGIqJOO zU3vf`%+6o1qifE&Cls_;n19&+xSYfhzQ?4LWY0v#R|iDb&Xi}L-Q<0kKeyt>TM6#J z=oh7-SkaJnAgZEsh48n&1^=mSUo%`mKMKDC`qeGzpQ%pBrq5x0yk5M(?gq~`P1hp- zN}71r#{O^rHfFJ-z`V9p z{qq0RE7+gsw!*$>ze}zb1{aTETuGq;;H$uQp2j%IBBgim#eG}laT)j_xr_CyNv3W$ zPaF?43Z9X#Uww1o9Ag=4M{w}W6+LAhC$SV(){aNHFErN zxZ?D;xZ;ly#&2=OKYsl3TU_y5T=AF4&p&iraqmC4TmP|rVWpaFZ^|A?w46?HzI)#y zYMk6%Xq6gG7iRl@5(RC=M_(R*s5MP*AGvI@tNmGm`~H72Ilt}y^G#O%?aa2PfJx@x z$O;M)ABo$QkVEa|J6n^q9JDOVb<0M|XgY=C+S zwUU>0oU9byNygq`|U~1P*7rWEi4RXA8i8C zn?~>vqtu~$3|3?k$MizPy{?DwMRiMm&%ot8XQ+!BD|0_aY!cU$nQIjp;;@qJUhbPY zCeo?+pyU9&FvU&WvAJ*grF0rNRy*jpx%DW{KWgjl{iRd3>-L5=eu<>Cu>;Au)6W%8 z@%x&jrMLlUC7-E!mo$k1-n#fSi&5K`HK!?&>(n(vK3f7c6*8wfQJNA|HR9~>aiNl? z$T3Aq@JQ&<5qaK15s%Q*B*^x;=|Q&KC))<0xUO8Wy^_$j7@J6V^1wms(~72ScMkAQ z(kUB4KXheChO9V4+^dJDZ)ap`p9((_aT$7uXg(SlU2}8~I_KqqJInj@Uk4}Ww5T(a zuXT7Z+`rewMRTRK%@vKr!1^<1$8=s=67!0vCq~QT)sI^t*`>e7Ka}Es?(uY9&RyNd zi+;wg|BdLIILEzZ86&G@vbUIGOCL_8I>_$dN#tMRH(I;>USY&YEn@qM3rw$a`@*Hj zh_q&x99Bb;s+o0YB$b*BU(2E_LoBOr6kbRb?_@RMdn<`XzpF4twzz;O``TrMvot8* z;Uo$oA!!S1hSuqBaRoR?t(B?tF$)kTQC3I>X()U14b=HA>9uSQ4)Y9k(IgpMKEi-+ z(jU->ZK?n(1(?0HOwnkpbD*5uNhw%RZfeE)8z3J9Hub6J4d(Sap2+n=*mEg#&9B7tG(+zGkOwX9IA4=mOF(;lXD_2Io7Lh|;mft&b1m++ehUL8 z!_c97?HlyuuzJuYQA<+MB}iAme{y*t*>Gp%*37<{Sz_RUXl6qte7G#mh7(BGMK7v0O|^vx(a)5n{T`1?>3y_E+TP;d$$|;n9s;B=XS3x+F$VCIQL%^74UwJrsb+IF znWs25U4aoJI(5Pk$%dz7bLO}F6H-*Yt1|F;Rvs1=aed0>O;wMUodRiyC)~cM`J#f= zmRLuT8_sE>3nvRJp3=LO!MvdaOFItAt5+R3I>a9busjAwk?=KFKb)n(Ae@_MT z-Ggx#M`lS}M!kn}4=pm)zq`_G7#}Rbe+CQ0A+m|S=kRMrly%;))cuzAnqqbY*|EXvUAy;l9AAF_~M654OyIk^I-|7VD z(_7Y3qxPXZ={Nly*Rx{0Q1&Ug7Rjc8DU$*y-drniIqIx77k8ya8GC&MQIhBcurxYA zjnc@9xyAkAqIi)puK2{~r*uEkwHQX!T=Tr(wrX#v%FkM|>XWi67UmaF!;^-yi57bM zzU;*o-j@Wc!@J3>S}hujLYzewPl&mn1niPM+n1@&fKru7$#q|-*JpUTJ7 z&w@6U77V$!%h%AJM8}vxOW^~CKsM2YbXmqt2QB0H6G?FwHe}VsY82>n#OL4RIQOC4 zLaQszHdw7FW5w>{WAq0wE#utf6S?rAav76C$@{zq4jIM--BZ(B-VklHqjcn_uggDu zMV|+ZouMZzi?#|=PWRVo15B41t8+I!1NYVinR1~)lkKSa*YlpnQq*hj(~Iu9fXQbz zhE`gzygGzWWZ^>~=3}0GL$Wp0G6>72+qm9d-du063H!YmH0zRD{QJ<^4V{IkfD|R*Rl0KzWgy(ke+!9LfTSC-Kaca ztw73KQ!pa)ttO}JDw9H+z}cuLz+CFipB&=^r{%@>6DwgJy^}Q;x#no za7UmE_zu4$5?nGyMYcn{MbVC3L?>w#E%9~E= ztI2hD8*09fsJ`Q=Kt9jZ9V*As%I-bTLoqd_FdUwRj0jd{yK%4E&2tPEt+4Laukqzd zrtJ#(ez(`xurGVR)U{h{7Du2=oD_8tid^3S)YY|Hsotu_{7A1%h3%y1Ze2;cl{O@X zn$uQ&Yv8vY{Pqm~6E8{bpg_f4UN^A=c^@~!nPk+@2lCVlh1A<;@J3C!0|USIpMnHc z=WCW3TK6(+lgea=Ay|wZtNsL zQ`akfYyBr?*z|>;=weB-X{#(G^{}5y@j4ALLB`DhJpi}j$nC_+xuaxmB(BeI`;)pZ zFtat{iUY3w6P63~qad$aFW@f=dfo#9SU?1>7ww^JsE`#m^dWxJJTEqPeyE&x|-FTJA`@<6bx)yO3FJ|1V2o^bJN-8%wg0+o;y?Uo#=H}LUc<@e zt(J{sw6aW|hDKkga*|hkLl*?O&({LhAwHE9z9nDus>%V>)LUH%CcLc{w<|ZFG2jEJ zY^pzy5-uj#Rhsn)mG8i9M`7NPjw&*;heS^vWCpGEnMu+4CXevrrfkGVq=zKy6&TdS zQe)bN%jvhpND-sH89NDf`VNy`ZmZ}VFIKN+%1HuEjo@SRGQ;C~doLN;RznUqjzFjl z`rcm^KWbDoVtjxLq**Xn3eFiXp=IE&>6}P?(Q)0>=k(3=1Z_L28BI+ZT6gH4wOw8& zs@HWMMcek4S%?v9@k{emtL&TTjH4bEz$UlX8{XDtogSfS(!FO_;HTZb*uvxt%6lr9@+1$&xqh7DrPLDmGmw?xl2VzueafYU5&3QUeXVRoi zO~zwxXqxjL%oO8bEsBr`pc{djc5~W}f?w<3sC5Wc)CdT?v*I63lDlq9f)TsV9Ws>DL;dCr2_GBTw|DFLy$Ds`rKo)P>h&QOsrUxsqe`e{w(T*}tdR+OV6sjf;dSQwp)!yO1HNnKcTWWOYxucUTQ(Vg7U3aIW*LFJI?8Oy*ySMWu?iM)|#uB`j1WxJ>2kCEI> zZOyorq0O@0E2MIENqD@GI-cYnAmq~6a9CYYj)`GCffT&nr{}jEFj{bWCynei*pPFfw1AU6y)rw-ICs*do!qjzU+koo+kP@gVqv@v=1!s11iU=@ z!d*OKZR=A@1@-zPVm-?40KU!{LCBXjJy(2B7w3i|Y*#-g`!$^unU6s{HMNlEqz+Z? z?7PN0KOQB;plMjJX8zg*KGwY+onB*^!^!=8?0m&%D&_9e-Sgp-pYmCQ0trk0Lv+^X zis0z8u6aRE_!3EcqwiDSQksA)O%K04@F)xj>?Or9dF!QGK}E2w|Jny09(d5!&BR|4 zFYG;BYsr64Zo4^|hD;!Jrp#_Z>eFp(_8qpp1VMQP;%l*mIY}2|N1FsL?!6h=^!N(9 zRpW^ib5pxsq0|oebm*Sh!k8$(gP85SM@K#Khgma3Y^miQhigNz9>X*G1 zE%tS$|4x`?UcS&ZVMM8w!H@D`RLfFJn+H|b0FUSo%k)2DcIjTJzbJ*xEkVY*DUt61 zZU;F2-2&$*$(!`zOD=ww?2TXBWdEr$&u&Zy5QhNxZ`YB2h~=Z?-NcTudRPiT;Z6q` zXOWF;gTshpr`OT1GswDLk-nd)?2Y{%!2ucU=vk{Qx>xE6Sq3<=&$+D_6znQ{*k-qS z@K$^cVa$NimbZMUwd&R&cOIp!Bl+o($@1uTlDKu<6;KLTaEqrh7o`fZ(2WPbEw6Vl&?u?X`grwj%CyBNY{Ax4lPJZ$N{}D;Q0`b zx!j3{YTdscLVd7HUAnYbtVmr#I^61z?6Rk&juV=#Lun&o&Uj0=jXYAs)x3qg;sT*u z5ydLo24TndKUhuMEuO$Gc6!_cDBf7T7hnLfgg~YF40eV3GnEa+?}^ez?A$F1bK6+Y z_`!ylsyi!UJs2`)#%lqCkZZl6()bF^MSy&LiLP-W370n=g*ZI_#LNa$<1$y1H8N1A zc(vEH#r@vv4&y7~^;hLVk)yUpYtZ^H?gUVcWW7)Gk27DI_srqrPto<()=hiTe#@Z9 z8@*Z*C`bR?JcJLjO{evl3TZ~p>YWNIMbcf6(UUdS)$yaqCTRM|PDVfW)tQXy(LGN1xnkCFc3TcmCEwr18WSmXU@iQU}DT~FV&K%HjMOy}6 z*AlO3WQ)Q#Mkw~L`ll%=J8<ZDi8BY(q``pvP4f29u1KET7&v8#uO zqsT_gX$knmYEUkCYTywxJ3KrHAV(x}xnYj)HZY|BO)Sx$!HE7>+o+YWJ>(<$nr@KH zJ+1954gJccRx0o{>a*P}KsY^(kTJt9bt7s4tJ@*o;$OYmxAXpe)3I+3>fg&r{mnuB z%|ZQNUGddI0JST$^0WwP zP!Vs@atD>&yX{-?@j_z8*}l#72Wk)Nia?$RG}N~*41K)p8D+c3GB*`!U%xH_i400a zuIpoqExzosZzy(WL4K(J`H>Q1A7z5oCrr81c6LvcZ4%o!%0^t|s83tYd!a?yErRdG zZu^F~Y|xo%ZR&mC`AoH24t?9QQ==g5?w6NA&c4U^nd(cq&x|sX6faZL+eOh84n%w@ z&gUcS+Ho8s^HvDHlX8$R#V>7|3bp&z#BaU$m#6m6_bkHlRd+hSz6BwHF;Wt@GGmHp z{g)lZ(&tyz#DRFUH!z!Ee8>)mA3@@VgwEVqE^dX@VPDqcgBVPly~LZb^fzCK>v&Uo%7pQC^O_1 z&SvJ@dm$`=ffvg5_ddlMO_mAxuwpSt{$eP*QD`-C^Dv~R_yO`=WZ1%ZG^ITmY2P!x zT_39!vBYBSAqjDJ_hHnyarIZT{(n;draXGxpQz0O_MR#<=aZhjMziK?oe3XT?>DV8 ztsOj<+mU2csd9UJ`BU?}bSr-h{Dh0!a5pesAPR%$O93l4sLw0T4F zk__hhJqn$lXXn08QM?mVp=hyhc&LBhx^*|12_=geIg2&{C@K#R6}Nm{YcUcfMO zF)7-4X$9_xl;#I6pYXkVT>g_zDfIO$IDhB~M)hjWWQVD6y!zf!PW{^D2PZuxTc_$C zki7zxsF9V@fBUQ0~qoGD)b@ zrZJ9I6l0Ue^l^FWMWxT2fPd6dkYPPy_*zjm*_!c@*DAQaoCUXTNURUfzkxqq+3L*| zGvVyAB;OtLZpQgIuc+8?PhhF^<^+bbZEG2D>J8OHB=a&|-bxvXG%!hBIo|0koS^NVRA zmYiK6KMiBPJH+!H!@)(o_De=b(}EHsn4im<`{R!ak>NL8F$QiR=_X?(G;~ODK@@p0QUQ|l2>Y$R?71h9;fk$7SRfk zyk-07%dyuNCPeWXO`Vo#Cqh)Vi0XMU9A^k-I2BWXJ#K8N;cE9@hcA|1p+ChMr^R2t zqh{S?=H9aQVW7Y1^zFSU7p?#dXXflfS$j;L?xM%Nbi>;Ag0!P`w~?=&IZMaj==>aV z`r2Qa@i8YcTRhh~yWO~!)&audQO@yumMZD*?0Cc9Zx;+P3m5u4SfPy^*Yy&t%qvnf zeF$=4Yj+*po*QuTRXPNayQ+7a<60&?5$hEKcji$Y^{j|&Ta*~vI2%Vv%AzW07I|i^ zcS)LVEGg}=wNt|f_DjaE(`I|NUbv4urw539MW0-q5<#fr(W^Zi+Q|Yv#e8=_3M~xN zcKYRn8Lf;=g|-xW9B;NKQE9wkr;eOehpIefT;1nvMz*RQI({)ZC^JvX5UHJBP_mFc zb)>sr=Gl0#OMhanBl@JiCHHXSD}}4Zs*Yo-Y8HfiL5M+Jad8Z>sCfVg&|E*TRB;k;iC-sP>6~6`C{| zW4D-3$ZM880j8F0yb2%q!1XbSz0=D%BPG83U2Vx+27cr<+t&11_3JfRvM8~~W?a@ZMe(~eG|ZI&;GdUIMLv-Qx+#rzx9w}(@Q1Yw zJ}H?ChKurE&Cb-)IWy`w0xy@b=u&7Os4`A0PaBPTUf6MgI5s&ISOa6XVpUjD)$Ah6 zIcA>HAMlcd?q`#p!M)r%;$*G>lB@2|KdadeaKnB0tHJEw41)dXdfyUJZvNdM(7!1W zT&yEz+>vU5ow%8t?#s zh+#2fJF}zE#lY=sF+w5&Vg_Dvl+go>Ux#?RpWC3i4ZD)ZcYX8bOsf#f!q@en6vhhL zkJQ_r1c;oU?oz;SQnki+f1&Td?Yq7blRpv^_y4>tU-Vj1ejr&IBXaT%RU?xmz@xS( zelDU|PgLdW`(Dbs95H{6w1d!5Dh=oUkckRC^J>!@03ilm-l=6=e9{{9R@(y^TCS?Tw$quA?hvU3#4Ow|$~-@9y~V;NovP6PLbq0`S=U z-ksnno){>jxBh=V5dVWLo^NeS{3Qe2zt~r-jsP?gNE-blva*;0csSt5jbP9He{16j z_43dbQqxG%%4r}YavInCtYw1xjs2l|l(H)6KKvy6zPOg1kaVxf1!Ech6Ob5K>%Be!vxxCFZgFRY(a zF7bD~3;9rK+9rJ$d5~=6)*oD1_p0?{NC6d=o)gGflJw#qA}j0O^h{K2`@e&ttP zxZc_y!^wfxPjCZL1#~`f7`p;K#Bni*w1?;R{#i-dkdA5EWwmGVCFn2=A3%iQB4&(~ z$9}vxB7O#IXLHNC09AhtJOO(SV@LIjtv~I0_3qSrg*oF&8jNzJgpHSbZHY1UVIRN^ zW7tBFnJgMTDY)-8$J$4d_sBZxLCUM7@7^bgbdosg`&$_W<&Pn)R+}U)aj@-8d&0Lk zJ))gpDWg8Y)+rsyrY_47qXcF)M*Ck#bqP{m-qz8>95oqz@ZE!w*wl~qFq1ihi$V0` z?)N^*tAjp}xk;ARqoDzcjkd9l59;}v>hBDmh(!u*7>dPe;U3(qE)~7MIY*&aXL+B{ z^T8x7tSZaECSt7VkYHhi&lby$Lmuhg$ecU3iR69jyeqBy)q%+wzUCG1EZ&7gdc4Sc z8$MyaDhl?Q9+}L%+J}q?P9@FC;rh;hrm9rcj!+}6yA&WPn<86dz-2&~-IP8LbC;MJ|~kg`LR^0@07yz5k$(g~eM6FF7mgwMXjRXEZSxJI=_?`TSh znGRyFJXn&W$$l=qpQ%($kd^k1-14x7cdeFOiT@ z`{0nEWN;AB(N69H%#D95Yxp;0{=c<*|IzJJ77bN4_~R1Neq*}-M>p@^!*u_TuJ~== zzh@-mt$zmCMsw;Lbh*M+#|2~~m-nMT+5$!vg6K{8{uwUC<1u`Lb!QBw5s5fpoHikSi?Rv6 zX2qRYkucRE?7rYu}KNot2Ir>r<)u-lb zb{5mJb`$d>T7NNV`jK2i=0}ZZz{Oo1oiG5naotlWXWg@rHLHvtJih#r_HCU58vIwN zm;bNB5gIT=Lu?lYv_W>QF!VZm?-ogG)tqyeUn6-};2GbUvCbV^tg&FpqaLk;5T_WDc6Xa*49pI#YqWPH=R`IlN-=_KGhN8$(K#Rc>_se=2biW ztCbbB4b7javepo<*^sg?>}QP3*FC_WlD1{(7TedRo#@uQ&?`w1zdld4g0_l+?xr|A z^w`qCr8pw{2neUpc@m^d0iVzd>E6P&hNvTzC029tztpsYq~5UDT9u;S!a_IO8&`nY zS7gtZ7C`ccDGOj@zuTXwptGN;Bqd}$xu-fxabN2C(v-QjdGPEGY)XL(m%_FM)&El0 zmw&<2joo_(mPMW(-&{>XD$o5;*O#WCujeAZ{Zqgl{x7*x5nKRY6pHh@@7B6_t*cf~ z4(VBS?#$aP-aG1;pFKU&Q(qpsT+8)_M2&9`-M2UCk2E|e0;8+#?vR#3!4>^t(BpP- z-npfV66V(2<6qCSHqjkj5E~n(|m8)2K+lOC77?q1fA^E|ov6ZsR=l#oWbhFi;#{OSg` z8$)~=cX#C?2c>Vt7jX?+uPdTMhX`zLfN(o~rSt4fI5Yl8F`y^$Xin7a?N)pjzkIH0U$D~ME z3!!A+%9@>#WiT0IjNh%rQRn@>zxRF5>Aa`+_`}To@XY-@?zx}m`P|p%`drua|DRQ} zfNO`N)9wtxKJO}xfJ&{V>ClV&0}ba@(+uRrb+ZvLnV)>rgbZZ&K12K7;i)@XGn&= zrs@TB@HbiXHfAB-@t+AO+M93!nUKO)SoWm+7#*cK1ue@e*2LQy^Isf1q(tWB%Absp z?!jjCt156#KATSdZ$#LfoW=A|M2n=z7sFbJGtUI$8b7^OTjcY*)?ZQbY7x(`&b0Ou z1PTaqRk{#fx#GoC0JCf!LXiq>v=RPSL zIIS$^^&G3mFsD(Bg9;od<(_YVlmFGR`Pu%juLR0Os;L6Vxl!Y$I%3gmq67*b8UL68 z(vVyCTTdvB60fwTezTQ_NTBSc!`K-DbUrWIi*HRSNz8Alm1J?!`_fuQ+)Z2?U!NM7 z3~8Y`|yTdpw0WnF;oWI=!a&kP1XbUg=V1I3{3 zGf-MjD7tkPITCGi0gxzTizgIhb2PhxUx2{&8}?L6M!Oipw+f5#pKE=FE8Aw_$~ZEf zbZ$<4_|}{xG0B~bJ4m`V$=%V*@M(Y!qt@lwq0UiknTi@sE z^tYw`Ed3Al#9*2!;+u3hS7dAUMP@%~ax||m5)xE^?Eu00w!dUx#v4AKZmX0k>(TMx zg9S^{h;dN2qM3bfI))a2oWi<8#Pdn-3~m~n_E~*3_Dpj$R0TqLXR`ZPRd{V4!p<~o&tE79)-?1qCz-~UD-V3=>rO`K-I zD6xq2rtclw`ZcYXQrwPYcn}5*?Hv#+a9zT{W3Ihu29j0saTGbZS)D3>9+_>K)(=>v z4FyDeYiqj#hvu5^!~fssw+i`sF8|>>OIe=9QT@(#_>&t=%J{4B>v_9e*`mr=y;BOz zG|pJxnZ{6)Iv>qIUZ&QwPVZ%2&)b)~vEQcIDf0c%2h8!06C_m@gSX`$;V^FyAPugB z#oiC?Gv@LLmMGyHNvYK~6(t{gyMueKf ziMKF?WvP|K)w=cwjrY833(q_yob$TGTLV^G#e}MeuYvKO`L1q=O9+^*(Wpg1$)|y# zO(m;e8?NM#HBmw1%lofHioejhd2K7MIB6Po8*l*{(9WP9jx?Tf7+7!^=dxNtHx@z* zkp?)gtGw z{w$7HvoYr!&Dc2EA`bv%!23a~wEavCa|c%aBhCZY#d?m}$JBnfzaBAfqoJ1y=f^3S z0P7j(5nzmt=sKEBN0n^IyO7r^Nu7awM?qH9W^8sL1X8T0y0FXRBfUT3Z+t@${YM2% zKiTir=JuzQKF3jpcU~a=3a$h6m}UP%M3E8_M7DRZ`~J{4=y#;-|C963VfW4J;)S_< zf!6hFXjNriOpaTt0pGaxEwe8UmFVjK768vn=jT*8>lr%%xSc6Ryr@0b{ETmp0DOB| zojORO2hz3h69)b2jlg;h8FO?7VxJo{f<^X8ju801(IXg~@AZeR=ccJ62# z!p5=Hx*ylies;E#n8=%xp2C-w&mNo?oCT08ViYW#z`_Uw%ez}SoDg)e>+tuMyp-Da zq!L{A2j<^&{%Y-2_bwO}Umf?`a;n>t|Cv008Ob{81YEN|N2R=0uPNDTk2i!c3FbHC zw9`#BgQ;EnN(D&b{!-pbI^?UiTS@JaUXgnt_~UzftWZ_S|vpk!NEY!zQ`kA1gN0jDI@$?o~=Qytks~(S!y( zaU?OwS0<(6lCY85MYoOUW}-cknKVLD*Dua5%NladdkMRmc_dDA0FF`(U8I(&clZL2-oO7L2!o(2O#rM4lkJ>yZu@1g^Kh4xZ+$0 zV<2N4y|$o~>v~4^8VB;L=AOul-D-EMt|q7-q^@;kN#6a7LPH~-hN4$6D2Sj-X}jz& z1KxT&bgvtF@KU?54zcy{(y0|5DP0xIz&a9_3?RMrlbYz~VcD)}=$K2SrmX<=^O~%l zj%3}mI*t-YeSP0&2J&#Et=#FC+)cIHrmC1h1PQV8FO4=qW3Zb(I@I{wsq;)XCAY46 zSQ~ZrNa75nb#eMFU`5U)lXO}QR^!(WA4e&7kd{_FQ0`fjD>ZCbDO0{56}35`sBbc? z*6Kvac1=U;%WmkkyXte^W2=mHgwon! zN9fH_tg34jGQEd-Z_pm+dp(eTeGwK+I!=qZ=RLM$D!u8@aT^Wun|G>xxrTPXTMaIp z2V;wWz`6dW&zQ%6EEYY>H2y?21*|)%D26UQDuu~O*U$hft7$=4H@;ry1;B;Bp;XOc z4FBstafc`JR-Bt9nyOdE4>^$vttU6#9WWfUL$A&2I%Q3?O`o`fet)=<=~9Mk1G93L znBOl~`A>#r@eIV%rt#6z&4%NoqZTJSCl1HOIOlmHx`YY(^uyWNq%ZJ4W?vxY5SX zKG}s3lKZrGLaq83+Y##kR>mkai1+i(WPwSKNaewx+yG0o?D$Lap#Bknwm{nc zgMiuv()I#r`#%oX|Ev|&!f}3#GSL4yuHzGtDSaPB;*Amo%2z?`-Po#KpT4OUlTS*C z7an{`%v4+|fPGO&N;2+$8Nn?o~Grf4O!h4JLR5sokRGIh%K{e;Q zg1wy{90F+MZxWTO<`r|N_pZa%7~|jPP5vTQe_596PspRgwWgmv(97m+3Y0EI?`M|R z=gadzpfAzN%%-)I;p_P}Phxs!AeW_@B!FtQOO{RVo1O~Oe!{BaslXI}bTHS8k3o&=ugJo5dAm_BO{*(E8S?RPf3JFu#Pa}0fJNXqFdyHS2F zfp-2ywulursiJTkUc5tJ`Mx%Xx*==beC4=T!aL_)E$& zDa|8|8exAe)pnurM7yHb;MNfyq^Sd=m{&h^!Ev6pi+Si3HCd=-1W>XA26Ss_n;Sfs za`H%r4B%Lhr_$xdZ>)MI^17Gz6Bq9RKs?d7GvM99gyBk7VlnwqM6FOdugueSvkq!% z&srz%$=gtwJlRz1j?yNFiaW)WWL;bx5uP(~TpnM}cOx;q#~c&EIQA^#j*@g4^x`E{ zqNQxK3o)`{^DkVJ)dz!ma!~Eqfjh3Zj!uveoQgtnRqznO;=23DGwRKQ_C#@cjiRHc zPrTcLa>q2mW>0?{Qrn0N54 z`a*Tr?_P*6S-0;kUcWz?xzV@S8ylYMdcHu`nv|hiTk)L-+6FoJ*j_ee01Fo zGuG*X6{} z_{X#kyin1S5pY$kwWv)TR5N@LnN^CumSJ#l)IMG`m!tKtKh)lo>V3&@kXiP!k>Say zU}=@hlUeOHe#Vn%rbAXy=^N`ku5~{)S%gDFdyNREe2~vX^)FtVJ>yd%i`%i?OA_h@9c7ITZmKXm*DF|9 zg)M0fd}+L0iD+KmpK#VjWnJv#>8svCnEhtCnMXMoWv!vhr9RxSNXcSy`cAO!3M*=0 zNK0R*^saMssaJ{S{}5WIxm^FAb1xg8kbim_dSwfS^h81^_i0KZ^jZkp<`#h+Q`JFM zHdT+}-=}hhNba++;m{tmRl+W%HK{b+t&cbNsejdCw8h^&PE?Og0cmOqkJ)|H?*^5# zR-Yl7lc{~K$#z)pwaxVpeNoqw4D>HND^WMT0#MZ8&R0!6j5hr@weU@?{glT;qSz%z zFg3w%`htzTz4NxcQrAFYB$Is}6_rxs2kcXd@73IV+}t*@k>R6EB{@LtP}gW|OvN4a zhAj3hV^Qf&rRFa5^N@s|83;!f>W-Mo_^`K9j7=uOQMwXiKMKRwU+!9EaGuO zcb+K;&fp(F`RJ#Da0e$SL;D+6wzeF|#3o;X$Sry$#@y|`aYmgvl zg@@a9tBt;5Q{d3v{6lXO3k>W(m4OYakyqnN^x*0JYEAvdnD&ZEHju6m%)-Jd3SE!V zS7y+VwJHLU&yZR_dqjJtnBk&2P7A*P- zb)&c&v+3ZU;fHfhBO$5IcO&y~Jz>~8)KxeWSBOu9SB#Jtmk0A&ps$~>x)jgfU@+qf zEBcZVBT;>~c6G8cyfzY4CKophNlz8!=6$c75dS*QT}*Q{Tm;ZY@ez`s(4>m8==;)u zpL~0-19d07@sL#E)K>7=!BH5)bGc)b$Ehe{KpF#)f~C0eaVG5M0W zy^=L3&Fdde;2sKtbWw*n%VNq>H*N=oneFgQkfA*lLGff@xt>2*<*QjZO0GIe>l2bz^>NmftIz3gwX!FVCLWl!oEZ2uI zM>~V1(lh~JV@_}2F~a8)CfN}6PcP&Rjml>SFcr11NP~c3u5+@WAUu6!z;q(xA4+aZ zAu};m4De*y>OpU~r#zwNf|xZ(T$85{>9hC~u-!@bkM+5?D)P^sX642JQ8V+&h^#8k zN}uz?XbV?UnzkiHwP0`m7=+)%!-}?g>M+ot#&gK~7K%$=XL7UhU{ZYQiz|8pcWP8C zy$xF-7Mxw5{+<_Y3@ec1qofp(%XawN`=(ZgWu>^DRY-Rl45mLHHQAZLU`f#}|IbJ=KkTU;pMt1;#q3Y8g5jy+ zxJ(O{lHt6rZPGws&-(NGpRRMTcz)>hf3%kkBRcDtK_xh#X-CQi>lhRrwGHURlh7<_ z_?sQ<9h&1nou71K@`v)Xh;6^j!rAb24};w0fa&@Oi{T=$$d2R{dnOE3;y`)N1vDqP zCYI*u56+@^1eG2JHlmDVd1}+~)wVC4B=oh{oA*5}k-ej*xc}|Q+dKfzM{kjmbmHN)nBsG{bXddZITj7<*zc zJomN>gjlVvd*M|mD0-Bj9CW5m5ZPE)8F~kz+y^Vj-QMEN+UDebte_s)&c#68W|pSZ%>8z@Uge6K7IPrv1N*Xx(V2_($O z>g!<+!T#bsnyp!TII;Ic@oXabSgS|5rM5_gk(Lx+$kEP~tU~q>v%Tvi>JoRtWhhXcr6~6=l8B7s%s{vnVWZ1{9<4g zJS+}*Bv7$9+hMfsLkjP%l5G!R{fnOHj@9la?VyD`n3Qg_Jm@j-a+7{J!ymTqd2v8f zwZVCts-p8dbUk(6qaHbJELi*2azHrsdeE~jl)nWoyPo)LQWaHMpO|31iAT!2fm{8h zKuVmbk|HN}JoRB|R*6L4;gT2Y9<~)X>X$t(E9no*r=?3!-W}m9uW8`ydL8l5(UzKQ zVPdMW<->$sY-Gomqt%X|nE zIkLfPIwnaN|K?;voZ!X7p#;I>0VM51@_O}v>TIL*i=*afq4_z-2$RRxZf}rK*Eqbc zV3We86M&Gs!zoc!dP-Qt$7da^@VJv#V{einE@rgK{zs7@cZ+l`+Y23sdCe5AXr^s49uOSazf@M0=aF)&2CW-A%6DWzGcOYud>VRv7P)*>fJ7ngKv*7Pt-g%vCiy}-kpvd z)%IIgMcVgfcKN-KY?8UedgsJ0Sh*z9N>$9?igG-;pr2aC zS$i-jBZ72S``CFNy$t&SW5P&j;q^04qNgE?iqxzPmVf9h+hB9@vEz+|59B1nR*L5} zle%+UM{f&9g)ec+sms!9kz+3q!LmywSgyNYvkR$DvF3Wg{xSOTk;CVA#bB-_$VXr7 z)e?NC6^Efdtbh<`@c{$IBk(@b(h+uskNPyGm9>TQI>43BD=?P3+;jA{+t?r}(8)L9 zmYx@D`6QDkOmT6%ak3RwWmH7AZfFN{t+7l8VvDiW##;|dbc8ADDtnUyb&6<-wY%5Z zb7hBqxS80r|Kzc~I(6;>LKj!LevoYHK6%6wp+AoHgbUx*u(h^%$9E+x@u|{rxsr6f_s@=^{wwGI$D5Vq&BLY3TxCF1aGKr_BW)eyOv8IY4+hUbmeaielq5+0 zp}C3qFx3xgul$x0nsdC9DZi7B3O2+QceUvc7TC)fR#>b8RRL*~eB(-o(=vucQVY21?Uc?%vTJ=!DDfsUP+e`k-Ff7e*zD$pCkA6Gvw#~%yZ<#9R`3vH@$8}UIe&3 zj80py)DmYPoh`uc4_l+&1b&x^6#}=9%W}O%P|8Ql;tU2@okPtYF5r{%FX@H$!hHDu z%z=LkhYrb7>NyNT(<(yfT#9o}i?lOo4w7CiJ>9C5;g2hMjZYZ3i0xF1QwHVcxjbjx zK2Ct#^PQ{TVS-Cv&BLC@yx{#Lr4cma>c)WsJga_CY&v)Tp#N19Dcl{7`SRQyZudl} z%e3~%tTPQ8O0Z~n5T>DHUeqc!eN!Pf-Kcp3{1MNb`ZPkdN* ziOA_XD#IkHFq&g{#FQRaohy{G5#B==cxkLJ<*n}?0XqwBpDh2Cw7Ylb^fAIvSa2i= z#x;R2-(|U5Q_Mr_u~tICU*7+c9OheNOnG#_p(jPUK646~Ea74pjS@?5a;q7}-l=V>r2l4_X8B*}#yEYnxK~xfchwO{-vG zw`unsMZ&$gwjVZ6C52ye?tSeRVw~|XlI$RBJ!o(YN7R4JE$qE{*%`^PN0^49JyS&$ z^*Ma)6*sx2rZ%V|`ljbFE@{1RuU z-bAH_u00c{-yk>YR>bS3o$%?p2m>|)?Z$0GrrqIcm-r(R6yj3)k1qkX1o>x7rYVyR z@?1fD+zPz6q?cbONeWdzmMdm*4uR*lTtzp^4)IIdTlDgkr)}$VSefg&p~LLuc-k>x zT#kWkU6^H*w~X5gaShG4Z_{)PPHRr28io{>NLzfMmCM$X82mcWu}U}lE1lMB8p&L+ zLLWBXdkylN)U=KlNBIyHb)65YMs&*_%T*f}54cPWqq7{W<*Su=+ojmz0qMwfUZH)# z2z`xlM$~<)^6Hc0B;h!`t~%%AuOW5i*T=6(M-Ab=j1hJ(uKzDDAhSc{Eg}Cu0l|#OErP zch>C#m83ocxmHv>)kcJl+|dQx_Dp6fI<*65_8xbyVgw#w8&mMeB+axsGa+hhJB$ol z&s-0K4!fo0fxRaH$|$24P1viHq3GlyQMa#z(za<=5b1T!Qq1-z*=OV&!)gPRCX_ol z4Gvbt)QkD=WVIw?T6~_Jyoaw4s5dv$U!S;$rSgpsMPTjC z^)At$ut}z*%DPJkZ!YX$V^iWovjqD+>kPP+R)ri=wzayI-{Shz54!}+ z@^u{CTL>y2Om3@jJ?+EbX?Q?$KzLlJVs4jDw$Dw1ImlVMjbSlL1-Mb(3_3t_v)lBm zzT&x)uELBCJjIlZe=s-$sa0b3B_lVHmzFArSnKFF0p7PdbZYi4>$UFA2M4c%Vb zC=~Xoy766}d!}zy>1(I)~EBp&*ovfcbKIx1*gWcBEKB9xDzTdxHC@TTpM_1`yr9gcTIbKsce;(1B}y z-F{!KZ*s3OW@Y+**i%Z#070W8fo}FYu&k)YTAdkrY=r=*z3gy1I%cQXtS4En?(~`e z-V^*!D2e$$pQ3h%OCzy05mRj*JS%SXdNULOZsnDmUf!TU;btlVuIYZXvj_7RdG_Aq zhG{y91|qwZfU^IyJMgcF(QIc+CrB#i+f>HT^Z2@Md>u-aXY%shBGnZ*U%N#7%(xFY zcNS4ZXEi#;8BlfIKvr85+!*GU$J?|ZH|}j@0YS{5+=2g3j1Kv${Cz=vHS%+5>hHWH zzf)g@FY%Ym2)J~OUeB4!lC^|kXV0PO+K1~+tm*|gh`dgA>E8adAz5$h`|J#=zTBt3 zm~lDsC`gxkGF^zgc8IY$i^r?>--^8Z`2%=a=p;E!@A~~>b+s`NfrTe2efp?y!ST-c z*hyeeTA>~F>9ZS5;x5#FbFiDcYo3w?b3gU@`LhalESv2)+CKH^{OPliZzL8@2Pbc#65Y+wP3~#j7m}vJX^5JVRnkqKc0`S1kqnF6H z@GZ#e{6*$yGL#!vBB#^)>H5g0MEgAnm)d6?W2pwWvpuyVCop~A(N9=?pJ3epl>fJ$ zr1p8sab4$c4j2Y~ZRw(z9675QtWzxrE$pQ^fL_aC4z+iMYSLG^?kZ{9i0Co{&jpvK zK~s@g^GW7t`|C^&WbcQ9>7lqJK!ZvNDF5b}`WC<=KDX=TzwpaHyZ$v@2CjcG%pI4K z2?gUEuU$XA(pcy_K_2ng!Jqtb`oC7D@jvtbpvDoi-0>4hbSMJ{O1R}!+L-b+{TiSQ zg$Hn4;;N&q0VT2A^)o&X!+pl<|B%P%H#*y0|(6lFKAT5$( zLor1}TPB6{pl7M7rRh)sXLksJ%{oOa_70r0X?srgqjXtOa38gCOtoQ$)fN!`*Tzll?*L+W&g-)3W68C&Oj8X^2qmS!1X3W zCrTpmByio{L%$96WK5d-f`wV5Q^XwKoIEE6QQ$iRar7JPB8-YhkzJYzPBoU*0QM(& z8bEV$^8Z0u)1pVesy<|QAfFlgy-x-3j6J=Q^`HX3MHxhdBQ#0^lBQwiQ@OZGIc)C% zwuN8+Cu5S%SxgT_v`F$>0j5!H-U9yr?>r~I$NwLFi~k3`Wat9$jcXc{=02MMxJd)D z#9Jll4ep0&@zZQjWq-Wcq9<=D{4=raP6 z0z>!*uJTFzNpZ#7ADXd*-PNtG(Ru82dh;(Aj8P-Qg>lETHl(=ekWMMC+j{aMtaspG zRYC+E&bD@?J)3HeE45=4zZ&#}zsw@Ga2PxX-@$$zrTG|hO+9x{7^O(Z_uZzG?mk=} z1+=yKY&&dM^5xfE018Zf=eh^hWaMC3V#|8yb~3bnV?@oeHVIp_~H)#@jx6F^YLj-ZxZbe2n~9eiW(5*})4N zNu$em2Kt6B{~)y6p3i*gx%(z$TcL+<^QVr?K*lPodMk>D*(KUUfs64wB4IVnY(PIt zVGoDl1W8fDNBiAqZM#Nt;Pz1eYQv(4&^s(@Q%7#W%}>d%y5vvef03-QdHa%&)8sMq zW+x*FnNmJRUQ3FL?hP&11S^-P$xm{*c&o^_bo{)ZEZR&&v-g#y9OqMfkuucQepxrq zNfD!5?;&k=@x=3nLQxC8H=R$zgq<}wi1LV+s771Cy?dDwZEGxS`!|+{8*uQz#4zJ; zZS^k}MP@0ksw;Ti7>hqMg7#H>nrldaE%N@bdh>DTO(ZTF#`FSzM3$xY(ww`IK6I3) zmygN*BU2w9rK^mKp=|RR*tnZU#^x+5iZX{iP~7K^Id?oG-9T+Wr*ULllKnC@??a1* zgzb$o7x9}3hl)$96Sy|Jhp2|PZy)x-s~huC7V)Rm=^YOGrQu0avq+&8`C{^3Lu4Uz z#eJRi_8Uc`9+390l3d=XRNa$y2F>nrDe4ux3c(nU8e9*S_$qot^Nib3MYVp7&Bxj` zHuN^;v^g{&5EH|g{WrIS>@_!CKOW0{)8p*MlWsv|_|g&CVpf8~vsQS2QaJwz=e=N( z1Y7rQaZJDfN9`oiOh#wB7B-NEvA-SS61RPlW7=-? z{I062DM^BYoX}sY$w5)Q8_~S~tRg(9qZQuJn_Ihiouan!9^sEi{xRxH1J^Jg?c#I;cnn7 z_q=|qC&pJ4GAv!6<|++{mjk%?V_(4=rTloA+C|+I)W_@jXJRRo`@6F z1}j6-qFIunE2l%7SRbUUJnj4-bX-Q{%<-(7WP3NE42&g={k?ZA)r3-IWFMQyHe0DZX^|$x9PBE4 z$}afWIA)}{pu&{R@K~PHg|OiyZB5^buoouALW!P-F9+niB+E(caN|_$D7qv@E%m00 zT69{JQ;p>@1+JEE8C+?pyIM4LO(^S<2TiNqh&MV3*z)hXjO#juBwos&t}GyKSUZQRO?>se_}t19jH-t^prX zwNDz=vYb2>oU>OjAE*(#pK`N9n&G2HO>xK_V`D6HfmY~|rl@ENU~(Ak3j1gDH1tUC ztVok_U`uXLPwp1*$OdIz)ps+hvl&W-_y?u9Ytu&n!#0q5=sRA@;-74LDPUGd=c$aA z;%N-vsTv0|p1l6kuu_nU7_}xkv}cBknx`!V%1Dh)SAG^eNJj!a$*zyf70UUl%nJ$> zO5+$u=}{km1QEDzuWp*IVkDh(OvcUMB2|!Gk1@SLMsWrFMu*#OT#29aIO(>CTPH3X zGcW`3fsXDl#=^ST&`;Zjx#&JgG;u0Iu%m6FguWiEa|WSn{@IlJDgTxvk%~*Y7}eP=zf?4{iTZFEXf;J*LbCTadgk7>gQo1o??P*)tp$Q(n9& zp8nja(wAer7U(u|55<4!69458{ekMQvsm{+znmBJ%X!uaLr5&q=}vFb`f zAGR!<_U|0+4XUO0qbJKI8`2p|6tZkYIaHYwk{#=&3yeFb5HpZusD67fOdZP}QDQA@ z$8o=EDDRyEVQh#HblNEcnT|jF^vspRrdOBP8I9?NgIRHVRmD_n?xA}GSro#ou;ywM zzyDj>Sm8#^lyuqTJIq zb~tYla{^fhHJ)$P$TR2Qh-R&2x}%qmO3#z@NonfnW%x8ehf(YD>`*Nxc^>O0r*?zP zN&*>pt-zwkuFek%z6$%`4S0c3lAk|Ue%?q-pTm1ZYd_A5cVO4Z^b&Exr`KEv)5}DG zLvzjiN{@y1f(_XpH3zh(*_nut0WBe>Cn$v|r9NQS0Gu?u9bF43@y{3Z86#}M6~9Yf z(KajM6ZZTssSfe8McxbNEAaQ3M}!{f6v4m~OM3$NaFz;V(9Hb}Q7_+ExloMHMT7N- zir9pLi=50Ts{g>c(_pC&afO1KiN3h86LXT;vRP0M#OMYb(WVpE(sjvs7sDr>9-cAl z$7ctUCz$s7;$dr>NN>Se@1<{;kIS0(7&Ibp?x;TJdi(Bm%+gyq7A#5NQRgnk7@Y>d z)#nB*5a@j6(R9T@z%Eve|Jrz_BCxRY!e}hajfE@a?{zB>BzE@esZ2Um@W6tjEFQ4F zVcHD~zR%zf{e!%Y{-_x4OFxTveigR2pCC}kGZ2+71mIl{ym#=UxE^GvxLpM(9vRC2 zBXKCsFI7(u6YKA~($5u#LRpFC8lvi!+?8P-3euHyekfItpwf{GW2gDC3>$j89?d<% z_H+b>Fs9g-XPw=4hw+Rg9?WGEc9!0(gE^GGT+<-U)(@AYkKLY9e2_6!)kq;991z9B(Hh!XW8M>L>kTvNMbtZO47PYrj-6MM#Hlq{+g*jf>2i}u-DIzA zbY3oRx0AG`Rp_v6@J&pD=uTf{>Pr*6=M#j**q-88n0F0xXX|#noWE5&?cNlQPR)<6 z$7hC)ix;1ST^iL7$D779^(Wmpr7d3$+gsQw;I8yIV4b_@ZU|{#4h%~ZBc7fpGoJ@xsv za0@8XylR)L}=4+lHNKC~S(gZh5lA zI_I zHCOLB>%Ffe3UP8C!SY$#bJm)4oGc{^akogdPA&_Rw7v){Oe1hvap^8galm38-0ZL` z<=xbsRP3bpvQ>CW*13o`Swvg!{u}tJhu2NE`6A`_oUm%;ORMHNw^eAS^Awt--?-1RRPXWi6wIOUJ5FRQYi!BsG^JhplR{{>5FKcO zRr{dZHsi*8J`I~CTJ>@!_DL@vDRufRM>S%|XL!xOHNFi--DOnq!Pb zpVgZ#hn+?Rsjk|mCWUo_Nkr(l$7aaPKsv06BD-u=jh|7jpzN^f<9GXd4js@eNz@9j z4O7z#+N`op<5u4Upamp0>1Q0h7@8I zHaVvpCGp3uRzbPL3)t^#2FmPV)2!9Zf3mi$$63g^-@8uhR;!%Avdaz~*_lYM>NF=! zcil!pp@?F;O4JAjk)$2&Z*EX#6R2sUWZ#M2eAv`%q^V@3r+ZohzHG1In;L|0QE*55 zP}kUwQwH*6+3~(AE$!gmSDhCEijbinKwb*6lI8_ZAL_`ps~&Dr&DywjBtH{R(?hkj zN|xS;kbR`a`virLx)$B8`7DE?jO6TLtA0YQScfEk}Le~zNPc?ZbLsvVFCD*d-b!IG(Xq) zjTiQHJW3ztFvL_{yVJE|w6DE)h4(RfvYDmg6P}vuJsdb2%>-7+3VkXb|3jI1!4*t+ zz<|2e6C7onpbvE08)pmUZe`I>{mQBg*myK47@_xDS?>8z+COM7i3}U!GkSAD4;5Ad zNycLI?RwSwE_7T7eE6CQEvdoW=qT?Hiq6cooKUkJZGN?9zgK+fu<^a!>iQ*<`tPX9 zmMXH`#1xkm9@$4)JURVTjo4PHoySY*JrNn?GhnWhW)4C}{PHF+{NALb>JLgEW5p)R ztZ7HvrqXcvpwg)&IZS|_Bl==G;U?4e+x_7r)rUAM!0owL4{9pGRLD|d?6970d=-fY zJwY4qDCwe#VIsz7Ni_YB10kwp}FJd1VV5DNpfFr$9-bs@{MR}0fCqzF@aD7T;a zED4)bRQN(MCMxw~sF7tufncrUDUjlQgnBateWPu3k97UB z#=cxWP!rpx--{XEo%h@`f>N`3^hT1mi`vfoBv>ew2*fE8>Jop#P5F<5%`;`1M++Iwoi7ZW7u!EA^zZG z*xmagy{9>tRx9S}zLG!45Asu>#4&VWEn8c#V) z18m}4R!iu{LTF)Jp9m14c( zEM*^M9Ihzp=yZ>JGjUq~dThhWL-$rh)IGA`WmFk6Ot`E{7_I5f9XrdUK)T7o7RU+w zI+TCWHkTXk48*XmQqq?BrQx(GujBE!T*!1eJXu8`@@a+L_tc67srUc4Zv5 zJ6!P_0r;lb+B(0m&S`Q7YRG#kS$ZTAhcJT<+so^wsVmrp+st1J>U>dwPaQCQfg2S+ z{8`qf8OYj*QTSR;GocS+HM?eCQO}`O!r;75{m=x{zw_K&*!O=+Uge7jyp;zUsE>6{ zx}_~5>&!qXLj)DS-2V8%Ibrw%ytbX}v?<9e224QM$3NCxK#S&I(F^T``S35yfko+a zB<3RsGOFL$`rGXT>p&-qMz9>}=tx4@!9PL`<}V{~8Tt1%F9x9LLtnr-J-tPyYUp#us$V6svp7w6Uu4c z3D-qGo9wZ^VSi!u^1iD;MX)pfXNl1y81f)*|G=vE%lpV4+)ML5XR)b!SlvK%g$v<* z-`~kJXGMyo?z6Wt1p2fbdLP!^xZF4cFmjy^HL^Exzw%-u5C~ z0bz6llIWu8Ku3kmS1qPM*TLOInCq$Qz?f>*{Q+Zo<=rf(0=}X-i0RKn_7`LWeiac| zkPQGA2ISBD5ud+W=Mig`96~&vfzV%2NeC!oDVCibFm0aGS?W{efI{6?Zo-7xIePsr zA6yrUEwi6=D~=NNF&R0sv(MGGgMD&U1(TAf?f7*OpER7n5MVkJ8Oko;0@FGDXZ~ND z^^Bchxt%FSyr@0bG(poe#`ljvhwY}-se^Nc$m;X!lSi<~KFJXR-#4rE!CX;XN!bM^ z^#W`{0b9>aQ%4XU18cVMWPO3iOys`#PaW8o6+1Qcect5eZOJg8fnn@5>(hfrZS+5|G@Xq#QF z!`vGF0lUoST4W-y!TeO4{HFtm^RJ(U_P-X*E{xM(Yn&RZaa}t%PDSQyhf-nVB()0|vCi|mu1C3K-Yp_=7s&-8&&;fN+jizDhQO4c1qAriz0Len<`ZrwA zAI!S=J;z!~@XTwFv|9zTOrIOZOT}o()9fG&w#6sSE|&MU{ARKIvpNiM%KQ3+`98lc zHzY48FU@r`;L+B|OfeeDg)m_OWEdP1W{ZA4rcK?)X_6Q|y%tF>*F&U2q=R_US9ttr?%RSI&+=*rtO@&k?iMNumcMdJBNH$Im%F(Vii;{s+CYB ziadG6E#0o=GU7xG(5Ty@;qzc{2Vnka9sPun9KS9Vc94rInZ5@v8tA0EB`nZNcBR4BzT(4jzxTibO=-*h* z*PcPY2uYYk0_(M0rOGf67g*3P|IGpLe>V}a$bVj`!AQSbJL$qSVFpsqPIRWtK;G}| zbHYtCSu1(dy*ilepOe4 zI9K=}IzQAev8EL;;bkXJ{GKRk;j->1J;}nJdXGZ z7|-9I)m5X}r%p9BgIm!8Pwg)n_VQ%|(iV!2+6LSdQgVOwf6sAUT@4qzf$&0Bf?P+= z;jaAuQl9kB%*p_jqnoF0c$xD&)KpCEqbnn-s=jGYF>Fwl8!FA(NR&>qBCzPGHXfAs-c{j$j zK6zTT#o!+6%T=6iuSFx#cl(X?^BluhVOOQ-#zw@w;$BJdGhULQpB^3|zV`Xq6(1kk z%S64NfOE?fT#`+U*6|?$G0X?9Z#DKb?yc0Q85}I@&VRo2qNTGhEQ_xazOfINH>H_^ zN_V-Tz#Ke9LmX$8Rdx8_?@SQ&&R1_^PWRpt6{zPL7;^t zF5Rmi2K6D?Wfe}ox|t_gC%j&zKU_`jqj*pp&*nkLFa_eMc-; zZ|tv+2&&LNtC_7AD;g715`8hrPp>8*M%}RgW&XZwn2kaBvOGJN>Y5b3-7PBDOf6#@ zDxr7N&us$G(Jb-ickv4r0G~PZ=l23WXBPmUf8{ARZ=Zhx_`F>JeCA~H7XY6H0>gL0 z)jtCWv;g@0ED`#`dH&oW*cnH=s-t}fw3}sQ2rKFgb}Vp7B`{hI+-XS zn_e+hoPJ5>Jnwx+Q8&-R3sNRtrFGKRgGkT%9T40*z590@y@*+bT>APdCZAcGj0M!xB=@0; zK<@Uz#*~hyy_}Ua*8DMxOn2q2`{0o|{AZnEqxftq610Ug=vY2ccON`XXQSutr}|Yy zHS(S0i-q3jI8b-61|S)EWq~^QPpE_cMP-4%j0VR4>}^it9mu0pW37ZfYg}lGdE8ZJ z3S-q9Z~%i6#Cf;{28|XEz)phDdv?+I|5d3vF#{pr?N?xDCgcXzw|~?t0Zxd81pPDc zP9}T@y$06bJ`y!5L?{eN0@q#g&ywb5K^C+Pgk>&G!F_%wv20}MxOsy6=Ck5DFMy+L z9s*eSw_+T!&>WhkW##m6{w)tB+m~)%g)1ZHj5JBv`9SH8*K@2M!<Q)IoUHEq`L(O6lKCr9)Ei z&vB!?H_5Pjb1k7fmi@bN!)vJWWti&}#l(6yfi(QS!#)kR#_E()?;FYW)g<*>nEZm` zFbDXQczpkfUL!7fbv-kmwIwFT3Y7gyQ=H_|btBDT+_PZE93XfiEZ2x{UyeV02k4$r2xBCBM?>*p}*w*!7Y+wU6N)ZsEBBDeNzK%Y#$3QOI@B3#XloU zrr+IFo3HFM%6wr~Ch{5o&L+#%ysUr-P6hsaBn9V%;pf`KuqZkn6nLz1upk=%s)czZO2RRoVzo{{PD$3aqQ~iHJVp_ zRXbTw$?Qx!h}rK|sV>ug6C9jeyBhe5kJMI*Sli(I zWmw>yqz2#W#E$Xxdu<3bE<7Nk`3Jqt3FIx2R_C3~^kx_ck++PEYz;VR5u`3v?uK(BJ=*Zw1;(LX8! zjQb|TWZ!rGTn4iF7$pR>gm!l(jSYn-_O>m6 z^w66OiapIb=ByO(fGN#Odh82@&*xiF{;ueo{~cLhV)uvJiYgZy~B-%9$J zm{-!9;%lSPo8m|Rp*ID+DZUSd^s$0IY|>|tuOqv{(dg zA?qz|8Y8?Wc2AM7wCyOB{En)8bJV^<-{dlRZy>YDfvNYZN`0k&Ul*Pv4CumJRwMu4 z$ijbTcmE-W$Co7r)O0DDrZQEQU-x700TQRtviK2M+Pd2 z*wz88i$({4zV6qq)Axg`eMf$>283UOZ!M%)0)au--+mMI{kdDy_vvjy?*sp7AJ|d| zTV_E4TB(XNuw~Bon+%@V7U+LC)Ou~>6J;p4j0bX>=XgR{Fy*UQ>PE#oUsXU!{8)a5 zP!$qC0w*Mr^$9VU4%YP$gA6@n8fcLxknC?f{QpC3LkGqFy8|)kpx7S) zjp(2l0E(f$fMRvXWG}HAwzzJ{?$u;kFTb9fGFldR=owtCkPAK;&*4pkHR3$-ik?ajr>D(!DT=3ZU6;Ju z|H?yh9>(hCVx5xNU3m(9VyP?MUBUQpjS757VnfH)mK`#{F)=D+Qy-T8X^GP2j=CUn zooGk)&E>BsVKOO^?oGQ@%kmyfnBs2cUZAnW)C1)KSwO&5;j zIGU&I`m$bsc%l@ITgw!a$P1Lc#xXk76Ad9WVT9)pJPj_XL#NfV@{6w*R3*##%-tM! z7rIcEq2ldSY9+}mbwhmpfhT8{Eftkk$0&XeK2@^7Cx;ECsI`{WBCv=2M$J6sq9X3j zPkOow+rOD|d8X^rhJELI?jM0@f8;wl(Dnm$`w3`cD^WWg>0VFf$)ZL83i9OG(6fNAIK7+E zU#>4B5`9Ra52W27!!@nv z#Ggn=`m|a*swxp55^`bJfS`*X06M{bO-9^>n*npx@S2`4YS^;esYh%L1ryJ#D9YlQ zC;^jzv#nG3!-yD(xgS|#AL+LWc>rpb#5@uM_?E(r_x?~c{Mr4z@0@(ilX@;$M=c*3 zO*?r)D)GI}{iM0@EM+TXg?j=(V48!hx1=A&hlaZ!spaT*nE$vX-BtBHqCNrYa=nzD z<($ClX=R-!-)wKNLx_gPk&480>!58O*91A5?T6STL|8pudhBtWCF~w|XYJK1FSaH3 zJ#20Xs_VS(VQU~S;*Hu1>i0J1nCL~$A0DP^Uk5*|?FgD#83Ikk)-)emX8VxZc--Ky z$a5`b3sf!BI&X(wLE`Zn$;RiW^e5ul@?~$g{v)31?>vR>m|e&*r^mBqk>B^eWiX-u z8H^zMem*}CG5y;=U@~8j9+}x!=+h7vWb8T+%I;khxsmX}`7vtwj2W`jOr6>aa}S`g zYfa8oQWsJ)I003$h|j-M73*GtQG%azDL8nF(Nr$MzX>MweGyDbrXBUkL{+Y^*ZY3` zOWZ~l1RmxxK)#y6z~b5789br*)+VB>6BDA3(Xw zx%kGm&%WOPF);EQK=ywtbJSl{>Ew4q2K^rYSNAzyfL(lOdXr&5lCY2rgh{|r0DF1I z-#f^uRT9=jY``TF4g*O9WQ=HKoi2+9 z?S>pl2Qr7e1+45(|JIOw;*7@Mz}nBQ-{B0p+}84IU|-Cu9SIRfqW)$8Olo2fVR!#2 z8}+|2Mg0FJfzlL(2^e*!$`an!|JZ$(v2-SP7Y)y<*B?!WM)+O@RH?cZBWJ->a9!Cc zWbGVC9sd?pt=cyUT5uQ+tT!Ok6TU=s8ADUR0d6#FlF*V41+|vRphbh zk5ZepT1Y2ze_2{>pK7-5axf%U7X(wOZb1 zkl9+|KXM(tHd`H{wbP|qnPc9*zmHev>XL8xV3$maRpVT(?6D8c4}jbmOK^YMe)CBn zn2$)5oEd=xG(Ty;3|jTfI<_yN*@=Nbvv88J`Gra=zkQ1HCpM>j-ekBFY8Y{IHMOej z+C$V}HbISe6B~tBpB%T0UA4a=v2I&~Tj~^N>1c-R<$ay%x0vM(d8EjHK6U!~4aRGH z@TO0~)dy}JPc@V@Xu->vJcl-DD;=5!A8E&Fz4=CCkdm#%1-#MQUD`?>;{rAr<_`zYpj>u&NvQEphr(~%lp^lDB3 zIVf<0vzg}CR??*v1ij#)(`vdkg2ow%O`SH$RJfKIQ+KkRabPfW1v6(VMp{?*_14Th z$$i*Bu?Q^=gb5{kOp~>T*t4HqTOsSLdJh<3@=s~#he2rub(9NjEXm3Hm>Tn4%V`3e z4EOIfG&(TyW!RJ1T9xfMoMD;IRsFnI{{3F$fJ{POiCRiQ+#B_s zH4`Jb@Tn0}qAP*t*?_D%>nj~)4i(Ekv0mCH`g6b;TJMWun;If=JQ{>2Nq2p?IiSUL zrgs}a`<$0Q_rHY?^&{%cBJ&1jnIAx>eDxb}Htn>demZN&2gordN7@w4;O*Hi$WI@z zAGLnSWk>j$;#X@QIB^_4dcC9-A#2Y}u_a_bUJIC5Ju_{=A0)McPAkVve-653f_ge> z93>nobu$%w)Eh|=V^-Q|QvJ5Eu*pXjAMeEsa15-PKy1!)>rz-#J6;jfB@OnQ3~oAKu1C%SjkC`CdJy8{HW~I*YSRchziy#1U#=sCURMSgBvM{e z(V;Zgb|lWz9yDkV`L-d50GCneq)jEx3X$Iv#&SPEcZuXZ5C%_QDvHi1#+gDTsM zyz)76rw}FOpQEo1RatFERRozLN!tN^Pb>#bKXM&zO~Nl!MC#`jQSi1*byJ$^?W?|5i0Mfbk=C&fB{7OEj2FEo%J_&MHg3!ep4!(pt!NYL z*V0h!g`H={Kb;*xJ{AhU?qbIq$u{Y4q1;vmRsFwfSRQ*4|FXtnTj?-heFf9B`o#PvGHxVh%*M6C_G85$eg z`8SItm+u&d!WUQrARKv^pxDbEQM%~(%84-fAc{}zqy=W5$wx_s|3Q4PPf{QO_Vp?z;+&s zErtj@GXtG01e|ieX3zQu7^=B*1&qX-K*kDw-|ON4mMvmRm@Tw*txk}|&$)N`ex&4u zF?G=VVY`;4tD8yO*^t3(LBv#c-_KcX1}rfT_LDYF0pM=>Of}%rPa^ydi!YGp0+J9M zl??ly(xw8llst8N7&17Ge8fL1v_#!;8uj&W83XMRu%_4h<=^x^|Nb@m19DmP>-@JN z0e{Kg!hp(3gGA5je-}7?#j5*T_8L`ZAU11yC7pL6mVto!mVbBXTRz_Zk#zK1{}7?& zIQ`z}?ev%H1B?T|3Yc!BBR`o;0*Uv|f0s1X%&uV0x#)e9I* z(JR?X#JE{=ze$QWrtLlJh;x=VkuS*?_6u?!>Q$bOvUgzJA11Q>4~2XFj!gGgx(5wg zAc9|QHSLWlght_CR<(;s&*{QR;0X=Tbg@C9z)|8m|VAye?8onCAAHFLRc<# z{bk0qzIz-SGPQ}N#bY-v^t6W>>b&03i?f+5LJKdtb(~S5? zxxy?H(B`}LU=Klx=~xg3x50_&#GN~xM9>pz;EmPuPZ#9qXl?tr8| zJ1Z-MkBkjcre#832d2%a%#ku{nGXs&UAhfY>590&Bd$eT_7F?bFf`edwH$oZYGE32 zFE3q32Wl(DwMUOja_0*~432j|`Mfv>Q?m2x~Z-h(8rCA>w z?HCdH<0a;57H8ER;v$ z$*ng-4+MkKxfYKkN8sL|2XZHRUOycus82R+X$N;xEQdYa!5;Agw;N?Yq7N4r`kW2C zrxR$hGSy{Z7+7z8)8N!Ctd-pHgkn~i6DRB}VNxS9Ih6->d7zRj$UXhDr(y(@?wUZB zvKJvAEM5)|9JPzR2+wnJg8ST<3|bZnej0IUQz95wAK14xLk6NlBMTT{3$q^*|^grUexA}k*pAN4Dbe;r#}2w``h>Oc0?bl zEA(7v^RL@st4)TW=9XpHx(qo7=ozC5Ex@$%yMWtNrFG11$1ei1IxT4np6DuP(t)^H zvw1fd8)nSPIm$?y=UW?KH^@2ZJV5cZPTxPM=5JooSlyE(IR z!!;#k3I4_pig|fEf%IKpUpq0PEA2wvDq7uXNSy#DusxN#)b;8a@x+u-ka|A zeJ5UffMZzhWu>|Oe)TKo<)O(G?XIo8)L5y>GH`vgpx3NL6btI4SGN^={IltfeyZjJ4lUJ${0=S+cH-L%+y;cV%l=XekL zn?g>$yzmyyzyBqpU%s!x-kR84#aoV|B=5er6k#72t36lS&Z@mz7K7G%Br=RyE`v$> zIX&f!QYsllsCBJWo@&U!2SOeSEUx8>Ts$1lMb8Ljxkc+`m45hCyG6w-WxztomfP!~ za1a4=&~rxi5F9T3+4Cs$pr!o9g=VW{du~5|{Waj1;u#9jFtlSI+orX2<*B=Zq13j+ z%vU*2jzvVdTI4L%s&L+3yxC0(JCMyg5|HV5 z((tI0X2eePSShyYFk6c8EwqvzhC$v?TWYA2p{k@I& zoJmRS{j_lwXbYYYESC&>=}^twa%04DN^A za3)f-+cPmr zv6NV382^bNwOEVb3UlMS*_5vHNddeVmu1zt6uQK9ei~G1p zSoZN`d-c_@C%=;X?(Tm4zO)oRvB}W4ZPd+zX=yH2;l=C8q8&F=Q#lquzm!ta1Rv(^uCy}x-UBZX;zm4A|hS!tTmjmzB?N~bmVN4zg~u+ zrj+nH4#NUC4cS!Eg;lnNub@%Mf$k^jGYcT9H51~M(9{P3%ZfW~ddFC5J~ZD&E$gm0 zlV>2$vzakn`xoFPMD9%nK{1t0hU`RA&}1Uz5;;CzjM|F`rg{TTpeLGPR2SH|ng-wn zIur*Yk6`MbmXREgXCaRQ&O!reSpLJb>Ltx870r7W1^CWFi_n806B2xFtuWr1xuPW^ zPfW7zv6IBL*4go2LM}VXG`uH`-t0?#Hkq-X+_}lHcN)`6JM=bJ;E0_8M6OX-$<^GW z!m~~5A2dW25>G&v z^O2DguJ?jjXU&c3MC5!YL5#A0O;%uu857R$P2@MHH|67(e?2|870b^eEwTIXlq^^%{~l;jdhz}E~oJ$95jGXh-*nlBSY zerj@|t-65MmHoPSi8>HeE+Ewxp9vUa#ac3e3HKva71+8QxqMp5Xf4T(x>oX~A-9g(0QYqPr5;{3{fbrgo1YpG{rl#({?V6z?$`AF-`;V0 zThQC$=RV+wrTC`;Oj2xfaWv&YHgZsMh2uXSGFDuMNRNsIgf#{P9Y-fZPJgkK6qkN= zRcG0Mb6r!WXp!<0&a2y~qXzaJLo-YpHE+I0I{eqiH~xur&!1T{{a-tGy9WwCv&o=7 z26CRTXvfG{^xa<7YCIGE1bGjTAt=rFIUO|q_dsL)0wjI~WfJr~`+5WJ zdI$0`NZ);^0;8@D!@-gTeRIh5-N0+f$gZPtZkMu%;7>S@$_<8u;MQ0M4320xYH(>N zQnJ&Yw#U@JY+G*cduNt{0jnbl@J?tX`<8L(UcRQ_xC@;8`R6gWyPcSQ*qKkx!JK)VgXl(1Md5!b=plb|0v66Y)FeK>Ip_Z-R0-^p%0xh}k3A7r29zy=e zz7cYCMl8#x*&lFI?FY3${Q>PZkd7aG&=&+OJD$_-mGnD*3`_fjd?avX4eu~&=oF9= zQWYM+cfe5WF&A~aluO=3*lv4&?OXxbWg;N#-9Mq2+t*w8&UPDKT1jd>V2Sk^ma!yF za$!)aO(i2qZZvYDXu0r+>2^yl^6ziMUTGWoSb2I3%OaY3@?>#5O)GG%lQj6Lv*^8sQ6*$s zvKsrnOPzriz>&neZyxjR>@-~u9sv8Cf6|oHch(&oi+^EqFG8`4qa#E8>C{yUkX$g_ zot8gxbCV&1V;;31IcAQ`@_YfAsrDv9a47D0WC>cyv|`O_!!T5eH(()hG3wMFAEVK2 zyvm+|cdi+^cZxJiE<7xM#@5c!6yh8s(Dlh0bZxw2Lf(BuAZdCo=intDY^Y|o#lR)= zWCcHX8{)Xp2UF5VzWi~Wg7Il{qf1IXgT3)roK4VOvZn8pKdnBVIw)~#c(SWh6}QO{ z%&Q-K{^qSwVu+WZX}i0cTa5fC*B9ij&KqyS-map0FMl%AXc??HS{wVI;*Rm1+N^69_WNN6>#L}muDGSPWKnp0yk$&lb^BOQ_nQ+p z5=!?^TqyMA0&+ju+lN0n&6aB<((S#1)(0YYc#V2-frHa#os)CJJ<~QTh@6KSuZPM( z5qodQG3;=?<@x$*7jv!@!ojL|&+J`W_|epaprukhLoa!KA;YsC*oeU7d8eK=<501g zH@g*#7d0LUFy-#XJ3sV3pVt}hYpI4dZ(ZnGs}a(p2uL_A&4y%ttG`qje!N=8uvxU83gx}td5ha zZzWgsUGy`=Tu?8pZv`7N8m8~bo=|4*bXes@Ae34PWH+?Lj=!{hR}ScOofpU!WrJ_U@bEfYdHWzkx3jgJcQ9nqorJGAAyD6=mU za-RtZc?o#CztJ-c&V1}0nyQ#8{?u0R%}WsuyQ7)G$wJ5aJRF1K#xK#jE6tbJ&B`q36{)y^<3$Cy(W0 z-nV!2dUPZ5+l(mZOY?(wfjmadvW_vm${fFPrZX`m{z#VCJ=IrAc%9KOICD*Uzpq+x z#91}&_3Sg-+EFD4^AwKgn0Gb(Aqr5k4#^NnN9)lD(GScz*_=B4-;-`0bj z4t^;bIDMuHPaQu}gbZ!KBEik`r6Lp&x~=tpSl3%f~ypqGB*8+(VkiLi1mC^SNq$FXil4(x1nQVtI)#f3fj z#8o$TdAy-cfU}N_Eke{o7{`f$BcIUM9>&3;{kLRf(7_Q=r*k&^c-(F| zFb_s`kX_f#5Rb(3v{+)D4=*noF^;+FBKq6BVz)T2!KW3ZYWR+Q?rrPw%?r6O3~hoG zvQi#nmUzf=9gzpY!CH-{Jl~JH8g-$duSuVVJgI+!gADgAg0ehNKmFuJ9qa7#5@}6Jk-oi{WbU!?Q1lK=R+2!e-lffTMG3-1!*1o#u zp`dPP=y|4DC#{KQPsg(ZPS#^iu@!sozw;(nv`>{Yl)&yUoqo zGj}R7B<|I*qOg#7+1w?uJF7=O+(b)UW)O;x9Jdgm=9e74fu=^DeqVdwNSQQoeBpqeHMk#w{ZQ`uKp_g0 z2^*q^Y-v;uy84sdn77yhyPEwvF5z)KKm>A!2Kt4zwVMn*>nK7wMi`R22$}Q;PG+Kj{RC*9 zw0mJhL$WFbPu*ch&0?t)BPW`Y!ooHgzLnEQ+72R{87Ucatlx`)maG)vZAmJG8r~?9EgQ*PC_<=T6BQw%V}v{H0#k# zhPjPq((n)<(){hNHn!0?%EEyvkmrHSr@!9U{EgeJt15*A5U_^-^)6uL1=8@5<7yA? zkfEIbHA*6kgd^|v!&Fj5yDWt-Y-4$$uXQGPcqD34h4%qm2faJnRN^zwB^wgLqB384 zj)1&Xs=C40nX|l>L@O_#&HMI{y~css<2Ii8#XB!Rfq?U?0OoZ&Y>pSU(83h`<)F@* z7v{Z1BGn&G?S;H`M1DBOZ~o20GW&@k`bVICdKo&y>~H(PpJACT zQo!IUbxr@P&`4wj<#BKN4C-J)NujuZ5D}GzokL7gOYhKJZ@DX;1NaLDhe8vI>=cE- z7rFXlTbG(2V=anr+)a<)moGz*aMStx)-xzP`HFXLYC>cHv!Dxq3msJX4OIDywFO;M z`#&R?{8t_Shnm{abx)SrN8$$AEFw~FL@nDbe5P zjMg@1>@i)~joJ_$+PHT&UT4iTj5L81*0FZ;V%txIW*}{VRO~nP)8YGA1ayenOi%sp zEz&#-$8}Xx1}=4}8}rtI2GFJ~;6qZ+xCK1>@)bLLirmF~v(L*GhCQ3e!dpX2$u;=A4*u8j%p z=o&%pkq3}|#iQhQVfZ+#;F4a7JrCK__c$dg6)!jg{AR{a{?#|^EuaL^0W(=gF#*uT zF~}=4V<1ftkTNPELTKm3sXM%ihVFuNo@_GgJ3q?e=H+)2{*dULxyNVPF$vQBAx)=O}82LJG_WVtz6ULU;r#r^|uJpGBRaufo-;cTJ8N6#$IPMVsfLn3_ z75@@aNd<3c7@1d&9h@jF5HzyXX*pi)f?{@aaZcS60E;Pxs*dn>=Cq~Ux_KLK!Fj}@ z4J`~g-*6*Sl&e?6BYm7E-&xi(asbC99yo82@hW@ZSO@FE?j@;W_`Kycr!w1Bv|q~? z5YCY{oaLgub~E_O<&=ypi_D(8cxjq4mW+K=2}Gb5_-g*lv}^UJagW=g7Qro;gaN-m z?MGA~J*`?1mz*0*s;mjAWtFLA4C*zE#q?irq_ zIL>5;mijG>C8<>~=E+ zx5@WurozmX$?(a+sDjnST^3WBMz6&;c2|Cq9N<1NyMOYH?n~}lO-3filco)?8MV)S ze(m`r^gY^lX+FAJ;<;7L&V$%UU%A$%!L*3*$R;1f3+PuIqocy+X&}dN9O>%pnP~i4 zT7kEG&Lm4kiINZJ>HSf&u?vXfGa*v1a#V(n4`W#i?}5>KSWFP9kK(NO9F#O}#FsAB z>TejvT3HiVVzM#1g{B#@)&jD67Low5%@D3&CG)7t0p54(sCi|ie+>AL$C$h^Z zSo&!3jplgw2F~`?6Oly?F8Toanw;e20hfT^@scbjV{=u^mUI<`xRqvB$AK7hu=ST% z@_#vaW44UDM{t#p>3^j39PxU~2}P3%emWt~{Vi6(8UB?;r4>2ykY6Zv8S^aHsot@-*-Ho>3pdK z)KjEIS_@^Raj-o8nyEGM$6ogG)8AK-x}wzlq|^_b+Ug5&d{uFzz&tUPBSZP(?3IMP zn9`H{{K4lnTC_g>8`HLbkO)5h_k(D<}mxJe1ml@m2r7@r^Z|H{h?9NA;eiLC}xpKe6Rj}-D2wv~FnbLWYw zt;U^@6Ta!R=C>W_NZX43osRTZI)@XIf2v^0hU999U4y?bc81JbdIXP3?g*ni5J3)c ztXzqAwPw1djT9cOvJ$qOpCiY2nMYaX@;hgOyGff2H14QL8j@*Jm2yU;nL=azToyDE z3J%-QVCx46o}IykB*^|r81cF(ibMgu1T8fdLI^~N92p!WllY|&gkFT+F7(boe@W;g z!(V!!Ihz8gd;I=cqm8${yt`iGJeAh*@IZ5$-*hT0T@v&C1xtL$LpwcXu*sglDZ zr*2hb+X$>2ER;)*)95L-4I418^Ae9T6rHb=#3WPBVe{fRbMrcm1~DEup7MVD!O%j6 zbPK?gynDEP08NCO`N(}9tD!zO~X62Kj#fGxa zv*kj;#xY-h@M8fLi_eh?VG$T>RG`J-C8zWcI;j%PNR%WNpi1VG%@sh$L)9wO# zS8jSE!Dp|&U^Q7!)6u`05JQ?c%X`w@!$KW zMt8T9I}I|ru}fhA3%<~7)9JPFn4{r=1KcAR14RbPsRE+yaDx*2g|jhwr}ug+!ekKD zkZo&qpdhiU19x^Ba;)_SCl{pdYGJ7;UhY4A-!HQ`UTpt$lu`PsiI#Fvr(hD(VA(Zl z5fC*7WNyN?yHLl;*hbW^gxAFycRocx*hq<>dX-BycA@$8W|}lf}z}VfXoH~5kKl1<2jUhu}2R;pM$b8 z>~G#M(X4rWYbN}H^ubu(f=oMENsc18j-9lL*%NpckJ0>OBg1YYbS+}3`OYhNKui37 zAsKMcirp^ClORVU1K( zflnE@Hngsj^wjy&4<~F62DhK5fIAgAY0b1F#0ZyzZ%vAOZ6|Is#gc6+hu%7&B#Yyw zr7#Hi=i~<&rut#2)wV|RyyNK#2m(&5>!s7{lyly;u^YKnptslKm@{}!XsCtCqGQTV zAes>ZMCUVgF7{3E_=DS&%|uLu=lAS+c{L|2>c#3tm@V}M1u_f{JDfie=)p8u?bn0r zY>m(e;@gtwbBb~MRp`iy2Ucp5t@()cMy9gg{tbCVhe_ex{5|HUX}4qRUWiUePF=;n zO(6;L3QV5K3@SS;*O4+%3`!cZVCIO!Vc2?zxvKZbb)A*jqyDjN=v1v3b5NAJs7Q^5 zKHw$V{!c?s+%Y@L6-6KO=hqoF`@7%8QSF?X&%t@p>-SxnE7#Y@|B@~eq1aleH}*;~ z@))=}k+{B9<;|r}?3g9Yp;eA$kPf<=8e`kd>uZ~bU`KF_Y%+ZLf*tU#`*-M)LlBj; z!H-DUdC&vU15XS^?302r=xYH#70lKDp0DOei|TN|S%pKKpEq1Vg|yg1>zu@!g#5EO zZik$h^b~%o9lUCbZZ(VZtm9%wEydQF36;3#K?Q;3ex@6;0?x-k|3q&IdM}{A5A?Bs zJ}S~@1o~|F@63iF>zcQafN`NpITlVhccAPPkQ+16h$A^`W}Ei&GO*Nt9z687!9?`W z^tPh+fjhkJfV#`{Cc}UvVIdjFM+`>+ikv(C-a$^SlCUOX11^zp7(j8Rnj639x9>u< z3_d35nOHS^W@^zpp86_)HdScgvW`JDsqzluKdzoDN0l;Y!zR|1$hQ^BU1{?y*(yX)4H zJ$8d&4bzLkguAelh<62NDj`RnqLL){Ft<%4o&{u5oYFdqxz$2BTtRBb@*AZIOf9_RN5zi;APh z8_rJl-jU9C=F)Uu{&+h~ebP330&2%{MZr#HhLM?-mR|O(3%6$c$!xC~rYlD4i`K^@ z#k~5jOrM}HOrMIN;~9tkRYduZ3uGPkg#m-=BgM$!!WgbAs!Z-aSOD7}V-spC;h&95 zSIoOw#(8psn4{o|%YIuZwYB@jJ1jJ#hx$P)R^T(v;><7~Y(vaoN8Gq|Ftkuf=p|3l zM&=na&xdmEvf=aN?p;F6b)vB#h1o79jgG;6;!Vc_?Xkrr;#(qxV&@RYrv?qmOSYkt zg({j3n!kS^?wU8ovqCLg@Cu6}$B`FvrhO=3nr#lzTDhSdQ#)>=I3)eI+iiF5xV6KZ zqp`tx&xeCMxY0ZC*OPjqvW{Cv8h%8d-NWzwqLFOYTU?3jJif4U<|J}-O<;yZg->a+ z0?x@zhK1dve^-^c8gk`2S@Pqh19ChUYU9#@}3F*P?G0bgZ|Z|hqRR9OW6WN=B-7Rv?kx4;)}8& z^?u;&C0KCwNil(f@W&Z1Y%-cN{NI(ZCY4WK(bdi@oblPQhGKcWOYMowI{E;<_QB^s z&t(;eTkTzo0bWvg221c6v*;=~n|pGAKxDRqq}qksQ{G-FoE&Mg$#3MX;tCr{XXBq` zE$6bTV_q9euB9^QJYkhwD70}UOr8I%E|lRb)kdpMVss%Cde7wV;^CEzQMsfSq^a2H z=x7z9aw^|?QIz#anZ4^oTsS$DKkVJB^z7Q^;wqk~*T^&ZxxZx9^1X>(1Dwhfa^W~k znHsuxvR-RrB4X|)Kt*AvWV9GfJXHfJoP2QQif(#_qR+d$4hN^FHu(x<^mk<9N zQjbi~r6C-6L=foSUgp*LWS4LE70C&ymFY#@;rbU$^Dfbv?4~loCO#VfYlE` z83Snf$*xd8c<{Owpiy_ferO{PSz+T$?InEnyD`~yM}d{LAB}n#) zgkc5pcj~yY4WO*yP8ZVVK}#{FK-78(0c9e)`ik{y#r;TTG0HK{rWQp0q%XOBa*EmTzSMZS{cjqTx`_vnyh&iG2NJ9~xIpB(=FK zj0od_u0u>nfy~sIQ$vG^8>b&4vkojwwht|pNuU=Hz%vuuFz*4FLB@Ge2dD?s^Gu0) zqt-(){`*Y<`Ru>eRc2X<^bVF@Pop&^fM!+BzF(Opt|l)@Su3vg5nOB_bZ|&wVdd1tp>Ns?N3X%Gv#MZ|1zS1Vygf4<35gdWhgOVN~rY zEPCY=b+=xKWRuXf8(Z}AneCf39>tH~1o}SjLJQ#0wM7md$08pc@Bm&et}sb0_Djtt zoB4gNmwIzMs>e1fjpk6a0xB&e{H761`NZ1M&aA)~$LsGSHiof5vjF38bsE)FWW5K; z0!Im(jej1UvFqNJ%CW+J^I4h*M-QpytW33a++<{QxJM;?lVQL6(Pb@@m74h#^v`-> z@d6`;QVsd3$2J}T8b9JQ|I~f|CzPcCX{vu)mFkb4%bI(&I3zco{nSxxsS(j%)AW*R zpXC8SiNO4?kv9+W;|xXB2Yz1juNuGKt=8$YWQ_k06J+b1GT zLjw3)SvnGTq#ZfACbltCCP-`PKu(giK8MXStJ083lo`l7NSPZlTX&NI1$nfFr5rVJ zrqBQsz)uHwxj5rC86Y{xAp^YFT0kPO--d8Q-mr%ds6vyfYpUy&)LYRM5=E;3^Wzd> z#7lWYBX^7mHtAS>pVJ4eL{PKUgfIhN9833u8=V0rd zz8cim0L`v>1@#it%M(V~WKbiuQp2eL-O&tKzbGR2&!c}_B=NWB%ou_F`cP9po^md1 zF_1hiMj{YVjz3)jFna7Tgbgn0Dh$vY`SCzM9`PE|7lJ*3Y(`aWz<;{{a8vgm1Kd_r zH)in-G5WWM`tg_o8#vl~4Vo`as1Nqz5~{zoDYOdn+Y|kG$R5xT>uNoWFo{R^KiuQ>{r2lMR`yu^jqrYwRw~anX{)fqpK1kB%Ho)rniJDZvUHuJ7(KbT*si2iRuO^>6PhYko@R* zHm>`5UT%(}S4ER{@Y+paFyB}>mIIkq*Qp&gCwz&AfeQ*1}TFIA}B^$SfpWp>4B$!6^OG zJ9~0jh7Y*DfYyS3kr#KxfjD5sTKg|@8uohK#cuag zq(bvIwXc(-two>T;l$4fpLkB3{hQG-H$S(7<&xMN?pwii1he`n)G1 z{czNR=ydTu%1LHlNno{aT7_I;i=&YxUuF{fpxDG7uiFGs@c`LO@OAYp={-)juX}W67hPvaM zQ;OE+spUhDc&L+A%)%!Y$34fMY~2GgAz!*49-)&%@^jk%Qbs*BuTO7P$E%#S?@TPo zdaa+B>w*j6kjM}MuZ2b8$=FUq>)Q|PCl~!*h$|A&O_;v37LFR#@k$3D_65z=cHB%% znGC7yzA+-+@X}mhqVDMFOO01F4r0BQ;5bSd1n>AVjr+kh-kP(}Ez>Z4M1|aC@4#SD z<67~zwd1h=kG<~)W zDQ!ZHzT{Sko^8Dc( zM|)!oud0p|E(z#2<$=K@buA+P`Ks`0lIn#A=ZFX__$d>8Lh!EL)>=Gh(zM+9~sbRQcW;hv0%_C6PNV=enq-0n=Y6>Z|?P9~Xa$-D#CzxD2_ z2eo9^!tIkGa)fo$&`(89ZUcH@QeNJPG}n@sm?j1EnO6DopKkkkMJL)sSZKhn6|C0+ z^Hr4;4yw#B8_6dx-PKlp{P!LX5_A=*CvW+e#2E810Nn8?&N(ziz=wMPkkEurs#M-e zo!O;-SaVmE0=;n?{TP~J5Pyj4kO)it#Tw-rbtTY`haV>R;RQc#gugl${Qr=1{c(=} zy#+Hr?D=1_=anVujh=l_rQO_!I`t+Vy2`mr|K~3WqrAcK#MnBOSw$d^(S`cnfBu(b z!ykAyQ@MpCASrduY8THWvp1?z7KLnTiO61Z@te}fRNW4{g5iGTzqJi)6W@NP?;QB? z^G|1kBdgGon+IBju6$RzWm&IYj8N8j1GoD$3rC^P){ z`Fm}U{Rn7Xx&SF*Rks3qoT+YR$JVw1=^Hjk))Mv!ziJXy3J zR#&2B1U}_2`pVI~k7YueXw46m;oJ(GDzpcxDzB`4y__q672XnJ;~RGk zS>@d5xY(MZvt1jTx9>%^IeZLUlo0UJUOwbS?OJmIKGWLQ(Xk&VIkf^K%P6}rapczX zz#+=7!;Rxs{Q2yZr|w?)JafqTtUZW+>qGI*Isdu3hXWCgrs*Fq6#?I>2|3ET!4?Q4 zuyY-^4hqFJ`)8rZX2UQvsY8I@Na(k{0y@@^<@pdoQ zgq_eXyuLT%!>#q4!ZSo6|oliW*mneu^ej1)w2Dbpd9*y(UV!YBd7wnzSADRi8t;W|F84b)6uYA46 zWY^cA>zwH@*JR0}P|Ev$xkQfV0F`xaRDekAUNt+?!8`iN)SP6qb@B`H8SJrLEPRMF zEqX3AZldWdm-xQR&(hp4av~F^j(Rei5&!UG@7Q z@eyQg8;aamOu0s6M5FZuv<~9m0YgMWGlr<_W|s%E-f%Kr<2CYrb6!&OZs+a|@R#eO zsnT;$mSX2EXP1!XrQj6=G)pu&qkifGs7GEkQ#3t8o#V`tNHe8N%Zt1#g}VJ>3=U*4 zRBVK&xP))|@aV4f%g_1^D)5{B4|_sp{*jfC5z34#u$||u*a!V`P<0GWG~DG;r_O=( z`B=1pmT`Y}i_ZIRPiSN+Ze!`?Sh+j?&=T%_LCB;GB~)Xxzyq_+Ksn+A?l44?H{nEJ zX*azT_v^Qj#;p0-Er6eK_JzkJ#g>=rp7S{syn`LhgQR-SyqtO;DQ6+w9F)BIaxD>K z_*TBOC@-c1L5#)a`ogdDH~ABs&Tta#U2&Y)mlvI#uSsbPO9z%AMLlIq;@kyg)pYek zw5N&?`d2aD*5VfQ_1jXr`~`KmrJq~u!GG0!Z5DF|Xi5JO=wKLM)?GzWsgFo%MLyg6 z3E%|3-n{u&=Ewto_PXF6I&ih*&=|d?-}P*J5ku$Dj1^XoJ8AiF(oZE?JJU!(d)y0m zhFg?o-gEx1E2h47b>@{gOIjxpYf|z)|H9pSMX9!?vCiGXfH4>iLZjNA;2-`u+$(?Z zWAa_KMd8+zVh4E{r1huin{?%QvpcGE`Or<)Ws;XQ6K~#mNC~Xn?iwT#Lr!TY*Z$D2EnPS&OT|&1$)*kJoZ7 zH|G@-Q#$9YQ-T-dJO5;PqLH98(h~QxOD3QLVP?^G39$P>7xzTwRhZP$O{Om!Su>kU zxWq(U>{DhnqXDAk3n?zNfQCgsG3vELIOnSv0T*?rlwiTo+CQ`DUtXX7)b@u>khnCg z+FEM_i4wnLmI+MtJW+c>t)kGWHOG99iONJ#tK!35N~}X(PyU*1e!Tvdedb@yF1pf= zObesupCt8~pR9GEkA%%9r}e$+=I=CZd7tS4-4B8V!PFsX+A$IeIm`VIG@eIu?~O(0Kk5TJsN73qmpNoOzEReo#qVlOZ~f*x!j%ms!plcJ5Mpd+m7Dk~Lf zHv0bHwhR>OCyQ0VCkFbhelao-`|z*qtny~(r4-&5_`A7fi>%C4Mp^_N3&+vGvNOs? zq2KclIF_-KQqSHUEPSV|7f2UJOyzkPxviJuE9a6uFc^C|m?hzKQ5Sy+Z%aIV8 zPmWn@R)0%rFJqT`6$7=X?w@Jpk%vCYT=jc<%%629mJDBT86kcW<6zyj=FFLYdRjQNoox=2&H#}o~jXA}*~EG6FaxU(V=YCE>93NcC-{mb6orX1}Y^EeaO+q=Z&jDg~f^0M=%f8an~}GcM7!n!ZJ!QV>{Mi8`9f%5^R;{sYtYP z22X0HwqXjVo8$ECT_^UqNc(N8?U>y*V(4zlaT(NnRWwDyr>Z$n;f4fj?nNQ~nEo zSK@i+%tN^luD$XSuT94wwWgZVr?R{Fw(^4bNxINW#LSlk=sEM6_oZ_^(rrv$tb~#r zU$yXCd;1`86@jIP{FIF~YQxB=D58j^@tKt^H!c82$U!9iy`q=D9p@1EY(b3>Bu8uMBb4HRB%#;&h+=+u+QKwVPN4l|B<5xc6>kb@IP$u!v}u& zz(0u({1?kWf8Jw!0zZeYu_5!F`vxciz#g$^&n}RQy2?A;44t-A>D{aV3O;+%$c^!E zrc$V{?{gOI+c*Bz0_J~_?{m--MO@qmsZB$JFeUg=BrbPP=oR(iE*|}4NTq>DM-#M@to0zZy_dl|28DU!H>G73)rj#{Se_O?zu>_vuvb=}8mv1)4Y33xgmqHwom z00@{4{~87QtH;pYj9qP@Y$?YvUAo-wjSD-NNWAplUj111?BR{HMw!rY*fY0#Di+39 z5=A~0g`Pk4#N=(TW{xUPqj6|BlgsPKC93c{=*I^%I4vVWZUkx9byynhKDm@TP*r9T zV36l2eknVZS+!f^@|VI_jjC$rU~WQn7P*R7oLGX>RXe7ZOf_6DMhKpCii(=|JC(ku z4L8K`-|vZwFzLP9aXaqL)HyHosgFd;uV9>k z*m;wSG+#gZB6;j?L%$yHfSZeygixbXi!UsbB!-J@rXMybJ@QS% z+Yj_{E$jnXPdA-2=Sv+!NcJ<7f(%x1p5%D1{7$pr2grto9V2c`$(Eo1t&ffKSvgt6 zoNs@FD7SmYIRW-g_)O2X;ilVqjwHM)XsJ14sB!q28lz@U!|9nmJDe3M*-z%0v!Q3A zWOWtn(4ePpkAZ5dhi*}xy#GO;h#+}KyR`ZxuDpow#wu6+-n2fnndc=N1xdZMH->ME z(N*LgyUtW)sn~bxvR`To#0TfYIt~pmjMz&1pHf&F5uxOIy^*#YO69hjYlG@WYeXZE zzWqSG=7e1J$osaP{?oxH^Dp#tZb&Tqo#DPFovT1Md8PQ(!iq!woBKmm?yjU+^rBJy zZqbXFAc}36n|9`Ma%=CpGYOsFH#h$rz!8(yZ$^Ghio}-$#%%uQZEP|Y zl~U)B@-PutiH5EH{easpO4QCFx7!atvKQZ)5U;WYle zz_<^-OFOpssnx8(5Hqvc9U!Bb-wvqa*&D4boHwh;%FQMr)419UFY@xI>1vu3K3b^j za?i=)?WKsMr-w(=OE8jVOQ;>}+jKLUCW4P;PiNFgw7$=jMaR8Ph__mlT4+qry$wFC zc)B}bg`>gi!5DE}tiO7=5x#a5neL^#4fPX#2eGnm5aKEm$6^Ib~%5b}Ugp6=Okjxk&#Kv$0|l=#o@!eS5x&F*MD zcqJre2F?aq!-qYm3PSLJ0o|+;#YR|)L$;MfeCOuw|2jsvHRjgcEi)oTGlmcc7J?^c z#Xg*C@E1@bL8w3eTO7d|Sr7fG8K`;Ho*X-co#wyhY7*uS6gmQjkdh`CT7_gy%0rUB z>;e9p-~NwggK1*H#09bLs*h7tD6q(91KGa+K!{Dt9Wq1Sth4bJPViU9LF6QpSC_&1BC;0H4R|(IgbH_qb1!(t${E)j zbUFNRF2inm;Z9Bln!SQ7D#ZOf=4gX1tZ32y&G7d&EI4X8dM)K8#PmwL zasWZ)6MYNO>V>`h{RwjQmD$T}f~YojXF5}I%!qu82~O4=ki-XS?o zw!VDRuU!04_@f}Pzbdw35^E`ML)ca!ZY6FFJuh}xKC*t=MQ92Y1};1pcQ0zL&NnkM z$UYzm9a}%s6Pl218_~Cx$d{~tB0UK4k-{8o75zGfA?g*xUx2;l>G zKU2G&@kHsl%E0CIsW7>_(KbxXKwbLtdnObyB41vHOi$V|g)k>&gWzi>yPUlrmmP{` zq7_2w!KJVuN6D?LiJ}S`UidHwpB+XX^`*~|$2sBorC>jFEuf(=a*H=Y^Ht%Nbae)` z10vIldSs-Is@n(U|CIhRKec?JyY+*I^NhYc-UfY}BtwAflVT@@ym-spN??aZVgt;` z1gotJC|&Ds0I2vv2PIMowk2Pxfig)U1Gfb1@7tfY1UYIq7f`O4UX>P%-knJyfRGvjvM>vwr3@9RJgm z?eBTcS~!l~;W(G)GeCe-LSOZ>0=5%VETf@rt5a@m8$tu;1zzub{~bf1+fe^UGQPho z1HUc5_G9c1JN@v1A3pGp`2g36kJ!Q5ENy%lOwQHN1R8;svNGxfP58aq zb+=g-|3dqw7w2>f$7$j~{CPH2p22~Xqu5bWa*Wh)`OL$4nuDEmrb_2eoTt;NSy2C# zn#RoCw#Qcstm?;3RVj==Y%Fy8;AhN2)2(gN7)Ug(g9y^QwHC)ae;yjgYhJxmm%IS{ z>)lS8-Mji)LGqSqBvBd&TB7d6yutWgFGn0FIPC1YTW)!})^-Ev(cQZrS7g)vBERn+ zyN>^L|35r}Y?4E6Yaqzk$R?w?y(#z&(?5MhDSDN6-Cu%aO=$wSCcBdBXH$O12Q9!& zXi=V{mQ39Lzzv=NvdG^x__TI7wEskdZ*+9xhX&t2ZD9YS&F0?|4gMoz|EdO`Cjs3Z z6-$CGsQ#?D2gHs~fV{=8=A^%J{Idw%oqf<^{DOu9*fQZcj)G9#%+UD;oto6jm%t=~ z>7nPdDXKmn04i4#@V3849sco0HK0l6@9%z!*7B?Rl14bfACYKC_ig- z+0Om$ksyw2u$uQ4BPaM7yw+B7PMrVK#X={UJofy`d*)FJ|GciJJnZ+B$4ar6V{76v z{S`yxFJgpq^GJn<*wsE{J#Y!#60QIAU4HeP23dz%W7Ep_(ec=Y#gSa8H=8%nY7S#+ z>qPKLNPr0S)aFT`9$u&KH(PcK{bFp5mUXgZ1f3x8XqU zn-4$8AI(8p8?~RN)Z}^^nk;zoQs2d`bp#I8l;JZ+O=t)>*~mPEbhUZ{EzUb2iXA6?5=ye55me29G2 zIM&xUvQlt@3NfPHvE0HUCRcyLWzjl@v7(YcCd+0jya604D z3iMq-rzyZ*O16>Z+HRzAc$cwC#fM;_*6h&!-j2Mw=}7I_ybivI3BKu&^5%fe96b1# zV^VXwl25Kzv{qQ@yI#&h$1Gzr=QF{5vHf-;U447ePoic*@Vx{D#ky@Dr&i?#{}@!_ z$kKf4isIPAHRa;nY1W;XeTI+az3Ym) z*^~7Y-7ODSxav%O=;&nLyqx+HEcZY_e|3DF*@@E5mEL8>b7vA@e2;FXFZz(qU^2wV ztP^dl2&22J-1=x_ZF2Q6O3JFc+GgmQ+t+%*;I`-nfDu%Z3EG7v7V23NP^x>;0U7x^ z>+-G!1k-O3D*lle+C$l-vP!P{@RFiZ+JsQRY}J)s-S}W)Lvhr7uHQbJS;`M;Oea+ zlx;hex%N}Ub1$;%1FN{LoYp8NmAd=)!B|4tl1Yz&Q_{Fc@=a)DWu! z7^J5jbjah2rmDT)hW@nB8UxmgwJa{w^`1qnqphPz{`Xnx6E@ncWJaWiDWG>9heIzCb`tCw4b(=ZHc&4io9_78y(+*? zdQzU#^;ghu&hLX5!K@McAa7LG>-j0~G~F=u=!Lt6!zC--p96$7WznVt)Dw}7fx9+% zw=}E6!F@4&*j=aTq9v+(PeH@)kX4S6ZSZX-fENv| zH#=2=7isx;(7&F(+e-B`C`YdU94p^PKAnD7?vzv`ce_OCK-?^ zY6yVMy4Qa-!-4+RF+knjh+Qx<1Pl+3_Ot5+R+Vmey0;~c;tw#%zsl$|eA#*o6qbC_ zIu7Iy|3cLNyBv-*~JqvTiqwi`;| zbA~jVt3U|xbGcb2PCVpQ&e$rwB@3N#icl+;sZwjl!AE1?^o4=EQI&${xhP}VR@9b| ze{d|zwo+r-6rp)<_7JuX#cbF-t7LT=xh-Vd*w}OX(eQ91Gx=#+da{zps~LS)Qd%?L z+vY))?v(VDtd9JFzvceF^>06@iN87g`3FP6AMgL)5V-%a{Xf3x`FF6L0I~7GBP&OI zFp3N$o;z+~SCg-E>^9UIdRgg}yB6b)-qYgFCBi3HSH!36;K4&xJ{L1D9A&Htm%gbg zk6)r&(VKijLJ}~$(_eCDZ`RE?+EP48LKEH)egv`vV$^CcP4s4PTe=R;7%SAQn2O*a zX)+o&NL~}rUaNriV^Utda8XgqpMW16@)83&iu=Vzb9xKj(m&lUq`nO7l@xdZ8QH7@ z=@9-e=wkW{p^d{X%u^<-_3$%;h^qv@twMLf)Y@l{jk&XEXSGQD>?@!|I>LoG$U7(A z-VrrQ=3%TjhKpiEjD}H z#dl#@k>iOS$q4RJ;^74PMW=f1KDu*-R4JP6)z8&72qglwec42$RqA>XZKAVjgFoEA z236ulrNJ{lmDjn(SN`g!{Bv^!;%)xPimr~?gE&9S^R4!0TO@u3bKcE)^N1!|)nu+7 z#iBZnq8nQh7o9DOaAKk9^?Rco+Stc&T2`KuvPVEtSaQhqJc7}xi3hNlHT*mi^u6EU z_?~5z#zefS_KW@)*MRGs#jTqo>0ejK!(N$PG8FIDM6cP$P%xl>j|?^wA~-e#oh$6U zP!`H|KVaT&Wo+Q?Qlr^ff<|fK9Zk^ZS&yOXjseDmj{N7V1 zJ&8_*bZD)9~y;)rL4;>CFVEAc^b}7AG3nq$63Gax5*I zJjEEp;W&^bcy`NsXkaBSka`q9 zjyy>UG12h&_RUbL@F`2-`O5_3UFveI%Q( za=;P$4%@_pFJD|-vrazzQ%;w>w!ZS;cIL_MXndV6ZU5>bv+T~-cU>F@B= zziqdYf9CJ5;tiCtqBjZ@N%2djesdHbY(HnBNIzWaSt`NxNG4w=rz?Dx|_q6;q1PMp`(~i5w@*DhQx2v= zG86vA4!m|&4lK^5v&pt)r4bA+M+&Z*h3O1sOoi9%rp|8OtvV5)=9LdUWwVmGbP^YO z(JPPa>*_6`{H|GgRT&o6(LSe9>kvP}^PKQW>AY2Wq>FfD+{UNj^rDg|{rQxS6yD4b zMjqD2)8Te9lT3NS#bwXVNYWE#M{G+~=1+U&F)_zCt#uq9d#_Ueq^9K?`XK>Dd78Ql zdulB1+thmIecx*0DS!iNf>c{j&?K+n6Un+i5X!F&5Fr#sc^vDuy_0p|t5qoUo(9=H0(})$* z<)|Zl@5=8;8c$nCFq6!Y`>*JQ{yKS)~@%o1MKqU?2=(b=j(z9hD=dt$6is zt-f3jo`{zeCW&za!|t z+ei`SKfN`RA-J~rd^*7y<-=c~HPKc!xDmZ-s_<>yj$hN*ropQ`=&J4pWJxq7Ya-^& zr$p_k)zc;#F7_*^BbkTGRdjeAtnB+b7a`334$?wF2ajn7ot;buNSv1pf!1J*JFr5H zo=nz<5kU~NZfu|* z5d4a_Duo4{K0V;h4diI*ehsvxu{0CuiSt;$jJe>2uw_G9-;oHofPdwAf8H6c=oN$* z;TgBc!FS-bcVzC0X~mV+Xp|3b$huaFKR0MsxHX>=yrC8MTM;Y{U}gH1Ooq>U9K}a{ z11?Elqw6tMX%7vW!W3~eLS4$TA9Au}RC?%a%=>iHErl5v!;(Oc7`k43U<+a};G zY%SKbN_D~)eA-|yTKlw zMN)SnL&X6RIfg3xNoJ63Yxew`s5Zfb7kU?~bGJr6bW4qeD%Mqsq&c5`C9sCey$2iF z2Yr-T4oNnq$b7)|E!)pV&7K{rYVBV#2>WAza%$XdRMpm*cUGuYRzpN506WJCW!)$}^&arlAD0h)I# zte#O%QTL*$F-=NoSkhWWH1|_6={6vu?OLo{t%7zbv$FwUt3q8D~_i*IC3eOSAahRQY=DIgT;0>JHt~ z#R#;G*~OyZx!aiV)izVI#Z{`F^Mn`4@+^MRvsP;;6NfFrd@S_|FfloQ8KOIliY1~u z(a+$7JIsf@@LFYlK3ms}vk%ip(Exa+s*)=eOp2c9IK3jL)cuvV#}0N=7Y)o>Q~pd4LJ&8)?rACj1#A;;5+yw<|?j00#O+<$u4P}oeUs&a@y{4AAqVIM@e zySl?ZTD3$yst2e6x1zQq){~ff>{jCz6?@{)IzUA~pey?HF};iEEdA9vmLc^|O>XZY zz6&E3x7a z^cVN03lI8nD-}fZxvA}jRydQjOL0!Lx0zAGQ`g`Wo1nY!v zn~$e9%MsP)Ep(mZQ1s-pF?NiI+1KHgp6lJ-&KWL}pIB&iRmoD+5hG}Wy0JaaqOk4D zPo{6(rNfBERC>lV3LokDl@L#ww@OcMiZ0Wds4O#+!5wKsb(3umxIAM#RX#nn>xGCR zYh)2nVnE|5(u*l=JDYSa!@D;n1A5o5p8>k>)fiTtKF++H*@0+O*~kRMQV&@Z9=Hf0 zACw(L9=fIkTn9ZbMOB@-k&q09X+(b>lAyj1qHPvLe*>h@vS=swLBpT6in_2iZR+af z-S(Wl#^ARn@Bu(#VA+rC<>fg}SJ&5BB1G$a8RWpQVgg^;A&bo;i!w6PYf_&vI1SjN zw1{ZMy-=h%>KMhDuRQXxaFebp z35Eyuu9SzU;}kKb+~c*C4(O<5dU0{rqFO>*?WiMzPDT77$~7aCk{9A@hm5g2%Z^^`9OxECo|WfTF&q1zs7gCKaX8 z8v7unVEqVrD;i*K$-1%??cIVXrR0vUk_4WKCZ4cwQF7`+))2 zO^;A~$r6&F?|5?W!#>DrA$>wVYJdZ{FY362>;)BlU=jX!@X50zJA(i2is(UD{OLpO z%xXiVQ$%yEAUOS5Vn!0mYDXT&hiIg~|NT;OVI)Un{?!CTu`8@v4M@;+(I7! z$S@tzKp0Ot(zcDmgT&{X1sny(MO{~~gR3FTL?vuk@sSc=&2z6Ecw+L+?8suZ1O!{p z$=i-6&}(S5e~J=`AEgn0lOFh6`BDD@<9?Jz{7)z3{uN3i{ulOc|Efh1*k6hw;Ec#W zs)^`a0akA2PnG~Jt#4hhU(2pxUBsO4g=zIXaB;eI7WR0w9nZdg z%{4mCK-RS4Lp$-zD_wJmUh_4vd|&)?Ex~aDECTM+n75Jmoq!0#5tpQy8zThor@Rc@5F#WLve7IQ4WiRQ$ zXVepubJ}(+!^?8P0+Obg&)fjstwBKv&IDf;y27!6<{}xJujIWujlh&FmrR%gyBohP z6}AsU+W>cLG73Gg9uZfCRjTPd*OBkJ4`LBxP@l01ULR4Y%FT+;l%BsIy>+Smp^FxW zn;%2+xJ{Quced9j=s=N0hM*RG+nkN~71svUN&YWG3QgP|L^HYWg6fVke=66=GGcol zB$5uKBi_8Rj^hf_tNfORPJ@ctWPD@oqg*que2lw|zc?aPK{B>|_(?3BrrnNyA5>?R z9NW-(ih&1%b^ETMLT)w7sXpWl`(D#9eIFVeQPd6{F`PrykYJ7kG!+`ZWTHawXC0!5 zz2aN93jZbT!WE0Gs^5Z6pt*mab-9PWNJ>8%Za>gy*HW_&I=)MpKo*@jx!0wB3+r|V zM{hR*g0P`Vd2%l z5Yu2_JMrdz@H-ve-mbk0U?-^%yN*8GC+c{vPZZW}$uXJhSGr2|XY>KzHSLIhQes9n|6KeyENBIwX1OM3ucBlffGaXus3WGurzk9yz>3M_A7J@r1F5;p1 zz$IT~@dcqC4_ox`xA+(DvqKvyETT=NtjS!9-UiDxQ+F#%o&xn&MZdX{?MFK0M~dkG zn>5*vbjpvE$bVN_>`l>T#0eV=XPI4?ru&QUvJL#SH$%aSJ1`mmIp!eDxiYVKeaxhr zW5E)-;7rJQ5%#?;ep*Zoz&GV{y0jHSayg7FNx_jPODQr$^;*o8SGbfh6{or^<~Y#& zeEQI@(go>h>1M?s1h(EuU^oSs-Je#$w?2@PJqZyLVf_^k($lZsYVdbOQ&>V80h~${ z0EztQL3@u!*eM$uSC3tV@QYB>mB7@zYb36 zy=#n;XyLf_arO_wX@7&wpHtaaQ`qGw%D$>}>N17@z}z=1M!*vQ&J+&$>repZ%F_TZ zbe;dTzKvn)(+zgwH`;~X0;on5P?G95Ox*<_zJ1VJ>|9a?aR_^%3cyGXu@T*=)fNX| zT@E+AS=JV}r$ADgv_ae-Cz~oe2S#0Pd@Q&7cSrwek@e#5&QJofljyVq=8=Mvn~vdT zhX{LJCB4%8DG5%Bmsw#{PN9j8y+eRmF%2Nk0Z8ClDqSa4?=uk6C?S9oODcwM52XIj zj!G#9ldRE!&<(Z_Dw!p4E0pAvdI|QD84$Ga?A50W^yekHHY&!5cA~H@WXBD7#1dfq^Tgl+ zNc-4!ClL1i`XcQ%K%dv7p`Yx79zcg_%}CXgHd1>xC6cbl;m3TDuBVXRuGf$K<&0wb zBtvxOd>OmIc53K{B!=;Z(HrWOVmtFfYju-zD|?gZqa?CU_sac;FGe+z=2&rny_@BM z*az}E??rxruXpM&U4u>ghX7~DN%Wg7v^w_cvMQf0^omd1-VKt-C=|0=@y}mtP7l)$ySpL&d!+~X9{^xmy?d# z2z*J`Jd)A*2iD~8*^^2vK3 zz}R$_V12;o&+evl+6y$-kE$CaUL<~dGVa7w=J94{p`y@aU@sMN2DKB?08A^vpj86E zA6Ev*}sJ3MG97j0@Q?m8C9{7R=|VXdQ3v*nDMvgc>U7fG@M6dPyXPMX+MwwEyfx|M~u2;5wNZ#9o7C+7vNqIgt|fvU#f@ zj&6Me*S zYiO&zOr(t#59LH}aR;;YwsL>}*Mp6>=7mauHRvJb#GBei$J1YdOxI4p<++2-dv=Z3 z@ytV9lNv?JpKiEP!a#oP1n^cakp)5n5hNZwWj6HstL#OL?p_+!ftdxN)h}8+!e(PH zHD{h7-LB!cKt4%gS0+N2JD5;@7H3*7oO5YS>}zQQ(lWz@hO&rL_2MyP$Uf-uFmR>e zYM>p@Es4EQM=GDK`XusH&7}e6_LFy9v`iAV(RcPzvG)~xaXe>B;q+yL`S*78u75^< zw8uyyE|c!hcv-D{82DPBd`#`LM=p`t1)u}Tj!M-0t4DeCBWTih+SPmhWSS$>$JR%SjkN~QwMU5#8w!`S6NIMOvPZTnU_v8UI)Je zl8eh@A;=BZcq+AwgjXWOZqX9U>d-HZ{8RJ3SOL0$v01!haeK<)i;H@9FP^>Jbd@ES z!SqY6Q_EbL4`|1AYHnF(0u{n}#?{6nIIgm;%q43H9*<%{nvZc)B#FWv2l@@`Fuakv z)36a_iRVg50kxefX$n;@m8AW=VhsMcx?(a`!b;SDDXWSm%RY3YbDX-)hBSvi83I;= zG5Q);voWft-`i7p2Qd;FngeHQVEo-Xezy7 zvJ_MNZiE_$M5fZ8O!vQhFT8C}Xp?qJ{Dtyf{Kn1WkuzdIg&uM*+h52SD2tohDk*Ha zk4^WBAVdAfjAZH}o((w$X5OFu94zO1hi)DleQBF(Epxc;QUdDS(gK_rutCd)*eY`i z3QS=sx)Ij9%I(KbdyPteFBAht1g}Ss_+XSkQRFDJyt7&CFnaU3J8?5fTnRiq$w)BIPqFNoEbr%o7&Wwk8Ao?b@S?2;X<2T8JGgO2D%dqw9A4 zKN0<^pc9_}N(OwIS->^XZZmpIn#AWwh@Wsr4T};H-riM2F-3-wI9`=n@+BhvB(dF|Ux-18WnJ~ym#1QWs>ABY^ zifoDCD=$*Lgg?imthUPfka?JQ;=q?@OOZ}>qy`Ye4mfw8X1vs~Y06k9{v)zRVjW1b z>I1hETFQnx`VH8xYRugQA>T-FiXtBg{=m*ig^)cJ~uN z^hGp7k0Df#YuOUGCzVw(*hk1`hsyEizel9nc<7_m=U4RoEi+1vMFE6kuOw#cRv%P* zya}uh9Ks1}P=^=WxaKwE1L9UH?;H~G)zt94QQ1Amm0WI`2T8%0_F~6TJV$#=jus?# zd8*9}Gc2|FnQESfkppI4Sw#8|ws4*!zq!%4YF)J%4t6miw=O&dAiELr-kE)GQY zr5@cQUnPl6DTED?o_^JmhM=wh zh}+e|iR3|D@vk9)Cmn)_xwUq#sqj?kdN}0FgOmKUlklOUt;lXFy%+m;s&>G)QyprcVf51Ndmq^-^rNL!mOKkHX(s1eam*^tLO~C1GPw(^zbhcdXO?ZKR!T6=gpl# zob(2EIgG%DABP8Mgw&$l&o;jbcfHQ@ep2kX4fHUAw>(9~!tUhz^3Mxq(&MP%;TnIr zKE4luEiVkltYl})BS->P^=}C9$LCASUUP_Eze%%s8vO)nfesidYY+?S@A;{NF}Uei z6zyXdYuc?=^}f{pV$$l_A@f%cSXr%rrQ(6V9%^Eh8GwAR`3YW??+as}r3lV|RL~j# zj$4qq7O|-UOi^bkj~q!&m-a#EWXjUk5&&RE!T`PN0+k&=cB%211hvy4N5a2v5BLm* zEdT+IQqA<(113(v)upF|Hn!Pei{$o=n?z5nP;^6$}V@IN*+fAjnP z=uGlIowfh}tux8*D|!F7AA1PfyR!vPa#4iUpC3Or?1NZQ$!Bx7^m%*aACDnqQg`#F4?UGoK@Mezra&pd~Jx z>p|(k^G{G}sS`^R$n?64F1KdUCSOHLJ~iQ~$rvKq%SkqhDT?V(!=aN?wW5xKhgi??6<#=%_ZsG>bBU0 z@yJZ56uqLjoCCJkPvHw@tWXLEP8DP&p zU!`N67`;fS?hiqp8)y&p`iy?qRi$K?TYt<21oU^vjrH-XUYoS8qq5H2%@>x>7m3Jk zytnXBsyAuV7<~27t25sAsB|r>^skFL!U9|ekipfZT89jpMOPj(r`s4>d%Tequl*E= zfITWKEPY8>n3kN~o=J`!?_dgJ*1Wz%p6cyindu%Ju4;G6k!)j!y&9;)z>cK$SG6{d z%CW2Um)1YxgVOO_4;XxIA2|+f*09* z$vXM!R5(m}(%eQ?>mArV#)8)<;$w85wYN%yypTfJk`{|^s{A;NuT<-s%nbaNsbQTr zPxC7Ya{rmcqlHm7hd~q}TZ4wvS`tS6x${6!clU|Ju#JDX?He1YrE9;RW)JNrhdRn$gH5n8iLKQHIgIIzx@?{=|joqBQ#IXS;F zfv9C-^x^SBLy8}yd%Hs-=kjy($9YVckH>IwV7 zBxeY-gnolo?<(drP}TSuZRA*K*X)1V)fs3@0*n1#OUR)@{`eoYgsb5t0~Ad#Q3`A{ zwS-TlNFns0d$azTDHJih{+XdBZ-cP%=@W1{8<~#uF}~X9wZxbeGcKZgLE(Yy zSHdWxb!T0pSb43iW?~`G3RWgZ*6}6eRMhDB*UxD$rk|Kx#GjEk0arCK#<}wh9+#V{ zc8wjSDXe<*_!LSlM;3D{tEq!4x+U%Fe02v)SyAHFgJ9WVkn5Ae&`S1$NOU(Hr}>*IWFY6FSASm|Bt;lkB7S7`^QI;A|@fBOl2pL zB^4QMBxS8+ol4opk|f)hQ7StjgffJXG|9frWG7j&jcj9=ea1S>()X=%o$FlJIp;q2 zx$ob7f4}Ga9@ihn_?XXUKA-pcdcB^{*YlObZ2bV#1nwu@_-8ldKUDkvP$K*{QDc<- zrIzo?FO@Z9L6E{Kz?qdXA4iRx|A>LM8A~s2zRC_kzsCX!;?aK_F#o@W9{fSp`0w^F zcS!s;Z&t)M5T3upRMa0vn)`}gD((}nXScqOE5j`u?8vt~GvTpa^hvy)YXh!20)!$) zp48VsgUwzlUa%6@2~KGxZWypX!&KdQ=kkErhLvkvZR`Ie!kuy&nU z<5rBFJ^+ytHrQ8{sKc3_sw0=f!fHDsxcXCcY#MO!vWP10JUb1sj)l%Sg^RcZ2s3!j zMz>kw^YGr|u?K2`Wg9?FO5Ny)6x;;Qmhjgs+21I zS=s#Y-Y#kuq5w-aa9Ka-L7Fc>OSiEg^qG}@n5uSt%cf>j<>p_Q#Vf{)@nmSPtp55o zGr+%x$HBq;0odn%0ya+>aarG^0{DBa+n86;nbbt)`B%EkV%5cD=qFqRZ1I}< zc@Wr?vtaXPug9*-YJd^XIP6wfaye9(-Eld)f6lbwq!Cj|;pX{@D+{ zn}CG!IRHFmk9=zQqFvYZA@97BrM4+lX}lGFRqsTfLOw_aEi%XE*wMD=u7)C?Y!&3j0wgo%vk)? zK1>rE^D0$o7``rezjcGEKZz(O<>BbjrzDxh@?d;vZDNNR^vJsQFC$VWQFEVOs|f3s zY*86|xr__MI1~EzqhV8vR77=$OUVm;)LF&8vS3XC)1)JUWBo=`J5I%TEw9=SFs)_JF*^N7gAh}L1#7UW4kIfO%;+4E+( zFN?aO`=Y)$6fSsQy0TiRD$rcG-O8BR&T>)Okn4j6WrG3WdkJibT{Tlc}eoIEWhHLu(b42`fS$sAE}%U9;LmD+WztkO;=l+3-ge|ML<+9)GX%haznKN8znKBKQpwzp z>g!77G~ron94}~ z5p2*Mib?p2&$_t%9-W>y^Hn7>|M=OUSE&LcIc6>)8Za&&rKiPL4-mH65IpG0wdG20 zq8k;Y;T9%Ch);-hS*v|uwTr=#^g;c&hCC3vC$cfGuz3Hy$@8BbOAA=I;w}dF_R|2{ z)g$aqs*2|VwwrJ`dj+6pK49M6B94ba_}2(@UI?c4v=5Vk;z5Apl?s@#8x=nwXg|1d z8ZB%2jq%v#>pzTRvRGj2{$cG{pFRiq043WUn$)autA9>depPANY zZ10wH0`w=X0c$2cjcJo>$BL&4!AB;(Gbjg`w+NmZ0^qsBZo{Vl%p*fTKm~t_t7?N~ z9@}X${{sU2*ma&+!p!D>AyfaiV`TqZ-#4xyi@Nxcsy{5;Y(%wPyPZ*hK8m_QO_iKu zqr8@um;83a`PiG$PlCW~Wdu`~$i493K~{Xn3;#n^4_HC|VT?-;c7)UO^QzQgu}6a^ zUeqdXQUyBXzb(`c?gaUNg+wi(^w%GP+?H5+Hx(IQiyZDKzdG7vx;*o|ShFVTu}ED} z4(1j#D9U{PWKR3Dlg{cUEu{VUJu`@%Dw#Pd*#Su=>Pp~}o$@^yqmobP(qy6TL7HJv z@cl5f)(z;z)rf7(_9R{kx?DK~nR)v`gYb1B`g^J|O@g{G!v2tG((W=ZT5zXd)X3E; zk0yuy0^da!q&Rf1880QYOEw;c9`aKUSf%Ebv}5ebupjjgJGp+jePZu=E(7_N48|vM zgcypFzk@TMb_?+-=r4Kj10vK42K7_Lh+gQW6{8fwLIJJ|<~Mk_C}7C;X)1Eu=mV~a z&||EmVT&w1S1 zAlsf3tdA!?JSo>fo-myF>`g4hR4Os|nN!WHC9iYFJ3pNm?Jaj64A?uC_O~BhQSLR>Sl?(j*g{mJh*m3lHdQ7(jozy^Z}@!Q{gG0Y zi2Kgk#b(2H#G+&!UHdNsssblnd~?tBSNgZgIFu;MVe0XcizCMM+8 z8HJJ=gdFMtqd=Sr#~pihKk@aoIx|0w*QNbSYNLWGT%PiP@7NpMH)<@?%hf~cnX{rc z6>aeo)@+zOu51+XV#qz2y&iw`vloA$Wj=i`>3dQWn z$N1x+Wt|JohDW|~H>~YUl3f%T8MR>G@apPiAhCq}Aa+RVL^AXS7+5G~OA(Bx+)y&i zJ@ESTXPWZ-Q56EN@^OPnmMiEmfOV=Jh*0f%X9QT% z!;U;|NxSXdB0B@8N<9d18g6}h?AWo~!)K%lKOoW}sGH<~Ri|=l=99AcKx8D8b^$I4ef7hKwm)mgG%vtHv3Ea1Mbw%J3aXaOQ@YB@Eef)>Z@3BF2eMiH#v`jgXt`*t0vZ#i3 zzr`x5M0#lnZuP*`BEpgmQwY`Yh~3RTnFBd#suvr|TAEA7Or{rRi4>c@QD}7{XSN47 z`5fy$ngRQ3V1LU$Q8B_yMgq(J6-Dt}F(WMy@xnLz2gC-~yTE75+*M(lyJ!x)KvMAj z#MLOQYwLyyvsn|CN7_oIB5$zMmk{HYVZb%pN*GJ%`6*7(^N)?0KRbi}%fDHj31j== zv^RL)PQsX+8%R1#e*f1;UN^2qt`h`N%IMp`W%CtWe+J^SY%ey0v?kW%4Ca~EmG>vh zU{BzSdoVe7M==9qK3=r)A;_I4vc5`mHYHo@PuO2jbNH+%a!WJ%iy&daoo9 zHd#IVVj{2qb2|9nSj^I{Km+UHw6?-Ws(4%#+{v| z?c&v8tpN1aznTi^hbznT_<`z)=FK$ zq~C+pkjz%dY8R+22bkjU({`_eSD;b<2*Um2fB%3?6x+(wh@LV0g_97EI@}vUv7Qbu z+pNf^yD2)1i-nt-Zw4o;{${NHom7W_ACN1gi&B`C%gb+ypdVY#j50OEHaeu&U=)gE zcRWHL`UDnvg(+5X9iZm7|GP2I|J&yY^>WPJL*DOpj^OU8zLk%hbXA# z7L@IgeX(-puUluc7@u_hcl6q@_L)>r+MI_lcFQHlt>7z4>(8Ww!JC+e(6j3^D!{YN zW&(vk*|epYUHw@3mMetGCY4TDf6F2%uIA% zQ`={>3l^>6a&GspTs#_;AC}eqO3=;N`;~8fjc#9vN7##J)%*=d2SiJz`@f!-s!||G zU(Mx5e(Uu3V9?LJZ)mn}Qag>>zNF))ExiBrI|mVXMMZ&K2rMypToWsakwuXS?A!>%H0M+043RRb;}4FK81+bmhz_dK#SZsncEJ-vIr_I5d={QP#QtyyO2Ce4yy&yxA_ zn2tQFLwvt%%2ITXy*txTCtciL^n%_MlCov|fGPb_=_Rk6gtlU8nveqT(*-O0e394D zO$bfdSHn{z>#yOqLBSs*JCGXcohtk4_Px^{%*|(9wA2{8^v=V5=E{aq_gkLekJ@|` zeg_>rqn_D-n(U#}iBqxyH6mh(2*~BWbnCCiLI(a57ur2Rjf$odb~VeXxc$<0IpK=) zaqldql-QpH(vkVORIHf8YiE5lvNfxWZ-qZJ>x}c<{(RS%57TY20hyO6Mu&>^idWv7 zm{V$3GfG$^dFCuyV=JCYd*9D|!g1H*LihWdf$vK09BPU8+;IP1V&t*X?2e?!(84df zKED=~u|qB2t)r&vb&j5SqR?89Byq2xp6m6_{HwK6*`k7-G1|?izGc0h>9ETw74H`d{6p3|7m1Du z(}OnbDqMWZ33+_CM>_Pr%+BJ)thC;WS=TyMQ}k3KCTU$0Y-iom7Kt6p@4aqLHKj~- ze4!|B^e+py1}!OQn6MYzE5!xN7udL{^tp6@Jod27`0eOLp6)vS+=3Zc>cCbJgzZ7Oi%`PLR!~Z28UX z{p0=L*)RTRpTEFg{~K-bXMx%Xa}f~PiHs7eB9PO+KNx}!PGN#XW+cfBHbv~uS3HCn z1WF1R@A_x=jz2#4FI&(0cL>A(L_Zm=q-F29C~~tH1e~C3i~_tUt{VQ>L^bmB){mk` z+U|Or0RKEt#g?d&ST?moPW?#>asdkH_;owv3#Qbi_w|iLyqzGxze8{qRDH4xK%n)X ztb|!@b&;vnQ3E^5Z#1ET)~J>y=;P63gAdFI7nOnE*F?K~1NIhzn`+KcIF z1)Bs;94&R-sO?V)rgD4`V5R-(i;UWV-!CWrYqcAG?HbbJ;c3K5B_YP@t%d}cv$jS6 zYv2MBm1OzRR(cGU2SaQ!!4i{EULdz1Z$y4$-C=11GC-E6hYRU5Jj@t_jF$kXOayTS z9(p!8;1gl-xIKJj0J-L~>IP16x%mTPp$(u%?bm2FsIDU$RMRqQ-$Tys z67EFwY>-B;#jVEk!QxKIj^3sC(UK&TS;TVYQVkvxm5$=n+HR0*f#ds~>#vEXz?XFF+KfZWQr#MFU-Arr|5I@fnIaQQco zGd&QTbPv-oCrMk`n8%C`Lf6hiw1181Y>tDU1A@JLofOiL35lJlLlF0_lD4G&DMs*hg*#l?vtrDT*M3!@V#+ z7H!?c!x$&`;RL=b47Slk*oWuh~cum-Lg_&sOv4gYh1_bNY%ZTcx*=9fue8~ z%V33*Mzl4+Uy0MFNz+f27SYsWk9r~IgBNkZ08f!;p+@Muw7b^CPh%~3G=k5_&{nGI zi~O*O1e~VOR_!S4p9ZY#7DjCH2G8ge6?itIXPHD2`z-Sa>R6f96SMifP3tcv2vBw7 z*H~_9h(U)=W8w4qX2@+~DY}8cCy$5Gue85@WTT7BirL3%C18%`A~?y{3ORk=r+hD) z18k~}^0&H_eK=oSSMa52GAg*O$R=v7xx{x=1?-GD}ozOg?3OFN2(UHTc7UA%C#K zw%G!tLBt$&hU%4WVSYi}Zph%vi8;#xGzL?7l&LIZr{sxVbXtN%dcaT|4Aum=+*6;> zHrBtycVhiuJGD%D*-+qq*^W{_#SZS$hxT6DJ)>rNdUF5Fypy?k1PE?#q)qbFJbp<& zxw&J^#ZUy92T-><=Xc-gZm5y*uQ!8`ZQMlnSjUP*U*Ic(@akCQ($rBZE7}9f@kUX?z4P{v%tr4Jl&Z1YXlTs31G@C ziD11GST`B6{iOdFG#maOpxM%PAa<6Dy$HGZQy2WNCjRe3NnmxzI!NogHe(g}q0|H=l4ZU#`9uZIB5STcn z@swccoKyD-DOR*INaSQ}Em{+-PdWu^mvu+@vkIPF;17TBa#fjSgN4S@Ah7EDQRJe+ zVx5P4fiHgsgviR^7w~U5oj_GxwPa+g^OJEyY!$D%Q-!8=b}_oO+#)X80(I%MC~DqF zcj9o)$(|;x3n1e~-=&qb^ymd_ zjyUA9NpCLOL3Y22;vuLy~b10r-1m zU^Z-BC3u095|wp817VZtFoW(o{B7M#e+H5L03`AN{vmP^Ro%2eaw9UH^}$G#U6crdOW-Td@{e)Lv3dLEB=DYs*hIc1mx+#Z5WTwje z!+lw5D=yNB>n1ZqKsB(f1~o~_FQ^33^L_hXh|q~T!-yU*h^&OK*Oa=K(_Fi>m08x0 zW}%Fz1r!b&#Um*Poy)A<`8BXKHM%Zk@o-*xhci>KhzC(nh~QM`%&5v@514BUPt%gU zNUA=`AGn327ryJ{&GQhH3XL7zw6Rvb|Kg+p(ez>D$LvtR)@sxk<6>ECTKEkgl7DFD~?moj&JOb~Q-Fhbg=? zf@840{{dM5@nsHRRNnT5ULtCo(BaOoE4mv`DR`=F#_)6T$8h5t9WcXt3mQXt7dasG z44{NUr)K|vXwJ0KzjCM+b}t&iD)sZ?(6B&Xct0)ilS_}+zkf(3>)Nz=UV+W3^z*2BYW`U+fRuf^kgX+tFIX;DzIMm`etN?df4suy|D*$ zcDi*FtTc|&B@m%zJ9{t1!EA%i%Xod7LWrSGf`)YMrE5^rx1}nR9CL%3*d!0QwHdI# zL4U&JeL-;0WnFL?&Mm}Ug z9)J1w4pL^Nl++x_S^l~ZGqyteTId?P%#YVxCRf_>srfljEql3C7Lzfc5fPDvCjEf$sv}%Or57LQ`FqJ3K>YykwKWtlnbAiOOJ=BP{qFg`)tA`JNwdncChG~lZ$Iiflb&4byhpx2@3=kVMfoL7KIXA0(>aNN*=M3&Q)?@jPvdlGPD4K5*C>I zM&YO*xzk)x(WiThxJ^88w;s?#8G;3`!1j4ckPN$cqC74QKS7^k))97$Q*jhM&Y(lI za}O35?p9N9eZKOi{m*@MbetW!zWaC;Kj4)d>CiDw)Pb~Y(Ag+jn7ZA`^sL_Z62sJ@kv?>5b`O zx*t|lsKb<|RZ|LVwx!f0(9z7Y;VQ{SzruHz#)a;Th3!Ed+}br{`EY;9@x95= zw^CUWPf{F(!hhr7zxLbn8c!%_ce-5LKef|3%PlzZLBl}3xHkuVfkD6@Luqz+OnE!K z>n?Rw@7KzRsPluLUT(hFVNs~m)31HkpXv3oE0`Y6i2ea_&Or>fq-=}E>8%}$(%AV? zB&lE{0|cprnv>iQoV@4D`)DV+ajC;wN&n%~5t>kye4;+oZ8*yL&A#@p+6 z#L?xfb}VT?WoedBSnrpTaG}pQ;Ak$QTy39F1XDNwOa=zqL9mZKf%tNb7p+~^aZZsi z8kHTy1E>D!5ETOyhmM0v_BufIu$l~t96WMm-ttoqVRG=3`$nK4hlNJZMY+rdG9QF1 zdf~z}*qGfKN>u2(zNu3G7Ul0)C|}7o!bM3&Azs*FPpgv;X6NeccBwDhaglKQ9LZsU zni=yjW9`lZd8L}y4?AzW4DoFy!&ei?xVnlxgXZSNPV*1wpKG>U_MRX7j;fg{HW|{c zk{7h{lpkR$e^6RpnBe{8EI&$G5Z8rcWA>uAwbSM7ksjH#a#d1n-=6B!LVQnbK9z_$ z=033%PVR?6!6cUy0Qy|Ndpu3pSuZC>hgLU`Z$yE0h^zAFvpxe!3}@ci6AA+!7F<5V zdE8wpK?my)RxQ@Xj=y@!+%&)KrV`+8XcOJ`aTM4pl;=L8aNmsgklqg{cXK&u)Ce;& z>-k}Kk8)|c;Eblo3z8s8=G~R)2i34S4t=Vn2ro#S$iXp|Whp-(m#LhT>g!Q9uD&d- zYA=>Hu9B!Wq%hLb;1YAg0m8TsOo!&-vp*n5bzNxRjR1L(f4{pLhW6cL)Ham48)ZQj ziIzys8XP>k)mm)+P%(mSz;kV6Po^?M)Tq1!n`OD44DBcUS3ez%2hQr(8&ICJ{A1OcW! z+eaz>0comTU@Ujdk#gbH@Zh99R6MN4b%5*mO`VvKEp9{*Ab3O5Pv@_tU!az~MjgG) zBo>>yJEy(8>cWmflVK5Jx|WjH!bjE#<1cR5v3^}9QK^Dc{)li4*&xT-xI^QF?y7lK z%GQ@o=yhCAvnm7adm2~2K$TjQq2DSxaL=X^4ivw1f#Wgy)jhFwXX1dfm-sb{H>(o( z4!0$qHrC*E;;L|V{kw9qr#FF(uK*>#aw}r!*OfbWi!eVSY~}4Mmi`#$sDtJ41~!5 z#jirU+n#+@LuvierLPHnzZ{~AW;3QW@-D!0DcH}qY(l$9KaaqN_ahbzR}xM$B0`zQ zyvjC!0r&IJ56JduAQ&F;t{pmyvR!-;pQ-{Z>^~i&YManHeQlm$tViL`Ynlaf2tSX2 z8z&5=&M5oSV1o2F5_ZkNjrn=#Pd9Cv2J4}=jvrO}-D6~KG#~imAwb{!(G!0Rr(dS_ zA9Lc5)#;BV_TOTkxbn+p=U}d=(fP8w=;x-2GTTzGfLhLqm5B~H-~QRvHB0dLidGRB z*B%x3L8LQg7?uJkds2J<8GPLUWZ))Forf~B@Xx!4S4|rLR|wPxyrc0adZg^sFgbNm z23r%C4G@NRe!l{#$;f%R#u%*WuK&ccw! zC&uYIx;lsxl3)7Z`k_}2C6@x)=7r?HezuIWYXlCblduY^VO!?BC$7e2eJ?0NGIt;G zXU62(3&habHl}wohV&M&3;^UbrQY_}m-)@#TR=PxJ_eLemfy@R<5b9#^e53XR?i3U zF_R-2V!qqV>5|jpe1;YoZ~tG#O#Y|7SMhhuvlX=n>T=wal_^LpfJOTvi?})|Zl{uu z0gbDUn%^T=|9}wu8||w<&iSLA{>R2Z4;euZeb#;Ims%ejz^jN$>GK1z^>n6~&pT}O zGT&#^&sQqzhB)A3UON42c;w$3gZy{KPygla{W_MXV1*pv=PqaKsrBfXsB68$V3@~+ z)J|d}fou>Ff+hP=_xSSZ=Hhtm419qCfjPPju?se$U6Z4G)#k8zodNo3d3RcZ{z+T6 zMw`PJUc)wa3|afrq9<)++p9OP1sEf_21<3vwTxj{zzBJvxWunmH!@`H=QhtjZ|<6oI#I`7TT7k*uej#wJR zOX7M#y-?Naz2zq4aAdY;Wyuu)#vnpHu6;D^Z`|qkF7j2H5JcBLtfIy!t5Rzxw2e4t zJ1a}MiSi-0)axvHG&%%BPs|~Lx>hIYytX5YRLQ#NDWVc8POg9tXaq3qA|EuIKb8i^ zKb59kOnH0qC!IvvWP#)2Ra%vwh8;jn{4tH3_#Y7WY>FNM2_H%(EuL8*3^>q@legfp zj7NYnl$ik-@d_p#KOi3v^ccP$5HBEM8%$)LjwQ@TjO*$E4D1(9Tk@7-@7ElFD0GqiF_Ix6DTSgTYq=KiphS<|0yShwaAf`izU%^q^lhV^)pBhz$;NQ0RoP z9pH+bwF*>!1Fi1)=kHn+KJMEo1Q6dRks}cs+*TM;H9$~=Tx#XHX9Q%CPMN30a=u=z zlqmJh2tT=ut0sLjA5jgwO(hL~f3M;bpAuzpvSE@vf$^zZr9$oO*PIp3^;qHi4=r*X zA0+SCdc#fB%bqc60qTE?dAe2Zv6)!7Qs}tz`Jn6wtQTZn?%?)U-;jJln%f$C_Rs@5 za*0$6B53M}QQ4`C^6vMA7jzbD0=D?UZ>q@mH4QW4mCh|{)%uK?9ET63leqefXxz1= zm(C*up^47-@Au|MH;k<`T@fzxZ*gN@hM)(h2t^syjBE31_oI}LxJI4#(SE9Nz|*6) z+ujxVwol6SSz;|lU13__*u2PzF|B=%gv#VO>5i0uCoaiJ9TsKZgjvVqxw#;i(^l7S zmL6L&rZu(4BVPT0&8l4@bWJtrUueZkek7Djd(TSMH@B-zI&YSA_T)=n-*jLsP$6JJ?PNLfT3 zG8yrMBivj2{6AU2o~7lTJ=k*mP{}bcj^j|rBMkTW2zVXmszs|>A_aFv5#ZYS*3~Ay0Uh!iMeFJ zKKmvYTv?pzmg0B&>ND(oF!nNNw@}o{t{Y3nx1az_zM1B6!sf8=dWINIaF&$4nmRn1zz|xh=SFBL^OIO=f00^vBwWN zRmk%E8`(0O99!HvBOa`CdpA_KXS$J~($_+A~DGEEq=k(rCn2L{EA@XV#Z zrP#mBExUgW{$%H#y=L~NM&e_!$8iZv4Rjw~h^oz1i^sa}?^?EgZUj|kSx!m#OoBU9 z1oy#~zUErUhiaUl>Q|~Uzh;?V&ZE7PT|7H#GS@f)STVd84$s?kh+20atE}}rmO|mJ zNhsV*;Ow`hE!qI^--dx_!cHsISScFg&%1d5P7UU1p#bU5Z4<{)&q{vkAhDxtkNwNs zwM^6aa2>G?fFc(HOTO|nQ>2mOqL6}czsa2;Fir`TR;stneTonEyDll;q3|?#X zO(lHopqsw4KYq+RthQukQA3J#Lo%;xIqVEyRNs}N=SAe~;E!!Zg9aBjR&kXP0r32_ zFcf>1Hhi_Vc}&J_ykQ+zj%NYn!W@+0VIh;iuZ|nikB}rkZ@qTFV}lr%F%Kqu^jCJhVv-q`{dx8Uf`rAG<)D0o0-Y zT#iZFGPUT*MEs-kdm9gXCbJ)Q{;b!L?@@ad->n=bF_2!`IX5Dsc(tDuAX_;f}a1R0(pQg~eGV z)<>VuyP9Rt`J#tgBEqI|fIzJ3Aaba)z@Bqpu=#GybgO6+xHS(i!Djm-|1N44tvi-92m>| z@d{-e4|)PHD6|uqr<`|>dDpp_YAB3WtNJtJRTpueG2h}=H;C#y*=AGB1FzpBWJZ3Wl_x z@qi2*20?o2Gqo(0n0VfG;)3ez3=ke`-CVvo0TbM|3bUv-vR~rTx7Y@)M*gL6u&QhwpvI+YfQF(iP>B}$TvSz);(HL}gSv%uG%MYvGdu@W%}^B^Q|Yi?O6Hcy znUZPF>SG%(uMEzzgjM&!=H!L!_w=;JZvw#pH|q(zjd--mbUxj4l2~klWwzpB0AjHn zt_Uj}NJ(LbJ@NiMyMljHTq<*VlbEXs;|8ack3ZQ(xZ2&WuJ|EhYa4q3vys3fMuyflAA0a2Jykv>Hf0?lGmj%4|&bk5;T_eRyvAY4F8T+)!24^hA4^2Su3mZ`(dw_#G%2{HM;KZ=}R*w%imUM$GI`NGF!a={;^CJ8pLo9XAH))XCuy@~SyvqUmOLy+0BwsAYxG5%dp zD}9?EoKNGV0wyF$S~R22oWGKYq$K}<)OJ!%v|u`{ir+$(VSd=fYn%1y5V8n7S`b(% zwqTR{In0%YBnE~dl45Z@E{-pk{yqz%zeO<4tG~cSO+Hj2P zeLmv?z}MJGev0-cDmn3Ls2i(Kp6}~kf2Mc?RN-?Ta7fU@ayGiZJ$rRq5dq_cJ0OJUXS&i#getm1<)X*yRwm2>LJD4)z>%HvN0dMfKA18K z?Sv{QDK$J^E-!ubRnkbVGRS_AE~<9YTAR%b4twc5%1`YkDt>%+`_?I?OV?z+8M)1X zS?|=b%B1>=HSPJ%baAzcA0kCq@lYDhx@7j9CeXgflDAsv?NxobzF))PTL=DtXM|3D z&q`w1UHIh77s=4?VMh)-!$-SUWwC3*$`e5y!Z88Eh#&$x#)-6-u3Q}U{{2K}s&Ttk zGx{mB^#`QLBLFW~&sCVC{=g&blhy$P_RcHD#HQjR^F$BR12><2npFP$3tv6vO75qz zHkL;hAZ~h$FQTeSa;3m+Xps$}rc>O`P<2w8FmEjn>t&aicoK-C z{VHWo!9$(Xiu(Y;bmQizOX%PpGF#bY_bl^E0-2^PxI=)W_sAc0DsM?u*|uqopE~&t zb(x|aDA>#_@;H7==bBsaq*!UJ#X?Ntsm*#GE_&eeF{tC63tS&xXCX#LO2eS5qhx_V zd82OWRBgAgnqc~gYqxJ)847=VHQUl`wZ%|OK=Isq%eb>0>xN7l9i&2s*B+TuQS;zd z58KZqM}e zM|7x*rt4--uW>p#hP;feMRC#2feoAYd}--db(OuvXJdFJxTRPZrJ=WMei-s_W~~FIS%>CAymJL68kv1H8lD>fz0v&Lz+d>;?PWcI(qD zI@(9B6L*M8ErYr1UCs-fLHYWYY$u$-2aN$bm$@PgFQ zS+iHoh49J5k&2=r++6%9>`7^quA8q$y7Tt++m0^*0d7gFaqW3TRTSO}$SFEvp(y9P zr-Sn&YEG9z0~=XYmd{1bRkF)y-Q$8NZn`@(ybsxC#c34O_xPy+fe*~0@qG+PsX32cpv!~q6KVQM1g-)?o`3g0$=p%@ z8ws2L53UK7WMtrF^vQ0OXH6ao_BmfLqY5K7Clj(mOY7xFW$b`)Gx|Bofa+mQF|W?E z7YdfGo4(>aSBoMHBd8MkPs`PEfOn8n$?>0Bs*{PXn2NO8f0BQT+*{Z1t`so%IjYfLW2e2YMQ_D?A_50{ZOFE|b-N{shuq)s zy-i2yOgi>PBXCtEnFrBdNx~?@;q}Ax)BA@$+))e3s#7Dr$5blxG>-~T3CDB3Bz_|l z%KOn}-_Q@0$97zJDu{aRu~l#iAp~|&rG}7^ZRz3sx77{U0WH2#4=0LXsTCCUNC*4c z*7VLNlK}~b$C3|~x%3$i9m$~hYhO5AeYAcr2-Ebi4_Rf9_0$4a`0gm~p;dSLX@F48 zBP^!j8#}|DS+G1g3ut*_ElrF{#8E)dzpff0M`>Mv=lH7qhctX$feo-)_p7XfGua@( zz<&kZywt`z^iRjBKOi(r|2eco-C5Q`D_@|#MgYlONg6zTlMw^ZaVr`^9FPfN`2C*7 z!}s@i;|ut68bY_c8+w34!V6TZ?#zrxvD0@Nu&1?uPW#sUu(!OADU>)Y&)BJeA>Arj z;AQrZb}pA)Gr#O0sIy;{6;#E;g6Q7*VVFPWB14lAYDgGPT-Jkbq%*Is%hPVt&vl?h zQMxpAykawiYl*eqf!4Zui}8WZGemgUYZd96SJ_r0VbNo%*-2F9sV5XirYH-usbcS zPt>4@M5nbdh`8L|cfQDaA@(PH_*~DzOtm}~O<~#cWl3`L3F{cblZaukWH0pg4HpHZ zxVhw<70d4P#9`HaKwlhhrz?EqQeo?WmGXgRxu;|fm}M|kX&2^t?Onny4OP(K>CfX# z62i)xgeDz6sH~e(%vU!kxY`p5eHX)#lFx=wd971c_E10OQPd^ZiSt=jwW`Jk*Ih{k z5OfJ$n(?+=i9#@lC7v<3q`mWsO$H;!7{=jpYS?Am3SO=z#eydn3VQ&?eAO{ zQ*~I=@BLPUI$XiZ^eh5trC;DZB&Kd2NGin4Ao9XMR@npW$`_Ln!fYIFY7j;p7TXA| z+J26ntL}~?+E8c6wAsQ`n!~=pI<%|iekcBVOw}^g7qAyIF`Ly(<@Ezd%R&L5{w_;9 zHJW)ov2{%tRN9KDcUeCn1)rJ4oMfOW?Dg^ii-3a9YxcOr2r_s|mNGedEXzIz{nfzY zdT}i81BFjxAanT}&L6X=laJUKBYZ9m9%pi48YRABeoA*vOR;$D+b}M#GGWKzyzlU_ zY#RLY$YGGh>_HEfLwEoR)_XJRStf@ih%rPPu){%HfN(>E4=xM$bw{57bOqM}Y&r40 zz|||=NMJxg_`(8Y!9d7xPy-O-0_xiBuy=fWgy(Kf=*v}H44NOza>XN#E8ARn>E!t# zACsxWM&)wP9j`=LT|Th>5?kk|;Z1Ix6(AQ?=hn-Z){9rI*k~f#l(F9yD!q3$$`w2o zj?0*@^&sJf^}>}zo%mR`1*F=W?R(p1zuE(C_7+~AQ4GKRN_R9P_(tdb?M8!F;ET-) zr)K85b0fV$UJq5yMoZWm3o1rNta?#l*(hGBbdH%rdtUy7`bWOmOTwS-y-BAT=^hGA zorZ2GY$Jdtcff`0!S2KhP1}3jd%LFa@nNyI$rx_7D7k6WrT19xgtEl8QZLio$fh>- zIeq`Ov%`rhh7ZT(7jrINS6*qKb;^l*cF1tU=wBnJOgG`86qjK^+9d7b5zJ$gv~!pT z8F<;|W$gNn^UUwK3jvIazEWJ?QNSla%eVk@SVcuuu+*DPo&e-u?1zP1KXHK#)k-r$x0}GX zj!NwP22z@ueUF41`g0eIllH)E8KNVLk(qTk<_bvd0j?gKd}rq)r#Eo25E<8dcD1lm zL&bjXj`PprN-el0%)I7KFerLdeh^U=C@i$hp-DJxJ~m*#NU{RWD2c!y5#(Q&Wb zeY9JC#|eFIs}Y@eWzT-<*|Q$8+s79JACtRQY-a^&(tY$}QrjpL1 zR%qMkg5<8%3Xp#*5HcT$)7kA5^Yk>Z7IIM@!@6pobIJH8hXS1pwtoj@LPy!FeU(Pp zALEq!sG{}>ry^r5R!0aTXfaRrI>bk4tybsZbCkXvA)*pzB9mU4%vRCQk~5El2)lCy^H4)M7Q;4etj~AkdZY8VA(M7Q9)`~6y>J<6$yxKc4?P7m zWo7x+A`(YJD}b@_ zJx;e!SDrVBwSl)rZkMb{ad%D1;jG9PXCk z>WZdJC(@kI#%3kRM|Y@Y1|1`%0?{L08Ya7!`TN~C7(KW zA*ceOpn&Xn1YM04KuSyN@W~&L?sULTK~Sri=y`bSFd_x$g4sK<4u@y0a+w?GdR6Ql z0s98kIf4~f5WJp%zpn}70n(Qr)UAI&;2y9X0Hi&-L9I_IFBUCsoWO%!>FIYeoU!d^ zCIE~)AOZ}wmr-OvDS~brj!o+RBn&T#bnp=#1XbXPeug}u>%UOYhVIouE z2LxLdu|zfec~$^83xlot<+Kx1+2Cn>IWTlp*dtf$oF`zmYr=M6sz0DM?=hO`esH^u6>$SPK(}goE5XJr0)^6Un$gJ zo5|ChK3%eN#G5Z}r{2j#tkha)X=1!~e?A3E7v0?)-MOOtoCO~5RCm~^{{1fFA*4+_ zdr!*P=sZWZkr>wY?1?GZ7dzJB1+Zn;1ft->jmlS^?oDtFmBMWYH)KJToWt^DbR8Mm zZSI%i5iZNi- z)ogFQ((7+$3-s6ztRvxi=sT^(W^%=Ci9F){i41fN2ZF$q!#6tn_UVXO%IFyD%xZdMp);A$qJVVq*?u zrY=BxFif4W0EQdvGyaK7Il5qpI16kWvASwzNyIqfOwl0R?04z}e@XfOKkU5;Jd}Ui z|2_61X_F9BQFf9o>nI^4lATPYtRb=+GbAe8SVEDd>{%w+*RikJW6Hi|NirkGFiZch z-?ja&=f3XydEMLpzMkj)yud0Ch~^af8*=CSfw z)534H=@~Qc4V#(=Cjnk%z4Z1UVu*W+mHNLynM#~|{hTB)! zq4_@$45R-~Y?B#Ky;)e@jUF_KlCo-(3mSj^II38WBy4`et;l<}CQZ`5jCoL#bIdpR zFS&?6e*bgK1^>65hrj3nb`+tRz$~_l{SKUDw5j|My6%Z>MSI1QkdI#({Pqlopsu|C zXP4^!pB$9@@7*@gd`dqz6cFDLmr{-Cz>sAV)>WU}LzG-*Bp>6n{ z*{L@u8KxtR-3J3Tkp~f12@Isn_OXVnzI#XeK2~pL9K2iTNW9?06>D{+I?i+~lhdZ& zfar$5HYG#Ke>w;V+>;(CkV6?4)JCS=m#c*NvUV{sSya_1>`k2z`Yo6jbK7!=1Lq zsXjBrVx`xGwzs>z-!E%BL1>}KBfeuC&-j^!-CRCjJ;Ky=GtK?hErbDxSqzdYl72xr z`a-?r$*~)eY0hia0vqGUAip7F0MU#C)^`~kpnU&tSSDty|4$}(>)V#tV?a4E7XTI5 z9Z?b52JR#0<}b*15RE;n37fKnP~VpSRbz0-65P!faMVFcz5}F~VV;PZwfa`YO>IGm zuCKlzVO^7;2fZ@pF9w(F7_!W8L;~*9X`~jF1^Zd(G)0&TB_w`ex#aN+()ZqhVHjo4 z9g1yRV>OzU?@DbYhm*yKW7Og-6#KZZfp*QAnJWc?v6baW|K;8C_U9A8re5 zBLnoSNzIkw#aH=EMXQLBRiRL--FZ`{IUNL&gD4{Q$Hd;M54{bYI`l{#8Y+VjCaBik zAJ?rq<+a292*-0U+wOa*xwV#A@p1}9Y?sR|jGq+|HhB8f)(+DneqrisB$SlaXMR-U zF}I*mEfHi%@%!>`K0Y6~BK8A5_LIntC3#7-SI#b5&hi!bcz}voFXMBXZr-I%tumiB$#NB&(XGyQy6MRCgv)#5 zW%lzBX1J|7bg)bz#woF6+VXsWqn2^>rMC?()KDbI`Zwe7Ockku4G)O!Yj+JYFxi5< z7Efo@+5b-TaZh4VffAvVw5R)}4h=@}qZSHD5wr%&I$Ax$@8lo{STymwN8RUd@k@W2 z5XKB+Bj9C^5qu;z=2yAbxy<6m9h#Q1krpJBsSArm8TL|QWz0p+VTvy4O&;PZHq1Ha zezfeEKriGGQjl;Kz-(I)Q3f|&<<`8al#woM)3OaU>KI@pVuE3%9M2nMoX=~rXAPKF z12LyE+%!MvKptB!)A*u!BUVei#1J1(Eb)J$+qp7o##((|m!-JHa7|UZ7y0=c{5|@K z9J`#0(Wvkj?WsmgI`Sg07rLpBr?%(6=^V1olN7LoPzKsKYv#IGS%gSBHR7zmOLUn)#qt0YHTK-H$A=BDPLHmJpCSOM` zrz3J(Rjm0JdF)wH z(;8fQv8LCyQBBn9xqZX1WZkHu-F?BWVw<^9XA4$+UagkW1BQ&|a^n;j2{s3R&h>a& zx8wzSXSvfGr%jfp@TSabxAMB2Q`vFPXiI4JI-!7T1h3RI8NPo z4Kj_VIliAJaJr}1WWpZeB<2miD0issFAYekWlrGwa2hii@9Z?a(H5v=qPKov+47^r z!8dnK65gv^2vLXjd9@XeK9arJI(Rjg$@+G9I93CYZRsCE(T~>*B4qX4j~ShO8RNNy z=|7XL!%_tCv|%AsWX`}3S$<0nY-tfR5|bZ}400y65H(0^&IC-O_q(&qKPFjT#dlrA zG>?&aY5V61u5L`E6;F4`BLqSvAUk$j9NW(7o%sdH9PgRBx@~L>F@YXIb(Z?kW@#q= zd4JC6aDco~3+nfZp#k=PZ(VsZ-49v53qsV4AodaKh@HfAoh7#b#Vsrz#N>`S>>51f zc5{t8H$M0hL~GIvwxYIj>^=yRLTAy^*g0D$e4W*vA})2{7i8cvjJ#~fJnGPU7ul!^JlxD|vz*gKqSCjjJSuX%-ggUlJGBez#Kg94Bw0@>f zdlG5L5>6XWuV^8e>eQ*uMg-ZP%9M3^ZMJCk455RXi!ut-Fs$R;ug*^BE1B==;{2W3~COfJuH^s#JP@J@VKwteKJxWfhgzz(t)3Mgnwos_G< z%$cVRoql@wVh3;rdultlzSxm;|KZL`#W~ne<#*Z%-^7jaezZ>27u?HUyU^)S>;MOJQCvYL}CETt3-P30Mqi2uf63 zx#y;J0<$b+Nn*!?JXh#Q9n6(Gc-RV z9K3k%ZlNNh=`ToR9b2IZJnTi^n0`=PJ2Mzw_9Lh;_3j9Xup|Sdbkk{Tmc7_9r zTdjYu>G*q{RwQ6;6k!YA@zhul+h#HRd_Wb$x6NdGd4b zY}An%J}pgAOZgSJ9>t#&>xS>2?aTKOoHFT*e7UhFmke){g2EA6gwP*FC;=UTS{LyE zyP)%Oz257smRZ}|!?HLT`9(X$!d;QU%RjH?^CWp+WK|wucxZ54-==b4i?_lgta&?GZHFG>>H$=0E; z-e)E^Smlzf_>tv@ZV$EAfD#uifZJ03bg(Jkv&9AsH5{CBXKa{%Ycl^EkKSx+krR&^ z=d=C$*Fe^Yky?%49s{B@Y^_7B9%}~>a}7+`VN9KJo(k9D`tncqjQs2ne~vYwA`5`F zFO~-0HXhgg!h2o&rp+xQedUR(HrkRM1qwHB4E$%5ZT}*Wr593`ekop^d3YHmdaG#J zxxEffzDd$(vYe>7MWx)Z7BVze z*xTOOTdZHtVsGEbiuxe7QEJ!_Z&a7OXg4WdRE@4KP+EE>MbXLA3COpupX}L+(}=eK z6y@;r`}M|J%yS^BB_Vh8Fj?Zon+0rezWSuHXJ z=H*xBw>N<_8Zd`5Xz`=45gr-GsN0sNT{fJL&IwXsZRiBIp(o`wKNBhP0miUFp zSq=^~zo{l-H2x&#Tx>Omi;}2gn9(=-@%1rF=;Ju|v6{~LpS<)9{9ubO5qBaC@Ynp$ zz8-f)cQ-=&f?zTY5~O!1F5hkQhQynq!VT zz_5b-Y&I)6rA$h)&)>0{DNXXxrN$Ckv0rF(4bgP(CDJB1ww)J1jz@^2iHL`WSQ*tn zU97$J?W6Q)!#h&YcpX>aZeZ$fqjP9m#|4Bg;V6I3T37Mqn&}AWhLMT}U|!v<6Tltw zugTe(kht+jX}Y+lu+^{5+x;!D4e=H1Rud`#VzU=A1!We5v*-{{y(k021l#b7+w4p9 zi1moC(@lLfR26~g&a0BO&TIe>AZdyqoI`cI7lUzSVJwB!|*vL)$m7n+790c zFBS8`MCH6%icQnDw^J0K)EA5^8&Ae=_4*YKj&jPAYkQik^f# z$2M2|P&-aa9-Z!U5uGWPY389$57Ou;YJ>A|QjAq;^wZl#VI+&0S6sHsF(3ZaV_g}O zeDmOHV)xvIdY>yTyC#>jbr^5s)^j6Zb})$VyV0uxgSRXF=y;o1DO=P+?Cn6k#tnl4 zlIvvg8*;lKcLqnOn1&`!N(Gju5n!v_E?=MgH=Vmt0WF8sZP{xRplR|#4^NuL8$rj8%p2mg!(@Bn9*m#SG@UVI>4TRot%(&P9uV{g$a+y{ z;oI(E7thq4i3B&iQH!H$SL^vMkzbG;Cty!!fuEj5mJ<}LLkxzB1XDgDWb*pNw%|lZ z>SGFQhW+5w&sO=a0Iv%_ykZVEyrA1rd_z$ousjpo@0>kx2=zSkOt}~4@s!xW>`%L) zC9h3mIdWGnMF4*;35Z@Ela8G@Rd5rbh!0%i;*q@|sl<7Vj)K3nlH@IJx;W|pMpju{eDiS zKUoU@Nq2d5CY(G?+aChL+Dz*}Onx_2Xwo1Xm|*NZ2eIa3oD-3pgRWjyE2bB3>vy=f zJ8kO4nNtV5FaRQ-$(k->OyeD%ACPq}lxrLpHYF|wsrgV#VGmNx=H%p)&V!hd1qU7e00&AW2TnkfAv&yn&h4)o_HQ@utbm>ki*!r{T5GOF*ktD1+A7pp09iEI+Vd z$vj;MJ&x>4yMT|Lf0Wa63jRaOc^e4GI#gI#ISbFh?{ixIY^0qS3FaEJzPVJq0M5ed z+dXJWi8xR~EyM*7EPh@8WSOIiJMZtRyUerjb?UN$a)idQ~+ovMK?u?XH5 zA}SK)AqRLMpYX8gayZ0!bhkLc18;q$kx~@ohxt7Hxrg|=g7jt9SKQ6sWaNw?)O~>f z59b6GD5VB%;5ig_sPJ5QLLBe&SV71D%er{%cRRk?o6cfQ$TO9|hGDaZBzQ!&L`<4R zC3!T1u~=`O3%H~|qE#tu6j%Hn?A>#3z7NixYYAuxjZiY?<>m8IJG@@#-QD<|U((%AglCGhw1!Bupv4RZC$u@$u@dP z|7{R-R8eLH9t-qcnb`@f!z?gqnikgDil!>WP&_5JkxBeP(w;|iQ`GgMl(niFDvTZu z=BolRwsJBI(Tapy&THhC2y`M2#oL)=8 zKkZ19p=P+2tHLy@YRV)Vte@pFu%`jWlYS7v7f^)k#xMIkd@!O}F;KPhVy!XbAOkUt z`nK-$JV%&M#qmnRSBRVwjQ(alARn%ucmz~m9I*A^<`7u5kt(2p7da(WtlV>Xt#V4^ zy!q{{TqnSFhzv%yqCON*I7*X@KJf@^zR>0(KKe~=C*7HIpE-)f2W5tgV*EmWyx+nUfe@bgJ@jm7gi?Z02qUfuT%54G2K->Z zi*T)M?rxBWt!jbJTLH6DU}_tt#!@nP`$i2Dq;giB?@2m>>R6Wh8qhh33T#ZxX@yTX zy0KC)C(IYU_UWT)XZ5{uwEB-$k5}vPIfL-v&P~&)Y42YU37@SAGhj&^h^FmNC)jqI z!6i(d>gQ6NQQDxRzlYF>eH0KNsK0Vt9fRXovwBUp7{fxJJeJUc>H(vwuN)L*j&{`M zp(@HR$nGcL$N}Z{X{d6`2i^o`-?oWQ0NapBzKmKQU|D<`1_M>A565Ejk0-jB_Btp~ zjtVDsPh1NjzZ=-$5ZzYj^24tiMjfuc)zen|~=U_H7*;5j@~Pj!fCKJes>hi}d?UhZ&ka zK#{;FJrTH><9NgBBewM&lNbC!e9uNxvu7Iw@deDEi7X`d*^6^k&gNq)>VogrZMJ5o z_c=5it~3`fBL_w_Dl<8j9#?riWXKrTS@`AZC~RAs6SxUSFp(_$&1d}d`)6$`Z4Ai9 zzINnxD7&k;XX}OQg9&3;T?pkiKD}kqdCsg=xrd!zgKzwr_lMVJgK+%%boAwm<7?X| z?o=TOHApWkS(4V*AcN;>>)aA6;64-5+jTd~2Q#ubDyd)eZYNhX2-+P4I|ZAa{slRS zT0RZZnFu1HK`Jy$z2V^)^tc{JEoB z4XXnmdi_I1F%O|cO1*!-NR6DAYxwax3K;fM=fqZf)(=jO3+4voGi(6Gg~;g=)blzE zg77zjVBv@B`%l@w0f#;carmV*f`8!a%>-#lj@euHAHnu{IwGX-l~;#^KvdE-JXfu` z#_2acC2j(_<2-{UiFkFRnk7N>h%<1_NxkaU1S^S`#(H+fjgvHpNafH^B%blxXc8P? z7NQ&p^NdsOY}yHy+ZA%BRny9M*1$ZrkrXh8rT2|6xrNe8F{luH@RJS-AH`mFMLK2) z#9Y^eXznEutqnT#eGKuKmVhEX9ig?n+OVneaTAUs%LLzejjG{AZk@wPn}CuY2ae67 z^>3!FExr}EJES-+WgEOhHC=Oydhz&EWoq=kBGj{VynY1c5NULGcj)VE*y3ffwz{*P zI=E|fvzxPU{I)8&w&??fv@I~GvT{uPRy58>IHah%tS)oDH}4z>)yXZ z>x3A4g4)1;hcn)P=Vy5>4Jc1QOW|pjr9sO^CM@p$-SP{%vtUP|EFinSD?<6i`I!Pn zxiO_w=jh^&Sp!OF`l|KC>1{v$DKM245LL@9>e`NyPu(*lCV7>R$0tA%?=I_a1^oYH znm-;T-yO>Zok!4gin4Tzu(HZlF*kb3+0oF}DeIvt;}<>y{e;`d)&{!tIr|PVsuNL0 zu2B4^o2wrStcPXl~91{;{Dn6d@ZBdObNrs^tSidaa5FHmB{W3ET%D9@aWw~o#=Qy6@l6c@x z?c-E0Sm*6)UGQgJ{sp;f@EK=F3Vo+HhdR;>g#9;qLSJ|o*icso8vwl6B!36L9t{kg ztkV02A51^|sd$39VHGr&;VOq`Z;48X>DE1-6294UpGnKN4QRbkv`M@K(Vn>qjLR&v z>{3qVoRpNZyRkvjG#dVs;1&DBdCLiQ*!PS`WRD8H-9m-7oHW5Fggp5tUXKDc6sk&C zr6DD;GCERC>FAqU-L8@ye3#^Q`*VDsw(y@yeKUM6deP(?7jwCpUPTkNd9w+`{A(vDrppP$^LNI$dVb87PE#O7KA%&VNu6_L5??Ye;&&U&M8GZKLQ-}l%vSU zEa{wqzechC9^mzo>p_5?DNcC~=#@-&4zSFvH$rzV0zWkj2yqRpZnHIRJX3+)T^lq; ziGr;%{DKHEPHz#xAcqe*Nis`k`?By0LcdowOu9)NqM{M*gvvRiRUWqVk`2~F?r58O z=k_)1%quko`EPc$FLkEfbgA}XyH;Lf=-|y~$L@|31Mw+N#Wb&e$_B%^y5A-)$2eNxoJF5jxnfs_rQ182!62v| z3@N^&c~Jdemr_N+T_KHdnqcedpNALt!@>(Gdi&D#>a1Rtyw2wn>!zt9N0eBaTn^5r z7)I+~GNanejt0TbH%KB|Ei0Qj*t_02)wD>q^(Xd+z?Dc+O}mw76;0;&?B}_Da63YL z@T$$$e1ACn5bA*xe~KP+HLWL3zrk|h+GKOG zQBe>JDjIfQSnVLmatc^L$LYw4nECXt!u78%YQ(REdk%BU=go43+I-Gi{%uB6tUb>a zRaD0SZe^3&s@S{UK`+GJ`;^V@A2}dXJ@!oD9&U+)ax8)v9bsC@e|SjJUFp1zN(J}A zT#%Xo;eLB10W+5{Ax|!L6{@=b%oYR%jmoI8G^rKueD>0b7B=>#n-?(|d(9lS)L~SC zgt4d)5qGOsXXGu+=Ns&+ni5rXG zmNgGe_{ot?h`dreGqBS(aPl>4ixn8V7uUv!-sQ>qJcbQkq8CBo8GJtR=^a(d`su2T zNsE44c=ec9?br%Y zSFsEFNz^d{C>>*=gysO7y85RyR!tCdeSrOeLxU1R*n)(@UaTSzN_hfZb|x&*SOKGZ z7Kk&|qAaOlXxe3tO*s-k#+jhtqHl8sLm+Gs1W4g?0Gl1@_h9a8yJI#`w2dBZ@TWcn z^Wm4k9Z;H5uxv{1SpBEz;{Q8`4}Yxt9}MN#w@>(-2^VHaOVLczdDZdg8m%pTs>V9` zR>x><;)S=SD*Nxfw2=R?rI`NRg1Y?Mn=T!rQ5~b{I8in-2ixT9exf*S6Lq(L{g(Xv zs@D9fHcpg<$j?>#FTQ~;Hs<)QBIW%E_PLZiNR5Eidyb=vc+o{r>pbXZUz_iXs1p0_ zO!9G*|4Jp)QUPkIR&5A-kFWpo5eyRg*R{chwuW8R%D^MgBLj73HlXy?-X5XBuC3-w z1Ui7n?pJkMOY^q9%-;H;U_&_ls@*~G*y;|ZJ`MfRXTP_87}!v8iu+b3c&sKc{PBPM zaX$XIuKsj?c&i?{v;A&DN%VkQ(Szh`@uP3e-+h=^&V4cKxLJMRxVzQ7L$863rbqYn z>l^k3`?QkFOdf{|=ZpVq2&BjWb-g*|`qOpfF;7wDG%*4%P3zn)PWcwsUu1p*7r97> zHeqR{@a=EdIWPmOF_8V&*0%RLcD8?qO=z@Uq&85j7oqqIa{AsbE?dDZ+mD2P4!x8J zYVdZS1K%H!Z=wGr6W9M}&+~`P{J+#~`%l36^nZN!zwdbdVKe_$2g|- ze!N58H;J2O=$0Cj_FU=K8>a=A!omET#(IYkd>W6Y2t3rQK-@0&!*qXNnre7VEk_C{ zpc|x}WHhCn9Xey`p%TqVm9B!-7`=`K^e`jz+e5}K$Ciqi+ zw3ap0)ubo;3&k6{?VR?R{!xw;0ZPJ@W4yGhx3k<&;;NReJM+wvdw=->-W?Tj_lP1{GbVfno#Ac0BkXvj!*FlLda--Fc?=j?1 zG&z3o zxYbO=C|+;McoH6rE!C>qg|@TPwH%7G7OhmDs2K>BI@;kCO*&OoeOIds2Sn%&j0BQ7R7M)p_NZ00S~`S7>Wyu@W(CW_x`-o9kY z&B+cf6m{rpIR0k;~7Jr+T&-iN*?2YF7119ZDL= z=UUq2Q){!vZ8+wlPzn0h+b!Io{lO3J@f~oc6ubckYkMrPB71+>B4+}}i90t3)lVKT z!80`Nf+$>>d82|?vsv0|`gJ5J)A%a^-# zM2@MdPw8&`nBTP_8P8tb#78>Kk>T-V$$3X*W|n%0aaK;RpR?O(owJ5572n*5p|QUp ziL0PQ+mI!JHj>62(w2Te>UPzoGaZKoV7e4>!u{kGn&79Rm^*i0=3%Dc5&WkHBRzHftB zz|!-htx4YGneqogJxz;POH#F~_N~NZxUT0?Gb9(YKlRWr)1$ z>KDHZCRM*hAnXQN8OjcApZUPP?0dTA?%V8se+bEn#;S;C9k&_0M^KG=`zGkPn&lRz z+s-KK@p(;@29{Gr%5B~N;>2@p`NY*Ik~{oKIv<4#z?cHxl8xct<~`H_+SV^tNrKI% zV4uYNZ3c$%2U*U-A#bSrIvTJr~xWsu)1?>6-Q4sQl6+hLHDWcdOj$kz>a__9E3 z5qqnj63_(nZ935;At6fi5DE(~UZSbafZUoPqbCVhkQULEV_X1=SIvs3pPy%N^ubA@ zTy2SW>@!z>ax$*PFznzKWA6gYY7@OyKd&(-6<$faQ_1GHPwuA|gmVq`0o~lCCWJ5^ z{yuhQba|$vUE7kNO9e^s$8d^JSuGxsCUhTO_NG&Hnc;^43&{r8IK^E)B#{vlz@s!VtCKCpbm z)gZ^>#nbuA%lV9d?j+;=A?~p$1xL4J$AsG;{#?z|k9V+gD!Y_rXxEJ}Vtw_qItUOq_8Dlut@Qj2a2l`GoM;w{5?(#30 z&+l_!*m^Xx-3Vi+a1Gj)4;9(kn?Tj3ww65n5&oplB$0{7BvRb?A64~dNCV=pyQuY(V3wm#{*k~E+`w%2(Fqm4J#$MN$(m~7Huzi%>d$F&UC;~+PS6~_!FfBY_dR&=k2=ra^y*8o~*IHY%o zkq*@qhJS}_b1s=<22iAP_RU=Jts1Sk8*Y^kTm%hSK*qEsl^Hk$s13<3sG|?)7Dd;K z(A2=u6*C99EpM6$)~oDV6Z6scd<6_0*bTu|=+=+Ds}e#JM|E8#a8Lv5JxoR^;KOmT zbI@I~|3V(a0FoZnWr<&+B8reZBzu}>s>0rtj%n=w*Hm`%=(i-!s!7*q@^8|$Q|t%` zi>djIz%GRRixzf)ed|pfkpxH z1J$I8vbS<$_O*c?nI035y9HTYl+q<@>0G)Pb;aQ#tJ2*P3h$eaS+{*(9Hq~-h8}a_ zQ!-8cO|V>8@Nh`Dg@jAFm`+jgtWN9zrcY)pMHdUK*`c8H63SpUI~{d))MaL4rVc^L zgxn**p)wVbb9#?F0B2+j}hG_uZ!% zw?Ho+X6d!#GM))f!56n7w;&$a<8^mf9@C)TwvX}^-?>{i|JrP(vJK8T>08^uQ?&Lm z?AnC<1d$tqLy?5-!=FAup14;6SCl7A3xrc_>Z-^lpN9EANV|O;V3Z}@@mnw-)RQ?0 zQFTmMNiR(gy-P@H>t)k><)bMZ;xVAtegM>1yc^OK7T)^i&rrBUDpu@~YmQ++&Rw^5#p=jQui1`c~=V*J{ z1l#9|Vfa4DdjOq{w z)06`mq=W0m(Wm+97hXX)ZoN-Y-lfgN7^nH??H+t-Q zew)H*xG=pjnSXl9e7t+o=K^cw+H^vc39LrR=!W`HyI~sQwh(!o*)g)IVfnK%-LHHzX2*ejK%!`&80lp0qIAHXB%?< zBCTKc;?HS7Mct)E%@}_LU7#B&km7-r$dXR0Hs;P-{_|R6WN)p_!|?o0O2TvoLKhjv z-vc`OH=a&}KuxDvj@f&_2F03=xUr~U z8@w#?!jL}E?wbz9@t=&bk}vz6+9UPv#UH7{^txbo)i{7U&F0OWJh2keO^|S~-^KC$ zRRz)=`(s13E}o`k9B5z_zs}>LWKic=j$gIiuB-y#yVie880UZLJxLjBg|_vGWcy~f z{ZrHMtM9H|V^JmCns6y^lH2H5+s3Du5c*rV?S}O9)P|qM0|I49BI;|$+J3TThmS<# zadyAh#WZ@eHB=BnIm1_~QTp?jZ=>#*&+oeh`MhznurTC)gwb$0Q@q}wTj65=Zkx4P zZZ1Hq#)Q7FMNDe+7%%ue%~5+n7fJ|z>o&IX>YR4O+6uXG!pn7wEHYXpH_q#MgTqVy5z_cP{JD(+h)(1=B)fd4IZtrjsM^Jipx*N* z5g4wAwAr@}+GyUptE`5@Df_7<{JbYB33J|W3|S@Car6Z#iNV6s`MucgrHK?*TyW3TJ}d!^aCH|P3T;-JMD8aoQN7zCN`ImI0^e4wsZHHzqT*;1G;^WKWaczXkRd= zKoeeU{tj`cvW9kUqKs}E-H07aX-O~ROXy3_YKwRRe}&cn21>|jONB2;Szcd4{b$Ua ze&9Np4k0wGB7*lGZ>Tz%ox)vN7~5yZcW;n=!M?yg0!&u*(}Q52m5w2$Ya@>8(sA=)`<&OUo-@YF z(BZNnh)giyI7m`SQvh?vWCY#%+gl5HU2_d*sUb9ZYl;vlQbpNgQMM+f=e}*@7U%9x zp`$$Q)WCY7Wag3mnfI*GP82I@e!~$bSm}B@T|qOCY~oU11Roe(;tES9EyoRxKGm^h zul9d*4Z{eaHkomRG0DuyRq{%5xK=@_qN>=mg|0N??Qn?8Uf6Z-$77lYwh&DUp;c9G z$bG^I5C%WO84+6Zd7;?Y432iGAlSx3{k_7yUywJrwWTLPAiiPKFHyN*gVm+zQWMrt z6ii&If%(>6tQj7$r%-#A7`6zV{K>Chp4d8@~cx0PWC6{ttL zBEaYgh^wwQ`ovE!>&RG$OuV_o7$|Mih{c;le0>mg0NrRbiE6TuEI)Bg0SWziM}<=q zMDxy*?1^cl61>U1A)RUiaX*(OW@xm#wIEGml%|Z(DyL1>$57wisJG~YV0=jhWG7#P zxpV1CsAeb_!UUledblwOE?Wf!@WUu)M}n-?{Uf4zEB!!g^aC{sW$$(XA%l-fSWI<3 zxqU&4FWvRJd-As5aSK@R3s1JsW?yr`RY4Lxmk)rkmWPf$JbKo$x?FzzBO_pW5`$f4 zMx9oNW~NJi)S>)$T;8HJj`aMf>04VY@>gG=Vxh>CuIi?ds{EW6Az(#Z`F zN8D@-g?cWnTn|4_DP_Dg67>*HUp}eP&VMMx=+nGInggY&1`typu+BwhGi#A!_fzA; zQodTexErK|8HW;chmag3-KFdAYoc%2Xx@iQglTi`gy7Sg)Xq?|Xi}Q)7rGSVyy6@W zUTp_%72k1f>1Afiu99Qs@ z*r!Nl3haGlqE*1nV|VV|42}=~hESbj<2b(J4C)XM_wssQW=Hx02M9R>em+Y@*@Wr{ zt()YIC&>iN7btObgvPmQx@%MJv8{waS9(4TR)@Yf`Ds8orrZi{u;-N1jv6i(w4)n7 zV^Ad2d=zvpI_h9 zAAyy3CyJcY>8M9Gmp*!+%BhJA1XrRM1w!ye{(>0QMBMSP@Zmt-~59{)4Fb?;}9|plkkXkL@2s&HpO;<-ddU$vA%MVENp1`BG~E zih*)^q$v3k->2A$#_P&v6p$nAuJaj`n7gj$5LTsbZ?ODFm*7kK0_jLIpN z2VS51;WI55tz*0O&iIBpe>GWpW!L5nL8*sqk>HYfn)V1JIl{q!e2sf1kfo)<|L(2v z(BR9F=QlCUsxYJKp~asrb`F9o)&8TggoY|BA)_iGDv&521^7h<-he{8w5O5|o6S=> z3UuRR_Kcyyk~|K!d=^IC(x#jnTcR@Nf>~-F!#1486Ve3xxx+%;6t#kn#W$V5UhFz^ z%6mc#kER`Uc|>~*A~Vy*SbU0e`sUGe+HM9%70@=bObVqS)-Wq^t=j^yhmW0SM28y) z^11n%U#+KlWKolkEM7p~JX6FBp-E@g+j5dOmacqqRl7=3@7*z5EcXW@i*1B6^Rw%% zlOk_DVy*k~fFtUZUxT9pM@J8(t^O zu!#Phg5xT&l*S5D_=1vT>L{iSBi0{+3_eZr zurYq&_d$_iFAgB($lz>#j*^;X53M=@N>4~;vrA1?iEhu5FQ4yzrTvqW+=NGRKAok8 zXEg8VQSulH|C02?&{1p4hTkE0lT}<gdQwV7R)qtxkW z8x?n^z!V#COzOGZp*oI>b035m-jlQyzR(05j6BC5=3xD4oYR1Pfa=F9m#{|CI7wW1 z91Q<@nTkc0lUwk7l?dhc>*NHGH<&a5buyi$`6wLoZ>n)AAV;OQCk>Io(TM%MWwlK( zsEyI>e}-NCTF}2tPkZV@?7iD}uy%nhoZ3tcFVo1NoP8|rGI`=6MT{WTI0NNcIklm( zg_)fG(q^K@``P6L|5dQPmA{1M{I=Ja?j}vM$$gk^VYA)9G;tgeHaO&@tHkySwkZI| z!uVhd3C`17P1smWBkCBcvkgo%aUaun4`&?c{-JgNoYKofAX%D0yIbYIM*ICT@LW~{ zS`}Rv_sVks5K~Zmb7q|qU%&y%!@6r~AR-S9_?Ax+tovKB&;8AsF>Dh~yHA$m+H?2m zLp+nrO#Qt|`+dClYBIWx$crsaKL>hqWU)bL zeB`2HJhEeE4#UNq{OcD>JaJr=N31{k*F8NK1iP%_2O5G4Rd(^@!_mrS(chq8UKW0| zb4s(jgZukO=Y{E^tjfx`Zzkb_7p_h)*_6?3a&E765d;jYWONV=P%Sxn+Bod+i(ilq zB5FpfE*UqAd5)3aQBrOBu@YMJ63IZ?o%PY4tsR9MzMw0|Act+_2uC|{mLbsdw}U!P z;u39c=>&R7v=-Iq@IHb*y7h6IjuPm?rY9A)9w*0bC(>AaWO2+&aUtxUbPxAfw||KG zVVngXL(^V^(rQ^3Rtxt+vjQ}b^0)joT#`Dh0w*bJO^1>BkFf z*>`vhNZ#-W*chc?>z%$9O1i<7_M(4nKgf2%fF&Uy2A1=jE^RX((38 z-3h=mcl+{<5o79xePA{qFW9Bb|D@+7{DFk&4&-cy(DE#c6>ey9U z+9P(T<{o$I{NaMNoSaF<*iVuy1n*G5k<(A|T5#4*{Fl8v-t zC=Uqq8O_fxQhNR5{-bWtf7u0HKcn1CdUn7M1^ccN50TsnVy!Z($?x*((vvQ3!{)7f zrzuBavj$(Rx;2k@&!9pBHMIdJ^=L1?Xh$+E2z-8we_9yBL@EO3>N7zr5$!cg+HMXx ztD8){V=?K^6L708rtI1y-iO9OP|&x|3*TWw3P2FY%3s8CUYsslE545PLph+o)V9D+ z)N%P8FqLl06Te@#mZ^wogGQhZlyijAB%MMlD0!gUR? zL(V_eKc^G>M?6!J{mr{u{jXdE7^A*Ki_N^N&T@)(s7a__D?OIXaAHe*waf+jIEAH|3vBc6o*$JFYgebu4zrRTCor`{~YaqIuM30f78(e3eJO2lHfEYit88TCOmcI@36vkHIjP}WqWylbObwt}V z+k;K*q>nYv)lUItW1cS%^Z|@(q&x&0L8G(!B{l5J02H?+M5sAp=zN* zvd=x_*v!|>p1gGB{75k%gjNPq*xyZBSLqMLAMaa1i8lfutHYhRnsNwExrx> z6zGSF#6wAH=0cHXUoqeH@M%jOC{Ee&Ul6H}Wo7Z=(#x92F>M$2sMNwklC9=1Zu`## z9oo?8y-V7rT}idwx*p?5_1Pw$n17wi64DC*_5&`Jy*42xc+t@1!9 zv*60Fbn0>U%hcdLMXzACSik27&ZJuxH{Z(R|6z@;3KQf2!Q_GTNSYFSyg}8xvqgj- zmWrr`yP=xo%2Iqt!Fz?>-)GF#A+~Bads&xD}fUtgqPsd&n#)}h3X<(S2AJg#_zSsRNARCstM!xByV&O)b^?G3 zl0mGyyhP|w?^65Pxd->O=Ao&GpK!pKH^C%~;LlBm4k?I5qj(_Hq&k&BV0u zmi+_h)<={m&}QBX?dZ9V-|H{eG`P+i4xxMnhc{*Uj4EucR#B)3>14L5K;dK^<0RRi z4B@|n=}P{8HCJ{O*<(hGq^2@B5&>|=)twHpIW=zm22d9!WESub$OzGnjYk506w}cS z5wmZ$5v{|!R=X6KAChz#_prywMQK6;O`w!x0dsZU?;E9T_p`3SUlNa@S~?nlR@MS) zdk~WVz}t7&*?Hihx3mGohCx@w!M`_jhsA63u^&eayh2OJ;Geq`aGE@9l*mE=9`g0MeWj(-oo@%Ntr1-ID`MH6o7xNH*@Fkpo?s(f>nd^|!9|V!qAWlt16e zMq5CW^qo^O@zpH(k^4JqiT}gid&f1k?)kz&P!z?8RB2IZ0t!l%8Wd?FQWOvnq9W1+ zM5KrzL6P1B1O%iBNGBpyDUn{3UP6-+nlwqMffV0|ea@M)XWlz!?w!w>``L46|3g@W zm9o}Te&t(A4-dSVyy-0xQVQod<-@V1RK_>(Jnis?(Z|?_EPgs_`z4_1l-!$iUmw-z z$hO=@-Y2+6RLeLfo19tI`4aX4z&<}@W{U-^JN6*$=ty)WIskatOA>GB>6CuItl*sF$6bl9YCC1;-Ocdi zueLsaOOgHvk52aRwd0}G9;+zmZtSjU(!%c#4H#a7kBo!?R_KrHbpumtLN@Jk*JhDo z70e&L#(^$;G>!f~^X#bzB`jw*rD9JL=n3%fbetM&S#^2zM|D0CjTvov@@(KygYELeiT08Q1IhiYI6#e*0Oow*h08*9eathzO`D%}TeM#~LeF z5CK>YS@J3q#j&dY$rtlYk$ZBRoj&=ShxMt&rP$y^=i-coyqNta`(M+KU={E5r43;1 zvBC=s#X64A{EZa=ZPw`oh-FP-YnMsaQVHMmS34En_D1yNmdJ)+*m78dlEfJ)vEusMk!vz^+wseezaYXbnZ&NR zPn7(1(=AZ?iY9_x&l142FJpQ!LpYjVjLu(#CjafzY0!S?a3D=PB^D?w<919lO_ONf z;A>YlFnj5K&1v7?#HSF>8z!E?)i>@=h9CDaHB7h{AtMr_#_vwnN8bNP5+{uBF5+#D z-_|I*$^TdbrA9eV_`ceLiRkGNt7)p<_8%+1&M7-V8%C7+ueQKW$OT#p=-!DAnjU!! z?FiXjr5#!#OQq8PK#%TrV0ZPtjE7A#7%<`{pw%!Z!gf7}xym~+&C|D67JADU*0k>n zs6f8FH=xgOis0$qz$9Uv^?kQeIZ%A#4z*X4mTsPX!#iWzv!UmBG|N#axqY=F>XuYW z@)>%f=avd-*qs2w=Pm43N3thfJ$<3Lna4vKQQ|F8=7bMFin~=BB^WTJ&YXR*19crW z0V{Euf3AEN9`dBhL-z!a2ip^cPPEvTBuNCja=*f4X$pPJ>TWzsqo9j3=-8hQdD<9N zP)un%Id*XS{ckALeYm%@Uh{r$M4ijA%gc1p9IGo<5ux$)PbntbWY2kNRqQsnm1W;T9%xoawQgkJAMVADy?vcnbdcymeZHsTnHy{_k03jOlK7gSF0R7Uctdxt>9()OI7AwFCR`x+#Ss-nP zeYNy8-6Wh)hh|3455mYZHwV!lv>J3ISo8`m{(?M0`#)>C1?FC4=59A5aPnYd1sch< z$4KwSN7aJ`!c!UVG(E#ETaeEI!w9Vx=P+$=uX6MRQjm?I^yWPZ9yl zge3iKt+lkHea4VW| z)=Q+&oca-NC!QLCJuX<ZPpWaSDSo<1mA0N@!UK@HyWZNXn(M+L&{eQX*pTQgFoP> z8kKL}Bq)_@^zG9M;x)Pqx2%*$ksgq6PlbNzUAU&ZV)*txZYdrY5^37=5NVdxk?qiP zcB|;TIvo%W6G3&r%Cdt|bW6dGf@{eaqoXxvX1$6$rvn??YL$?8UhFdWHYn%hC3{!E zv%Ec50DvN}0n1Er!m7t9Uu`V4j}VJrh7t@Yx};e4rteieN*+gw8eRYj26XE#=^4yd zm2*M!VxyC?m__+x@d5mt&2GO*oMWc_2YWCC@+x?=CdIVTIFUhL)eB(EMWI`hB*|sd z*uNNi(x~2^6t?y?ZKTr7$#b8)nTS}D(Ckb;exWeagdPnb<#|!S_2&{T0Lzb_1mLK( zTZQPQicQSWlW8N@Wk=%l@8yCZd|+7PU#VPyLTZg%4(og z`?qTEpO1fDi~s9m;BPLL)`Kk|>6y77@Bu>+_2#RDZhuDLwLZ5|;Fqpt|MnB(3x9X( zkBZFyBjnQmGe5)Cyjzp*X-87Iy+TIoYgzh#{(O68?N+V`Up~QY%a_=J3#SP=%ClZ;!oBD^AA|Eb&AHQws*%0x&2;HUSh?njDz zB9%>to)eZ;)vY}GJ%_&I2W9|Gau4?{E00+bPdLWfAy4D82hkW@dj%h5$E1GWBekBB+AJlLBFeL{N2Wovk;Jqf8p~Mwb!irw@yfv3Hqa%t zD(^!bie-f=#P4_OfS}}-Dqs$S5_5d~mmU1YFL(&1=*zsO8>EN^c9Hzvo%(@e65mdp zTuCb?@W1t&rM838EJc=@yrCf56`I2DfAeFg|G5zAYIkX4ud@7mwPa=gu30q>hsa>; zj9EW+$qy}7a}x>p1E)Jd|K~Q(p6UvsC3Qz!^)zT?#9`nVwLR@mA=^g8=Gwsg23^%8 zVux2Ib@yi(xrVF&;(GZ#jDf(8=@j%idgf6tdg(Ln=l1eE0F#V2&?+Nm)%YX-8lW(; z02eDA6F{^C@54=DBx+H@%u}gIFuKyYsU*?@I42)8_==b*;h?FXLDM{^PpiETR3yS7 z4@_MMwzR7pfN*WexX#}AuMXMh0Hnl|rZW&obpu^_%g3pV=$YhOzmZD@y7zS`4h?lY z6Jhxx&rJbM{1?wrJ@Nc<#Mg5v)Du#S9YM1D36JOlv7ydP0v-qvy2t&B$4UD zt9lkqBW3+zW|@_48*{$r?cSAED`l0J6_^iWgiZJPsz^^lmx&mpGx3N=x!lilmv%C3 zCUFb#^)QkM;sr(AasOaGw`!W-+?W#X9jY07WCOF0BK+}FMcJ(~4z&(pM%kZI?R^Ti zKz_U49=?wv^6uf5CglMrod5H?aYBX-8awOm@vloMr+#WjD4w zW;CUHu@ID)6A+dT#UIOKM-2|qr(woZ!xTT-O+YtIA0Jrq;H8Qa)bYyrr55j*G{Uxo z9erX^3D7K$+=Wv;*xLZiA)BT}-GezmsM917a07N*XqA|?qRz^$7w9$p0`wB_UlUvY zv1jcBqT2`S)CS6)bEpUw3SjhDEG;x4-@Hd@sR;8LPM!NHIGbg{*Vw3EO8Vbh6=C$=#ol3a_r z=_Hf2-OI;iHFtuivrufPzVVZxjaW|EXRY^-pL#5hlA)XFH#D+nnb+g>8TJL*CYw;& zA%W~2MOBP6X8i0W+3k(JItl_zy*hacKaG}qaRJQ7xfpgmv(4y*`EIZi-n-!2-qnyya{ux`C?NYMg?=9nqLi<3neC5d0x(y zX$s`uoPI**rtQj~`>>Ic-t4%wy2bN3Vt$HOQ;y+a^V2O%L+rhRgOj*rTMDa46N^JC zqfLOlR!Rnhzm#sXkS)`?Q0TXN%^o%VY+`wpXA|e+SH)+g+v>a#$Ge>GE&C}|4iXf)VXmRqfg5g?7QiNmL@|l z+?UTLnPJ>I_e`99G$2o2|FxIH@G9Eks|svZcE=OEnGpf0C>dNzZasDLIB&_BB}7j7 z^!5Ucfay#niECyZakDR*7Jf)Wwfyk4udtT-Z01OTFZ z-a=yi>7%D1_dPmLt&3P9i5kd8cm2N*XwWY!J5TY^r4e)N=py?X#X&h#%^9 zO~Jj)CiJ>6AT0(&w2@V!neA`yxMM~)oC@DgmmxL47ndL@%1#_;$XgiPNm16G)qtRM zaXO$P-pER_Z5pvzsam#4mfq-+?b6{<7tm}tlFeWw@MhTT`ibjWI3R>{0Le_u!bZ?| zeymx1h`#b%_m$g7?E|pz$5V{;Sf?o7HZRE%Zy(-yp9AEtQRgnXPF5}H89h33roE~w z4FlXRTqJ`_yh`wM8QEGAOWLs6y6rUR8{vfaQOOZz5qP@*;l!Kanr1=ti}tbKDZB)s zu1Jbow*A%*#PJL{6)8|h#)0*Y@S3DwFYRFd@?!L)i^<(C*{uw@Io0I+M3Uu{m$Cq1 zRqJEG1>WMubmB%)gfCMwag`FR*g}0)u~wM)y90nig#X^0&mGGFpULzJq}7gp5`%2O z{yiCue1i0(2qhMC6pd@gt)>Q`XUzK0OCW&}gVY$QZ-3d$SpZkGNWTz*vIhgzOeV%U z&u-1Bd20jH=|H^)u2zPZgg)yJxr)}zB-bi|l-M2RcH@wgw8`#$rZ^bk`Z?FaT3Pd< zxcasi2OMX-x3NPdjfP&f)6NN=GB0FXPwb(%`UW_ihBvlUOW> zX?=CBQH{-2gPlV+6xXt$-O;CL0OjzvU)eX$vC*yJ{(h>~-+x6I_2B;UB<#1>1^8D( zkkK@g45zF; z*gZp?_-4r+Vek1p3v^4T#|SR$Ou|wsK->bWv3*`&X`0`yX|I1-m2q8R!(y-eEmy0Cv?HX#;kecn}9O{(DauFzWm_EYx(X$!DoWQ$b`SiuD73o+Kf%TR%+7+}X7afoz+STlhr9 zM(yR5BHMO*(KchhAa0v!Or^${<*L-5sM3G%mnuE>hYOxeQU-LJdHU{-ya95tSN!sR zo=DsO&R6|N1W=x;?A-s9a|KHL1<|9XPM4Jl0V%RYX@dRjZAHPK3>N}qT}mt98{Pe; z6NIZi61c0MK5?GC<`_k8P~!*L7F3d?OWmY2$I3?JJ}l5!htR+3U>5yy6~#C{X&%rKpy%{%CGQcQ9Q}lMhw4QD8G=n z_2MADmKmI^v*6k>xpHrkFgm)ENN(%uP&>3Y!i+A%y*`mY%_SHt#=lzdKAQei<^^2r zblqO(jtQ7psh-!Ja+K*_mM|BogspXaHvKwLJkf{=F&&pE>qV-lG&L2!I$;*vBK%2z zWEyXbYw8Mh8*c1)bmvVA4E0zOh;3}`C&BI#wx>GE%RG-aV4lERZ1*TcByh-%;*kl}o&>*^Xj68kYClKAyfvS4wz?6E-BV^Vq$ zqz_pDU8fc|<)jS9?+|uan!k;i{!oo7xaAdjJ!H3jPT^jZC$cb+a)q=>7W2YRB7$Ky znO%-Na|$teyQTwJ!gcHkq~<`GMsfeU%F5nj{$_$AV32Uty(^V?l1r&KQB^I2Zs~Px zgN-;>L)V?F4<~<3fPXICOd!_tiaOL#?XU)0{&rv%NbhJ09PsT3QSu!Sy@EAdaYtzj zKotgZDeS$=!hM#nzeZN2Y!ZbqD-+Y9v!f0rSGBeu+tF`HKZLInVe^_p=y^Q66AOc` zR1i<1zZSO4*a05SCc8_6rhk*8fDA8?xpEw*d2VIb(XC@(#;@ugy`9x26l| z<#6(C3*>`dd};rva~kLVH`|yFm(Wyp_lZygR?iHbt@pfbC5ib2PrOS|EmTX~Y{7_{ z9(j~)&KK^H8Nx+S?Soz0eDCB(Ls?Un){eqPb@5KP@1>R2VNYZ}qJ}3}@1Af}__7NS zgve1Sg`&07dk?y^6SiCo z*TckPttG>`vzvU*+4Orli>D^ZZVEO${Kj|{V)K8%EB((Zy#L4N-Jq0jqqYH;0;1umbLqL$;!06H1CX@+ zU=z4if2u-FY26BJZTqca^_v`Tt;z4Z4yU(j02OCUrD*m}cxT^jP70gL6fC4x0BH`Y zQ96C^KluqAG8SZlka@Pw_8&(Nmt1@4&H@UsozT$+nr>4z)e}Kw!fK$c!REKf&q`9V zeeZBgMp}!j_2hgO${jV1ShFgGFMa+6`EiqO_c{4IsAIDBVwd)Sts5ch=zoQ#0)WPa z>)Ipg{wInYTT+i#s}OKniR;GLh(j4CSW3dW4{W99nE&ubZsVde4KK^12zadcFNiaG zlpD?EfK%lrtaOeF3EA9tF2$Asz3hG4ExMj*q^DuWj-)3*8Q+@vp*ns+AY~KbDaxhM z7CQGoJf-iDhStP@RCXEh0ll2VSju|NS{vUCqp!fR_pJW~2?`4bJPUxhoT`l{TGwrRU(ed7a3MA< zYJ#1FC4QZP2caLJLtqXSW+(k0NLjRgqW61i`qSuw=7DP6tsH8~lV6YserTl|Q*AnT z%!y|0i)m0fYUnS>LuPS&VBpGziq)*Zz_f<`_<6?n;!D8-En=SOG@%8=bMvj|cBys#&LFg-9`feO(J1k?!KK52}y(=>MELVZ$xo?2ZML__2>dSn;usvwLnP+Jq z6Pm;2JXUZpJF0Y&U`I!wPJWM%K_tTo#$CJ?w>91`ls|s)ZPfB6?guIzeKa-$F*bHl zLpzw^Enpk27)~!?LSS5&hjE(G7mJWNHA>x#eE{)-3G8hX*;W)$e?h%q3^RFga~ZI`zkI*;qaYg1C`tjxjllo?Za3_8l;w_Fn^a(9S;E zXVczPf<9tLL@xCwP#~5C@$C)xj2lQBi()sjDXgIQ)R9fj00s5IkLUuILbntJf?jsx zIVK;e&5?^XM%gaCpB3PP@`DL#utR3xY^4GjSje%(xUX|VXKSr(Yp;8oj`72K?-G87 z9!+xCS9d&zNi{ZOD{!>*4ihLC>;Xf}#WX5xESJyw<)$4Fi&GK*3UHH=;k#OOk@OR{ z$8I-EWCjKGpKraGhvtLZ!w(}*5^BD;_Mb{J(0HgG^_D6Q_SI?A7D-lA_xO3637NPx zouI;aky_~x$Ty;7kUNmo62SWgDaT8CKDu)Cu}7}#?lxJFvcH0)MA-Uv`$~SUjKjAf zAfly8tHT!mAdH7`U8vI628@S^ z(vuN>L?2`Kw1WI*?vJY6v~RU+B=uY#Dm%N42>cAjkRq)HV%|^Ir=khPiSzs2wXrXxJohiT80cOEl@ zrs;%(-T#xO)J=~F8GIX4B=EqG>WmCL7B}{L# z$m6qQ(8SsW`N2;kC$~_%LFjvS4}{cKTQ(Z7 z==CY3n+IU}?eYI{9Q~iyjQR7m|Gakp;TU-6B~H`S*0VOfG-<qUT@fcu4T(HWeWcN* z_>s5s=L7>!Y;-){fI#%wcXdH>RzJ1+3Ien~S@_l~!ICRkRBs^E*&x)IaFDM&=gSO@txHjVwPMb`Mn~o~1eBfj z4(!%5X=K!c1PuthGlsmflc>A^uboJE4{RC^%^XqnSX#1TCEHM>^@D)BZW+%f){f{w z;k#8aP0wPDiANJY&pBQy=5#d)zm5oN0lXp$+6 z9pl6SlTbME#&y+-M~$zgZh{96l1_g6ky7UyUtObB%l0rOjbEn`0p<@r@G9wY$8?pU zRhUL^aZ^Ea7I^spq{_>$^{!q%b7M}XJkos;XXuynjLG%MQp-{C-M?uf>}32aT^4Eq zIMEN?cDVKz#66#Q9dKCxZjpqnL+I1$&}%nuP-oMiBjSji1E7Qa86JC12t)BZhyD(0 zkMm`kQw3W4ihy^u^c@@fWb+&H7J%Ek7lM*O6&lDs&=a>Q5s5ge4}86VY2x<>5b+IL z(3K<>n;P+RR97;EWH^+V&da)Ek5GJk2-8g-i8Wz`Jk5(1F7NH=cX=ue?w_qrSLPpJ z_A*`WBO8$%@hvYOrdMM_m@msf@>!x?t#1sg;uA_C5Iyd^OuHYJ=gJ*sbPX z%ku%UGbOwR^ebDM9cQbrWW=fLM_mP>x+YELD;~;y+v~wNTB=u|vo8u^M#H6+zYir; zsD0{@u?YIPI%kw{`4A)f4$HJ8hm}`fBd2%aM!4X5vGX%GHLfRyH_RW;%F%^i9-QH~s%4AQWRDY8d505f{gZr+()JD3+&1w&Z);D#*dEc|T;7R@~)@WS>K1Yg!Ku#*MUhJHpEP{t8KhF`{hU`3&U04bllv zNR5d6At^%L&IPbnJ@SYw6entce}5IhwyE%*!oB;?XN0)I=-V*kT$vYQA|^vfoHQmw z!U3lnnxTkZr)%qytAb~wyg_OEoM_vr@CMNa(N-B9o-4DwPBs_CyhEHIs0=Xqq-y1~ zgD^|7<={Ll9rxd1p6dTiJ@4Ujhygp%X7rG9L$>lEWUATgk3=pn53#sqd>V z5JT(Bg^eLY1?G7cfD*C6veg~xM2|hK3EqrG5a3Su= z#K~QJb<#9qMQ94`r^vR=>~jQ{=E|?>rpC*-GC&N022LDtdGh?cMZW)?1CP8-@r5fh z@i?QgtD`efp~q2WE7&r|6%jOXy|Ke%Hls8=qH&Xb>IuXI=U1wmddw@jzH?A6S}Q?{ z%{IKg*cIW3u2Z7z>%w>OHenp(!k%o5mc4NW-t@Y)5U!2*(ZkP*(Y{; z3#&bjWn~H+YlIk%X36>Trh3;(ft5K0-6lX0U+yOl5aTIq>ogr0*g~TAuxLI&0fAu# z)O|+u0ih3cAX}2c_`21Lt9^qaKEc7I4j?9oAntu$zx zK1r0*HC~#NT**b+u*5?RTldhLYL(fSn3TOQBRg1C&7t1C;y_}ETtAshb=!@=nnM@FYquB5s1+p^Ouf783j3GFQDd- z2X^`J;q4R6ov6~bbh21n<>~VZ3MUQd91kVlzs+k~hy-2G{UW3pWu^4CDK3%yMRbgg z&j7Na7gmM~pdBQko|IIDtQtn0XV(_wHJ$Ei#)gi>H@k4^6(FSL=9Jjh)Y72Jo!-3$ zHzN)bM#S^ilw_ zMa8~aZ~v!JCCWF(8YHx|n;~xP&4UQFr4t0BzY*%H4#FkMN4H1-r@zcr0I#&O3!I^ynY<*Ad=!D%{20w1XcuBT871540*Dt>Gc|2)w z;9FRExxD=258H5bN z;qk^p<5pT1@mQ111K%Gv*J^>m>qQZAVM2`~kA1Mo%+T_i2x^l5d>c-?1Q_{+%lTJG+R1osZ3@#bUKV&u9R z&8f9Op`BY}Qs1v!{Ol!(VLHJGJNogTEY`1@{a7#%rhwQt#cd#RHIa`*&*%)o_d>FV zgTFg~py+<@hLm5pC)6P%&;H_+`Yj?+-x=!e#getmFA0cKD&r|8( zGlgDmv|1A^k^*|or#q8=gE1QHU$lD3ef11|8{7d+2jxYI4h-Kwwy}&T0bNAwal379 z{xVP7$NoYQnhY!vn^ev*GhvMNxtG|*_Y890PcSXe(WObBdEZ)rt~?9#4}C%+5{!Iy z^fT?<-R=n+d?q8i!|l4P0j;}(J^)i)LOw;gSw;#bq+Y8q8(6)o0riXNt;v0EauNq9 z&DP`_V@MX_O%q&1@2%UdY$g0`rb}Q^R zvl2p=xN9)+p6AzD-rn<7?JeuO*R$-ja*9hMT$8akMYLD@{>hXEwsr`pnvZ;~!%XW3 zcrND7Jv|?@YdVDvD%6XpK|5<6ag&-RVt!DW>PuPW}!TeEtAj~~9N3td= z5|3z{{uK4bwZ$Al2pK161*O*43V-aMzK5X?XkBS7;66jYU9okG6zoLgnNkU#e5>O+ zn{$#kC3f+f+$N?ua%g?)b2Uk{&|+b`v)4ZQ{B71R7@Q=8qQ8fb?BJd-?30YP7qV<( z2N=&@Llgg(%T+Iqi^5ChN2_ev=*){%1-z1#4^LXETSRMwr-uhIOqZDvMZ#qC57?KC zFIE(fo)xb=a7P;P23zjAG9Vc_vfZG_8e7bep_sz$t7=M3rwQs1;dra8{E|3L5979N z%!TaoIiXg#ydc&7HNPwA=O16(OALM z#e1snGSZHE;%x);L#x?yhW)>n9{|dUgd5Wmp~8Mdk=rFvOi$h3?i1fV9*%XB0UeTC zRr=G2m{fw{>hr)ll$fP+`_M;A)rAvUS0Nr3 zcFwMRHzmrGx3@j5)S)2+^5abYx$Mpw>(hzlk4x^H681e+fZ+8?>8n#^uN{z>)egH5 zXF1QFUbOVt_T|Hw&jUqq11xUq=l5YHvCf!LJ&LOvJV04Vb@bL+%G@!>wP1A=k}RIFzd5Cx@*r;?|z^^qhObi~B^tlKXLuoX33l&6SF zf{IBuha`E@5P&XqzrZ*4htfzufCBsr^D2lVkVWWKz|8=%w>2~+bXRN_l`t`d$eAEy zR@482sNKS@KG*u)tAH|47eyZ1;e7ztA=Ov;#AnOI}50KtF?BMCQy3M8LG z>Jyg5p5zvtyW%Hgdb&)1d77B}iPlrqMeb08rJ>8R8}$wwl0XAX5Kw^8}FUY;sAS*tP zs=gF2VJlq;0B)~_2Gd|s8-Q&dXM~kY$iIB|+*3 z(w{_0(p_?ZP^}{I@qs14I>Q1@CW%i$Nn%=|8(qh|H0za=T8p@}--^#yV%T6Mku^T@7aEp6t|*e@i-X zbG^06M>cQCW1O?scEH8V=wMW^;G`)&b)}dXtsjhLlYCeHNowK4Cno9UOKFGD)?F{R ze?hoZB`q8(F6;~zcg(eGcUs79xvZ2C-4G<6j3F~Gb5V}-Aqio88EXYllAu#~>4-#y zU{t^>rMuxi4S1J;S`K2J4Q}Jds;0LhfO^eD00_i>dQyWUS3s|V(8^jMytf=XRa5)- z$(C(`E{ShYlKbv7MPjMBau1XGJcWU)GZ;!GgNf<{CMuDL`mmctFBKCl+OJE@#(WQ;rojh5WufcC2{0<%Z)XS~4-t6VjY}_Tx4^5w7ZAY1NM{(E zl3g#~F3~i_jCZYJdJsb}yl^qmbc*fs`|2V|w&^Fy)TN$GY9fk5dp47>q10t$&5+vr z8Ou!4(j>?ReIQWthRx7jGF)tQ+1K@}vF1*DlrbQ7xk?f%3-h#Mu83_mmu;u96iC;Z=qbuv2+vwbt6bAI%1xD(YsOeTU;dnW^{3N(f{t#pW+5JZx7V>cUefPtY`yN1& z)7*>Xx3oXIgpZ#r4>ag|CzsF|XvRA zRD~+n>zdu?Gj_Am@ga)e@H0phf{B&y_x_Hf_{{@4AFeAg zo8n)D9+&-T^CaE)Yw|-RlpUWxWh|oUQS;Ni9O#SrS02qgz;(jfav#N+c+!weg0Gg) z*k`f+LV$Z=oKDG}pBUK*q|j{henIMg0&e7XIk4aqzZ(7lG2fuv!5C#k}u z{MZZnW8(?HHq2Q`(F7ZG>X_9XiHj=v9Odszt1cG9VCM?&6je+)#ttO$m$jK15~j)> zCc>@5Ow$EDXIA53Iy)(?V`XyjBn=^?D475*z`;MF$BgJD-un-tLm)AoZq0Z`hp1a+ z{R)pJE1*LG)5sQMDA>rTO*=Kupu7Ai8(<*~J8J7VSI+zIn zU^!9piSJry*OReJOS`ybOvGVH-?DptPg2b4BPgcRmY6__p2*7S?S`uf_bj@wAEr+L zj4~gz$sFxpEs|ZHA=X$F`l5v|e|Ca&#xzK##%+rCfK5vMP7zL+-nXd(?{ThqyJ%X>51=0(S{1y)VtWyZcGD~;k$#PvXfA~u$o~a#f~&fR`+VbH&*PYxGn761?sQMHZB06}we|dTO#ksoVM+%@!eG&Zt zJ}iHm)K@rqd(5JZKlYQZ^gj6`>xd6cpw_Ofe!HI zHTQ^de#Jl^%ibf+ajzwJNsZ~-t(mudc7HV%-Gjs-A9IafY+ZK2KXrK?F3*5U=3JXgaborRdA`ub-ixg^hmQ zit9u1ljM9D^{dqLwX-}j*8A9Z@(GM}iokR{bWon(;+kr|P;zl1*8zlbX@E`!#9{g{E|sILAzXxf?Fsm`*8I#gvRP_h0L$!xRHqAwqE?f~%M z^{9_yK0RO9Mj)d0q8gAJ{j#B!r&YEepS7pXU1Y!d6%P-Lb9c+Jc%FiCwK68(w=*Bi zC4?+gs|X0c%ky|GvTHiygbPxY92%=B(aCpF_bcTE6cG^pipGa;q#pNL91%Y>_`tO7 z%q2_#d@_ajnW9iCbY0GG*L1GSkH$bzC$-_V1JZ4tt-}(Z`NP9e4YW=;Q%iknz%DB3 zWJjZ2dJ^_3WK$Hn%CmGWYf&fjFR1bOZATvEj|F1Y(JgGT^N3PLKlcf4FYZlcC9gqG z*GBD;M>avY;|whk5!3zvs;o30drvUd=4b(`HeU&SWa7YAkv?(NF zef;}vIflEzmr^!&50iOO_0|oKEtbZ`pX+rU<+$_(=r`7^Z#|<%C`%F?>QA_w6Zq8s z>d3X5wz3;MIzf#N5Df^O+K~XRhfdaIWnmgeteH5Xe+zk^^u?V|=w;mLYr;n#d)}*FlmsNDWA4JYuKVXoDYSG3 z!oqz7$Z*;ht5d{)V%uj00;89B&IZUKuG`VCqK8WKQW~aEZ`WSr^&GKXu-Y0c-W`O$ zK%R4%j67^+M}VkyKKDFIF=_fcVgC;(-ugdNa7N?Ld-8ue7Js0&r}VPbx3Nr$?0jA9 zBv3~XVD)iKE(-k{uCjLhFy){(tji2T-b4@qZp%_6@gEH@1Yw@yAG2ij`g?8cTrKY} z$g7V&j=f10z10-0#>lj<=$F(h!2J33=H>rvP3jMF-htcbmQ3u`wXcyRO45>5Hc0dT zg04jGp({P6KBq|7mLBNQU?1^%{%<*?e;)rPGW+jxZ2!DI|J3WVPyXsMif?2mK3^ou zwQyU{Tfcq@A$8zYL~LC|{JBHY6Rq~o9wIrvZJ4ZVyzfovk0pxyto!pG{8R72fA_J@ z@TzFxCVv73NNG&IpWsMskh?%ajku6@7}#&DAlw(@qwKx;)1QhjuicY4>LU)Ca$Ak0 zGLgpBU|imPsTN!D)8$^%GIWyx~&Nsl9=rpL5{);MNZsGl7 z-!Kb<=~-#eE*MEPwQjX+q$1Qv@>9$!@pUmQ678T5!8As`-2QAArc`k4yL4Y6ijCBF z$>j(s!?2I{ZsDwG7aLtL!VrKFIiU@R;HWS6M=ClwPfWiTpKTu@pvuSjh?+?`0i#<_ zW83(ByBRlb91g9K>s{s>_jYMjEtMQ+pGB8I8;4NhNZI1gA$R46r;IPEe8JrOUl`Zr z1H8lO7J@LT_{+!u33!nF)vV44nAO$Z!DPel46~!ePR9g{WrKd6uH!Gr$*%~YNS&2g zHh-<`pJ85?N3xxIQ*AkYm`}njX#S#XALSuQ{u!am?9^TK^0tA|HCHn@!^{QB)t(Rn zvWL=ys-PHB6E*->U}Aq^#&Zr8O(plc!U`g6bflHZUl2YA+QEU-wY{rCFSI*7@?SCg z=uDzXqLPtM4rY{9yGZSa>?_dof;DwLM+uO0F}KYS!+JBCL#$30W#@(fRh!(tPGfqG z_xGo~LUxYflDtZ2Os9yjCXRKrzO$~{StugD*5sY9EK&pSq~4_X7B}MmEuOV?G}xoV z#_-1Q(3k^jfn0a#T7|}?7`0<}5_kKyVAMA*pdjDbXVSl7CKL4LyhJs>15k|LI!lB$ zet+CcHZ6(kyRLb}L_^>a1_va&nK#h>G`@;HnW`tD_x3AkKwK8gH824i&zyvN_SHG3 zYhGNrVW9JH9u)Zgjd>m*k2gu?ri`oafl&|tUO8@a0_hQTpHz&O%07AfL(u_WdE{Xb zV+F(jLAODyC)O6eBfJIMYy=WtCkeU-DHd7MyigU#?PpuAcP5w%4$f~2fTT9_uLVE4 zM&uKKoiN7ix%O#d=_sHHCcu&X3xbeB|IoyuRF#flncVSS)CRNw!gpNg&iJYRRt1=7_nK=6J;KD4B zMJw_>HCWJGI8~!{y;<+uLqA|Th8>xoESs-K1arQ#OStnnA(J^mL6)4r^JeJ$qBzJ3 z9Wld$Ez`Cyem=OxzA0;D{YU}y_jo3IEaw{|{ZxfWj(Pe{IScD&FT6bz^2pII4XRI) zCtvbPnrzOD=hpbj<+soJtQw|N8C1>o%T@Zw!B>tmOg`3U->K78gS}G)ZMql94(CUU zdkMXuO4yF(Mmt2Fpg6rrHd(<3Dh%@^67yDQTu31tO4M=dGb+JWyhZx%`@>(-n6b|% z_PfKv%(IqFij`Vi__vVvO&ReW%oKUNp4K%q)=~Njnu(fAO@u!XhdzfL+%yvklgk-= z*l~(pp$Vlxc@ULJ%Ef!Yy~b-GuI__KAVv}O3j!J!MfIN)7i&3Mi%De7vyYBU8ZfR( z*1>l~%Hf+$=vWW;CJM`)3Fukv`E7AZereji98wHTBdKc}x|Tnn)reg?JC3eI0|^!$ z8V5_?BfZ~j(>|kcw?ULVt(nT zlP`eJK>SuaHFlN8O8U5D)K{3ICt+)=#QA)@)})S?zl_S_wLP^JP885c(jO>{d-UTb z2mip14}h(F@$T~5JY0il=uzH^j5~H3)c**Z?yCDqQH2!M0fCxc7eRj@1CxCxqu=!WTV0Hl&Rky^Q`IcW0wzAb=?#%elH8asAZ+D(`ibsK3nAp$wcN2~` zo|?#gwA7&v;bb=%m)FThyi)Pu)au>j>T-^BzmM9Lt!dEn)D+ad#fcNucWoV+3j+0z zL*?|USrFwgMpgEO)PqP)`^;Nk^4k3*7!~r+2f-I|o80o>pOR|JZFzbs6LKVRL-^iU zOxIP2dfejEy*=%*&m!q7jS1Ap1tSTV64O=FW*FhdwlPVY?xIK*1f>D)1l<;z!Wu)< zvWHJ#aBd)LAfP{suW`~2wVaOR(@xm&!!7lIW!4M+{eZ6k*yHvURlTzyWncc(>l|w( z?l0!)BTh`+>Zd0CtY!6DT>M!-oOCTAAykZ@fEkg!S9x+0cY=tR;twXeHF$EPG9E@s zryI<8ra|XxLw5bPUQ@%6Is}aQr^>#Eb2WX#U-yUWjEK)Ue&q>QeGo7w9ALayblz@X zXK;~~7uq0TtJ(8~*PHYA?};x+GVPUOR0X}P#t)u;tHXPhGhP|@wbX#|JLu2V87P}k zQxn*z+3a;-STo5I1+0#XBnpIoWbAZ9i1ZdX3G6qF6M-;6ekj{aha*}5hA$xBQ>gJH z@5+N#_Qi>hEnh%0E0iXAd2snj>+1xV3%mLxVi`aJ!&FZO44<8=C>fL2Zhwjy>Kabn zdfJORK)S387qVJd8Iqo^1!Grt%a;I*(`rQ)6IIe0O=jqEd){y+BKJRa({ z?H?a0N{h-aOhwsAmXc*i_85{S+a!@CTh_#k2qjDiMT|&9noCI5DSKp>v4tV~He(%T z>3h2F>$;!oS$@y!e!jo^dG70ezW(ULm`|VOJdg7@&+~n}kM|Kw;=I5`e_g2+B0ViR zF=u8l18A=oXkTdPrK2l^w4@Qj1K@sTx_%s)I;_GNm$n_Ycni zRizB4>{l6w*v4~eg3pZ$-}msDoBsAn=OJ298A$$(#9;RNA|6zqsp`fS44+r-J)%0S zFcn4n2{9Yc_L{L3rfAYz%At*Jhi;xZ>B6ma>QfWdiIJEdwJ{P4eGh$HePs5IR+2Yj z;0;XK#UQ|7BD?`36EVpds{YOTddHpu!8+cEFwX>w?=9}L0ewGMKqBxVI(}G*I4{b6 z?kz#v>uu%=bs$YV59nKMU9l>)yT95&W=EXA2bPmZ6&S*UVQ#X6C!WS0^Bu7WPnIJLMQ-;6_# z)jQmjB{MCZ*L4JA&wtay^5=aQ=X#K1GWJy%?M?@CEsl5vzR8xo#Y;`a z_5$UJOZm_+has=e*e_ckn?swv?;M!)fL>k?e##da2~6+w50kN0b%xFRitG$q-gkdO zpjSXSP#4}5Q2r07Qw8+ML9XPxU9ZiR13B0a>3t&y38co)-!;2VoSJ}uB!)97}Slr+EX${Wp^ z9RvfO`V0ATxpcU@uhlej>KTb>ZDssWY<*Xs?nnW{7h z9t#{}OAnSnTr7WvS2^^k?x8(*OV(3;UmMz0++UdBH*Gj*4ysNUgZ2DK^vfa(EQ3T= zUV_Q&2+?GT>#+i-D`kBtg=_^>9=dJCK#5~Pa$eg#=S5EWYa0)xUsY6> z#g9327fRLj`f`I>(pQThOuEy1JjfoO0+${;#!5;Xq%7V2bkq7i!jIkpBf|AtudH|5 zk}kx#f3$Z8SU4C8P+Gi)>dAnV5$oOsOG(w!X3-bcu)Pc7`X4&vQr%SBPDP0r9UolH zLvj<-6GqddjvYMx9n|HyNYGL;%emwItHu*3`xDQ!D!*L1&`D7kpk5`qHI%yUz3_rX zZ-EcrvnuEL{bGkT_BrD&7C+#u&sldYDJ#8!+uz%-;l*Z`7$FX0!hx)(+ps=cd_%{O z>oDI#4L_#k+)9iKwT^_Y^*9{BxpmLY+%RQGYcQz6no*7KlM)khWc!%?Cr@bsWf56Z-K7^uDzIQM0>03 zMY{5JVAi~VT{t&ZqxG)9^Vi>9Q=-k1rn{{#52`ij%G+xY+j7SAUxgVKB~A2yKug}) z7Mzl&JrFJ}FCu9_?B1i}8>J3b7&WS=kgcM`xplu{(m|@NZH`#}oinaK254xoo=czc zOTsWnW`HmOpJ+5gY$BNeHb&3EaGB}h#5Xi3(1pjUWwH4{Ui;Xz9XW>Gz0dpjSP0Ca z50&*P*evUfRjL(YgCRGULBs8M)CvxbUe!M>0H*DohHWOh&m+-~Xa5=Bee{KDS}D9#2d5fySouV>p{qaxY%$<7q|eQJrW> zK~_YJMG|zL^0-A)S(i5<3PLjIQ=GA_C7Q2ik|ICzJnCE=*3oTGTJ#ieIvqY5mldaX z;RT0^=2=h>n!24+p!97r992Sm@BgKs)s@cTP8^WErr_Afv01bQJorWGO1r))+RL_XWziBo$sfboS)iS6r zvUu3GfcBhg9J%2lRTi(iz_`8d*+armV^SJv!MfDe8@=t(FLcKWY@YrI&V$qT_R2&2 ze?oQvE!x*$a#`!%ilU8>5rE;Xl1#e;sNZ2Y#vc~+(@yoVvQo^G;v0_%-i)JGCt*_{ zjZF+P^Jfh8|KC27crv-xQ-aPp+s#(fFw$I}vVs`6XNA@#AA47E=IGv3M%5k$a`UTz z$Kyi=(64(IQKzv(s0gpSumi<9?Zld_RctLZ)I2E}!v)e3)7)OVV$M_usIDRL0bpHH zn<~9$CTVHR>iP_C=z%bhn^~1vh9KJ@4g$>|M*tjD1Ldaj>SKUI+3NZe5*!7Y&3$K1 z=Q6KFZSfSKmI}40QdmHrT$KfJw~-qm7xMHba0K5VFy+2X!P6=1k>iNkaGMcgf+dFH zB*K_K5G<ivema2S z321?n{WBE`^A%d;c!cob24GCEljZqZ`D<%Eo4r=f$j@TJpmIirNgn zFt14(A?na{J$R;iZ3@1eSC1a=mSX^NGs>=`IJgw6yC#%K!T@M$r5V~)c@`7}ZmrDY zaQ`7pTLDXf+N^Fm66pBhB0^Lu=mN4t1pThY*44mBD!ct?_l&W!)}+9n=$+^?L%Ug z5I5;fxMIX-TtGWf8bjss6KZ6g0c{c)O$EE6bR&A=7>qar5+*RuVM~N8I{$IZ z%8Kx|uHwSTzr1o7XhdGJWJe`^9I$v|w{e2s#Dcs})=uF)Ca; z=y6c@+>09!k5))vqAoAPbG`*l9-&qC1_x8=69ZxSyqosxWuW??N4Tm$S+`IA(~mu` zE~Xkqhei8ju*!qCi$M0|6TT}T_Dz@SHTa56Dzv)?norPrlacE=#_5U&uDsZc8A(}~ zAIH;SR9&4{@zuu6V5X2K#%D%xa=h$Ldpyl zkim0HfQ4XzEFl8wi6C9=!W3l*fy5n!7C=0qJgS1ywX6r=`MNcAWpfDW8L-I2vOKe} zxv&|sh3|p!sB#O}qgjTgyA(Rl#(~TP1t?KZ$+66tmlTyG8PnJsq68En%^#r3dr-B_ zm4dY1Hz4zOG$oxkqV0V8nJ$ruJOtUVJpwir)v*Wuug}a!5f?BAQ0*L`_lgMU&9Pzz z6S{yY;m~LlHWw^k^^_%Q*o%EvcnW)8e}j!auNJ5*aT-%U7S~(07dQI!7md3I<^n$( zeeA*BV1sw6gJ@5vGN^G_c;-IPz)s z2&KE+o*-piCK%|*TK80{d#*gcBsEbv^x_GMw+lfQhSoWnom`v}lkLIJb2Q|uQXRa| zi(MLTF59Or(rO1l`V#u_2$L@+{6dX|%X_7-t)&vK-&EP@b;Al|OvdS)*F&v*2Pz`E zTtSD0MjAShp{%TRVuPUjR<&Aouhswjo!$Yvd|^#bvG0!ThQf}2_#Uy(!A zmeB=K?cW)m?NzZy^(2xB6$og}fR(FR~3({{WBhF(AIyk|-4oB)QsJPTmydnH9rQk4DTa zP>&(Gpuy!dpj-J90`^=J(Ol8 zF4}y@iMhI%$T=Ju0!a!vPSrLsm2MmH@+NEb7h;S56j236chq=;7ySzsvlhND-+G+# zZE52+XU!c$RTBivB^qJ&@h2p(I#Ogq zc&&}-to?Zh4tTmIVK0)iUq9yeH3A>nUBy^aQf`@DIFxkSD|lBql%e&tM{4$2}>&S*?0HXXI6qPP5Io--})hH{jrZ{@s3mU{riH^BdZJu2%M%D3Mp&Wrh9~ zdi7`deC0>D!~CZ(T}{XxJdCAM>29w4YIzV7A$F`@%iD%O@r7m3NfCCfkn4LmV9T#E zm|BkxUF?Ll9y@SGWiAB~p3bjd>nBIv{o}`n&KG#NCd4ekpyXqrtKvzs`w`!BUdxyk zBlbG+?sorWhBN!wlz-A$rQSq#$WokEAHK!#zP7rNjb(lCehtr1i`3A9uI$VZMMdOc zvaKstrMTiwfBRNpt51p_I@^i(;C**SsUSJ-j7vQ`L`E_?Wt%N_r>XhOKB$I4W(b$i zvh(&|{_Q`bI#7jTzjoCrb_U@WjT81|Dxxu^z78aJE$GB7G7R`^@qFNX)>)c|Jcry zstNH#o+h&r5ZnP#lJ1KQti@XWpViyMOS@OUE9sV4NW9Jbz{12LCFW5RAHw(bE7BhG zrd8n&@t=*n(t*>M{Oh*;jdvJ+LXyJdU7{dSl3eN$@|vBJNL#wanwsPX1>aOk7k&kofmOmToY_9CK!5uV_5q8VdPAMw zDz1oJf2t&qcArMr?A$)!LDBmh$(}nc^q(2p zP7CeW(9S5^@k9R;p4y=XUD&#ED7|FD9I;)w_pL%n&?mIq=-Ol+UlFGY+;S#cx4WwC zfUW2aNrzvGjWylx5_b4>e~O*&KRXZa|MvMIkE?ImR(!-d5&1(3yVV+pd9Z?=w^Jy_ zkIX6G7Po?S4L9s&BzxI1xnAisvVnJe)IKnA#dn}DJxT=o9A@(GHi-Xr!?^SL-*2M- z7q^K_kn=0pL!V=hce`cY6w%o-uTQ~K*~~S1eIHlMIH)?*MQF? z%uV%m|03o8hteU8c?KSGXQd|MSTJ?w^&obP8%;w(Nu`gaOVU0ImEW1)6Z{f>jRy#P ze!>vv`5Ebf$l_iA@?Cd2W+*f@>$uq?dsZ|AC*7MKPDxr>dZivrm9tdCA~7x`{!(ajK*VDY7qDR?Yl93~2Cbdo3_auVuE9)rmZ*3_XZ z66NDwgM@0N5M0J8-zF~hnF*q();Q5C2A6n#HsJ^rwgLEkMj%^%U%@*Y7rpYX_v-0^ z_+G>T)M&cEZlXlTtQ6&)+T?0nZ)=n4k4U3ICEa*HlxH+q9Ek31`N?qg|$9ir23ofW%=%8yCsW}-}<>^ zW8}s7eSOgKkRbjn@OpAS{L*G0DvsW)A%x7mFz=@`5-XAWiPQ_Xg(qSzMokAz&KL~A zPlo`3?_X2yKzwRibgcd0@XQrV{DcB9u)6X{Dx0)=b&r(PfoCf>Z&8eZFUvZ27mIQUi>)ZAsHpmXM%V? zm)o3`Nxxd~=JIzx5Y3Zg*i0rS)AyyW&t(G**DN2esdO2HXOJN6MWraw?92781pt(P zN?<%$rD~Ovx@1q}V4YFhAAszC{oRu9LCV=@ZNd3t(tN1TipA1g0ht?S{w#PYn@B^0 z;4=8iGeGPZg_HdbfqX(0m}Mt%gvxg7Vux>w)kMN+<^$r3xb4=M0L7Gy zQVMg8H_Zz8gL5->z3e$o@qO^2aj|Ld8vx-)(?w99RaX%YZADVe6W1#rOs%|w|32KR zDM#Ov{?g5Re)TJ7VfPS*^mfdy+jQoc$$_sEf#$#5ZJ#r6lhpPJm#^tJrI?Wi??G!ZVSQyq6Cy5S zpU~d>+p97(BVCpe1Hz-eIgRlBF{gcdqxUI5<(3EB&4g77o*HF>WALx{XS4PEJ8 z!{U@Dv$nn3rt2tl-XWXH)yuo}?fty>9KtwhljEL9y|@mLdicvEXL{q+=olEHVOP6+ zXnr;N`g0=goa!-MQaae@)u7{)gZ7Unr5dR{v5}GBU=p9@WnWMPf@{k;jR>Maupi#Y zw=-_|0I9bOP4;Jv!A(5|WM(_gtxGed%Q+|~eLASs4UHE7QR-0lfK1nDL<2!JqB@cc z2hH0>tbeR|ui%VqV<<=dq8~1~R_sXx8er62^0js@1s&Xy%WxK|z*g$jH%eWUqfHWNlxxKwgL|L0S zWv~QJMfzabu2og7MawdA56vYY&QZ4`mG{V8jaT#oIgWq4VX|78lXudDS zAD4eZK5*w?UAFEKQ1cq29^u2L^_4Q#Uq7~y5Nh%*IBRcXquqXfeu38F95Ju_^0F9P ztgog%n)~ucfU0}Xz@at@;^lxU% z_H_qZqR&!4<*B)5~bwgj! zi|@9_7BjR4sSqX6wYYyzA8N8*pS$)SO*D$5II^%*Bmu^5G22;f*=sPXnz-JEahLF+ z+%xVHGKD5yTX{kJ0|`5?9?r~Rb+r@digzw7y|qiTI8uf`{nl6wdOqk9xG9p%JSNnQ zxGY$j_Fl~p#hM=;kx++5HM~GbqNFfHu=RJkrzd#LeQ60?Vtw1dm-6(oWvEzaws4Q=$GMaY4|Y%6jQXP$dQ!@>@MZOPq~f- zavIydd^Tp2M3OkNIuQDN3q*_XSj+8!@Y`I#!9JdP@NCgptZ3#?K@bA&tao41mssN_98so43?B!h zrE}z$H9&=ApM#A@Uzm@G)!$|R2?0w+#!=W`XSY?>FvKtK;)wZm#x6Sz+#oB z)?S@0@>1JZvEe1`)WhQT7Zcu<&(Uy5iI30B7F`%fc za2*J`w}%E@!t=%?Ncdm5`M_2Yn_LOZdG`Q`{oGf%I>#_&U+lMu8!>UcIaV`V_lsFK zoES+cu!s!xr|K0w365=QtJ)CFA26q~Va9f^zg#TnKc#Y2^b@w<^xJ9MRBbBLli4fj zNNbu1<8tFCDA7PtZzghmpx9pJGshZh4KQGAs4}y%`QjUu=FulqI>(yHu+rKfndSbo z($gVgPFn)0gh9E|?)jIcd(ZH2?f%x_=bZ#(4@Liyfc3VWWBSV&ob~(6d(Y&(SLvoX zS~@@~s)iTzF6?4YiB(0`Y2}Sujrh~~zpt3^BS>$b?CoW^DjyM)N=IuDu@4J;g%Ttg znfT`xSPwb7W;h?QXxy*|g_Jn5r({OE|0UV$w;BAe$zT73pTYEe9bOQiz2G~FI~t@$ zq)tn__zB-ODy?vg3p%n@qdYwsQcqgSDeyxZk+ z&Q4ps49F(pt{c9QJ@o4mZ)9N~svo$0U>-jqbp8#JK0VYNwbA_(5(6bJ;i3?$M=QYB z7^>?!BjqAu9VG@UJ_RJ~!78H>=vnxW)Jt#e1JWrYa5^AbRMSOK9ePXjEffjc$GUxK zYA(GaxpSNChXO7mmJL+_{|3pn045aSFAnQ<0Enq&k zz#Nqqg%PZ0>!ZS#2g%ym5R_`l_oPC?$WhcS8-(?-hZFlNTHVXG@QeQQrP-;_`+*1A zCUq@Ool1Q(hU2Ohy+Ik5%}d*?G(s_PP<$z{h7;sd84XO3M<8RyuK_H27(O?@2vw)% zUHX`C{_MFaqb@Aavm&5MZZ*(cN3jotz78H6`bgC;#j9g3KS3zixpdTkC<>3wwe|LL|oLbmzzX)Ch;tLpadkjbs+FSgscR{;j z2RG_*oK)4Vt54wUhS?S$yt;6ji>f@xmTGT07~~-NeINTbvMWVenp*k`73gvcsxnbM zdsO!a#FX5#Qan}W_`NJZP#NC?n73Dh>_vC?4-o;)L>WKeM#Qh%x|ec7vxTYRu>N~= zqy$VpgEb0Am`Oha$Y4q-u-|?Y&!d>rIWY58KOrzqXi85Ks1E^Dq(>BCsGM}vL$G6d zjvIZGF4@4#^U`BICKyE#rgzvLC-=KZl}Og?IxCy9ovv|;fQh)klnq{RFXLe1Fyr0b zNEPupC|Nl8foG6p@l)0pxsxHPmGRC;we|~Mzk0cTQ&@OtGRf8kU3E6(M2{Q&drphH z;rHgNM-VAMEpQ$-hCLDxKW^P=sbBS@H0e`ZFYa<4Lg~)!>pU0kfQ>>sS$JbUW%zJX zFruC_Q;FPWbaq*BtQa<$cZG@U;h5oOI*@@>HXr=3PO>VLIevMqew#MG#}FjHdAQNR z3g!Z&0eHVls3vCXU#VaH+xbq_fMEKEzx8g@!HOAT#^Bt}be6(xwV{~9z&PM14N5QjxF;k+sD8KZ;~cH-TUa_7<#d8 z?PLhd07b}B=7|Oc=QKe5U!VusnGTveBO12Rhi!wCua7($S>?pgDPZ(4QNiP8RQ`l) z^k=Yk;>b?zOsEbmC}rLl5UzPf+qd@CZZj~cfPoZokev1tq7IB|sYsi8#-na&YkxcV z5{Pc|dJMom$G!2+)c_kSgFSXO$bQ_R`NgFEHTJVQgW5}bf_9Dka;pw+Q;=_HPFt}E z1U`4E(bD<@9_=qTFLdRu5bz_t-=Stu&o;g@4=F2w+adkNor)q4D>6cGV z?)_$^5@>F&wntHQi{@MmIJ&*kz%>hj_RKwlV606*CvZ$5h?hOcD+zNpkyMV9(ON?A z1(iX64(_$qJ7Dc#W$vb+3lVY*foV&@dXemNs5S2?N+d@bHXuubSP_8-<%8GU8R804 znt;My6_G3oCB_d#6OmqO&}77ijr*1Agnk<`ZEnnYO=peN4r9}#?ZlQZv4Xyy+#SbLs+sT)a@+iNRwJbUtGzRg# z5|qqCQ#3|%XnQ13@>zfV4LCcLfHQ`jE@Y|AS>)!cY|Iz#E1Vsi3i`Xha}B;{X2Z-f zrlRJipWyK=JYL&~ETq+)X91;5zMUBsA**Nn5?^ao6dtL3HVU$IlMsG3&zXL_i>h%r zRlicdDR9Xo&TRV6GCA#fS8yB+$M}QduDHz-@ld5gtP3f6SnFA14#${mJl=5lb#l6zBA5$g$#jwJ`9#Pf-M5%`n~x}}Plm0x=G46L{G zEX3kNbXwN?Yxoi^22KeP!+BgA!>Vu&+`XcHr0m=2TH8*As)!V9EKMQ z99{TNz38IcvG(cP2OSUx4Krs$qeR{sj{Xx)`+pXz*m;hB4+!nF$)BK2a%zDVk%KC% zN?X04o&8_*h_GWSII1BJZ7u;@QG*-O=%6E8Z;Z$0Bj+teGRx5-|E$cd|Iz1umHpS< z`L5q~={rCF1+my^i@%{Q;@-Ge9W(2Zisx8#R*0|QyD_V)rDE7fc>Tn&()lLh>79dt z?{FUgD8rp5@l*zE@@O)L7F?VD!&I=-zpb zKPS=tZ|N8FKSojWbKcpHHBu7_Jm^(}Ks#j=ipKr$>BY^>{Dh=kHv`HFfB0y`>`kuJRRITq~|>@f7fPss8K5WK;k0y4ZR|DWGoWdLc@|GW*h^Xz|~zN@bOmg+wiENl2g z+sm8J-SA9nm?ePK8ejYf9zBn3ksj1Qkh5-)4!;gRxY2jVv$x4Ay@z4zrDJc(gOg6! zf+*;glbFZ*!Jm-!H5`q~L4Nd-JhJ|va@T%cc?UJvL}nti?E~xs%n1O3(ib8pdl)FX z>sF{u5Cj4lk)h~c|0k_<`+t_wiYOq6+}`#QKo9cJL0B=0N}YvnbQ^)1bp38WAyf20 zI-Nze#RFhX>mwUP^Rbcj_63mD47(6XTkS#Jri0+a%1SP3#6S&NjB)_92Fx5NpdcoW zecxB(7{4`mW34U}$BhGz^AobU*#lE}>*m^sDCU27#zr^)>>2tVA=Cv34O2vNOn76> zE%`1p-nieI?HYy)`h^UCr`sW6K+RuaU01+Yi`fJ@zDHXfc?%k+x_}MANb&V(`=Rl-x=rJHwl8%AOuB;L4S4C~~PF|d>AN3W4akfQJ}hzpr6wmXIE-_-&?mzo!h;8 z90;E!d60!!AKS+z(~s^)KLstWL6>VM`%6EDO~bslVV|jNS_NJBi=a%G6cYf2%$zaA z?-(}JG#mIfh!-*gBMenb0C0)cNLvRj8F`0XL=W45ZuGzNpdTw5^Puf=hb@I^Q_tP1 z8`;`Dgc@NAxd75`|K$cjFrp!pvoZ=td}0mot8NYJ#y^~lfdU;76`=!ik3)ezjdCj_ z0K1@0QJx)PTsraCxJRf;s&CJr{FV%WOE%m__RW)Morsi?Vnm}fThR6L2%ErOeaDM? z5530e0rJOODM1B80fGZ>6P9@0F2F0Di+|1N`a$^yI0789bls{_N(*&v@zTHk!!#q5 zzV8SSKI8YMZD4T#in1871eBXU{4MaScefm*u$H~`QM4Uu=H;jSU2R`ff2`-JoI{ha z&v9;Owt%o2TgLM04tL7|i*7}eDC);9Acw#jeSy}ko-=2TPe8SwzgkH5VW8BmTOl?# z!Jw#aBzOMdcRb-UxsakJ`Z?hmGrf!#PPRe-i=TQ2GV75s5af^GBs?AIhFdD6%l*r* z@?O-hzK+ph@=0 z_G8!Ht(Fc7Dvfy+A8dEzuEdS=Ap8{h6H<(>H_&Jp@O?l(UZ(V#W#)un>?#Ar&(!pn zXZ`ecJ-tt4d$|qFq2jW|k=Amjo}%=rDvu5ZLp1FM&ACLvl~R^U$xne$L9jgZql?#f zE2k$}yG)ah^UmN#leUy}M@6O9sV~dUZ(y0g8BYWHmDR^uf-fix5kHQa<55Hdm3_sq z&}>Ho>`C_@H$oIg$fEwWI1Z|INyixRWva|CN457S9wbMR9mm%d$%7u(-G}$Li-4?- zgwtM#k}H;%gQ~Sv{3eH#U=~t{{jy} z7Q25!XeHSXG)Dm?Y)HF;K``F59ufpl)C1#Mj3u zrtbuG7c3R8`|hz8`P_OdAfYh{xvz7o$5+>`H0Cho1p{D05piSi`8AS0K{Z8sZS!4$ zT?1--W`#QUglta0u~CglzONtI<=Q4aufBD(41)As!j#|_PGON9(8H&R!_fw z;eji$-`0Q_1C$~<7$i@m1}+x4AG`X@!^{^%$|~sXuzfelCZorcj#WID^5Ed|@=pIY z^VMHQZu7P58Q~mwbJm|^>n7S<)(W`;W0O*j5xTyt8L*W$w1s~ebh$@)WN0eKWu=ck zy@q|?V)sS;To2Xck!xPxdeWCeZ|3nnKx#Z!to6n1<-DBXpj_t2RBq!51lJ~?ImY)Y~ra|6kbC+|s}R5{;- zeD%y)&K2_xXj=LsBJRpzt&YNp{4Q2*>|SEkipAU!1<&VL?~;#Pesf~0^+8FuwEMqJ zE+T7d`r17ntV;)=fpBr7YRCWRo_!}naVJA_#|Hk}Z9w$P#+%{^m^?$z^2npZKOxy% z^`5S_*(6yw%zh`#pmLde=__Wx>?gq`zuDr@`)Ho);U$N~^jybAzaSb-9ond53q zv2qafaGLrSmWPlz6B%7SCP@zHj=id0XKX6}%t?B0pzuMkgB zN7}O;0Rg!*$N_NY+iA<3#5Je3D^s#l^wHDN3JO4})+K#S%iy|EKE%?2P4P{Olt z6~46_z>_1p{*!NV#FD0E)efWVr!lMh0Bl&jN4}W#&=-14+FiZo065HT|E7gyWuf}TJ; z1uR)q#IlQkGW>{6YTO|P74fyq%`oeu=efBvSbB{0)%F2s@M>Xy>&Zon{^ZxrEe@&# z%4?9sHGv?DvKKq-g%p&w=~VAjg@Sg;s!HDW!l)a8N{fwTW>B5@ z1L6smFO|LzHO4*ZK)Kz>Ob_I}MOE5g2V0?>1E?TlKaOBeWdtCTm2{Ia0HdIzQ0i7c z@?zyJI)C^Wbd#3)D@*N{N)3SD7NJHQLsU}foUl)@&9(Q`a03sL;OOD!`#uyl+$Ffy z0R^r;+Y{ho{kWwfc-Z!Bc}{Cmd}@U$s|=$!q7mzn05Gikg z5;-fZi>b@GKT47~T3#A2sU6vlSNs?8Lexaz7XA~0sDs{w%wd+Qf+;^Cl4!tb^QV1O zBx~mJc~7LHSjgt|eF+Zs_ek*sj{?C8G|W#u4&jR&f-dX$!nY<-i9y>SXYEZybm^Wx zX+edUwF#ZlLwX##)w0r7?%4N@X}r1Q_dO*xqPG3KEi zcheY-+lRva4(2q@?joq}UxS5sBZ>jM^%{(jaPNF15tZ*w|3~HI$5kMfgx&T?i9rEGYw3j4@F5k}s-K$WAo-S@BpVe!b~{ZHJd_hfi@n@v=j!q4@as-8 z3&c%&C1Y10OPr)b!s1JhY>0QYGEo6TI#yeH!7S&f-upMkxEEfCC%EE``KmG25LM}5 z4-MU9!)pgZbygCm6~3RaP+C6hHoxn*_Cri9k|PQ6i{2nti|~sV=3mOg zpVEcCy<7CTw=d;es#xKP^BjXg`$+50iK&goE|%*l_+6aL%{M{LA~XnDiz^It`bB*V z%Vzb+{S`w^bJ-Nn65?K-rU#4Fr+hx+n7sV1>f2-(#g8mK#*tQ%!tK+fW;)PkecimH z-2YKA{~eJL!=AAUDcJ|topw!d*w4!@v?uj#neg_S&K+ZcL`8XBsnP_BtG_4O{G)#I z8nYCYGP0MT$6D2{IW4cOj7N37Q2dcu{_KXh7<-QCs&ZBXzG2DAGeESbGb|A zM|yGM#?zPcqgj3Qzh38>LxV!>$Z25_0ezbs?r~Q7wS7AxzJWm7#CI5cjQo^=GVAg{afaKOxX8tFoGy>kYbG%t4+lV~p)D|hd!SFYf@p~>NY5HR@9bk}yiGyZpi4?AtL(#9i(XE(maZ?_}iv?Jm4 z_pkx|k#uK~3})mU{VqDtS`$!g8YeDihVXxoLivfp)MtRI@D4`3(*}QU8{lXNx=KoH z4DsP9xoOke5!2>c^bR%Vc%SRBDs}j#Z47qoHT%e+KZ#TG4-v@LV13p>E2fm zsLz)w**IRH4-W9Cjovh$Dy@h2->Tb!$6_Umzbo7-iPsXV-`8Mpo2e0q) zv17-oZp^hl6fl8yRkxEhC}IJ!1Zx@i!DvU?nV{CJ0?18d6Q*bgv4_gM+kgM#GB&R* zd{q&rqTGAkNjE+>;}9hi5lBwv;%k#IH-KC)AiFW8xF{qG^-4l*FK5Mk)!BM^-ar$?6$8Fv`Y*~R0!IUENiB>fvC!(d!i~y4vPA_>~nygkOxBa~L>Fnp` zt%&z=lL!COWU41Y?-#sFypeR)eapNQ*)a0K;~_|$Me$iXZ58Be+|D|_PMsU#T9+g% z18Uc!-`)~OTv_)gme!ySx8#xoJ0$@?$&$;?Fq>@j@KzkN=IyIjH-*kB+V?C2QH=>e z8pKmMWFo2Jsdnl9(ks|90;9ib;wBCB@Hrc}DRG{&*Dv1sxNqAhI8O#6eIM7^(~<8) z&npe%06P|7gj46JN4r$C)#=?4i)~guacu{pv|LwR>6X%E8LVkIcP2)H!0UaDi|xzU zFVdl^$|P7VsG6aFmk^L{^>tM}0g(qQEgFHlX-d&#MAa<7z$dii0=CGLmg?tvZ53+=V|mC8Li z=YQNrOjyac2Z>9K99Rt#VAe&Xu-xn*LNrJp2;g-zxG{0VV?C9lxBso5 zIG?eI1sg<4=|(k3n-2{Q;_d~!%l$3b>2|@UjIr}yKrs^>FiCfKc#6dB$8WS8`AegS z(vJ{ClY>f{rHPfQE#9Y9jX-IXJ)@ZZjRxFddG)>BQkg?{<8R8ZVQSjRrzT79%2&TE zuyGGN5v}f}CLm7Qzy67gCiTuy6gmv{w|V9>9@PZoHzt&wG%xis!2C!Thcun0lwRFv z97#ZAVt_BQ{|R9Tg2jr)rX8~d>w?etIot2I3oWc3y9RWgu!Zc8f@}f=9yYH(+8z<8 z6V?J>>VE`Vm~};5A?b7EL9K#0Bbh#)Vdux zj#%zI8Q(l6?$mavQ1+0RbOf%1ITp!gd*fKylvA5>*$@d)RxLAL zz-TKo?$MV|z&$z{S5W+MJZ)v=HWXAzCUJqX%x{Lz#B}i+e!p}*cuf&&M#NCrn;X_O zc@y#uQF{?}aPq0Tt)BM=neg@70tx`H4$yi7o^_s-gnp;(AQU6vK>V5yN`>eH-|SaR zu77m<{uu!cfyB8x(#a<{CSXPI&#mVOthK31+++1y83+2N-~{G%gorgP~v zmEB{Gh0h2uiLT(m8GpsK|`e3qENl^^>7;jeda4h$2=eoX*Mn>=KezZ$TQA zUuTV+H-~pg+A$t8a;%i-D#^?!$K0z4fH_-QJii?yChCMuhFS1lUm>G5KNJ5+Iz=C`HdjDy|2Y%StMmt)#_NgM+Z34B`p4fbpob~b) zyRog^+KIo%3VP=nJ3Fx*8~F3sfCchQC!1M!8gYN_SgB~h?Fb%|-r}5dhI;xh+aJC> zU@EM2Pj<@np+Fm9?8)_rM!A7itN{7ag_5ED?IyOaVQlQ~+=2K(LgA4*b@?)~szdU( z7T0oQ9^=f8*u(!S{i_|Zg@3%*!eL|06W1@KKrD4S7ln>Ayu!Gow>8*Y zbcj7AS_w^(M6i{wkG4qS)tG^-?whi=;o(870idX6{XK zOm{Xs(GpY-w-v2rsknOGzT{L;aL^^v(aI07TP@48ioT}|`bRI{cJ9AJ zircY)9UJ(YY~YjfIl-?mIHtY3pjt=c3*KwH z{vN6C#(yBo{Xe=e$TlvIHbXpIbLjG!5o%(M`l#NMEOs{EOir}y=RHn?~7ut_yVzqgT+PMn`qt5FHy@OhIM;lLWpkeV{h*1SjG29 z7ra#S*X#SV`tJ3nxvX+-eNTMo&>H5xE!(~P`fsP<-@_q})gcYcrFx%;suAT(_dVRX zm_6h>1jyKkdNW_3l>)old`6!!JVwd2$5PbkoyOocwMfi`Q)X5n-m3X*>>1Yy_@neA z$+dy=vxW3AjR&_q#pm?Ks)fn&%J9r4Q|&bFAy#kOB<7>jhV70))xk+L)PW)MN_1TR z<=aGt!)` z8@%Y_J*Du(eY*u3$_m*9Z873QYiGX(EmJS%^Os{T@tqTIdiBo#GyU{qi}#ltcd5xl zO}BHmf>`RE9N~S?3cEsf($z#u_nAkU>}J|VI|>wk9tDb>zS!xDzlROPpKLuQ_-?{( zxU##!c8`qChC-I7@DuTF5r=DB_PL@qax!eS&2#q6U+>H+CJGzo;j>hI<1D#kJ|3rZ z+UAd8A&t4>`YsApci&}Yes{jZ9?-(jGgggbjmoh-2R9*A)(Y26thl(47O!R|c+R9d zy&JFL1T}&IG$0!C_jvxDYwV1J|A%cLL?kn{D%k1pte)vLA<9Fm87tm%;nMQ{j4d~} zdJJDV=YL6)^IXesiGS+;)!}PQiMnu~xke^gYQ@As>VZ~peiiX2Bt-J`XE`f=UT^=9MpDh+h zQgBdE2a>$`;6>=7*r`Ecn;A*Iy2$umZqNP_H9i(2nTuWcW+)c0c7J7) z@6~d~LZ4;pj7FPGXIM=Kur{C~BYL%Z=DRn7tvJSxYEO(0R}*?xr%SAO++N#sHl0z& zCRGcOPr?t(CLc4RJsT)C`IFuMV16ye_MY&#?x1;Bb0kvEK zu1QvnP2=r@c{!?P|0_U7JfTfJUJj=&vu~dVokdD-b`p@+S7C_)+lPBOzkmDt+3O`z zpLedd({4K!azcA&Ed1^je)kePKIAXH^!Ikzv5?(!_JE9PrtTgGP-w! z{P$cQFJhXjy6J~)gz0x#V1(UWu&F1Y>}GFvAFPLpSjnoPex62&DQ_P(;AC|l%;paf zP|lr8_a`L%B6`8aCX5EJ2L%$9rN0V9E@y2Yups^9_Y;2mA3N9EX}=wN*%=?`9e=Rn zPj}{zo%wQSeXz5>+F4KTteSroab#js^|rFDGW7_T2TiyLbdki9iZ)0-gP zI36sX)m_ai-=I7mwMzbWf|4g%Uo@?nSZ^YmFa2P{lPVsTRiORV+Y)jA_Ugi`+&e0+ z8BxRBkma$fTURlbdMmfgvf)wMJsmYhwVGN_kFx*R7>kb*XM6iMrTXm@+4^rUvbEC> zJ88E&Hn3v@|JnwULn_dW!y$+2(nCLD`2s%Q9#`mHSUD1VP^rOIA+~`l^gA78Z)@u9 zduqN_C{ieWZRxtFa{Iv5J}L#)o~0#yFdVt^JtoEMNi0AFTdA?0tD4lxzF{luD5#WJ#vgnpCzDrp=Zl z>y-7B%1-v(grsCiwg^M^b(B5(zGWXf+4nGG9gOjN2Im}lPo2*1J?H!O_Rb&1Xr9Kn zpXScZ`TV$nc?uA&o z;W&*D$FkgRl4GIW9;8O07#=5HDLUCIz#(v$wjnZ&M3E)ly>63Oa~nGx?fhgSTRt+I zlBL@<&bk@LQpO|g9fEwTuHemnxlnr)&R|hg$R|XH3}}}tq?}Hchmd1b$BX$Y^mHih ziB$nd#TvL`$sY8L%Os2BYL2u`a_o2=my z$H)ZJH>&%?EO|SV1X&-Z_ZzvYJCJM7MP90ue=0YZsHBP->a5kz81__y$QVU0M%LCp zYPGtps2y}t-RW(jiqhcg$5$syJL#k4VhU(^{2PE_U4%*8gf$@80Ko%%e9hrD(@QtyNRZD%(bcdp;M>SXQ3FDhjDfRn@0wnxN z%S)aMj(Vp+>x56ld(?Co>7|<$1~FKB2#_3iEiY=!LCJ)<= zv-unsIc?NBQrN5AHx?_WFbH?b+dG6lIj!7Ts-ApC5;Pu)&!){kyRpfe)9d+y29SP=W&i{SV!f-2gDW>#NaA`rn*fLHPfFe5#mC z!ynC&PXT!o7VM7~jIb_*b@^FsfZz-y+*3dxDQfBxB(KB=a}uG{3e20bcUtjvA{pN1 zh>OTbL|9SJ=-pP3~+111D6MTJ{uj>@#R zXHT=mCW~2+5G&5Df}G*ROvp(08ENIq!8Fv>s@W{1GgiS*iHXz>OJ8xRD*=kj+~MLD zalv9evVfjx{+0$sUN^nBZ3#IkcPT`rKSeaowsL1(OHX$_p_9Kop)XgI+9mkp_`79Z zdGGovtq}xs7BEILk+S=HjxMJ!XSn-R_?~t!jSJebWI3I|f!)jnG~;$vIQY{*a%rOo ztbORcA~M^Ra-d9JT{ZQD&asz8$KIuqlXj>)@Z<%K1T;8CTeGJk)+(r{F@gT!Y+|z% zNmqL^)$S4NGl)}|2%BazJ>6Du+Ybt}tjmfnhif5cicc}on_3^=X2&XE?nWmgd%lJ} zBndqj)GW|UifM5>t>o1rsM#)sK7)u2RuZuj9Y9*Z4_$})Wqa6p6f2svcAOdh6fYXB zbmNgb*Bk;KmKgy4FabS^rvAKx`X$b^=v0KQuQf8GAg`}+*~3n*^W5%Z9;cREP_yXy z1X_?7jNcr4icD#FC=>R6++2{Sq?L2fa{?zlM`bCBv@`8{kG+D#CQ;W+B><d=lE-BGJ|AfG6-{3RE;3oa5{ydXF_;Q~^!}pV-OUYq&^*aGmq&zsFPK*;6gCrv ze?aU9j>@u+gV_M*25!99>GVgdASD!}>Uak>Yz)FLfPX-fV{X%@FI4~fmcSn7r{epk%#_ts_=wAAf@yTjN$2$o)~$3L_@gB9jC<>hMr zdXrcDlrmrm81TA5{qdWD+f1{Zm%8l<@AN~glU5!oDHV3^Y zQkOHVs~PtU42du_RZgFzp}PauxTzg~T-p@yt>JZ>?MxBoZ7Vu_OZBXWRPf~mA0zhC z#BAF*QEP#_HPlJ$>10m9Y*-KV;*ZHz7t}b@)o9c^*7p4$ZT`>rPXP>HUAOsF5X+KB zjR*cA`zr3>u7s5t(GBDL{GqF8#UOtbbjrhE+9Mh}f`1q<{?#9?o93qNLwKwI;97ZA zp05$M@L0cf5lQIDINs15lFXIWj@6u>KUAppdmQYEI7v;XZOuc~VRS#)2{=I2k10s? z!hFmpKET}Uf1Q4~6JRVzo8fr>(`@i8F}O@Pz``9sc+qo!$DDkmjA|WM!RS(xMXW7{S#VG@N;bxdn4rexuw z_1JM^XT%wgzJrP7`(_qf!kqJw#aG)e!}(roV06>clj073`i6_Mv5Wu$?66j?FmhP-Co6O{ogNvcH}9nTvG zmZhFevPytbQUnfO-Y~Ob?Ddw!S=xKNZYk-Cz0kgWh!mS)h~n(X;G9lTyD*%?z3&v$ z_N*f03q8i(uO?v4-9T~`2pP8{Uc7I;7Li5qVOjO)ui5uT0G(IS@5QC}!2T?s2NT}u zKeSG-fZHbdfW0~U8uoHiehuJqbWknDd{#4Jr{A0(y`xYyR__?y*5=SBz{H=eDf=Sp zqRVxKadFiG0j zn~ub}&cIaTmV9JUQM+WYyBfSVne9`dx1xr)Fcmb=_RLlqM~J8E63Epv&_!)6Pk&Pq z%#BRx(&}+4cV!@{*r*O$5PQ>7`XYme%M|+JPtv=4_pNl1e>HK}&I2%jvjn6753u}1 zXowd8_S~gK?DqmPsf@*1HO09+(of>inubzmKe{z%8q%`8Hi_c3%G9@Xu4dzx^HM-I zzI9=LymVx15Cs1pnbo}PrT_}?G&nSn~L5-nzTM7!FQgy#IiGR1kx3+;NmpP+;O zMmippPlD-d42;{Wj9uu$UWEh7@s~UWOZ9rbw_~J2U zA+gLHPjfs_w}EaNJmb#`Vsk!EX8$X62_W3Sb`|1W(Jx@uCxB)D3s7LG5gR{xg}h8L zp&7B`fVj`Yf}tE|+?{LIJ{5@!H|Z@NdM^dzlmhCjkF(Y(q#FXT*?{2V&HgNch?&aA zv!8U-q?&rz@%c{WBf}?mA`5eF*M*POYL4kG;thHYbMJkJz0%ZXoQ(rUAg*JxR``@3 z4koy*|9-di6@Y4uS=spO3|-yBW!bB;Y5F-t<(qv;otd~)V~+La#MKtx9d`CRQ5#le zOn)JWS@G5T)(3!kvMhRw{vWcEFg?y?18{Z}}dJ zEFZVQhv>C{iWN(>-&yrDcYHz6L2-*) z2&d*NIj|t!40312lesONhflv@6V$0YG{pc7Qgn@$YAau|*~J@t_cAm*{xu-4F%I5x zZlSCXsY$q6HoGInvrnRnq>m>}m#8^CR78IGbI&97cPS_;#$&M`L}3GPgH8B8Vr6FW zSF#Wxv3xq+co0to&;XPK0l)tb;`e_i19M^%nIqP{-}F(qGb9Ml3%=O!EqVa*@hX$^ z=~7osmd5d}!yoyfN~#YOZ@P9}FcC^8c6oa4i6PZIfOIG@-`-q_$~K?t+WnTNvj^&D z(Odol5gdi%m=ul=wPFjkt<8yWtJem0)#0q1D~M z*Pn4g!!u#Pt^Q4qQ0a4CP>Y?iz{>SQnoDwx@kzZFuk-vyk8H`4ikhDubJe*T4V>n< zt9LWPCS!TcbwKvgBE!;YVar{4{XsIdcLH{I56G7B;5qgK?ao)rJ|sFUI1@M`*L@NdbFan?XQ*ys>RFWx-m*>+Xr7cW*`}LU6nij=tl;R&Kx!1$qd#r@XJpZZW zV#8xCFQbY%RMKsRr_>Y7dz0kPvexWf#8zD_8Z@Qmk4dXF`kDkoCk`J@%L!2KPX^`U zb$eIoq+k7}>PW$I_UlRAYVJ$-ul4PU)$mntFxGAMpzax$lS8TuhK^Ioc9~5q$+DLV zm2cJ#;h*0k-5^4%Ef(b9q76Hm%g9w9fB}npd3bmRAxdr^^UOKLQAp=ZEvik=;I0Bk za%c9}xK6r{Fa?{18R%^m9y&G@Q(C>a z7r=#fV0qBgLpejX3F&(AaS!G+3y^OvCMgR$m9^ZqGSo^=ee3%wJ9+Gi3rooM-@aJU zt{J-?@ANRg!fJzeP%~=JRN3#Sr@?NC|c?%CdpHFE}oL2>^+kj;T%I_ z@(sm?B3^-Ovqi98@U$NmvIx~mz9E#@*YTipD z%n!Zm(7A-qtiq8da?-++U`$n%vk}-e!=8e$hOm=G&|>xl5DM z40-}c^ViAGgUD>lxXUq82l6Tt?5nSCNX!s-ADD(CjQdZPyk-$EDOLPNyFeq});#{# z+B|;pwwMY*BM84od$uC^Gy3FDSKib#>oV%J>C#q57O>k6(eac$FO(wFH_x0H^e+W- z56Mv~=Tra%K%Zd#7DjYjT;9|_phY}bv$iI#Al7Bb?AlH3z>|FMIW2!9L;J|k#L$_D zLqLbYg|H2^M;<;K1Liv?w4R(NNFe_w1fFuH1N30`REZqb-S=%u%RE(!Ji1D4MRc4g zp9a%K@MTU7W=1eH>@%TG=+px!QRk#B1wkyINqD>gomy3drk()t0g{ZX?c$eB&ma`I zS^1OO`QPM<|JD?x3)`P+rj`rihixQr26)8Qg8*#&guuqSqU@_(i5aEPm`Lbr3+7UK z_k@Jqr@?2JpfO?@2HG=dTFbkG6k0xLZ!E~2}_Gdl7bAVWQyw?tDkonQ$? zqEdTydMWM<(j?)2fqtv|ha`;Uy^|G0RUf(N?>yi%kXO;Qf`3(}qOuCYE7~p)SgD_b zmBQ!irhOaUIr!<|4K-<7T#*t+bYV~Ik z^mWUMfHe1({$@*~KJp~Ee@2SM}H_r2kcTkyUQb*)7c zK_x0)UTBXxtn+JUd*R1qS0QL+9G-CR`V+$a1;MrbSG%^nO>L9VjS(2ydVFO8DNEiI zT-f+lfd)$z&A84Cv_6TED;2=Er%01m>yQRV7-=FHmboVr0Z&G>IZvb7+la+;1(j$X zWPsg^Q)t6u3y5P<&lEGe`~+=v^2i)wo++a=YKQK80<*`^%gdT07dXE*7vTdwV-q0Y zYU`tGHp#ozmiSK!yB~UVP59ZD=fn^c8_<4pl#`84j5dF#*$iZ|R#khAZ#Pia7La#D zNXtmu%{#}{OL?00uQj;u-;AyS&$r3xpQl<+WHaI5F|Uz!v4Gaz@sIsLAK-f;fcqNh zMe!K!AI3B8eS3M8lv?Fq=zhcI@bO%rn}Fr6jZ6BeY)*6D^%Z;Z2J+B=6++A7Qe#q} z9Nxe6y2owmb?2d4+{Ivz?{%-+YH^And6_QOPk1!#QTKuD%a$RF59`w=$CWPuw7Vyu z#6-xrt0`ntU#s;gMIWTurqVlBlIHQ~tD+(`0DG$;%$xeToE*NhF7mz4xau)L6>eh- zbiEq_UGFqj)0O%iX6$Tjwu)x}YSfJMSyIN*IQdNuUNdi*2B@#C*k%#9w6M@wKTsX$ zo$mmJ1U?mO@yA|9tKeS5?oN2^lxV!c6eI|wCER9vK5662{2cXKv7xBuG`jN}Jq9Uw zwhHJ)AdD{isTz{;X_q>ytfT5tW|QTt+q-i6a8-c-#*)6&1Tuoh2lsG7m@ zK2U0vd2wSz^R3H_N5tm*jf5rY?PS+%2uMV%TtH@R(87Qn3ov^oYTT%!7~^UZU%Sz* zhpHUE$)+`Fqo>baP#-;dowKX%8#cG5oFq88pU}xsxS(l(RLd>z!r;5+9P=WmjGE>< zDb8~|Mtx^wi%PER&P{htkq5~6@QX*f0j8Eg9H5SEDS~Jc@8*9VRj5?@*1>seo#~q< z9LGCZHR?EG41nf#t0Cb`y?m3zxm+}YH_MqI0`-iDyDMoT9mcj!%#Y!T`DlQcSN#St4U zVB7`>#0&Q9+x!;}$>eCU`(VuDJI8%FPgYdz^J#+m$-If+Bd%Nec$$f}k^&)xZyLRC z3NWU>tHlM?Po3Kh5c5Ox2zE5W)$}mCk|syz6Y3`CqAH5hVvMSHU&~anvNg#!m&M|% z*FT(-+l?P$Is+#R_W1D!djPlaH_9FI`*{8>tQ&s9iB3g&>JHV)^f}r7|9(WSU3Rn(6Qrh z(&KsF>xL87-KUdG_B8lp>ON;T|A#3?(WRz7lh({%*MZ~#GLT@Pv5GKr{%7d=!H=5! z^}GNrn!a6#;cHnnr0NNHt)D~RXHqlZ%z&WtZ{xHM2{e6P?TT|n-EIkeVt^#zCKULvqN;G}s~T6Y0zcv9^}f?y z{Tu_4;u7c6#JL!Si(>Q&91eXs#03yy1D=?-8CjPPr!N?~EU1xTIB%jp9@V>hD_`A= zy}bD0{1&t}Aze+22E0%woR0+cp z^rL(Nf5VU$M4_N3x+f;qdoQ5w)O^3JJk`G==d;)wjT6@C= zUl-Ob1A?}K_P?L2D9a?H-UpX$JMyZBsJvvmZ&`}X*>tkrB%3Jp@qjVAn!K6P>**+w zN;&}=%5zUo83Ky2dCByJoi>cjn8rCsh!J9eUaeO&U?m7P?CNccfAkrJwH;gqbyC6Z zE3JYaL0QLwwdPG%LBZ#w*S|p{RzYhoNw0VstQ1^g{P$kgO$!N!XTq}71TdK-8227^ zB17tVt%ivvrGdlj1Bhq)!|jeYnR#S!Qcn&-iv28rk z^PDwH{FQ*&-2hX?cb9Nl$1g&;9b*Nj3eyU*`E*N}nTY}#m-$ygwRG(9C`R~>V2Z|t z)8ej_O5JU|-JR5nD{6fbNZgA;SXSe#n&(iShT3BPN$}dEmD;>;-oj4s+VW9*A}yoZ zT5uK>qHB+;eru~Gx^@ZZ|MMlYP+iFTNe?gPE<_*#=*-nphR?rK(;TPQ`!KTb3lqeo zY!=~RTJEFId>j$jKg~01DcTO<>gTZa_*exlt;vWlLho)k14FyZ$(v_xS~Ek_7UeV-=LaLiE>P(_8z0W!Vq(X^Qwx z9eec15)s^Vb9h15Wtq5-3BDa8gpKXjz7MV7dqz{4Kj_CZM<<0E9J7-y1y6CWkRYXn zt;_Paq=%bR#jicrwO}Q+c-2YqQc}I;E^4Uj1X|%0?R_p@*j@#mJSWbp=|s2%39Q(B zCI@tIHQ#Q7R#e3I+)_$o-?KH`Sn>8|G|SvQucm%Q79dwsvMY;0QqOIolk%X_q9bO92+PjQX}^-z>*HeE-?xyf=Ahdf&ST|V zLbeT3stT(aI|BA(k2!6=3OY`o8wy}fT76qEr?2g@P5(~jDIs!cT(uhIyMO7B-9&Kq z)9S08M+EmE!f@N~2s1xrdU#98l6v4FEPoNMRv7S*z24(nCWCkhc_`4hZf;>4RwuD$ zOkrzV@bSO@VpNHCmdE7%QB}|N7~0UIYIcj;#vHxozw~znqXS!ovu2w^Uo#v)!ClFW*QHs=x{VW&-ei>4S`< zwonCRQf)E-hzf5ECP`ezCDN5H;G}vWl>wdpHm5M@O0p~IW`_3JE-^~MEKag=$lYV7 zNjxf(#%I}J+h-wxajPJDyLiA2CGQ8)EYM&0+Sc6jrj`7cBA!`&5B(_NRA91a`{J^M zo1(Bf^19>9vD_lm&>Q{so$j*nms)CumM^Uwa`KK}HZX7&am_3vBKHw`pa^=K+- zNEZdEhi{JBG7}I!-V3NiDIuOB@|EwM6V{UI_lT)|hTE}1hYpLOIpEc3_%=~{H_eWH z61*_}D7BCTkKqe8H-Htp_h**%vybJSP@MHFIKO)OSj3w}NI>YiFScDjKrky=2)&m@=uo=h?j$ zTaX-)O3C*_s%?C=u%;t+nT6oVb8`ZXTA`OBDQ*5X=4qQ?y%>?UYCbTycqJ zE%VEcnzv7O5j0awzO<%q{(RxB(=u{fx6NT^vj~>^8a(5A+F8%0?@2itbYRA5W) zVYcoItZIk+VcA^s!9We+sVjKr8wva%{Py1;Cu7?adj$O1D}}hOrp@nyBVpAR!s(VYKbxzIkk#pnFM_tS;;?hRm#5Zq~;-bfo{ot5(g?Lys zbIt74$lX^#7M{zcz|ewYV-n>vYFkrMyiZwANqYrn{(6G`t;Ak0F9X5`nz7K6D+e+! z3-e;kJjU~}Oo*||(x6F|>5^)n zc`Lgp4j}15f_?sSox{}d(AE7TtM(^2{W~Me{2g{C!39l>%e~f*J{?XIk`3Oexeh3_ zSb(MMvzem?FLTiHY?u`si5VP&3HQRfVwO1n#j1WUbjrv6sSueZlL~I*yLP2V4-sep z=({zQ89Ur;!xFWUPQS3&PCj?7r7Ym1cCwf#osaXF4sc*%irs@!>-t6i6lq~wy2XsI zK{8+acX_vy;!P7bxn}OgHP=4vgCb8=F!&?oYI`1f=e;18UkqN<(-tbwxA8xl2H&Wp zLtC|89wIeNqDX?V!Pmlfo`7F*?Z}7?>Hl#dy}nP&1hnvlG}++!o*6ZvV?GaS&H8e4 zFDl&>z#^9kqLa&NO-Tf>35Pz+b9kFZr z^MryO4i?nURp3Rp4@e!*C3}o| z_L3}hA~0YsbS~gNJe^lQJkxyZGB4%+&B*sh8UN?p1I4iP*VGRg9WvVI4D9D)m|dxF zZ;w2p<3Wd;f?RC{&KMfFD~g;I*OL?L&a_MEUPl+srev9&@TP*VgIB1isi=Up-TQ~2 z;4R{R`^7ljOmOC_9QL(}%09fqqlaqSj0R9oi|*cCZaZG(cb<9*oAO^=RI%qn!_T@fxdYJDv_%YRR&z zO*0fXjGhEd2tePwhGms6-8=-`2_L>O6OOffqMCUf$MZFLV_Z|Y0YCKBcK)AaR;=}k zF`h7^6`cmy8n(d-l}MM_efbhctrza}HK_6>ssuy%aoAywdmbhB@{a`GcUNZn=8h9% zcY_@?)TYnI0i1xa86(TroYU9-y@D#`{=U=+wLM3=L*AWDULmuX3wWA&$%=kSw1*>5 z)VdIr*CQ=8N)wMdI~`ew=F33>2O`SXo}J*r4M%KZ1r`pW2|({@tYyFA z4WljGTREL4J-mDLc*Y@Z_{@v?MZX~bPE-zt)Kx?*E(%?S94dirj|vmwdmv;J)#T0+ zXZ7Un9kIjW%rmh*O9xtU#@0!1UA~Xs*|aaWh4z|o^pWbH%cP!_0!nHc01I(J)e;pV z616hHR=V^{qug0$&v(`K(jxq-^PYpR&YdQMWyFnH=6x*@wO}66__rPTdXBf1H2O6@ z_*C17Ims5CE@W2aZ%M}+7mz2Dp#v&uvnfr=L#XEyT_NbuF-w2HWj8fc^5A)7Y!A9x zC|>P|yY9YSb=o<$qu2#TdtoYBmOm?TzI=TYM^~0r{EWnxI0Ru5&;*n(FUPAy8w9CeSZ<({D z1nS8>cZEjMLsvHtMr}*JR)MKXE?>S=!svBh{>otficS_04oz853tKCo`HvhG(Lyde zb#U3&(Lb;)z?}Q*Sj2TK;$6ED@fbN`rqCB*sy`9n6m|Ae*a z?#Cimr_`j$PIp=EQ6)t$)tpZqKN13}G+qVS4CUNNUm0SqGUUU)crfNcJ>>gzxma55 zzTFAHNp0yY{?jx<$3d8@U~$d7wyT3>fUr<;c^L_=AJ2FF?`Vp=bsb zTrU`Xyaw1*OO4Zi6)gotibXsn7HAwRaUp&~?3vE}+_x;vhA;hq#e+T1J3JIe*V(;# z443-e;5}&%Z%G)c)Naty$MLju=(*Yjwm+ez&yN1H!BJ;kdZ23AUuYB&gq3+>;V|ra zTGAHIfXTXPVRf=BbiPy!$gVeC~ke66gU}c+Sp<2WqSh^;u93?2~g7_4Un__oGu<8YuBjhn(dqTuJ3vtKQabO&ExKo1AA?v}M=w$}t3a?2Q^Y!HYxkjW?sw!$n zy!Uu|n0=-u9_QwqpF_^`t;{tf?4R)ZrC2?npyo>V?&(jk!Nx)_%pnqS%vz|jo{ok% z2V&}Jy#}Xg!_r%nYSP{?64j`I&R?cpM2%bq0Gn(Z+D*v;=zE?k3_tGY=+nHck{`VD zZi^nO5MZVs`qDls9-dh*rzfj`1egU#fElYeVzB^}6!9DB%CAi3@ycthSjz?Boc}dO z;zoyUXHe)lqNXKTgP_p3&=b%6!C6vh*3T z(miCyQ(`p)nIAg}*jB5Mz@P3uV6Oh=Ypze^9=MvN{D4g{BX$m9@9Qbv*B5_IAOuF= zR|?y1=bZcsAg4_F4R_i_{uPk5OMJ`X{Vr<2~imRPrs) zJ=`dvof`BCh#!D#8qooV9rjbaTm_}7-pex~S%9~b)UZr)e_eW=sSLd%8k%@6N4?)C-Vl=!`xx{rVA^Uq9*jCVVn-Z^ zzs>3Rql{WD3jS(Acw%SR5ojuw#o5jQW>#aXQ>Kd)tlWN%eN^gknxRDR;g6wj%-GAS zA@(yuWloR*^=WmxuZdsS2c+8m9wEb@P&lmUc0~2EKR^BK`n{2k=t#7?4#ISlo%KO; zV`xD$w|&~M686W@596sGg{#l-ltR0E0qPihb+H?aTYxsL70-Z&`b)4okuqia;WzNY zTnCg&hQZ$aPqJS2)fPOlyAF8O?DtetwBb&2!XCPJ=klp+wCX_}EXxwyx?t&EtQEuX z*NPjPcS{9-Z#K0h(>m9OJaovTvtmJ~5w;dc*hnXMyG)inlgNR+$7a|?0kK~KAJndd zrJcTccE(eFT%*{NZHy+|bonYtDWXmb%YrjZz}{KP`v^FmI{d>H5AWR(^#!TN|PVJ%R)@~FLt6{pl7_dxwwNbA_IQbCQatQ4D;>Z5a;P6NK?&;+{ zxUX8-FnQzu37o|aDQ*t6xoBb&1!hGk;wpgBF!LTx#T*jNg_=^bvm6)5%sKS zo2U=P8a3U$z3{z-*2wtY@{F&h-u4m=Wm)lMDT z=DxE&{9tPve}gw0wly_8)2^#D*U6#e&`mwYNdg@C@Yx%Wap zV*l=PSUVzSmyiCwD^Ld;+f;YR{`Q@>h*(!ab;hcZN;s5@ znoThGgcpgNMue@+2fYvJySdcP*!+^}&Pk^h>$>xXM@>#(9A>HxV|RKLWatqyYy!}S zT8sO?Ct=@wjqFL8sxqLawx&LCVxZLwNdwXl+}L^sQfjpIKHd~@(ZB}oAJH}?0uwgKhh7m^qT|^|s&Q!UT0H!m4UWqHM3+=a^tyoO-HI20%BaER z@GP~+-8}nERW)1#JX12>xXu9Kb*M|9<7>QX8uBUN$>J_SDV8U09ZmDRJ@-(1_FD3D z(6*ftDf^JyhSPEnWj~~=Y|d6Cw+v&{owCPXjEj#5*{dV9w?j#t?GR&A)ZzGBE9Fgg zK0H+06qhjWW|q z^-Zq4@9??M*3J4b*vX`bS^>P*<8-d9V=?SNfRGh3cSl#pyXnCIXFxH~e`-{>|rE$b1c_;^fWOQ1QT?K){S3!N6 ztDq+|#qe3qF*s%?^D2l^O-pY+%pesypCdJR4M3MMutnf_Nus`P%w-$kjj!NhRzZ(t zU}$bwA!(n~63v_xb~_+xevU{>I#9*T# zNGt@4h1sJGzPt+3yYq`1U>n^rA&Ote%eW|@mKg5GlD~j$y{di{w43ueWJIkAf+CT^ zG37RNOo()ALsG|JY~*tg@>LK7++zbYtZcc;RP zJ?I0<+uL_VhY@={STm9b6Z0#qyvrJ>uw})l=}_Qz>g9xC_mTHB!%ZVGLbUo5k*VH{ z>H+868tYoDU1juoe)*-*qaj<-%amzo32yK-Bnhc5!r7|;Y{F=MhhHJGSH3PFbq#gH z_FM&Y_mk8r#D48pm&3FvkBgC{m?akEzGi4?x;-H|J=31`D8v8m{Xxbk=YYXS)vTp~ zgE|-CG+)@{9~xWJ$eOj;bQ3IquxWq%P0OQQRv>gt*w4&9_O!?@-SD|pkXdd;$HbD1 z-uwy+u6f-jom&O@d7#eNP#AC^oZ{~V@I94x-TsPIMku6tg$H^#rj@7fx^i_@=7LGg zGK&c5+oUm8R&B{EzwSHWI|=^>?tljk(Wj~pfvD~Wvkz1o6yzRxWqDoG%FB_RBCX*n z65XDg*CBZpetoL2|D;nMTTX3owUL!2f-}X-gHye4>I<**l?aiQhj(rQKZqo^d&qkCfqE!=zKM5O!I|2HLEtR*i z9VFFvs?ZNrKv~VsM15T5qupa@WlJzAn$+@ko#|Dlf|5XdmgJL?6h6HI@v%CutBZ<> zPN)s2KF5D2y#GQ)oK`5>{QFW76*xZeVPq!6Jwn|35#nCLm&t1O=K6xC#YPYnoUYNQ z$0k0jcZIh8b#jHyF{e?aXNj$N3e0>qoWz!mIY2H@S=pQ?tk8r7=_V}Qb#az4&qStP z*dnS60PKGse{zz8YTGd65O+t8R3RsIP<)G#SMehemAoVD!#YztEu1E46CGoc1lM<) z-=MoiSk)}ZcBj>AT^m&0fQyjqx!$EaJ}^Dj)Ci9kdEb@8kFM1V(|P}U1;gLzp9wYI zM!o&d)_91U7_Q!GB}w!>N%M$LO4p9vx*yP0Z|v3SvP5Z+%Iw#{pdio1+T`fyKCD3L zG;)`{G+Udma;ol?3kz_b!7pC`@I+;}i<<3EV?UU;#_JpK3MrL4!NJNTsETazJ}xeb z-WAy$Xq?kpA*t~>g1wu1tak<&vdbb227B~Yp2uD-zNyDI^1Wp*bgjk~h5uI<*@Qi_ z1wHw1z(v02mgN8w3D5KZAQ&W8{Wwk;kGScy<93XEaQCr{e$zo3cV-o2QcHS7fLvO& zlFCXt-Syr(gufhu_@;z$9#N6Wqw~-Dj`Ft8f&ppFOWS8(K*QU5)D8(iVzq;r#C2z@ zs-m&)`HF}#M#pwqLcUB1mqn{O>HryuN^UDc)mNSW%q{6wOuWY4TcuDROF13?#qWgb zn$q!rmM9^KBp{vvKSC0@1@Y{^D76FQa`6dA$e7Evfj1Wb>~@K~NB@mw1ja~gii0`j zW#&bV=P&!2-%*b;oq;{ElM7Y@-OEJNJwcxtcqMov-8p(NbtRkWlkW*YUsWlTw3%dQ zJro-8_OZ}ijDN45_Hqe*VA_%}7A?e^ZB~;%ZMC2M*_Sx7nx^-B6adn&w>{P1HapweP;tOU%OREP?!Rlq^7`r!Y z+nWo5kS;{D9i3vWdM_!9{?S)^VJF}bI(po1#G_~`8@Xc0wTOwEzzk!Id(e~<9U)_SPt{8ToikTK4}s!zE5D1U zf$hArZ1b)!KvH92Y(kU(7c6KS22h`0qzm6l@o5McB>|)S!d?JH!+)4iA6NwuV?!Y7I(zYgJ^8 zN2f>&8#1Cc7jO++flwN>cIogflr54sZMySg*KsG(ci5d~DP1|Sqt%VC{M6wT!uMFO zdw+-n(|21i7fT&s57Bl#LwpbrVDPAIO&|d&MaOIeKny}tR{&^L8r3~!cW6yd@qL-* znx_dRC_V^^kLN#L#-WRm>J7SAR^of0bCjB@y{qmT%67mk6YbMtj}>XzCt@GFUuwr4fTAcy!v0l}^co=K z0Zj~h%Niq(NiD$Wz$gGw68wuxzoxfZ6D5%obW?cY+JD&1{|`ysuIKJTGxflF!N1DS z>iHuQ3HFubNjNR+UD2>WM? zeB;NIioWX3#~r`(GKfeH#4EM5rh^SiZ}-wj9~GTX-7FVn!;puojVjQ@SwAp*s3>wo z`C5f%a2qwyqF)&hlOD1?$Yq*c&NLEGR6cMhEZcd7``FZ7puD?h4z$JmMt<*IKm_J; zT4+F#0xG5)i9XiGlp>d29Wpl+Sh80WiJoxTYdN7}p?fW)_@P&$B8UOn!+HFVkKo)#Iz0rS5hrl_0f~w}xK~7(mWIX=Egg=7oT6w(sf&iOX8s zdIbbx4i82Bqx~m@G~oOA4*+f7Dyrw()L4`z0O{$nBkj1#l>+ic*c^l>t)MJyj15h?GMOId= ztr})9h2Fz#l+UaJszM+>gQ7IzHEw(E?GGsEvpe@n9b*}e5Fa$c+sGZ&KpEZ7y9CD@ z3j3p+Ubrd@e;o2Ze5xT_`=*|fB>8vBD#zEjIW&wA@dy#`2Z(suUv@i^ZElK8EbHxl zGM6PqIVw{dHnv~n$)qKR%9UyUSLYGYW*$;N^dQ_EMpd&H5Ak^BE7fcD+wAyBzP-A8qhFz;Wfv^hFN+@Qhy|qZc^BGyPTDb zrHx6v_K<8GG4bJ=cxKYNLa3Vs(!i%X^dvCF`|^CjueIC8cVc&}S$!&*d`kZVbz=*P zT!Ui=ZA{1QRS$6jbijjq++?>L)*;E{~Et{7LKrQaLqMzc20PoR-* zNaUfkzH9oB0UK#?hikSRagRh0a-Da!FA4FwCY#|z`b882pD59@+s`D^a)^OhYylk&`a_<4kku$nEDvbRL|-}I)U zG81B^DV#m)GpH(0KQ#N@ef)6-cnr&3jiB!Ob=Cu6Ai|F_!=lv84nJz)s2;K0Epl0n zwGNpp*nN$h+b0S&B1{0I>qZKH4o=qpTFy0L$Rh`?qt1^dPm-rF>&R4Cs{PE!6ab9W+QC2FNSmc z#|Org`sR|LNuDQ%j5)I8!$K=sz>_UV7@6+yJCvI@-~Ngnu^DH}BW^#sNI#0rlLq7+ z`H44f@>@hlSLj+_P!*M6j~=_C*QF#qD=lmp)wmgk+#^3ll^U48_tg{f%6O6+^gB;} z3;@*2%y{)O=@6h^4pQuWO`Y;`;1{Fw@oJdMTN_{J-9K5$d}f44SbJ7$pYUTv$@bD8 zc;lC1mG%8W4$x)dN9srsbd&@g_IEKC1Rrs0F>{JROi^K*jrMJ$!_hA_UUqw1 zKS9yt*?fj1seszN3K%ieVUft7aPD@TF^7jWTSd&^&6|O5h9-e-fkdJaht0~arDi_Y zqp$jl{OU^%NgNls63co|s!pMXj898{cdV4JxIfZ4#nIWs#x&Z3z4)9SUc~Iyb!uP^ z^0^7}FR7+4KuWB25Om||cu@f4kHI{g0ysgNJ74He{%GOOh8>4ld; zsXa06u+_M!m7;Rxr1<;llkqMftg!7J+G9L(+r6shqx!P33T(F{1AWWt2k*$8i@r`Z zXg!ACbH}?jA0|O>-9mx3Djb$Z>3!t&o$k7^AzwD9YF0+Od@wc{h-e)umcH#G6TuPi z4xO;zU~_HefeD%PQ&1nCc8~iK8^LD;Ya+__brB_O?jl)w3NUL;qV87?*0b&Lj9JpZPM`Obdmb zstWU!Ho42b^g=?Iy4)H+b@JCQBAAb*TA3TuvV7yI=E|BrwC^P`2PUIkfQC7eHCI*i zI8Q3QyDPHgs`DYI+wBKJnRVcF$`;3q^|(Akw4YFxt;x+qzX)S^wZO0XW&D&wOzj+m zY<>3NUy67HAqrulVKR3~`w+BWsNsOadkwst^@!I^Bc3@KHzl`17hoK1>vr^5n zJ~*YVBY{V(^!1aV-i1@GIlQbn7)x{^B{)yL93~= z{xF-b-`6o_ETGHV*UGf3o#le&5e*{8PLhRVtWVFTA2L>e7Mu-!{)1ND7cBuB^Gp%YHK=5Oz-$wS!fqY+`Xg$&VmPoAeLVhq z>Ub_~8(vu(U0eIxP)d9VQQPP~o6fVRmz?*DROOiPDS5*kZZB}-iR-e}NYmejHQjmEZ8$pv@S9YX0T*18972W@8Lhy=$0mE#Llx@I178dV9M2sP#6`W^w^ zn#!BDmk{AJqvC_ZsiwW}4irfRGrRc@CVd=<mN?scKaPl>e(yZH2YYV0*1D=px0&%G+ayOc1LVcFdIU4>*||K5i4CaH{eK# zYrgO@j6A=^z*S~vnYMYaDu+1#E2Bx7fkW2mR$ zZZ;94-grbd5wEcKR+n1Bq6SZ&vXQ1B|GNu!hqdPDM{@l{qVH@2l z$O_W|SaY62T2o;50=P%rLWqt7%csupE=A_lFXuk^97pk_j!L$Rf@G6)N(=%d;pR6< z0;y=O0kBF8_hwXuD|O_P9wVCKPFL6wK*FqcQu`AyfYyDGn?o|sVne}Z=@5=F>23Hj!akZ*olNtpoOXThiT6jxpm zIwp3ohwEU<%U{$H*O&S=F5tX%r(FC3kXnf{p@rN>PjYI`+Feu7R;c-Z?VWi%+i4!h z9aVZ_(bhfPY{y-#6-Tu(twF;Qs*W~Dwd<-ylxm8wjMgo!tMj60r6neoQCHf!*177H z22D`6q|{Lw=TJuXNz9?_?91#wo7w&SmEYgLe4pR*Jm2r<^ZtmLhuKl6^s%P_!T z&YGh7Le(gANB)ly&b;6~1uD(7=dHG~()VLe``^wzTwCx*p z^0;jkU{8fkn9vE6_fMErsG*9eGAG?eH?JzK*-lOMeRJq%M{iCW2d@qOWlb{6IT#xh z+f?X!;#;g=-$Wg@O6~}*E5T$LbN3f?%n2cZ?bqe)y!Bo?I1cPE)V;e0fuKk+f48LA z;ARK=f+3=`-k0>6lCP#q0olpVO6Ds-$!zeSD4C&jB?u48dw5vp7~qc|S51C!^{;{- z`4AlA;)X1PtV}JnZUKUs&P)f8GSiEMcWFqEUDESO6cj5K z%kT~o1z4dL#Fl*4<@YSpsO#RSMiaEf(b)-29M4!(aki8{YV(S6&IR@#`M;CkGpW>mF}HF8-ON4 zF#Ib3SO}?@k*y0Fc^k8E!5bum;Q9)?5*gjPN)H9oSv8x}5DoD89K&7J*KYoi=g$Ws z4pi*6`rA2Mc~f$Y(RS?=rQC)|GXxoeAVa(d8G2k4QF(@ZG(g_URYbako5-18E!RVJ zA2v`f1SDo#>^WoA`ta^9WJ$5~<2{Lt=9ng$WKvT1;*-k6H2ZV5hjrfP63234=LoqQ z`;p>7wq6UGSM;oq%C-FSWIg5`4LI?smSYR?4N4reWYRypYkoLkDYGtU4w^3r+p1^o z^N(*YsW#lbNKB{4nx`rX9~B2gW=kStS#f)jjojg!1T>q2W^=qYn_DquyGO%Pb;m9y ztu3Ia?w<{bXE7FXqhw7V^}zD&9OzOm5qjh-42d>u+3ToR@aJN?pJ>dP`W4hZ4k%B! zedG4Fj|Puno&5uaO9qnWGlL^#_GiVt2Tha6V6ftc zcl+%~boHLMoTX3|9$2 z5L910Os%uZQyaAQ^vIzQ0ygp$e5vDgEl@M+xV7ShedCf;SC!*OPoCI%iGm)S^%~{? zVzN&XNUd7kt|?B33?}`+W$v0n?Z&CV0AoZ^K|5~B9`yvRrWV(9LGrGz1cL7rTdmV@ zx7*+Q{n6Inj8QED^!z9eEDy8D%w&WXg>4$$C^lSk9izU3arU1PV>Lp(*#lhU|IGiR>jIF3^DYK1B4iYT+4~zQZSW zlK4(0yRHY50-C?d$r|4d?h!0<=?6c|%^S6usw?IlJ{^lMnbh<%XJ`_KVM=uS{4iCI zKkem@y#aq?N1C*tE+A+^w-Wb9dKeGXlX7Akrhi&|W*3$D%jVo03413G4ro-T^&+b& zG57d~%L8p_mxb_3iQT!&!O!iTWyRICmzlWNxoEkUu|$1z+jgO_=G=kzs3Anig9v#% zK*$67-miYSWllbT0K}8C<{mOR@#0htUI+-SdgmVjFM%~D#>T3KzN)*r8W!FZa-^8@ zYE|BAAEUDTm8Qe!nRr7%rw8&X0<3r&a^wAer2Y-WTTx?|ag&t5i(DM@kCr*k?+zOQ zmr(uB2p!edGxvc1H34IVD|Irl^?Xv0mUIl(6^?OWW{j+aMm)z-@?Fq==&S=?sYdWf z7yXMp+1qPUj&YS;A#b5^95jyOxpCZ<85jHp3J_Xt0;hy+98)LVl}I4Xt-7&y3RwbD zq_``uOT?wKx_xaYD)Lg&V14KD7YTB4hlCu(Oo!15DgBQE@-B774r*s0rthBuO6Jpa xYh&btjW;{!S&|QW2hcl!Y5=MMs0N@KfNB7$0jLI`8h~m5s)0>4!2j{}KLG;l(pUfh diff --git a/MindSPONGE/applications/AlphaFold3/requirements.txt b/MindSPONGE/applications/AlphaFold3/requirements.txt deleted file mode 100644 index 1c230c665..000000000 --- a/MindSPONGE/applications/AlphaFold3/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -mindSpore==2.5.0 -absl-py==2.1.0 -numpy==1.26.0 -rdkit==2024.3.5 -scipy==1.14.1 -tqdm==4.67.0 \ No newline at end of file diff --git a/MindSPONGE/applications/AlphaFold3/run_alphafold.py b/MindSPONGE/applications/AlphaFold3/run_alphafold.py deleted file mode 100644 index b8502ea0d..000000000 --- a/MindSPONGE/applications/AlphaFold3/run_alphafold.py +++ /dev/null @@ -1,664 +0,0 @@ -# Copyright 2024 DeepMind Technologies Limited -# Copyright (C) 2025 Huawei Technologies Co., Ltd -# -# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of -# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ -# -# To request access to the AlphaFold 3 model parameters, follow the process set -# out at https://github.com/google-deepmind/alphafold3. You may only use these -# if received directly from Google. Use is subject to terms of use available at -# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md -# -# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend - -"""script for AF3 Inference""" -from collections.abc import Callable, Iterable, Sequence -import csv -import dataclasses -import datetime -import functools -import multiprocessing -import os -import pathlib -import shutil -import string -import textwrap -import time -import typing -from typing import Protocol, Self, TypeVar, overload, Optional, Union - -from absl import app -from absl import flags -from alphafold3.common import base_config -from alphafold3.common import folding_input -from alphafold3.common import resources -from alphafold3.constants import chemical_components -import alphafold3.cpp -from alphafold3.data import featurisation -from alphafold3.data import pipeline -from alphafold3.utils.attention import attention -from alphafold3.model import features -from alphafold3.model.diffusion.load_ckpt import load_diffuser -# from alphafold3.model import params -from alphafold3.model import post_processing -from alphafold3.model.components import base_model -from alphafold3.model.components import utils -from alphafold3.model.diffusion import model as diffusion_model -from alphafold3.model.feat_batch import Batch -import mindspore as ms -import numpy as np - - -_HOME_DIR = pathlib.Path(os.environ.get('HOME')) -_DEFAULT_MODEL_DIR = _HOME_DIR / 'ckpt' -_DEFAULT_DB_DIR = _HOME_DIR / 'public_databases' - - -# Input and output paths. -_JSON_PATH = flags.DEFINE_string( - 'json_path', - None, - 'Path to the input JSON file.', -) -_INPUT_DIR = flags.DEFINE_string( - 'input_dir', - None, - 'Path to the directory containing input JSON files.', -) -_OUTPUT_DIR = flags.DEFINE_string( - 'output_dir', - None, - 'Path to a directory where the results will be saved.', -) -MODEL_DIR = flags.DEFINE_string( - 'model_dir', - _DEFAULT_MODEL_DIR.as_posix(), - 'Path to the model to use for inference.', -) - -# Control which stages to run. -_RUN_DATA_PIPELINE = flags.DEFINE_bool( - 'run_data_pipeline', - True, - 'Whether to run the data pipeline on the fold inputs.', -) -_RUN_INFERENCE = flags.DEFINE_bool( - 'run_inference', - True, - 'Whether to run inference on the fold inputs.', -) - -# Binary paths. -_JACKHMMER_BINARY_PATH = flags.DEFINE_string( - 'jackhmmer_binary_path', - shutil.which('jackhmmer'), - 'Path to the Jackhmmer binary.', -) -_NHMMER_BINARY_PATH = flags.DEFINE_string( - 'nhmmer_binary_path', - shutil.which('nhmmer'), - 'Path to the Nhmmer binary.', -) -_HMMALIGN_BINARY_PATH = flags.DEFINE_string( - 'hmmalign_binary_path', - shutil.which('hmmalign'), - 'Path to the Hmmalign binary.', -) -_HMMSEARCH_BINARY_PATH = flags.DEFINE_string( - 'hmmsearch_binary_path', - shutil.which('hmmsearch'), - 'Path to the Hmmsearch binary.', -) -_HMMBUILD_BINARY_PATH = flags.DEFINE_string( - 'hmmbuild_binary_path', - shutil.which('hmmbuild'), - 'Path to the Hmmbuild binary.', -) - -# Database paths. -DB_DIR = flags.DEFINE_multi_string( - 'db_dir', - (_DEFAULT_DB_DIR.as_posix(),), - 'Path to the directory containing the databases. Can be specified multiple' - ' times to search multiple directories in order.', -) - -_SMALL_BFD_DATABASE_PATH = flags.DEFINE_string( - 'small_bfd_database_path', - '${DB_DIR}/bfd-first_non_consensus_sequences.fasta', - 'Small BFD database path, used for protein MSA search.', -) -_MGNIFY_DATABASE_PATH = flags.DEFINE_string( - 'mgnify_database_path', - '${DB_DIR}/mgy_clusters_2022_05.fa', - 'Mgnify database path, used for protein MSA search.', -) -_UNIPROT_CLUSTER_ANNOT_DATABASE_PATH = flags.DEFINE_string( - 'uniprot_cluster_annot_database_path', - '${DB_DIR}/uniprot_all_2021_04.fa', - 'UniProt database path, used for protein paired MSA search.', -) -_UNIREF90_DATABASE_PATH = flags.DEFINE_string( - 'uniref90_database_path', - '${DB_DIR}/uniref90_2022_05.fa', - 'UniRef90 database path, used for MSA search. The MSA obtained by ' - 'searching it is used to construct the profile for template search.', -) -_NTRNA_DATABASE_PATH = flags.DEFINE_string( - 'ntrna_database_path', - '${DB_DIR}/nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta', - 'NT-RNA database path, used for RNA MSA search.', -) -_RFAM_DATABASE_PATH = flags.DEFINE_string( - 'rfam_database_path', - '${DB_DIR}/rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta', - 'Rfam database path, used for RNA MSA search.', -) -_RNA_CENTRAL_DATABASE_PATH = flags.DEFINE_string( - 'rna_central_database_path', - '${DB_DIR}/rnacentral_active_seq_id_90_cov_80_linclust.fasta', - 'RNAcentral database path, used for RNA MSA search.', -) -_PDB_DATABASE_PATH = flags.DEFINE_string( - 'pdb_database_path', - '${DB_DIR}/mmcif_files', - 'PDB database directory with mmCIF files path, used for template search.', -) -_SEQRES_DATABASE_PATH = flags.DEFINE_string( - 'seqres_database_path', - '${DB_DIR}/pdb_seqres_2022_09_28.fasta', - 'PDB sequence database path, used for template search.', -) - -# Number of CPUs to use for MSA tools. -_JACKHMMER_N_CPU = flags.DEFINE_integer( - 'jackhmmer_n_cpu', - min(multiprocessing.cpu_count(), 8), - 'Number of CPUs to use for Jackhmmer. Default to min(cpu_count, 8). Going' - ' beyond 8 CPUs provides very little additional speedup.', -) -_NHMMER_N_CPU = flags.DEFINE_integer( - 'nhmmer_n_cpu', - min(multiprocessing.cpu_count(), 8), - 'Number of CPUs to use for Nhmmer. Default to min(cpu_count, 8). Going' - ' beyond 8 CPUs provides very little additional speedup.', -) - -# Template search configuration. -_MAX_TEMPLATE_DATE = flags.DEFINE_string( - 'max_template_date', - '2021-09-30', # By default, use the date from the AlphaFold 3 paper. - 'Maximum template release date to consider. Format: YYYY-MM-DD. All ' - 'templates released after this date will be ignored.', -) - - -_BUCKETS = flags.DEFINE_list( - 'buckets', - # pyformat: disable - ['256', '512', '768', '1024', '1280', '1536', '2048', '2560', '3072', - '3584', '4096', '4608', '5120'], - # pyformat: enable - 'Strictly increasing order of token sizes for which to cache compilations.' - ' For any input with more tokens than the largest bucket size, a new bucket' - ' is created for exactly that number of tokens.', -) - -class ConfigurableModel(Protocol): - """A model with a nested config class.""" - - class Config(base_config.BaseConfig): - ... - - def __call__(self, config: Config) -> Self: - ... - - @classmethod - def get_inference_result( - cls: Self, - batch: features.BatchDict, - result: base_model.ModelResult, - target_name: str = '', - ) -> Iterable[base_model.InferenceResult]: - ... - - -ModelT = TypeVar('ModelT', bound=ConfigurableModel) - - -def make_model_config(): - print('not implemented make_model_config') - return 'ab' - -def make_model_config( - *, - model_class: type[ModelT] = diffusion_model.Diffuser, -): - """Make model config""" - config = model_class.Config() - return config - - -class ModelRunner: - """Helper class to run structure prediction stages.""" - - def __init__( - self, - model_class: ConfigurableModel, - config: base_config.BaseConfig, - model_dir: pathlib.Path, - ): - self._model_class = model_class - self._model_config = config - self._model_dir = model_dir - - @functools.cached_property - def model_params(self): - """Loads model parameters from the model directory.""" - # Load parameters from checkpoint file - # param_dict = ms.load_checkpoint(self._model_dir / "test.ckpt") - # return param_dict - - @functools.cached_property - def _model( - self - ) -> Callable[[np.ndarray, features.BatchDict], base_model.ModelResult]: - """Loads model parameters and returns a model forward pass.""" - - def forward_fn(batch): - num_residues = batch.token_features.residue_index.shape[0] - model = self._model_class(self._model_config, 447, (256, 447), (num_residues, 256, 128), (256, 256, 128), - (256, 384), (256, 24, 3), 128, 4, dtype=ms.float32) - load_diffuser(model, self._model_dir, dtype=ms.float32) - res = model(batch, 42) - return res - - return forward_fn - - def run_inference( - self, featurised_example: features.BatchDict - ) -> base_model.ModelResult: - """Computes a forward pass of the model on a featurised example.""" - featurised_example = Batch.from_data_dict(featurised_example) - featurised_example.convert_to_tensor(ms.float32) - - result = self._model(featurised_example) - - # Convert identifier to bytes - if '__identifier__' in result: - result['__identifier__'] = result['__identifier__'].tobytes() - return result - - def extract_structures( - self, - batch: features.BatchDict, - result: base_model.ModelResult, - target_name: str, - ) -> list[base_model.InferenceResult]: - """Generates structures from model outputs.""" - batch = Batch.from_data_dict(batch) - batch.convert_to_tensor(ms.float32) - return list( - self._model_class.get_inference_result( - batch=batch, result=result, target_name=target_name - ) - ) - - -@dataclasses.dataclass(frozen=True) -class ResultsForSeed: - """Stores the inference results (diffusion samples) for a single seed. - - Attributes: - seed: The seed used to generate the samples. - inference_results: The inference results, one per sample. - full_fold_input: The fold input that must also include the results of - running the data pipeline - MSA and templates. - """ - - seed: int - inference_results: Sequence[base_model.InferenceResult] - full_fold_input: folding_input.Input - - -def predict_structure( - fold_input: folding_input.Input, - model_runner: ModelRunner, - buckets: Optional[Sequence[int]] = None, -) -> Sequence[ResultsForSeed]: - """Runs the full inference pipeline to predict structures for each seed.""" - - print(f'Featurising data for seeds {fold_input.rng_seeds}...') - featurisation_start_time = time.time() - ccd = chemical_components.cached_ccd(user_ccd=fold_input.user_ccd) - featurised_examples = featurisation.featurise_input( - fold_input=fold_input, buckets=buckets, ccd=ccd, verbose=True - ) - print( - f'Featurising data for seeds {fold_input.rng_seeds} took ' - f' {time.time() - featurisation_start_time:.2f} seconds.' - ) - all_inference_start_time = time.time() - all_inference_results = [] - for seed, example in zip(fold_input.rng_seeds, featurised_examples): - print(f'Running model inference for seed {seed}...') - inference_start_time = time.time() - result = model_runner.run_inference(example) - print( - f'Running model inference for seed {seed} took ' - f' {time.time() - inference_start_time:.2f} seconds.' - ) - print( - f'Extracting output structures (one per sample) for seed {seed}...') - extract_structures = time.time() - inference_results = model_runner.extract_structures( - batch=example, result=result, target_name=fold_input.name - ) - print( - f'Extracting output structures (one per sample) for seed {seed} took ' - f' {time.time() - extract_structures:.2f} seconds.' - ) - all_inference_results.append( - ResultsForSeed( - seed=seed, - inference_results=inference_results, - full_fold_input=fold_input, - ) - ) - print( - 'Running model inference and extracting output structures for seed' - f' {seed} took {time.time() - inference_start_time:.2f} seconds.' - ) - print( - 'Running model inference and extracting output structures for seeds' - f' {fold_input.rng_seeds} took ' - f' {time.time() - all_inference_start_time:.2f} seconds.' - ) - return all_inference_results - - -def write_fold_input_json( - fold_input: folding_input.Input, - output_dir: Union[os.PathLike[str], str], -) -> None: - """Writes the input JSON to the output directory.""" - os.makedirs(output_dir, exist_ok=True) - with open(os.path.join(output_dir, f'{fold_input.sanitised_name()}_data.json'), 'wt', encoding='utf-8') as f: - f.write(fold_input.to_json()) - - -def write_outputs( - all_inference_results: Sequence[ResultsForSeed], - output_dir: Union[os.PathLike[str], str], - job_name: str, -) -> None: - """Writes outputs to the specified output directory.""" - ranking_scores = [] - max_ranking_score = None - max_ranking_result = None - - os.makedirs(output_dir, exist_ok=True) - for results_for_seed in all_inference_results: - seed = results_for_seed.seed - for sample_idx, result in enumerate(results_for_seed.inference_results): - sample_dir = os.path.join( - output_dir, f'seed-{seed}_sample-{sample_idx}') - os.makedirs(sample_dir, exist_ok=True) - post_processing.write_output( - inference_result=result, output_dir=sample_dir - ) - ranking_score = float(result.metadata['ranking_score']) - ranking_scores.append((seed, sample_idx, ranking_score)) - if max_ranking_score is None or ranking_score > max_ranking_score: - max_ranking_score = ranking_score - max_ranking_result = result - - if max_ranking_result is not None: # True iff ranking_scores non-empty. - post_processing.write_output( - inference_result=max_ranking_result, - output_dir=output_dir, - # The output terms of use are the same for all seeds/samples. - # terms_of_use=output_terms, - terms_of_use=None, - name=job_name, - ) - # Save csv of ranking scores with seeds and sample indices, to allow easier - # comparison of ranking scores across different runs. - with open(os.path.join(output_dir, 'ranking_scores.csv'), 'wt', encoding='utf-8') as f: - writer = csv.writer(f) - writer.writerow(['seed', 'sample', 'ranking_score']) - writer.writerows(ranking_scores) - - -@overload -def process_fold_input( - fold_input: folding_input.Input, - data_pipeline_config: Optional[pipeline.DataPipelineConfig], - model_runner: None, - output_dir: Union[os.PathLike[str], str], - buckets: Optional[Sequence[int]] = None, -) -> folding_input.Input: - ... - - -@overload -def process_fold_input( - fold_input: folding_input.Input, - data_pipeline_config: Optional[pipeline.DataPipelineConfig], - model_runner: ModelRunner, - output_dir: Union[os.PathLike[str], str], - buckets: Optional[Sequence[int]] = None, -) -> Sequence[ResultsForSeed]: - ... - - -def replace_db_dir(path_with_db_dir: str, db_dirs: Sequence[str]) -> str: - """Replaces the DB_DIR placeholder in a path with the given DB_DIR.""" - template = string.Template(path_with_db_dir) - if 'DB_DIR' in template.get_identifiers(): - for db_dir in db_dirs: - path = template.substitute(DB_DIR=db_dir) - if os.path.exists(path): - return path - raise FileNotFoundError( - f'{path_with_db_dir} with ${{DB_DIR}} not found in any of {db_dirs}.' - ) - if not os.path.exists(path_with_db_dir): - raise FileNotFoundError(f'{path_with_db_dir} does not exist.') - return path_with_db_dir - - -def process_fold_input( - fold_input: folding_input.Input, - data_pipeline_config: Optional[pipeline.DataPipelineConfig], - model_runner: Optional[ModelRunner], - output_dir: Union[os.PathLike[str], str], - buckets: Optional[Sequence[int]] = None, -) -> Union[folding_input.Input, Sequence[ResultsForSeed]]: - """Runs data pipeline and/or inference on a single fold input. - - Args: - fold_input: Fold input to process. - data_pipeline_config: Data pipeline config to use. If None, skip the data - pipeline. - model_runner: Model runner to use. If None, skip inference. - output_dir: Output directory to write to. - buckets: Bucket sizes to pad the data to, to avoid excessive re-compilation - of the model. If None, calculate the appropriate bucket size from the - number of tokens. If not None, must be a sequence of at least one integer, - in strictly increasing order. Will raise an error if the number of tokens - is more than the largest bucket size. - - Returns: - The processed fold input, or the inference results for each seed. - - Raises: - ValueError: If the fold input has no chains. - """ - print(f'Processing fold input {fold_input.name}') - - if not fold_input.chains: - raise ValueError('Fold input has no chains.') - - if os.path.exists(output_dir) and os.listdir(output_dir): - new_output_dir = ( - f'{output_dir}_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}' - ) - print( - f'Output directory {output_dir} exists and non-empty, using instead ' - f' {new_output_dir}.' - ) - output_dir = new_output_dir - - if model_runner is not None: - # If we're running inference, check we can load the model parameters before - # (possibly) launching the data pipeline. - print('Checking we can load the model parameters...') - _ = model_runner.model_params - - if data_pipeline_config is None: - print('Skipping data pipeline...') - else: - print('Running data pipeline...') - fold_input = pipeline.DataPipeline( - data_pipeline_config).process(fold_input) - - print(f'Output directory: {output_dir}') - print(f'Writing model input JSON to {output_dir}') - write_fold_input_json(fold_input, output_dir) - if model_runner is None: - print('Skipping inference...') - output = fold_input - else: - print( - f'Predicting 3D structure for {fold_input.name} for seed(s)' - f' {fold_input.rng_seeds}...' - ) - all_inference_results = predict_structure( - fold_input=fold_input, - model_runner=model_runner, - buckets=buckets, - ) - print( - f'Writing outputs for {fold_input.name} for seed(s)' - f' {fold_input.rng_seeds}...' - ) - write_outputs( - all_inference_results=all_inference_results, - output_dir=output_dir, - job_name=fold_input.sanitised_name(), - ) - output = all_inference_results - - print(f'Done processing fold input {fold_input.name}.') - return output - - -def main(_): - - if (_JSON_PATH.value is None) == (_INPUT_DIR.value is None): - raise ValueError( - 'Exactly one of --json_path or --input_dir must be specified.' - ) - - if not _RUN_INFERENCE.value and not _RUN_DATA_PIPELINE.value: - raise ValueError( - 'At least one of --run_inference or --run_data_pipeline must be' - ' set to true.' - ) - - if _INPUT_DIR.value is not None: - fold_inputs = folding_input.load_fold_inputs_from_dir( - pathlib.Path(_INPUT_DIR.value) - ) - elif _JSON_PATH.value is not None: - fold_inputs = folding_input.load_fold_inputs_from_path( - pathlib.Path(_JSON_PATH.value) - ) - else: - raise AssertionError( - 'Exactly one of --json_path or --input_dir must be specified.' - ) - - # Make sure we can create the output directory before running anything. - try: - os.makedirs(_OUTPUT_DIR.value, exist_ok=True) - except OSError as e: - print(f'Failed to create output directory {_OUTPUT_DIR.value}: {e}') - raise - - notice = textwrap.wrap( - 'Running AlphaFold 3. Please note that standard AlphaFold 3 model' - ' parameters are only available under terms of use provided at' - ' https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md.' - ' If you do not agree to these terms and are using AlphaFold 3 derived' - ' model parameters, cancel execution of AlphaFold 3 inference with' - ' CTRL-C, and do not use the model parameters.', - break_long_words=False, - break_on_hyphens=False, - width=80, - ) - print('\n'.join(notice)) - - if _RUN_DATA_PIPELINE.value: - def expand_path(x): - return replace_db_dir(x, DB_DIR.value) - max_template_date = datetime.date.fromisoformat( - _MAX_TEMPLATE_DATE.value) - data_pipeline_config = pipeline.DataPipelineConfig( - jackhmmer_binary_path=_JACKHMMER_BINARY_PATH.value, - nhmmer_binary_path=_NHMMER_BINARY_PATH.value, - hmmalign_binary_path=_HMMALIGN_BINARY_PATH.value, - hmmsearch_binary_path=_HMMSEARCH_BINARY_PATH.value, - hmmbuild_binary_path=_HMMBUILD_BINARY_PATH.value, - small_bfd_database_path=expand_path( - _SMALL_BFD_DATABASE_PATH.value), - mgnify_database_path=expand_path(_MGNIFY_DATABASE_PATH.value), - uniprot_cluster_annot_database_path=expand_path( - _UNIPROT_CLUSTER_ANNOT_DATABASE_PATH.value - ), - uniref90_database_path=expand_path(_UNIREF90_DATABASE_PATH.value), - ntrna_database_path=expand_path(_NTRNA_DATABASE_PATH.value), - rfam_database_path=expand_path(_RFAM_DATABASE_PATH.value), - rna_central_database_path=expand_path( - _RNA_CENTRAL_DATABASE_PATH.value), - pdb_database_path=expand_path(_PDB_DATABASE_PATH.value), - seqres_database_path=expand_path(_SEQRES_DATABASE_PATH.value), - jackhmmer_n_cpu=_JACKHMMER_N_CPU.value, - nhmmer_n_cpu=_NHMMER_N_CPU.value, - max_template_date=max_template_date, - ) - else: - print('Skipping running the data pipeline.') - data_pipeline_config = None - - if _RUN_INFERENCE.value: - print('Building model from scratch...') - model_runner = ModelRunner( - model_class=diffusion_model.Diffuser, - config=make_model_config(), - model_dir=pathlib.Path(MODEL_DIR.value), - ) - else: - print('Skipping running model inference.') - model_runner = None - - print(f'Processing {len(fold_inputs)} fold inputs.') - for fold_input in fold_inputs: - process_fold_input( - fold_input=fold_input, - data_pipeline_config=data_pipeline_config, - model_runner=model_runner, - output_dir=os.path.join( - _OUTPUT_DIR.value, fold_input.sanitised_name()), - buckets=tuple(int(bucket) for bucket in _BUCKETS.value), - ) - - print(f'Done processing {len(fold_inputs)} fold inputs.') - - -if __name__ == '__main__': - flags.mark_flags_as_required([ - 'output_dir', - ]) - app.run(main) diff --git a/MindSPONGE/applications/AlphaFold3/set_path.sh b/MindSPONGE/applications/AlphaFold3/set_path.sh deleted file mode 100644 index 27f1e8e35..000000000 --- a/MindSPONGE/applications/AlphaFold3/set_path.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Get the script directory to make paths more reliable -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# From AlphaFold3 directory, go up to the mindscience directory -MINDSCIENCE_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)" - -# Check if the base directory exists -if [ ! -d "$MINDSCIENCE_PATH" ]; then - echo "Error: MindScience path not found: $MINDSCIENCE_PATH" - echo "Please run this script from the correct directory" - exit 1 -fi - -# Function to add to PYTHONPATH if directory exists -add_to_pythonpath() { - local dir_path="$1" - if [ -d "$dir_path" ]; then - export PYTHONPATH="$PYTHONPATH:$dir_path" - echo "Added to PYTHONPATH: $dir_path" - else - echo "Warning: Directory not found, skipping: $dir_path" - fi -} - -add_to_pythonpath "$MINDSCIENCE_PATH" -add_to_pythonpath "$MINDSCIENCE_PATH/MindSPONGE/applications/AlphaFold3" - -# Add directories to PATH -export PATH=$PATH:/hmmer/bin - -# Display current PYTHONPATH -echo "Current PYTHONPATH:" -echo "$PYTHONPATH" | tr ':' '\n' | sed 's/^/ /' - -echo "Environment setup completed." diff --git a/mindscience/common/__init__.py b/mindscience/common/__init__.py index b45138386..1e04ef7ec 100644 --- a/mindscience/common/__init__.py +++ b/mindscience/common/__init__.py @@ -20,7 +20,6 @@ from .optimizers import AdaHessian from .math import get_grid_1d, get_grid_2d, get_grid_3d from .utils import to_2tuple, to_3tuple, unpatchify, patchify, get_2d_sin_cos_pos_embed, \ pixel_shuffle, pixel_unshuffle, PixelShuffle, PixelUnshuffle, SpectralNorm -from .initializer import lecun_init, glorot_uniform __all__ = ["get_poly_lr", "get_multi_step_lr", @@ -35,6 +34,5 @@ __all__ = ["get_poly_lr", "get_grid_1d", "get_grid_2d", "get_grid_3d", "to_2tuple", "to_3tuple", "unpatchify", "patchify", "get_2d_sin_cos_pos_embed", "pixel_shuffle", "pixel_unshuffle", "PixelShuffle", "PixelUnshuffle", - "SpectralNorm", - "lecun_init", "glorot_uniform", + "SpectralNorm" ] diff --git a/mindscience/common/memory_reduce.py b/mindscience/common/memory_reduce.py deleted file mode 100644 index 863a3878c..000000000 --- a/mindscience/common/memory_reduce.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2021 The AIMM Group at Shenzhen Bay Laboratory & Peking University & Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ -"""utils module""" - -from mindspore.ops import operations as P -from mindspore.ops import functional as F -def _memory_reduce(body, batched_inputs, nonbatched_inputs, slice_num, dim=0): - """memory reduce function""" - if slice_num <= 1: - inputs = batched_inputs + nonbatched_inputs - return body(*inputs) - inner_batched_inputs = [] - for val in batched_inputs: - inner_val = P.Split(dim, slice_num)(val) - inner_batched_inputs.append(inner_val) - # for depend - inner_split_batched_inputs = () - for _, inner_batched_input in enumerate(inner_batched_inputs): - inner_split_batched_inputs = inner_split_batched_inputs + (inner_batched_input[0],) - inner_split_inputs = inner_split_batched_inputs + nonbatched_inputs - inner_split_res = body(*inner_split_inputs) - res = (inner_split_res,) - for i in range(1, slice_num): - inner_split_batched_inputs = () - for _, inner_batched_input in enumerate(inner_batched_inputs): - inner_split_batched_inputs = inner_split_batched_inputs + (inner_batched_input[i],) - inner_split_inputs = inner_split_batched_inputs + nonbatched_inputs - inner_split_inputs = F.depend(inner_split_inputs, res[-1]) - inner_split_res = body(*inner_split_inputs) - res = res + (inner_split_res,) - res = P.Concat()(res) - return res diff --git a/mindscience/models/layers/__init__.py b/mindscience/models/layers/__init__.py index be7091283..203fc4563 100644 --- a/mindscience/models/layers/__init__.py +++ b/mindscience/models/layers/__init__.py @@ -23,7 +23,6 @@ like UNet2D. from .activation import get_activation from .basic_block import LinearBlock, ResBlock, InputScale, FCSequential, MultiScaleFCSequential, DropPath from .unet2d import UNet2D -from .mask import MaskedLayerNorm __all__ = ["get_activation", "LinearBlock", "ResBlock", "InputScale", "FCSequential", - "MultiScaleFCSequential", "DropPath", "UNet2D", "MaskedLayerNorm"] + "MultiScaleFCSequential", "DropPath", "UNet2D"] diff --git a/mindscience/sciops/dft/README.md b/mindscience/sciops/dft/README.md new file mode 100644 index 000000000..3e19a1def --- /dev/null +++ b/mindscience/sciops/dft/README.md @@ -0,0 +1,138 @@ +# DFT + +## DFT 介绍 + +DFT(离散傅里叶变换)是将离散时域信号映射到频域的经典运算,由连续傅里叶变换离散化而来。最初用于信号分析,现已是深度学习与科学计算的核心张量工具。此模块实现基于 **DFT 矩阵(dense matrix)和 矩阵乘法(matmul)** 的可微离散变换,包含: + +- 实数离散傅里叶变换:`RDFTn` / `IRDFTn` +- 复数离散傅里叶变换:`DFTn` / `IDFTn` +- 一维离散余弦变换:`DCT` / `IDCT` +- 一维离散正弦变换:`DST` / `IDST` + +> 说明:本模块的接口设计与 `scipy.fft` 保持尽量对齐(可参考[SciPy 文档](https://docs.scipy.org/doc/scipy/reference/fft.html)),但并未完全覆盖 `scipy.fft` 的所有函数与参数。建议在对照使用时同时参考 SciPy 的文档以补充公式。 + +## 基本公式 + +- **复数 DFT(长度 N):** + + X[k] = Σ_{n=0}^{N-1} x[n] · e^{-i·2π·k·n/N} + +- **逆 DFT:** + + x[n] = (1/N) · Σ_{k=0}^{N-1} X[k] · e^{+i·2π·k·n/N} + +- **实数 RDFT 与 IRDFT:** + + 对实值输入,频谱满足共轭对称性(Hermitian symmetry),因此仅需存储/计算 **前半频率分量**(通常含 Nyquist 分量),从而节省存储与计算。如需其他支持,请参照 scipy.fft 文档。 + +## DFT 算法流程 + +1. 解析输入参数:`shape`、`dim`、`norm`、`modes` 等,进行合法性校验与标准化。 +2. 在初始化阶段根据目标 `shape` 构造一维 DFT 矩阵(避免每次调用重复生成)。 +3. 将待变换轴移动到最后以便使用矩阵乘法(`matmul`)。 +4. 通过矩阵乘法执行一维变换;对多维情况逐轴重复执行以得到 n 维结果。 +5. 恢复轴顺序并拼接输出,返回最终张量。 + +## `sciops.DFT` 模块优化点 + +sciops 模块中的 DFT 算子进行了如下优化: + +- **初始化与计算分离**:在算子初始化阶段完成 DFT 矩阵生成与参数校验,避免运行时重复构建与额外开销,提高运行效率和接口稳定性。 +- **实数输入优化(RDFTn / IRDFTn)**:针对实数输入只生成/存储前半频率分量(利用 Hermitian 对称性),将计算与存储量约减一半(视具体 N 而定),并在逆变换时正确重构完整频谱。 +- **实部 / 虚部分别处理以规避复数类型**:对某些后端设备或数值框架对复数不友好的情况,模块提供将实部与虚部分开计算/存储的实现路径,以提高兼容性和数值稳定性。 +- **modes 参数支持(额外特性)**:`DFTn`、`IDFTn`、`RDFTn`、`IRDFTn` 提供 `modes` 参数用于截断输出模态(默认 `None` 表示不截断)。当只需低频/部分模式时可设置 `modes`,从而减少矩阵规模与计算量,适用于谱截断或降阶近似场景。 +- **环境适配与兼容实现**:为规避某些后端(如 Ascend)和运行模式下原生算子的兼容问题,模块提供自定义 `MyRoll`、`MyFlip` 实现,并根据设备运行时动态选择实现。 + +## 输入/输出 尺寸说明 + +- **`shape` 参数的含义**:通常 `shape` 表示对每个要变换维度的一维长度。例如当输入张量 `x` 形状为 `(B, ..., M, N)`,若希望对最后两个轴做二维 DFT,可设置 `shape=(M, N)`;也可仅传单轴长度来做 1D 变换。 + +- **RDFTn / IRDFTn 的输入输出尺寸**: + 设输入张量的末尾维度为:[..., n1, n2, ..., nk] + 其中最后 k 个维度需要做 RDFTn(k = len(shape)) + 对于最后一个维度: + 输入长度:`N` + 输出长度:`N//2 + 1` + RDFTn的输出形式:[..., n1, n2, ..., n_{k-1}, n_k//2 + 1] + IRDFTn的输入必须是:[..., n1, n2, ..., n_{k-1}, n_k//2 + 1] + 输出将恢复为完整的 n_k:[..., n1, n2, ..., n_k] +- **DFTn / IDFTn 的输入输出尺寸**: + 对于复数 DFT,若对长度为 `N` 的轴做全量 DFT,则输出长度依然为 `N`。 + 当对多轴做 n 维变换时,`shape` 应列出每个变换轴对应的一维长度,或在算子初始化时指定要变换的轴索引。 + +## modes 参数的详细说明 + +本模块额外提供 `modes` 参数,用于在某些维度上 截断频域模态,减少计算量。 + +例如:`modes = (None, 64)` + +表示: + +- 前一维不截断 +- 最后一维只保留前 64 个频率 + +modes 的约束: + +- 必须 `≤ shape[i]//2` +- 默认 `None` 表示使用全量模态 +- 对 RDFTn/IRDFTn 最后一维若 `mode=None` 使用标准 `n//2+1` + +该功能 **scipy.fft 并不支持**,是本库强化特性,特别适用于 PINN / 流体建模中频率裁剪以降低开销。 + +## 与 scipy.fft 的对比示例 + +下面给出若干最小示例,展示模块算子与 `scipy.fft` 的对应用法,供用户参考: + +- **RDFT 与 scipy.fft.rfft** + +```python +# 使用 sciops +rdft = RDFTn(shape=(N,)) +out_real, out_imag = rdft(x_real) + +# 对应 scipy +from scipy.fft import rfft +X = rfft(x_real, n=N) + +``` + +- **DFT 与 scipy.fft.fft** + +```python +# 使用 sciops +dft = DFTn(shape=(N,)) +Xr, Xi = dft(x_complex_as_two_arrays) + +# 对应 scipy +from scipy.fft import fft +X = fft(x_complex, n=N) + +``` + +(请根据模块实际返回格式调整示例:若算子返回分开的实部/虚部,示例中应保持一致。) + +## 使用样例 + +- RDFT 使用样例 + +```python +import mindspore as ms +from mindscience.sciops import RDFTn + +x = ms.ops.rand((2, 32, 512)) +rdft = RDFTn(shape=x.shape[-2:]) +br, bi = rdft(x) +``` + +- DCT 使用样例 + +```python +import mindspore as ms +from mindscience.sciops import DCT + +a = ms.ops.rand((4, 128)) +dct = DCT(shape=(128,)) +b = dct(a) + +``` + -- Gitee