├── setup.py ├── pyrama ├── config.py └── __init__.py ├── README.md ├── bin └── pyrama └── .gitignore /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='pyrama', 5 | version='2.0.1', 6 | scripts=['bin/pyrama'], 7 | packages=['pyrama'], 8 | package_data={'pyrama': ['data/*.data']}, 9 | url='https://github.com/gerdos/PyRAMA', 10 | license='', 11 | author='gerdos', 12 | author_email='gerdos@caesar.elte.hu', 13 | description='Ramachandran plot generator', 14 | install_requires=['biopython', 'numpy', 'matplotlib'] 15 | ) 16 | -------------------------------------------------------------------------------- /pyrama/config.py: -------------------------------------------------------------------------------- 1 | 2 | import matplotlib.colors as mplcolors 3 | import os 4 | 5 | 6 | RAMA_PREFERENCES = { 7 | "General": { 8 | "file": os.path.join('data', 'pref_general.data'), 9 | "cmap": mplcolors.ListedColormap(['#FFFFFF', '#B3E8FF', '#7FD9FF']), 10 | "bounds": [0, 0.0005, 0.02, 1], 11 | }, 12 | "GLY": { 13 | "file": os.path.join('data', 'pref_glycine.data'), 14 | "cmap": mplcolors.ListedColormap(['#FFFFFF', '#FFE8C5', '#FFCC7F']), 15 | "bounds": [0, 0.002, 0.02, 1], 16 | }, 17 | "PRO": { 18 | "file": os.path.join('data', 'pref_proline.data'), 19 | "cmap": mplcolors.ListedColormap(['#FFFFFF', '#D0FFC5', '#7FFF8C']), 20 | "bounds": [0, 0.002, 0.02, 1], 21 | }, 22 | "PRE-PRO": { 23 | "file": os.path.join('data', 'pref_preproline.data'), 24 | "cmap": mplcolors.ListedColormap(['#FFFFFF', '#B3E8FF', '#7FD9FF']), 25 | "bounds": [0, 0.002, 0.02, 1], 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyRAMA 2 | Python3 implementation of the Ramachandran plot 3 | 4 | *Package does work with Python2.7 but it is recommended to use Python3* 5 | 6 | ### For easy installation use pip: 7 | 8 | pip3 install pyrama 9 | 10 | 11 | ### Usage: 12 | 13 | pyrama my_pdb_file.pdb -o output_folder --no-show 14 | 15 | Note: The script is able to read in multiple PDB files, and can also process multiple chain automatically now. The outliers would be annotated with labels. 16 | 17 | ### Example output: 18 | 19 | ![Example output](https://i.imgur.com/zOGxZ2r.png) 20 | 21 | ### Dependencies: 22 | 23 | Running PyRAMA requires *matplotlib*, *numpy* and *biopython* 24 | 25 | To install these on a standard Linux system: 26 | 27 | apt install python3-matplotlib 28 | apt install python3-biopython 29 | apt install python3-numpy 30 | 31 | For the standard PSI and PHI preferences see: 32 | 33 | Lovell *et al*. Structure validation by Calpha geometry: phi,psi and Cbeta deviation. 2003; DOI: 10.1002/prot.10286 34 | -------------------------------------------------------------------------------- /bin/pyrama: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Written by Gábor Erdős, 2017 6 | Contact info: gerdos[at]caesar.elte.hu 7 | 8 | The preferences were calculated from the following artice: 9 | Lovell et al. Structure validation by Calpha geometry: phi,psi and Cbeta deviation. 2003 10 | DOI: 10.1002/prot.10286 11 | """ 12 | 13 | import sys 14 | import argparse 15 | from pyrama import calc_ramachandran, plot_ramachandran 16 | 17 | if __name__ == "__main__": 18 | parser = argparse.ArgumentParser(description="Ramachandran plot tool") 19 | parser.add_argument("pdb_files", nargs="+", help="PDB file(s) to analyze") 20 | parser.add_argument("-o", "--output-dir", default=None, help="Directory to save plot files") 21 | parser.add_argument("--no-show", action="store_true", help="Do not display plots, only save") 22 | args = parser.parse_args() 23 | 24 | results = calc_ramachandran(args.pdb_files) 25 | plot_ramachandran(results, output_dir=args.output_dir, show_plots=not args.no_show) 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /pyrama/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | 3 | import math 4 | import os 5 | import sys 6 | 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | from Bio import PDB 10 | from matplotlib import colors 11 | 12 | from .config import RAMA_PREFERENCES 13 | 14 | RAMA_PREF_VALUES = None 15 | 16 | 17 | def _cache_RAMA_PREF_VALUES(): 18 | f_path = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) 19 | RAMA_PREF_VALUES = {} 20 | for key, val in RAMA_PREFERENCES.items(): 21 | RAMA_PREF_VALUES[key] = np.full((360, 360), 0, dtype=np.float64) 22 | with open(os.path.join(f_path, val["file"])) as fn: 23 | for line in fn: 24 | if line.startswith("#"): 25 | continue 26 | else: 27 | x = int(float(line.split()[1])) 28 | y = int(float(line.split()[0])) 29 | RAMA_PREF_VALUES[key][x + 180][y + 180] \ 30 | = RAMA_PREF_VALUES[key][x + 179][y + 179] \ 31 | = RAMA_PREF_VALUES[key][x + 179][y + 180] \ 32 | = RAMA_PREF_VALUES[key][x + 180][y + 179] \ 33 | = float(line.split()[2]) 34 | return RAMA_PREF_VALUES 35 | 36 | 37 | def calc_ramachandran(file_name_list): 38 | """ 39 | Main calculation and plotting definition 40 | :param file_name_list: List of PDB files to plot 41 | :return: results dict by chain, with 'normals'/'outliers' keys for each chain 42 | """ 43 | global RAMA_PREF_VALUES 44 | 45 | if RAMA_PREF_VALUES is None: 46 | RAMA_PREF_VALUES = _cache_RAMA_PREF_VALUES() 47 | 48 | results = {} 49 | # Calculate the torsion angle of the inputs 50 | for inp in file_name_list: 51 | if not os.path.isfile(inp): 52 | continue 53 | structure = PDB.PDBParser().get_structure('input_structure', inp) 54 | for model in structure: 55 | for chain in model: 56 | chain_id = chain.id 57 | # 初始化结构,兼容plot_ramachandran 58 | if chain_id not in results: 59 | results[chain_id] = {"normals": {}, "outliers": {}} 60 | for key in RAMA_PREFERENCES.keys(): 61 | results[chain_id]["normals"][key] = {"x": [], "y": [], "residues": []} 62 | results[chain_id]["outliers"][key] = {"x": [], "y": [], "residues": []} 63 | polypeptides = PDB.PPBuilder().build_peptides(chain) 64 | for poly_index, poly in enumerate(polypeptides): 65 | phi_psi = poly.get_phi_psi_list() 66 | for res_index, residue in enumerate(poly): 67 | res_name = "{}".format(residue.resname) 68 | res_num = residue.id[1] 69 | phi, psi = phi_psi[res_index] 70 | if phi and psi: 71 | if res_index + 1 < len(poly) and str(poly[res_index + 1].resname) == "PRO": 72 | aa_type = "PRE-PRO" 73 | elif res_name == "PRO": 74 | aa_type = "PRO" 75 | elif res_name == "GLY": 76 | aa_type = "GLY" 77 | else: 78 | aa_type = "General" 79 | phi_deg = math.degrees(phi) 80 | psi_deg = math.degrees(psi) 81 | residue_info = {"name": res_name, "number": res_num} 82 | if RAMA_PREF_VALUES[aa_type][int(psi_deg) + 180][int(phi_deg) + 180] < \ 83 | RAMA_PREFERENCES[aa_type]["bounds"][1]: 84 | results[chain_id]["outliers"][aa_type]["x"].append(phi_deg) 85 | results[chain_id]["outliers"][aa_type]["y"].append(psi_deg) 86 | results[chain_id]["outliers"][aa_type]["residues"].append(residue_info) 87 | else: 88 | results[chain_id]["normals"][aa_type]["x"].append(phi_deg) 89 | results[chain_id]["normals"][aa_type]["y"].append(psi_deg) 90 | results[chain_id]["normals"][aa_type]["residues"].append(residue_info) 91 | return results 92 | 93 | 94 | def plot_ramachandran(results, output_dir=None, show_plots=True): 95 | """ 96 | Plot Ramachandran plots for each chain separately 97 | :param results: Dictionary with chain-specific data 98 | :param output_dir: Directory to save plot files (if None, won't save) 99 | :param show_plots: Whether to display plots (True) or just save them (False) 100 | :return: List of paths to saved plot files 101 | """ 102 | global RAMA_PREF_VALUES 103 | if RAMA_PREF_VALUES is None: 104 | RAMA_PREF_VALUES = _cache_RAMA_PREF_VALUES() 105 | 106 | saved_files = [] 107 | 108 | # Create a separate figure for each chain 109 | for chain_id, chain_data in results.items(): 110 | plt.figure(figsize=(12, 10)) 111 | plt.suptitle(f"Ramachandran Plot - Chain {chain_id}") 112 | 113 | normals = chain_data["normals"] 114 | outliers = chain_data["outliers"] 115 | 116 | for idx, (key, val) in enumerate(sorted(RAMA_PREFERENCES.items(), key=lambda x: x[0].lower())): 117 | plt.subplot(2, 2, idx + 1) 118 | plt.title(key) 119 | plt.imshow(RAMA_PREF_VALUES[key], cmap=RAMA_PREFERENCES[key]["cmap"], 120 | norm=colors.BoundaryNorm(RAMA_PREFERENCES[key]["bounds"], RAMA_PREFERENCES[key]["cmap"].N), 121 | extent=(-180, 180, 180, -180)) 122 | 123 | # Plot normal points 124 | if normals[key]["x"]: 125 | plt.scatter(normals[key]["x"], normals[key]["y"], label="Normal") 126 | 127 | # Plot outliers 128 | if outliers[key]["x"]: 129 | plt.scatter(outliers[key]["x"], outliers[key]["y"], color="red", label="Outlier") 130 | # Add annotations for outliers 131 | for x, y, res in zip(outliers[key]["x"], outliers[key]["y"], outliers[key]["residues"]): 132 | plt.annotate(f"{res['name']}{res['number']}", (x, y), textcoords="offset points", xytext=(0, 5), ha='center', fontsize=7, color='red') 133 | 134 | plt.xlim([-180, 180]) 135 | plt.ylim([-180, 180]) 136 | plt.plot([-180, 180], [0, 0], color="black") 137 | plt.plot([0, 0], [-180, 180], color="black") 138 | plt.locator_params(axis='x', nbins=7) 139 | plt.xlabel(r'$\phi$') 140 | plt.ylabel(r'$\psi$') 141 | plt.grid() 142 | 143 | if normals[key]["x"] or outliers[key]["x"]: 144 | plt.legend() 145 | 146 | plt.tight_layout() 147 | 148 | # Save plot if output directory is provided 149 | if output_dir: 150 | os.makedirs(output_dir, exist_ok=True) 151 | plot_file = os.path.join(output_dir, f"ramachandran_chain_{chain_id}.png") 152 | plt.savefig(plot_file, dpi=300) 153 | saved_files.append(plot_file) 154 | 155 | if show_plots: 156 | plt.show() 157 | else: 158 | plt.close() 159 | 160 | return saved_files 161 | --------------------------------------------------------------------------------