Package csb :: Package bio :: Package structure
[frames] | no frames]

Source Code for Package csb.bio.structure

   1  """ 
   2  3D and secondary structure APIs. 
   3   
   4  This module defines some of the most fundamental abstractions in the library: 
   5  L{Structure}, L{Chain}, L{Residue} and L{Atom}. Instances of these objects may 
   6  exist independently and that is perfectly fine, but usually they are part of a 
   7  Composite aggregation. The root node in this Composite is a L{Structure} (or 
   8  L{Ensemble}). L{Structure}s are composed of L{Chain}s, and each L{Chain} is a 
   9  collection of L{Residue}s. The leaf node is L{Atom}.  
  10   
  11  All of these objects implement the base L{AbstractEntity} interface. Therefore, 
  12  every node in the Composite can be transformed: 
  13       
  14      >>> r, t = [rotation matrix], [translation vector] 
  15      >>> entity.transform(r, t) 
  16       
  17  and it knows its immediate children: 
  18   
  19      >>> entity.items 
  20      <iterator>    # over all immediate child entities 
  21       
  22  If you want to traverse the complete Composite tree, starting at arbitrary level, 
  23  and down to the lowest level, use one of the L{CompositeEntityIterator}s. Or just 
  24  call L{AbstractEntity.components}: 
  25   
  26      >>> entity.components() 
  27      <iterator>   # over all descendants, of any type, at any level 
  28      >>> entity.components(klass=Residue) 
  29      <iterator>   # over all Residue descendants 
  30       
  31  Some of the inner objects in this hierarchy behave just like dictionaries 
  32  (but are not): 
  33   
  34      >>> structure.chains['A']       # access chain A by ID 
  35      <Chain A: Protein> 
  36      >>> structure['A']              # the same 
  37      <Chain A: Protein> 
  38      >>> residue.atoms['CS']           
  39      <Atom: CA>                      # access an atom by its name 
  40      >>> residue.atoms['CS']           
  41      <Atom: CA>                      # the same 
  42           
  43  Others behave like list collections: 
  44   
  45      >>> chain.residues[10]               # 1-based access to the residues in the chain 
  46      <ProteinResidue [10]: PRO 10> 
  47      >>> chain[10]                        # 0-based, list-like access 
  48      <ProteinResidue [11]: GLY 11> 
  49       
  50  Step-wise building of L{Ensemble}s, L{Chain}s and L{Residue}s is supported through 
  51  a number of C{append} methods, for example: 
  52   
  53      >>> residue = ProteinResidue(401, ProteinAlphabet.ALA) 
  54      >>> s.chains['A'].residues.append(residue) 
  55       
  56  See L{EnsembleModelsCollection}, L{StructureChainsTable}, L{ChainResiduesCollection} 
  57  and L{ResidueAtomsTable} for more details. 
  58   
  59  Some other objects in this module of potential interest are the self-explanatory 
  60  L{SecondaryStructure} and L{TorsionAngles}.      
  61  """ 
  62   
  63  import os 
  64  import re 
  65  import copy 
  66  import math 
  67  import numpy 
  68   
  69  import csb.io 
  70  import csb.core 
  71  import csb.numeric 
  72  import csb.bio.utils 
  73   
  74  from abc import ABCMeta, abstractmethod, abstractproperty 
  75   
  76  from csb.bio.sequence import SequenceTypes, SequenceAlphabets, AlignmentTypes 
77 78 79 -class AngleUnits(csb.core.enum):
80 """ 81 Torsion angle unit types 82 """ 83 Degrees='deg'; Radians='rad'
84
85 -class SecStructures(csb.core.enum):
86 """ 87 Secondary structure types 88 """ 89 Helix='H'; Strand='E'; Coil='C'; Turn='T'; Bend='S'; 90 Helix3='G'; PiHelix='I'; BetaBridge='B'; Gap='-'
91
92 -class ChemElements(csb.core.enum):
93 """ 94 Periodic table elements 95 """ 96 H=1; He=2; Li=3; Be=4; B=5; C=6; N=7; O=8; F=9; Ne=10; Na=11; Mg=12; Al=13; Si=14; P=15; 97 S=16; Cl=17; Ar=18; K=19; Ca=20; Sc=21; Ti=22; V=23; Cr=24; Mn=25; Fe=26; Co=27; Ni=28; 98 Cu=29; Zn=30; Ga=31; Ge=32; As=33; Se=34; Br=35; Kr=36; Rb=37; Sr=38; Y=39; Zr=40; Nb=41; 99 Mo=42; Tc=43; Ru=44; Rh=45; Pd=46; Ag=47; Cd=48; In=49; Sn=50; Sb=51; Te=52; I=53; Xe=54; 100 Cs=55; Ba=56; Hf=72; Ta=73; W=74; Re=75; Os=76; Ir=77; Pt=78; Au=79; Hg=80; Tl=81; Pb=82; 101 Bi=83; Po=84; At=85; Rn=86; Fr=87; Ra=88; Rf=104; Db=105; Sg=106; Bh=107; Hs=108; Mt=109; 102 Ds=110; Rg=111; La=57; Ce=58; Pr=59; Nd=60; Pm=61; Sm=62; Eu=63; Gd=64; Tb=65; Dy=66; 103 Ho=67; Er=68; Tm=69; Yb=70; Lu=71; Ac=89; Th=90; Pa=91; U=92; Np=93; Pu=94; Am=95; Cm=96; 104 Bk=97; Cf=98; Es=99; Fm=100; Md=101; No=102; Lr=103; x=-1
105
106 107 -class Broken3DStructureError(ValueError):
108 pass
109
110 -class Missing3DStructureError(Broken3DStructureError):
111 pass
112
113 -class InvalidOperation(Exception):
114 pass
115
116 -class EntityNotFoundError(csb.core.ItemNotFoundError):
117 pass
118
119 -class ChainNotFoundError(EntityNotFoundError):
120 pass
121
122 -class AtomNotFoundError(EntityNotFoundError):
123 pass
124
125 -class EntityIndexError(csb.core.CollectionIndexError):
126 pass
127
128 -class DuplicateModelIDError(csb.core.DuplicateKeyError):
129 pass
130
131 -class DuplicateChainIDError(csb.core.DuplicateKeyError):
132 pass
133
134 -class DuplicateResidueIDError(csb.core.DuplicateKeyError):
135 pass
136
137 -class DuplicateAtomIDError(csb.core.DuplicateKeyError):
138 pass
139
140 -class AlignmentArgumentLengthError(ValueError):
141 pass
142
143 -class BrokenSecStructureError(ValueError):
144 pass
145
146 -class UnknownSecStructureError(BrokenSecStructureError):
147 pass
148
149 -class AbstractEntity(object):
150 """ 151 Base class for all protein structure entities. 152 153 This class defines uniform interface of all entities (e.g. L{Structure}, 154 L{Chain}, L{Residue}) according to the Composite pattern. 155 """ 156 157 __metaclass__ = ABCMeta 158 159 @abstractproperty
160 - def items(self):
161 """ 162 Iterator over all immediate children of the entity 163 @rtype: iterator of L{AbstractEntity} 164 """ 165 pass
166
167 - def components(self, klass=None):
168 """ 169 Return an iterator over all descendants of the entity. 170 171 @param klass: return entities of the specified L{AbstractEntity} subclass 172 only. If None, traverse the hierarchy down to the lowest level. 173 @param klass: class 174 """ 175 for entity in CompositeEntityIterator.create(self, klass): 176 if klass is None or isinstance(entity, klass): 177 yield entity
178
179 - def transform(self, rotation, translation):
180 """ 181 Apply in place RotationMatrix and translation Vector to all atoms. 182 183 @type rotation: numpy array 184 @type translation: numpy array 185 """ 186 for node in self.items: 187 node.transform(rotation, translation)
188
189 - def list_coordinates(self, what=None, skip=False):
190 """ 191 Extract the coordinates of the specified kind(s) of atoms and return 192 them as a list. 193 194 @param what: a list of atom kinds, e.g. ['N', 'CA', 'C'] 195 @type what: list or None 196 197 @return: a list of lists, each internal list corresponding to the coordinates 198 of a 3D vector 199 @rtype: list 200 201 @raise Broken3DStructureError: if a specific atom kind cannot be retrieved from a residue 202 """ 203 coords = [ ] 204 205 for residue in self.components(klass=Residue): 206 for atom_kind in (what or residue.atoms): 207 try: 208 coords.append(residue.atoms[atom_kind].vector) 209 except csb.core.ItemNotFoundError: 210 if skip: 211 continue 212 raise Broken3DStructureError('Could not retrieve {0} atom from the structure'.format(atom_kind)) 213 214 return numpy.array(coords)
215
216 -class CompositeEntityIterator(object):
217 """ 218 Iterates over composite L{AbstractEntity} hierarchies. 219 220 @param node: root entity to traverse 221 @type node: L{AbstractEntity} 222 """ 223
224 - def __init__(self, node):
225 226 if not isinstance(node, AbstractEntity): 227 raise TypeError(node) 228 229 self._node = node 230 self._stack = csb.core.Stack() 231 232 self._inspect(node)
233
234 - def __iter__(self):
235 return self
236
237 - def __next__(self):
238 return self.next()
239
240 - def next(self):
241 242 while True: 243 if self._stack.empty(): 244 raise StopIteration() 245 246 try: 247 current = self._stack.peek() 248 node = next(current) 249 self._inspect(node) 250 return node 251 252 except StopIteration: 253 self._stack.pop()
254
255 - def _inspect(self, node):
256 """ 257 Push C{node}'s children to the stack. 258 """ 259 self._stack.push(node.items)
260 261 @staticmethod
262 - def create(node, leaf=None):
263 """ 264 Create a new composite iterator. 265 266 @param leaf: if not None, return a L{ConfinedEntityIterator} 267 @type leaf: class 268 @rtype: L{CompositeEntityIterator} 269 """ 270 if leaf is None: 271 return CompositeEntityIterator(node) 272 else: 273 return ConfinedEntityIterator(node, leaf)
274
275 -class ConfinedEntityIterator(CompositeEntityIterator):
276 """ 277 Iterates over composite L{AbstractEntity} hierarchies, but terminates 278 the traversal of a branch once a specific node type is encountered. 279 280 @param node: root entity to traverse 281 @type node: L{AbstractEntity} 282 @param leaf: traverse the hierarchy down to the specified L{AbstractEntity} 283 @type leaf: class 284 """
285 - def __init__(self, node, leaf):
286 287 if not issubclass(leaf, AbstractEntity): 288 raise TypeError(leaf) 289 290 self._leaf = leaf 291 super(ConfinedEntityIterator, self).__init__(node)
292
293 - def _inspect(self, node):
294 295 if not isinstance(node, self._leaf): 296 self._stack.push(node.items)
297
298 -class Ensemble(csb.core.AbstractNIContainer, AbstractEntity):
299 """ 300 Represents an ensemble of multiple L{Structure} models. 301 Provides a list-like access to these models: 302 303 >>> ensemble[0] 304 <Structure Model 1: accn, x chains> 305 >>> ensemble.models[1] 306 <Structure Model 1: accn, x chains> 307 """ 308
309 - def __init__(self):
310 self._models = EnsembleModelsCollection()
311
312 - def __repr__(self):
313 return "<Ensemble: {0} models>".format(self.models.length)
314 315 @property
316 - def _children(self):
317 return self._models
318 319 @property
320 - def models(self):
321 """ 322 Access Ensembles's models by model ID 323 @rtype: L{EnsembleModelsCollection} 324 """ 325 return self._models
326 327 @property
328 - def items(self):
329 return iter(self._models)
330 331 @property
332 - def first_model(self):
333 """ 334 The first L{Structure} in the ensemble (if available) 335 @rtype: L{Structure} or None 336 """ 337 if len(self._models) > 0: 338 return self[0] 339 return None
340
341 - def to_pdb(self, output_file=None):
342 """ 343 Dump the ensemble in PDB format. 344 345 @param output_file: output file name or open stream 346 @type output_file: str or stream 347 """ 348 from csb.bio.io.wwpdb import PDBEnsembleFileBuilder 349 350 if self.models.length < 1: 351 raise InvalidOperation("Can't dump an empty ensemble") 352 353 temp = csb.io.MemoryStream() 354 355 builder = PDBEnsembleFileBuilder(temp) 356 builder.add_header(self.first_model) 357 358 for model in self.models: 359 builder.add_structure(model) 360 361 builder.finalize() 362 363 data = temp.getvalue() 364 temp.close() 365 366 if not output_file: 367 return data 368 else: 369 with csb.io.EntryWriter(output_file, close=False) as out: 370 out.write(data)
371
372 -class EnsembleModelsCollection(csb.core.CollectionContainer):
373
374 - def __init__(self):
375 376 super(EnsembleModelsCollection, self).__init__(type=Structure, start_index=1) 377 self._models = set()
378
379 - def append(self, structure):
380 """ 381 Add a new model 382 383 @param structure: model to append 384 @type structure: L{Structure} 385 """ 386 387 if not structure.model_id or not str(structure.model_id).strip(): 388 raise ValueError("Invalid model identifier: '{0.model_id}'".format(structure)) 389 if structure.model_id in self._models: 390 raise DuplicateModelIDError(structure.model_id) 391 else: 392 return super(EnsembleModelsCollection, self).append(structure)
393 394 @property
395 - def _exception(self):
396 return EntityIndexError
397
398 399 -class Structure(csb.core.AbstractNIContainer, AbstractEntity):
400 """ 401 Represents a single model of a PDB 3-Dimensional molecular structure. 402 Provides access to the L{Chain} objects, contained in the model: 403 404 >>> structure['A'] 405 <Chain A: Protein> 406 >>> structure.chains['A'] 407 <Chain A: Protein> 408 >>> structure.items 409 <iterator of Chain-s> 410 411 @param accession: accession number of the structure 412 @type accession: str 413 """
414 - def __init__(self, accession):
415 416 self._accession = None 417 self._chains = StructureChainsTable(self) 418 self._model_id = None 419 420 self.accession = accession
421
422 - def __repr__(self):
423 return "<Structure Model {0.model_id}: {0.accession}, {1} chains>".format(self, self.chains.length)
424 425 @property
426 - def _children(self):
427 return self._chains
428 429 @property
430 - def chains(self):
431 """ 432 Access chains by their chain identifiers 433 @rtype: L{StructureChainsTable} 434 """ 435 return self._chains
436 437 @property
438 - def items(self):
439 for chain in self._chains: 440 yield self._chains[chain]
441 442 @property
443 - def first_chain(self):
444 """ 445 The first L{Chain} in the structure (if available) 446 @rtype: L{Chain} or None 447 """ 448 if len(self._chains) > 0: 449 return next(self.items) 450 return None
451 452 @property
453 - def accession(self):
454 """ 455 Accession number 456 @rtype: str 457 """ 458 return self._accession
459 @accession.setter
460 - def accession(self, accession):
461 if accession is None: 462 raise ValueError(accession) 463 self._accession = str(accession).strip().lower() 464 for c in self.chains: 465 self.chains[c]._accession = self._accession
466 467 @property
468 - def model_id(self):
469 """ 470 Model ID 471 @rtype: int 472 """ 473 return self._model_id
474 @model_id.setter
475 - def model_id(self, value):
476 self._model_id = value
477
478 - def to_fasta(self):
479 """ 480 Dump the structure in FASTA format. 481 482 @return: FASTA-formatted string with all chains in the structure 483 @rtype: str 484 485 @deprecated: this method will be removed soon. Use 486 L{csb.bio.sequence.ChainSequence.create} instead 487 """ 488 fasta = [] 489 490 for chain in self.items: 491 492 if chain.length > 0: 493 fasta.append('>{0}'.format(chain.header)) 494 fasta.append(chain.sequence) 495 496 return os.linesep.join(fasta)
497
498 - def to_pdb(self, output_file=None):
499 """ 500 Dump the whole structure in PDB format. 501 502 @param output_file: output file name or open stream 503 @type output_file: str or stream 504 """ 505 from csb.bio.io.wwpdb import PDBFileBuilder 506 507 temp = csb.io.MemoryStream() 508 builder = PDBFileBuilder(temp) 509 510 builder.add_header(self) 511 builder.add_structure(self) 512 builder.finalize() 513 514 data = temp.getvalue() 515 temp.close() 516 517 if not output_file: 518 return data 519 else: 520 with csb.io.EntryWriter(output_file, close=False) as out: 521 out.write(data)
522
523 -class StructureChainsTable(csb.core.DictionaryContainer):
524
525 - def __init__(self, structure=None, chains=None):
526 self.__container = structure 527 super(StructureChainsTable, self).__init__() 528 529 if chains is not None: 530 for chain in chains: 531 self.append(chain)
532
533 - def __repr__(self):
534 if len(self) > 0: 535 return "<StructureChains: {0}>".format(', '.join(self)) 536 else: 537 return "<StructureChains: empty>"
538 539 @property
540 - def _exception(self):
541 return ChainNotFoundError
542
543 - def append(self, chain):
544 """ 545 Add a new Chain to the structure. 546 547 @param chain: the new chain to be appended 548 @type chain: L{Chain} 549 550 @raise DuplicateChainIDError: if a chain with same ID is already defined 551 @raise InvalidOperation: if the chain is already associated with a structure 552 """ 553 554 if chain._structure and chain._structure is not self.__container: 555 raise InvalidOperation('This chain is already part of another structure') 556 if chain.id in self: 557 raise DuplicateChainIDError('A chain with ID {0} is already defined'.format(chain.id)) 558 559 super(StructureChainsTable, self).append(chain.id, chain) 560 561 if self.__container: 562 chain._accession = self.__container.accession 563 chain._structure = self.__container
564
565 - def remove(self, id):
566 """ 567 Remove a chain from the structure. 568 569 @param id: ID of the chain to be detached 570 @type id: str 571 """ 572 chain = self[id] 573 self._remove(id) 574 chain._structure = None
575
576 - def _update_chain_id(self, chain, new_id):
577 578 if chain.id not in self or self[chain.id] is not chain: 579 raise InvalidOperation(chain) 580 581 self._remove(chain.id) 582 583 if new_id in self: 584 raise DuplicateChainIDError('Chain ID {0} is already defined'.format(id)) 585 586 super(StructureChainsTable, self).append(new_id, chain)
587
588 -class Chain(csb.core.AbstractNIContainer, AbstractEntity):
589 """ 590 Represents a polymeric chain. Provides list-like and rank-based access to 591 the residues in the chain: 592 593 >>> chain[0] 594 <ProteinResidue [1]: SER None> 595 >>> chain.residues[1] 596 <ProteinResidue [1]: SER None> 597 598 You can also access residues by their PDB sequence number: 599 600 >>> chain.find(sequence_number=5, insertion_code='A') 601 <ProteinResidue [1]: SER 5A> 602 603 @param chain_id: ID of the new chain 604 @type chain_id: str 605 @param type: sequence type (a member of the L{SequenceTypes} enum) 606 @type type: L{csb.core.EnumItem} 607 @param name: name of the chain 608 @type name: str 609 @param residues: initialization list of L{Residue}-s 610 @type residues: list 611 @param accession: accession number of the chain 612 @type accession: str 613 @param molecule_id: MOL ID of the chain, if part of a polymer 614 615 """
616 - def __init__(self, chain_id, type=SequenceTypes.Protein, name='', 617 residues=None, accession=None, molecule_id=None):
618 619 self._id = str(chain_id).strip() 620 self._accession = None 621 self._type = None 622 self._residues = ChainResiduesCollection(self, residues) 623 self._secondary_structure = None 624 self._molecule_id = molecule_id 625 self._torsion_computed = False 626 self._name = str(name).strip() 627 628 self._structure = None 629 630 self.type = type 631 if accession is not None: 632 self.accession = accession
633 634 @staticmethod
635 - def from_sequence(sequence, id="_"):
636 """ 637 Create a new chain from an existing sequence. 638 639 @param sequence: source sequence 640 @type sequence: L{csb.bio.sequence.AbstractSequence} 641 642 @rtype: L{Chain} 643 """ 644 645 chain = Chain(id, type=sequence.type) 646 647 for ri in sequence.residues: 648 residue = Residue.create(sequence.type, ri.rank, ri.type, sequence_number=ri.rank) 649 chain.residues.append(residue) 650 651 return chain
652 653 @property
654 - def _children(self):
655 return self._residues
656
657 - def __repr__(self):
658 return "<Chain {0.id}: {0.type!r}>".format(self)
659
660 - def __len__(self):
661 return self._residues.length
662 663 @property
664 - def id(self):
665 """ 666 Chain's ID 667 @rtype: str 668 """ 669 return self._id
670 @id.setter
671 - def id(self, id):
672 if not isinstance(id, csb.core.string): 673 raise ValueError(id) 674 id = id.strip() 675 if self._structure: 676 self._structure.chains._update_chain_id(self, id) 677 self._id = id
678 679 @property
680 - def accession(self):
681 """ 682 Accession number 683 @rtype: str 684 """ 685 return self._accession
686 @accession.setter
687 - def accession(self, accession):
688 if self._structure: 689 raise InvalidOperation("Only the accession of the parent structure can be altered") 690 if accession is None: 691 raise ValueError(accession) 692 self._accession = str(accession).strip()
693 694 @property
695 - def type(self):
696 """ 697 Chain type - any member of L{SequenceTypes} 698 @rtype: enum item 699 """ 700 return self._type
701 @type.setter
702 - def type(self, type):
703 if type.enum is not SequenceTypes: 704 raise TypeError(type) 705 self._type = type
706 707 @property
708 - def residues(self):
709 """ 710 Rank-based access to Chain's L{Residue}s 711 @rtype: L{ChainResiduesCollection} 712 """ 713 return self._residues
714 715 @property
716 - def items(self):
717 return iter(self._residues)
718 719 @property
720 - def torsion(self):
721 """ 722 Torsion angles 723 @rtype: L{TorsionAnglesCollection} 724 """ 725 if not self._torsion_computed: 726 raise InvalidOperation('The correctness of the data is not guaranteed ' 727 'until chain.compute_torsion() is invoked.') 728 729 torsion = TorsionAnglesCollection() 730 731 for r in self.residues: 732 if r.torsion is None: 733 torsion.append(TorsionAngles(None, None, None)) 734 else: 735 torsion.append(r.torsion) 736 737 return torsion
738 739 @property
740 - def has_torsion(self):
741 """ 742 True if C{Chain.compute_torsion} had been invoked 743 @rtype: bool 744 """ 745 return self._torsion_computed
746 747 @property
748 - def length(self):
749 """ 750 Number of residues 751 @rtype: int 752 """ 753 return self._residues.length
754 755 @property
756 - def entry_id(self):
757 """ 758 Accession number + chain ID 759 @rtype: str 760 """ 761 if self._accession and self._id: 762 return self._accession + self._id 763 else: 764 return None
765 766 @property
767 - def name(self):
768 """ 769 Chain name 770 @rtype: str 771 """ 772 return self._name
773 @name.setter
774 - def name(self, value):
775 if value is not None: 776 value = str(value).strip() 777 self._name = value
778 779 @property
780 - def molecule_id(self):
781 """ 782 PDB MOL ID of this chain 783 @rtype: int 784 """ 785 return self._molecule_id
786 @molecule_id.setter
787 - def molecule_id(self, value):
788 self._molecule_id = value
789 790 @property
791 - def header(self):
792 """ 793 FASTA header in PDB format 794 @rtype: str 795 """ 796 header = "{0._accession}_{0._id} mol:{1} length:{0.length} {0.name}" 797 return header.format(self, str(self.type).lower())
798 799 @property
800 - def sequence(self):
801 """ 802 Chain sequence 803 @rtype: str 804 """ 805 sequence = [] 806 gap = str(self.alphabet.GAP) 807 808 for residue in self.residues: 809 if residue and residue.type: 810 sequence.append(str(residue.type)) 811 else: 812 sequence.append(gap) 813 814 return ''.join(sequence)
815 816 @property
817 - def alphabet(self):
818 """ 819 Sequence alphabet corresponding to the current chain type 820 @rtype: L{csb.core.enum} 821 """ 822 return SequenceAlphabets.get(self.type)
823 824 @property
825 - def secondary_structure(self):
826 """ 827 Secondary structure (if available) 828 @rtype: L{SecondaryStructure} 829 """ 830 return self._secondary_structure
831 @secondary_structure.setter
832 - def secondary_structure(self, ss):
833 if not isinstance(ss, SecondaryStructure): 834 raise TypeError(ss) 835 if len(ss) > 0: 836 if (ss[ss.last_index].end > self._residues.last_index): 837 raise ValueError('Secondary structure out of range') 838 self._secondary_structure = ss
839
840 - def clone(self):
841 """ 842 Make a deep copy of the chain. If this chain is part of a structure, 843 detach from it. 844 845 @return: a deep copy of self 846 @rtype: L{Chain} 847 """ 848 start, end = self.residues.start_index, self.residues.last_index 849 return self.subregion(start, end, clone=True)
850
851 - def subregion(self, start, end, clone=False):
852 """ 853 Extract a subchain defined by [start, end]. If clone is True, this 854 is a deep copy of the chain. Otherwise same as: 855 856 >>> chain.residues[start : end + 1] 857 858 but coordinates are checked and a Chain instance is returned. 859 860 @param start: start position of the sub-region 861 @type start: int 862 @param end: end position 863 @type end: int 864 @param clone: if True, a deep copy of the sub-region is returned, 865 otherwise - a shallow one 866 @type clone: bool 867 868 869 @return: a new chain, made from the residues of the extracted region 870 @rtype: L{Chain} 871 872 @raise IndexError: if start/end positions are out of range 873 """ 874 if start < self.residues.start_index or start > self.residues.last_index: 875 raise IndexError('The start position is out of range {0.start_index} .. {0.last_index}'.format(self.residues)) 876 if end < self.residues.start_index or end > self.residues.last_index: 877 raise IndexError('The end position is out of range {0.start_index} .. {0.last_index}'.format(self.residues)) 878 879 residues = self.residues[start : end + 1] 880 881 if clone: 882 residues = [r.clone() for r in residues] 883 884 chain = Chain(self.id, accession=self.accession, name=self.name, 885 type=self.type, residues=residues, molecule_id=self.molecule_id) 886 if chain.secondary_structure: 887 chain.secondary_structure = self.secondary_structure.subregion(start, end) 888 chain._torsion_computed = self._torsion_computed 889 890 return chain
891
892 - def find(self, sequence_number, insertion_code=None):
893 """ 894 Get a residue by its original Residue Sequence Number and Insertion Code. 895 896 @param sequence_number: PDB sequence number of the residue 897 @type sequence_number: str 898 @param insertion_code: PDB insertion code of the residue (if any) 899 @type insertion_code: str 900 901 @return: the residue object with such an ID 902 @rtype: L{Residue} 903 904 @raise csb.core.ItemNotFoundError: if no residue with that ID exists 905 """ 906 res_id = str(sequence_number).strip() 907 908 if insertion_code is not None: 909 insertion_code = str(insertion_code).strip() 910 res_id += insertion_code 911 912 return self.residues._get_residue(res_id)
913
914 - def compute_torsion(self):
915 """ 916 Iterate over all residues in the chain, compute and set their torsion property. 917 918 @raise Missing3DStructureError: when a 3D structure is absent 919 @raise Broken3DStructureError: when a given atom cannot be retrieved from any residue 920 """ 921 if self.type != SequenceTypes.Protein: 922 raise NotImplementedError() 923 924 for i, residue in enumerate(self.residues, start=self.residues.start_index): 925 926 prev_residue, next_residue = None, None 927 928 if i > self.residues.start_index: 929 prev_residue = self.residues[i - 1] 930 if i < self.residues.last_index: 931 next_residue = self.residues[i + 1] 932 933 residue.torsion = residue.compute_torsion(prev_residue, next_residue, strict=False) 934 935 self._torsion_computed = True
936
937 - def superimpose(self, other, what=['CA'], how=AlignmentTypes.Global):
938 """ 939 Find the optimal fit between C{self} and C{other}. Return L{SuperimposeInfo} 940 (RotationMatrix, translation Vector and RMSD), such that: 941 942 >>> other.transform(rotation_matrix, translation_vector) 943 944 will result in C{other}'s coordinates superimposed over C{self}. 945 946 @param other: the subject (movable) chain 947 @type other: L{Chain} 948 @param what: a list of atom kinds, e.g. ['CA'] 949 @type what: list 950 @param how: fitting method (global or local) - a member of the L{AlignmentTypes} enum 951 @type how: L{csb.core.EnumItem} 952 953 @return: superimposition info object, containing rotation matrix, translation 954 vector and computed RMSD 955 @rtype: L{SuperimposeInfo} 956 957 @raise AlignmentArgumentLengthError: when the lengths of the argument chains differ 958 """ 959 if self.length != other.length or self.length < 1: 960 raise AlignmentArgumentLengthError('Both chains must be of the same and positive length') 961 962 x = self.list_coordinates(what) 963 y = other.list_coordinates(what) 964 assert len(x) == len(y) 965 966 if how == AlignmentTypes.Global: 967 r, t = csb.bio.utils.fit(x, y) 968 else: 969 r, t = csb.bio.utils.fit_wellordered(x, y) 970 971 rmsd = csb.bio.utils.rmsd(x, y) 972 973 return SuperimposeInfo(r, t, rmsd=rmsd) 974
975 - def align(self, other, what=['CA'], how=AlignmentTypes.Global):
976 """ 977 Align C{other}'s alpha carbons over self in space and return L{SuperimposeInfo}. 978 Coordinates of C{other} are overwritten in place using the rotation matrix 979 and translation vector in L{SuperimposeInfo}. Alias for:: 980 981 R, t = self.superimpose(other, what=['CA']) 982 other.transform(R, t) 983 984 @param other: the subject (movable) chain 985 @type other: L{Chain} 986 @param what: a list of atom kinds, e.g. ['CA'] 987 @type what: list 988 @param how: fitting method (global or local) - a member of the L{AlignmentTypes} enum 989 @type how: L{csb.core.EnumItem} 990 991 @return: superimposition info object, containing rotation matrix, translation 992 vector and computed RMSD 993 @rtype: L{SuperimposeInfo} 994 """ 995 result = self.superimpose(other, what=what, how=how) 996 other.transform(result.rotation, result.translation) 997 998 return result 999
1000 - def rmsd(self, other, what=['CA']):
1001 """ 1002 Compute the C-alpha RMSD against another chain (assuming equal length). 1003 Chains are superimposed with Least Squares Fit / Singular Value Decomposition. 1004 1005 @param other: the subject (movable) chain 1006 @type other: L{Chain} 1007 @param what: a list of atom kinds, e.g. ['CA'] 1008 @type what: list 1009 1010 @return: computed RMSD over the specified atom kinds 1011 @rtype: float 1012 """ 1013 1014 if self.length != other.length or self.length < 1: 1015 raise ValueError('Both chains must be of the same and positive length ' 1016 '(got {0} and {1})'.format(self.length, other.length)) 1017 1018 x = self.list_coordinates(what) 1019 y = other.list_coordinates(what) 1020 assert len(x) == len(y) 1021 1022 return csb.bio.utils.rmsd(x, y)
1023
1024 - def tm_superimpose(self, other, what=['CA'], how=AlignmentTypes.Global):
1025 """ 1026 Find the optimal fit between C{self} and C{other}. Return L{SuperimposeInfo} 1027 (RotationMatrix, translation Vector and TM-score), such that: 1028 1029 >>> other.transform(rotation_matrix, translation_vector) 1030 1031 will result in C{other}'s coordinates superimposed over C{self}. 1032 1033 @param other: the subject (movable) chain 1034 @type other: L{Chain} 1035 @param what: a list of atom kinds, e.g. ['CA'] 1036 @type what: list 1037 @param how: fitting method (global or local) - a member of the L{AlignmentTypes} enum 1038 @type how: L{csb.core.EnumItem} 1039 1040 @return: superimposition info object, containing rotation matrix, translation 1041 vector and computed TM-score 1042 @rtype: L{SuperimposeInfo} 1043 1044 @raise AlignmentArgumentLengthError: when the lengths of the argument chains differ 1045 """ 1046 1047 if self.length != other.length or self.length < 1: 1048 raise ValueError('Both chains must be of the same and positive length') 1049 1050 x = self.list_coordinates(what) 1051 y = other.list_coordinates(what) 1052 assert len(x) == len(y) 1053 1054 L_ini_min = 0 1055 if how == AlignmentTypes.Global: 1056 fit = csb.bio.utils.fit 1057 elif how == AlignmentTypes.Local: 1058 fit = csb.bio.utils.fit_wellordered 1059 else: 1060 # TMscore.f like search (slow) 1061 fit = csb.bio.utils.fit 1062 L_ini_min = 4 1063 1064 r, t, tm = csb.bio.utils.tm_superimpose(x, y, fit, None, None, L_ini_min) 1065 1066 return SuperimposeInfo(r,t, tm_score=tm) 1067
1068 - def tm_score(self, other, what=['CA']):
1069 """ 1070 Compute the C-alpha TM-Score against another chain (assuming equal chain length 1071 and optimal configuration - no fitting is done). 1072 1073 @param other: the subject (movable) chain 1074 @type other: L{Chain} 1075 @param what: a list of atom kinds, e.g. ['CA'] 1076 @type what: list 1077 1078 @return: computed TM-Score over the specified atom kinds 1079 @rtype: float 1080 """ 1081 1082 if self.length != other.length or self.length < 1: 1083 raise ValueError('Both chains must be of the same and positive length') 1084 1085 x = self.list_coordinates(what) 1086 y = other.list_coordinates(what) 1087 assert len(x) == len(y) 1088 1089 return csb.bio.utils.tm_score(x, y)
1090
1091 -class ChainResiduesCollection(csb.core.CollectionContainer):
1092
1093 - def __init__(self, chain, residues):
1094 super(ChainResiduesCollection, self).__init__(type=Residue, start_index=1) 1095 self.__container = chain 1096 self.__lookup = { } 1097 1098 if residues is not None: 1099 for residue in residues: 1100 self.append(residue)
1101
1102 - def __repr__(self):
1103 if len(self) > 0: 1104 return "<ChainResidues: {0} ... {1}>".format(self[self.start_index], self[self.last_index]) 1105 else: 1106 return "<ChainResidues: empty>"
1107 1108 @property
1109 - def _exception(self):
1110 return EntityIndexError
1111
1112 - def append(self, residue):
1113 """ 1114 Append a new residue to the chain. 1115 1116 @param residue: the new residue 1117 @type residue: L{Residue} 1118 1119 @raise DuplicateResidueIDError: if a residue with the same ID already exists 1120 """ 1121 if residue.id and residue.id in self.__lookup: 1122 raise DuplicateResidueIDError('A residue with ID {0} is already defined within the chain'.format(residue.id)) 1123 index = super(ChainResiduesCollection, self).append(residue) 1124 residue._container = self 1125 self.__container._torsion_computed = False 1126 self._add(residue) 1127 return index
1128
1129 - def _contains(self, id):
1130 return id in self.__lookup
1131
1132 - def _remove(self, id):
1133 if id in self.__lookup: 1134 del self.__lookup[id]
1135
1136 - def _add(self, residue):
1137 self.__lookup[residue.id] = residue
1138
1139 - def _get_residue(self, id):
1140 try: 1141 return self.__lookup[id] 1142 except KeyError: 1143 raise csb.core.ItemNotFoundError(id)
1144
1145 -class Residue(csb.core.AbstractNIContainer, AbstractEntity):
1146 """ 1147 Base class representing a single residue. Provides a dictionary-like 1148 access to the atoms contained in the residue: 1149 1150 >>> residue['CA'] 1151 <Atom [3048]: CA> 1152 >>> residue.atoms['CA'] 1153 <Atom [3048]: CA> 1154 >>> residue.items 1155 <iterator of Atom-s> 1156 1157 @param rank: rank of the residue with respect to the chain 1158 @type rank: int 1159 @param type: residue type - a member of any L{SequenceAlphabets} 1160 @type type: L{csb.core.EnumItem} 1161 @param sequence_number: PDB sequence number of the residue 1162 @type sequence_number: str 1163 @param insertion_code: PDB insertion code, if any 1164 @type insertion_code: str 1165 """
1166 - def __init__(self, rank, type, sequence_number=None, insertion_code=None):
1167 1168 self._type = None 1169 self._pdb_name = None 1170 self._rank = int(rank) 1171 self._atoms = ResidueAtomsTable(self) 1172 self._secondary_structure = None 1173 self._torsion = None 1174 self._sequence_number = None 1175 self._insertion_code = None 1176 self._container = None 1177 1178 self.type = type 1179 self.id = sequence_number, insertion_code 1180 self._pdb_name = repr(type)
1181 1182 @property
1183 - def _children(self):
1184 return self._atoms
1185
1186 - def __repr__(self):
1187 return '<{1} [{0.rank}]: {0.type!r} {0.id}>'.format(self, self.__class__.__name__)
1188 1189 @property
1190 - def type(self):
1191 """ 1192 Residue type - a member of any sequence alphabet 1193 @rtype: enum item 1194 """ 1195 return self._type
1196 @type.setter
1197 - def type(self, type):
1198 if type.enum not in (SequenceAlphabets.Protein, SequenceAlphabets.Nucleic, SequenceAlphabets.Unknown): 1199 raise TypeError(type) 1200 self._type = type
1201 1202 @property
1203 - def rank(self):
1204 """ 1205 Residue's position in the sequence (1-based) 1206 @rtype: int 1207 """ 1208 return self._rank
1209 1210 @property
1211 - def secondary_structure(self):
1212 """ 1213 Secondary structure element this residue is part of 1214 @rtype: L{SecondaryStructureElement} 1215 """ 1216 return self._secondary_structure
1217 @secondary_structure.setter
1218 - def secondary_structure(self, structure):
1219 if not isinstance(structure, SecondaryStructureElement): 1220 raise TypeError(structure) 1221 self._secondary_structure = structure
1222 1223 @property
1224 - def torsion(self):
1225 """ 1226 Torsion angles 1227 @rtype: L{TorsionAngles} 1228 """ 1229 return self._torsion
1230 @torsion.setter
1231 - def torsion(self, torsion):
1232 if not isinstance(torsion, TorsionAngles): 1233 raise TypeError(torsion) 1234 self._torsion = torsion
1235 1236 @property
1237 - def atoms(self):
1238 """ 1239 Access residue's atoms by atom name 1240 @rtype: L{ResidueAtomsTable} 1241 """ 1242 return self._atoms
1243 1244 @property
1245 - def items(self):
1246 for atom in self._atoms: 1247 yield self._atoms[atom]
1248 1249 @property
1250 - def sequence_number(self):
1251 """ 1252 PDB sequence number (if residue.has_structure is True) 1253 @rtype: int 1254 """ 1255 return self._sequence_number
1256 1257 @property
1258 - def insertion_code(self):
1259 """ 1260 PDB insertion code (if defined) 1261 @rtype: str 1262 """ 1263 return self._insertion_code
1264 1265 @property
1266 - def id(self):
1267 """ 1268 PDB sequence number [+ insertion code] 1269 @rtype: str 1270 """ 1271 if self._sequence_number is None: 1272 return None 1273 elif self._insertion_code is not None: 1274 return str(self._sequence_number) + self._insertion_code 1275 else: 1276 return str(self._sequence_number)
1277 @id.setter
1278 - def id(self, value):
1279 sequence_number, insertion_code = value 1280 old_id = self.id 1281 id = '' 1282 if sequence_number is not None: 1283 sequence_number = int(sequence_number) 1284 id = str(sequence_number) 1285 if insertion_code is not None: 1286 insertion_code = str(insertion_code).strip() 1287 id += insertion_code 1288 if sequence_number is None: 1289 raise InvalidOperation('sequence_number must be defined when an insertion_code is specified.') 1290 if old_id != id: 1291 if self._container: 1292 if self._container._contains(id): 1293 raise DuplicateResidueIDError('A residue with ID {0} is already defined within the chain'.format(id)) 1294 self._container._remove(old_id) 1295 self._sequence_number = sequence_number 1296 self._insertion_code = insertion_code 1297 if self._container: 1298 self._container._add(self)
1299 1300 @property
1301 - def has_structure(self):
1302 """ 1303 True if this residue has any atoms 1304 @rtype: bool 1305 """ 1306 return len(self.atoms) > 0
1307
1308 - def list_coordinates(self, what=None, skip=False):
1309 1310 coords = [] 1311 1312 if not self.has_structure: 1313 if skip: 1314 return numpy.array([]) 1315 raise Missing3DStructureError(self) 1316 1317 for atom_kind in (what or self.atoms): 1318 if atom_kind in self.atoms: 1319 coords.append(self.atoms[atom_kind].vector) 1320 else: 1321 if skip: 1322 continue 1323 raise Broken3DStructureError('Could not retrieve {0} atom'.format(atom_kind)) 1324 1325 return numpy.array(coords)
1326
1327 - def clone(self):
1328 1329 container = self._container 1330 self._container = None 1331 clone = copy.deepcopy(self) 1332 self._container = container 1333 1334 return clone
1335 1336 @staticmethod
1337 - def create(sequence_type, *a, **k):
1338 """ 1339 Residue factory method, which returns the proper L{Residue} instance based on 1340 the specified C{sequence_type}. All additional arguments are used to initialize 1341 the subclass by passing them automatically to the underlying constructor. 1342 1343 @param sequence_type: create a Residue of that SequenceType 1344 @type sequence_type: L{csb.core.EnumItem} 1345 1346 @return: a new residue of the proper subclass 1347 @rtype: L{Residue} subclass 1348 1349 @raise ValueError: if the sequence type is not known 1350 """ 1351 if sequence_type == SequenceTypes.Protein: 1352 return ProteinResidue(*a, **k) 1353 elif sequence_type == SequenceTypes.NucleicAcid: 1354 return NucleicResidue(*a, **k) 1355 elif sequence_type == SequenceTypes.Unknown: 1356 return UnknownResidue(*a, **k) 1357 else: 1358 raise ValueError(sequence_type)
1359
1360 -class ProteinResidue(Residue):
1361 """ 1362 Represents a single amino acid residue. 1363 1364 @param rank: rank of the residue with respect to the chain 1365 @type rank: int 1366 @param type: residue type - a member of 1367 L{csb.bio.sequence.SequenceAlphabets.Protein} 1368 @type type: L{csb.core.EnumItem} 1369 @param sequence_number: PDB sequence number of the residue 1370 @type sequence_number: str 1371 @param insertion_code: PDB insertion code, if any 1372 @type insertion_code: str 1373 """ 1374
1375 - def __init__(self, rank, type, sequence_number=None, insertion_code=None):
1376 1377 if isinstance(type, csb.core.string): 1378 try: 1379 if len(type) == 3: 1380 type = csb.core.Enum.parsename(SequenceAlphabets.Protein, type) 1381 else: 1382 type = csb.core.Enum.parse(SequenceAlphabets.Protein, type) 1383 except (csb.core.EnumMemberError, csb.core.EnumValueError): 1384 raise ValueError("'{0}' is not a valid amino acid".format(type)) 1385 elif type.enum is not SequenceAlphabets.Protein: 1386 raise TypeError(type) 1387 1388 super(ProteinResidue, self).__init__(rank, type, sequence_number, insertion_code)
1389
1390 - def compute_torsion(self, prev_residue, next_residue, strict=True):
1391 """ 1392 Compute the torsion angles of the current residue with neighboring residues 1393 C{prev_residue} and C{next_residue}. 1394 1395 @param prev_residue: the previous residue in the chain 1396 @type prev_residue: L{Residue} 1397 @param next_residue: the next residue in the chain 1398 @type next_residue: L{Residue} 1399 @param strict: if True, L{Broken3DStructureError} is raised if either C{prev_residue} 1400 or C{next_residue} has a broken structure, else the error is silently 1401 ignored and an empty L{TorsionAngles} instance is created 1402 @type strict: bool 1403 1404 @return: a L{TorsionAngles} object, holding the phi, psi and omega values 1405 @rtype: L{TorsionAngles} 1406 1407 @raise Broken3DStructureError: when a specific atom cannot be found 1408 """ 1409 if prev_residue is None and next_residue is None: 1410 raise ValueError('At least one neighboring residue is required to compute the torsion.') 1411 1412 angles = TorsionAngles(None, None, None, units=AngleUnits.Degrees) 1413 1414 for residue in (self, prev_residue, next_residue): 1415 if residue is not None and not residue.has_structure: 1416 if strict: 1417 raise Missing3DStructureError(repr(residue)) 1418 elif residue is self: 1419 return angles 1420 1421 try: 1422 n = self._atoms['N'].vector 1423 ca = self._atoms['CA'].vector 1424 c = self._atoms['C'].vector 1425 except csb.core.ItemNotFoundError as missing_atom: 1426 if strict: 1427 raise Broken3DStructureError('Could not retrieve {0} atom from the current residue {1!r}.'.format( 1428 missing_atom, self)) 1429 else: 1430 return angles 1431 1432 try: 1433 if prev_residue is not None and prev_residue.has_structure: 1434 prev_c = prev_residue._atoms['C'].vector 1435 angles.phi = csb.numeric.dihedral_angle(prev_c, n, ca, c) 1436 except csb.core.ItemNotFoundError as missing_prevatom: 1437 if strict: 1438 raise Broken3DStructureError('Could not retrieve {0} atom from the i-1 residue {1!r}.'.format( 1439 missing_prevatom, prev_residue)) 1440 try: 1441 if next_residue is not None and next_residue.has_structure: 1442 next_n = next_residue._atoms['N'].vector 1443 angles.psi = csb.numeric.dihedral_angle(n, ca, c, next_n) 1444 next_ca = next_residue._atoms['CA'].vector 1445 angles.omega = csb.numeric.dihedral_angle(ca, c, next_n, next_ca) 1446 except csb.core.ItemNotFoundError as missing_nextatom: 1447 if strict: 1448 raise Broken3DStructureError('Could not retrieve {0} atom from the i+1 residue {1!r}.'.format( 1449 missing_nextatom, next_residue)) 1450 1451 return angles
1452
1453 -class NucleicResidue(Residue):
1454 """ 1455 Represents a single nucleotide residue. 1456 1457 @param rank: rank of the residue with respect to the chain 1458 @type rank: int 1459 @param type: residue type - a member of 1460 L{csb.bio.sequence.SequenceAlphabets.Nucleic} 1461 @type type: L{csb.core.EnumItem} 1462 @param sequence_number: PDB sequence number of the residue 1463 @type sequence_number: str 1464 @param insertion_code: PDB insertion code, if any 1465 @type insertion_code: str 1466 """ 1467
1468 - def __init__(self, rank, type, sequence_number=None, insertion_code=None):
1469 1470 if isinstance(type, csb.core.string): 1471 try: 1472 if len(type) > 1: 1473 type = csb.core.Enum.parsename(SequenceAlphabets.Nucleic, type) 1474 else: 1475 type = csb.core.Enum.parse(SequenceAlphabets.Nucleic, type) 1476 except (csb.core.EnumMemberError, csb.core.EnumValueError): 1477 raise ValueError("'{0}' is not a valid nucleotide".format(type)) 1478 elif type.enum is not SequenceAlphabets.Nucleic: 1479 raise TypeError(type) 1480 1481 super(NucleicResidue, self).__init__(rank, type, sequence_number, insertion_code) 1482 self._pdb_name = str(type)
1483
1484 -class UnknownResidue(Residue):
1485
1486 - def __init__(self, rank, type, sequence_number=None, insertion_code=None):
1490
1491 -class ResidueAtomsTable(csb.core.DictionaryContainer):
1492 """ 1493 Represents a collection of atoms. Provides dictionary-like access, 1494 where PDB atom names are used for lookup. 1495 """
1496 - def __init__(self, residue, atoms=None):
1497 1498 self.__residue = residue 1499 super(ResidueAtomsTable, self).__init__() 1500 1501 if atoms is not None: 1502 for atom in atoms: 1503 self.append(atom)
1504
1505 - def __repr__(self):
1506 if len(self) > 0: 1507 return "<ResidueAtoms: {0}>".format(', '.join(self.keys())) 1508 else: 1509 return "<ResidueAtoms: empty>"
1510 1511 @property
1512 - def _exception(self):
1513 return AtomNotFoundError
1514
1515 - def append(self, atom):
1516 """ 1517 Append a new Atom to the catalog. 1518 1519 If the atom has an alternate position, a disordered proxy will be created instead and the 1520 atom will be appended to the L{DisorderedAtom}'s list of children. If a disordered atom 1521 with that name already exists, the atom will be appended to its children only. 1522 If an atom with the same name exists, but it was erroneously not marked as disordered, 1523 that terrible condition will be fixed too. 1524 1525 @param atom: the new atom to append 1526 @type atom: L{Atom} 1527 1528 @raise DuplicateAtomIDError: if an atom with the same sequence number and 1529 insertion code already exists in that residue 1530 """ 1531 if atom._residue and atom._residue is not self.__residue: 1532 raise InvalidOperation('This atom is part of another residue') 1533 if atom.alternate or (atom.name in self and isinstance(self[atom.name], DisorderedAtom)): 1534 if atom.name not in self: 1535 atom.residue = self.__residue 1536 dis_atom = DisorderedAtom(atom) 1537 super(ResidueAtomsTable, self).append(dis_atom.name, dis_atom) 1538 else: 1539 if not isinstance(self[atom.name], DisorderedAtom): 1540 buggy_atom = self[atom.name] 1541 assert buggy_atom.alternate in (None, False) 1542 buggy_atom.alternate = True 1543 self.update(atom.name, DisorderedAtom(buggy_atom)) 1544 if not atom.alternate: 1545 atom.alternate = True 1546 self[atom.name].append(atom) 1547 else: 1548 if atom.name in self: 1549 raise DuplicateAtomIDError('Atom {0} is already defined for {1}'.format( 1550 atom.name, self.__residue)) 1551 else: 1552 super(ResidueAtomsTable, self).append(atom.name, atom) 1553 atom._residue = self.__residue
1554
1555 - def update(self, atom_name, atom):
1556 """ 1557 Update the atom with the specified name. 1558 1559 @param atom_name: update key 1560 @type atom_name: str 1561 @param atom: new value for this key 1562 @type atom: L{Atom} 1563 1564 @raise ValueError: if C{atom} has a different name than C{atom_name} 1565 """ 1566 if atom.name != atom_name: 1567 raise ValueError('Atom\'s name differs from the specified update key.') 1568 if atom.residue is not self.__residue: 1569 atom._residue = self.__residue 1570 1571 super(ResidueAtomsTable, self)._update({atom_name: atom})
1572
1573 -class Atom(AbstractEntity):
1574 """ 1575 Represents a single atom in space. 1576 1577 @param serial_number: atom's UID 1578 @type serial_number: int 1579 @param name: atom's name 1580 @type name: str 1581 @param element: corresponding L{ChemElements} 1582 @type element: L{csb.core.EnumItem} 1583 @param vector: atom's coordinates 1584 @type vector: numpy array 1585 @param alternate: if True, means that this is a wobbling atom with multiple alternative 1586 locations 1587 @type alternate: bool 1588 """
1589 - def __init__(self, serial_number, name, element, vector, alternate=False):
1590 1591 self._serial_number = None 1592 self._name = None 1593 self._element = None 1594 self._residue = None 1595 self._vector = None 1596 self._alternate = False 1597 self._temperature = None 1598 self._occupancy = None 1599 self._charge = None 1600 1601 if not isinstance(name, csb.core.string): 1602 raise TypeError(name) 1603 name_compact = name.strip() 1604 if len(name_compact) < 1: 1605 raise ValueError(name) 1606 self._name = name_compact 1607 self._full_name = name 1608 1609 if isinstance(element, csb.core.string): 1610 element = csb.core.Enum.parsename(ChemElements, element) 1611 elif element is None: 1612 pass 1613 elif element.enum is not ChemElements: 1614 raise TypeError(element) 1615 self._element = element 1616 1617 # pass type- and value-checking control to setters 1618 self.serial_number = serial_number 1619 self.vector = vector 1620 self.alternate = alternate
1621
1622 - def __repr__(self):
1623 return "<Atom [{0.serial_number}]: {0.name}>".format(self)
1624
1625 - def __lt__(self, other):
1626 return self.serial_number < other.serial_number
1627
1628 - def transform(self, rotation, translation):
1629 1630 vector = numpy.dot(self.vector, numpy.transpose(rotation)) + translation 1631 self.vector = vector
1632
1633 - def list_coordinates(self, what=None, skip=False):
1634 1635 if what is None: 1636 what = [self.name] 1637 1638 if self.name in what: 1639 return numpy.array([self.vector.copy()]) 1640 elif skip: 1641 return numpy.array([]) 1642 else: 1643 raise Missing3DStructureError()
1644
1645 - def clone(self):
1646 1647 residue = self._residue 1648 self._residue = None 1649 clone = copy.deepcopy(self) 1650 self._residue = residue 1651 1652 return clone
1653 1654 @property
1655 - def serial_number(self):
1656 """ 1657 PDB serial number 1658 @rtype: int 1659 """ 1660 return self._serial_number
1661 @serial_number.setter
1662 - def serial_number(self, number):
1663 if not isinstance(number, int) or number < 1: 1664 raise TypeError(number) 1665 self._serial_number = number
1666 1667 @property
1668 - def name(self):
1669 """ 1670 PDB atom name (trimmed) 1671 @rtype: str 1672 """ 1673 return self._name
1674 1675 @property
1676 - def element(self):
1677 """ 1678 Chemical element - a member of L{ChemElements} 1679 @rtype: enum item 1680 """ 1681 return self._element
1682 1683 @property
1684 - def residue(self):
1685 """ 1686 Residue instance that owns this atom (if available) 1687 @rtype: L{Residue} 1688 """ 1689 return self._residue
1690 @residue.setter
1691 - def residue(self, residue):
1692 if self._residue: 1693 raise InvalidOperation('This atom is already part of a residue.') 1694 if not isinstance(residue, Residue): 1695 raise TypeError(residue) 1696 self._residue = residue
1697 1698 @property
1699 - def vector(self):
1700 """ 1701 Atom's 3D coordinates (x, y, z) 1702 @rtype: numpy.array 1703 """ 1704 return self._vector
1705 @vector.setter
1706 - def vector(self, vector):
1707 if numpy.shape(vector) != (3,): 1708 raise ValueError("Three dimensional vector expected") 1709 self._vector = numpy.array(vector)
1710 1711 @property
1712 - def alternate(self):
1713 """ 1714 Alternative location flag 1715 @rtype: str 1716 """ 1717 return self._alternate
1718 @alternate.setter
1719 - def alternate(self, value):
1720 self._alternate = value
1721 1722 @property
1723 - def temperature(self):
1724 """ 1725 Temperature factor 1726 @rtype: float 1727 """ 1728 return self._temperature
1729 @temperature.setter
1730 - def temperature(self, value):
1731 self._temperature = value
1732 1733 @property
1734 - def occupancy(self):
1735 """ 1736 Occupancy number 1737 @rtype: float 1738 """ 1739 return self._occupancy
1740 @occupancy.setter
1741 - def occupancy(self, value):
1742 self._occupancy = value
1743 1744 @property
1745 - def charge(self):
1746 """ 1747 Charge 1748 @rtype: int 1749 """ 1750 return self._charge
1751 @charge.setter
1752 - def charge(self, value):
1753 self._charge = value
1754 1755 @property
1756 - def items(self):
1757 return iter([])
1758
1759 -class DisorderedAtom(csb.core.CollectionContainer, Atom):
1760 """ 1761 A wobbling atom, which has alternative locations. Each alternative is represented 1762 as a 'normal' L{Atom}. The atom with a highest occupancy is selected as a representative, 1763 hence a DisorderedAtom behaves as a regular L{Atom} (proxy of the representative) as well 1764 as a collection of Atoms. 1765 1766 @param atom: the first atom to be appended to the collection of alternatives. It 1767 is automatically defined as a representative, until a new atom with 1768 higher occupancy is appended to the collection 1769 @type atom: L{Atom} 1770 """ 1771
1772 - def __init__(self, atom):
1773 super(DisorderedAtom, self).__init__(type=Atom) 1774 self._rep = None 1775 self.append(atom)
1776
1777 - def append(self, atom):
1778 """ 1779 Append a new atom to the collection of alternatives. 1780 1781 @param atom: the new alternative 1782 @type atom: L{Atom} 1783 """ 1784 self.__update_rep(atom) 1785 super(DisorderedAtom, self).append(atom)
1786
1787 - def transform(self, rotation, translation):
1788 1789 for atom in self: 1790 atom.transform(rotation, translation) 1791 self._vector = self._rep._vector
1792
1793 - def __update_rep(self, atom):
1794 1795 if self._rep is None or \ 1796 ((self._rep.occupancy != atom.occupancy) and (self._rep.occupancy < atom.occupancy)): 1797 1798 self._rep = atom 1799 1800 self._serial_number = self._rep.serial_number 1801 self._name = self._rep.name 1802 self._full_name = self._rep._full_name 1803 self._element = self._rep.element 1804 self.alternate = self._rep.alternate 1805 self._residue = self._rep.residue 1806 self._vector = self._rep.vector 1807 self.temperature = self._rep.temperature 1808 self.occupancy = self._rep.occupancy 1809 self.charge = self._rep.charge
1810
1811 - def __repr__(self):
1812 return "<DisorderedAtom: {0.length} alternative locations>".format(self)
1813
1814 -class SuperimposeInfo(object):
1815 """ 1816 Describes a structural alignment result. 1817 1818 @type rotation: Numpy Array 1819 @type translation: L{Vector} 1820 @type rmsd: float 1821 """
1822 - def __init__(self, rotation, translation, rmsd=None, tm_score=None):
1823 1824 self.rotation = rotation 1825 self.translation = translation 1826 self.rmsd = rmsd 1827 self.tm_score = tm_score
1828
1829 -class SecondaryStructureElement(object):
1830 """ 1831 Describes a Secondary Structure Element. 1832 1833 @param start: start position with reference to the chain 1834 @type start: float 1835 @param end: end position with reference to the chain 1836 @type end: float 1837 @param type: element type - a member of the L{SecStructures} enum 1838 @type type: csb.core.EnumItem 1839 @param score: secondary structure prediction confidence, if available 1840 @type score: int 1841 1842 @raise IndexError: if start/end positions are out of range 1843 """
1844 - def __init__(self, start, end, type, score=None):
1845 1846 if not (0 < start <= end): 1847 raise IndexError('Element coordinates are out of range: 1 <= start <= end.') 1848 1849 self._start = None 1850 self._end = None 1851 self._type = None 1852 self._score = None 1853 1854 self.start = start 1855 self.end = end 1856 self.type = type 1857 1858 if score is not None: 1859 self.score = score
1860
1861 - def __lt__(self, other):
1862 return self.start < other.start
1863
1864 - def __eq__(self, other):
1865 return (self.type == other.type 1866 and self.start == other.start 1867 and self.end == other.end)
1868
1869 - def __str__(self):
1870 return self.to_string()
1871
1872 - def __repr__(self):
1873 return "<{0.type!r}: {0.start}-{0.end}>".format(self)
1874 1875 @property
1876 - def start(self):
1877 """ 1878 Start position (1-based) 1879 @rtype: int 1880 """ 1881 return self._start
1882 @start.setter
1883 - def start(self, value):
1884 if value is not None: 1885 value = int(value) 1886 if value < 1: 1887 raise ValueError(value) 1888 self._start = value
1889 1890 @property
1891 - def end(self):
1892 """ 1893 End position (1-based) 1894 @rtype: int 1895 """ 1896 return self._end
1897 @end.setter
1898 - def end(self, value):
1899 if value is not None: 1900 value = int(value) 1901 if value < 1: 1902 raise ValueError(value) 1903 self._end = value
1904 1905 @property
1906 - def type(self):
1907 """ 1908 Secondary structure type - a member of L{SecStructures} 1909 @rtype: enum item 1910 """ 1911 return self._type
1912 @type.setter
1913 - def type(self, value):
1914 if isinstance(value, csb.core.string): 1915 value = csb.core.Enum.parse(SecStructures, value) 1916 if not value.enum is SecStructures: 1917 raise TypeError(value) 1918 self._type = value
1919 1920 @property
1921 - def length(self):
1922 """ 1923 Number of residues covered by this element 1924 @rtype: int 1925 """ 1926 return self.end - self.start + 1
1927 1928 @property
1929 - def score(self):
1930 """ 1931 Secondary structure confidence values for each residue 1932 @rtype: L{CollectionContainer} 1933 """ 1934 return self._score
1935 @score.setter
1936 - def score(self, scores):
1937 if not len(scores) == self.length: 1938 raise ValueError('There must be a score entry for each residue in the element.') 1939 self._score = csb.core.CollectionContainer( 1940 items=list(scores), type=int, start_index=self.start)
1941
1942 - def overlaps(self, other):
1943 """ 1944 Return True if C{self} overlaps with C{other}. 1945 1946 @type other: L{SecondaryStructureElement} 1947 @rtype: bool 1948 """ 1949 this = set(range(self.start, self.end + 1)) 1950 that = set(range(other.start, other.end + 1)) 1951 return not this.isdisjoint(that)
1952
1953 - def merge(self, other):
1954 """ 1955 Merge C{self} and C{other}. 1956 1957 @type other: L{SecondaryStructureElement} 1958 1959 @return: a new secondary structure element 1960 @rtype: L{SecondaryStructureElement} 1961 1962 @bug: confidence scores are lost 1963 """ 1964 if not self.overlaps(other): 1965 raise ValueError("Can't merge non-overlapping secondary structures") 1966 elif self.type != other.type: 1967 raise ValueError("Can't merge secondary structures of different type") 1968 1969 start = min(self.start, other.start) 1970 end = max(self.end, other.end) 1971 assert self.type == other.type 1972 1973 return SecondaryStructureElement(start, end, self.type)
1974
1975 - def to_string(self):
1976 """ 1977 Dump the element as a string. 1978 1979 @return: string representation of the element 1980 @rtype: str 1981 """ 1982 return str(self.type) * self.length
1983
1984 - def simplify(self):
1985 """ 1986 Convert to three-state secondary structure (Helix, Strand, Coil). 1987 """ 1988 if self.type in (SecStructures.Helix, SecStructures.Helix3, SecStructures.PiHelix): 1989 self.type = SecStructures.Helix 1990 elif self.type in (SecStructures.Strand, SecStructures.BetaBridge): 1991 self.type = SecStructures.Strand 1992 elif self.type in (SecStructures.Coil, SecStructures.Turn, SecStructures.Bend): 1993 self.type = SecStructures.Coil 1994 elif self.type == SecStructures.Gap or self.type is None: 1995 pass 1996 else: 1997 assert False, 'Unhandled SS type: ' + repr(self.type)
1998
1999 -class SecondaryStructure(csb.core.CollectionContainer):
2000 """ 2001 Describes the secondary structure of a chain. 2002 Provides an index-based access to the secondary structure elements of the chain. 2003 2004 @param string: a secondary structure string (e.g. a PSI-PRED output) 2005 @type string: str 2006 @param conf_string: secondary structure prediction confidence values, if available 2007 @type conf_string: str 2008 """
2009 - def __init__(self, string=None, conf_string=None):
2010 2011 super(SecondaryStructure, self).__init__(type=SecondaryStructureElement, start_index=1) 2012 2013 self._minstart = None 2014 self._maxend = None 2015 2016 if string is not None: 2017 for motif in SecondaryStructure.parse(string, conf_string): 2018 self.append(motif)
2019
2020 - def __str__(self):
2021 return self.to_string()
2022
2023 - def append(self, element):
2024 """ 2025 Add a new SecondaryStructureElement. Then sort all elements by 2026 their start position. 2027 """ 2028 super(SecondaryStructure, self).append(element) 2029 super(SecondaryStructure, self)._sort() 2030 2031 if self._minstart is None or element.start < self._minstart: 2032 self._minstart = element.start 2033 if self._maxend is None or element.end > self._maxend: 2034 self._maxend = element.end
2035 2036 @staticmethod
2037 - def parse(string, conf_string=None):
2038 """ 2039 Parse secondary structure from DSSP/PSI-PRED output string. 2040 2041 @param string: a secondary structure string (e.g. a PSI-PRED output) 2042 @type string: str 2043 @param conf_string: secondary structure prediction confidence values, if available 2044 @type conf_string: str 2045 2046 @return: a list of L{SecondaryStructureElement}s. 2047 @rtype: list 2048 2049 @raise ValueError: if the confidence string is not of the same length 2050 """ 2051 if not isinstance(string, csb.core.string): 2052 raise TypeError(string) 2053 2054 string = ''.join(re.split('\s+', string)) 2055 if conf_string is not None: 2056 conf_string = ''.join(re.split('\s+', conf_string)) 2057 if not len(string) == len(conf_string): 2058 raise ValueError('The confidence string has unexpected length.') 2059 motifs = [ ] 2060 2061 if not len(string) > 0: 2062 raise ValueError('Empty Secondary Structure string') 2063 2064 currel = string[0] 2065 start = 0 2066 2067 for i, char in enumerate(string + '.'): 2068 2069 if currel != char: 2070 try: 2071 type = csb.core.Enum.parse(SecStructures, currel) 2072 except csb.core.EnumValueError: 2073 raise UnknownSecStructureError(currel) 2074 confidence = None 2075 if conf_string is not None: 2076 confidence = list(conf_string[start : i]) 2077 confidence = list(map(int, confidence)) 2078 motif = SecondaryStructureElement(start + 1, i, type, confidence) 2079 motifs.append(motif) 2080 2081 currel = char 2082 start = i 2083 2084 return motifs
2085 2086 @property
2087 - def start(self):
2088 """ 2089 Start position of the leftmost element 2090 @rtype: int 2091 """ 2092 return self._minstart
2093 2094 @property
2095 - def end(self):
2096 """ 2097 End position of the rightmost element 2098 @rtype: int 2099 """ 2100 return self._maxend
2101
2102 - def clone(self):
2103 """ 2104 @return: a deep copy of the object 2105 """ 2106 return copy.deepcopy(self)
2107
2108 - def to_three_state(self):
2109 """ 2110 Convert to three-state secondary structure (Helix, Strand, Coil). 2111 """ 2112 for e in self: 2113 e.simplify()
2114
2115 - def to_string(self, chain_length=None):
2116 """ 2117 Get back the string representation of the secondary structure. 2118 2119 @return: a string of secondary structure elements 2120 @rtype: str 2121 2122 @bug: [CSB 0000003] If conflicting elements are found at a given rank, 2123 this position is represented as a coil. 2124 """ 2125 gap = str(SecStructures.Gap) 2126 coil = str(SecStructures.Coil) 2127 2128 if chain_length is None: 2129 chain_length = max(e.end for e in self) 2130 2131 ss = [] 2132 2133 for pos in range(1, chain_length + 1): 2134 elements = self.at(pos) 2135 if len(elements) > 0: 2136 if len(set(e.type for e in elements)) > 1: 2137 ss.append(coil) # [CSB 0000003] 2138 else: 2139 ss.append(elements[0].to_string()) 2140 else: 2141 ss.append(gap) 2142 2143 return ''.join(ss)
2144
2145 - def at(self, rank, type=None):
2146 """ 2147 @return: all secondary structure elements covering the specifid position 2148 @rtype: tuple of L{SecondaryStructureElement}s 2149 """ 2150 return self.scan(start=rank, end=rank, filter=type, loose=True, cut=True)
2151
2152 - def scan(self, start, end, filter=None, loose=True, cut=True):
2153 """ 2154 Get all secondary structure elements within the specified [start, end] region. 2155 2156 @param start: the start position of the region, 1-based, inclusive 2157 @type start: int 2158 @param end: the end position of the region, 1-based, inclusive 2159 @type end: int 2160 @param filter: return only elements of the specified L{SecStructures} kind 2161 @type filter: L{csb.core.EnumItem} 2162 @param loose: grab all fully or partially matching elements within the region. 2163 if False, return only the elements which strictly reside within 2164 the region 2165 @type loose: bool 2166 @param cut: if an element is partially overlapping with the start..end region, 2167 cut its start and/or end to make it fit into the region. If False, 2168 return the elements with their real lengths 2169 @type cut: bool 2170 2171 @return: a list of deep-copied L{SecondaryStructureElement}s, sorted by their 2172 start position 2173 @rtype: tuple of L{SecondaryStructureElement}s 2174 """ 2175 matches = [ ] 2176 2177 for m in self: 2178 if filter and m.type != filter: 2179 continue 2180 2181 if loose: 2182 if start <= m.start <= end or start <= m.end <= end or (m.start <= start and m.end >= end): 2183 partmatch = copy.deepcopy(m) 2184 if cut: 2185 if partmatch.start < start: 2186 partmatch.start = start 2187 if partmatch.end > end: 2188 partmatch.end = end 2189 if partmatch.score: 2190 partmatch.score = partmatch.score[start : end + 1] 2191 matches.append(partmatch) 2192 else: 2193 if m.start >= start and m.end <= end: 2194 matches.append(copy.deepcopy(m)) 2195 2196 matches.sort() 2197 return tuple(matches)
2198
2199 - def q3(self, reference, relaxed=True):
2200 """ 2201 Compute Q3 score. 2202 2203 @param reference: reference secondary structure 2204 @type reference: L{SecondaryStructure} 2205 @param relaxed: if True, treat gaps as coils 2206 @type relaxed: bool 2207 2208 @return: the percentage of C{reference} residues with identical 2209 3-state secondary structure. 2210 @rtype: float 2211 """ 2212 2213 this = self.clone() 2214 this.to_three_state() 2215 2216 ref = reference.clone() 2217 ref.to_three_state() 2218 2219 total = 0 2220 identical = 0 2221 2222 def at(ss, rank): 2223 elements = ss.at(rank) 2224 if len(elements) == 0: 2225 return None 2226 elif len(elements) > 1: 2227 raise ValueError('Flat secondary structure expected') 2228 else: 2229 return elements[0]
2230 2231 for rank in range(ref.start, ref.end + 1): 2232 q = at(this, rank) 2233 s = at(ref, rank) 2234 2235 if s: 2236 if relaxed or s.type != SecStructures.Gap: 2237 total += 1 2238 if q: 2239 if q.type == s.type: 2240 identical += 1 2241 elif relaxed: 2242 pair = set([q.type, s.type]) 2243 match = set([SecStructures.Gap, SecStructures.Coil]) 2244 if pair.issubset(match): 2245 identical += 1 2246 2247 if total == 0: 2248 return 0.0 2249 else: 2250 return identical * 100.0 / total
2251
2252 - def subregion(self, start, end):
2253 """ 2254 Same as C{ss.scan(...cut=True)}, but also shift the start-end positions 2255 of all motifs and return a L{SecondaryStructure} instance instead of a list. 2256 2257 @param start: start position of the subregion, with reference to the chain 2258 @type start: int 2259 @param end: start position of the subregion, with reference to the chain 2260 @type end: int 2261 2262 @return: a deep-copy sub-fragment of the original L{SecondaryStructure} 2263 @rtype: L{SecondaryStructure} 2264 """ 2265 sec_struct = SecondaryStructure() 2266 2267 for motif in self.scan(start, end, loose=True, cut=True): 2268 2269 motif.start = motif.start - start + 1 2270 motif.end = motif.end - start + 1 2271 if motif.score: 2272 motif.score = list(motif.score) # this will automatically fix the score indices in the setter 2273 sec_struct.append(motif) 2274 2275 return sec_struct
2276
2277 -class TorsionAnglesCollection(csb.core.CollectionContainer):
2278 """ 2279 Describes a collection of torsion angles. Provides 1-based list-like access. 2280 2281 @param items: an initialization list of L{TorsionAngles} 2282 @type items: list 2283 """
2284 - def __init__(self, items=None, start=1):
2288
2289 - def __repr__(self):
2290 if len(self) > 0: 2291 return "<TorsionAnglesList: {0} ... {1}>".format(self[self.start_index], self[self.last_index]) 2292 else: 2293 return "<TorsionAnglesList: empty>"
2294 2295 @property
2296 - def phi(self):
2297 """ 2298 List of all phi angles 2299 @rtype: list 2300 """ 2301 return [a.phi for a in self]
2302 2303 @property
2304 - def psi(self):
2305 """ 2306 List of all psi angles 2307 @rtype: list 2308 """ 2309 return [a.psi for a in self]
2310 2311 @property
2312 - def omega(self):
2313 """ 2314 List of all omega angles 2315 @rtype: list 2316 """ 2317 return [a.omega for a in self]
2318
2319 - def update(self, angles):
2320 self._update(angles)
2321
2322 - def rmsd(self, other):
2323 """ 2324 Calculate the Circular RSMD against another TorsionAnglesCollection. 2325 2326 @param other: subject (right-hand-term) 2327 @type other: L{TorsionAnglesCollection} 2328 2329 @return: RMSD based on torsion angles 2330 @rtype: float 2331 2332 @raise Broken3DStructureError: on discontinuous torsion angle collections 2333 (phi and psi values are still allowed to be absent at the termini) 2334 @raise ValueError: on mismatching torsion angles collection lengths 2335 """ 2336 if len(self) != len(other) or len(self) < 1: 2337 raise ValueError('Both collections must be of the same and positive length') 2338 2339 length = len(self) 2340 query, subject = [], [] 2341 2342 for n, (q, s) in enumerate(zip(self, other), start=1): 2343 2344 q = q.copy() 2345 q.to_radians() 2346 2347 s = s.copy() 2348 s.to_radians() 2349 2350 if q.phi is None or s.phi is None: 2351 if n == 1: 2352 q.phi = s.phi = 0.0 2353 else: 2354 raise Broken3DStructureError('Discontinuous torsion angles collection at {0}'.format(n)) 2355 2356 if q.psi is None or s.psi is None: 2357 if n == length: 2358 q.psi = s.psi = 0.0 2359 else: 2360 raise Broken3DStructureError('Discontinuous torsion angles collection at {0}'.format(n)) 2361 2362 query.append([q.phi, q.psi]) 2363 subject.append([s.phi, s.psi]) 2364 2365 return csb.bio.utils.torsion_rmsd(numpy.array(query), numpy.array(subject))
2366
2367 -class TorsionAngles(object):
2368 """ 2369 Describes a collection of phi, psi and omega backbone torsion angles. 2370 2371 It is assumed that the supplied values are either None, or fitting into 2372 the range of [-180, +180] for AngleUnites.Degrees and [0, 2pi] for Radians. 2373 2374 @param phi: phi angle value in C{units} 2375 @type phi: float 2376 @param psi: psi angle value in C{units} 2377 @type psi: float 2378 @param omega: omega angle value in C{units} 2379 @type omega: float 2380 @param units: any of L{AngleUnits}'s enum members 2381 @type units: L{csb.core.EnumItem} 2382 2383 @raise ValueError: on invalid/unknown units 2384 """ 2385
2386 - def __init__(self, phi, psi, omega, units=AngleUnits.Degrees):
2387 2388 try: 2389 if isinstance(units, csb.core.string): 2390 units = csb.core.Enum.parse(AngleUnits, units, ignore_case=True) 2391 else: 2392 if units.enum is not AngleUnits: 2393 raise TypeError(units) 2394 2395 except ValueError: 2396 raise ValueError('Unknown angle unit type {0}'.format(units)) 2397 2398 self._units = units 2399 2400 self._phi = None 2401 self._psi = None 2402 self._omega = None 2403 2404 self.phi = phi 2405 self.psi = psi 2406 self.omega = omega
2407
2408 - def __repr__(self):
2409 return "<TorsionAngles: phi={0.phi}, psi={0.psi}, omega={0.omega}>".format(self)
2410
2411 - def __nonzero__(self):
2412 return self.__bool__()
2413
2414 - def __bool__(self):
2415 return self.phi is not None \ 2416 or self.psi is not None \ 2417 or self.omega is not None
2418 2419 @property
2420 - def units(self):
2421 """ 2422 Current torsion angle units - a member of L{AngleUnits} 2423 @rtype: enum item 2424 """ 2425 return self._units
2426 2427 @property
2428 - def phi(self):
2429 return self._phi
2430 @phi.setter
2431 - def phi(self, phi):
2432 TorsionAngles.check_angle(phi, self._units) 2433 self._phi = phi
2434 2435 @property
2436 - def psi(self):
2437 return self._psi
2438 @psi.setter
2439 - def psi(self, psi):
2440 TorsionAngles.check_angle(psi, self._units) 2441 self._psi = psi
2442 2443 @property
2444 - def omega(self):
2445 return self._omega
2446 @omega.setter
2447 - def omega(self, omega):
2448 TorsionAngles.check_angle(omega, self._units) 2449 self._omega = omega
2450
2451 - def copy(self):
2452 """ 2453 @return: a deep copy of C{self} 2454 """ 2455 return TorsionAngles(self.phi, self.psi, self.omega, self.units)
2456
2457 - def to_degrees(self):
2458 """ 2459 Set angle measurement units to degrees. 2460 Convert the angles in this TorsionAngles instance to degrees. 2461 """ 2462 2463 if self._units != AngleUnits.Degrees: 2464 2465 phi = TorsionAngles.deg(self._phi) 2466 psi = TorsionAngles.deg(self._psi) 2467 omega = TorsionAngles.deg(self._omega) 2468 2469 # if no ValueError is raised by TorsionAngles.check_angle in TorsionAngles.deg: 2470 # (we assign directly to the instance variables to avoid check_angle being invoked again in setters) 2471 self._phi, self._psi, self._omega = phi, psi, omega 2472 self._units = AngleUnits.Degrees
2473 2474
2475 - def to_radians(self):
2476 """ 2477 Set angle measurement units to radians. 2478 Convert the angles in this TorsionAngles instance to radians. 2479 """ 2480 2481 if self._units != AngleUnits.Radians: 2482 2483 phi = TorsionAngles.rad(self._phi) 2484 psi = TorsionAngles.rad(self._psi) 2485 omega = TorsionAngles.rad(self._omega) 2486 2487 # if no ValueError is raised by TorsionAngles.check_angle in TorsionAngles.rad: 2488 # (we assign directly to the instance variables to avoid check_angle being invoked again in setters) 2489 self._phi, self._psi, self._omega = phi, psi, omega 2490 self._units = AngleUnits.Radians
2491 2492 @staticmethod
2493 - def check_angle(angle, units):
2494 """ 2495 Check the value of a torsion angle expressed in the specified units. 2496 """ 2497 if angle is None: 2498 return 2499 elif units == AngleUnits.Degrees: 2500 if not (-180 <= angle <= 180): 2501 raise ValueError('Torsion angle {0} is out of range -180..180'.format(angle)) 2502 elif units == AngleUnits.Radians: 2503 if not (0 <= angle <= (2 * math.pi)): 2504 raise ValueError('Torsion angle {0} is out of range 0..2pi'.format(angle)) 2505 else: 2506 raise ValueError('Unknown angle unit type {0}'.format(units))
2507 2508 @staticmethod
2509 - def rad(angle):
2510 """ 2511 Convert a torsion angle value, expressed in degrees, to radians. 2512 Negative angles are converted to their positive counterparts: rad(ang + 360deg). 2513 2514 Return the calculated value in the range of [0, 2pi] radians. 2515 """ 2516 TorsionAngles.check_angle(angle, AngleUnits.Degrees) 2517 2518 if angle is not None: 2519 if angle < 0: 2520 angle += 360. 2521 angle = math.radians(angle) 2522 return angle
2523 2524 @staticmethod
2525 - def deg(angle):
2526 """ 2527 Convert a torsion angle value, expressed in radians, to degrees. 2528 Negative angles are not accepted, it is assumed that negative torsion angles have been 2529 converted to their ang+2pi counterparts beforehand. 2530 2531 Return the calculated value in the range of [-180, +180] degrees. 2532 """ 2533 TorsionAngles.check_angle(angle, AngleUnits.Radians) 2534 2535 if angle is not None: 2536 if angle > math.pi: 2537 angle = -((2. * math.pi) - angle) 2538 angle = math.degrees(angle) 2539 2540 return angle
2541