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.get_coordinates(what)
963 y = other.get_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.get_coordinates(what)
1019 y = other.get_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.get_coordinates(what)
1051 y = other.get_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.get_coordinates(what)
1086 y = other.get_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 atom._residue = self.__residue
1547 self[atom.name].append(atom)
1548 else:
1549 if atom.name in self:
1550 raise DuplicateAtomIDError('Atom {0} is already defined for {1}'.format(
1551 atom.name, self.__residue))
1552 else:
1553 super(ResidueAtomsTable, self).append(atom.name, atom)
1554 atom._residue = self.__residue
1555
1556 - def update(self, atom_name, atom):
1557 """
1558 Update the atom with the specified name.
1559
1560 @param atom_name: update key
1561 @type atom_name: str
1562 @param atom: new value for this key
1563 @type atom: L{Atom}
1564
1565 @raise ValueError: if C{atom} has a different name than C{atom_name}
1566 """
1567 if atom.name != atom_name:
1568 raise ValueError("Atom's name differs from the specified key.")
1569 if atom.residue is not self.__residue:
1570 atom._residue = self.__residue
1571
1572 super(ResidueAtomsTable, self)._update({atom_name: atom})
1573
1574 -class Atom(AbstractEntity):
1575 """
1576 Represents a single atom in space.
1577
1578 @param serial_number: atom's UID
1579 @type serial_number: int
1580 @param name: atom's name
1581 @type name: str
1582 @param element: corresponding L{ChemElements}
1583 @type element: L{csb.core.EnumItem}
1584 @param vector: atom's coordinates
1585 @type vector: numpy array
1586 @param alternate: if True, means that this is a wobbling atom with multiple alternative
1587 locations
1588 @type alternate: bool
1589 """
1590 - def __init__(self, serial_number, name, element, vector, alternate=False):
1622
1624 return "<Atom [{0.serial_number}]: {0.name}>".format(self)
1625
1628
1633
1645
1654
1655 @property
1657 """
1658 PDB serial number
1659 @rtype: int
1660 """
1661 return self._serial_number
1662 @serial_number.setter
1667
1668 @property
1670 """
1671 PDB atom name (trimmed)
1672 @rtype: str
1673 """
1674 return self._name
1675
1676 @property
1678 """
1679 Chemical element - a member of L{ChemElements}
1680 @rtype: enum item
1681 """
1682 return self._element
1683
1684 @property
1686 """
1687 Residue instance that owns this atom (if available)
1688 @rtype: L{Residue}
1689 """
1690 return self._residue
1691 @residue.setter
1698
1699 @property
1701 """
1702 Atom's 3D coordinates (x, y, z)
1703 @rtype: numpy.array
1704 """
1705 return self._vector
1706 @vector.setter
1708 if numpy.shape(vector) != (3,):
1709 raise ValueError("Three dimensional vector expected")
1710 self._vector = numpy.array(vector)
1711
1712 @property
1714 """
1715 Alternative location flag
1716 @rtype: str
1717 """
1718 return self._alternate
1719 @alternate.setter
1721 self._alternate = value
1722
1723 @property
1725 """
1726 Temperature factor
1727 @rtype: float
1728 """
1729 return self._bfactor
1730 @bfactor.setter
1732 self._bfactor = value
1733
1734 @property
1736 """
1737 Occupancy number
1738 @rtype: float
1739 """
1740 return self._occupancy
1741 @occupancy.setter
1743 self._occupancy = value
1744
1745 @property
1747 """
1748 Charge
1749 @rtype: int
1750 """
1751 return self._charge
1752 @charge.setter
1754 self._charge = value
1755
1756 @property
1759
1761 """
1762 A wobbling atom, which has alternative locations. Each alternative is represented
1763 as a 'normal' L{Atom}. The atom with a highest occupancy is selected as a representative,
1764 hence a DisorderedAtom behaves as a regular L{Atom} (proxy of the representative) as well
1765 as a collection of Atoms.
1766
1767 @param atom: the first atom to be appended to the collection of alternatives. It
1768 is automatically defined as a representative, until a new atom with
1769 higher occupancy is appended to the collection
1770 @type atom: L{Atom}
1771 """
1772
1781
1783 try:
1784 return object.__getattribute__(self, name)
1785 except AttributeError:
1786 subject = object.__getattribute__(self, '_DisorderedAtom__rep')
1787 return getattr(subject, name)
1788
1790 """
1791 Append a new atom to the collection of alternatives.
1792
1793 @param atom: the new alternative
1794 @type atom: L{Atom}
1795 """
1796 self.__update_rep(atom)
1797 self.__alt[atom.alternate] = atom
1798
1799 super(DisorderedAtom, self).append(atom)
1800
1801 - def find(self, altloc):
1802 """
1803 Retrieve a specific atom by its altloc identifier.
1804
1805 @param altloc: alternative location identifier
1806 @type altloc: str
1807
1808 @rtype: L{Atom}
1809 """
1810 if altloc in self.__alt:
1811 return self.__alt[altloc]
1812 else:
1813 for atom in self:
1814 if atom.alternate == altloc:
1815 return Atom
1816
1817 raise EntityNotFoundError(altloc)
1818
1823
1830
1832 return "<DisorderedAtom: {0.length} alternative locations>".format(self)
1833
1835 """
1836 Describes a structural alignment result.
1837
1838 @type rotation: Numpy Array
1839 @type translation: L{Vector}
1840 @type rmsd: float
1841 """
1842 - def __init__(self, rotation, translation, rmsd=None, tm_score=None):
1843
1844 self.rotation = rotation
1845 self.translation = translation
1846 self.rmsd = rmsd
1847 self.tm_score = tm_score
1848
1850 """
1851 Describes a Secondary Structure Element.
1852
1853 @param start: start position with reference to the chain
1854 @type start: float
1855 @param end: end position with reference to the chain
1856 @type end: float
1857 @param type: element type - a member of the L{SecStructures} enum
1858 @type type: csb.core.EnumItem
1859 @param score: secondary structure prediction confidence, if available
1860 @type score: int
1861
1862 @raise IndexError: if start/end positions are out of range
1863 """
1864 - def __init__(self, start, end, type, score=None):
1865
1866 if not (0 < start <= end):
1867 raise IndexError('Element coordinates are out of range: 1 <= start <= end.')
1868
1869 self._start = None
1870 self._end = None
1871 self._type = None
1872 self._score = None
1873
1874 self.start = start
1875 self.end = end
1876 self.type = type
1877
1878 if score is not None:
1879 self.score = score
1880
1883
1888
1891
1893 return "<{0.type!r}: {0.start}-{0.end}>".format(self)
1894
1895 @property
1897 """
1898 Start position (1-based)
1899 @rtype: int
1900 """
1901 return self._start
1902 @start.setter
1903 - def start(self, value):
1909
1910 @property
1912 """
1913 End position (1-based)
1914 @rtype: int
1915 """
1916 return self._end
1917 @end.setter
1918 - def end(self, value):
1924
1925 @property
1927 """
1928 Secondary structure type - a member of L{SecStructures}
1929 @rtype: enum item
1930 """
1931 return self._type
1932 @type.setter
1933 - def type(self, value):
1939
1940 @property
1942 """
1943 Number of residues covered by this element
1944 @rtype: int
1945 """
1946 return self.end - self.start + 1
1947
1948 @property
1950 """
1951 Secondary structure confidence values for each residue
1952 @rtype: L{CollectionContainer}
1953 """
1954 return self._score
1955 @score.setter
1956 - def score(self, scores):
1961
1963 """
1964 Return True if C{self} overlaps with C{other}.
1965
1966 @type other: L{SecondaryStructureElement}
1967 @rtype: bool
1968 """
1969 this = set(range(self.start, self.end + 1))
1970 that = set(range(other.start, other.end + 1))
1971 return not this.isdisjoint(that)
1972
1973 - def merge(self, other):
1974 """
1975 Merge C{self} and C{other}.
1976
1977 @type other: L{SecondaryStructureElement}
1978
1979 @return: a new secondary structure element
1980 @rtype: L{SecondaryStructureElement}
1981
1982 @bug: confidence scores are lost
1983 """
1984 if not self.overlaps(other):
1985 raise ValueError("Can't merge non-overlapping secondary structures")
1986 elif self.type != other.type:
1987 raise ValueError("Can't merge secondary structures of different type")
1988
1989 start = min(self.start, other.start)
1990 end = max(self.end, other.end)
1991 assert self.type == other.type
1992
1993 return SecondaryStructureElement(start, end, self.type)
1994
1996 """
1997 Dump the element as a string.
1998
1999 @return: string representation of the element
2000 @rtype: str
2001 """
2002 return str(self.type) * self.length
2003
2018
2020 """
2021 Describes the secondary structure of a chain.
2022 Provides an index-based access to the secondary structure elements of the chain.
2023
2024 @param string: a secondary structure string (e.g. a PSI-PRED output)
2025 @type string: str
2026 @param conf_string: secondary structure prediction confidence values, if available
2027 @type conf_string: str
2028 """
2029 - def __init__(self, string=None, conf_string=None):
2039
2042
2055
2056 @staticmethod
2057 - def parse(string, conf_string=None):
2058 """
2059 Parse secondary structure from DSSP/PSI-PRED output string.
2060
2061 @param string: a secondary structure string (e.g. a PSI-PRED output)
2062 @type string: str
2063 @param conf_string: secondary structure prediction confidence values, if available
2064 @type conf_string: str
2065
2066 @return: a list of L{SecondaryStructureElement}s.
2067 @rtype: list
2068
2069 @raise ValueError: if the confidence string is not of the same length
2070 """
2071 if not isinstance(string, csb.core.string):
2072 raise TypeError(string)
2073
2074 string = ''.join(re.split('\s+', string))
2075 if conf_string is not None:
2076 conf_string = ''.join(re.split('\s+', conf_string))
2077 if not len(string) == len(conf_string):
2078 raise ValueError('The confidence string has unexpected length.')
2079 motifs = [ ]
2080
2081 if not len(string) > 0:
2082 raise ValueError('Empty Secondary Structure string')
2083
2084 currel = string[0]
2085 start = 0
2086
2087 for i, char in enumerate(string + '.'):
2088
2089 if currel != char:
2090 try:
2091 type = csb.core.Enum.parse(SecStructures, currel)
2092 except csb.core.EnumValueError:
2093 raise UnknownSecStructureError(currel)
2094 confidence = None
2095 if conf_string is not None:
2096 confidence = list(conf_string[start : i])
2097 confidence = list(map(int, confidence))
2098 motif = SecondaryStructureElement(start + 1, i, type, confidence)
2099 motifs.append(motif)
2100
2101 currel = char
2102 start = i
2103
2104 return motifs
2105
2106 @property
2108 """
2109 Start position of the leftmost element
2110 @rtype: int
2111 """
2112 return self._minstart
2113
2114 @property
2116 """
2117 End position of the rightmost element
2118 @rtype: int
2119 """
2120 return self._maxend
2121
2123 """
2124 @return: a deep copy of the object
2125 """
2126 return copy.deepcopy(self)
2127
2129 """
2130 Convert to three-state secondary structure (Helix, Strand, Coil).
2131 """
2132 for e in self:
2133 e.simplify()
2134
2136 """
2137 Get back the string representation of the secondary structure.
2138
2139 @return: a string of secondary structure elements
2140 @rtype: str
2141
2142 @bug: [CSB 0000003] If conflicting elements are found at a given rank,
2143 this position is represented as a coil.
2144 """
2145 gap = str(SecStructures.Gap)
2146 coil = str(SecStructures.Coil)
2147
2148 if chain_length is None:
2149 chain_length = max(e.end for e in self)
2150
2151 ss = []
2152
2153 for pos in range(1, chain_length + 1):
2154 elements = self.at(pos)
2155 if len(elements) > 0:
2156 if len(set(e.type for e in elements)) > 1:
2157 ss.append(coil)
2158 else:
2159 ss.append(elements[0].to_string())
2160 else:
2161 ss.append(gap)
2162
2163 return ''.join(ss)
2164
2165 - def at(self, rank, type=None):
2166 """
2167 @return: all secondary structure elements covering the specifid position
2168 @rtype: tuple of L{SecondaryStructureElement}s
2169 """
2170 return self.scan(start=rank, end=rank, filter=type, loose=True, cut=True)
2171
2172 - def scan(self, start, end, filter=None, loose=True, cut=True):
2173 """
2174 Get all secondary structure elements within the specified [start, end] region.
2175
2176 @param start: the start position of the region, 1-based, inclusive
2177 @type start: int
2178 @param end: the end position of the region, 1-based, inclusive
2179 @type end: int
2180 @param filter: return only elements of the specified L{SecStructures} kind
2181 @type filter: L{csb.core.EnumItem}
2182 @param loose: grab all fully or partially matching elements within the region.
2183 if False, return only the elements which strictly reside within
2184 the region
2185 @type loose: bool
2186 @param cut: if an element is partially overlapping with the start..end region,
2187 cut its start and/or end to make it fit into the region. If False,
2188 return the elements with their real lengths
2189 @type cut: bool
2190
2191 @return: a list of deep-copied L{SecondaryStructureElement}s, sorted by their
2192 start position
2193 @rtype: tuple of L{SecondaryStructureElement}s
2194 """
2195 matches = [ ]
2196
2197 for m in self:
2198 if filter and m.type != filter:
2199 continue
2200
2201 if loose:
2202 if start <= m.start <= end or start <= m.end <= end or (m.start <= start and m.end >= end):
2203 partmatch = copy.deepcopy(m)
2204 if cut:
2205 if partmatch.start < start:
2206 partmatch.start = start
2207 if partmatch.end > end:
2208 partmatch.end = end
2209 if partmatch.score:
2210 partmatch.score = partmatch.score[start : end + 1]
2211 matches.append(partmatch)
2212 else:
2213 if m.start >= start and m.end <= end:
2214 matches.append(copy.deepcopy(m))
2215
2216 matches.sort()
2217 return tuple(matches)
2218
2219 - def q3(self, reference, relaxed=True):
2220 """
2221 Compute Q3 score.
2222
2223 @param reference: reference secondary structure
2224 @type reference: L{SecondaryStructure}
2225 @param relaxed: if True, treat gaps as coils
2226 @type relaxed: bool
2227
2228 @return: the percentage of C{reference} residues with identical
2229 3-state secondary structure.
2230 @rtype: float
2231 """
2232
2233 this = self.clone()
2234 this.to_three_state()
2235
2236 ref = reference.clone()
2237 ref.to_three_state()
2238
2239 total = 0
2240 identical = 0
2241
2242 def at(ss, rank):
2243 elements = ss.at(rank)
2244 if len(elements) == 0:
2245 return None
2246 elif len(elements) > 1:
2247 raise ValueError('Flat secondary structure expected')
2248 else:
2249 return elements[0]
2250
2251 for rank in range(ref.start, ref.end + 1):
2252 q = at(this, rank)
2253 s = at(ref, rank)
2254
2255 if s:
2256 if relaxed or s.type != SecStructures.Gap:
2257 total += 1
2258 if q:
2259 if q.type == s.type:
2260 identical += 1
2261 elif relaxed:
2262 pair = set([q.type, s.type])
2263 match = set([SecStructures.Gap, SecStructures.Coil])
2264 if pair.issubset(match):
2265 identical += 1
2266
2267 if total == 0:
2268 return 0.0
2269 else:
2270 return identical * 100.0 / total
2271
2273 """
2274 Same as C{ss.scan(...cut=True)}, but also shift the start-end positions
2275 of all motifs and return a L{SecondaryStructure} instance instead of a list.
2276
2277 @param start: start position of the subregion, with reference to the chain
2278 @type start: int
2279 @param end: start position of the subregion, with reference to the chain
2280 @type end: int
2281
2282 @return: a deep-copy sub-fragment of the original L{SecondaryStructure}
2283 @rtype: L{SecondaryStructure}
2284 """
2285 sec_struct = SecondaryStructure()
2286
2287 for motif in self.scan(start, end, loose=True, cut=True):
2288
2289 motif.start = motif.start - start + 1
2290 motif.end = motif.end - start + 1
2291 if motif.score:
2292 motif.score = list(motif.score)
2293 sec_struct.append(motif)
2294
2295 return sec_struct
2296
2298 """
2299 Describes a collection of torsion angles. Provides 1-based list-like access.
2300
2301 @param items: an initialization list of L{TorsionAngles}
2302 @type items: list
2303 """
2304 - def __init__(self, items=None, start=1):
2308
2310 if len(self) > 0:
2311 return "<TorsionAnglesList: {0} ... {1}>".format(self[self.start_index], self[self.last_index])
2312 else:
2313 return "<TorsionAnglesList: empty>"
2314
2315 @property
2317 """
2318 List of all phi angles
2319 @rtype: list
2320 """
2321 return [a.phi for a in self]
2322
2323 @property
2325 """
2326 List of all psi angles
2327 @rtype: list
2328 """
2329 return [a.psi for a in self]
2330
2331 @property
2333 """
2334 List of all omega angles
2335 @rtype: list
2336 """
2337 return [a.omega for a in self]
2338
2341
2342 - def rmsd(self, other):
2343 """
2344 Calculate the Circular RSMD against another TorsionAnglesCollection.
2345
2346 @param other: subject (right-hand-term)
2347 @type other: L{TorsionAnglesCollection}
2348
2349 @return: RMSD based on torsion angles
2350 @rtype: float
2351
2352 @raise Broken3DStructureError: on discontinuous torsion angle collections
2353 (phi and psi values are still allowed to be absent at the termini)
2354 @raise ValueError: on mismatching torsion angles collection lengths
2355 """
2356 if len(self) != len(other) or len(self) < 1:
2357 raise ValueError('Both collections must be of the same and positive length')
2358
2359 length = len(self)
2360 query, subject = [], []
2361
2362 for n, (q, s) in enumerate(zip(self, other), start=1):
2363
2364 q = q.copy()
2365 q.to_radians()
2366
2367 s = s.copy()
2368 s.to_radians()
2369
2370 if q.phi is None or s.phi is None:
2371 if n == 1:
2372 q.phi = s.phi = 0.0
2373 else:
2374 raise Broken3DStructureError('Discontinuous torsion angles collection at {0}'.format(n))
2375
2376 if q.psi is None or s.psi is None:
2377 if n == length:
2378 q.psi = s.psi = 0.0
2379 else:
2380 raise Broken3DStructureError('Discontinuous torsion angles collection at {0}'.format(n))
2381
2382 query.append([q.phi, q.psi])
2383 subject.append([s.phi, s.psi])
2384
2385 return csb.bio.utils.torsion_rmsd(numpy.array(query), numpy.array(subject))
2386
2388 """
2389 Describes a collection of phi, psi and omega backbone torsion angles.
2390
2391 It is assumed that the supplied values are either None, or fitting into
2392 the range of [-180, +180] for AngleUnites.Degrees and [0, 2pi] for Radians.
2393
2394 @param phi: phi angle value in C{units}
2395 @type phi: float
2396 @param psi: psi angle value in C{units}
2397 @type psi: float
2398 @param omega: omega angle value in C{units}
2399 @type omega: float
2400 @param units: any of L{AngleUnits}'s enum members
2401 @type units: L{csb.core.EnumItem}
2402
2403 @raise ValueError: on invalid/unknown units
2404 """
2405
2427
2429 return "<TorsionAngles: phi={0.phi}, psi={0.psi}, omega={0.omega}>".format(self)
2430
2433
2435 return self.phi is not None \
2436 or self.psi is not None \
2437 or self.omega is not None
2438
2439 @property
2441 """
2442 Current torsion angle units - a member of L{AngleUnits}
2443 @rtype: enum item
2444 """
2445 return self._units
2446
2447 @property
2450 @phi.setter
2451 - def phi(self, phi):
2454
2455 @property
2458 @psi.setter
2459 - def psi(self, psi):
2462
2463 @property
2466 @omega.setter
2467 - def omega(self, omega):
2470
2476
2493
2494
2511
2512 @staticmethod
2514 """
2515 Check the value of a torsion angle expressed in the specified units.
2516 """
2517 if angle is None:
2518 return
2519 elif units == AngleUnits.Degrees:
2520 if not (-180 <= angle <= 180):
2521 raise ValueError('Torsion angle {0} is out of range -180..180'.format(angle))
2522 elif units == AngleUnits.Radians:
2523 if not (0 <= angle <= (2 * math.pi)):
2524 raise ValueError('Torsion angle {0} is out of range 0..2pi'.format(angle))
2525 else:
2526 raise ValueError('Unknown angle unit type {0}'.format(units))
2527
2528 @staticmethod
2530 """
2531 Convert a torsion angle value, expressed in degrees, to radians.
2532 Negative angles are converted to their positive counterparts: rad(ang + 360deg).
2533
2534 Return the calculated value in the range of [0, 2pi] radians.
2535 """
2536 TorsionAngles.check_angle(angle, AngleUnits.Degrees)
2537
2538 if angle is not None:
2539 if angle < 0:
2540 angle += 360.
2541 angle = math.radians(angle)
2542 return angle
2543
2544 @staticmethod
2546 """
2547 Convert a torsion angle value, expressed in radians, to degrees.
2548 Negative angles are not accepted, it is assumed that negative torsion angles have been
2549 converted to their ang+2pi counterparts beforehand.
2550
2551 Return the calculated value in the range of [-180, +180] degrees.
2552 """
2553 TorsionAngles.check_angle(angle, AngleUnits.Radians)
2554
2555 if angle is not None:
2556 if angle > math.pi:
2557 angle = -((2. * math.pi) - angle)
2558 angle = math.degrees(angle)
2559
2560 return angle
2561