1# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*-
2# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8
3#
4# MDAnalysis --- https://www.mdanalysis.org
5# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors
6# (see the file AUTHORS for the full list of names)
7#
8# Released under the GNU Public Licence, v2 or any higher version
9#
10# Please cite your use of MDAnalysis in published work:
11#
12# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler,
13# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein.
14# MDAnalysis: A Python package for the rapid analysis of molecular dynamics
15# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th
16# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy.
17#
18# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein.
19# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations.
20# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787
21#
22
23"""\
24Topology attribute objects --- :mod:`MDAnalysis.core.topologyattrs`
25===================================================================
26
27Common :class:`TopologyAttr` instances that are used by most topology
28parsers.
29
30TopologyAttrs are used to contain attributes such as atom names or resids.
31These are usually read by the TopologyParser.
32"""
33from __future__ import division, absolute_import
34import six
35from six.moves import zip, range
36
37import Bio.Seq
38import Bio.SeqRecord
39import Bio.Alphabet
40from collections import defaultdict
41import copy
42import functools
43import itertools
44import numbers
45import numpy as np
46import warnings
47
48from numpy.lib.utils import deprecate
49
50from . import flags
51from ..lib.util import cached, convert_aa_code, iterable, warn_if_not_unique
52from ..lib import transformations, mdamath
53from ..exceptions import NoDataError, SelectionError
54from .topologyobjects import TopologyGroup
55from . import selection
56from .groups import (ComponentBase, GroupBase,
57                     Atom, Residue, Segment,
58                     AtomGroup, ResidueGroup, SegmentGroup)
59from .. import _TOPOLOGY_ATTRS
60
61
62def _check_length(func):
63    """Wrapper which checks the length of inputs to set_X
64
65    Eg:
66
67    @_check_length
68    def set_X(self, group, values):
69
70    Will check the length of *values* compared to *group* before proceeding with
71    anything in the *set_X* method.
72
73    Pseudo code for the check:
74
75    if group in (Atom, Residue, Segment):
76        values must be single values, ie int, float or string
77    else:
78        values must be single value OR same length as group
79
80    """
81    _SINGLE_VALUE_ERROR = ("Setting {cls} {attrname} with wrong sized input. "
82                           "Must use single value, length of supplied values: {lenvalues}.")
83    # Eg "Setting Residue resid with wrong sized input. Must use single value, length of supplied
84    # values: 2."
85
86    _GROUP_VALUE_ERROR = ("Setting {group} {attrname} with wrong sized array. "
87                          "Length {group}: {lengroup}, length of supplied values: {lenvalues}.")
88    # Eg "Setting AtomGroup masses with wrong sized array. Length AtomGroup: 100, length of
89    # supplied values: 50."
90
91    def _attr_len(values):
92        # quasi len measurement
93        # strings, floats, ints are len 0, ie not iterable
94        # other iterables are just len'd
95        if iterable(values):
96            return len(values)
97        else:
98            return 0  # special case
99
100    @functools.wraps(func)
101    def wrapper(attr, group, values):
102        val_len = _attr_len(values)
103
104        if isinstance(group, ComponentBase):
105            if not val_len == 0:
106                raise ValueError(_SINGLE_VALUE_ERROR.format(
107                    cls=group.__class__.__name__, attrname=attr.singular,
108                    lenvalues=val_len))
109        else:
110            if not (val_len == 0 or val_len == len(group)):
111                raise ValueError(_GROUP_VALUE_ERROR.format(
112                    group=group.__class__.__name__, attrname=attr.attrname,
113                    lengroup=len(group), lenvalues=val_len))
114        # if everything went OK, continue with the function
115        return func(attr, group, values)
116
117    return wrapper
118
119
120def _wronglevel_error(attr, group):
121    """Generate an error for setting attr at wrong level
122
123    attr : TopologyAttr that was accessed
124    group : Offending Component/Group
125
126    Eg:
127    setting mass of residue, gets called with attr=Masses, group=residue
128
129    raises a NotImplementedError with:
130    'Cannot set masses from Residue.  Use 'Residue.atoms.masses'
131
132    Mainly used to ensure consistent and helpful error messages
133    """
134    if isinstance(group, (Atom, AtomGroup)):
135        group_level = 1
136    elif isinstance(group, (Residue, ResidueGroup)):
137        group_level = 2
138    elif isinstance(group, (Segment, SegmentGroup)):
139        group_level = 3
140
141    # What level to go to before trying to set this attr
142    if isinstance(attr, AtomAttr):
143        corr_classes = ('atoms', 'atom')
144        attr_level = 1
145    elif isinstance(attr, ResidueAttr):
146        corr_classes = ('residues', 'residue')
147        attr_level = 2
148    elif isinstance(attr, SegmentAttr):
149        corr_classes = ('segments', 'segment')
150        attr_level = 3
151
152    if isinstance(group, ComponentBase) and (attr_level > group_level):
153        # ie going downards use plurals, going upwards use singulars
154        # Residue.atom!s!.mass!es! but Atom.segment!!.segid!!
155        correct = corr_classes[1]
156        attrname = attr.singular
157    else:
158        correct = corr_classes[0]
159        attrname = attr.attrname
160
161    err_msg = "Cannot set {attr} from {cls}. Use '{cls}.{correct}.{attr} = '"
162    # eg "Cannot set masses from Residue.  'Use Residue.atoms.masses = '"
163
164    return NotImplementedError(err_msg.format(
165        attr=attrname, cls=group.__class__.__name__, correct=correct,
166    ))
167
168
169class _TopologyAttrMeta(type):
170    # register TopologyAttrs
171    def __init__(cls, name, bases, classdict):
172        type.__init__(type, name, bases, classdict)
173        for attr in ['attrname', 'singular']:
174            try:
175                attrname = classdict[attr]
176            except KeyError:
177                pass
178            else:
179                _TOPOLOGY_ATTRS[attrname] = cls
180
181
182class TopologyAttr(six.with_metaclass(_TopologyAttrMeta, object)):
183    """Base class for Topology attributes.
184
185    Note
186    ----
187    This class is intended to be subclassed, and mostly amounts to
188    a skeleton. The methods here should be present in all
189    :class:`TopologyAttr` child classes, but by default they raise
190    appropriate exceptions.
191
192
193    Attributes
194    ----------
195    attrname : str
196        the name used for the attribute when attached to a ``Topology`` object
197    singular : str
198        name for the attribute on a singular object (Atom/Residue/Segment)
199    per_object : str
200        If there is a strict mapping between Component and Attribute
201    dtype : int/float/object
202        Type to coerce this attribute to be.  For string use 'object'
203    top : Topology
204        handle for the Topology object TopologyAttr is associated with
205
206    """
207    attrname = 'topologyattrs'
208    singular = 'topologyattr'
209    per_object = None  # ie Resids per_object = 'residue'
210    top = None  # pointer to Topology object
211    transplants = defaultdict(list)
212    target_classes = []
213
214    groupdoc = None
215    singledoc = None
216    dtype = None
217
218    def __init__(self, values, guessed=False):
219        if self.dtype is None:
220            self.values = values
221        else:
222            self.values = np.asarray(values, dtype=self.dtype)
223        self._guessed = guessed
224
225    @staticmethod
226    def _gen_initial_values(n_atoms, n_residues, n_segments):
227        """Populate an initial empty data structure for this Attribute
228
229        The only provided parameters are the "shape" of the Universe
230
231        Eg for charges, provide np.zeros(n_atoms)
232        """
233        raise NotImplementedError("No default values")
234
235    @classmethod
236    def from_blank(cls, n_atoms=None, n_residues=None, n_segments=None,
237                   values=None):
238        """Create a blank version of this TopologyAttribute
239
240        Parameters
241        ----------
242        n_atoms : int, optional
243          Size of the TopologyAttribute atoms
244        n_residues: int, optional
245          Size of the TopologyAttribute residues
246        n_segments : int, optional
247          Size of the TopologyAttribute segments
248        values : optional
249          Initial values for the TopologyAttribute
250        """
251        if values is None:
252            values = cls._gen_initial_values(n_atoms, n_residues, n_segments)
253        elif cls.dtype is not None:
254            # if supplied starting values and statically typed
255            values = np.asarray(values, dtype=cls.dtype)
256        return cls(values)
257
258    def copy(self):
259        """Return a deepcopy of this attribute"""
260        return self.__class__(self.values.copy(), guessed=self._guessed)
261
262    def __len__(self):
263        """Length of the TopologyAttr at its intrinsic level."""
264        return len(self.values)
265
266    def __getitem__(self, group):
267        """Accepts an AtomGroup, ResidueGroup or SegmentGroup"""
268        if isinstance(group, (Atom, AtomGroup)):
269            return self.get_atoms(group)
270        elif isinstance(group, (Residue, ResidueGroup)):
271            return self.get_residues(group)
272        elif isinstance(group, (Segment, SegmentGroup)):
273            return self.get_segments(group)
274
275    def __setitem__(self, group, values):
276        if isinstance(group, (Atom, AtomGroup)):
277            return self.set_atoms(group, values)
278        elif isinstance(group, (Residue, ResidueGroup)):
279            return self.set_residues(group, values)
280        elif isinstance(group, (Segment, SegmentGroup)):
281            return self.set_segments(group, values)
282
283    @property
284    def is_guessed(self):
285        """Bool of if the source of this information is a guess"""
286        return self._guessed
287
288    def get_atoms(self, ag):
289        """Get atom attributes for a given AtomGroup"""
290        raise NoDataError
291
292    def set_atoms(self, ag, values):
293        """Set atom attributes for a given AtomGroup"""
294        raise NotImplementedError
295
296    def get_residues(self, rg):
297        """Get residue attributes for a given ResidueGroup"""
298        raise NoDataError
299
300    def set_residues(self, rg, values):
301        """Set residue attributes for a given ResidueGroup"""
302        raise NotImplementedError
303
304    def get_segments(self, sg):
305        """Get segment attributes for a given SegmentGroup"""
306        raise NoDataError
307
308    def set_segments(self, sg, values):
309        """Set segmentattributes for a given SegmentGroup"""
310        raise NotImplementedError
311
312
313# core attributes
314
315class Atomindices(TopologyAttr):
316    """Globally unique indices for each atom in the group.
317
318    If the group is an AtomGroup, then this gives the index for each atom in
319    the group. This is the unambiguous identifier for each atom in the
320    topology, and it is not alterable.
321
322    If the group is a ResidueGroup or SegmentGroup, then this gives the indices
323    of each atom represented in the group in a 1-D array, in the order of the
324    elements in that group.
325
326    """
327    attrname = 'indices'
328    singular = 'index'
329    target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom]
330
331    def __init__(self):
332        self._guessed = False
333
334    def set_atoms(self, ag, values):
335        raise AttributeError("Atom indices are fixed; they cannot be reset")
336
337    def get_atoms(self, ag):
338        return ag.ix
339
340    def get_residues(self, rg):
341        return list(self.top.tt.residues2atoms_2d(rg.ix))
342
343    def get_segments(self, sg):
344        return list(self.top.tt.segments2atoms_2d(sg.ix))
345
346
347class Resindices(TopologyAttr):
348    """Globally unique resindices for each residue in the group.
349
350    If the group is an AtomGroup, then this gives the resindex for each atom in
351    the group. This unambiguously determines each atom's residue membership.
352    Resetting these values changes the residue membership of the atoms.
353
354    If the group is a ResidueGroup or SegmentGroup, then this gives the
355    resindices of each residue represented in the group in a 1-D array, in the
356    order of the elements in that group.
357
358    """
359    attrname = 'resindices'
360    singular = 'resindex'
361    target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom, Residue]
362
363    def __init__(self):
364        self._guessed = False
365
366    def get_atoms(self, ag):
367        return self.top.tt.atoms2residues(ag.ix)
368
369    def get_residues(self, rg):
370        return rg.ix
371
372    def set_residues(self, rg, values):
373        raise AttributeError("Residue indices are fixed; they cannot be reset")
374
375    def get_segments(self, sg):
376        return list(self.top.tt.segments2residues_2d(sg.ix))
377
378
379class Segindices(TopologyAttr):
380    """Globally unique segindices for each segment in the group.
381
382    If the group is an AtomGroup, then this gives the segindex for each atom in
383    the group. This unambiguously determines each atom's segment membership.
384    It is not possible to set these, since membership in a segment is an
385    attribute of each atom's residue.
386
387    If the group is a ResidueGroup or SegmentGroup, then this gives the
388    segindices of each segment represented in the group in a 1-D array, in the
389    order of the elements in that group.
390
391    """
392    attrname = 'segindices'
393    singular = 'segindex'
394    target_classes = [AtomGroup, ResidueGroup, SegmentGroup,
395                      Atom, Residue, Segment]
396
397    def __init__(self):
398        self._guessed = False
399
400    def get_atoms(self, ag):
401        return self.top.tt.atoms2segments(ag.ix)
402
403    def get_residues(self, rg):
404        return self.top.tt.residues2segments(rg.ix)
405
406    def get_segments(self, sg):
407        return sg.ix
408
409    def set_segments(self, sg, values):
410        raise AttributeError("Segment indices are fixed; they cannot be reset")
411
412
413# atom attributes
414
415class AtomAttr(TopologyAttr):
416    """Base class for atom attributes.
417
418    """
419    attrname = 'atomattrs'
420    singular = 'atomattr'
421    target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom]
422
423    def get_atoms(self, ag):
424        return self.values[ag.ix]
425
426    @_check_length
427    def set_atoms(self, ag, values):
428        self.values[ag.ix] = values
429
430    def get_residues(self, rg):
431        """By default, the values for each atom present in the set of residues
432        are returned in a single array. This behavior can be overriden in child
433        attributes.
434
435        """
436        aixs = self.top.tt.residues2atoms_2d(rg.ix)
437        return [self.values[aix] for aix in aixs]
438
439    def set_residues(self, rg, values):
440        raise _wronglevel_error(self, rg)
441
442    def get_segments(self, sg):
443        """By default, the values for each atom present in the set of residues
444        are returned in a single array. This behavior can be overriden in child
445        attributes.
446
447        """
448        aixs = self.top.tt.segments2atoms_2d(sg.ix)
449        return [self.values[aix] for aix in aixs]
450
451    def set_segments(self, sg, values):
452        raise _wronglevel_error(self, sg)
453
454
455# TODO: update docs to property doc
456class Atomids(AtomAttr):
457    """ID for each atom.
458    """
459    attrname = 'ids'
460    singular = 'id'
461    per_object = 'atom'
462    dtype = int
463
464    @staticmethod
465    def _gen_initial_values(na, nr, ns):
466        return np.arange(1, na + 1)
467
468
469# TODO: update docs to property doc
470class Atomnames(AtomAttr):
471    """Name for each atom.
472    """
473    attrname = 'names'
474    singular = 'name'
475    per_object = 'atom'
476    dtype = object
477    transplants = defaultdict(list)
478
479    @staticmethod
480    def _gen_initial_values(na, nr, ns):
481        return np.array(['' for _ in range(na)], dtype=object)
482
483    def getattr__(atomgroup, name):
484        try:
485            return atomgroup._get_named_atom(name)
486        except selection.SelectionError:
487            raise AttributeError("'{0}' object has no attribute '{1}'".format(
488                    atomgroup.__class__.__name__, name))
489
490    def _get_named_atom(group, name):
491        """Get all atoms with name *name* in the current AtomGroup.
492
493        For more than one atom it returns a list of :class:`Atom`
494        instance. A single :class:`Atom` is returned just as such. If
495        no atoms are found, a :exc:`SelectionError` is raised.
496
497        .. versionadded:: 0.9.2
498
499        .. deprecated:: 0.16.2
500           *Instant selectors* will be removed in the 1.0 release.
501           Use ``AtomGroup.select_atoms('name <name>')`` instead.
502           See issue `#1377
503           <https://github.com/MDAnalysis/mdanalysis/issues/1377>`_ for
504           more details.
505
506        """
507        # There can be more than one atom with the same name
508        atomlist = group.atoms.unique[group.atoms.unique.names == name]
509        if len(atomlist) == 0:
510            raise selection.SelectionError(
511                "No atoms with name '{0}'".format(name))
512        elif len(atomlist) == 1:
513            # XXX: keep this, makes more sense for names
514            atomlist = atomlist[0]
515        warnings.warn("Instant selector AtomGroup['<name>'] or AtomGroup.<name> "
516                      "is deprecated and will be removed in 1.0. "
517                      "Use AtomGroup.select_atoms('name <name>') instead.",
518                      DeprecationWarning)
519        return atomlist
520
521    # AtomGroup already has a getattr
522#    transplants[AtomGroup].append(
523#        ('__getattr__', getattr__))
524
525    transplants[Residue].append(
526        ('__getattr__', getattr__))
527
528    # this is also getitem for a residue
529    transplants[Residue].append(
530        ('__getitem__', getattr__))
531
532    transplants[AtomGroup].append(
533        ('_get_named_atom', _get_named_atom))
534
535    transplants[Residue].append(
536        ('_get_named_atom', _get_named_atom))
537
538    def phi_selection(residue):
539        """AtomGroup corresponding to the phi protein backbone dihedral
540        C'-N-CA-C.
541
542        Returns
543        -------
544        AtomGroup
545            4-atom selection in the correct order. If no C' found in the
546            previous residue (by resid) then this method returns ``None``.
547        """
548        # TODO: maybe this can be reformulated into one selection string without
549        # the additions later
550        sel_str = "segid {} and resid {} and name C".format(
551            residue.segment.segid, residue.resid - 1)
552        sel = (residue.universe.select_atoms(sel_str) +
553               residue.atoms.select_atoms('name N', 'name CA', 'name C'))
554
555        # select_atoms doesnt raise errors if nothing found, so check size
556        if len(sel) == 4:
557            return sel
558        else:
559            return None
560
561    transplants[Residue].append(('phi_selection', phi_selection))
562
563    def psi_selection(residue):
564        """AtomGroup corresponding to the psi protein backbone dihedral
565        N-CA-C-N'.
566
567        Returns
568        -------
569        AtomGroup
570            4-atom selection in the correct order. If no N' found in the
571            following residue (by resid) then this method returns ``None``.
572        """
573        sel_str = "segid {} and resid {} and name N".format(
574            residue.segment.segid, residue.resid + 1)
575
576        sel = (residue.atoms.select_atoms('name N', 'name CA', 'name C') +
577               residue.universe.select_atoms(sel_str))
578
579        if len(sel) == 4:
580            return sel
581        else:
582            return None
583
584    transplants[Residue].append(('psi_selection', psi_selection))
585
586    def omega_selection(residue):
587        """AtomGroup corresponding to the omega protein backbone dihedral
588        CA-C-N'-CA'.
589
590        omega describes the -C-N- peptide bond. Typically, it is trans (180
591        degrees) although cis-bonds (0 degrees) are also occasionally observed
592        (especially near Proline).
593
594        Returns
595        -------
596        AtomGroup
597            4-atom selection in the correct order. If no C' found in the
598            previous residue (by resid) then this method returns ``None``.
599
600        """
601        nextres = residue.resid + 1
602        segid = residue.segment.segid
603        sel = (residue.atoms.select_atoms('name CA', 'name C') +
604               residue.universe.select_atoms(
605                   'segid {} and resid {} and name N'.format(segid, nextres),
606                   'segid {} and resid {} and name CA'.format(segid, nextres)))
607        if len(sel) == 4:
608            return sel
609        else:
610            return None
611
612    transplants[Residue].append(('omega_selection', omega_selection))
613
614    def chi1_selection(residue):
615        """AtomGroup corresponding to the chi1 sidechain dihedral N-CA-CB-CG.
616
617        Returns
618        -------
619        AtomGroup
620            4-atom selection in the correct order. If no CB and/or CG is found
621            then this method returns ``None``.
622
623        .. versionadded:: 0.7.5
624        """
625        ag = residue.atoms.select_atoms('name N', 'name CA',
626                                        'name CB', 'name CG')
627        if len(ag) == 4:
628            return ag
629        else:
630            return None
631
632    transplants[Residue].append(('chi1_selection', chi1_selection))
633
634
635# TODO: update docs to property doc
636class Atomtypes(AtomAttr):
637    """Type for each atom"""
638    attrname = 'types'
639    singular = 'type'
640    per_object = 'atom'
641    dtype = object
642
643    @staticmethod
644    def _gen_initial_values(na, nr, ns):
645        return np.array(['' for _ in range(na)], dtype=object)
646
647
648# TODO: update docs to property doc
649class Elements(AtomAttr):
650    """Element for each atom"""
651    attrname = 'elements'
652    singular = 'element'
653    dtype = object
654
655    @staticmethod
656    def _gen_initial_values(na, nr, ns):
657        return np.array(['' for _ in range(na)], dtype=object)
658
659
660# TODO: update docs to property doc
661class Radii(AtomAttr):
662    """Radii for each atom"""
663    attrname = 'radii'
664    singular = 'radius'
665    per_object = 'atom'
666    dtype = float
667
668    @staticmethod
669    def _gen_initial_values(na, nr, ns):
670        return np.zeros(na)
671
672
673class RecordTypes(AtomAttr):
674    """For PDB-like formats, indicates if ATOM or HETATM
675
676    Defaults to 'ATOM'
677    """
678    # internally encodes {True: 'ATOM', False: 'HETATM'}
679    attrname = 'record_types'
680    singular = 'record_type'
681    per_object = 'atom'
682
683    def __init__(self, values, guessed=False):
684        self.values = np.where(values == 'ATOM', True, False)
685        self._guessed = guessed
686
687    @staticmethod
688    def _gen_initial_values(na, nr, ns):
689        return np.array(['ATOM'] * na, dtype=object)
690
691    def get_atoms(self, atomgroup):
692        return np.where(self.values[atomgroup.ix], 'ATOM', 'HETATM')
693
694    @_check_length
695    def set_atoms(self, atomgroup, values):
696        self.values[atomgroup.ix] = np.where(values == 'ATOM', True, False)
697
698    def get_residues(self, rg):
699        return [self.get_atoms(r.atoms) for r in rg]
700
701    def get_segments(self, sg):
702        return [self.get_atoms(s.atoms) for s in sg]
703
704
705class ChainIDs(AtomAttr):
706    """ChainID per atom
707
708    Note
709    ----
710    This is an attribute of the Atom, not Residue or Segment
711    """
712    attrname = 'chainIDs'
713    singular = 'chainID'
714    per_object = 'atom'
715    dtype = object
716
717    @staticmethod
718    def _gen_initial_values(na, nr, ns):
719        return np.array(['' for _ in range(na)], dtype=object)
720
721
722class Tempfactors(AtomAttr):
723    """Tempfactor for atoms"""
724    attrname = 'tempfactors'
725    singular = 'tempfactor'
726    per_object = 'atom'
727    dtype = float
728
729    @staticmethod
730    def _gen_initial_values(na, nr, ns):
731        return np.zeros(na)
732
733
734class Masses(AtomAttr):
735    attrname = 'masses'
736    singular = 'mass'
737    per_object = 'atom'
738    target_classes = [AtomGroup, ResidueGroup, SegmentGroup,
739                      Atom, Residue, Segment]
740    transplants = defaultdict(list)
741    dtype = np.float64
742
743    groupdoc = """Mass of each component in the Group.
744
745    If the Group is an AtomGroup, then the masses are for each atom. If the
746    Group is a ResidueGroup or SegmentGroup, the masses are for each residue or
747    segment, respectively. These are obtained by summation of the member atoms
748    for each component.
749    """
750
751    singledoc = """Mass of the component."""
752
753    @staticmethod
754    def _gen_initial_values(na, nr, ns):
755        return np.zeros(na)
756
757    def get_residues(self, rg):
758        resatoms = self.top.tt.residues2atoms_2d(rg.ix)
759
760        if isinstance(rg._ix, numbers.Integral):
761            # for a single residue
762            masses = self.values[resatoms].sum()
763        else:
764            # for a residuegroup
765            masses = np.empty(len(rg))
766            for i, row in enumerate(resatoms):
767                masses[i] = self.values[row].sum()
768
769        return masses
770
771    def get_segments(self, sg):
772        segatoms = self.top.tt.segments2atoms_2d(sg.ix)
773
774        if isinstance(sg._ix, numbers.Integral):
775            # for a single segment
776            masses = self.values[tuple(segatoms)].sum()
777        else:
778            # for a segmentgroup
779            masses = np.array([self.values[row].sum() for row in segatoms])
780
781        return masses
782
783    @warn_if_not_unique
784    def center_of_mass(group, pbc=None, compound='group'):
785        """Center of mass of (compounds of) the group.
786
787        Computes the center of mass of atoms in the group.
788        Centers of mass per residue or per segment can be obtained by setting
789        the `compound` parameter accordingly.
790
791        Parameters
792        ----------
793        pbc : bool, optional
794            If ``True`` and `compound` is 'group', move all atoms to the primary
795            unit cell before calculation.
796            If ``True`` and `compound` is 'segments' or 'residues', the centers
797            of mass of each compound will be calculated without moving any
798            atoms to keep the compounds intact. Instead, the resulting
799            center-of-mass position vectors will be moved to the primary unit
800            cell after calculation.
801        compound : {'group', 'segments', 'residues'}, optional
802            If 'group', the center of mass of all atoms in the atomgroup will
803            be returned as a single position vector. Else, the centers of mass
804            of each segment or residue will be returned as an array of position
805            vectors, i.e. a 2d array. Note that, in any case, *only* the
806            positions of atoms *belonging to the atomgroup* will be
807            taken into account.
808
809        Returns
810        -------
811        center : numpy.ndarray
812            Position vector(s) of the center(s) of mass of the group.
813            If `compound` was set to 'group', the output will be a single
814            position vector.
815            If `compound` was set to 'segments' or 'residues', the output will
816            be a 2d array of shape ``(n, 3)`` where ``n`` is the number
817            compounds.
818
819        Note
820        ----
821        The :class:`MDAnalysis.core.flags` flag *use_pbc* when set to
822        ``True`` allows the *pbc* flag to be used by default.
823
824
825        .. versionchanged:: 0.8 Added `pbc` parameter
826        .. versionchanged:: 0.19.0 Added `compound` parameter
827        """
828        atoms = group.atoms
829        return atoms.center(weights=atoms.masses, pbc=pbc, compound=compound)
830
831    transplants[GroupBase].append(
832        ('center_of_mass', center_of_mass))
833
834    @warn_if_not_unique
835    def total_mass(group):
836        """Total mass of the Group.
837
838        """
839        return group.masses.sum()
840
841    transplants[GroupBase].append(
842        ('total_mass', total_mass))
843
844    @warn_if_not_unique
845    def moment_of_inertia(group, **kwargs):
846        """Tensor moment of inertia relative to center of mass as 3x3 numpy
847        array.
848
849        Parameters
850        ----------
851        pbc : bool, optional
852            If ``True``, move all atoms within the primary unit cell before
853            calculation. [``False``]
854
855        Note
856        ----
857        The :class:`MDAnalysis.core.flags` flag *use_pbc* when set to
858        ``True`` allows the *pbc* flag to be used by default.
859
860
861        .. versionchanged:: 0.8 Added *pbc* keyword
862
863        """
864        atomgroup = group.atoms
865        pbc = kwargs.pop('pbc', flags['use_pbc'])
866
867        # Convert to local coordinates
868        com = atomgroup.center_of_mass(pbc=pbc)
869        if pbc:
870            pos = atomgroup.pack_into_box(inplace=False) - com
871        else:
872            pos = atomgroup.positions - com
873
874        masses = atomgroup.masses
875        # Create the inertia tensor
876        # m_i = mass of atom i
877        # (x_i, y_i, z_i) = pos of atom i
878        # Ixx = sum(m_i*(y_i^2+z_i^2));
879        # Iyy = sum(m_i*(x_i^2+z_i^2));
880        # Izz = sum(m_i*(x_i^2+y_i^2))
881        # Ixy = Iyx = -1*sum(m_i*x_i*y_i)
882        # Ixz = Izx = -1*sum(m_i*x_i*z_i)
883        # Iyz = Izy = -1*sum(m_i*y_i*z_i)
884        tens = np.zeros((3, 3), dtype=np.float64)
885        # xx
886        tens[0][0] = (masses * (pos[:, 1] ** 2 + pos[:, 2] ** 2)).sum()
887        # xy & yx
888        tens[0][1] = tens[1][0] = - (masses * pos[:, 0] * pos[:, 1]).sum()
889        # xz & zx
890        tens[0][2] = tens[2][0] = - (masses * pos[:, 0] * pos[:, 2]).sum()
891        # yy
892        tens[1][1] = (masses * (pos[:, 0] ** 2 + pos[:, 2] ** 2)).sum()
893        # yz + zy
894        tens[1][2] = tens[2][1] = - (masses * pos[:, 1] * pos[:, 2]).sum()
895        # zz
896        tens[2][2] = (masses * (pos[:, 0] ** 2 + pos[:, 1] ** 2)).sum()
897
898        return tens
899
900    transplants[GroupBase].append(
901        ('moment_of_inertia', moment_of_inertia))
902
903    @warn_if_not_unique
904    def radius_of_gyration(group, **kwargs):
905        """Radius of gyration.
906
907        Parameters
908        ----------
909        pbc : bool, optional
910            If ``True``, move all atoms within the primary unit cell before
911            calculation. [``False``]
912
913        Note
914        ----
915        The :class:`MDAnalysis.core.flags` flag *use_pbc* when set to
916        ``True`` allows the *pbc* flag to be used by default.
917
918
919        .. versionchanged:: 0.8 Added *pbc* keyword
920
921        """
922        atomgroup = group.atoms
923        pbc = kwargs.pop('pbc', flags['use_pbc'])
924        masses = atomgroup.masses
925
926        com = atomgroup.center_of_mass(pbc=pbc)
927        if pbc:
928            recenteredpos = atomgroup.pack_into_box(inplace=False) - com
929        else:
930            recenteredpos = atomgroup.positions - com
931
932        rog_sq = np.sum(masses * np.sum(recenteredpos**2,
933                                        axis=1)) / atomgroup.total_mass()
934
935        return np.sqrt(rog_sq)
936
937    transplants[GroupBase].append(
938        ('radius_of_gyration', radius_of_gyration))
939
940    @warn_if_not_unique
941    def shape_parameter(group, **kwargs):
942        """Shape parameter.
943
944        See [Dima2004a]_ for background information.
945
946        Parameters
947        ----------
948        pbc : bool, optional
949            If ``True``, move all atoms within the primary unit cell before
950            calculation. [``False``]
951
952        Note
953        ----
954        The :class:`MDAnalysis.core.flags` flag *use_pbc* when set to
955        ``True`` allows the *pbc* flag to be used by default.
956
957
958        References
959        ----------
960        .. [Dima2004a] Dima, R. I., & Thirumalai, D. (2004). Asymmetry
961           in the shapes of folded and denatured states of
962           proteins. *J Phys Chem B*, 108(21),
963           6564-6570. doi:`10.1021/jp037128y
964           <https://doi.org/10.1021/jp037128y>`_
965
966
967        .. versionadded:: 0.7.7
968        .. versionchanged:: 0.8 Added *pbc* keyword
969
970        """
971        atomgroup = group.atoms
972        pbc = kwargs.pop('pbc', flags['use_pbc'])
973        masses = atomgroup.masses
974
975        com = atomgroup.center_of_mass(pbc=pbc)
976        if pbc:
977            recenteredpos = atomgroup.pack_into_box(inplace=False) - com
978        else:
979            recenteredpos = atomgroup.positions - com
980        tensor = np.zeros((3, 3))
981
982        for x in range(recenteredpos.shape[0]):
983            tensor += masses[x] * np.outer(recenteredpos[x, :],
984                                           recenteredpos[x, :])
985        tensor /= atomgroup.total_mass()
986        eig_vals = np.linalg.eigvalsh(tensor)
987        shape = 27.0 * np.prod(eig_vals - np.mean(eig_vals)) / np.power(np.sum(eig_vals), 3)
988
989        return shape
990
991    transplants[GroupBase].append(
992        ('shape_parameter', shape_parameter))
993
994    @warn_if_not_unique
995    def asphericity(group, pbc=None):
996        """Asphericity.
997
998        See [Dima2004b]_ for background information.
999
1000        Parameters
1001        ----------
1002        pbc : bool, optional
1003            If ``True``, move all atoms within the primary unit cell before
1004            calculation. If ``None`` use value defined in
1005            MDAnalysis.core.flags['use_pbc']
1006
1007        Note
1008        ----
1009        The :class:`MDAnalysis.core.flags` flag *use_pbc* when set to
1010        ``True`` allows the *pbc* flag to be used by default.
1011
1012
1013        References
1014        ----------
1015
1016        .. [Dima2004b] Dima, R. I., & Thirumalai, D. (2004). Asymmetry
1017           in the shapes of folded and denatured states of
1018           proteins. *J Phys Chem B*, 108(21),
1019           6564-6570. doi:`10.1021/jp037128y
1020           <https://doi.org/10.1021/jp037128y>`_
1021
1022
1023
1024        .. versionadded:: 0.7.7
1025        .. versionchanged:: 0.8 Added *pbc* keyword
1026
1027        """
1028        atomgroup = group.atoms
1029        if pbc is None:
1030            pbc = flags['use_pbc']
1031        masses = atomgroup.masses
1032
1033        if pbc:
1034            recenteredpos = (atomgroup.pack_into_box(inplace=False) -
1035                             atomgroup.center_of_mass(pbc=True))
1036        else:
1037            recenteredpos = (atomgroup.positions -
1038                             atomgroup.center_of_mass(pbc=False))
1039
1040        tensor = np.zeros((3, 3))
1041        for x in range(recenteredpos.shape[0]):
1042            tensor += masses[x] * np.outer(recenteredpos[x],
1043                                           recenteredpos[x])
1044
1045        tensor /= atomgroup.total_mass()
1046        eig_vals = np.linalg.eigvalsh(tensor)
1047        shape = (3.0 / 2.0) * (np.sum((eig_vals - np.mean(eig_vals))**2) /
1048                               np.sum(eig_vals)**2)
1049
1050        return shape
1051
1052    transplants[GroupBase].append(
1053        ('asphericity', asphericity))
1054
1055    @warn_if_not_unique
1056    def principal_axes(group, pbc=None):
1057        """Calculate the principal axes from the moment of inertia.
1058
1059        e1,e2,e3 = AtomGroup.principal_axes()
1060
1061        The eigenvectors are sorted by eigenvalue, i.e. the first one
1062        corresponds to the highest eigenvalue and is thus the first principal
1063        axes.
1064
1065        Parameters
1066        ----------
1067        pbc : bool, optional
1068            If ``True``, move all atoms within the primary unit cell before
1069            calculation. If ``None`` use value defined in setup flags.
1070
1071        Returns
1072        -------
1073        axis_vectors : array
1074            3 x 3 array with ``v[0]`` as first, ``v[1]`` as second, and
1075            ``v[2]`` as third eigenvector.
1076
1077        Note
1078        ----
1079        The :class:`MDAnalysis.core.flags` flag *use_pbc* when set to
1080        ``True`` allows the *pbc* flag to be used by default.
1081
1082
1083        .. versionchanged:: 0.8 Added *pbc* keyword
1084
1085        """
1086        atomgroup = group.atoms
1087        if pbc is None:
1088            pbc = flags['use_pbc']
1089        e_val, e_vec = np.linalg.eig(atomgroup.moment_of_inertia(pbc=pbc))
1090
1091        # Sort
1092        indices = np.argsort(e_val)[::-1]
1093        # Return transposed in more logical form. See Issue 33.
1094        return e_vec[:, indices].T
1095
1096    transplants[GroupBase].append(
1097        ('principal_axes', principal_axes))
1098
1099    def align_principal_axis(group, axis, vector):
1100        """Align principal axis with index `axis` with `vector`.
1101
1102        Parameters
1103        ----------
1104        axis : {0, 1, 2}
1105            Index of the principal axis (0, 1, or 2), as produced by
1106            :meth:`~principal_axes`.
1107        vector : array_like
1108            Vector to align principal axis with.
1109
1110        Notes
1111        -----
1112        To align the long axis of a channel (the first principal axis, i.e.
1113        *axis* = 0) with the z-axis::
1114
1115          u.atoms.align_principal_axis(0, [0,0,1])
1116          u.atoms.write("aligned.pdb")
1117        """
1118        p = group.principal_axes()[axis]
1119        angle = np.degrees(mdamath.angle(p, vector))
1120        ax = transformations.rotaxis(p, vector)
1121        # print "principal[%d] = %r" % (axis, p)
1122        # print "axis = %r, angle = %f deg" % (ax, angle)
1123        return group.rotateby(angle, ax)
1124
1125    transplants[GroupBase].append(
1126        ('align_principal_axis', align_principal_axis))
1127
1128
1129# TODO: update docs to property doc
1130class Charges(AtomAttr):
1131    attrname = 'charges'
1132    singular = 'charge'
1133    per_object = 'atom'
1134    target_classes = [AtomGroup, ResidueGroup, SegmentGroup,
1135                      Atom, Residue, Segment]
1136    transplants = defaultdict(list)
1137    dtype = float
1138
1139    @staticmethod
1140    def _gen_initial_values(na, nr, ns):
1141        return np.zeros(na)
1142
1143    def get_residues(self, rg):
1144        resatoms = self.top.tt.residues2atoms_2d(rg.ix)
1145
1146        if isinstance(rg._ix, numbers.Integral):
1147            charges = self.values[resatoms].sum()
1148        else:
1149            charges = np.empty(len(rg))
1150            for i, row in enumerate(resatoms):
1151                charges[i] = self.values[row].sum()
1152
1153        return charges
1154
1155    def get_segments(self, sg):
1156        segatoms = self.top.tt.segments2atoms_2d(sg.ix)
1157
1158        if isinstance(sg._ix, numbers.Integral):
1159            # for a single segment
1160            charges = self.values[tuple(segatoms)].sum()
1161        else:
1162            # for a segmentgroup
1163            charges = np.array([self.values[row].sum() for row in segatoms])
1164
1165        return charges
1166
1167    @warn_if_not_unique
1168    def total_charge(group):
1169        """Total charge of the Group.
1170
1171        """
1172        return group.charges.sum()
1173
1174    transplants[GroupBase].append(
1175        ('total_charge', total_charge))
1176
1177
1178# TODO: update docs to property doc
1179class Bfactors(AtomAttr):
1180    """Crystallographic B-factors in A**2 for each atom"""
1181    attrname = 'bfactors'
1182    singular = 'bfactor'
1183    per_object = 'atom'
1184    dtype = float
1185
1186    @staticmethod
1187    def _gen_initial_values(na, nr, ns):
1188        return np.zeros(na)
1189
1190
1191# TODO: update docs to property doc
1192class Occupancies(AtomAttr):
1193    attrname = 'occupancies'
1194    singular = 'occupancy'
1195    per_object = 'atom'
1196    dtype = float
1197
1198    @staticmethod
1199    def _gen_initial_values(na, nr, ns):
1200        return np.zeros(na)
1201
1202
1203# TODO: update docs to property doc
1204class AltLocs(AtomAttr):
1205    """AltLocs for each atom"""
1206    attrname = 'altLocs'
1207    singular = 'altLoc'
1208    per_object = 'atom'
1209    dtype = object
1210
1211    @staticmethod
1212    def _gen_initial_values(na, nr, ns):
1213        return np.array(['' for _ in range(na)], dtype=object)
1214
1215
1216class ResidueAttr(TopologyAttr):
1217    attrname = 'residueattrs'
1218    singular = 'residueattr'
1219    target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Residue]
1220    per_object = 'residue'
1221
1222    def get_atoms(self, ag):
1223        rix = self.top.tt.atoms2residues(ag.ix)
1224        return self.values[rix]
1225
1226    def set_atoms(self, ag, values):
1227        raise _wronglevel_error(self, ag)
1228
1229    def get_residues(self, rg):
1230        return self.values[rg.ix]
1231
1232    @_check_length
1233    def set_residues(self, rg, values):
1234        self.values[rg.ix] = values
1235
1236    def get_segments(self, sg):
1237        """By default, the values for each residue present in the set of
1238        segments are returned in a single array. This behavior can be overriden
1239        in child attributes.
1240
1241        """
1242        rixs = self.top.tt.segments2residues_2d(sg.ix)
1243        return [self.values[rix] for rix in rixs]
1244
1245    def set_segments(self, sg, values):
1246        raise _wronglevel_error(self, sg)
1247
1248
1249# TODO: update docs to property doc
1250class Resids(ResidueAttr):
1251    """Residue ID"""
1252    attrname = 'resids'
1253    singular = 'resid'
1254    target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom, Residue]
1255    dtype = int
1256
1257    @staticmethod
1258    def _gen_initial_values(na, nr, ns):
1259        return np.arange(1, nr + 1)
1260
1261
1262# TODO: update docs to property doc
1263class Resnames(ResidueAttr):
1264    attrname = 'resnames'
1265    singular = 'resname'
1266    target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom, Residue]
1267    transplants = defaultdict(list)
1268    dtype = object
1269
1270    @staticmethod
1271    def _gen_initial_values(na, nr, ns):
1272        return np.array(['' for _ in range(nr)], dtype=object)
1273
1274    def getattr__(residuegroup, resname):
1275        try:
1276            return residuegroup._get_named_residue(resname)
1277        except selection.SelectionError:
1278            raise AttributeError("'{0}' object has no attribute '{1}'".format(
1279                    residuegroup.__class__.__name__, resname))
1280
1281    transplants[ResidueGroup].append(('__getattr__', getattr__))
1282    # This transplant is hardcoded for now to allow for multiple getattr things
1283    #transplants[Segment].append(('__getattr__', getattr__))
1284
1285    def _get_named_residue(group, resname):
1286        """Get all residues with name *resname* in the current ResidueGroup
1287        or Segment.
1288
1289        For more than one residue it returns a
1290        :class:`MDAnalysis.core.groups.ResidueGroup` instance. A single
1291        :class:`MDAnalysis.core.group.Residue` is returned for a single match.
1292        If no residues are found, a :exc:`SelectionError` is raised.
1293
1294        .. versionadded:: 0.9.2
1295
1296        .. deprecated:: 0.16.2
1297           *Instant selectors* will be removed in the 1.0 release.
1298           Use ``ResidueGroup[ResidueGroup.resnames == '<name>']``
1299           or ``Segment.residues[Segment.residues == '<name>']``
1300           instead.
1301           See issue `#1377
1302           <https://github.com/MDAnalysis/mdanalysis/issues/1377>`_ for
1303           more details.
1304
1305        """
1306        # There can be more than one residue with the same name
1307        residues = group.residues.unique[
1308                group.residues.unique.resnames == resname]
1309        if len(residues) == 0:
1310            raise selection.SelectionError(
1311                "No residues with resname '{0}'".format(resname))
1312        warnings.warn("Instant selector ResidueGroup.<name> "
1313                      "or Segment.<name> "
1314                      "is deprecated and will be removed in 1.0. "
1315                      "Use ResidueGroup[ResidueGroup.resnames == '<name>'] "
1316                      "or Segment.residues[Segment.residues == '<name>'] "
1317                      "instead.",
1318                      DeprecationWarning)
1319        if len(residues) == 1:
1320            # XXX: keep this, makes more sense for names
1321            return residues[0]
1322        else:
1323            # XXX: but inconsistent (see residues and Issue 47)
1324            return residues
1325
1326    transplants[ResidueGroup].append(
1327        ('_get_named_residue', _get_named_residue))
1328
1329    def sequence(self, **kwargs):
1330        """Returns the amino acid sequence.
1331
1332        The format of the sequence is selected with the keyword *format*:
1333
1334        ============== ============================================
1335        *format*       description
1336        ============== ============================================
1337        'SeqRecord'    :class:`Bio.SeqRecord.SeqRecord` (default)
1338        'Seq'          :class:`Bio.Seq.Seq`
1339        'string'       string
1340        ============== ============================================
1341
1342        The sequence is returned by default (keyword ``format = 'SeqRecord'``)
1343        as a :class:`Bio.SeqRecord.SeqRecord` instance, which can then be
1344        further processed. In this case, all keyword arguments (such as the
1345        *id* string or the *name* or the *description*) are directly passed to
1346        :class:`Bio.SeqRecord.SeqRecord`.
1347
1348        If the keyword *format* is set to ``'Seq'``, all *kwargs* are ignored
1349        and a :class:`Bio.Seq.Seq` instance is returned. The difference to the
1350        record is that the record also contains metadata and can be directly
1351        used as an input for other functions in :mod:`Bio`.
1352
1353        If the keyword *format* is set to ``'string'``, all *kwargs* are
1354        ignored and a Python string is returned.
1355
1356        .. rubric:: Example: Write FASTA file
1357
1358        Use :func:`Bio.SeqIO.write`, which takes sequence records::
1359
1360           import Bio.SeqIO
1361
1362           # get the sequence record of a protein component of a Universe
1363           protein = u.select_atoms("protein")
1364           record = protein.sequence(id="myseq1", name="myprotein")
1365
1366           Bio.SeqIO.write(record, "single.fasta", "fasta")
1367
1368        A FASTA file with multiple entries can be written with ::
1369
1370           Bio.SeqIO.write([record1, record2, ...], "multi.fasta", "fasta")
1371
1372        Parameters
1373        ----------
1374        format : string, optional
1375           - ``"string"``: return sequence as a string of 1-letter codes
1376           - ``"Seq"``: return a :class:`Bio.Seq.Seq` instance
1377           - ``"SeqRecord"``: return a :class:`Bio.SeqRecord.SeqRecord`
1378             instance
1379
1380            Default is ``"SeqRecord"``
1381        id : optional
1382           Sequence ID for SeqRecord (should be different for different
1383           sequences)
1384        name : optional
1385           Name of the protein.
1386        description : optional
1387           Short description of the sequence.
1388        kwargs : optional
1389           Any other keyword arguments that are understood by
1390           class:`Bio.SeqRecord.SeqRecord`.
1391
1392        Raises
1393        ------
1394        :exc:`ValueError` if a residue name cannot be converted to a
1395        1-letter IUPAC protein amino acid code; make sure to only
1396        select protein residues.
1397
1398        :exc:`TypeError` if an unknown *format* is selected.
1399
1400        .. versionadded:: 0.9.0
1401        """
1402        formats = ('string', 'Seq', 'SeqRecord')
1403
1404        format = kwargs.pop("format", "SeqRecord")
1405        if format not in formats:
1406            raise TypeError("Unknown format='{0}': must be one of: {1}".format(
1407                    format, ", ".join(formats)))
1408        try:
1409            sequence = "".join([convert_aa_code(r) for r in self.residues.resnames])
1410        except KeyError as err:
1411            raise ValueError("AtomGroup contains a residue name '{0}' that "
1412                             "does not have a IUPAC protein 1-letter "
1413                             "character".format(err.message))
1414        if format == "string":
1415            return sequence
1416        seq = Bio.Seq.Seq(sequence, alphabet=Bio.Alphabet.IUPAC.protein)
1417        if format == "Seq":
1418            return seq
1419        return Bio.SeqRecord.SeqRecord(seq, **kwargs)
1420
1421    transplants[ResidueGroup].append(
1422        ('sequence', sequence))
1423
1424
1425# TODO: update docs to property doc
1426class Resnums(ResidueAttr):
1427    attrname = 'resnums'
1428    singular = 'resnum'
1429    target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom, Residue]
1430    dtype = int
1431
1432    @staticmethod
1433    def _gen_initial_values(na, nr, ns):
1434        return np.arange(1, nr + 1)
1435
1436
1437class ICodes(ResidueAttr):
1438    """Insertion code for Atoms"""
1439    attrname = 'icodes'
1440    singular = 'icode'
1441    dtype = object
1442
1443    @staticmethod
1444    def _gen_initial_values(na, nr, ns):
1445        return np.array(['' for _ in range(nr)], dtype=object)
1446
1447
1448class Moltypes(ResidueAttr):
1449    """Name of the molecule type
1450
1451    Two molecules that share a molecule type share a common template topology.
1452    """
1453    attrname = 'moltypes'
1454    singular = 'moltype'
1455    target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom, Residue]
1456    dtype = object
1457
1458
1459class Molnums(ResidueAttr):
1460    """Name of the molecule type
1461
1462    Two molecules that share a molecule type share a common template topology.
1463    """
1464    attrname = 'molnums'
1465    singular = 'molnum'
1466    target_classes = [AtomGroup, ResidueGroup, Atom, Residue]
1467    dtype = int
1468
1469# segment attributes
1470
1471class SegmentAttr(TopologyAttr):
1472    """Base class for segment attributes.
1473
1474    """
1475    attrname = 'segmentattrs'
1476    singular = 'segmentattr'
1477    target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Segment]
1478    per_object = 'segment'
1479
1480    def get_atoms(self, ag):
1481        six = self.top.tt.atoms2segments(ag.ix)
1482        return self.values[six]
1483
1484    def set_atoms(self, ag, values):
1485        raise _wronglevel_error(self, ag)
1486
1487    def get_residues(self, rg):
1488        six = self.top.tt.residues2segments(rg.ix)
1489        return self.values[six]
1490
1491    def set_residues(self, rg, values):
1492        raise _wronglevel_error(self, rg)
1493
1494    def get_segments(self, sg):
1495        return self.values[sg.ix]
1496
1497    @_check_length
1498    def set_segments(self, sg, values):
1499        self.values[sg.ix] = values
1500
1501
1502# TODO: update docs to property doc
1503class Segids(SegmentAttr):
1504    attrname = 'segids'
1505    singular = 'segid'
1506    target_classes = [AtomGroup, ResidueGroup, SegmentGroup,
1507                      Atom, Residue, Segment]
1508    transplants = defaultdict(list)
1509    dtype = object
1510
1511    @staticmethod
1512    def _gen_initial_values(na, nr, ns):
1513        return np.array(['' for _ in range(ns)], dtype=object)
1514
1515    def getattr__(segmentgroup, segid):
1516        try:
1517            return segmentgroup._get_named_segment(segid)
1518        except selection.SelectionError:
1519            raise AttributeError("'{0}' object has no attribute '{1}'".format(
1520                    segmentgroup.__class__.__name__, segid))
1521
1522    transplants[SegmentGroup].append(
1523        ('__getattr__', getattr__))
1524
1525    def _get_named_segment(group, segid):
1526        """Get all segments with name *segid* in the current SegmentGroup.
1527
1528        For more than one residue it returns a
1529        :class:`MDAnalysis.core.groups.SegmentGroup` instance. A single
1530        :class:`MDAnalysis.core.group.Segment` is returned for a single match.
1531        If no residues are found, a :exc:`SelectionError` is raised.
1532
1533        .. versionadded:: 0.9.2
1534
1535        .. deprecated:: 0.16.2
1536           *Instant selectors* will be removed in the 1.0 release.
1537           Use ``SegmentGroup[SegmentGroup.segids == '<name>']`` instead.
1538           See issue `#1377
1539           <https://github.com/MDAnalysis/mdanalysis/issues/1377>`_ for
1540           more details.
1541
1542        """
1543        # Undo adding 's' if segid started with digit
1544        if segid.startswith('s') and len(segid) >= 2 and segid[1].isdigit():
1545            segid = segid[1:]
1546
1547        # There can be more than one segment with the same name
1548        segments = group.segments.unique[
1549                group.segments.unique.segids == segid]
1550        if len(segments) == 0:
1551            raise selection.SelectionError(
1552                "No segments with segid '{0}'".format(segid))
1553        warnings.warn("Instant selector SegmentGroup.<name> "
1554                      "is deprecated and will be removed in 1.0. "
1555                      "Use SegmentGroup[SegmentGroup.segids == '<name>'] "
1556                      "instead.",
1557                      DeprecationWarning)
1558        if len(segments) == 1:
1559            # XXX: keep this, makes more sense for names
1560            return segments[0]
1561        else:
1562            # XXX: but inconsistent (see residues and Issue 47)
1563            return segments
1564
1565    transplants[SegmentGroup].append(
1566        ('_get_named_segment', _get_named_segment))
1567
1568
1569class _Connection(AtomAttr):
1570    """Base class for connectivity between atoms"""
1571    def __init__(self, values, types=None, guessed=False, order=None):
1572        self.values = list(values)
1573        if types is None:
1574            types = [None] * len(values)
1575        self.types = types
1576        if guessed in (True, False):
1577            # if single value passed, multiply this across
1578            # all bonds
1579            guessed = [guessed] * len(values)
1580        self._guessed = guessed
1581        if order is None:
1582            order = [None] * len(values)
1583        self.order = order
1584        self._cache = dict()
1585
1586    def copy(self):
1587        """Return a deepcopy of this attribute"""
1588        return self.__class__(copy.copy(self.values),
1589                              copy.copy(self.types),
1590                              copy.copy(self._guessed),
1591                              copy.copy(self.order))
1592
1593    def __len__(self):
1594        return len(self._bondDict)
1595
1596    @property
1597    @cached('bd')
1598    def _bondDict(self):
1599        """Lazily built mapping of atoms:bonds"""
1600        bd = defaultdict(list)
1601
1602        for b, t, g, o in zip(self.values, self.types,
1603                              self._guessed, self.order):
1604            # We always want the first index
1605            # to be less than the last
1606            # eg (0, 1) not (1, 0)
1607            # and (4, 10, 8) not (8, 10, 4)
1608            if b[0] > b[-1]:
1609                b = b[::-1]
1610            for a in b:
1611                bd[a].append((b, t, g, o))
1612        return bd
1613
1614    def set_atoms(self, ag):
1615        return NotImplementedError("Cannot set bond information")
1616
1617    def get_atoms(self, ag):
1618        try:
1619            unique_bonds = set(itertools.chain(
1620                *[self._bondDict[a] for a in ag.ix]))
1621        except TypeError:
1622            # maybe we got passed an Atom
1623            unique_bonds = self._bondDict[ag.ix]
1624        bond_idx, types, guessed, order = np.hsplit(
1625            np.array(sorted(unique_bonds)), 4)
1626        bond_idx = np.array(bond_idx.ravel().tolist(), dtype=np.int32)
1627        types = types.ravel()
1628        guessed = guessed.ravel()
1629        order = order.ravel()
1630        return TopologyGroup(bond_idx, ag.universe,
1631                             self.singular[:-1],
1632                             types,
1633                             guessed,
1634                             order)
1635
1636    def add_bonds(self, values, types=None, guessed=True, order=None):
1637        if types is None:
1638            types = itertools.cycle((None,))
1639        if guessed in (True, False):
1640            guessed = itertools.cycle((guessed,))
1641        if order is None:
1642            order = itertools.cycle((None,))
1643
1644        existing = set(self.values)
1645        for v, t, g, o in zip(values, types, guessed, order):
1646            if v not in existing:
1647                self.values.append(v)
1648                self.types.append(t)
1649                self._guessed.append(g)
1650                self.order.append(o)
1651        # kill the old cache of bond Dict
1652        try:
1653            del self._cache['bd']
1654        except KeyError:
1655            pass
1656
1657
1658class Bonds(_Connection):
1659    """Bonds between two atoms
1660
1661    Must be initialised by a list of zero based tuples.
1662    These indices refer to the atom indices.
1663    E.g., ` [(0, 1), (1, 2), (2, 3)]`
1664
1665    Also adds the `bonded_atoms`, `fragment` and `fragments`
1666    attributes.
1667    """
1668    attrname = 'bonds'
1669    # Singular is the same because one Atom might have
1670    # many bonds, so still asks for "bonds" in the plural
1671    singular = 'bonds'
1672    transplants = defaultdict(list)
1673
1674    def bonded_atoms(self):
1675        """An AtomGroup of all atoms bonded to this Atom"""
1676        idx = [b.partner(self).index for b in self.bonds]
1677        return self.universe.atoms[idx]
1678
1679    transplants[Atom].append(
1680        ('bonded_atoms', property(bonded_atoms, None, None,
1681                                  bonded_atoms.__doc__)))
1682
1683    def fragment(self):
1684        """The fragment that this Atom is part of
1685
1686        .. versionadded:: 0.9.0
1687        """
1688        return self.universe._fragdict[self]
1689
1690    def fragments(self):
1691        """Read-only list of fragments.
1692
1693        Contains all fragments that any Atom in this AtomGroup is
1694        part of, the contents of the fragments may extend beyond the
1695        contents of this AtomGroup.
1696
1697        .. versionadded 0.9.0
1698        """
1699        return tuple(sorted(
1700            set(a.fragment for a in self),
1701            key=lambda x: x[0].index
1702        ))
1703
1704    transplants[Atom].append(
1705        ('fragment', property(fragment, None, None,
1706                              fragment.__doc__)))
1707
1708    transplants[AtomGroup].append(
1709        ('fragments', property(fragments, None, None,
1710                               fragments.__doc__)))
1711
1712
1713class Angles(_Connection):
1714    """Angles between three atoms
1715
1716    Initialise with a list of 3 long tuples
1717    E.g.,  `[(0, 1, 2), (1, 2, 3), (2, 3, 4)]`
1718
1719    These indices refer to the atom indices.
1720    """
1721    attrname = 'angles'
1722    singular = 'angles'
1723    transplants = defaultdict(list)
1724
1725
1726class Dihedrals(_Connection):
1727    """A connection between four sequential atoms"""
1728    attrname = 'dihedrals'
1729    singular = 'dihedrals'
1730    transplants = defaultdict(list)
1731
1732
1733class Impropers(_Connection):
1734    """An imaginary dihedral between four atoms"""
1735    attrname = 'impropers'
1736    singular = 'impropers'
1737    transplants = defaultdict(list)
1738