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