Spaces:
No application file
No application file
# Copyright (C) 2002, Thomas Hamelryck ([email protected]) | |
# | |
# This file is part of the Biopython distribution and governed by your | |
# choice of the "Biopython License Agreement" or the "BSD 3-Clause 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 collections import deque | |
from copy import copy | |
import numpy as np | |
from Bio.PDB.PDBExceptions import PDBConstructionException | |
class Entity: | |
"""Basic container object for PDB hierarchy. | |
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.""" | |
yield from self.child_list | |
# 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 | |
def id(self): | |
"""Return identifier.""" | |
return self._id | |
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( | |
f"Cannot change id from `{self._id}` to `{value}`." | |
f" The id `{value}` is already used for a sibling of this entity." | |
) | |
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(f"{entity_id} defined twice") | |
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(f"{entity_id} defined twice") | |
entity.set_parent(self) | |
self.child_list[pos:pos] = [entity] | |
self.child_dict[entity_id] = entity | |
def get_iterator(self): | |
"""Return iterator over children.""" | |
yield from self.child_list | |
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.full_id = self._generate_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 | |
-------- | |
This is an incomplete but illustrative example:: | |
from numpy import pi, array | |
from Bio.PDB.vectors import Vector, rotmat | |
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 center_of_mass(self, geometric=False): | |
"""Return the center of mass of the Entity as a numpy array. | |
If geometric is True, returns the center of geometry instead. | |
""" | |
# Recursively iterate through children until we get all atom coordinates | |
if not len(self): | |
raise ValueError(f"{self} does not have children") | |
maybe_disordered = {"R", "C"} # to know when to use get_unpacked_list | |
only_atom_level = {"A"} | |
entities = deque([self]) # start with [self] to avoid auto-unpacking | |
while True: | |
e = entities.popleft() | |
if e.level in maybe_disordered: | |
entities += e.get_unpacked_list() | |
else: | |
entities += e.child_list | |
elevels = {e.level for e in entities} | |
if elevels == only_atom_level: | |
break # nothing else to unpack | |
coords = np.asarray([a.coord for a in entities], dtype=np.float32) | |
if geometric: | |
masses = None | |
else: | |
masses = np.asarray([a.mass for a in entities], dtype=np.float32) | |
return np.average(coords, axis=0, weights=masses) | |
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: | |
"""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 copy(self): | |
"""Copy disorderd entity recursively.""" | |
shallow = copy(self) | |
shallow.child_dict = {} | |
shallow.detach_parent() | |
for child in self.disordered_get_list(): | |
shallow.disordered_add(child.copy()) | |
return shallow | |
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 disordered_remove(self, child): | |
"""Remove 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()) | |