pandamap 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pandamap might be problematic. Click here for more details.
- pandamap-1.0.0/LICENSE +21 -0
- pandamap-1.0.0/PKG-INFO +115 -0
- pandamap-1.0.0/README.md +71 -0
- pandamap-1.0.0/pyproject.toml +35 -0
- pandamap-1.0.0/setup.cfg +4 -0
- pandamap-1.0.0/setup.py +8 -0
- pandamap-1.0.0/src/pandamap/__init__.py +12 -0
- pandamap-1.0.0/src/pandamap/cli.py +48 -0
- pandamap-1.0.0/src/pandamap/core.py +766 -0
- pandamap-1.0.0/src/pandamap.egg-info/PKG-INFO +115 -0
- pandamap-1.0.0/src/pandamap.egg-info/SOURCES.txt +13 -0
- pandamap-1.0.0/src/pandamap.egg-info/dependency_links.txt +1 -0
- pandamap-1.0.0/src/pandamap.egg-info/entry_points.txt +2 -0
- pandamap-1.0.0/src/pandamap.egg-info/requires.txt +3 -0
- pandamap-1.0.0/src/pandamap.egg-info/top_level.txt +1 -0
pandamap-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Pritam Kumar Panda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pandamap-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pandamap
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python package for visualizing protein-ligand interactions with 2D ligand structure representation
|
|
5
|
+
Author-email: Your Name <your.email@example.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Pritam Kumar Panda
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/yourusername/pandamap
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/yourusername/pandamap/issues
|
|
30
|
+
Keywords: bioinformatics,protein-ligand,interactions,visualization,drug-design
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
35
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
37
|
+
Requires-Python: >=3.7
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: numpy>=1.20.0
|
|
41
|
+
Requires-Dist: matplotlib>=3.4.0
|
|
42
|
+
Requires-Dist: biopython>=1.79
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# PandaMap
|
|
46
|
+
|
|
47
|
+
**P**rotein **AND** lig**A**nd interaction **MAP**per: A Python package for visualizing protein-ligand interactions with 2D ligand structure representation
|
|
48
|
+
|
|
49
|
+
## Overview
|
|
50
|
+
|
|
51
|
+
PandaMap is a lightweight tool for visualizing protein-ligand interactions from PDB files. It generates intuitive 2D interaction diagrams that display both the ligand structure and its interactions with protein residues.
|
|
52
|
+
|
|
53
|
+
Key features:
|
|
54
|
+
- Visualization of protein-ligand interactions with minimal dependencies
|
|
55
|
+
- 2D representation of ligand structure without requiring RDKit
|
|
56
|
+
- Detection of multiple interaction types (hydrogen bonds, π-stacking, hydrophobic)
|
|
57
|
+
- Command-line interface for quick analysis
|
|
58
|
+
- Python API for integration into computational workflows
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install pandamap
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Dependencies
|
|
67
|
+
|
|
68
|
+
- NumPy
|
|
69
|
+
- Matplotlib
|
|
70
|
+
- BioPython
|
|
71
|
+
|
|
72
|
+
## Basic Usage
|
|
73
|
+
|
|
74
|
+
### Command Line Interface
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Basic usage
|
|
78
|
+
pandamap protein_ligand.pdb --output interactions.png
|
|
79
|
+
|
|
80
|
+
# Specify a particular ligand by residue name
|
|
81
|
+
pandamap protein_ligand.pdb --ligand LIG
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Python API
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from pandamap import HybridProtLigMapper
|
|
88
|
+
|
|
89
|
+
# Initialize with PDB file
|
|
90
|
+
mapper = HybridProtLigMapper("protein_ligand.pdb", ligand_resname="LIG")
|
|
91
|
+
|
|
92
|
+
# Run analysis and generate visualization
|
|
93
|
+
output_file = mapper.run_analysis(output_file="interactions.png")
|
|
94
|
+
|
|
95
|
+
# Or run steps separately
|
|
96
|
+
mapper.detect_interactions()
|
|
97
|
+
mapper.estimate_solvent_accessibility()
|
|
98
|
+
mapper.visualize(output_file="interactions.png")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Example Output
|
|
102
|
+
|
|
103
|
+

|
|
104
|
+
|
|
105
|
+
## Citation
|
|
106
|
+
|
|
107
|
+
If you use PandaMap in your research, please cite:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Pritam Kumar Panda. (2025). **P**rotein **AND** lig**A**nd interaction **MAP**per: A Python package for visualizing protein-ligand interactions with 2D ligand structure representation. GitHub repository. https://github.com/pritampanda15/PandaMap
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
pandamap-1.0.0/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# PandaMap
|
|
2
|
+
|
|
3
|
+
**P**rotein **AND** lig**A**nd interaction **MAP**per: A Python package for visualizing protein-ligand interactions with 2D ligand structure representation
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
PandaMap is a lightweight tool for visualizing protein-ligand interactions from PDB files. It generates intuitive 2D interaction diagrams that display both the ligand structure and its interactions with protein residues.
|
|
8
|
+
|
|
9
|
+
Key features:
|
|
10
|
+
- Visualization of protein-ligand interactions with minimal dependencies
|
|
11
|
+
- 2D representation of ligand structure without requiring RDKit
|
|
12
|
+
- Detection of multiple interaction types (hydrogen bonds, π-stacking, hydrophobic)
|
|
13
|
+
- Command-line interface for quick analysis
|
|
14
|
+
- Python API for integration into computational workflows
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install pandamap
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Dependencies
|
|
23
|
+
|
|
24
|
+
- NumPy
|
|
25
|
+
- Matplotlib
|
|
26
|
+
- BioPython
|
|
27
|
+
|
|
28
|
+
## Basic Usage
|
|
29
|
+
|
|
30
|
+
### Command Line Interface
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Basic usage
|
|
34
|
+
pandamap protein_ligand.pdb --output interactions.png
|
|
35
|
+
|
|
36
|
+
# Specify a particular ligand by residue name
|
|
37
|
+
pandamap protein_ligand.pdb --ligand LIG
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Python API
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from pandamap import HybridProtLigMapper
|
|
44
|
+
|
|
45
|
+
# Initialize with PDB file
|
|
46
|
+
mapper = HybridProtLigMapper("protein_ligand.pdb", ligand_resname="LIG")
|
|
47
|
+
|
|
48
|
+
# Run analysis and generate visualization
|
|
49
|
+
output_file = mapper.run_analysis(output_file="interactions.png")
|
|
50
|
+
|
|
51
|
+
# Or run steps separately
|
|
52
|
+
mapper.detect_interactions()
|
|
53
|
+
mapper.estimate_solvent_accessibility()
|
|
54
|
+
mapper.visualize(output_file="interactions.png")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Example Output
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
## Citation
|
|
62
|
+
|
|
63
|
+
If you use PandaMap in your research, please cite:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Pritam Kumar Panda. (2025). **P**rotein **AND** lig**A**nd interaction **MAP**per: A Python package for visualizing protein-ligand interactions with 2D ligand structure representation. GitHub repository. https://github.com/pritampanda15/PandaMap
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pandamap"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A Python package for visualizing protein-ligand interactions with 2D ligand structure representation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Your Name", email = "your.email@example.com"}
|
|
12
|
+
]
|
|
13
|
+
license = {file = "LICENSE"}
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
21
|
+
]
|
|
22
|
+
keywords = ["bioinformatics", "protein-ligand", "interactions", "visualization", "drug-design"]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"numpy>=1.20.0",
|
|
25
|
+
"matplotlib>=3.4.0",
|
|
26
|
+
"biopython>=1.79",
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.7"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
"Homepage" = "https://github.com/yourusername/pandamap"
|
|
32
|
+
"Bug Tracker" = "https://github.com/yourusername/pandamap/issues"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
pandamap = "pandamap.cli:main"
|
pandamap-1.0.0/setup.cfg
ADDED
pandamap-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PandaMap: Protein AND ligAnd interaction MAPper
|
|
3
|
+
|
|
4
|
+
A Python package for visualizing protein-ligand interactions
|
|
5
|
+
with 2D ligand structure representation and minimal external dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
from .core import SimpleLigandStructure, HybridProtLigMapper
|
|
11
|
+
|
|
12
|
+
__all__ = ["SimpleLigandStructure", "HybridProtLigMapper"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Command-line interface for PandaMap.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import argparse
|
|
8
|
+
from pandamap.core import HybridProtLigMapper
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
"""Command-line interface for PandaMap."""
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
description='PandaMap: Visualize protein-ligand interactions from PDB files')
|
|
14
|
+
|
|
15
|
+
parser.add_argument('pdb_file', help='Path to PDB file with protein-ligand complex')
|
|
16
|
+
parser.add_argument('--output', '-o', help='Output image file path')
|
|
17
|
+
parser.add_argument('--ligand', '-l', help='Specific ligand residue name to analyze')
|
|
18
|
+
parser.add_argument('--dpi', type=int, default=300, help='Image resolution (default: 300 dpi)')
|
|
19
|
+
parser.add_argument('--title', '-t', help='Custom title for the visualization')
|
|
20
|
+
parser.add_argument('--version', '-v', action='store_true', help='Show version information')
|
|
21
|
+
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
# Handle version flag
|
|
25
|
+
if args.version:
|
|
26
|
+
from pandamap import __version__
|
|
27
|
+
print(f"PandaMap version {__version__}")
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
# Check for required pdb_file
|
|
31
|
+
if not hasattr(args, 'pdb_file') or args.pdb_file is None:
|
|
32
|
+
parser.print_help()
|
|
33
|
+
return 1
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Initialize and run the analysis
|
|
37
|
+
mapper = HybridProtLigMapper(args.pdb_file, ligand_resname=args.ligand)
|
|
38
|
+
output_file = mapper.run_analysis(output_file=args.output)
|
|
39
|
+
|
|
40
|
+
print(f"Analysis complete. Visualization saved to: {output_file}")
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"Error: {e}")
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
sys.exit(main())
|
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Core functionality for PandaMap: A Python package for visualizing
|
|
4
|
+
protein-ligand interactions with 2D ligand structure representation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import math
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import matplotlib.pyplot as plt
|
|
13
|
+
from matplotlib.patches import Circle, FancyArrowPatch, Wedge, Rectangle, Polygon
|
|
14
|
+
from matplotlib.lines import Line2D
|
|
15
|
+
import matplotlib.patheffects as path_effects
|
|
16
|
+
|
|
17
|
+
# BioPython imports
|
|
18
|
+
from Bio.PDB import PDBParser, NeighborSearch
|
|
19
|
+
|
|
20
|
+
# Define three_to_one conversion manually if import isn't available
|
|
21
|
+
try:
|
|
22
|
+
from Bio.PDB.Polypeptide import three_to_one
|
|
23
|
+
except ImportError:
|
|
24
|
+
# Define the conversion dictionary manually
|
|
25
|
+
_aa_index = {
|
|
26
|
+
'ALA': 'A', 'CYS': 'C', 'ASP': 'D', 'GLU': 'E',
|
|
27
|
+
'PHE': 'F', 'GLY': 'G', 'HIS': 'H', 'ILE': 'I',
|
|
28
|
+
'LYS': 'K', 'LEU': 'L', 'MET': 'M', 'ASN': 'N',
|
|
29
|
+
'PRO': 'P', 'GLN': 'Q', 'ARG': 'R', 'SER': 'S',
|
|
30
|
+
'THR': 'T', 'VAL': 'V', 'TRP': 'W', 'TYR': 'Y'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def three_to_one(residue):
|
|
34
|
+
"""Convert amino acid three letter code to one letter code."""
|
|
35
|
+
if residue in _aa_index:
|
|
36
|
+
return _aa_index[residue]
|
|
37
|
+
else:
|
|
38
|
+
return "X" # Unknown amino acid
|
|
39
|
+
|
|
40
|
+
class SimpleLigandStructure:
|
|
41
|
+
"""
|
|
42
|
+
Class to create a simplified 2D representation of a ligand structure
|
|
43
|
+
without requiring RDKit or other external dependencies.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, ligand_atoms):
|
|
47
|
+
"""
|
|
48
|
+
Initialize with a list of ligand atoms from a BioPython structure.
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
-----------
|
|
52
|
+
ligand_atoms : list
|
|
53
|
+
List of BioPython Atom objects from the ligand
|
|
54
|
+
"""
|
|
55
|
+
self.ligand_atoms = ligand_atoms
|
|
56
|
+
self.atom_coords = {}
|
|
57
|
+
self.element_colors = {
|
|
58
|
+
'C': '#808080', # Grey
|
|
59
|
+
'N': '#0000FF', # Blue
|
|
60
|
+
'O': '#FF0000', # Red
|
|
61
|
+
'S': '#FFFF00', # Yellow
|
|
62
|
+
'P': '#FFA500', # Orange
|
|
63
|
+
'F': '#00FF00', # Green
|
|
64
|
+
'Cl': '#00FF00', # Green
|
|
65
|
+
'Br': '#A52A2A', # Brown
|
|
66
|
+
'I': '#A020F0', # Purple
|
|
67
|
+
'H': '#FFFFFF' # White
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Record atom coordinates and elements
|
|
71
|
+
for atom in ligand_atoms:
|
|
72
|
+
atom_id = atom.get_id()
|
|
73
|
+
self.atom_coords[atom_id] = {
|
|
74
|
+
'element': atom.element,
|
|
75
|
+
'coord': atom.get_coord() # 3D coordinates from PDB
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def generate_2d_coords(self):
|
|
79
|
+
"""
|
|
80
|
+
Generate simplified 2D coordinates for the ligand atoms based on their 3D coordinates.
|
|
81
|
+
This is a very basic projection - in a real application, you would use a proper
|
|
82
|
+
2D layout algorithm.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
--------
|
|
86
|
+
dict : Dictionary mapping atom IDs to 2D coordinates
|
|
87
|
+
"""
|
|
88
|
+
if not self.atom_coords:
|
|
89
|
+
return {}
|
|
90
|
+
|
|
91
|
+
# Simple projection onto the xy-plane
|
|
92
|
+
coords_2d = {}
|
|
93
|
+
|
|
94
|
+
# Get all 3D coordinates and find center
|
|
95
|
+
all_coords = np.array([info['coord'] for info in self.atom_coords.values()])
|
|
96
|
+
center = np.mean(all_coords, axis=0)
|
|
97
|
+
|
|
98
|
+
# Subtract center to center the molecule
|
|
99
|
+
centered_coords = all_coords - center
|
|
100
|
+
|
|
101
|
+
# Simple PCA-like approach to find main plane
|
|
102
|
+
# (This is a very simplified approach)
|
|
103
|
+
cov_matrix = np.cov(centered_coords.T)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
# Get eigenvectors and eigenvalues
|
|
107
|
+
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
|
|
108
|
+
|
|
109
|
+
# Sort by eigenvalue in descending order
|
|
110
|
+
idx = eigenvalues.argsort()[::-1]
|
|
111
|
+
eigenvalues = eigenvalues[idx]
|
|
112
|
+
eigenvectors = eigenvectors[:, idx]
|
|
113
|
+
|
|
114
|
+
# Use the first two eigenvectors to define the plane
|
|
115
|
+
plane_vectors = eigenvectors[:, :2]
|
|
116
|
+
|
|
117
|
+
# Project the centered coordinates onto the plane
|
|
118
|
+
projected_coords = np.dot(centered_coords, plane_vectors)
|
|
119
|
+
|
|
120
|
+
# Scale to fit nicely in the visualization
|
|
121
|
+
max_dim = np.max(np.abs(projected_coords))
|
|
122
|
+
scaling_factor = 50.0 / max_dim if max_dim > 0 else 1.0
|
|
123
|
+
projected_coords *= scaling_factor
|
|
124
|
+
|
|
125
|
+
# Store the 2D coordinates
|
|
126
|
+
for i, atom_id in enumerate(self.atom_coords.keys()):
|
|
127
|
+
coords_2d[atom_id] = projected_coords[i]
|
|
128
|
+
|
|
129
|
+
except np.linalg.LinAlgError:
|
|
130
|
+
# Fallback if eigendecomposition fails
|
|
131
|
+
print("Warning: Could not compute optimal projection. Using simple XY projection.")
|
|
132
|
+
for atom_id, info in self.atom_coords.items():
|
|
133
|
+
# Simple scaling of x, y coordinates
|
|
134
|
+
coords_2d[atom_id] = np.array([info['coord'][0], info['coord'][1]]) * 10.0
|
|
135
|
+
|
|
136
|
+
return coords_2d
|
|
137
|
+
|
|
138
|
+
def find_bonds(self, distance_threshold=2.0):
|
|
139
|
+
"""
|
|
140
|
+
Find bonds between atoms based on distance.
|
|
141
|
+
This is a simplified approach - in reality, you'd use chemical knowledge.
|
|
142
|
+
|
|
143
|
+
Parameters:
|
|
144
|
+
-----------
|
|
145
|
+
distance_threshold : float
|
|
146
|
+
Maximum distance between atoms to be considered bonded (in Angstroms)
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
--------
|
|
150
|
+
list : List of tuples (atom_id1, atom_id2) representing bonds
|
|
151
|
+
"""
|
|
152
|
+
bonds = []
|
|
153
|
+
atom_ids = list(self.atom_coords.keys())
|
|
154
|
+
|
|
155
|
+
for i in range(len(atom_ids)):
|
|
156
|
+
for j in range(i+1, len(atom_ids)):
|
|
157
|
+
atom1_id = atom_ids[i]
|
|
158
|
+
atom2_id = atom_ids[j]
|
|
159
|
+
|
|
160
|
+
coord1 = self.atom_coords[atom1_id]['coord']
|
|
161
|
+
coord2 = self.atom_coords[atom2_id]['coord']
|
|
162
|
+
|
|
163
|
+
# Calculate Euclidean distance
|
|
164
|
+
distance = np.linalg.norm(coord1 - coord2)
|
|
165
|
+
|
|
166
|
+
# If distance is below threshold, consider them bonded
|
|
167
|
+
if distance < distance_threshold:
|
|
168
|
+
bonds.append((atom1_id, atom2_id))
|
|
169
|
+
|
|
170
|
+
return bonds
|
|
171
|
+
|
|
172
|
+
def draw_on_axes(self, ax, center=(0, 0), radius=80):
|
|
173
|
+
"""
|
|
174
|
+
Draw a simplified 2D representation of the ligand on the given axes.
|
|
175
|
+
|
|
176
|
+
Parameters:
|
|
177
|
+
-----------
|
|
178
|
+
ax : matplotlib.axes.Axes
|
|
179
|
+
The axes on which to draw
|
|
180
|
+
center : tuple
|
|
181
|
+
The (x, y) coordinates where the center of the molecule should be
|
|
182
|
+
radius : float
|
|
183
|
+
The approximate radius the molecule should occupy
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
--------
|
|
187
|
+
dict : Dictionary mapping atom IDs to their 2D positions in the plot
|
|
188
|
+
"""
|
|
189
|
+
# Generate 2D coordinates
|
|
190
|
+
coords_2d = self.generate_2d_coords()
|
|
191
|
+
|
|
192
|
+
if not coords_2d:
|
|
193
|
+
# If we couldn't generate coordinates, draw a simple placeholder
|
|
194
|
+
print("Warning: Could not generate ligand coordinates. Drawing placeholder.")
|
|
195
|
+
circle = Circle(center, radius/2, fill=False, edgecolor='black', linestyle='-')
|
|
196
|
+
ax.add_patch(circle)
|
|
197
|
+
ax.text(center[0], center[1], "Ligand", ha='center', va='center')
|
|
198
|
+
return {}
|
|
199
|
+
|
|
200
|
+
# Find bonds
|
|
201
|
+
bonds = self.find_bonds()
|
|
202
|
+
|
|
203
|
+
# Scale coordinates to fit within the specified radius
|
|
204
|
+
all_coords = np.array(list(coords_2d.values()))
|
|
205
|
+
max_extent = np.max(np.abs(all_coords))
|
|
206
|
+
scaling_factor = radius / (max_extent * 1.2) # Leave some margin
|
|
207
|
+
|
|
208
|
+
# Create a mapping of atom IDs to positions in the plot
|
|
209
|
+
atom_positions = {}
|
|
210
|
+
|
|
211
|
+
# Draw bonds first (so they're below atoms)
|
|
212
|
+
for atom1_id, atom2_id in bonds:
|
|
213
|
+
pos1 = coords_2d[atom1_id] * scaling_factor + center
|
|
214
|
+
pos2 = coords_2d[atom2_id] * scaling_factor + center
|
|
215
|
+
|
|
216
|
+
# Draw bond as a line
|
|
217
|
+
line = Line2D([pos1[0], pos2[0]], [pos1[1], pos2[1]],
|
|
218
|
+
color='black', linewidth=1.5, zorder=2)
|
|
219
|
+
ax.add_line(line)
|
|
220
|
+
|
|
221
|
+
# Draw atoms as circles
|
|
222
|
+
for atom_id, coord in coords_2d.items():
|
|
223
|
+
# Scale and shift the position
|
|
224
|
+
pos = coord * scaling_factor + center
|
|
225
|
+
atom_positions[atom_id] = pos
|
|
226
|
+
|
|
227
|
+
element = self.atom_coords[atom_id]['element']
|
|
228
|
+
color = self.element_colors.get(element, 'gray')
|
|
229
|
+
|
|
230
|
+
# Determine size based on element (larger for heavier atoms)
|
|
231
|
+
size = 8 if element in ['C', 'H'] else 10
|
|
232
|
+
|
|
233
|
+
# Draw atom
|
|
234
|
+
circle = Circle(pos, size, facecolor=color, edgecolor='black',
|
|
235
|
+
linewidth=1, alpha=0.8, zorder=3)
|
|
236
|
+
ax.add_patch(circle)
|
|
237
|
+
|
|
238
|
+
# Add element label (except for carbon)
|
|
239
|
+
if element != 'C':
|
|
240
|
+
ax.text(pos[0], pos[1], element, ha='center', va='center',
|
|
241
|
+
fontsize=8, fontweight='bold', color='white', zorder=4)
|
|
242
|
+
|
|
243
|
+
return atom_positions
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class HybridProtLigMapper:
|
|
247
|
+
"""
|
|
248
|
+
Class for analyzing protein-ligand interactions and creating
|
|
249
|
+
visualizations with a simplified ligand structure.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, pdb_file, ligand_resname=None):
|
|
253
|
+
"""
|
|
254
|
+
Initialize with a PDB file containing a protein-ligand complex.
|
|
255
|
+
|
|
256
|
+
Parameters:
|
|
257
|
+
-----------
|
|
258
|
+
pdb_file : str
|
|
259
|
+
Path to the PDB file with protein and ligand
|
|
260
|
+
ligand_resname : str, optional
|
|
261
|
+
Specific residue name of the ligand to focus on
|
|
262
|
+
"""
|
|
263
|
+
self.pdb_file = pdb_file
|
|
264
|
+
self.ligand_resname = ligand_resname
|
|
265
|
+
|
|
266
|
+
# Parse the PDB file
|
|
267
|
+
self.parser = PDBParser(QUIET=True)
|
|
268
|
+
self.structure = self.parser.get_structure('complex', pdb_file)
|
|
269
|
+
self.model = self.structure[0]
|
|
270
|
+
|
|
271
|
+
# Separate ligand from protein
|
|
272
|
+
self.protein_atoms = []
|
|
273
|
+
self.ligand_atoms = []
|
|
274
|
+
self.protein_residues = {}
|
|
275
|
+
self.ligand_residue = None
|
|
276
|
+
|
|
277
|
+
for residue in self.model.get_residues():
|
|
278
|
+
# Store ligand atoms (HETATM records)
|
|
279
|
+
if residue.id[0] != ' ': # Non-standard residue (HETATM)
|
|
280
|
+
if ligand_resname is None or residue.resname == ligand_resname:
|
|
281
|
+
for atom in residue:
|
|
282
|
+
self.ligand_atoms.append(atom)
|
|
283
|
+
if self.ligand_residue is None:
|
|
284
|
+
self.ligand_residue = residue
|
|
285
|
+
else: # Standard residues (protein)
|
|
286
|
+
res_id = (residue.resname, residue.id[1])
|
|
287
|
+
self.protein_residues[res_id] = residue
|
|
288
|
+
for atom in residue:
|
|
289
|
+
self.protein_atoms.append(atom)
|
|
290
|
+
|
|
291
|
+
# Check if we found a ligand
|
|
292
|
+
if not self.ligand_atoms:
|
|
293
|
+
raise ValueError("No ligand (HETATM) found in the PDB file.")
|
|
294
|
+
|
|
295
|
+
# Storage for the interaction data
|
|
296
|
+
self.interactions = {
|
|
297
|
+
'hydrogen_bonds': [],
|
|
298
|
+
'carbon_pi': [],
|
|
299
|
+
'pi_pi_stacking': [],
|
|
300
|
+
'donor_pi': [],
|
|
301
|
+
'amide_pi': [],
|
|
302
|
+
'hydrophobic': []
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Will store residues that interact with the ligand
|
|
306
|
+
self.interacting_residues = set()
|
|
307
|
+
|
|
308
|
+
# For solvent accessibility information (simplified)
|
|
309
|
+
self.solvent_accessible = set()
|
|
310
|
+
|
|
311
|
+
# Create the simple ligand structure
|
|
312
|
+
self.ligand_structure = SimpleLigandStructure(self.ligand_atoms)
|
|
313
|
+
|
|
314
|
+
def detect_interactions(self,
|
|
315
|
+
h_bond_cutoff=3.5,
|
|
316
|
+
pi_stack_cutoff=5.5,
|
|
317
|
+
hydrophobic_cutoff=4.0):
|
|
318
|
+
"""
|
|
319
|
+
Detect all interactions between protein and ligand.
|
|
320
|
+
|
|
321
|
+
Parameters:
|
|
322
|
+
-----------
|
|
323
|
+
h_bond_cutoff : float
|
|
324
|
+
Distance cutoff for hydrogen bonds in Angstroms
|
|
325
|
+
pi_stack_cutoff : float
|
|
326
|
+
Distance cutoff for pi-stacking interactions in Angstroms
|
|
327
|
+
hydrophobic_cutoff : float
|
|
328
|
+
Distance cutoff for hydrophobic interactions in Angstroms
|
|
329
|
+
"""
|
|
330
|
+
# Use neighbor search for efficiency
|
|
331
|
+
ns = NeighborSearch(self.protein_atoms)
|
|
332
|
+
max_cutoff = max(h_bond_cutoff, pi_stack_cutoff, hydrophobic_cutoff)
|
|
333
|
+
|
|
334
|
+
# Define amino acid categories
|
|
335
|
+
aromatic_residues = {'PHE', 'TYR', 'TRP', 'HIS'}
|
|
336
|
+
h_bond_donors = {'ARG', 'LYS', 'HIS', 'ASN', 'GLN', 'SER', 'THR', 'TYR', 'TRP'}
|
|
337
|
+
h_bond_acceptors = {'ASP', 'GLU', 'ASN', 'GLN', 'HIS', 'SER', 'THR', 'TYR'}
|
|
338
|
+
neg_charged = {'ASP', 'GLU'}
|
|
339
|
+
amide_residues = {'ASN', 'GLN'}
|
|
340
|
+
hydrophobic_residues = {'ALA', 'VAL', 'LEU', 'ILE', 'MET', 'PHE', 'TRP', 'PRO', 'TYR'}
|
|
341
|
+
|
|
342
|
+
# Check each ligand atom for interactions
|
|
343
|
+
for lig_atom in self.ligand_atoms:
|
|
344
|
+
# Find protein atoms within cutoff distance
|
|
345
|
+
nearby_atoms = ns.search(lig_atom.get_coord(), max_cutoff)
|
|
346
|
+
|
|
347
|
+
for prot_atom in nearby_atoms:
|
|
348
|
+
prot_res = prot_atom.get_parent()
|
|
349
|
+
distance = lig_atom - prot_atom
|
|
350
|
+
|
|
351
|
+
# Store interacting residue for later visualization
|
|
352
|
+
res_id = (prot_res.resname, prot_res.id[1])
|
|
353
|
+
self.interacting_residues.add(res_id)
|
|
354
|
+
|
|
355
|
+
# Determine interaction types based on distance and atom/residue types
|
|
356
|
+
|
|
357
|
+
# 1. Hydrogen bonds - N and O atoms within cutoff
|
|
358
|
+
if distance <= h_bond_cutoff:
|
|
359
|
+
if lig_atom.element in ['N', 'O'] and prot_atom.element in ['N', 'O']:
|
|
360
|
+
self.interactions['hydrogen_bonds'].append({
|
|
361
|
+
'ligand_atom': lig_atom,
|
|
362
|
+
'protein_atom': prot_atom,
|
|
363
|
+
'protein_residue': prot_res,
|
|
364
|
+
'distance': distance
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
# 2. Pi-stacking interactions - aromatic residues
|
|
368
|
+
if distance <= pi_stack_cutoff and prot_res.resname in aromatic_residues:
|
|
369
|
+
if lig_atom.element == 'C' and prot_atom.element == 'C':
|
|
370
|
+
self.interactions['pi_pi_stacking'].append({
|
|
371
|
+
'ligand_atom': lig_atom,
|
|
372
|
+
'protein_atom': prot_atom,
|
|
373
|
+
'protein_residue': prot_res,
|
|
374
|
+
'distance': distance
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
# 3. Carbon-Pi interactions
|
|
378
|
+
if distance <= pi_stack_cutoff and prot_res.resname in aromatic_residues:
|
|
379
|
+
if lig_atom.element == 'C':
|
|
380
|
+
self.interactions['carbon_pi'].append({
|
|
381
|
+
'ligand_atom': lig_atom,
|
|
382
|
+
'protein_atom': prot_atom,
|
|
383
|
+
'protein_residue': prot_res,
|
|
384
|
+
'distance': distance
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
# 4. Donor-Pi interactions - negatively charged residues
|
|
388
|
+
if distance <= pi_stack_cutoff and prot_res.resname in neg_charged:
|
|
389
|
+
if lig_atom.element == 'C':
|
|
390
|
+
self.interactions['donor_pi'].append({
|
|
391
|
+
'ligand_atom': lig_atom,
|
|
392
|
+
'protein_atom': prot_atom,
|
|
393
|
+
'protein_residue': prot_res,
|
|
394
|
+
'distance': distance
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
# 5. Amide-Pi interactions
|
|
398
|
+
if distance <= pi_stack_cutoff and prot_res.resname in amide_residues:
|
|
399
|
+
if lig_atom.element == 'C':
|
|
400
|
+
self.interactions['amide_pi'].append({
|
|
401
|
+
'ligand_atom': lig_atom,
|
|
402
|
+
'protein_atom': prot_atom,
|
|
403
|
+
'protein_residue': prot_res,
|
|
404
|
+
'distance': distance
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
# 6. Hydrophobic interactions
|
|
408
|
+
if distance <= hydrophobic_cutoff:
|
|
409
|
+
if (prot_res.resname in hydrophobic_residues and
|
|
410
|
+
lig_atom.element == 'C' and prot_atom.element == 'C'):
|
|
411
|
+
self.interactions['hydrophobic'].append({
|
|
412
|
+
'ligand_atom': lig_atom,
|
|
413
|
+
'protein_atom': prot_atom,
|
|
414
|
+
'protein_residue': prot_res,
|
|
415
|
+
'distance': distance
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
# Deduplicate interactions by residue for cleaner visualization
|
|
419
|
+
# Keep only one interaction of each type per residue
|
|
420
|
+
for interaction_type in self.interactions:
|
|
421
|
+
by_residue = defaultdict(list)
|
|
422
|
+
for interaction in self.interactions[interaction_type]:
|
|
423
|
+
res_id = (interaction['protein_residue'].resname,
|
|
424
|
+
interaction['protein_residue'].id[1])
|
|
425
|
+
by_residue[res_id].append(interaction)
|
|
426
|
+
|
|
427
|
+
# Keep only the closest interaction for each residue and type
|
|
428
|
+
closest_interactions = []
|
|
429
|
+
for res_id, res_interactions in by_residue.items():
|
|
430
|
+
closest = min(res_interactions, key=lambda x: x['distance'])
|
|
431
|
+
closest_interactions.append(closest)
|
|
432
|
+
|
|
433
|
+
self.interactions[interaction_type] = closest_interactions
|
|
434
|
+
|
|
435
|
+
def estimate_solvent_accessibility(self):
|
|
436
|
+
"""
|
|
437
|
+
Estimate which residues might be solvent accessible.
|
|
438
|
+
This is a simplified approach since we're trying to match the example image.
|
|
439
|
+
In a real implementation, you'd use DSSP or a similar tool.
|
|
440
|
+
"""
|
|
441
|
+
# For simplicity, mark all residues as solvent accessible
|
|
442
|
+
# In a real implementation, you'd use a proper algorithm
|
|
443
|
+
self.solvent_accessible = self.interacting_residues.copy()
|
|
444
|
+
|
|
445
|
+
def visualize(self, output_file='protein_ligand_interactions.png',
|
|
446
|
+
figsize=(12, 12), dpi=300, title=None):
|
|
447
|
+
"""
|
|
448
|
+
Generate a 2D visualization of protein-ligand interactions
|
|
449
|
+
matching the style of the reference image.
|
|
450
|
+
|
|
451
|
+
Parameters:
|
|
452
|
+
-----------
|
|
453
|
+
output_file : str
|
|
454
|
+
Path where the output image will be saved
|
|
455
|
+
figsize : tuple
|
|
456
|
+
Figure size in inches (width, height)
|
|
457
|
+
dpi : int
|
|
458
|
+
Resolution in dots per inch
|
|
459
|
+
title : str, optional
|
|
460
|
+
Title for the plot
|
|
461
|
+
"""
|
|
462
|
+
# Create figure
|
|
463
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
464
|
+
|
|
465
|
+
# Add light blue background for ligand (matching reference)
|
|
466
|
+
ligand_radius = 90
|
|
467
|
+
ligand_pos = (0, 0)
|
|
468
|
+
ligand_circle = Circle(ligand_pos, ligand_radius, facecolor='#ADD8E6',
|
|
469
|
+
edgecolor='none', alpha=0.4, zorder=1)
|
|
470
|
+
ax.add_patch(ligand_circle)
|
|
471
|
+
|
|
472
|
+
# Draw the simplified ligand structure
|
|
473
|
+
atom_positions = self.ligand_structure.draw_on_axes(ax, center=ligand_pos, radius=ligand_radius*0.8)
|
|
474
|
+
|
|
475
|
+
# Place interacting residues in a circle around the ligand
|
|
476
|
+
n_residues = len(self.interacting_residues)
|
|
477
|
+
if n_residues == 0:
|
|
478
|
+
print("Warning: No interacting residues detected.")
|
|
479
|
+
n_residues = 1 # Avoid division by zero
|
|
480
|
+
|
|
481
|
+
# Calculate positions for residues
|
|
482
|
+
radius = 250 # Distance from center to residues
|
|
483
|
+
residue_positions = {}
|
|
484
|
+
|
|
485
|
+
# Arrange residues in a circle
|
|
486
|
+
for i, res_id in enumerate(sorted(self.interacting_residues)):
|
|
487
|
+
angle = 2 * math.pi * i / n_residues
|
|
488
|
+
x = radius * math.cos(angle)
|
|
489
|
+
y = radius * math.sin(angle)
|
|
490
|
+
residue_positions[res_id] = (x, y)
|
|
491
|
+
|
|
492
|
+
# Draw solvent accessibility highlight (light blue circle behind residue)
|
|
493
|
+
if res_id in self.solvent_accessible:
|
|
494
|
+
solvent_circle = Circle((x, y), 40, facecolor='#ADD8E6',
|
|
495
|
+
edgecolor='none', alpha=0.3, zorder=1)
|
|
496
|
+
ax.add_patch(solvent_circle)
|
|
497
|
+
|
|
498
|
+
# Draw residue node as rectangle with rounded corners like in reference image
|
|
499
|
+
# For simplicity, we'll use a white rectangle with black border
|
|
500
|
+
rect_width = 60
|
|
501
|
+
rect_height = 30
|
|
502
|
+
residue_box = Rectangle((x-rect_width/2, y-rect_height/2), rect_width, rect_height,
|
|
503
|
+
facecolor='white', edgecolor='black', linewidth=1.5,
|
|
504
|
+
zorder=2, alpha=1.0)
|
|
505
|
+
ax.add_patch(residue_box)
|
|
506
|
+
|
|
507
|
+
# Add residue label (NAME NUMBER) like in reference image
|
|
508
|
+
resname, resnum = res_id
|
|
509
|
+
label = f"{resname} {resnum}"
|
|
510
|
+
text = ax.text(x, y, label, ha='center', va='center',
|
|
511
|
+
fontsize=11, fontweight='bold', zorder=3)
|
|
512
|
+
text.set_path_effects([path_effects.withStroke(linewidth=2,
|
|
513
|
+
foreground='white')])
|
|
514
|
+
|
|
515
|
+
# Define interaction styles to match the reference image
|
|
516
|
+
interaction_styles = {
|
|
517
|
+
'hydrogen_bonds': {
|
|
518
|
+
'color': 'green',
|
|
519
|
+
'linestyle': '-',
|
|
520
|
+
'linewidth': 1.5,
|
|
521
|
+
'marker_text': 'H',
|
|
522
|
+
'marker_color': 'green',
|
|
523
|
+
'marker_bg': '#E0FFE0', # Light green bg
|
|
524
|
+
'name': 'Hydrogen Bond'
|
|
525
|
+
},
|
|
526
|
+
'carbon_pi': {
|
|
527
|
+
'color': '#666666', # Dark gray
|
|
528
|
+
'linestyle': '--',
|
|
529
|
+
'linewidth': 1.5,
|
|
530
|
+
'marker_text': 'C',
|
|
531
|
+
'marker_color': '#666666',
|
|
532
|
+
'marker_bg': 'white',
|
|
533
|
+
'name': 'Carbon-Pi interaction'
|
|
534
|
+
},
|
|
535
|
+
'pi_pi_stacking': {
|
|
536
|
+
'color': '#9370DB', # Medium purple
|
|
537
|
+
'linestyle': '--',
|
|
538
|
+
'linewidth': 1.5,
|
|
539
|
+
'marker_text': 'π',
|
|
540
|
+
'marker_color': '#9370DB',
|
|
541
|
+
'marker_bg': 'white',
|
|
542
|
+
'name': 'Pi-Pi stacking'
|
|
543
|
+
},
|
|
544
|
+
'donor_pi': {
|
|
545
|
+
'color': '#FF69B4', # Hot pink
|
|
546
|
+
'linestyle': '--',
|
|
547
|
+
'linewidth': 1.5,
|
|
548
|
+
'marker_text': 'D',
|
|
549
|
+
'marker_color': '#FF69B4',
|
|
550
|
+
'marker_bg': 'white',
|
|
551
|
+
'name': 'Donor-Pi interaction'
|
|
552
|
+
},
|
|
553
|
+
'amide_pi': {
|
|
554
|
+
'color': '#A52A2A', # Brown
|
|
555
|
+
'linestyle': '--',
|
|
556
|
+
'linewidth': 1.5,
|
|
557
|
+
'marker_text': 'A',
|
|
558
|
+
'marker_color': '#A52A2A',
|
|
559
|
+
'marker_bg': 'white',
|
|
560
|
+
'name': 'Amide-Pi interaction'
|
|
561
|
+
},
|
|
562
|
+
'hydrophobic': {
|
|
563
|
+
'color': '#808080', # Gray
|
|
564
|
+
'linestyle': ':',
|
|
565
|
+
'linewidth': 1.0,
|
|
566
|
+
'marker_text': 'h',
|
|
567
|
+
'marker_color': '#808080',
|
|
568
|
+
'marker_bg': 'white',
|
|
569
|
+
'name': 'Hydrophobic'
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
# Draw interaction lines for each residue
|
|
574
|
+
for interaction_type, interactions in self.interactions.items():
|
|
575
|
+
if interaction_type not in interaction_styles:
|
|
576
|
+
continue
|
|
577
|
+
|
|
578
|
+
style = interaction_styles[interaction_type]
|
|
579
|
+
|
|
580
|
+
for interaction in interactions:
|
|
581
|
+
res = interaction['protein_residue']
|
|
582
|
+
res_id = (res.resname, res.id[1])
|
|
583
|
+
lig_atom = interaction['ligand_atom']
|
|
584
|
+
|
|
585
|
+
if res_id in residue_positions:
|
|
586
|
+
res_pos = residue_positions[res_id]
|
|
587
|
+
|
|
588
|
+
# Try to use actual ligand atom position if available
|
|
589
|
+
if lig_atom.get_id() in atom_positions:
|
|
590
|
+
lig_pos = atom_positions[lig_atom.get_id()]
|
|
591
|
+
else:
|
|
592
|
+
# Determine the point on ligand circle edge as fallback
|
|
593
|
+
# Get the angle from center to residue
|
|
594
|
+
dx = res_pos[0] - ligand_pos[0]
|
|
595
|
+
dy = res_pos[1] - ligand_pos[1]
|
|
596
|
+
angle = math.atan2(dy, dx)
|
|
597
|
+
|
|
598
|
+
# Calculate point on ligand circle edge
|
|
599
|
+
lig_edge_x = ligand_pos[0] + ligand_radius * math.cos(angle)
|
|
600
|
+
lig_edge_y = ligand_pos[1] + ligand_radius * math.sin(angle)
|
|
601
|
+
lig_pos = (lig_edge_x, lig_edge_y)
|
|
602
|
+
|
|
603
|
+
# Draw curved line from ligand to residue
|
|
604
|
+
line = FancyArrowPatch(
|
|
605
|
+
lig_pos, res_pos,
|
|
606
|
+
connectionstyle="arc3,rad=0.1",
|
|
607
|
+
color=style['color'],
|
|
608
|
+
linestyle=style['linestyle'],
|
|
609
|
+
linewidth=style['linewidth'],
|
|
610
|
+
arrowstyle='-',
|
|
611
|
+
alpha=0.7,
|
|
612
|
+
zorder=4
|
|
613
|
+
)
|
|
614
|
+
ax.add_patch(line)
|
|
615
|
+
|
|
616
|
+
# Add interaction marker (circle with letter)
|
|
617
|
+
midpoint_x = (lig_pos[0] + res_pos[0]) / 2
|
|
618
|
+
midpoint_y = (lig_pos[1] + res_pos[1]) / 2
|
|
619
|
+
|
|
620
|
+
# Draw hexagonal marker like in reference image
|
|
621
|
+
n_sides = 6 if 'pi' in interaction_type else 0 # Hexagon for pi interactions
|
|
622
|
+
marker_radius = 12
|
|
623
|
+
|
|
624
|
+
if n_sides > 0:
|
|
625
|
+
# Draw a hexagon for pi interactions
|
|
626
|
+
angles = np.linspace(0, 2*np.pi, n_sides+1)[:-1]
|
|
627
|
+
hex_vertices = np.array([
|
|
628
|
+
[midpoint_x + marker_radius * np.cos(angle),
|
|
629
|
+
midpoint_y + marker_radius * np.sin(angle)]
|
|
630
|
+
for angle in angles
|
|
631
|
+
])
|
|
632
|
+
hex_patch = Polygon(
|
|
633
|
+
hex_vertices,
|
|
634
|
+
closed=True,
|
|
635
|
+
facecolor=style.get('marker_bg', 'white'),
|
|
636
|
+
edgecolor=style['marker_color'],
|
|
637
|
+
linewidth=1.5,
|
|
638
|
+
alpha=0.9,
|
|
639
|
+
zorder=5
|
|
640
|
+
)
|
|
641
|
+
ax.add_patch(hex_patch)
|
|
642
|
+
else:
|
|
643
|
+
# Draw circle for other interactions
|
|
644
|
+
marker_circle = Circle(
|
|
645
|
+
(midpoint_x, midpoint_y), marker_radius,
|
|
646
|
+
facecolor=style.get('marker_bg', 'white'),
|
|
647
|
+
edgecolor=style['marker_color'],
|
|
648
|
+
linewidth=1.5,
|
|
649
|
+
alpha=0.9,
|
|
650
|
+
zorder=5
|
|
651
|
+
)
|
|
652
|
+
ax.add_patch(marker_circle)
|
|
653
|
+
|
|
654
|
+
# Add interaction symbol
|
|
655
|
+
ax.text(
|
|
656
|
+
midpoint_x, midpoint_y,
|
|
657
|
+
style['marker_text'],
|
|
658
|
+
ha='center', va='center',
|
|
659
|
+
fontsize=9, color=style['marker_color'],
|
|
660
|
+
fontweight='bold',
|
|
661
|
+
zorder=6
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
# Add legend box with title matching reference image
|
|
665
|
+
legend_title = "Interacting structural groups"
|
|
666
|
+
legend_elements = []
|
|
667
|
+
|
|
668
|
+
# Add a "RES 1" element for residue representation
|
|
669
|
+
residue_patch = Rectangle((0, 0), 1, 1, facecolor='white',
|
|
670
|
+
edgecolor='black', label='Interacting structural groups')
|
|
671
|
+
legend_elements.append(residue_patch)
|
|
672
|
+
|
|
673
|
+
# Interaction type markers for legend
|
|
674
|
+
for int_type, style in interaction_styles.items():
|
|
675
|
+
# Only include interaction types that are present
|
|
676
|
+
if self.interactions[int_type]:
|
|
677
|
+
if int_type == 'hydrogen_bonds':
|
|
678
|
+
# Special handling for H-bonds to match reference
|
|
679
|
+
line = Line2D([0], [0], color=style['color'],
|
|
680
|
+
linestyle=style['linestyle'], linewidth=style['linewidth'],
|
|
681
|
+
marker='o', markerfacecolor=style.get('marker_bg', 'white'),
|
|
682
|
+
markeredgecolor=style['color'],
|
|
683
|
+
markersize=8, label=style['name'])
|
|
684
|
+
elif 'pi' in int_type:
|
|
685
|
+
# Hexagonal markers for pi interactions
|
|
686
|
+
line = Line2D([0], [0], color=style['color'],
|
|
687
|
+
linestyle=style['linestyle'], linewidth=style['linewidth'],
|
|
688
|
+
marker='h', markerfacecolor=style.get('marker_bg', 'white'),
|
|
689
|
+
markeredgecolor=style['color'],
|
|
690
|
+
markersize=8, label=style['name'])
|
|
691
|
+
else:
|
|
692
|
+
# Circular markers for other interactions
|
|
693
|
+
line = Line2D([0], [0], color=style['color'],
|
|
694
|
+
linestyle=style['linestyle'], linewidth=style['linewidth'],
|
|
695
|
+
marker='o', markerfacecolor=style.get('marker_bg', 'white'),
|
|
696
|
+
markeredgecolor=style['color'],
|
|
697
|
+
markersize=8, label=style['name'])
|
|
698
|
+
legend_elements.append(line)
|
|
699
|
+
|
|
700
|
+
# Add solvent accessibility indicator
|
|
701
|
+
if self.solvent_accessible:
|
|
702
|
+
solvent_patch = Rectangle((0, 0), 1, 1, facecolor='#ADD8E6',
|
|
703
|
+
alpha=0.3, edgecolor=None, label='Solvent accessible')
|
|
704
|
+
legend_elements.append(solvent_patch)
|
|
705
|
+
|
|
706
|
+
# Create legend box in top right corner like reference image
|
|
707
|
+
legend = ax.legend(
|
|
708
|
+
handles=legend_elements,
|
|
709
|
+
title=legend_title,
|
|
710
|
+
loc='upper right',
|
|
711
|
+
frameon=True,
|
|
712
|
+
framealpha=0.7,
|
|
713
|
+
fontsize=9,
|
|
714
|
+
title_fontsize=10
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
# Set plot limits and appearance
|
|
718
|
+
max_coord = radius + 100
|
|
719
|
+
ax.set_xlim(-max_coord, max_coord)
|
|
720
|
+
ax.set_ylim(-max_coord, max_coord)
|
|
721
|
+
ax.set_aspect('equal')
|
|
722
|
+
ax.axis('off')
|
|
723
|
+
|
|
724
|
+
# Add title
|
|
725
|
+
if title:
|
|
726
|
+
plt.title(title, fontsize=16)
|
|
727
|
+
else:
|
|
728
|
+
plt.title(f"Protein-Ligand Interactions: {os.path.basename(self.pdb_file)}",
|
|
729
|
+
fontsize=16)
|
|
730
|
+
|
|
731
|
+
# Save figure
|
|
732
|
+
plt.tight_layout()
|
|
733
|
+
plt.savefig(output_file, dpi=dpi, bbox_inches='tight')
|
|
734
|
+
plt.close()
|
|
735
|
+
|
|
736
|
+
print(f"Interaction diagram saved to {output_file}")
|
|
737
|
+
return output_file
|
|
738
|
+
|
|
739
|
+
def run_analysis(self, output_file=None):
|
|
740
|
+
"""
|
|
741
|
+
Run the complete analysis pipeline.
|
|
742
|
+
|
|
743
|
+
Parameters:
|
|
744
|
+
-----------
|
|
745
|
+
output_file : str, optional
|
|
746
|
+
Path where the output image will be saved. If None, a default name will be generated.
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
--------
|
|
750
|
+
str : Path to the generated visualization file
|
|
751
|
+
"""
|
|
752
|
+
if output_file is None:
|
|
753
|
+
base_name = os.path.splitext(os.path.basename(self.pdb_file))[0]
|
|
754
|
+
output_file = f"{base_name}_interactions.png"
|
|
755
|
+
|
|
756
|
+
# Detect protein-ligand interactions
|
|
757
|
+
print("Detecting interactions...")
|
|
758
|
+
self.detect_interactions()
|
|
759
|
+
|
|
760
|
+
# Estimate solvent accessibility
|
|
761
|
+
print("Estimating solvent accessibility...")
|
|
762
|
+
self.estimate_solvent_accessibility()
|
|
763
|
+
|
|
764
|
+
# Generate visualization
|
|
765
|
+
print("Generating visualization...")
|
|
766
|
+
return self.visualize(output_file=output_file)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pandamap
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python package for visualizing protein-ligand interactions with 2D ligand structure representation
|
|
5
|
+
Author-email: Your Name <your.email@example.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Pritam Kumar Panda
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/yourusername/pandamap
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/yourusername/pandamap/issues
|
|
30
|
+
Keywords: bioinformatics,protein-ligand,interactions,visualization,drug-design
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
35
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
37
|
+
Requires-Python: >=3.7
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: numpy>=1.20.0
|
|
41
|
+
Requires-Dist: matplotlib>=3.4.0
|
|
42
|
+
Requires-Dist: biopython>=1.79
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# PandaMap
|
|
46
|
+
|
|
47
|
+
**P**rotein **AND** lig**A**nd interaction **MAP**per: A Python package for visualizing protein-ligand interactions with 2D ligand structure representation
|
|
48
|
+
|
|
49
|
+
## Overview
|
|
50
|
+
|
|
51
|
+
PandaMap is a lightweight tool for visualizing protein-ligand interactions from PDB files. It generates intuitive 2D interaction diagrams that display both the ligand structure and its interactions with protein residues.
|
|
52
|
+
|
|
53
|
+
Key features:
|
|
54
|
+
- Visualization of protein-ligand interactions with minimal dependencies
|
|
55
|
+
- 2D representation of ligand structure without requiring RDKit
|
|
56
|
+
- Detection of multiple interaction types (hydrogen bonds, π-stacking, hydrophobic)
|
|
57
|
+
- Command-line interface for quick analysis
|
|
58
|
+
- Python API for integration into computational workflows
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install pandamap
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Dependencies
|
|
67
|
+
|
|
68
|
+
- NumPy
|
|
69
|
+
- Matplotlib
|
|
70
|
+
- BioPython
|
|
71
|
+
|
|
72
|
+
## Basic Usage
|
|
73
|
+
|
|
74
|
+
### Command Line Interface
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Basic usage
|
|
78
|
+
pandamap protein_ligand.pdb --output interactions.png
|
|
79
|
+
|
|
80
|
+
# Specify a particular ligand by residue name
|
|
81
|
+
pandamap protein_ligand.pdb --ligand LIG
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Python API
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from pandamap import HybridProtLigMapper
|
|
88
|
+
|
|
89
|
+
# Initialize with PDB file
|
|
90
|
+
mapper = HybridProtLigMapper("protein_ligand.pdb", ligand_resname="LIG")
|
|
91
|
+
|
|
92
|
+
# Run analysis and generate visualization
|
|
93
|
+
output_file = mapper.run_analysis(output_file="interactions.png")
|
|
94
|
+
|
|
95
|
+
# Or run steps separately
|
|
96
|
+
mapper.detect_interactions()
|
|
97
|
+
mapper.estimate_solvent_accessibility()
|
|
98
|
+
mapper.visualize(output_file="interactions.png")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Example Output
|
|
102
|
+
|
|
103
|
+

|
|
104
|
+
|
|
105
|
+
## Citation
|
|
106
|
+
|
|
107
|
+
If you use PandaMap in your research, please cite:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Pritam Kumar Panda. (2025). **P**rotein **AND** lig**A**nd interaction **MAP**per: A Python package for visualizing protein-ligand interactions with 2D ligand structure representation. GitHub repository. https://github.com/pritampanda15/PandaMap
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
src/pandamap/__init__.py
|
|
6
|
+
src/pandamap/cli.py
|
|
7
|
+
src/pandamap/core.py
|
|
8
|
+
src/pandamap.egg-info/PKG-INFO
|
|
9
|
+
src/pandamap.egg-info/SOURCES.txt
|
|
10
|
+
src/pandamap.egg-info/dependency_links.txt
|
|
11
|
+
src/pandamap.egg-info/entry_points.txt
|
|
12
|
+
src/pandamap.egg-info/requires.txt
|
|
13
|
+
src/pandamap.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pandamap
|