├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── action.yml ├── .gitignore ├── CHANGES.txt ├── DOCUMENTATION.md ├── Dockerfile ├── Dockerfile.20.04 ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── build_multi.sh ├── plip ├── __init__.py ├── basic │ ├── __init__.py │ ├── config.py │ ├── logger.py │ ├── parallel.py │ ├── remote.py │ └── supplemental.py ├── exchange │ ├── __init__.py │ ├── json.py │ ├── report.py │ ├── webservices.py │ └── xml.py ├── plipcmd.py ├── structure │ ├── __init__.py │ ├── detection.py │ └── preparation.py ├── test │ ├── __init__.py │ ├── pdb │ │ ├── 1acj.pdb │ │ ├── 1aku.pdb │ │ ├── 1ay8.pdb │ │ ├── 1bju.pdb │ │ ├── 1bma.pdb │ │ ├── 1eve.pdb │ │ ├── 1h2t.pdb │ │ ├── 1het.pdb │ │ ├── 1hii.pdb │ │ ├── 1hpx.pdb │ │ ├── 1hvi.pdb │ │ ├── 1hwu.pdb │ │ ├── 1n7g.pdb │ │ ├── 1osn.pdb │ │ ├── 1p5e.pdb │ │ ├── 1rla.pdb │ │ ├── 1rmd.pdb │ │ ├── 1tf6.pdb │ │ ├── 1vfy.pdb │ │ ├── 1vsn.pdb │ │ ├── 1x0n_state_1.pdb │ │ ├── 1xdn.pdb │ │ ├── 2efj.pdb │ │ ├── 2iuz.pdb │ │ ├── 2ndo.pdb │ │ ├── 2pvb.pdb │ │ ├── 2q8q.pdb │ │ ├── 2reg.pdb │ │ ├── 2w0s.pdb │ │ ├── 2zoz.pdb │ │ ├── 3ems.pdb │ │ ├── 3o1h.pdb │ │ ├── 3og7.pdb │ │ ├── 3pxf.pdb │ │ ├── 3r0t.pdb │ │ ├── 3shy.pdb │ │ ├── 3tah.pdb │ │ ├── 3thy.pdb │ │ ├── 4agl.pdb │ │ ├── 4alw.pdb │ │ ├── 4cum.pdb │ │ ├── 4day.pdb │ │ ├── 4dst.pdb │ │ ├── 4dst_protonated.pdb │ │ ├── 4gql.pdb │ │ ├── 4kya.pdb │ │ ├── 4pjt.pdb │ │ ├── 4qnb.pdb │ │ ├── 4rao.pdb │ │ ├── 4rdl.pdb │ │ ├── 4yb0.pdb │ │ ├── 5ddr.pdb │ │ ├── 6g99.pdb │ │ ├── 6nhb.pdb │ │ └── 6r79.pdb │ ├── run_all_tests.sh │ ├── special │ │ ├── empty.pdb │ │ └── non-pdb.pdb │ ├── test_basic_functions.py │ ├── test_command_line.py │ ├── test_hydrogen_bonds.py │ ├── test_intra.py │ ├── test_literature_validated.py │ ├── test_metal_coordination.py │ ├── test_pi_stacking.py │ ├── test_remote_services.py │ ├── test_salt_bridges.py │ ├── test_structure_processing.py │ ├── test_visualization.py │ ├── test_water_bridges.py │ ├── test_xml_parser.py │ ├── test_xml_writer.py │ └── xml │ │ └── 1vsn.report.xml └── visualization │ ├── __init__.py │ ├── chimera.py │ ├── pymol.py │ └── visualize.py ├── pliplogo.png ├── pliplogo.svg ├── requirements.txt ├── setup.cfg └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | name: PLIP Build 2 | 3 | on: 4 | [push, pull_request] 5 | 6 | jobs: 7 | docker-hub: 8 | name: Deployment 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Deploy Step 13 | uses: docker/build-push-action@v1 14 | with: 15 | username: ${{ secrets.DOCKER_HUB_USER }} 16 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 17 | repository: pharmai/plip 18 | tag_with_ref: true 19 | push: ${{ startsWith(github.ref, 'refs/tags/') }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | # C extensions 5 | *.so 6 | # Distribution / packaging 7 | .Python 8 | env/ 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | downloads/ 13 | eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | # PyInstaller 23 | # Usually these files are written by a python script from a template 24 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 25 | *.manifest 26 | *.spec 27 | # Installer logs 28 | pip-log.txt 29 | pip-delete-this-directory.txt 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | # Translations 38 | *.mo 39 | *.pot 40 | # Django stuff: 41 | *.log 42 | # Sphinx documentation 43 | docs/_build/ 44 | # PyBuilder 45 | target/ 46 | # PyCharm 47 | .idea/* 48 | # Other 49 | *~ 50 | .nfs* 51 | tests/ -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | # 2.4.0 4 | * new "--chains" flag to enable detection of interactions between protein chains by @PhiCMS and @snbolz 5 | * update setup.py, attempt to fix and install broken python openbabel bindings 3.1.1.1 6 | * update Dockerfile 7 | 8 | # 2.3.1 9 | * fixes in interaction detection with peptide ligands by @kalinni in #153 10 | * fix in hydrogen bond acceptor identification by @kalinni in #151 11 | * fixes in waterbridge detection and filtering by @kalinni in #152 12 | * fix NumPy DeprecationWarning when using numpy.uint32 with negative numbers by @QY0831 in #150 13 | 14 | # 2.3.0 15 | * fixes an issue that caused encoding errors to be thrown 16 | * salt bridge interaction improved 17 | * minor bugs fixed 18 | 19 | # 2.2.2 20 | * fixes an issue that caused encoding errors to be thrown 21 | 22 | # 2.2.1 23 | * updates citation information to latest paper: https://doi.org/10.1093/nar/gkab294 24 | 25 | # 2.2.0 26 | * new minor release 27 | * increased detail level of report (individual atoms on protein side) 28 | * improved handling of multi-model structures 29 | * minor bug fixes and code optimizations 30 | 31 | # 2.1.9 32 | * fixes issues with DNA receptor handling 33 | 34 | # 2.1.8 35 | * report of individual binding site atoms 36 | 37 | # 2.1.7 38 | * bug fixes 39 | 40 | # 2.1.6 41 | * fetch URL for PDB files updated to avoid issues with RCSB API changes 42 | 43 | # 2.1.5 44 | * option added to handle specific model in NMR structures 45 | * fixes a bug in alt-location handling 46 | 47 | # 2.1.0 48 | * maintainer changed to PharmAI GmbH 49 | * full compatibility to Python 3 and OpenBabel 3 50 | * Docker and Singularity support, Deployment to Docker Hub 51 | * dropped support for OpenBabel 2, Python 2 52 | * reorganization of modules 53 | * migrates to proper logging pattern 54 | * code quality enhancements 55 | * adds option to disable non-deterministic protonation of structures (`--nohydro`) 56 | * protonated structures are now stored to guarantee consistent interaction detection 57 | * new options to change verbosity levels 58 | * bug fixes in code and deployment 59 | * multi-architecture builds on https://hub.docker.com/r/pharmai/plip 60 | * Docker build for Ubuntu 20.04 LTS 61 | 62 | # 1.4.5 63 | * Updates contact info 64 | 65 | # 1.4.4 66 | * Improved parameters for water bridge detection 67 | * Added unit test for PDB structure 1HPX 68 | * PEP8 Changes 69 | * Improved imports 70 | 71 | # 1.4.3 72 | * Adds information on covalent linkages to XML files 73 | * __Anaconda package__ (provided by `Ikagami`) 74 | 75 | # 1.4.2 76 | * Adds "--name" option to change name of output files 77 | * Adds "--nopdbcanmap" option to skip calculation of canonical->PDB mapping which can lead to segfaults with OpenBabel 78 | * Improved handling of ligand names 79 | 80 | # 1.4.1 81 | * Improved import of modules 82 | * Corrections for README and documentation 83 | * Several improvements for error handling 84 | * independence from PyMOL when used without visualization features 85 | 86 | # 1.4.0 87 | * __Full Python 3 Compatibility__ 88 | * __Read PDB files from stdin with "-f -" and write to stdout with "-O" (capital "O")__ 89 | * Improves handling of fixed PDB files 90 | * Option to turn off writing fixed PDB structures to a file ("--nofixfile") 91 | 92 | ### 1.3.5 93 | * Preparation for Python 3: Imports 94 | * small correction for PDB fixing 95 | * includes TODO files with user suggestions 96 | * license adapted to GNU GPLv2 97 | 98 | ### 1.3.4 99 | * __DNA/RNA can be selected as receptor with --dnareceptor__ 100 | * __Composite ligands and intra-protein mode: Annotation which part of the ligand interacts with the receptor__ 101 | * Improved handling of NMR structures 102 | * Filter for extremely large ligands 103 | * Speed-up for file reading and parallel visualization 104 | * More debugging messages 105 | 106 | ### 1.3.3 107 | * __Adds XML Parser module for PLIP XML files__ 108 | * Detection of intramolecular interactions with --intra 109 | * improved error correction in PDB files 110 | 111 | ### 1.3.2 112 | * __Processing of protein-peptide interactions via --peptides__ 113 | * option to keep modified residues as ligands (--keepmod) 114 | * Improved code for reports 115 | * Smart ordering of ligand in composite compounds 116 | * Fixes handling and visualization of DNA/RNA 117 | 118 | ### 1.3.1 119 | * __Support for amino acids as ligands__ 120 | * __Plugin-ready for PyMOL and Chimera__ 121 | * Refactores code and optimized input 122 | * Improved verbose and debug log system 123 | * Bugfixes for problems affecting some structures with aromatic rings 124 | 125 | ### 1.3.0 126 | * __Batch processing__ 127 | * Improvements to verbose mode and textual output 128 | 129 | ### 1.2.3 130 | * __Better support for files from MD and docking software__ 131 | * __Fixes issues with large and complex structures__ 132 | * Speed optimizations 133 | 134 | 135 | ### 1.2.2 136 | * __Option to consider alternate atom locations (e.g. for ligands with several conformations__ 137 | * Automatic fixing of missing ligand names 138 | * Improved handling of broken PDB files and non-standard filenames 139 | * Improved error handling 140 | 141 | ### 1.2.1 142 | * __Mapping of canonical atom order to PDB atom order for each ligand__ 143 | * __Introduction of debug mode (--debug)__ 144 | * More robust visualization 145 | * Handling of negative residue numbers for more cases 146 | * Composite members in alphabetical order 147 | * Fixes errors in aromatic ring detection 148 | * Code improvements 149 | 150 | ### 1.2.0 151 | * __Support for DNA and RNA as ligands__ 152 | * __Detection of metal complexes with proteins/ligands, including prediction of geometry__ 153 | * __Extended result files with detailed information on binding site residues and unpaired atoms__ 154 | *__Support for zipped and gzipped files__ 155 | * Rich verbose mode in command line with information on detected functional groups and interactions 156 | * Automatic fixing of common errors in custom PDB files 157 | * Refined binding site selection 158 | * Better overall performance 159 | * Initial test suite for metal coordination 160 | * Classification of ligands 161 | * Improves detection of aromatic rings and interactions involving aromatic rings 162 | * Single nucleotides and ions not excluded anymore as ligands 163 | * Generation of canonical smiles for complete (composite) ligands 164 | * Generation of txt files is now optional 165 | * Basic support for PDBQT files 166 | * Correct handling of negative chain positions of ligands 167 | * Improved check for valid PDB IDs 168 | * Fixes several bugs 169 | 170 | 171 | 172 | ### 1.1.1 173 | * __Detailed information on binding site residues in XML files__ 174 | * Improved extraction of binding site residues 175 | * Information whether halogen bonds are made with side- or main chain of protein 176 | 177 | #### 1.1.0 178 | * __Folder structure and setup.py for automatic installation using pip__ 179 | * __H-Bond Donor Prioritization (see documentation for details)__ 180 | * Adds separate changelog 181 | * Updated documentation and citation information 182 | * Reduction of blacklist usage 183 | * Information on excluded ligands in result files 184 | 185 | #### 1.0.2 186 | * __Automatic grouping of composite ligands (e.g. polysaccharides)__ 187 | * __Proper handling of alternative conformations in PDB structures__ 188 | * __Exclusion of modified residues as ligands__ 189 | * __Improved detection of hydrogen bonds__ 190 | * __Prioritization of hydrogen bonds__ 191 | * Adds atom type description in the output files 192 | * Basic support for usage on Windows (without multithreading) 193 | * Option to turn multithreading off by setting maxthreads to 0 194 | * Improved detection of hydrogen bond donors in ligands 195 | * Adaption of standard parameters 196 | * Fixes a bug in PyMOL visualization script leading to missing or wrong interactions with pseudoatoms 197 | * Fixes a bug leading to duplicate or triplicate detection of identical pi-cation interactions with guanidine 198 | * Adds now unit tests 199 | * Small changes to existing unit tests for new features 200 | 201 | #### 1.0.1 202 | * __Option to change detection thresholds permanently or for single runs__ 203 | * Option to (de)activate output for images, PyMOL session files and XML files 204 | * Changed standard behaviour to output of RST report only 205 | * Information on sidechain/backbone hydrogen bond type 206 | * Sorted output 207 | * Detection of more flavors of halogen bonds 208 | * Fixed bug leading to duplicate interactions with quartamine groups 209 | 210 | #### 1.0.0 211 | * __Initial Release__ 212 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable 2 | 3 | LABEL maintainer="PharmAI GmbH " \ 4 | org.label-schema.name="PLIP: The Protein-Ligand Interaction Profiler" \ 5 | org.label-schema.description="https://www.doi.org/10.1093/nar/gkv315" 6 | 7 | ENV DEBIAN_FRONTEND=noninteractive 8 | RUN apt-get update && apt-get install -y \ 9 | pymol \ 10 | python3-distutils \ 11 | python3-lxml \ 12 | python3-openbabel \ 13 | python3-pymol; \ 14 | apt-get clean && rm -rf /var/lib/apt/lists/* 15 | 16 | # copy PLIP source code 17 | WORKDIR /src 18 | ADD plip/ plip/ 19 | RUN chmod +x plip/plipcmd.py 20 | ENV PYTHONPATH=/src 21 | 22 | # execute tests 23 | WORKDIR /src/plip/test 24 | RUN chmod +x run_all_tests.sh 25 | RUN ./run_all_tests.sh 26 | WORKDIR / 27 | 28 | # set entry point to plipcmd.py 29 | ENTRYPOINT ["python3", "/src/plip/plipcmd.py"] 30 | -------------------------------------------------------------------------------- /Dockerfile.20.04: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 AS builder 2 | 3 | LABEL maintainer="PharmAI GmbH " \ 4 | org.label-schema.name="PLIP: The Protein-Ligand Interaction Profiler" \ 5 | org.label-schema.description="https://www.doi.org/10.1093/nar/gkv315" 6 | 7 | ENV DEBIAN_FRONTEND noninteractive 8 | RUN apt-get update && apt-get install -y \ 9 | git \ 10 | libopenbabel-dev \ 11 | libopenbabel6 \ 12 | pymol \ 13 | python3-distutils \ 14 | python3-lxml \ 15 | python3-openbabel \ 16 | python3-pymol \ 17 | openbabel 18 | 19 | # copy PLIP source code 20 | WORKDIR /src 21 | ADD plip/ plip/ 22 | RUN chmod +x plip/plipcmd.py 23 | ENV PYTHONPATH $PYTHONPATH:/src 24 | 25 | # execute tests 26 | WORKDIR /src/plip/test 27 | RUN chmod +x run_all_tests.sh 28 | RUN ./run_all_tests.sh 29 | WORKDIR / 30 | 31 | # set entry point to plipcmd.py 32 | ENTRYPOINT ["python3", "/src/plip/plipcmd.py"] -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include plip *.py 2 | include *.txt 3 | include *.md 4 | prune plip/test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Protein-Ligand Interaction Profiler (PLIP) 2 | 3 | ![PLIP Build](https://github.com/pharmai/plip/workflows/PLIP%20Build/badge.svg) 4 | ![GitHub](https://img.shields.io/github/license/pharmai/plip?style=social) 5 | ![GitHub All Releases](https://img.shields.io/github/downloads/pharmai/plip/total?style=social) 6 | ![Docker Pulls](https://img.shields.io/docker/pulls/pharmai/plip?style=social&logo=docker) 7 | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/pharmai/plip/latest?style=social&logo=docker) 8 | 9 | Analyze noncovalent protein-ligand interactions in 3D structures with ease. 10 | 11 | PLIP Logo 12 | 13 | 14 | | Use Case | [Web Server](https://plip-tool.biotec.tu-dresden.de) | Docker | Singularity | Python Module | [Colab](https://colab.research.google.com/drive/1KV0Yx6vMFXvtyIRtmkPoMLN0Q6GKG1pm?usp=sharing) | 15 | |---------------------------------------------------------------------------|--------------------|--------------------|--------------------|--------------------|--------------------| 16 | | "I want to analyze my protein-ligand complex!" | :heavy_check_mark: | :heavy_check_mark: | :yellow_circle: | :x: | :heavy_check_mark: | 17 | | "I want to analyze *a billion* protein-ligand complexes!" | :x: | :yellow_circle: | :heavy_check_mark: | :yellow_circle: | :x: | 18 | | "I love the Linux command line and want to build a workflow around PLIP!" | :x: | :heavy_check_mark: | :heavy_check_mark: | :yellow_circle: |:x: | 19 | | "I'm a Python programmer and want to use PLIP in my project!" | :x: | :yellow_circle: | :yellow_circle: | :heavy_check_mark: | :yellow_circle: | 20 | | "I want to analyze large complexes!" | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 21 | 22 | --- 23 | 24 | ## Quickstart 25 | 26 | ### Docker 27 | If you have Docker installed, you can run a PLIP analysis for the structure `1vsn` with the following shell command: 28 | 29 | On Linux / MacOS: 30 | ```bash 31 | $ docker run --rm \ 32 | -v ${PWD}:/results \ 33 | -w /results \ 34 | -u $(id -u ${USER}):$(id -g ${USER}) \ 35 | pharmai/plip:latest -i 1vsn -yv 36 | ``` 37 | 38 | On Windows: 39 | ```bash 40 | $ docker run --rm \ 41 | -v ${PWD}:/results \ 42 | -w /results \ 43 | -u $(id -u ${USER}):$(id -g ${USER}) \ 44 | pharmai/plip:latest -i 1vsn -yv 45 | ``` 46 | ### Singularity 47 | 48 | The equivalent command for our pre-built [Singularity](https://singularity.lbl.gov/) image for Linux (available under [Releases](https://github.com/pharmai/plip/releases)) is as follows: 49 | 50 | ```bash 51 | $ ./plip.simg -i 1vsn -yv 52 | ``` 53 | 54 | Singularity allows to use PLIP with ease in HPC environments. Note that you need to have Singularity installed on your base system. 55 | 56 | ### Google Colab 57 | PLIP is available as a [Google Colab](https://colab.research.google.com/drive/1KV0Yx6vMFXvtyIRtmkPoMLN0Q6GKG1pm?usp=sharing) to be used without the need to install it locally. It can be used without constraints in terms of 58 | PDB file and protein sizes. 59 | 60 | --- 61 | 62 | ## Usage 63 | 64 | This README provides instructions for setup and using basic functions of PLIP. 65 | For more details, see the [Documentation](DOCUMENTATION.md). 66 | 67 | ### 1. Install PLIP 68 | 69 | #### Containerized Image (recommended) 70 | :exclamation: We ship PLIP as pre-built containers for multiple architectures (amd64/ARM), available on the [Docker Hub](https://hub.docker.com/r/pharmai/plip) or as pre-built Singularity image under [Releases](https://github.com/pharmai/plip/releases). See the quickstart section above for usage instructions. 71 | 72 | #### Dependencies 73 | If you cannot use the containerized bundle or want to use PLIP sources, make sure you have the following requirements installed: 74 | - Python >= 3.6.9 75 | - [OpenBabel](#Installing-OpenBabel) >= 3.0.0 with [Python bindings](https://open-babel.readthedocs.io/en/latest/UseTheLibrary/PythonInstall.html) 76 | - PyMOL >= 2.3.0 with Python bindings (optional, for visualization only) 77 | - ImageMagick >= 7.0 (optional) 78 | 79 | **Python:** If you are on a system where Python 3 is executed using `python3` instead of just `python`, replace the `python` and `pip` commands in the following steps with `python3` and `pip3` accordingly. 80 | 81 | **OpenBabel:** Many users have trouble setting up OpenBabel with Python bindings correctly. We therefore provide some [installation help for OpenBabel](#Installing-OpenBabel) below. 82 | 83 | #### From Source 84 | 85 | Open a terminal and clone this repository using 86 | ```bash 87 | $ git clone https://github.com/pharmai/plip.git 88 | ``` 89 | 90 | Either set your `PYTHONPATH` environment variable to the root directory of your PLIP repository or run the following command in it 91 | 92 | ```bash 93 | $ python setup.py install 94 | ``` 95 | 96 | #### Via PyPi 97 | We deploy the PLIP package to [PyPi](https://pypi.org/project/plip/). You can install PLIP as Python module with: 98 | 99 | ```bash 100 | $ pip install plip 101 | ``` 102 | 103 | **Note:** Be aware that you still have to install the above mentioned dependencies and link them correctly. 104 | 105 | ### 2. Run PLIP 106 | 107 | #### Command Line Tool 108 | 109 | Run the `plipcmd.py` script inside the PLIP folder to detect, report, and visualize interactions. The following example creates a PYMOL visualization for the interactions between the inhibitor [NFT](https://www.rcsb.org/ligand/NFT) and its target protein in the PDB structure [1vsn](https://www.rcsb.org/structure/1VSN). 110 | 111 | **Note:** If you have installed PLIP with `python setup.py install` or PyPi, you will not have to set an alias for the `plip` command. 112 | 113 | ```bash 114 | # Set an alias to make your life easier and create and enter /tmp/1vsn 115 | $ alias plip='python ~/plip/plip/plipcmd.py' 116 | $ mkdir /tmp/1vsn && cd /tmp/1vsn 117 | # Run PLIP for 1vsn and open the resulting visualization in PyMOL 118 | $ plip -i 1vsn -yv 119 | $ pymol 1VSN_NFT_A_283.pse 120 | ``` 121 | 122 | #### Python Module 123 | In your terminal, add the PLIP repository to your `PYTHONPATH` variable. For our example, we also download a PDB file for testing. 124 | ```bash 125 | $ export PYTHONPATH=~/plip:${PYTHONPATH} 126 | $ cd /tmp && wget http://files.rcsb.org/download/1EVE.pdb 127 | $ python 128 | ``` 129 | In python, import the PLIP modules, load a PDB structure and run the analysis. 130 | This small example shows how to print all numbers of residues involved in pi-stacking: 131 | 132 | ```python 133 | from plip.structure.preparation import PDBComplex 134 | 135 | my_mol = PDBComplex() 136 | my_mol.load_pdb('/tmp/1EVE.pdb') # Load the PDB file into PLIP class 137 | print(my_mol) # Shows name of structure and ligand binding sites 138 | my_bsid = 'E20:A:2001' # Unique binding site identifier (HetID:Chain:Position) 139 | my_mol.analyze() 140 | my_interactions = my_mol.interaction_sets[my_bsid] # Contains all interaction data 141 | 142 | # Now print numbers of all residues taking part in pi-stacking 143 | print([pistack.resnr for pistack in my_interactions.pistacking]) # Prints [84, 129] 144 | ``` 145 | 146 | ### 3. Investigate the Results 147 | PLIP offers various output formats, ranging from renderes images and PyMOL session files to human-readable text files and XML files. By default, all files are deposited in the working directory unless and output path is provided. For a full documentation of running options and output formats, please refer to the [Documentation](DOCUMENTATION.md). 148 | 149 | ## Versions and Branches 150 | For production environments, you should use the latest tagged commit from the `master` branch or refer to the [Releases](https://github.com/pharmai/plip/releases) page. Newer commits from the `master` and `development` branch may contain new but untested and not documented features. 151 | 152 | ## Contributors 153 | - Sebastian Salentin (original author) | [github.com/ssalentin](https://github.com/ssalentin) 154 | - Joachim Haupt | [github.com/vjhaupt](https://github.com/vjhaupt) 155 | - Melissa F. Adasme Mora | [github.com/madasme](https://github.com/madasme) 156 | - Alexandre Mestiashvili | [github.com/mestia](https://github.com/mestia) 157 | - Christoph Leberecht | [github.com/cleberecht](https://github.com/cleberecht) 158 | - Florian Kaiser | [github.com/fkaiserbio](https://github.com/fkaiserbio) 159 | - Katja Linnemann | [github.com/kalinni](https://github.com/kalinni) 160 | 161 | ## PLIP Web Server 162 | Visit our PLIP Web Server on [plip-tool.biotec.tu-dresden.de](https://plip-tool.biotec.tu-dresden.de). 163 | 164 | ## License Information 165 | PLIP is published under the GNU GPLv2. For more information, please read the `LICENSE.txt` file. 166 | Using PLIP in your commercial or non-commercial project is generally possible when giving a proper reference to this project and the publication in NAR. 167 | 168 | ## Citation Information 169 | If you are using PLIP in your work, please cite 170 | > Adasme,M. et al. PLIP 2021: expanding the scope of the protein-ligand interaction profiler to DNA and RNA. 171 | > Nucl. Acids Res. (05 May 2021), gkab294. doi: 10.1093/nar/gkab294 172 | 173 | or 174 | 175 | > Salentin,S. et al. PLIP: fully automated protein-ligand interaction profiler. 176 | > Nucl. Acids Res. (1 July 2015) 43 (W1): W443-W447. doi: 10.1093/nar/gkv315 177 | 178 | ## FAQ 179 | > I try to run PLIP, but I'm getting an error message saying: 180 | > ValueError: [...] is not a recognised Open Babel descriptor type 181 | > 182 | Make sure OpenBabel is correctly installed. This error can occur if the installed Python bindings don't match the OpenBabel version on your machine. 183 | We don't offer technical support for installation of third-party packages but added some [installation help for OpenBabel](#Installing-OpenBabel) below. 184 | Alternatively you can refer to their [website](https://openbabel.org/docs/dev/Installation/install.html). 185 | 186 | > I'm unsure on how to run PLIP and don't have much Linux experience. 187 | > 188 | You should consider running PLIP as Docker image, as we describe above. 189 | 190 | > PLIP is reporting different interactions on several runs! 191 | > 192 | Due to the non-deterministic nature on how hydrogen atoms can be added to the input structure, it cannot be guaranteed that each run returns exactly the same set of interactions. If you want to make sure to achieve consistent results, you can: 193 | 194 | - protonate the input structure once with PLIP or your tool of preference 195 | - run PLIP with `--nohydro` 196 | 197 | > How does PLIP handle NMR structures? 198 | > 199 | By default PLIP uses the first model it sees in a PDB file. You can change this behavior with the flag `--model`. 200 | 201 | ## Installing OpenBabel 202 | As many users encounter problems with installing the required OpenBabel tools, we want to provide some help here. However, we cannot offer technical support. Comprehensive information about the installation of OpenBabel for Windows, Linux, and macOS can be found in the [OpenBabel wiki](http://openbabel.org/wiki/Category:Installation) and the [OpenBabel Docs](https://open-babel.readthedocs.io/en/latest/Installation/install.html). 203 | Information about the installation of [OpenBabel Python bindings](https://open-babel.readthedocs.io/en/latest/UseTheLibrary/PythonInstall.html) can also be found there. 204 | 205 | ### Using Conda, HomeBrew or the binary Windows Installer 206 | 207 | Install OpenBabel using the [binary from GitHub](https://github.com/openbabel/openbabel/releases/latest) or with 208 | ```bash 209 | # For Conda users 210 | $ conda install openbabel -c conda-forge 211 | # On macOS 212 | $ brew install open-babel 213 | ``` 214 | Install the Python bindings with 215 | ```bash 216 | $ pip install openbabel 217 | ``` 218 | **Note:** If you have trouble, make sure the OpenBabel version matches the one for the python bindings! 219 | 220 | ### Using your Package Manager (Example for Ubuntu 20.04) 221 | 222 | ```bash 223 | $ apt-get update && apt-get install -y \ 224 | libopenbabel-dev \ 225 | libopenbabel6 \ 226 | python3-openbabel \ 227 | openbabel 228 | ``` 229 | ### From Source (Example for Ubuntu 18.04) 230 | Clone the OpenBabel repository into the /src directory 231 | ```bash 232 | $ git clone -b openbabel-3-0-0 \ 233 | https://github.com/openbabel/openbabel.git 234 | ``` 235 | 236 | Within /src/openbabel create end enter a directory /build and configure the build using 237 | ```bash 238 | $ cmake .. \ 239 | -DPYTHON_EXECUTABLE=/usr/bin/python3.6 \ 240 | -DPYTHON_BINDINGS=ON \ 241 | -DCMAKE_INSTALL_PREFIX=/usr/local \ 242 | -DRUN_SWIG=ON 243 | ``` 244 | From within the same directory (/src/openbabel/build) compile and install using 245 | ```bash 246 | $ make -j$(nproc --all) install 247 | ``` 248 | ## Contact / Maintainer 249 | As of April 2020 PLIP is now officially maintained by [PharmAI GmbH](https://pharm.ai). Do you have feature requests, found a bug or want to use PLIP in your project? Commercial support is available upon request. 250 | 251 | ![](https://www.pharm.ai/wp-content/uploads/2020/04/PharmAI_logo_color_no_slogan_500px.png) 252 | 253 | Please get in touch: `hello@pharm.ai` 254 | -------------------------------------------------------------------------------- /build_multi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version=$(grep version plip/basic/config.py | awk '{print $3}' | sed s/\'//g) 4 | echo "Building multi-architecture version $version..." 5 | export DOCKER_CLI_EXPERIMENTAL=enabled 6 | docker run --rm --privileged docker/binfmt:820fdd95a9972a5308930a2bdfb8573dd4447ad3 7 | docker buildx create --use --name mybuilder 8 | docker buildx build -t pharmai/plip:"$version"-multi --platform=linux/arm/v7,linux/arm64/v8,linux/amd64 --push . -------------------------------------------------------------------------------- /plip/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharmai/plip/e4181b2c40fe5938b5f39aaf00ae3ea9d14ac9cd/plip/__init__.py -------------------------------------------------------------------------------- /plip/basic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharmai/plip/e4181b2c40fe5938b5f39aaf00ae3ea9d14ac9cd/plip/basic/__init__.py -------------------------------------------------------------------------------- /plip/basic/config.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.4.0' 2 | __maintainer__ = 'PharmAI GmbH (2020-2021) - www.pharm.ai - hello@pharm.ai' 3 | __citation_information__ = "Adasme,M. et al. PLIP 2021: expanding the scope of the protein-ligand interaction profiler to DNA and RNA. " \ 4 | "Nucl. Acids Res. (05 May 2021), gkab294. doi: 10.1093/nar/gkab294" 5 | 6 | import logging 7 | 8 | DEFAULT_LOG_LEVEL = logging.INFO 9 | VERBOSE = False # Set verbose mode 10 | QUIET = False # Set verbose mode 11 | SILENT = False # Set verbose mode 12 | MAXTHREADS = 1 # Maximum number of main threads for binding site visualization 13 | XML = False 14 | TXT = False 15 | PICS = False 16 | PYMOL = False 17 | STDOUT = False 18 | RAWSTRING = False # use raw strings for input / output 19 | OUTPATH = './' 20 | BASEPATH = './' 21 | BREAKCOMPOSITE = False # Break up composite ligands with covalent bonds 22 | ALTLOC = False # Consider alternate locations 23 | PLUGIN_MODE = False # Special mode for PLIP in Plugins (e.g. PyMOL) 24 | NOFIX = False # Turn off fixing of errors in PDB files 25 | NOFIXFILE = False # Turn off writing to files for fixed PDB structures 26 | PEPTIDES = [] # Definition which chains should be considered as peptide ligands 27 | INTRA = None 28 | RESIDUES = {} 29 | KEEPMOD = False 30 | DNARECEPTOR = False 31 | OUTPUTFILENAME = "report" # Naming for the TXT and XML report files 32 | NOPDBCANMAP = False # Skip calculation of mapping canonical atom order: PDB atom order 33 | NOHYDRO = False # Do not add hydrogen bonds (in case already present in the structure) 34 | MODEL = 1 # The model to be selected for multi-model structures (default = 1). 35 | CHAINS = None # Define chains for protein-protein interaction detection 36 | 37 | 38 | # Configuration file for Protein-Ligand Interaction Profiler (PLIP) 39 | # Set thresholds for detection of interactions 40 | 41 | # Thresholds for detection (global variables) 42 | BS_DIST = 7.5 # Determines maximum distance to include binding site residues 43 | AROMATIC_PLANARITY = 5.0 # Determines allowed deviation from planarity in aromatic rings 44 | MIN_DIST = 0.5 # Minimum distance for all distance thresholds 45 | # Some distance thresholds were extended (max. 1.0A) if too restrictive too account for low-quality structures 46 | HYDROPH_DIST_MAX = 4.0 # Distance cutoff for detection of hydrophobic contacts 47 | HBOND_DIST_MAX = 4.1 # Max. distance between hydrogen bond donor and acceptor (Hubbard & Haider, 2001) + 0.6 A 48 | HBOND_DON_ANGLE_MIN = 100 # Min. angle at the hydrogen bond donor (Hubbard & Haider, 2001) + 10 49 | PISTACK_DIST_MAX = 5.5 # Max. distance for parallel or offset pistacking (McGaughey, 1998) 50 | PISTACK_ANG_DEV = 30 # Max. Deviation from parallel or perpendicular orientation (in degrees) 51 | PISTACK_OFFSET_MAX = 2.0 # Maximum offset of the two rings (corresponds to the radius of benzene + 0.5 A) 52 | PICATION_DIST_MAX = 6.0 # Max. distance between charged atom and aromatic ring center (Gallivan and Dougherty, 1999) 53 | SALTBRIDGE_DIST_MAX = 5.5 # Max. distance between centers of charge for salt bridges (Barlow and Thornton, 1983) + 1.5 54 | HALOGEN_DIST_MAX = 4.0 # Max. distance between oxy. and halogen (Halogen bonds in biological molecules., Auffinger)+0.5 55 | HALOGEN_ACC_ANGLE = 120 # Optimal acceptor angle (Halogen bonds in biological molecules., Auffinger) 56 | HALOGEN_DON_ANGLE = 165 # Optimal donor angle (Halogen bonds in biological molecules., Auffinger) 57 | HALOGEN_ANGLE_DEV = 30 # Max. deviation from optimal angle 58 | WATER_BRIDGE_MINDIST = 2.5 # Min. distance between water oxygen and polar atom (Jiang et al., 2005) -0.1 59 | WATER_BRIDGE_MAXDIST = 4.1 # Max. distance between water oxygen and polar atom (Jiang et al., 2005) +0.5 60 | WATER_BRIDGE_OMEGA_MIN = 71 # Min. angle between acceptor, water oxygen and donor hydrogen (Jiang et al., 2005) - 9 61 | WATER_BRIDGE_OMEGA_MAX = 140 # Max. angle between acceptor, water oxygen and donor hydrogen (Jiang et al., 2005) 62 | WATER_BRIDGE_THETA_MIN = 100 # Min. angle between water oxygen, donor hydrogen and donor atom (Jiang et al., 2005) 63 | METAL_DIST_MAX = 3.0 # Max. distance between metal ion and interacting atom (Harding, 2001) 64 | 65 | # Other thresholds 66 | MAX_COMPOSITE_LENGTH = 200 # Filter out ligands with more than 200 fragments 67 | 68 | ######### 69 | # Names # 70 | ######### 71 | 72 | # Names of RNA and DNA residues to be considered (detection by name) 73 | RNA = ['U', 'A', 'C', 'G'] 74 | DNA = ['DT', 'DA', 'DC', 'DG'] 75 | 76 | ############# 77 | # Whitelist # 78 | ############# 79 | 80 | # Metal cations which can be complexed 81 | 82 | METAL_IONS = ['CA', 'CO', 'MG', 'MN', 'FE', 'CU', 'ZN', 'FE2', 'FE3', 'FE4', 'LI', 'NA', 'K', 'RB', 'SR', 'CS', 'BA', 83 | 'CR', 'NI', 'FE1', 'NI', 'RU', 'RU1', 'RH', 'RH1', 'PD', 'AG', 'CD', 'LA', 'W', 'W1', 'OS', 'IR', 'PT', 84 | 'PT1', 'AU', 'HG', 'CE', 'PR', 'SM', 'EU', 'GD', 'TB', 'YB', 'LU', 'AL', 'GA', 'IN', 'SB', 'TL', 'PB'] 85 | 86 | ############## 87 | # Blacklists # 88 | ############## 89 | 90 | # Other Ions/Atoms (not yet supported) 91 | anions = ['CL', 'IOD', 'BR'] 92 | other = ['MO', 'RE', 'HO'] 93 | UNSUPPORTED = anions + other 94 | 95 | # BioLiP list of suspicious ligands from http://zhanglab.ccmb.med.umich.edu/BioLiP/ligand_list (2014-07-10) 96 | # Add ligands here to get warnings for possible artifacts. 97 | biolip_list = ['ACE', 'HEX', 'TMA', 'SOH', 'P25', 'CCN', 'PR', 'PTN', 'NO3', 'TCN', 'BU1', 'BCN', 'CB3', 'HCS', 'NBN', 98 | 'SO2', 'MO6', 'MOH', 'CAC', 'MLT', 'KR', '6PH', 'MOS', 'UNL', 'MO3', 'SR', 'CD3', 'PB', 'ACM', 'LUT', 99 | 'PMS', 'OF3', 'SCN', 'DHB', 'E4N', '13P', '3PG', 'CYC', 'NC', 'BEN', 'NAO', 'PHQ', 'EPE', 'BME', 'TB', 100 | 'ETE', 'EU', 'OES', 'EAP', 'ETX', 'BEZ', '5AD', 'OC2', 'OLA', 'GD3', 'CIT', 'DVT', 'OC6', 'MW1', 'OC3', 101 | 'SRT', 'LCO', 'BNZ', 'PPV', 'STE', 'PEG', 'RU', 'PGE', 'MPO', 'B3P', 'OGA', 'IPA', 'LU', 'EDO', 'MAC', 102 | '9PE', 'IPH', 'MBN', 'C1O', '1PE', 'YF3', 'PEF', 'GD', '8PE', 'DKA', 'RB', 'YB', 'GGD', 'SE4', 'LHG', 103 | 'SMO', 'DGD', 'CMO', 'MLI', 'MW2', 'DTT', 'DOD', '7PH', 'PBM', 'AU', 'FOR', 'PSC', 'TG1', 'KAI', '1PG', 104 | 'DGA', 'IR', 'PE4', 'VO4', 'ACN', 'AG', 'MO4', 'OCL', '6UL', 'CHT', 'RHD', 'CPS', 'IR3', 'OC4', 'MTE', 105 | 'HGC', 'CR', 'PC1', 'HC4', 'TEA', 'BOG', 'PEO', 'PE5', '144', 'IUM', 'LMG', 'SQU', 'MMC', 'GOL', 'NVP', 106 | 'AU3', '3PH', 'PT4', 'PGO', 'ICT', 'OCM', 'BCR', 'PG4', 'L4P', 'OPC', 'OXM', 'SQD', 'PQ9', 'BAM', 'PI', 107 | 'PL9', 'P6G', 'IRI', '15P', 'MAE', 'MBO', 'FMT', 'L1P', 'DUD', 'PGV', 'CD1', 'P33', 'DTU', 'XAT', 'CD', 108 | 'THE', 'U1', 'NA', 'MW3', 'BHG', 'Y1', 'OCT', 'BET', 'MPD', 'HTO', 'IBM', 'D01', 'HAI', 'HED', 'CAD', 109 | 'CUZ', 'TLA', 'SO4', 'OC5', 'ETF', 'MRD', 'PT', 'PHB', 'URE', 'MLA', 'TGL', 'PLM', 'NET', 'LAC', 'AUC', 110 | 'UNX', 'GA', 'DMS', 'MO2', 'LA', 'NI', 'TE', 'THJ', 'NHE', 'HAE', 'MO1', 'DAO', '3PE', 'LMU', 'DHJ', 111 | 'FLC', 'SAL', 'GAI', 'ORO', 'HEZ', 'TAM', 'TRA', 'NEX', 'CXS', 'LCP', 'HOH', 'OCN', 'PER', 'ACY', 'MH2', 112 | 'ARS', '12P', 'L3P', 'PUT', 'IN', 'CS', 'NAW', 'SB', 'GUN', 'SX', 'CON', 'C2O', 'EMC', 'BO4', 'BNG', 113 | 'MN5', '__O', 'K', 'CYN', 'H2S', 'MH3', 'YT3', 'P22', 'KO4', '1AG', 'CE', 'IPL', 'PG6', 'MO5', 'F09', 114 | 'HO', 'AL', 'TRS', 'EOH', 'GCP', 'MSE', 'AKR', 'NCO', 'PO4', 'L2P', 'LDA', 'SIN', 'DMI', 'SM', 'DTD', 115 | 'SGM', 'DIO', 'PPI', 'DDQ', 'DPO', 'HCA', 'CO5', 'PD', 'OS', 'OH', 'NA6', 'NAG', 'W', 'ENC', 'NA5', 116 | 'LI1', 'P4C', 'GLV', 'DMF', 'ACT', 'BTB', '6PL', 'BGL', 'OF1', 'N8E', 'LMT', 'THM', 'EU3', 'PGR', 'NA2', 117 | 'FOL', '543', '_CP', 'PEK', 'NSP', 'PEE', 'OCO', 'CHD', 'CO2', 'TBU', 'UMQ', 'MES', 'NH4', 'CD5', 'HTG', 118 | 'DEP', 'OC1', 'KDO', '2PE', 'PE3', 'IOD', 'NDG', 'CL', 'HG', 'F', 'XE', 'TL', 'BA', 'LI', 'BR', 'TAU', 119 | 'TCA', 'SPD', 'SPM', 'SAR', 'SUC', 'PAM', 'SPH', 'BE7', 'P4G', 'OLC', 'OLB', 'LFA', 'D10', 'D12', 'DD9', 120 | 'HP6', 'R16', 'PX4', 'TRD', 'UND', 'FTT', 'MYR', 'RG1', 'IMD', 'DMN', 'KEN', 'C14', 'UPL', 'CMJ', 'ULI', 121 | 'MYS', 'TWT', 'M2M', 'P15', 'PG0', 'PEU', 'AE3', 'TOE', 'ME2', 'PE8', '6JZ', '7PE', 'P3G', '7PG', 'PG5', 122 | '16P', 'XPE', 'PGF', 'AE4', '7E8', '7E9', 'MVC', 'TAR', 'DMR', 'LMR', 'NER', '02U', 'NGZ', 'LXB', 'A2G', 123 | 'BM3', 'NAA', 'NGA', 'LXZ', 'PX6', 'PA8', 'LPP', 'PX2', 'MYY', 'PX8', 'PD7', 'XP4', 'XPA', 'PEV', '6PE', 124 | 'PEX', 'PEH', 'PTY', 'YB2', 'PGT', 'CN3', 'AGA', 'DGG', 'CD4', 'CN6', 'CDL', 'PG8', 'MGE', 'DTV', 'L44', 125 | 'L2C', '4AG', 'B3H', '1EM', 'DDR', 'I42', 'CNS', 'PC7', 'HGP', 'PC8', 'HGX', 'LIO', 'PLD', 'PC2', 'PCF', 126 | 'MC3', 'P1O', 'PLC', 'PC6', 'HSH', 'BXC', 'HSG', 'DPG', '2DP', 'POV', 'PCW', 'GVT', 'CE9', 'CXE', 'C10', 127 | 'CE1', 'SPJ', 'SPZ', 'SPK', 'SPW', 'HT3', 'HTH', '2OP', '3NI', 'BO3', 'DET', 'D1D', 'SWE', 'SOG'] 128 | -------------------------------------------------------------------------------- /plip/basic/logger.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import logging 3 | 4 | 5 | def get_logger(): 6 | """ 7 | Configures a base logger and returns a module-specific sub-logger of the calling module. 8 | """ 9 | frame = inspect.stack()[1] 10 | module_name = inspect.getmodule(frame[0]).__name__ 11 | if module_name != '__main__': 12 | logger = logging.getLogger(module_name) 13 | if not logger.parent.handlers: 14 | ch = logging.StreamHandler() 15 | formatter = logging.Formatter('%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(name)s: %(message)s') 16 | ch.setFormatter(formatter) 17 | logger.parent.addHandler(ch) 18 | else: 19 | logger = logging.getLogger('plip') 20 | 21 | return logger 22 | -------------------------------------------------------------------------------- /plip/basic/parallel.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import multiprocessing 3 | from builtins import zip 4 | from functools import partial 5 | 6 | from numpy import asarray 7 | 8 | 9 | class SubProcessError(Exception): 10 | def __init__(self, e, exitcode=1): 11 | self.exitcode = exitcode 12 | super(SubProcessError, self).__init__(e) 13 | 14 | pass 15 | 16 | 17 | def universal_worker(input_pair): 18 | """This is a wrapper function expecting a tiplet of function, single 19 | argument, dict of keyword arguments. The provided function is called 20 | with the appropriate arguments.""" 21 | function, arg, kwargs = input_pair 22 | return function(arg, **kwargs) 23 | 24 | 25 | def pool_args(function, sequence, kwargs): 26 | """Return a single iterator of n elements of lists of length 3, given a sequence of len n.""" 27 | return zip(itertools.repeat(function), sequence, itertools.repeat(kwargs)) 28 | 29 | 30 | def parallel_fn(f): 31 | """Simple wrapper function, returning a parallel version of the given function f. 32 | The function f must have one argument and may have an arbitray number of 33 | keyword arguments. """ 34 | 35 | def simple_parallel(func, sequence, **args): 36 | """ f takes an element of sequence as input and the keyword args in **args""" 37 | if 'processes' in args: 38 | processes = args.get('processes') 39 | del args['processes'] 40 | else: 41 | processes = multiprocessing.cpu_count() 42 | 43 | pool = multiprocessing.Pool(processes) # depends on available cores 44 | 45 | result = pool.map_async(universal_worker, pool_args(func, sequence, args)) 46 | pool.close() 47 | pool.join() 48 | cleaned = [x for x in result.get() if x is not None] # getting results 49 | cleaned = asarray(cleaned) 50 | return cleaned 51 | 52 | return partial(simple_parallel, f) 53 | -------------------------------------------------------------------------------- /plip/basic/remote.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | hbonds_info = namedtuple('hbonds_info', 'ldon_id lig_don_id prot_acc_id pdon_id prot_don_id lig_acc_id') 4 | hydrophobic_info = namedtuple('hydrophobic_info', 'bs_ids lig_ids pairs_ids') 5 | halogen_info = namedtuple('halogen_info', 'don_id acc_id') 6 | pistack_info = namedtuple('pistack_info', 'proteinring_atoms, proteinring_center ligandring_atoms ' 7 | 'ligandring_center type') 8 | pication_info = namedtuple('pication_info', 'ring_center charge_center ring_atoms charge_atoms, protcharged') 9 | sbridge_info = namedtuple('sbridge_info', 'positive_atoms negative_atoms positive_center negative_center protispos') 10 | wbridge_info = namedtuple('wbridge_info', 'don_id acc_id water_id protisdon') 11 | metal_info = namedtuple('metal_info', 'metal_id, target_id location') 12 | 13 | 14 | class VisualizerData: 15 | """Contains all information on a complex relevant for visualization. Can be pickled""" 16 | 17 | def __init__(self, mol, site): 18 | pcomp = mol 19 | pli = mol.interaction_sets[site] 20 | ligand = pli.ligand 21 | 22 | # General Information 23 | self.lig_members = sorted(pli.ligand.members) 24 | self.sourcefile = pcomp.sourcefiles['pdbcomplex'] 25 | self.corrected_pdb = pcomp.corrected_pdb 26 | self.pdbid = mol.pymol_name 27 | self.hetid = ligand.hetid 28 | self.ligandtype = ligand.type 29 | self.chain = ligand.chain if not ligand.chain == "0" else "" # #@todo Fix this 30 | self.position = str(ligand.position) 31 | self.uid = ":".join([self.hetid, self.chain, self.position]) 32 | self.outpath = mol.output_path 33 | self.metal_ids = [x.m_orig_idx for x in pli.ligand.metals] 34 | self.unpaired_hba_idx = pli.unpaired_hba_orig_idx 35 | self.unpaired_hbd_idx = pli.unpaired_hbd_orig_idx 36 | self.unpaired_hal_idx = pli.unpaired_hal_orig_idx 37 | 38 | # Information on Interactions 39 | 40 | # Hydrophobic Contacts 41 | # Contains IDs of contributing binding site, ligand atoms and the pairings 42 | hydroph_pairs_id = [(h.bsatom_orig_idx, h.ligatom_orig_idx) for h in pli.hydrophobic_contacts] 43 | self.hydrophobic_contacts = hydrophobic_info(bs_ids=[hp[0] for hp in hydroph_pairs_id], 44 | lig_ids=[hp[1] for hp in hydroph_pairs_id], 45 | pairs_ids=hydroph_pairs_id) 46 | 47 | # Hydrogen Bonds 48 | # #@todo Don't use indices, simplify this code here 49 | hbonds_ldon, hbonds_pdon = pli.hbonds_ldon, pli.hbonds_pdon 50 | hbonds_ldon_id = [(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_ldon] 51 | hbonds_pdon_id = [(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_pdon] 52 | self.hbonds = hbonds_info(ldon_id=[(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_ldon], 53 | lig_don_id=[hb[1] for hb in hbonds_ldon_id], 54 | prot_acc_id=[hb[0] for hb in hbonds_ldon_id], 55 | pdon_id=[(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_pdon], 56 | prot_don_id=[hb[1] for hb in hbonds_pdon_id], 57 | lig_acc_id=[hb[0] for hb in hbonds_pdon_id]) 58 | 59 | # Halogen Bonds 60 | self.halogen_bonds = [halogen_info(don_id=h.don_orig_idx, acc_id=h.acc_orig_idx) 61 | for h in pli.halogen_bonds] 62 | 63 | # Pistacking 64 | self.pistacking = [pistack_info(proteinring_atoms=pistack.proteinring.atoms_orig_idx, 65 | proteinring_center=pistack.proteinring.center, 66 | ligandring_atoms=pistack.ligandring.atoms_orig_idx, 67 | ligandring_center=pistack.ligandring.center, 68 | type=pistack.type) for pistack in pli.pistacking] 69 | 70 | # Pi-cation interactions 71 | self.pication = [pication_info(ring_center=picat.ring.center, 72 | charge_center=picat.charge.center, 73 | ring_atoms=picat.ring.atoms_orig_idx, 74 | charge_atoms=picat.charge.atoms_orig_idx, 75 | protcharged=picat.protcharged) 76 | for picat in pli.pication_paro + pli.pication_laro] 77 | 78 | # Salt Bridges 79 | self.saltbridges = [sbridge_info(positive_atoms=sbridge.positive.atoms_orig_idx, 80 | negative_atoms=sbridge.negative.atoms_orig_idx, 81 | positive_center=sbridge.positive.center, 82 | negative_center=sbridge.negative.center, 83 | protispos=sbridge.protispos) 84 | for sbridge in pli.saltbridge_lneg + pli.saltbridge_pneg] 85 | 86 | # Water Bridgese('wbridge_info', 'don_id acc_id water_id protisdon') 87 | self.waterbridges = [wbridge_info(don_id=wbridge.d_orig_idx, 88 | acc_id=wbridge.a_orig_idx, 89 | water_id=wbridge.water_orig_idx, 90 | protisdon=wbridge.protisdon) for wbridge in pli.water_bridges] 91 | 92 | # Metal Complexes 93 | self.metal_complexes = [metal_info(metal_id=metalc.metal_orig_idx, 94 | target_id=metalc.target_orig_idx, 95 | location=metalc.location) for metalc in pli.metal_complexes] 96 | -------------------------------------------------------------------------------- /plip/basic/supplemental.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import itertools 3 | import os 4 | import re 5 | import subprocess 6 | import sys 7 | import tempfile 8 | import zipfile 9 | from collections import namedtuple 10 | 11 | import numpy as np 12 | from openbabel import pybel 13 | from openbabel.pybel import Atom 14 | 15 | from plip.basic import config, logger 16 | 17 | logger = logger.get_logger() 18 | 19 | def tmpfile(prefix, direc): 20 | """Returns the path to a newly created temporary file.""" 21 | return tempfile.mktemp(prefix=prefix, suffix='.pdb', dir=direc) 22 | 23 | 24 | def is_lig(hetid): 25 | """Checks if a PDB compound can be excluded as a small molecule ligand""" 26 | h = hetid.upper() 27 | return not (h == 'HOH' or h in config.UNSUPPORTED) 28 | 29 | 30 | def extract_pdbid(string): 31 | """Use regular expressions to get a PDB ID from a string""" 32 | p = re.compile("[0-9][0-9a-z]{3}") 33 | m = p.search(string.lower()) 34 | try: 35 | return m.group() 36 | except AttributeError: 37 | return "UnknownProtein" 38 | 39 | 40 | def whichrestype(atom): 41 | """Returns the residue name of an Pybel or OpenBabel atom.""" 42 | atom = atom if not isinstance(atom, Atom) else atom.OBAtom # Convert to OpenBabel Atom 43 | return atom.GetResidue().GetName() if atom.GetResidue() is not None else None 44 | 45 | 46 | def whichresnumber(atom): 47 | """Returns the residue number of an Pybel or OpenBabel atom (numbering as in original PDB file).""" 48 | atom = atom if not isinstance(atom, Atom) else atom.OBAtom # Convert to OpenBabel Atom 49 | return atom.GetResidue().GetNum() if atom.GetResidue() is not None else None 50 | 51 | 52 | def whichchain(atom): 53 | """Returns the residue number of an PyBel or OpenBabel atom.""" 54 | atom = atom if not isinstance(atom, Atom) else atom.OBAtom # Convert to OpenBabel Atom 55 | return atom.GetResidue().GetChain() if atom.GetResidue() is not None else None 56 | 57 | 58 | def residue_belongs_to_receptor(res, config): 59 | """tests whether the residue is defined as receptor and is not part of a peptide or residue ligand.""" 60 | if config.CHAINS: 61 | if config.CHAINS[0] and config.CHAINS[1]: # if receptor and ligand chains were given 62 | return res.GetChain() in config.CHAINS[0] and res.GetChain() not in config.CHAINS[1] 63 | # True if residue is part of receptor chains and not of ligand chains 64 | if config.CHAINS[1]: # if only ligand chains were given 65 | return res.GetChain() not in config.CHAINS[1] # True if residue is not part of ligand chains 66 | return False # if only receptor chains were given or both is empty 67 | return res.GetChain() not in config.PEPTIDES # True if residue is not part of peptide ligand. 68 | 69 | 70 | ######################### 71 | # Mathematical operations 72 | ######################### 73 | 74 | 75 | def euclidean3d(v1, v2): 76 | """Faster implementation of euclidean distance for the 3D case.""" 77 | if not len(v1) == 3 and len(v2) == 3: 78 | return None 79 | return np.sqrt((v1[0] - v2[0]) ** 2 + (v1[1] - v2[1]) ** 2 + (v1[2] - v2[2]) ** 2) 80 | 81 | 82 | def vector(p1, p2): 83 | """Vector from p1 to p2. 84 | :param p1: coordinates of point p1 85 | :param p2: coordinates of point p2 86 | :returns : numpy array with vector coordinates 87 | """ 88 | return None if len(p1) != len(p2) else np.array([p2[i] - p1[i] for i in range(len(p1))]) 89 | 90 | 91 | def vecangle(v1, v2, deg=True): 92 | """Calculate the angle between two vectors 93 | :param v1: coordinates of vector v1 94 | :param v2: coordinates of vector v2 95 | :param deg: whether to return degrees or radians 96 | :returns : angle in degree or rad 97 | """ 98 | if np.array_equal(v1, v2): 99 | return 0.0 100 | dm = np.dot(v1, v2) 101 | cm = np.linalg.norm(v1) * np.linalg.norm(v2) 102 | angle = np.arccos(dm / cm) # Round here to prevent floating point errors 103 | return np.degrees([angle, ])[0] if deg else angle 104 | 105 | 106 | def normalize_vector(v): 107 | """Take a vector and return the normalized vector 108 | :param v: a vector v 109 | :returns : normalized vector v 110 | """ 111 | norm = np.linalg.norm(v) 112 | return v / norm if not norm == 0 else v 113 | 114 | 115 | def centroid(coo): 116 | """Calculates the centroid from a 3D point cloud and returns the coordinates 117 | :param coo: Array of coordinate arrays 118 | :returns : centroid coordinates as list 119 | """ 120 | return list(map(np.mean, (([c[0] for c in coo]), ([c[1] for c in coo]), ([c[2] for c in coo])))) 121 | 122 | 123 | def projection(pnormal1, ppoint, tpoint): 124 | """Calculates the centroid from a 3D point cloud and returns the coordinates 125 | :param pnormal1: normal of plane 126 | :param ppoint: coordinates of point in the plane 127 | :param tpoint: coordinates of point to be projected 128 | :returns : coordinates of point orthogonally projected on the plane 129 | """ 130 | # Choose the plane normal pointing to the point to be projected 131 | pnormal2 = [coo * (-1) for coo in pnormal1] 132 | d1 = euclidean3d(tpoint, pnormal1 + ppoint) 133 | d2 = euclidean3d(tpoint, pnormal2 + ppoint) 134 | pnormal = pnormal1 if d1 < d2 else pnormal2 135 | # Calculate the projection of tpoint to the plane 136 | sn = -np.dot(pnormal, vector(ppoint, tpoint)) 137 | sd = np.dot(pnormal, pnormal) 138 | sb = sn / sd 139 | return [c1 + c2 for c1, c2 in zip(tpoint, [sb * pn for pn in pnormal])] 140 | 141 | 142 | def cluster_doubles(double_list): 143 | """Given a list of doubles, they are clustered if they share one element 144 | :param double_list: list of doubles 145 | :returns : list of clusters (tuples) 146 | """ 147 | location = {} # hashtable of which cluster each element is in 148 | clusters = [] 149 | # Go through each double 150 | for t in double_list: 151 | a, b = t[0], t[1] 152 | # If they both are already in different clusters, merge the clusters 153 | if a in location and b in location: 154 | if location[a] != location[b]: 155 | if location[a] < location[b]: 156 | clusters[location[a]] = clusters[location[a]].union(clusters[location[b]]) # Merge clusters 157 | clusters = clusters[:location[b]] + clusters[location[b] + 1:] 158 | else: 159 | clusters[location[b]] = clusters[location[b]].union(clusters[location[a]]) # Merge clusters 160 | clusters = clusters[:location[a]] + clusters[location[a] + 1:] 161 | # Rebuild index of locations for each element as they have changed now 162 | location = {} 163 | for i, cluster in enumerate(clusters): 164 | for c in cluster: 165 | location[c] = i 166 | else: 167 | # If a is already in a cluster, add b to that cluster 168 | if a in location: 169 | clusters[location[a]].add(b) 170 | location[b] = location[a] 171 | # If b is already in a cluster, add a to that cluster 172 | if b in location: 173 | clusters[location[b]].add(a) 174 | location[a] = location[b] 175 | # If neither a nor b is in any cluster, create a new one with a and b 176 | if not (b in location and a in location): 177 | clusters.append(set(t)) 178 | location[a] = len(clusters) - 1 179 | location[b] = len(clusters) - 1 180 | return map(tuple, clusters) 181 | 182 | 183 | ################# 184 | # File operations 185 | ################# 186 | 187 | def tilde_expansion(folder_path): 188 | """Tilde expansion, i.e. converts '~' in paths into .""" 189 | return os.path.expanduser(folder_path) if '~' in folder_path else folder_path 190 | 191 | 192 | def folder_exists(folder_path): 193 | """Checks if a folder exists""" 194 | return os.path.exists(folder_path) 195 | 196 | 197 | def create_folder_if_not_exists(folder_path): 198 | """Creates a folder if it does not exists.""" 199 | folder_path = tilde_expansion(folder_path) 200 | folder_path = "".join([folder_path, '/']) if not folder_path[-1] == '/' else folder_path 201 | direc = os.path.dirname(folder_path) 202 | if not folder_exists(direc): 203 | os.makedirs(direc) 204 | 205 | 206 | def cmd_exists(c): 207 | return subprocess.call("type " + c, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 208 | 209 | 210 | ################ 211 | # PyMOL-specific 212 | ################ 213 | 214 | 215 | def initialize_pymol(options): 216 | """Initializes PyMOL""" 217 | import pymol 218 | # Pass standard arguments of function to prevent PyMOL from printing out PDB headers (workaround) 219 | pymol.finish_launching(args=['pymol', options, '-K']) 220 | pymol.cmd.reinitialize() 221 | 222 | 223 | def start_pymol(quiet=False, options='-p', run=False): 224 | """Starts up PyMOL and sets general options. Quiet mode suppresses all PyMOL output. 225 | Command line options can be passed as the second argument.""" 226 | import pymol 227 | pymol.pymol_argv = ['pymol', '%s' % options] + sys.argv[1:] 228 | if run: 229 | initialize_pymol(options) 230 | if quiet: 231 | pymol.cmd.feedback('disable', 'all', 'everything') 232 | 233 | 234 | def nucleotide_linkage(residues): 235 | """Support for DNA/RNA ligands by finding missing covalent linkages to stitch DNA/RNA together.""" 236 | 237 | nuc_covalent = [] 238 | ####################################### 239 | # Basic support for RNA/DNA as ligand # 240 | ####################################### 241 | nucleotides = ['A', 'C', 'T', 'G', 'U', 'DA', 'DC', 'DT', 'DG', 'DU'] 242 | dna_rna = {} # Dictionary of DNA/RNA residues by chain 243 | covlinkage = namedtuple("covlinkage", "id1 chain1 pos1 conf1 id2 chain2 pos2 conf2") 244 | # Create missing covlinkage entries for DNA/RNA 245 | for ligand in residues: 246 | resname, chain, pos = ligand 247 | if resname in nucleotides: 248 | if chain not in dna_rna: 249 | dna_rna[chain] = [(resname, pos), ] 250 | else: 251 | dna_rna[chain].append((resname, pos)) 252 | for chain in dna_rna: 253 | nuc_list = dna_rna[chain] 254 | for i, nucleotide in enumerate(nuc_list): 255 | if not i == len(nuc_list) - 1: 256 | name, pos = nucleotide 257 | nextnucleotide = nuc_list[i + 1] 258 | nextname, nextpos = nextnucleotide 259 | newlink = covlinkage(id1=name, chain1=chain, pos1=pos, conf1='', 260 | id2=nextname, chain2=chain, pos2=nextpos, conf2='') 261 | nuc_covalent.append(newlink) 262 | 263 | return nuc_covalent 264 | 265 | 266 | def ring_is_planar(ring, r_atoms): 267 | """Given a set of ring atoms, check if the ring is sufficiently planar 268 | to be considered aromatic""" 269 | normals = [] 270 | for a in r_atoms: 271 | adj = pybel.ob.OBAtomAtomIter(a.OBAtom) 272 | # Check for neighboring atoms in the ring 273 | n_coords = [pybel.Atom(neigh).coords for neigh in adj if ring.IsMember(neigh)] 274 | vec1, vec2 = vector(a.coords, n_coords[0]), vector(a.coords, n_coords[1]) 275 | normals.append(np.cross(vec1, vec2)) 276 | # Given all normals of ring atoms and their neighbors, the angle between any has to be 5.0 deg or less 277 | for n1, n2 in itertools.product(normals, repeat=2): 278 | arom_angle = vecangle(n1, n2) 279 | if all([arom_angle > config.AROMATIC_PLANARITY, arom_angle < 180.0 - config.AROMATIC_PLANARITY]): 280 | return False 281 | return True 282 | 283 | 284 | def classify_by_name(names): 285 | """Classify a (composite) ligand by the HETID(s)""" 286 | if len(names) > 3: # Polymer 287 | if len(set(config.RNA).intersection(set(names))) != 0: 288 | ligtype = 'RNA' 289 | elif len(set(config.DNA).intersection(set(names))) != 0: 290 | ligtype = 'DNA' 291 | else: 292 | ligtype = "POLYMER" 293 | else: 294 | ligtype = 'SMALLMOLECULE' 295 | 296 | for name in names: 297 | if name in config.METAL_IONS: 298 | if len(names) == 1: 299 | ligtype = 'ION' 300 | else: 301 | if "ION" not in ligtype: 302 | ligtype += '+ION' 303 | return ligtype 304 | 305 | 306 | def sort_members_by_importance(members): 307 | """Sort the members of a composite ligand according to two criteria: 308 | 1. Split up in main and ion group. Ion groups are located behind the main group. 309 | 2. Within each group, sort by chain and position.""" 310 | main = [x for x in members if x[0] not in config.METAL_IONS] 311 | ion = [x for x in members if x[0] in config.METAL_IONS] 312 | sorted_main = sorted(main, key=lambda x: (x[1], x[2])) 313 | sorted_ion = sorted(ion, key=lambda x: (x[1], x[2])) 314 | return sorted_main + sorted_ion 315 | 316 | 317 | def get_isomorphisms(reference, lig): 318 | """Get all isomorphisms of the ligand.""" 319 | query = pybel.ob.CompileMoleculeQuery(reference.OBMol) 320 | mappr = pybel.ob.OBIsomorphismMapper.GetInstance(query) 321 | if all: 322 | isomorphs = pybel.ob.vvpairUIntUInt() 323 | mappr.MapAll(lig.OBMol, isomorphs) 324 | else: 325 | isomorphs = pybel.ob.vpairUIntUInt() 326 | mappr.MapFirst(lig.OBMol, isomorphs) 327 | isomorphs = [isomorphs] 328 | logger.debug(f'number of isomorphisms: {len(isomorphs)}') 329 | # @todo Check which isomorphism to take 330 | return isomorphs 331 | 332 | 333 | def canonicalize(lig, preserve_bond_order=False): 334 | """Get the canonical atom order for the ligand.""" 335 | atomorder = None 336 | # Get canonical atom order 337 | 338 | lig = pybel.ob.OBMol(lig.OBMol) 339 | if not preserve_bond_order: 340 | for bond in pybel.ob.OBMolBondIter(lig): 341 | if bond.GetBondOrder() != 1: 342 | bond.SetBondOrder(1) 343 | lig.DeleteData(pybel.ob.StereoData) 344 | lig = pybel.Molecule(lig) 345 | testcan = lig.write(format='can') 346 | try: 347 | pybel.readstring('can', testcan) 348 | reference = pybel.readstring('can', testcan) 349 | except IOError: 350 | testcan, reference = '', '' 351 | if testcan != '': 352 | reference.removeh() 353 | isomorphs = get_isomorphisms(reference, lig) # isomorphs now holds all isomorphisms within the molecule 354 | if not len(isomorphs) == 0: 355 | smi_dict = {} 356 | smi_to_can = isomorphs[0] 357 | for x in smi_to_can: 358 | smi_dict[int(x[1]) + 1] = int(x[0]) + 1 359 | atomorder = [smi_dict[x + 1] for x in range(len(lig.atoms))] 360 | else: 361 | atomorder = None 362 | return atomorder 363 | 364 | 365 | def int32_to_negative(int32): 366 | """Checks if a suspicious number (e.g. ligand position) is in fact a negative number represented as a 367 | 32 bit integer and returns the actual number. 368 | """ 369 | dct = {} 370 | if int32 == 4294967295: # Special case in some structures (note, this is just a workaround) 371 | return -1 372 | for i in range(-1000, -1): 373 | dct[int(np.array(i).astype(np.uint32))] = i 374 | if int32 in dct: 375 | return dct[int32] 376 | else: 377 | return int32 378 | 379 | 380 | def read_pdb(pdbfname, as_string=False): 381 | """Reads a given PDB file and returns a Pybel Molecule.""" 382 | pybel.ob.obErrorLog.StopLogging() # Suppress all OpenBabel warnings 383 | return readmol(pdbfname, as_string=as_string) 384 | 385 | 386 | def read(fil): 387 | """Returns a file handler and detects gzipped files.""" 388 | if os.path.splitext(fil)[-1] == '.gz': 389 | return gzip.open(fil, 'rb') 390 | elif os.path.splitext(fil)[-1] == '.zip': 391 | zf = zipfile.ZipFile(fil, 'r') 392 | return zf.open(zf.infolist()[0].filename) 393 | else: 394 | return open(fil, 'r') 395 | 396 | 397 | def readmol(path, as_string=False): 398 | """Reads the given molecule file and returns the corresponding Pybel molecule as well as the input file type. 399 | In contrast to the standard Pybel implementation, the file is closed properly.""" 400 | supported_formats = ['pdb'] 401 | # Fix for Windows-generated files: Remove carriage return characters 402 | if "\r" in path and as_string: 403 | path = path.replace('\r', '') 404 | 405 | for sformat in supported_formats: 406 | obc = pybel.ob.OBConversion() 407 | obc.SetInFormat(sformat) 408 | logger.debug(f'detected {sformat} as format, trying to read file with OpenBabel') 409 | 410 | # Read molecules with single bond information 411 | if as_string: 412 | try: 413 | mymol = pybel.readstring(sformat, path) 414 | except IOError: 415 | logger.error('no valid file format provided') 416 | sys.exit(1) 417 | else: 418 | read_file = pybel.readfile(format=sformat, filename=path, opt={"s": None}) 419 | try: 420 | mymol = next(read_file) 421 | except StopIteration: 422 | logger.error('file contains no valid molecules') 423 | sys.exit(1) 424 | 425 | logger.debug('molecule successfully read') 426 | 427 | # Assign multiple bonds 428 | mymol.OBMol.PerceiveBondOrders() 429 | return mymol, sformat 430 | 431 | logger.error('no valid file format provided') 432 | sys.exit(1) 433 | -------------------------------------------------------------------------------- /plip/exchange/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharmai/plip/e4181b2c40fe5938b5f39aaf00ae3ea9d14ac9cd/plip/exchange/__init__.py -------------------------------------------------------------------------------- /plip/exchange/json.py: -------------------------------------------------------------------------------- 1 | # place holder for module to add Json support -------------------------------------------------------------------------------- /plip/exchange/webservices.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from urllib.error import HTTPError 3 | from urllib.request import urlopen 4 | 5 | import lxml.etree as et 6 | 7 | from plip.basic import logger 8 | 9 | logger = logger.get_logger() 10 | 11 | 12 | def check_pdb_status(pdbid): 13 | """Returns the status and up-to-date entry in the PDB for a given PDB ID""" 14 | url = 'http://www.rcsb.org/pdb/rest/idStatus?structureId=%s' % pdbid 15 | xmlf = urlopen(url) 16 | xml = et.parse(xmlf) 17 | xmlf.close() 18 | status = None 19 | current_pdbid = pdbid 20 | for df in xml.xpath('//record'): 21 | status = df.attrib['status'] # Status of an entry can be either 'UNKWOWN', 'OBSOLETE', or 'CURRENT' 22 | if status == 'OBSOLETE': 23 | current_pdbid = df.attrib['replacedBy'] # Contains the up-to-date PDB ID for obsolete entries 24 | return [status, current_pdbid.lower()] 25 | 26 | 27 | def fetch_pdb(pdbid): 28 | """Get the newest entry from the RCSB server for the given PDB ID. Exits with '1' if PDB ID is invalid.""" 29 | pdbid = pdbid.lower() 30 | # logger.info(f'checking status of PDB-ID {pdbid}') 31 | # @todo re-implement state check with ew RCSB API, see https://www.rcsb.org/news?year=2020&article=5eb18ccfd62245129947212a&feature=true 32 | # state, current_entry = check_pdb_status(pdbid) # Get state and current PDB ID 33 | # 34 | # if state == 'OBSOLETE': 35 | # logger.info(f'entry is obsolete, getting {current_entry} instead') 36 | # elif state == 'CURRENT': 37 | # logger.info('entry is up-to-date') 38 | # elif state == 'UNKNOWN': 39 | # logger.error('invalid PDB-ID (entry does not exist on PDB server)') 40 | # sys.exit(1) 41 | logger.info('downloading file from PDB') 42 | # get URL for current entry 43 | # @todo needs update to react properly on response codes of RCSB servers 44 | pdburl = f'https://files.rcsb.org/download/{pdbid}.pdb' 45 | try: 46 | pdbfile = urlopen(pdburl).read().decode() 47 | # If no PDB file is available, a text is now shown with "We're sorry, but ..." 48 | # Could previously be distinguished by an HTTP error 49 | if 'sorry' in pdbfile: 50 | logger.error('no file in PDB format available from wwPDB for the given PDB ID.') 51 | sys.exit(1) 52 | except HTTPError: 53 | logger.error('no file in PDB format available from wwPDB for the given PDB ID') 54 | sys.exit(1) 55 | return [pdbfile, pdbid] 56 | -------------------------------------------------------------------------------- /plip/exchange/xml.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | 3 | 4 | class XMLStorage: 5 | """Generic class for storing XML data from PLIP XML files.""" 6 | 7 | @staticmethod 8 | def getdata(tree, location, force_string=False): 9 | """Gets XML data from a specific element and handles types.""" 10 | found = tree.xpath('%s/text()' % location) 11 | if not found: 12 | return None 13 | else: 14 | data = found[0] 15 | if force_string: 16 | return data 17 | if data == 'True': 18 | return True 19 | elif data == 'False': 20 | return False 21 | else: 22 | try: 23 | return int(data) 24 | except ValueError: 25 | try: 26 | return float(data) 27 | except ValueError: 28 | # It's a string 29 | return data 30 | 31 | @staticmethod 32 | def getcoordinates(tree, location): 33 | """Gets coordinates from a specific element in PLIP XML""" 34 | return tuple(float(x) for x in tree.xpath('.//%s/*/text()' % location)) 35 | 36 | 37 | class Interaction(XMLStorage): 38 | """Stores information on a specific interaction type""" 39 | 40 | def __init__(self, interaction_part): 41 | self.id = interaction_part.get('id') 42 | self.resnr = self.getdata(interaction_part, 'resnr') 43 | self.restype = self.getdata(interaction_part, 'restype', force_string=True) 44 | self.reschain = self.getdata(interaction_part, 'reschain', force_string=True) 45 | self.resnr_lig = self.getdata(interaction_part, 'resnr_lig') 46 | self.restype_lig = self.getdata(interaction_part, 'restype_lig', force_string=True) 47 | self.reschain_lig = self.getdata(interaction_part, 'reschain_lig', force_string=True) 48 | self.ligcoo = self.getcoordinates(interaction_part, 'ligcoo') 49 | self.protcoo = self.getcoordinates(interaction_part, 'protcoo') 50 | 51 | 52 | class HydrophobicInteraction(Interaction): 53 | """Stores information on a hydrophobic interaction""" 54 | 55 | def __init__(self, hydrophobic_part): 56 | Interaction.__init__(self, hydrophobic_part) 57 | self.dist = self.getdata(hydrophobic_part, 'dist') 58 | self.ligcarbonidx = self.getdata(hydrophobic_part, 'ligcarbonidx') 59 | self.protcarbonidx = self.getdata(hydrophobic_part, 'protcarbonidx') 60 | 61 | 62 | class HydrogenBond(Interaction): 63 | """Stores information on a hydrogen bond interaction""" 64 | 65 | def __init__(self, hbond_part): 66 | Interaction.__init__(self, hbond_part) 67 | self.sidechain = self.getdata(hbond_part, 'sidechain') 68 | self.dist_h_a = self.getdata(hbond_part, 'dist_h-a') 69 | self.dist_d_a = self.getdata(hbond_part, 'dist_d-a') 70 | self.dist = self.dist_d_a 71 | 72 | self.don_angle = self.getdata(hbond_part, 'don_angle') 73 | self.protisdon = self.getdata(hbond_part, 'protisdon') 74 | self.donoridx = self.getdata(hbond_part, 'donoridx') 75 | self.acceptoridx = self.getdata(hbond_part, 'acceptoridx') 76 | self.donortype = self.getdata(hbond_part, 'donortype', force_string=True) 77 | self.acceptortype = self.getdata(hbond_part, 'acceptortype', force_string=True) 78 | 79 | 80 | class WaterBridge(Interaction): 81 | """Stores information on a water bridge interaction""" 82 | 83 | def __init__(self, wbridge_part): 84 | Interaction.__init__(self, wbridge_part) 85 | self.dist_a_w = self.getdata(wbridge_part, 'dist_a-w') 86 | self.dist_d_w = self.getdata(wbridge_part, 'dist_d-w') 87 | self.don_angle = self.getdata(wbridge_part, 'don_angle') 88 | self.water_angle = self.getdata(wbridge_part, 'water_angle') 89 | self.protisdon = self.getdata(wbridge_part, 'protisdon') 90 | self.dist = self.dist_a_w if self.protisdon else self.dist_d_w 91 | 92 | self.donor_idx = self.getdata(wbridge_part, 'donor_idx') 93 | self.acceptor_idx = self.getdata(wbridge_part, 'acceptor_idx') 94 | self.donortype = self.getdata(wbridge_part, 'donortype', force_string=True) 95 | self.acceptortype = self.getdata(wbridge_part, 'acceptortype', force_string=True) 96 | self.water_idx = self.getdata(wbridge_part, 'water_idx') 97 | self.watercoo = self.getcoordinates(wbridge_part, 'watercoo') 98 | 99 | 100 | class SaltBridge(Interaction): 101 | """Stores information on a salt bridge interaction""" 102 | 103 | def __init__(self, sbridge_part): 104 | Interaction.__init__(self, sbridge_part) 105 | self.dist = self.getdata(sbridge_part, 'dist') 106 | self.protispos = self.getdata(sbridge_part, 'protispos') 107 | self.lig_group = self.getdata(sbridge_part, 'lig_group', force_string=True) 108 | self.lig_idx_list = [int(tagpart.text) for tagpart in 109 | sbridge_part.xpath('lig_idx_list/idx')] 110 | self.prot_idx_list = [int(tagpart.text) for tagpart in 111 | sbridge_part.xpath('prot_idx_list/idx')] 112 | 113 | 114 | class PiStacking(Interaction): 115 | """Stores information on a pi stacking interaction""" 116 | 117 | def __init__(self, pistack_part): 118 | Interaction.__init__(self, pistack_part) 119 | self.centdist = self.getdata(pistack_part, 'centdist') 120 | self.dist = self.centdist 121 | self.angle = self.getdata(pistack_part, 'angle') 122 | self.offset = self.getdata(pistack_part, 'offset') 123 | self.type = self.getdata(pistack_part, 'type') 124 | self.lig_idx_list = [int(tagpart.text) for tagpart in 125 | pistack_part.xpath('lig_idx_list/idx')] 126 | self.prot_idx_list = [int(tagpart.text) for tagpart in 127 | pistack_part.xpath('prot_idx_list/idx')] 128 | 129 | 130 | class PiCation(Interaction): 131 | """Stores information on a pi cation interaction""" 132 | 133 | def __init__(self, pication_part): 134 | Interaction.__init__(self, pication_part) 135 | self.dist = self.getdata(pication_part, 'dist') 136 | self.offset = self.getdata(pication_part, 'offset') 137 | self.protcharged = self.getdata(pication_part, 'protcharged') 138 | self.lig_group = self.getdata(pication_part, 'lig_group') 139 | self.lig_idx_list = [int(tag.text) for tag in pication_part.xpath('.//lig_idx_list/idx')] 140 | 141 | 142 | class HalogenBond(Interaction): 143 | """Stores information on a halogen bond interaction""" 144 | 145 | def __init__(self, halogen_part): 146 | Interaction.__init__(self, halogen_part) 147 | self.dist = self.getdata(halogen_part, 'dist') 148 | self.don_angle = self.getdata(halogen_part, 'don_angle') 149 | self.acc_angle = self.getdata(halogen_part, 'acc_angle') 150 | self.donortype = self.getdata(halogen_part, 'donortype', force_string=True) 151 | self.acceptortype = self.getdata(halogen_part, 'acceptortype', force_string=True) 152 | self.don_idx = self.getdata(halogen_part, 'don_idx') 153 | self.acc_idx = self.getdata(halogen_part, 'acc_idx') 154 | self.sidechain = self.getdata(halogen_part, 'sidechain') 155 | 156 | 157 | class MetalComplex(Interaction): 158 | """Stores information on a metal complexe interaction""" 159 | 160 | def __init__(self, metalcomplex_part): 161 | Interaction.__init__(self, metalcomplex_part) 162 | self.metal_idx = self.getdata(metalcomplex_part, 'metal_idx') 163 | self.metal_type = self.getdata(metalcomplex_part, 'metal_type', force_string=True) 164 | self.target_idx = self.getdata(metalcomplex_part, 'target_idx') 165 | self.target_type = self.getdata(metalcomplex_part, 'target_type', force_string=True) 166 | self.coordination = self.getdata(metalcomplex_part, 'coordination') 167 | self.dist = self.getdata(metalcomplex_part, 'dist') 168 | self.location = self.getdata(metalcomplex_part, 'location', force_string=True) 169 | self.rms = self.getdata(metalcomplex_part, 'rms') 170 | self.geometry = self.getdata(metalcomplex_part, 'geometry', force_string=True) 171 | self.complexnum = self.getdata(metalcomplex_part, 'complexnum') 172 | self.targetcoo = self.getcoordinates(metalcomplex_part, 'targetcoo') 173 | self.metalcoo = self.getcoordinates(metalcomplex_part, 'metalcoo') 174 | 175 | 176 | class BSite(XMLStorage): 177 | """Stores all information about an specific binding site.""" 178 | 179 | def __init__(self, bindingsite, pdbid): 180 | self.bindingsite = bindingsite 181 | self.pdbid = pdbid 182 | self.bsid = ":".join(bindingsite.xpath('identifiers/*/text()')[2:5]) 183 | self.uniqueid = ":".join([self.pdbid, self.bsid]) 184 | self.hetid = self.getdata(bindingsite, 'identifiers/hetid', force_string=True) 185 | self.longname = self.getdata(bindingsite, 'identifiers/longname', force_string=True) 186 | self.ligtype = self.getdata(bindingsite, 'identifiers/ligtype', force_string=True) 187 | self.smiles = self.getdata(bindingsite, 'identifiers/smiles', force_string=True) 188 | self.inchikey = self.getdata(bindingsite, 'identifiers/inchikey', force_string=True) 189 | self.position = self.getdata(bindingsite, 'identifiers/position') 190 | self.chain = self.getdata(bindingsite, 'identifiers/chain', force_string=True) 191 | 192 | # Information on binding site members 193 | self.members = [] 194 | for member in bindingsite.xpath('identifiers/members/member'): 195 | self.members += member.xpath('text()') 196 | 197 | self.composite = self.getdata(bindingsite, 'identifiers/composite') 198 | 199 | # Ligand Properties 200 | self.heavy_atoms = self.getdata(bindingsite, 'lig_properties/num_heavy_atoms') 201 | self.hbd = self.getdata(bindingsite, 'lig_properties/num_hbd') 202 | self.unpaired_hbd = self.getdata(bindingsite, 'lig_properties/num_unpaired_hbd') 203 | self.hba = self.getdata(bindingsite, 'lig_properties/num_hba') 204 | self.unpaired_hba = self.getdata(bindingsite, 'lig_properties/num_unpaired_hba') 205 | self.hal = self.getdata(bindingsite, 'lig_properties/num_hal') 206 | self.unpaired_hal = self.getdata(bindingsite, 'lig_properties/num_unpaired_hal') 207 | self.molweight = self.getdata(bindingsite, 'lig_properties/molweight') 208 | self.logp = self.getdata(bindingsite, 'lig_properties/logp') 209 | self.rotatable_bonds = self.getdata(bindingsite, 'lig_properties/num_rotatable_bonds') 210 | self.rings = self.getdata(bindingsite, 'lig_properties/num_aromatic_rings') 211 | 212 | # Binding Site residues 213 | self.bs_res = [] 214 | for tagpart in bindingsite.xpath('bs_residues/bs_residue'): 215 | resnumber, reschain = tagpart.text[:-1], tagpart.text[-1] 216 | aa, contact, min_dist = tagpart.get('aa'), tagpart.get('contact'), tagpart.get('min_dist') 217 | new_bs_res = {'resnr': int(resnumber), 'reschain': reschain, 'aa': aa, 218 | 'contact': True if contact == 'True' else False, 'min_dist': float(min_dist)} 219 | self.bs_res.append(new_bs_res) 220 | 221 | # Interacting chains 222 | self.interacting_chains = [] 223 | for chain in bindingsite.xpath('interacting_chains/interacting_chain'): 224 | self.interacting_chains += chain.xpath('text()') 225 | 226 | # Interactions 227 | interactions = bindingsite.xpath('interactions')[0] 228 | self.hydrophobics = [HydrophobicInteraction(x) for x in 229 | interactions.xpath('hydrophobic_interactions/hydrophobic_interaction')] 230 | self.hbonds = [HydrogenBond(x) for x in interactions.xpath('hydrogen_bonds/hydrogen_bond')] 231 | self.wbridges = [WaterBridge(x) for x in interactions.xpath('water_bridges/water_bridge')] 232 | self.sbridges = [SaltBridge(x) for x in interactions.xpath('salt_bridges/salt_bridge')] 233 | self.pi_stacks = [PiStacking(x) for x in interactions.xpath('pi_stacks/pi_stack')] 234 | self.pi_cations = [PiCation(x) for x in interactions.xpath('pi_cation_interactions/pi_cation_interaction')] 235 | self.halogens = [HalogenBond(x) for x in interactions.xpath('halogen_bonds/halogen_bond')] 236 | self.metal_complexes = [MetalComplex(x) for x in interactions.xpath('metal_complexes/metal_complex')] 237 | self.num_contacts = len(self.hydrophobics) + len(self.hbonds) + len(self.wbridges) + len(self.sbridges) + \ 238 | len(self.pi_stacks) + len(self.pi_cations) + len(self.halogens) + len(self.metal_complexes) 239 | self.has_interactions = self.num_contacts > 0 240 | 241 | self.get_atom_mapping() 242 | self.counts = self.get_counts() 243 | 244 | def get_atom_mapping(self): 245 | """Parses the ligand atom mapping.""" 246 | # Atom mappings 247 | smiles_to_pdb_mapping = self.bindingsite.xpath('mappings/smiles_to_pdb/text()') 248 | if not smiles_to_pdb_mapping: 249 | self.mappings = {'smiles_to_pdb': None, 'pdb_to_smiles': None} 250 | else: 251 | smiles_to_pdb_mapping = {int(y[0]): int(y[1]) for y in [x.split(':') 252 | for x in smiles_to_pdb_mapping[0].split(',')]} 253 | self.mappings = {'smiles_to_pdb': smiles_to_pdb_mapping} 254 | self.mappings['pdb_to_smiles'] = {v: k for k, v in self.mappings['smiles_to_pdb'].items()} 255 | 256 | def get_counts(self): 257 | """counts the interaction types and backbone hydrogen bonding in a binding site""" 258 | 259 | hbondsback = len([hb for hb in self.hbonds if not hb.sidechain]) 260 | counts = {'hydrophobics': len(self.hydrophobics), 'hbonds': len(self.hbonds), 261 | 'wbridges': len(self.wbridges), 'sbridges': len(self.sbridges), 'pistacks': len(self.pi_stacks), 262 | 'pications': len(self.pi_cations), 'halogens': len(self.halogens), 'metal': len(self.metal_complexes), 263 | 'hbond_back': hbondsback, 'hbond_nonback': (len(self.hbonds) - hbondsback)} 264 | counts['total'] = counts['hydrophobics'] + counts['hbonds'] + counts['wbridges'] + \ 265 | counts['sbridges'] + counts['pistacks'] + counts['pications'] + counts['halogens'] + counts[ 266 | 'metal'] 267 | return counts 268 | 269 | 270 | class PlipXML(XMLStorage): 271 | """Parses and stores all information from a PLIP XML file.""" 272 | 273 | def __init__(self, xmlfile): 274 | self.load_data(xmlfile) 275 | 276 | # Parse general information 277 | self.version = self.getdata(self.doc, '/report/plipversion/') 278 | self.pdbid = self.getdata(self.doc, '/report/pdbid', force_string=True) 279 | self.filetype = self.getdata(self.doc, '/report/filetype') 280 | self.fixed = self.getdata(self.doc, '/report/pdbfixes/') 281 | self.filename = self.getdata(self.doc, '/report/filename') 282 | self.excluded = self.doc.xpath('/report/excluded_ligands/excluded_ligand/text()') 283 | 284 | # Parse binding site information 285 | self.bsites = {BSite(bs, self.pdbid).bsid: BSite(bs, self.pdbid) for bs in self.doc.xpath('//bindingsite')} 286 | self.num_bsites = len(self.bsites) 287 | 288 | def load_data(self, xmlfile): 289 | """Loads/parses an XML file and saves it as a tree if successful.""" 290 | self.doc = etree.parse(xmlfile) 291 | -------------------------------------------------------------------------------- /plip/plipcmd.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """ 3 | Protein-Ligand Interaction Profiler - Analyze and visualize protein-ligand interactions in PDB files. 4 | plipcmd.py - Main script for PLIP command line execution. 5 | """ 6 | 7 | # system imports 8 | import argparse 9 | import logging 10 | import multiprocessing 11 | import os 12 | import sys 13 | import ast 14 | from argparse import ArgumentParser 15 | from collections import namedtuple 16 | 17 | from plip.basic import config, logger 18 | 19 | logger = logger.get_logger() 20 | 21 | from plip.basic.config import __version__ 22 | from plip.basic.parallel import parallel_fn 23 | from plip.basic.remote import VisualizerData 24 | from plip.exchange.report import StructureReport 25 | from plip.exchange.webservices import fetch_pdb 26 | from plip.structure.preparation import create_folder_if_not_exists, extract_pdbid 27 | from plip.structure.preparation import tilde_expansion, PDBComplex 28 | 29 | description = f"The Protein-Ligand Interaction Profiler (PLIP) Version {__version__} " \ 30 | "is a command-line based tool to analyze interactions in a protein-ligand complex. " \ 31 | "If you are using PLIP in your work, please cite: " \ 32 | f"{config.__citation_information__} " \ 33 | f"Supported and maintained by: {config.__maintainer__}" 34 | 35 | 36 | def threshold_limiter(aparser, arg): 37 | arg = float(arg) 38 | if arg <= 0: 39 | aparser.error("All thresholds have to be values larger than zero.") 40 | return arg 41 | 42 | def residue_list(input_string): 43 | """Parse mix of residue numbers and ranges passed with the --residues flag into one list""" 44 | result = [] 45 | for part in input_string.split(','): 46 | if '-' in part: 47 | start, end = map(int, part.split('-')) 48 | result.extend(range(start, end + 1)) 49 | else: 50 | result.append(int(part)) 51 | return result 52 | 53 | def process_pdb(pdbfile, outpath, as_string=False, outputprefix='report'): 54 | """Analysis of a single PDB file with optional chain filtering.""" 55 | if not as_string: 56 | pdb_file_name = pdbfile.split('/')[-1] 57 | startmessage = f'starting analysis of {pdb_file_name}' 58 | else: 59 | startmessage = 'starting analysis from STDIN' 60 | logger.info(startmessage) 61 | mol = PDBComplex() 62 | mol.output_path = outpath 63 | mol.load_pdb(pdbfile, as_string=as_string) 64 | for ligand in mol.ligands: 65 | mol.characterize_complex(ligand) 66 | 67 | create_folder_if_not_exists(outpath) 68 | 69 | # Generate the report files 70 | streport = StructureReport(mol, outputprefix=outputprefix) 71 | 72 | config.MAXTHREADS = min(config.MAXTHREADS, len(mol.interaction_sets)) 73 | 74 | ###################################### 75 | # PyMOL Visualization (parallelized) # 76 | ###################################### 77 | 78 | if config.PYMOL or config.PICS: 79 | from plip.visualization.visualize import visualize_in_pymol 80 | complexes = [VisualizerData(mol, site) for site in sorted(mol.interaction_sets) 81 | if not len(mol.interaction_sets[site].interacting_res) == 0] 82 | if config.MAXTHREADS > 1: 83 | logger.info(f'generating visualizations in parallel on {config.MAXTHREADS} cores') 84 | parfn = parallel_fn(visualize_in_pymol) 85 | parfn(complexes, processes=config.MAXTHREADS) 86 | else: 87 | [visualize_in_pymol(plcomplex) for plcomplex in complexes] 88 | 89 | if config.XML: # Generate report in xml format 90 | streport.write_xml(as_string=config.STDOUT) 91 | 92 | if config.TXT: # Generate report in txt (rst) format 93 | streport.write_txt(as_string=config.STDOUT) 94 | 95 | 96 | def download_structure(inputpdbid): 97 | """Given a PDB ID, downloads the corresponding PDB structure. 98 | Checks for validity of ID and handles error while downloading. 99 | Returns the path of the downloaded file.""" 100 | try: 101 | if len(inputpdbid) != 4 or extract_pdbid(inputpdbid.lower()) == 'UnknownProtein': 102 | logger.error(f'invalid PDB-ID (wrong format): {inputpdbid}') 103 | sys.exit(1) 104 | pdbfile, pdbid = fetch_pdb(inputpdbid.lower()) 105 | pdbpath = tilde_expansion('%s/%s.pdb' % (config.BASEPATH.rstrip('/'), pdbid)) 106 | create_folder_if_not_exists(config.BASEPATH) 107 | with open(pdbpath, 'w') as g: 108 | g.write(pdbfile) 109 | logger.info(f'file downloaded as {pdbpath}') 110 | return pdbpath, pdbid 111 | 112 | except ValueError: # Invalid PDB ID, cannot fetch from RCBS server 113 | logger.error(f'PDB-ID does not exist: {inputpdbid}') 114 | sys.exit(1) 115 | 116 | 117 | def remove_duplicates(slist): 118 | """Checks input lists for duplicates and returns 119 | a list with unique entries""" 120 | unique = list(set(slist)) 121 | difference = len(slist) - len(unique) 122 | if difference == 1: 123 | logger.info('removed one duplicate entry from input list') 124 | if difference > 1: 125 | logger.info(f'Removed {difference} duplicate entries from input list') 126 | return unique 127 | 128 | 129 | def run_analysis(inputstructs, inputpdbids, chains=None): 130 | """Main function. Calls functions for processing, report generation and visualization.""" 131 | pdbid, pdbpath = None, None 132 | # @todo For multiprocessing, implement better stacktracing for errors 133 | # Print title and version 134 | logger.info(f'Protein-Ligand Interaction Profiler (PLIP) {__version__}') 135 | logger.info(f'brought to you by: {config.__maintainer__}') 136 | logger.info(f'please cite: {config.__citation_information__}') 137 | output_prefix = config.OUTPUTFILENAME 138 | 139 | if inputstructs is not None: # Process PDB file(s) 140 | num_structures = len(inputstructs) # @question: how can it become more than one file? The tilde_expansion function does not consider this case. 141 | inputstructs = remove_duplicates(inputstructs) 142 | read_from_stdin = False 143 | for inputstruct in inputstructs: 144 | if inputstruct == '-': # @expl: when user gives '-' as input, pdb file is read from stdin 145 | inputstruct = sys.stdin.read() 146 | read_from_stdin = True 147 | if config.RAWSTRING: 148 | if sys.version_info < (3,): 149 | inputstruct = bytes(inputstruct).decode('unicode_escape') # @expl: in Python2, the bytes object is just a string. 150 | else: 151 | inputstruct = bytes(inputstruct, 'utf8').decode('unicode_escape') 152 | else: 153 | if os.path.getsize(inputstruct) == 0: 154 | logger.error('empty PDB file') 155 | sys.exit(1) 156 | if num_structures > 1: 157 | basename = inputstruct.split('.')[-2].split('/')[-1] 158 | config.OUTPATH = '/'.join([config.BASEPATH, basename]) 159 | output_prefix = 'report' 160 | process_pdb(inputstruct, config.OUTPATH, as_string=read_from_stdin, outputprefix=output_prefix) 161 | else: # Try to fetch the current PDB structure(s) directly from the RCBS server 162 | num_pdbids = len(inputpdbids) 163 | inputpdbids = remove_duplicates(inputpdbids) 164 | for inputpdbid in inputpdbids: 165 | pdbpath, pdbid = download_structure(inputpdbid) 166 | if num_pdbids > 1: 167 | config.OUTPATH = '/'.join([config.BASEPATH, pdbid[1:3].upper(), pdbid.upper()]) 168 | output_prefix = 'report' 169 | process_pdb(pdbpath, config.OUTPATH, outputprefix=output_prefix) 170 | 171 | if (pdbid is not None or inputstructs is not None) and config.BASEPATH is not None: 172 | if config.BASEPATH in ['.', './']: 173 | logger.info('finished analysis, find the result files in the working directory') 174 | else: 175 | logger.info(f'finished analysis, find the result files in {config.BASEPATH}') 176 | 177 | 178 | def main(): 179 | """Parse command line arguments and start main script for analysis.""" 180 | parser = ArgumentParser(prog="PLIP", description=description) 181 | pdbstructure = parser.add_mutually_exclusive_group(required=True) # Needs either PDB ID or file 182 | # '-' as file name reads from stdin 183 | pdbstructure.add_argument("-f", "--file", dest="input", nargs="+", help="Set input file, '-' reads from stdin") 184 | pdbstructure.add_argument("-i", "--input", dest="pdbid", nargs="+") 185 | outputgroup = parser.add_mutually_exclusive_group(required=False) # Needs either outpath or stdout 186 | outputgroup.add_argument("-o", "--out", dest="outpath", default="./") 187 | outputgroup.add_argument("-O", "--stdout", dest="stdout", action="store_true", default=False, 188 | help="Write to stdout instead of file") 189 | parser.add_argument("--rawstring", dest="use_raw_string", default=False, action="store_true", 190 | help="Use Python raw strings for stdin") 191 | parser.add_argument("-v", "--verbose", dest="verbose", default=False, help="Turn on verbose mode", 192 | action="store_true") 193 | parser.add_argument("-q", "--quiet", dest="quiet", default=False, help="Turn on quiet mode", action="store_true") 194 | parser.add_argument("-s", "--silent", dest="silent", default=False, help="Turn on silent mode", action="store_true") 195 | parser.add_argument("-p", "--pics", dest="pics", default=False, help="Additional pictures", action="store_true") 196 | parser.add_argument("-x", "--xml", dest="xml", default=False, help="Generate report file in XML format", 197 | action="store_true") 198 | parser.add_argument("-t", "--txt", dest="txt", default=False, help="Generate report file in TXT (RST) format", 199 | action="store_true") 200 | parser.add_argument("-y", "--pymol", dest="pymol", default=False, help="Additional PyMOL session files", 201 | action="store_true") 202 | parser.add_argument("--maxthreads", dest="maxthreads", default=multiprocessing.cpu_count(), 203 | help="Set maximum number of main threads (number of binding sites processed simultaneously)." 204 | "If not set, PLIP uses all available CPUs if possible.", 205 | type=int) 206 | parser.add_argument("--breakcomposite", dest="breakcomposite", default=False, 207 | help="Don't combine ligand fragments with covalent bonds but treat them as single ligands for the analysis.", 208 | action="store_true") 209 | parser.add_argument("--altlocation", dest="altlocation", default=False, 210 | help="Also consider alternate locations for atoms (e.g. alternate conformations).", 211 | action="store_true") 212 | parser.add_argument("--nofix", dest="nofix", default=False, 213 | help="Turns off fixing of PDB files.", 214 | action="store_true") 215 | parser.add_argument("--nofixfile", dest="nofixfile", default=False, 216 | help="Turns off writing files for fixed PDB files.", 217 | action="store_true") 218 | parser.add_argument("--nopdbcanmap", dest="nopdbcanmap", default=False, 219 | help="Turns off calculation of mapping between canonical and PDB atom order for ligands.", 220 | action="store_true") 221 | parser.add_argument("--dnareceptor", dest="dnareceptor", default=False, 222 | help="Treat nucleic acids as part of the receptor structure (together with any present protein) instead of as a ligand.", 223 | action="store_true") 224 | parser.add_argument("--name", dest="outputfilename", default="report", 225 | help="Set a filename for the report TXT and XML files. Will only work when processing single structures.") 226 | ligandtype = parser.add_mutually_exclusive_group() # Either peptide/inter or intra mode 227 | ligandtype.add_argument("--peptides", "--inter", dest="peptides", default=[], 228 | help="Allows to define one or multiple chains as peptide ligands or to detect inter-chain contacts", 229 | nargs="+") 230 | ligandtype.add_argument("--intra", dest="intra", help="Allows to define one chain to analyze intra-chain contacts.") 231 | parser.add_argument("--residues", dest="residues", default=[], nargs="+", 232 | help="""Allows to specify which residues of the chain(s) should be considered as peptide ligands. 233 | Give single residues (separated with comma) or ranges (with dash) or both, for several chains separate selections with one space""") 234 | parser.add_argument("--keepmod", dest="keepmod", default=False, 235 | help="Keep modified residues as ligands", 236 | action="store_true") 237 | parser.add_argument("--nohydro", dest="nohydro", default=False, 238 | help="Do not add polar hydrogens in case your structure already contains hydrogens.", 239 | action="store_true") 240 | parser.add_argument("--model", dest="model", default=1, type=int, 241 | help="Model number to be used for multi-model structures.") 242 | # Optional threshold arguments, not shown in help 243 | thr = namedtuple('threshold', 'name type') 244 | thresholds = [thr(name='aromatic_planarity', type='angle'), 245 | thr(name='hydroph_dist_max', type='distance'), thr(name='hbond_dist_max', type='distance'), 246 | thr(name='hbond_don_angle_min', type='angle'), thr(name='pistack_dist_max', type='distance'), 247 | thr(name='pistack_ang_dev', type='other'), thr(name='pistack_offset_max', type='distance'), 248 | thr(name='pication_dist_max', type='distance'), thr(name='saltbridge_dist_max', type='distance'), 249 | thr(name='halogen_dist_max', type='distance'), thr(name='halogen_acc_angle', type='angle'), 250 | thr(name='halogen_don_angle', type='angle'), thr(name='halogen_angle_dev', type='other'), 251 | thr(name='water_bridge_mindist', type='distance'), thr(name='water_bridge_maxdist', type='distance'), 252 | thr(name='water_bridge_omega_min', type='angle'), thr(name='water_bridge_omega_max', type='angle'), 253 | thr(name='water_bridge_theta_min', type='angle')] 254 | for t in thresholds: 255 | parser.add_argument('--%s' % t.name, dest=t.name, type=lambda val: threshold_limiter(parser, val), 256 | help=argparse.SUPPRESS) 257 | 258 | # Add argument to define receptor and ligand chains 259 | parser.add_argument("--chains", dest="chains", type=str, 260 | help="Specify chains as receptor/ligand groups, e.g., '[['A'], ['B']]'. " 261 | "Use format [['A'], ['B', 'C']] to define A as receptor, and B, C as ligands.") 262 | 263 | 264 | arguments = parser.parse_args() 265 | # make sure, residues is only used together with --inter (could be expanded to --intra in the future) 266 | if arguments.residues and not (arguments.peptides or arguments.intra): 267 | parser.error("The --residues option requires specification of a chain with --inter or --peptide") 268 | if arguments.residues and len(arguments.residues)!=len(arguments.peptides): 269 | parser.error("Please provide residue numbers or ranges for each chain specified. Separate selections with a single space.") 270 | # configure log levels 271 | config.VERBOSE = True if arguments.verbose else False 272 | config.QUIET = True if arguments.quiet else False 273 | config.SILENT = True if arguments.silent else False 274 | if config.VERBOSE: 275 | logger.setLevel(logging.DEBUG) 276 | elif config.QUIET: 277 | logger.setLevel(logging.WARN) 278 | elif config.SILENT: 279 | logger.setLevel(logging.CRITICAL) 280 | else: 281 | logger.setLevel(config.DEFAULT_LOG_LEVEL) 282 | config.MAXTHREADS = arguments.maxthreads 283 | config.XML = arguments.xml 284 | config.TXT = arguments.txt 285 | config.PICS = arguments.pics 286 | config.PYMOL = arguments.pymol 287 | config.STDOUT = arguments.stdout 288 | config.RAWSTRING = arguments.use_raw_string 289 | config.OUTPATH = arguments.outpath 290 | config.OUTPATH = tilde_expansion("".join([config.OUTPATH, '/']) 291 | if not config.OUTPATH.endswith('/') else config.OUTPATH) 292 | config.BASEPATH = config.OUTPATH # Used for batch processing 293 | config.BREAKCOMPOSITE = arguments.breakcomposite 294 | config.ALTLOC = arguments.altlocation 295 | config.PEPTIDES = arguments.peptides 296 | config.RESIDUES = dict(zip(arguments.peptides, map(residue_list, arguments.residues))) 297 | config.INTRA = arguments.intra 298 | config.NOFIX = arguments.nofix 299 | config.NOFIXFILE = arguments.nofixfile 300 | config.NOPDBCANMAP = bool(arguments.nopdbcanmap or config.INTRA or config.PEPTIDES) 301 | config.KEEPMOD = arguments.keepmod 302 | config.DNARECEPTOR = arguments.dnareceptor 303 | config.OUTPUTFILENAME = arguments.outputfilename 304 | config.NOHYDRO = arguments.nohydro 305 | config.MODEL = arguments.model 306 | 307 | try: 308 | # add inner quotes for python backend 309 | if not arguments.chains: 310 | config.CHAINS = None 311 | else: 312 | import re 313 | quoted_input = re.sub(r'(? 10: # Check value for angle thresholds 337 | parser.error("Threshold for distances must not be larger than 10 Angstrom.") 338 | elif tvalue > config.BS_DIST + 1: # Dynamically adapt the search space for binding site residues 339 | config.BS_DIST = tvalue + 1 340 | setattr(config, t.name.upper(), tvalue) 341 | # Check additional conditions for interdependent thresholds 342 | if not config.HALOGEN_ACC_ANGLE > config.HALOGEN_ANGLE_DEV: 343 | parser.error("The halogen acceptor angle has to be larger than the halogen angle deviation.") 344 | if not config.HALOGEN_DON_ANGLE > config.HALOGEN_ANGLE_DEV: 345 | parser.error("The halogen donor angle has to be larger than the halogen angle deviation.") 346 | if not config.WATER_BRIDGE_MINDIST < config.WATER_BRIDGE_MAXDIST: 347 | parser.error("The water bridge minimum distance has to be smaller than the water bridge maximum distance.") 348 | if not config.WATER_BRIDGE_OMEGA_MIN < config.WATER_BRIDGE_OMEGA_MAX: 349 | parser.error("The water bridge omega minimum angle has to be smaller than the water bridge omega maximum angle") 350 | expanded_path = tilde_expansion(arguments.input) if arguments.input is not None else None 351 | run_analysis(expanded_path, arguments.pdbid) # Start main script 352 | 353 | 354 | if __name__ == '__main__': 355 | main() 356 | -------------------------------------------------------------------------------- /plip/structure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharmai/plip/e4181b2c40fe5938b5f39aaf00ae3ea9d14ac9cd/plip/structure/__init__.py -------------------------------------------------------------------------------- /plip/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharmai/plip/e4181b2c40fe5938b5f39aaf00ae3ea9d14ac9cd/plip/test/__init__.py -------------------------------------------------------------------------------- /plip/test/run_all_tests.sh: -------------------------------------------------------------------------------- 1 | python3 -m unittest discover -s . -p 'test_*.py' 2 | -------------------------------------------------------------------------------- /plip/test/special/empty.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharmai/plip/e4181b2c40fe5938b5f39aaf00ae3ea9d14ac9cd/plip/test/special/empty.pdb -------------------------------------------------------------------------------- /plip/test/special/non-pdb.pdb: -------------------------------------------------------------------------------- 1 | This is not a PDB file. -------------------------------------------------------------------------------- /plip/test/test_basic_functions.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Protein-Ligand Interaction Profiler - Analyze and visualize protein-ligand interactions in PDB files. 4 | test_basic_functions.py - Unit Tests for basic functionality. 5 | """ 6 | 7 | import random 8 | # Python Standard Library 9 | import unittest 10 | 11 | import numpy 12 | 13 | from plip.basic.supplemental import euclidean3d, vector, vecangle, projection 14 | from plip.basic.supplemental import normalize_vector, cluster_doubles, centroid 15 | # Own modules 16 | from plip.structure.preparation import PDBComplex 17 | 18 | 19 | class TestLigandSupport(unittest.TestCase): 20 | """Test for support of different ligands""" 21 | 22 | def test_dna_rna(self): 23 | """Test if DNA and RNA is correctly processed as ligands""" 24 | tmpmol = PDBComplex() 25 | tmpmol.load_pdb('./pdb/1tf6.pdb') 26 | # DNA ligand four times consisting of 31 parts (composite) 27 | self.assertEqual([len(ligand.members) for ligand in tmpmol.ligands].count(31), 4) 28 | for ligset in [set((x[0] for x in ligand.members)) for ligand in tmpmol.ligands]: 29 | if len(ligset) == 4: 30 | # DNA only contains four bases 31 | self.assertEqual(ligset, {'DG', 'DC', 'DA', 'DT'}) 32 | 33 | def test_composite_ligand_alternate_locations(self): 34 | pdb_complex = PDBComplex() 35 | pdb_complex.load_pdb('./pdb/4gql.pdb') 36 | for ligand in pdb_complex.ligands: 37 | pdb_complex.characterize_complex(ligand) 38 | self.assertEqual(len(pdb_complex.ligands[0].can_to_pdb), 53) 39 | 40 | 41 | class TestMapping(unittest.TestCase): 42 | """Test""" 43 | 44 | def test_ids(self): 45 | """Test if the atom IDs are correctly mapped from internal to original PDB.""" 46 | tmpmol = PDBComplex() 47 | tmpmol.load_pdb('./pdb/1vsn.pdb') 48 | bsid = 'NFT:A:283' 49 | for ligand in tmpmol.ligands: 50 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == bsid: 51 | tmpmol.characterize_complex(ligand) 52 | s = tmpmol.interaction_sets[bsid] 53 | for contact in s.hydrophobic_contacts: 54 | if contact.restype == 'ALA' and contact.resnr == 133: 55 | self.assertEqual(contact.ligatom_orig_idx, 1636) 56 | self.assertEqual(contact.bsatom_orig_idx, 994) 57 | if contact.restype == 'ASP' and contact.resnr == 61: 58 | self.assertEqual(contact.ligatom_orig_idx, 1639) 59 | self.assertEqual(contact.bsatom_orig_idx, 448) 60 | for contact in s.hbonds_ldon + s.hbonds_pdon: 61 | if contact.restype == 'GLN' and contact.resnr == 19: 62 | self.assertEqual(contact.a_orig_idx, 1649) 63 | self.assertEqual(contact.d_orig_idx, 153) 64 | if contact.restype == 'CYS' and contact.resnr == 25: 65 | self.assertEqual(contact.a_orig_idx, 1649) 66 | self.assertEqual(contact.d_orig_idx, 183) 67 | if contact.restype == 'ASN' and contact.resnr == 158: 68 | self.assertEqual(contact.d_orig_idx, 1629) 69 | self.assertEqual(contact.a_orig_idx, 1199) 70 | for contact in s.halogen_bonds: 71 | if contact.restype == 'TYR' and contact.resnr == 67: 72 | self.assertEqual(contact.don.x_orig_idx, 1627) 73 | self.assertEqual(contact.acc.o_orig_idx, 485) 74 | if contact.restype == 'LEU' and contact.resnr == 157: 75 | self.assertEqual(contact.don.x_orig_idx, 1628) 76 | self.assertEqual(contact.acc.o_orig_idx, 1191) 77 | 78 | 79 | class GeometryTest(unittest.TestCase): 80 | """Tests for geometrical calculations in PLIP""" 81 | 82 | @staticmethod 83 | def vector_magnitude(v): 84 | return numpy.sqrt(sum(x ** 2 for x in v)) 85 | 86 | # noinspection PyUnusedLocal 87 | def setUp(self): 88 | """Generate random data for the tests""" 89 | # Generate two random n-dimensional float vectors, with -100 <= n <= 100 and values 0 <= i <= 1 90 | dim = random.randint(1, 100) 91 | self.rnd_vec = [random.uniform(-100, 100) for i in range(dim)] 92 | 93 | def test_euclidean(self): 94 | """Tests for mathematics.euclidean""" 95 | # Are the results correct? 96 | self.assertEqual(euclidean3d([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]), 0) 97 | self.assertEqual(euclidean3d([2.0, 3.0, 4.0], [2.0, 3.0, 4.0]), 0) 98 | self.assertEqual(euclidean3d([4.0, 5.0, 6.0], [4.0, 5.0, 8.0]), 2.0) 99 | # Does the function take vectors or tuples as an input? What about integers? 100 | self.assertEqual(euclidean3d((4.0, 5.0, 6.0), [4.0, 5.0, 8.0]), 2.0) 101 | self.assertEqual(euclidean3d((4.0, 5.0, 6.0), (4.0, 5.0, 8.0)), 2.0) 102 | self.assertEqual(euclidean3d((4, 5, 6), (4.0, 5.0, 8.0)), 2.0) 103 | # Is the output a float? 104 | self.assertIsInstance(euclidean3d([2.0, 3.0, 4.0], [2.0, 3.0, 4.0]), float) 105 | 106 | def test_vector(self): 107 | """Tests for mathematics.vector""" 108 | # Are the results correct? 109 | self.assertEqual(list(vector([1, 1, 1], [0, 1, 0])), [-1, 0, -1]) 110 | self.assertEqual(list(vector([0, 0, 10], [0, 0, 4])), [0, 0, -6]) 111 | # Do I get an Numpy Array? 112 | self.assertIsInstance(vector([1, 1, 1], [0, 1, 0]), numpy.ndarray) 113 | # Do I get 'None' if the points have different dimensions? 114 | self.assertEqual(vector([1, 1, 1], [0, 1, 0, 1]), None) 115 | 116 | def test_vecangle(self): 117 | """Tests for mathematics.vecangle""" 118 | # Are the results correct? 119 | self.assertEqual(vecangle([3, 4], [-8, 6], deg=False), numpy.radians(90.0)) 120 | self.assertEqual(vecangle([3, 4], [-8, 6]), 90.0) 121 | self.assertAlmostEqual(vecangle([-1, -1], [1, 1], deg=False), numpy.pi) 122 | # Correct if both vectors are equal? 123 | self.assertEqual(vecangle([3, 3], [3, 3]), 0.0) 124 | 125 | def test_centroid(self): 126 | """Tests for mathematics.centroid""" 127 | # Are the results correct? 128 | self.assertEqual(centroid([[0, 0, 0], [2, 2, 2]]), [1.0, 1.0, 1.0]) 129 | self.assertEqual(centroid([[-5, 1, 2], [10, 2, 2]]), [2.5, 1.5, 2.0]) 130 | 131 | def test_normalize_vector(self): 132 | """Tests for mathematics.normalize_vector""" 133 | # Are the results correct? 134 | self.assertAlmostEqual(self.vector_magnitude(normalize_vector(self.rnd_vec)), 1) 135 | 136 | def test_projection(self): 137 | """Tests for mathematics.projection""" 138 | # Are the results correct? 139 | self.assertEqual(projection([-1, 0, 0], [3, 3, 3], [1, 1, 1]), [3, 1, 1]) 140 | 141 | def test_cluster_doubles(self): 142 | """Tests for mathematics.cluster_doubles""" 143 | # Are the results correct? 144 | self.assertEqual(set(cluster_doubles([(1, 3), (4, 1), (5, 6), (7, 5)])), {(1, 3, 4), (5, 6, 7)}) 145 | -------------------------------------------------------------------------------- /plip/test/test_command_line.py: -------------------------------------------------------------------------------- 1 | """ 2 | Protein-Ligand Interaction Profiler - Analyze and visualize protein-ligand interactions in PDB files. 3 | test_command_line.py - Unit Tests for special cases. 4 | """ 5 | import os 6 | import subprocess 7 | import sys 8 | import tempfile 9 | import unittest 10 | 11 | 12 | class CommandLineTest(unittest.TestCase): 13 | """Checks special and extreme cases for input files.""" 14 | 15 | def setUp(self) -> None: 16 | self.tmp_dir = tempfile.TemporaryDirectory() 17 | 18 | def tearDown(self) -> None: 19 | self.tmp_dir.cleanup() 20 | 21 | def test_empty_input_file(self): 22 | """Input file is empty.""" 23 | exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -f ./special/empty.pdb -o {self.tmp_dir.name}', 24 | shell=True) 25 | self.assertEqual(exitcode, 1) 26 | 27 | def test_invalid_pdb_id(self): 28 | """A PDB ID with no valid PDB record is provided.""" 29 | exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -i xx1x -o {self.tmp_dir.name}', shell=True) 30 | self.assertEqual(exitcode, 1) 31 | 32 | def test_invalid_input_file(self): 33 | """A file is provided which is not a PDB file.""" 34 | exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -f ./special/non-pdb.pdb -o {self.tmp_dir.name}', 35 | shell=True) 36 | self.assertEqual(exitcode, 1) 37 | 38 | def test_pdb_format_not_available(self): 39 | """A valid PDB ID is provided, but there is no entry in PDB format from wwPDB""" 40 | exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -i 4v59 -o {self.tmp_dir.name}', shell=True) 41 | self.assertEqual(exitcode, 1) 42 | 43 | def test_pdb_format_available(self): 44 | """A valid PDB ID is provided, but there is no entry in PDB format from wwPDB""" 45 | exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -i 1acj -o {self.tmp_dir.name}', shell=True) 46 | self.assertEqual(exitcode, 0) 47 | 48 | def test_valid_pdb(self): 49 | """A PDB ID with no valid PDB record is provided.""" 50 | exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -x -f ./pdb/1eve.pdb -o {self.tmp_dir.name}', 51 | shell=True) 52 | self.assertEqual(len(os.listdir(self.tmp_dir.name)), 2) 53 | self.assertEqual(exitcode, 0) 54 | 55 | def test_stdout(self): 56 | """A PDB ID with no valid PDB record is provided.""" 57 | exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -t -f ./pdb/1eve.pdb -O', shell=True) 58 | self.assertEqual(exitcode, 0) 59 | 60 | def test_help(self): 61 | """A PDB ID with no valid PDB record is provided.""" 62 | exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -h', shell=True) 63 | self.assertEqual(exitcode, 0) 64 | 65 | -------------------------------------------------------------------------------- /plip/test/test_hydrogen_bonds.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from plip.basic import config 4 | from plip.structure.preparation import PDBComplex, PLInteraction 5 | 6 | 7 | def characterize_complex(pdb_file: str, binding_site_id: str) -> PLInteraction: 8 | pdb_complex = PDBComplex() 9 | pdb_complex.load_pdb(pdb_file) 10 | for ligand in pdb_complex.ligands: 11 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == binding_site_id: 12 | pdb_complex.characterize_complex(ligand) 13 | return pdb_complex.interaction_sets[binding_site_id] 14 | 15 | 16 | class HydrogenBondTestCase(unittest.TestCase): 17 | 18 | def test_4dst_nondeterministic_protonation(self): 19 | config.NOHYDRO = False 20 | for i in range(0, 10): 21 | interactions = characterize_complex('./pdb/4dst.pdb', 'GCP:A:202') 22 | all_hbonds = interactions.hbonds_ldon + interactions.hbonds_pdon 23 | self.assertTrue(len(all_hbonds) == 16 or len(all_hbonds) == 17) 24 | 25 | def test_4dst_deterministic_protonation(self): 26 | config.NOHYDRO = True 27 | for i in range(0, 10): 28 | interactions = characterize_complex('./pdb/4dst_protonated.pdb', 'GCP:A:202') 29 | all_hbonds = interactions.hbonds_ldon + interactions.hbonds_pdon 30 | self.assertTrue(len(all_hbonds) == 16) 31 | 32 | def test_no_protonation(self): 33 | config.NOHYDRO = True 34 | interactions1 = characterize_complex('./pdb/1x0n_state_1.pdb', 'DTF:A:174') 35 | self.assertEqual(len(interactions1.hbonds_ldon), 0) 36 | config.NOHYDRO = False 37 | interactions2 = characterize_complex('./pdb/1x0n_state_1.pdb', 'DTF:A:174') 38 | self.assertEqual(len(interactions2.hbonds_ldon), 1) -------------------------------------------------------------------------------- /plip/test/test_intra.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from plip.basic import config 4 | from plip.exchange.report import StructureReport 5 | from plip.structure.preparation import PDBComplex 6 | 7 | 8 | class IntraTest(unittest.TestCase): 9 | 10 | def test_4day(self): 11 | config.PEPTIDES = ['C'] 12 | pdb_complex = PDBComplex() 13 | pdb_complex.load_pdb('./pdb/4day.pdb') 14 | for ligand in pdb_complex.ligands: 15 | pdb_complex.characterize_complex(ligand) 16 | structure_report = StructureReport(pdb_complex, outputprefix="test_") 17 | structure_report.write_xml(as_string=True) 18 | config.PEPTIDES = [] 19 | -------------------------------------------------------------------------------- /plip/test/test_metal_coordination.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Protein-Ligand Interaction Profiler - Analyze and visualize protein-ligand interactions in PDB files. 4 | test_metal_coordination.py - Unit Tests for Metal Coordination. 5 | """ 6 | 7 | 8 | import unittest 9 | 10 | from plip.structure.preparation import PDBComplex 11 | 12 | 13 | class MetalCoordinationTest(unittest.TestCase): 14 | """Checks predictions against literature-validated interactions for metal coordination.""" 15 | 16 | ############################################### 17 | # Literature-validated cases from publication # 18 | ############################################### 19 | 20 | def test_1rmd(self): 21 | """Zinc binding sites in RAG1 dimerization domain (1rmd) 22 | Reference: Harding. The architecture of metal coordination groups in proteins. (2004), Fig. 1a 23 | """ 24 | 25 | tmpmol = PDBComplex() 26 | tmpmol.load_pdb('./pdb/1rmd.pdb') 27 | bsid = 'ZN:A:119' 28 | for ligand in tmpmol.ligands: 29 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == bsid: 30 | tmpmol.characterize_complex(ligand) 31 | s = tmpmol.interaction_sets[bsid] 32 | # Coordination by three cysteines and one histidine of the protein 33 | metalres = [mres.restype for mres in s.metal_complexes] 34 | self.assertEqual(metalres.count('CYS'), 3) 35 | self.assertEqual(metalres.count('HIS'), 1) 36 | # Zn atom with tetrahedral geometry (coordination number 4) 37 | self.assertEqual(s.metal_complexes[0].coordination_num, 4) 38 | self.assertEqual(s.metal_complexes[0].geometry, 'tetrahedral') 39 | 40 | def test_1rla(self): 41 | """Rat liver arginase, a binuclear manganese metalloenzyme (1rmd) 42 | Reference: Harding. The architecture of metal coordination groups in proteins. (2004), Fig. 1b 43 | """ 44 | 45 | tmpmol = PDBComplex() 46 | tmpmol.load_pdb('./pdb/1rla.pdb') 47 | bsid = 'MN:A:500' 48 | for ligand in tmpmol.ligands: 49 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == bsid: 50 | tmpmol.characterize_complex(ligand) 51 | s = tmpmol.interaction_sets[bsid] 52 | # Coordination by one histidine, three aspartic acid residues, and one water molecule 53 | metalres = [mres.restype for mres in s.metal_complexes] 54 | self.assertEqual(metalres.count('HIS'), 1) 55 | self.assertEqual(metalres.count('ASP'), 3) 56 | self.assertEqual(metalres.count('HOH'), 1) 57 | # Mn atom with square pyramidal geometry (coordination number 5) 58 | self.assertEqual(s.metal_complexes[0].coordination_num, 5) 59 | self.assertEqual(s.metal_complexes[0].geometry, 'square.pyramidal') 60 | 61 | def test_1het(self): 62 | """Liver alcohol deshydrogenase (1het) 63 | Reference: Harding. The architecture of metal coordination groups in proteins. (2004), Fig. 2 64 | """ 65 | 66 | tmpmol = PDBComplex() 67 | tmpmol.load_pdb('./pdb/1het.pdb') 68 | bsid = 'ZN:A:401' 69 | for ligand in tmpmol.ligands: 70 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == bsid: 71 | tmpmol.characterize_complex(ligand) 72 | s = tmpmol.interaction_sets[bsid] 73 | # Coordination by four cysteines 74 | metalres = [mres.restype + str(mres.resnr) for mres in s.metal_complexes] 75 | self.assertEqual(set(metalres), {'CYS97', 'CYS100', 'CYS103', 'CYS111'}) 76 | # Zn atom with tetrahedral geometry (coordination number 4) 77 | self.assertEqual(s.metal_complexes[0].coordination_num, 4) 78 | self.assertEqual(s.metal_complexes[0].geometry, 'tetrahedral') 79 | 80 | def test_1vfy(self): 81 | """Phosphatidylinositol-3-phosphate binding FYVE domain of VPS27P protein (1vfy) 82 | Reference: Harding. The architecture of metal coordination groups in proteins. (2004), Fig. 5 83 | """ 84 | 85 | tmpmol = PDBComplex() 86 | tmpmol.load_pdb('./pdb/1vfy.pdb') 87 | bsid = 'ZN:A:300' 88 | for ligand in tmpmol.ligands: 89 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == bsid: 90 | tmpmol.characterize_complex(ligand) 91 | s = tmpmol.interaction_sets[bsid] 92 | # Coordination by four cysteines 93 | metalres = [mres.restype for mres in s.metal_complexes] 94 | self.assertEqual(set(metalres), {'CYS'}) 95 | # Zn atom with tetrahedral geometry (coordination number 4) 96 | self.assertEqual(s.metal_complexes[0].coordination_num, 4) 97 | self.assertEqual(s.metal_complexes[0].geometry, 'tetrahedral') 98 | 99 | def test_2pvb(self): 100 | """Pike parvalbumin binding calcium (2pvb) 101 | Reference: Harding. The architecture of metal coordination groups in proteins. (2004), Fig. 6 102 | """ 103 | 104 | tmpmol = PDBComplex() 105 | tmpmol.load_pdb('./pdb/2pvb.pdb') 106 | bsid = 'CA:A:110' 107 | for ligand in tmpmol.ligands: 108 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == bsid: 109 | tmpmol.characterize_complex(ligand) 110 | s = tmpmol.interaction_sets[bsid] 111 | # Ca atom with square pyramidal geometry (coordination number 5) 112 | self.assertEqual(s.metal_complexes[0].coordination_num, 5) 113 | self.assertEqual(s.metal_complexes[0].geometry, 'square.pyramidal') 114 | 115 | def test_2q8q(self): 116 | """Crystal Structure of S. aureus IsdE complexed with heme (2q8q) 117 | Reference: Grigg et al. Heme coordination by Staphylococcus aureus IsdE. (2007) 118 | """ 119 | 120 | tmpmol = PDBComplex() 121 | tmpmol.load_pdb('./pdb/2q8q.pdb') 122 | bsid = 'HEM:A:300' 123 | for ligand in tmpmol.ligands: 124 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == bsid: 125 | tmpmol.characterize_complex(ligand) 126 | s = tmpmol.interaction_sets[bsid] 127 | # Coordination by four nitrogens of heme itself and one additional histidine from the protein 128 | metalres = [mres.restype for mres in s.metal_complexes] 129 | self.assertEqual(metalres.count('HEM'), 4) 130 | self.assertEqual(metalres.count('HIS'), 1) 131 | # Fe atom with square pyramidal geometry (coordination number 5) 132 | self.assertEqual(s.metal_complexes[0].coordination_num, 5) 133 | self.assertEqual(s.metal_complexes[0].geometry, 'square.pyramidal') 134 | -------------------------------------------------------------------------------- /plip/test/test_pi_stacking.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from plip.basic import config 4 | from plip.structure.preparation import PDBComplex, PLInteraction 5 | 6 | 7 | def characterize_complex(pdb_file: str, binding_site_id: str) -> PLInteraction: 8 | pdb_complex = PDBComplex() 9 | pdb_complex.load_pdb(pdb_file) 10 | for ligand in pdb_complex.ligands: 11 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == binding_site_id: 12 | pdb_complex.characterize_complex(ligand) 13 | return pdb_complex.interaction_sets[binding_site_id] 14 | 15 | 16 | class RingDetectionTest(unittest.TestCase): 17 | 18 | def test_consistent_ring_detection(self): 19 | config.NOHYDRO = True 20 | angles = set() 21 | for i in range(0, 10): 22 | interactions = characterize_complex('./pdb/4dst_protonated.pdb', 'GCP:A:202') 23 | angles.add(interactions.pistacking[0].angle) 24 | self.assertTrue(len(angles) == 1) 25 | config.NOHYDRO = False 26 | -------------------------------------------------------------------------------- /plip/test/test_remote_services.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Protein-Ligand Interaction Profiler - Analyze and visualize protein-ligand interactions in PDB files. 4 | test_remote_services.py - Unit Tests for remote services. 5 | """ 6 | 7 | 8 | import unittest 9 | 10 | from plip.exchange.webservices import check_pdb_status 11 | 12 | 13 | class TestPDB(unittest.TestCase): 14 | """Test PDB Web Service methods""" 15 | 16 | @unittest.skip("needs re-implementation to new RCSB API standards") 17 | def test_pdb_entry_status(self): 18 | # 1a0v is an obsolete entry and is replaced by 1y46 19 | status, current_pdbid = check_pdb_status('1a0v') 20 | self.assertEqual(status, 'OBSOLETE') 21 | self.assertEqual(current_pdbid, '1y46') 22 | 23 | # 1vsn is an current entry 24 | status, current_pdbid = check_pdb_status('1vsn') 25 | self.assertEqual(status, 'CURRENT') 26 | self.assertEqual(current_pdbid, '1vsn') 27 | 28 | # xxxx is not an PDB entry 29 | status, current_pdbid = check_pdb_status('xxxx') 30 | self.assertEqual(status, 'UNKNOWN') 31 | self.assertEqual(current_pdbid, 'xxxx') 32 | -------------------------------------------------------------------------------- /plip/test/test_salt_bridges.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from plip.basic import config 4 | from plip.structure.preparation import PDBComplex, PLInteraction 5 | 6 | 7 | def characterize_complex(pdb_file: str, binding_site_id: str) -> PLInteraction: 8 | pdb_complex = PDBComplex() 9 | pdb_complex.load_pdb(pdb_file) 10 | for ligand in pdb_complex.ligands: 11 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == binding_site_id: 12 | pdb_complex.characterize_complex(ligand) 13 | return pdb_complex.interaction_sets[binding_site_id] 14 | 15 | class SaltBridgeTest(unittest.TestCase): 16 | 17 | def test_4yb0(self): 18 | '''test salt bridge detection for nucleic acids as part of the receptor''' 19 | 20 | config.DNARECEPTOR = True 21 | interactions = characterize_complex('./pdb/4yb0.pdb', 'C2E:R:102') 22 | salt_bridges = interactions.saltbridge_lneg + interactions.saltbridge_pneg 23 | self.assertTrue(len(salt_bridges) == 1) -------------------------------------------------------------------------------- /plip/test/test_structure_processing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from plip.basic import config 4 | from plip.structure.preparation import PDBComplex, PLInteraction 5 | 6 | 7 | def characterize_complex(pdb_file: str, binding_site_id: str) -> PLInteraction: 8 | pdb_complex = PDBComplex() 9 | pdb_complex.load_pdb(pdb_file) 10 | for ligand in pdb_complex.ligands: 11 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == binding_site_id: 12 | pdb_complex.characterize_complex(ligand) 13 | return pdb_complex.interaction_sets[binding_site_id] 14 | 15 | 16 | class StructureProcessingTestCase(unittest.TestCase): 17 | def test_nmr(self): 18 | all_hydrogen_bonds = set() 19 | for i in range(1, 10): 20 | config.MODEL = i 21 | interactions = characterize_complex('./pdb/2ndo.pdb', 'SFQ:A:201') 22 | all_hbonds = interactions.hbonds_ldon + interactions.hbonds_pdon 23 | all_hydrogen_bonds.add(len(all_hbonds)) 24 | # models contain from 0-2 hydrogen bonds 25 | self.assertEqual(all_hydrogen_bonds, {0, 1, 2}) 26 | 27 | def test_nmr_invalid_model(self): 28 | config.MODEL = 11 29 | interactions = characterize_complex('./pdb/2ndo.pdb', 'SFQ:A:201') 30 | all_hbonds = interactions.hbonds_ldon + interactions.hbonds_pdon 31 | self.assertEqual(len(all_hbonds), 1) 32 | -------------------------------------------------------------------------------- /plip/test/test_visualization.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import unittest 4 | 5 | from plip.basic import config 6 | from plip.basic.remote import VisualizerData 7 | from plip.structure.preparation import PDBComplex 8 | from plip.visualization.visualize import visualize_in_pymol 9 | 10 | 11 | class VisualizationTest(unittest.TestCase): 12 | 13 | def setUp(self) -> None: 14 | self.tmp_dir = tempfile.mkdtemp() 15 | 16 | def test_visualization(self) -> None: 17 | 18 | pdb_file = './pdb/2ndo.pdb' 19 | binding_site_id = 'SFQ:A:201' 20 | config.PYMOL = True 21 | config.MODEL = 2 22 | config.OUTPATH = str(self.tmp_dir) 23 | pdb_complex = PDBComplex() 24 | pdb_complex.load_pdb(pdb_file) 25 | for ligand in pdb_complex.ligands: 26 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == binding_site_id: 27 | pdb_complex.characterize_complex(ligand) 28 | visualizer_complexes = [VisualizerData(pdb_complex, site) for site in sorted(pdb_complex.interaction_sets) if 29 | not len(pdb_complex.interaction_sets[site].interacting_res) == 0] 30 | visualize_in_pymol(visualizer_complexes[0]) 31 | self.assertEqual(1, len(os.listdir(self.tmp_dir))) 32 | -------------------------------------------------------------------------------- /plip/test/test_water_bridges.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from plip.structure.preparation import PDBComplex, PLInteraction 4 | 5 | 6 | def characterize_complex(pdb_file: str, binding_site_id: str) -> PLInteraction: 7 | pdb_complex = PDBComplex() 8 | pdb_complex.load_pdb(pdb_file) 9 | for ligand in pdb_complex.ligands: 10 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == binding_site_id: 11 | pdb_complex.characterize_complex(ligand) 12 | return pdb_complex.interaction_sets[binding_site_id] 13 | 14 | 15 | class WaterBridgeTest(unittest.TestCase): 16 | 17 | def test_3ems(self): 18 | interactions = characterize_complex('./pdb/3ems.pdb', 'ARG:A:131') 19 | water_bridges = interactions.water_bridges 20 | self.assertEqual(len(water_bridges), 4) 21 | -------------------------------------------------------------------------------- /plip/test/test_xml_parser.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Protein-Ligand Interaction Profiler - Analyze and visualize protein-ligand interactions in PDB files. 4 | test_xml_parser.py - Unit Tests for XML Parser. 5 | """ 6 | 7 | 8 | import unittest 9 | 10 | from plip.exchange.xml import PlipXML 11 | 12 | 13 | class XMLParserTest(unittest.TestCase): 14 | """Checks if the XML parser is working correctly""" 15 | 16 | def setUp(self): 17 | self.px = PlipXML('./xml/1vsn.report.xml') 18 | self.bsite = self.px.bsites['NFT:A:283'] 19 | self.smiles = 'CC(C)CC(NC(c1ccc(cc1)c1ccc(cc1)S(N)(=O)=O)C(F)(F)F)C(=O)NCC=N' 20 | 21 | def test_general_information(self): 22 | """Test if general information is correctly parsed.""" 23 | self.assertEqual(self.px.version, '1.4.2') 24 | self.assertEqual(self.px.pdbid, '1VSN') 25 | self.assertFalse(self.px.fixed) 26 | self.assertEqual(self.px.filename, '1vsn.pdb') 27 | self.assertEqual(self.px.excluded, []) 28 | 29 | def test_bsite_information(self): 30 | """Test if the binding site information is correctly parsed.""" 31 | self.assertEqual(self.bsite.pdbid, '1VSN') 32 | self.assertEqual(self.bsite.uniqueid, '1VSN:NFT:A:283') 33 | self.assertEqual(self.bsite.hetid, 'NFT') 34 | self.assertEqual(self.bsite.longname, 'NFT') 35 | self.assertEqual(self.bsite.ligtype, 'SMALLMOLECULE') 36 | self.assertEqual(self.bsite.smiles, self.smiles) 37 | self.assertEqual(self.bsite.members, ['NFT:A:283']) 38 | self.assertFalse(self.bsite.composite) 39 | 40 | # ligand properties 41 | self.assertEqual(self.bsite.heavy_atoms, 33) 42 | self.assertEqual(self.bsite.hbd, 5) 43 | self.assertEqual(self.bsite.unpaired_hbd, 0) 44 | self.assertEqual(self.bsite.hba, 7) 45 | self.assertEqual(self.bsite.unpaired_hba, 2) 46 | self.assertEqual(self.bsite.hal, 3) 47 | self.assertEqual(self.bsite.unpaired_hal, 1) 48 | self.assertEqual(self.bsite.rings, 2) 49 | self.assertEqual(self.bsite.rotatable_bonds, 12) 50 | self.assertAlmostEqual(self.bsite.molweight, 484, 0) 51 | self.assertAlmostEqual(self.bsite.logp, 6, 0) 52 | 53 | # Atom mappings (non-exhaustive test) 54 | lmap = self.bsite.mappings['pdb_to_smiles'] 55 | self.assertEqual(lmap[1625], 24) 56 | self.assertEqual(lmap[1649], 33) 57 | self.assertEqual(lmap[1617], 14) 58 | 59 | # Binding site residues 60 | self.assertEqual(len(self.bsite.bs_res), 35) 61 | 62 | # Interacting chains 63 | self.assertEqual(self.bsite.interacting_chains, ['A']) 64 | 65 | # Has Interactions? 66 | self.assertTrue(self.bsite.has_interactions, True) 67 | 68 | def test_interactions(self): 69 | """Test if interaction information is correctly parsed.""" 70 | 71 | # Hydrophobic Contacts 72 | self.assertEqual(len(self.bsite.hydrophobics), 4) 73 | hydrophobic1 = self.bsite.hydrophobics[0] 74 | self.assertEqual(hydrophobic1.dist, 3.67) 75 | self.assertEqual(hydrophobic1.resnr, 61) 76 | self.assertEqual(hydrophobic1.restype, 'ASP') 77 | self.assertEqual(hydrophobic1.reschain, 'A') 78 | self.assertEqual(hydrophobic1.ligcarbonidx, 1639) 79 | self.assertEqual(hydrophobic1.protcarbonidx, 448) 80 | self.assertEqual(hydrophobic1.ligcoo, (-7.395, 24.225, 6.614)) 81 | self.assertEqual(hydrophobic1.protcoo, (-6.900, 21.561, 9.090)) 82 | 83 | # Hydrogen Bonds 84 | self.assertEqual(len(self.bsite.hbonds), 6) 85 | hbond1 = self.bsite.hbonds[0] 86 | self.assertEqual(hbond1.resnr, 19) 87 | self.assertEqual(hbond1.restype, 'GLN') 88 | self.assertEqual(hbond1.reschain, 'A') 89 | self.assertTrue(hbond1.sidechain) 90 | self.assertEqual(hbond1.dist_h_a, 2.16) 91 | self.assertEqual(hbond1.dist_d_a, 3.11) 92 | self.assertEqual(hbond1.don_angle, 160.05) 93 | self.assertTrue(hbond1.protisdon) 94 | self.assertEqual(hbond1.donoridx, 153) 95 | self.assertEqual(hbond1.donortype, 'Nam') 96 | self.assertEqual(hbond1.acceptoridx, 1649) 97 | self.assertEqual(hbond1.acceptortype, 'N2') 98 | self.assertEqual(hbond1.ligcoo, (2.820, 18.145, 6.806)) 99 | self.assertEqual(hbond1.protcoo, (3.976, 15.409, 7.712)) 100 | 101 | # Water Bridges 102 | self.assertEqual(len(self.bsite.wbridges), 1) 103 | wbridge1 = self.bsite.wbridges[0] 104 | self.assertEqual(wbridge1.resnr, 159) 105 | self.assertEqual(wbridge1.restype, 'HIS') 106 | self.assertEqual(wbridge1.reschain, 'A') 107 | self.assertEqual(wbridge1.dist_a_w, 3.67) 108 | self.assertEqual(wbridge1.dist_d_w, 3.13) 109 | self.assertEqual(wbridge1.don_angle, 126.73) 110 | self.assertEqual(wbridge1.water_angle, 116.36) 111 | self.assertTrue(wbridge1.protisdon) 112 | self.assertEqual(wbridge1.donor_idx, 1210) 113 | self.assertEqual(wbridge1.donortype, 'Nar') 114 | self.assertEqual(wbridge1.acceptor_idx, 1649) 115 | self.assertEqual(wbridge1.acceptortype, 'N2') 116 | self.assertEqual(wbridge1.ligcoo, (2.820, 18.145, 6.806)) 117 | self.assertEqual(wbridge1.protcoo, (6.401, 19.307, 4.971)) 118 | self.assertEqual(wbridge1.watercoo, (3.860, 18.563, 3.309)) 119 | 120 | # Salt Bridges 121 | self.assertEqual(len(self.bsite.sbridges), 0) 122 | 123 | # Pi stacking 124 | self.assertEqual(len(self.bsite.pi_stacks), 0) 125 | 126 | # Pi cation interactions 127 | self.assertEqual(len(self.bsite.pi_cations), 0) 128 | 129 | # Halogen Bonds 130 | self.assertEqual(len(self.bsite.halogens), 2) 131 | hal1 = self.bsite.halogens[0] 132 | self.assertEqual(hal1.resnr, 67) 133 | self.assertEqual(hal1.restype, 'TYR') 134 | self.assertEqual(hal1.reschain, 'A') 135 | self.assertTrue(hal1.sidechain) 136 | self.assertEqual(hal1.dist, 3.37) 137 | self.assertEqual(hal1.don_angle, 156.70) 138 | self.assertEqual(hal1.acc_angle, 100.53) 139 | self.assertEqual(hal1.don_idx, 1627) 140 | self.assertEqual(hal1.donortype, 'F') 141 | self.assertEqual(hal1.acc_idx, 485) 142 | self.assertEqual(hal1.acceptortype, 'O3') 143 | self.assertEqual(hal1.ligcoo, (-1.862, 29.303, 4.507)) 144 | self.assertEqual(hal1.protcoo, (-1.005, 26.276, 3.287)) 145 | 146 | # Metal complexes 147 | self.assertEqual(len(self.bsite.metal_complexes), 0) 148 | -------------------------------------------------------------------------------- /plip/test/test_xml_writer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from plip.exchange.report import StructureReport 4 | from plip.structure.preparation import PDBComplex 5 | 6 | 7 | class XMLWriterTest(unittest.TestCase): 8 | def test_pi_stacking(self): 9 | pdb_complex = PDBComplex() 10 | pdb_complex.load_pdb('./pdb/4dst_protonated.pdb') 11 | for ligand in pdb_complex.ligands: 12 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == 'GCP:A:202': 13 | pdb_complex.characterize_complex(ligand) 14 | structure_report = StructureReport(pdb_complex, outputprefix="test_") 15 | structure_report.write_xml(as_string=True) 16 | 17 | def test_pication(self): 18 | pdb_complex = PDBComplex() 19 | pdb_complex.load_pdb('./pdb/6nhb.pdb') 20 | for ligand in pdb_complex.ligands: 21 | if ':'.join([ligand.hetid, ligand.chain, str(ligand.position)]) == 'H4B:A:802': 22 | pdb_complex.characterize_complex(ligand) 23 | structure_report = StructureReport(pdb_complex, outputprefix="test_") 24 | structure_report.write_xml(as_string=True) -------------------------------------------------------------------------------- /plip/test/xml/1vsn.report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.4.2 4 | 5 | 6 | NFT 7 | SMALLMOLECULE 8 | NFT 9 | A 10 | 283 11 | False 12 | 13 | NFT:A:283 14 | 15 | CC(C)CC(NC(c1ccc(cc1)c1ccc(cc1)S(N)(=O)=O)C(F)(F)F)C(=O)NCC=N 16 | LUFZQYFBEDDLGO-UHFFFAOYSA-N 17 | 18 | 19 | 33 20 | 5 21 | 0 22 | 7 23 | 2 24 | 3 25 | 1 26 | 2 27 | 12 28 | 483.5270496 29 | 5.9141 30 | 31 | 32 | A 33 | 34 | 35 | 64A 36 | 157A 37 | 176A 38 | 68A 39 | 29A 40 | 160A 41 | 26A 42 | 24A 43 | 22A 44 | 20A 45 | 63A 46 | 58A 47 | 61A 48 | 133A 49 | 67A 50 | 65A 51 | 28A 52 | 205A 53 | 69A 54 | 177A 55 | 159A 56 | 158A 57 | 161A 58 | 27A 59 | 1156A 60 | 23A 61 | 59A 62 | 62A 63 | 132A 64 | 60A 65 | 134A 66 | 57A 67 | 19A 68 | 70A 69 | 66A 70 | 71 | 72 | 73 | 74 | 61 75 | ASP 76 | A 77 | 283 78 | NFT 79 | A 80 | 3.67 81 | 1639 82 | 448 83 | 84 | -7.395 85 | 24.225 86 | 6.614 87 | 88 | 89 | -6.900 90 | 21.561 91 | 9.090 92 | 93 | 94 | 95 | 67 96 | TYR 97 | A 98 | 283 99 | NFT 100 | A 101 | 3.73 102 | 1622 103 | 482 104 | 105 | -2.692 106 | 25.499 107 | 5.958 108 | 109 | 110 | -2.449 111 | 29.118 112 | 6.843 113 | 114 | 115 | 116 | 67 117 | TYR 118 | A 119 | 283 120 | NFT 121 | A 122 | 3.84 123 | 1636 124 | 481 125 | 126 | 3.244 127 | 26.542 128 | 6.729 129 | 130 | 131 | 0.294 132 | 28.886 133 | 7.491 134 | 135 | 136 | 137 | 133 138 | ALA 139 | A 140 | 283 141 | NFT 142 | A 143 | 3.97 144 | 1636 145 | 994 146 | 147 | 3.244 148 | 26.542 149 | 6.729 150 | 151 | 152 | 6.575 153 | 27.872 154 | 5.025 155 | 156 | 157 | 158 | 159 | 160 | 19 161 | GLN 162 | A 163 | 283 164 | NFT 165 | A 166 | True 167 | 2.16 168 | 3.11 169 | 160.05 170 | True 171 | 153 172 | Nam 173 | 1649 174 | N2 175 | 176 | 2.820 177 | 18.145 178 | 6.806 179 | 180 | 181 | 3.976 182 | 15.409 183 | 7.712 184 | 185 | 186 | 187 | 61 188 | ASP 189 | A 190 | 283 191 | NFT 192 | A 193 | True 194 | 2.26 195 | 2.99 196 | 134.61 197 | True 198 | 451 199 | O3 200 | 1645 201 | N3 202 | 203 | -9.805 204 | 23.545 205 | 10.596 206 | 207 | 208 | -9.236 209 | 20.949 210 | 9.223 211 | 212 | 213 | 214 | 61 215 | ASP 216 | A 217 | 283 218 | NFT 219 | A 220 | True 221 | 1.98 222 | 2.99 223 | 170.22 224 | False 225 | 1645 226 | N3 227 | 451 228 | O3 229 | 230 | -9.805 231 | 23.545 232 | 10.596 233 | 234 | 235 | -9.236 236 | 20.949 237 | 9.223 238 | 239 | 240 | 241 | 66 242 | GLY 243 | A 244 | 283 245 | NFT 246 | A 247 | False 248 | 2.33 249 | 3.18 250 | 140.42 251 | False 252 | 1631 253 | N3 254 | 473 255 | O2 256 | 257 | 0.027 258 | 24.446 259 | 5.449 260 | 261 | 262 | 0.194 263 | 25.010 264 | 8.576 265 | 266 | 267 | 268 | 66 269 | GLY 270 | A 271 | 283 272 | NFT 273 | A 274 | False 275 | 2.05 276 | 2.95 277 | 150.59 278 | True 279 | 470 280 | Nam 281 | 1638 282 | O2 283 | 284 | 0.422 285 | 22.065 286 | 7.006 287 | 288 | 289 | -1.810 290 | 23.106 291 | 8.621 292 | 293 | 294 | 295 | 158 296 | ASN 297 | A 298 | 283 299 | NFT 300 | A 301 | False 302 | 1.95 303 | 2.92 304 | 167.45 305 | False 306 | 1629 307 | Nam 308 | 1199 309 | O2 310 | 311 | 1.645 312 | 21.274 313 | 5.306 314 | 315 | 316 | 3.137 317 | 21.570 318 | 2.817 319 | 320 | 321 | 322 | 323 | 324 | 159 325 | HIS 326 | A 327 | 283 328 | NFT 329 | A 330 | 3.67 331 | 3.13 332 | 126.73 333 | 116.36 334 | True 335 | 1210 336 | Nar 337 | 1649 338 | N2 339 | 1758 340 | 341 | 2.820 342 | 18.145 343 | 6.806 344 | 345 | 346 | 6.401 347 | 19.307 348 | 4.971 349 | 350 | 351 | 3.860 352 | 18.563 353 | 3.309 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 67 363 | TYR 364 | A 365 | 283 366 | NFT 367 | A 368 | True 369 | 3.37 370 | 156.70 371 | 100.53 372 | 1627 373 | F 374 | 485 375 | O3 376 | 377 | -1.862 378 | 29.303 379 | 4.507 380 | 381 | 382 | -1.005 383 | 26.276 384 | 3.287 385 | 386 | 387 | 388 | 157 389 | LEU 390 | A 391 | 283 392 | NFT 393 | A 394 | False 395 | 3.24 396 | 141.32 397 | 129.13 398 | 1628 399 | F 400 | 1191 401 | O2 402 | 403 | 2.348 404 | 25.905 405 | 0.550 406 | 407 | 408 | 0.124 409 | 24.593 410 | 2.500 411 | 412 | 413 | 414 | 415 | 416 | 417 | 1:1636,2:1634,3:1635,4:1633,5:1632,6:1631,7:1624,8:1623,9:1621,10:1620,11:1619,12:1618,13:1622,14:1617,15:1639,16:1640,17:1641,18:1642,19:1643,20:1644,21:1645,22:1646,23:1647,24:1625,25:1626,26:1627,27:1628,28:1637,29:1638,30:1629,31:1630,32:1648,33:1649 418 | 419 | 420 | 2018/08/07 421 | Salentin,S. et al. PLIP: fully automated protein-ligand interaction profiler. Nucl. Acids Res. (1 July 2015) 43 (W1): W443-W447. doi: 10.1093/nar/gkv315 422 | default 423 | 1VSN 424 | ./1vsn.pdb 425 | False 426 | 1vsn.pdb 427 | 428 | 429 | -------------------------------------------------------------------------------- /plip/visualization/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharmai/plip/e4181b2c40fe5938b5f39aaf00ae3ea9d14ac9cd/plip/visualization/__init__.py -------------------------------------------------------------------------------- /plip/visualization/chimera.py: -------------------------------------------------------------------------------- 1 | class ChimeraVisualizer: 2 | """Provides visualization for Chimera.""" 3 | 4 | def __init__(self, plcomplex, chimera_module, tid): 5 | self.chimera = chimera_module 6 | self.tid = tid 7 | self.uid = plcomplex.uid 8 | self.plipname = 'PLIP-%i' % self.tid 9 | self.hetid, self.chain, self.pos = self.uid.split(':') 10 | self.pos = int(self.pos) 11 | self.colorbyname = self.chimera.colorTable.getColorByName 12 | self.rc = self.chimera.runCommand 13 | self.getPseudoBondGroup = self.chimera.misc.getPseudoBondGroup 14 | 15 | if plcomplex is not None: 16 | self.plcomplex = plcomplex 17 | self.protname = plcomplex.pdbid # Name of protein with binding site 18 | self.ligname = plcomplex.hetid # Name of ligand 19 | self.metal_ids = plcomplex.metal_ids 20 | self.water_ids = [] 21 | self.bs_res_ids = [] 22 | self.models = self.chimera.openModels 23 | 24 | for md in self.models.list(): 25 | if md.name == self.plipname: 26 | self.model = md 27 | 28 | self.atoms = self.atom_by_serialnumber() 29 | 30 | def set_initial_representations(self): 31 | """Set the initial representations""" 32 | self.update_model_dict() 33 | self.rc("background solid white") 34 | self.rc("setattr g display 0") # Hide all pseudobonds 35 | self.rc("~display #%i & :/isHet & ~:%s" % (self.model_dict[self.plipname], self.hetid)) 36 | 37 | def update_model_dict(self): 38 | """Updates the model dictionary""" 39 | dct = {} 40 | models = self.chimera.openModels 41 | for md in models.list(): 42 | dct[md.name] = md.id 43 | self.model_dict = dct 44 | 45 | def atom_by_serialnumber(self): 46 | """Provides a dictionary mapping serial numbers to their atom objects.""" 47 | atm_by_snum = {} 48 | for atom in self.model.atoms: 49 | atm_by_snum[atom.serialNumber] = atom 50 | return atm_by_snum 51 | 52 | def show_hydrophobic(self): 53 | """Visualizes hydrophobic contacts.""" 54 | grp = self.getPseudoBondGroup("Hydrophobic Interactions-%i" % self.tid, associateWith=[self.model]) 55 | grp.lineType = self.chimera.Dash 56 | grp.lineWidth = 3 57 | grp.color = self.colorbyname('gray') 58 | for i in self.plcomplex.hydrophobic_contacts.pairs_ids: 59 | self.bs_res_ids.append(i[0]) 60 | 61 | def show_hbonds(self): 62 | """Visualizes hydrogen bonds.""" 63 | grp = self.getPseudoBondGroup("Hydrogen Bonds-%i" % self.tid, associateWith=[self.model]) 64 | grp.lineWidth = 3 65 | for i in self.plcomplex.hbonds.ldon_id: 66 | b = grp.newPseudoBond(self.atoms[i[0]], self.atoms[i[1]]) 67 | b.color = self.colorbyname('blue') 68 | self.bs_res_ids.append(i[0]) 69 | for i in self.plcomplex.hbonds.pdon_id: 70 | b = grp.newPseudoBond(self.atoms[i[0]], self.atoms[i[1]]) 71 | b.color = self.colorbyname('blue') 72 | self.bs_res_ids.append(i[1]) 73 | 74 | def show_halogen(self): 75 | """Visualizes halogen bonds.""" 76 | grp = self.getPseudoBondGroup("HalogenBonds-%i" % self.tid, associateWith=[self.model]) 77 | grp.lineWidth = 3 78 | for i in self.plcomplex.halogen_bonds: 79 | b = grp.newPseudoBond(self.atoms[i[0]], self.atoms[i[1]]) 80 | b.color = self.colorbyname('turquoise') 81 | 82 | self.bs_res_ids.append(i.acc_id) 83 | 84 | def show_stacking(self): 85 | """Visualizes pi-stacking interactions.""" 86 | grp = self.getPseudoBondGroup("pi-Stacking-%i" % self.tid, associateWith=[self.model]) 87 | grp.lineWidth = 3 88 | grp.lineType = self.chimera.Dash 89 | for i, stack in enumerate(self.plcomplex.pistacking): 90 | 91 | m = self.model 92 | r = m.newResidue("pseudoatoms", " ", 1, " ") 93 | centroid_prot = m.newAtom("CENTROID", self.chimera.Element("CENTROID")) 94 | x, y, z = stack.proteinring_center 95 | centroid_prot.setCoord(self.chimera.Coord(x, y, z)) 96 | r.addAtom(centroid_prot) 97 | 98 | centroid_lig = m.newAtom("CENTROID", self.chimera.Element("CENTROID")) 99 | x, y, z = stack.ligandring_center 100 | centroid_lig.setCoord(self.chimera.Coord(x, y, z)) 101 | r.addAtom(centroid_lig) 102 | 103 | b = grp.newPseudoBond(centroid_lig, centroid_prot) 104 | b.color = self.colorbyname('forest green') 105 | 106 | self.bs_res_ids += stack.proteinring_atoms 107 | 108 | def show_cationpi(self): 109 | """Visualizes cation-pi interactions""" 110 | grp = self.getPseudoBondGroup("Cation-Pi-%i" % self.tid, associateWith=[self.model]) 111 | grp.lineWidth = 3 112 | grp.lineType = self.chimera.Dash 113 | for i, cat in enumerate(self.plcomplex.pication): 114 | 115 | m = self.model 116 | r = m.newResidue("pseudoatoms", " ", 1, " ") 117 | chargecenter = m.newAtom("CHARGE", self.chimera.Element("CHARGE")) 118 | x, y, z = cat.charge_center 119 | chargecenter.setCoord(self.chimera.Coord(x, y, z)) 120 | r.addAtom(chargecenter) 121 | 122 | centroid = m.newAtom("CENTROID", self.chimera.Element("CENTROID")) 123 | x, y, z = cat.ring_center 124 | centroid.setCoord(self.chimera.Coord(x, y, z)) 125 | r.addAtom(centroid) 126 | 127 | b = grp.newPseudoBond(centroid, chargecenter) 128 | b.color = self.colorbyname('orange') 129 | 130 | if cat.protcharged: 131 | self.bs_res_ids += cat.charge_atoms 132 | else: 133 | self.bs_res_ids += cat.ring_atoms 134 | 135 | def show_sbridges(self): 136 | """Visualizes salt bridges.""" 137 | # Salt Bridges 138 | grp = self.getPseudoBondGroup("Salt Bridges-%i" % self.tid, associateWith=[self.model]) 139 | grp.lineWidth = 3 140 | grp.lineType = self.chimera.Dash 141 | for i, sbridge in enumerate(self.plcomplex.saltbridges): 142 | 143 | m = self.model 144 | r = m.newResidue("pseudoatoms", " ", 1, " ") 145 | chargecenter1 = m.newAtom("CHARGE", self.chimera.Element("CHARGE")) 146 | x, y, z = sbridge.positive_center 147 | chargecenter1.setCoord(self.chimera.Coord(x, y, z)) 148 | r.addAtom(chargecenter1) 149 | 150 | chargecenter2 = m.newAtom("CHARGE", self.chimera.Element("CHARGE")) 151 | x, y, z = sbridge.negative_center 152 | chargecenter2.setCoord(self.chimera.Coord(x, y, z)) 153 | r.addAtom(chargecenter2) 154 | 155 | b = grp.newPseudoBond(chargecenter1, chargecenter2) 156 | b.color = self.colorbyname('yellow') 157 | 158 | if sbridge.protispos: 159 | self.bs_res_ids += sbridge.positive_atoms 160 | else: 161 | self.bs_res_ids += sbridge.negative_atoms 162 | 163 | def show_wbridges(self): 164 | """Visualizes water bridges""" 165 | grp = self.getPseudoBondGroup("Water Bridges-%i" % self.tid, associateWith=[self.model]) 166 | grp.lineWidth = 3 167 | for i, wbridge in enumerate(self.plcomplex.waterbridges): 168 | c = grp.newPseudoBond(self.atoms[wbridge.water_id], self.atoms[wbridge.acc_id]) 169 | c.color = self.colorbyname('cornflower blue') 170 | self.water_ids.append(wbridge.water_id) 171 | b = grp.newPseudoBond(self.atoms[wbridge.don_id], self.atoms[wbridge.water_id]) 172 | b.color = self.colorbyname('cornflower blue') 173 | self.water_ids.append(wbridge.water_id) 174 | if wbridge.protisdon: 175 | self.bs_res_ids.append(wbridge.don_id) 176 | else: 177 | self.bs_res_ids.append(wbridge.acc_id) 178 | 179 | def show_metal(self): 180 | """Visualizes metal coordination.""" 181 | grp = self.getPseudoBondGroup("Metal Coordination-%i" % self.tid, associateWith=[self.model]) 182 | grp.lineWidth = 3 183 | for i, metal in enumerate(self.plcomplex.metal_complexes): 184 | c = grp.newPseudoBond(self.atoms[metal.metal_id], self.atoms[metal.target_id]) 185 | c.color = self.colorbyname('magenta') 186 | 187 | if metal.location == 'water': 188 | self.water_ids.append(metal.target_id) 189 | 190 | if metal.location.startswith('protein'): 191 | self.bs_res_ids.append(metal.target_id) 192 | 193 | def cleanup(self): 194 | """Clean up the visualization.""" 195 | 196 | if not len(self.water_ids) == 0: 197 | # Hide all non-interacting water molecules 198 | water_selection = [] 199 | for wid in self.water_ids: 200 | water_selection.append('serialNumber=%i' % wid) 201 | self.rc("~display :HOH") 202 | self.rc("display :@/%s" % " or ".join(water_selection)) 203 | 204 | # Show all interacting binding site residues 205 | self.rc("~display #%i & ~:/isHet" % self.model_dict[self.plipname]) 206 | self.rc("display :%s" % ",".join([str(self.atoms[bsid].residue.id) for bsid in self.bs_res_ids])) 207 | self.rc("color lightblue :HOH") 208 | 209 | def zoom_to_ligand(self): 210 | """Centers the view on the ligand and its binding site residues.""" 211 | self.rc("center #%i & :%s" % (self.model_dict[self.plipname], self.hetid)) 212 | 213 | def refinements(self): 214 | """Details for the visualization.""" 215 | self.rc("setattr a color gray @CENTROID") 216 | self.rc("setattr a radius 0.3 @CENTROID") 217 | self.rc("represent sphere @CENTROID") 218 | self.rc("setattr a color orange @CHARGE") 219 | self.rc("setattr a radius 0.4 @CHARGE") 220 | self.rc("represent sphere @CHARGE") 221 | self.rc("display :pseudoatoms") 222 | -------------------------------------------------------------------------------- /plip/visualization/pymol.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | from time import sleep 5 | 6 | from pymol import cmd 7 | 8 | from plip.basic import config 9 | 10 | 11 | class PyMOLVisualizer: 12 | 13 | def __init__(self, plcomplex): 14 | if plcomplex is not None: 15 | self.plcomplex = plcomplex 16 | self.protname = plcomplex.pdbid # Name of protein with binding site 17 | self.hetid = plcomplex.hetid 18 | self.ligandtype = plcomplex.ligandtype 19 | self.ligname = "Ligand_" + self.hetid # Name of ligand 20 | self.metal_ids = plcomplex.metal_ids 21 | 22 | def set_initial_representations(self): 23 | """General settings for PyMOL""" 24 | self.standard_settings() 25 | cmd.set('dash_gap', 0) # Show not dashes, but lines for the pliprofiler 26 | cmd.set('ray_shadow', 0) # Turn on ray shadows for clearer ray-traced images 27 | cmd.set('cartoon_color', 'mylightblue') 28 | 29 | # Set clipping planes for full view 30 | cmd.clip('far', -1000) 31 | cmd.clip('near', 1000) 32 | 33 | @staticmethod 34 | def make_initial_selections(): 35 | """Make empty selections for structures and interactions""" 36 | for group in ['Hydrophobic-P', 'Hydrophobic-L', 'HBondDonor-P', 37 | 'HBondDonor-L', 'HBondAccept-P', 'HBondAccept-L', 38 | 'HalogenAccept', 'HalogenDonor', 'Water', 'MetalIons', 'StackRings-P', 39 | 'PosCharge-P', 'PosCharge-L', 'NegCharge-P', 'NegCharge-L', 40 | 'PiCatRing-P', 'StackRings-L', 'PiCatRing-L', 'Metal-M', 'Metal-P', 41 | 'Metal-W', 'Metal-L', 'Unpaired-HBA', 'Unpaired-HBD', 'Unpaired-HAL', 42 | 'Unpaired-RINGS']: 43 | cmd.select(group, 'None') 44 | 45 | def standard_settings(self): 46 | """Sets up standard settings for a nice visualization.""" 47 | cmd.set('bg_rgb', [1.0, 1.0, 1.0]) # White background 48 | cmd.set('depth_cue', 0) # Turn off depth cueing (no fog) 49 | cmd.set('cartoon_side_chain_helper', 1) # Improve combined visualization of sticks and cartoon 50 | cmd.set('cartoon_fancy_helices', 1) # Nicer visualization of helices (using tapered ends) 51 | cmd.set('transparency_mode', 1) # Turn on multilayer transparency 52 | cmd.set('dash_radius', 0.05) 53 | self.set_custom_colorset() 54 | 55 | @staticmethod 56 | def set_custom_colorset(): 57 | """Defines a colorset with matching colors. Provided by Joachim.""" 58 | cmd.set_color('myorange', '[253, 174, 97]') 59 | cmd.set_color('mygreen', '[171, 221, 164]') 60 | cmd.set_color('myred', '[215, 25, 28]') 61 | cmd.set_color('myblue', '[43, 131, 186]') 62 | cmd.set_color('mylightblue', '[158, 202, 225]') 63 | cmd.set_color('mylightgreen', '[229, 245, 224]') 64 | 65 | @staticmethod 66 | def select_by_ids(selname, idlist, selection_exists=False, chunksize=20, restrict=None): 67 | """Selection with a large number of ids concatenated into a selection 68 | list can cause buffer overflow in PyMOL. This function takes a selection 69 | name and and list of IDs (list of integers) as input and makes a careful 70 | step-by-step selection (packages of 20 by default)""" 71 | idlist = list(set(idlist)) # Remove duplicates 72 | if not selection_exists: 73 | cmd.select(selname, 'None') # Empty selection first 74 | idchunks = [idlist[i:i + chunksize] for i in range(0, len(idlist), chunksize)] 75 | for idchunk in idchunks: 76 | cmd.select(selname, '%s or (id %s)' % (selname, '+'.join(map(str, idchunk)))) 77 | if restrict is not None: 78 | cmd.select(selname, '%s and %s' % (selname, restrict)) 79 | 80 | @staticmethod 81 | def object_exists(object_name): 82 | """Checks if an object exists in the open PyMOL session.""" 83 | return object_name in cmd.get_names("objects") 84 | 85 | def show_hydrophobic(self): 86 | """Visualizes hydrophobic contacts.""" 87 | hydroph = self.plcomplex.hydrophobic_contacts 88 | if not len(hydroph.bs_ids) == 0: 89 | self.select_by_ids('Hydrophobic-P', hydroph.bs_ids, restrict=self.protname) 90 | self.select_by_ids('Hydrophobic-L', hydroph.lig_ids, restrict=self.ligname) 91 | 92 | for i in hydroph.pairs_ids: 93 | cmd.select('tmp_bs', 'id %i & %s' % (i[0], self.protname)) 94 | cmd.select('tmp_lig', 'id %i & %s' % (i[1], self.ligname)) 95 | cmd.distance('Hydrophobic', 'tmp_bs', 'tmp_lig') 96 | if self.object_exists('Hydrophobic'): 97 | cmd.set('dash_gap', 0.5, 'Hydrophobic') 98 | cmd.set('dash_color', 'grey50', 'Hydrophobic') 99 | else: 100 | cmd.select('Hydrophobic-P', 'None') 101 | 102 | def show_hbonds(self): 103 | """Visualizes hydrogen bonds.""" 104 | hbonds = self.plcomplex.hbonds 105 | for group in [['HBondDonor-P', hbonds.prot_don_id], 106 | ['HBondAccept-P', hbonds.prot_acc_id]]: 107 | if not len(group[1]) == 0: 108 | self.select_by_ids(group[0], group[1], restrict=self.protname) 109 | for group in [['HBondDonor-L', hbonds.lig_don_id], 110 | ['HBondAccept-L', hbonds.lig_acc_id]]: 111 | if not len(group[1]) == 0: 112 | self.select_by_ids(group[0], group[1], restrict=self.ligname) 113 | for i in hbonds.ldon_id: 114 | cmd.select('tmp_bs', 'id %i & %s' % (i[0], self.protname)) 115 | cmd.select('tmp_lig', 'id %i & %s' % (i[1], self.ligname)) 116 | cmd.distance('HBonds', 'tmp_bs', 'tmp_lig') 117 | for i in hbonds.pdon_id: 118 | cmd.select('tmp_bs', 'id %i & %s' % (i[1], self.protname)) 119 | cmd.select('tmp_lig', 'id %i & %s' % (i[0], self.ligname)) 120 | cmd.distance('HBonds', 'tmp_bs', 'tmp_lig') 121 | if self.object_exists('HBonds'): 122 | cmd.set('dash_color', 'blue', 'HBonds') 123 | 124 | def show_halogen(self): 125 | """Visualize halogen bonds.""" 126 | halogen = self.plcomplex.halogen_bonds 127 | all_don_x, all_acc_o = [], [] 128 | for h in halogen: 129 | all_don_x.append(h.don_id) 130 | all_acc_o.append(h.acc_id) 131 | cmd.select('tmp_bs', 'id %i & %s' % (h.acc_id, self.protname)) 132 | cmd.select('tmp_lig', 'id %i & %s' % (h.don_id, self.ligname)) 133 | 134 | cmd.distance('HalogenBonds', 'tmp_bs', 'tmp_lig') 135 | if not len(all_acc_o) == 0: 136 | self.select_by_ids('HalogenAccept', all_acc_o, restrict=self.protname) 137 | self.select_by_ids('HalogenDonor', all_don_x, restrict=self.ligname) 138 | if self.object_exists('HalogenBonds'): 139 | cmd.set('dash_color', 'greencyan', 'HalogenBonds') 140 | 141 | def show_stacking(self): 142 | """Visualize pi-stacking interactions.""" 143 | stacks = self.plcomplex.pistacking 144 | for i, stack in enumerate(stacks): 145 | pires_ids = '+'.join(map(str, stack.proteinring_atoms)) 146 | pilig_ids = '+'.join(map(str, stack.ligandring_atoms)) 147 | cmd.select('StackRings-P', 'StackRings-P or (id %s & %s)' % (pires_ids, self.protname)) 148 | cmd.select('StackRings-L', 'StackRings-L or (id %s & %s)' % (pilig_ids, self.ligname)) 149 | cmd.select('StackRings-P', 'byres StackRings-P') 150 | cmd.show('sticks', 'StackRings-P') 151 | 152 | cmd.pseudoatom('ps-pistack-1-%i' % i, pos=stack.proteinring_center) 153 | cmd.pseudoatom('ps-pistack-2-%i' % i, pos=stack.ligandring_center) 154 | cmd.pseudoatom('Centroids-P', pos=stack.proteinring_center) 155 | cmd.pseudoatom('Centroids-L', pos=stack.ligandring_center) 156 | 157 | if stack.type == 'P': 158 | cmd.distance('PiStackingP', 'ps-pistack-1-%i' % i, 'ps-pistack-2-%i' % i) 159 | if stack.type == 'T': 160 | cmd.distance('PiStackingT', 'ps-pistack-1-%i' % i, 'ps-pistack-2-%i' % i) 161 | if self.object_exists('PiStackingP'): 162 | cmd.set('dash_color', 'green', 'PiStackingP') 163 | cmd.set('dash_gap', 0.3, 'PiStackingP') 164 | cmd.set('dash_length', 0.6, 'PiStackingP') 165 | if self.object_exists('PiStackingT'): 166 | cmd.set('dash_color', 'smudge', 'PiStackingT') 167 | cmd.set('dash_gap', 0.3, 'PiStackingT') 168 | cmd.set('dash_length', 0.6, 'PiStackingT') 169 | 170 | def show_cationpi(self): 171 | """Visualize cation-pi interactions.""" 172 | for i, p in enumerate(self.plcomplex.pication): 173 | cmd.pseudoatom('ps-picat-1-%i' % i, pos=p.ring_center) 174 | cmd.pseudoatom('ps-picat-2-%i' % i, pos=p.charge_center) 175 | if p.protcharged: 176 | cmd.pseudoatom('Chargecenter-P', pos=p.charge_center) 177 | cmd.pseudoatom('Centroids-L', pos=p.ring_center) 178 | pilig_ids = '+'.join(map(str, p.ring_atoms)) 179 | cmd.select('PiCatRing-L', 'PiCatRing-L or (id %s & %s)' % (pilig_ids, self.ligname)) 180 | for a in p.charge_atoms: 181 | cmd.select('PosCharge-P', 'PosCharge-P or (id %i & %s)' % (a, self.protname)) 182 | else: 183 | cmd.pseudoatom('Chargecenter-L', pos=p.charge_center) 184 | cmd.pseudoatom('Centroids-P', pos=p.ring_center) 185 | pires_ids = '+'.join(map(str, p.ring_atoms)) 186 | cmd.select('PiCatRing-P', 'PiCatRing-P or (id %s & %s)' % (pires_ids, self.protname)) 187 | for a in p.charge_atoms: 188 | cmd.select('PosCharge-L', 'PosCharge-L or (id %i & %s)' % (a, self.ligname)) 189 | cmd.distance('PiCation', 'ps-picat-1-%i' % i, 'ps-picat-2-%i' % i) 190 | if self.object_exists('PiCation'): 191 | cmd.set('dash_color', 'orange', 'PiCation') 192 | cmd.set('dash_gap', 0.3, 'PiCation') 193 | cmd.set('dash_length', 0.6, 'PiCation') 194 | 195 | def show_sbridges(self): 196 | """Visualize salt bridges.""" 197 | for i, saltb in enumerate(self.plcomplex.saltbridges): 198 | if saltb.protispos: 199 | for patom in saltb.positive_atoms: 200 | cmd.select('PosCharge-P', 'PosCharge-P or (id %i & %s)' % (patom, self.protname)) 201 | for latom in saltb.negative_atoms: 202 | cmd.select('NegCharge-L', 'NegCharge-L or (id %i & %s)' % (latom, self.ligname)) 203 | for sbgroup in [['ps-sbl-1-%i' % i, 'Chargecenter-P', saltb.positive_center], 204 | ['ps-sbl-2-%i' % i, 'Chargecenter-L', saltb.negative_center]]: 205 | cmd.pseudoatom(sbgroup[0], pos=sbgroup[2]) 206 | cmd.pseudoatom(sbgroup[1], pos=sbgroup[2]) 207 | cmd.distance('Saltbridges', 'ps-sbl-1-%i' % i, 'ps-sbl-2-%i' % i) 208 | else: 209 | for patom in saltb.negative_atoms: 210 | cmd.select('NegCharge-P', 'NegCharge-P or (id %i & %s)' % (patom, self.protname)) 211 | for latom in saltb.positive_atoms: 212 | cmd.select('PosCharge-L', 'PosCharge-L or (id %i & %s)' % (latom, self.ligname)) 213 | for sbgroup in [['ps-sbp-1-%i' % i, 'Chargecenter-P', saltb.negative_center], 214 | ['ps-sbp-2-%i' % i, 'Chargecenter-L', saltb.positive_center]]: 215 | cmd.pseudoatom(sbgroup[0], pos=sbgroup[2]) 216 | cmd.pseudoatom(sbgroup[1], pos=sbgroup[2]) 217 | cmd.distance('Saltbridges', 'ps-sbp-1-%i' % i, 'ps-sbp-2-%i' % i) 218 | 219 | if self.object_exists('Saltbridges'): 220 | cmd.set('dash_color', 'yellow', 'Saltbridges') 221 | cmd.set('dash_gap', 0.5, 'Saltbridges') 222 | 223 | def show_wbridges(self): 224 | """Visualize water bridges.""" 225 | for bridge in self.plcomplex.waterbridges: 226 | if bridge.protisdon: 227 | cmd.select('HBondDonor-P', 'HBondDonor-P or (id %i & %s)' % (bridge.don_id, self.protname)) 228 | cmd.select('HBondAccept-L', 'HBondAccept-L or (id %i & %s)' % (bridge.acc_id, self.ligname)) 229 | cmd.select('tmp_don', 'id %i & %s' % (bridge.don_id, self.protname)) 230 | cmd.select('tmp_acc', 'id %i & %s' % (bridge.acc_id, self.ligname)) 231 | else: 232 | cmd.select('HBondDonor-L', 'HBondDonor-L or (id %i & %s)' % (bridge.don_id, self.ligname)) 233 | cmd.select('HBondAccept-P', 'HBondAccept-P or (id %i & %s)' % (bridge.acc_id, self.protname)) 234 | cmd.select('tmp_don', 'id %i & %s' % (bridge.don_id, self.ligname)) 235 | cmd.select('tmp_acc', 'id %i & %s' % (bridge.acc_id, self.protname)) 236 | cmd.select('Water', 'Water or (id %i & resn HOH)' % bridge.water_id) 237 | cmd.select('tmp_water', 'id %i & resn HOH' % bridge.water_id) 238 | cmd.distance('WaterBridges', 'tmp_acc', 'tmp_water') 239 | cmd.distance('WaterBridges', 'tmp_don', 'tmp_water') 240 | if self.object_exists('WaterBridges'): 241 | cmd.set('dash_color', 'lightblue', 'WaterBridges') 242 | cmd.delete('tmp_water or tmp_acc or tmp_don') 243 | cmd.color('lightblue', 'Water') 244 | cmd.show('spheres', 'Water') 245 | 246 | def show_metal(self): 247 | """Visualize metal coordination.""" 248 | metal_complexes = self.plcomplex.metal_complexes 249 | if not len(metal_complexes) == 0: 250 | self.select_by_ids('Metal-M', self.metal_ids) 251 | for metal_complex in metal_complexes: 252 | cmd.select('tmp_m', 'id %i' % metal_complex.metal_id) 253 | cmd.select('tmp_t', 'id %i' % metal_complex.target_id) 254 | if metal_complex.location == 'water': 255 | cmd.select('Metal-W', 'Metal-W or id %s' % metal_complex.target_id) 256 | if metal_complex.location.startswith('protein'): 257 | cmd.select('tmp_t', 'tmp_t & %s' % self.protname) 258 | cmd.select('Metal-P', 'Metal-P or (id %s & %s)' % (metal_complex.target_id, self.protname)) 259 | if metal_complex.location == 'ligand': 260 | cmd.select('tmp_t', 'tmp_t & %s' % self.ligname) 261 | cmd.select('Metal-L', 'Metal-L or (id %s & %s)' % (metal_complex.target_id, self.ligname)) 262 | cmd.distance('MetalComplexes', 'tmp_m', 'tmp_t') 263 | cmd.delete('tmp_m or tmp_t') 264 | if self.object_exists('MetalComplexes'): 265 | cmd.set('dash_color', 'violetpurple', 'MetalComplexes') 266 | cmd.set('dash_gap', 0.5, 'MetalComplexes') 267 | # Show water molecules for metal complexes 268 | cmd.show('spheres', 'Metal-W') 269 | cmd.color('lightblue', 'Metal-W') 270 | 271 | def selections_cleanup(self): 272 | """Cleans up non-used selections""" 273 | 274 | if not len(self.plcomplex.unpaired_hba_idx) == 0: 275 | self.select_by_ids('Unpaired-HBA', self.plcomplex.unpaired_hba_idx, selection_exists=True) 276 | if not len(self.plcomplex.unpaired_hbd_idx) == 0: 277 | self.select_by_ids('Unpaired-HBD', self.plcomplex.unpaired_hbd_idx, selection_exists=True) 278 | if not len(self.plcomplex.unpaired_hal_idx) == 0: 279 | self.select_by_ids('Unpaired-HAL', self.plcomplex.unpaired_hal_idx, selection_exists=True) 280 | 281 | selections = cmd.get_names("selections") 282 | for selection in selections: 283 | try: 284 | empty = len(cmd.get_model(selection).atom) == 0 285 | except: 286 | empty = True 287 | if empty: 288 | cmd.delete(selection) 289 | cmd.deselect() 290 | cmd.delete('tmp*') 291 | cmd.delete('ps-*') 292 | 293 | def selections_group(self): 294 | """Group all selections""" 295 | cmd.group('Structures', '%s %s %sCartoon' % (self.protname, self.ligname, self.protname)) 296 | cmd.group('Interactions', 'Hydrophobic HBonds HalogenBonds WaterBridges PiCation PiStackingP PiStackingT ' 297 | 'Saltbridges MetalComplexes') 298 | cmd.group('Atoms', '') 299 | cmd.group('Atoms.Protein', 'Hydrophobic-P HBondAccept-P HBondDonor-P HalogenAccept Centroids-P PiCatRing-P ' 300 | 'StackRings-P PosCharge-P NegCharge-P AllBSRes Chargecenter-P Metal-P') 301 | cmd.group('Atoms.Ligand', 'Hydrophobic-L HBondAccept-L HBondDonor-L HalogenDonor Centroids-L NegCharge-L ' 302 | 'PosCharge-L NegCharge-L ChargeCenter-L StackRings-L PiCatRing-L Metal-L Metal-M ' 303 | 'Unpaired-HBA Unpaired-HBD Unpaired-HAL Unpaired-RINGS') 304 | cmd.group('Atoms.Other', 'Water Metal-W') 305 | cmd.order('*', 'y') 306 | 307 | def additional_cleanup(self): 308 | """Cleanup of various representations""" 309 | 310 | cmd.remove('not alt ""+A') # Remove alternate conformations 311 | cmd.hide('labels', 'Interactions') # Hide labels of lines 312 | cmd.disable('%sCartoon' % self.protname) 313 | cmd.hide('everything', 'hydrogens') 314 | 315 | def zoom_to_ligand(self): 316 | """Zoom in too ligand and its interactions.""" 317 | cmd.center(self.ligname) 318 | cmd.orient(self.ligname) 319 | cmd.turn('x', 110) # If the ligand is aligned with the longest axis, aromatic rings are hidden 320 | if 'AllBSRes' in cmd.get_names("selections"): 321 | cmd.zoom('%s or AllBSRes' % self.ligname, 3) 322 | else: 323 | if self.object_exists(self.ligname): 324 | cmd.zoom(self.ligname, 3) 325 | cmd.origin(self.ligname) 326 | 327 | def save_session(self, outfolder, override=None): 328 | """Saves a PyMOL session file.""" 329 | filename = '%s_%s' % (self.protname.upper(), "_".join( 330 | [self.hetid, self.plcomplex.chain, self.plcomplex.position])) 331 | if override is not None: 332 | filename = override 333 | cmd.save("/".join([outfolder, "%s.pse" % filename])) 334 | 335 | @staticmethod 336 | def png_workaround(filepath, width=1200, height=800): 337 | """Workaround for (a) severe bug(s) in PyMOL preventing ray-traced images to be produced in command-line mode. 338 | Use this function in case neither cmd.ray() or cmd.png() work. 339 | """ 340 | sys.stdout = sys.__stdout__ 341 | cmd.feedback('disable', 'movie', 'everything') 342 | cmd.viewport(width, height) 343 | cmd.zoom('visible', 1.5) # Adapt the zoom to the viewport 344 | cmd.set('ray_trace_frames', 1) # Frames are raytraced before saving an image. 345 | cmd.mpng(filepath, config.MODEL, config.MODEL) # Use batch png mode with 1 frame only 346 | cmd.mplay() # cmd.mpng needs the animation to 'run' 347 | cmd.refresh() 348 | originalfile = "".join( 349 | [filepath, (4 - len(str(config.MODEL))) * '0' + str(config.MODEL) + '.png']) 350 | newfile = "".join([filepath, '.png']) 351 | 352 | ################################################# 353 | # Wait for file for max. 1 second and rename it # 354 | ################################################# 355 | 356 | attempts = 0 357 | while not os.path.isfile(originalfile) and attempts <= 10: 358 | sleep(0.1) 359 | attempts += 1 360 | if os.name == 'nt': # In Windows, make sure there is no file of the same name, cannot be overwritten as in Unix 361 | if os.path.isfile(newfile): 362 | os.remove(newfile) 363 | os.rename(originalfile, newfile) # Remove frame number in filename 364 | 365 | # Check if imagemagick is available and crop + resize the images 366 | if subprocess.call("magick -version", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0: 367 | attempts, ecode = 0, 1 368 | # Check if file is truncated and wait if that's the case 369 | while ecode != 0 and attempts <= 10: 370 | ecode = subprocess.call(['magick', newfile, os.devnull], stdout=open(os.devnull, 'w'), 371 | stderr=subprocess.STDOUT) 372 | sleep(0.1) 373 | attempts += 1 374 | trim = f'magick {newfile} -trim -bordercolor White -border 20x20 {newfile}' # Trim the image 375 | os.system(trim) 376 | # Get the width of the new image 377 | getwidth = f'magick {newfile} -ping -format "%w" info:' 378 | w = int(subprocess.run(getwidth, capture_output=True, text=True).stdout) 379 | # Get the hight of the new image 380 | getheight = f'magick {newfile} -ping -format "%h" info:' 381 | h = int(subprocess.run(getheight, capture_output=True, text=True).stdout) 382 | # Set quadratic ratio 383 | if w > h: 384 | newr= f'{w}x{w}' 385 | else: 386 | newr= f'{h}x{h}' 387 | quadratic = f'magick {newfile} -gravity center -extent {newr} {newfile}' # Fill with whitespace 388 | os.system(quadratic) 389 | else: 390 | sys.stderr.write('Imagemagick not available. Images will not be resized or cropped.') 391 | 392 | def save_picture(self, outfolder, filename): 393 | """Saves a picture""" 394 | self.set_fancy_ray() 395 | self.png_workaround("/".join([outfolder, filename])) 396 | 397 | @staticmethod 398 | def set_fancy_ray(): 399 | """Give the molecule a flat, modern look.""" 400 | cmd.set('light_count', 6) 401 | cmd.set('spec_count', 1.5) 402 | cmd.set('shininess', 4) 403 | cmd.set('specular', 0.3) 404 | cmd.set('reflect', 1.6) 405 | cmd.set('ambient', 0) 406 | cmd.set('direct', 0) 407 | cmd.set('ray_shadow', 0) # Gives the molecules a flat, modern look 408 | cmd.set('ambient_occlusion_mode', 1) 409 | cmd.set('ray_opaque_background', 0) # Transparent background 410 | 411 | def adapt_for_peptides(self): 412 | """Adapt visualization for peptide ligands and interchain contacts""" 413 | cmd.hide('sticks', self.ligname) 414 | cmd.set('cartoon_color', 'lightorange', self.ligname) 415 | cmd.show('cartoon', self.ligname) 416 | cmd.show('sticks', "byres *-L") 417 | cmd.util.cnc(self.ligname) 418 | cmd.remove('%sCartoon and chain %s' % (self.protname, self.plcomplex.chain)) 419 | cmd.set('cartoon_side_chain_helper', 0) 420 | 421 | def adapt_for_intra(self): 422 | """Adapt visualization for intra-protein interactions""" 423 | 424 | def refinements(self): 425 | """Refinements for the visualization""" 426 | 427 | # Show sticks for all residues interacing with the ligand 428 | cmd.select('AllBSRes', 'byres (Hydrophobic-P or HBondDonor-P or HBondAccept-P or PosCharge-P or NegCharge-P or ' 429 | 'StackRings-P or PiCatRing-P or HalogenAcc or Metal-P)') 430 | cmd.show('sticks', 'AllBSRes') 431 | # Show spheres for the ring centroids 432 | cmd.hide('everything', 'centroids*') 433 | cmd.show('nb_spheres', 'centroids*') 434 | # Show spheres for centers of charge 435 | if self.object_exists('Chargecenter-P') or self.object_exists('Chargecenter-L'): 436 | cmd.hide('nonbonded', 'chargecenter*') 437 | cmd.show('spheres', 'chargecenter*') 438 | cmd.set('sphere_scale', 0.4, 'chargecenter*') 439 | cmd.color('yellow', 'chargecenter*') 440 | 441 | cmd.set('valence', 1) # Show bond valency (e.g. double bonds) 442 | # Optional cartoon representation of the protein 443 | cmd.copy('%sCartoon' % self.protname, self.protname) 444 | cmd.show('cartoon', '%sCartoon' % self.protname) 445 | cmd.show('sticks', '%sCartoon' % self.protname) 446 | cmd.set('stick_transparency', 1, '%sCartoon' % self.protname) 447 | 448 | # Resize water molecules. Sometimes they are not heteroatoms HOH, but part of the protein 449 | cmd.set('sphere_scale', 0.2, 'resn HOH or Water') # Needs to be done here because of the copy made 450 | cmd.set('sphere_transparency', 0.4, '!(resn HOH or Water)') 451 | 452 | if 'Centroids*' in cmd.get_names("selections"): 453 | cmd.color('grey80', 'Centroids*') 454 | cmd.hide('spheres', '%sCartoon' % self.protname) 455 | cmd.hide('cartoon', '%sCartoon and resn DA+DG+DC+DU+DT+A+G+C+U+T' % self.protname) # Hide DNA/RNA Cartoon 456 | if self.ligname == 'SF4': # Special case for iron-sulfur clusters, can't be visualized with sticks 457 | cmd.show('spheres', '%s' % self.ligname) 458 | 459 | cmd.hide('everything', 'resn HOH &!Water') # Hide all non-interacting water molecules 460 | cmd.hide('sticks', '%s and !%s and !AllBSRes' % 461 | (self.protname, self.ligname)) # Hide all non-interacting residues 462 | 463 | if self.ligandtype in ['PEPTIDE', 'INTRA']: 464 | self.adapt_for_peptides() 465 | 466 | if self.ligandtype == 'INTRA': 467 | self.adapt_for_intra() 468 | -------------------------------------------------------------------------------- /plip/visualization/visualize.py: -------------------------------------------------------------------------------- 1 | from pymol import cmd 2 | 3 | from plip.basic import config, logger 4 | from plip.basic.supplemental import start_pymol 5 | from plip.visualization.pymol import PyMOLVisualizer 6 | 7 | logger = logger.get_logger() 8 | 9 | 10 | def visualize_in_pymol(plcomplex): 11 | """Visualizes the given Protein-Ligand complex at one site in PyMOL.""" 12 | 13 | vis = PyMOLVisualizer(plcomplex) 14 | 15 | ##################### 16 | # Set everything up # 17 | ##################### 18 | 19 | pdbid = plcomplex.pdbid 20 | lig_members = plcomplex.lig_members 21 | chain = plcomplex.chain 22 | if config.PEPTIDES or config.CHAINS: 23 | vis.ligname = 'PeptideChain%s' % plcomplex.chain 24 | if config.INTRA is not None: 25 | vis.ligname = 'Intra%s' % plcomplex.chain 26 | 27 | ligname = vis.ligname 28 | hetid = plcomplex.hetid 29 | 30 | metal_ids = plcomplex.metal_ids 31 | metal_ids_str = '+'.join([str(i) for i in metal_ids]) 32 | 33 | ######################## 34 | # Basic visualizations # 35 | ######################## 36 | 37 | start_pymol(run=True, options='-pcq', quiet=not config.VERBOSE and not config.SILENT) 38 | vis.set_initial_representations() 39 | 40 | cmd.load(plcomplex.sourcefile) 41 | cmd.frame(config.MODEL) 42 | current_name = cmd.get_object_list(selection='(all)')[0] 43 | 44 | logger.debug(f'setting current_name to {current_name} and PDB-ID to {pdbid}') 45 | cmd.set_name(current_name, pdbid) 46 | cmd.hide('everything', 'all') 47 | if config.PEPTIDES: 48 | if plcomplex.chain in config.RESIDUES.keys(): 49 | cmd.select(ligname, 'chain %s and not resn HOH and resi %s' % (plcomplex.chain, "+".join(map(str, config.RESIDUES[plcomplex.chain])))) 50 | else: 51 | cmd.select(ligname, 'chain %s and not resn HOH' % plcomplex.chain) 52 | else: 53 | cmd.select(ligname, 'resn %s and chain %s and resi %s*' % (hetid, chain, plcomplex.position)) 54 | logger.debug(f'selecting ligand for PDBID {pdbid} and ligand name {ligname}') 55 | logger.debug(f'resn {hetid} and chain {chain} and resi {plcomplex.position}') 56 | 57 | # Visualize and color metal ions if there are any 58 | if not len(metal_ids) == 0: 59 | vis.select_by_ids(ligname, metal_ids, selection_exists=True) 60 | cmd.show('spheres', 'id %s and %s' % (metal_ids_str, pdbid)) 61 | 62 | # Additionally, select all members of composite ligands 63 | if len(lig_members) > 1: 64 | for member in lig_members: 65 | resid, chain, resnr = member[0], member[1], str(member[2]) 66 | cmd.select(ligname, '%s or (resn %s and chain %s and resi %s)' % (ligname, resid, chain, resnr)) 67 | 68 | cmd.show('sticks', ligname) 69 | cmd.color('myblue') 70 | cmd.color('myorange', ligname) 71 | cmd.util.cnc('all') 72 | if not len(metal_ids) == 0: 73 | cmd.color('hotpink', 'id %s' % metal_ids_str) 74 | cmd.hide('sticks', 'id %s' % metal_ids_str) 75 | cmd.set('sphere_scale', 0.3, ligname) 76 | cmd.deselect() 77 | 78 | vis.make_initial_selections() 79 | 80 | vis.show_hydrophobic() # Hydrophobic Contacts 81 | vis.show_hbonds() # Hydrogen Bonds 82 | vis.show_halogen() # Halogen Bonds 83 | vis.show_stacking() # pi-Stacking Interactions 84 | vis.show_cationpi() # pi-Cation Interactions 85 | vis.show_sbridges() # Salt Bridges 86 | vis.show_wbridges() # Water Bridges 87 | vis.show_metal() # Metal Coordination 88 | 89 | vis.refinements() 90 | 91 | vis.zoom_to_ligand() 92 | 93 | vis.selections_cleanup() 94 | 95 | vis.selections_group() 96 | vis.additional_cleanup() 97 | if config.DNARECEPTOR: 98 | # Rename Cartoon selection to Line selection and change repr. 99 | cmd.set_name('%sCartoon' % plcomplex.pdbid, '%sLines' % plcomplex.pdbid) 100 | cmd.hide('cartoon', '%sLines' % plcomplex.pdbid) 101 | cmd.show('lines', '%sLines' % plcomplex.pdbid) 102 | 103 | if config.PEPTIDES or config.CHAINS: 104 | filename = "%s_PeptideChain%s" % (pdbid.upper(), plcomplex.chain) 105 | if config.PYMOL: 106 | vis.save_session(config.OUTPATH, override=filename) 107 | elif config.INTRA is not None: 108 | filename = "%s_IntraChain%s" % (pdbid.upper(), plcomplex.chain) 109 | if config.PYMOL: 110 | vis.save_session(config.OUTPATH, override=filename) 111 | else: 112 | filename = '%s_%s' % (pdbid.upper(), "_".join([hetid, plcomplex.chain, plcomplex.position])) 113 | if config.PYMOL: 114 | vis.save_session(config.OUTPATH) 115 | if config.PICS: 116 | vis.save_picture(config.OUTPATH, filename) 117 | -------------------------------------------------------------------------------- /pliplogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharmai/plip/e4181b2c40fe5938b5f39aaf00ae3ea9d14ac9cd/pliplogo.png -------------------------------------------------------------------------------- /pliplogo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 44 | 109 | 110 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml~=4.2.1 2 | numpy~=1.13.3 3 | pymol~=2.3.0 4 | openbabel~=3.0.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from setuptools.command.build_ext import build_ext 3 | from setuptools.command.install import install 4 | from distutils.command.build import build 5 | 6 | from plip.basic import config 7 | 8 | def install_pkg_via_pip(package): 9 | import sys 10 | import subprocess 11 | subprocess.check_call([sys.executable, '-m', 'pip', 'install', package]) 12 | 13 | def build_ob_py_bindings(): 14 | """ Fix openbabel issue (https://github.com/openbabel/openbabel/issues/2408) manually 15 | - Download openbabel bindings from pypi 16 | - extract to a tmpdir 17 | - fix version in $tmpdir/openbabel-3.1.1.1/openbabel/__init__.py to have 2 dot version only 18 | - install the fixed version with setuptools/pip 19 | - cleanup the tmpdir 20 | """ 21 | import requests 22 | import tarfile 23 | import tempfile 24 | import shutil 25 | import fileinput 26 | import subprocess 27 | import sys 28 | 29 | openbabel_pypi_url='https://files.pythonhosted.org/packages/9d/3f/f08f5d1422d74ed0e1e612870b343bfcc26313bdf9efec9165c3ea4b3ae2/openbabel-3.1.1.1.tar.gz' 30 | 31 | print (f"Downloading openbabel package from : {openbabel_pypi_url}") 32 | obtar=requests.get(openbabel_pypi_url) 33 | obtmpdir = tempfile.mkdtemp() 34 | obtmp = obtmpdir+'/openbabel-3.1.1.1.tar' 35 | open(obtmp,'wb').write(obtar.content) 36 | print(f"Saving openbabel tar.gz to {obtmpdir}") 37 | versfile = obtmpdir+'/openbabel-3.1.1.1/openbabel/__init__.py' 38 | 39 | with tarfile.open(obtmp,mode='r') as tf: 40 | tf.extractall(obtmpdir) 41 | 42 | print ('Fix versions: s/3.1.1.1/3.1.1/ to make StrictVersion() in openbabel\'s setup.py happy') 43 | print ('See https://github.com/openbabel/openbabel/issues/2408 for more details') 44 | with fileinput.input(files=versfile,inplace=True) as f: 45 | for line in f: 46 | op = line.replace('__version__ = "3.1.1.1"', '__version__ = "3.1.1"') 47 | print(op, end='') 48 | 49 | install_pkg_via_pip(obtmpdir+'/openbabel-3.1.1.1/') 50 | print (f"Cleanup tmpdir: {obtmpdir}") 51 | shutil.rmtree(obtmpdir) 52 | 53 | class CustomBuild(build): 54 | """Ensure build_ext runs first in build command.""" 55 | def run(self): 56 | self.run_command('build_ext') 57 | build.run(self) 58 | 59 | class CustomInstall(install): 60 | """Ensure build_ext runs first in install command.""" 61 | def run(self): 62 | self.run_command('build_ext') 63 | install.run(self) 64 | 65 | class CustomBuildExt(build_ext): 66 | """ Check if openbabel bindings are installed || build them """ 67 | def run(self): 68 | try: import openbabel 69 | except ModuleNotFoundError: 70 | try: import requests 71 | except ModuleNotFoundError: 72 | install_pkg_via_pip('requests') 73 | build_ob_py_bindings() 74 | return 75 | 76 | setup(name='plip', 77 | version=config.__version__, 78 | description='PLIP - Fully automated protein-ligand interaction profiler', 79 | classifiers=[ 80 | 'Development Status :: 5 - Production/Stable', 81 | 'Intended Audience :: Science/Research', 82 | 'Natural Language :: English', 83 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 84 | 'Programming Language :: Python :: 3.6', 85 | 'Topic :: Scientific/Engineering :: Bio-Informatics' 86 | ], 87 | url='https://github.com/pharmai/plip', 88 | author='PharmAI GmbH', 89 | author_email='hello@pharm.ai', 90 | license='GPLv2', 91 | packages=find_packages(), 92 | scripts=['plip/plipcmd.py'], 93 | cmdclass={'build': CustomBuild, 'build_ext': CustomBuildExt, 'install': CustomInstall}, 94 | install_requires=[ 95 | 'numpy', 96 | 'lxml' 97 | ], 98 | entry_points={ 99 | "console_scripts": [ 100 | "plip = plip.plipcmd:main" 101 | ] 102 | }, 103 | zip_safe=False) 104 | --------------------------------------------------------------------------------