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 Modification: 2025-02-09 23:32:12
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 Modification: 2025-02-09 23:32:12 9""" 10 11# pylint: disable=W1203 # use of print 12# pylint: disable=C0103 # invalid name 13# pylint: disable=C0301 # line too long 14# pylint: disable=C0302 # too many lines in module 15# pylint: disable=W0212 # access to protected member 16# pylint: disable=W0613 # unused argument 17# pylint: disable=C2801 # no dunder method 18 19# Cα N, Cα, Cβ, C', Sγ Å ° ρ 20 21import copy 22import logging 23import math 24import warnings 25from math import cos 26 27import numpy as np 28import pyvista as pv 29from scipy.optimize import minimize 30 31# import proteusPy 32from proteusPy.atoms import ( 33 ATOM_COLORS, 34 ATOM_RADII_COVALENT, 35 ATOM_RADII_CPK, 36 BOND_COLOR, 37 BOND_RADIUS, 38 BS_SCALE, 39 SPEC_POWER, 40 SPECULARITY, 41) 42from proteusPy.DisulfideClassManager import DisulfideClassManager 43from proteusPy.DisulfideExceptions import ( 44 DisulfideConstructionException, 45 DisulfideConstructionWarning, 46 ProteusPyWarning, 47) 48from proteusPy.DisulfideList import DisulfideList 49from proteusPy.logger_config import create_logger 50from proteusPy.ProteusGlobals import _ANG_INIT, _FLOAT_INIT, FONTSIZE, WINSIZE 51from proteusPy.Residue import build_residue 52from proteusPy.ssparser import ( 53 get_phipsi_atoms_coordinates, 54 get_residue_atoms_coordinates, 55) 56from proteusPy.turtle3D import ORIENT_SIDECHAIN, Turtle3D 57from proteusPy.utility import set_pyvista_theme 58from proteusPy.vector3D import ( 59 Vector3D, 60 calc_dihedral, 61 calculate_bond_angle, 62 distance3d, 63 rms_difference, 64) 65 66np.set_printoptions(suppress=True) 67pv.global_theme.color = "white" 68 69# Configure logging 70logging.basicConfig( 71 level=logging.WARNING, 72 format="%(asctime)s - %(levelname)s - %(message)s", 73) 74 75 76# Suppress findfont debug messages 77logging.getLogger("matplotlib.font_manager").setLevel(logging.WARNING) 78 79ORIGIN = Vector3D(0.0, 0.0, 0.0) 80 81_logger = create_logger(__name__) 82_logger.setLevel(logging.ERROR) 83 84 85# class for the Disulfide bond 86class Disulfide: 87 r""" 88 This class provides a Python object and methods representing a physical disulfide bond 89 either extracted from the RCSB protein databank or built using the 90 [proteusPy.Turtle3D](turtle3D.html) class. The disulfide bond is an important 91 intramolecular stabilizing structural element and is characterized by: 92 93 * Atomic coordinates for the atoms N, Cα, Cβ, C', Sγ for both residues. 94 These are stored as both raw atomic coordinates as read from the RCSB file 95 and internal local coordinates. 96 * The dihedral angles Χ1 - Χ5 for the disulfide bond 97 * A name, by default {pdb_id}{prox_resnumb}{prox_chain}_{distal_resnum}{distal_chain} 98 * Proximal residue number 99 * Distal residue number 100 * Approximate bond torsional energy (kcal/mol): 101 102 $$ 103 E_{kcal/mol} \\approx 2.0 * cos(3.0 * \\chi_{1}) + cos(3.0 * \\chi_{5}) + cos(3.0 * \\chi_{2}) + 104 $$ 105 $$ 106 cos(3.0 * \chi_{4}) + 3.5 * cos(2.0 * \chi_{3}) + 0.6 * cos(3.0 * \chi_{3}) + 10.1 107 $$ 108 109 The equation embodies the typical 3-fold rotation barriers associated with single bonds, 110 (Χ1, Χ5, Χ2, Χ4) and a high 2-fold barrier for Χ3, resulting from the partial double bond 111 character of the S-S bond. This property leads to two major disulfide families, characterized 112 by the sign of Χ3. *Left-handed* disulfides have Χ3 < 0° and *right-handed* disulfides have 113 Χ3 > 0°. Within this breakdown there are numerous subfamilies, broadly known as the *hook*, 114 *spiral* and *staple*. These are under characgterization. 115 116 * Euclidean length of the dihedral angles (degrees) defined as: 117 $$\sqrt(\chi_{1}^{2} + \chi_{2}^{2} + \chi_{3}^{2} + \chi_{4}^{2} + \chi_{5}^{2})$$ 118 * Cα - Cα distance (Å) 119 * Cβ - Cβ distance (Å) 120 * Sγ - Sγ distance (Å) 121 * The previous C' and next N for both the proximal and distal residues. These are needed 122 to calculate the backbone dihedral angles Φ and Ψ. 123 * Backbone dihedral angles Φ and Ψ, when possible. Not all structures are complete and 124 in those cases the atoms needed may be undefined. In this case the Φ and Ψ angles are set 125 to -180°. 126 127 The class also provides a rendering capabilities using the excellent [PyVista](https://pyvista.org) 128 library, and can display disulfides interactively in a variety of display styles: 129 * 'sb' - Split Bonds style - bonds colored by their atom type 130 * 'bs' - Ball and Stick style - split bond coloring with small atoms 131 * 'pd' - Proximal/Distal style - bonds colored *Red* for proximal residue and *Green* for 132 the distal residue. 133 * 'cpk' - CPK style rendering, colored by atom type: 134 * Carbon - Grey 135 * Nitrogen - Blue 136 * Sulfur - Yellow 137 * Oxygen - Red 138 * Hydrogen - White 139 140 Individual renderings can be saved to a file, and animations created. 141 """ 142 143 def __init__( 144 self, 145 name: str = "SSBOND", 146 proximal: int = -1, 147 distal: int = -1, 148 proximal_chain: str = "A", 149 distal_chain: str = "A", 150 pdb_id: str = "1egs", 151 quiet: bool = True, 152 torsions: list = None, 153 ) -> None: 154 """ 155 Initialize the class to defined internal values. If torsions are provided, the 156 Disulfide object is built using the torsions and initialized. 157 158 :param name: Disulfide name, by default "SSBOND" 159 :param proximal: Proximal residue number, by default -1 160 :param distal: Distal residue number, by default -1 161 :param proximal_chain: Chain identifier for the proximal residue, by default "A" 162 :param distal_chain: Chain identifier for the distal residue, by default "A" 163 :param pdb_id: PDB identifier, by default "1egs" 164 :param quiet: If True, suppress output, by default True 165 :param torsions: List of torsion angles, by default None 166 """ 167 self.name = name 168 self.proximal = proximal 169 self.distal = distal 170 self.energy = _FLOAT_INIT 171 self.proximal_chain = proximal_chain 172 self.distal_chain = distal_chain 173 self.pdb_id = pdb_id 174 self.quiet = quiet 175 self.proximal_secondary = "Nosecondary" 176 self.distal_secondary = "Nosecondary" 177 self.ca_distance = _FLOAT_INIT 178 self.cb_distance = _FLOAT_INIT 179 self.sg_distance = _FLOAT_INIT 180 self.torsion_array = np.array( 181 (_ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT) 182 ) 183 self.phiprox = _ANG_INIT 184 self.psiprox = _ANG_INIT 185 self.phidist = _ANG_INIT 186 self.psidist = _ANG_INIT 187 188 # global coordinates for the Disulfide, typically as 189 # returned from the PDB file 190 191 self.n_prox = ORIGIN 192 self.ca_prox = ORIGIN 193 self.c_prox = ORIGIN 194 self.o_prox = ORIGIN 195 self.cb_prox = ORIGIN 196 self.sg_prox = ORIGIN 197 self.sg_dist = ORIGIN 198 self.cb_dist = ORIGIN 199 self.ca_dist = ORIGIN 200 self.n_dist = ORIGIN 201 self.c_dist = ORIGIN 202 self.o_dist = ORIGIN 203 204 # set when we can't find previous or next prox or distal 205 # C' or N atoms. 206 self.missing_atoms = False 207 self.modelled = False 208 self.resolution = -1.0 209 210 # need these to calculate backbone dihedral angles 211 self.c_prev_prox = ORIGIN 212 self.n_next_prox = ORIGIN 213 self.c_prev_dist = ORIGIN 214 self.n_next_dist = ORIGIN 215 216 # local coordinates for the Disulfide, computed using the Turtle3D in 217 # Orientation #1. these are generally private. 218 219 self._n_prox = ORIGIN 220 self._ca_prox = ORIGIN 221 self._c_prox = ORIGIN 222 self._o_prox = ORIGIN 223 self._cb_prox = ORIGIN 224 self._sg_prox = ORIGIN 225 self._sg_dist = ORIGIN 226 self._cb_dist = ORIGIN 227 self._ca_dist = ORIGIN 228 self._n_dist = ORIGIN 229 self._c_dist = ORIGIN 230 self._o_dist = ORIGIN 231 232 # need these to calculate backbone dihedral angles 233 self._c_prev_prox = ORIGIN 234 self._n_next_prox = ORIGIN 235 self._c_prev_dist = ORIGIN 236 self._n_next_dist = ORIGIN 237 238 # Dihedral angles for the disulfide bond itself, set to _ANG_INIT 239 self.chi1 = _ANG_INIT 240 self.chi2 = _ANG_INIT 241 self.chi3 = _ANG_INIT 242 self.chi4 = _ANG_INIT 243 self.chi5 = _ANG_INIT 244 self._rho = _ANG_INIT # new dihedral angle: Nprox - Ca_prox - Ca_dist - N_dist 245 246 self.torsion_length = _FLOAT_INIT 247 248 if torsions is not None and len(torsions) == 5: 249 # computes energy, torsion length and rho 250 self.dihedrals = torsions 251 self.build_yourself() 252 253 # comparison operators, used for sorting. keyed to SS bond energy 254 def __lt__(self, other): 255 if isinstance(other, Disulfide): 256 return self.energy < other.energy 257 return NotImplemented 258 259 def __le__(self, other): 260 if isinstance(other, Disulfide): 261 return self.energy <= other.energy 262 return NotImplemented 263 264 def __gt__(self, other): 265 if isinstance(other, Disulfide): 266 return self.energy > other.energy 267 return NotImplemented 268 269 def __ge__(self, other): 270 if isinstance(other, Disulfide): 271 return self.energy >= other.energy 272 return NotImplemented 273 274 def __eq__(self, other): 275 if isinstance(other, Disulfide): 276 return ( 277 math.isclose(self.torsion_length, other.torsion_length, rel_tol=1e-1) 278 and self.proximal == other.proximal 279 and self.distal == other.distal 280 ) 281 return False 282 283 def __ne__(self, other): 284 if isinstance(other, Disulfide): 285 return self.proximal != other.proximal or self.distal != other.distal 286 return NotImplemented 287 288 def __repr__(self): 289 """ 290 Representation for the Disulfide class 291 """ 292 s1 = self.repr_ss_info() 293 res = f"{s1}>" 294 return res 295 296 def _draw_bonds( 297 self, 298 pvp, 299 coords, 300 bond_radius=BOND_RADIUS, 301 style="sb", 302 bcolor=BOND_COLOR, 303 all_atoms=True, 304 res=100, 305 ): 306 """ 307 Generate the appropriate pyVista cylinder objects to represent 308 a particular disulfide bond. This utilizes a connection table 309 for the starting and ending atoms and a color table for the 310 bond colors. Used internally. 311 312 :param pvp: input plotter object to be updated 313 :param bradius: bond radius 314 :param style: bond style. One of sb, plain, pd 315 :param bcolor: pyvista color 316 :param missing: True if atoms are missing, False othersie 317 :param all_atoms: True if rendering O, False if only backbone rendered 318 319 :return pvp: Updated Plotter object. 320 321 """ 322 _bond_conn = np.array( 323 [ 324 [0, 1], # n-ca 325 [1, 2], # ca-c 326 [2, 3], # c-o 327 [1, 4], # ca-cb 328 [4, 5], # cb-sg 329 [6, 7], # n-ca 330 [7, 8], # ca-c 331 [8, 9], # c-o 332 [7, 10], # ca-cb 333 [10, 11], # cb-sg 334 [5, 11], # sg -sg 335 [12, 0], # cprev_prox-n 336 [2, 13], # c-nnext_prox 337 [14, 6], # cprev_dist-n_dist 338 [8, 15], # c-nnext_dist 339 ] 340 ) 341 342 # modeled disulfides only have backbone atoms since 343 # phi and psi are undefined, which makes the carbonyl 344 # oxygen (O) undefined as well. Their previous and next N 345 # are also undefined. 346 347 missing = self.missing_atoms 348 bradius = bond_radius 349 350 _bond_conn_backbone = np.array( 351 [ 352 [0, 1], # n-ca 353 [1, 2], # ca-c 354 [1, 4], # ca-cb 355 [4, 5], # cb-sg 356 [6, 7], # n-ca 357 [7, 8], # ca-c 358 [7, 10], # ca-cb 359 [10, 11], # cb-sg 360 [5, 11], # sg -sg 361 ] 362 ) 363 364 # colors for the bonds. Index into ATOM_COLORS array 365 _bond_split_colors = np.array( 366 [ 367 ("N", "C"), 368 ("C", "C"), 369 ("C", "O"), 370 ("C", "C"), 371 ("C", "SG"), 372 ("N", "C"), 373 ("C", "C"), 374 ("C", "O"), 375 ("C", "C"), 376 ("C", "SG"), 377 ("SG", "SG"), 378 # prev and next C-N bonds - color by atom Z 379 ("C", "N"), 380 ("C", "N"), 381 ("C", "N"), 382 ("C", "N"), 383 ] 384 ) 385 386 _bond_split_colors_backbone = np.array( 387 [ 388 ("N", "C"), 389 ("C", "C"), 390 ("C", "C"), 391 ("C", "SG"), 392 ("N", "C"), 393 ("C", "C"), 394 ("C", "C"), 395 ("C", "SG"), 396 ("SG", "SG"), 397 ] 398 ) 399 # work through connectivity and colors 400 orig_col = dest_col = bcolor 401 402 if all_atoms: 403 bond_conn = _bond_conn 404 bond_split_colors = _bond_split_colors 405 else: 406 bond_conn = _bond_conn_backbone 407 bond_split_colors = _bond_split_colors_backbone 408 409 for i, bond in enumerate(bond_conn): 410 if all_atoms: 411 if i > 10 and missing: # skip missing atoms 412 continue 413 414 orig, dest = bond 415 col = bond_split_colors[i] 416 417 # get the coords 418 prox_pos = coords[orig] 419 distal_pos = coords[dest] 420 421 # compute a direction vector 422 direction = distal_pos - prox_pos 423 424 # compute vector length. divide by 2 since split bond 425 height = math.dist(prox_pos, distal_pos) / 2.0 426 427 # the cylinder origins are actually in the 428 # middle so we translate 429 430 origin = prox_pos + 0.5 * direction # for a single plain bond 431 origin1 = prox_pos + 0.25 * direction 432 origin2 = prox_pos + 0.75 * direction 433 434 if style == "plain": 435 orig_col = dest_col = bcolor 436 437 # proximal-distal red/green coloring 438 elif style == "pd": 439 if i <= 4 or i == 11 or i == 12: 440 orig_col = dest_col = "red" 441 else: 442 orig_col = dest_col = "green" 443 if i == 10: 444 orig_col = dest_col = "yellow" 445 else: 446 orig_col = ATOM_COLORS[col[0]] 447 dest_col = ATOM_COLORS[col[1]] 448 449 if i >= 11: # prev and next residue atoms for phi/psi calcs 450 bradius = bradius * 0.5 # make smaller to distinguish 451 452 cap1 = pv.Sphere(center=prox_pos, radius=bradius) 453 cap2 = pv.Sphere(center=distal_pos, radius=bradius) 454 455 if style == "plain": 456 cyl = pv.Cylinder( 457 origin, direction, radius=bradius, height=height * 2.0 458 ) 459 pvp.add_mesh(cyl, color=orig_col) 460 else: 461 cyl1 = pv.Cylinder( 462 origin1, 463 direction, 464 radius=bradius, 465 height=height, 466 capping=False, 467 resolution=res, 468 ) 469 cyl2 = pv.Cylinder( 470 origin2, 471 direction, 472 radius=bradius, 473 height=height, 474 capping=False, 475 resolution=res, 476 ) 477 pvp.add_mesh(cyl1, color=orig_col) 478 pvp.add_mesh(cyl2, color=dest_col) 479 480 pvp.add_mesh(cap1, color=orig_col) 481 pvp.add_mesh(cap2, color=dest_col) 482 483 return pvp # end draw_bonds 484 485 def _render( 486 self, 487 pvplot: pv.Plotter, 488 style="bs", 489 plain=False, 490 bondcolor=BOND_COLOR, 491 bs_scale=BS_SCALE, 492 spec=SPECULARITY, 493 specpow=SPEC_POWER, 494 translate=True, 495 bond_radius=BOND_RADIUS, 496 res=100, 497 ): 498 """ 499 Update the passed pyVista plotter() object with the mesh data for the 500 input Disulfide Bond. Used internally. 501 502 :param pvplot: pyvista.Plotter object 503 :type pvplot: pv.Plotter 504 505 :param style: Rendering style, by default 'bs'. One of 'bs', 'st', 'cpk'. Render as \ 506 CPK, ball-and-stick or stick. Bonds are colored by atom color, unless \ 507 'plain' is specified. 508 :type style: str, optional 509 510 :param plain: Used internally, by default False 511 :type plain: bool, optional 512 513 :param bondcolor: pyVista color name, optional bond color for simple bonds, by default BOND_COLOR 514 :type bondcolor: str, optional 515 516 :param bs_scale: Scale factor (0-1) to reduce the atom sizes for ball and stick, by default BS_SCALE 517 :type bs_scale: float, optional 518 519 :param spec: Specularity (0-1), where 1 is totally smooth and 0 is rough, by default SPECULARITY 520 :type spec: float, optional 521 522 :param specpow: Exponent used for specularity calculations, by default SPEC_POWER 523 :type specpow: int, optional 524 525 :param translate: Flag used internally to indicate if we should translate \ 526 the disulfide to its geometric center of mass, by default True. 527 :type translate: bool, optional 528 529 :returns: Updated pv.Plotter object with atoms and bonds. 530 :rtype: pv.Plotter 531 """ 532 533 def add_atoms(pvp, coords, atoms, radii, colors, spec, specpow): 534 for i, atom in enumerate(atoms): 535 rad = radii[atom] 536 if style == "bs" and i > 11: 537 rad *= 0.75 538 pvp.add_mesh( 539 pv.Sphere(center=coords[i], radius=rad), 540 color=colors[atom], 541 smooth_shading=True, 542 specular=spec, 543 specular_power=specpow, 544 ) 545 546 def draw_bonds(pvp, coords, style, all_atoms, bond_radius, bondcolor=None): 547 return self._draw_bonds( 548 pvp, 549 coords, 550 style=style, 551 all_atoms=all_atoms, 552 bond_radius=bond_radius, 553 bcolor=bondcolor, 554 ) 555 556 model = self.modelled 557 coords = self.internal_coords 558 if translate: 559 coords -= self.cofmass 560 561 atoms = ( 562 "N", 563 "C", 564 "C", 565 "O", 566 "C", 567 "SG", 568 "N", 569 "C", 570 "C", 571 "O", 572 "C", 573 "SG", 574 "C", 575 "N", 576 "C", 577 "N", 578 ) 579 pvp = pvplot 580 all_atoms = not model 581 582 if style == "cpk": 583 add_atoms(pvp, coords, atoms, ATOM_RADII_CPK, ATOM_COLORS, spec, specpow) 584 elif style == "cov": 585 add_atoms( 586 pvp, coords, atoms, ATOM_RADII_COVALENT, ATOM_COLORS, spec, specpow 587 ) 588 elif style == "bs": 589 add_atoms( 590 pvp, 591 coords, 592 atoms, 593 {atom: ATOM_RADII_CPK[atom] * bs_scale for atom in atoms}, 594 ATOM_COLORS, 595 spec, 596 specpow, 597 ) 598 pvp = draw_bonds(pvp, coords, "bs", all_atoms, bond_radius) 599 elif style in ["sb", "pd", "plain"]: 600 pvp = draw_bonds( 601 pvp, 602 coords, 603 style, 604 all_atoms, 605 bond_radius, 606 bondcolor if style == "plain" else None, 607 ) 608 609 return pvp 610 611 def _plot( 612 self, 613 pvplot, 614 style="bs", 615 plain=False, 616 bondcolor=BOND_COLOR, 617 bs_scale=BS_SCALE, 618 spec=SPECULARITY, 619 specpow=SPEC_POWER, 620 translate=True, 621 bond_radius=BOND_RADIUS, 622 res=100, 623 ): 624 """ 625 Update the passed pyVista plotter() object with the mesh data for the 626 input Disulfide Bond. Used internally 627 628 Parameters 629 ---------- 630 pvplot : pv.Plotter 631 pyvista.Plotter object 632 633 style : str, optional 634 Rendering style, by default 'bs'. One of 'bs', 'st', 'cpk', Render as \ 635 CPK, ball-and-stick or stick. Bonds are colored by atom color, unless \ 636 'plain' is specified. 637 638 plain : bool, optional 639 Used internally, by default False 640 641 bondcolor : pyVista color name, optional bond color for simple bonds, by default BOND_COLOR 642 643 bs_scale : float, optional 644 scale factor (0-1) to reduce the atom sizes for ball and stick, by default BS_SCALE 645 646 spec : float, optional 647 specularity (0-1), where 1 is totally smooth and 0 is rough, by default SPECULARITY 648 649 specpow : int, optional 650 exponent used for specularity calculations, by default SPEC_POWER 651 652 translate : bool, optional 653 Flag used internally to indicate if we should translate \ 654 the disulfide to its geometric center of mass, by default True. 655 656 Returns 657 ------- 658 pv.Plotter 659 Updated pv.Plotter object with atoms and bonds. 660 """ 661 662 _bradius = bond_radius 663 coords = self.internal_coords 664 665 model = self.modelled 666 if model: 667 all_atoms = False 668 else: 669 all_atoms = True 670 671 if translate: 672 coords -= self.cofmass 673 674 atoms = ( 675 "N", 676 "C", 677 "C", 678 "O", 679 "C", 680 "SG", 681 "N", 682 "C", 683 "C", 684 "O", 685 "C", 686 "SG", 687 "C", 688 "N", 689 "C", 690 "N", 691 ) 692 pvp = pvplot.copy() 693 694 # bond connection table with atoms in the specific order shown above: 695 # returned by ss.get_internal_coords() 696 697 if style == "cpk": 698 for i, atom in enumerate(atoms): 699 rad = ATOM_RADII_CPK[atom] 700 pvp.append(pv.Sphere(center=coords[i], radius=rad)) 701 702 elif style == "cov": 703 for i, atom in enumerate(atoms): 704 rad = ATOM_RADII_COVALENT[atom] 705 pvp.append(pv.Sphere(center=coords[i], radius=rad)) 706 707 elif style == "bs": # ball and stick 708 for i, atom in enumerate(atoms): 709 rad = ATOM_RADII_CPK[atom] * bs_scale 710 if i > 11: 711 rad = rad * 0.75 712 713 pvp.append(pv.Sphere(center=coords[i])) 714 pvp = self._draw_bonds(pvp, coords, style="bs", all_atoms=all_atoms) 715 716 else: 717 pvp = self._draw_bonds(pvp, coords, style=style, all_atoms=all_atoms) 718 719 return 720 721 def _handle_SS_exception(self, message: str): 722 """ 723 This method catches an exception that occurs in the Disulfide 724 object (if quiet), or raises it again, this time adding the 725 PDB line number to the error message. (private). 726 727 :param message: Error message 728 :raises DisulfideConstructionException: Fatal construction exception. 729 730 """ 731 # message = "%s at line %i." % (message) 732 message = f"{message}" 733 734 if self.quiet: 735 # just print a warning - some residues/atoms may be missing 736 warnings.warn( 737 f"DisulfideConstructionException: {message}\n", 738 "Exception ignored.\n", 739 "Some atoms may be missing in the data structure.", 740 DisulfideConstructionWarning, 741 ) 742 else: 743 # exceptions are fatal - raise again with new message (including line nr) 744 raise DisulfideConstructionException(message) from None 745 746 @property 747 def binary_class_string(self): 748 """ 749 Return a binary string representation of the disulfide bond class. 750 """ 751 return DisulfideClassManager.class_string_from_dihedral( 752 self.chi1, self.chi2, self.chi3, self.chi4, self.chi5, base=2 753 ) 754 755 @property 756 def octant_class_string(self): 757 """ 758 Return the octant string representation of the disulfide bond class. 759 """ 760 return DisulfideClassManager.class_string_from_dihedral( 761 self.chi1, self.chi2, self.chi3, self.chi4, self.chi5, base=8 762 ) 763 764 @property 765 def bond_angle_ideality(self): 766 """ 767 Calculate all bond angles for a disulfide bond and compare them to idealized angles. 768 769 :param np.ndarray atom_coordinates: Array containing coordinates of atoms in the order: 770 N1, CA1, C1, O1, CB1, SG1, N2, CA2, C2, O2, CB2, SG2 771 :return: RMS difference between calculated bond angles and idealized bond angles. 772 :rtype: float 773 """ 774 775 atom_coordinates = self.coords_array 776 verbose = not self.quiet 777 if verbose: 778 _logger.setLevel(logging.INFO) 779 780 idealized_angles = { 781 ("N1", "CA1", "C1"): 111.0, 782 ("N1", "CA1", "CB1"): 108.5, 783 ("CA1", "CB1", "SG1"): 112.8, 784 ("CB1", "SG1", "SG2"): 103.8, # This angle is for the disulfide bond itself 785 ("SG1", "SG2", "CB2"): 103.8, # This angle is for the disulfide bond itself 786 ("SG2", "CB2", "CA2"): 112.8, 787 ("CB2", "CA2", "N2"): 108.5, 788 ("N2", "CA2", "C2"): 111.0, 789 } 790 791 # List of triplets for which we need to calculate bond angles 792 # I am omitting the proximal and distal backbone angle N, Ca, C 793 # to focus on the disulfide bond angles themselves. 794 angle_triplets = [ 795 ("N1", "CA1", "C1"), 796 ("N1", "CA1", "CB1"), 797 ("CA1", "CB1", "SG1"), 798 ("CB1", "SG1", "SG2"), 799 ("SG1", "SG2", "CB2"), 800 ("SG2", "CB2", "CA2"), 801 ("CB2", "CA2", "N2"), 802 ("N2", "CA2", "C2"), 803 ] 804 805 atom_indices = { 806 "N1": 0, 807 "CA1": 1, 808 "C1": 2, 809 "CB1": 4, 810 "SG1": 5, 811 "SG2": 11, 812 "CB2": 10, 813 "CA2": 7, 814 "N2": 6, 815 "C2": 8, 816 } 817 818 calculated_angles = [] 819 for triplet in angle_triplets: 820 a = atom_coordinates[atom_indices[triplet[0]]] 821 b = atom_coordinates[atom_indices[triplet[1]]] 822 c = atom_coordinates[atom_indices[triplet[2]]] 823 ideal = idealized_angles[triplet] 824 try: 825 angle = calculate_bond_angle(a, b, c) 826 except ValueError as e: 827 print(f"Error calculating angle for atoms {triplet}: {e}") 828 return None 829 calculated_angles.append(angle) 830 if verbose: 831 _logger.info( 832 f"Calculated angle for atoms {triplet}: {angle:.2f}, Ideal angle: {ideal:.2f}" 833 ) 834 835 # Convert idealized angles to a list 836 idealized_angles_list = [ 837 idealized_angles[triplet] for triplet in angle_triplets 838 ] 839 840 # Calculate RMS difference 841 rms_diff = rms_difference( 842 np.array(calculated_angles), np.array(idealized_angles_list) 843 ) 844 845 if verbose: 846 _logger.info(f"RMS bond angle deviation:, {rms_diff:.2f}") 847 848 return rms_diff 849 850 @property 851 def bond_length_ideality(self): 852 """ 853 Calculate bond lengths for a disulfide bond and compare them to idealized lengths. 854 855 :param np.ndarray atom_coordinates: Array containing coordinates of atoms in the order: 856 N1, CA1, C1, O1, CB1, SG1, N2, CA2, C2, O2, CB2, SG2 857 :return: RMS difference between calculated bond lengths and idealized bond lengths. 858 :rtype: float 859 """ 860 861 atom_coordinates = self.coords_array 862 verbose = not self.quiet 863 if verbose: 864 _logger.setLevel(logging.INFO) 865 866 idealized_bonds = { 867 ("N1", "CA1"): 1.46, 868 ("CA1", "C1"): 1.52, 869 ("CA1", "CB1"): 1.52, 870 ("CB1", "SG1"): 1.86, 871 ("SG1", "SG2"): 2.044, # This angle is for the disulfide bond itself 872 ("SG2", "CB2"): 1.86, 873 ("CB2", "CA2"): 1.52, 874 ("CA2", "C2"): 1.52, 875 ("N2", "CA2"): 1.46, 876 } 877 878 # List of triplets for which we need to calculate bond angles 879 # I am omitting the proximal and distal backbone angle N, Ca, C 880 # to focus on the disulfide bond angles themselves. 881 distance_pairs = [ 882 ("N1", "CA1"), 883 ("CA1", "C1"), 884 ("CA1", "CB1"), 885 ("CB1", "SG1"), 886 ("SG1", "SG2"), # This angle is for the disulfide bond itself 887 ("SG2", "CB2"), 888 ("CB2", "CA2"), 889 ("CA2", "C2"), 890 ("N2", "CA2"), 891 ] 892 893 atom_indices = { 894 "N1": 0, 895 "CA1": 1, 896 "C1": 2, 897 "CB1": 4, 898 "SG1": 5, 899 "SG2": 11, 900 "CB2": 10, 901 "CA2": 7, 902 "N2": 6, 903 "C2": 8, 904 } 905 906 calculated_distances = [] 907 for pair in distance_pairs: 908 a = atom_coordinates[atom_indices[pair[0]]] 909 b = atom_coordinates[atom_indices[pair[1]]] 910 ideal = idealized_bonds[pair] 911 try: 912 distance = math.dist(a, b) 913 except ValueError as e: 914 _logger.error(f"Error calculating bond length for atoms {pair}: {e}") 915 return None 916 calculated_distances.append(distance) 917 if verbose: 918 _logger.info( 919 f"Calculated distance for atoms {pair}: {distance:.2f}A, Ideal distance: {ideal:.2f}A" 920 ) 921 922 # Convert idealized distances to a list 923 idealized_distance_list = [idealized_bonds[pair] for pair in distance_pairs] 924 925 # Calculate RMS difference 926 rms_diff = rms_difference( 927 np.array(calculated_distances), np.array(idealized_distance_list) 928 ) 929 930 if verbose: 931 _logger.info( 932 f"RMS distance deviation from ideality for SS atoms: {rms_diff:.2f}" 933 ) 934 935 # Reset logger level 936 _logger.setLevel(logging.WARNING) 937 938 return rms_diff 939 940 @property 941 def internal_coords_array(self): 942 """ 943 Return an array of internal coordinates for the disulfide bond. 944 945 This function collects the coordinates of the backbone atoms involved in the 946 disulfide bond and returns them as a numpy array. 947 948 :param self: The instance of the Disulfide class. 949 :type self: Disulfide 950 :return: A numpy array containing the coordinates of the atoms. 951 :rtype: np.ndarray 952 """ 953 coords = [] 954 coords.append(self._n_prox.get_array()) 955 coords.append(self._ca_prox.get_array()) 956 coords.append(self._c_prox.get_array()) 957 coords.append(self._o_prox.get_array()) 958 coords.append(self._cb_prox.get_array()) 959 coords.append(self._sg_prox.get_array()) 960 coords.append(self._n_dist.get_array()) 961 coords.append(self._ca_dist.get_array()) 962 coords.append(self._c_dist.get_array()) 963 coords.append(self._o_dist.get_array()) 964 coords.append(self._cb_dist.get_array()) 965 coords.append(self._sg_dist.get_array()) 966 967 return np.array(coords) 968 969 @property 970 def coords_array(self): 971 """ 972 Return an array of coordinates for the disulfide bond. 973 974 This function collects the coordinates of backbone atoms involved in the 975 disulfide bond and returns them as a numpy array. 976 977 :param self: The instance of the Disulfide class. 978 :type self: Disulfide 979 :return: A numpy array containing the coordinates of the atoms. 980 :rtype: np.ndarray 981 """ 982 coords = [] 983 coords.append(self.n_prox.get_array()) 984 coords.append(self.ca_prox.get_array()) 985 coords.append(self.c_prox.get_array()) 986 coords.append(self.o_prox.get_array()) 987 coords.append(self.cb_prox.get_array()) 988 coords.append(self.sg_prox.get_array()) 989 coords.append(self.n_dist.get_array()) 990 coords.append(self.ca_dist.get_array()) 991 coords.append(self.c_dist.get_array()) 992 coords.append(self.o_dist.get_array()) 993 coords.append(self.cb_dist.get_array()) 994 coords.append(self.sg_dist.get_array()) 995 996 return np.array(coords) 997 998 @property 999 def dihedrals(self) -> list: 1000 """ 1001 Return a list containing the dihedral angles for the disulfide. 1002 1003 """ 1004 return [self.chi1, self.chi2, self.chi3, self.chi4, self.chi5] 1005 1006 @dihedrals.setter 1007 def dihedrals(self, dihedrals: list) -> None: 1008 """ 1009 Sets the disulfide dihedral angles to the inputs specified in the list and 1010 computes the torsional energy and length of the disulfide bond. 1011 1012 :param dihedrals: list of dihedral angles. 1013 """ 1014 self.chi1 = dihedrals[0] 1015 self.chi2 = dihedrals[1] 1016 self.chi3 = dihedrals[2] 1017 self.chi4 = dihedrals[3] 1018 self.chi5 = dihedrals[4] 1019 self.torsion_array = np.array(dihedrals) 1020 self._compute_torsional_energy() 1021 self._compute_torsion_length() 1022 self._compute_rho() 1023 1024 def bounding_box(self) -> np.array: 1025 """ 1026 Return the bounding box array for the given disulfide. 1027 1028 :return: np.array 1029 Array containing the min, max for X, Y, and Z respectively. 1030 Does not currently take the atom's radius into account. 1031 """ 1032 coords = self.internal_coords 1033 1034 xmin, ymin, zmin = coords.min(axis=0) 1035 xmax, ymax, zmax = coords.max(axis=0) 1036 1037 res = np.array([[xmin, xmax], [ymin, ymax], [zmin, zmax]]) 1038 1039 return res 1040 1041 def build_yourself(self) -> None: 1042 """ 1043 Build a model Disulfide based its internal dihedral state 1044 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1045 Cb, with N on left), builds disulfide, and updates the object's internal 1046 coordinates. It also adds the distal protein backbone, 1047 and computes the disulfide conformational energy. 1048 """ 1049 self.build_model(self.chi1, self.chi2, self.chi3, self.chi4, self.chi5) 1050 1051 def build_model( 1052 self, chi1: float, chi2: float, chi3: float, chi4: float, chi5: float 1053 ) -> None: 1054 """ 1055 Build a model Disulfide based on the input dihedral angles. 1056 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1057 Cb, with N on left), builds disulfide, and updates the object's internal 1058 coordinates. It also adds the distal protein backbone, 1059 and computes the disulfide conformational energy. 1060 1061 :param chi1: Chi1 (degrees) 1062 :param chi2: Chi2 (degrees) 1063 :param chi3: Chi3 (degrees) 1064 :param chi4: Chi4 (degrees) 1065 :param chi5: Chi5 (degrees) 1066 1067 Example: 1068 >>> import proteusPy as pp 1069 >>> modss = pp.Disulfide('model') 1070 >>> modss.build_model(-60, -60, -90, -60, -60) 1071 >>> modss.display(style='sb', light="auto") 1072 """ 1073 1074 self.dihedrals = [chi1, chi2, chi3, chi4, chi5] 1075 self.proximal = 1 1076 self.distal = 2 1077 1078 tmp = Turtle3D("tmp") 1079 tmp.Orientation = 1 1080 1081 n = ORIGIN 1082 ca = ORIGIN 1083 cb = ORIGIN 1084 c = ORIGIN 1085 1086 self.ca_prox = tmp._position 1087 tmp.schain_to_bbone() 1088 n, ca, cb, c = build_residue(tmp) 1089 1090 self.n_prox = n 1091 self.ca_prox = ca 1092 self.c_prox = c 1093 self.cb_prox = cb 1094 1095 tmp.bbone_to_schain() 1096 tmp.move(1.53) 1097 tmp.roll(self.chi1) 1098 tmp.yaw(112.8) 1099 self.cb_prox = Vector3D(tmp._position) 1100 1101 tmp.move(1.86) 1102 tmp.roll(self.chi2) 1103 tmp.yaw(103.8) 1104 self.sg_prox = Vector3D(tmp._position) 1105 1106 tmp.move(2.044) 1107 tmp.roll(self.chi3) 1108 tmp.yaw(103.8) 1109 self.sg_dist = Vector3D(tmp._position) 1110 1111 tmp.move(1.86) 1112 tmp.roll(self.chi4) 1113 tmp.yaw(112.8) 1114 self.cb_dist = Vector3D(tmp._position) 1115 1116 tmp.move(1.53) 1117 tmp.roll(self.chi5) 1118 tmp.pitch(180.0) 1119 1120 tmp.schain_to_bbone() 1121 1122 n, ca, cb, c = build_residue(tmp) 1123 1124 self.n_dist = n 1125 self.ca_dist = ca 1126 self.c_dist = c 1127 self._compute_torsional_energy() 1128 self._compute_local_coords() 1129 self._compute_torsion_length() 1130 self._compute_rho() 1131 self.ca_distance = distance3d(self.ca_prox, self.ca_dist) 1132 self.cb_distance = distance3d(self.cb_prox, self.cb_dist) 1133 self.sg_distance = distance3d(self.sg_prox, self.sg_dist) 1134 self.torsion_array = np.array([chi1, chi2, chi3, chi4, chi5]) 1135 self.missing_atoms = True 1136 self.modelled = True 1137 1138 @property 1139 def cofmass(self) -> np.array: 1140 """ 1141 Return the geometric center of mass for the internal coordinates of 1142 the given Disulfide. Missing atoms are not included. 1143 1144 :return: 3D array for the geometric center of mass 1145 """ 1146 1147 res = self.internal_coords.mean(axis=0) 1148 return res 1149 1150 @property 1151 def coord_cofmass(self) -> np.array: 1152 """ 1153 Return the geometric center of mass for the global coordinates of 1154 the given Disulfide. Missing atoms are not included. 1155 1156 :return: 3D array for the geometric center of mass 1157 """ 1158 1159 res = self.coords.mean(axis=0) 1160 return res 1161 1162 def copy(self): 1163 """ 1164 Copy the Disulfide. 1165 1166 :return: A copy of self. 1167 """ 1168 return copy.deepcopy(self) 1169 1170 def _compute_local_coords(self) -> None: 1171 """ 1172 Compute the internal coordinates for a properly initialized Disulfide Object. 1173 1174 :param self: SS initialized Disulfide object 1175 :returns: None, modifies internal state of the input 1176 """ 1177 1178 turt = Turtle3D("tmp") 1179 # get the coordinates as np.array for Turtle3D use. 1180 cpp = self.c_prev_prox.get_array() 1181 nnp = self.n_next_prox.get_array() 1182 1183 n = self.n_prox.get_array() 1184 ca = self.ca_prox.get_array() 1185 c = self.c_prox.get_array() 1186 cb = self.cb_prox.get_array() 1187 o = self.o_prox.get_array() 1188 sg = self.sg_prox.get_array() 1189 1190 sg2 = self.sg_dist.get_array() 1191 cb2 = self.cb_dist.get_array() 1192 ca2 = self.ca_dist.get_array() 1193 c2 = self.c_dist.get_array() 1194 n2 = self.n_dist.get_array() 1195 o2 = self.o_dist.get_array() 1196 1197 cpd = self.c_prev_dist.get_array() 1198 nnd = self.n_next_dist.get_array() 1199 1200 turt.orient_from_backbone(n, ca, c, cb, ORIENT_SIDECHAIN) 1201 1202 # internal (local) coordinates, stored as Vector objects 1203 # to_local returns np.array objects 1204 1205 self._n_prox = Vector3D(turt.to_local(n)) 1206 self._ca_prox = Vector3D(turt.to_local(ca)) 1207 self._c_prox = Vector3D(turt.to_local(c)) 1208 self._o_prox = Vector3D(turt.to_local(o)) 1209 self._cb_prox = Vector3D(turt.to_local(cb)) 1210 self._sg_prox = Vector3D(turt.to_local(sg)) 1211 1212 self._c_prev_prox = Vector3D(turt.to_local(cpp)) 1213 self._n_next_prox = Vector3D(turt.to_local(nnp)) 1214 self._c_prev_dist = Vector3D(turt.to_local(cpd)) 1215 self._n_next_dist = Vector3D(turt.to_local(nnd)) 1216 1217 self._n_dist = Vector3D(turt.to_local(n2)) 1218 self._ca_dist = Vector3D(turt.to_local(ca2)) 1219 self._c_dist = Vector3D(turt.to_local(c2)) 1220 self._o_dist = Vector3D(turt.to_local(o2)) 1221 self._cb_dist = Vector3D(turt.to_local(cb2)) 1222 self._sg_dist = Vector3D(turt.to_local(sg2)) 1223 1224 def _compute_torsional_energy(self) -> float: 1225 """ 1226 Compute the approximate torsional energy for the Disulfide's 1227 conformation and sets its internal state. 1228 1229 :return: Energy (kcal/mol) 1230 """ 1231 # @TODO find citation for the ss bond energy calculation 1232 1233 def torad(deg): 1234 return np.radians(deg) 1235 1236 chi1 = self.chi1 1237 chi2 = self.chi2 1238 chi3 = self.chi3 1239 chi4 = self.chi4 1240 chi5 = self.chi5 1241 1242 energy = 2.0 * (cos(torad(3.0 * chi1)) + cos(torad(3.0 * chi5))) 1243 energy += cos(torad(3.0 * chi2)) + cos(torad(3.0 * chi4)) 1244 energy += 3.5 * cos(torad(2.0 * chi3)) + 0.6 * cos(torad(3.0 * chi3)) + 10.1 1245 1246 self.energy = energy 1247 return energy 1248 1249 def display( 1250 self, single=True, style="sb", light="auto", shadows=False, winsize=WINSIZE 1251 ) -> None: 1252 """ 1253 Display the Disulfide bond in the specific rendering style. 1254 1255 :param single: Display the bond in a single panel in the specific style. 1256 :param style: Rendering style: One of: 1257 * 'sb' - split bonds 1258 * 'bs' - ball and stick 1259 * 'cpk' - CPK style 1260 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1261 * 'plain' - boring single color 1262 :param light: If True, light background, if False, dark 1263 1264 Example: 1265 >>> import proteusPy as pp 1266 1267 >>> PDB_SS = pp.Load_PDB_SS(verbose=False, subset=True) 1268 >>> ss = PDB_SS[0] 1269 >>> ss.display(style='cpk', light="auto") 1270 >>> ss.screenshot(style='bs', fname='proteus_logo_sb.png') 1271 """ 1272 src = self.pdb_id 1273 enrg = self.energy 1274 1275 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} Å, Sg: {self.sg_distance:.2f} Å Tors: {self.torsion_length:.2f}°" 1276 1277 set_pyvista_theme(light) 1278 fontsize = 8 1279 1280 if single: 1281 _pl = pv.Plotter(window_size=winsize) 1282 _pl.add_title(title=title, font_size=fontsize) 1283 _pl.enable_anti_aliasing("msaa") 1284 1285 self._render( 1286 _pl, 1287 style=style, 1288 ) 1289 _pl.reset_camera() 1290 if shadows: 1291 _pl.enable_shadows() 1292 _pl.show() 1293 1294 else: 1295 pl = pv.Plotter(window_size=winsize, shape=(2, 2)) 1296 pl.subplot(0, 0) 1297 1298 pl.add_title(title=title, font_size=fontsize) 1299 pl.enable_anti_aliasing("msaa") 1300 1301 # pl.add_camera_orientation_widget() 1302 1303 self._render( 1304 pl, 1305 style="cpk", 1306 ) 1307 1308 pl.subplot(0, 1) 1309 pl.add_title(title=title, font_size=fontsize) 1310 1311 self._render( 1312 pl, 1313 style="bs", 1314 ) 1315 1316 pl.subplot(1, 0) 1317 pl.add_title(title=title, font_size=fontsize) 1318 1319 self._render( 1320 pl, 1321 style="sb", 1322 ) 1323 1324 pl.subplot(1, 1) 1325 pl.add_title(title=title, font_size=fontsize) 1326 1327 self._render( 1328 pl, 1329 style="pd", 1330 ) 1331 1332 pl.link_views() 1333 pl.reset_camera() 1334 if shadows: 1335 pl.enable_shadows() 1336 pl.show() 1337 return 1338 1339 @property 1340 def TorsionEnergy(self) -> float: 1341 """ 1342 Return the energy of the Disulfide bond. 1343 """ 1344 return self._compute_torsional_energy() 1345 1346 @property 1347 def TorsionLength(self) -> float: 1348 """ 1349 Return the energy of the Disulfide bond. 1350 """ 1351 return self._compute_torsion_length() 1352 1353 def Distance_neighbors(self, others: DisulfideList, cutoff: float) -> DisulfideList: 1354 """ 1355 Return list of Disulfides whose RMS atomic distance is within 1356 the cutoff (Å) in the others list. 1357 1358 :param others: DisulfideList to search 1359 :param cutoff: Distance cutoff (Å) 1360 :return: DisulfideList within the cutoff 1361 """ 1362 1363 res = [ss.copy() for ss in others if self.Distance_RMS(ss) < cutoff] 1364 return DisulfideList(res, "neighbors") 1365 1366 def Distance_RMS(self, other) -> float: 1367 """ 1368 Calculate the RMS distance between the internal coordinates of self and another Disulfide. 1369 :param other: Comparison Disulfide 1370 :return: RMS distance (Å) 1371 """ 1372 1373 # Get internal coordinates of both objects 1374 ic1 = self.internal_coords 1375 ic2 = other.internal_coords 1376 1377 # Compute the sum of squared differences between corresponding internal coordinates 1378 totsq = sum(math.dist(p1, p2) ** 2 for p1, p2 in zip(ic1, ic2)) 1379 1380 # Compute the mean of the squared distances 1381 totsq /= len(ic1) 1382 1383 # Take the square root of the mean to get the RMS distance 1384 return math.sqrt(totsq) 1385 1386 def Torsion_RMS(self, other) -> float: 1387 """ 1388 Calculate the RMS distance between the dihedral angles of self and another Disulfide. 1389 1390 :param other: Disulfide object to compare against. 1391 :type other: Disulfide 1392 :return: RMS distance in degrees. 1393 :rtype: float 1394 :raises ValueError: If the torsion arrays of self and other are not of equal length. 1395 """ 1396 # Get internal coordinates of both objects 1397 ic1 = self.torsion_array 1398 ic2 = other.torsion_array 1399 1400 # Ensure both torsion arrays have the same length 1401 if len(ic1) != len(ic2): 1402 raise ValueError("Torsion arrays must be of the same length.") 1403 1404 # Compute the total squared difference between corresponding internal coordinates 1405 total_squared_diff = sum( 1406 (angle1 - angle2) ** 2 for angle1, angle2 in zip(ic1, ic2) 1407 ) 1408 mean_squared_diff = total_squared_diff / len(ic1) 1409 1410 # Return the square root of the mean squared difference as the RMS distance 1411 return math.sqrt(mean_squared_diff) 1412 1413 def get_chains(self) -> tuple: 1414 """ 1415 Return the proximal and distal chain IDs for the Disulfide. 1416 1417 :return: tuple (proximal, distal) chain IDs 1418 """ 1419 prox = self.proximal_chain 1420 dist = self.distal_chain 1421 return tuple(prox, dist) 1422 1423 def get_full_id(self) -> tuple: 1424 """ 1425 Return the Disulfide full IDs (Used with BIO.PDB) 1426 1427 :return: Disulfide full IDs 1428 """ 1429 return (self.proximal, self.distal) 1430 1431 @property 1432 def internal_coords(self) -> np.array: 1433 """ 1434 Return the internal coordinates for the Disulfide. 1435 1436 :return: Array containing the coordinates, [16][3]. 1437 """ 1438 return self._internal_coords() 1439 1440 def _internal_coords(self) -> np.array: 1441 """ 1442 Return the internal coordinates for the Disulfide. 1443 If there are missing atoms the extra atoms for the proximal 1444 and distal N and C are set to [0,0,0]. This is needed for the center of 1445 mass calculations, used when rendering. 1446 1447 :return: Array containing the coordinates, [16][3]. 1448 """ 1449 1450 # if we don't have the prior and next atoms we initialize those 1451 # atoms to the origin so as to not effect the center of mass calculations 1452 if self.missing_atoms: 1453 res_array = np.array( 1454 ( 1455 self._n_prox.get_array(), 1456 self._ca_prox.get_array(), 1457 self._c_prox.get_array(), 1458 self._o_prox.get_array(), 1459 self._cb_prox.get_array(), 1460 self._sg_prox.get_array(), 1461 self._n_dist.get_array(), 1462 self._ca_dist.get_array(), 1463 self._c_dist.get_array(), 1464 self._o_dist.get_array(), 1465 self._cb_dist.get_array(), 1466 self._sg_dist.get_array(), 1467 [0, 0, 0], 1468 [0, 0, 0], 1469 [0, 0, 0], 1470 [0, 0, 0], 1471 ) 1472 ) 1473 else: 1474 res_array = np.array( 1475 ( 1476 self._n_prox.get_array(), 1477 self._ca_prox.get_array(), 1478 self._c_prox.get_array(), 1479 self._o_prox.get_array(), 1480 self._cb_prox.get_array(), 1481 self._sg_prox.get_array(), 1482 self._n_dist.get_array(), 1483 self._ca_dist.get_array(), 1484 self._c_dist.get_array(), 1485 self._o_dist.get_array(), 1486 self._cb_dist.get_array(), 1487 self._sg_dist.get_array(), 1488 self._c_prev_prox.get_array(), 1489 self._n_next_prox.get_array(), 1490 self._c_prev_dist.get_array(), 1491 self._n_next_dist.get_array(), 1492 ) 1493 ) 1494 return res_array 1495 1496 @property 1497 def coords(self) -> np.array: 1498 """ 1499 Return the coordinates for the Disulfide as an array. 1500 1501 :return: Array containing the coordinates, [16][3]. 1502 """ 1503 return self._coords() 1504 1505 def _coords(self) -> np.array: 1506 """ 1507 Return the coordinates for the Disulfide as an array. 1508 If there are missing atoms the extra atoms for the proximal 1509 and distal N and C are set to [0,0,0]. This is needed for the center of 1510 mass calculations, used when rendering. 1511 1512 :return: Array containing the coordinates, [16][3]. 1513 """ 1514 1515 # if we don't have the prior and next atoms we initialize those 1516 # atoms to the origin so as to not effect the center of mass calculations 1517 if self.missing_atoms: 1518 res_array = np.array( 1519 ( 1520 self.n_prox.get_array(), 1521 self.ca_prox.get_array(), 1522 self.c_prox.get_array(), 1523 self.o_prox.get_array(), 1524 self.cb_prox.get_array(), 1525 self.sg_prox.get_array(), 1526 self.n_dist.get_array(), 1527 self.ca_dist.get_array(), 1528 self.c_dist.get_array(), 1529 self.o_dist.get_array(), 1530 self.cb_dist.get_array(), 1531 self.sg_dist.get_array(), 1532 [0, 0, 0], 1533 [0, 0, 0], 1534 [0, 0, 0], 1535 [0, 0, 0], 1536 ) 1537 ) 1538 else: 1539 res_array = np.array( 1540 ( 1541 self.n_prox.get_array(), 1542 self.ca_prox.get_array(), 1543 self.c_prox.get_array(), 1544 self.o_prox.get_array(), 1545 self.cb_prox.get_array(), 1546 self.sg_prox.get_array(), 1547 self.n_dist.get_array(), 1548 self.ca_dist.get_array(), 1549 self.c_dist.get_array(), 1550 self.o_dist.get_array(), 1551 self.cb_dist.get_array(), 1552 self.sg_dist.get_array(), 1553 self.c_prev_prox.get_array(), 1554 self.n_next_prox.get_array(), 1555 self.c_prev_dist.get_array(), 1556 self.n_next_dist.get_array(), 1557 ) 1558 ) 1559 return res_array 1560 1561 def internal_coords_res(self, resnumb) -> np.array: 1562 """ 1563 Return the internal coordinates for the Disulfide. Missing atoms are not included. 1564 1565 :return: Array containing the coordinates, [12][3]. 1566 """ 1567 return self._internal_coords_res(resnumb) 1568 1569 def _internal_coords_res(self, resnumb) -> np.array: 1570 """ 1571 Return the internal coordinates for the internal coordinates of 1572 the given Disulfide. Missing atoms are not included. 1573 1574 :param resnumb: Residue number for disulfide 1575 :raises DisulfideConstructionWarning: Warning raised if the residue number is invalid 1576 :return: Array containing the internal coordinates for the disulfide 1577 """ 1578 res_array = np.zeros(shape=(6, 3)) 1579 1580 if resnumb == self.proximal: 1581 res_array = np.array( 1582 ( 1583 self._n_prox.get_array(), 1584 self._ca_prox.get_array(), 1585 self._c_prox.get_array(), 1586 self._o_prox.get_array(), 1587 self._cb_prox.get_array(), 1588 self._sg_prox.get_array(), 1589 ) 1590 ) 1591 return res_array 1592 1593 elif resnumb == self.distal: 1594 res_array = np.array( 1595 ( 1596 self._n_dist.get_array(), 1597 self._ca_dist.get_array(), 1598 self._c_dist.get_array(), 1599 self._o_dist.get_array(), 1600 self._cb_dist.get_array(), 1601 self._sg_dist.get_array(), 1602 ) 1603 ) 1604 return res_array 1605 else: 1606 mess = f"-> Disulfide._internal_coords(): Invalid argument. \ 1607 Unable to find residue: {resnumb} " 1608 raise DisulfideConstructionWarning(mess) 1609 1610 def make_movie( 1611 self, style="sb", fname="ssbond.mp4", verbose=False, steps=360 1612 ) -> None: 1613 """ 1614 Create an animation for ```self``` rotating one revolution about the Y axis, 1615 in the given ```style```, saving to ```filename```. 1616 1617 :param style: Rendering style, defaults to 'sb', one of: 1618 * 'sb' - split bonds 1619 * 'bs' - ball and stick 1620 * 'cpk' - CPK style 1621 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1622 * 'plain' - boring single color 1623 1624 :param fname: Output filename, defaults to ```ssbond.mp4``` 1625 :param verbose: Verbosity, defaults to False 1626 :param steps: Number of steps for one complete rotation, defaults to 360. 1627 """ 1628 1629 # src = self.pdb_id 1630 # name = self.name 1631 # enrg = self.energy 1632 # 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}" 1633 1634 if verbose: 1635 print(f"Rendering animation to {fname}...") 1636 1637 pl = pv.Plotter(window_size=WINSIZE, off_screen=True, theme="document") 1638 pl.open_movie(fname) 1639 path = pl.generate_orbital_path(n_points=steps) 1640 1641 # 1642 # pl.add_title(title=title, font_size=FONTSIZE) 1643 pl.enable_anti_aliasing("msaa") 1644 pl = self._render( 1645 pl, 1646 style=style, 1647 ) 1648 pl.reset_camera() 1649 pl.orbit_on_path(path, write_frames=True) 1650 pl.close() 1651 1652 if verbose: 1653 print(f"Saved mp4 animation to: {fname}") 1654 1655 def spin(self, style="sb", verbose=False, steps=360, theme="auto") -> None: 1656 """ 1657 Spin the object by rotating it one revolution about the Y axis in the given style. 1658 1659 :param style: Rendering style, defaults to 'sb', one of: 1660 * 'sb' - split bonds 1661 * 'bs' - ball and stick 1662 * 'cpk' - CPK style 1663 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1664 * 'plain' - boring single color 1665 1666 :param verbose: Verbosity, defaults to False 1667 :param steps: Number of steps for one complete rotation, defaults to 360. 1668 """ 1669 1670 src = self.pdb_id 1671 enrg = self.energy 1672 1673 title = f"{src}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol, Cα: {self.ca_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 1674 1675 set_pyvista_theme(theme) 1676 1677 if verbose: 1678 _logger.info("Spinning object: %d steps...", steps) 1679 1680 # Create a Plotter instance 1681 pl = pv.Plotter(window_size=WINSIZE, off_screen=False) 1682 pl.add_title(title=title, font_size=FONTSIZE) 1683 1684 # Enable anti-aliasing for smoother rendering 1685 pl.enable_anti_aliasing("msaa") 1686 1687 # Generate an orbital path for spinning 1688 path = pl.generate_orbital_path(n_points=steps) 1689 1690 # Render the object in the specified style 1691 pl = self._render(pl, style=style) 1692 1693 pl.reset_camera() 1694 pl.show(auto_close=False) 1695 1696 # Orbit the camera along the generated path 1697 pl.orbit_on_path(path, write_frames=False, step=1 / steps) 1698 1699 if verbose: 1700 print("Spinning completed.") 1701 1702 def plot( 1703 self, pl, single=True, style="sb", light="True", shadows=False 1704 ) -> pv.Plotter: 1705 """ 1706 Return the pyVista Plotter object for the Disulfide bond in the specific rendering style. 1707 1708 :param single: Display the bond in a single panel in the specific style. 1709 :param style: Rendering style: One of: 1710 * 'sb' - split bonds 1711 * 'bs' - ball and stick 1712 * 'cpk' - CPK style 1713 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1714 * 'plain' - boring single color 1715 :param light: If True, light background, if False, dark 1716 """ 1717 src = self.pdb_id 1718 enrg = self.energy 1719 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}°" 1720 1721 if light: 1722 pv.set_plot_theme("document") 1723 else: 1724 pv.set_plot_theme("dark") 1725 1726 pl.clear() 1727 1728 if single: 1729 pl = pv.Plotter(window_size=WINSIZE) 1730 pl.add_title(title=title, font_size=FONTSIZE) 1731 pl.enable_anti_aliasing("msaa") 1732 # pl.add_camera_orientation_widget() 1733 1734 self._render( 1735 pl, 1736 style=style, 1737 bs_scale=BS_SCALE, 1738 spec=SPECULARITY, 1739 specpow=SPEC_POWER, 1740 ) 1741 pl.reset_camera() 1742 if shadows: 1743 pl.enable_shadows() 1744 else: 1745 pl = pv.Plotter(shape=(2, 2)) 1746 pl.subplot(0, 0) 1747 1748 # pl.add_title(title=title, font_size=FONTSIZE) 1749 pl.enable_anti_aliasing("msaa") 1750 1751 # pl.add_camera_orientation_widget() 1752 1753 self._render( 1754 pl, 1755 style="cpk", 1756 ) 1757 1758 pl.subplot(0, 1) 1759 1760 self._render( 1761 pl, 1762 style="bs", 1763 ) 1764 1765 pl.subplot(1, 0) 1766 1767 self._render( 1768 pl, 1769 style="sb", 1770 ) 1771 1772 pl.subplot(1, 1) 1773 self._render( 1774 pl, 1775 style="pd", 1776 ) 1777 1778 pl.link_views() 1779 pl.reset_camera() 1780 if shadows: 1781 pl.enable_shadows() 1782 return pl 1783 1784 def pprint(self) -> None: 1785 """ 1786 Pretty print general info for the Disulfide 1787 """ 1788 s1 = self.repr_ss_info() 1789 s2 = self.repr_ss_ca_dist() 1790 s2b = self.repr_ss_sg_dist() 1791 s3 = self.repr_ss_conformation() 1792 s4 = self.repr_ss_torsion_length() 1793 res = f"{s1} \n{s3} \n{s2} \n{s2b} \n{s4}>" 1794 print(res) 1795 1796 def pprint_all(self) -> None: 1797 """ 1798 Pretty print all info for the Disulfide 1799 """ 1800 s1 = self.repr_ss_info() + "\n" 1801 s2 = self.repr_ss_coords() 1802 s3 = self.repr_ss_local_coords() 1803 s4 = self.repr_ss_conformation() 1804 s4b = self.repr_phipsi() 1805 s6 = self.repr_ss_ca_dist() 1806 s6b = self.repr_ss_cb_dist() 1807 s6c = self.repr_ss_sg_dist() 1808 s7 = self.repr_ss_torsion_length() 1809 s8 = self.repr_ss_secondary_structure() 1810 1811 res = f"{s1} {s2} {s3} {s4}\n {s4b}\n {s6}\n {s6b}\n {s6c}\n {s7}\n {s8}>" 1812 1813 print(res) 1814 1815 # repr functions. The class is large, so I split it up into sections 1816 def repr_ss_info(self) -> str: 1817 """ 1818 Representation for the Disulfide class 1819 """ 1820 s1 = f"<Disulfide {self.name}, Source: {self.pdb_id}, Resolution: {self.resolution} Å" 1821 return s1 1822 1823 def repr_ss_coords(self) -> str: 1824 """ 1825 Representation for Disulfide coordinates 1826 """ 1827 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" 1828 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" 1829 stot = f"{s2} {s3}" 1830 return stot 1831 1832 def repr_ss_conformation(self) -> str: 1833 """ 1834 Representation for Disulfide conformation 1835 """ 1836 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" 1837 stot = f"{s4}" 1838 return stot 1839 1840 def repr_ss_local_coords(self) -> str: 1841 """ 1842 Representation for the Disulfide internal coordinates. 1843 """ 1844 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" 1845 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" 1846 stot = f"{s2i}{s3i}" 1847 return stot 1848 1849 def repr_ss_residue_ids(self) -> str: 1850 """ 1851 Representation for Disulfide chain IDs 1852 """ 1853 return f"Proximal Residue fullID: <{self.proximal}> Distal Residue fullID: <{self.distal}>" 1854 1855 def repr_ss_ca_dist(self) -> str: 1856 """ 1857 Representation for Disulfide Ca distance 1858 """ 1859 s1 = f"Cα Distance: {self.ca_distance:.2f} Å" 1860 return s1 1861 1862 def repr_ss_cb_dist(self) -> str: 1863 """ 1864 Representation for Disulfide Ca distance 1865 """ 1866 s1 = f"Cβ Distance: {self.cb_distance:.2f} Å" 1867 return s1 1868 1869 def repr_ss_sg_dist(self) -> str: 1870 """ 1871 Representation for Disulfide Ca distance 1872 """ 1873 s1 = f"Sγ Distance: {self.sg_distance:.2f} Å" 1874 return s1 1875 1876 def repr_ss_torsion_length(self) -> str: 1877 """ 1878 Representation for Disulfide torsion length 1879 """ 1880 s1 = f"Torsion length: {self.torsion_length:.2f} deg" 1881 return s1 1882 1883 def repr_ss_secondary_structure(self) -> str: 1884 """ 1885 Representation for Disulfide secondary structure 1886 """ 1887 s1 = f"Proximal secondary: {self.proximal_secondary} Distal secondary: {self.distal_secondary}" 1888 return s1 1889 1890 def repr_phipsi(self) -> str: 1891 """ 1892 Representation for Disulfide phi psi angles 1893 """ 1894 s1 = f"PhiProx: {self.phiprox:.2f}° PsiProx: {self.psiprox:.2f}°, PhiDist: {self.phidist:.2f}° PsiDist: {self.psidist:.2f}°" 1895 return s1 1896 1897 def repr_all(self) -> str: 1898 """ 1899 Return a string representation for all Disulfide information 1900 contained in self. 1901 """ 1902 1903 s1 = self.repr_ss_info() + "\n" 1904 s2 = self.repr_ss_coords() 1905 s3 = self.repr_ss_local_coords() 1906 s4 = self.repr_ss_conformation() 1907 s4b = self.repr_phipsi() 1908 s6 = self.repr_ss_ca_dist() 1909 s8 = self.repr_ss_cb_dist() 1910 s7 = self.repr_ss_torsion_length() 1911 s9 = self.repr_ss_secondary_structure() 1912 1913 res = f"{s1} {s2} {s3} {s4} {s4b} {s6} {s7} {s8} {s9}>" 1914 return res 1915 1916 def repr_compact(self) -> str: 1917 """ 1918 Return a compact representation of the Disulfide object 1919 :return: string 1920 """ 1921 return f"{self.repr_ss_info()} {self.repr_ss_conformation()}" 1922 1923 def repr_conformation(self) -> str: 1924 """ 1925 Return a string representation of the Disulfide object's conformation. 1926 :return: string 1927 """ 1928 return f"{self.repr_ss_conformation()}" 1929 1930 def repr_coords(self) -> str: 1931 """ 1932 Return a string representation of the Disulfide object's coordinates. 1933 :return: string 1934 """ 1935 return f"{self.repr_ss_coords()}" 1936 1937 def repr_internal_coords(self) -> str: 1938 """ 1939 Return a string representation of the Disulfide object's internal coordinaes. 1940 :return: string 1941 """ 1942 return f"{self.repr_ss_local_coords()}" 1943 1944 def repr_chain_ids(self) -> str: 1945 """ 1946 Return a string representation of the Disulfide object's chain ids. 1947 :return: string 1948 """ 1949 return f"{self.repr_ss_residue_ids()}" 1950 1951 @property 1952 def rho(self) -> float: 1953 """ 1954 Return the dihedral angle rho for the Disulfide. 1955 """ 1956 return self._compute_rho() 1957 1958 @rho.setter 1959 def rho(self, value: float): 1960 """ 1961 Set the dihedral angle rho for the Disulfide. 1962 """ 1963 self._rho = value 1964 1965 def _compute_rho(self) -> float: 1966 """ 1967 Compute the dihedral angle rho for a Disulfide object and 1968 sets the internal state of the object. 1969 """ 1970 1971 v1 = self.n_prox - self.ca_prox 1972 v2 = self.c_prox - self.ca_prox 1973 n1 = np.cross(v2.get_array(), v1.get_array()) 1974 1975 v4 = self.n_dist - self.ca_dist 1976 v3 = self.c_dist - self.ca_dist 1977 n2 = np.cross(v4.get_array(), v3.get_array()) 1978 self._rho = calc_dihedral( 1979 Vector3D(n1), self.ca_prox, self.ca_dist, Vector3D(n2) 1980 ) 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="Auto", 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 2029 src = self.pdb_id 2030 enrg = self.energy 2031 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} Å, Sγ: {self.sg_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 2032 2033 set_pyvista_theme(light) 2034 2035 if verbose: 2036 _logger.info("Rendering screenshot to file {fname}") 2037 2038 if single: 2039 pl = pv.Plotter(window_size=WINSIZE, off_screen=False) 2040 pl.add_title(title=title, font_size=FONTSIZE) 2041 pl.enable_anti_aliasing("msaa") 2042 self._render( 2043 pl, 2044 style=style, 2045 ) 2046 pl.reset_camera() 2047 if shadows: 2048 pl.enable_shadows() 2049 2050 pl.show(auto_close=False) # allows for manipulation 2051 # Take the screenshot after ensuring the plotter is still active 2052 try: 2053 pl.screenshot(fname) 2054 except RuntimeError as e: 2055 _logger.error(f"Error saving screenshot: {e}") 2056 2057 else: 2058 pl = pv.Plotter(window_size=WINSIZE, shape=(2, 2), off_screen=False) 2059 pl.subplot(0, 0) 2060 2061 pl.add_title(title=title, font_size=FONTSIZE) 2062 pl.enable_anti_aliasing("msaa") 2063 2064 # pl.add_camera_orientation_widget() 2065 self._render( 2066 pl, 2067 style="cpk", 2068 ) 2069 2070 pl.subplot(0, 1) 2071 pl.add_title(title=title, font_size=FONTSIZE) 2072 self._render( 2073 pl, 2074 style="pd", 2075 ) 2076 2077 pl.subplot(1, 0) 2078 pl.add_title(title=title, font_size=FONTSIZE) 2079 self._render( 2080 pl, 2081 style="bs", 2082 ) 2083 2084 pl.subplot(1, 1) 2085 pl.add_title(title=title, font_size=FONTSIZE) 2086 self._render( 2087 pl, 2088 style="sb", 2089 ) 2090 2091 pl.link_views() 2092 pl.reset_camera() 2093 if shadows: 2094 pl.enable_shadows() 2095 2096 # Take the screenshot after ensuring the plotter is still active 2097 pl.show(auto_close=False) # allows for manipulation 2098 2099 try: 2100 pl.screenshot(fname) 2101 except RuntimeError as e: 2102 _logger.error(f"Error saving screenshot: {e}") 2103 2104 if verbose: 2105 print(f"Screenshot saved as: {fname}") 2106 2107 def save_meshes_as_stl(self, meshes, filename) -> None: 2108 """Save a list of meshes as a single STL file. 2109 2110 Args: 2111 meshes (list): List of pyvista mesh objects to save. 2112 filename (str): Path to save the STL file to. 2113 """ 2114 merged_mesh = pv.UnstructuredGrid() 2115 for mesh in meshes: 2116 merged_mesh += mesh 2117 merged_mesh.save(filename) 2118 2119 def export(self, style="sb", verbose=True, fname="ssbond_plt") -> None: 2120 """ 2121 Create and save a screenshot of the Disulfide in the given style and filename. 2122 2123 :param single: Display a single vs panel view, defaults to True 2124 :param style: Rendering style, one of: 2125 * 'sb' - split bonds 2126 * 'bs' - ball and stick 2127 * 'cpk' - CPK style 2128 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2129 * 'plain' - boring single color, 2130 2131 :param fname: output filename,, defaults to 'ssbond.stl' 2132 :param verbose: Verbosit, defaults to False 2133 """ 2134 2135 if verbose: 2136 print(f"-> screenshot(): Rendering screenshot to file {fname}") 2137 2138 pl = pv.PolyData() 2139 2140 self._plot( 2141 pl, 2142 style=style, 2143 ) 2144 2145 self.save_meshes_as_stl(pl, fname) 2146 2147 return 2148 2149 def set_positions( 2150 self, 2151 n_prox: Vector3D, 2152 ca_prox: Vector3D, 2153 c_prox: Vector3D, 2154 o_prox: Vector3D, 2155 cb_prox: Vector3D, 2156 sg_prox: Vector3D, 2157 n_dist: Vector3D, 2158 ca_dist: Vector3D, 2159 c_dist: Vector3D, 2160 o_dist: Vector3D, 2161 cb_dist: Vector3D, 2162 sg_dist: Vector3D, 2163 c_prev_prox: Vector3D, 2164 n_next_prox: Vector3D, 2165 c_prev_dist: Vector3D, 2166 n_next_dist: Vector3D, 2167 ) -> None: 2168 """ 2169 Set the atomic coordinates for all atoms in the Disulfide object. 2170 2171 :param n_prox: Proximal N position 2172 :param ca_prox: Proximal Cα position 2173 :param c_prox: Proximal C' position 2174 :param o_prox: Proximal O position 2175 :param cb_prox: Proximal Cβ position 2176 :param sg_prox: Proximal Sγ position 2177 :param n_dist: Distal N position 2178 :param ca_dist: Distal Cα position 2179 :param c_dist: Distal C' position 2180 :param o_dist: Distal O position 2181 :param cb_dist: Distal Cβ position 2182 :param sg_dist: Distal Sγ position 2183 :param c_prev_prox: Proximal previous C' 2184 :param n_next_prox: Proximal next N 2185 :param c_prev_dist: Distal previous C' 2186 :param n_next_dist: Distal next N 2187 """ 2188 2189 # deep copy 2190 self.n_prox = n_prox.copy() 2191 self.ca_prox = ca_prox.copy() 2192 self.c_prox = c_prox.copy() 2193 self.o_prox = o_prox.copy() 2194 self.cb_prox = cb_prox.copy() 2195 self.sg_prox = sg_prox.copy() 2196 self.sg_dist = sg_dist.copy() 2197 self.cb_dist = cb_dist.copy() 2198 self.ca_dist = ca_dist.copy() 2199 self.n_dist = n_dist.copy() 2200 self.c_dist = c_dist.copy() 2201 self.o_dist = o_dist.copy() 2202 2203 self.c_prev_prox = c_prev_prox.copy() 2204 self.n_next_prox = n_next_prox.copy() 2205 self.c_prev_dist = c_prev_dist.copy() 2206 self.n_next_dist = n_next_dist.copy() 2207 self._compute_local_coords() 2208 2209 def set_name(self, namestr="Disulfide") -> None: 2210 """ 2211 Set the Disulfide's name. 2212 2213 :param namestr: Name, by default "Disulfide" 2214 """ 2215 self.name = namestr 2216 2217 def set_resnum(self, proximal: int, distal: int) -> None: 2218 """ 2219 Set the proximal and residue numbers for the Disulfide. 2220 2221 :param proximal: Proximal residue number 2222 :param distal: Distal residue number 2223 """ 2224 self.proximal = proximal 2225 self.distal = distal 2226 2227 def _compute_torsion_length(self) -> float: 2228 """ 2229 Compute the 5D Euclidean length of the Disulfide object. Update the disulfide internal state. 2230 2231 :return: Torsion length (Degrees) 2232 """ 2233 # Use numpy array to compute element-wise square 2234 tors2 = np.square(self.torsion_array) 2235 2236 # Compute the sum of squares using numpy's sum function 2237 dist = math.sqrt(np.sum(tors2)) 2238 2239 # Update the internal state 2240 self.torsion_length = dist 2241 2242 return dist 2243 2244 def torsion_distance(self, other) -> float: 2245 """ 2246 Calculate the 5D Euclidean distance between `self` and another Disulfide 2247 object. This is used to compare Disulfide Bond torsion angles to 2248 determine their torsional similarity via a 5-Dimensional Euclidean distance metric. 2249 2250 :param other: Comparison Disulfide 2251 :raises ProteusPyWarning: Warning if `other` is not a Disulfide object 2252 :return: Euclidean distance (Degrees) between `self` and `other`. 2253 """ 2254 2255 # Check length of torsion arrays 2256 if len(self.torsion_array) != 5 or len(other.torsion_array) != 5: 2257 raise ProteusPyWarning( 2258 "--> Torsion_Distance() requires vectors of length 5!" 2259 ) 2260 2261 # Convert to numpy arrays 2262 p1 = np.array(self.torsion_array) 2263 p2 = np.array(other.torsion_array) 2264 2265 # Compute the difference and handle angle wrapping 2266 diff = np.abs(p1 - p2) 2267 diff = np.where(diff > 180, 360 - diff, diff) 2268 2269 # Compute the 5D Euclidean distance using numpy's linalg.norm function 2270 dist = np.linalg.norm(diff) 2271 2272 return dist 2273 2274 def torsion_neighbors(self, others, cutoff) -> DisulfideList: 2275 """ 2276 Return a list of Disulfides within the angular cutoff in the others list. 2277 This routine is used to find Disulfides having the same torsion length 2278 within the others list. This is used to find families of Disulfides with 2279 similar conformations. Assumes self is properly initialized. 2280 2281 *NB* The routine will not distinguish between +/- 2282 dihedral angles. *i.e.* [-60, -60, -90, -60, -60] would have the same 2283 torsion length as [60, 60, 90, 60, 60], two clearly different structures. 2284 2285 :param others: ```DisulfideList``` to search 2286 :param cutoff: Dihedral angle degree cutoff 2287 :return: DisulfideList within the cutoff 2288 2289 Example: 2290 In this example we load the disulfide database subset, find the disulfides with 2291 the lowest and highest energies, and then find the nearest conformational neighbors. 2292 Finally, we display the neighbors overlaid against a common reference frame. 2293 2294 >>> import proteusPy as pp 2295 >>> _theme = pp.set_pyvista_theme("auto") 2296 >>> PDB_SS = pp.Load_PDB_SS(verbose=False, subset=True) 2297 >>> ss_list = pp.DisulfideList([], 'tmp') 2298 2299 We point to the complete list to search for lowest and highest energies. 2300 >>> sslist = PDB_SS.SSList 2301 >>> ssmin_enrg, ssmax_enrg = PDB_SS.SSList.minmax_energy 2302 2303 Make an empty list and find the nearest neighbors within 10 degrees avg RMS in 2304 sidechain dihedral angle space. 2305 2306 >>> low_energy_neighbors = DisulfideList([],'Neighbors') 2307 >>> low_energy_neighbors = ssmin_enrg.torsion_neighbors(sslist, 8) 2308 2309 Display the number found, and then display them overlaid onto their common reference frame. 2310 2311 >>> tot = low_energy_neighbors.length 2312 >>> print(f'Neighbors: {tot}') 2313 Neighbors: 8 2314 >>> low_energy_neighbors.display_overlay(light="auto") 2315 2316 """ 2317 2318 res = [ss for ss in others if self.torsion_distance(ss) <= cutoff] 2319 return DisulfideList(res, "neighbors") 2320 2321 def translate(self, translation_vector: Vector3D): 2322 """Translate the Disulfide object by the given vector.""" 2323 2324 self.n_prox += translation_vector 2325 self.ca_prox += translation_vector 2326 self.c_prox += translation_vector 2327 self.o_prox += translation_vector 2328 self.cb_prox += translation_vector 2329 self.sg_prox += translation_vector 2330 self.sg_dist += translation_vector 2331 self.cb_dist += translation_vector 2332 self.ca_dist += translation_vector 2333 self.n_dist += translation_vector 2334 self.c_dist += translation_vector 2335 self.o_dist += translation_vector 2336 2337 self.c_prev_prox += translation_vector 2338 self.n_next_prox += translation_vector 2339 self.c_prev_dist += translation_vector 2340 self.n_next_dist += translation_vector 2341 self._compute_local_coords() 2342 2343 2344# Class defination ends 2345 2346 2347def disulfide_energy_function(x: list) -> float: 2348 """ 2349 Compute the approximate torsional energy (kcal/mpl) for the input dihedral angles. 2350 2351 :param x: A list of dihedral angles: [chi1, chi2, chi3, chi4, chi5] 2352 :return: Energy in kcal/mol 2353 2354 Example: 2355 >>> from proteusPy import disulfide_energy_function 2356 >>> dihed = [-60.0, -60.0, -90.0, -60.0, -90.0] 2357 >>> res = disulfide_energy_function(dihed) 2358 >>> float(res) 2359 2.5999999999999996 2360 """ 2361 2362 chi1, chi2, chi3, chi4, chi5 = x 2363 energy = 2.0 * (np.cos(np.deg2rad(3.0 * chi1)) + np.cos(np.deg2rad(3.0 * chi5))) 2364 energy += np.cos(np.deg2rad(3.0 * chi2)) + np.cos(np.deg2rad(3.0 * chi4)) 2365 energy += ( 2366 3.5 * np.cos(np.deg2rad(2.0 * chi3)) 2367 + 0.6 * np.cos(np.deg2rad(3.0 * chi3)) 2368 + 10.1 2369 ) 2370 return energy 2371 2372 2373def minimize_ss_energy(inputSS: Disulfide) -> Disulfide: 2374 """ 2375 Minimizes the energy of a Disulfide object using the Nelder-Mead optimization method. 2376 2377 Parameters: 2378 inputSS (Disulfide): The Disulfide object to be minimized. 2379 2380 Returns: 2381 Disulfide: The minimized Disulfide object. 2382 2383 """ 2384 2385 initial_guess = inputSS.torsion_array 2386 result = minimize(disulfide_energy_function, initial_guess, method="Nelder-Mead") 2387 minimum_conformation = result.x 2388 modelled_min = Disulfide("minimized", minimum_conformation) 2389 # modelled_min.dihedrals = minimum_conformation 2390 # modelled_min.build_yourself() 2391 return modelled_min 2392 2393 2394def Initialize_Disulfide_From_Coords( 2395 ssbond_atom_data, 2396 pdb_id, 2397 proximal_chain_id, 2398 distal_chain_id, 2399 proximal, 2400 distal, 2401 resolution, 2402 proximal_secondary, 2403 distal_secondary, 2404 verbose=False, 2405 quiet=True, 2406 dbg=False, 2407) -> Disulfide: 2408 """ 2409 Initialize a new Disulfide object with atomic coordinates from 2410 the proximal and distal coordinates, typically taken from a PDB file. 2411 This routine is primarily used internally when building the compressed 2412 database. 2413 2414 :param ssbond_atom_data: Dictionary containing atomic data for the disulfide bond. 2415 :type ssbond_atom_data: dict 2416 :param pdb_id: PDB identifier for the structure. 2417 :type pdb_id: str 2418 :param proximal_chain_id: Chain identifier for the proximal residue. 2419 :type proximal_chain_id: str 2420 :param distal_chain_id: Chain identifier for the distal residue. 2421 :type distal_chain_id: str 2422 :param proximal: Residue number for the proximal residue. 2423 :type proximal: int 2424 :param distal: Residue number for the distal residue. 2425 :type distal: int 2426 :param resolution: Structure resolution. 2427 :type resolution: float 2428 :param verbose: If True, enables verbose logging. Defaults to False. 2429 :type verbose: bool, optional 2430 :param quiet: If True, suppresses logging output. Defaults to True. 2431 :type quiet: bool, optional 2432 :param dbg: If True, enables debug mode. Defaults to False. 2433 :type dbg: bool, optional 2434 :return: An instance of the Disulfide class initialized with the provided coordinates. 2435 :rtype: Disulfide 2436 :raises DisulfideConstructionWarning: Raised when the disulfide bond is not parsed correctly. 2437 2438 """ 2439 2440 ssbond_name = f"{pdb_id}_{proximal}{proximal_chain_id}_{distal}{distal_chain_id}" 2441 new_ss = Disulfide(ssbond_name) 2442 2443 new_ss.pdb_id = pdb_id 2444 new_ss.resolution = resolution 2445 new_ss.proximal_secondary = proximal_secondary 2446 new_ss.distal_secondary = distal_secondary 2447 prox_atom_list = [] 2448 dist_atom_list = [] 2449 2450 if quiet: 2451 _logger.setLevel(logging.ERROR) 2452 logging.getLogger().setLevel(logging.CRITICAL) 2453 2454 # set the objects proximal and distal values 2455 new_ss.set_resnum(proximal, distal) 2456 2457 if resolution is not None: 2458 new_ss.resolution = resolution 2459 else: 2460 new_ss.resolution = -1.0 2461 2462 new_ss.proximal_chain = proximal_chain_id 2463 new_ss.distal_chain = distal_chain_id 2464 2465 # restore loggins 2466 if quiet: 2467 _logger.setLevel(logging.ERROR) 2468 logging.getLogger().setLevel(logging.CRITICAL) ## may want to be CRITICAL 2469 2470 # Get the coordinates for the proximal and distal residues as vectors 2471 # so we can do math on them later. Trap errors here to avoid problems 2472 # with missing residues or atoms. 2473 2474 # proximal residue 2475 2476 try: 2477 prox_atom_list = get_residue_atoms_coordinates( 2478 ssbond_atom_data, proximal_chain_id, proximal 2479 ) 2480 2481 n1 = prox_atom_list[0] 2482 ca1 = prox_atom_list[1] 2483 c1 = prox_atom_list[2] 2484 o1 = prox_atom_list[3] 2485 cb1 = prox_atom_list[4] 2486 sg1 = prox_atom_list[5] 2487 2488 except KeyError: 2489 # i'm torn on this. there are a lot of missing coordinates, so is 2490 # it worth the trouble to note them? I think so. 2491 _logger.error(f"Invalid/missing coordinates for: {id}, proximal: {proximal}") 2492 return None 2493 2494 # distal residue 2495 try: 2496 dist_atom_list = get_residue_atoms_coordinates( 2497 ssbond_atom_data, distal_chain_id, distal 2498 ) 2499 n2 = dist_atom_list[0] 2500 ca2 = dist_atom_list[1] 2501 c2 = dist_atom_list[2] 2502 o2 = dist_atom_list[3] 2503 cb2 = dist_atom_list[4] 2504 sg2 = dist_atom_list[5] 2505 2506 except KeyError: 2507 _logger.error(f"Invalid/missing coordinates for: {id}, distal: {distal}") 2508 return False 2509 2510 # previous residue and next residue - optional, used for phi, psi calculations 2511 prevprox_atom_list = get_phipsi_atoms_coordinates( 2512 ssbond_atom_data, proximal_chain_id, "proximal-1" 2513 ) 2514 2515 nextprox_atom_list = get_phipsi_atoms_coordinates( 2516 ssbond_atom_data, proximal_chain_id, "proximal+1" 2517 ) 2518 2519 prevdist_atom_list = get_phipsi_atoms_coordinates( 2520 ssbond_atom_data, distal_chain_id, "distal-1" 2521 ) 2522 2523 nextdist_atom_list = get_phipsi_atoms_coordinates( 2524 ssbond_atom_data, distal_chain_id, "distal+1" 2525 ) 2526 2527 if len(prevprox_atom_list) != 0: 2528 cprev_prox = prevprox_atom_list[1] 2529 new_ss.phiprox = calc_dihedral(cprev_prox, n1, ca1, c1) 2530 2531 else: 2532 cprev_prox = Vector3D(-1.0, -1.0, -1.0) 2533 new_ss.missing_atoms = True 2534 if verbose: 2535 _logger.warning( 2536 f"Missing Proximal coords for: {id} {proximal}-1. SS: {proximal}-{distal}, phi/psi not computed." 2537 ) 2538 2539 if len(prevdist_atom_list) != 0: 2540 # list is N, C 2541 cprev_dist = prevdist_atom_list[1] 2542 new_ss.phidist = calc_dihedral(cprev_dist, n2, ca2, c2) 2543 else: 2544 cprev_dist = nnext_dist = Vector3D(-1.0, -1.0, -1.0) 2545 new_ss.missing_atoms = True 2546 if verbose: 2547 _logger.warning( 2548 f"Missing Distal coords for: {id} {distal}-1). S:S {proximal}-{distal}, phi/psi not computed." 2549 ) 2550 2551 if len(nextprox_atom_list) != 0: 2552 nnext_prox = nextprox_atom_list[0] 2553 new_ss.psiprox = calc_dihedral(n1, ca1, c1, nnext_prox) 2554 else: 2555 nnext_prox = Vector3D(-1.0, -1.0, -1.0) 2556 new_ss.missing_atoms = True 2557 _logger.warning( 2558 f"Missing Proximal coords for: {id} {proximal}+1). SS: {proximal}-{distal}, phi/psi not computed." 2559 ) 2560 2561 if len(nextdist_atom_list) != 0: 2562 nnext_dist = nextdist_atom_list[0] 2563 new_ss.psidist = calc_dihedral(n2, ca2, c2, nnext_dist) 2564 else: 2565 nnext_dist = Vector3D(-1.0, -1.0, -1.0) 2566 new_ss.missing_atoms = True 2567 _logger.warning( 2568 f"Missing Distal coords for: {id} {distal}+1). SS: {proximal}-{distal}, phi/psi not computed." 2569 ) 2570 2571 # update the positions and conformation 2572 new_ss.set_positions( 2573 n1, 2574 ca1, 2575 c1, 2576 o1, 2577 cb1, 2578 sg1, 2579 n2, 2580 ca2, 2581 c2, 2582 o2, 2583 cb2, 2584 sg2, 2585 cprev_prox, 2586 nnext_prox, 2587 cprev_dist, 2588 nnext_dist, 2589 ) 2590 2591 # calculate and set the disulfide dihedral angles 2592 new_ss.chi1 = calc_dihedral(n1, ca1, cb1, sg1) 2593 new_ss.chi2 = calc_dihedral(ca1, cb1, sg1, sg2) 2594 new_ss.chi3 = calc_dihedral(cb1, sg1, sg2, cb2) 2595 new_ss.chi4 = calc_dihedral(sg1, sg2, cb2, ca2) 2596 new_ss.chi5 = calc_dihedral(sg2, cb2, ca2, n2) 2597 new_ss.ca_distance = distance3d(new_ss.ca_prox, new_ss.ca_dist) 2598 new_ss.cb_distance = distance3d(new_ss.cb_prox, new_ss.cb_dist) 2599 new_ss.sg_distance = distance3d(new_ss.sg_prox, new_ss.sg_dist) 2600 2601 new_ss.torsion_array = np.array( 2602 (new_ss.chi1, new_ss.chi2, new_ss.chi3, new_ss.chi4, new_ss.chi5) 2603 ) 2604 new_ss._compute_torsion_length() 2605 2606 # calculate and set the SS bond torsional energy 2607 new_ss._compute_torsional_energy() 2608 2609 # compute and set the local coordinates 2610 new_ss._compute_local_coords() 2611 2612 # compute rho 2613 new_ss._compute_rho() 2614 2615 # turn warnings back on 2616 if quiet: 2617 _logger.setLevel(logging.ERROR) 2618 2619 if verbose: 2620 _logger.info(f"Disulfide {ssbond_name} initialized.") 2621 2622 return new_ss 2623 2624 2625if __name__ == "__main__": 2626 import doctest 2627 2628 doctest.testmod() 2629 2630# End of file
87class Disulfide: 88 r""" 89 This class provides a Python object and methods representing a physical disulfide bond 90 either extracted from the RCSB protein databank or built using the 91 [proteusPy.Turtle3D](turtle3D.html) class. The disulfide bond is an important 92 intramolecular stabilizing structural element and is characterized by: 93 94 * Atomic coordinates for the atoms N, Cα, Cβ, C', Sγ for both residues. 95 These are stored as both raw atomic coordinates as read from the RCSB file 96 and internal local coordinates. 97 * The dihedral angles Χ1 - Χ5 for the disulfide bond 98 * A name, by default {pdb_id}{prox_resnumb}{prox_chain}_{distal_resnum}{distal_chain} 99 * Proximal residue number 100 * Distal residue number 101 * Approximate bond torsional energy (kcal/mol): 102 103 $$ 104 E_{kcal/mol} \\approx 2.0 * cos(3.0 * \\chi_{1}) + cos(3.0 * \\chi_{5}) + cos(3.0 * \\chi_{2}) + 105 $$ 106 $$ 107 cos(3.0 * \chi_{4}) + 3.5 * cos(2.0 * \chi_{3}) + 0.6 * cos(3.0 * \chi_{3}) + 10.1 108 $$ 109 110 The equation embodies the typical 3-fold rotation barriers associated with single bonds, 111 (Χ1, Χ5, Χ2, Χ4) and a high 2-fold barrier for Χ3, resulting from the partial double bond 112 character of the S-S bond. This property leads to two major disulfide families, characterized 113 by the sign of Χ3. *Left-handed* disulfides have Χ3 < 0° and *right-handed* disulfides have 114 Χ3 > 0°. Within this breakdown there are numerous subfamilies, broadly known as the *hook*, 115 *spiral* and *staple*. These are under characgterization. 116 117 * Euclidean length of the dihedral angles (degrees) defined as: 118 $$\sqrt(\chi_{1}^{2} + \chi_{2}^{2} + \chi_{3}^{2} + \chi_{4}^{2} + \chi_{5}^{2})$$ 119 * Cα - Cα distance (Å) 120 * Cβ - Cβ distance (Å) 121 * Sγ - Sγ distance (Å) 122 * The previous C' and next N for both the proximal and distal residues. These are needed 123 to calculate the backbone dihedral angles Φ and Ψ. 124 * Backbone dihedral angles Φ and Ψ, when possible. Not all structures are complete and 125 in those cases the atoms needed may be undefined. In this case the Φ and Ψ angles are set 126 to -180°. 127 128 The class also provides a rendering capabilities using the excellent [PyVista](https://pyvista.org) 129 library, and can display disulfides interactively in a variety of display styles: 130 * 'sb' - Split Bonds style - bonds colored by their atom type 131 * 'bs' - Ball and Stick style - split bond coloring with small atoms 132 * 'pd' - Proximal/Distal style - bonds colored *Red* for proximal residue and *Green* for 133 the distal residue. 134 * 'cpk' - CPK style rendering, colored by atom type: 135 * Carbon - Grey 136 * Nitrogen - Blue 137 * Sulfur - Yellow 138 * Oxygen - Red 139 * Hydrogen - White 140 141 Individual renderings can be saved to a file, and animations created. 142 """ 143 144 def __init__( 145 self, 146 name: str = "SSBOND", 147 proximal: int = -1, 148 distal: int = -1, 149 proximal_chain: str = "A", 150 distal_chain: str = "A", 151 pdb_id: str = "1egs", 152 quiet: bool = True, 153 torsions: list = None, 154 ) -> None: 155 """ 156 Initialize the class to defined internal values. If torsions are provided, the 157 Disulfide object is built using the torsions and initialized. 158 159 :param name: Disulfide name, by default "SSBOND" 160 :param proximal: Proximal residue number, by default -1 161 :param distal: Distal residue number, by default -1 162 :param proximal_chain: Chain identifier for the proximal residue, by default "A" 163 :param distal_chain: Chain identifier for the distal residue, by default "A" 164 :param pdb_id: PDB identifier, by default "1egs" 165 :param quiet: If True, suppress output, by default True 166 :param torsions: List of torsion angles, by default None 167 """ 168 self.name = name 169 self.proximal = proximal 170 self.distal = distal 171 self.energy = _FLOAT_INIT 172 self.proximal_chain = proximal_chain 173 self.distal_chain = distal_chain 174 self.pdb_id = pdb_id 175 self.quiet = quiet 176 self.proximal_secondary = "Nosecondary" 177 self.distal_secondary = "Nosecondary" 178 self.ca_distance = _FLOAT_INIT 179 self.cb_distance = _FLOAT_INIT 180 self.sg_distance = _FLOAT_INIT 181 self.torsion_array = np.array( 182 (_ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT) 183 ) 184 self.phiprox = _ANG_INIT 185 self.psiprox = _ANG_INIT 186 self.phidist = _ANG_INIT 187 self.psidist = _ANG_INIT 188 189 # global coordinates for the Disulfide, typically as 190 # returned from the PDB file 191 192 self.n_prox = ORIGIN 193 self.ca_prox = ORIGIN 194 self.c_prox = ORIGIN 195 self.o_prox = ORIGIN 196 self.cb_prox = ORIGIN 197 self.sg_prox = ORIGIN 198 self.sg_dist = ORIGIN 199 self.cb_dist = ORIGIN 200 self.ca_dist = ORIGIN 201 self.n_dist = ORIGIN 202 self.c_dist = ORIGIN 203 self.o_dist = ORIGIN 204 205 # set when we can't find previous or next prox or distal 206 # C' or N atoms. 207 self.missing_atoms = False 208 self.modelled = False 209 self.resolution = -1.0 210 211 # need these to calculate backbone dihedral angles 212 self.c_prev_prox = ORIGIN 213 self.n_next_prox = ORIGIN 214 self.c_prev_dist = ORIGIN 215 self.n_next_dist = ORIGIN 216 217 # local coordinates for the Disulfide, computed using the Turtle3D in 218 # Orientation #1. these are generally private. 219 220 self._n_prox = ORIGIN 221 self._ca_prox = ORIGIN 222 self._c_prox = ORIGIN 223 self._o_prox = ORIGIN 224 self._cb_prox = ORIGIN 225 self._sg_prox = ORIGIN 226 self._sg_dist = ORIGIN 227 self._cb_dist = ORIGIN 228 self._ca_dist = ORIGIN 229 self._n_dist = ORIGIN 230 self._c_dist = ORIGIN 231 self._o_dist = ORIGIN 232 233 # need these to calculate backbone dihedral angles 234 self._c_prev_prox = ORIGIN 235 self._n_next_prox = ORIGIN 236 self._c_prev_dist = ORIGIN 237 self._n_next_dist = ORIGIN 238 239 # Dihedral angles for the disulfide bond itself, set to _ANG_INIT 240 self.chi1 = _ANG_INIT 241 self.chi2 = _ANG_INIT 242 self.chi3 = _ANG_INIT 243 self.chi4 = _ANG_INIT 244 self.chi5 = _ANG_INIT 245 self._rho = _ANG_INIT # new dihedral angle: Nprox - Ca_prox - Ca_dist - N_dist 246 247 self.torsion_length = _FLOAT_INIT 248 249 if torsions is not None and len(torsions) == 5: 250 # computes energy, torsion length and rho 251 self.dihedrals = torsions 252 self.build_yourself() 253 254 # comparison operators, used for sorting. keyed to SS bond energy 255 def __lt__(self, other): 256 if isinstance(other, Disulfide): 257 return self.energy < other.energy 258 return NotImplemented 259 260 def __le__(self, other): 261 if isinstance(other, Disulfide): 262 return self.energy <= other.energy 263 return NotImplemented 264 265 def __gt__(self, other): 266 if isinstance(other, Disulfide): 267 return self.energy > other.energy 268 return NotImplemented 269 270 def __ge__(self, other): 271 if isinstance(other, Disulfide): 272 return self.energy >= other.energy 273 return NotImplemented 274 275 def __eq__(self, other): 276 if isinstance(other, Disulfide): 277 return ( 278 math.isclose(self.torsion_length, other.torsion_length, rel_tol=1e-1) 279 and self.proximal == other.proximal 280 and self.distal == other.distal 281 ) 282 return False 283 284 def __ne__(self, other): 285 if isinstance(other, Disulfide): 286 return self.proximal != other.proximal or self.distal != other.distal 287 return NotImplemented 288 289 def __repr__(self): 290 """ 291 Representation for the Disulfide class 292 """ 293 s1 = self.repr_ss_info() 294 res = f"{s1}>" 295 return res 296 297 def _draw_bonds( 298 self, 299 pvp, 300 coords, 301 bond_radius=BOND_RADIUS, 302 style="sb", 303 bcolor=BOND_COLOR, 304 all_atoms=True, 305 res=100, 306 ): 307 """ 308 Generate the appropriate pyVista cylinder objects to represent 309 a particular disulfide bond. This utilizes a connection table 310 for the starting and ending atoms and a color table for the 311 bond colors. Used internally. 312 313 :param pvp: input plotter object to be updated 314 :param bradius: bond radius 315 :param style: bond style. One of sb, plain, pd 316 :param bcolor: pyvista color 317 :param missing: True if atoms are missing, False othersie 318 :param all_atoms: True if rendering O, False if only backbone rendered 319 320 :return pvp: Updated Plotter object. 321 322 """ 323 _bond_conn = np.array( 324 [ 325 [0, 1], # n-ca 326 [1, 2], # ca-c 327 [2, 3], # c-o 328 [1, 4], # ca-cb 329 [4, 5], # cb-sg 330 [6, 7], # n-ca 331 [7, 8], # ca-c 332 [8, 9], # c-o 333 [7, 10], # ca-cb 334 [10, 11], # cb-sg 335 [5, 11], # sg -sg 336 [12, 0], # cprev_prox-n 337 [2, 13], # c-nnext_prox 338 [14, 6], # cprev_dist-n_dist 339 [8, 15], # c-nnext_dist 340 ] 341 ) 342 343 # modeled disulfides only have backbone atoms since 344 # phi and psi are undefined, which makes the carbonyl 345 # oxygen (O) undefined as well. Their previous and next N 346 # are also undefined. 347 348 missing = self.missing_atoms 349 bradius = bond_radius 350 351 _bond_conn_backbone = np.array( 352 [ 353 [0, 1], # n-ca 354 [1, 2], # ca-c 355 [1, 4], # ca-cb 356 [4, 5], # cb-sg 357 [6, 7], # n-ca 358 [7, 8], # ca-c 359 [7, 10], # ca-cb 360 [10, 11], # cb-sg 361 [5, 11], # sg -sg 362 ] 363 ) 364 365 # colors for the bonds. Index into ATOM_COLORS array 366 _bond_split_colors = np.array( 367 [ 368 ("N", "C"), 369 ("C", "C"), 370 ("C", "O"), 371 ("C", "C"), 372 ("C", "SG"), 373 ("N", "C"), 374 ("C", "C"), 375 ("C", "O"), 376 ("C", "C"), 377 ("C", "SG"), 378 ("SG", "SG"), 379 # prev and next C-N bonds - color by atom Z 380 ("C", "N"), 381 ("C", "N"), 382 ("C", "N"), 383 ("C", "N"), 384 ] 385 ) 386 387 _bond_split_colors_backbone = np.array( 388 [ 389 ("N", "C"), 390 ("C", "C"), 391 ("C", "C"), 392 ("C", "SG"), 393 ("N", "C"), 394 ("C", "C"), 395 ("C", "C"), 396 ("C", "SG"), 397 ("SG", "SG"), 398 ] 399 ) 400 # work through connectivity and colors 401 orig_col = dest_col = bcolor 402 403 if all_atoms: 404 bond_conn = _bond_conn 405 bond_split_colors = _bond_split_colors 406 else: 407 bond_conn = _bond_conn_backbone 408 bond_split_colors = _bond_split_colors_backbone 409 410 for i, bond in enumerate(bond_conn): 411 if all_atoms: 412 if i > 10 and missing: # skip missing atoms 413 continue 414 415 orig, dest = bond 416 col = bond_split_colors[i] 417 418 # get the coords 419 prox_pos = coords[orig] 420 distal_pos = coords[dest] 421 422 # compute a direction vector 423 direction = distal_pos - prox_pos 424 425 # compute vector length. divide by 2 since split bond 426 height = math.dist(prox_pos, distal_pos) / 2.0 427 428 # the cylinder origins are actually in the 429 # middle so we translate 430 431 origin = prox_pos + 0.5 * direction # for a single plain bond 432 origin1 = prox_pos + 0.25 * direction 433 origin2 = prox_pos + 0.75 * direction 434 435 if style == "plain": 436 orig_col = dest_col = bcolor 437 438 # proximal-distal red/green coloring 439 elif style == "pd": 440 if i <= 4 or i == 11 or i == 12: 441 orig_col = dest_col = "red" 442 else: 443 orig_col = dest_col = "green" 444 if i == 10: 445 orig_col = dest_col = "yellow" 446 else: 447 orig_col = ATOM_COLORS[col[0]] 448 dest_col = ATOM_COLORS[col[1]] 449 450 if i >= 11: # prev and next residue atoms for phi/psi calcs 451 bradius = bradius * 0.5 # make smaller to distinguish 452 453 cap1 = pv.Sphere(center=prox_pos, radius=bradius) 454 cap2 = pv.Sphere(center=distal_pos, radius=bradius) 455 456 if style == "plain": 457 cyl = pv.Cylinder( 458 origin, direction, radius=bradius, height=height * 2.0 459 ) 460 pvp.add_mesh(cyl, color=orig_col) 461 else: 462 cyl1 = pv.Cylinder( 463 origin1, 464 direction, 465 radius=bradius, 466 height=height, 467 capping=False, 468 resolution=res, 469 ) 470 cyl2 = pv.Cylinder( 471 origin2, 472 direction, 473 radius=bradius, 474 height=height, 475 capping=False, 476 resolution=res, 477 ) 478 pvp.add_mesh(cyl1, color=orig_col) 479 pvp.add_mesh(cyl2, color=dest_col) 480 481 pvp.add_mesh(cap1, color=orig_col) 482 pvp.add_mesh(cap2, color=dest_col) 483 484 return pvp # end draw_bonds 485 486 def _render( 487 self, 488 pvplot: pv.Plotter, 489 style="bs", 490 plain=False, 491 bondcolor=BOND_COLOR, 492 bs_scale=BS_SCALE, 493 spec=SPECULARITY, 494 specpow=SPEC_POWER, 495 translate=True, 496 bond_radius=BOND_RADIUS, 497 res=100, 498 ): 499 """ 500 Update the passed pyVista plotter() object with the mesh data for the 501 input Disulfide Bond. Used internally. 502 503 :param pvplot: pyvista.Plotter object 504 :type pvplot: pv.Plotter 505 506 :param style: Rendering style, by default 'bs'. One of 'bs', 'st', 'cpk'. Render as \ 507 CPK, ball-and-stick or stick. Bonds are colored by atom color, unless \ 508 'plain' is specified. 509 :type style: str, optional 510 511 :param plain: Used internally, by default False 512 :type plain: bool, optional 513 514 :param bondcolor: pyVista color name, optional bond color for simple bonds, by default BOND_COLOR 515 :type bondcolor: str, optional 516 517 :param bs_scale: Scale factor (0-1) to reduce the atom sizes for ball and stick, by default BS_SCALE 518 :type bs_scale: float, optional 519 520 :param spec: Specularity (0-1), where 1 is totally smooth and 0 is rough, by default SPECULARITY 521 :type spec: float, optional 522 523 :param specpow: Exponent used for specularity calculations, by default SPEC_POWER 524 :type specpow: int, optional 525 526 :param translate: Flag used internally to indicate if we should translate \ 527 the disulfide to its geometric center of mass, by default True. 528 :type translate: bool, optional 529 530 :returns: Updated pv.Plotter object with atoms and bonds. 531 :rtype: pv.Plotter 532 """ 533 534 def add_atoms(pvp, coords, atoms, radii, colors, spec, specpow): 535 for i, atom in enumerate(atoms): 536 rad = radii[atom] 537 if style == "bs" and i > 11: 538 rad *= 0.75 539 pvp.add_mesh( 540 pv.Sphere(center=coords[i], radius=rad), 541 color=colors[atom], 542 smooth_shading=True, 543 specular=spec, 544 specular_power=specpow, 545 ) 546 547 def draw_bonds(pvp, coords, style, all_atoms, bond_radius, bondcolor=None): 548 return self._draw_bonds( 549 pvp, 550 coords, 551 style=style, 552 all_atoms=all_atoms, 553 bond_radius=bond_radius, 554 bcolor=bondcolor, 555 ) 556 557 model = self.modelled 558 coords = self.internal_coords 559 if translate: 560 coords -= self.cofmass 561 562 atoms = ( 563 "N", 564 "C", 565 "C", 566 "O", 567 "C", 568 "SG", 569 "N", 570 "C", 571 "C", 572 "O", 573 "C", 574 "SG", 575 "C", 576 "N", 577 "C", 578 "N", 579 ) 580 pvp = pvplot 581 all_atoms = not model 582 583 if style == "cpk": 584 add_atoms(pvp, coords, atoms, ATOM_RADII_CPK, ATOM_COLORS, spec, specpow) 585 elif style == "cov": 586 add_atoms( 587 pvp, coords, atoms, ATOM_RADII_COVALENT, ATOM_COLORS, spec, specpow 588 ) 589 elif style == "bs": 590 add_atoms( 591 pvp, 592 coords, 593 atoms, 594 {atom: ATOM_RADII_CPK[atom] * bs_scale for atom in atoms}, 595 ATOM_COLORS, 596 spec, 597 specpow, 598 ) 599 pvp = draw_bonds(pvp, coords, "bs", all_atoms, bond_radius) 600 elif style in ["sb", "pd", "plain"]: 601 pvp = draw_bonds( 602 pvp, 603 coords, 604 style, 605 all_atoms, 606 bond_radius, 607 bondcolor if style == "plain" else None, 608 ) 609 610 return pvp 611 612 def _plot( 613 self, 614 pvplot, 615 style="bs", 616 plain=False, 617 bondcolor=BOND_COLOR, 618 bs_scale=BS_SCALE, 619 spec=SPECULARITY, 620 specpow=SPEC_POWER, 621 translate=True, 622 bond_radius=BOND_RADIUS, 623 res=100, 624 ): 625 """ 626 Update the passed pyVista plotter() object with the mesh data for the 627 input Disulfide Bond. Used internally 628 629 Parameters 630 ---------- 631 pvplot : pv.Plotter 632 pyvista.Plotter object 633 634 style : str, optional 635 Rendering style, by default 'bs'. One of 'bs', 'st', 'cpk', Render as \ 636 CPK, ball-and-stick or stick. Bonds are colored by atom color, unless \ 637 'plain' is specified. 638 639 plain : bool, optional 640 Used internally, by default False 641 642 bondcolor : pyVista color name, optional bond color for simple bonds, by default BOND_COLOR 643 644 bs_scale : float, optional 645 scale factor (0-1) to reduce the atom sizes for ball and stick, by default BS_SCALE 646 647 spec : float, optional 648 specularity (0-1), where 1 is totally smooth and 0 is rough, by default SPECULARITY 649 650 specpow : int, optional 651 exponent used for specularity calculations, by default SPEC_POWER 652 653 translate : bool, optional 654 Flag used internally to indicate if we should translate \ 655 the disulfide to its geometric center of mass, by default True. 656 657 Returns 658 ------- 659 pv.Plotter 660 Updated pv.Plotter object with atoms and bonds. 661 """ 662 663 _bradius = bond_radius 664 coords = self.internal_coords 665 666 model = self.modelled 667 if model: 668 all_atoms = False 669 else: 670 all_atoms = True 671 672 if translate: 673 coords -= self.cofmass 674 675 atoms = ( 676 "N", 677 "C", 678 "C", 679 "O", 680 "C", 681 "SG", 682 "N", 683 "C", 684 "C", 685 "O", 686 "C", 687 "SG", 688 "C", 689 "N", 690 "C", 691 "N", 692 ) 693 pvp = pvplot.copy() 694 695 # bond connection table with atoms in the specific order shown above: 696 # returned by ss.get_internal_coords() 697 698 if style == "cpk": 699 for i, atom in enumerate(atoms): 700 rad = ATOM_RADII_CPK[atom] 701 pvp.append(pv.Sphere(center=coords[i], radius=rad)) 702 703 elif style == "cov": 704 for i, atom in enumerate(atoms): 705 rad = ATOM_RADII_COVALENT[atom] 706 pvp.append(pv.Sphere(center=coords[i], radius=rad)) 707 708 elif style == "bs": # ball and stick 709 for i, atom in enumerate(atoms): 710 rad = ATOM_RADII_CPK[atom] * bs_scale 711 if i > 11: 712 rad = rad * 0.75 713 714 pvp.append(pv.Sphere(center=coords[i])) 715 pvp = self._draw_bonds(pvp, coords, style="bs", all_atoms=all_atoms) 716 717 else: 718 pvp = self._draw_bonds(pvp, coords, style=style, all_atoms=all_atoms) 719 720 return 721 722 def _handle_SS_exception(self, message: str): 723 """ 724 This method catches an exception that occurs in the Disulfide 725 object (if quiet), or raises it again, this time adding the 726 PDB line number to the error message. (private). 727 728 :param message: Error message 729 :raises DisulfideConstructionException: Fatal construction exception. 730 731 """ 732 # message = "%s at line %i." % (message) 733 message = f"{message}" 734 735 if self.quiet: 736 # just print a warning - some residues/atoms may be missing 737 warnings.warn( 738 f"DisulfideConstructionException: {message}\n", 739 "Exception ignored.\n", 740 "Some atoms may be missing in the data structure.", 741 DisulfideConstructionWarning, 742 ) 743 else: 744 # exceptions are fatal - raise again with new message (including line nr) 745 raise DisulfideConstructionException(message) from None 746 747 @property 748 def binary_class_string(self): 749 """ 750 Return a binary string representation of the disulfide bond class. 751 """ 752 return DisulfideClassManager.class_string_from_dihedral( 753 self.chi1, self.chi2, self.chi3, self.chi4, self.chi5, base=2 754 ) 755 756 @property 757 def octant_class_string(self): 758 """ 759 Return the octant string representation of the disulfide bond class. 760 """ 761 return DisulfideClassManager.class_string_from_dihedral( 762 self.chi1, self.chi2, self.chi3, self.chi4, self.chi5, base=8 763 ) 764 765 @property 766 def bond_angle_ideality(self): 767 """ 768 Calculate all bond angles for a disulfide bond and compare them to idealized angles. 769 770 :param np.ndarray atom_coordinates: Array containing coordinates of atoms in the order: 771 N1, CA1, C1, O1, CB1, SG1, N2, CA2, C2, O2, CB2, SG2 772 :return: RMS difference between calculated bond angles and idealized bond angles. 773 :rtype: float 774 """ 775 776 atom_coordinates = self.coords_array 777 verbose = not self.quiet 778 if verbose: 779 _logger.setLevel(logging.INFO) 780 781 idealized_angles = { 782 ("N1", "CA1", "C1"): 111.0, 783 ("N1", "CA1", "CB1"): 108.5, 784 ("CA1", "CB1", "SG1"): 112.8, 785 ("CB1", "SG1", "SG2"): 103.8, # This angle is for the disulfide bond itself 786 ("SG1", "SG2", "CB2"): 103.8, # This angle is for the disulfide bond itself 787 ("SG2", "CB2", "CA2"): 112.8, 788 ("CB2", "CA2", "N2"): 108.5, 789 ("N2", "CA2", "C2"): 111.0, 790 } 791 792 # List of triplets for which we need to calculate bond angles 793 # I am omitting the proximal and distal backbone angle N, Ca, C 794 # to focus on the disulfide bond angles themselves. 795 angle_triplets = [ 796 ("N1", "CA1", "C1"), 797 ("N1", "CA1", "CB1"), 798 ("CA1", "CB1", "SG1"), 799 ("CB1", "SG1", "SG2"), 800 ("SG1", "SG2", "CB2"), 801 ("SG2", "CB2", "CA2"), 802 ("CB2", "CA2", "N2"), 803 ("N2", "CA2", "C2"), 804 ] 805 806 atom_indices = { 807 "N1": 0, 808 "CA1": 1, 809 "C1": 2, 810 "CB1": 4, 811 "SG1": 5, 812 "SG2": 11, 813 "CB2": 10, 814 "CA2": 7, 815 "N2": 6, 816 "C2": 8, 817 } 818 819 calculated_angles = [] 820 for triplet in angle_triplets: 821 a = atom_coordinates[atom_indices[triplet[0]]] 822 b = atom_coordinates[atom_indices[triplet[1]]] 823 c = atom_coordinates[atom_indices[triplet[2]]] 824 ideal = idealized_angles[triplet] 825 try: 826 angle = calculate_bond_angle(a, b, c) 827 except ValueError as e: 828 print(f"Error calculating angle for atoms {triplet}: {e}") 829 return None 830 calculated_angles.append(angle) 831 if verbose: 832 _logger.info( 833 f"Calculated angle for atoms {triplet}: {angle:.2f}, Ideal angle: {ideal:.2f}" 834 ) 835 836 # Convert idealized angles to a list 837 idealized_angles_list = [ 838 idealized_angles[triplet] for triplet in angle_triplets 839 ] 840 841 # Calculate RMS difference 842 rms_diff = rms_difference( 843 np.array(calculated_angles), np.array(idealized_angles_list) 844 ) 845 846 if verbose: 847 _logger.info(f"RMS bond angle deviation:, {rms_diff:.2f}") 848 849 return rms_diff 850 851 @property 852 def bond_length_ideality(self): 853 """ 854 Calculate bond lengths for a disulfide bond and compare them to idealized lengths. 855 856 :param np.ndarray atom_coordinates: Array containing coordinates of atoms in the order: 857 N1, CA1, C1, O1, CB1, SG1, N2, CA2, C2, O2, CB2, SG2 858 :return: RMS difference between calculated bond lengths and idealized bond lengths. 859 :rtype: float 860 """ 861 862 atom_coordinates = self.coords_array 863 verbose = not self.quiet 864 if verbose: 865 _logger.setLevel(logging.INFO) 866 867 idealized_bonds = { 868 ("N1", "CA1"): 1.46, 869 ("CA1", "C1"): 1.52, 870 ("CA1", "CB1"): 1.52, 871 ("CB1", "SG1"): 1.86, 872 ("SG1", "SG2"): 2.044, # This angle is for the disulfide bond itself 873 ("SG2", "CB2"): 1.86, 874 ("CB2", "CA2"): 1.52, 875 ("CA2", "C2"): 1.52, 876 ("N2", "CA2"): 1.46, 877 } 878 879 # List of triplets for which we need to calculate bond angles 880 # I am omitting the proximal and distal backbone angle N, Ca, C 881 # to focus on the disulfide bond angles themselves. 882 distance_pairs = [ 883 ("N1", "CA1"), 884 ("CA1", "C1"), 885 ("CA1", "CB1"), 886 ("CB1", "SG1"), 887 ("SG1", "SG2"), # This angle is for the disulfide bond itself 888 ("SG2", "CB2"), 889 ("CB2", "CA2"), 890 ("CA2", "C2"), 891 ("N2", "CA2"), 892 ] 893 894 atom_indices = { 895 "N1": 0, 896 "CA1": 1, 897 "C1": 2, 898 "CB1": 4, 899 "SG1": 5, 900 "SG2": 11, 901 "CB2": 10, 902 "CA2": 7, 903 "N2": 6, 904 "C2": 8, 905 } 906 907 calculated_distances = [] 908 for pair in distance_pairs: 909 a = atom_coordinates[atom_indices[pair[0]]] 910 b = atom_coordinates[atom_indices[pair[1]]] 911 ideal = idealized_bonds[pair] 912 try: 913 distance = math.dist(a, b) 914 except ValueError as e: 915 _logger.error(f"Error calculating bond length for atoms {pair}: {e}") 916 return None 917 calculated_distances.append(distance) 918 if verbose: 919 _logger.info( 920 f"Calculated distance for atoms {pair}: {distance:.2f}A, Ideal distance: {ideal:.2f}A" 921 ) 922 923 # Convert idealized distances to a list 924 idealized_distance_list = [idealized_bonds[pair] for pair in distance_pairs] 925 926 # Calculate RMS difference 927 rms_diff = rms_difference( 928 np.array(calculated_distances), np.array(idealized_distance_list) 929 ) 930 931 if verbose: 932 _logger.info( 933 f"RMS distance deviation from ideality for SS atoms: {rms_diff:.2f}" 934 ) 935 936 # Reset logger level 937 _logger.setLevel(logging.WARNING) 938 939 return rms_diff 940 941 @property 942 def internal_coords_array(self): 943 """ 944 Return an array of internal coordinates for the disulfide bond. 945 946 This function collects the coordinates of the backbone atoms involved in the 947 disulfide bond and returns them as a numpy array. 948 949 :param self: The instance of the Disulfide class. 950 :type self: Disulfide 951 :return: A numpy array containing the coordinates of the atoms. 952 :rtype: np.ndarray 953 """ 954 coords = [] 955 coords.append(self._n_prox.get_array()) 956 coords.append(self._ca_prox.get_array()) 957 coords.append(self._c_prox.get_array()) 958 coords.append(self._o_prox.get_array()) 959 coords.append(self._cb_prox.get_array()) 960 coords.append(self._sg_prox.get_array()) 961 coords.append(self._n_dist.get_array()) 962 coords.append(self._ca_dist.get_array()) 963 coords.append(self._c_dist.get_array()) 964 coords.append(self._o_dist.get_array()) 965 coords.append(self._cb_dist.get_array()) 966 coords.append(self._sg_dist.get_array()) 967 968 return np.array(coords) 969 970 @property 971 def coords_array(self): 972 """ 973 Return an array of coordinates for the disulfide bond. 974 975 This function collects the coordinates of backbone atoms involved in the 976 disulfide bond and returns them as a numpy array. 977 978 :param self: The instance of the Disulfide class. 979 :type self: Disulfide 980 :return: A numpy array containing the coordinates of the atoms. 981 :rtype: np.ndarray 982 """ 983 coords = [] 984 coords.append(self.n_prox.get_array()) 985 coords.append(self.ca_prox.get_array()) 986 coords.append(self.c_prox.get_array()) 987 coords.append(self.o_prox.get_array()) 988 coords.append(self.cb_prox.get_array()) 989 coords.append(self.sg_prox.get_array()) 990 coords.append(self.n_dist.get_array()) 991 coords.append(self.ca_dist.get_array()) 992 coords.append(self.c_dist.get_array()) 993 coords.append(self.o_dist.get_array()) 994 coords.append(self.cb_dist.get_array()) 995 coords.append(self.sg_dist.get_array()) 996 997 return np.array(coords) 998 999 @property 1000 def dihedrals(self) -> list: 1001 """ 1002 Return a list containing the dihedral angles for the disulfide. 1003 1004 """ 1005 return [self.chi1, self.chi2, self.chi3, self.chi4, self.chi5] 1006 1007 @dihedrals.setter 1008 def dihedrals(self, dihedrals: list) -> None: 1009 """ 1010 Sets the disulfide dihedral angles to the inputs specified in the list and 1011 computes the torsional energy and length of the disulfide bond. 1012 1013 :param dihedrals: list of dihedral angles. 1014 """ 1015 self.chi1 = dihedrals[0] 1016 self.chi2 = dihedrals[1] 1017 self.chi3 = dihedrals[2] 1018 self.chi4 = dihedrals[3] 1019 self.chi5 = dihedrals[4] 1020 self.torsion_array = np.array(dihedrals) 1021 self._compute_torsional_energy() 1022 self._compute_torsion_length() 1023 self._compute_rho() 1024 1025 def bounding_box(self) -> np.array: 1026 """ 1027 Return the bounding box array for the given disulfide. 1028 1029 :return: np.array 1030 Array containing the min, max for X, Y, and Z respectively. 1031 Does not currently take the atom's radius into account. 1032 """ 1033 coords = self.internal_coords 1034 1035 xmin, ymin, zmin = coords.min(axis=0) 1036 xmax, ymax, zmax = coords.max(axis=0) 1037 1038 res = np.array([[xmin, xmax], [ymin, ymax], [zmin, zmax]]) 1039 1040 return res 1041 1042 def build_yourself(self) -> None: 1043 """ 1044 Build a model Disulfide based its internal dihedral state 1045 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1046 Cb, with N on left), builds disulfide, and updates the object's internal 1047 coordinates. It also adds the distal protein backbone, 1048 and computes the disulfide conformational energy. 1049 """ 1050 self.build_model(self.chi1, self.chi2, self.chi3, self.chi4, self.chi5) 1051 1052 def build_model( 1053 self, chi1: float, chi2: float, chi3: float, chi4: float, chi5: float 1054 ) -> None: 1055 """ 1056 Build a model Disulfide based on the input dihedral angles. 1057 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1058 Cb, with N on left), builds disulfide, and updates the object's internal 1059 coordinates. It also adds the distal protein backbone, 1060 and computes the disulfide conformational energy. 1061 1062 :param chi1: Chi1 (degrees) 1063 :param chi2: Chi2 (degrees) 1064 :param chi3: Chi3 (degrees) 1065 :param chi4: Chi4 (degrees) 1066 :param chi5: Chi5 (degrees) 1067 1068 Example: 1069 >>> import proteusPy as pp 1070 >>> modss = pp.Disulfide('model') 1071 >>> modss.build_model(-60, -60, -90, -60, -60) 1072 >>> modss.display(style='sb', light="auto") 1073 """ 1074 1075 self.dihedrals = [chi1, chi2, chi3, chi4, chi5] 1076 self.proximal = 1 1077 self.distal = 2 1078 1079 tmp = Turtle3D("tmp") 1080 tmp.Orientation = 1 1081 1082 n = ORIGIN 1083 ca = ORIGIN 1084 cb = ORIGIN 1085 c = ORIGIN 1086 1087 self.ca_prox = tmp._position 1088 tmp.schain_to_bbone() 1089 n, ca, cb, c = build_residue(tmp) 1090 1091 self.n_prox = n 1092 self.ca_prox = ca 1093 self.c_prox = c 1094 self.cb_prox = cb 1095 1096 tmp.bbone_to_schain() 1097 tmp.move(1.53) 1098 tmp.roll(self.chi1) 1099 tmp.yaw(112.8) 1100 self.cb_prox = Vector3D(tmp._position) 1101 1102 tmp.move(1.86) 1103 tmp.roll(self.chi2) 1104 tmp.yaw(103.8) 1105 self.sg_prox = Vector3D(tmp._position) 1106 1107 tmp.move(2.044) 1108 tmp.roll(self.chi3) 1109 tmp.yaw(103.8) 1110 self.sg_dist = Vector3D(tmp._position) 1111 1112 tmp.move(1.86) 1113 tmp.roll(self.chi4) 1114 tmp.yaw(112.8) 1115 self.cb_dist = Vector3D(tmp._position) 1116 1117 tmp.move(1.53) 1118 tmp.roll(self.chi5) 1119 tmp.pitch(180.0) 1120 1121 tmp.schain_to_bbone() 1122 1123 n, ca, cb, c = build_residue(tmp) 1124 1125 self.n_dist = n 1126 self.ca_dist = ca 1127 self.c_dist = c 1128 self._compute_torsional_energy() 1129 self._compute_local_coords() 1130 self._compute_torsion_length() 1131 self._compute_rho() 1132 self.ca_distance = distance3d(self.ca_prox, self.ca_dist) 1133 self.cb_distance = distance3d(self.cb_prox, self.cb_dist) 1134 self.sg_distance = distance3d(self.sg_prox, self.sg_dist) 1135 self.torsion_array = np.array([chi1, chi2, chi3, chi4, chi5]) 1136 self.missing_atoms = True 1137 self.modelled = True 1138 1139 @property 1140 def cofmass(self) -> np.array: 1141 """ 1142 Return the geometric center of mass for the internal coordinates of 1143 the given Disulfide. Missing atoms are not included. 1144 1145 :return: 3D array for the geometric center of mass 1146 """ 1147 1148 res = self.internal_coords.mean(axis=0) 1149 return res 1150 1151 @property 1152 def coord_cofmass(self) -> np.array: 1153 """ 1154 Return the geometric center of mass for the global coordinates of 1155 the given Disulfide. Missing atoms are not included. 1156 1157 :return: 3D array for the geometric center of mass 1158 """ 1159 1160 res = self.coords.mean(axis=0) 1161 return res 1162 1163 def copy(self): 1164 """ 1165 Copy the Disulfide. 1166 1167 :return: A copy of self. 1168 """ 1169 return copy.deepcopy(self) 1170 1171 def _compute_local_coords(self) -> None: 1172 """ 1173 Compute the internal coordinates for a properly initialized Disulfide Object. 1174 1175 :param self: SS initialized Disulfide object 1176 :returns: None, modifies internal state of the input 1177 """ 1178 1179 turt = Turtle3D("tmp") 1180 # get the coordinates as np.array for Turtle3D use. 1181 cpp = self.c_prev_prox.get_array() 1182 nnp = self.n_next_prox.get_array() 1183 1184 n = self.n_prox.get_array() 1185 ca = self.ca_prox.get_array() 1186 c = self.c_prox.get_array() 1187 cb = self.cb_prox.get_array() 1188 o = self.o_prox.get_array() 1189 sg = self.sg_prox.get_array() 1190 1191 sg2 = self.sg_dist.get_array() 1192 cb2 = self.cb_dist.get_array() 1193 ca2 = self.ca_dist.get_array() 1194 c2 = self.c_dist.get_array() 1195 n2 = self.n_dist.get_array() 1196 o2 = self.o_dist.get_array() 1197 1198 cpd = self.c_prev_dist.get_array() 1199 nnd = self.n_next_dist.get_array() 1200 1201 turt.orient_from_backbone(n, ca, c, cb, ORIENT_SIDECHAIN) 1202 1203 # internal (local) coordinates, stored as Vector objects 1204 # to_local returns np.array objects 1205 1206 self._n_prox = Vector3D(turt.to_local(n)) 1207 self._ca_prox = Vector3D(turt.to_local(ca)) 1208 self._c_prox = Vector3D(turt.to_local(c)) 1209 self._o_prox = Vector3D(turt.to_local(o)) 1210 self._cb_prox = Vector3D(turt.to_local(cb)) 1211 self._sg_prox = Vector3D(turt.to_local(sg)) 1212 1213 self._c_prev_prox = Vector3D(turt.to_local(cpp)) 1214 self._n_next_prox = Vector3D(turt.to_local(nnp)) 1215 self._c_prev_dist = Vector3D(turt.to_local(cpd)) 1216 self._n_next_dist = Vector3D(turt.to_local(nnd)) 1217 1218 self._n_dist = Vector3D(turt.to_local(n2)) 1219 self._ca_dist = Vector3D(turt.to_local(ca2)) 1220 self._c_dist = Vector3D(turt.to_local(c2)) 1221 self._o_dist = Vector3D(turt.to_local(o2)) 1222 self._cb_dist = Vector3D(turt.to_local(cb2)) 1223 self._sg_dist = Vector3D(turt.to_local(sg2)) 1224 1225 def _compute_torsional_energy(self) -> float: 1226 """ 1227 Compute the approximate torsional energy for the Disulfide's 1228 conformation and sets its internal state. 1229 1230 :return: Energy (kcal/mol) 1231 """ 1232 # @TODO find citation for the ss bond energy calculation 1233 1234 def torad(deg): 1235 return np.radians(deg) 1236 1237 chi1 = self.chi1 1238 chi2 = self.chi2 1239 chi3 = self.chi3 1240 chi4 = self.chi4 1241 chi5 = self.chi5 1242 1243 energy = 2.0 * (cos(torad(3.0 * chi1)) + cos(torad(3.0 * chi5))) 1244 energy += cos(torad(3.0 * chi2)) + cos(torad(3.0 * chi4)) 1245 energy += 3.5 * cos(torad(2.0 * chi3)) + 0.6 * cos(torad(3.0 * chi3)) + 10.1 1246 1247 self.energy = energy 1248 return energy 1249 1250 def display( 1251 self, single=True, style="sb", light="auto", shadows=False, winsize=WINSIZE 1252 ) -> None: 1253 """ 1254 Display the Disulfide bond in the specific rendering style. 1255 1256 :param single: Display the bond in a single panel in the specific style. 1257 :param style: Rendering style: One of: 1258 * 'sb' - split bonds 1259 * 'bs' - ball and stick 1260 * 'cpk' - CPK style 1261 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1262 * 'plain' - boring single color 1263 :param light: If True, light background, if False, dark 1264 1265 Example: 1266 >>> import proteusPy as pp 1267 1268 >>> PDB_SS = pp.Load_PDB_SS(verbose=False, subset=True) 1269 >>> ss = PDB_SS[0] 1270 >>> ss.display(style='cpk', light="auto") 1271 >>> ss.screenshot(style='bs', fname='proteus_logo_sb.png') 1272 """ 1273 src = self.pdb_id 1274 enrg = self.energy 1275 1276 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} Å, Sg: {self.sg_distance:.2f} Å Tors: {self.torsion_length:.2f}°" 1277 1278 set_pyvista_theme(light) 1279 fontsize = 8 1280 1281 if single: 1282 _pl = pv.Plotter(window_size=winsize) 1283 _pl.add_title(title=title, font_size=fontsize) 1284 _pl.enable_anti_aliasing("msaa") 1285 1286 self._render( 1287 _pl, 1288 style=style, 1289 ) 1290 _pl.reset_camera() 1291 if shadows: 1292 _pl.enable_shadows() 1293 _pl.show() 1294 1295 else: 1296 pl = pv.Plotter(window_size=winsize, shape=(2, 2)) 1297 pl.subplot(0, 0) 1298 1299 pl.add_title(title=title, font_size=fontsize) 1300 pl.enable_anti_aliasing("msaa") 1301 1302 # pl.add_camera_orientation_widget() 1303 1304 self._render( 1305 pl, 1306 style="cpk", 1307 ) 1308 1309 pl.subplot(0, 1) 1310 pl.add_title(title=title, font_size=fontsize) 1311 1312 self._render( 1313 pl, 1314 style="bs", 1315 ) 1316 1317 pl.subplot(1, 0) 1318 pl.add_title(title=title, font_size=fontsize) 1319 1320 self._render( 1321 pl, 1322 style="sb", 1323 ) 1324 1325 pl.subplot(1, 1) 1326 pl.add_title(title=title, font_size=fontsize) 1327 1328 self._render( 1329 pl, 1330 style="pd", 1331 ) 1332 1333 pl.link_views() 1334 pl.reset_camera() 1335 if shadows: 1336 pl.enable_shadows() 1337 pl.show() 1338 return 1339 1340 @property 1341 def TorsionEnergy(self) -> float: 1342 """ 1343 Return the energy of the Disulfide bond. 1344 """ 1345 return self._compute_torsional_energy() 1346 1347 @property 1348 def TorsionLength(self) -> float: 1349 """ 1350 Return the energy of the Disulfide bond. 1351 """ 1352 return self._compute_torsion_length() 1353 1354 def Distance_neighbors(self, others: DisulfideList, cutoff: float) -> DisulfideList: 1355 """ 1356 Return list of Disulfides whose RMS atomic distance is within 1357 the cutoff (Å) in the others list. 1358 1359 :param others: DisulfideList to search 1360 :param cutoff: Distance cutoff (Å) 1361 :return: DisulfideList within the cutoff 1362 """ 1363 1364 res = [ss.copy() for ss in others if self.Distance_RMS(ss) < cutoff] 1365 return DisulfideList(res, "neighbors") 1366 1367 def Distance_RMS(self, other) -> float: 1368 """ 1369 Calculate the RMS distance between the internal coordinates of self and another Disulfide. 1370 :param other: Comparison Disulfide 1371 :return: RMS distance (Å) 1372 """ 1373 1374 # Get internal coordinates of both objects 1375 ic1 = self.internal_coords 1376 ic2 = other.internal_coords 1377 1378 # Compute the sum of squared differences between corresponding internal coordinates 1379 totsq = sum(math.dist(p1, p2) ** 2 for p1, p2 in zip(ic1, ic2)) 1380 1381 # Compute the mean of the squared distances 1382 totsq /= len(ic1) 1383 1384 # Take the square root of the mean to get the RMS distance 1385 return math.sqrt(totsq) 1386 1387 def Torsion_RMS(self, other) -> float: 1388 """ 1389 Calculate the RMS distance between the dihedral angles of self and another Disulfide. 1390 1391 :param other: Disulfide object to compare against. 1392 :type other: Disulfide 1393 :return: RMS distance in degrees. 1394 :rtype: float 1395 :raises ValueError: If the torsion arrays of self and other are not of equal length. 1396 """ 1397 # Get internal coordinates of both objects 1398 ic1 = self.torsion_array 1399 ic2 = other.torsion_array 1400 1401 # Ensure both torsion arrays have the same length 1402 if len(ic1) != len(ic2): 1403 raise ValueError("Torsion arrays must be of the same length.") 1404 1405 # Compute the total squared difference between corresponding internal coordinates 1406 total_squared_diff = sum( 1407 (angle1 - angle2) ** 2 for angle1, angle2 in zip(ic1, ic2) 1408 ) 1409 mean_squared_diff = total_squared_diff / len(ic1) 1410 1411 # Return the square root of the mean squared difference as the RMS distance 1412 return math.sqrt(mean_squared_diff) 1413 1414 def get_chains(self) -> tuple: 1415 """ 1416 Return the proximal and distal chain IDs for the Disulfide. 1417 1418 :return: tuple (proximal, distal) chain IDs 1419 """ 1420 prox = self.proximal_chain 1421 dist = self.distal_chain 1422 return tuple(prox, dist) 1423 1424 def get_full_id(self) -> tuple: 1425 """ 1426 Return the Disulfide full IDs (Used with BIO.PDB) 1427 1428 :return: Disulfide full IDs 1429 """ 1430 return (self.proximal, self.distal) 1431 1432 @property 1433 def internal_coords(self) -> np.array: 1434 """ 1435 Return the internal coordinates for the Disulfide. 1436 1437 :return: Array containing the coordinates, [16][3]. 1438 """ 1439 return self._internal_coords() 1440 1441 def _internal_coords(self) -> np.array: 1442 """ 1443 Return the internal coordinates for the Disulfide. 1444 If there are missing atoms the extra atoms for the proximal 1445 and distal N and C are set to [0,0,0]. This is needed for the center of 1446 mass calculations, used when rendering. 1447 1448 :return: Array containing the coordinates, [16][3]. 1449 """ 1450 1451 # if we don't have the prior and next atoms we initialize those 1452 # atoms to the origin so as to not effect the center of mass calculations 1453 if self.missing_atoms: 1454 res_array = np.array( 1455 ( 1456 self._n_prox.get_array(), 1457 self._ca_prox.get_array(), 1458 self._c_prox.get_array(), 1459 self._o_prox.get_array(), 1460 self._cb_prox.get_array(), 1461 self._sg_prox.get_array(), 1462 self._n_dist.get_array(), 1463 self._ca_dist.get_array(), 1464 self._c_dist.get_array(), 1465 self._o_dist.get_array(), 1466 self._cb_dist.get_array(), 1467 self._sg_dist.get_array(), 1468 [0, 0, 0], 1469 [0, 0, 0], 1470 [0, 0, 0], 1471 [0, 0, 0], 1472 ) 1473 ) 1474 else: 1475 res_array = np.array( 1476 ( 1477 self._n_prox.get_array(), 1478 self._ca_prox.get_array(), 1479 self._c_prox.get_array(), 1480 self._o_prox.get_array(), 1481 self._cb_prox.get_array(), 1482 self._sg_prox.get_array(), 1483 self._n_dist.get_array(), 1484 self._ca_dist.get_array(), 1485 self._c_dist.get_array(), 1486 self._o_dist.get_array(), 1487 self._cb_dist.get_array(), 1488 self._sg_dist.get_array(), 1489 self._c_prev_prox.get_array(), 1490 self._n_next_prox.get_array(), 1491 self._c_prev_dist.get_array(), 1492 self._n_next_dist.get_array(), 1493 ) 1494 ) 1495 return res_array 1496 1497 @property 1498 def coords(self) -> np.array: 1499 """ 1500 Return the coordinates for the Disulfide as an array. 1501 1502 :return: Array containing the coordinates, [16][3]. 1503 """ 1504 return self._coords() 1505 1506 def _coords(self) -> np.array: 1507 """ 1508 Return the coordinates for the Disulfide as an array. 1509 If there are missing atoms the extra atoms for the proximal 1510 and distal N and C are set to [0,0,0]. This is needed for the center of 1511 mass calculations, used when rendering. 1512 1513 :return: Array containing the coordinates, [16][3]. 1514 """ 1515 1516 # if we don't have the prior and next atoms we initialize those 1517 # atoms to the origin so as to not effect the center of mass calculations 1518 if self.missing_atoms: 1519 res_array = np.array( 1520 ( 1521 self.n_prox.get_array(), 1522 self.ca_prox.get_array(), 1523 self.c_prox.get_array(), 1524 self.o_prox.get_array(), 1525 self.cb_prox.get_array(), 1526 self.sg_prox.get_array(), 1527 self.n_dist.get_array(), 1528 self.ca_dist.get_array(), 1529 self.c_dist.get_array(), 1530 self.o_dist.get_array(), 1531 self.cb_dist.get_array(), 1532 self.sg_dist.get_array(), 1533 [0, 0, 0], 1534 [0, 0, 0], 1535 [0, 0, 0], 1536 [0, 0, 0], 1537 ) 1538 ) 1539 else: 1540 res_array = np.array( 1541 ( 1542 self.n_prox.get_array(), 1543 self.ca_prox.get_array(), 1544 self.c_prox.get_array(), 1545 self.o_prox.get_array(), 1546 self.cb_prox.get_array(), 1547 self.sg_prox.get_array(), 1548 self.n_dist.get_array(), 1549 self.ca_dist.get_array(), 1550 self.c_dist.get_array(), 1551 self.o_dist.get_array(), 1552 self.cb_dist.get_array(), 1553 self.sg_dist.get_array(), 1554 self.c_prev_prox.get_array(), 1555 self.n_next_prox.get_array(), 1556 self.c_prev_dist.get_array(), 1557 self.n_next_dist.get_array(), 1558 ) 1559 ) 1560 return res_array 1561 1562 def internal_coords_res(self, resnumb) -> np.array: 1563 """ 1564 Return the internal coordinates for the Disulfide. Missing atoms are not included. 1565 1566 :return: Array containing the coordinates, [12][3]. 1567 """ 1568 return self._internal_coords_res(resnumb) 1569 1570 def _internal_coords_res(self, resnumb) -> np.array: 1571 """ 1572 Return the internal coordinates for the internal coordinates of 1573 the given Disulfide. Missing atoms are not included. 1574 1575 :param resnumb: Residue number for disulfide 1576 :raises DisulfideConstructionWarning: Warning raised if the residue number is invalid 1577 :return: Array containing the internal coordinates for the disulfide 1578 """ 1579 res_array = np.zeros(shape=(6, 3)) 1580 1581 if resnumb == self.proximal: 1582 res_array = np.array( 1583 ( 1584 self._n_prox.get_array(), 1585 self._ca_prox.get_array(), 1586 self._c_prox.get_array(), 1587 self._o_prox.get_array(), 1588 self._cb_prox.get_array(), 1589 self._sg_prox.get_array(), 1590 ) 1591 ) 1592 return res_array 1593 1594 elif resnumb == self.distal: 1595 res_array = np.array( 1596 ( 1597 self._n_dist.get_array(), 1598 self._ca_dist.get_array(), 1599 self._c_dist.get_array(), 1600 self._o_dist.get_array(), 1601 self._cb_dist.get_array(), 1602 self._sg_dist.get_array(), 1603 ) 1604 ) 1605 return res_array 1606 else: 1607 mess = f"-> Disulfide._internal_coords(): Invalid argument. \ 1608 Unable to find residue: {resnumb} " 1609 raise DisulfideConstructionWarning(mess) 1610 1611 def make_movie( 1612 self, style="sb", fname="ssbond.mp4", verbose=False, steps=360 1613 ) -> None: 1614 """ 1615 Create an animation for ```self``` rotating one revolution about the Y axis, 1616 in the given ```style```, saving to ```filename```. 1617 1618 :param style: Rendering style, defaults to 'sb', one of: 1619 * 'sb' - split bonds 1620 * 'bs' - ball and stick 1621 * 'cpk' - CPK style 1622 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1623 * 'plain' - boring single color 1624 1625 :param fname: Output filename, defaults to ```ssbond.mp4``` 1626 :param verbose: Verbosity, defaults to False 1627 :param steps: Number of steps for one complete rotation, defaults to 360. 1628 """ 1629 1630 # src = self.pdb_id 1631 # name = self.name 1632 # enrg = self.energy 1633 # 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}" 1634 1635 if verbose: 1636 print(f"Rendering animation to {fname}...") 1637 1638 pl = pv.Plotter(window_size=WINSIZE, off_screen=True, theme="document") 1639 pl.open_movie(fname) 1640 path = pl.generate_orbital_path(n_points=steps) 1641 1642 # 1643 # pl.add_title(title=title, font_size=FONTSIZE) 1644 pl.enable_anti_aliasing("msaa") 1645 pl = self._render( 1646 pl, 1647 style=style, 1648 ) 1649 pl.reset_camera() 1650 pl.orbit_on_path(path, write_frames=True) 1651 pl.close() 1652 1653 if verbose: 1654 print(f"Saved mp4 animation to: {fname}") 1655 1656 def spin(self, style="sb", verbose=False, steps=360, theme="auto") -> None: 1657 """ 1658 Spin the object by rotating it one revolution about the Y axis in the given style. 1659 1660 :param style: Rendering style, defaults to 'sb', one of: 1661 * 'sb' - split bonds 1662 * 'bs' - ball and stick 1663 * 'cpk' - CPK style 1664 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1665 * 'plain' - boring single color 1666 1667 :param verbose: Verbosity, defaults to False 1668 :param steps: Number of steps for one complete rotation, defaults to 360. 1669 """ 1670 1671 src = self.pdb_id 1672 enrg = self.energy 1673 1674 title = f"{src}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol, Cα: {self.ca_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 1675 1676 set_pyvista_theme(theme) 1677 1678 if verbose: 1679 _logger.info("Spinning object: %d steps...", steps) 1680 1681 # Create a Plotter instance 1682 pl = pv.Plotter(window_size=WINSIZE, off_screen=False) 1683 pl.add_title(title=title, font_size=FONTSIZE) 1684 1685 # Enable anti-aliasing for smoother rendering 1686 pl.enable_anti_aliasing("msaa") 1687 1688 # Generate an orbital path for spinning 1689 path = pl.generate_orbital_path(n_points=steps) 1690 1691 # Render the object in the specified style 1692 pl = self._render(pl, style=style) 1693 1694 pl.reset_camera() 1695 pl.show(auto_close=False) 1696 1697 # Orbit the camera along the generated path 1698 pl.orbit_on_path(path, write_frames=False, step=1 / steps) 1699 1700 if verbose: 1701 print("Spinning completed.") 1702 1703 def plot( 1704 self, pl, single=True, style="sb", light="True", shadows=False 1705 ) -> pv.Plotter: 1706 """ 1707 Return the pyVista Plotter object for the Disulfide bond in the specific rendering style. 1708 1709 :param single: Display the bond in a single panel in the specific style. 1710 :param style: Rendering style: One of: 1711 * 'sb' - split bonds 1712 * 'bs' - ball and stick 1713 * 'cpk' - CPK style 1714 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1715 * 'plain' - boring single color 1716 :param light: If True, light background, if False, dark 1717 """ 1718 src = self.pdb_id 1719 enrg = self.energy 1720 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}°" 1721 1722 if light: 1723 pv.set_plot_theme("document") 1724 else: 1725 pv.set_plot_theme("dark") 1726 1727 pl.clear() 1728 1729 if single: 1730 pl = pv.Plotter(window_size=WINSIZE) 1731 pl.add_title(title=title, font_size=FONTSIZE) 1732 pl.enable_anti_aliasing("msaa") 1733 # pl.add_camera_orientation_widget() 1734 1735 self._render( 1736 pl, 1737 style=style, 1738 bs_scale=BS_SCALE, 1739 spec=SPECULARITY, 1740 specpow=SPEC_POWER, 1741 ) 1742 pl.reset_camera() 1743 if shadows: 1744 pl.enable_shadows() 1745 else: 1746 pl = pv.Plotter(shape=(2, 2)) 1747 pl.subplot(0, 0) 1748 1749 # pl.add_title(title=title, font_size=FONTSIZE) 1750 pl.enable_anti_aliasing("msaa") 1751 1752 # pl.add_camera_orientation_widget() 1753 1754 self._render( 1755 pl, 1756 style="cpk", 1757 ) 1758 1759 pl.subplot(0, 1) 1760 1761 self._render( 1762 pl, 1763 style="bs", 1764 ) 1765 1766 pl.subplot(1, 0) 1767 1768 self._render( 1769 pl, 1770 style="sb", 1771 ) 1772 1773 pl.subplot(1, 1) 1774 self._render( 1775 pl, 1776 style="pd", 1777 ) 1778 1779 pl.link_views() 1780 pl.reset_camera() 1781 if shadows: 1782 pl.enable_shadows() 1783 return pl 1784 1785 def pprint(self) -> None: 1786 """ 1787 Pretty print general info for the Disulfide 1788 """ 1789 s1 = self.repr_ss_info() 1790 s2 = self.repr_ss_ca_dist() 1791 s2b = self.repr_ss_sg_dist() 1792 s3 = self.repr_ss_conformation() 1793 s4 = self.repr_ss_torsion_length() 1794 res = f"{s1} \n{s3} \n{s2} \n{s2b} \n{s4}>" 1795 print(res) 1796 1797 def pprint_all(self) -> None: 1798 """ 1799 Pretty print all info for the Disulfide 1800 """ 1801 s1 = self.repr_ss_info() + "\n" 1802 s2 = self.repr_ss_coords() 1803 s3 = self.repr_ss_local_coords() 1804 s4 = self.repr_ss_conformation() 1805 s4b = self.repr_phipsi() 1806 s6 = self.repr_ss_ca_dist() 1807 s6b = self.repr_ss_cb_dist() 1808 s6c = self.repr_ss_sg_dist() 1809 s7 = self.repr_ss_torsion_length() 1810 s8 = self.repr_ss_secondary_structure() 1811 1812 res = f"{s1} {s2} {s3} {s4}\n {s4b}\n {s6}\n {s6b}\n {s6c}\n {s7}\n {s8}>" 1813 1814 print(res) 1815 1816 # repr functions. The class is large, so I split it up into sections 1817 def repr_ss_info(self) -> str: 1818 """ 1819 Representation for the Disulfide class 1820 """ 1821 s1 = f"<Disulfide {self.name}, Source: {self.pdb_id}, Resolution: {self.resolution} Å" 1822 return s1 1823 1824 def repr_ss_coords(self) -> str: 1825 """ 1826 Representation for Disulfide coordinates 1827 """ 1828 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" 1829 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" 1830 stot = f"{s2} {s3}" 1831 return stot 1832 1833 def repr_ss_conformation(self) -> str: 1834 """ 1835 Representation for Disulfide conformation 1836 """ 1837 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" 1838 stot = f"{s4}" 1839 return stot 1840 1841 def repr_ss_local_coords(self) -> str: 1842 """ 1843 Representation for the Disulfide internal coordinates. 1844 """ 1845 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" 1846 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" 1847 stot = f"{s2i}{s3i}" 1848 return stot 1849 1850 def repr_ss_residue_ids(self) -> str: 1851 """ 1852 Representation for Disulfide chain IDs 1853 """ 1854 return f"Proximal Residue fullID: <{self.proximal}> Distal Residue fullID: <{self.distal}>" 1855 1856 def repr_ss_ca_dist(self) -> str: 1857 """ 1858 Representation for Disulfide Ca distance 1859 """ 1860 s1 = f"Cα Distance: {self.ca_distance:.2f} Å" 1861 return s1 1862 1863 def repr_ss_cb_dist(self) -> str: 1864 """ 1865 Representation for Disulfide Ca distance 1866 """ 1867 s1 = f"Cβ Distance: {self.cb_distance:.2f} Å" 1868 return s1 1869 1870 def repr_ss_sg_dist(self) -> str: 1871 """ 1872 Representation for Disulfide Ca distance 1873 """ 1874 s1 = f"Sγ Distance: {self.sg_distance:.2f} Å" 1875 return s1 1876 1877 def repr_ss_torsion_length(self) -> str: 1878 """ 1879 Representation for Disulfide torsion length 1880 """ 1881 s1 = f"Torsion length: {self.torsion_length:.2f} deg" 1882 return s1 1883 1884 def repr_ss_secondary_structure(self) -> str: 1885 """ 1886 Representation for Disulfide secondary structure 1887 """ 1888 s1 = f"Proximal secondary: {self.proximal_secondary} Distal secondary: {self.distal_secondary}" 1889 return s1 1890 1891 def repr_phipsi(self) -> str: 1892 """ 1893 Representation for Disulfide phi psi angles 1894 """ 1895 s1 = f"PhiProx: {self.phiprox:.2f}° PsiProx: {self.psiprox:.2f}°, PhiDist: {self.phidist:.2f}° PsiDist: {self.psidist:.2f}°" 1896 return s1 1897 1898 def repr_all(self) -> str: 1899 """ 1900 Return a string representation for all Disulfide information 1901 contained in self. 1902 """ 1903 1904 s1 = self.repr_ss_info() + "\n" 1905 s2 = self.repr_ss_coords() 1906 s3 = self.repr_ss_local_coords() 1907 s4 = self.repr_ss_conformation() 1908 s4b = self.repr_phipsi() 1909 s6 = self.repr_ss_ca_dist() 1910 s8 = self.repr_ss_cb_dist() 1911 s7 = self.repr_ss_torsion_length() 1912 s9 = self.repr_ss_secondary_structure() 1913 1914 res = f"{s1} {s2} {s3} {s4} {s4b} {s6} {s7} {s8} {s9}>" 1915 return res 1916 1917 def repr_compact(self) -> str: 1918 """ 1919 Return a compact representation of the Disulfide object 1920 :return: string 1921 """ 1922 return f"{self.repr_ss_info()} {self.repr_ss_conformation()}" 1923 1924 def repr_conformation(self) -> str: 1925 """ 1926 Return a string representation of the Disulfide object's conformation. 1927 :return: string 1928 """ 1929 return f"{self.repr_ss_conformation()}" 1930 1931 def repr_coords(self) -> str: 1932 """ 1933 Return a string representation of the Disulfide object's coordinates. 1934 :return: string 1935 """ 1936 return f"{self.repr_ss_coords()}" 1937 1938 def repr_internal_coords(self) -> str: 1939 """ 1940 Return a string representation of the Disulfide object's internal coordinaes. 1941 :return: string 1942 """ 1943 return f"{self.repr_ss_local_coords()}" 1944 1945 def repr_chain_ids(self) -> str: 1946 """ 1947 Return a string representation of the Disulfide object's chain ids. 1948 :return: string 1949 """ 1950 return f"{self.repr_ss_residue_ids()}" 1951 1952 @property 1953 def rho(self) -> float: 1954 """ 1955 Return the dihedral angle rho for the Disulfide. 1956 """ 1957 return self._compute_rho() 1958 1959 @rho.setter 1960 def rho(self, value: float): 1961 """ 1962 Set the dihedral angle rho for the Disulfide. 1963 """ 1964 self._rho = value 1965 1966 def _compute_rho(self) -> float: 1967 """ 1968 Compute the dihedral angle rho for a Disulfide object and 1969 sets the internal state of the object. 1970 """ 1971 1972 v1 = self.n_prox - self.ca_prox 1973 v2 = self.c_prox - self.ca_prox 1974 n1 = np.cross(v2.get_array(), v1.get_array()) 1975 1976 v4 = self.n_dist - self.ca_dist 1977 v3 = self.c_dist - self.ca_dist 1978 n2 = np.cross(v4.get_array(), v3.get_array()) 1979 self._rho = calc_dihedral( 1980 Vector3D(n1), self.ca_prox, self.ca_dist, Vector3D(n2) 1981 ) 1982 return self._rho 1983 1984 def reset(self) -> None: 1985 """ 1986 Resets the disulfide object to its initial state. All distances, 1987 angles and positions are reset. The name is unchanged. 1988 """ 1989 self.__init__(self) 1990 1991 def same_chains(self) -> bool: 1992 """ 1993 Function checks if the Disulfide is cross-chain or not. 1994 1995 Returns 1996 ------- 1997 bool \n 1998 True if the proximal and distal residues are on the same chains, 1999 False otherwise. 2000 """ 2001 2002 (prox, dist) = self.get_chains() 2003 return prox == dist 2004 2005 def screenshot( 2006 self, 2007 single=True, 2008 style="sb", 2009 fname="ssbond.png", 2010 verbose=False, 2011 shadows=False, 2012 light="Auto", 2013 ) -> None: 2014 """ 2015 Create and save a screenshot of the Disulfide in the given style 2016 and filename 2017 2018 :param single: Display a single vs panel view, defaults to True 2019 :param style: Rendering style, one of: 2020 * 'sb' - split bonds 2021 * 'bs' - ball and stick 2022 * 'cpk' - CPK style 2023 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2024 * 'plain' - boring single color, 2025 :param fname: output filename,, defaults to 'ssbond.png' 2026 :param verbose: Verbosit, defaults to False 2027 :param shadows: Enable shadows, defaults to False 2028 """ 2029 2030 src = self.pdb_id 2031 enrg = self.energy 2032 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} Å, Sγ: {self.sg_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 2033 2034 set_pyvista_theme(light) 2035 2036 if verbose: 2037 _logger.info("Rendering screenshot to file {fname}") 2038 2039 if single: 2040 pl = pv.Plotter(window_size=WINSIZE, off_screen=False) 2041 pl.add_title(title=title, font_size=FONTSIZE) 2042 pl.enable_anti_aliasing("msaa") 2043 self._render( 2044 pl, 2045 style=style, 2046 ) 2047 pl.reset_camera() 2048 if shadows: 2049 pl.enable_shadows() 2050 2051 pl.show(auto_close=False) # allows for manipulation 2052 # Take the screenshot after ensuring the plotter is still active 2053 try: 2054 pl.screenshot(fname) 2055 except RuntimeError as e: 2056 _logger.error(f"Error saving screenshot: {e}") 2057 2058 else: 2059 pl = pv.Plotter(window_size=WINSIZE, shape=(2, 2), off_screen=False) 2060 pl.subplot(0, 0) 2061 2062 pl.add_title(title=title, font_size=FONTSIZE) 2063 pl.enable_anti_aliasing("msaa") 2064 2065 # pl.add_camera_orientation_widget() 2066 self._render( 2067 pl, 2068 style="cpk", 2069 ) 2070 2071 pl.subplot(0, 1) 2072 pl.add_title(title=title, font_size=FONTSIZE) 2073 self._render( 2074 pl, 2075 style="pd", 2076 ) 2077 2078 pl.subplot(1, 0) 2079 pl.add_title(title=title, font_size=FONTSIZE) 2080 self._render( 2081 pl, 2082 style="bs", 2083 ) 2084 2085 pl.subplot(1, 1) 2086 pl.add_title(title=title, font_size=FONTSIZE) 2087 self._render( 2088 pl, 2089 style="sb", 2090 ) 2091 2092 pl.link_views() 2093 pl.reset_camera() 2094 if shadows: 2095 pl.enable_shadows() 2096 2097 # Take the screenshot after ensuring the plotter is still active 2098 pl.show(auto_close=False) # allows for manipulation 2099 2100 try: 2101 pl.screenshot(fname) 2102 except RuntimeError as e: 2103 _logger.error(f"Error saving screenshot: {e}") 2104 2105 if verbose: 2106 print(f"Screenshot saved as: {fname}") 2107 2108 def save_meshes_as_stl(self, meshes, filename) -> None: 2109 """Save a list of meshes as a single STL file. 2110 2111 Args: 2112 meshes (list): List of pyvista mesh objects to save. 2113 filename (str): Path to save the STL file to. 2114 """ 2115 merged_mesh = pv.UnstructuredGrid() 2116 for mesh in meshes: 2117 merged_mesh += mesh 2118 merged_mesh.save(filename) 2119 2120 def export(self, style="sb", verbose=True, fname="ssbond_plt") -> None: 2121 """ 2122 Create and save a screenshot of the Disulfide in the given style and filename. 2123 2124 :param single: Display a single vs panel view, defaults to True 2125 :param style: Rendering style, one of: 2126 * 'sb' - split bonds 2127 * 'bs' - ball and stick 2128 * 'cpk' - CPK style 2129 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2130 * 'plain' - boring single color, 2131 2132 :param fname: output filename,, defaults to 'ssbond.stl' 2133 :param verbose: Verbosit, defaults to False 2134 """ 2135 2136 if verbose: 2137 print(f"-> screenshot(): Rendering screenshot to file {fname}") 2138 2139 pl = pv.PolyData() 2140 2141 self._plot( 2142 pl, 2143 style=style, 2144 ) 2145 2146 self.save_meshes_as_stl(pl, fname) 2147 2148 return 2149 2150 def set_positions( 2151 self, 2152 n_prox: Vector3D, 2153 ca_prox: Vector3D, 2154 c_prox: Vector3D, 2155 o_prox: Vector3D, 2156 cb_prox: Vector3D, 2157 sg_prox: Vector3D, 2158 n_dist: Vector3D, 2159 ca_dist: Vector3D, 2160 c_dist: Vector3D, 2161 o_dist: Vector3D, 2162 cb_dist: Vector3D, 2163 sg_dist: Vector3D, 2164 c_prev_prox: Vector3D, 2165 n_next_prox: Vector3D, 2166 c_prev_dist: Vector3D, 2167 n_next_dist: Vector3D, 2168 ) -> None: 2169 """ 2170 Set the atomic coordinates for all atoms in the Disulfide object. 2171 2172 :param n_prox: Proximal N position 2173 :param ca_prox: Proximal Cα position 2174 :param c_prox: Proximal C' position 2175 :param o_prox: Proximal O position 2176 :param cb_prox: Proximal Cβ position 2177 :param sg_prox: Proximal Sγ position 2178 :param n_dist: Distal N position 2179 :param ca_dist: Distal Cα position 2180 :param c_dist: Distal C' position 2181 :param o_dist: Distal O position 2182 :param cb_dist: Distal Cβ position 2183 :param sg_dist: Distal Sγ position 2184 :param c_prev_prox: Proximal previous C' 2185 :param n_next_prox: Proximal next N 2186 :param c_prev_dist: Distal previous C' 2187 :param n_next_dist: Distal next N 2188 """ 2189 2190 # deep copy 2191 self.n_prox = n_prox.copy() 2192 self.ca_prox = ca_prox.copy() 2193 self.c_prox = c_prox.copy() 2194 self.o_prox = o_prox.copy() 2195 self.cb_prox = cb_prox.copy() 2196 self.sg_prox = sg_prox.copy() 2197 self.sg_dist = sg_dist.copy() 2198 self.cb_dist = cb_dist.copy() 2199 self.ca_dist = ca_dist.copy() 2200 self.n_dist = n_dist.copy() 2201 self.c_dist = c_dist.copy() 2202 self.o_dist = o_dist.copy() 2203 2204 self.c_prev_prox = c_prev_prox.copy() 2205 self.n_next_prox = n_next_prox.copy() 2206 self.c_prev_dist = c_prev_dist.copy() 2207 self.n_next_dist = n_next_dist.copy() 2208 self._compute_local_coords() 2209 2210 def set_name(self, namestr="Disulfide") -> None: 2211 """ 2212 Set the Disulfide's name. 2213 2214 :param namestr: Name, by default "Disulfide" 2215 """ 2216 self.name = namestr 2217 2218 def set_resnum(self, proximal: int, distal: int) -> None: 2219 """ 2220 Set the proximal and residue numbers for the Disulfide. 2221 2222 :param proximal: Proximal residue number 2223 :param distal: Distal residue number 2224 """ 2225 self.proximal = proximal 2226 self.distal = distal 2227 2228 def _compute_torsion_length(self) -> float: 2229 """ 2230 Compute the 5D Euclidean length of the Disulfide object. Update the disulfide internal state. 2231 2232 :return: Torsion length (Degrees) 2233 """ 2234 # Use numpy array to compute element-wise square 2235 tors2 = np.square(self.torsion_array) 2236 2237 # Compute the sum of squares using numpy's sum function 2238 dist = math.sqrt(np.sum(tors2)) 2239 2240 # Update the internal state 2241 self.torsion_length = dist 2242 2243 return dist 2244 2245 def torsion_distance(self, other) -> float: 2246 """ 2247 Calculate the 5D Euclidean distance between `self` and another Disulfide 2248 object. This is used to compare Disulfide Bond torsion angles to 2249 determine their torsional similarity via a 5-Dimensional Euclidean distance metric. 2250 2251 :param other: Comparison Disulfide 2252 :raises ProteusPyWarning: Warning if `other` is not a Disulfide object 2253 :return: Euclidean distance (Degrees) between `self` and `other`. 2254 """ 2255 2256 # Check length of torsion arrays 2257 if len(self.torsion_array) != 5 or len(other.torsion_array) != 5: 2258 raise ProteusPyWarning( 2259 "--> Torsion_Distance() requires vectors of length 5!" 2260 ) 2261 2262 # Convert to numpy arrays 2263 p1 = np.array(self.torsion_array) 2264 p2 = np.array(other.torsion_array) 2265 2266 # Compute the difference and handle angle wrapping 2267 diff = np.abs(p1 - p2) 2268 diff = np.where(diff > 180, 360 - diff, diff) 2269 2270 # Compute the 5D Euclidean distance using numpy's linalg.norm function 2271 dist = np.linalg.norm(diff) 2272 2273 return dist 2274 2275 def torsion_neighbors(self, others, cutoff) -> DisulfideList: 2276 """ 2277 Return a list of Disulfides within the angular cutoff in the others list. 2278 This routine is used to find Disulfides having the same torsion length 2279 within the others list. This is used to find families of Disulfides with 2280 similar conformations. Assumes self is properly initialized. 2281 2282 *NB* The routine will not distinguish between +/- 2283 dihedral angles. *i.e.* [-60, -60, -90, -60, -60] would have the same 2284 torsion length as [60, 60, 90, 60, 60], two clearly different structures. 2285 2286 :param others: ```DisulfideList``` to search 2287 :param cutoff: Dihedral angle degree cutoff 2288 :return: DisulfideList within the cutoff 2289 2290 Example: 2291 In this example we load the disulfide database subset, find the disulfides with 2292 the lowest and highest energies, and then find the nearest conformational neighbors. 2293 Finally, we display the neighbors overlaid against a common reference frame. 2294 2295 >>> import proteusPy as pp 2296 >>> _theme = pp.set_pyvista_theme("auto") 2297 >>> PDB_SS = pp.Load_PDB_SS(verbose=False, subset=True) 2298 >>> ss_list = pp.DisulfideList([], 'tmp') 2299 2300 We point to the complete list to search for lowest and highest energies. 2301 >>> sslist = PDB_SS.SSList 2302 >>> ssmin_enrg, ssmax_enrg = PDB_SS.SSList.minmax_energy 2303 2304 Make an empty list and find the nearest neighbors within 10 degrees avg RMS in 2305 sidechain dihedral angle space. 2306 2307 >>> low_energy_neighbors = DisulfideList([],'Neighbors') 2308 >>> low_energy_neighbors = ssmin_enrg.torsion_neighbors(sslist, 8) 2309 2310 Display the number found, and then display them overlaid onto their common reference frame. 2311 2312 >>> tot = low_energy_neighbors.length 2313 >>> print(f'Neighbors: {tot}') 2314 Neighbors: 8 2315 >>> low_energy_neighbors.display_overlay(light="auto") 2316 2317 """ 2318 2319 res = [ss for ss in others if self.torsion_distance(ss) <= cutoff] 2320 return DisulfideList(res, "neighbors") 2321 2322 def translate(self, translation_vector: Vector3D): 2323 """Translate the Disulfide object by the given vector.""" 2324 2325 self.n_prox += translation_vector 2326 self.ca_prox += translation_vector 2327 self.c_prox += translation_vector 2328 self.o_prox += translation_vector 2329 self.cb_prox += translation_vector 2330 self.sg_prox += translation_vector 2331 self.sg_dist += translation_vector 2332 self.cb_dist += translation_vector 2333 self.ca_dist += translation_vector 2334 self.n_dist += translation_vector 2335 self.c_dist += translation_vector 2336 self.o_dist += translation_vector 2337 2338 self.c_prev_prox += translation_vector 2339 self.n_next_prox += translation_vector 2340 self.c_prev_dist += translation_vector 2341 self.n_next_dist += translation_vector 2342 self._compute_local_coords()
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 (Å)
- Sγ - Sγ 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.
144 def __init__( 145 self, 146 name: str = "SSBOND", 147 proximal: int = -1, 148 distal: int = -1, 149 proximal_chain: str = "A", 150 distal_chain: str = "A", 151 pdb_id: str = "1egs", 152 quiet: bool = True, 153 torsions: list = None, 154 ) -> None: 155 """ 156 Initialize the class to defined internal values. If torsions are provided, the 157 Disulfide object is built using the torsions and initialized. 158 159 :param name: Disulfide name, by default "SSBOND" 160 :param proximal: Proximal residue number, by default -1 161 :param distal: Distal residue number, by default -1 162 :param proximal_chain: Chain identifier for the proximal residue, by default "A" 163 :param distal_chain: Chain identifier for the distal residue, by default "A" 164 :param pdb_id: PDB identifier, by default "1egs" 165 :param quiet: If True, suppress output, by default True 166 :param torsions: List of torsion angles, by default None 167 """ 168 self.name = name 169 self.proximal = proximal 170 self.distal = distal 171 self.energy = _FLOAT_INIT 172 self.proximal_chain = proximal_chain 173 self.distal_chain = distal_chain 174 self.pdb_id = pdb_id 175 self.quiet = quiet 176 self.proximal_secondary = "Nosecondary" 177 self.distal_secondary = "Nosecondary" 178 self.ca_distance = _FLOAT_INIT 179 self.cb_distance = _FLOAT_INIT 180 self.sg_distance = _FLOAT_INIT 181 self.torsion_array = np.array( 182 (_ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT, _ANG_INIT) 183 ) 184 self.phiprox = _ANG_INIT 185 self.psiprox = _ANG_INIT 186 self.phidist = _ANG_INIT 187 self.psidist = _ANG_INIT 188 189 # global coordinates for the Disulfide, typically as 190 # returned from the PDB file 191 192 self.n_prox = ORIGIN 193 self.ca_prox = ORIGIN 194 self.c_prox = ORIGIN 195 self.o_prox = ORIGIN 196 self.cb_prox = ORIGIN 197 self.sg_prox = ORIGIN 198 self.sg_dist = ORIGIN 199 self.cb_dist = ORIGIN 200 self.ca_dist = ORIGIN 201 self.n_dist = ORIGIN 202 self.c_dist = ORIGIN 203 self.o_dist = ORIGIN 204 205 # set when we can't find previous or next prox or distal 206 # C' or N atoms. 207 self.missing_atoms = False 208 self.modelled = False 209 self.resolution = -1.0 210 211 # need these to calculate backbone dihedral angles 212 self.c_prev_prox = ORIGIN 213 self.n_next_prox = ORIGIN 214 self.c_prev_dist = ORIGIN 215 self.n_next_dist = ORIGIN 216 217 # local coordinates for the Disulfide, computed using the Turtle3D in 218 # Orientation #1. these are generally private. 219 220 self._n_prox = ORIGIN 221 self._ca_prox = ORIGIN 222 self._c_prox = ORIGIN 223 self._o_prox = ORIGIN 224 self._cb_prox = ORIGIN 225 self._sg_prox = ORIGIN 226 self._sg_dist = ORIGIN 227 self._cb_dist = ORIGIN 228 self._ca_dist = ORIGIN 229 self._n_dist = ORIGIN 230 self._c_dist = ORIGIN 231 self._o_dist = ORIGIN 232 233 # need these to calculate backbone dihedral angles 234 self._c_prev_prox = ORIGIN 235 self._n_next_prox = ORIGIN 236 self._c_prev_dist = ORIGIN 237 self._n_next_dist = ORIGIN 238 239 # Dihedral angles for the disulfide bond itself, set to _ANG_INIT 240 self.chi1 = _ANG_INIT 241 self.chi2 = _ANG_INIT 242 self.chi3 = _ANG_INIT 243 self.chi4 = _ANG_INIT 244 self.chi5 = _ANG_INIT 245 self._rho = _ANG_INIT # new dihedral angle: Nprox - Ca_prox - Ca_dist - N_dist 246 247 self.torsion_length = _FLOAT_INIT 248 249 if torsions is not None and len(torsions) == 5: 250 # computes energy, torsion length and rho 251 self.dihedrals = torsions 252 self.build_yourself()
Initialize the class to defined internal values. If torsions are provided, the Disulfide object is built using the torsions and initialized.
Parameters
- name: Disulfide name, by default "SSBOND"
- proximal: Proximal residue number, by default -1
- distal: Distal residue number, by default -1
- proximal_chain: Chain identifier for the proximal residue, by default "A"
- distal_chain: Chain identifier for the distal residue, by default "A"
- pdb_id: PDB identifier, by default "1egs"
- quiet: If True, suppress output, by default True
- torsions: List of torsion angles, by default None
747 @property 748 def binary_class_string(self): 749 """ 750 Return a binary string representation of the disulfide bond class. 751 """ 752 return DisulfideClassManager.class_string_from_dihedral( 753 self.chi1, self.chi2, self.chi3, self.chi4, self.chi5, base=2 754 )
Return a binary string representation of the disulfide bond class.
756 @property 757 def octant_class_string(self): 758 """ 759 Return the octant string representation of the disulfide bond class. 760 """ 761 return DisulfideClassManager.class_string_from_dihedral( 762 self.chi1, self.chi2, self.chi3, self.chi4, self.chi5, base=8 763 )
Return the octant string representation of the disulfide bond class.
765 @property 766 def bond_angle_ideality(self): 767 """ 768 Calculate all bond angles for a disulfide bond and compare them to idealized angles. 769 770 :param np.ndarray atom_coordinates: Array containing coordinates of atoms in the order: 771 N1, CA1, C1, O1, CB1, SG1, N2, CA2, C2, O2, CB2, SG2 772 :return: RMS difference between calculated bond angles and idealized bond angles. 773 :rtype: float 774 """ 775 776 atom_coordinates = self.coords_array 777 verbose = not self.quiet 778 if verbose: 779 _logger.setLevel(logging.INFO) 780 781 idealized_angles = { 782 ("N1", "CA1", "C1"): 111.0, 783 ("N1", "CA1", "CB1"): 108.5, 784 ("CA1", "CB1", "SG1"): 112.8, 785 ("CB1", "SG1", "SG2"): 103.8, # This angle is for the disulfide bond itself 786 ("SG1", "SG2", "CB2"): 103.8, # This angle is for the disulfide bond itself 787 ("SG2", "CB2", "CA2"): 112.8, 788 ("CB2", "CA2", "N2"): 108.5, 789 ("N2", "CA2", "C2"): 111.0, 790 } 791 792 # List of triplets for which we need to calculate bond angles 793 # I am omitting the proximal and distal backbone angle N, Ca, C 794 # to focus on the disulfide bond angles themselves. 795 angle_triplets = [ 796 ("N1", "CA1", "C1"), 797 ("N1", "CA1", "CB1"), 798 ("CA1", "CB1", "SG1"), 799 ("CB1", "SG1", "SG2"), 800 ("SG1", "SG2", "CB2"), 801 ("SG2", "CB2", "CA2"), 802 ("CB2", "CA2", "N2"), 803 ("N2", "CA2", "C2"), 804 ] 805 806 atom_indices = { 807 "N1": 0, 808 "CA1": 1, 809 "C1": 2, 810 "CB1": 4, 811 "SG1": 5, 812 "SG2": 11, 813 "CB2": 10, 814 "CA2": 7, 815 "N2": 6, 816 "C2": 8, 817 } 818 819 calculated_angles = [] 820 for triplet in angle_triplets: 821 a = atom_coordinates[atom_indices[triplet[0]]] 822 b = atom_coordinates[atom_indices[triplet[1]]] 823 c = atom_coordinates[atom_indices[triplet[2]]] 824 ideal = idealized_angles[triplet] 825 try: 826 angle = calculate_bond_angle(a, b, c) 827 except ValueError as e: 828 print(f"Error calculating angle for atoms {triplet}: {e}") 829 return None 830 calculated_angles.append(angle) 831 if verbose: 832 _logger.info( 833 f"Calculated angle for atoms {triplet}: {angle:.2f}, Ideal angle: {ideal:.2f}" 834 ) 835 836 # Convert idealized angles to a list 837 idealized_angles_list = [ 838 idealized_angles[triplet] for triplet in angle_triplets 839 ] 840 841 # Calculate RMS difference 842 rms_diff = rms_difference( 843 np.array(calculated_angles), np.array(idealized_angles_list) 844 ) 845 846 if verbose: 847 _logger.info(f"RMS bond angle deviation:, {rms_diff:.2f}") 848 849 return rms_diff
Calculate all bond angles for a disulfide bond and compare them to idealized angles.
Parameters
- np.ndarray atom_coordinates: Array containing coordinates of atoms in the order: N1, CA1, C1, O1, CB1, SG1, N2, CA2, C2, O2, CB2, SG2
Returns
RMS difference between calculated bond angles and idealized bond angles.
851 @property 852 def bond_length_ideality(self): 853 """ 854 Calculate bond lengths for a disulfide bond and compare them to idealized lengths. 855 856 :param np.ndarray atom_coordinates: Array containing coordinates of atoms in the order: 857 N1, CA1, C1, O1, CB1, SG1, N2, CA2, C2, O2, CB2, SG2 858 :return: RMS difference between calculated bond lengths and idealized bond lengths. 859 :rtype: float 860 """ 861 862 atom_coordinates = self.coords_array 863 verbose = not self.quiet 864 if verbose: 865 _logger.setLevel(logging.INFO) 866 867 idealized_bonds = { 868 ("N1", "CA1"): 1.46, 869 ("CA1", "C1"): 1.52, 870 ("CA1", "CB1"): 1.52, 871 ("CB1", "SG1"): 1.86, 872 ("SG1", "SG2"): 2.044, # This angle is for the disulfide bond itself 873 ("SG2", "CB2"): 1.86, 874 ("CB2", "CA2"): 1.52, 875 ("CA2", "C2"): 1.52, 876 ("N2", "CA2"): 1.46, 877 } 878 879 # List of triplets for which we need to calculate bond angles 880 # I am omitting the proximal and distal backbone angle N, Ca, C 881 # to focus on the disulfide bond angles themselves. 882 distance_pairs = [ 883 ("N1", "CA1"), 884 ("CA1", "C1"), 885 ("CA1", "CB1"), 886 ("CB1", "SG1"), 887 ("SG1", "SG2"), # This angle is for the disulfide bond itself 888 ("SG2", "CB2"), 889 ("CB2", "CA2"), 890 ("CA2", "C2"), 891 ("N2", "CA2"), 892 ] 893 894 atom_indices = { 895 "N1": 0, 896 "CA1": 1, 897 "C1": 2, 898 "CB1": 4, 899 "SG1": 5, 900 "SG2": 11, 901 "CB2": 10, 902 "CA2": 7, 903 "N2": 6, 904 "C2": 8, 905 } 906 907 calculated_distances = [] 908 for pair in distance_pairs: 909 a = atom_coordinates[atom_indices[pair[0]]] 910 b = atom_coordinates[atom_indices[pair[1]]] 911 ideal = idealized_bonds[pair] 912 try: 913 distance = math.dist(a, b) 914 except ValueError as e: 915 _logger.error(f"Error calculating bond length for atoms {pair}: {e}") 916 return None 917 calculated_distances.append(distance) 918 if verbose: 919 _logger.info( 920 f"Calculated distance for atoms {pair}: {distance:.2f}A, Ideal distance: {ideal:.2f}A" 921 ) 922 923 # Convert idealized distances to a list 924 idealized_distance_list = [idealized_bonds[pair] for pair in distance_pairs] 925 926 # Calculate RMS difference 927 rms_diff = rms_difference( 928 np.array(calculated_distances), np.array(idealized_distance_list) 929 ) 930 931 if verbose: 932 _logger.info( 933 f"RMS distance deviation from ideality for SS atoms: {rms_diff:.2f}" 934 ) 935 936 # Reset logger level 937 _logger.setLevel(logging.WARNING) 938 939 return rms_diff
Calculate bond lengths for a disulfide bond and compare them to idealized lengths.
Parameters
- np.ndarray atom_coordinates: Array containing coordinates of atoms in the order: N1, CA1, C1, O1, CB1, SG1, N2, CA2, C2, O2, CB2, SG2
Returns
RMS difference between calculated bond lengths and idealized bond lengths.
941 @property 942 def internal_coords_array(self): 943 """ 944 Return an array of internal coordinates for the disulfide bond. 945 946 This function collects the coordinates of the backbone atoms involved in the 947 disulfide bond and returns them as a numpy array. 948 949 :param self: The instance of the Disulfide class. 950 :type self: Disulfide 951 :return: A numpy array containing the coordinates of the atoms. 952 :rtype: np.ndarray 953 """ 954 coords = [] 955 coords.append(self._n_prox.get_array()) 956 coords.append(self._ca_prox.get_array()) 957 coords.append(self._c_prox.get_array()) 958 coords.append(self._o_prox.get_array()) 959 coords.append(self._cb_prox.get_array()) 960 coords.append(self._sg_prox.get_array()) 961 coords.append(self._n_dist.get_array()) 962 coords.append(self._ca_dist.get_array()) 963 coords.append(self._c_dist.get_array()) 964 coords.append(self._o_dist.get_array()) 965 coords.append(self._cb_dist.get_array()) 966 coords.append(self._sg_dist.get_array()) 967 968 return np.array(coords)
Return an array of internal coordinates for the disulfide bond.
This function collects the coordinates of the backbone atoms involved in the disulfide bond and returns them as a numpy array.
Parameters
- self: The instance of the Disulfide class.
Returns
A numpy array containing the coordinates of the atoms.
970 @property 971 def coords_array(self): 972 """ 973 Return an array of coordinates for the disulfide bond. 974 975 This function collects the coordinates of backbone atoms involved in the 976 disulfide bond and returns them as a numpy array. 977 978 :param self: The instance of the Disulfide class. 979 :type self: Disulfide 980 :return: A numpy array containing the coordinates of the atoms. 981 :rtype: np.ndarray 982 """ 983 coords = [] 984 coords.append(self.n_prox.get_array()) 985 coords.append(self.ca_prox.get_array()) 986 coords.append(self.c_prox.get_array()) 987 coords.append(self.o_prox.get_array()) 988 coords.append(self.cb_prox.get_array()) 989 coords.append(self.sg_prox.get_array()) 990 coords.append(self.n_dist.get_array()) 991 coords.append(self.ca_dist.get_array()) 992 coords.append(self.c_dist.get_array()) 993 coords.append(self.o_dist.get_array()) 994 coords.append(self.cb_dist.get_array()) 995 coords.append(self.sg_dist.get_array()) 996 997 return np.array(coords)
Return an array of coordinates for the disulfide bond.
This function collects the coordinates of backbone atoms involved in the disulfide bond and returns them as a numpy array.
Parameters
- self: The instance of the Disulfide class.
Returns
A numpy array containing the coordinates of the atoms.
999 @property 1000 def dihedrals(self) -> list: 1001 """ 1002 Return a list containing the dihedral angles for the disulfide. 1003 1004 """ 1005 return [self.chi1, self.chi2, self.chi3, self.chi4, self.chi5]
Return a list containing the dihedral angles for the disulfide.
1025 def bounding_box(self) -> np.array: 1026 """ 1027 Return the bounding box array for the given disulfide. 1028 1029 :return: np.array 1030 Array containing the min, max for X, Y, and Z respectively. 1031 Does not currently take the atom's radius into account. 1032 """ 1033 coords = self.internal_coords 1034 1035 xmin, ymin, zmin = coords.min(axis=0) 1036 xmax, ymax, zmax = coords.max(axis=0) 1037 1038 res = np.array([[xmin, xmax], [ymin, ymax], [zmin, zmax]]) 1039 1040 return res
Return the bounding box array for the given disulfide.
Returns
np.array Array containing the min, max for X, Y, and Z respectively. Does not currently take the atom's radius into account.
1042 def build_yourself(self) -> None: 1043 """ 1044 Build a model Disulfide based its internal dihedral state 1045 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1046 Cb, with N on left), builds disulfide, and updates the object's internal 1047 coordinates. It also adds the distal protein backbone, 1048 and computes the disulfide conformational energy. 1049 """ 1050 self.build_model(self.chi1, self.chi2, self.chi3, self.chi4, self.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.
1052 def build_model( 1053 self, chi1: float, chi2: float, chi3: float, chi4: float, chi5: float 1054 ) -> None: 1055 """ 1056 Build a model Disulfide based on the input dihedral angles. 1057 Routine assumes turtle is in orientation #1 (at Ca, headed toward 1058 Cb, with N on left), builds disulfide, and updates the object's internal 1059 coordinates. It also adds the distal protein backbone, 1060 and computes the disulfide conformational energy. 1061 1062 :param chi1: Chi1 (degrees) 1063 :param chi2: Chi2 (degrees) 1064 :param chi3: Chi3 (degrees) 1065 :param chi4: Chi4 (degrees) 1066 :param chi5: Chi5 (degrees) 1067 1068 Example: 1069 >>> import proteusPy as pp 1070 >>> modss = pp.Disulfide('model') 1071 >>> modss.build_model(-60, -60, -90, -60, -60) 1072 >>> modss.display(style='sb', light="auto") 1073 """ 1074 1075 self.dihedrals = [chi1, chi2, chi3, chi4, chi5] 1076 self.proximal = 1 1077 self.distal = 2 1078 1079 tmp = Turtle3D("tmp") 1080 tmp.Orientation = 1 1081 1082 n = ORIGIN 1083 ca = ORIGIN 1084 cb = ORIGIN 1085 c = ORIGIN 1086 1087 self.ca_prox = tmp._position 1088 tmp.schain_to_bbone() 1089 n, ca, cb, c = build_residue(tmp) 1090 1091 self.n_prox = n 1092 self.ca_prox = ca 1093 self.c_prox = c 1094 self.cb_prox = cb 1095 1096 tmp.bbone_to_schain() 1097 tmp.move(1.53) 1098 tmp.roll(self.chi1) 1099 tmp.yaw(112.8) 1100 self.cb_prox = Vector3D(tmp._position) 1101 1102 tmp.move(1.86) 1103 tmp.roll(self.chi2) 1104 tmp.yaw(103.8) 1105 self.sg_prox = Vector3D(tmp._position) 1106 1107 tmp.move(2.044) 1108 tmp.roll(self.chi3) 1109 tmp.yaw(103.8) 1110 self.sg_dist = Vector3D(tmp._position) 1111 1112 tmp.move(1.86) 1113 tmp.roll(self.chi4) 1114 tmp.yaw(112.8) 1115 self.cb_dist = Vector3D(tmp._position) 1116 1117 tmp.move(1.53) 1118 tmp.roll(self.chi5) 1119 tmp.pitch(180.0) 1120 1121 tmp.schain_to_bbone() 1122 1123 n, ca, cb, c = build_residue(tmp) 1124 1125 self.n_dist = n 1126 self.ca_dist = ca 1127 self.c_dist = c 1128 self._compute_torsional_energy() 1129 self._compute_local_coords() 1130 self._compute_torsion_length() 1131 self._compute_rho() 1132 self.ca_distance = distance3d(self.ca_prox, self.ca_dist) 1133 self.cb_distance = distance3d(self.cb_prox, self.cb_dist) 1134 self.sg_distance = distance3d(self.sg_prox, self.sg_dist) 1135 self.torsion_array = np.array([chi1, chi2, chi3, chi4, chi5]) 1136 self.missing_atoms = True 1137 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:
>>> import proteusPy as pp
>>> modss = pp.Disulfide('model')
>>> modss.build_model(-60, -60, -90, -60, -60)
>>> modss.display(style='sb', light="auto")
1139 @property 1140 def cofmass(self) -> np.array: 1141 """ 1142 Return the geometric center of mass for the internal coordinates of 1143 the given Disulfide. Missing atoms are not included. 1144 1145 :return: 3D array for the geometric center of mass 1146 """ 1147 1148 res = self.internal_coords.mean(axis=0) 1149 return res
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
1151 @property 1152 def coord_cofmass(self) -> np.array: 1153 """ 1154 Return the geometric center of mass for the global coordinates of 1155 the given Disulfide. Missing atoms are not included. 1156 1157 :return: 3D array for the geometric center of mass 1158 """ 1159 1160 res = self.coords.mean(axis=0) 1161 return res
Return the geometric center of mass for the global coordinates of the given Disulfide. Missing atoms are not included.
Returns
3D array for the geometric center of mass
1163 def copy(self): 1164 """ 1165 Copy the Disulfide. 1166 1167 :return: A copy of self. 1168 """ 1169 return copy.deepcopy(self)
Copy the Disulfide.
Returns
A copy of self.
1250 def display( 1251 self, single=True, style="sb", light="auto", shadows=False, winsize=WINSIZE 1252 ) -> None: 1253 """ 1254 Display the Disulfide bond in the specific rendering style. 1255 1256 :param single: Display the bond in a single panel in the specific style. 1257 :param style: Rendering style: One of: 1258 * 'sb' - split bonds 1259 * 'bs' - ball and stick 1260 * 'cpk' - CPK style 1261 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1262 * 'plain' - boring single color 1263 :param light: If True, light background, if False, dark 1264 1265 Example: 1266 >>> import proteusPy as pp 1267 1268 >>> PDB_SS = pp.Load_PDB_SS(verbose=False, subset=True) 1269 >>> ss = PDB_SS[0] 1270 >>> ss.display(style='cpk', light="auto") 1271 >>> ss.screenshot(style='bs', fname='proteus_logo_sb.png') 1272 """ 1273 src = self.pdb_id 1274 enrg = self.energy 1275 1276 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} Å, Sg: {self.sg_distance:.2f} Å Tors: {self.torsion_length:.2f}°" 1277 1278 set_pyvista_theme(light) 1279 fontsize = 8 1280 1281 if single: 1282 _pl = pv.Plotter(window_size=winsize) 1283 _pl.add_title(title=title, font_size=fontsize) 1284 _pl.enable_anti_aliasing("msaa") 1285 1286 self._render( 1287 _pl, 1288 style=style, 1289 ) 1290 _pl.reset_camera() 1291 if shadows: 1292 _pl.enable_shadows() 1293 _pl.show() 1294 1295 else: 1296 pl = pv.Plotter(window_size=winsize, shape=(2, 2)) 1297 pl.subplot(0, 0) 1298 1299 pl.add_title(title=title, font_size=fontsize) 1300 pl.enable_anti_aliasing("msaa") 1301 1302 # pl.add_camera_orientation_widget() 1303 1304 self._render( 1305 pl, 1306 style="cpk", 1307 ) 1308 1309 pl.subplot(0, 1) 1310 pl.add_title(title=title, font_size=fontsize) 1311 1312 self._render( 1313 pl, 1314 style="bs", 1315 ) 1316 1317 pl.subplot(1, 0) 1318 pl.add_title(title=title, font_size=fontsize) 1319 1320 self._render( 1321 pl, 1322 style="sb", 1323 ) 1324 1325 pl.subplot(1, 1) 1326 pl.add_title(title=title, font_size=fontsize) 1327 1328 self._render( 1329 pl, 1330 style="pd", 1331 ) 1332 1333 pl.link_views() 1334 pl.reset_camera() 1335 if shadows: 1336 pl.enable_shadows() 1337 pl.show() 1338 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 as pp
>>> PDB_SS = pp.Load_PDB_SS(verbose=False, subset=True)
>>> ss = PDB_SS[0]
>>> ss.display(style='cpk', light="auto")
>>> ss.screenshot(style='bs', fname='proteus_logo_sb.png')
1340 @property 1341 def TorsionEnergy(self) -> float: 1342 """ 1343 Return the energy of the Disulfide bond. 1344 """ 1345 return self._compute_torsional_energy()
Return the energy of the Disulfide bond.
1347 @property 1348 def TorsionLength(self) -> float: 1349 """ 1350 Return the energy of the Disulfide bond. 1351 """ 1352 return self._compute_torsion_length()
Return the energy of the Disulfide bond.
1354 def Distance_neighbors(self, others: DisulfideList, cutoff: float) -> DisulfideList: 1355 """ 1356 Return list of Disulfides whose RMS atomic distance is within 1357 the cutoff (Å) in the others list. 1358 1359 :param others: DisulfideList to search 1360 :param cutoff: Distance cutoff (Å) 1361 :return: DisulfideList within the cutoff 1362 """ 1363 1364 res = [ss.copy() for ss in others if self.Distance_RMS(ss) < cutoff] 1365 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
1367 def Distance_RMS(self, other) -> float: 1368 """ 1369 Calculate the RMS distance between the internal coordinates of self and another Disulfide. 1370 :param other: Comparison Disulfide 1371 :return: RMS distance (Å) 1372 """ 1373 1374 # Get internal coordinates of both objects 1375 ic1 = self.internal_coords 1376 ic2 = other.internal_coords 1377 1378 # Compute the sum of squared differences between corresponding internal coordinates 1379 totsq = sum(math.dist(p1, p2) ** 2 for p1, p2 in zip(ic1, ic2)) 1380 1381 # Compute the mean of the squared distances 1382 totsq /= len(ic1) 1383 1384 # Take the square root of the mean to get the RMS distance 1385 return math.sqrt(totsq)
Calculate the RMS distance between the internal coordinates of self and another Disulfide.
Parameters
- other: Comparison Disulfide
Returns
RMS distance (Å)
1387 def Torsion_RMS(self, other) -> float: 1388 """ 1389 Calculate the RMS distance between the dihedral angles of self and another Disulfide. 1390 1391 :param other: Disulfide object to compare against. 1392 :type other: Disulfide 1393 :return: RMS distance in degrees. 1394 :rtype: float 1395 :raises ValueError: If the torsion arrays of self and other are not of equal length. 1396 """ 1397 # Get internal coordinates of both objects 1398 ic1 = self.torsion_array 1399 ic2 = other.torsion_array 1400 1401 # Ensure both torsion arrays have the same length 1402 if len(ic1) != len(ic2): 1403 raise ValueError("Torsion arrays must be of the same length.") 1404 1405 # Compute the total squared difference between corresponding internal coordinates 1406 total_squared_diff = sum( 1407 (angle1 - angle2) ** 2 for angle1, angle2 in zip(ic1, ic2) 1408 ) 1409 mean_squared_diff = total_squared_diff / len(ic1) 1410 1411 # Return the square root of the mean squared difference as the RMS distance 1412 return math.sqrt(mean_squared_diff)
Calculate the RMS distance between the dihedral angles of self and another Disulfide.
Parameters
- other: Disulfide object to compare against.
Returns
RMS distance in degrees.
Raises
- ValueError: If the torsion arrays of self and other are not of equal length.
1414 def get_chains(self) -> tuple: 1415 """ 1416 Return the proximal and distal chain IDs for the Disulfide. 1417 1418 :return: tuple (proximal, distal) chain IDs 1419 """ 1420 prox = self.proximal_chain 1421 dist = self.distal_chain 1422 return tuple(prox, dist)
Return the proximal and distal chain IDs for the Disulfide.
Returns
tuple (proximal, distal) chain IDs
1424 def get_full_id(self) -> tuple: 1425 """ 1426 Return the Disulfide full IDs (Used with BIO.PDB) 1427 1428 :return: Disulfide full IDs 1429 """ 1430 return (self.proximal, self.distal)
Return the Disulfide full IDs (Used with BIO.PDB)
Returns
Disulfide full IDs
1432 @property 1433 def internal_coords(self) -> np.array: 1434 """ 1435 Return the internal coordinates for the Disulfide. 1436 1437 :return: Array containing the coordinates, [16][3]. 1438 """ 1439 return self._internal_coords()
Return the internal coordinates for the Disulfide.
Returns
Array containing the coordinates, [16][3].
1497 @property 1498 def coords(self) -> np.array: 1499 """ 1500 Return the coordinates for the Disulfide as an array. 1501 1502 :return: Array containing the coordinates, [16][3]. 1503 """ 1504 return self._coords()
Return the coordinates for the Disulfide as an array.
Returns
Array containing the coordinates, [16][3].
1562 def internal_coords_res(self, resnumb) -> np.array: 1563 """ 1564 Return the internal coordinates for the Disulfide. Missing atoms are not included. 1565 1566 :return: Array containing the coordinates, [12][3]. 1567 """ 1568 return self._internal_coords_res(resnumb)
Return the internal coordinates for the Disulfide. Missing atoms are not included.
Returns
Array containing the coordinates, [12][3].
1611 def make_movie( 1612 self, style="sb", fname="ssbond.mp4", verbose=False, steps=360 1613 ) -> None: 1614 """ 1615 Create an animation for ```self``` rotating one revolution about the Y axis, 1616 in the given ```style```, saving to ```filename```. 1617 1618 :param style: Rendering style, defaults to 'sb', one of: 1619 * 'sb' - split bonds 1620 * 'bs' - ball and stick 1621 * 'cpk' - CPK style 1622 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1623 * 'plain' - boring single color 1624 1625 :param fname: Output filename, defaults to ```ssbond.mp4``` 1626 :param verbose: Verbosity, defaults to False 1627 :param steps: Number of steps for one complete rotation, defaults to 360. 1628 """ 1629 1630 # src = self.pdb_id 1631 # name = self.name 1632 # enrg = self.energy 1633 # 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}" 1634 1635 if verbose: 1636 print(f"Rendering animation to {fname}...") 1637 1638 pl = pv.Plotter(window_size=WINSIZE, off_screen=True, theme="document") 1639 pl.open_movie(fname) 1640 path = pl.generate_orbital_path(n_points=steps) 1641 1642 # 1643 # pl.add_title(title=title, font_size=FONTSIZE) 1644 pl.enable_anti_aliasing("msaa") 1645 pl = self._render( 1646 pl, 1647 style=style, 1648 ) 1649 pl.reset_camera() 1650 pl.orbit_on_path(path, write_frames=True) 1651 pl.close() 1652 1653 if verbose: 1654 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.
1656 def spin(self, style="sb", verbose=False, steps=360, theme="auto") -> None: 1657 """ 1658 Spin the object by rotating it one revolution about the Y axis in the given style. 1659 1660 :param style: Rendering style, defaults to 'sb', one of: 1661 * 'sb' - split bonds 1662 * 'bs' - ball and stick 1663 * 'cpk' - CPK style 1664 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1665 * 'plain' - boring single color 1666 1667 :param verbose: Verbosity, defaults to False 1668 :param steps: Number of steps for one complete rotation, defaults to 360. 1669 """ 1670 1671 src = self.pdb_id 1672 enrg = self.energy 1673 1674 title = f"{src}: {self.proximal}{self.proximal_chain}-{self.distal}{self.distal_chain}: {enrg:.2f} kcal/mol, Cα: {self.ca_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 1675 1676 set_pyvista_theme(theme) 1677 1678 if verbose: 1679 _logger.info("Spinning object: %d steps...", steps) 1680 1681 # Create a Plotter instance 1682 pl = pv.Plotter(window_size=WINSIZE, off_screen=False) 1683 pl.add_title(title=title, font_size=FONTSIZE) 1684 1685 # Enable anti-aliasing for smoother rendering 1686 pl.enable_anti_aliasing("msaa") 1687 1688 # Generate an orbital path for spinning 1689 path = pl.generate_orbital_path(n_points=steps) 1690 1691 # Render the object in the specified style 1692 pl = self._render(pl, style=style) 1693 1694 pl.reset_camera() 1695 pl.show(auto_close=False) 1696 1697 # Orbit the camera along the generated path 1698 pl.orbit_on_path(path, write_frames=False, step=1 / steps) 1699 1700 if verbose: 1701 print("Spinning completed.")
Spin the object by rotating it one revolution about the Y axis in the given style.
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
verbose: Verbosity, defaults to False
- steps: Number of steps for one complete rotation, defaults to 360.
1703 def plot( 1704 self, pl, single=True, style="sb", light="True", shadows=False 1705 ) -> pv.Plotter: 1706 """ 1707 Return the pyVista Plotter object for the Disulfide bond in the specific rendering style. 1708 1709 :param single: Display the bond in a single panel in the specific style. 1710 :param style: Rendering style: One of: 1711 * 'sb' - split bonds 1712 * 'bs' - ball and stick 1713 * 'cpk' - CPK style 1714 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 1715 * 'plain' - boring single color 1716 :param light: If True, light background, if False, dark 1717 """ 1718 src = self.pdb_id 1719 enrg = self.energy 1720 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}°" 1721 1722 if light: 1723 pv.set_plot_theme("document") 1724 else: 1725 pv.set_plot_theme("dark") 1726 1727 pl.clear() 1728 1729 if single: 1730 pl = pv.Plotter(window_size=WINSIZE) 1731 pl.add_title(title=title, font_size=FONTSIZE) 1732 pl.enable_anti_aliasing("msaa") 1733 # pl.add_camera_orientation_widget() 1734 1735 self._render( 1736 pl, 1737 style=style, 1738 bs_scale=BS_SCALE, 1739 spec=SPECULARITY, 1740 specpow=SPEC_POWER, 1741 ) 1742 pl.reset_camera() 1743 if shadows: 1744 pl.enable_shadows() 1745 else: 1746 pl = pv.Plotter(shape=(2, 2)) 1747 pl.subplot(0, 0) 1748 1749 # pl.add_title(title=title, font_size=FONTSIZE) 1750 pl.enable_anti_aliasing("msaa") 1751 1752 # pl.add_camera_orientation_widget() 1753 1754 self._render( 1755 pl, 1756 style="cpk", 1757 ) 1758 1759 pl.subplot(0, 1) 1760 1761 self._render( 1762 pl, 1763 style="bs", 1764 ) 1765 1766 pl.subplot(1, 0) 1767 1768 self._render( 1769 pl, 1770 style="sb", 1771 ) 1772 1773 pl.subplot(1, 1) 1774 self._render( 1775 pl, 1776 style="pd", 1777 ) 1778 1779 pl.link_views() 1780 pl.reset_camera() 1781 if shadows: 1782 pl.enable_shadows() 1783 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
1785 def pprint(self) -> None: 1786 """ 1787 Pretty print general info for the Disulfide 1788 """ 1789 s1 = self.repr_ss_info() 1790 s2 = self.repr_ss_ca_dist() 1791 s2b = self.repr_ss_sg_dist() 1792 s3 = self.repr_ss_conformation() 1793 s4 = self.repr_ss_torsion_length() 1794 res = f"{s1} \n{s3} \n{s2} \n{s2b} \n{s4}>" 1795 print(res)
Pretty print general info for the Disulfide
1797 def pprint_all(self) -> None: 1798 """ 1799 Pretty print all info for the Disulfide 1800 """ 1801 s1 = self.repr_ss_info() + "\n" 1802 s2 = self.repr_ss_coords() 1803 s3 = self.repr_ss_local_coords() 1804 s4 = self.repr_ss_conformation() 1805 s4b = self.repr_phipsi() 1806 s6 = self.repr_ss_ca_dist() 1807 s6b = self.repr_ss_cb_dist() 1808 s6c = self.repr_ss_sg_dist() 1809 s7 = self.repr_ss_torsion_length() 1810 s8 = self.repr_ss_secondary_structure() 1811 1812 res = f"{s1} {s2} {s3} {s4}\n {s4b}\n {s6}\n {s6b}\n {s6c}\n {s7}\n {s8}>" 1813 1814 print(res)
Pretty print all info for the Disulfide
1817 def repr_ss_info(self) -> str: 1818 """ 1819 Representation for the Disulfide class 1820 """ 1821 s1 = f"<Disulfide {self.name}, Source: {self.pdb_id}, Resolution: {self.resolution} Å" 1822 return s1
Representation for the Disulfide class
1824 def repr_ss_coords(self) -> str: 1825 """ 1826 Representation for Disulfide coordinates 1827 """ 1828 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" 1829 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" 1830 stot = f"{s2} {s3}" 1831 return stot
Representation for Disulfide coordinates
1833 def repr_ss_conformation(self) -> str: 1834 """ 1835 Representation for Disulfide conformation 1836 """ 1837 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" 1838 stot = f"{s4}" 1839 return stot
Representation for Disulfide conformation
1841 def repr_ss_local_coords(self) -> str: 1842 """ 1843 Representation for the Disulfide internal coordinates. 1844 """ 1845 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" 1846 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" 1847 stot = f"{s2i}{s3i}" 1848 return stot
Representation for the Disulfide internal coordinates.
1850 def repr_ss_residue_ids(self) -> str: 1851 """ 1852 Representation for Disulfide chain IDs 1853 """ 1854 return f"Proximal Residue fullID: <{self.proximal}> Distal Residue fullID: <{self.distal}>"
Representation for Disulfide chain IDs
1856 def repr_ss_ca_dist(self) -> str: 1857 """ 1858 Representation for Disulfide Ca distance 1859 """ 1860 s1 = f"Cα Distance: {self.ca_distance:.2f} Å" 1861 return s1
Representation for Disulfide Ca distance
1863 def repr_ss_cb_dist(self) -> str: 1864 """ 1865 Representation for Disulfide Ca distance 1866 """ 1867 s1 = f"Cβ Distance: {self.cb_distance:.2f} Å" 1868 return s1
Representation for Disulfide Ca distance
1870 def repr_ss_sg_dist(self) -> str: 1871 """ 1872 Representation for Disulfide Ca distance 1873 """ 1874 s1 = f"Sγ Distance: {self.sg_distance:.2f} Å" 1875 return s1
Representation for Disulfide Ca distance
1877 def repr_ss_torsion_length(self) -> str: 1878 """ 1879 Representation for Disulfide torsion length 1880 """ 1881 s1 = f"Torsion length: {self.torsion_length:.2f} deg" 1882 return s1
Representation for Disulfide torsion length
1884 def repr_ss_secondary_structure(self) -> str: 1885 """ 1886 Representation for Disulfide secondary structure 1887 """ 1888 s1 = f"Proximal secondary: {self.proximal_secondary} Distal secondary: {self.distal_secondary}" 1889 return s1
Representation for Disulfide secondary structure
1891 def repr_phipsi(self) -> str: 1892 """ 1893 Representation for Disulfide phi psi angles 1894 """ 1895 s1 = f"PhiProx: {self.phiprox:.2f}° PsiProx: {self.psiprox:.2f}°, PhiDist: {self.phidist:.2f}° PsiDist: {self.psidist:.2f}°" 1896 return s1
Representation for Disulfide phi psi angles
1898 def repr_all(self) -> str: 1899 """ 1900 Return a string representation for all Disulfide information 1901 contained in self. 1902 """ 1903 1904 s1 = self.repr_ss_info() + "\n" 1905 s2 = self.repr_ss_coords() 1906 s3 = self.repr_ss_local_coords() 1907 s4 = self.repr_ss_conformation() 1908 s4b = self.repr_phipsi() 1909 s6 = self.repr_ss_ca_dist() 1910 s8 = self.repr_ss_cb_dist() 1911 s7 = self.repr_ss_torsion_length() 1912 s9 = self.repr_ss_secondary_structure() 1913 1914 res = f"{s1} {s2} {s3} {s4} {s4b} {s6} {s7} {s8} {s9}>" 1915 return res
Return a string representation for all Disulfide information contained in self.
1917 def repr_compact(self) -> str: 1918 """ 1919 Return a compact representation of the Disulfide object 1920 :return: string 1921 """ 1922 return f"{self.repr_ss_info()} {self.repr_ss_conformation()}"
Return a compact representation of the Disulfide object
Returns
string
1924 def repr_conformation(self) -> str: 1925 """ 1926 Return a string representation of the Disulfide object's conformation. 1927 :return: string 1928 """ 1929 return f"{self.repr_ss_conformation()}"
Return a string representation of the Disulfide object's conformation.
Returns
string
1931 def repr_coords(self) -> str: 1932 """ 1933 Return a string representation of the Disulfide object's coordinates. 1934 :return: string 1935 """ 1936 return f"{self.repr_ss_coords()}"
Return a string representation of the Disulfide object's coordinates.
Returns
string
1938 def repr_internal_coords(self) -> str: 1939 """ 1940 Return a string representation of the Disulfide object's internal coordinaes. 1941 :return: string 1942 """ 1943 return f"{self.repr_ss_local_coords()}"
Return a string representation of the Disulfide object's internal coordinaes.
Returns
string
1945 def repr_chain_ids(self) -> str: 1946 """ 1947 Return a string representation of the Disulfide object's chain ids. 1948 :return: string 1949 """ 1950 return f"{self.repr_ss_residue_ids()}"
Return a string representation of the Disulfide object's chain ids.
Returns
string
1952 @property 1953 def rho(self) -> float: 1954 """ 1955 Return the dihedral angle rho for the Disulfide. 1956 """ 1957 return self._compute_rho()
Return the dihedral angle rho for the Disulfide.
1984 def reset(self) -> None: 1985 """ 1986 Resets the disulfide object to its initial state. All distances, 1987 angles and positions are reset. The name is unchanged. 1988 """ 1989 self.__init__(self)
Resets the disulfide object to its initial state. All distances, angles and positions are reset. The name is unchanged.
1991 def same_chains(self) -> bool: 1992 """ 1993 Function checks if the Disulfide is cross-chain or not. 1994 1995 Returns 1996 ------- 1997 bool \n 1998 True if the proximal and distal residues are on the same chains, 1999 False otherwise. 2000 """ 2001 2002 (prox, dist) = self.get_chains() 2003 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.
2005 def screenshot( 2006 self, 2007 single=True, 2008 style="sb", 2009 fname="ssbond.png", 2010 verbose=False, 2011 shadows=False, 2012 light="Auto", 2013 ) -> None: 2014 """ 2015 Create and save a screenshot of the Disulfide in the given style 2016 and filename 2017 2018 :param single: Display a single vs panel view, defaults to True 2019 :param style: Rendering style, one of: 2020 * 'sb' - split bonds 2021 * 'bs' - ball and stick 2022 * 'cpk' - CPK style 2023 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2024 * 'plain' - boring single color, 2025 :param fname: output filename,, defaults to 'ssbond.png' 2026 :param verbose: Verbosit, defaults to False 2027 :param shadows: Enable shadows, defaults to False 2028 """ 2029 2030 src = self.pdb_id 2031 enrg = self.energy 2032 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} Å, Sγ: {self.sg_distance:.2f} Å, Tors: {self.torsion_length:.2f}" 2033 2034 set_pyvista_theme(light) 2035 2036 if verbose: 2037 _logger.info("Rendering screenshot to file {fname}") 2038 2039 if single: 2040 pl = pv.Plotter(window_size=WINSIZE, off_screen=False) 2041 pl.add_title(title=title, font_size=FONTSIZE) 2042 pl.enable_anti_aliasing("msaa") 2043 self._render( 2044 pl, 2045 style=style, 2046 ) 2047 pl.reset_camera() 2048 if shadows: 2049 pl.enable_shadows() 2050 2051 pl.show(auto_close=False) # allows for manipulation 2052 # Take the screenshot after ensuring the plotter is still active 2053 try: 2054 pl.screenshot(fname) 2055 except RuntimeError as e: 2056 _logger.error(f"Error saving screenshot: {e}") 2057 2058 else: 2059 pl = pv.Plotter(window_size=WINSIZE, shape=(2, 2), off_screen=False) 2060 pl.subplot(0, 0) 2061 2062 pl.add_title(title=title, font_size=FONTSIZE) 2063 pl.enable_anti_aliasing("msaa") 2064 2065 # pl.add_camera_orientation_widget() 2066 self._render( 2067 pl, 2068 style="cpk", 2069 ) 2070 2071 pl.subplot(0, 1) 2072 pl.add_title(title=title, font_size=FONTSIZE) 2073 self._render( 2074 pl, 2075 style="pd", 2076 ) 2077 2078 pl.subplot(1, 0) 2079 pl.add_title(title=title, font_size=FONTSIZE) 2080 self._render( 2081 pl, 2082 style="bs", 2083 ) 2084 2085 pl.subplot(1, 1) 2086 pl.add_title(title=title, font_size=FONTSIZE) 2087 self._render( 2088 pl, 2089 style="sb", 2090 ) 2091 2092 pl.link_views() 2093 pl.reset_camera() 2094 if shadows: 2095 pl.enable_shadows() 2096 2097 # Take the screenshot after ensuring the plotter is still active 2098 pl.show(auto_close=False) # allows for manipulation 2099 2100 try: 2101 pl.screenshot(fname) 2102 except RuntimeError as e: 2103 _logger.error(f"Error saving screenshot: {e}") 2104 2105 if verbose: 2106 print(f"Screenshot saved as: {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
2108 def save_meshes_as_stl(self, meshes, filename) -> None: 2109 """Save a list of meshes as a single STL file. 2110 2111 Args: 2112 meshes (list): List of pyvista mesh objects to save. 2113 filename (str): Path to save the STL file to. 2114 """ 2115 merged_mesh = pv.UnstructuredGrid() 2116 for mesh in meshes: 2117 merged_mesh += mesh 2118 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.
2120 def export(self, style="sb", verbose=True, fname="ssbond_plt") -> None: 2121 """ 2122 Create and save a screenshot of the Disulfide in the given style and filename. 2123 2124 :param single: Display a single vs panel view, defaults to True 2125 :param style: Rendering style, one of: 2126 * 'sb' - split bonds 2127 * 'bs' - ball and stick 2128 * 'cpk' - CPK style 2129 * 'pd' - Proximal/Distal style - Red=proximal, Green=Distal 2130 * 'plain' - boring single color, 2131 2132 :param fname: output filename,, defaults to 'ssbond.stl' 2133 :param verbose: Verbosit, defaults to False 2134 """ 2135 2136 if verbose: 2137 print(f"-> screenshot(): Rendering screenshot to file {fname}") 2138 2139 pl = pv.PolyData() 2140 2141 self._plot( 2142 pl, 2143 style=style, 2144 ) 2145 2146 self.save_meshes_as_stl(pl, fname) 2147 2148 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
2150 def set_positions( 2151 self, 2152 n_prox: Vector3D, 2153 ca_prox: Vector3D, 2154 c_prox: Vector3D, 2155 o_prox: Vector3D, 2156 cb_prox: Vector3D, 2157 sg_prox: Vector3D, 2158 n_dist: Vector3D, 2159 ca_dist: Vector3D, 2160 c_dist: Vector3D, 2161 o_dist: Vector3D, 2162 cb_dist: Vector3D, 2163 sg_dist: Vector3D, 2164 c_prev_prox: Vector3D, 2165 n_next_prox: Vector3D, 2166 c_prev_dist: Vector3D, 2167 n_next_dist: Vector3D, 2168 ) -> None: 2169 """ 2170 Set the atomic coordinates for all atoms in the Disulfide object. 2171 2172 :param n_prox: Proximal N position 2173 :param ca_prox: Proximal Cα position 2174 :param c_prox: Proximal C' position 2175 :param o_prox: Proximal O position 2176 :param cb_prox: Proximal Cβ position 2177 :param sg_prox: Proximal Sγ position 2178 :param n_dist: Distal N position 2179 :param ca_dist: Distal Cα position 2180 :param c_dist: Distal C' position 2181 :param o_dist: Distal O position 2182 :param cb_dist: Distal Cβ position 2183 :param sg_dist: Distal Sγ position 2184 :param c_prev_prox: Proximal previous C' 2185 :param n_next_prox: Proximal next N 2186 :param c_prev_dist: Distal previous C' 2187 :param n_next_dist: Distal next N 2188 """ 2189 2190 # deep copy 2191 self.n_prox = n_prox.copy() 2192 self.ca_prox = ca_prox.copy() 2193 self.c_prox = c_prox.copy() 2194 self.o_prox = o_prox.copy() 2195 self.cb_prox = cb_prox.copy() 2196 self.sg_prox = sg_prox.copy() 2197 self.sg_dist = sg_dist.copy() 2198 self.cb_dist = cb_dist.copy() 2199 self.ca_dist = ca_dist.copy() 2200 self.n_dist = n_dist.copy() 2201 self.c_dist = c_dist.copy() 2202 self.o_dist = o_dist.copy() 2203 2204 self.c_prev_prox = c_prev_prox.copy() 2205 self.n_next_prox = n_next_prox.copy() 2206 self.c_prev_dist = c_prev_dist.copy() 2207 self.n_next_dist = n_next_dist.copy() 2208 self._compute_local_coords()
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
2210 def set_name(self, namestr="Disulfide") -> None: 2211 """ 2212 Set the Disulfide's name. 2213 2214 :param namestr: Name, by default "Disulfide" 2215 """ 2216 self.name = namestr
Set the Disulfide's name.
Parameters
- namestr: Name, by default "Disulfide"
2218 def set_resnum(self, proximal: int, distal: int) -> None: 2219 """ 2220 Set the proximal and residue numbers for the Disulfide. 2221 2222 :param proximal: Proximal residue number 2223 :param distal: Distal residue number 2224 """ 2225 self.proximal = proximal 2226 self.distal = distal
Set the proximal and residue numbers for the Disulfide.
Parameters
- proximal: Proximal residue number
- distal: Distal residue number
2245 def torsion_distance(self, other) -> float: 2246 """ 2247 Calculate the 5D Euclidean distance between `self` and another Disulfide 2248 object. This is used to compare Disulfide Bond torsion angles to 2249 determine their torsional similarity via a 5-Dimensional Euclidean distance metric. 2250 2251 :param other: Comparison Disulfide 2252 :raises ProteusPyWarning: Warning if `other` is not a Disulfide object 2253 :return: Euclidean distance (Degrees) between `self` and `other`. 2254 """ 2255 2256 # Check length of torsion arrays 2257 if len(self.torsion_array) != 5 or len(other.torsion_array) != 5: 2258 raise ProteusPyWarning( 2259 "--> Torsion_Distance() requires vectors of length 5!" 2260 ) 2261 2262 # Convert to numpy arrays 2263 p1 = np.array(self.torsion_array) 2264 p2 = np.array(other.torsion_array) 2265 2266 # Compute the difference and handle angle wrapping 2267 diff = np.abs(p1 - p2) 2268 diff = np.where(diff > 180, 360 - diff, diff) 2269 2270 # Compute the 5D Euclidean distance using numpy's linalg.norm function 2271 dist = np.linalg.norm(diff) 2272 2273 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
.
2275 def torsion_neighbors(self, others, cutoff) -> DisulfideList: 2276 """ 2277 Return a list of Disulfides within the angular cutoff in the others list. 2278 This routine is used to find Disulfides having the same torsion length 2279 within the others list. This is used to find families of Disulfides with 2280 similar conformations. Assumes self is properly initialized. 2281 2282 *NB* The routine will not distinguish between +/- 2283 dihedral angles. *i.e.* [-60, -60, -90, -60, -60] would have the same 2284 torsion length as [60, 60, 90, 60, 60], two clearly different structures. 2285 2286 :param others: ```DisulfideList``` to search 2287 :param cutoff: Dihedral angle degree cutoff 2288 :return: DisulfideList within the cutoff 2289 2290 Example: 2291 In this example we load the disulfide database subset, find the disulfides with 2292 the lowest and highest energies, and then find the nearest conformational neighbors. 2293 Finally, we display the neighbors overlaid against a common reference frame. 2294 2295 >>> import proteusPy as pp 2296 >>> _theme = pp.set_pyvista_theme("auto") 2297 >>> PDB_SS = pp.Load_PDB_SS(verbose=False, subset=True) 2298 >>> ss_list = pp.DisulfideList([], 'tmp') 2299 2300 We point to the complete list to search for lowest and highest energies. 2301 >>> sslist = PDB_SS.SSList 2302 >>> ssmin_enrg, ssmax_enrg = PDB_SS.SSList.minmax_energy 2303 2304 Make an empty list and find the nearest neighbors within 10 degrees avg RMS in 2305 sidechain dihedral angle space. 2306 2307 >>> low_energy_neighbors = DisulfideList([],'Neighbors') 2308 >>> low_energy_neighbors = ssmin_enrg.torsion_neighbors(sslist, 8) 2309 2310 Display the number found, and then display them overlaid onto their common reference frame. 2311 2312 >>> tot = low_energy_neighbors.length 2313 >>> print(f'Neighbors: {tot}') 2314 Neighbors: 8 2315 >>> low_energy_neighbors.display_overlay(light="auto") 2316 2317 """ 2318 2319 res = [ss for ss in others if self.torsion_distance(ss) <= cutoff] 2320 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.
>>> import proteusPy as pp
>>> _theme = pp.set_pyvista_theme("auto")
>>> PDB_SS = pp.Load_PDB_SS(verbose=False, subset=True)
>>> ss_list = pp.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, 8)
Display the number found, and then display them overlaid onto their common reference frame.
>>> tot = low_energy_neighbors.length
>>> print(f'Neighbors: {tot}')
Neighbors: 8
>>> low_energy_neighbors.display_overlay(light="auto")
2322 def translate(self, translation_vector: Vector3D): 2323 """Translate the Disulfide object by the given vector.""" 2324 2325 self.n_prox += translation_vector 2326 self.ca_prox += translation_vector 2327 self.c_prox += translation_vector 2328 self.o_prox += translation_vector 2329 self.cb_prox += translation_vector 2330 self.sg_prox += translation_vector 2331 self.sg_dist += translation_vector 2332 self.cb_dist += translation_vector 2333 self.ca_dist += translation_vector 2334 self.n_dist += translation_vector 2335 self.c_dist += translation_vector 2336 self.o_dist += translation_vector 2337 2338 self.c_prev_prox += translation_vector 2339 self.n_next_prox += translation_vector 2340 self.c_prev_dist += translation_vector 2341 self.n_next_dist += translation_vector 2342 self._compute_local_coords()
Translate the Disulfide object by the given vector.
2348def disulfide_energy_function(x: list) -> float: 2349 """ 2350 Compute the approximate torsional energy (kcal/mpl) for the input dihedral angles. 2351 2352 :param x: A list of dihedral angles: [chi1, chi2, chi3, chi4, chi5] 2353 :return: Energy in kcal/mol 2354 2355 Example: 2356 >>> from proteusPy import disulfide_energy_function 2357 >>> dihed = [-60.0, -60.0, -90.0, -60.0, -90.0] 2358 >>> res = disulfide_energy_function(dihed) 2359 >>> float(res) 2360 2.5999999999999996 2361 """ 2362 2363 chi1, chi2, chi3, chi4, chi5 = x 2364 energy = 2.0 * (np.cos(np.deg2rad(3.0 * chi1)) + np.cos(np.deg2rad(3.0 * chi5))) 2365 energy += np.cos(np.deg2rad(3.0 * chi2)) + np.cos(np.deg2rad(3.0 * chi4)) 2366 energy += ( 2367 3.5 * np.cos(np.deg2rad(2.0 * chi3)) 2368 + 0.6 * np.cos(np.deg2rad(3.0 * chi3)) 2369 + 10.1 2370 ) 2371 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 import disulfide_energy_function
>>> dihed = [-60.0, -60.0, -90.0, -60.0, -90.0]
>>> res = disulfide_energy_function(dihed)
>>> float(res)
2.5999999999999996
2374def minimize_ss_energy(inputSS: Disulfide) -> Disulfide: 2375 """ 2376 Minimizes the energy of a Disulfide object using the Nelder-Mead optimization method. 2377 2378 Parameters: 2379 inputSS (Disulfide): The Disulfide object to be minimized. 2380 2381 Returns: 2382 Disulfide: The minimized Disulfide object. 2383 2384 """ 2385 2386 initial_guess = inputSS.torsion_array 2387 result = minimize(disulfide_energy_function, initial_guess, method="Nelder-Mead") 2388 minimum_conformation = result.x 2389 modelled_min = Disulfide("minimized", minimum_conformation) 2390 # modelled_min.dihedrals = minimum_conformation 2391 # modelled_min.build_yourself() 2392 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.
2395def Initialize_Disulfide_From_Coords( 2396 ssbond_atom_data, 2397 pdb_id, 2398 proximal_chain_id, 2399 distal_chain_id, 2400 proximal, 2401 distal, 2402 resolution, 2403 proximal_secondary, 2404 distal_secondary, 2405 verbose=False, 2406 quiet=True, 2407 dbg=False, 2408) -> Disulfide: 2409 """ 2410 Initialize a new Disulfide object with atomic coordinates from 2411 the proximal and distal coordinates, typically taken from a PDB file. 2412 This routine is primarily used internally when building the compressed 2413 database. 2414 2415 :param ssbond_atom_data: Dictionary containing atomic data for the disulfide bond. 2416 :type ssbond_atom_data: dict 2417 :param pdb_id: PDB identifier for the structure. 2418 :type pdb_id: str 2419 :param proximal_chain_id: Chain identifier for the proximal residue. 2420 :type proximal_chain_id: str 2421 :param distal_chain_id: Chain identifier for the distal residue. 2422 :type distal_chain_id: str 2423 :param proximal: Residue number for the proximal residue. 2424 :type proximal: int 2425 :param distal: Residue number for the distal residue. 2426 :type distal: int 2427 :param resolution: Structure resolution. 2428 :type resolution: float 2429 :param verbose: If True, enables verbose logging. Defaults to False. 2430 :type verbose: bool, optional 2431 :param quiet: If True, suppresses logging output. Defaults to True. 2432 :type quiet: bool, optional 2433 :param dbg: If True, enables debug mode. Defaults to False. 2434 :type dbg: bool, optional 2435 :return: An instance of the Disulfide class initialized with the provided coordinates. 2436 :rtype: Disulfide 2437 :raises DisulfideConstructionWarning: Raised when the disulfide bond is not parsed correctly. 2438 2439 """ 2440 2441 ssbond_name = f"{pdb_id}_{proximal}{proximal_chain_id}_{distal}{distal_chain_id}" 2442 new_ss = Disulfide(ssbond_name) 2443 2444 new_ss.pdb_id = pdb_id 2445 new_ss.resolution = resolution 2446 new_ss.proximal_secondary = proximal_secondary 2447 new_ss.distal_secondary = distal_secondary 2448 prox_atom_list = [] 2449 dist_atom_list = [] 2450 2451 if quiet: 2452 _logger.setLevel(logging.ERROR) 2453 logging.getLogger().setLevel(logging.CRITICAL) 2454 2455 # set the objects proximal and distal values 2456 new_ss.set_resnum(proximal, distal) 2457 2458 if resolution is not None: 2459 new_ss.resolution = resolution 2460 else: 2461 new_ss.resolution = -1.0 2462 2463 new_ss.proximal_chain = proximal_chain_id 2464 new_ss.distal_chain = distal_chain_id 2465 2466 # restore loggins 2467 if quiet: 2468 _logger.setLevel(logging.ERROR) 2469 logging.getLogger().setLevel(logging.CRITICAL) ## may want to be CRITICAL 2470 2471 # Get the coordinates for the proximal and distal residues as vectors 2472 # so we can do math on them later. Trap errors here to avoid problems 2473 # with missing residues or atoms. 2474 2475 # proximal residue 2476 2477 try: 2478 prox_atom_list = get_residue_atoms_coordinates( 2479 ssbond_atom_data, proximal_chain_id, proximal 2480 ) 2481 2482 n1 = prox_atom_list[0] 2483 ca1 = prox_atom_list[1] 2484 c1 = prox_atom_list[2] 2485 o1 = prox_atom_list[3] 2486 cb1 = prox_atom_list[4] 2487 sg1 = prox_atom_list[5] 2488 2489 except KeyError: 2490 # i'm torn on this. there are a lot of missing coordinates, so is 2491 # it worth the trouble to note them? I think so. 2492 _logger.error(f"Invalid/missing coordinates for: {id}, proximal: {proximal}") 2493 return None 2494 2495 # distal residue 2496 try: 2497 dist_atom_list = get_residue_atoms_coordinates( 2498 ssbond_atom_data, distal_chain_id, distal 2499 ) 2500 n2 = dist_atom_list[0] 2501 ca2 = dist_atom_list[1] 2502 c2 = dist_atom_list[2] 2503 o2 = dist_atom_list[3] 2504 cb2 = dist_atom_list[4] 2505 sg2 = dist_atom_list[5] 2506 2507 except KeyError: 2508 _logger.error(f"Invalid/missing coordinates for: {id}, distal: {distal}") 2509 return False 2510 2511 # previous residue and next residue - optional, used for phi, psi calculations 2512 prevprox_atom_list = get_phipsi_atoms_coordinates( 2513 ssbond_atom_data, proximal_chain_id, "proximal-1" 2514 ) 2515 2516 nextprox_atom_list = get_phipsi_atoms_coordinates( 2517 ssbond_atom_data, proximal_chain_id, "proximal+1" 2518 ) 2519 2520 prevdist_atom_list = get_phipsi_atoms_coordinates( 2521 ssbond_atom_data, distal_chain_id, "distal-1" 2522 ) 2523 2524 nextdist_atom_list = get_phipsi_atoms_coordinates( 2525 ssbond_atom_data, distal_chain_id, "distal+1" 2526 ) 2527 2528 if len(prevprox_atom_list) != 0: 2529 cprev_prox = prevprox_atom_list[1] 2530 new_ss.phiprox = calc_dihedral(cprev_prox, n1, ca1, c1) 2531 2532 else: 2533 cprev_prox = Vector3D(-1.0, -1.0, -1.0) 2534 new_ss.missing_atoms = True 2535 if verbose: 2536 _logger.warning( 2537 f"Missing Proximal coords for: {id} {proximal}-1. SS: {proximal}-{distal}, phi/psi not computed." 2538 ) 2539 2540 if len(prevdist_atom_list) != 0: 2541 # list is N, C 2542 cprev_dist = prevdist_atom_list[1] 2543 new_ss.phidist = calc_dihedral(cprev_dist, n2, ca2, c2) 2544 else: 2545 cprev_dist = nnext_dist = Vector3D(-1.0, -1.0, -1.0) 2546 new_ss.missing_atoms = True 2547 if verbose: 2548 _logger.warning( 2549 f"Missing Distal coords for: {id} {distal}-1). S:S {proximal}-{distal}, phi/psi not computed." 2550 ) 2551 2552 if len(nextprox_atom_list) != 0: 2553 nnext_prox = nextprox_atom_list[0] 2554 new_ss.psiprox = calc_dihedral(n1, ca1, c1, nnext_prox) 2555 else: 2556 nnext_prox = Vector3D(-1.0, -1.0, -1.0) 2557 new_ss.missing_atoms = True 2558 _logger.warning( 2559 f"Missing Proximal coords for: {id} {proximal}+1). SS: {proximal}-{distal}, phi/psi not computed." 2560 ) 2561 2562 if len(nextdist_atom_list) != 0: 2563 nnext_dist = nextdist_atom_list[0] 2564 new_ss.psidist = calc_dihedral(n2, ca2, c2, nnext_dist) 2565 else: 2566 nnext_dist = Vector3D(-1.0, -1.0, -1.0) 2567 new_ss.missing_atoms = True 2568 _logger.warning( 2569 f"Missing Distal coords for: {id} {distal}+1). SS: {proximal}-{distal}, phi/psi not computed." 2570 ) 2571 2572 # update the positions and conformation 2573 new_ss.set_positions( 2574 n1, 2575 ca1, 2576 c1, 2577 o1, 2578 cb1, 2579 sg1, 2580 n2, 2581 ca2, 2582 c2, 2583 o2, 2584 cb2, 2585 sg2, 2586 cprev_prox, 2587 nnext_prox, 2588 cprev_dist, 2589 nnext_dist, 2590 ) 2591 2592 # calculate and set the disulfide dihedral angles 2593 new_ss.chi1 = calc_dihedral(n1, ca1, cb1, sg1) 2594 new_ss.chi2 = calc_dihedral(ca1, cb1, sg1, sg2) 2595 new_ss.chi3 = calc_dihedral(cb1, sg1, sg2, cb2) 2596 new_ss.chi4 = calc_dihedral(sg1, sg2, cb2, ca2) 2597 new_ss.chi5 = calc_dihedral(sg2, cb2, ca2, n2) 2598 new_ss.ca_distance = distance3d(new_ss.ca_prox, new_ss.ca_dist) 2599 new_ss.cb_distance = distance3d(new_ss.cb_prox, new_ss.cb_dist) 2600 new_ss.sg_distance = distance3d(new_ss.sg_prox, new_ss.sg_dist) 2601 2602 new_ss.torsion_array = np.array( 2603 (new_ss.chi1, new_ss.chi2, new_ss.chi3, new_ss.chi4, new_ss.chi5) 2604 ) 2605 new_ss._compute_torsion_length() 2606 2607 # calculate and set the SS bond torsional energy 2608 new_ss._compute_torsional_energy() 2609 2610 # compute and set the local coordinates 2611 new_ss._compute_local_coords() 2612 2613 # compute rho 2614 new_ss._compute_rho() 2615 2616 # turn warnings back on 2617 if quiet: 2618 _logger.setLevel(logging.ERROR) 2619 2620 if verbose: 2621 _logger.info(f"Disulfide {ssbond_name} initialized.") 2622 2623 return new_ss
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
- ssbond_atom_data: Dictionary containing atomic data for the disulfide bond.
- pdb_id: PDB identifier for the structure.
- proximal_chain_id: Chain identifier for the proximal residue.
- distal_chain_id: Chain identifier for the distal residue.
- proximal: Residue number for the proximal residue.
- distal: Residue number for the distal residue.
- resolution: Structure resolution.
- verbose: If True, enables verbose logging. Defaults to False.
- quiet: If True, suppresses logging output. Defaults to True.
- dbg: If True, enables debug mode. Defaults to False.
Returns
An instance of the Disulfide class initialized with the provided coordinates.
Raises
- DisulfideConstructionWarning: Raised when the disulfide bond is not parsed correctly.