# -*- coding: utf-8 -*-
"""This module defines :class:`HierView` class that builds a hierarchical
views of atom groups."""
from numpy import unique, zeros, arange, concatenate
from prody.utilities.misctools import count
from .atomgroup import AtomGroup
from .selection import Selection
from .chain import Chain
from .atommap import AtomMap
from .residue import Residue
from .segment import Segment
__all__ = ['HierView']
[docs]class HierView(object):
"""Hierarchical views can be generated for :class:`.AtomGroup`,
:class:`.Selection`, and :class:`.Chain` instances. Indexing a
:class:`HierView` instance returns a :class:`.Chain` instance.
Some :class:`object` methods are customized as follows:
* :func:`len` returns the number of atoms, i.e. :meth:`numChains`
* :func:`iter` yields :class:`.Chain` instances
* indexing by:
- *segment name* (:func:`str`), e.g. ``"PROT"``, returns
a :class:`.Segment`
- *chain identifier* (:func:`str`), e.g. ``"A"``, returns
a :class:`.Chain`
- *[segment name,] chain identifier, residue number[, insertion code]*
(:func:`tuple`), e.g. ``"A", 10`` or ``"A", 10, "B"`` or
``"PROT", "A", 10, "B"``, returns a :class:`.Residue`
Note that when an :class:`.AtomGroup` instance have distinct segments,
they will be considered when building the hierarchical view.
A :class:`.Segment` instance will be generated for each distinct segment
name. Then, for each segment chains and residues will be evaluated.
Having segments in the structure will not change most behaviors of this
class, except indexing. For example, when indexing a hierarchical view
for chain P in segment PROT needs to be indexed as ``hv['PROT', 'P']``."""
def __init__(self, atoms, **kwargs):
if not isinstance(atoms, (AtomGroup, Selection, Chain, Segment, AtomMap)):
raise TypeError('atoms must be an AtomGroup, Selection, Chain, AtomMap or Segment instance')
self._atoms = atoms
self.update(**kwargs)
def __repr__(self):
if self._segments:
return ('<HierView: {0} ({1} segments, {2} chains, {3} '
'residues)>').format(str(self._atoms), self.numSegments(),
self.numChains(), self.numResidues())
else:
return ('<HierView: {0} ({1} chains, {2} residues)>'
).format(str(self._atoms), self.numChains(), self.numResidues())
def __str__(self):
return 'HierView of {0}'.format(str(self._atoms))
def __len__(self):
return len(self._chains)
def __getitem__(self, key):
if isinstance(key, str):
return self.getSegment(key) or self.getChain(key)
elif isinstance(key, tuple):
length = len(key)
if length == 1:
return self.__getitem__(key[0])
if length == 2:
return (self.getChain(key[1], key[0]) or
self.getResidue(*key) or
self.getResidue(None, key[0], key[1]))
if length == 3:
return self.getResidue(*key) or self.getResidue(key[1],
key[2], None, key[0])
if length == 4:
return self.getResidue(key[1], key[2], key[3], key[0])
elif isinstance(key, int):
return self.getResidue(None, key)
def _getSegname(self):
"""Returns name of the segment when there is only one segment."""
if self.numSegments() == 1:
return self._ag._getSegnames()[0]
def _getChid(self):
"""Returns identifier of the chain when there is only one chain."""
if self.numChains() == 1:
return self._ag._getChids()[0]
def _getResidue(self, index):
try:
residue = self._residues[index]
except IndexError:
pass
else:
if residue is not None:
try:
residue.getAtomGroup()
except AttributeError:
residue = self._residues[index] = Residue(self._ag,
residue, self, self._acsi,
unique=True, selstr=self._selstr)
return residue
def _getChain(self, index):
try:
chain = self._chains[index]
except IndexError:
pass
else:
if chain is not None:
try:
chain.getAtomGroup()
except AttributeError:
chain = self._chains[index] = Chain(self._ag,
chain, self, self._acsi,
unique=True, selstr=self._selstr)
return chain
def _getSegment(self, index):
try:
segment = self._segments[index]
except IndexError:
pass
else:
if segment is not None:
try:
segment.getAtomGroup()
except AttributeError:
segment = self._segments[index] = Segment(self._ag,
segment, self, self._acsi,
unique=True, selstr=self._selstr)
return segment
[docs] def getAtoms(self):
"""Returns atoms for which the hierarchical view was built."""
return self._atoms
[docs] def update(self, **kwargs):
"""Update (or build) hierarchical view of atoms. This method is called
at instantiation, but can be used to rebuild the hierarchical view when
attributes of atoms change."""
self._acsi = self._atoms.getACSIndex()
try:
self._ag = self._atoms.getAtomGroup()
except AttributeError:
self._selstr = None
self._update(**kwargs)
else:
self._selhv(**kwargs)
def _selhv(self, **kwargs):
"""Build hierarchical view for :class:`.Selection` instances."""
atoms = self._atoms
ag = self._ag
indices = atoms._getIndices()
self._selstr = atoms.getSelstr()
self._dict = ag.getHierView()._dict
self._segments = _segments = [None] * ag.numSegments()
self._residues = _residues = [None] * ag.numResidues()
self._chains = _chains = [None] * ag.numChains()
for hvidx, _list in [(atoms._getSegindices(), _segments),
(atoms._getChindices(), _chains),
(atoms._getResindices(), _residues),]:
if not _list: continue
pidx = hvidx[0]
pi = 0
for i, idx in enumerate(hvidx):
if pidx == idx: continue
subset = _list[pidx]
if subset is None:
_list[pidx] = indices[pi:i]
else:
_list[pidx] = concatenate((subset, indices[pi:i]))
pidx, pi = idx, i
subset = _list[pidx]
if subset is None:
_list[pidx] = indices[pi:]
else:
_list[pidx] = concatenate((subset, indices[pi:]))
def _update(self, **kwargs):
"""Build hierarchical view for :class:`.AtomGroup` instances."""
ag = self._ag = self._atoms
n_atoms = len(ag)
_indices = arange(n_atoms)
self._dict = _dict = {}
self._residues = _residues = []
self._segments = _segments = []
self._chains = _chains = []
nones = None
getnones = lambda: [None] * n_atoms if nones is None else nones
termini = ag.getFlags('pdbter')
if termini is None:
termini = getnones()
# identify segments
segindex = -1
segindices = zeros(n_atoms, int)
sgnms = ag._getSegnames()
if sgnms is None:
_segments = None
else:
s = sgnms[0]
if len(unique(sgnms)) == 1:
# 1 segment
if s:
_segments.append(_indices)
_dict[s] = 0
else:
_segments = None
else:
ps = None # previous segment name
for i, s in enumerate(sgnms):
if s == ps or s in _dict:
continue
ps = s
idx = _indices[i:][sgnms[i:] == s]
segindex += 1
segindices[idx] = segindex
_dict[s or None] = segindex
_segments.append(idx)
ag._data['segindex'] = segindices
# identify chains
chindex = -1
chindices = zeros(n_atoms, int)
chids = ag._getChids()
if chids is None:
_chains = None
else:
if _segments is None:
if len(unique(chids)) == 1:
_dict[(None, chids[0] or None)] = 0
_chains.append(_indices)
else:
pc = None
for i, c in enumerate(chids):
if c == pc or (None, c) in _dict:
continue
pc = c
idx = _indices[i:][chids[i:] == c]
chindex += 1
chindices[idx] = chindex
_dict[(None, c)] = chindex
_chains.append(idx)
else:
pc = chids[0]
ps = sgnms[0]
_i = 0
for i, c in enumerate(chids):
s = sgnms[i]
if c == pc and s == ps:
continue
s_c = (ps, pc or None)
cid = _dict.get(s_c)
idx = _indices[_i:i]
if cid is None:
#segment = _dict[ps]
chindex += 1
chindices[idx] = chindex
_dict[s_c] = chindex
_chains.append(idx)
else:
chain = _chains[cid]
chindices[idx] = cid
_chains[cid] = concatenate((chain, idx))
pc = c
ps = s
_i = i
s_c = (ps or None, pc or None)
cid = _dict.get(s_c)
idx = _indices[_i:]
if cid is None:
#segment = _dict[ps]
chindex += 1
chindices[idx] = chindex
_dict[s_c] = chindex
_chains.append(idx)
else:
chain = _chains[cid]
chindices[idx] = cid
_chains[cid] = concatenate((chain, idx))
ag._data['chindex'] = chindices
if kwargs.get('chain') == True:
return
# identify residues
resindex = -1
resindices = zeros(n_atoms, int)
rnums = ag._getResnums()
if rnums is None:
raise ValueError('resnums are not set')
if _segments is None:
sgnms = nones = getnones()
if _chains is None:
chids = nones = getnones()
icods = ag._getIcodes()
if icods is None:
icods = nones = getnones()
pr = rnums[0]
pi = icods[0] or None
pc = chids[0]
ps = sgnms[0]
_j = 0
_append = _residues.append
_get = _dict.get
_set = _dict.__setitem__
for j, r in enumerate(rnums):
i = icods[j] or None
c = chids[j]
s = sgnms[j]
if r != pr or i != pi or c != pc or s != ps or (j and termini[j-1]):
s_c_r_i = (ps, pc, pr, pi)
rid = _get(s_c_r_i)
idx = _indices[_j:j]
if (rid is None or isinstance(rid, list) or
termini[_residues[rid][-1]]):
resindex += 1
resindices[idx] = resindex
_append(idx)
if rid is None:
_set(s_c_r_i, resindex)
elif isinstance(rid, list):
rid.append(resindex)
else:
_set(s_c_r_i, [rid, resindex])
else:
residue = _residues[rid]
resindices[idx] = rid
_residues[rid] = concatenate((residue, idx))
ps = s
pc = c
pr = r
pi = i
_j = j
s_c_r_i = (ps, pc, pr, pi)
rid = _get(s_c_r_i)
idx = _indices[_j:]
if rid is None or isinstance(rid, list) or termini[_residues[rid][-1]]:
resindex += 1
resindices[idx] = resindex
_append(idx)
if rid is None:
_set(s_c_r_i, resindex)
elif isinstance(rid, list):
rid.append(resindex)
else:
_set(s_c_r_i, [rid, resindex])
else:
residue = _residues[rid]
resindices[idx] = rid
_residues[rid] = concatenate((residue, idx))
ag._data['resindex'] = resindices
[docs] def getResidue(self, chid, resnum, icode=None, segname=None):
"""Returns residue with number *resnum* and insertion code *icode* from
the chain with identifier *chid* in segment with name *segname*."""
try:
index = self._dict[(segname or self._getSegname(),
chid or self._getChid(),
resnum, icode or None)]
except KeyError:
pass
else:
if isinstance(index, list):
return [r for r in [self._getResidue(i) for i in index]
if r is not None]
else:
return self._getResidue(index)
[docs] def numResidues(self):
"""Returns number of residues."""
return (len(self._residues) if self._ag is self._atoms else
len(self._residues) - count(self._residues, None))
[docs] def iterResidues(self):
"""Yield residues."""
alist = self._residues
ag = self._ag
acsi = self._acsi
selstr = self._selstr
for i, item in enumerate(alist):
if item is None:
continue
try:
item.dtype
except AttributeError:
pass
else:
item = alist[i] = Residue(ag, item, self, acsi, selstr=selstr,
unique=True)
yield item
[docs] def getChain(self, chid, segname=None):
"""Returns chain with identifier *chid*, if it is present."""
try:
index = self._dict[(segname or self._getSegname() or None,
chid or None)]
except KeyError:
try:
index = None
for key in self._dict.keys():
if isinstance(key, tuple) and len(key) == 2:
if key[1] == 'BU':
index = self._dict[key]
if index is None:
raise ValueError('invalid chain ID')
except ValueError:
pass
else:
return self._getChain(index)
else:
return self._getChain(index)
[docs] def iterChains(self):
"""Yield chains."""
alist = self._chains
ag = self._ag
acsi = self._acsi
selstr = self._selstr
for i, item in enumerate(alist):
if item is None:
continue
try:
item.dtype
except AttributeError:
pass
else:
item = alist[i] = Chain(ag, item, self, acsi, selstr=selstr,
unique=True)
yield item
__iter__ = iterChains
[docs] def numChains(self):
"""Returns number of chains."""
return (len(self._chains) if self._ag is self._atoms else
len(self._chains) - count(self._chains, None))
[docs] def getSegment(self, segname):
"""Returns segment with name *segname*, if it is present."""
try:
index = self._dict[segname or None]
except KeyError:
pass
else:
return self._getSegment(index)
[docs] def numSegments(self):
"""Returns number of chains."""
return (len(self._segments) if self._ag is self._atoms else
len(self._segments) - count(self._segments, None))
[docs] def iterSegments(self):
"""Yield segments."""
alist = self._segments
ag = self._ag
acsi = self._acsi
selstr = self._selstr
for i, item in enumerate(alist):
if item is None:
continue
try:
item.dtype
except AttributeError:
pass
else:
item = alist[i] = Segment(ag, item, self, acsi, selstr=selstr,
unique=True)
yield item