# Copyright (C) 2002, Thomas Hamelryck (thamelry@binf.ku.dk) # This code is part of the Biopython distribution and governed by its # license. Please see the LICENSE file that should have been included # as part of this package. """Base class for Residue, Chain, Model and Structure classes. It is a simple container class, with list and dictionary like properties. """ from copy import copy from Bio.PDB.PDBExceptions import PDBConstructionException class Entity(object): """Basic container object for PDB heirachy. Structure, Model, Chain and Residue are subclasses of Entity. It deals with storage and lookup. """ def __init__(self, id): """Initialize the class.""" self._id = id self.full_id = None self.parent = None self.child_list = [] self.child_dict = {} # Dictionary that keeps additional properties self.xtra = {} # Special methods def __len__(self): """Return the number of children.""" return len(self.child_list) def __getitem__(self, id): """Return the child with given id.""" return self.child_dict[id] def __delitem__(self, id): """Remove a child.""" return self.detach_child(id) def __contains__(self, id): """Check if there is a child element with the given id.""" return id in self.child_dict def __iter__(self): """Iterate over children.""" for child in self.child_list: yield child # Generic id-based comparison methods considers all parents as well as children # Works for all Entities - Atoms have comparable custom operators def __eq__(self, other): """Test for equality. This compares full_id including the IDs of all parents.""" if isinstance(other, type(self)): if self.parent is None: return self.id == other.id else: return self.full_id[1:] == other.full_id[1:] else: return NotImplemented def __ne__(self, other): """Test for inequality.""" if isinstance(other, type(self)): if self.parent is None: return self.id != other.id else: return self.full_id[1:] != other.full_id[1:] else: return NotImplemented def __gt__(self, other): """Test greater than.""" if isinstance(other, type(self)): if self.parent is None: return self.id > other.id else: return self.full_id[1:] > other.full_id[1:] else: return NotImplemented def __ge__(self, other): """Test greater or equal.""" if isinstance(other, type(self)): if self.parent is None: return self.id >= other.id else: return self.full_id[1:] >= other.full_id[1:] else: return NotImplemented def __lt__(self, other): """Test less than.""" if isinstance(other, type(self)): if self.parent is None: return self.id < other.id else: return self.full_id[1:] < other.full_id[1:] else: return NotImplemented def __le__(self, other): """Test less or equal.""" if isinstance(other, type(self)): if self.parent is None: return self.id <= other.id else: return self.full_id[1:] <= other.full_id[1:] else: return NotImplemented def __hash__(self): """Hash method to allow uniqueness (set).""" return hash(self.full_id) # Private methods def _reset_full_id(self): """Reset the full_id (PRIVATE). Resets the full_id of this entity and recursively of all its children based on their ID. """ for child in self: try: child._reset_full_id() except AttributeError: pass # Atoms do not cache their full ids. self.full_id = self._generate_full_id() def _generate_full_id(self): """Generate full_id (PRIVATE). Generate the full_id of the Entity based on its Id and the IDs of the parents. """ entity_id = self.get_id() parts = [entity_id] parent = self.get_parent() while parent is not None: entity_id = parent.get_id() parts.append(entity_id) parent = parent.get_parent() parts.reverse() return tuple(parts) # Public methods @property def id(self): """Return identifier.""" return self._id @id.setter def id(self, value): """Change the id of this entity. This will update the child_dict of this entity's parent and invalidate all cached full ids involving this entity. @raises: ValueError """ if value == self._id: return if self.parent: if value in self.parent.child_dict: raise ValueError( "Cannot change id from `{}` to `{}`. " "The id `{}` is already used for a sibling of" " this entity.".format(self._id, value, value)) del self.parent.child_dict[self._id] self.parent.child_dict[value] = self self._id = value self._reset_full_id() def get_level(self): """Return level in hierarchy. A - atom R - residue C - chain M - model S - structure """ return self.level def set_parent(self, entity): """Set the parent Entity object.""" self.parent = entity self._reset_full_id() def detach_parent(self): """Detach the parent.""" self.parent = None def detach_child(self, id): """Remove a child.""" child = self.child_dict[id] child.detach_parent() del self.child_dict[id] self.child_list.remove(child) def add(self, entity): """Add a child to the Entity.""" entity_id = entity.get_id() if self.has_id(entity_id): raise PDBConstructionException( "%s defined twice" % str(entity_id)) entity.set_parent(self) self.child_list.append(entity) self.child_dict[entity_id] = entity def insert(self, pos, entity): """Add a child to the Entity at a specified position.""" entity_id = entity.get_id() if self.has_id(entity_id): raise PDBConstructionException( "%s defined twice" % str(entity_id)) entity.set_parent(self) self.child_list[pos:pos] = [entity] self.child_dict[entity_id] = entity def get_iterator(self): """Return iterator over children.""" for child in self.child_list: yield child def get_list(self): """Return a copy of the list of children.""" return copy(self.child_list) def has_id(self, id): """Check if a child with given id exists.""" return id in self.child_dict def get_parent(self): """Return the parent Entity object.""" return self.parent def get_id(self): """Return the id.""" return self.id def get_full_id(self): """Return the full id. The full id is a tuple containing all id's starting from the top object (Structure) down to the current object. A full id for a Residue object e.g. is something like: ("1abc", 0, "A", (" ", 10, "A")) This corresponds to: Structure with id "1abc" Model with id 0 Chain with id "A" Residue with id (" ", 10, "A") The Residue id indicates that the residue is not a hetero-residue (or a water) because it has a blank hetero field, that its sequence identifier is 10 and its insertion code "A". """ if self.full_id is None: self._reset_full_id() return self.full_id def transform(self, rot, tran): """Apply rotation and translation to the atomic coordinates. :param rot: A right multiplying rotation matrix :type rot: 3x3 Numeric array :param tran: the translation vector :type tran: size 3 Numeric array Examples -------- >>> rotation = rotmat(pi, Vector(1, 0, 0)) >>> translation = array((0, 0, 1), 'f') >>> entity.transform(rotation, translation) """ for o in self.get_list(): o.transform(rot, tran) def copy(self): """Copy entity recursively.""" shallow = copy(self) shallow.child_list = [] shallow.child_dict = {} shallow.xtra = copy(self.xtra) shallow.detach_parent() for child in self.child_list: shallow.add(child.copy()) return shallow class DisorderedEntityWrapper(object): """Wrapper class to group equivalent Entities. This class is a simple wrapper class that groups a number of equivalent Entities and forwards all method calls to one of them (the currently selected object). DisorderedResidue and DisorderedAtom are subclasses of this class. E.g.: A DisorderedAtom object contains a number of Atom objects, where each Atom object represents a specific position of a disordered atom in the structure. """ def __init__(self, id): """Initialize the class.""" self.id = id self.child_dict = {} self.selected_child = None self.parent = None # Special methods def __getattr__(self, method): """Forward the method call to the selected child.""" if method == '__setstate__': # Avoid issues with recursion when attempting deepcopy raise AttributeError if not hasattr(self, 'selected_child'): # Avoid problems with pickling # Unpickling goes into infinite loop! raise AttributeError return getattr(self.selected_child, method) def __getitem__(self, id): """Return the child with the given id.""" return self.selected_child[id] # XXX Why doesn't this forward to selected_child? # (NB: setitem was here before getitem, iter, len, sub) def __setitem__(self, id, child): """Add a child, associated with a certain id.""" self.child_dict[id] = child def __contains__(self, id): """Check if the child has the given id.""" return id in self.selected_child def __iter__(self): """Return the number of children.""" return iter(self.selected_child) def __len__(self): """Return the number of children.""" return len(self.selected_child) def __sub__(self, other): """Subtraction with another object.""" return self.selected_child - other # Sorting # Directly compare the selected child def __gt__(self, other): """Return if child is greater than other.""" return self.selected_child > other def __ge__(self, other): """Return if child is greater or equal than other.""" return self.selected_child >= other def __lt__(self, other): """Return if child is less than other.""" return self.selected_child < other def __le__(self, other): """Return if child is less or equal than other.""" return self.selected_child <= other # Public methods def get_id(self): """Return the id.""" return self.id def disordered_has_id(self, id): """Check if there is an object present associated with this id.""" return id in self.child_dict def detach_parent(self): """Detach the parent.""" self.parent = None for child in self.disordered_get_list(): child.detach_parent() def get_parent(self): """Return parent.""" return self.parent def set_parent(self, parent): """Set the parent for the object and its children.""" self.parent = parent for child in self.disordered_get_list(): child.set_parent(parent) def disordered_select(self, id): """Select the object with given id as the currently active object. Uncaught method calls are forwarded to the selected child object. """ self.selected_child = self.child_dict[id] def disordered_add(self, child): """Add disordered entry. This is implemented by DisorderedAtom and DisorderedResidue. """ raise NotImplementedError def is_disordered(self): """Return 2, indicating that this Entity is a collection of Entities.""" return 2 def disordered_get_id_list(self): """Return a list of id's.""" # sort id list alphabetically return sorted(self.child_dict) def disordered_get(self, id=None): """Get the child object associated with id. If id is None, the currently selected child is returned. """ if id is None: return self.selected_child return self.child_dict[id] def disordered_get_list(self): """Return list of children.""" return list(self.child_dict.values())