proteusPy.Disulfide
This module, Disulfide, is part of the proteusPy package, a Python package for the analysis and modeling of protein structures, with an emphasis on disulfide bonds. It represents the core of the current implementation of proteusPy.
This work is based on the original C/C++ implementation by Eric G. Suchanek.
Author: Eric G. Suchanek, PhD Last revision: 2/17/2024
1""" 2This module, *Disulfide*, is part of the proteusPy package, a Python package for 3the analysis and modeling of protein structures, with an emphasis on disulfide bonds. 4It represents the core of the current implementation of *proteusPy*. 5 6This work is based on the original C/C++ implementation by Eric G. Suchanek. \n 7Author: Eric G. Suchanek, PhD 8Last revision: 2/17/2024 9""" 10 11# Cα N, Cα, Cβ, C', Sγ Å ° ρ 12 13import copy 14import datetime 15import glob 16import math 17import pickle 18import time 19from math import cos 20 21import numpy as np 22import pandas 23import pyvista as pv 24 25np.set_printoptions(suppress=True) 26pv.global_theme.color = "white" 27 28try: 29 # Check if running in Jupyter 30 shell = get_ipython().__class__.__name__ 31 if shell == "ZMQInteractiveShell": 32 from tqdm.notebook import tqdm 33 else: 34 from tqdm import tqdm 35except NameError: 36 from tqdm import tqdm 37 38from Bio.PDB import PDBList, PDBParser, Vector 39from Bio.PDB.vectors import calc_dihedral 40 41import proteusPy 42from proteusPy.atoms import * 43from proteusPy.data import ( 44 PROBLEM_ID_FILE, 45 SS_DICT_PICKLE_FILE, 46 SS_ID_FILE, 47 SS_PICKLE_FILE, 48 SS_TORSIONS_FILE, 49) 50from proteusPy.DisulfideExceptions import * 51from proteusPy.DisulfideList import DisulfideList 52 53# tqdm progress bar width 54from proteusPy.ProteusGlobals import ( 55 _ANG_INIT, 56 _FLOAT_INIT, 57 MODEL_DIR, 58 PBAR_COLS, 59 PDB_DIR, 60 WINSIZE, 61) 62from proteusPy.Residue import build_residue 63from proteusPy.turtle3D import ORIENT_SIDECHAIN, Turtle3D 64from proteusPy.utility import distance3d, prune_extra_ss 65 66# columns for the torsions file dataframe. 67Torsion_DF_Cols = [ 68 "source", 69 "ss_id", 70 "proximal", 71 "distal", 72 "chi1", 73 "chi2", 74 "chi3", 75 "chi4", 76 "chi5", 77 "energy", 78 "ca_distance", 79 "cb_distance", 80 "phi_prox", 81 "psi_prox", 82 "phi_dist", 83 "psi_dist", 84 "torsion_length", 85 "rho", 86] 87 88 89# class for the Disulfide bond 90class Disulfide: 91 """ 92 This class provides a Python object and methods representing a physical disulfide bond 93 either extracted from the RCSB protein databank or built using the 94 [proteusPy.Turtle3D](turtle3D.html) class. The disulfide bond is an important 95 intramolecular stabilizing structural element and is characterized by: 96 97 * Atomic coordinates for the atoms N, Cα, Cβ, C', Sγ for both residues. 98 These are stored as both raw atomic coordinates as read from the RCSB file 99 and internal local coordinates. 100 * The dihedral angles Χ1 - Χ5 for the disulfide bond 101 * A name, by default {pdb_id}{prox_resnumb}{prox_chain}_{distal_resnum}{distal_chain} 102 * Proximal residue number 103 * Distal residue number 104 * Approximate bond torsional energy (kcal/mol): 105 106 $$ 107 E_{kcal/mol} \\approx 2.0 * cos(3.0 * \\chi_{1}) + cos(3.0 * \\chi_{5}) + cos(3.0 * \\chi_{2}) + 108 $$ 109 $$ 110 cos(3.0 * \chi_{4}) + 3.5 * cos(2.0 * \chi_{3}) + 0.6 * cos(3.0 * \chi_{3}) + 10.1 111 $$ 112 113 The equation embodies the typical 3-fold rotation barriers associated with single bonds, 114 (Χ1, Χ5, Χ2, Χ4) and a high 2-fold barrier for Χ3, resulting from the partial double bond 115 character of the S-S bond. This property leads to two major disulfide families, characterized 116 by the sign of Χ3. *Left-handed* disulfides have Χ3 < 0° and *right-handed* disulfides have 117 Χ3 > 0°. Within this breakdown there are numerous subfamilies, broadly known as the *hook*, 118 *spiral* and *staple*. These are under characgterization. 119 120 * Euclidean length of the dihedral angles (degrees) defined as: 121 $$\sqrt(\chi_{1}^{2} + \chi_{2}^{2} + \chi_{3}^{2} + \chi_{4}^{2} + \chi_{5}^{2})$$ 122 * Cα - Cα distance (Å) 123 * Cβ - Cβ distance (Å) 124 * The previous C' and next N for both the proximal and distal residues. These are needed 125 to calculate the backbone dihedral angles Φ and Ψ. 126 * Backbone dihedral angles Φ and Ψ, when possible. Not all structures are complete and 127 in those cases the atoms needed may be undefined. In this case the Φ and Ψ angles are set 128 to -180°. 129 130 The class also provides a rendering capabilities using the excellent [PyVista](https://pyvista.org) 131 library, and can display disulfides interactively in a variety of display styles: 132 * 'sb' - Split Bonds style - bonds colored by their atom type 133 * 'bs' - Ball and Stick style - split bond coloring with small atoms 134 * 'pd' - Proximal/Distal style - bonds colored *Red* for proximal residue and *Green* for 135 the distal residue. 136 * 'cpk' - CPK style rendering, colored by atom type: 137 * Carbon - Grey 138 * Nitrogen - Blue 139 * Sulfur - Yellow 140 * Oxygen - Red 141 * Hydrogen - White 142 143 Individual renderings can be saved to a file, and animations created. 144 """ 145 146 def __init__( 147 self, 148 name: str = "SSBOND", 149 proximal: int = -1, 150 distal: int = -1, 151 proximal_chain: str = "A", 152 distal_chain: str = "A", 153 pdb_id: str = "1egs", 154 quiet: bool = True, 155 permissive: bool = True, 156 ) -> None: 157 """ 158 __init__ Initialize the class to defined internal values. 159 160 :param name: Disulfide name, by default "SSBOND" 161 162 """ 163 self.name = name 164 self.proximal = proximal 165 self.distal = distal 166 self.energy = _FLOAT_INIT 167 self.proximal_chain = proximal_chain 168 self.distal_chain = distal_chain 169 self.pdb_id = pdb_id 170 self.proximal_residue_fullid = str("") 171 self.distal_residue_fullid = str("") 172 self.PERMISSIVE = permissive 173 self.QUIET = quiet 174 self.ca_distance = _FLOAT_INIT 175 self.cb_distance = _FLOAT_INIT 176 self.torsion_array = np.array( 177 (_ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT) 178 ) 179 self.phiprox = _ANG_INIT 180 self.psiprox = _ANG_INIT 181 self.phidist = _ANG_INIT 182 self.psidist = _ANG_INIT 183 184 # global coordinates for the Disulfide, typically as 185 # returned from the PDB file 186 187 self.n_prox = Vector(0, 0, 0) 188 self.ca_prox = Vector(0, 0, 0) 189 self.c_prox = Vector(0, 0, 0) 190 self.o_prox = Vector(0, 0, 0) 191 self.cb_prox = Vector(0, 0, 0) 192 self.sg_prox = Vector(0, 0, 0) 193 self.sg_dist = Vector(0, 0, 0) 194 self.cb_dist = Vector(0, 0, 0) 195 self.ca_dist = Vector(0, 0, 0) 196 self.n_dist = Vector(0, 0, 0) 197 self.c_dist = Vector(0, 0, 0) 198 self.o_dist = Vector(0, 0, 0) 199 200 # set when we can't find previous or next prox or distal 201 # C' or N atoms. 202 self.missing_atoms = False 203 self.modelled = False 204 self.resolution = -1.0 205 206 # need these to calculate backbone dihedral angles 207 self.c_prev_prox = Vector(0, 0, 0) 208 self.n_next_prox = Vector(0, 0, 0) 209 self.c_prev_dist = Vector(0, 0, 0) 210 self.n_next_dist = Vector(0, 0, 0) 211 212 # local coordinates for the Disulfide, computed using the Turtle3D in 213 # Orientation #1. these are generally private. 214 215 self._n_prox = Vector(0, 0, 0) 216 self._ca_prox = Vector(0, 0, 0) 217 self._c_prox = Vector(0, 0, 0) 218 self._o_prox = Vector(0, 0, 0) 219 self._cb_prox = Vector(0, 0, 0) 220 self._sg_prox = Vector(0, 0, 0) 221 self._sg_dist = Vector(0, 0, 0) 222 self._cb_dist = Vector(0, 0, 0) 223 self._ca_dist = Vector(0, 0, 0) 224 self._n_dist = Vector(0, 0, 0) 225 self._c_dist = Vector(0, 0, 0) 226 self._o_dist = Vector(0, 0, 0) 227 228 # need these to calculate backbone dihedral angles 229 self._c_prev_prox = Vector(0, 0, 0) 230 self._n_next_prox = Vector(0, 0, 0) 231 self._c_prev_dist = Vector(0, 0, 0) 232 self._n_next_dist = Vector(0, 0, 0) 233 234 # Dihedral angles for the disulfide bond itself, set to _ANG_INIT 235 self.chi1 = _ANG_INIT 236 self.chi2 = _ANG_INIT 237 self.chi3 = _ANG_INIT 238 self.chi4 = _ANG_INIT 239 self.chi5 = _ANG_INIT 240 self.rho = _ANG_INIT # new dihedral angle: Nprox - Ca_prox - Ca_dist - N_dist 241 242 self.torsion_length = _FLOAT_INIT 243 244 # comparison operators, used for sorting. keyed to SS bond energy 245 def __lt__(self, other): 246 if isinstance(other, Disulfide): 247 return self.energy < other.energy 248 249 def __le__(self, other): 250 if isinstance(other, Disulfide): 251 return self.energy <= other.energy 252 253 def __gt__(self, other): 254 if isinstance(other, Disulfide): 255 return self.energy > other.energy 256 257 def __ge__(self, other): 258 if isinstance(other, Disulfide): 259 return self.energy >= other.energy 260 261 def __eq__(self, other): 262 if isinstance(other, Disulfide): 263 return self.energy == other.energy 264 265 def __ne__(self, other): 266 if isinstance(other, Disulfide): 267 return self.energy != other.energy 268 269 def __repr__(self): 270 """ 271 Representation for the Disulfide class 272 """ 273 s1 = self.repr_ss_info() 274 res = f"{s1}>" 275 return res 276 277 def _render( 278 self, 279 pvplot: pv.Plotter, 280 style="bs", 281 plain=False, 282 bondcolor=BOND_COLOR, 283 bs_scale=BS_SCALE, 284 spec=SPECULARITY, 285 specpow=SPEC_POWER, 286 translate=True, 287 bond_radius=BOND_RADIUS, 288 res=100, 289 ): 290 """ 291 Update the passed pyVista plotter() object with the mesh data for the 292 input Disulfide Bond. Used internally 293 294 Parameters 295 ---------- 296 pvplot : pv.Plotter 297 pyvista.Plotter object 298 299 style : str, optional 300 Rendering style, by default 'bs'. One of 'bs', 'st', 'cpk', Render as \ 301 CPK, ball-and-stick or stick. Bonds are colored by atom color, unless \ 302 'plain' is specified. 303 304 plain : bool, optional 305 Used internally, by default False 306 307 bondcolor : pyVista color name, optional bond color for simple bonds, by default BOND_COLOR 308 309 bs_scale : float, optional 310 scale factor (0-1) to reduce the atom sizes for ball and stick, by default BS_SCALE 311 312 spec : float, optional 313 specularity (0-1), where 1 is totally smooth and 0 is rough, by default SPECULARITY 314 315 specpow : int, optional 316 exponent used for specularity calculations, by default SPEC_POWER 317 318 translate : bool, optional 319 Flag used internally to indicate if we should translate \ 320 the disulfide to its geometric center of mass, by default True. 321 322 Returns 323 ------- 324 pv.Plotter 325 Updated pv.Plotter object with atoms and bonds. 326 """ 327 328 _bradius = bond_radius 329 coords = self.internal_coords() 330 missing_atoms = self.missing_atoms 331 clen = coords.shape[0] 332 333 model = self.modelled 334 if model: 335 all_atoms = False 336 else: 337 all_atoms = True 338 339 if translate: 340 cofmass = self.cofmass() 341 for i in range(clen): 342 coords[i] = coords[i] - cofmass 343 344 atoms = ( 345 "N", 346 "C", 347 "C", 348 "O", 349 "C", 350 "SG", 351 "N", 352 "C", 353 "C", 354 "O", 355 "C", 356 "SG", 357 "C", 358 "N", 359 "C", 360 "N", 361 ) 362 pvp = pvplot 363 364 # bond connection table with atoms in the specific order shown above: 365 # returned by ss.get_internal_coords() 366 367 def draw_bonds( 368 pvp, 369 bradius=BOND_RADIUS, 370 style="sb", 371 bcolor=BOND_COLOR, 372 missing=True, 373 all_atoms=True, 374 res=100, 375 ): 376 """ 377 Generate the appropriate pyVista cylinder objects to represent 378 a particular disulfide bond. This utilizes a connection table 379 for the starting and ending atoms and a color table for the 380 bond colors. Used internally. 381 382 :param pvp: input plotter object to be updated 383 :param bradius: bond radius 384 :param style: bond style. One of sb, plain, pd 385 :param bcolor: pyvista color 386 :param missing: True if atoms are missing, False othersie 387 :param all_atoms: True if rendering O, False if only backbone rendered 388 389 :return pvp: Updated Plotter object. 390 391 """ 392 _bond_conn = np.array( 393 [ 394 [0, 1], # n-ca 395 [1, 2], # ca-c 396 [2, 3], # c-o 397 [1, 4], # ca-cb 398 [4, 5], # cb-sg 399 [6, 7], # n-ca 400 [7, 8], # ca-c 401 [8, 9], # c-o 402 [7, 10], # ca-cb 403 [10, 11], # cb-sg 404 [5, 11], # sg -sg 405 [12, 0], # cprev_prox-n 406 [2, 13], # c-nnext_prox 407 [14, 6], # cprev_dist-n_dist 408 [8, 15], # c-nnext_dist 409 ] 410 ) 411 412 # modeled disulfides only have backbone atoms since 413 # phi and psi are undefined, which makes the carbonyl 414 # oxygen (O) undefined as well. Their previous and next N 415 # are also undefined. 416 417 _bond_conn_backbone = np.array( 418 [ 419 [0, 1], # n-ca 420 [1, 2], # ca-c 421 [1, 4], # ca-cb 422 [4, 5], # cb-sg 423 [6, 7], # n-ca 424 [7, 8], # ca-c 425 [7, 10], # ca-cb 426 [10, 11], # cb-sg 427 [5, 11], # sg -sg 428 ] 429 ) 430 431 # colors for the bonds. Index into ATOM_COLORS array 432 _bond_split_colors = np.array( 433 [ 434 ("N", "C"), 435 ("C", "C"), 436 ("C", "O"), 437 ("C", "C"), 438 ("C", "SG"), 439 ("N", "C"), 440 ("C", "C"), 441 ("C", "O"), 442 ("C", "C"), 443 ("C", "SG"), 444 ("SG", "SG"), 445 # prev and next C-N bonds - color by atom Z 446 ("C", "N"), 447 ("C", "N"), 448 ("C", "N"), 449 ("C", "N"), 450 ] 451 ) 452 453 _bond_split_colors_backbone = np.array( 454 [ 455 ("N", "C"), 456 ("C", "C"), 457 ("C", "C"), 458 ("C", "SG"), 459 ("N", "C"), 460 ("C", "C"), 461 ("C", "C"), 462 ("C", "SG"), 463 ("SG", "SG"), 464 ] 465 ) 466 # work through connectivity and colors 467 orig_col = dest_col = bcolor 468 469 if all_atoms: 470 bond_conn = _bond_conn 471 bond_split_colors = _bond_split_colors 472 else: 473 bond_conn = _bond_conn_backbone 474 bond_split_colors = _bond_split_colors_backbone 475 476 for i in range(len(bond_conn)): 477 if all_atoms: 478 if i > 10 and missing_atoms == True: # skip missing atoms 479 continue 480 481 bond = bond_conn[i] 482 483 # get the indices for the origin and destination atoms 484 orig = bond[0] 485 dest = bond[1] 486 487 col = bond_split_colors[i] 488 489 # get the coords 490 prox_pos = coords[orig] 491 distal_pos = coords[dest] 492 493 # compute a direction vector 494 direction = distal_pos - prox_pos 495 496 # compute vector length. divide by 2 since split bond 497 height = math.dist(prox_pos, distal_pos) / 2.0 498 499 # the cylinder origins are actually in the 500 # middle so we translate 501 502 origin = prox_pos + 0.5 * direction # for a single plain bond 503 origin1 = prox_pos + 0.25 * direction 504 origin2 = prox_pos + 0.75 * direction 505 506 bradius = _bradius 507 508 if style == "plain": 509 orig_col = dest_col = bcolor 510 511 # proximal-distal red/green coloring 512 elif style == "pd": 513 if i <= 4 or i == 11 or i == 12: 514 orig_col = dest_col = "red" 515 else: 516 orig_col = dest_col = "green" 517 if i == 10: 518 orig_col = dest_col = "yellow" 519 else: 520 orig_col = ATOM_COLORS[col[0]] 521 dest_col = ATOM_COLORS[col[1]] 522 523 if i >= 11: # prev and next residue atoms for phi/psi calcs 524 bradius = _bradius * 0.5 # make smaller to distinguish 525 526 cap1 = pv.Sphere(center=prox_pos, radius=bradius) 527 cap2 = pv.Sphere(center=distal_pos, radius=bradius) 528 529 if style == "plain": 530 cyl = pv.Cylinder( 531 origin, direction, radius=bradius, height=height * 2.0 532 ) 533 pvp.add_mesh(cyl, color=orig_col) 534 else: 535 cyl1 = pv.Cylinder( 536 origin1, 537 direction, 538 radius=bradius, 539 height=height, 540 capping=False, 541 resolution=res, 542 ) 543 cyl2 = pv.Cylinder( 544 origin2, 545 direction, 546 radius=bradius, 547 height=height, 548 capping=False, 549 resolution=res, 550 ) 551 pvp.add_mesh(cyl1, color=orig_col) 552 pvp.add_mesh(cyl2, color=dest_col) 553 554 pvp.add_mesh(cap1, color=orig_col) 555 pvp.add_mesh(cap2, color=dest_col) 556 557 return pvp # end draw_bonds 558 559 if style == "cpk": 560 i = 0 561 for atom in atoms: 562 rad = ATOM_RADII_CPK[atom] 563 pvp.add_mesh( 564 pv.Sphere(center=coords[i], radius=rad), 565 color=ATOM_COLORS[atom], 566 smooth_shading=True, 567 specular=spec, 568 specular_power=specpow, 569 ) 570 i += 1 571 572 elif style == "cov": 573 i = 0 574 for atom in atoms: 575 rad = ATOM_RADII_COVALENT[atom] 576 pvp.add_mesh( 577 pv.Sphere(center=coords[i], radius=rad), 578 color=ATOM_COLORS[atom], 579 smooth_shading=True, 580 specular=spec, 581 specular_power=specpow, 582 ) 583 i += 1 584 585 elif style == "bs": # ball and stick 586 i = 0 587 for atom in atoms: 588 rad = ATOM_RADII_CPK[atom] * bs_scale 589 if i > 11: 590 rad = rad * 0.75 591 592 pvp.add_mesh( 593 pv.Sphere(center=coords[i], radius=rad), 594 color=ATOM_COLORS[atom], 595 smooth_shading=True, 596 specular=spec, 597 specular_power=specpow, 598 ) 599 i += 1 600 pvp = draw_bonds( 601 pvp, style="bs", missing=missing_atoms, all_atoms=all_atoms 602 ) 603 604 elif style == "sb": # splitbonds 605 pvp = draw_bonds( 606 pvp, style="sb", missing=missing_atoms, all_atoms=all_atoms 607 ) 608 609 elif style == "pd": # proximal-distal 610 pvp = draw_bonds( 611 pvp, style="pd", missing=missing_atoms, all_atoms=all_atoms 612 ) 613 614 else: # plain 615 pvp = draw_bonds( 616 pvp, 617 style="plain", 618 bcolor=bondcolor, 619 missing=missing_atoms, 620 all_atoms=all_atoms, 621 ) 622 623 return pvp 624 625 def _plot( 626 self, 627 pvplot, 628 style="bs", 629 plain=False, 630 bondcolor=BOND_COLOR, 631 bs_scale=BS_SCALE, 632 spec=SPECULARITY, 633 specpow=SPEC_POWER, 634 translate=True, 635 bond_radius=BOND_RADIUS, 636 res=100, 637 ): 638 """ 639 Update the passed pyVista plotter() object with the mesh data for the 640 input Disulfide Bond. Used internally 641 642 Parameters 643 ---------- 644 pvplot : pv.Plotter 645 pyvista.Plotter object 646 647 style : str, optional 648 Rendering style, by default 'bs'. One of 'bs', 'st', 'cpk', Render as \ 649 CPK, ball-and-stick or stick. Bonds are colored by atom color, unless \ 650 'plain' is specified. 651 652 plain : bool, optional 653 Used internally, by default False 654 655 bondcolor : pyVista color name, optional bond color for simple bonds, by default BOND_COLOR 656 657 bs_scale : float, optional 658 scale factor (0-1) to reduce the atom sizes for ball and stick, by default BS_SCALE 659 660 spec : float, optional 661 specularity (0-1), where 1 is totally smooth and 0 is rough, by default SPECULARITY 662 663 specpow : int, optional 664 exponent used for specularity calculations, by default SPEC_POWER 665 666 translate : bool, optional 667 Flag used internally to indicate if we should translate \ 668 the disulfide to its geometric center of mass, by default True. 669 670 Returns 671 ------- 672 pv.Plotter 673 Updated pv.Plotter object with atoms and bonds. 674 """ 675 676 _bradius = bond_radius 677 coords = self.internal_coords() 678 missing_atoms = self.missing_atoms 679 clen = coords.shape[0] 680 681 model = self.modelled 682 if model: 683 all_atoms = False 684 else: 685 all_atoms = True 686 687 if translate: 688 cofmass = self.cofmass() 689 for i in range(clen): 690 coords[i] = coords[i] - cofmass 691 692 atoms = ( 693 "N", 694 "C", 695 "C", 696 "O", 697 "C", 698 "SG", 699 "N", 700 "C", 701 "C", 702 "O", 703 "C", 704 "SG", 705 "C", 706 "N", 707 "C", 708 "N", 709 ) 710 pvp = pvplot.copy() 711 712 # bond connection table with atoms in the specific order shown above: 713 # returned by ss.get_internal_coords() 714 715 def draw_bonds( 716 pvp, 717 bradius=BOND_RADIUS, 718 style="sb", 719 bcolor=BOND_COLOR, 720 missing=True, 721 all_atoms=True, 722 res=100, 723 ): 724 """ 725 Generate the appropriate pyVista cylinder objects to represent 726 a particular disulfide bond. This utilizes a connection table 727 for the starting and ending atoms and a color table for the 728 bond colors. Used internally. 729 730 :param pvp: input plotter object to be updated 731 :param bradius: bond radius 732 :param style: bond style. One of sb, plain, pd 733 :param bcolor: pyvista color 734 :param missing: True if atoms are missing, False othersie 735 :param all_atoms: True if rendering O, False if only backbone rendered 736 737 :return pvp: Updated Plotter object. 738 739 """ 740 _bond_conn = np.array( 741 [ 742 [0, 1], # n-ca 743 [1, 2], # ca-c 744 [2, 3], # c-o 745 [1, 4], # ca-cb 746 [4, 5], # cb-sg 747 [6, 7], # n-ca 748 [7, 8], # ca-c 749 [8, 9], # c-o 750 [7, 10], # ca-cb 751 [10, 11], # cb-sg 752 [5, 11], # sg -sg 753 [12, 0], # cprev_prox-n 754 [2, 13], # c-nnext_prox 755 [14, 6], # cprev_dist-n_dist 756 [8, 15], # c-nnext_dist 757 ] 758 ) 759 760 # modeled disulfides only have backbone atoms since 761 # phi and psi are undefined, which makes the carbonyl 762 # oxygen (O) undefined as well. Their previous and next N 763 # are also undefined. 764 765 _bond_conn_backbone = np.array( 766 [ 767 [0, 1], # n-ca 768 [1, 2], # ca-c 769 [1, 4], # ca-cb 770 [4, 5], # cb-sg 771 [6, 7], # n-ca 772 [7, 8], # ca-c 773 [7, 10], # ca-cb 774 [10, 11], # cb-sg 775 [5, 11], # sg -sg 776 ] 777 ) 778 779 # colors for the bonds. Index into ATOM_COLORS array 780 _bond_split_colors = np.array( 781 [ 782 ("N", "C"), 783 ("C", "C"), 784 ("C", "O"), 785 ("C", "C"), 786 ("C", "SG"), 787 ("N", "C"), 788 ("C", "C"), 789 ("C", "O"), 790 ("C", "C"), 791 ("C", "SG"), 792 ("SG", "SG"), 793 # prev and next C-N bonds - color by atom Z 794 ("C", "N"), 795 ("C", "N"), 796 ("C", "N"), 797 ("C", "N"), 798 ] 799 ) 800 801 _bond_split_colors_backbone = np.array( 802 [ 803 ("N", "C"), 804 ("C", "C"), 805 ("C", "C"), 806 ("C", "SG"), 807 ("N", "C"), 808 ("C", "C"), 809 ("C", "C"), 810 ("C", "SG"), 811 ("SG", "SG"), 812 ] 813 ) 814 # work through connectivity and colors 815 orig_col = dest_col = bcolor 816 817 if all_atoms: 818 bond_conn = _bond_conn 819 bond_split_colors = _bond_split_colors 820 else: 821 bond_conn = _bond_conn_backbone 822 bond_split_colors = _bond_split_colors_backbone 823 824 for i in range(len(bond_conn)): 825 if all_atoms: 826 if i > 10 and missing_atoms == True: # skip missing atoms 827 continue 828 829 bond = bond_conn[i] 830 831 # get the indices for the origin and destination atoms 832 orig = bond[0] 833 dest = bond[1] 834 835 col = bond_split_colors[i] 836 837 # get the coords 838 prox_pos = coords[orig] 839 distal_pos = coords[dest] 840 841 # compute a direction vector 842 direction = distal_pos - prox_pos 843 844 # compute vector length. divide by 2 since split bond 845 height = math.dist(prox_pos, distal_pos) / 2.0 846 847 # the cylinder origins are actually in the 848 # middle so we translate 849 850 origin = prox_pos + 0.5 * direction # for a single plain bond 851 origin1 = prox_pos + 0.25 * direction 852 origin2 = prox_pos + 0.75 * direction 853 854 bradius = _bradius 855 856 if style == "plain": 857 orig_col = dest_col = bcolor 858 859 # proximal-distal red/green coloring 860 elif style == "pd": 861 if i <= 4 or i == 11 or i == 12: 862 orig_col = dest_col = "red" 863 else: 864 orig_col = dest_col = "green" 865 if i == 10: 866 orig_col = dest_col = "yellow" 867 else: 868 orig_col = ATOM_COLORS[col[0]] 869 dest_col = ATOM_COLORS[col[1]] 870 871 if i >= 11: # prev and next residue atoms for phi/psi calcs 872 bradius = _bradius * 0.5 # make smaller to distinguish 873 874 cap1 = pv.Sphere(center=prox_pos, radius=bradius) 875 cap2 = pv.Sphere(center=distal_pos, radius=bradius) 876 877 if style == "plain": 878 cyl = pv.Cylinder( 879 origin, direction, radius=bradius, height=height * 2.0 880 ) 881 # pvp.add_mesh(cyl, color=orig_col) 882 pvp.append(cyl) 883 else: 884 cyl1 = pv.Cylinder( 885 origin1, 886 direction, 887 radius=bradius, 888 height=height, 889 capping=False, 890 resolution=res, 891 ) 892 cyl2 = pv.Cylinder( 893 origin2, 894 direction, 895 radius=bradius, 896 height=height, 897 capping=False, 898 resolution=res, 899 ) 900 # pvp.add_mesh(cyl1, color=orig_col) 901 # pvp.add_mesh(cyl2, color=dest_col) 902 pvp.append(cyl1) 903 pvp.append(cyl2) 904 905 # pvp.add_mesh(cap1, color=orig_col) 906 # pvp.add_mesh(cap2, color=dest_col) 907 pvp.append(cap1) 908 pvp.append(cap2) 909 910 return pvp.copy() # end draw_bonds 911 912 if style == "cpk": 913 i = 0 914 for atom in atoms: 915 rad = ATOM_RADII_CPK[atom] 916 pvp.append(pv.Sphere(center=coords[i], radius=rad)) 917 i += 1 918 919 elif style == "cov": 920 i = 0 921 for atom in atoms: 922 rad = ATOM_RADII_COVALENT[atom] 923 pvp.append(pv.Sphere(center=coords[i], radius=rad)) 924 i += 1 925 926 elif style == "bs": # ball and stick 927 i = 0 928 for atom in atoms: 929 rad = ATOM_RADII_CPK[atom] * bs_scale 930 if i > 11: 931 rad = rad * 0.75 932 933 pvp.append(pv.Sphere(center=coords[i])) 934 i += 1 935 pvp = draw_bonds( 936 pvp, style="bs", missing=missing_atoms, all_atoms=all_atoms 937 ) 938 939 elif style == "sb": # splitbonds 940 pvp = draw_bonds( 941 pvp, style="sb", missing=missing_atoms, all_atoms=all_atoms 942 ) 943 944 elif style == "pd": # proximal-distal 945 pvp = draw_bonds( 946 pvp, style="pd", missing=missing_atoms, all_atoms=all_atoms 947 ) 948 949 else: # plain 950 pvp = draw_bonds( 951 pvp, 952 style="plain", 953 bcolor=bondcolor, 954 missing=missing_atoms, 955 all_atoms=all_atoms, 956 ) 957 958 return 959 960 def _handle_SS_exception(self, message: str): 961 """ 962 This method catches an exception that occurs in the Disulfide 963 object (if PERMISSIVE), or raises it again, this time adding the 964 PDB line number to the error message. (private). 965 966 :param message: Error message 967 :raises DisulfideConstructionException: Fatal construction exception. 968 969 """ 970 # message = "%s at line %i." % (message) 971 message = f"{message}" 972 973 if self.PERMISSIVE: 974 # just print a warning - some residues/atoms may be missing 975 warnings.warn( 976 "DisulfideConstructionException: %s\n" 977 "Exception ignored.\n" 978 "Some atoms may be missing in the data structure." % message, 979 DisulfideConstructionWarning, 980 ) 981 else: 982 # exceptions are fatal - raise again with new message (including line nr) 983 raise DisulfideConstructionException(message) from None 984 985 @property 986 def dihedrals(self) -> list: 987 """ 988 Return a list containing the dihedral angles for the disulfide. 989 990 """ 991 return [self.chi1, self.chi2, self.chi3, self.chi4, self.chi5] 992 993 @dihedrals.setter 994 def dihedrals(self, dihedrals: list) -> None: 995 """ 996 Sets the disulfide dihedral angles to the inputs specified in the list. 997 998 :param dihedrals: list of dihedral angles. 999 """ 1000 self.chi1 = dihedrals[0] 1001 self.chi2 = dihedrals[1] 1002 self.chi3 = dihedrals[2] 1003 self.chi4 = dihedrals[3] 1004 self.chi5 = dihedrals[4] 1005 self.compute_torsional_energy() 1006 self.compute_torsion_length() 1007 1008 def bounding_box(self) -> np.array: 1009 """ 1010 Return the bounding box array for the given disulfide 1011 1012 Returns 1013 ------- 1014 :return: np.Array(3,2): Array containing the min, max for X, Y, and Z respectively. 1015 Does not currently take the atom's radius into account. 1016 1017 """ 1018 res = np.zeros(shape=(3, 2)) 1019 xmin, xmax = self.compute_extents("x") 1020 ymin, ymax = self.compute_extents("y") 1021 zmin, zmax = self.compute_extents("z") 1022 1023 res[0] = [xmin, xmax] 1024 res[1] = [ymin, ymax] 1025 res[2] = [zmin, zmax] 1026 1027 return res 1028 1029 def build_yourself(self) -> None: 1030 """ 1031 Build a model Disulfide based its internal dihedral state 1032 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1033 Cb, with N on left), builds disulfide, and updates the object's internal 1034 coordinates. It also adds the distal protein backbone, 1035 and computes the disulfide conformational energy. 1036 """ 1037 chi1 = self.chi1 1038 chi2 = self.chi2 1039 chi3 = self.chi3 1040 chi4 = self.chi4 1041 chi5 = self.chi5 1042 self.build_model(chi1, chi2, chi3, chi4, chi5) 1043 1044 def build_model( 1045 self, chi1: float, chi2: float, chi3: float, chi4: float, chi5: float 1046 ) -> None: 1047 """ 1048 Build a model Disulfide based on the input dihedral angles. 1049 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1050 Cb, with N on left), builds disulfide, and updates the object's internal 1051 coordinates. It also adds the distal protein backbone, 1052 and computes the disulfide conformational energy. 1053 1054 :param chi1: Chi1 (degrees) 1055 :param chi2: Chi2 (degrees) 1056 :param chi3: Chi3 (degrees) 1057 :param chi4: Chi4 (degrees) 1058 :param chi5: Chi5 (degrees) 1059 1060 Example: 1061 >>> from proteusPy.Disulfide import Disulfide 1062 >>> modss = Disulfide('model') 1063 >>> modss.build_model(-60, -60, -90, -60, -60) 1064 >>> modss.display(style='sb') 1065 """ 1066 1067 self.set_dihedrals(chi1, chi2, chi3, chi4, chi5) 1068 self.proximal = 1 1069 self.distal = 2 1070 1071 tmp = Turtle3D("tmp") 1072 tmp.Orientation = 1 1073 1074 n = Vector(0, 0, 0) 1075 ca = Vector(0, 0, 0) 1076 cb = Vector(0, 0, 0) 1077 c = Vector(0, 0, 0) 1078 1079 self.ca_prox = tmp._position 1080 tmp.schain_to_bbone() 1081 n, ca, cb, c = build_residue(tmp) 1082 1083 self.n_prox = n 1084 self.ca_prox = ca 1085 self.c_prox = c 1086 self.cb_prox = cb 1087 1088 tmp.bbone_to_schain() 1089 tmp.move(1.53) 1090 tmp.roll(self.chi1) 1091 tmp.yaw(112.8) 1092 self.cb_prox = Vector(tmp._position) 1093 1094 tmp.move(1.86) 1095 tmp.roll(self.chi2) 1096 tmp.yaw(103.8) 1097 self.sg_prox = Vector(tmp._position) 1098 1099 tmp.move(2.044) 1100 tmp.roll(self.chi3) 1101 tmp.yaw(103.8) 1102 self.sg_dist = Vector(tmp._position) 1103 1104 tmp.move(1.86) 1105 tmp.roll(self.chi4) 1106 tmp.yaw(112.8) 1107 self.cb_dist = Vector(tmp._position) 1108 1109 tmp.move(1.53) 1110 tmp.roll(self.chi5) 1111 tmp.pitch(180.0) 1112 1113 tmp.schain_to_bbone() 1114 1115 n, ca, cb, c = build_residue(tmp) 1116 1117 self.n_dist = n 1118 self.ca_dist = ca 1119 self.c_dist = c 1120 self.compute_torsional_energy() 1121 self.compute_local_coords() 1122 self.ca_distance = distance3d(self.ca_prox, self.ca_dist) 1123 self.cb_distance = distance3d(self.cb_prox, self.cb_dist) 1124 self.torsion_array = np.array( 1125 (self.chi1, self.chi2, self.chi3, self.chi4, self.chi5) 1126 ) 1127 self.compute_torsion_length() 1128 self.compute_rho() 1129 self.missing_atoms = True 1130 self.modelled = True 1131 1132 def cofmass(self) -> np.array: 1133 """ 1134 Return the geometric center of mass for the internal coordinates of 1135 the given Disulfide. Missing atoms are not included. 1136 1137 :return: 3D array for the geometric center of mass 1138 """ 1139 1140 res = self.internal_coords() 1141 return res.mean(axis=0) 1142 1143 def copy(self): 1144 """ 1145 Copy the Disulfide. 1146 1147 :return: A copy of self. 1148 """ 1149 return copy.deepcopy(self) 1150 1151 def compute_extents(self, dim="z"): 1152 """ 1153 Calculate the internal coordinate extents for the input axis. 1154 1155 :param dim: Axis, one of 'x', 'y', 'z', by default 'z' 1156 :return: min, max 1157 """ 1158 1159 ic = self.internal_coords() 1160 # set default index to 'z' 1161 idx = 2 1162 1163 if dim == "x": 1164 idx = 0 1165 elif dim == "y": 1166 idx = 1 1167 elif dim == "z": 1168 idx = 2 1169 1170 _min = ic[:, idx].min() 1171 _max = ic[:, idx].max() 1172 return _min, _max 1173 1174 def compute_local_coords(self) -> None: 1175 """ 1176 Compute the internal coordinates for a properly initialized Disulfide Object. 1177 1178 :param self: SS initialized Disulfide object 1179 :returns: None, modifies internal state of the input 1180 """ 1181 1182 turt = Turtle3D("tmp") 1183 # get the coordinates as np.array for Turtle3D use. 1184 cpp = self.c_prev_prox.get_array() 1185 nnp = self.n_next_prox.get_array() 1186 1187 n = self.n_prox.get_array() 1188 ca = self.ca_prox.get_array() 1189 c = self.c_prox.get_array() 1190 cb = self.cb_prox.get_array() 1191 o = self.o_prox.get_array() 1192 sg = self.sg_prox.get_array() 1193 1194 sg2 = self.sg_dist.get_array() 1195 cb2 = self.cb_dist.get_array() 1196 ca2 = self.ca_dist.get_array() 1197 c2 = self.c_dist.get_array() 1198 n2 = self.n_dist.get_array() 1199 o2 = self.o_dist.get_array() 1200 1201 cpd = self.c_prev_dist.get_array() 1202 nnd = self.n_next_dist.get_array() 1203 1204 turt.orient_from_backbone(n, ca, c, cb, ORIENT_SIDECHAIN) 1205 1206 # internal (local) coordinates, stored as Vector objects 1207 # to_local returns np.array objects 1208 1209 self._n_prox = Vector(turt.to_local(n)) 1210 self._ca_prox = Vector(turt.to_local(ca)) 1211 self._c_prox = Vector(turt.to_local(c)) 1212 self._o_prox = Vector(turt.to_local(o)) 1213 self._cb_prox = Vector(turt.to_local(cb)) 1214 self._sg_prox = Vector(turt.to_local(sg)) 1215 1216 self._c_prev_prox = Vector(turt.to_local(cpp)) 1217 self._n_next_prox = Vector(turt.to_local(nnp)) 1218 self._c_prev_dist = Vector(turt.to_local(cpd)) 1219 self._n_next_dist = Vector(turt.to_local(nnd)) 1220 1221 self._n_dist = Vector(turt.to_local(n2)) 1222 self._ca_dist = Vector(turt.to_local(ca2)) 1223 self._c_dist = Vector(turt.to_local(c2)) 1224 self._o_dist = Vector(turt.to_local(o2)) 1225 self._cb_dist = Vector(turt.to_local(cb2)) 1226 self._sg_dist = Vector(turt.to_local(sg2)) 1227 1228 def compute_torsional_energy(self) -> float: 1229 """ 1230 Compute the approximate torsional energy for the Disulfide's 1231 conformation and sets its internal state. 1232 1233 :return: Energy (kcal/mol) 1234 """ 1235 # @TODO find citation for the ss bond energy calculation 1236 1237 def torad(deg): 1238 return np.radians(deg) 1239 1240 chi1 = self.chi1 1241 chi2 = self.chi2 1242 chi3 = self.chi3 1243 chi4 = self.chi4 1244 chi5 = self.chi5 1245 1246 energy = 2.0 * (cos(torad(3.0 * chi1)) + cos(torad(3.0 * chi5))) 1247 energy += cos(torad(3.0 * chi2)) + cos(torad(3.0 * chi4)) 1248 energy += 3.5 * cos(torad(2.0 * chi3)) + 0.6 * cos(torad(3.0 * chi3)) + 10.1 1249 1250 self.energy = energy 1251 return energy 1252 1253 def display(self, single=True, style="sb", light=True, shadows=False) -> None: 1254 """ 1255 Display the Disulfide bond in the specific rendering style. 1256 1257 :param single: Display the bond in a single panel in the specific style. 1258 :param style: Rendering style: One of: 1259 * 'sb' - split bonds 1260 * 'bs' - ball and stick 1261 * 'cpk' - CPK style 1262 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1263 * 'plain' - boring single color 1264 :param light: If True, light background, if False, dark 1265 1266 Example: 1267 >>> import proteusPy 1268 >>> from proteusPy.Disulfide import Disulfide 1269 >>> from proteusPy.DisulfideLoader import DisulfideLoader, Load_PDB_SS 1270 1271 >>> PDB_SS = Load_PDB_SS(verbose=False, subset=True) 1272 >>> ss = PDB_SS[0] 1273 >>> ss.display(style='cpk') 1274 >>> ss.screenshot(style='bs', fname='proteus_logo_sb.png') 1275 """ 1276 src = self.pdb_id 1277 enrg = self.energy 1278 1279 title = f"{src}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol. Cα: {self.ca_distance:.2f} Å Cβ: {self.cb_distance:.2f} Å Tors: {self.torsion_length:.2f}°" 1280 1281 if light: 1282 pv.set_plot_theme("document") 1283 else: 1284 pv.set_plot_theme("dark") 1285 1286 if single == True: 1287 _pl = pv.Plotter(window_size=WINSIZE) 1288 _pl.add_title(title=title, font_size=FONTSIZE) 1289 _pl.enable_anti_aliasing("msaa") 1290 # _pl.add_camera_orientation_widget() 1291 1292 self._render( 1293 _pl, 1294 style=style, 1295 bs_scale=BS_SCALE, 1296 spec=SPECULARITY, 1297 specpow=SPEC_POWER, 1298 ) 1299 _pl.reset_camera() 1300 if shadows == True: 1301 _pl.enable_shadows() 1302 _pl.show() 1303 1304 else: 1305 pl = pv.Plotter(window_size=WINSIZE, shape=(2, 2)) 1306 pl.subplot(0, 0) 1307 1308 pl.add_title(title=title, font_size=FONTSIZE) 1309 pl.enable_anti_aliasing("msaa") 1310 1311 # pl.add_camera_orientation_widget() 1312 1313 self._render( 1314 pl, 1315 style="cpk", 1316 bondcolor=BOND_COLOR, 1317 bs_scale=BS_SCALE, 1318 spec=SPECULARITY, 1319 specpow=SPEC_POWER, 1320 ) 1321 1322 pl.subplot(0, 1) 1323 pl.add_title(title=title, font_size=FONTSIZE) 1324 1325 self._render( 1326 pl, 1327 style="bs", 1328 bondcolor=BOND_COLOR, 1329 bs_scale=BS_SCALE, 1330 spec=SPECULARITY, 1331 specpow=SPEC_POWER, 1332 ) 1333 1334 pl.subplot(1, 0) 1335 pl.add_title(title=title, font_size=FONTSIZE) 1336 1337 self._render( 1338 pl, 1339 style="sb", 1340 bondcolor=BOND_COLOR, 1341 bs_scale=BS_SCALE, 1342 spec=SPECULARITY, 1343 specpow=SPEC_POWER, 1344 ) 1345 1346 pl.subplot(1, 1) 1347 pl.add_title(title=title, font_size=FONTSIZE) 1348 1349 self._render( 1350 pl, 1351 style="pd", 1352 bondcolor=BOND_COLOR, 1353 bs_scale=BS_SCALE, 1354 spec=SPECULARITY, 1355 specpow=SPEC_POWER, 1356 ) 1357 1358 pl.link_views() 1359 pl.reset_camera() 1360 if shadows == True: 1361 pl.enable_shadows() 1362 pl.show() 1363 return 1364 1365 def plot( 1366 self, pl, single=True, style="sb", light=True, shadows=False 1367 ) -> pv.Plotter: 1368 """ 1369 Return the pyVista Plotter object for the Disulfide bond in the specific rendering style. 1370 1371 :param single: Display the bond in a single panel in the specific style. 1372 :param style: Rendering style: One of: 1373 * 'sb' - split bonds 1374 * 'bs' - ball and stick 1375 * 'cpk' - CPK style 1376 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1377 * 'plain' - boring single color 1378 :param light: If True, light background, if False, dark 1379 """ 1380 src = self.pdb_id 1381 enrg = self.energy 1382 1383 title = f"{src}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol. Cα: {self.ca_distance:.2f} Å Cβ: {self.cb_distance:.2f} Å Tors: {self.torsion_length:.2f}°" 1384 1385 if light: 1386 pv.set_plot_theme("document") 1387 else: 1388 pv.set_plot_theme("dark") 1389 1390 if single == True: 1391 # _pl = pv.Plotter(window_size=WINSIZE) 1392 # _pl.add_title(title=title, font_size=FONTSIZE) 1393 pl.clear() 1394 pl.enable_anti_aliasing("msaa") 1395 # pl.add_camera_orientation_widget() 1396 1397 self._render( 1398 pl, 1399 style=style, 1400 bs_scale=BS_SCALE, 1401 spec=SPECULARITY, 1402 specpow=SPEC_POWER, 1403 ) 1404 pl.reset_camera() 1405 if shadows == True: 1406 pl.enable_shadows() 1407 else: 1408 pl = pv.Plotter(shape=(2, 2)) 1409 pl.subplot(0, 0) 1410 1411 # pl.add_title(title=title, font_size=FONTSIZE) 1412 pl.enable_anti_aliasing("msaa") 1413 1414 # pl.add_camera_orientation_widget() 1415 1416 self._render( 1417 pl, 1418 style="cpk", 1419 bondcolor=BOND_COLOR, 1420 bs_scale=BS_SCALE, 1421 spec=SPECULARITY, 1422 specpow=SPEC_POWER, 1423 ) 1424 1425 pl.subplot(0, 1) 1426 1427 self._render( 1428 pl, 1429 style="bs", 1430 bondcolor=BOND_COLOR, 1431 bs_scale=BS_SCALE, 1432 spec=SPECULARITY, 1433 specpow=SPEC_POWER, 1434 ) 1435 1436 pl.subplot(1, 0) 1437 1438 self._render( 1439 pl, 1440 style="sb", 1441 bondcolor=BOND_COLOR, 1442 bs_scale=BS_SCALE, 1443 spec=SPECULARITY, 1444 specpow=SPEC_POWER, 1445 ) 1446 1447 pl.subplot(1, 1) 1448 self._render( 1449 pl, 1450 style="pd", 1451 bondcolor=BOND_COLOR, 1452 bs_scale=BS_SCALE, 1453 spec=SPECULARITY, 1454 specpow=SPEC_POWER, 1455 ) 1456 1457 pl.link_views() 1458 pl.reset_camera() 1459 if shadows == True: 1460 pl.enable_shadows() 1461 return pl 1462 1463 def Distance_neighbors(self, others: DisulfideList, cutoff: float) -> DisulfideList: 1464 """ 1465 Return list of Disulfides whose RMS atomic distance is within 1466 the cutoff (Å) in the others list. 1467 1468 :param others: DisulfideList to search 1469 :param cutoff: Distance cutoff (Å) 1470 :return: DisulfideList within the cutoff 1471 """ 1472 1473 res = [ss.copy() for ss in others if self.Distance_RMS(ss) < cutoff] 1474 return DisulfideList(res, "neighbors") 1475 1476 def Distance_RMS(self, other) -> float: 1477 """ 1478 Calculate the RMS distance between the internal coordinates of self and another Disulfide. 1479 :param other: Comparison Disulfide 1480 :return: RMS distance (Å) 1481 """ 1482 1483 # Get internal coordinates of both objects 1484 ic1 = self.internal_coords() 1485 ic2 = other.internal_coords() 1486 1487 # Compute the sum of squared differences between corresponding internal coordinates 1488 totsq = sum(math.dist(p1, p2) ** 2 for p1, p2 in zip(ic1, ic2)) 1489 1490 # Compute the mean of the squared distances 1491 totsq /= len(ic1) 1492 1493 # Take the square root of the mean to get the RMS distance 1494 return math.sqrt(totsq) 1495 1496 def Torsion_RMS(self, other) -> float: 1497 """ 1498 Calculate the RMS distance between the dihedral angles of self and another Disulfide. 1499 :param other: Comparison Disulfide 1500 :return: RMS distance (deg). 1501 """ 1502 import math 1503 1504 # Get internal coordinates of both objects 1505 ic1 = self.torsion_array 1506 ic2 = other.torsion_array 1507 1508 # Compute the sum of squared differences between corresponding internal coordinates 1509 totsq = sum((p1 - p2) ** 2 for p1, p2 in zip(ic1, ic2)) 1510 # Compute the mean of the squared distances 1511 totsq /= len(ic1) 1512 1513 # Take the square root of the mean to get the RMS distance 1514 return math.sqrt(totsq) 1515 1516 def get_chains(self) -> tuple: 1517 """ 1518 Return the proximal and distal chain IDs for the Disulfide. 1519 1520 :return: tuple (proximal, distal) chain IDs 1521 """ 1522 prox = self.proximal_chain 1523 dist = self.distal_chain 1524 return tuple(prox, dist) 1525 1526 def get_permissive(self) -> bool: 1527 """ 1528 Return the Permissive flag state. (Used in PDB parsing) 1529 1530 :return: Permissive state 1531 """ 1532 return self.PERMISIVE 1533 1534 def get_full_id(self) -> tuple: 1535 """ 1536 Return the Disulfide full IDs (Used with BIO.PDB) 1537 1538 :return: Disulfide full IDs 1539 """ 1540 return (self.proximal_residue_fullid, self.distal_residue_fullid) 1541 1542 def initialize_disulfide_from_chain( 1543 self, chain1, chain2, proximal, distal, resolution, quiet=True 1544 ) -> None: 1545 """ 1546 Initialize a new Disulfide object with atomic coordinates from 1547 the proximal and distal coordinates, typically taken from a PDB file. 1548 This routine is primarily used internally when building the compressed 1549 database. 1550 1551 :param chain1: list of Residues in the model, eg: chain = model['A'] 1552 :param chain2: list of Residues in the model, eg: chain = model['A'] 1553 :param proximal: proximal residue sequence ID 1554 :param distal: distal residue sequence ID 1555 :param resolution: structure resolution 1556 :param quiet: Quiet or noisy parsing, defaults to True 1557 :raises DisulfideConstructionWarning: Raised when not parsed correctly 1558 """ 1559 id = chain1.get_full_id()[0] 1560 self.pdb_id = id 1561 1562 chi1 = chi2 = chi3 = chi4 = chi5 = _ANG_INIT 1563 1564 prox = int(proximal) 1565 dist = int(distal) 1566 1567 prox_residue = chain1[prox] 1568 dist_residue = chain2[dist] 1569 1570 if prox_residue.get_resname() != "CYS" or dist_residue.get_resname() != "CYS": 1571 print( 1572 f"build_disulfide() requires CYS at both residues: {prox} {prox_residue.get_resname()} {dist} {dist_residue.get_resname()} Chain: {prox_residue.get_segid()}" 1573 ) 1574 1575 # set the objects proximal and distal values 1576 self.set_resnum(proximal, distal) 1577 1578 if resolution is not None: 1579 self.resolution = resolution 1580 1581 self.proximal_chain = chain1.get_id() 1582 self.distal_chain = chain2.get_id() 1583 1584 self.proximal_residue_fullid = prox_residue.get_full_id() 1585 self.distal_residue_fullid = dist_residue.get_full_id() 1586 1587 if quiet: 1588 warnings.filterwarnings("ignore", category=DisulfideConstructionWarning) 1589 else: 1590 warnings.simplefilter("always") 1591 1592 # grab the coordinates for the proximal and distal residues as vectors 1593 # so we can do math on them later 1594 # proximal residue 1595 1596 try: 1597 n1 = prox_residue["N"].get_vector() 1598 ca1 = prox_residue["CA"].get_vector() 1599 c1 = prox_residue["C"].get_vector() 1600 o1 = prox_residue["O"].get_vector() 1601 cb1 = prox_residue["CB"].get_vector() 1602 sg1 = prox_residue["SG"].get_vector() 1603 1604 except Exception: 1605 raise DisulfideConstructionWarning( 1606 f"Invalid or missing coordinates for proximal residue {proximal}" 1607 ) from None 1608 1609 # distal residue 1610 try: 1611 n2 = dist_residue["N"].get_vector() 1612 ca2 = dist_residue["CA"].get_vector() 1613 c2 = dist_residue["C"].get_vector() 1614 o2 = dist_residue["O"].get_vector() 1615 cb2 = dist_residue["CB"].get_vector() 1616 sg2 = dist_residue["SG"].get_vector() 1617 1618 except Exception: 1619 raise DisulfideConstructionWarning( 1620 f"Invalid or missing coordinates for distal residue {distal}" 1621 ) from None 1622 1623 # previous residue and next residue - optional, used for phi, psi calculations 1624 try: 1625 prevprox = chain1[prox - 1] 1626 nextprox = chain1[prox + 1] 1627 1628 prevdist = chain2[dist - 1] 1629 nextdist = chain2[dist + 1] 1630 1631 cprev_prox = prevprox["C"].get_vector() 1632 nnext_prox = nextprox["N"].get_vector() 1633 1634 cprev_dist = prevdist["C"].get_vector() 1635 nnext_dist = nextdist["N"].get_vector() 1636 1637 # compute phi, psi for prox and distal 1638 self.phiprox = np.degrees(calc_dihedral(cprev_prox, n1, ca1, c1)) 1639 self.psiprox = np.degrees(calc_dihedral(n1, ca1, c1, nnext_prox)) 1640 self.phidist = np.degrees(calc_dihedral(cprev_dist, n2, ca2, c2)) 1641 self.psidist = np.degrees(calc_dihedral(n2, ca2, c2, nnext_dist)) 1642 1643 except Exception: 1644 mess = f"Missing coords for: {id} {prox-1} or {dist+1} for SS {proximal}-{distal}" 1645 cprev_prox = nnext_prox = cprev_dist = nnext_dist = Vector(-1.0, -1.0, -1.0) 1646 self.missing_atoms = True 1647 warnings.warn(mess, DisulfideConstructionWarning) 1648 1649 # update the positions and conformation 1650 self.set_positions( 1651 n1, 1652 ca1, 1653 c1, 1654 o1, 1655 cb1, 1656 sg1, 1657 n2, 1658 ca2, 1659 c2, 1660 o2, 1661 cb2, 1662 sg2, 1663 cprev_prox, 1664 nnext_prox, 1665 cprev_dist, 1666 nnext_dist, 1667 ) 1668 1669 # calculate and set the disulfide dihedral angles 1670 self.chi1 = np.degrees(calc_dihedral(n1, ca1, cb1, sg1)) 1671 self.chi2 = np.degrees(calc_dihedral(ca1, cb1, sg1, sg2)) 1672 self.chi3 = np.degrees(calc_dihedral(cb1, sg1, sg2, cb2)) 1673 self.chi4 = np.degrees(calc_dihedral(sg1, sg2, cb2, ca2)) 1674 self.chi5 = np.degrees(calc_dihedral(sg2, cb2, ca2, n2)) 1675 self.rho = np.degrees(calc_dihedral(n1, ca1, ca2, n2)) 1676 1677 self.ca_distance = distance3d(self.ca_prox, self.ca_dist) 1678 self.cb_distance = distance3d(self.cb_prox, self.cb_dist) 1679 self.torsion_array = np.array( 1680 (self.chi1, self.chi2, self.chi3, self.chi4, self.chi5) 1681 ) 1682 self.compute_torsion_length() 1683 1684 # calculate and set the SS bond torsional energy 1685 self.compute_torsional_energy() 1686 1687 # compute and set the local coordinates 1688 self.compute_local_coords() 1689 1690 def internal_coords(self) -> np.array: 1691 """ 1692 Return the internal coordinates for the Disulfide. 1693 If there are missing atoms the extra atoms for the proximal 1694 and distal N and C are set to [0,0,0]. This is needed for the center of 1695 mass calculations, used when rendering. 1696 1697 :return: Array containing the coordinates, [16][3]. 1698 """ 1699 1700 # if we don't have the prior and next atoms we initialize those 1701 # atoms to the origin so as to not effect the center of mass calculations 1702 if self.missing_atoms: 1703 res_array = np.array( 1704 ( 1705 self._n_prox.get_array(), 1706 self._ca_prox.get_array(), 1707 self._c_prox.get_array(), 1708 self._o_prox.get_array(), 1709 self._cb_prox.get_array(), 1710 self._sg_prox.get_array(), 1711 self._n_dist.get_array(), 1712 self._ca_dist.get_array(), 1713 self._c_dist.get_array(), 1714 self._o_dist.get_array(), 1715 self._cb_dist.get_array(), 1716 self._sg_dist.get_array(), 1717 [0, 0, 0], 1718 [0, 0, 0], 1719 [0, 0, 0], 1720 [0, 0, 0], 1721 ) 1722 ) 1723 else: 1724 res_array = np.array( 1725 ( 1726 self._n_prox.get_array(), 1727 self._ca_prox.get_array(), 1728 self._c_prox.get_array(), 1729 self._o_prox.get_array(), 1730 self._cb_prox.get_array(), 1731 self._sg_prox.get_array(), 1732 self._n_dist.get_array(), 1733 self._ca_dist.get_array(), 1734 self._c_dist.get_array(), 1735 self._o_dist.get_array(), 1736 self._cb_dist.get_array(), 1737 self._sg_dist.get_array(), 1738 self._c_prev_prox.get_array(), 1739 self._n_next_prox.get_array(), 1740 self._c_prev_dist.get_array(), 1741 self._n_next_dist.get_array(), 1742 ) 1743 ) 1744 return res_array 1745 1746 def internal_coords_res(self, resnumb) -> np.array: 1747 """ 1748 Return the internal coordinates for the internal coordinates of 1749 the given Disulfide. Missing atoms are not included. 1750 1751 :param resnumb: Residue number for disulfide 1752 :raises DisulfideConstructionWarning: Warning raised if the residue number is invalid 1753 :return: Array containing the internal coordinates for the disulfide 1754 """ 1755 res_array = np.zeros(shape=(6, 3)) 1756 1757 if resnumb == self.proximal: 1758 res_array = np.array( 1759 ( 1760 self._n_prox.get_array(), 1761 self._ca_prox.get_array(), 1762 self._c_prox.get_array(), 1763 self._o_prox.get_array(), 1764 self._cb_prox.get_array(), 1765 self._sg_prox.get_array(), 1766 ) 1767 ) 1768 return res_array 1769 1770 elif resnumb == self.distal: 1771 res_array = np.array( 1772 ( 1773 self._n_dist.get_array(), 1774 self._ca_dist.get_array(), 1775 self._c_dist.get_array(), 1776 self._o_dist.get_array(), 1777 self._cb_dist.get_array(), 1778 self._sg_dist.get_array(), 1779 ) 1780 ) 1781 return res_array 1782 else: 1783 mess = f"-> Disulfide.internal_coords(): Invalid argument. \ 1784 Unable to find residue: {resnumb} " 1785 raise DisulfideConstructionWarning(mess) 1786 1787 def make_movie( 1788 self, style="sb", fname="ssbond.mp4", verbose=False, steps=360 1789 ) -> None: 1790 """ 1791 Create an animation for ```self``` rotating one revolution about the Y axis, 1792 in the given ```style```, saving to ```filename```. 1793 1794 :param style: Rendering style, defaults to 'sb', one of: 1795 * 'sb' - split bonds 1796 * 'bs' - ball and stick 1797 * 'cpk' - CPK style 1798 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1799 * 'plain' - boring single color 1800 1801 :param fname: Output filename, defaults to ```ssbond.mp4``` 1802 :param verbose: Verbosity, defaults to False 1803 :param steps: Number of steps for one complete rotation, defaults to 360. 1804 """ 1805 src = self.pdb_id 1806 name = self.name 1807 enrg = self.energy 1808 1809 title = f"{src} {name}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol, Cα: {self.ca_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 1810 1811 if verbose: 1812 print(f"Rendering animation to {fname}...") 1813 1814 pl = pv.Plotter(window_size=WINSIZE, off_screen=True) 1815 pl.open_movie(fname) 1816 path = pl.generate_orbital_path(n_points=steps) 1817 1818 # 1819 # pl.add_title(title=title, font_size=FONTSIZE) 1820 pl.enable_anti_aliasing("msaa") 1821 # pl.add_camera_orientation_widget() 1822 pl = self._render( 1823 pl, 1824 style=style, 1825 bondcolor=BOND_COLOR, 1826 bs_scale=BS_SCALE, 1827 spec=SPECULARITY, 1828 specpow=SPEC_POWER, 1829 ) 1830 pl.reset_camera() 1831 pl.orbit_on_path(path, write_frames=True) 1832 pl.close() 1833 1834 if verbose: 1835 print(f"Saved mp4 animation to: {fname}") 1836 1837 def pprint(self) -> None: 1838 """ 1839 Pretty print general info for the Disulfide 1840 """ 1841 s1 = self.repr_ss_info() 1842 s2 = self.repr_ss_ca_dist() 1843 s3 = self.repr_ss_conformation() 1844 s4 = self.repr_ss_torsion_length() 1845 res = f"{s1} \n{s3} \n{s2} \n{s4}>" 1846 print(res) 1847 1848 def pprint_all(self) -> None: 1849 """ 1850 Pretty print all info for the Disulfide 1851 """ 1852 s1 = self.repr_ss_info() + "\n" 1853 s2 = self.repr_ss_coords() 1854 s3 = self.repr_ss_local_coords() 1855 s4 = self.repr_ss_conformation() 1856 s5 = self.repr_chain_ids() 1857 s6 = self.repr_ss_ca_dist() 1858 s7 = self.repr_ss_torsion_length() 1859 1860 res = f"{s1} {s5} {s2} {s3} {s4}\n {s6}\n {s7}>" 1861 1862 print(res) 1863 1864 # repr functions. The class is large, so I split it up into sections 1865 def repr_ss_info(self) -> str: 1866 """ 1867 Representation for the Disulfide class 1868 """ 1869 s1 = f"<Disulfide {self.name}, Source: {self.pdb_id}, Resolution: {self.resolution} Å" 1870 return s1 1871 1872 def repr_ss_coords(self) -> str: 1873 """ 1874 Representation for Disulfide coordinates 1875 """ 1876 s2 = f"\nProximal Coordinates:\n N: {self.n_prox}\n Cα: {self.ca_prox}\n C: {self.c_prox}\n O: {self.o_prox}\n Cβ: {self.cb_prox}\n Sγ: {self.sg_prox}\n Cprev {self.c_prev_prox}\n Nnext: {self.n_next_prox}\n" 1877 s3 = f"Distal Coordinates:\n N: {self.n_dist}\n Cα: {self.ca_dist}\n C: {self.c_dist}\n O: {self.o_dist}\n Cβ: {self.cb_dist}\n Sγ: {self.sg_dist}\n Cprev {self.c_prev_dist}\n Nnext: {self.n_next_dist}\n\n" 1878 stot = f"{s2} {s3}" 1879 return stot 1880 1881 def repr_ss_conformation(self) -> str: 1882 """ 1883 Representation for Disulfide conformation 1884 """ 1885 s4 = f"Χ1-Χ5: {self.chi1:.2f}°, {self.chi2:.2f}°, {self.chi3:.2f}°, {self.chi4:.2f}° {self.chi5:.2f}°, {self.rho:.2f}°, {self.energy:.2f} kcal/mol" 1886 stot = f"{s4}" 1887 return stot 1888 1889 def repr_ss_local_coords(self) -> str: 1890 """ 1891 Representation for the Disulfide internal coordinates. 1892 """ 1893 s2i = f"Proximal Internal Coords:\n N: {self._n_prox}\n Cα: {self._ca_prox}\n C: {self._c_prox}\n O: {self._o_prox}\n Cβ: {self._cb_prox}\n Sγ: {self._sg_prox}\n Cprev {self.c_prev_prox}\n Nnext: {self.n_next_prox}\n" 1894 s3i = f"Distal Internal Coords:\n N: {self._n_dist}\n Cα: {self._ca_dist}\n C: {self._c_dist}\n O: {self._o_dist}\n Cβ: {self._cb_dist}\n Sγ: {self._sg_dist}\n Cprev {self.c_prev_dist}\n Nnext: {self.n_next_dist}\n" 1895 stot = f"{s2i}{s3i}" 1896 return stot 1897 1898 def repr_ss_chain_ids(self) -> str: 1899 """ 1900 Representation for Disulfide chain IDs 1901 """ 1902 return f"Proximal Chain fullID: <{self.proximal_residue_fullid}> Distal Chain fullID: <{self.distal_residue_fullid}>" 1903 1904 def repr_ss_ca_dist(self) -> str: 1905 """ 1906 Representation for Disulfide Ca distance 1907 """ 1908 s1 = f"Cα Distance: {self.ca_distance:.2f} Å" 1909 return s1 1910 1911 def repr_ss_cb_dist(self) -> str: 1912 """ 1913 Representation for Disulfide Ca distance 1914 """ 1915 s1 = f"Cβ Distance: {self.cb_distance:.2f} Å" 1916 return s1 1917 1918 def repr_ss_torsion_length(self) -> str: 1919 """ 1920 Representation for Disulfide torsion length 1921 """ 1922 s1 = f"Torsion length: {self.torsion_length:.2f} deg" 1923 return s1 1924 1925 def repr_all(self) -> str: 1926 """ 1927 Return a string representation for all Disulfide information 1928 contained in self. 1929 """ 1930 1931 s1 = self.repr_ss_info() + "\n" 1932 s2 = self.repr_ss_coords() 1933 s3 = self.repr_ss_local_coords() 1934 s4 = self.repr_ss_conformation() 1935 s5 = self.repr_chain_ids() 1936 s6 = self.repr_ss_ca_dist() 1937 s8 = self.repr_ss_cb_dist() 1938 s7 = self.repr_ss_torsion_length() 1939 1940 res = f"{s1} {s5} {s2} {s3} {s4} {s6} {s7} {s8}>" 1941 return res 1942 1943 def repr_compact(self) -> str: 1944 """ 1945 Return a compact representation of the Disulfide object 1946 :return: string 1947 """ 1948 return f"{self.repr_ss_info()} {self.repr_ss_conformation()}" 1949 1950 def repr_conformation(self) -> str: 1951 """ 1952 Return a string representation of the Disulfide object's conformation. 1953 :return: string 1954 """ 1955 return f"{self.repr_ss_conformation()}" 1956 1957 def repr_coords(self) -> str: 1958 """ 1959 Return a string representation of the Disulfide object's coordinates. 1960 :return: string 1961 """ 1962 return f"{self.repr_ss_coords()}" 1963 1964 def repr_internal_coords(self) -> str: 1965 """ 1966 Return a string representation of the Disulfide object's internal coordinaes. 1967 :return: string 1968 """ 1969 return f"{self.repr_ss_local_coords()}" 1970 1971 def repr_chain_ids(self) -> str: 1972 """ 1973 Return a string representation of the Disulfide object's chain ids. 1974 :return: string 1975 """ 1976 return f"{self.repr_ss_chain_ids()}" 1977 1978 def compute_rho(self) -> float: 1979 self.rho = calc_dihedral(self.n_prox, self.ca_prox, self.ca_dist, self.n_dist) 1980 return self.rho 1981 1982 def reset(self) -> None: 1983 """ 1984 Resets the disulfide object to its initial state. All distances, 1985 angles and positions are reset. The name is unchanged. 1986 """ 1987 self.__init__(self) 1988 1989 def same_chains(self) -> bool: 1990 """ 1991 Function checks if the Disulfide is cross-chain or not. 1992 1993 Returns 1994 ------- 1995 bool \n 1996 True if the proximal and distal residues are on the same chains, 1997 False otherwise. 1998 """ 1999 2000 (prox, dist) = self.get_chains() 2001 return prox == dist 2002 2003 def screenshot( 2004 self, 2005 single=True, 2006 style="sb", 2007 fname="ssbond.png", 2008 verbose=False, 2009 shadows=False, 2010 light=True, 2011 ) -> None: 2012 """ 2013 Create and save a screenshot of the Disulfide in the given style 2014 and filename 2015 2016 :param single: Display a single vs panel view, defaults to True 2017 :param style: Rendering style, one of: 2018 * 'sb' - split bonds 2019 * 'bs' - ball and stick 2020 * 'cpk' - CPK style 2021 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2022 * 'plain' - boring single color, 2023 :param fname: output filename,, defaults to 'ssbond.png' 2024 :param verbose: Verbosit, defaults to False 2025 :param shadows: Enable shadows, defaults to False 2026 """ 2027 src = self.pdb_id 2028 name = self.name 2029 enrg = self.energy 2030 2031 title = f"{src} {name}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol, Cα: {self.ca_distance:.2f} Å, Cβ: {self.cb_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 2032 2033 if light: 2034 pv.set_plot_theme("document") 2035 else: 2036 pv.set_plot_theme("dark") 2037 2038 if verbose: 2039 print(f"-> screenshot(): Rendering screenshot to file {fname}") 2040 2041 if single: 2042 pl = pv.Plotter(window_size=WINSIZE) 2043 # pl.add_title(title=title, font_size=FONTSIZE) 2044 pl.enable_anti_aliasing("msaa") 2045 # pl.add_camera_orientation_widget() 2046 self._render( 2047 pl, 2048 style=style, 2049 bondcolor=BOND_COLOR, 2050 bs_scale=BS_SCALE, 2051 spec=SPECULARITY, 2052 specpow=SPEC_POWER, 2053 ) 2054 pl.reset_camera() 2055 if shadows: 2056 pl.enable_shadows() 2057 2058 pl.show(auto_close=False) 2059 pl.screenshot(fname) 2060 pl.clear() 2061 2062 else: 2063 pl = pv.Plotter(window_size=WINSIZE, shape=(2, 2)) 2064 pl.subplot(0, 0) 2065 2066 # pl.add_title(title=title, font_size=FONTSIZE) 2067 pl.enable_anti_aliasing("msaa") 2068 2069 # pl.add_camera_orientation_widget() 2070 self._render( 2071 pl, 2072 style="cpk", 2073 bondcolor=BOND_COLOR, 2074 bs_scale=BS_SCALE, 2075 spec=SPECULARITY, 2076 specpow=SPEC_POWER, 2077 ) 2078 2079 pl.subplot(0, 1) 2080 # pl.add_title(title=title, font_size=FONTSIZE) 2081 self._render( 2082 pl, 2083 style="pd", 2084 bondcolor=BOND_COLOR, 2085 bs_scale=BS_SCALE, 2086 spec=SPECULARITY, 2087 specpow=SPEC_POWER, 2088 ) 2089 2090 pl.subplot(1, 0) 2091 # pl.add_title(title=title, font_size=FONTSIZE) 2092 self._render( 2093 pl, 2094 style="bs", 2095 bondcolor=BOND_COLOR, 2096 bs_scale=BS_SCALE, 2097 spec=SPECULARITY, 2098 specpow=SPEC_POWER, 2099 ) 2100 2101 pl.subplot(1, 1) 2102 # pl.add_title(title=title, font_size=FONTSIZE) 2103 self._render( 2104 pl, 2105 style="sb", 2106 bondcolor=BOND_COLOR, 2107 bs_scale=BS_SCALE, 2108 spec=SPECULARITY, 2109 specpow=SPEC_POWER, 2110 ) 2111 2112 pl.link_views() 2113 pl.reset_camera() 2114 if shadows: 2115 pl.enable_shadows() 2116 2117 pl.show(auto_close=False) 2118 pl.screenshot(fname) 2119 2120 if verbose: 2121 print(f"Saved: {fname}") 2122 2123 def save_meshes_as_stl(self, meshes, filename) -> None: 2124 """Save a list of meshes as a single STL file. 2125 2126 Args: 2127 meshes (list): List of pyvista mesh objects to save. 2128 filename (str): Path to save the STL file to. 2129 """ 2130 merged_mesh = pv.UnstructuredGrid() 2131 for mesh in meshes: 2132 merged_mesh += mesh 2133 merged_mesh.save(filename) 2134 2135 def export(self, style="sb", verbose=True, fname="ssbond_plt") -> None: 2136 """ 2137 Create and save a screenshot of the Disulfide in the given style and filename. 2138 2139 :param single: Display a single vs panel view, defaults to True 2140 :param style: Rendering style, one of: 2141 * 'sb' - split bonds 2142 * 'bs' - ball and stick 2143 * 'cpk' - CPK style 2144 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2145 * 'plain' - boring single color, 2146 2147 :param fname: output filename,, defaults to 'ssbond.stl' 2148 :param verbose: Verbosit, defaults to False 2149 """ 2150 2151 if verbose: 2152 print(f"-> screenshot(): Rendering screenshot to file {fname}") 2153 2154 pl = pv.PolyData() 2155 2156 self._plot( 2157 pl, 2158 style=style, 2159 bondcolor=BOND_COLOR, 2160 bs_scale=BS_SCALE, 2161 spec=SPECULARITY, 2162 specpow=SPEC_POWER, 2163 ) 2164 2165 self.save_meshes_as_stl(pl, fname) 2166 2167 return 2168 2169 def set_permissive(self, perm: bool) -> None: 2170 """ 2171 Set PERMISSIVE flag for Disulfide parsing. 2172 2173 :return: None 2174 """ 2175 2176 self.PERMISSIVE = perm 2177 2178 def set_positions( 2179 self, 2180 n_prox: Vector, 2181 ca_prox: Vector, 2182 c_prox: Vector, 2183 o_prox: Vector, 2184 cb_prox: Vector, 2185 sg_prox: Vector, 2186 n_dist: Vector, 2187 ca_dist: Vector, 2188 c_dist: Vector, 2189 o_dist: Vector, 2190 cb_dist: Vector, 2191 sg_dist: Vector, 2192 c_prev_prox: Vector, 2193 n_next_prox: Vector, 2194 c_prev_dist: Vector, 2195 n_next_dist: Vector, 2196 ) -> None: 2197 """ 2198 Set the atomic coordinates for all atoms in the Disulfide object. 2199 2200 :param n_prox: Proximal N position 2201 :param ca_prox: Proximal Cα position 2202 :param c_prox: Proximal C' position 2203 :param o_prox: Proximal O position 2204 :param cb_prox: Proximal Cβ position 2205 :param sg_prox: Proximal Sγ position 2206 :param n_dist: Distal N position 2207 :param ca_dist: Distal Cα position 2208 :param c_dist: Distal C' position 2209 :param o_dist: Distal O position 2210 :param cb_dist: Distal Cβ position 2211 :param sg_dist: Distal Sγ position 2212 :param c_prev_prox: Proximal previous C' 2213 :param n_next_prox: Proximal next N 2214 :param c_prev_dist: Distal previous C' 2215 :param n_next_dist: Distal next N 2216 """ 2217 2218 # deep copy 2219 self.n_prox = n_prox.copy() 2220 self.ca_prox = ca_prox.copy() 2221 self.c_prox = c_prox.copy() 2222 self.o_prox = o_prox.copy() 2223 self.cb_prox = cb_prox.copy() 2224 self.sg_prox = sg_prox.copy() 2225 self.sg_dist = sg_dist.copy() 2226 self.cb_dist = cb_dist.copy() 2227 self.ca_dist = ca_dist.copy() 2228 self.n_dist = n_dist.copy() 2229 self.c_dist = c_dist.copy() 2230 self.o_dist = o_dist.copy() 2231 2232 self.c_prev_prox = c_prev_prox.copy() 2233 self.n_next_prox = n_next_prox.copy() 2234 self.c_prev_dist = c_prev_dist.copy() 2235 self.n_next_dist = n_next_dist.copy() 2236 2237 def set_dihedrals( 2238 self, chi1: float, chi2: float, chi3: float, chi4: float, chi5: float 2239 ) -> None: 2240 """ 2241 Set the disulfide's dihedral angles, Chi1-Chi5. -180 - 180 degrees. 2242 2243 :param chi1: Chi1 2244 :param chi2: Chi2 2245 :param chi3: Chi3 2246 :param chi4: Chi4 2247 :param chi5: Chi5 2248 """ 2249 self.chi1 = chi1 2250 self.chi2 = chi2 2251 self.chi3 = chi3 2252 self.chi4 = chi4 2253 self.chi5 = chi5 2254 self.torsion_array = np.array([chi1, chi2, chi3, chi4, chi5]) 2255 self.compute_torsional_energy() 2256 self.compute_torsion_length() 2257 2258 def set_name(self, namestr="Disulfide") -> None: 2259 """ 2260 Set the Disulfide's name. 2261 2262 :param namestr: Name, by default "Disulfide" 2263 """ 2264 self.name = namestr 2265 2266 def set_resnum(self, proximal: int, distal: int) -> None: 2267 """ 2268 Set the proximal and residue numbers for the Disulfide. 2269 2270 :param proximal: Proximal residue number 2271 :param distal: Distal residue number 2272 """ 2273 self.proximal = proximal 2274 self.distal = distal 2275 2276 def compute_torsion_length(self) -> float: 2277 """ 2278 Compute the 5D Euclidean length of the Disulfide object. Update the disulfide internal state. 2279 2280 :return: Torsion length (Degrees) 2281 """ 2282 # Use numpy array to compute element-wise square 2283 tors2 = np.square(self.torsion_array) 2284 2285 # Compute the sum of squares using numpy's sum function 2286 dist = math.sqrt(np.sum(tors2)) 2287 2288 # Update the internal state 2289 self.torsion_length = dist 2290 2291 return dist 2292 2293 def Torsion_Distance(self, other) -> float: 2294 """ 2295 Calculate the 5D Euclidean distance between ```self``` and another Disulfide 2296 object. This is used to compare Disulfide Bond torsion angles to 2297 determine their torsional similarity via a 5-Dimensional Euclidean distance metric. 2298 2299 :param other: Comparison Disulfide 2300 :raises ProteusPyWarning: Warning if ```other``` is not a Disulfide object 2301 :return: Euclidean distance (Degrees) between ```self``` and ```other```. 2302 """ 2303 2304 from proteusPy.ProteusPyWarning import ProteusPyWarning 2305 2306 # Check length of torsion arrays 2307 if len(self.torsion_array) != 5 or len(other.torsion_array) != 5: 2308 raise ProteusPyWarning( 2309 "--> Torsion_Distance() requires vectors of length 5!" 2310 ) 2311 2312 # Convert to numpy arrays and add 180 to each element 2313 p1 = np.array(self.torsion_array) + 180.0 2314 p2 = np.array(other.torsion_array) + 180.0 2315 2316 # Compute the 5D Euclidean distance using numpy's linalg.norm function 2317 dist = np.linalg.norm(p1 - p2) 2318 2319 return dist 2320 2321 def Torsion_neighbors(self, others, cutoff) -> DisulfideList: 2322 """ 2323 Return a list of Disulfides within the angular cutoff in the others list. 2324 This routine is used to find Disulfides having the same torsion length 2325 within the others list. This is used to find families of Disulfides with 2326 similar conformations. Assumes self is properly initialized. 2327 2328 *NB* The routine will not distinguish between +/- 2329 dihedral angles. *i.e.* [-60, -60, -90, -60, -60] would have the same 2330 torsion length as [60, 60, 90, 60, 60], two clearly different structures. 2331 2332 :param others: ```DisulfideList``` to search 2333 :param cutoff: Dihedral angle degree cutoff 2334 :return: DisulfideList within the cutoff 2335 2336 Example: 2337 In this example we load the disulfide database subset, find the disulfides with 2338 the lowest and highest energies, and then find the nearest conformational neighbors. 2339 Finally, we display the neighbors overlaid against a common reference frame. 2340 2341 >>> from proteusPy import * 2342 >>> from proteusPy.DisulfideLoader import Load_PDB_SS 2343 >>> from proteusPy.DisulfideList import DisulfideList 2344 >>> from proteusPy.Disulfide import Disulfide 2345 >>> PDB_SS = Load_PDB_SS(verbose=False, subset=True) 2346 >>> ss_list = DisulfideList([], 'tmp') 2347 2348 We point to the complete list to search for lowest and highest energies. 2349 >>> sslist = PDB_SS.SSList 2350 >>> ssmin_enrg, ssmax_enrg = PDB_SS.SSList.minmax_energy 2351 2352 Make an empty list and find the nearest neighbors within 10 degrees avg RMS in 2353 sidechain dihedral angle space. 2354 2355 >>> low_energy_neighbors = DisulfideList([],'Neighbors') 2356 >>> low_energy_neighbors = ssmin_enrg.Torsion_neighbors(sslist, 10) 2357 2358 Display the number found, and then display them overlaid onto their common reference frame. 2359 2360 >>> tot = low_energy_neighbors.length 2361 >>> print(f'Neighbors: {tot}') 2362 Neighbors: 2 2363 >>> low_energy_neighbors.display_overlay() 2364 2365 """ 2366 res = [ss for ss in others if self.Torsion_Distance(ss) <= cutoff] 2367 return DisulfideList(res, "neighbors") 2368 2369 def torsion_to_sixclass(self) -> str: 2370 """ 2371 Return the sextant class string for ``self``. 2372 2373 :raises DisulfideIOException: _description_ 2374 :return: Sextant string 2375 """ 2376 from proteusPy.DisulfideClasses import get_sixth_quadrant 2377 2378 tors = self.torsion_array 2379 res = [get_sixth_quadrant(x) for x in tors] 2380 return "".join([str(r) for r in res]) 2381 2382 2383# Class defination ends 2384 2385 2386def parse_ssbond_header_rec(ssbond_dict: dict) -> list: 2387 """ 2388 Parse the SSBOND dict returned by parse_pdb_header. 2389 NB: Requires EGS-Modified BIO.parse_pdb_header.py. 2390 This is used internally. 2391 2392 :param ssbond_dict: the input SSBOND dict 2393 :return: A list of tuples representing the proximal, 2394 distal residue ids for the Disulfide. 2395 2396 """ 2397 disulfide_list = [] 2398 for ssb in ssbond_dict.items(): 2399 disulfide_list.append(ssb[1]) 2400 2401 return disulfide_list 2402 2403 2404# 2405# Function reads a comma separated list of PDB IDs and download the corresponding 2406# .ent files to the PDB_DIR global. 2407# Used to download the list of proteins containing at least one SS bond 2408# with the ID list generated from: http://www.rcsb.org/ 2409# 2410 2411 2412def Download_Disulfides( 2413 pdb_home=PDB_DIR, model_home=MODEL_DIR, verbose=False, reset=False 2414) -> None: 2415 """ 2416 Read a comma separated list of PDB IDs and download them 2417 to the pdb_home path. 2418 2419 This utility function is used to download proteins containing at least 2420 one SS bond with the ID list generated from: http://www.rcsb.org/. 2421 2422 This is the primary data loader for the proteusPy Disulfide 2423 analysis package. The list of IDs represents files in the 2424 RCSB containing > 1 disulfide bond, and it contains 2425 over 39000 structures. The total download takes about 12 hours. The 2426 function keeps track of downloaded files so it's possible to interrupt and 2427 restart the download without duplicating effort. 2428 2429 :param pdb_home: Path for downloaded files, defaults to PDB_DIR 2430 :param model_home: Path for extracted data, defaults to MODEL_DIR 2431 :param verbose: Verbosity, defaults to False 2432 :param reset: Reset the downloaded files index. Used to restart the download. 2433 :raises DisulfideIOException: I/O error raised when the PDB file is not found. 2434 """ 2435 import os 2436 import time 2437 2438 start = time.time() 2439 donelines = [] 2440 SS_done = [] 2441 ssfile = None 2442 2443 cwd = os.getcwd() 2444 os.chdir(pdb_home) 2445 2446 pdblist = PDBList(pdb=pdb_home, verbose=verbose) 2447 ssfilename = f"{model_home}{SS_ID_FILE}" 2448 print(ssfilename) 2449 2450 # list of IDs containing >1 SSBond record 2451 try: 2452 ssfile = open(ssfilename) 2453 Line = ssfile.readlines() 2454 except Exception: 2455 raise DisulfideIOException(f"Cannot open file: {ssfile}") 2456 2457 for line in Line: 2458 entries = line.split(",") 2459 2460 print(f"Found: {len(entries)} entries") 2461 completed = {"xxx"} # set to keep track of downloaded 2462 2463 # file to track already downloaded entries. 2464 if reset is True: 2465 completed_file = open(f"{model_home}ss_completed.txt", "w") 2466 donelines = [] 2467 SS_DONE = [] 2468 else: 2469 completed_file = open(f"{model_home}ss_completed.txt", "w+") 2470 donelines = completed_file.readlines() 2471 2472 if len(donelines) > 0: 2473 for dl in donelines[0]: 2474 # create a list of pdb id already downloaded 2475 SS_done = dl.split(",") 2476 2477 count = len(SS_done) - 1 2478 completed.update(SS_done) # update the completed set with what's downloaded 2479 2480 # Loop over all entries, 2481 pbar = tqdm(entries, ncols=PBAR_COLS) 2482 for entry in pbar: 2483 pbar.set_postfix({"Entry": entry}) 2484 if entry not in completed: 2485 if pdblist.retrieve_pdb_file(entry, file_format="pdb", pdir=pdb_home): 2486 completed.update(entry) 2487 completed_file.write(f"{entry},") 2488 count += 1 2489 2490 completed_file.close() 2491 2492 end = time.time() 2493 elapsed = end - start 2494 2495 print(f"Overall files processed: {count}") 2496 print(f"Complete. Elapsed time: {datetime.timedelta(seconds=elapsed)} (h:m:s)") 2497 os.chdir(cwd) 2498 return 2499 2500 2501# Function extracts the disulfide bonds from the PDB files and creates the .pkl files 2502# needed for the proteusPy.DisulfideLoader.DisulfideLoader class. 2503 2504 2505def Extract_Disulfides( 2506 numb=-1, 2507 verbose=False, 2508 quiet=True, 2509 pdbdir=PDB_DIR, 2510 datadir=MODEL_DIR, 2511 picklefile=SS_PICKLE_FILE, 2512 torsionfile=SS_TORSIONS_FILE, 2513 problemfile=PROBLEM_ID_FILE, 2514 dictfile=SS_DICT_PICKLE_FILE, 2515 dist_cutoff=-1.0, 2516) -> None: 2517 """ 2518 Read the PDB files contained in ``pdbdir`` and create the .pkl files needed for the 2519 proteusPy.DisulfideLoader.DisulfideLoader class. 2520 The ```Disulfide``` objects are contained in a ```DisulfideList``` object and 2521 ```Dict``` within these files. In addition, .csv files containing all of 2522 the torsions for the disulfides and problem IDs are written. The optional 2523 ```dist_cutoff``` allows for removal of Disufides whose Cα-Cα distance is > 2524 than the cutoff value. If it's -1.0 then the function keeps all Disulfides. 2525 2526 :param numb: number of entries to process, defaults to all 2527 :param verbose: more messages 2528 :param quiet: turns off DisulfideConstruction warnings 2529 :param pdbdir: path to PDB files 2530 :param datadir: path to resulting .pkl files 2531 :param picklefile: name of the disulfide .pkl file 2532 :param torsionfile: name of the disulfide torsion file .csv created 2533 :param problemfile: name of the .csv file containing problem ids 2534 :param dictfile: name of the .pkl file 2535 :param dist_cutoff: Ca distance cutoff to reject a Disulfide. 2536 """ 2537 2538 def name_to_id(fname: str) -> str: 2539 """ 2540 Returns the PDB ID from the filename. 2541 2542 :param fname: Complete PDB filename 2543 :return: PDB ID 2544 """ 2545 ent = fname[3:-4] 2546 return ent 2547 2548 import os 2549 2550 entrylist = [] 2551 problem_ids = [] 2552 bad = bad_dist = 0 2553 2554 # we use the specialized list class DisulfideList to contain our disulfides 2555 # we'll use a dict to store DisulfideList objects, indexed by the structure ID 2556 All_ss_dict = {} 2557 All_ss_list = DisulfideList([], "PDB_SS") 2558 All_ss_dict2 = {} # new dict of pointers to indices 2559 2560 start = time.time() 2561 cwd = os.getcwd() 2562 2563 # Build a list of PDB files in PDB_DIR that are readable. These files were downloaded 2564 # via the RCSB web query interface for structures containing >= 1 SS Bond. 2565 2566 os.chdir(pdbdir) 2567 2568 ss_filelist = glob.glob("*.ent") 2569 tot = len(ss_filelist) 2570 2571 if verbose: 2572 print(f"PDB Directory {pdbdir} contains: {tot} files") 2573 2574 # the filenames are in the form pdb{entry}.ent, I loop through them and extract 2575 # the PDB ID, with Disulfide.name_to_id(), then add to entrylist. 2576 2577 for entry in ss_filelist: 2578 entrylist.append(name_to_id(entry)) 2579 2580 # create a dataframe with the following columns for the disulfide conformations 2581 # extracted from the structure 2582 2583 SS_df = pandas.DataFrame(columns=Torsion_DF_Cols) 2584 2585 # define a tqdm progressbar using the fully loaded entrylist list. 2586 # If numb is passed then 2587 # only do the last numb entries. 2588 2589 if numb > 0: 2590 pbar = tqdm(entrylist[:numb], ncols=PBAR_COLS) 2591 else: 2592 pbar = tqdm(entrylist, ncols=PBAR_COLS) 2593 2594 tot = 0 2595 cnt = 0 2596 # loop over ss_filelist, create disulfides and initialize them 2597 for entry in pbar: 2598 pbar.set_postfix( 2599 {"ID": entry, "Bad": bad, "Ca": bad_dist, "Cnt": tot} 2600 ) # update the progress bar 2601 2602 # returns an empty list if none are found. 2603 _sslist = DisulfideList([], entry) 2604 _sslist = proteusPy.DisulfideList.load_disulfides_from_id( 2605 entry, model_numb=0, verbose=verbose, quiet=quiet, pdb_dir=pdbdir 2606 ) 2607 sslist, xchain = prune_extra_ss(_sslist) 2608 2609 if len(sslist) > 0: 2610 sslist2 = [] # list to hold indices for ss_dict2 2611 for ss in sslist: 2612 # Ca distance cutoff 2613 dist = ss.ca_distance 2614 if dist >= dist_cutoff and dist_cutoff != -1.0: 2615 bad_dist += 1 2616 continue 2617 2618 All_ss_list.append(ss) 2619 new_row = [ 2620 ss.pdb_id, 2621 ss.name, 2622 ss.proximal, 2623 ss.distal, 2624 ss.chi1, 2625 ss.chi2, 2626 ss.chi3, 2627 ss.chi4, 2628 ss.chi5, 2629 ss.energy, 2630 ss.ca_distance, 2631 ss.cb_distance, 2632 ss.phiprox, 2633 ss.psiprox, 2634 ss.phidist, 2635 ss.psidist, 2636 ss.torsion_length, 2637 ss.rho, 2638 ] 2639 2640 # add the row to the end of the dataframe 2641 SS_df.loc[len(SS_df.index)] = new_row.copy() # deep copy 2642 sslist2.append(cnt) 2643 cnt += 1 2644 tot += 1 2645 2646 # All_ss_dict[entry] = sslist 2647 # print(f'Entry: {entry}. Dict indices: {sslist2}') 2648 All_ss_dict2[entry] = sslist2 2649 # print(f'{entry} ss dict adding: {sslist2}') 2650 2651 else: 2652 # at this point I really shouldn't have any bad non-parsible file 2653 bad += 1 2654 problem_ids.append(entry) 2655 # os.remove(f'pdb{entry}.ent') 2656 2657 if bad > 0: 2658 prob_cols = ["id"] 2659 problem_df = pandas.DataFrame(columns=prob_cols) 2660 problem_df["id"] = problem_ids 2661 2662 print( 2663 f"-> Extract_Disulfides(): Found and removed: {len(problem_ids)} non-parsable structures." 2664 ) 2665 print( 2666 f"-> Extract_Disulfides(): Saving problem IDs to file: {datadir}{problemfile}" 2667 ) 2668 2669 problem_df.to_csv(f"{datadir}{problemfile}") 2670 else: 2671 if verbose: 2672 print("-> Extract_Disulfides(): No non-parsable structures found.") 2673 2674 if bad_dist > 0: 2675 print(f"-> Extract_Disulfides(): Found and ignored: {bad_dist} long SS bonds.") 2676 else: 2677 if verbose: 2678 print("No problems found.") 2679 2680 # dump the all_ss list of disulfides to a .pkl file. ~520 MB. 2681 fname = f"{datadir}{picklefile}" 2682 print( 2683 f"-> Extract_Disulfides(): Saving {len(All_ss_list)} Disulfides to file: {fname}" 2684 ) 2685 2686 with open(fname, "wb+") as f: 2687 pickle.dump(All_ss_list, f) 2688 2689 # dump the dict2 disulfides to a .pkl file. ~520 MB. 2690 dict_len = len(All_ss_dict2) 2691 fname = f"{datadir}{dictfile}" 2692 print( 2693 f"-> Extract_Disulfides(): Saving indices of {dict_len} Disulfide-containing PDB IDs to file: {fname}" 2694 ) 2695 2696 with open(fname, "wb+") as f: 2697 pickle.dump(All_ss_dict2, f) 2698 2699 # save the torsions 2700 fname = f"{datadir}{torsionfile}" 2701 print(f"-> Extract_Disulfides(): Saving torsions to file: {fname}") 2702 SS_df.to_csv(fname) 2703 2704 end = time.time() 2705 elapsed = end - start 2706 2707 print( 2708 f"-> Extract_Disulfides(): Disulfide Extraction complete! Elapsed time:\ 2709 {datetime.timedelta(seconds=elapsed)} (h:m:s)" 2710 ) 2711 2712 # return to original directory 2713 os.chdir(cwd) 2714 return 2715 2716 2717def check_header_from_file( 2718 filename: str, model_numb=0, verbose=False, dbg=False 2719) -> bool: 2720 """ 2721 Check the Disulfides in the PDB file. 2722 2723 NB: Requires EGS-Modified BIO.parse_pdb_header.py from https://github.com/suchanek/biopython/ 2724 2725 :param filename: the name of the PDB entry. 2726 :param model_numb: model number to use, defaults to 0 for single structure files. 2727 :param verbose: print info while parsing 2728 :return: True if parsable 2729 2730 Example: 2731 Assuming ```MODEL_DIR``` has the pdb5rsa.ent file we can load the disulfides 2732 with the following: 2733 2734 >>> from proteusPy.Disulfide import Disulfide, check_header_from_file 2735 >>> MODEL_DIR = '../data/' 2736 >>> OK = False 2737 >>> OK = check_header_from_file(f'{MODEL_DIR}pdb5rsa.ent', verbose=True) 2738 -> check_header_from_file() - Parsing file: ../data/pdb5rsa.ent: 2739 -> SSBond: 1: tmp: 26A - 84A 2740 -> SSBond: 2: tmp: 40A - 95A 2741 -> SSBond: 3: tmp: 58A - 110A 2742 -> SSBond: 4: tmp: 65A - 72A 2743 >>> OK 2744 True 2745 """ 2746 import os 2747 2748 i = 1 2749 proximal = distal = -1 2750 _chaina = None 2751 _chainb = None 2752 2753 parser = PDBParser(PERMISSIVE=True) 2754 2755 # Biopython uses the Structure -> Model -> Chain hierarchy to organize 2756 # structures. All are iterable. 2757 2758 structure = parser.get_structure("tmp", file=filename) 2759 struct_name = structure.get_id() 2760 2761 model = structure[model_numb] 2762 2763 if verbose: 2764 print(f"-> check_header_from_file() - Parsing file: {filename}:") 2765 2766 ssbond_dict = structure.header["ssbond"] # NB: this requires the modified code 2767 2768 # list of tuples with (proximal distal chaina chainb) 2769 ssbonds = parse_ssbond_header_rec(ssbond_dict) 2770 2771 for pair in ssbonds: 2772 # in the form (proximal, distal, chain) 2773 proximal = pair[0] 2774 distal = pair[1] 2775 2776 if not proximal.isnumeric() or not distal.isnumeric(): 2777 if verbose: 2778 mess = f" ! Cannot parse SSBond record (non-numeric IDs):\ 2779 {struct_name} Prox: {proximal} {chain1_id} Dist: {distal} {chain2_id}" 2780 warnings.warn(mess, DisulfideParseWarning) 2781 continue # was pass 2782 else: 2783 proximal = int(proximal) 2784 distal = int(distal) 2785 2786 chain1_id = pair[2] 2787 chain2_id = pair[3] 2788 2789 _chaina = model[chain1_id] 2790 _chainb = model[chain2_id] 2791 2792 if chain1_id != chain2_id: 2793 if verbose: 2794 mess = f" -> Cross Chain SS for: Prox: {proximal}{chain1_id} Dist: {distal}{chain2_id}" 2795 warnings.warn(mess, DisulfideParseWarning) 2796 pass # was break 2797 2798 try: 2799 prox_res = _chaina[proximal] 2800 dist_res = _chainb[distal] 2801 except KeyError: 2802 print( 2803 f" ! Cannot parse SSBond record (KeyError): {struct_name} Prox: <{proximal}> {chain1_id} Dist: <{distal}> {chain2_id}" 2804 ) 2805 return False 2806 2807 if (_chaina is not None) and (_chainb is not None): 2808 if _chaina[proximal].is_disordered() or _chainb[distal].is_disordered(): 2809 continue 2810 else: 2811 if verbose: 2812 print( 2813 f" -> SSBond: {i}: {struct_name}: {proximal}{chain1_id} - {distal}{chain2_id}" 2814 ) 2815 else: 2816 if dbg: 2817 print( 2818 f" -> NULL chain(s): {struct_name}: {proximal}{chain1_id} - {distal}{chain2_id}" 2819 ) 2820 i += 1 2821 return True 2822 2823 2824def check_header_from_id( 2825 struct_name: str, pdb_dir=".", model_numb=0, verbose=False, dbg=False 2826) -> bool: 2827 """ 2828 Check parsability PDB ID and initializes the Disulfide objects. 2829 Assumes the file is downloaded in ```MODEL_DIR``` path. 2830 2831 NB: Requires EGS-Modified BIO.parse_pdb_header.py from https://github.com/suchanek/biopython/ 2832 2833 :param struct_name: the name of the PDB entry. 2834 :param pdb_dir: path to the PDB files, defaults to PDB_DIR 2835 :param model_numb: model number to use, defaults to 0 for single structure files. 2836 :param verbose: print info while parsing 2837 :param dbg: Debugging Flag 2838 :return: True if OK, False otherwise 2839 2840 Example: 2841 Assuming the PDB_DIR has the pdb5rsa.ent file we can check the file thusly: 2842 2843 >>> from proteusPy.Disulfide import Disulfide, check_header_from_id 2844 >>> MODEL_DIR = '/Users/egs/PDB/good/' 2845 >>> OK = False 2846 >>> OK = check_header_from_id('5rsa', pdb_dir=MODEL_DIR, verbose=True) 2847 -> SSBond: 1: 5rsa: 26A - 84A 2848 -> SSBond: 2: 5rsa: 40A - 95A 2849 -> SSBond: 3: 5rsa: 58A - 110A 2850 -> SSBond: 4: 5rsa: 65A - 72A 2851 >>> OK 2852 True 2853 """ 2854 parser = PDBParser(PERMISSIVE=True, QUIET=True) 2855 structure = parser.get_structure(struct_name, file=f"{pdb_dir}pdb{struct_name}.ent") 2856 model = structure[0] 2857 2858 ssbond_dict = structure.header["ssbond"] # NB: this requires the modified code 2859 2860 bondlist = [] 2861 i = 0 2862 2863 # get a list of tuples containing the proximal, distal residue IDs for 2864 # all SSBonds in the chain. 2865 bondlist = parse_ssbond_header_rec(ssbond_dict) 2866 2867 if len(bondlist) == 0: 2868 if verbose: 2869 print("-> check_header_from_id(): no bonds found in bondlist.") 2870 return False 2871 2872 for pair in bondlist: 2873 # in the form (proximal, distal, chain) 2874 proximal = pair[0] 2875 distal = pair[1] 2876 chain1 = pair[2] 2877 chain2 = pair[3] 2878 2879 chaina = model[chain1] 2880 chainb = model[chain2] 2881 2882 try: 2883 prox_residue = chaina[proximal] 2884 dist_residue = chainb[distal] 2885 2886 prox_residue.disordered_select("CYS") 2887 dist_residue.disordered_select("CYS") 2888 2889 if ( 2890 prox_residue.get_resname() != "CYS" 2891 or dist_residue.get_resname() != "CYS" 2892 ): 2893 if verbose: 2894 print( 2895 f"build_disulfide() requires CYS at both residues:\ 2896 {prox_residue.get_resname()} {dist_residue.get_resname()}" 2897 ) 2898 return False 2899 except KeyError: 2900 if dbg: 2901 print( 2902 f"Keyerror: {struct_name}: {proximal} {chain1} - {distal} {chain2}" 2903 ) 2904 return False 2905 2906 if verbose: 2907 print( 2908 f" -> SSBond: {i+1}: {struct_name}: {proximal}{chain1} - {distal}{chain2}" 2909 ) 2910 2911 i += 1 2912 return True 2913 2914 2915def Disulfide_Energy_Function(x: list) -> float: 2916 """ 2917 Compute the approximate torsional energy (kcal/mpl) for the input dihedral angles. 2918 2919 :param x: A list of dihedral angles: [chi1, chi2, chi3, chi4, chi5] 2920 :return: Energy in kcal/mol 2921 2922 Example: 2923 >>> from proteusPy.Disulfide import Disulfide_Energy_Function 2924 >>> dihed = [-60.0, -60.0, -90.0, -60.0, -90.0] 2925 >>> res = Disulfide_Energy_Function(dihed) 2926 >>> res 2927 2.5999999999999996 2928 """ 2929 import numpy as np 2930 2931 chi1, chi2, chi3, chi4, chi5 = x 2932 energy = 2.0 * (np.cos(np.deg2rad(3.0 * chi1)) + np.cos(np.deg2rad(3.0 * chi5))) 2933 energy += np.cos(np.deg2rad(3.0 * chi2)) + np.cos(np.deg2rad(3.0 * chi4)) 2934 energy += ( 2935 3.5 * np.cos(np.deg2rad(2.0 * chi3)) 2936 + 0.6 * np.cos(np.deg2rad(3.0 * chi3)) 2937 + 10.1 2938 ) 2939 return energy 2940 2941 2942def Minimize(inputSS: Disulfide) -> Disulfide: 2943 """ 2944 Minimizes the energy of a Disulfide object using the Nelder-Mead optimization method. 2945 2946 Parameters: 2947 inputSS (Disulfide): The Disulfide object to be minimized. 2948 2949 Returns: 2950 Disulfide: The minimized Disulfide object. 2951 2952 """ 2953 from scipy.optimize import minimize 2954 2955 from proteusPy import Disulfide, Disulfide_Energy_Function 2956 2957 initial_guess = inputSS.torsion_array 2958 result = minimize(Disulfide_Energy_Function, initial_guess, method="Nelder-Mead") 2959 minimum_conformation = result.x 2960 modelled_min = Disulfide("minimized") 2961 modelled_min.dihedrals = minimum_conformation 2962 modelled_min.build_yourself() 2963 return modelled_min 2964 2965 2966if __name__ == "__main__": 2967 import doctest 2968 2969 doctest.testmod() 2970 2971# End of file
91class Disulfide: 92 """ 93 This class provides a Python object and methods representing a physical disulfide bond 94 either extracted from the RCSB protein databank or built using the 95 [proteusPy.Turtle3D](turtle3D.html) class. The disulfide bond is an important 96 intramolecular stabilizing structural element and is characterized by: 97 98 * Atomic coordinates for the atoms N, Cα, Cβ, C', Sγ for both residues. 99 These are stored as both raw atomic coordinates as read from the RCSB file 100 and internal local coordinates. 101 * The dihedral angles Χ1 - Χ5 for the disulfide bond 102 * A name, by default {pdb_id}{prox_resnumb}{prox_chain}_{distal_resnum}{distal_chain} 103 * Proximal residue number 104 * Distal residue number 105 * Approximate bond torsional energy (kcal/mol): 106 107 $$ 108 E_{kcal/mol} \\approx 2.0 * cos(3.0 * \\chi_{1}) + cos(3.0 * \\chi_{5}) + cos(3.0 * \\chi_{2}) + 109 $$ 110 $$ 111 cos(3.0 * \chi_{4}) + 3.5 * cos(2.0 * \chi_{3}) + 0.6 * cos(3.0 * \chi_{3}) + 10.1 112 $$ 113 114 The equation embodies the typical 3-fold rotation barriers associated with single bonds, 115 (Χ1, Χ5, Χ2, Χ4) and a high 2-fold barrier for Χ3, resulting from the partial double bond 116 character of the S-S bond. This property leads to two major disulfide families, characterized 117 by the sign of Χ3. *Left-handed* disulfides have Χ3 < 0° and *right-handed* disulfides have 118 Χ3 > 0°. Within this breakdown there are numerous subfamilies, broadly known as the *hook*, 119 *spiral* and *staple*. These are under characgterization. 120 121 * Euclidean length of the dihedral angles (degrees) defined as: 122 $$\sqrt(\chi_{1}^{2} + \chi_{2}^{2} + \chi_{3}^{2} + \chi_{4}^{2} + \chi_{5}^{2})$$ 123 * Cα - Cα distance (Å) 124 * Cβ - Cβ distance (Å) 125 * The previous C' and next N for both the proximal and distal residues. These are needed 126 to calculate the backbone dihedral angles Φ and Ψ. 127 * Backbone dihedral angles Φ and Ψ, when possible. Not all structures are complete and 128 in those cases the atoms needed may be undefined. In this case the Φ and Ψ angles are set 129 to -180°. 130 131 The class also provides a rendering capabilities using the excellent [PyVista](https://pyvista.org) 132 library, and can display disulfides interactively in a variety of display styles: 133 * 'sb' - Split Bonds style - bonds colored by their atom type 134 * 'bs' - Ball and Stick style - split bond coloring with small atoms 135 * 'pd' - Proximal/Distal style - bonds colored *Red* for proximal residue and *Green* for 136 the distal residue. 137 * 'cpk' - CPK style rendering, colored by atom type: 138 * Carbon - Grey 139 * Nitrogen - Blue 140 * Sulfur - Yellow 141 * Oxygen - Red 142 * Hydrogen - White 143 144 Individual renderings can be saved to a file, and animations created. 145 """ 146 147 def __init__( 148 self, 149 name: str = "SSBOND", 150 proximal: int = -1, 151 distal: int = -1, 152 proximal_chain: str = "A", 153 distal_chain: str = "A", 154 pdb_id: str = "1egs", 155 quiet: bool = True, 156 permissive: bool = True, 157 ) -> None: 158 """ 159 __init__ Initialize the class to defined internal values. 160 161 :param name: Disulfide name, by default "SSBOND" 162 163 """ 164 self.name = name 165 self.proximal = proximal 166 self.distal = distal 167 self.energy = _FLOAT_INIT 168 self.proximal_chain = proximal_chain 169 self.distal_chain = distal_chain 170 self.pdb_id = pdb_id 171 self.proximal_residue_fullid = str("") 172 self.distal_residue_fullid = str("") 173 self.PERMISSIVE = permissive 174 self.QUIET = quiet 175 self.ca_distance = _FLOAT_INIT 176 self.cb_distance = _FLOAT_INIT 177 self.torsion_array = np.array( 178 (_ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT) 179 ) 180 self.phiprox = _ANG_INIT 181 self.psiprox = _ANG_INIT 182 self.phidist = _ANG_INIT 183 self.psidist = _ANG_INIT 184 185 # global coordinates for the Disulfide, typically as 186 # returned from the PDB file 187 188 self.n_prox = Vector(0, 0, 0) 189 self.ca_prox = Vector(0, 0, 0) 190 self.c_prox = Vector(0, 0, 0) 191 self.o_prox = Vector(0, 0, 0) 192 self.cb_prox = Vector(0, 0, 0) 193 self.sg_prox = Vector(0, 0, 0) 194 self.sg_dist = Vector(0, 0, 0) 195 self.cb_dist = Vector(0, 0, 0) 196 self.ca_dist = Vector(0, 0, 0) 197 self.n_dist = Vector(0, 0, 0) 198 self.c_dist = Vector(0, 0, 0) 199 self.o_dist = Vector(0, 0, 0) 200 201 # set when we can't find previous or next prox or distal 202 # C' or N atoms. 203 self.missing_atoms = False 204 self.modelled = False 205 self.resolution = -1.0 206 207 # need these to calculate backbone dihedral angles 208 self.c_prev_prox = Vector(0, 0, 0) 209 self.n_next_prox = Vector(0, 0, 0) 210 self.c_prev_dist = Vector(0, 0, 0) 211 self.n_next_dist = Vector(0, 0, 0) 212 213 # local coordinates for the Disulfide, computed using the Turtle3D in 214 # Orientation #1. these are generally private. 215 216 self._n_prox = Vector(0, 0, 0) 217 self._ca_prox = Vector(0, 0, 0) 218 self._c_prox = Vector(0, 0, 0) 219 self._o_prox = Vector(0, 0, 0) 220 self._cb_prox = Vector(0, 0, 0) 221 self._sg_prox = Vector(0, 0, 0) 222 self._sg_dist = Vector(0, 0, 0) 223 self._cb_dist = Vector(0, 0, 0) 224 self._ca_dist = Vector(0, 0, 0) 225 self._n_dist = Vector(0, 0, 0) 226 self._c_dist = Vector(0, 0, 0) 227 self._o_dist = Vector(0, 0, 0) 228 229 # need these to calculate backbone dihedral angles 230 self._c_prev_prox = Vector(0, 0, 0) 231 self._n_next_prox = Vector(0, 0, 0) 232 self._c_prev_dist = Vector(0, 0, 0) 233 self._n_next_dist = Vector(0, 0, 0) 234 235 # Dihedral angles for the disulfide bond itself, set to _ANG_INIT 236 self.chi1 = _ANG_INIT 237 self.chi2 = _ANG_INIT 238 self.chi3 = _ANG_INIT 239 self.chi4 = _ANG_INIT 240 self.chi5 = _ANG_INIT 241 self.rho = _ANG_INIT # new dihedral angle: Nprox - Ca_prox - Ca_dist - N_dist 242 243 self.torsion_length = _FLOAT_INIT 244 245 # comparison operators, used for sorting. keyed to SS bond energy 246 def __lt__(self, other): 247 if isinstance(other, Disulfide): 248 return self.energy < other.energy 249 250 def __le__(self, other): 251 if isinstance(other, Disulfide): 252 return self.energy <= other.energy 253 254 def __gt__(self, other): 255 if isinstance(other, Disulfide): 256 return self.energy > other.energy 257 258 def __ge__(self, other): 259 if isinstance(other, Disulfide): 260 return self.energy >= other.energy 261 262 def __eq__(self, other): 263 if isinstance(other, Disulfide): 264 return self.energy == other.energy 265 266 def __ne__(self, other): 267 if isinstance(other, Disulfide): 268 return self.energy != other.energy 269 270 def __repr__(self): 271 """ 272 Representation for the Disulfide class 273 """ 274 s1 = self.repr_ss_info() 275 res = f"{s1}>" 276 return res 277 278 def _render( 279 self, 280 pvplot: pv.Plotter, 281 style="bs", 282 plain=False, 283 bondcolor=BOND_COLOR, 284 bs_scale=BS_SCALE, 285 spec=SPECULARITY, 286 specpow=SPEC_POWER, 287 translate=True, 288 bond_radius=BOND_RADIUS, 289 res=100, 290 ): 291 """ 292 Update the passed pyVista plotter() object with the mesh data for the 293 input Disulfide Bond. Used internally 294 295 Parameters 296 ---------- 297 pvplot : pv.Plotter 298 pyvista.Plotter object 299 300 style : str, optional 301 Rendering style, by default 'bs'. One of 'bs', 'st', 'cpk', Render as \ 302 CPK, ball-and-stick or stick. Bonds are colored by atom color, unless \ 303 'plain' is specified. 304 305 plain : bool, optional 306 Used internally, by default False 307 308 bondcolor : pyVista color name, optional bond color for simple bonds, by default BOND_COLOR 309 310 bs_scale : float, optional 311 scale factor (0-1) to reduce the atom sizes for ball and stick, by default BS_SCALE 312 313 spec : float, optional 314 specularity (0-1), where 1 is totally smooth and 0 is rough, by default SPECULARITY 315 316 specpow : int, optional 317 exponent used for specularity calculations, by default SPEC_POWER 318 319 translate : bool, optional 320 Flag used internally to indicate if we should translate \ 321 the disulfide to its geometric center of mass, by default True. 322 323 Returns 324 ------- 325 pv.Plotter 326 Updated pv.Plotter object with atoms and bonds. 327 """ 328 329 _bradius = bond_radius 330 coords = self.internal_coords() 331 missing_atoms = self.missing_atoms 332 clen = coords.shape[0] 333 334 model = self.modelled 335 if model: 336 all_atoms = False 337 else: 338 all_atoms = True 339 340 if translate: 341 cofmass = self.cofmass() 342 for i in range(clen): 343 coords[i] = coords[i] - cofmass 344 345 atoms = ( 346 "N", 347 "C", 348 "C", 349 "O", 350 "C", 351 "SG", 352 "N", 353 "C", 354 "C", 355 "O", 356 "C", 357 "SG", 358 "C", 359 "N", 360 "C", 361 "N", 362 ) 363 pvp = pvplot 364 365 # bond connection table with atoms in the specific order shown above: 366 # returned by ss.get_internal_coords() 367 368 def draw_bonds( 369 pvp, 370 bradius=BOND_RADIUS, 371 style="sb", 372 bcolor=BOND_COLOR, 373 missing=True, 374 all_atoms=True, 375 res=100, 376 ): 377 """ 378 Generate the appropriate pyVista cylinder objects to represent 379 a particular disulfide bond. This utilizes a connection table 380 for the starting and ending atoms and a color table for the 381 bond colors. Used internally. 382 383 :param pvp: input plotter object to be updated 384 :param bradius: bond radius 385 :param style: bond style. One of sb, plain, pd 386 :param bcolor: pyvista color 387 :param missing: True if atoms are missing, False othersie 388 :param all_atoms: True if rendering O, False if only backbone rendered 389 390 :return pvp: Updated Plotter object. 391 392 """ 393 _bond_conn = np.array( 394 [ 395 [0, 1], # n-ca 396 [1, 2], # ca-c 397 [2, 3], # c-o 398 [1, 4], # ca-cb 399 [4, 5], # cb-sg 400 [6, 7], # n-ca 401 [7, 8], # ca-c 402 [8, 9], # c-o 403 [7, 10], # ca-cb 404 [10, 11], # cb-sg 405 [5, 11], # sg -sg 406 [12, 0], # cprev_prox-n 407 [2, 13], # c-nnext_prox 408 [14, 6], # cprev_dist-n_dist 409 [8, 15], # c-nnext_dist 410 ] 411 ) 412 413 # modeled disulfides only have backbone atoms since 414 # phi and psi are undefined, which makes the carbonyl 415 # oxygen (O) undefined as well. Their previous and next N 416 # are also undefined. 417 418 _bond_conn_backbone = np.array( 419 [ 420 [0, 1], # n-ca 421 [1, 2], # ca-c 422 [1, 4], # ca-cb 423 [4, 5], # cb-sg 424 [6, 7], # n-ca 425 [7, 8], # ca-c 426 [7, 10], # ca-cb 427 [10, 11], # cb-sg 428 [5, 11], # sg -sg 429 ] 430 ) 431 432 # colors for the bonds. Index into ATOM_COLORS array 433 _bond_split_colors = np.array( 434 [ 435 ("N", "C"), 436 ("C", "C"), 437 ("C", "O"), 438 ("C", "C"), 439 ("C", "SG"), 440 ("N", "C"), 441 ("C", "C"), 442 ("C", "O"), 443 ("C", "C"), 444 ("C", "SG"), 445 ("SG", "SG"), 446 # prev and next C-N bonds - color by atom Z 447 ("C", "N"), 448 ("C", "N"), 449 ("C", "N"), 450 ("C", "N"), 451 ] 452 ) 453 454 _bond_split_colors_backbone = np.array( 455 [ 456 ("N", "C"), 457 ("C", "C"), 458 ("C", "C"), 459 ("C", "SG"), 460 ("N", "C"), 461 ("C", "C"), 462 ("C", "C"), 463 ("C", "SG"), 464 ("SG", "SG"), 465 ] 466 ) 467 # work through connectivity and colors 468 orig_col = dest_col = bcolor 469 470 if all_atoms: 471 bond_conn = _bond_conn 472 bond_split_colors = _bond_split_colors 473 else: 474 bond_conn = _bond_conn_backbone 475 bond_split_colors = _bond_split_colors_backbone 476 477 for i in range(len(bond_conn)): 478 if all_atoms: 479 if i > 10 and missing_atoms == True: # skip missing atoms 480 continue 481 482 bond = bond_conn[i] 483 484 # get the indices for the origin and destination atoms 485 orig = bond[0] 486 dest = bond[1] 487 488 col = bond_split_colors[i] 489 490 # get the coords 491 prox_pos = coords[orig] 492 distal_pos = coords[dest] 493 494 # compute a direction vector 495 direction = distal_pos - prox_pos 496 497 # compute vector length. divide by 2 since split bond 498 height = math.dist(prox_pos, distal_pos) / 2.0 499 500 # the cylinder origins are actually in the 501 # middle so we translate 502 503 origin = prox_pos + 0.5 * direction # for a single plain bond 504 origin1 = prox_pos + 0.25 * direction 505 origin2 = prox_pos + 0.75 * direction 506 507 bradius = _bradius 508 509 if style == "plain": 510 orig_col = dest_col = bcolor 511 512 # proximal-distal red/green coloring 513 elif style == "pd": 514 if i <= 4 or i == 11 or i == 12: 515 orig_col = dest_col = "red" 516 else: 517 orig_col = dest_col = "green" 518 if i == 10: 519 orig_col = dest_col = "yellow" 520 else: 521 orig_col = ATOM_COLORS[col[0]] 522 dest_col = ATOM_COLORS[col[1]] 523 524 if i >= 11: # prev and next residue atoms for phi/psi calcs 525 bradius = _bradius * 0.5 # make smaller to distinguish 526 527 cap1 = pv.Sphere(center=prox_pos, radius=bradius) 528 cap2 = pv.Sphere(center=distal_pos, radius=bradius) 529 530 if style == "plain": 531 cyl = pv.Cylinder( 532 origin, direction, radius=bradius, height=height * 2.0 533 ) 534 pvp.add_mesh(cyl, color=orig_col) 535 else: 536 cyl1 = pv.Cylinder( 537 origin1, 538 direction, 539 radius=bradius, 540 height=height, 541 capping=False, 542 resolution=res, 543 ) 544 cyl2 = pv.Cylinder( 545 origin2, 546 direction, 547 radius=bradius, 548 height=height, 549 capping=False, 550 resolution=res, 551 ) 552 pvp.add_mesh(cyl1, color=orig_col) 553 pvp.add_mesh(cyl2, color=dest_col) 554 555 pvp.add_mesh(cap1, color=orig_col) 556 pvp.add_mesh(cap2, color=dest_col) 557 558 return pvp # end draw_bonds 559 560 if style == "cpk": 561 i = 0 562 for atom in atoms: 563 rad = ATOM_RADII_CPK[atom] 564 pvp.add_mesh( 565 pv.Sphere(center=coords[i], radius=rad), 566 color=ATOM_COLORS[atom], 567 smooth_shading=True, 568 specular=spec, 569 specular_power=specpow, 570 ) 571 i += 1 572 573 elif style == "cov": 574 i = 0 575 for atom in atoms: 576 rad = ATOM_RADII_COVALENT[atom] 577 pvp.add_mesh( 578 pv.Sphere(center=coords[i], radius=rad), 579 color=ATOM_COLORS[atom], 580 smooth_shading=True, 581 specular=spec, 582 specular_power=specpow, 583 ) 584 i += 1 585 586 elif style == "bs": # ball and stick 587 i = 0 588 for atom in atoms: 589 rad = ATOM_RADII_CPK[atom] * bs_scale 590 if i > 11: 591 rad = rad * 0.75 592 593 pvp.add_mesh( 594 pv.Sphere(center=coords[i], radius=rad), 595 color=ATOM_COLORS[atom], 596 smooth_shading=True, 597 specular=spec, 598 specular_power=specpow, 599 ) 600 i += 1 601 pvp = draw_bonds( 602 pvp, style="bs", missing=missing_atoms, all_atoms=all_atoms 603 ) 604 605 elif style == "sb": # splitbonds 606 pvp = draw_bonds( 607 pvp, style="sb", missing=missing_atoms, all_atoms=all_atoms 608 ) 609 610 elif style == "pd": # proximal-distal 611 pvp = draw_bonds( 612 pvp, style="pd", missing=missing_atoms, all_atoms=all_atoms 613 ) 614 615 else: # plain 616 pvp = draw_bonds( 617 pvp, 618 style="plain", 619 bcolor=bondcolor, 620 missing=missing_atoms, 621 all_atoms=all_atoms, 622 ) 623 624 return pvp 625 626 def _plot( 627 self, 628 pvplot, 629 style="bs", 630 plain=False, 631 bondcolor=BOND_COLOR, 632 bs_scale=BS_SCALE, 633 spec=SPECULARITY, 634 specpow=SPEC_POWER, 635 translate=True, 636 bond_radius=BOND_RADIUS, 637 res=100, 638 ): 639 """ 640 Update the passed pyVista plotter() object with the mesh data for the 641 input Disulfide Bond. Used internally 642 643 Parameters 644 ---------- 645 pvplot : pv.Plotter 646 pyvista.Plotter object 647 648 style : str, optional 649 Rendering style, by default 'bs'. One of 'bs', 'st', 'cpk', Render as \ 650 CPK, ball-and-stick or stick. Bonds are colored by atom color, unless \ 651 'plain' is specified. 652 653 plain : bool, optional 654 Used internally, by default False 655 656 bondcolor : pyVista color name, optional bond color for simple bonds, by default BOND_COLOR 657 658 bs_scale : float, optional 659 scale factor (0-1) to reduce the atom sizes for ball and stick, by default BS_SCALE 660 661 spec : float, optional 662 specularity (0-1), where 1 is totally smooth and 0 is rough, by default SPECULARITY 663 664 specpow : int, optional 665 exponent used for specularity calculations, by default SPEC_POWER 666 667 translate : bool, optional 668 Flag used internally to indicate if we should translate \ 669 the disulfide to its geometric center of mass, by default True. 670 671 Returns 672 ------- 673 pv.Plotter 674 Updated pv.Plotter object with atoms and bonds. 675 """ 676 677 _bradius = bond_radius 678 coords = self.internal_coords() 679 missing_atoms = self.missing_atoms 680 clen = coords.shape[0] 681 682 model = self.modelled 683 if model: 684 all_atoms = False 685 else: 686 all_atoms = True 687 688 if translate: 689 cofmass = self.cofmass() 690 for i in range(clen): 691 coords[i] = coords[i] - cofmass 692 693 atoms = ( 694 "N", 695 "C", 696 "C", 697 "O", 698 "C", 699 "SG", 700 "N", 701 "C", 702 "C", 703 "O", 704 "C", 705 "SG", 706 "C", 707 "N", 708 "C", 709 "N", 710 ) 711 pvp = pvplot.copy() 712 713 # bond connection table with atoms in the specific order shown above: 714 # returned by ss.get_internal_coords() 715 716 def draw_bonds( 717 pvp, 718 bradius=BOND_RADIUS, 719 style="sb", 720 bcolor=BOND_COLOR, 721 missing=True, 722 all_atoms=True, 723 res=100, 724 ): 725 """ 726 Generate the appropriate pyVista cylinder objects to represent 727 a particular disulfide bond. This utilizes a connection table 728 for the starting and ending atoms and a color table for the 729 bond colors. Used internally. 730 731 :param pvp: input plotter object to be updated 732 :param bradius: bond radius 733 :param style: bond style. One of sb, plain, pd 734 :param bcolor: pyvista color 735 :param missing: True if atoms are missing, False othersie 736 :param all_atoms: True if rendering O, False if only backbone rendered 737 738 :return pvp: Updated Plotter object. 739 740 """ 741 _bond_conn = np.array( 742 [ 743 [0, 1], # n-ca 744 [1, 2], # ca-c 745 [2, 3], # c-o 746 [1, 4], # ca-cb 747 [4, 5], # cb-sg 748 [6, 7], # n-ca 749 [7, 8], # ca-c 750 [8, 9], # c-o 751 [7, 10], # ca-cb 752 [10, 11], # cb-sg 753 [5, 11], # sg -sg 754 [12, 0], # cprev_prox-n 755 [2, 13], # c-nnext_prox 756 [14, 6], # cprev_dist-n_dist 757 [8, 15], # c-nnext_dist 758 ] 759 ) 760 761 # modeled disulfides only have backbone atoms since 762 # phi and psi are undefined, which makes the carbonyl 763 # oxygen (O) undefined as well. Their previous and next N 764 # are also undefined. 765 766 _bond_conn_backbone = np.array( 767 [ 768 [0, 1], # n-ca 769 [1, 2], # ca-c 770 [1, 4], # ca-cb 771 [4, 5], # cb-sg 772 [6, 7], # n-ca 773 [7, 8], # ca-c 774 [7, 10], # ca-cb 775 [10, 11], # cb-sg 776 [5, 11], # sg -sg 777 ] 778 ) 779 780 # colors for the bonds. Index into ATOM_COLORS array 781 _bond_split_colors = np.array( 782 [ 783 ("N", "C"), 784 ("C", "C"), 785 ("C", "O"), 786 ("C", "C"), 787 ("C", "SG"), 788 ("N", "C"), 789 ("C", "C"), 790 ("C", "O"), 791 ("C", "C"), 792 ("C", "SG"), 793 ("SG", "SG"), 794 # prev and next C-N bonds - color by atom Z 795 ("C", "N"), 796 ("C", "N"), 797 ("C", "N"), 798 ("C", "N"), 799 ] 800 ) 801 802 _bond_split_colors_backbone = np.array( 803 [ 804 ("N", "C"), 805 ("C", "C"), 806 ("C", "C"), 807 ("C", "SG"), 808 ("N", "C"), 809 ("C", "C"), 810 ("C", "C"), 811 ("C", "SG"), 812 ("SG", "SG"), 813 ] 814 ) 815 # work through connectivity and colors 816 orig_col = dest_col = bcolor 817 818 if all_atoms: 819 bond_conn = _bond_conn 820 bond_split_colors = _bond_split_colors 821 else: 822 bond_conn = _bond_conn_backbone 823 bond_split_colors = _bond_split_colors_backbone 824 825 for i in range(len(bond_conn)): 826 if all_atoms: 827 if i > 10 and missing_atoms == True: # skip missing atoms 828 continue 829 830 bond = bond_conn[i] 831 832 # get the indices for the origin and destination atoms 833 orig = bond[0] 834 dest = bond[1] 835 836 col = bond_split_colors[i] 837 838 # get the coords 839 prox_pos = coords[orig] 840 distal_pos = coords[dest] 841 842 # compute a direction vector 843 direction = distal_pos - prox_pos 844 845 # compute vector length. divide by 2 since split bond 846 height = math.dist(prox_pos, distal_pos) / 2.0 847 848 # the cylinder origins are actually in the 849 # middle so we translate 850 851 origin = prox_pos + 0.5 * direction # for a single plain bond 852 origin1 = prox_pos + 0.25 * direction 853 origin2 = prox_pos + 0.75 * direction 854 855 bradius = _bradius 856 857 if style == "plain": 858 orig_col = dest_col = bcolor 859 860 # proximal-distal red/green coloring 861 elif style == "pd": 862 if i <= 4 or i == 11 or i == 12: 863 orig_col = dest_col = "red" 864 else: 865 orig_col = dest_col = "green" 866 if i == 10: 867 orig_col = dest_col = "yellow" 868 else: 869 orig_col = ATOM_COLORS[col[0]] 870 dest_col = ATOM_COLORS[col[1]] 871 872 if i >= 11: # prev and next residue atoms for phi/psi calcs 873 bradius = _bradius * 0.5 # make smaller to distinguish 874 875 cap1 = pv.Sphere(center=prox_pos, radius=bradius) 876 cap2 = pv.Sphere(center=distal_pos, radius=bradius) 877 878 if style == "plain": 879 cyl = pv.Cylinder( 880 origin, direction, radius=bradius, height=height * 2.0 881 ) 882 # pvp.add_mesh(cyl, color=orig_col) 883 pvp.append(cyl) 884 else: 885 cyl1 = pv.Cylinder( 886 origin1, 887 direction, 888 radius=bradius, 889 height=height, 890 capping=False, 891 resolution=res, 892 ) 893 cyl2 = pv.Cylinder( 894 origin2, 895 direction, 896 radius=bradius, 897 height=height, 898 capping=False, 899 resolution=res, 900 ) 901 # pvp.add_mesh(cyl1, color=orig_col) 902 # pvp.add_mesh(cyl2, color=dest_col) 903 pvp.append(cyl1) 904 pvp.append(cyl2) 905 906 # pvp.add_mesh(cap1, color=orig_col) 907 # pvp.add_mesh(cap2, color=dest_col) 908 pvp.append(cap1) 909 pvp.append(cap2) 910 911 return pvp.copy() # end draw_bonds 912 913 if style == "cpk": 914 i = 0 915 for atom in atoms: 916 rad = ATOM_RADII_CPK[atom] 917 pvp.append(pv.Sphere(center=coords[i], radius=rad)) 918 i += 1 919 920 elif style == "cov": 921 i = 0 922 for atom in atoms: 923 rad = ATOM_RADII_COVALENT[atom] 924 pvp.append(pv.Sphere(center=coords[i], radius=rad)) 925 i += 1 926 927 elif style == "bs": # ball and stick 928 i = 0 929 for atom in atoms: 930 rad = ATOM_RADII_CPK[atom] * bs_scale 931 if i > 11: 932 rad = rad * 0.75 933 934 pvp.append(pv.Sphere(center=coords[i])) 935 i += 1 936 pvp = draw_bonds( 937 pvp, style="bs", missing=missing_atoms, all_atoms=all_atoms 938 ) 939 940 elif style == "sb": # splitbonds 941 pvp = draw_bonds( 942 pvp, style="sb", missing=missing_atoms, all_atoms=all_atoms 943 ) 944 945 elif style == "pd": # proximal-distal 946 pvp = draw_bonds( 947 pvp, style="pd", missing=missing_atoms, all_atoms=all_atoms 948 ) 949 950 else: # plain 951 pvp = draw_bonds( 952 pvp, 953 style="plain", 954 bcolor=bondcolor, 955 missing=missing_atoms, 956 all_atoms=all_atoms, 957 ) 958 959 return 960 961 def _handle_SS_exception(self, message: str): 962 """ 963 This method catches an exception that occurs in the Disulfide 964 object (if PERMISSIVE), or raises it again, this time adding the 965 PDB line number to the error message. (private). 966 967 :param message: Error message 968 :raises DisulfideConstructionException: Fatal construction exception. 969 970 """ 971 # message = "%s at line %i." % (message) 972 message = f"{message}" 973 974 if self.PERMISSIVE: 975 # just print a warning - some residues/atoms may be missing 976 warnings.warn( 977 "DisulfideConstructionException: %s\n" 978 "Exception ignored.\n" 979 "Some atoms may be missing in the data structure." % message, 980 DisulfideConstructionWarning, 981 ) 982 else: 983 # exceptions are fatal - raise again with new message (including line nr) 984 raise DisulfideConstructionException(message) from None 985 986 @property 987 def dihedrals(self) -> list: 988 """ 989 Return a list containing the dihedral angles for the disulfide. 990 991 """ 992 return [self.chi1, self.chi2, self.chi3, self.chi4, self.chi5] 993 994 @dihedrals.setter 995 def dihedrals(self, dihedrals: list) -> None: 996 """ 997 Sets the disulfide dihedral angles to the inputs specified in the list. 998 999 :param dihedrals: list of dihedral angles. 1000 """ 1001 self.chi1 = dihedrals[0] 1002 self.chi2 = dihedrals[1] 1003 self.chi3 = dihedrals[2] 1004 self.chi4 = dihedrals[3] 1005 self.chi5 = dihedrals[4] 1006 self.compute_torsional_energy() 1007 self.compute_torsion_length() 1008 1009 def bounding_box(self) -> np.array: 1010 """ 1011 Return the bounding box array for the given disulfide 1012 1013 Returns 1014 ------- 1015 :return: np.Array(3,2): Array containing the min, max for X, Y, and Z respectively. 1016 Does not currently take the atom's radius into account. 1017 1018 """ 1019 res = np.zeros(shape=(3, 2)) 1020 xmin, xmax = self.compute_extents("x") 1021 ymin, ymax = self.compute_extents("y") 1022 zmin, zmax = self.compute_extents("z") 1023 1024 res[0] = [xmin, xmax] 1025 res[1] = [ymin, ymax] 1026 res[2] = [zmin, zmax] 1027 1028 return res 1029 1030 def build_yourself(self) -> None: 1031 """ 1032 Build a model Disulfide based its internal dihedral state 1033 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1034 Cb, with N on left), builds disulfide, and updates the object's internal 1035 coordinates. It also adds the distal protein backbone, 1036 and computes the disulfide conformational energy. 1037 """ 1038 chi1 = self.chi1 1039 chi2 = self.chi2 1040 chi3 = self.chi3 1041 chi4 = self.chi4 1042 chi5 = self.chi5 1043 self.build_model(chi1, chi2, chi3, chi4, chi5) 1044 1045 def build_model( 1046 self, chi1: float, chi2: float, chi3: float, chi4: float, chi5: float 1047 ) -> None: 1048 """ 1049 Build a model Disulfide based on the input dihedral angles. 1050 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1051 Cb, with N on left), builds disulfide, and updates the object's internal 1052 coordinates. It also adds the distal protein backbone, 1053 and computes the disulfide conformational energy. 1054 1055 :param chi1: Chi1 (degrees) 1056 :param chi2: Chi2 (degrees) 1057 :param chi3: Chi3 (degrees) 1058 :param chi4: Chi4 (degrees) 1059 :param chi5: Chi5 (degrees) 1060 1061 Example: 1062 >>> from proteusPy.Disulfide import Disulfide 1063 >>> modss = Disulfide('model') 1064 >>> modss.build_model(-60, -60, -90, -60, -60) 1065 >>> modss.display(style='sb') 1066 """ 1067 1068 self.set_dihedrals(chi1, chi2, chi3, chi4, chi5) 1069 self.proximal = 1 1070 self.distal = 2 1071 1072 tmp = Turtle3D("tmp") 1073 tmp.Orientation = 1 1074 1075 n = Vector(0, 0, 0) 1076 ca = Vector(0, 0, 0) 1077 cb = Vector(0, 0, 0) 1078 c = Vector(0, 0, 0) 1079 1080 self.ca_prox = tmp._position 1081 tmp.schain_to_bbone() 1082 n, ca, cb, c = build_residue(tmp) 1083 1084 self.n_prox = n 1085 self.ca_prox = ca 1086 self.c_prox = c 1087 self.cb_prox = cb 1088 1089 tmp.bbone_to_schain() 1090 tmp.move(1.53) 1091 tmp.roll(self.chi1) 1092 tmp.yaw(112.8) 1093 self.cb_prox = Vector(tmp._position) 1094 1095 tmp.move(1.86) 1096 tmp.roll(self.chi2) 1097 tmp.yaw(103.8) 1098 self.sg_prox = Vector(tmp._position) 1099 1100 tmp.move(2.044) 1101 tmp.roll(self.chi3) 1102 tmp.yaw(103.8) 1103 self.sg_dist = Vector(tmp._position) 1104 1105 tmp.move(1.86) 1106 tmp.roll(self.chi4) 1107 tmp.yaw(112.8) 1108 self.cb_dist = Vector(tmp._position) 1109 1110 tmp.move(1.53) 1111 tmp.roll(self.chi5) 1112 tmp.pitch(180.0) 1113 1114 tmp.schain_to_bbone() 1115 1116 n, ca, cb, c = build_residue(tmp) 1117 1118 self.n_dist = n 1119 self.ca_dist = ca 1120 self.c_dist = c 1121 self.compute_torsional_energy() 1122 self.compute_local_coords() 1123 self.ca_distance = distance3d(self.ca_prox, self.ca_dist) 1124 self.cb_distance = distance3d(self.cb_prox, self.cb_dist) 1125 self.torsion_array = np.array( 1126 (self.chi1, self.chi2, self.chi3, self.chi4, self.chi5) 1127 ) 1128 self.compute_torsion_length() 1129 self.compute_rho() 1130 self.missing_atoms = True 1131 self.modelled = True 1132 1133 def cofmass(self) -> np.array: 1134 """ 1135 Return the geometric center of mass for the internal coordinates of 1136 the given Disulfide. Missing atoms are not included. 1137 1138 :return: 3D array for the geometric center of mass 1139 """ 1140 1141 res = self.internal_coords() 1142 return res.mean(axis=0) 1143 1144 def copy(self): 1145 """ 1146 Copy the Disulfide. 1147 1148 :return: A copy of self. 1149 """ 1150 return copy.deepcopy(self) 1151 1152 def compute_extents(self, dim="z"): 1153 """ 1154 Calculate the internal coordinate extents for the input axis. 1155 1156 :param dim: Axis, one of 'x', 'y', 'z', by default 'z' 1157 :return: min, max 1158 """ 1159 1160 ic = self.internal_coords() 1161 # set default index to 'z' 1162 idx = 2 1163 1164 if dim == "x": 1165 idx = 0 1166 elif dim == "y": 1167 idx = 1 1168 elif dim == "z": 1169 idx = 2 1170 1171 _min = ic[:, idx].min() 1172 _max = ic[:, idx].max() 1173 return _min, _max 1174 1175 def compute_local_coords(self) -> None: 1176 """ 1177 Compute the internal coordinates for a properly initialized Disulfide Object. 1178 1179 :param self: SS initialized Disulfide object 1180 :returns: None, modifies internal state of the input 1181 """ 1182 1183 turt = Turtle3D("tmp") 1184 # get the coordinates as np.array for Turtle3D use. 1185 cpp = self.c_prev_prox.get_array() 1186 nnp = self.n_next_prox.get_array() 1187 1188 n = self.n_prox.get_array() 1189 ca = self.ca_prox.get_array() 1190 c = self.c_prox.get_array() 1191 cb = self.cb_prox.get_array() 1192 o = self.o_prox.get_array() 1193 sg = self.sg_prox.get_array() 1194 1195 sg2 = self.sg_dist.get_array() 1196 cb2 = self.cb_dist.get_array() 1197 ca2 = self.ca_dist.get_array() 1198 c2 = self.c_dist.get_array() 1199 n2 = self.n_dist.get_array() 1200 o2 = self.o_dist.get_array() 1201 1202 cpd = self.c_prev_dist.get_array() 1203 nnd = self.n_next_dist.get_array() 1204 1205 turt.orient_from_backbone(n, ca, c, cb, ORIENT_SIDECHAIN) 1206 1207 # internal (local) coordinates, stored as Vector objects 1208 # to_local returns np.array objects 1209 1210 self._n_prox = Vector(turt.to_local(n)) 1211 self._ca_prox = Vector(turt.to_local(ca)) 1212 self._c_prox = Vector(turt.to_local(c)) 1213 self._o_prox = Vector(turt.to_local(o)) 1214 self._cb_prox = Vector(turt.to_local(cb)) 1215 self._sg_prox = Vector(turt.to_local(sg)) 1216 1217 self._c_prev_prox = Vector(turt.to_local(cpp)) 1218 self._n_next_prox = Vector(turt.to_local(nnp)) 1219 self._c_prev_dist = Vector(turt.to_local(cpd)) 1220 self._n_next_dist = Vector(turt.to_local(nnd)) 1221 1222 self._n_dist = Vector(turt.to_local(n2)) 1223 self._ca_dist = Vector(turt.to_local(ca2)) 1224 self._c_dist = Vector(turt.to_local(c2)) 1225 self._o_dist = Vector(turt.to_local(o2)) 1226 self._cb_dist = Vector(turt.to_local(cb2)) 1227 self._sg_dist = Vector(turt.to_local(sg2)) 1228 1229 def compute_torsional_energy(self) -> float: 1230 """ 1231 Compute the approximate torsional energy for the Disulfide's 1232 conformation and sets its internal state. 1233 1234 :return: Energy (kcal/mol) 1235 """ 1236 # @TODO find citation for the ss bond energy calculation 1237 1238 def torad(deg): 1239 return np.radians(deg) 1240 1241 chi1 = self.chi1 1242 chi2 = self.chi2 1243 chi3 = self.chi3 1244 chi4 = self.chi4 1245 chi5 = self.chi5 1246 1247 energy = 2.0 * (cos(torad(3.0 * chi1)) + cos(torad(3.0 * chi5))) 1248 energy += cos(torad(3.0 * chi2)) + cos(torad(3.0 * chi4)) 1249 energy += 3.5 * cos(torad(2.0 * chi3)) + 0.6 * cos(torad(3.0 * chi3)) + 10.1 1250 1251 self.energy = energy 1252 return energy 1253 1254 def display(self, single=True, style="sb", light=True, shadows=False) -> None: 1255 """ 1256 Display the Disulfide bond in the specific rendering style. 1257 1258 :param single: Display the bond in a single panel in the specific style. 1259 :param style: Rendering style: One of: 1260 * 'sb' - split bonds 1261 * 'bs' - ball and stick 1262 * 'cpk' - CPK style 1263 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1264 * 'plain' - boring single color 1265 :param light: If True, light background, if False, dark 1266 1267 Example: 1268 >>> import proteusPy 1269 >>> from proteusPy.Disulfide import Disulfide 1270 >>> from proteusPy.DisulfideLoader import DisulfideLoader, Load_PDB_SS 1271 1272 >>> PDB_SS = Load_PDB_SS(verbose=False, subset=True) 1273 >>> ss = PDB_SS[0] 1274 >>> ss.display(style='cpk') 1275 >>> ss.screenshot(style='bs', fname='proteus_logo_sb.png') 1276 """ 1277 src = self.pdb_id 1278 enrg = self.energy 1279 1280 title = f"{src}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol. Cα: {self.ca_distance:.2f} Å Cβ: {self.cb_distance:.2f} Å Tors: {self.torsion_length:.2f}°" 1281 1282 if light: 1283 pv.set_plot_theme("document") 1284 else: 1285 pv.set_plot_theme("dark") 1286 1287 if single == True: 1288 _pl = pv.Plotter(window_size=WINSIZE) 1289 _pl.add_title(title=title, font_size=FONTSIZE) 1290 _pl.enable_anti_aliasing("msaa") 1291 # _pl.add_camera_orientation_widget() 1292 1293 self._render( 1294 _pl, 1295 style=style, 1296 bs_scale=BS_SCALE, 1297 spec=SPECULARITY, 1298 specpow=SPEC_POWER, 1299 ) 1300 _pl.reset_camera() 1301 if shadows == True: 1302 _pl.enable_shadows() 1303 _pl.show() 1304 1305 else: 1306 pl = pv.Plotter(window_size=WINSIZE, shape=(2, 2)) 1307 pl.subplot(0, 0) 1308 1309 pl.add_title(title=title, font_size=FONTSIZE) 1310 pl.enable_anti_aliasing("msaa") 1311 1312 # pl.add_camera_orientation_widget() 1313 1314 self._render( 1315 pl, 1316 style="cpk", 1317 bondcolor=BOND_COLOR, 1318 bs_scale=BS_SCALE, 1319 spec=SPECULARITY, 1320 specpow=SPEC_POWER, 1321 ) 1322 1323 pl.subplot(0, 1) 1324 pl.add_title(title=title, font_size=FONTSIZE) 1325 1326 self._render( 1327 pl, 1328 style="bs", 1329 bondcolor=BOND_COLOR, 1330 bs_scale=BS_SCALE, 1331 spec=SPECULARITY, 1332 specpow=SPEC_POWER, 1333 ) 1334 1335 pl.subplot(1, 0) 1336 pl.add_title(title=title, font_size=FONTSIZE) 1337 1338 self._render( 1339 pl, 1340 style="sb", 1341 bondcolor=BOND_COLOR, 1342 bs_scale=BS_SCALE, 1343 spec=SPECULARITY, 1344 specpow=SPEC_POWER, 1345 ) 1346 1347 pl.subplot(1, 1) 1348 pl.add_title(title=title, font_size=FONTSIZE) 1349 1350 self._render( 1351 pl, 1352 style="pd", 1353 bondcolor=BOND_COLOR, 1354 bs_scale=BS_SCALE, 1355 spec=SPECULARITY, 1356 specpow=SPEC_POWER, 1357 ) 1358 1359 pl.link_views() 1360 pl.reset_camera() 1361 if shadows == True: 1362 pl.enable_shadows() 1363 pl.show() 1364 return 1365 1366 def plot( 1367 self, pl, single=True, style="sb", light=True, shadows=False 1368 ) -> pv.Plotter: 1369 """ 1370 Return the pyVista Plotter object for the Disulfide bond in the specific rendering style. 1371 1372 :param single: Display the bond in a single panel in the specific style. 1373 :param style: Rendering style: One of: 1374 * 'sb' - split bonds 1375 * 'bs' - ball and stick 1376 * 'cpk' - CPK style 1377 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1378 * 'plain' - boring single color 1379 :param light: If True, light background, if False, dark 1380 """ 1381 src = self.pdb_id 1382 enrg = self.energy 1383 1384 title = f"{src}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol. Cα: {self.ca_distance:.2f} Å Cβ: {self.cb_distance:.2f} Å Tors: {self.torsion_length:.2f}°" 1385 1386 if light: 1387 pv.set_plot_theme("document") 1388 else: 1389 pv.set_plot_theme("dark") 1390 1391 if single == True: 1392 # _pl = pv.Plotter(window_size=WINSIZE) 1393 # _pl.add_title(title=title, font_size=FONTSIZE) 1394 pl.clear() 1395 pl.enable_anti_aliasing("msaa") 1396 # pl.add_camera_orientation_widget() 1397 1398 self._render( 1399 pl, 1400 style=style, 1401 bs_scale=BS_SCALE, 1402 spec=SPECULARITY, 1403 specpow=SPEC_POWER, 1404 ) 1405 pl.reset_camera() 1406 if shadows == True: 1407 pl.enable_shadows() 1408 else: 1409 pl = pv.Plotter(shape=(2, 2)) 1410 pl.subplot(0, 0) 1411 1412 # pl.add_title(title=title, font_size=FONTSIZE) 1413 pl.enable_anti_aliasing("msaa") 1414 1415 # pl.add_camera_orientation_widget() 1416 1417 self._render( 1418 pl, 1419 style="cpk", 1420 bondcolor=BOND_COLOR, 1421 bs_scale=BS_SCALE, 1422 spec=SPECULARITY, 1423 specpow=SPEC_POWER, 1424 ) 1425 1426 pl.subplot(0, 1) 1427 1428 self._render( 1429 pl, 1430 style="bs", 1431 bondcolor=BOND_COLOR, 1432 bs_scale=BS_SCALE, 1433 spec=SPECULARITY, 1434 specpow=SPEC_POWER, 1435 ) 1436 1437 pl.subplot(1, 0) 1438 1439 self._render( 1440 pl, 1441 style="sb", 1442 bondcolor=BOND_COLOR, 1443 bs_scale=BS_SCALE, 1444 spec=SPECULARITY, 1445 specpow=SPEC_POWER, 1446 ) 1447 1448 pl.subplot(1, 1) 1449 self._render( 1450 pl, 1451 style="pd", 1452 bondcolor=BOND_COLOR, 1453 bs_scale=BS_SCALE, 1454 spec=SPECULARITY, 1455 specpow=SPEC_POWER, 1456 ) 1457 1458 pl.link_views() 1459 pl.reset_camera() 1460 if shadows == True: 1461 pl.enable_shadows() 1462 return pl 1463 1464 def Distance_neighbors(self, others: DisulfideList, cutoff: float) -> DisulfideList: 1465 """ 1466 Return list of Disulfides whose RMS atomic distance is within 1467 the cutoff (Å) in the others list. 1468 1469 :param others: DisulfideList to search 1470 :param cutoff: Distance cutoff (Å) 1471 :return: DisulfideList within the cutoff 1472 """ 1473 1474 res = [ss.copy() for ss in others if self.Distance_RMS(ss) < cutoff] 1475 return DisulfideList(res, "neighbors") 1476 1477 def Distance_RMS(self, other) -> float: 1478 """ 1479 Calculate the RMS distance between the internal coordinates of self and another Disulfide. 1480 :param other: Comparison Disulfide 1481 :return: RMS distance (Å) 1482 """ 1483 1484 # Get internal coordinates of both objects 1485 ic1 = self.internal_coords() 1486 ic2 = other.internal_coords() 1487 1488 # Compute the sum of squared differences between corresponding internal coordinates 1489 totsq = sum(math.dist(p1, p2) ** 2 for p1, p2 in zip(ic1, ic2)) 1490 1491 # Compute the mean of the squared distances 1492 totsq /= len(ic1) 1493 1494 # Take the square root of the mean to get the RMS distance 1495 return math.sqrt(totsq) 1496 1497 def Torsion_RMS(self, other) -> float: 1498 """ 1499 Calculate the RMS distance between the dihedral angles of self and another Disulfide. 1500 :param other: Comparison Disulfide 1501 :return: RMS distance (deg). 1502 """ 1503 import math 1504 1505 # Get internal coordinates of both objects 1506 ic1 = self.torsion_array 1507 ic2 = other.torsion_array 1508 1509 # Compute the sum of squared differences between corresponding internal coordinates 1510 totsq = sum((p1 - p2) ** 2 for p1, p2 in zip(ic1, ic2)) 1511 # Compute the mean of the squared distances 1512 totsq /= len(ic1) 1513 1514 # Take the square root of the mean to get the RMS distance 1515 return math.sqrt(totsq) 1516 1517 def get_chains(self) -> tuple: 1518 """ 1519 Return the proximal and distal chain IDs for the Disulfide. 1520 1521 :return: tuple (proximal, distal) chain IDs 1522 """ 1523 prox = self.proximal_chain 1524 dist = self.distal_chain 1525 return tuple(prox, dist) 1526 1527 def get_permissive(self) -> bool: 1528 """ 1529 Return the Permissive flag state. (Used in PDB parsing) 1530 1531 :return: Permissive state 1532 """ 1533 return self.PERMISIVE 1534 1535 def get_full_id(self) -> tuple: 1536 """ 1537 Return the Disulfide full IDs (Used with BIO.PDB) 1538 1539 :return: Disulfide full IDs 1540 """ 1541 return (self.proximal_residue_fullid, self.distal_residue_fullid) 1542 1543 def initialize_disulfide_from_chain( 1544 self, chain1, chain2, proximal, distal, resolution, quiet=True 1545 ) -> None: 1546 """ 1547 Initialize a new Disulfide object with atomic coordinates from 1548 the proximal and distal coordinates, typically taken from a PDB file. 1549 This routine is primarily used internally when building the compressed 1550 database. 1551 1552 :param chain1: list of Residues in the model, eg: chain = model['A'] 1553 :param chain2: list of Residues in the model, eg: chain = model['A'] 1554 :param proximal: proximal residue sequence ID 1555 :param distal: distal residue sequence ID 1556 :param resolution: structure resolution 1557 :param quiet: Quiet or noisy parsing, defaults to True 1558 :raises DisulfideConstructionWarning: Raised when not parsed correctly 1559 """ 1560 id = chain1.get_full_id()[0] 1561 self.pdb_id = id 1562 1563 chi1 = chi2 = chi3 = chi4 = chi5 = _ANG_INIT 1564 1565 prox = int(proximal) 1566 dist = int(distal) 1567 1568 prox_residue = chain1[prox] 1569 dist_residue = chain2[dist] 1570 1571 if prox_residue.get_resname() != "CYS" or dist_residue.get_resname() != "CYS": 1572 print( 1573 f"build_disulfide() requires CYS at both residues: {prox} {prox_residue.get_resname()} {dist} {dist_residue.get_resname()} Chain: {prox_residue.get_segid()}" 1574 ) 1575 1576 # set the objects proximal and distal values 1577 self.set_resnum(proximal, distal) 1578 1579 if resolution is not None: 1580 self.resolution = resolution 1581 1582 self.proximal_chain = chain1.get_id() 1583 self.distal_chain = chain2.get_id() 1584 1585 self.proximal_residue_fullid = prox_residue.get_full_id() 1586 self.distal_residue_fullid = dist_residue.get_full_id() 1587 1588 if quiet: 1589 warnings.filterwarnings("ignore", category=DisulfideConstructionWarning) 1590 else: 1591 warnings.simplefilter("always") 1592 1593 # grab the coordinates for the proximal and distal residues as vectors 1594 # so we can do math on them later 1595 # proximal residue 1596 1597 try: 1598 n1 = prox_residue["N"].get_vector() 1599 ca1 = prox_residue["CA"].get_vector() 1600 c1 = prox_residue["C"].get_vector() 1601 o1 = prox_residue["O"].get_vector() 1602 cb1 = prox_residue["CB"].get_vector() 1603 sg1 = prox_residue["SG"].get_vector() 1604 1605 except Exception: 1606 raise DisulfideConstructionWarning( 1607 f"Invalid or missing coordinates for proximal residue {proximal}" 1608 ) from None 1609 1610 # distal residue 1611 try: 1612 n2 = dist_residue["N"].get_vector() 1613 ca2 = dist_residue["CA"].get_vector() 1614 c2 = dist_residue["C"].get_vector() 1615 o2 = dist_residue["O"].get_vector() 1616 cb2 = dist_residue["CB"].get_vector() 1617 sg2 = dist_residue["SG"].get_vector() 1618 1619 except Exception: 1620 raise DisulfideConstructionWarning( 1621 f"Invalid or missing coordinates for distal residue {distal}" 1622 ) from None 1623 1624 # previous residue and next residue - optional, used for phi, psi calculations 1625 try: 1626 prevprox = chain1[prox - 1] 1627 nextprox = chain1[prox + 1] 1628 1629 prevdist = chain2[dist - 1] 1630 nextdist = chain2[dist + 1] 1631 1632 cprev_prox = prevprox["C"].get_vector() 1633 nnext_prox = nextprox["N"].get_vector() 1634 1635 cprev_dist = prevdist["C"].get_vector() 1636 nnext_dist = nextdist["N"].get_vector() 1637 1638 # compute phi, psi for prox and distal 1639 self.phiprox = np.degrees(calc_dihedral(cprev_prox, n1, ca1, c1)) 1640 self.psiprox = np.degrees(calc_dihedral(n1, ca1, c1, nnext_prox)) 1641 self.phidist = np.degrees(calc_dihedral(cprev_dist, n2, ca2, c2)) 1642 self.psidist = np.degrees(calc_dihedral(n2, ca2, c2, nnext_dist)) 1643 1644 except Exception: 1645 mess = f"Missing coords for: {id} {prox-1} or {dist+1} for SS {proximal}-{distal}" 1646 cprev_prox = nnext_prox = cprev_dist = nnext_dist = Vector(-1.0, -1.0, -1.0) 1647 self.missing_atoms = True 1648 warnings.warn(mess, DisulfideConstructionWarning) 1649 1650 # update the positions and conformation 1651 self.set_positions( 1652 n1, 1653 ca1, 1654 c1, 1655 o1, 1656 cb1, 1657 sg1, 1658 n2, 1659 ca2, 1660 c2, 1661 o2, 1662 cb2, 1663 sg2, 1664 cprev_prox, 1665 nnext_prox, 1666 cprev_dist, 1667 nnext_dist, 1668 ) 1669 1670 # calculate and set the disulfide dihedral angles 1671 self.chi1 = np.degrees(calc_dihedral(n1, ca1, cb1, sg1)) 1672 self.chi2 = np.degrees(calc_dihedral(ca1, cb1, sg1, sg2)) 1673 self.chi3 = np.degrees(calc_dihedral(cb1, sg1, sg2, cb2)) 1674 self.chi4 = np.degrees(calc_dihedral(sg1, sg2, cb2, ca2)) 1675 self.chi5 = np.degrees(calc_dihedral(sg2, cb2, ca2, n2)) 1676 self.rho = np.degrees(calc_dihedral(n1, ca1, ca2, n2)) 1677 1678 self.ca_distance = distance3d(self.ca_prox, self.ca_dist) 1679 self.cb_distance = distance3d(self.cb_prox, self.cb_dist) 1680 self.torsion_array = np.array( 1681 (self.chi1, self.chi2, self.chi3, self.chi4, self.chi5) 1682 ) 1683 self.compute_torsion_length() 1684 1685 # calculate and set the SS bond torsional energy 1686 self.compute_torsional_energy() 1687 1688 # compute and set the local coordinates 1689 self.compute_local_coords() 1690 1691 def internal_coords(self) -> np.array: 1692 """ 1693 Return the internal coordinates for the Disulfide. 1694 If there are missing atoms the extra atoms for the proximal 1695 and distal N and C are set to [0,0,0]. This is needed for the center of 1696 mass calculations, used when rendering. 1697 1698 :return: Array containing the coordinates, [16][3]. 1699 """ 1700 1701 # if we don't have the prior and next atoms we initialize those 1702 # atoms to the origin so as to not effect the center of mass calculations 1703 if self.missing_atoms: 1704 res_array = np.array( 1705 ( 1706 self._n_prox.get_array(), 1707 self._ca_prox.get_array(), 1708 self._c_prox.get_array(), 1709 self._o_prox.get_array(), 1710 self._cb_prox.get_array(), 1711 self._sg_prox.get_array(), 1712 self._n_dist.get_array(), 1713 self._ca_dist.get_array(), 1714 self._c_dist.get_array(), 1715 self._o_dist.get_array(), 1716 self._cb_dist.get_array(), 1717 self._sg_dist.get_array(), 1718 [0, 0, 0], 1719 [0, 0, 0], 1720 [0, 0, 0], 1721 [0, 0, 0], 1722 ) 1723 ) 1724 else: 1725 res_array = np.array( 1726 ( 1727 self._n_prox.get_array(), 1728 self._ca_prox.get_array(), 1729 self._c_prox.get_array(), 1730 self._o_prox.get_array(), 1731 self._cb_prox.get_array(), 1732 self._sg_prox.get_array(), 1733 self._n_dist.get_array(), 1734 self._ca_dist.get_array(), 1735 self._c_dist.get_array(), 1736 self._o_dist.get_array(), 1737 self._cb_dist.get_array(), 1738 self._sg_dist.get_array(), 1739 self._c_prev_prox.get_array(), 1740 self._n_next_prox.get_array(), 1741 self._c_prev_dist.get_array(), 1742 self._n_next_dist.get_array(), 1743 ) 1744 ) 1745 return res_array 1746 1747 def internal_coords_res(self, resnumb) -> np.array: 1748 """ 1749 Return the internal coordinates for the internal coordinates of 1750 the given Disulfide. Missing atoms are not included. 1751 1752 :param resnumb: Residue number for disulfide 1753 :raises DisulfideConstructionWarning: Warning raised if the residue number is invalid 1754 :return: Array containing the internal coordinates for the disulfide 1755 """ 1756 res_array = np.zeros(shape=(6, 3)) 1757 1758 if resnumb == self.proximal: 1759 res_array = np.array( 1760 ( 1761 self._n_prox.get_array(), 1762 self._ca_prox.get_array(), 1763 self._c_prox.get_array(), 1764 self._o_prox.get_array(), 1765 self._cb_prox.get_array(), 1766 self._sg_prox.get_array(), 1767 ) 1768 ) 1769 return res_array 1770 1771 elif resnumb == self.distal: 1772 res_array = np.array( 1773 ( 1774 self._n_dist.get_array(), 1775 self._ca_dist.get_array(), 1776 self._c_dist.get_array(), 1777 self._o_dist.get_array(), 1778 self._cb_dist.get_array(), 1779 self._sg_dist.get_array(), 1780 ) 1781 ) 1782 return res_array 1783 else: 1784 mess = f"-> Disulfide.internal_coords(): Invalid argument. \ 1785 Unable to find residue: {resnumb} " 1786 raise DisulfideConstructionWarning(mess) 1787 1788 def make_movie( 1789 self, style="sb", fname="ssbond.mp4", verbose=False, steps=360 1790 ) -> None: 1791 """ 1792 Create an animation for ```self``` rotating one revolution about the Y axis, 1793 in the given ```style```, saving to ```filename```. 1794 1795 :param style: Rendering style, defaults to 'sb', one of: 1796 * 'sb' - split bonds 1797 * 'bs' - ball and stick 1798 * 'cpk' - CPK style 1799 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1800 * 'plain' - boring single color 1801 1802 :param fname: Output filename, defaults to ```ssbond.mp4``` 1803 :param verbose: Verbosity, defaults to False 1804 :param steps: Number of steps for one complete rotation, defaults to 360. 1805 """ 1806 src = self.pdb_id 1807 name = self.name 1808 enrg = self.energy 1809 1810 title = f"{src} {name}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol, Cα: {self.ca_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 1811 1812 if verbose: 1813 print(f"Rendering animation to {fname}...") 1814 1815 pl = pv.Plotter(window_size=WINSIZE, off_screen=True) 1816 pl.open_movie(fname) 1817 path = pl.generate_orbital_path(n_points=steps) 1818 1819 # 1820 # pl.add_title(title=title, font_size=FONTSIZE) 1821 pl.enable_anti_aliasing("msaa") 1822 # pl.add_camera_orientation_widget() 1823 pl = self._render( 1824 pl, 1825 style=style, 1826 bondcolor=BOND_COLOR, 1827 bs_scale=BS_SCALE, 1828 spec=SPECULARITY, 1829 specpow=SPEC_POWER, 1830 ) 1831 pl.reset_camera() 1832 pl.orbit_on_path(path, write_frames=True) 1833 pl.close() 1834 1835 if verbose: 1836 print(f"Saved mp4 animation to: {fname}") 1837 1838 def pprint(self) -> None: 1839 """ 1840 Pretty print general info for the Disulfide 1841 """ 1842 s1 = self.repr_ss_info() 1843 s2 = self.repr_ss_ca_dist() 1844 s3 = self.repr_ss_conformation() 1845 s4 = self.repr_ss_torsion_length() 1846 res = f"{s1} \n{s3} \n{s2} \n{s4}>" 1847 print(res) 1848 1849 def pprint_all(self) -> None: 1850 """ 1851 Pretty print all info for the Disulfide 1852 """ 1853 s1 = self.repr_ss_info() + "\n" 1854 s2 = self.repr_ss_coords() 1855 s3 = self.repr_ss_local_coords() 1856 s4 = self.repr_ss_conformation() 1857 s5 = self.repr_chain_ids() 1858 s6 = self.repr_ss_ca_dist() 1859 s7 = self.repr_ss_torsion_length() 1860 1861 res = f"{s1} {s5} {s2} {s3} {s4}\n {s6}\n {s7}>" 1862 1863 print(res) 1864 1865 # repr functions. The class is large, so I split it up into sections 1866 def repr_ss_info(self) -> str: 1867 """ 1868 Representation for the Disulfide class 1869 """ 1870 s1 = f"<Disulfide {self.name}, Source: {self.pdb_id}, Resolution: {self.resolution} Å" 1871 return s1 1872 1873 def repr_ss_coords(self) -> str: 1874 """ 1875 Representation for Disulfide coordinates 1876 """ 1877 s2 = f"\nProximal Coordinates:\n N: {self.n_prox}\n Cα: {self.ca_prox}\n C: {self.c_prox}\n O: {self.o_prox}\n Cβ: {self.cb_prox}\n Sγ: {self.sg_prox}\n Cprev {self.c_prev_prox}\n Nnext: {self.n_next_prox}\n" 1878 s3 = f"Distal Coordinates:\n N: {self.n_dist}\n Cα: {self.ca_dist}\n C: {self.c_dist}\n O: {self.o_dist}\n Cβ: {self.cb_dist}\n Sγ: {self.sg_dist}\n Cprev {self.c_prev_dist}\n Nnext: {self.n_next_dist}\n\n" 1879 stot = f"{s2} {s3}" 1880 return stot 1881 1882 def repr_ss_conformation(self) -> str: 1883 """ 1884 Representation for Disulfide conformation 1885 """ 1886 s4 = f"Χ1-Χ5: {self.chi1:.2f}°, {self.chi2:.2f}°, {self.chi3:.2f}°, {self.chi4:.2f}° {self.chi5:.2f}°, {self.rho:.2f}°, {self.energy:.2f} kcal/mol" 1887 stot = f"{s4}" 1888 return stot 1889 1890 def repr_ss_local_coords(self) -> str: 1891 """ 1892 Representation for the Disulfide internal coordinates. 1893 """ 1894 s2i = f"Proximal Internal Coords:\n N: {self._n_prox}\n Cα: {self._ca_prox}\n C: {self._c_prox}\n O: {self._o_prox}\n Cβ: {self._cb_prox}\n Sγ: {self._sg_prox}\n Cprev {self.c_prev_prox}\n Nnext: {self.n_next_prox}\n" 1895 s3i = f"Distal Internal Coords:\n N: {self._n_dist}\n Cα: {self._ca_dist}\n C: {self._c_dist}\n O: {self._o_dist}\n Cβ: {self._cb_dist}\n Sγ: {self._sg_dist}\n Cprev {self.c_prev_dist}\n Nnext: {self.n_next_dist}\n" 1896 stot = f"{s2i}{s3i}" 1897 return stot 1898 1899 def repr_ss_chain_ids(self) -> str: 1900 """ 1901 Representation for Disulfide chain IDs 1902 """ 1903 return f"Proximal Chain fullID: <{self.proximal_residue_fullid}> Distal Chain fullID: <{self.distal_residue_fullid}>" 1904 1905 def repr_ss_ca_dist(self) -> str: 1906 """ 1907 Representation for Disulfide Ca distance 1908 """ 1909 s1 = f"Cα Distance: {self.ca_distance:.2f} Å" 1910 return s1 1911 1912 def repr_ss_cb_dist(self) -> str: 1913 """ 1914 Representation for Disulfide Ca distance 1915 """ 1916 s1 = f"Cβ Distance: {self.cb_distance:.2f} Å" 1917 return s1 1918 1919 def repr_ss_torsion_length(self) -> str: 1920 """ 1921 Representation for Disulfide torsion length 1922 """ 1923 s1 = f"Torsion length: {self.torsion_length:.2f} deg" 1924 return s1 1925 1926 def repr_all(self) -> str: 1927 """ 1928 Return a string representation for all Disulfide information 1929 contained in self. 1930 """ 1931 1932 s1 = self.repr_ss_info() + "\n" 1933 s2 = self.repr_ss_coords() 1934 s3 = self.repr_ss_local_coords() 1935 s4 = self.repr_ss_conformation() 1936 s5 = self.repr_chain_ids() 1937 s6 = self.repr_ss_ca_dist() 1938 s8 = self.repr_ss_cb_dist() 1939 s7 = self.repr_ss_torsion_length() 1940 1941 res = f"{s1} {s5} {s2} {s3} {s4} {s6} {s7} {s8}>" 1942 return res 1943 1944 def repr_compact(self) -> str: 1945 """ 1946 Return a compact representation of the Disulfide object 1947 :return: string 1948 """ 1949 return f"{self.repr_ss_info()} {self.repr_ss_conformation()}" 1950 1951 def repr_conformation(self) -> str: 1952 """ 1953 Return a string representation of the Disulfide object's conformation. 1954 :return: string 1955 """ 1956 return f"{self.repr_ss_conformation()}" 1957 1958 def repr_coords(self) -> str: 1959 """ 1960 Return a string representation of the Disulfide object's coordinates. 1961 :return: string 1962 """ 1963 return f"{self.repr_ss_coords()}" 1964 1965 def repr_internal_coords(self) -> str: 1966 """ 1967 Return a string representation of the Disulfide object's internal coordinaes. 1968 :return: string 1969 """ 1970 return f"{self.repr_ss_local_coords()}" 1971 1972 def repr_chain_ids(self) -> str: 1973 """ 1974 Return a string representation of the Disulfide object's chain ids. 1975 :return: string 1976 """ 1977 return f"{self.repr_ss_chain_ids()}" 1978 1979 def compute_rho(self) -> float: 1980 self.rho = calc_dihedral(self.n_prox, self.ca_prox, self.ca_dist, self.n_dist) 1981 return self.rho 1982 1983 def reset(self) -> None: 1984 """ 1985 Resets the disulfide object to its initial state. All distances, 1986 angles and positions are reset. The name is unchanged. 1987 """ 1988 self.__init__(self) 1989 1990 def same_chains(self) -> bool: 1991 """ 1992 Function checks if the Disulfide is cross-chain or not. 1993 1994 Returns 1995 ------- 1996 bool \n 1997 True if the proximal and distal residues are on the same chains, 1998 False otherwise. 1999 """ 2000 2001 (prox, dist) = self.get_chains() 2002 return prox == dist 2003 2004 def screenshot( 2005 self, 2006 single=True, 2007 style="sb", 2008 fname="ssbond.png", 2009 verbose=False, 2010 shadows=False, 2011 light=True, 2012 ) -> None: 2013 """ 2014 Create and save a screenshot of the Disulfide in the given style 2015 and filename 2016 2017 :param single: Display a single vs panel view, defaults to True 2018 :param style: Rendering style, one of: 2019 * 'sb' - split bonds 2020 * 'bs' - ball and stick 2021 * 'cpk' - CPK style 2022 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2023 * 'plain' - boring single color, 2024 :param fname: output filename,, defaults to 'ssbond.png' 2025 :param verbose: Verbosit, defaults to False 2026 :param shadows: Enable shadows, defaults to False 2027 """ 2028 src = self.pdb_id 2029 name = self.name 2030 enrg = self.energy 2031 2032 title = f"{src} {name}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol, Cα: {self.ca_distance:.2f} Å, Cβ: {self.cb_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 2033 2034 if light: 2035 pv.set_plot_theme("document") 2036 else: 2037 pv.set_plot_theme("dark") 2038 2039 if verbose: 2040 print(f"-> screenshot(): Rendering screenshot to file {fname}") 2041 2042 if single: 2043 pl = pv.Plotter(window_size=WINSIZE) 2044 # pl.add_title(title=title, font_size=FONTSIZE) 2045 pl.enable_anti_aliasing("msaa") 2046 # pl.add_camera_orientation_widget() 2047 self._render( 2048 pl, 2049 style=style, 2050 bondcolor=BOND_COLOR, 2051 bs_scale=BS_SCALE, 2052 spec=SPECULARITY, 2053 specpow=SPEC_POWER, 2054 ) 2055 pl.reset_camera() 2056 if shadows: 2057 pl.enable_shadows() 2058 2059 pl.show(auto_close=False) 2060 pl.screenshot(fname) 2061 pl.clear() 2062 2063 else: 2064 pl = pv.Plotter(window_size=WINSIZE, shape=(2, 2)) 2065 pl.subplot(0, 0) 2066 2067 # pl.add_title(title=title, font_size=FONTSIZE) 2068 pl.enable_anti_aliasing("msaa") 2069 2070 # pl.add_camera_orientation_widget() 2071 self._render( 2072 pl, 2073 style="cpk", 2074 bondcolor=BOND_COLOR, 2075 bs_scale=BS_SCALE, 2076 spec=SPECULARITY, 2077 specpow=SPEC_POWER, 2078 ) 2079 2080 pl.subplot(0, 1) 2081 # pl.add_title(title=title, font_size=FONTSIZE) 2082 self._render( 2083 pl, 2084 style="pd", 2085 bondcolor=BOND_COLOR, 2086 bs_scale=BS_SCALE, 2087 spec=SPECULARITY, 2088 specpow=SPEC_POWER, 2089 ) 2090 2091 pl.subplot(1, 0) 2092 # pl.add_title(title=title, font_size=FONTSIZE) 2093 self._render( 2094 pl, 2095 style="bs", 2096 bondcolor=BOND_COLOR, 2097 bs_scale=BS_SCALE, 2098 spec=SPECULARITY, 2099 specpow=SPEC_POWER, 2100 ) 2101 2102 pl.subplot(1, 1) 2103 # pl.add_title(title=title, font_size=FONTSIZE) 2104 self._render( 2105 pl, 2106 style="sb", 2107 bondcolor=BOND_COLOR, 2108 bs_scale=BS_SCALE, 2109 spec=SPECULARITY, 2110 specpow=SPEC_POWER, 2111 ) 2112 2113 pl.link_views() 2114 pl.reset_camera() 2115 if shadows: 2116 pl.enable_shadows() 2117 2118 pl.show(auto_close=False) 2119 pl.screenshot(fname) 2120 2121 if verbose: 2122 print(f"Saved: {fname}") 2123 2124 def save_meshes_as_stl(self, meshes, filename) -> None: 2125 """Save a list of meshes as a single STL file. 2126 2127 Args: 2128 meshes (list): List of pyvista mesh objects to save. 2129 filename (str): Path to save the STL file to. 2130 """ 2131 merged_mesh = pv.UnstructuredGrid() 2132 for mesh in meshes: 2133 merged_mesh += mesh 2134 merged_mesh.save(filename) 2135 2136 def export(self, style="sb", verbose=True, fname="ssbond_plt") -> None: 2137 """ 2138 Create and save a screenshot of the Disulfide in the given style and filename. 2139 2140 :param single: Display a single vs panel view, defaults to True 2141 :param style: Rendering style, one of: 2142 * 'sb' - split bonds 2143 * 'bs' - ball and stick 2144 * 'cpk' - CPK style 2145 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2146 * 'plain' - boring single color, 2147 2148 :param fname: output filename,, defaults to 'ssbond.stl' 2149 :param verbose: Verbosit, defaults to False 2150 """ 2151 2152 if verbose: 2153 print(f"-> screenshot(): Rendering screenshot to file {fname}") 2154 2155 pl = pv.PolyData() 2156 2157 self._plot( 2158 pl, 2159 style=style, 2160 bondcolor=BOND_COLOR, 2161 bs_scale=BS_SCALE, 2162 spec=SPECULARITY, 2163 specpow=SPEC_POWER, 2164 ) 2165 2166 self.save_meshes_as_stl(pl, fname) 2167 2168 return 2169 2170 def set_permissive(self, perm: bool) -> None: 2171 """ 2172 Set PERMISSIVE flag for Disulfide parsing. 2173 2174 :return: None 2175 """ 2176 2177 self.PERMISSIVE = perm 2178 2179 def set_positions( 2180 self, 2181 n_prox: Vector, 2182 ca_prox: Vector, 2183 c_prox: Vector, 2184 o_prox: Vector, 2185 cb_prox: Vector, 2186 sg_prox: Vector, 2187 n_dist: Vector, 2188 ca_dist: Vector, 2189 c_dist: Vector, 2190 o_dist: Vector, 2191 cb_dist: Vector, 2192 sg_dist: Vector, 2193 c_prev_prox: Vector, 2194 n_next_prox: Vector, 2195 c_prev_dist: Vector, 2196 n_next_dist: Vector, 2197 ) -> None: 2198 """ 2199 Set the atomic coordinates for all atoms in the Disulfide object. 2200 2201 :param n_prox: Proximal N position 2202 :param ca_prox: Proximal Cα position 2203 :param c_prox: Proximal C' position 2204 :param o_prox: Proximal O position 2205 :param cb_prox: Proximal Cβ position 2206 :param sg_prox: Proximal Sγ position 2207 :param n_dist: Distal N position 2208 :param ca_dist: Distal Cα position 2209 :param c_dist: Distal C' position 2210 :param o_dist: Distal O position 2211 :param cb_dist: Distal Cβ position 2212 :param sg_dist: Distal Sγ position 2213 :param c_prev_prox: Proximal previous C' 2214 :param n_next_prox: Proximal next N 2215 :param c_prev_dist: Distal previous C' 2216 :param n_next_dist: Distal next N 2217 """ 2218 2219 # deep copy 2220 self.n_prox = n_prox.copy() 2221 self.ca_prox = ca_prox.copy() 2222 self.c_prox = c_prox.copy() 2223 self.o_prox = o_prox.copy() 2224 self.cb_prox = cb_prox.copy() 2225 self.sg_prox = sg_prox.copy() 2226 self.sg_dist = sg_dist.copy() 2227 self.cb_dist = cb_dist.copy() 2228 self.ca_dist = ca_dist.copy() 2229 self.n_dist = n_dist.copy() 2230 self.c_dist = c_dist.copy() 2231 self.o_dist = o_dist.copy() 2232 2233 self.c_prev_prox = c_prev_prox.copy() 2234 self.n_next_prox = n_next_prox.copy() 2235 self.c_prev_dist = c_prev_dist.copy() 2236 self.n_next_dist = n_next_dist.copy() 2237 2238 def set_dihedrals( 2239 self, chi1: float, chi2: float, chi3: float, chi4: float, chi5: float 2240 ) -> None: 2241 """ 2242 Set the disulfide's dihedral angles, Chi1-Chi5. -180 - 180 degrees. 2243 2244 :param chi1: Chi1 2245 :param chi2: Chi2 2246 :param chi3: Chi3 2247 :param chi4: Chi4 2248 :param chi5: Chi5 2249 """ 2250 self.chi1 = chi1 2251 self.chi2 = chi2 2252 self.chi3 = chi3 2253 self.chi4 = chi4 2254 self.chi5 = chi5 2255 self.torsion_array = np.array([chi1, chi2, chi3, chi4, chi5]) 2256 self.compute_torsional_energy() 2257 self.compute_torsion_length() 2258 2259 def set_name(self, namestr="Disulfide") -> None: 2260 """ 2261 Set the Disulfide's name. 2262 2263 :param namestr: Name, by default "Disulfide" 2264 """ 2265 self.name = namestr 2266 2267 def set_resnum(self, proximal: int, distal: int) -> None: 2268 """ 2269 Set the proximal and residue numbers for the Disulfide. 2270 2271 :param proximal: Proximal residue number 2272 :param distal: Distal residue number 2273 """ 2274 self.proximal = proximal 2275 self.distal = distal 2276 2277 def compute_torsion_length(self) -> float: 2278 """ 2279 Compute the 5D Euclidean length of the Disulfide object. Update the disulfide internal state. 2280 2281 :return: Torsion length (Degrees) 2282 """ 2283 # Use numpy array to compute element-wise square 2284 tors2 = np.square(self.torsion_array) 2285 2286 # Compute the sum of squares using numpy's sum function 2287 dist = math.sqrt(np.sum(tors2)) 2288 2289 # Update the internal state 2290 self.torsion_length = dist 2291 2292 return dist 2293 2294 def Torsion_Distance(self, other) -> float: 2295 """ 2296 Calculate the 5D Euclidean distance between ```self``` and another Disulfide 2297 object. This is used to compare Disulfide Bond torsion angles to 2298 determine their torsional similarity via a 5-Dimensional Euclidean distance metric. 2299 2300 :param other: Comparison Disulfide 2301 :raises ProteusPyWarning: Warning if ```other``` is not a Disulfide object 2302 :return: Euclidean distance (Degrees) between ```self``` and ```other```. 2303 """ 2304 2305 from proteusPy.ProteusPyWarning import ProteusPyWarning 2306 2307 # Check length of torsion arrays 2308 if len(self.torsion_array) != 5 or len(other.torsion_array) != 5: 2309 raise ProteusPyWarning( 2310 "--> Torsion_Distance() requires vectors of length 5!" 2311 ) 2312 2313 # Convert to numpy arrays and add 180 to each element 2314 p1 = np.array(self.torsion_array) + 180.0 2315 p2 = np.array(other.torsion_array) + 180.0 2316 2317 # Compute the 5D Euclidean distance using numpy's linalg.norm function 2318 dist = np.linalg.norm(p1 - p2) 2319 2320 return dist 2321 2322 def Torsion_neighbors(self, others, cutoff) -> DisulfideList: 2323 """ 2324 Return a list of Disulfides within the angular cutoff in the others list. 2325 This routine is used to find Disulfides having the same torsion length 2326 within the others list. This is used to find families of Disulfides with 2327 similar conformations. Assumes self is properly initialized. 2328 2329 *NB* The routine will not distinguish between +/- 2330 dihedral angles. *i.e.* [-60, -60, -90, -60, -60] would have the same 2331 torsion length as [60, 60, 90, 60, 60], two clearly different structures. 2332 2333 :param others: ```DisulfideList``` to search 2334 :param cutoff: Dihedral angle degree cutoff 2335 :return: DisulfideList within the cutoff 2336 2337 Example: 2338 In this example we load the disulfide database subset, find the disulfides with 2339 the lowest and highest energies, and then find the nearest conformational neighbors. 2340 Finally, we display the neighbors overlaid against a common reference frame. 2341 2342 >>> from proteusPy import * 2343 >>> from proteusPy.DisulfideLoader import Load_PDB_SS 2344 >>> from proteusPy.DisulfideList import DisulfideList 2345 >>> from proteusPy.Disulfide import Disulfide 2346 >>> PDB_SS = Load_PDB_SS(verbose=False, subset=True) 2347 >>> ss_list = DisulfideList([], 'tmp') 2348 2349 We point to the complete list to search for lowest and highest energies. 2350 >>> sslist = PDB_SS.SSList 2351 >>> ssmin_enrg, ssmax_enrg = PDB_SS.SSList.minmax_energy 2352 2353 Make an empty list and find the nearest neighbors within 10 degrees avg RMS in 2354 sidechain dihedral angle space. 2355 2356 >>> low_energy_neighbors = DisulfideList([],'Neighbors') 2357 >>> low_energy_neighbors = ssmin_enrg.Torsion_neighbors(sslist, 10) 2358 2359 Display the number found, and then display them overlaid onto their common reference frame. 2360 2361 >>> tot = low_energy_neighbors.length 2362 >>> print(f'Neighbors: {tot}') 2363 Neighbors: 2 2364 >>> low_energy_neighbors.display_overlay() 2365 2366 """ 2367 res = [ss for ss in others if self.Torsion_Distance(ss) <= cutoff] 2368 return DisulfideList(res, "neighbors") 2369 2370 def torsion_to_sixclass(self) -> str: 2371 """ 2372 Return the sextant class string for ``self``. 2373 2374 :raises DisulfideIOException: _description_ 2375 :return: Sextant string 2376 """ 2377 from proteusPy.DisulfideClasses import get_sixth_quadrant 2378 2379 tors = self.torsion_array 2380 res = [get_sixth_quadrant(x) for x in tors] 2381 return "".join([str(r) for r in res])
This class provides a Python object and methods representing a physical disulfide bond either extracted from the RCSB protein databank or built using the proteusPy.Turtle3D class. The disulfide bond is an important intramolecular stabilizing structural element and is characterized by:
- Atomic coordinates for the atoms N, Cα, Cβ, C', Sγ for both residues. These are stored as both raw atomic coordinates as read from the RCSB file and internal local coordinates.
- The dihedral angles Χ1 - Χ5 for the disulfide bond
- A name, by default {pdb_id}{prox_resnumb}{prox_chain}_{distal_resnum}{distal_chain}
- Proximal residue number
- Distal residue number
- Approximate bond torsional energy (kcal/mol):
$$ E_{kcal/mol} \approx 2.0 * cos(3.0 * \chi_{1}) + cos(3.0 * \chi_{5}) + cos(3.0 * \chi_{2}) + $$ $$ cos(3.0 * \chi_{4}) + 3.5 * cos(2.0 * \chi_{3}) + 0.6 * cos(3.0 * \chi_{3}) + 10.1 $$
The equation embodies the typical 3-fold rotation barriers associated with single bonds, (Χ1, Χ5, Χ2, Χ4) and a high 2-fold barrier for Χ3, resulting from the partial double bond character of the S-S bond. This property leads to two major disulfide families, characterized by the sign of Χ3. Left-handed disulfides have Χ3 < 0° and right-handed disulfides have Χ3 > 0°. Within this breakdown there are numerous subfamilies, broadly known as the hook, spiral and staple. These are under characgterization.
- Euclidean length of the dihedral angles (degrees) defined as: $$\sqrt(\chi_{1}^{2} + \chi_{2}^{2} + \chi_{3}^{2} + \chi_{4}^{2} + \chi_{5}^{2})$$
- Cα - Cα distance (Å)
- Cβ - Cβ distance (Å)
- The previous C' and next N for both the proximal and distal residues. These are needed to calculate the backbone dihedral angles Φ and Ψ.
- Backbone dihedral angles Φ and Ψ, when possible. Not all structures are complete and in those cases the atoms needed may be undefined. In this case the Φ and Ψ angles are set to -180°.
The class also provides a rendering capabilities using the excellent PyVista library, and can display disulfides interactively in a variety of display styles:
- 'sb' - Split Bonds style - bonds colored by their atom type
- 'bs' - Ball and Stick style - split bond coloring with small atoms
- 'pd' - Proximal/Distal style - bonds colored Red for proximal residue and Green for the distal residue.
- 'cpk' - CPK style rendering, colored by atom type:
- Carbon - Grey
- Nitrogen - Blue
- Sulfur - Yellow
- Oxygen - Red
- Hydrogen - White
Individual renderings can be saved to a file, and animations created.
147 def __init__( 148 self, 149 name: str = "SSBOND", 150 proximal: int = -1, 151 distal: int = -1, 152 proximal_chain: str = "A", 153 distal_chain: str = "A", 154 pdb_id: str = "1egs", 155 quiet: bool = True, 156 permissive: bool = True, 157 ) -> None: 158 """ 159 __init__ Initialize the class to defined internal values. 160 161 :param name: Disulfide name, by default "SSBOND" 162 163 """ 164 self.name = name 165 self.proximal = proximal 166 self.distal = distal 167 self.energy = _FLOAT_INIT 168 self.proximal_chain = proximal_chain 169 self.distal_chain = distal_chain 170 self.pdb_id = pdb_id 171 self.proximal_residue_fullid = str("") 172 self.distal_residue_fullid = str("") 173 self.PERMISSIVE = permissive 174 self.QUIET = quiet 175 self.ca_distance = _FLOAT_INIT 176 self.cb_distance = _FLOAT_INIT 177 self.torsion_array = np.array( 178 (_ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT) 179 ) 180 self.phiprox = _ANG_INIT 181 self.psiprox = _ANG_INIT 182 self.phidist = _ANG_INIT 183 self.psidist = _ANG_INIT 184 185 # global coordinates for the Disulfide, typically as 186 # returned from the PDB file 187 188 self.n_prox = Vector(0, 0, 0) 189 self.ca_prox = Vector(0, 0, 0) 190 self.c_prox = Vector(0, 0, 0) 191 self.o_prox = Vector(0, 0, 0) 192 self.cb_prox = Vector(0, 0, 0) 193 self.sg_prox = Vector(0, 0, 0) 194 self.sg_dist = Vector(0, 0, 0) 195 self.cb_dist = Vector(0, 0, 0) 196 self.ca_dist = Vector(0, 0, 0) 197 self.n_dist = Vector(0, 0, 0) 198 self.c_dist = Vector(0, 0, 0) 199 self.o_dist = Vector(0, 0, 0) 200 201 # set when we can't find previous or next prox or distal 202 # C' or N atoms. 203 self.missing_atoms = False 204 self.modelled = False 205 self.resolution = -1.0 206 207 # need these to calculate backbone dihedral angles 208 self.c_prev_prox = Vector(0, 0, 0) 209 self.n_next_prox = Vector(0, 0, 0) 210 self.c_prev_dist = Vector(0, 0, 0) 211 self.n_next_dist = Vector(0, 0, 0) 212 213 # local coordinates for the Disulfide, computed using the Turtle3D in 214 # Orientation #1. these are generally private. 215 216 self._n_prox = Vector(0, 0, 0) 217 self._ca_prox = Vector(0, 0, 0) 218 self._c_prox = Vector(0, 0, 0) 219 self._o_prox = Vector(0, 0, 0) 220 self._cb_prox = Vector(0, 0, 0) 221 self._sg_prox = Vector(0, 0, 0) 222 self._sg_dist = Vector(0, 0, 0) 223 self._cb_dist = Vector(0, 0, 0) 224 self._ca_dist = Vector(0, 0, 0) 225 self._n_dist = Vector(0, 0, 0) 226 self._c_dist = Vector(0, 0, 0) 227 self._o_dist = Vector(0, 0, 0) 228 229 # need these to calculate backbone dihedral angles 230 self._c_prev_prox = Vector(0, 0, 0) 231 self._n_next_prox = Vector(0, 0, 0) 232 self._c_prev_dist = Vector(0, 0, 0) 233 self._n_next_dist = Vector(0, 0, 0) 234 235 # Dihedral angles for the disulfide bond itself, set to _ANG_INIT 236 self.chi1 = _ANG_INIT 237 self.chi2 = _ANG_INIT 238 self.chi3 = _ANG_INIT 239 self.chi4 = _ANG_INIT 240 self.chi5 = _ANG_INIT 241 self.rho = _ANG_INIT # new dihedral angle: Nprox - Ca_prox - Ca_dist - N_dist 242 243 self.torsion_length = _FLOAT_INIT
__init__ Initialize the class to defined internal values.
Parameters
- name: Disulfide name, by default "SSBOND"
986 @property 987 def dihedrals(self) -> list: 988 """ 989 Return a list containing the dihedral angles for the disulfide. 990 991 """ 992 return [self.chi1, self.chi2, self.chi3, self.chi4, self.chi5]
Return a list containing the dihedral angles for the disulfide.
1009 def bounding_box(self) -> np.array: 1010 """ 1011 Return the bounding box array for the given disulfide 1012 1013 Returns 1014 ------- 1015 :return: np.Array(3,2): Array containing the min, max for X, Y, and Z respectively. 1016 Does not currently take the atom's radius into account. 1017 1018 """ 1019 res = np.zeros(shape=(3, 2)) 1020 xmin, xmax = self.compute_extents("x") 1021 ymin, ymax = self.compute_extents("y") 1022 zmin, zmax = self.compute_extents("z") 1023 1024 res[0] = [xmin, xmax] 1025 res[1] = [ymin, ymax] 1026 res[2] = [zmin, zmax] 1027 1028 return res
Return the bounding box array for the given disulfide
Returns
Returns
np.Array(3,2): Array containing the min, max for X, Y, and Z respectively. Does not currently take the atom's radius into account.
1030 def build_yourself(self) -> None: 1031 """ 1032 Build a model Disulfide based its internal dihedral state 1033 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1034 Cb, with N on left), builds disulfide, and updates the object's internal 1035 coordinates. It also adds the distal protein backbone, 1036 and computes the disulfide conformational energy. 1037 """ 1038 chi1 = self.chi1 1039 chi2 = self.chi2 1040 chi3 = self.chi3 1041 chi4 = self.chi4 1042 chi5 = self.chi5 1043 self.build_model(chi1, chi2, chi3, chi4, chi5)
Build a model Disulfide based its internal dihedral state Routine assumes turtle is in orientation #1 (at Ca, headed toward Cb, with N on left), builds disulfide, and updates the object's internal coordinates. It also adds the distal protein backbone, and computes the disulfide conformational energy.
1045 def build_model( 1046 self, chi1: float, chi2: float, chi3: float, chi4: float, chi5: float 1047 ) -> None: 1048 """ 1049 Build a model Disulfide based on the input dihedral angles. 1050 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1051 Cb, with N on left), builds disulfide, and updates the object's internal 1052 coordinates. It also adds the distal protein backbone, 1053 and computes the disulfide conformational energy. 1054 1055 :param chi1: Chi1 (degrees) 1056 :param chi2: Chi2 (degrees) 1057 :param chi3: Chi3 (degrees) 1058 :param chi4: Chi4 (degrees) 1059 :param chi5: Chi5 (degrees) 1060 1061 Example: 1062 >>> from proteusPy.Disulfide import Disulfide 1063 >>> modss = Disulfide('model') 1064 >>> modss.build_model(-60, -60, -90, -60, -60) 1065 >>> modss.display(style='sb') 1066 """ 1067 1068 self.set_dihedrals(chi1, chi2, chi3, chi4, chi5) 1069 self.proximal = 1 1070 self.distal = 2 1071 1072 tmp = Turtle3D("tmp") 1073 tmp.Orientation = 1 1074 1075 n = Vector(0, 0, 0) 1076 ca = Vector(0, 0, 0) 1077 cb = Vector(0, 0, 0) 1078 c = Vector(0, 0, 0) 1079 1080 self.ca_prox = tmp._position 1081 tmp.schain_to_bbone() 1082 n, ca, cb, c = build_residue(tmp) 1083 1084 self.n_prox = n 1085 self.ca_prox = ca 1086 self.c_prox = c 1087 self.cb_prox = cb 1088 1089 tmp.bbone_to_schain() 1090 tmp.move(1.53) 1091 tmp.roll(self.chi1) 1092 tmp.yaw(112.8) 1093 self.cb_prox = Vector(tmp._position) 1094 1095 tmp.move(1.86) 1096 tmp.roll(self.chi2) 1097 tmp.yaw(103.8) 1098 self.sg_prox = Vector(tmp._position) 1099 1100 tmp.move(2.044) 1101 tmp.roll(self.chi3) 1102 tmp.yaw(103.8) 1103 self.sg_dist = Vector(tmp._position) 1104 1105 tmp.move(1.86) 1106 tmp.roll(self.chi4) 1107 tmp.yaw(112.8) 1108 self.cb_dist = Vector(tmp._position) 1109 1110 tmp.move(1.53) 1111 tmp.roll(self.chi5) 1112 tmp.pitch(180.0) 1113 1114 tmp.schain_to_bbone() 1115 1116 n, ca, cb, c = build_residue(tmp) 1117 1118 self.n_dist = n 1119 self.ca_dist = ca 1120 self.c_dist = c 1121 self.compute_torsional_energy() 1122 self.compute_local_coords() 1123 self.ca_distance = distance3d(self.ca_prox, self.ca_dist) 1124 self.cb_distance = distance3d(self.cb_prox, self.cb_dist) 1125 self.torsion_array = np.array( 1126 (self.chi1, self.chi2, self.chi3, self.chi4, self.chi5) 1127 ) 1128 self.compute_torsion_length() 1129 self.compute_rho() 1130 self.missing_atoms = True 1131 self.modelled = True
Build a model Disulfide based on the input dihedral angles. Routine assumes turtle is in orientation #1 (at Ca, headed toward Cb, with N on left), builds disulfide, and updates the object's internal coordinates. It also adds the distal protein backbone, and computes the disulfide conformational energy.
Parameters
- chi1: Chi1 (degrees)
- chi2: Chi2 (degrees)
- chi3: Chi3 (degrees)
- chi4: Chi4 (degrees)
- chi5: Chi5 (degrees)
Example:
>>> from proteusPy.Disulfide import Disulfide
>>> modss = Disulfide('model')
>>> modss.build_model(-60, -60, -90, -60, -60)
>>> modss.display(style='sb')
1133 def cofmass(self) -> np.array: 1134 """ 1135 Return the geometric center of mass for the internal coordinates of 1136 the given Disulfide. Missing atoms are not included. 1137 1138 :return: 3D array for the geometric center of mass 1139 """ 1140 1141 res = self.internal_coords() 1142 return res.mean(axis=0)
Return the geometric center of mass for the internal coordinates of the given Disulfide. Missing atoms are not included.
Returns
3D array for the geometric center of mass
1144 def copy(self): 1145 """ 1146 Copy the Disulfide. 1147 1148 :return: A copy of self. 1149 """ 1150 return copy.deepcopy(self)
Copy the Disulfide.
Returns
A copy of self.
1152 def compute_extents(self, dim="z"): 1153 """ 1154 Calculate the internal coordinate extents for the input axis. 1155 1156 :param dim: Axis, one of 'x', 'y', 'z', by default 'z' 1157 :return: min, max 1158 """ 1159 1160 ic = self.internal_coords() 1161 # set default index to 'z' 1162 idx = 2 1163 1164 if dim == "x": 1165 idx = 0 1166 elif dim == "y": 1167 idx = 1 1168 elif dim == "z": 1169 idx = 2 1170 1171 _min = ic[:, idx].min() 1172 _max = ic[:, idx].max() 1173 return _min, _max
Calculate the internal coordinate extents for the input axis.
Parameters
- dim: Axis, one of 'x', 'y', 'z', by default 'z'
Returns
min, max
1175 def compute_local_coords(self) -> None: 1176 """ 1177 Compute the internal coordinates for a properly initialized Disulfide Object. 1178 1179 :param self: SS initialized Disulfide object 1180 :returns: None, modifies internal state of the input 1181 """ 1182 1183 turt = Turtle3D("tmp") 1184 # get the coordinates as np.array for Turtle3D use. 1185 cpp = self.c_prev_prox.get_array() 1186 nnp = self.n_next_prox.get_array() 1187 1188 n = self.n_prox.get_array() 1189 ca = self.ca_prox.get_array() 1190 c = self.c_prox.get_array() 1191 cb = self.cb_prox.get_array() 1192 o = self.o_prox.get_array() 1193 sg = self.sg_prox.get_array() 1194 1195 sg2 = self.sg_dist.get_array() 1196 cb2 = self.cb_dist.get_array() 1197 ca2 = self.ca_dist.get_array() 1198 c2 = self.c_dist.get_array() 1199 n2 = self.n_dist.get_array() 1200 o2 = self.o_dist.get_array() 1201 1202 cpd = self.c_prev_dist.get_array() 1203 nnd = self.n_next_dist.get_array() 1204 1205 turt.orient_from_backbone(n, ca, c, cb, ORIENT_SIDECHAIN) 1206 1207 # internal (local) coordinates, stored as Vector objects 1208 # to_local returns np.array objects 1209 1210 self._n_prox = Vector(turt.to_local(n)) 1211 self._ca_prox = Vector(turt.to_local(ca)) 1212 self._c_prox = Vector(turt.to_local(c)) 1213 self._o_prox = Vector(turt.to_local(o)) 1214 self._cb_prox = Vector(turt.to_local(cb)) 1215 self._sg_prox = Vector(turt.to_local(sg)) 1216 1217 self._c_prev_prox = Vector(turt.to_local(cpp)) 1218 self._n_next_prox = Vector(turt.to_local(nnp)) 1219 self._c_prev_dist = Vector(turt.to_local(cpd)) 1220 self._n_next_dist = Vector(turt.to_local(nnd)) 1221 1222 self._n_dist = Vector(turt.to_local(n2)) 1223 self._ca_dist = Vector(turt.to_local(ca2)) 1224 self._c_dist = Vector(turt.to_local(c2)) 1225 self._o_dist = Vector(turt.to_local(o2)) 1226 self._cb_dist = Vector(turt.to_local(cb2)) 1227 self._sg_dist = Vector(turt.to_local(sg2))
Compute the internal coordinates for a properly initialized Disulfide Object.
Parameters
- self: SS initialized Disulfide object :returns: None, modifies internal state of the input
1229 def compute_torsional_energy(self) -> float: 1230 """ 1231 Compute the approximate torsional energy for the Disulfide's 1232 conformation and sets its internal state. 1233 1234 :return: Energy (kcal/mol) 1235 """ 1236 # @TODO find citation for the ss bond energy calculation 1237 1238 def torad(deg): 1239 return np.radians(deg) 1240 1241 chi1 = self.chi1 1242 chi2 = self.chi2 1243 chi3 = self.chi3 1244 chi4 = self.chi4 1245 chi5 = self.chi5 1246 1247 energy = 2.0 * (cos(torad(3.0 * chi1)) + cos(torad(3.0 * chi5))) 1248 energy += cos(torad(3.0 * chi2)) + cos(torad(3.0 * chi4)) 1249 energy += 3.5 * cos(torad(2.0 * chi3)) + 0.6 * cos(torad(3.0 * chi3)) + 10.1 1250 1251 self.energy = energy 1252 return energy
Compute the approximate torsional energy for the Disulfide's conformation and sets its internal state.
Returns
Energy (kcal/mol)
1254 def display(self, single=True, style="sb", light=True, shadows=False) -> None: 1255 """ 1256 Display the Disulfide bond in the specific rendering style. 1257 1258 :param single: Display the bond in a single panel in the specific style. 1259 :param style: Rendering style: One of: 1260 * 'sb' - split bonds 1261 * 'bs' - ball and stick 1262 * 'cpk' - CPK style 1263 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1264 * 'plain' - boring single color 1265 :param light: If True, light background, if False, dark 1266 1267 Example: 1268 >>> import proteusPy 1269 >>> from proteusPy.Disulfide import Disulfide 1270 >>> from proteusPy.DisulfideLoader import DisulfideLoader, Load_PDB_SS 1271 1272 >>> PDB_SS = Load_PDB_SS(verbose=False, subset=True) 1273 >>> ss = PDB_SS[0] 1274 >>> ss.display(style='cpk') 1275 >>> ss.screenshot(style='bs', fname='proteus_logo_sb.png') 1276 """ 1277 src = self.pdb_id 1278 enrg = self.energy 1279 1280 title = f"{src}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol. Cα: {self.ca_distance:.2f} Å Cβ: {self.cb_distance:.2f} Å Tors: {self.torsion_length:.2f}°" 1281 1282 if light: 1283 pv.set_plot_theme("document") 1284 else: 1285 pv.set_plot_theme("dark") 1286 1287 if single == True: 1288 _pl = pv.Plotter(window_size=WINSIZE) 1289 _pl.add_title(title=title, font_size=FONTSIZE) 1290 _pl.enable_anti_aliasing("msaa") 1291 # _pl.add_camera_orientation_widget() 1292 1293 self._render( 1294 _pl, 1295 style=style, 1296 bs_scale=BS_SCALE, 1297 spec=SPECULARITY, 1298 specpow=SPEC_POWER, 1299 ) 1300 _pl.reset_camera() 1301 if shadows == True: 1302 _pl.enable_shadows() 1303 _pl.show() 1304 1305 else: 1306 pl = pv.Plotter(window_size=WINSIZE, shape=(2, 2)) 1307 pl.subplot(0, 0) 1308 1309 pl.add_title(title=title, font_size=FONTSIZE) 1310 pl.enable_anti_aliasing("msaa") 1311 1312 # pl.add_camera_orientation_widget() 1313 1314 self._render( 1315 pl, 1316 style="cpk", 1317 bondcolor=BOND_COLOR, 1318 bs_scale=BS_SCALE, 1319 spec=SPECULARITY, 1320 specpow=SPEC_POWER, 1321 ) 1322 1323 pl.subplot(0, 1) 1324 pl.add_title(title=title, font_size=FONTSIZE) 1325 1326 self._render( 1327 pl, 1328 style="bs", 1329 bondcolor=BOND_COLOR, 1330 bs_scale=BS_SCALE, 1331 spec=SPECULARITY, 1332 specpow=SPEC_POWER, 1333 ) 1334 1335 pl.subplot(1, 0) 1336 pl.add_title(title=title, font_size=FONTSIZE) 1337 1338 self._render( 1339 pl, 1340 style="sb", 1341 bondcolor=BOND_COLOR, 1342 bs_scale=BS_SCALE, 1343 spec=SPECULARITY, 1344 specpow=SPEC_POWER, 1345 ) 1346 1347 pl.subplot(1, 1) 1348 pl.add_title(title=title, font_size=FONTSIZE) 1349 1350 self._render( 1351 pl, 1352 style="pd", 1353 bondcolor=BOND_COLOR, 1354 bs_scale=BS_SCALE, 1355 spec=SPECULARITY, 1356 specpow=SPEC_POWER, 1357 ) 1358 1359 pl.link_views() 1360 pl.reset_camera() 1361 if shadows == True: 1362 pl.enable_shadows() 1363 pl.show() 1364 return
Display the Disulfide bond in the specific rendering style.
Parameters
- single: Display the bond in a single panel in the specific style.
- style: Rendering style: One of:
- 'sb' - split bonds
- 'bs' - ball and stick
- 'cpk' - CPK style
- 'pd' - Proximal/Distal style - Red=proximal, Green=Distal
- 'plain' - boring single color
- light: If True, light background, if False, dark
Example:
>>> import proteusPy
>>> from proteusPy.Disulfide import Disulfide
>>> from proteusPy.DisulfideLoader import DisulfideLoader, Load_PDB_SS
>>> PDB_SS = Load_PDB_SS(verbose=False, subset=True)
>>> ss = PDB_SS[0]
>>> ss.display(style='cpk')
>>> ss.screenshot(style='bs', fname='proteus_logo_sb.png')
1366 def plot( 1367 self, pl, single=True, style="sb", light=True, shadows=False 1368 ) -> pv.Plotter: 1369 """ 1370 Return the pyVista Plotter object for the Disulfide bond in the specific rendering style. 1371 1372 :param single: Display the bond in a single panel in the specific style. 1373 :param style: Rendering style: One of: 1374 * 'sb' - split bonds 1375 * 'bs' - ball and stick 1376 * 'cpk' - CPK style 1377 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1378 * 'plain' - boring single color 1379 :param light: If True, light background, if False, dark 1380 """ 1381 src = self.pdb_id 1382 enrg = self.energy 1383 1384 title = f"{src}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol. Cα: {self.ca_distance:.2f} Å Cβ: {self.cb_distance:.2f} Å Tors: {self.torsion_length:.2f}°" 1385 1386 if light: 1387 pv.set_plot_theme("document") 1388 else: 1389 pv.set_plot_theme("dark") 1390 1391 if single == True: 1392 # _pl = pv.Plotter(window_size=WINSIZE) 1393 # _pl.add_title(title=title, font_size=FONTSIZE) 1394 pl.clear() 1395 pl.enable_anti_aliasing("msaa") 1396 # pl.add_camera_orientation_widget() 1397 1398 self._render( 1399 pl, 1400 style=style, 1401 bs_scale=BS_SCALE, 1402 spec=SPECULARITY, 1403 specpow=SPEC_POWER, 1404 ) 1405 pl.reset_camera() 1406 if shadows == True: 1407 pl.enable_shadows() 1408 else: 1409 pl = pv.Plotter(shape=(2, 2)) 1410 pl.subplot(0, 0) 1411 1412 # pl.add_title(title=title, font_size=FONTSIZE) 1413 pl.enable_anti_aliasing("msaa") 1414 1415 # pl.add_camera_orientation_widget() 1416 1417 self._render( 1418 pl, 1419 style="cpk", 1420 bondcolor=BOND_COLOR, 1421 bs_scale=BS_SCALE, 1422 spec=SPECULARITY, 1423 specpow=SPEC_POWER, 1424 ) 1425 1426 pl.subplot(0, 1) 1427 1428 self._render( 1429 pl, 1430 style="bs", 1431 bondcolor=BOND_COLOR, 1432 bs_scale=BS_SCALE, 1433 spec=SPECULARITY, 1434 specpow=SPEC_POWER, 1435 ) 1436 1437 pl.subplot(1, 0) 1438 1439 self._render( 1440 pl, 1441 style="sb", 1442 bondcolor=BOND_COLOR, 1443 bs_scale=BS_SCALE, 1444 spec=SPECULARITY, 1445 specpow=SPEC_POWER, 1446 ) 1447 1448 pl.subplot(1, 1) 1449 self._render( 1450 pl, 1451 style="pd", 1452 bondcolor=BOND_COLOR, 1453 bs_scale=BS_SCALE, 1454 spec=SPECULARITY, 1455 specpow=SPEC_POWER, 1456 ) 1457 1458 pl.link_views() 1459 pl.reset_camera() 1460 if shadows == True: 1461 pl.enable_shadows() 1462 return pl
Return the pyVista Plotter object for the Disulfide bond in the specific rendering style.
Parameters
- single: Display the bond in a single panel in the specific style.
- style: Rendering style: One of:
- 'sb' - split bonds
- 'bs' - ball and stick
- 'cpk' - CPK style
- 'pd' - Proximal/Distal style - Red=proximal, Green=Distal
- 'plain' - boring single color
- light: If True, light background, if False, dark
1464 def Distance_neighbors(self, others: DisulfideList, cutoff: float) -> DisulfideList: 1465 """ 1466 Return list of Disulfides whose RMS atomic distance is within 1467 the cutoff (Å) in the others list. 1468 1469 :param others: DisulfideList to search 1470 :param cutoff: Distance cutoff (Å) 1471 :return: DisulfideList within the cutoff 1472 """ 1473 1474 res = [ss.copy() for ss in others if self.Distance_RMS(ss) < cutoff] 1475 return DisulfideList(res, "neighbors")
Return list of Disulfides whose RMS atomic distance is within the cutoff (Å) in the others list.
Parameters
- others: DisulfideList to search
- cutoff: Distance cutoff (Å)
Returns
DisulfideList within the cutoff
1477 def Distance_RMS(self, other) -> float: 1478 """ 1479 Calculate the RMS distance between the internal coordinates of self and another Disulfide. 1480 :param other: Comparison Disulfide 1481 :return: RMS distance (Å) 1482 """ 1483 1484 # Get internal coordinates of both objects 1485 ic1 = self.internal_coords() 1486 ic2 = other.internal_coords() 1487 1488 # Compute the sum of squared differences between corresponding internal coordinates 1489 totsq = sum(math.dist(p1, p2) ** 2 for p1, p2 in zip(ic1, ic2)) 1490 1491 # Compute the mean of the squared distances 1492 totsq /= len(ic1) 1493 1494 # Take the square root of the mean to get the RMS distance 1495 return math.sqrt(totsq)
Calculate the RMS distance between the internal coordinates of self and another Disulfide.
Parameters
- other: Comparison Disulfide
Returns
RMS distance (Å)
1497 def Torsion_RMS(self, other) -> float: 1498 """ 1499 Calculate the RMS distance between the dihedral angles of self and another Disulfide. 1500 :param other: Comparison Disulfide 1501 :return: RMS distance (deg). 1502 """ 1503 import math 1504 1505 # Get internal coordinates of both objects 1506 ic1 = self.torsion_array 1507 ic2 = other.torsion_array 1508 1509 # Compute the sum of squared differences between corresponding internal coordinates 1510 totsq = sum((p1 - p2) ** 2 for p1, p2 in zip(ic1, ic2)) 1511 # Compute the mean of the squared distances 1512 totsq /= len(ic1) 1513 1514 # Take the square root of the mean to get the RMS distance 1515 return math.sqrt(totsq)
Calculate the RMS distance between the dihedral angles of self and another Disulfide.
Parameters
- other: Comparison Disulfide
Returns
RMS distance (deg).
1517 def get_chains(self) -> tuple: 1518 """ 1519 Return the proximal and distal chain IDs for the Disulfide. 1520 1521 :return: tuple (proximal, distal) chain IDs 1522 """ 1523 prox = self.proximal_chain 1524 dist = self.distal_chain 1525 return tuple(prox, dist)
Return the proximal and distal chain IDs for the Disulfide.
Returns
tuple (proximal, distal) chain IDs
1527 def get_permissive(self) -> bool: 1528 """ 1529 Return the Permissive flag state. (Used in PDB parsing) 1530 1531 :return: Permissive state 1532 """ 1533 return self.PERMISIVE
Return the Permissive flag state. (Used in PDB parsing)
Returns
Permissive state
1535 def get_full_id(self) -> tuple: 1536 """ 1537 Return the Disulfide full IDs (Used with BIO.PDB) 1538 1539 :return: Disulfide full IDs 1540 """ 1541 return (self.proximal_residue_fullid, self.distal_residue_fullid)
Return the Disulfide full IDs (Used with BIO.PDB)
Returns
Disulfide full IDs
1543 def initialize_disulfide_from_chain( 1544 self, chain1, chain2, proximal, distal, resolution, quiet=True 1545 ) -> None: 1546 """ 1547 Initialize a new Disulfide object with atomic coordinates from 1548 the proximal and distal coordinates, typically taken from a PDB file. 1549 This routine is primarily used internally when building the compressed 1550 database. 1551 1552 :param chain1: list of Residues in the model, eg: chain = model['A'] 1553 :param chain2: list of Residues in the model, eg: chain = model['A'] 1554 :param proximal: proximal residue sequence ID 1555 :param distal: distal residue sequence ID 1556 :param resolution: structure resolution 1557 :param quiet: Quiet or noisy parsing, defaults to True 1558 :raises DisulfideConstructionWarning: Raised when not parsed correctly 1559 """ 1560 id = chain1.get_full_id()[0] 1561 self.pdb_id = id 1562 1563 chi1 = chi2 = chi3 = chi4 = chi5 = _ANG_INIT 1564 1565 prox = int(proximal) 1566 dist = int(distal) 1567 1568 prox_residue = chain1[prox] 1569 dist_residue = chain2[dist] 1570 1571 if prox_residue.get_resname() != "CYS" or dist_residue.get_resname() != "CYS": 1572 print( 1573 f"build_disulfide() requires CYS at both residues: {prox} {prox_residue.get_resname()} {dist} {dist_residue.get_resname()} Chain: {prox_residue.get_segid()}" 1574 ) 1575 1576 # set the objects proximal and distal values 1577 self.set_resnum(proximal, distal) 1578 1579 if resolution is not None: 1580 self.resolution = resolution 1581 1582 self.proximal_chain = chain1.get_id() 1583 self.distal_chain = chain2.get_id() 1584 1585 self.proximal_residue_fullid = prox_residue.get_full_id() 1586 self.distal_residue_fullid = dist_residue.get_full_id() 1587 1588 if quiet: 1589 warnings.filterwarnings("ignore", category=DisulfideConstructionWarning) 1590 else: 1591 warnings.simplefilter("always") 1592 1593 # grab the coordinates for the proximal and distal residues as vectors 1594 # so we can do math on them later 1595 # proximal residue 1596 1597 try: 1598 n1 = prox_residue["N"].get_vector() 1599 ca1 = prox_residue["CA"].get_vector() 1600 c1 = prox_residue["C"].get_vector() 1601 o1 = prox_residue["O"].get_vector() 1602 cb1 = prox_residue["CB"].get_vector() 1603 sg1 = prox_residue["SG"].get_vector() 1604 1605 except Exception: 1606 raise DisulfideConstructionWarning( 1607 f"Invalid or missing coordinates for proximal residue {proximal}" 1608 ) from None 1609 1610 # distal residue 1611 try: 1612 n2 = dist_residue["N"].get_vector() 1613 ca2 = dist_residue["CA"].get_vector() 1614 c2 = dist_residue["C"].get_vector() 1615 o2 = dist_residue["O"].get_vector() 1616 cb2 = dist_residue["CB"].get_vector() 1617 sg2 = dist_residue["SG"].get_vector() 1618 1619 except Exception: 1620 raise DisulfideConstructionWarning( 1621 f"Invalid or missing coordinates for distal residue {distal}" 1622 ) from None 1623 1624 # previous residue and next residue - optional, used for phi, psi calculations 1625 try: 1626 prevprox = chain1[prox - 1] 1627 nextprox = chain1[prox + 1] 1628 1629 prevdist = chain2[dist - 1] 1630 nextdist = chain2[dist + 1] 1631 1632 cprev_prox = prevprox["C"].get_vector() 1633 nnext_prox = nextprox["N"].get_vector() 1634 1635 cprev_dist = prevdist["C"].get_vector() 1636 nnext_dist = nextdist["N"].get_vector() 1637 1638 # compute phi, psi for prox and distal 1639 self.phiprox = np.degrees(calc_dihedral(cprev_prox, n1, ca1, c1)) 1640 self.psiprox = np.degrees(calc_dihedral(n1, ca1, c1, nnext_prox)) 1641 self.phidist = np.degrees(calc_dihedral(cprev_dist, n2, ca2, c2)) 1642 self.psidist = np.degrees(calc_dihedral(n2, ca2, c2, nnext_dist)) 1643 1644 except Exception: 1645 mess = f"Missing coords for: {id} {prox-1} or {dist+1} for SS {proximal}-{distal}" 1646 cprev_prox = nnext_prox = cprev_dist = nnext_dist = Vector(-1.0, -1.0, -1.0) 1647 self.missing_atoms = True 1648 warnings.warn(mess, DisulfideConstructionWarning) 1649 1650 # update the positions and conformation 1651 self.set_positions( 1652 n1, 1653 ca1, 1654 c1, 1655 o1, 1656 cb1, 1657 sg1, 1658 n2, 1659 ca2, 1660 c2, 1661 o2, 1662 cb2, 1663 sg2, 1664 cprev_prox, 1665 nnext_prox, 1666 cprev_dist, 1667 nnext_dist, 1668 ) 1669 1670 # calculate and set the disulfide dihedral angles 1671 self.chi1 = np.degrees(calc_dihedral(n1, ca1, cb1, sg1)) 1672 self.chi2 = np.degrees(calc_dihedral(ca1, cb1, sg1, sg2)) 1673 self.chi3 = np.degrees(calc_dihedral(cb1, sg1, sg2, cb2)) 1674 self.chi4 = np.degrees(calc_dihedral(sg1, sg2, cb2, ca2)) 1675 self.chi5 = np.degrees(calc_dihedral(sg2, cb2, ca2, n2)) 1676 self.rho = np.degrees(calc_dihedral(n1, ca1, ca2, n2)) 1677 1678 self.ca_distance = distance3d(self.ca_prox, self.ca_dist) 1679 self.cb_distance = distance3d(self.cb_prox, self.cb_dist) 1680 self.torsion_array = np.array( 1681 (self.chi1, self.chi2, self.chi3, self.chi4, self.chi5) 1682 ) 1683 self.compute_torsion_length() 1684 1685 # calculate and set the SS bond torsional energy 1686 self.compute_torsional_energy() 1687 1688 # compute and set the local coordinates 1689 self.compute_local_coords()
Initialize a new Disulfide object with atomic coordinates from the proximal and distal coordinates, typically taken from a PDB file. This routine is primarily used internally when building the compressed database.
Parameters
- chain1: list of Residues in the model, eg: chain = model['A']
- chain2: list of Residues in the model, eg: chain = model['A']
- proximal: proximal residue sequence ID
- distal: distal residue sequence ID
- resolution: structure resolution
- quiet: Quiet or noisy parsing, defaults to True
Raises
- DisulfideConstructionWarning: Raised when not parsed correctly
1691 def internal_coords(self) -> np.array: 1692 """ 1693 Return the internal coordinates for the Disulfide. 1694 If there are missing atoms the extra atoms for the proximal 1695 and distal N and C are set to [0,0,0]. This is needed for the center of 1696 mass calculations, used when rendering. 1697 1698 :return: Array containing the coordinates, [16][3]. 1699 """ 1700 1701 # if we don't have the prior and next atoms we initialize those 1702 # atoms to the origin so as to not effect the center of mass calculations 1703 if self.missing_atoms: 1704 res_array = np.array( 1705 ( 1706 self._n_prox.get_array(), 1707 self._ca_prox.get_array(), 1708 self._c_prox.get_array(), 1709 self._o_prox.get_array(), 1710 self._cb_prox.get_array(), 1711 self._sg_prox.get_array(), 1712 self._n_dist.get_array(), 1713 self._ca_dist.get_array(), 1714 self._c_dist.get_array(), 1715 self._o_dist.get_array(), 1716 self._cb_dist.get_array(), 1717 self._sg_dist.get_array(), 1718 [0, 0, 0], 1719 [0, 0, 0], 1720 [0, 0, 0], 1721 [0, 0, 0], 1722 ) 1723 ) 1724 else: 1725 res_array = np.array( 1726 ( 1727 self._n_prox.get_array(), 1728 self._ca_prox.get_array(), 1729 self._c_prox.get_array(), 1730 self._o_prox.get_array(), 1731 self._cb_prox.get_array(), 1732 self._sg_prox.get_array(), 1733 self._n_dist.get_array(), 1734 self._ca_dist.get_array(), 1735 self._c_dist.get_array(), 1736 self._o_dist.get_array(), 1737 self._cb_dist.get_array(), 1738 self._sg_dist.get_array(), 1739 self._c_prev_prox.get_array(), 1740 self._n_next_prox.get_array(), 1741 self._c_prev_dist.get_array(), 1742 self._n_next_dist.get_array(), 1743 ) 1744 ) 1745 return res_array
Return the internal coordinates for the Disulfide. If there are missing atoms the extra atoms for the proximal and distal N and C are set to [0,0,0]. This is needed for the center of mass calculations, used when rendering.
Returns
Array containing the coordinates, [16][3].
1747 def internal_coords_res(self, resnumb) -> np.array: 1748 """ 1749 Return the internal coordinates for the internal coordinates of 1750 the given Disulfide. Missing atoms are not included. 1751 1752 :param resnumb: Residue number for disulfide 1753 :raises DisulfideConstructionWarning: Warning raised if the residue number is invalid 1754 :return: Array containing the internal coordinates for the disulfide 1755 """ 1756 res_array = np.zeros(shape=(6, 3)) 1757 1758 if resnumb == self.proximal: 1759 res_array = np.array( 1760 ( 1761 self._n_prox.get_array(), 1762 self._ca_prox.get_array(), 1763 self._c_prox.get_array(), 1764 self._o_prox.get_array(), 1765 self._cb_prox.get_array(), 1766 self._sg_prox.get_array(), 1767 ) 1768 ) 1769 return res_array 1770 1771 elif resnumb == self.distal: 1772 res_array = np.array( 1773 ( 1774 self._n_dist.get_array(), 1775 self._ca_dist.get_array(), 1776 self._c_dist.get_array(), 1777 self._o_dist.get_array(), 1778 self._cb_dist.get_array(), 1779 self._sg_dist.get_array(), 1780 ) 1781 ) 1782 return res_array 1783 else: 1784 mess = f"-> Disulfide.internal_coords(): Invalid argument. \ 1785 Unable to find residue: {resnumb} " 1786 raise DisulfideConstructionWarning(mess)
Return the internal coordinates for the internal coordinates of the given Disulfide. Missing atoms are not included.
Parameters
- resnumb: Residue number for disulfide
Raises
- DisulfideConstructionWarning: Warning raised if the residue number is invalid
Returns
Array containing the internal coordinates for the disulfide
1788 def make_movie( 1789 self, style="sb", fname="ssbond.mp4", verbose=False, steps=360 1790 ) -> None: 1791 """ 1792 Create an animation for ```self``` rotating one revolution about the Y axis, 1793 in the given ```style```, saving to ```filename```. 1794 1795 :param style: Rendering style, defaults to 'sb', one of: 1796 * 'sb' - split bonds 1797 * 'bs' - ball and stick 1798 * 'cpk' - CPK style 1799 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1800 * 'plain' - boring single color 1801 1802 :param fname: Output filename, defaults to ```ssbond.mp4``` 1803 :param verbose: Verbosity, defaults to False 1804 :param steps: Number of steps for one complete rotation, defaults to 360. 1805 """ 1806 src = self.pdb_id 1807 name = self.name 1808 enrg = self.energy 1809 1810 title = f"{src} {name}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol, Cα: {self.ca_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 1811 1812 if verbose: 1813 print(f"Rendering animation to {fname}...") 1814 1815 pl = pv.Plotter(window_size=WINSIZE, off_screen=True) 1816 pl.open_movie(fname) 1817 path = pl.generate_orbital_path(n_points=steps) 1818 1819 # 1820 # pl.add_title(title=title, font_size=FONTSIZE) 1821 pl.enable_anti_aliasing("msaa") 1822 # pl.add_camera_orientation_widget() 1823 pl = self._render( 1824 pl, 1825 style=style, 1826 bondcolor=BOND_COLOR, 1827 bs_scale=BS_SCALE, 1828 spec=SPECULARITY, 1829 specpow=SPEC_POWER, 1830 ) 1831 pl.reset_camera() 1832 pl.orbit_on_path(path, write_frames=True) 1833 pl.close() 1834 1835 if verbose: 1836 print(f"Saved mp4 animation to: {fname}")
Create an animation for self
rotating one revolution about the Y axis,
in the given style
, saving to filename
.
Parameters
style: Rendering style, defaults to 'sb', one of:
- 'sb' - split bonds
- 'bs' - ball and stick
- 'cpk' - CPK style
- 'pd' - Proximal/Distal style - Red=proximal, Green=Distal
- 'plain' - boring single color
fname: Output filename, defaults to
ssbond.mp4
- verbose: Verbosity, defaults to False
- steps: Number of steps for one complete rotation, defaults to 360.
1838 def pprint(self) -> None: 1839 """ 1840 Pretty print general info for the Disulfide 1841 """ 1842 s1 = self.repr_ss_info() 1843 s2 = self.repr_ss_ca_dist() 1844 s3 = self.repr_ss_conformation() 1845 s4 = self.repr_ss_torsion_length() 1846 res = f"{s1} \n{s3} \n{s2} \n{s4}>" 1847 print(res)
Pretty print general info for the Disulfide
1849 def pprint_all(self) -> None: 1850 """ 1851 Pretty print all info for the Disulfide 1852 """ 1853 s1 = self.repr_ss_info() + "\n" 1854 s2 = self.repr_ss_coords() 1855 s3 = self.repr_ss_local_coords() 1856 s4 = self.repr_ss_conformation() 1857 s5 = self.repr_chain_ids() 1858 s6 = self.repr_ss_ca_dist() 1859 s7 = self.repr_ss_torsion_length() 1860 1861 res = f"{s1} {s5} {s2} {s3} {s4}\n {s6}\n {s7}>" 1862 1863 print(res)
Pretty print all info for the Disulfide
1866 def repr_ss_info(self) -> str: 1867 """ 1868 Representation for the Disulfide class 1869 """ 1870 s1 = f"<Disulfide {self.name}, Source: {self.pdb_id}, Resolution: {self.resolution} Å" 1871 return s1
Representation for the Disulfide class
1873 def repr_ss_coords(self) -> str: 1874 """ 1875 Representation for Disulfide coordinates 1876 """ 1877 s2 = f"\nProximal Coordinates:\n N: {self.n_prox}\n Cα: {self.ca_prox}\n C: {self.c_prox}\n O: {self.o_prox}\n Cβ: {self.cb_prox}\n Sγ: {self.sg_prox}\n Cprev {self.c_prev_prox}\n Nnext: {self.n_next_prox}\n" 1878 s3 = f"Distal Coordinates:\n N: {self.n_dist}\n Cα: {self.ca_dist}\n C: {self.c_dist}\n O: {self.o_dist}\n Cβ: {self.cb_dist}\n Sγ: {self.sg_dist}\n Cprev {self.c_prev_dist}\n Nnext: {self.n_next_dist}\n\n" 1879 stot = f"{s2} {s3}" 1880 return stot
Representation for Disulfide coordinates
1882 def repr_ss_conformation(self) -> str: 1883 """ 1884 Representation for Disulfide conformation 1885 """ 1886 s4 = f"Χ1-Χ5: {self.chi1:.2f}°, {self.chi2:.2f}°, {self.chi3:.2f}°, {self.chi4:.2f}° {self.chi5:.2f}°, {self.rho:.2f}°, {self.energy:.2f} kcal/mol" 1887 stot = f"{s4}" 1888 return stot
Representation for Disulfide conformation
1890 def repr_ss_local_coords(self) -> str: 1891 """ 1892 Representation for the Disulfide internal coordinates. 1893 """ 1894 s2i = f"Proximal Internal Coords:\n N: {self._n_prox}\n Cα: {self._ca_prox}\n C: {self._c_prox}\n O: {self._o_prox}\n Cβ: {self._cb_prox}\n Sγ: {self._sg_prox}\n Cprev {self.c_prev_prox}\n Nnext: {self.n_next_prox}\n" 1895 s3i = f"Distal Internal Coords:\n N: {self._n_dist}\n Cα: {self._ca_dist}\n C: {self._c_dist}\n O: {self._o_dist}\n Cβ: {self._cb_dist}\n Sγ: {self._sg_dist}\n Cprev {self.c_prev_dist}\n Nnext: {self.n_next_dist}\n" 1896 stot = f"{s2i}{s3i}" 1897 return stot
Representation for the Disulfide internal coordinates.
1899 def repr_ss_chain_ids(self) -> str: 1900 """ 1901 Representation for Disulfide chain IDs 1902 """ 1903 return f"Proximal Chain fullID: <{self.proximal_residue_fullid}> Distal Chain fullID: <{self.distal_residue_fullid}>"
Representation for Disulfide chain IDs
1905 def repr_ss_ca_dist(self) -> str: 1906 """ 1907 Representation for Disulfide Ca distance 1908 """ 1909 s1 = f"Cα Distance: {self.ca_distance:.2f} Å" 1910 return s1
Representation for Disulfide Ca distance
1912 def repr_ss_cb_dist(self) -> str: 1913 """ 1914 Representation for Disulfide Ca distance 1915 """ 1916 s1 = f"Cβ Distance: {self.cb_distance:.2f} Å" 1917 return s1
Representation for Disulfide Ca distance
1919 def repr_ss_torsion_length(self) -> str: 1920 """ 1921 Representation for Disulfide torsion length 1922 """ 1923 s1 = f"Torsion length: {self.torsion_length:.2f} deg" 1924 return s1
Representation for Disulfide torsion length
1926 def repr_all(self) -> str: 1927 """ 1928 Return a string representation for all Disulfide information 1929 contained in self. 1930 """ 1931 1932 s1 = self.repr_ss_info() + "\n" 1933 s2 = self.repr_ss_coords() 1934 s3 = self.repr_ss_local_coords() 1935 s4 = self.repr_ss_conformation() 1936 s5 = self.repr_chain_ids() 1937 s6 = self.repr_ss_ca_dist() 1938 s8 = self.repr_ss_cb_dist() 1939 s7 = self.repr_ss_torsion_length() 1940 1941 res = f"{s1} {s5} {s2} {s3} {s4} {s6} {s7} {s8}>" 1942 return res
Return a string representation for all Disulfide information contained in self.
1944 def repr_compact(self) -> str: 1945 """ 1946 Return a compact representation of the Disulfide object 1947 :return: string 1948 """ 1949 return f"{self.repr_ss_info()} {self.repr_ss_conformation()}"
Return a compact representation of the Disulfide object
Returns
string
1951 def repr_conformation(self) -> str: 1952 """ 1953 Return a string representation of the Disulfide object's conformation. 1954 :return: string 1955 """ 1956 return f"{self.repr_ss_conformation()}"
Return a string representation of the Disulfide object's conformation.
Returns
string
1958 def repr_coords(self) -> str: 1959 """ 1960 Return a string representation of the Disulfide object's coordinates. 1961 :return: string 1962 """ 1963 return f"{self.repr_ss_coords()}"
Return a string representation of the Disulfide object's coordinates.
Returns
string
1965 def repr_internal_coords(self) -> str: 1966 """ 1967 Return a string representation of the Disulfide object's internal coordinaes. 1968 :return: string 1969 """ 1970 return f"{self.repr_ss_local_coords()}"
Return a string representation of the Disulfide object's internal coordinaes.
Returns
string
1972 def repr_chain_ids(self) -> str: 1973 """ 1974 Return a string representation of the Disulfide object's chain ids. 1975 :return: string 1976 """ 1977 return f"{self.repr_ss_chain_ids()}"
Return a string representation of the Disulfide object's chain ids.
Returns
string
1983 def reset(self) -> None: 1984 """ 1985 Resets the disulfide object to its initial state. All distances, 1986 angles and positions are reset. The name is unchanged. 1987 """ 1988 self.__init__(self)
Resets the disulfide object to its initial state. All distances, angles and positions are reset. The name is unchanged.
1990 def same_chains(self) -> bool: 1991 """ 1992 Function checks if the Disulfide is cross-chain or not. 1993 1994 Returns 1995 ------- 1996 bool \n 1997 True if the proximal and distal residues are on the same chains, 1998 False otherwise. 1999 """ 2000 2001 (prox, dist) = self.get_chains() 2002 return prox == dist
Function checks if the Disulfide is cross-chain or not.
Returns
bool
True if the proximal and distal residues are on the same chains,
False otherwise.
2004 def screenshot( 2005 self, 2006 single=True, 2007 style="sb", 2008 fname="ssbond.png", 2009 verbose=False, 2010 shadows=False, 2011 light=True, 2012 ) -> None: 2013 """ 2014 Create and save a screenshot of the Disulfide in the given style 2015 and filename 2016 2017 :param single: Display a single vs panel view, defaults to True 2018 :param style: Rendering style, one of: 2019 * 'sb' - split bonds 2020 * 'bs' - ball and stick 2021 * 'cpk' - CPK style 2022 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2023 * 'plain' - boring single color, 2024 :param fname: output filename,, defaults to 'ssbond.png' 2025 :param verbose: Verbosit, defaults to False 2026 :param shadows: Enable shadows, defaults to False 2027 """ 2028 src = self.pdb_id 2029 name = self.name 2030 enrg = self.energy 2031 2032 title = f"{src} {name}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol, Cα: {self.ca_distance:.2f} Å, Cβ: {self.cb_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 2033 2034 if light: 2035 pv.set_plot_theme("document") 2036 else: 2037 pv.set_plot_theme("dark") 2038 2039 if verbose: 2040 print(f"-> screenshot(): Rendering screenshot to file {fname}") 2041 2042 if single: 2043 pl = pv.Plotter(window_size=WINSIZE) 2044 # pl.add_title(title=title, font_size=FONTSIZE) 2045 pl.enable_anti_aliasing("msaa") 2046 # pl.add_camera_orientation_widget() 2047 self._render( 2048 pl, 2049 style=style, 2050 bondcolor=BOND_COLOR, 2051 bs_scale=BS_SCALE, 2052 spec=SPECULARITY, 2053 specpow=SPEC_POWER, 2054 ) 2055 pl.reset_camera() 2056 if shadows: 2057 pl.enable_shadows() 2058 2059 pl.show(auto_close=False) 2060 pl.screenshot(fname) 2061 pl.clear() 2062 2063 else: 2064 pl = pv.Plotter(window_size=WINSIZE, shape=(2, 2)) 2065 pl.subplot(0, 0) 2066 2067 # pl.add_title(title=title, font_size=FONTSIZE) 2068 pl.enable_anti_aliasing("msaa") 2069 2070 # pl.add_camera_orientation_widget() 2071 self._render( 2072 pl, 2073 style="cpk", 2074 bondcolor=BOND_COLOR, 2075 bs_scale=BS_SCALE, 2076 spec=SPECULARITY, 2077 specpow=SPEC_POWER, 2078 ) 2079 2080 pl.subplot(0, 1) 2081 # pl.add_title(title=title, font_size=FONTSIZE) 2082 self._render( 2083 pl, 2084 style="pd", 2085 bondcolor=BOND_COLOR, 2086 bs_scale=BS_SCALE, 2087 spec=SPECULARITY, 2088 specpow=SPEC_POWER, 2089 ) 2090 2091 pl.subplot(1, 0) 2092 # pl.add_title(title=title, font_size=FONTSIZE) 2093 self._render( 2094 pl, 2095 style="bs", 2096 bondcolor=BOND_COLOR, 2097 bs_scale=BS_SCALE, 2098 spec=SPECULARITY, 2099 specpow=SPEC_POWER, 2100 ) 2101 2102 pl.subplot(1, 1) 2103 # pl.add_title(title=title, font_size=FONTSIZE) 2104 self._render( 2105 pl, 2106 style="sb", 2107 bondcolor=BOND_COLOR, 2108 bs_scale=BS_SCALE, 2109 spec=SPECULARITY, 2110 specpow=SPEC_POWER, 2111 ) 2112 2113 pl.link_views() 2114 pl.reset_camera() 2115 if shadows: 2116 pl.enable_shadows() 2117 2118 pl.show(auto_close=False) 2119 pl.screenshot(fname) 2120 2121 if verbose: 2122 print(f"Saved: {fname}")
Create and save a screenshot of the Disulfide in the given style and filename
Parameters
- single: Display a single vs panel view, defaults to True
- style: Rendering style, one of:
- 'sb' - split bonds
- 'bs' - ball and stick
- 'cpk' - CPK style
- 'pd' - Proximal/Distal style - Red=proximal, Green=Distal
- 'plain' - boring single color,
- fname: output filename,, defaults to 'ssbond.png'
- verbose: Verbosit, defaults to False
- shadows: Enable shadows, defaults to False
2124 def save_meshes_as_stl(self, meshes, filename) -> None: 2125 """Save a list of meshes as a single STL file. 2126 2127 Args: 2128 meshes (list): List of pyvista mesh objects to save. 2129 filename (str): Path to save the STL file to. 2130 """ 2131 merged_mesh = pv.UnstructuredGrid() 2132 for mesh in meshes: 2133 merged_mesh += mesh 2134 merged_mesh.save(filename)
Save a list of meshes as a single STL file.
Args: meshes (list): List of pyvista mesh objects to save. filename (str): Path to save the STL file to.
2136 def export(self, style="sb", verbose=True, fname="ssbond_plt") -> None: 2137 """ 2138 Create and save a screenshot of the Disulfide in the given style and filename. 2139 2140 :param single: Display a single vs panel view, defaults to True 2141 :param style: Rendering style, one of: 2142 * 'sb' - split bonds 2143 * 'bs' - ball and stick 2144 * 'cpk' - CPK style 2145 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2146 * 'plain' - boring single color, 2147 2148 :param fname: output filename,, defaults to 'ssbond.stl' 2149 :param verbose: Verbosit, defaults to False 2150 """ 2151 2152 if verbose: 2153 print(f"-> screenshot(): Rendering screenshot to file {fname}") 2154 2155 pl = pv.PolyData() 2156 2157 self._plot( 2158 pl, 2159 style=style, 2160 bondcolor=BOND_COLOR, 2161 bs_scale=BS_SCALE, 2162 spec=SPECULARITY, 2163 specpow=SPEC_POWER, 2164 ) 2165 2166 self.save_meshes_as_stl(pl, fname) 2167 2168 return
Create and save a screenshot of the Disulfide in the given style and filename.
Parameters
- single: Display a single vs panel view, defaults to True
style: Rendering style, one of:
- 'sb' - split bonds
- 'bs' - ball and stick
- 'cpk' - CPK style
- 'pd' - Proximal/Distal style - Red=proximal, Green=Distal
- 'plain' - boring single color,
fname: output filename,, defaults to 'ssbond.stl'
- verbose: Verbosit, defaults to False
2170 def set_permissive(self, perm: bool) -> None: 2171 """ 2172 Set PERMISSIVE flag for Disulfide parsing. 2173 2174 :return: None 2175 """ 2176 2177 self.PERMISSIVE = perm
Set PERMISSIVE flag for Disulfide parsing.
Returns
None
2179 def set_positions( 2180 self, 2181 n_prox: Vector, 2182 ca_prox: Vector, 2183 c_prox: Vector, 2184 o_prox: Vector, 2185 cb_prox: Vector, 2186 sg_prox: Vector, 2187 n_dist: Vector, 2188 ca_dist: Vector, 2189 c_dist: Vector, 2190 o_dist: Vector, 2191 cb_dist: Vector, 2192 sg_dist: Vector, 2193 c_prev_prox: Vector, 2194 n_next_prox: Vector, 2195 c_prev_dist: Vector, 2196 n_next_dist: Vector, 2197 ) -> None: 2198 """ 2199 Set the atomic coordinates for all atoms in the Disulfide object. 2200 2201 :param n_prox: Proximal N position 2202 :param ca_prox: Proximal Cα position 2203 :param c_prox: Proximal C' position 2204 :param o_prox: Proximal O position 2205 :param cb_prox: Proximal Cβ position 2206 :param sg_prox: Proximal Sγ position 2207 :param n_dist: Distal N position 2208 :param ca_dist: Distal Cα position 2209 :param c_dist: Distal C' position 2210 :param o_dist: Distal O position 2211 :param cb_dist: Distal Cβ position 2212 :param sg_dist: Distal Sγ position 2213 :param c_prev_prox: Proximal previous C' 2214 :param n_next_prox: Proximal next N 2215 :param c_prev_dist: Distal previous C' 2216 :param n_next_dist: Distal next N 2217 """ 2218 2219 # deep copy 2220 self.n_prox = n_prox.copy() 2221 self.ca_prox = ca_prox.copy() 2222 self.c_prox = c_prox.copy() 2223 self.o_prox = o_prox.copy() 2224 self.cb_prox = cb_prox.copy() 2225 self.sg_prox = sg_prox.copy() 2226 self.sg_dist = sg_dist.copy() 2227 self.cb_dist = cb_dist.copy() 2228 self.ca_dist = ca_dist.copy() 2229 self.n_dist = n_dist.copy() 2230 self.c_dist = c_dist.copy() 2231 self.o_dist = o_dist.copy() 2232 2233 self.c_prev_prox = c_prev_prox.copy() 2234 self.n_next_prox = n_next_prox.copy() 2235 self.c_prev_dist = c_prev_dist.copy() 2236 self.n_next_dist = n_next_dist.copy()
Set the atomic coordinates for all atoms in the Disulfide object.
Parameters
- n_prox: Proximal N position
- ca_prox: Proximal Cα position
- c_prox: Proximal C' position
- o_prox: Proximal O position
- cb_prox: Proximal Cβ position
- sg_prox: Proximal Sγ position
- n_dist: Distal N position
- ca_dist: Distal Cα position
- c_dist: Distal C' position
- o_dist: Distal O position
- cb_dist: Distal Cβ position
- sg_dist: Distal Sγ position
- c_prev_prox: Proximal previous C'
- n_next_prox: Proximal next N
- c_prev_dist: Distal previous C'
- n_next_dist: Distal next N
2238 def set_dihedrals( 2239 self, chi1: float, chi2: float, chi3: float, chi4: float, chi5: float 2240 ) -> None: 2241 """ 2242 Set the disulfide's dihedral angles, Chi1-Chi5. -180 - 180 degrees. 2243 2244 :param chi1: Chi1 2245 :param chi2: Chi2 2246 :param chi3: Chi3 2247 :param chi4: Chi4 2248 :param chi5: Chi5 2249 """ 2250 self.chi1 = chi1 2251 self.chi2 = chi2 2252 self.chi3 = chi3 2253 self.chi4 = chi4 2254 self.chi5 = chi5 2255 self.torsion_array = np.array([chi1, chi2, chi3, chi4, chi5]) 2256 self.compute_torsional_energy() 2257 self.compute_torsion_length()
Set the disulfide's dihedral angles, Chi1-Chi5. -180 - 180 degrees.
Parameters
- chi1: Chi1
- chi2: Chi2
- chi3: Chi3
- chi4: Chi4
- chi5: Chi5
2259 def set_name(self, namestr="Disulfide") -> None: 2260 """ 2261 Set the Disulfide's name. 2262 2263 :param namestr: Name, by default "Disulfide" 2264 """ 2265 self.name = namestr
Set the Disulfide's name.
Parameters
- namestr: Name, by default "Disulfide"
2267 def set_resnum(self, proximal: int, distal: int) -> None: 2268 """ 2269 Set the proximal and residue numbers for the Disulfide. 2270 2271 :param proximal: Proximal residue number 2272 :param distal: Distal residue number 2273 """ 2274 self.proximal = proximal 2275 self.distal = distal
Set the proximal and residue numbers for the Disulfide.
Parameters
- proximal: Proximal residue number
- distal: Distal residue number
2277 def compute_torsion_length(self) -> float: 2278 """ 2279 Compute the 5D Euclidean length of the Disulfide object. Update the disulfide internal state. 2280 2281 :return: Torsion length (Degrees) 2282 """ 2283 # Use numpy array to compute element-wise square 2284 tors2 = np.square(self.torsion_array) 2285 2286 # Compute the sum of squares using numpy's sum function 2287 dist = math.sqrt(np.sum(tors2)) 2288 2289 # Update the internal state 2290 self.torsion_length = dist 2291 2292 return dist
Compute the 5D Euclidean length of the Disulfide object. Update the disulfide internal state.
Returns
Torsion length (Degrees)
2294 def Torsion_Distance(self, other) -> float: 2295 """ 2296 Calculate the 5D Euclidean distance between ```self``` and another Disulfide 2297 object. This is used to compare Disulfide Bond torsion angles to 2298 determine their torsional similarity via a 5-Dimensional Euclidean distance metric. 2299 2300 :param other: Comparison Disulfide 2301 :raises ProteusPyWarning: Warning if ```other``` is not a Disulfide object 2302 :return: Euclidean distance (Degrees) between ```self``` and ```other```. 2303 """ 2304 2305 from proteusPy.ProteusPyWarning import ProteusPyWarning 2306 2307 # Check length of torsion arrays 2308 if len(self.torsion_array) != 5 or len(other.torsion_array) != 5: 2309 raise ProteusPyWarning( 2310 "--> Torsion_Distance() requires vectors of length 5!" 2311 ) 2312 2313 # Convert to numpy arrays and add 180 to each element 2314 p1 = np.array(self.torsion_array) + 180.0 2315 p2 = np.array(other.torsion_array) + 180.0 2316 2317 # Compute the 5D Euclidean distance using numpy's linalg.norm function 2318 dist = np.linalg.norm(p1 - p2) 2319 2320 return dist
Calculate the 5D Euclidean distance between self
and another Disulfide
object. This is used to compare Disulfide Bond torsion angles to
determine their torsional similarity via a 5-Dimensional Euclidean distance metric.
Parameters
- other: Comparison Disulfide
Raises
- ProteusPyWarning: Warning if
other
is not a Disulfide object
Returns
Euclidean distance (Degrees) between
self
andother
.
2322 def Torsion_neighbors(self, others, cutoff) -> DisulfideList: 2323 """ 2324 Return a list of Disulfides within the angular cutoff in the others list. 2325 This routine is used to find Disulfides having the same torsion length 2326 within the others list. This is used to find families of Disulfides with 2327 similar conformations. Assumes self is properly initialized. 2328 2329 *NB* The routine will not distinguish between +/- 2330 dihedral angles. *i.e.* [-60, -60, -90, -60, -60] would have the same 2331 torsion length as [60, 60, 90, 60, 60], two clearly different structures. 2332 2333 :param others: ```DisulfideList``` to search 2334 :param cutoff: Dihedral angle degree cutoff 2335 :return: DisulfideList within the cutoff 2336 2337 Example: 2338 In this example we load the disulfide database subset, find the disulfides with 2339 the lowest and highest energies, and then find the nearest conformational neighbors. 2340 Finally, we display the neighbors overlaid against a common reference frame. 2341 2342 >>> from proteusPy import * 2343 >>> from proteusPy.DisulfideLoader import Load_PDB_SS 2344 >>> from proteusPy.DisulfideList import DisulfideList 2345 >>> from proteusPy.Disulfide import Disulfide 2346 >>> PDB_SS = Load_PDB_SS(verbose=False, subset=True) 2347 >>> ss_list = DisulfideList([], 'tmp') 2348 2349 We point to the complete list to search for lowest and highest energies. 2350 >>> sslist = PDB_SS.SSList 2351 >>> ssmin_enrg, ssmax_enrg = PDB_SS.SSList.minmax_energy 2352 2353 Make an empty list and find the nearest neighbors within 10 degrees avg RMS in 2354 sidechain dihedral angle space. 2355 2356 >>> low_energy_neighbors = DisulfideList([],'Neighbors') 2357 >>> low_energy_neighbors = ssmin_enrg.Torsion_neighbors(sslist, 10) 2358 2359 Display the number found, and then display them overlaid onto their common reference frame. 2360 2361 >>> tot = low_energy_neighbors.length 2362 >>> print(f'Neighbors: {tot}') 2363 Neighbors: 2 2364 >>> low_energy_neighbors.display_overlay() 2365 2366 """ 2367 res = [ss for ss in others if self.Torsion_Distance(ss) <= cutoff] 2368 return DisulfideList(res, "neighbors")
Return a list of Disulfides within the angular cutoff in the others list. This routine is used to find Disulfides having the same torsion length within the others list. This is used to find families of Disulfides with similar conformations. Assumes self is properly initialized.
NB The routine will not distinguish between +/- dihedral angles. i.e. [-60, -60, -90, -60, -60] would have the same torsion length as [60, 60, 90, 60, 60], two clearly different structures.
Parameters
- others:
DisulfideList
to search - cutoff: Dihedral angle degree cutoff
Returns
DisulfideList within the cutoff
Example: In this example we load the disulfide database subset, find the disulfides with the lowest and highest energies, and then find the nearest conformational neighbors. Finally, we display the neighbors overlaid against a common reference frame.
>>> from proteusPy import *
>>> from proteusPy.DisulfideLoader import Load_PDB_SS
>>> from proteusPy.DisulfideList import DisulfideList
>>> from proteusPy.Disulfide import Disulfide
>>> PDB_SS = Load_PDB_SS(verbose=False, subset=True)
>>> ss_list = DisulfideList([], 'tmp')
We point to the complete list to search for lowest and highest energies.
>>> sslist = PDB_SS.SSList
>>> ssmin_enrg, ssmax_enrg = PDB_SS.SSList.minmax_energy
Make an empty list and find the nearest neighbors within 10 degrees avg RMS in sidechain dihedral angle space.
>>> low_energy_neighbors = DisulfideList([],'Neighbors')
>>> low_energy_neighbors = ssmin_enrg.Torsion_neighbors(sslist, 10)
Display the number found, and then display them overlaid onto their common reference frame.
>>> tot = low_energy_neighbors.length
>>> print(f'Neighbors: {tot}')
Neighbors: 2
>>> low_energy_neighbors.display_overlay()
2370 def torsion_to_sixclass(self) -> str: 2371 """ 2372 Return the sextant class string for ``self``. 2373 2374 :raises DisulfideIOException: _description_ 2375 :return: Sextant string 2376 """ 2377 from proteusPy.DisulfideClasses import get_sixth_quadrant 2378 2379 tors = self.torsion_array 2380 res = [get_sixth_quadrant(x) for x in tors] 2381 return "".join([str(r) for r in res])
Return the sextant class string for self
.
Raises
- DisulfideIOException: _description_
Returns
Sextant string
2387def parse_ssbond_header_rec(ssbond_dict: dict) -> list: 2388 """ 2389 Parse the SSBOND dict returned by parse_pdb_header. 2390 NB: Requires EGS-Modified BIO.parse_pdb_header.py. 2391 This is used internally. 2392 2393 :param ssbond_dict: the input SSBOND dict 2394 :return: A list of tuples representing the proximal, 2395 distal residue ids for the Disulfide. 2396 2397 """ 2398 disulfide_list = [] 2399 for ssb in ssbond_dict.items(): 2400 disulfide_list.append(ssb[1]) 2401 2402 return disulfide_list
Parse the SSBOND dict returned by parse_pdb_header. NB: Requires EGS-Modified BIO.parse_pdb_header.py. This is used internally.
Parameters
- ssbond_dict: the input SSBOND dict
Returns
A list of tuples representing the proximal, distal residue ids for the Disulfide.
2413def Download_Disulfides( 2414 pdb_home=PDB_DIR, model_home=MODEL_DIR, verbose=False, reset=False 2415) -> None: 2416 """ 2417 Read a comma separated list of PDB IDs and download them 2418 to the pdb_home path. 2419 2420 This utility function is used to download proteins containing at least 2421 one SS bond with the ID list generated from: http://www.rcsb.org/. 2422 2423 This is the primary data loader for the proteusPy Disulfide 2424 analysis package. The list of IDs represents files in the 2425 RCSB containing > 1 disulfide bond, and it contains 2426 over 39000 structures. The total download takes about 12 hours. The 2427 function keeps track of downloaded files so it's possible to interrupt and 2428 restart the download without duplicating effort. 2429 2430 :param pdb_home: Path for downloaded files, defaults to PDB_DIR 2431 :param model_home: Path for extracted data, defaults to MODEL_DIR 2432 :param verbose: Verbosity, defaults to False 2433 :param reset: Reset the downloaded files index. Used to restart the download. 2434 :raises DisulfideIOException: I/O error raised when the PDB file is not found. 2435 """ 2436 import os 2437 import time 2438 2439 start = time.time() 2440 donelines = [] 2441 SS_done = [] 2442 ssfile = None 2443 2444 cwd = os.getcwd() 2445 os.chdir(pdb_home) 2446 2447 pdblist = PDBList(pdb=pdb_home, verbose=verbose) 2448 ssfilename = f"{model_home}{SS_ID_FILE}" 2449 print(ssfilename) 2450 2451 # list of IDs containing >1 SSBond record 2452 try: 2453 ssfile = open(ssfilename) 2454 Line = ssfile.readlines() 2455 except Exception: 2456 raise DisulfideIOException(f"Cannot open file: {ssfile}") 2457 2458 for line in Line: 2459 entries = line.split(",") 2460 2461 print(f"Found: {len(entries)} entries") 2462 completed = {"xxx"} # set to keep track of downloaded 2463 2464 # file to track already downloaded entries. 2465 if reset is True: 2466 completed_file = open(f"{model_home}ss_completed.txt", "w") 2467 donelines = [] 2468 SS_DONE = [] 2469 else: 2470 completed_file = open(f"{model_home}ss_completed.txt", "w+") 2471 donelines = completed_file.readlines() 2472 2473 if len(donelines) > 0: 2474 for dl in donelines[0]: 2475 # create a list of pdb id already downloaded 2476 SS_done = dl.split(",") 2477 2478 count = len(SS_done) - 1 2479 completed.update(SS_done) # update the completed set with what's downloaded 2480 2481 # Loop over all entries, 2482 pbar = tqdm(entries, ncols=PBAR_COLS) 2483 for entry in pbar: 2484 pbar.set_postfix({"Entry": entry}) 2485 if entry not in completed: 2486 if pdblist.retrieve_pdb_file(entry, file_format="pdb", pdir=pdb_home): 2487 completed.update(entry) 2488 completed_file.write(f"{entry},") 2489 count += 1 2490 2491 completed_file.close() 2492 2493 end = time.time() 2494 elapsed = end - start 2495 2496 print(f"Overall files processed: {count}") 2497 print(f"Complete. Elapsed time: {datetime.timedelta(seconds=elapsed)} (h:m:s)") 2498 os.chdir(cwd) 2499 return
Read a comma separated list of PDB IDs and download them to the pdb_home path.
This utility function is used to download proteins containing at least one SS bond with the ID list generated from: http://www.rcsb.org/.
This is the primary data loader for the proteusPy Disulfide analysis package. The list of IDs represents files in the RCSB containing > 1 disulfide bond, and it contains over 39000 structures. The total download takes about 12 hours. The function keeps track of downloaded files so it's possible to interrupt and restart the download without duplicating effort.
Parameters
- pdb_home: Path for downloaded files, defaults to PDB_DIR
- model_home: Path for extracted data, defaults to MODEL_DIR
- verbose: Verbosity, defaults to False
- reset: Reset the downloaded files index. Used to restart the download.
Raises
- DisulfideIOException: I/O error raised when the PDB file is not found.
2506def Extract_Disulfides( 2507 numb=-1, 2508 verbose=False, 2509 quiet=True, 2510 pdbdir=PDB_DIR, 2511 datadir=MODEL_DIR, 2512 picklefile=SS_PICKLE_FILE, 2513 torsionfile=SS_TORSIONS_FILE, 2514 problemfile=PROBLEM_ID_FILE, 2515 dictfile=SS_DICT_PICKLE_FILE, 2516 dist_cutoff=-1.0, 2517) -> None: 2518 """ 2519 Read the PDB files contained in ``pdbdir`` and create the .pkl files needed for the 2520 proteusPy.DisulfideLoader.DisulfideLoader class. 2521 The ```Disulfide``` objects are contained in a ```DisulfideList``` object and 2522 ```Dict``` within these files. In addition, .csv files containing all of 2523 the torsions for the disulfides and problem IDs are written. The optional 2524 ```dist_cutoff``` allows for removal of Disufides whose Cα-Cα distance is > 2525 than the cutoff value. If it's -1.0 then the function keeps all Disulfides. 2526 2527 :param numb: number of entries to process, defaults to all 2528 :param verbose: more messages 2529 :param quiet: turns off DisulfideConstruction warnings 2530 :param pdbdir: path to PDB files 2531 :param datadir: path to resulting .pkl files 2532 :param picklefile: name of the disulfide .pkl file 2533 :param torsionfile: name of the disulfide torsion file .csv created 2534 :param problemfile: name of the .csv file containing problem ids 2535 :param dictfile: name of the .pkl file 2536 :param dist_cutoff: Ca distance cutoff to reject a Disulfide. 2537 """ 2538 2539 def name_to_id(fname: str) -> str: 2540 """ 2541 Returns the PDB ID from the filename. 2542 2543 :param fname: Complete PDB filename 2544 :return: PDB ID 2545 """ 2546 ent = fname[3:-4] 2547 return ent 2548 2549 import os 2550 2551 entrylist = [] 2552 problem_ids = [] 2553 bad = bad_dist = 0 2554 2555 # we use the specialized list class DisulfideList to contain our disulfides 2556 # we'll use a dict to store DisulfideList objects, indexed by the structure ID 2557 All_ss_dict = {} 2558 All_ss_list = DisulfideList([], "PDB_SS") 2559 All_ss_dict2 = {} # new dict of pointers to indices 2560 2561 start = time.time() 2562 cwd = os.getcwd() 2563 2564 # Build a list of PDB files in PDB_DIR that are readable. These files were downloaded 2565 # via the RCSB web query interface for structures containing >= 1 SS Bond. 2566 2567 os.chdir(pdbdir) 2568 2569 ss_filelist = glob.glob("*.ent") 2570 tot = len(ss_filelist) 2571 2572 if verbose: 2573 print(f"PDB Directory {pdbdir} contains: {tot} files") 2574 2575 # the filenames are in the form pdb{entry}.ent, I loop through them and extract 2576 # the PDB ID, with Disulfide.name_to_id(), then add to entrylist. 2577 2578 for entry in ss_filelist: 2579 entrylist.append(name_to_id(entry)) 2580 2581 # create a dataframe with the following columns for the disulfide conformations 2582 # extracted from the structure 2583 2584 SS_df = pandas.DataFrame(columns=Torsion_DF_Cols) 2585 2586 # define a tqdm progressbar using the fully loaded entrylist list. 2587 # If numb is passed then 2588 # only do the last numb entries. 2589 2590 if numb > 0: 2591 pbar = tqdm(entrylist[:numb], ncols=PBAR_COLS) 2592 else: 2593 pbar = tqdm(entrylist, ncols=PBAR_COLS) 2594 2595 tot = 0 2596 cnt = 0 2597 # loop over ss_filelist, create disulfides and initialize them 2598 for entry in pbar: 2599 pbar.set_postfix( 2600 {"ID": entry, "Bad": bad, "Ca": bad_dist, "Cnt": tot} 2601 ) # update the progress bar 2602 2603 # returns an empty list if none are found. 2604 _sslist = DisulfideList([], entry) 2605 _sslist = proteusPy.DisulfideList.load_disulfides_from_id( 2606 entry, model_numb=0, verbose=verbose, quiet=quiet, pdb_dir=pdbdir 2607 ) 2608 sslist, xchain = prune_extra_ss(_sslist) 2609 2610 if len(sslist) > 0: 2611 sslist2 = [] # list to hold indices for ss_dict2 2612 for ss in sslist: 2613 # Ca distance cutoff 2614 dist = ss.ca_distance 2615 if dist >= dist_cutoff and dist_cutoff != -1.0: 2616 bad_dist += 1 2617 continue 2618 2619 All_ss_list.append(ss) 2620 new_row = [ 2621 ss.pdb_id, 2622 ss.name, 2623 ss.proximal, 2624 ss.distal, 2625 ss.chi1, 2626 ss.chi2, 2627 ss.chi3, 2628 ss.chi4, 2629 ss.chi5, 2630 ss.energy, 2631 ss.ca_distance, 2632 ss.cb_distance, 2633 ss.phiprox, 2634 ss.psiprox, 2635 ss.phidist, 2636 ss.psidist, 2637 ss.torsion_length, 2638 ss.rho, 2639 ] 2640 2641 # add the row to the end of the dataframe 2642 SS_df.loc[len(SS_df.index)] = new_row.copy() # deep copy 2643 sslist2.append(cnt) 2644 cnt += 1 2645 tot += 1 2646 2647 # All_ss_dict[entry] = sslist 2648 # print(f'Entry: {entry}. Dict indices: {sslist2}') 2649 All_ss_dict2[entry] = sslist2 2650 # print(f'{entry} ss dict adding: {sslist2}') 2651 2652 else: 2653 # at this point I really shouldn't have any bad non-parsible file 2654 bad += 1 2655 problem_ids.append(entry) 2656 # os.remove(f'pdb{entry}.ent') 2657 2658 if bad > 0: 2659 prob_cols = ["id"] 2660 problem_df = pandas.DataFrame(columns=prob_cols) 2661 problem_df["id"] = problem_ids 2662 2663 print( 2664 f"-> Extract_Disulfides(): Found and removed: {len(problem_ids)} non-parsable structures." 2665 ) 2666 print( 2667 f"-> Extract_Disulfides(): Saving problem IDs to file: {datadir}{problemfile}" 2668 ) 2669 2670 problem_df.to_csv(f"{datadir}{problemfile}") 2671 else: 2672 if verbose: 2673 print("-> Extract_Disulfides(): No non-parsable structures found.") 2674 2675 if bad_dist > 0: 2676 print(f"-> Extract_Disulfides(): Found and ignored: {bad_dist} long SS bonds.") 2677 else: 2678 if verbose: 2679 print("No problems found.") 2680 2681 # dump the all_ss list of disulfides to a .pkl file. ~520 MB. 2682 fname = f"{datadir}{picklefile}" 2683 print( 2684 f"-> Extract_Disulfides(): Saving {len(All_ss_list)} Disulfides to file: {fname}" 2685 ) 2686 2687 with open(fname, "wb+") as f: 2688 pickle.dump(All_ss_list, f) 2689 2690 # dump the dict2 disulfides to a .pkl file. ~520 MB. 2691 dict_len = len(All_ss_dict2) 2692 fname = f"{datadir}{dictfile}" 2693 print( 2694 f"-> Extract_Disulfides(): Saving indices of {dict_len} Disulfide-containing PDB IDs to file: {fname}" 2695 ) 2696 2697 with open(fname, "wb+") as f: 2698 pickle.dump(All_ss_dict2, f) 2699 2700 # save the torsions 2701 fname = f"{datadir}{torsionfile}" 2702 print(f"-> Extract_Disulfides(): Saving torsions to file: {fname}") 2703 SS_df.to_csv(fname) 2704 2705 end = time.time() 2706 elapsed = end - start 2707 2708 print( 2709 f"-> Extract_Disulfides(): Disulfide Extraction complete! Elapsed time:\ 2710 {datetime.timedelta(seconds=elapsed)} (h:m:s)" 2711 ) 2712 2713 # return to original directory 2714 os.chdir(cwd) 2715 return
Read the PDB files contained in pdbdir
and create the .pkl files needed for the
proteusPy.DisulfideLoader.DisulfideLoader class.
The Disulfide
objects are contained in a DisulfideList
object and
Dict
within these files. In addition, .csv files containing all of
the torsions for the disulfides and problem IDs are written. The optional
dist_cutoff
allows for removal of Disufides whose Cα-Cα distance is >
than the cutoff value. If it's -1.0 then the function keeps all Disulfides.
Parameters
- numb: number of entries to process, defaults to all
- verbose: more messages
- quiet: turns off DisulfideConstruction warnings
- pdbdir: path to PDB files
- datadir: path to resulting .pkl files
- picklefile: name of the disulfide .pkl file
- torsionfile: name of the disulfide torsion file .csv created
- problemfile: name of the .csv file containing problem ids
- dictfile: name of the .pkl file
- dist_cutoff: Ca distance cutoff to reject a Disulfide.
2718def check_header_from_file( 2719 filename: str, model_numb=0, verbose=False, dbg=False 2720) -> bool: 2721 """ 2722 Check the Disulfides in the PDB file. 2723 2724 NB: Requires EGS-Modified BIO.parse_pdb_header.py from https://github.com/suchanek/biopython/ 2725 2726 :param filename: the name of the PDB entry. 2727 :param model_numb: model number to use, defaults to 0 for single structure files. 2728 :param verbose: print info while parsing 2729 :return: True if parsable 2730 2731 Example: 2732 Assuming ```MODEL_DIR``` has the pdb5rsa.ent file we can load the disulfides 2733 with the following: 2734 2735 >>> from proteusPy.Disulfide import Disulfide, check_header_from_file 2736 >>> MODEL_DIR = '../data/' 2737 >>> OK = False 2738 >>> OK = check_header_from_file(f'{MODEL_DIR}pdb5rsa.ent', verbose=True) 2739 -> check_header_from_file() - Parsing file: ../data/pdb5rsa.ent: 2740 -> SSBond: 1: tmp: 26A - 84A 2741 -> SSBond: 2: tmp: 40A - 95A 2742 -> SSBond: 3: tmp: 58A - 110A 2743 -> SSBond: 4: tmp: 65A - 72A 2744 >>> OK 2745 True 2746 """ 2747 import os 2748 2749 i = 1 2750 proximal = distal = -1 2751 _chaina = None 2752 _chainb = None 2753 2754 parser = PDBParser(PERMISSIVE=True) 2755 2756 # Biopython uses the Structure -> Model -> Chain hierarchy to organize 2757 # structures. All are iterable. 2758 2759 structure = parser.get_structure("tmp", file=filename) 2760 struct_name = structure.get_id() 2761 2762 model = structure[model_numb] 2763 2764 if verbose: 2765 print(f"-> check_header_from_file() - Parsing file: {filename}:") 2766 2767 ssbond_dict = structure.header["ssbond"] # NB: this requires the modified code 2768 2769 # list of tuples with (proximal distal chaina chainb) 2770 ssbonds = parse_ssbond_header_rec(ssbond_dict) 2771 2772 for pair in ssbonds: 2773 # in the form (proximal, distal, chain) 2774 proximal = pair[0] 2775 distal = pair[1] 2776 2777 if not proximal.isnumeric() or not distal.isnumeric(): 2778 if verbose: 2779 mess = f" ! Cannot parse SSBond record (non-numeric IDs):\ 2780 {struct_name} Prox: {proximal} {chain1_id} Dist: {distal} {chain2_id}" 2781 warnings.warn(mess, DisulfideParseWarning) 2782 continue # was pass 2783 else: 2784 proximal = int(proximal) 2785 distal = int(distal) 2786 2787 chain1_id = pair[2] 2788 chain2_id = pair[3] 2789 2790 _chaina = model[chain1_id] 2791 _chainb = model[chain2_id] 2792 2793 if chain1_id != chain2_id: 2794 if verbose: 2795 mess = f" -> Cross Chain SS for: Prox: {proximal}{chain1_id} Dist: {distal}{chain2_id}" 2796 warnings.warn(mess, DisulfideParseWarning) 2797 pass # was break 2798 2799 try: 2800 prox_res = _chaina[proximal] 2801 dist_res = _chainb[distal] 2802 except KeyError: 2803 print( 2804 f" ! Cannot parse SSBond record (KeyError): {struct_name} Prox: <{proximal}> {chain1_id} Dist: <{distal}> {chain2_id}" 2805 ) 2806 return False 2807 2808 if (_chaina is not None) and (_chainb is not None): 2809 if _chaina[proximal].is_disordered() or _chainb[distal].is_disordered(): 2810 continue 2811 else: 2812 if verbose: 2813 print( 2814 f" -> SSBond: {i}: {struct_name}: {proximal}{chain1_id} - {distal}{chain2_id}" 2815 ) 2816 else: 2817 if dbg: 2818 print( 2819 f" -> NULL chain(s): {struct_name}: {proximal}{chain1_id} - {distal}{chain2_id}" 2820 ) 2821 i += 1 2822 return True
Check the Disulfides in the PDB file.
NB: Requires EGS-Modified BIO.parse_pdb_header.py from https://github.com/suchanek/biopython/
Parameters
- filename: the name of the PDB entry.
- model_numb: model number to use, defaults to 0 for single structure files.
- verbose: print info while parsing
Returns
True if parsable
Example:
Assuming MODEL_DIR
has the pdb5rsa.ent file we can load the disulfides
with the following:
>>> from proteusPy.Disulfide import Disulfide, check_header_from_file
>>> MODEL_DIR = '../data/'
>>> OK = False
>>> OK = check_header_from_file(f'{MODEL_DIR}pdb5rsa.ent', verbose=True)
-> check_header_from_file() - Parsing file: ../data/pdb5rsa.ent:
-> SSBond: 1: tmp: 26A - 84A
-> SSBond: 2: tmp: 40A - 95A
-> SSBond: 3: tmp: 58A - 110A
-> SSBond: 4: tmp: 65A - 72A
>>> OK
True
2825def check_header_from_id( 2826 struct_name: str, pdb_dir=".", model_numb=0, verbose=False, dbg=False 2827) -> bool: 2828 """ 2829 Check parsability PDB ID and initializes the Disulfide objects. 2830 Assumes the file is downloaded in ```MODEL_DIR``` path. 2831 2832 NB: Requires EGS-Modified BIO.parse_pdb_header.py from https://github.com/suchanek/biopython/ 2833 2834 :param struct_name: the name of the PDB entry. 2835 :param pdb_dir: path to the PDB files, defaults to PDB_DIR 2836 :param model_numb: model number to use, defaults to 0 for single structure files. 2837 :param verbose: print info while parsing 2838 :param dbg: Debugging Flag 2839 :return: True if OK, False otherwise 2840 2841 Example: 2842 Assuming the PDB_DIR has the pdb5rsa.ent file we can check the file thusly: 2843 2844 >>> from proteusPy.Disulfide import Disulfide, check_header_from_id 2845 >>> MODEL_DIR = '/Users/egs/PDB/good/' 2846 >>> OK = False 2847 >>> OK = check_header_from_id('5rsa', pdb_dir=MODEL_DIR, verbose=True) 2848 -> SSBond: 1: 5rsa: 26A - 84A 2849 -> SSBond: 2: 5rsa: 40A - 95A 2850 -> SSBond: 3: 5rsa: 58A - 110A 2851 -> SSBond: 4: 5rsa: 65A - 72A 2852 >>> OK 2853 True 2854 """ 2855 parser = PDBParser(PERMISSIVE=True, QUIET=True) 2856 structure = parser.get_structure(struct_name, file=f"{pdb_dir}pdb{struct_name}.ent") 2857 model = structure[0] 2858 2859 ssbond_dict = structure.header["ssbond"] # NB: this requires the modified code 2860 2861 bondlist = [] 2862 i = 0 2863 2864 # get a list of tuples containing the proximal, distal residue IDs for 2865 # all SSBonds in the chain. 2866 bondlist = parse_ssbond_header_rec(ssbond_dict) 2867 2868 if len(bondlist) == 0: 2869 if verbose: 2870 print("-> check_header_from_id(): no bonds found in bondlist.") 2871 return False 2872 2873 for pair in bondlist: 2874 # in the form (proximal, distal, chain) 2875 proximal = pair[0] 2876 distal = pair[1] 2877 chain1 = pair[2] 2878 chain2 = pair[3] 2879 2880 chaina = model[chain1] 2881 chainb = model[chain2] 2882 2883 try: 2884 prox_residue = chaina[proximal] 2885 dist_residue = chainb[distal] 2886 2887 prox_residue.disordered_select("CYS") 2888 dist_residue.disordered_select("CYS") 2889 2890 if ( 2891 prox_residue.get_resname() != "CYS" 2892 or dist_residue.get_resname() != "CYS" 2893 ): 2894 if verbose: 2895 print( 2896 f"build_disulfide() requires CYS at both residues:\ 2897 {prox_residue.get_resname()} {dist_residue.get_resname()}" 2898 ) 2899 return False 2900 except KeyError: 2901 if dbg: 2902 print( 2903 f"Keyerror: {struct_name}: {proximal} {chain1} - {distal} {chain2}" 2904 ) 2905 return False 2906 2907 if verbose: 2908 print( 2909 f" -> SSBond: {i+1}: {struct_name}: {proximal}{chain1} - {distal}{chain2}" 2910 ) 2911 2912 i += 1 2913 return True
Check parsability PDB ID and initializes the Disulfide objects.
Assumes the file is downloaded in MODEL_DIR
path.
NB: Requires EGS-Modified BIO.parse_pdb_header.py from https://github.com/suchanek/biopython/
Parameters
- struct_name: the name of the PDB entry.
- pdb_dir: path to the PDB files, defaults to PDB_DIR
- model_numb: model number to use, defaults to 0 for single structure files.
- verbose: print info while parsing
- dbg: Debugging Flag
Returns
True if OK, False otherwise
Example: Assuming the PDB_DIR has the pdb5rsa.ent file we can check the file thusly:
>>> from proteusPy.Disulfide import Disulfide, check_header_from_id
>>> MODEL_DIR = '/Users/egs/PDB/good/'
>>> OK = False
>>> OK = check_header_from_id('5rsa', pdb_dir=MODEL_DIR, verbose=True)
-> SSBond: 1: 5rsa: 26A - 84A
-> SSBond: 2: 5rsa: 40A - 95A
-> SSBond: 3: 5rsa: 58A - 110A
-> SSBond: 4: 5rsa: 65A - 72A
>>> OK
True
2916def Disulfide_Energy_Function(x: list) -> float: 2917 """ 2918 Compute the approximate torsional energy (kcal/mpl) for the input dihedral angles. 2919 2920 :param x: A list of dihedral angles: [chi1, chi2, chi3, chi4, chi5] 2921 :return: Energy in kcal/mol 2922 2923 Example: 2924 >>> from proteusPy.Disulfide import Disulfide_Energy_Function 2925 >>> dihed = [-60.0, -60.0, -90.0, -60.0, -90.0] 2926 >>> res = Disulfide_Energy_Function(dihed) 2927 >>> res 2928 2.5999999999999996 2929 """ 2930 import numpy as np 2931 2932 chi1, chi2, chi3, chi4, chi5 = x 2933 energy = 2.0 * (np.cos(np.deg2rad(3.0 * chi1)) + np.cos(np.deg2rad(3.0 * chi5))) 2934 energy += np.cos(np.deg2rad(3.0 * chi2)) + np.cos(np.deg2rad(3.0 * chi4)) 2935 energy += ( 2936 3.5 * np.cos(np.deg2rad(2.0 * chi3)) 2937 + 0.6 * np.cos(np.deg2rad(3.0 * chi3)) 2938 + 10.1 2939 ) 2940 return energy
Compute the approximate torsional energy (kcal/mpl) for the input dihedral angles.
Parameters
- x: A list of dihedral angles: [chi1, chi2, chi3, chi4, chi5]
Returns
Energy in kcal/mol
Example:
>>> from proteusPy.Disulfide import Disulfide_Energy_Function
>>> dihed = [-60.0, -60.0, -90.0, -60.0, -90.0]
>>> res = Disulfide_Energy_Function(dihed)
>>> res
2.5999999999999996
2943def Minimize(inputSS: Disulfide) -> Disulfide: 2944 """ 2945 Minimizes the energy of a Disulfide object using the Nelder-Mead optimization method. 2946 2947 Parameters: 2948 inputSS (Disulfide): The Disulfide object to be minimized. 2949 2950 Returns: 2951 Disulfide: The minimized Disulfide object. 2952 2953 """ 2954 from scipy.optimize import minimize 2955 2956 from proteusPy import Disulfide, Disulfide_Energy_Function 2957 2958 initial_guess = inputSS.torsion_array 2959 result = minimize(Disulfide_Energy_Function, initial_guess, method="Nelder-Mead") 2960 minimum_conformation = result.x 2961 modelled_min = Disulfide("minimized") 2962 modelled_min.dihedrals = minimum_conformation 2963 modelled_min.build_yourself() 2964 return modelled_min
Minimizes the energy of a Disulfide object using the Nelder-Mead optimization method.
Parameters: inputSS (Disulfide): The Disulfide object to be minimized.
Returns: Disulfide: The minimized Disulfide object.