# -*- coding: utf-8 -*-
"""This module defines a class for selecting subsets of atoms. You can read
this page in interactive sessions using ``help(select)``.
ProDy offers a fast and powerful atom selection class, :class:`.Select`.
Selection features, grammar, and keywords are similar to those of VMD.
Small differences, that is described below, should not affect most practical
uses of atom selections. With added flexibility of Python, ProDy selection
engine can also be used to identify intermolecular contacts. You may see
this and other usage examples in :ref:`contacts` and
:ref:`selection-operations`.
First, we import everything from ProDy and parse a protein-DNA-ligand
complex structure:
.. ipython:: python
from prody import *
p = parsePDB('3mht')
:func:`.parsePDB` returns :class:`.AtomGroup` instances, ``p`` in this case,
that stores all atomic data in the file. We can count different types of
atoms using :ref:`flags` and :meth:`~.AtomGroup.numAtoms` method as follows:
.. ipython:: python
p.numAtoms('protein')
p.numAtoms('nucleic')
p.numAtoms('hetero')
p.numAtoms('water')
Last two counts suggest that ligand has 26 atoms, i.e. number of :term:`hetero`
atoms less the number of :term:`water` atoms.
Atom flags
-------------------------------------------------------------------------------
We select subset of atoms by using :meth:`.AtomGroup.select` method.
All :ref:`flags` can be input arguments to this methods as follows:
.. ipython:: python
p.select('protein')
p.select('water')
This operation returns :class:`.Selection` instances, which can be an input
to functions that accepts an *atoms* argument.
Logical operators
-------------------------------------------------------------------------------
Flags can be combined using ``'and'`` and ``'or'`` operators:
.. ipython:: python
p.select('protein and water')
``'protein and water'`` did not result in selection of :term:`protein` and
:term:`water` atoms. This is because, no atom is flagged as a protein and a
water atom at the same time.
.. note::
**Interpreting selection strings**
You may think as if a selection string, such as ``'protein and water'``, is
evaluated on a per atom basis and an atom is selected if it satisfies the
given criterion. To select both water and protein atoms, ``'or'`` logical
operator should be used instead. A protein or a water atom would satisfy
``'protein or water'`` criterion.
.. ipython:: python
p.select('protein or water')
We can also use ``'not'`` operator to negate an atom flag. For example,
the following selection will only select ligand atoms:
.. ipython:: python
p.select('not water and hetero')
If you omit the ``'and'`` operator, you will get the same result:
.. ipython:: python
p.select('not water hetero')
.. note::
**Default operator** between two flags, or other selection tokens that will
be discussed later, is ``'and'``. For example, ``'not water hetero'``
is equivalent to ``'not water and hetero'``.
We can select Cα atoms of acidic residues by omitting the default logical
operator as follows:
.. ipython:: python
sel = p.select('acidic calpha')
sel
set(sel.getResnames())
Quick selections
-------------------------------------------------------------------------------
For simple selections, such as shown above, following may be preferable over
the :meth:`~.AtomGroup.select` method:
.. ipython:: python
p.acidic_calpha
The result is the same as using ``p.select('acidic calpha')``. Underscore,
``_``, is considered as a whitespace. The limitation of this approach is that
special characters cannot be used.
Atom data fields
-------------------------------------------------------------------------------
In addition to :ref:`flags`, :ref:`fields` can be used in atom selections
when combined with some values. For example, we can select Cα and Cβ atoms
of alanine residues as follows:
.. ipython:: python
p.select('resname ALA name CA CB')
Note that we omitted the default ``'and'`` operator.
.. note::
**Whitespace** or **empty string** can be specified using an ``'_'``.
Atoms with string data fields empty, such as those with no a chain
identifiers or alternate location identifiers, can be selected using
an underscore.
.. ipython:: python
p.select('chain _') # chain identifiers of all atoms are specified in 3mht
p.select('altloc _') # altloc identifiers for all atoms are empty
Numeric data fields can also be used to make selections:
.. ipython:: python
p.select('ca resnum 1 2 3 4')
A special case for residues is having insertion codes. Residue numbers and
insertion codes can be specified together as follows:
* ``'resnum 5'`` selects residue 5 (all insertion codes)
* ``'resnum 5A'`` selects residue 5 with insertion code A
* ``'resnum 5_'`` selects residue 5 with no insertion code
Number ranges
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A range of numbers using ``'to'`` or Python style slicing with ``':'``:
.. ipython:: python
p.select('ca resnum 1to4')
p.select('ca resnum 1:4')
p.select('ca resnum 1:4:2')
.. note::
**Number ranges** specify continuous intervals:
* ``'to'`` is all inclusive, e.g. ``'resnum 1 to 4'`` means
``'1 <= resnum <= 4'``
* ``':'`` is left inclusive, e.g. ``'resnum 1:4'`` means
``'1 <= resnum < 4'``
Consecutive use of ``':'``, however, specifies a discrete range of numbers,
e.g. ``'resnum 1:4:2'`` means ``'resnum 1 3'``
Special characters
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Following characters can be specified when using :ref:`fields` for atom
selections::
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
~@#$.:;_',
For example, ``"name C' N` O~ C$ C#"`` is a valid selection string.
.. note::
**Special characters** (``~!@#$%^&*()-_=+[{}]\|;:,<>./?()'"``) must be
escaped using grave accent characters (``````).
Negative numbers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Negative numbers and number ranges must also be escaped using grave accent
characters, since negative sign ``'-'`` is considered a special character
unless it indicates subtraction operation (see below).
.. ipython:: python
p.select('x `-25 to 25`')
p.select('x `-22.542`')
Omitting the grave accent character will cause a :exc:`.SelectionError`.
Regular expressions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Finally, you can specify regular expressions to select atoms based on
data fields with type string. Following will select residues whose names
start with capital letter A
.. ipython:: python
sel = p.select('resname "A.*"')
set(sel.getResnames())
.. note::
**Regular expressions** can be specified using double quotes, ``"..."``.
For more information on regular expressions see :mod:`re`.
Numerical comparisons
-------------------------------------------------------------------------------
:ref:`fields` with numeric types can be used as operands in numerical
comparisons:
.. ipython:: python
p.select('x < 0')
p.select('occupancy = 1')
========== =================================
Comparison Description
========== =================================
< less than
> greater than
<= less than or equal
>= greater than or equal
== equal
= equal
!= not equal
========== =================================
It is also possible to chain comparison statements as follows:
.. ipython:: python
p.select('-10 <= x < 0')
This would be the same as the following selection:
.. ipython:: python
p.select('-10 <= x and x < 0') == p.select('-10 <= x < 0')
Furthermore, numerical comparisons may involve the following operations:
========= ==================================
Operation Description
========= ==================================
x ** y x to the power y
x ^ y x to the power y
x * y x times y
x / y x divided by y
x // y x divided by y (floor division)
x % y x modulo y
x + y x plus y
x - y x minus y
========= ==================================
These operations must be used with a numerical comparison, e.g.
.. ipython:: python
p.select('x ** 2 < 10')
p.select('x ** 2 ** 2 < 10')
Finally, following functions can be used in numerical comparisons:
======== ===================================
Function Description
======== ===================================
abs(x) absolute value of x
acos(x) arccos of x
asin(x) arcsin of x
atan(x) arctan of x
ceil(x) smallest integer not less than x
cos(x) cosine of x
cosh(x) hyperbolic cosine of x
floor(x) largest integer not greater than x
exp(x) e to the power x
log(x) natural logarithm of x
log10(x) base 10 logarithm of x
sin(x) sine of x
sinh(x) hyperbolic sine of x
sq(x) square of x
sqrt(x) square-root of x
tan(x) tangent of x
tanh(x) hyperbolic tangent of x
======== ===================================
.. ipython:: python
p.select('sqrt(sq(x) + sq(y) + sq(z)) < 100') # within 100 A of origin
Distance based selections
-------------------------------------------------------------------------------
Atoms within a user specified distance (A) from a set of user specified atoms
can be selected using ``'within . of .'`` keyword, e.g. ``'within 5 of water'``
selects atoms that are within 5 A of water molecules. This setting will
results selecting water atoms as well.
User can avoid selecting specified atoms using ``exwithin . of ..`` setting,
e.g. ``'exwithin 5 of water'`` will not select water molecules and is
equivalent to ``'within 5 of water and not water'``
.. ipython:: python
p.select('exwithin 5 of water') == p.select('not water within 5 of water')
Sequence selections
-------------------------------------------------------------------------------
One-letter amino acid sequences can be used to make atom selections.
``'sequence SAR'`` will select **SER-ALA-ARG** residues in a chain. Note
that the selection does not consider connectivity within a chain. Regular
expressions can also be used to make selections: ``'sequence "MI.*KQ"'`` will
select **MET-ILE-(XXX)n-ASP-LYS-GLN** pattern, if present.
.. ipython:: python
sel = p.select('ca sequence "MI.*DKQ"')
sel
sel.getResnames()
Expanding selections
-------------------------------------------------------------------------------
A selection can be expanded to include the atoms in the same *residue*,
*chain*, or *segment* using ``same .. as ..`` setting, e.g.
``'same residue as exwithin 4 of water'`` will select residues that have
at least an atom within 4 A of any water molecule.
.. ipython:: python
p.select('same residue as exwithin 4 of water')
Additionally, a selection may be expanded to the immediately bonded atoms using
``bonded [n] to ...`` setting, e.g. ``bonded 1 to calpha`` will select atoms
bonded to Cα atoms. For this setting to work, bonds must be set by the user
using the :meth:`.AtomGroup.setBonds` or :meth:`.AtomGroup.inferBonds` method.
It is also possible to select bonded atoms by excluding the originating atoms
using ``exbonded [n] to ...`` setting. Number ``'[n]'`` indicates number of
bonds to consider from the originating selection.
Selection macros
-------------------------------------------------------------------------------
ProDy allows you to define a macro for any valid selection string. Below
functions are for manipulating selection macros:
* :func:`defSelectionMacro`
* :func:`delSelectionMacro`
* :func:`getSelectionMacro`
* :func:`isSelectionMacro`
.. ipython:: python
defSelectionMacro('alanine', 'resname ALA')
p.select('alanine') == p.select('resname ALA')
You can also use this macro as follows:
.. ipython:: python
p.alanine
Macros are stored in ProDy configuration file permanently. You can delete
them if you wish as follows:
.. ipython:: python
delSelectionMacro('alanine')
Keyword arguments
-------------------------------------------------------------------------------
:meth:`~.Select.select` method also accepts keyword arguments that can simplify
some selections. Consider the following case where you want to select some
protein atoms that are close to its center:
.. ipython:: python
protein = p.protein
calcCenter(protein).round(2)
sel1 = protein.select('sqrt(sq(x--21.17) + sq(y-35.86) + sq(z-79.97)) < 5')
sel1
Instead, you could pass a keyword argument and use the keyword in the
selection string:
.. ipython:: python
sel2 = protein.select('within 5 of center', center=calcCenter(protein))
sel2
sel1 == sel2
Note that selection string for *sel2* lists indices of atoms. This
substitution is performed automatically to ensure reproducibility of the
selection without the keyword *center*.
Keywords cannot be reserved words (see :func:`.listReservedWords`) and must be
all alphanumeric characters."""
import sys
from re import compile as re_compile
try:
# for python>=3.3
from collections.abc import Iterable
except ImportError:
# for python<3.3
from collections import Iterable
import numpy as np
from numpy import array, ndarray, ones, zeros, arange
from numpy import invert, unique, concatenate, all, any
from numpy import logical_and, logical_or, floor, ceil, where
import pyparsing as pp
try:
from pyparsing import operatorPrecedence as operatorPrecedence
except ImportError:
from pyparsing import infixNotation as operatorPrecedence
from prody import LOGGER, SETTINGS, PY2K
from .atomic import Atomic
from .fields import ATOMIC_FIELDS
from .flags import PLANTERS as FLAG_PLANTERS
from .atomgroup import AtomGroup
from .chain import Chain, getSequence
from .atomic import AAMAP
from .pointer import AtomPointer
from .selection import Selection
from .segment import Segment
from .atommap import AtomMap
from prody.utilities import rangeString
from prody.kdtree import KDTree
if PY2K:
range = xrange
DEBUG = 0
NUMB = 0 # Select instance will not really evaluate string for the atoms
TIMER = 0
def debug(sel, loc, *args):
if DEBUG:
print('')
if args:
print(args[0], args[1:])
print(repr(sel))
print(' ' * (loc + 1) + '^')
__all__ = ['Select', 'SelectionError', 'SelectionWarning',
'defSelectionMacro', 'delSelectionMacro', 'getSelectionMacro',
'isSelectionMacro']
ATOMGROUP = None
MACROS = SETTINGS.get('selection_macros', {})
MACROS_REGEX = None
[docs]def isSelectionMacro(word):
"""Returns **True** if *word* is a user defined selection macro."""
try:
return word in MACROS
except:
return False
[docs]def defSelectionMacro(name, selstr):
"""Define selection macro *selstr* with name *name*. Both *name* and
*selstr* must be string. An existing keyword cannot be used as a macro
name. If a macro with given *name* exists, it will be overwritten.
.. ipython:: python
defSelectionMacro('cbeta', 'name CB and protein')"""
if not isinstance(name, str) or not isinstance(selstr, str):
raise TypeError('both name and selstr must be strings')
elif isReserved(name):
raise ValueError('{0} is a reserved word and cannot be used as a '
'macro name'.format(repr(name)))
elif not (name.isalpha() and name.islower()):
raise ValueError('macro names must be all lower case letters, {0} '
'is not a valid macro name'.format(repr(name)))
LOGGER.info('Testing validity of selection string.')
try:
ATOMGROUP.select(selstr)
except SelectionError:
LOGGER.warn('{0} is not a valid selection string, macro {1} is not'
'defined.'.format(repr(selstr), repr(name)))
else:
LOGGER.info("Macro {0} is defined as {1}."
.format(repr(name), repr(selstr)))
MACROS[name] = selstr
SETTINGS['selection_macros'] = MACROS
SETTINGS.save()
[docs]def delSelectionMacro(name):
"""Delete the macro *name*.
.. ipython:: python
delSelectionMacro('cbeta')"""
try:
MACROS.pop(name)
except:
LOGGER.warn("Macro {0} is not found.".format(repr(name)))
else:
if MACROS_REGEX is not None: MACROS_REGEX.pop(name, None)
LOGGER.info("Macro {0} is deleted.".format(repr(name)))
SETTINGS['selection_macros'] = MACROS
SETTINGS.save()
[docs]def getSelectionMacro(name=None):
"""Returns the definition of the macro *name*. If *name* is not given,
returns a copy of the selection macros dictionary."""
if name is None:
return MACROS.copy()
try:
return MACROS[name]
except KeyError:
LOGGER.info("{0} is not a user defined macro name."
.format(repr(name)))
def replaceMacros(selstr):
global MACROS_REGEX
if MACROS_REGEX is None: MACROS_REGEX = {}
selstr = ' ' + selstr + ' '
if MACROS:
for name, macro in MACROS.items(): # PY3K: OK
re = MACROS_REGEX.setdefault(name,
re_compile('[( )]' + name + '[( )]'))
for match in re.finditer(selstr):
start, end = match.start(), match.end()
match = selstr[start:end]
selstr = (selstr[:start] + match[0] + '(' + macro+ ')' +
match[-1] + selstr[end:])
return selstr[1:-1]
def checkSelstr(selstr, what, error=ValueError):
"""Check *selstr* if it satisfies a selected condition. For now, only
whether coordinate/distance based selection are checked. If *error* is
a subclass of :exc:`Exception`, an exception will be raised, otherwise
return **True** or **False** will be returned."""
selstr = selstr.replace('(', ' ( ')
selstr = selstr.replace(')', ' ) ')
if what in set(['dist']):
for item in selstr.split():
if item in XYZDIST:
if issubclass(error, Exception):
raise error('invalid selection {0}, coordinate '
'based selections are not accepted'
.format(repr(selstr)))
else:
return False
[docs]class SelectionError(Exception):
"""Exception raised when there are errors in the selection string."""
def __init__(self, sel, loc=0, msg='', tkns=None):
if tkns:
for tkn in tkns:
tkn = str(tkn)
if sel.count(tkn, loc) == 1: loc = sel.index(tkn, loc)
msg = ("An invalid selection string is encountered:\n{0}\n"
.format(repr(sel)) +
' ' * (loc + 1) + '^ ' + msg)
Exception.__init__(self, msg)
[docs]class SelectionWarning(Warning):
"""A class used for issuing warning messages when potential typos are
detected in a selection string. Warnings are issued to ``sys.stderr``
via ProDy package logger. Use :func:`.confProDy` to selection warnings
*on* or *off*, e.g. ``confProDy(selection_warning=False)``."""
def __init__(self, sel='', loc=0, msg='', tkns=None):
if SETTINGS.get('selection_warning', True):
if tkns:
for tkn in tkns:
tkn = str(tkn)
if sel.count(tkn, loc) == 1: loc = sel.index(tkn, loc)
msg = ('Selection string contains typo(s):\n'
'{0}\n '.format(repr(sel)) +
' ' * loc + '^ ' + msg)
LOGGER.warn(msg)
FIELDS_SYNONYMS = {'chid': 'chain',
'fragment': 'fragindex',
'resid': 'resnum',
'secstr': 'secondary',
'segname': 'segment'}
XYZ2INDEX = {'x': 0, 'y': 1, 'z': 2}
FUNCTIONS = {
'sqrt' : np.sqrt,
'sq' : lambda num: np.power(num, 2),
'abs' : np.abs,
'floor' : np.floor,
'ceil' : np.ceil,
'sin' : np.sin,
'cos' : np.cos,
'tan' : np.tan,
'asin' : np.arcsin,
'acos' : np.arccos,
'atan' : np.arctan,
'sinh' : np.sinh,
'cosh' : np.cosh,
'tahn' : np.tanh,
'exp' : np.exp,
'log' : np.log,
'log10' : np.log10,
}
OPERATORS = {
'+' : np.add,
'-' : np.subtract,
'*' : np.multiply,
'/' : np.divide,
'%' : np.remainder,
'>' : np.greater,
'<' : np.less,
'>=' : np.greater_equal,
'<=' : np.less_equal,
'=' : np.equal,
'==' : np.equal,
'!=' : np.not_equal,
}
SAMEAS_MAP = {'residue': 'resindex', 'chain': 'chindex',
'segment': 'segindex', 'fragment': 'fragindex'}
# maybe a value to a field/data label or field label
XYZ = set(['x', 'y', 'z'])
XYZDIST = set(['x', 'y', 'z', 'within', 'exwithin'])
OR = pp.Keyword('or')
AND = pp.Keyword('and')
WORD = pp.Word(pp.alphanums + '''~@#$.:;_'`,''')
_ = list(FUNCTIONS)
FUNCNAMES = set(_)
kwfunc = pp.Keyword(_[0])
FUNCNAMES_OPLIST = kwfunc
FUNCNAMES_EXPR = ~kwfunc
for func in _[1:]:
kwfunc = pp.Keyword(func)
FUNCNAMES_OPLIST = FUNCNAMES_OPLIST | kwfunc
FUNCNAMES_EXPR += ~kwfunc
RE_SCHARS = re_compile('`[\w\W]*?`')
PP_SCHARS = pp.Regex(RE_SCHARS)
def specialCharsParseAction(sel, loc, token):
token = token[0][1:-1]
if not token:
raise SelectionError(sel, loc, '`` is invalid, no special characters')
if ':' in token or 'to' in token:
try:
token = PP_NRANGE.parseString(token)[0]
except pp.ParseException:
pass
return token
PP_SCHARS.setParseAction(specialCharsParseAction)
RE_REGEXP = re_compile('"[\w\W]*"')
PP_REGEXP = pp.Regex(RE_REGEXP.pattern)
def regularExpParseAction(sel, loc, token):
token = token[0]
if token == '""':
raise SelectionError(sel, loc, '"" is invalid, no regular expression')
try:
regexp = re_compile(token[1:-1])
except:
raise SelectionError(sel, loc, 'failed to compile regular '
'expression {0}'.format(repr(token)))
else:
return regexp
PP_REGEXP.setParseAction(regularExpParseAction)
_ = '[-+]?\d+(\.\d*)?([eE]\d+)?'
RE_NRANGE = re_compile(_ + '\ *(to|:)\ *' + _)
PP_NRANGE = pp.Group(pp.Regex(RE_NRANGE.pattern) +
pp.Optional(pp.Regex('(\ *:\ *' + _ + ')')))
def rangeParseAction(sel, loc, tokens):
tokens = tokens[0]
debug(sel, loc, '_nrange', tokens)
token = tokens[0]
sep = ':' if ':' in token else 'to'
first, last = token.split(sep)
try:
start = int(first)
except ValueError:
start = float(first)
try:
stop = int(last)
except ValueError:
stop = float(last)
if start > stop:
raise SelectionError(sel, loc, 'range start value ({0}) is greater '
'than stop value ({1})'
.format(repr(start), repr(stop)))
elif start == stop:
if sep == ':':
raise SelectionError(sel, loc, 'range start value ({0}) is greater '
'than or equal to stop value ({1})'
.format(repr(start), repr(stop)))
else:
return first
if sep == 'to':
comp = '<='
elif len(tokens) == 1:
comp = '<'
else:
try:
comp = int(tokens[1][1:])
except ValueError:
comp = float(tokens[1][1:])
return 'range', start, stop, comp
PP_NRANGE.setParseAction(rangeParseAction)
UNARY = set(['not', 'bonded', 'exbonded', 'within', 'exwithin', 'same'])
[docs]class Select(object):
"""Select subsets of atoms based on a selection string.
See :mod:`~.atomic.select` module documentation for selection grammar
and examples. This class makes use of pyparsing_ module."""
def __init__(self):
self._ag = None
self._atoms = None
self._indices = None
self._n_atoms = None
self._selstr = None
self._coords = None
self._kwargs = None
# set True when selection string alone cannot reproduce the selection
self._ss2idx = False
self._data = dict()
self._replace = False
self._parsers = {}
self._evalmap = {'resnum': self._resnum, 'resid': self._resnum,
'serial': self._serial, 'index': self._index,
'x': self._generic, 'y': self._generic, 'z': self._generic,
'chid': self._generic, 'secstr': self._generic,
'fragment': self._generic, 'fragindex': self._generic,
'segment': self._generic, 'segname': self._generic,
'sequence': self._sequence, }
def _reset(self):
self._ag = None
self._atoms = None
self._indices = None
self._n_atoms = None
self._coords = None
self._data.clear()
def _evalAtoms(self, atoms):
self._atoms = atoms
try:
self._ag = atoms.getAtomGroup()
except AttributeError:
self._ag = atoms
self._indices = None
else:
self._indices = atoms._getIndices()
if len(self._indices) == 1:
try:
index = atoms.getIndex()
except AttributeError:
pass
else:
self._atoms = Selection(self._ag, array([index]),
'index ' + str(index), atoms.getACSIndex())
[docs] def select(self, atoms, selstr, **kwargs):
"""Returns a :class:`.Selection` of atoms matching *selstr*, or
**None**, if selection string does not match any atoms.
:arg atoms: atoms to be evaluated
:type atoms: :class:`.Atomic`
:arg selstr: selection string
:type selstr: str
Note that, if *atoms* is an :class:`.AtomMap` instance, an
:class:`.AtomMap` is returned, instead of a a :class:`.Selection`.
.. note:
* A special case for making atom selections is passing an
:class:`.AtomMap` instance as *atoms* argument. Dummy
atoms will not be included in the result, but the order
of atoms will be preserved."""
self._ss2idx = False
self._replace = False
self._selstr = selstr
indices = self.getIndices(atoms, selstr, **kwargs)
self._kwargs = None
if len(indices) == 0:
return None
if not isinstance(atoms, AtomGroup):
indices = self._indices[indices]
ag = self._ag
try:
dummies = atoms.numDummies()
except AttributeError:
if self._ss2idx:
selstr = 'index {0}'.format(rangeString(indices))
else:
if self._replace:
for key, value in kwargs.items(): # PY3K: OK
if value in ag and key in selstr:
if value == ag:
ss = 'all'
else:
ss = value.getSelstr()
selstr = selstr.replace(key, '(' + ss + ')')
if isinstance(atoms, AtomPointer):
selstr = '({0}) and ({1})'.format(selstr,
atoms.getSelstr())
return Selection(ag, indices, selstr, atoms.getACSIndex(),
unique=True)
else:
return AtomMap(ag, indices, atoms.getACSIndex(), dummies=dummies,
title='Selection {0} from '.format(repr(selstr)) + str(atoms))
[docs] def getIndices(self, atoms, selstr, **kwargs):
"""Returns indices of atoms matching *selstr*. Indices correspond to
the order in *atoms* argument. If *atoms* is a subset of atoms, they
should not be used for indexing the corresponding :class:`.AtomGroup`
instance."""
ss = selstr.strip()
if (len(ss.split()) == 1 and ss.isalnum() and ss not in MACROS):
self._evalAtoms(atoms)
if ss == 'none':
return array([])
elif ss == 'all':
return arange(atoms.numAtoms())
elif atoms.isFlagLabel(ss):
return atoms._getFlags(ss).nonzero()[0]
elif atoms.isDataLabel(ss) or ss in self._evalmap:
raise SelectionError(selstr, 0, 'must be followed by values',
[ss])
else:
raise SelectionError(selstr, 0, 'is not a valid selection '
'string', [ss])
else:
torf = self.getBoolArray(atoms, selstr, **kwargs)
return torf.nonzero()[0]
[docs] def getBoolArray(self, atoms, selstr, **kwargs):
"""Returns a boolean array with **True** values for *atoms* matching
*selstr*. The length of the boolean :class:`numpy.ndarray` will be
equal to the length of *atoms* argument."""
if not isinstance(atoms, Atomic):
raise TypeError('atoms must be an Atomic instance, not {0}'
.format(type(atoms)))
self._reset()
for key in kwargs:
if not key.isalnum():
raise TypeError('{0} is not a valid keyword argument, '
'keywords must be all alpha numeric '
'characters'.format(repr(key)))
if isReserved(key):
loc = selstr.find(key)
if loc > -1:
raise SelectionError(selstr, loc, '{0} is a reserved '
'word and cannot be used as a keyword argument'
.format(repr(key)))
self._n_atoms = atoms.numAtoms()
self._selstr = selstr
self._kwargs = kwargs
if DEBUG: print('getBoolArray', selstr)
self._evalAtoms(atoms)
selstr = selstr.strip()
if (len(selstr.split()) == 1 and selstr.isalnum() and
selstr not in MACROS):
if selstr == 'none':
return zeros(atoms.numAtoms(), bool)
elif selstr == 'all':
return ones(atoms.numAtoms(), bool)
elif atoms.isFlagLabel(selstr):
return atoms.getFlags(selstr)
elif atoms.isDataLabel(selstr):
raise SelectionError(selstr, 0, 'must be followed by values')
else:
raise SelectionError(selstr, 0, 'is not a valid selection or '
'user data label')
selstr = replaceMacros(selstr)
try:
parser = self._getParser(selstr)
tokens = parser(selstr, parseAll=True)
except pp.ParseException as err:
self._parsers.pop(self._parser, None)
pass
which = selstr.rfind(' ', 0, err.column)
if which > -1:
if selstr[which + 1] == '(':
msg = ('an arithmetic, comparison, or logical operator '
'must precede the opening parenthesis')
elif selstr[which - 1] == ')':
msg = ('an arithmetic, comparison, or logical operator '
'must follow the closing parenthesis')
else:
msg = 'parsing failed here'
else:
msg = 'parsing failed here'
raise SelectionError(selstr, err.column, msg + '\n' + str(err))
else:
if DEBUG: print('_evalSelstr', tokens)
torf = tokens[0]
if not isinstance(torf, ndarray):
if DEBUG: print(torf)
raise SelectionError(selstr)
elif torf.dtype != bool:
if DEBUG:
print('_select torf.dtype', torf.dtype, isinstance(torf.dtype,
bool))
raise SelectionError(selstr)
if DEBUG:
print('_select', torf)
return torf
def _getParser(self, selstr):
"""Returns an efficient parser that can handle *selstr*."""
alnum = selstr
alpha = selstr
for ch in selstr:
if not ch.isalnum(): alnum = alnum.replace(ch, ' ')
if not ch.isalpha(): alpha = alpha.replace(ch, ' ')
items = set(alnum.split())
chars = set(selstr)
funcs = 4 if items.intersection(FUNCNAMES) else 0
opers = 2 if chars.intersection(OPERATORS) else 0
logic = 1 if 'or' in items or '(' in chars else 0
schars = 8 if '`' in chars and RE_SCHARS.search(selstr) else 0
regexp = 16 if '"' in chars and RE_REGEXP.search(selstr) else 0
nrange = 32 if ((':' in chars or ' to ' in alpha) and
RE_NRANGE.search(selstr)) else 0
self._parser = key = (logic + opers + funcs,
logic + funcs + schars + regexp + nrange)
if key == (0, 0):
return self._noParser
try:
return self._parsers[key][0].parseString
except KeyError:
pass
word = ~AND + ~OR
oplist = []
if funcs:
oplist.append((FUNCNAMES_OPLIST, 1, pp.opAssoc.RIGHT, self._func))
# following causes 20% slow down
#word += FUNCNAMES_EXPR
if funcs or opers:
oplist.extend([
(pp.oneOf('+ -'), 1, pp.opAssoc.RIGHT, self._sign),
(pp.oneOf('** ^'), 2, pp.opAssoc.LEFT, self._pow),
(pp.oneOf('* / %'), 2, pp.opAssoc.LEFT, self._binop),
(pp.oneOf('+ -'), 2, pp.opAssoc.LEFT, self._binop),
(pp.oneOf('< > <= >= == = !='), 2, pp.opAssoc.LEFT,
self._comp)])
oplist.extend([
(pp.Optional(AND), 2, pp.opAssoc.LEFT, self._and),
(OR, 2, pp.opAssoc.LEFT, self._or)])
word += WORD
expr = word
if schars: expr = PP_SCHARS | expr
if regexp: expr = PP_REGEXP | expr
if nrange: expr = PP_NRANGE | expr
parser = operatorPrecedence(expr, oplist)
parser.setParseAction(self._default)
parser.leaveWhitespace()
parser.enablePackrat()
self._parsers[key] = parser, expr, oplist
return parser.parseString
def _noParser(self, selstr, parseAll=True):
debug(selstr, 0, ['_noParser'])
return [self._default(selstr, 0, selstr.split())]
def _getZeros(self, subset=None):
"""Returns a bool array with zero elements."""
if subset is None:
return zeros(self._atoms.numAtoms(), bool)
else:
return zeros(len(subset), bool)
def _default(self, sel, loc, tokens):
debug(sel, loc, '_default', tokens)
if NUMB: return
if len(tokens) == 1:
torf, err = self._eval(sel, loc, tokens)
else:
torf, err = self._and2(sel, loc, tokens)
if err: raise err
return torf
def _eval(self, sel, loc, tokens, subset=None):
debug(sel, loc, '_eval', tokens)
if NUMB: return
#if isinstance(tokens, ndarray):
# return tokens
if len(tokens) == 1:
token = tokens[0]
try:
dtype = token.dtype
except AttributeError:
pass
else:
return token, False
if token == 'none':
return zeros(self._n_atoms if subset is None else len(subset),
bool), False
elif token == 'all':
return ones(self._n_atoms if subset is None else len(subset),
bool), False
elif self._atoms.isFlagLabel(token):
return self._getFlags(token, subset), False
elif self._atoms.isDataLabel(token):
data, err = self._getData(sel, loc, token)
if subset is None:
return data, False
else:
return None, SelectionError(sel, loc, 'subset??')
return data[subset], False
try:
arg = self._kwargs[token]
except KeyError:
try:
return float(token), False
except ValueError:
data, err = self._getData(sel, loc, token)
if data is not None:
return data, False
if token in ATOMIC_FIELDS:
return None, SelectionError(sel, loc, '{0} data is '
'not found'.format(repr(token)), [token])
else:
return None, SelectionError(sel, loc, '{0} could '
'not be evaluated'.format(repr(token)), [token])
else:
if arg in self._ag:
try:
dummies = arg.numDummies()
except AttributeError:
try:
indices = arg._getIndices()
except AttributeError:
indices = arange(self._ag.numAtoms())
else:
if dummies:
indices = arg._getIndices()[arg.getFlags('mapped')]
else:
indices = arg._getIndices()
torf = zeros(self._ag.numAtoms(), bool)
torf[indices] = True
if self._indices is not None:
torf = torf[self._indices]
self._replace = True
return torf, False
return token, False
else:
return self._evalmap.get(tokens[0], self._generic)(
sel, loc, tokens, subset)
def _getFlags(self, label, subset):
if subset is None:
# get a copy to avoid alterations
return self._atoms.getFlags(label)
else:
return self._atoms._getFlags(label)[subset]
def _or(self, sel, loc, tokens):
debug(sel, loc, '_or', tokens)
tokens = tokens[0]
if NUMB: return
flags = []
torfs = []
evals = []
atoms = self._atoms
isFlagLabel = atoms.isFlagLabel
isDataLabel = atoms.isDataLabel
for token in tokens:
# check whether token is an array to avoid array == str comparison
try:
dtype = token.dtype
except AttributeError:
if token == 'or':
continue
elif isFlagLabel(token):
flags.append(token)
elif isDataLabel(token) or token in self._evalmap:
evals.append([])
evals[-1].append(token)
else:
try:
evals[-1].append(token)
except IndexError:
raise SelectionError(sel, loc)
else:
if dtype == bool:
torfs.append(token)
else:
try:
evals[-1].append(token)
except IndexError:
raise SelectionError(sel, loc)
torf = None
if torfs:
torf = torfs.pop(0)
while torfs:
ss = where(torf == 0)[0]
if len(ss) == 0: return torf
torf[ss] = torfs.pop(0)[ss]
if flags:
if torf is None:
torf = atoms.getFlags(flags.pop(0))
while flags:
ss = where(torf == 0)[0]
if len(ss) == 0: return torf
torf[ss] = atoms._getFlags(flags.pop(0))[ss]
if evals:
if torf is None:
tokens = evals.pop(0)
first = str(tokens[0])
torf, err = self._eval(sel, loc, tokens)
if err: raise err
try:
dtype = torf.dtype
except AttributeError:
raise SelectionError(sel, loc, 'a problem '
'occurred when evaluating token {0}'
.format(repr(first)), [first])
else:
if dtype != bool:
raise SelectionError(sel, loc, 'a problem '
'occurred when evaluating token {0}'
.format(repr(first)), [first])
while evals:
ss = where(torf == 0)[0]
if len(ss) == 0: return torf
tokens = evals.pop(0)
first = str(tokens[0])
arr, err = self._eval(sel, loc, tokens, subset=ss)
if err: raise err
try:
dtype = arr.dtype
except AttributeError:
raise SelectionError(sel, loc, 'a problem '
'occurred when evaluating token {0}'
.format(repr(first)), [first])
else:
if dtype != bool:
raise SelectionError(sel, loc, 'a problem '
'occurred when evaluating token {0}'
.format(repr(first)), [first])
torf[ss] = arr
return torf
def _and(self, sel, loc, tokens):
debug(sel, loc, '_and', tokens)
tokens = tokens[0]
torf, err = self._and2(sel, loc, tokens)
if err: raise err
return torf
def _and2(self, sel, loc, tokens, subset=None):
debug(sel, loc, '_and2', tokens)
if NUMB: return
firsttoken = tokens[0] if not isinstance(tokens[0], Iterable) else list(tokens[0])
lasttoken = tokens[-1] if not isinstance(tokens[-1], Iterable) else list(tokens[-1])
if firsttoken == 'and' or lasttoken == 'and':
return None, SelectionError(sel, loc, '{0} operator must be '
'surrounded with arguments'.format(repr('and')), [tokens[0]])
flags = []
torfs = []
evals = []
unary = []
atoms = self._atoms
isFlagLabel = atoms.isFlagLabel
isDataLabel = atoms.isDataLabel
append = None
wasand = False
wasdata = False
while tokens:
# check whether token is an array to avoid array == str comparison
token = tokens.pop(0)
try:
dtype = token.dtype
except AttributeError:
if token == 'and':
if wasand:
return None, SelectionError(sel, loc, 'incorrect use '
'of `and` operator, expected {0}'
.format(repr('and ... and')), ['and', 'and'])
append = None
wasand = True
wasdata = False
continue
elif isFlagLabel(token) and not wasdata:
flags.append(token)
append = None
elif (isDataLabel(token) or token in self._evalmap or
token in ATOMIC_FIELDS):
# not evals, must start a new list
# last evals list must have more than one values
#
if not evals:
evals.append([])
append = evals[-1].append
elif len(evals[-1]) == 1:
if token in XYZ:
pass
else:
return None, SelectionError(sel, loc, '{0} '
'must be followed by values'
.format(evals[-1][0]), [evals[-1][0]])
elif token in XYZ:
if (not tokens or tokens[0] in self._evalmap or
tokens[0] in ATOMIC_FIELDS or
isDataLabel(tokens[0]) or tokens[0] == 'and'):
pass
else:
try:
float(tokens[0])
except ValueError as err:
pass
else:
evals.append([])
append = evals[-1].append
elif not tokens:
return None, SelectionError(sel, loc, '{0} '
'must be followed by values'
.format(token), [token])
else:
evals.append([])
append = evals[-1].append
append(token)
wasdata = True
elif token in UNARY:
unary.append([])
append = unary[-1].append
wasdata = False
if token == 'not':
append((token,))
elif token == 'same':
if len(tokens) < 3 or tokens[1] != 'as':
return None, SelectionError(sel, loc, 'incorrect '
'use of `same as` statement, expected {0}'
.format('same entity as ...'), [token])
append((token, tokens.pop(0), tokens.pop(0)))
elif token.endswith('within'):
if len(tokens) < 3 or tokens[1] != 'of':
return None, SelectionError(sel, loc, 'incorrect '
'use of `within` statement, expected {0}'
.format('[ex]within x.y of ...'), [token])
append((token, tokens.pop(0), tokens.pop(0)))
elif token.endswith('bonded'):
token2 = tokens.pop(0)
if len(tokens) < (1 + int(token2 == 'to')):
return None, SelectionError(sel, loc, 'incorrect '
'use of `bonded` statement, expected {0}'
.format('[ex]bonded [n] to ...'), [token2])
if token2 == 'to':
append((token, 'to'))
else:
append((token, token2, tokens.pop(0)))
anyargs = False
while tokens:
next = tokens[0]
try:
dtype = next.dtype
except AttributeError:
append(tokens.pop(0))
if next == 'and' or isFlagLabel(next):
break
if isDataLabel(next) or next in self._evalmap:
if anyargs:
break
else:
anyargs = True
else:
append(tokens.pop(0))
break
else:
try:
append(token)
except TypeError:
return None, SelectionError(sel, loc, 'a problem '
'occurred when evaluation token {0}'
.format(repr(token)), [token])
else:
if dtype == bool:
torfs.append(token)
else:
return None, SelectionError(sel, loc, 'a problem '
'occurred when evaluation token {0}'
.format(repr(token)), [token])
wasand = False
torf = None
if torfs:
torf = torfs.pop(0)
while torfs:
ss = torf.nonzero()[0]
if len(ss) == 0: return torf, False
torf[ss] = torfs.pop(0)[ss]
if flags:
if torf is None:
torf = atoms.getFlags(flags.pop(0))
while flags:
ss = torf.nonzero()[0]
if len(ss) == 0: return torf, False
torf[ss] = atoms._getFlags(flags.pop(0))[ss]
if unary:
if torf is None:
torf, err = self._unary(sel, loc, unary.pop(0))
if err: return None, err
while unary:
ss = torf.nonzero()[0]
if len(ss) == 0: return torf, False
arr, err = self._unary(sel, loc, unary.pop(0))
if err: return None, err
torf[ss] = arr[ss]
if evals:
if torf is None:
tokens = evals.pop(0)
first = str(tokens[0])
torf, err = self._eval(sel, loc, tokens, subset=subset)
if err: return None, err
try:
dtype = torf.dtype
except AttributeError:
return None, SelectionError(sel, loc, 'a problem '
'occurred when evaluating token {0}'
.format(repr(first)), [first])
else:
if dtype != bool:
return None, SelectionError(sel, loc, 'a problem '
'occurred when evaluating token {0}'
.format(repr(first)), [first])
while evals:
ss = torf.nonzero()[0]
if len(ss) == 0: return torf, False
tokens = evals.pop(0)
first = str(tokens[0])
arr, err = self._eval(sel, loc, tokens, subset=ss)
if err: return None, err
try:
dtype = arr.dtype
except AttributeError:
return None, SelectionError(sel, loc, 'a problem '
'occurred when evaluating token {0}'
.format(repr(first)), [first])
else:
if dtype != bool:
return None, SelectionError(sel, loc, 'a problem '
'occurred when evaluating token {0}'
.format(repr(first)), [first])
torf[ss] = arr
# ?? check torf.shape/ndim
if subset is None:
return torf, False
else:
return torf[subset], False
def _unary(self, sel, loc, tokens):
debug(sel, loc, '_unary', tokens)
if NUMB: return
what = tokens[0]
which = tokens[1:]
if not which:
return None, SelectionError(sel, loc, '{0} must be followed by '
.format(repr(' '.join(what))), what)
if len(which) == 1:
which, err = self._eval(sel, loc, which)
else:
which, err = self._and2(sel, loc, which)
if err: raise err
tokens = [what, which]
if what[0] == 'not':
return self._not(sel, loc, tokens)
elif what[0] == 'same':
return self._sameas(sel, loc, tokens)
elif what[-1] == 'to':
return self._bondedto(sel, loc, tokens)
else:
return self._within(sel, loc, tokens)
def _not(self, sel, loc, tokens):
"""Negate selection."""
debug(sel, loc, '_not', tokens)
label, torf = tokens
return invert(torf, torf), False
def _within(self, sel, loc, tokens):
"""Perform distance based selection."""
if DEBUG: print('_within', tokens)
label, which = tokens
within = label[1]
label = ' '.join(label)
try:
within = float(within)
except Exception as err:
return None, SelectionError('could not convert {0} in {1} to '
'float ({2})'.format(within, repr(label), str(err)),
[label, within])
exclude = label.startswith('ex')
other = False
try:
dtype = which.dtype
except AttributeError:
if which in self._kwargs:
coords = self._kwargs[which]
try:
ndim, shape = coords.ndim, coords.shape
except AttributeError:
try:
coords = coords._getCoords()
except AttributeError:
try:
coords = coords.getCoords()
except AttributeError:
return None, SelectionError(sel, loc,
'{0} must be a coordinate array or have '
'`getCoords` method'.format(repr(which)),
[label, which])
if coords is None:
return None, SelectionError(sel, loc,
'coordinates are not set for {0} ({1})'
.format(repr(which), repr(self._kwargs[which])),
[label, which])
else:
ndim, shape = coords.ndim, coords.shape
if ndim == 1 and shape[0] == 3:
coords = array([coords])
elif not (ndim == 2 and shape[1] == 3):
return None, SelectionError(sel, loc,
'{0} must be a coordinate array or have '
'`getCoords` method'.format(repr(which)),
[label, which])
exclude=False
self._ss2idx = True
which = arange(len(coords))
other = True
else:
if dtype == bool:
which = which.nonzero()[0]
coords = self._getCoords()
if coords is None:
return None, SelectionError(sel, loc, 'coordinates are '
'not set')
else:
return None, SelectionError(sel, loc, 'not understood')
if other or len(which) < 20:
kdtree = self._atoms._getKDTree()
get_indices = kdtree.getIndices
search = kdtree.search
get_count = kdtree.getCount
torf = zeros(self._ag.numAtoms(), bool)
for index in which:
search(within, coords[index])
if get_count():
torf[get_indices()] = True
if self._indices is not None:
torf = torf[self._indices]
if exclude:
torf[which] = False
else:
n_atoms = self._atoms.numAtoms()
torf = ones(n_atoms, bool)
torf[which] = False
check = torf.nonzero()[0]
torf = zeros(n_atoms, bool)
cxyz = coords[check]
kdtree = KDTree(coords[which])
search = kdtree.search
get_count = kdtree.getCount
select = []
append = select.append
for i, xyz in enumerate(cxyz):
search(within, xyz)
if get_count():
append(i)
torf[check[select]] = True
if not exclude:
torf[which] = True
return torf, False
def _sameas(self, sel, loc, tokens):
"""Evaluate ``'same entity as ...'`` expression."""
debug(self, loc, '_sameas', tokens)
label, which = tokens
what = label[1]
label = ' '.join(label)
index = SAMEAS_MAP.get(what)
if index is None:
return None, SelectionError(sel, loc, 'entity in "same ... as" '
'must be one of "chain", "residue", "segment", or "fragment",'
' not {0}'.format(repr(what)), [label])
indices, err = self._getData(sel, loc, index)
iset = set(indices[which])
torf = array([i in iset for i in indices], bool)
return torf, False
def _bondedto(self, sel, loc, tokens):
"""Expand selection to immediately bonded atoms."""
debug(sel, loc, '_bondedto', tokens)
label, torf = tokens
token = label[1]
label = ' '.join(label)
if token == 'to':
repeat = 1
else:
try:
repeat = int(token)
except TypeError:
return None, SelectionError(sel, loc, '{0} in {0} could not '
'be converted to an integer'.format(token, repr(label)),
[label])
else:
if float(token) != repeat:
SelectionWarning(sel, loc, 'number in {0} should be an '
'integer'.format(repr(label)), [label])
if repeat <= 0:
SelectionWarning(sel, loc, 'number in {0} should be a '
'positive integer'.format(repr(label)), [label])
return zeros(self._atoms.numAtoms(), bool), False
bmap = self._ag._bmap
if bmap is None:
return None, SelectionError(sel, loc, 'bonds are not set',
[label])
which = torf.nonzero()[0]
if not len(which):
return torf, False
indices = self._indices
if indices is not None:
bmap = bmap[indices]
n_atoms = self._ag.numAtoms()
for i in range(repeat):
torf = zeros(n_atoms, bool)
bonded = unique(bmap[which])
if bonded[0] == -1:
torf[bonded[1:]] = True
else:
torf[bonded] = True
if indices is not None:
torf = torf[indices]
if label.startswith('ex'):
torf[which] = False
else:
torf[which] = True
if i + 1 < repeat:
which = torf.nonzero()[0]
return torf, False
def _getNumeric(self, sel, loc, arg, copy=False):
"""Returns numeric data or a number."""
debug(sel, loc, '_getNumeric', arg)
# arg may be an array, a string, or a regular expression
try:
dtype, ndim = arg.dtype, arg.ndim
except AttributeError:
pass
else:
# i don't expect that a string array may show up
if dtype == bool:
return None, SelectionError(sel, loc, 'operands and function '
'arguments must be numbers or numeric data labels')
else:
return arg, False
# no regular expressions
try:
pattern = arg.pattern
except AttributeError:
pass
else:
return None, SelectionError(sel, sel.index(pattern, loc),
'operands and function arguments cannot be regular expressions')
if arg in XYZ2INDEX:
coords = self._getCoords() # how about atoms._getCoords() ?
if coords is None:
return None, SelectionError(sel, loc,
'coordinates are not set')
else:
if copy:
return coords[:, XYZ2INDEX[arg]].copy(), False
else:
return coords[:, XYZ2INDEX[arg]], False
arg = FIELDS_SYNONYMS.get(arg, arg)
try:
if copy:
data = self._atoms.getData(arg)
else:
data = self._atoms._getData(arg)
except Exception as err:
return None, SelectionError(sel, loc, 'following exception '
'occurred when evaluating {0}: {1}'
.format(repr(arg), str(err)))
if data is not None:
if data.dtype.char in 'US':
return None, SelectionError(sel, loc, '{0} is not a numeric '
'data label'.format(repr(arg)))
else:
return data, False
if arg == 'index':
try:
if copy:
return self._atoms.getIndices(), False
else:
return self._atoms._getIndices(), False
except AttributeError:
return arange(self._atoms.numAtoms()), False
try:
return float(arg), False
except Exception as err:
return None, SelectionError(sel, loc, '{0} is not a number or a '
'numeric data label'
.format(repr(arg), ))
def _comp(self, sel, loc, tokens):
"""Perform comparison."""
tokens = tokens[0]
debug(sel, loc, '_comp', tokens)
if NUMB: return
if len(tokens) >= 3 and len(tokens) % 2 != 1:
raise SelectionError(sel, loc,
'invalid number of operators and operands')
left, err = self._getNumeric(sel, loc, tokens.pop(0))
if err: raise err
torf = None
while tokens:
try:
binop = OPERATORS[tokens.pop(0)]
except KeyError:
raise SelectionError(sel, loc, 'invalid operator encountered')
right, err = self._getNumeric(sel, loc, tokens.pop(0))
if err: raise err
if torf is None:
torf = binop(left, right)
else:
logical_and(binop(left, right), torf, torf)
left = right
# check whether atomic data was contained in comparison
# i.e. len(atoms) == len(torf)
try:
ndim, shape = torf.ndim, torf.shape
except AttributeError:
raise SelectionError(sel, loc,
'comparison must contain atomic data')
else:
if ndim != 1 or shape[0] != self._atoms.numAtoms():
raise SelectionError(sel, loc,
'comparison must contain atomic data')
else:
return torf
def _binop(self, sel, loc, tokens):
"""Perform binary operation."""
tokens = tokens[0]
debug(sel, loc, '_binop', tokens)
if NUMB: return
if len(tokens) >= 3 and len(tokens) % 2 != 1:
raise SelectionError(sel, loc, 'invalid number of items')
left, err = self._getNumeric(sel, loc, tokens.pop(0), copy=True)
if err: raise err
while tokens:
binop = tokens.pop(0)
if binop not in OPERATORS:
raise SelectionError(sel, loc, 'invalid operator encountered')
right, err = self._getNumeric(sel, loc, tokens.pop(0))
if err: raise err
if DEBUG: print(binop, left, right)
if binop == '/' and any(right == 0.0):
raise SelectionError(sel, loc, 'zero division error')
binop = OPERATORS[binop]
try:
ndim = left.ndim
except:
left = binop(left, right)
else:
# ndim must not be zero for in place operation
if ndim:
binop(left, right, left)
else:
left = binop(left, right)
return left
def _pow(self, sel, loc, tokens):
"""Perform power operation. Expected operands are numbers
and numeric atom attributes."""
tokens = tokens[0]
debug(sel, loc, '_pow', tokens)
if NUMB: return
base, err = self._getNumeric(sel, loc, tokens.pop(0))
if err: raise err
power, err = self._getNumeric(sel, loc, tokens.pop())
if err: raise err
if tokens.pop() not in ('^', '**'):
raise SelectionError(sel, loc, 'invalid power operator')
while tokens:
number, err = self._getNumeric(sel, loc, tokens.pop())
if err: raise err
power = number ** power
if tokens.pop() not in ('^', '**'):
raise SelectionError(sel, loc, 'invalid power operator')
return base ** power
def _sign(self, sel, loc, tokens):
"""Change the sign of a selection argument."""
tokens = tokens[0]
debug(sel, loc, '_sign', tokens)
if NUMB: return
if len(tokens) != 2:
raise SelectionError(sel, loc,
'sign operators (+/-) must be followed '
'by single keyword, e.g. "-x", "-beta"')
arg, err = self._getNumeric(sel, loc, tokens[1])
if err: raise err
if tokens[0] == '-':
arg = -arg
return arg
def _func(self, sel, loc, tokens):
"""Evaluate functions used in selection strings."""
tokens = list(tokens[0])
debug(sel, loc, '_func', tokens)
if NUMB: return
if len(tokens) != 2:
raise SelectionError(sel, loc, '{0} accepts a single numeric '
'argument, e.g. {0}(x)'.format(repr(tokens[0])))
arg, err = self._getNumeric(sel, loc, tokens[1])
if err: raise err
debug(sel, loc, tokens[0], arg)
return FUNCTIONS[tokens[0]](arg)
def _generic(self, sel, loc, tokens, subset=None):
debug(sel, loc, '_generic', tokens)
label = tokens.pop(0)
data, err = self._getData(sel, loc, label)
if err: return None, err
if subset is not None:
data = data[subset]
subset = None
dtype = data.dtype
type_ = dtype.type
isstr = dtype.char == 'S' or dtype.char == 'U'
if isstr:
maxlen = int(dtype.str[2:])
torf = None
regexp = []
values = []
ranges = []
valset = True
for token in tokens:
# check for regular expressions which are only compatible with
# string data type
try:
token.pattern
except AttributeError:
pass
else:
if not isstr:
ptrn = '"{0}"'.format(token.pattern)
SelectionWarning(sel, loc, '{0} is a regular '
'expression and is not evaluated for {1}'
.format(ptrn, repr(label)), [label, ptrn])
else:
regexp.append(token)
continue
# check for ranges
if token[0] == 'range':
if isstr:
SelectionWarning(sel, loc, 'number ranges '
'are not evaluated with data type of {0}'
.format(repr(label)), [label, token[1]])
else:
if token[-1] in OPERATORS:
ranges.append(token)
else:
nrange = arange(*token[1:])
# if dtypes are not the same, don't use set method
if nrange.dtype != dtype: valset = False
values.extend(nrange)
continue
if isstr:
if token == '_':
values.append('')
values.append(' ')
else:
if len(token) > maxlen:
SelectionWarning(sel, loc, '{0} is longer than the '
'maximum characters for data field {1}'
.format(repr(token), repr(label)), [label, token])
values.append(token)
else:
try:
value = type_(token)
except Exception as err:
SelectionWarning(sel, loc, '{0} could not be '
'converted to type of {1} ({2})'
.format(repr(token), repr(label), str(err)),
[label, token])
continue
try:
val2 = float(token)
except:
pass
else:
if val2 != value:
SelectionWarning(sel, loc, '{0} has a different '
'values when converted to a float and to type of '
'{1}'.format(repr(token), repr(label)),
[label, token])
values.append(value)
if values:
# use first option only if values and data array has the same dtype
if valset and len(values) > 10:
valset = set(values)
torf = array([val in values for val in data], bool)
else:
torf = data == values.pop(0)
for val in values:
subset = (torf == False).nonzero()[0]
if len(subset) == 0: return torf, False
torf[subset] = data[subset] == val
if ranges:
while ranges:
_, start, stop, comp = ranges.pop(0)
if torf is None:
torf = start <= data
else:
subset = (torf == False).nonzero()[0]
if len(subset) == 0: return torf, False
subdata = data[subset]
torf[subset] = start <= subdata
if subset is None:
torf = logical_and(torf, OPERATORS[comp](data, stop), torf)
else:
torf[subset] = logical_and(torf[subset],
OPERATORS[comp](subdata, stop))
if regexp:
for re in regexp:
if torf is None:
torf = array([re.match(val) is not None
for val in data], bool)
else:
subset = (torf == False).nonzero()[0]
if len(subset) == 0: return torf, False
torf[subset] = [re.match(val) is not None
for val in data[subset]]
if torf is None:
torf = self._getZeros(subset)
return torf, False
def _index(self, sel, loc, tokens, subset=None):
debug(sel, loc, '_index', tokens)
label = tokens.pop(0)
torf = zeros(self._ag.numAtoms(), bool)
for token in tokens:
try:
remainder = token % 1.
except TypeError:
pass
else:
return None, SelectionError(sel, loc, 'it is a number, index')
if remainder == 0:
try:
torf[token] = True
except IndexError:
pass
else:
SelectionWarning(sel, loc, '{0} must be followed by '
'integers and/or number ranges'.format(repr(label)),
[label, token])
continue
try:
token.pattern
except AttributeError:
pass
else:
ptrn = '"{0}"'.format(token.pattern)
SelectionWarning(sel, loc, '{0} is a regular '
'expression and is not evaluated for {1}'
.format(ptrn, repr(label)), [label, ptrn])
continue
if token[0] == 'range':
_, start, stop, step = token
if step in OPERATORS:
if step == '<=':
stop += 1
step = 1
if start % 1.0 != 0 or stop % 1.0 != 0 or step % 1.0 != 0:
SelectionWarning(sel, loc, '{0} number ranges should be '
'specified by integers'.format(repr(label)),
[label, str(start)])
torf[start:stop:step] = True
continue
try:
val = float(token)
except TypeError:
return None, SelectionError(sel, loc, '{0} must be '
'followed by integers and/or number ranges',
[label, token])
else:
if val % 1.0 == 0:
try:
torf[int(val)] = True
except IndexError:
pass
else:
SelectionWarning(sel, loc, '{0} must be followed by '
'integers and/or number ranges'.format(repr(label)),
[label, token])
try:
indices = self._atoms._getIndices()
except AttributeError:
pass
else:
torf = torf[indices]
if subset is not None:
torf = torf[subset]
return torf, False
def _serial(self, sel, loc, tokens, subset=None):
debug(sel, loc, '_serial', tokens)
label = tokens.pop(0)
sn2i = self._ag._getSN2I()
if sn2i is None:
return None, SelectionError(sel, loc, 'serial numbers are not set',
['serial'])
torf = zeros(len(sn2i), bool)
for token in tokens:
try:
remainder = token % 1.
except TypeError:
pass
else:
return None, SelectionError(sel, loc, '??? it is a number, serial')
if remainder == 0:
try:
torf[token] = True
except IndexError:
pass
else:
SelectionWarning(sel, loc, '{0} must be followed by '
'integers and/or number ranges'.format(repr(label)),
[label, token])
continue
try:
pattern = token.pattern
except AttributeError:
pass
else:
ptrn = '"{0}"'.format(pattern)
SelectionWarning(sel, loc, '{0} is a regular '
'expression and is not evaluated for {1}'
.format(ptrn, repr(label)), [label, ptrn])
continue
if token[0] == 'range':
_, start, stop, step = token
if step in OPERATORS:
if step == '<=':
stop += 1
step = 1
if start % 1.0 != 0 or stop % 1.0 != 0 or step % 1.0 != 0:
SelectionWarning(sel, loc, '{0} number ranges should be '
'specified by integers'.format(repr(label)),
[label, str(start)])
torf[start:stop:step] = True
continue
try:
val = float(token)
except TypeError:
return None, SelectionError(sel, loc, '{0} must be '
'followed by integers and/or number ranges',
[label, token])
else:
if val % 1.0 == 0:
try:
torf[int(val)] = True
except IndexError:
pass
else:
SelectionWarning(sel, loc, '{0} must be followed by '
'integers and/or number ranges'.format(repr(label)),
[label, token])
indices = sn2i[torf]
indices = indices[where(indices != -1)[0]]
if len(indices) == 0:
return self._getZeros(subset), False
torf = zeros(self._ag.numAtoms(), bool)
torf[indices] = True
try:
indices = self._atoms._getIndices()
except AttributeError:
pass
else:
torf = torf[indices]
if subset is not None:
torf = torf[subset]
return torf, False
def _resnum(self, sel, loc, tokens, subset=None):
debug(sel, loc, '_resnum', tokens)
label = tokens.pop(0)
resnums, err = self._getData(sel, loc, 'resnum')
if err: return None, err
wicode = set([])
values = ['resnum']
for token in tokens:
try:
pattern = token.pattern
except AttributeError:
pass
else:
ptrn = '"{0}"'.format(pattern)
SelectionWarning(sel, loc, '{0} is a regular expression and '
'its use with {1} is not recommended'
.format(ptrn, repr(label)), [label, ptrn])
continue
if token[0] == 'range':
values.append(token)
continue
try:
value = float(token)
except (TypeError, ValueError):
icode = token[-1]
value = token[:-1]
try:
value = int(value)
except:
SelectionWarning(sel, loc, '{0} must be followed by '
'integers, number ranges, or integer and insertion '
'code combinations, e.g. {1}'
.format(repr(label), repr('10A')),
[label, token])
else:
wicode.add((value, '' if icode == '_' else icode))
else:
if value != int(token):
SelectionWarning(sel, loc, '{0} must be followed by '
'integers and/or number ranges'.format(repr(label)),
[label, token])
values.append(token)
torf = None
if len(values) > 1:
torf, _ = self._generic(sel, loc, values, subset)
if wicode:
icode, err = self._getData(sel, loc, 'icode')
if err: return None, err
if subset is None:
rnic = zip(resnums, icode) # PY3K: OK
else:
rnic = zip(resnums[subset], icode[subset]) # PY3K: OK
if torf is None:
torf = array([val in wicode for val in rnic], bool)
else:
torf = logical_or(torf, array([val in wicode for val in rnic],
bool), torf)
return torf, False
def _sequence(self, sel, loc, tokens, subset=None):
debug(sel, loc, '_sequence', tokens)
label = tokens.pop(0)
regexp = []
for token in tokens:
try:
token.pattern
except AttributeError:
if not token.isalpha() or not token.isupper():
SelectionWarning(sel, loc, '{0} does not look like a '
'valid sequence'.format(repr(token)),
[label, token])
try:
token = re_compile(token)
except Exception as err:
SelectionWarning(sel, loc, '{0} could not be compiled '
'as a regular expression for sequence evaluation'
.format(repr(token)), [label, token])
else:
regexp.append(token)
else:
regexp.append(token)
if not regexp:
return self._getZeros(subset), False
calpha = self._atoms.calpha
if calpha is None:
return self._getZeros(subset), False
matches = []
for chain in iter(HierView(calpha)):
sequence = chain.getSequence()
indices = chain._getIndices()
for re in regexp:
for match in re.finditer(sequence):
matches.extend(indices[match.start():match.end()])
if matches:
torf = zeros(self._ag.numAtoms(), bool)
torf[matches] = True
if self._indices is not None:
torf = torf[self._indices]
torf = self._sameas(sel, loc, [('same', 'residue', 'as'), torf])[0]
if subset is None:
return torf, False
else:
return torf[subset], False
else:
return self._getZeros(subset), False
def _getData(self, sel, loc, keyword):
"""Returns atomic data."""
data = self._data.get(keyword)
if data is not None:
return data, False
try:
idx = XYZ2INDEX[keyword]
except KeyError:
pass
else:
data = self._getCoords()
if data is not None:
data = data[:,idx]
self._data[keyword] = data
return data, False
if keyword == 'index':
try:
data = self._atoms._getIndices()
except AttributeError:
data = arange(self._atoms.numAtoms())
self._data['index'] = data
return data, False
field = ATOMIC_FIELDS.get(FIELDS_SYNONYMS.get(keyword, keyword))
if field is None:
data = self._atoms._getData(keyword)
if data is None:
return None, SelectionError(sel, loc, '{0} is not a valid '
'data label'.format(repr(keyword)))
elif not isinstance(data, ndarray) and data.ndim == 1:
return None, SelectionError(sel, loc, '{0} is not a 1d '
'array'.format(repr(keyword)))
else:
try:
data = getattr(self._atoms, '_get' + field.meth_pl)()
except Exception as err:
return None, SelectionError(sel, loc, str(err))
if data is None:
return None, SelectionError(sel, loc, '{0} is not set'
.format(repr(keyword)))
self._data[keyword] = data
return data, False
def _getCoords(self):
"""Returns coordinates of atoms."""
if self._coords is None:
self._coords = self._atoms._getCoords()
return self._coords
def _getAnisous(self):
"""Returns anisotropic temperature factors of atoms."""
if self._anisous is None:
self._anisous = self._atoms._getAnisous()
return self._anisous