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
80 """
81 Torsion angle unit types
82 """
83 Degrees='deg'; Radians='rad'
84
91
105
109
112
115
118
121
124
127
130
133
136
139
142
145
148
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
161 """
162 Iterator over all immediate children of the entity
163 @rtype: iterator of L{AbstractEntity}
164 """
165 pass
166
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
188
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
217 """
218 Iterates over composite L{AbstractEntity} hierarchies.
219
220 @param node: root entity to traverse
221 @type node: L{AbstractEntity}
222 """
223
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
236
239
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
256 """
257 Push C{node}'s children to the stack.
258 """
259 self._stack.push(node.items)
260
261 @staticmethod
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
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 """
292
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
311
314
315 @property
318
319 @property
321 """
322 Access Ensembles's models by model ID
323 @rtype: L{EnsembleModelsCollection}
324 """
325 return self._models
326
327 @property
329 return iter(self._models)
330
331 @property
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):
371
373
378
393
394 @property
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 """
421
423 return "<Structure Model {0.model_id}: {0.accession}, {1} chains>".format(self, self.chains.length)
424
425 @property
428
429 @property
431 """
432 Access chains by their chain identifiers
433 @rtype: L{StructureChainsTable}
434 """
435 return self._chains
436
437 @property
439 for chain in self._chains:
440 yield self._chains[chain]
441
442 @property
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
454 """
455 Accession number
456 @rtype: str
457 """
458 return self._accession
459 @accession.setter
466
467 @property
469 """
470 Model ID
471 @rtype: int
472 """
473 return self._model_id
474 @model_id.setter
476 self._model_id = value
477
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):
522
524
525 - def __init__(self, structure=None, chains=None):
532
534 if len(self) > 0:
535 return "<StructureChains: {0}>".format(', '.join(self))
536 else:
537 return "<StructureChains: empty>"
538
539 @property
542
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
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
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 """
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
652
653 @property
655 return self._residues
656
658 return "<Chain {0.id}: {0.type!r}>".format(self)
659
661 return self._residues.length
662
663 @property
665 """
666 Chain's ID
667 @rtype: str
668 """
669 return self._id
670 @id.setter
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
681 """
682 Accession number
683 @rtype: str
684 """
685 return self._accession
686 @accession.setter
693
694 @property
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):
706
707 @property
709 """
710 Rank-based access to Chain's L{Residue}s
711 @rtype: L{ChainResiduesCollection}
712 """
713 return self._residues
714
715 @property
717 return iter(self._residues)
718
719 @property
738
739 @property
741 """
742 True if C{Chain.compute_torsion} had been invoked
743 @rtype: bool
744 """
745 return self._torsion_computed
746
747 @property
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
768 """
769 Chain name
770 @rtype: str
771 """
772 return self._name
773 @name.setter
774 - def name(self, value):
778
779 @property
781 """
782 PDB MOL ID of this chain
783 @rtype: int
784 """
785 return self._molecule_id
786 @molecule_id.setter
788 self._molecule_id = value
789
790 @property
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
815
816 @property
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
826 """
827 Secondary structure (if available)
828 @rtype: L{SecondaryStructure}
829 """
830 return self._secondary_structure
831 @secondary_structure.setter
839
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
936
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
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
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
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
1092
1101
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
1111
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
1130 return id in self.__lookup
1131
1133 if id in self.__lookup:
1134 del self.__lookup[id]
1135
1136 - def _add(self, residue):
1138
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
1185
1187 return '<{1} [{0.rank}]: {0.type!r} {0.id}>'.format(self, self.__class__.__name__)
1188
1189 @property
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):
1201
1202 @property
1204 """
1205 Residue's position in the sequence (1-based)
1206 @rtype: int
1207 """
1208 return self._rank
1209
1210 @property
1212 """
1213 Secondary structure element this residue is part of
1214 @rtype: L{SecondaryStructureElement}
1215 """
1216 return self._secondary_structure
1217 @secondary_structure.setter
1222
1223 @property
1225 """
1226 Torsion angles
1227 @rtype: L{TorsionAngles}
1228 """
1229 return self._torsion
1230 @torsion.setter
1235
1236 @property
1238 """
1239 Access residue's atoms by atom name
1240 @rtype: L{ResidueAtomsTable}
1241 """
1242 return self._atoms
1243
1244 @property
1246 for atom in self._atoms:
1247 yield self._atoms[atom]
1248
1249 @property
1251 """
1252 PDB sequence number (if residue.has_structure is True)
1253 @rtype: int
1254 """
1255 return self._sequence_number
1256
1257 @property
1259 """
1260 PDB insertion code (if defined)
1261 @rtype: str
1262 """
1263 return self._insertion_code
1264
1265 @property
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):
1299
1300 @property
1302 """
1303 True if this residue has any atoms
1304 @rtype: bool
1305 """
1306 return len(self.atoms) > 0
1307
1326
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
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):
1389
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
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):
1483
1485
1486 - def __init__(self, rank, type, sequence_number=None, insertion_code=None):
1490
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):
1504
1506 if len(self) > 0:
1507 return "<ResidueAtoms: {0}>".format(', '.join(self.keys()))
1508 else:
1509 return "<ResidueAtoms: empty>"
1510
1511 @property
1514
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):
1621
1623 return "<Atom [{0.serial_number}]: {0.name}>".format(self)
1624
1627
1632
1644
1653
1654 @property
1656 """
1657 PDB serial number
1658 @rtype: int
1659 """
1660 return self._serial_number
1661 @serial_number.setter
1666
1667 @property
1669 """
1670 PDB atom name (trimmed)
1671 @rtype: str
1672 """
1673 return self._name
1674
1675 @property
1677 """
1678 Chemical element - a member of L{ChemElements}
1679 @rtype: enum item
1680 """
1681 return self._element
1682
1683 @property
1685 """
1686 Residue instance that owns this atom (if available)
1687 @rtype: L{Residue}
1688 """
1689 return self._residue
1690 @residue.setter
1697
1698 @property
1700 """
1701 Atom's 3D coordinates (x, y, z)
1702 @rtype: numpy.array
1703 """
1704 return self._vector
1705 @vector.setter
1707 if numpy.shape(vector) != (3,):
1708 raise ValueError("Three dimensional vector expected")
1709 self._vector = numpy.array(vector)
1710
1711 @property
1713 """
1714 Alternative location flag
1715 @rtype: str
1716 """
1717 return self._alternate
1718 @alternate.setter
1720 self._alternate = value
1721
1722 @property
1724 """
1725 Temperature factor
1726 @rtype: float
1727 """
1728 return self._temperature
1729 @temperature.setter
1731 self._temperature = value
1732
1733 @property
1735 """
1736 Occupancy number
1737 @rtype: float
1738 """
1739 return self._occupancy
1740 @occupancy.setter
1742 self._occupancy = value
1743
1744 @property
1746 """
1747 Charge
1748 @rtype: int
1749 """
1750 return self._charge
1751 @charge.setter
1753 self._charge = value
1754
1755 @property
1758
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
1776
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
1792
1810
1812 return "<DisorderedAtom: {0.length} alternative locations>".format(self)
1813
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
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
1863
1868
1871
1873 return "<{0.type!r}: {0.start}-{0.end}>".format(self)
1874
1875 @property
1877 """
1878 Start position (1-based)
1879 @rtype: int
1880 """
1881 return self._start
1882 @start.setter
1883 - def start(self, value):
1889
1890 @property
1892 """
1893 End position (1-based)
1894 @rtype: int
1895 """
1896 return self._end
1897 @end.setter
1898 - def end(self, value):
1904
1905 @property
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):
1919
1920 @property
1922 """
1923 Number of residues covered by this element
1924 @rtype: int
1925 """
1926 return self.end - self.start + 1
1927
1928 @property
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):
1941
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
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
1998
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):
2019
2022
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
2088 """
2089 Start position of the leftmost element
2090 @rtype: int
2091 """
2092 return self._minstart
2093
2094 @property
2096 """
2097 End position of the rightmost element
2098 @rtype: int
2099 """
2100 return self._maxend
2101
2103 """
2104 @return: a deep copy of the object
2105 """
2106 return copy.deepcopy(self)
2107
2109 """
2110 Convert to three-state secondary structure (Helix, Strand, Coil).
2111 """
2112 for e in self:
2113 e.simplify()
2114
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)
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
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)
2273 sec_struct.append(motif)
2274
2275 return sec_struct
2276
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
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
2297 """
2298 List of all phi angles
2299 @rtype: list
2300 """
2301 return [a.phi for a in self]
2302
2303 @property
2305 """
2306 List of all psi angles
2307 @rtype: list
2308 """
2309 return [a.psi for a in self]
2310
2311 @property
2313 """
2314 List of all omega angles
2315 @rtype: list
2316 """
2317 return [a.omega for a in self]
2318
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
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
2407
2409 return "<TorsionAngles: phi={0.phi}, psi={0.psi}, omega={0.omega}>".format(self)
2410
2413
2415 return self.phi is not None \
2416 or self.psi is not None \
2417 or self.omega is not None
2418
2419 @property
2421 """
2422 Current torsion angle units - a member of L{AngleUnits}
2423 @rtype: enum item
2424 """
2425 return self._units
2426
2427 @property
2430 @phi.setter
2431 - def phi(self, phi):
2434
2435 @property
2438 @psi.setter
2439 - def psi(self, psi):
2442
2443 @property
2446 @omega.setter
2447 - def omega(self, omega):
2450
2456
2473
2474
2491
2492 @staticmethod
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
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
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