Source code for diffpy.structure.structure

#!/usr/bin/env python
##############################################################################
#
# diffpy.structure  by DANSE Diffraction group
#                   Simon J. L. Billinge
#                   (c) 2007 trustees of the Michigan State University.
#                   All rights reserved.
#
# File coded by:    Pavol Juhas
#
# See AUTHORS.txt for a list of people who contributed.
# See LICENSE_DANSE.txt for license information.
#
##############################################################################

"""This module defines class `Structure`.
"""

import codecs
import copy as copymod

import numpy

from diffpy.structure.atom import Atom
from diffpy.structure.lattice import Lattice
from diffpy.structure.utils import _linkAtomAttribute, atomBareSymbol, isiterable

# ----------------------------------------------------------------------------


[docs] class Structure(list): """Define group of atoms in a specified lattice. Structure --> group of atoms. `Structure` class is inherited from Python `list`. It contains a list of `Atom` instances. `Structure` overloads `setitem` and `setslice` methods so that the `lattice` attribute of atoms get set to `lattice`. Parameters ---------- atoms : list of Atom or Structure, Optional List of `Atom` instances to be included in this `Structure`. When `atoms` argument is an existing `Structure` instance, the new structure is its copy. lattice : Lattice, Optional Instance of `Lattice` defining coordinate systems, property. title : str, Optional String description of the structure. filename : str, Optional Name of a file to load the structure from. format : str, Optional `Structure` format of the loaded `filename`. By default all structure formats are tried one by one. Ignored when `filename` has not been specified. Note ---- Cannot use `filename` and `atoms` arguments together. Overrides `atoms` argument when `filename` is specified. Attributes ---------- title : str String description of the structure. lattice : Lattice Instance of `Lattice` defining coordinate systems. pdffit : None or dict Dictionary of PDFFit-related metadata. Examples -------- ``Structure(stru)`` create a copy of `Structure` instance stru. >>> stru = Structure() >>> copystru = Structure(stru) `Structure` is inherited from a list it can use list expansions. >>> oxygen_atoms = [a for a in stru if a.element == "O" ] >>> oxygen_stru = Structure(oxygen_atoms, lattice=stru.lattice) """ # default values for instance attributes title = "" """str: default values for `title`.""" _lattice = None pdffit = None """None: default values for `pdffit`.""" def __init__(self, atoms=None, lattice=None, title=None, filename=None, format=None): # if filename is specified load it and return if filename is not None: if any((atoms, lattice, title)): emsg = "Cannot use filename and atoms arguments together." raise ValueError(emsg) readkwargs = (format is not None) and {"format": format} or {} self.read(filename, **readkwargs) return # copy initialization, must be first to allow lattice, title override if isinstance(atoms, Structure): Structure.__copy__(atoms, self) # assign arguments: if title is not None: self.title = title if lattice is not None: self.lattice = lattice elif self.lattice is None: self.lattice = Lattice() # insert atoms unless already done by __copy__ if not len(self) and atoms is not None: self.extend(atoms) return
[docs] def copy(self): """Return a copy of this `Structure` object.""" return copymod.copy(self)
def __copy__(self, target=None): """Create a deep copy of this instance. Parameters ---------- target : Optional target instance for copying, useful for copying a derived class. Defaults to new instance of the same type as self. Returns ------- A duplicate instance of this object. """ if target is None: target = Structure() elif target is self: return target # copy attributes as appropriate: target.title = self.title target.lattice = Lattice(self.lattice) target.pdffit = copymod.deepcopy(self.pdffit) # copy all atoms to the target target[:] = self return target def __str__(self): """Simple string representation.""" s_lattice = "lattice=%s" % self.lattice s_atoms = "\n".join([str(a) for a in self]) return s_lattice + "\n" + s_atoms
[docs] def addNewAtom(self, *args, **kwargs): """Add new `Atom` instance to the end of this `Structure`. Parameters ---------- *args, **kwargs : See `Atom` class constructor. """ kwargs["lattice"] = self.lattice a = Atom(*args, **kwargs) self.append(a, copy=False) return
[docs] def getLastAtom(self): """Return Reference to the last `Atom` in this structure.""" last_atom = self[-1] return last_atom
[docs] def assignUniqueLabels(self): """Set a unique label string for each `Atom` in this structure. The label strings are formatted as "%(baresymbol)s%(index)i", where baresymbol is the element right-stripped of "[0-9][+-]". """ elnum = {} # support duplicate atom instances islabeled = set() for a in self: if a in islabeled: continue baresmbl = atomBareSymbol(a.element) elnum[baresmbl] = elnum.get(baresmbl, 0) + 1 a.label = baresmbl + str(elnum[baresmbl]) islabeled.add(a) return
[docs] def distance(self, aid0, aid1): """Calculate distance between 2 `Atoms`, no periodic boundary conditions. Parameters ---------- aid0 : int or str Zero based index of the first `Atom` or a string label. aid1 : int or str Zero based index or string label of the second atom. Returns ------- float Distance between the two `Atoms` in Angstroms. Raises ------ IndexError If any of the `Atom` indices or labels are invalid. """ # lookup by labels a0, a1 = self[aid0, aid1] return self.lattice.dist(a0.xyz, a1.xyz)
[docs] def angle(self, aid0, aid1, aid2): """ The bond angle at the second of three `Atoms` in degrees. Parameters ---------- aid0 : int or str Zero based index of the first `Atom` or a string label. aid1 : int or str Index or string label for the second atom, where the angle is formed. aid2 : int or str Index or string label for the third atom. Returns ------- float The bond angle in degrees. Raises ------ IndexError If any of the arguments are invalid. """ a0, a1, a2 = self[aid0, aid1, aid2] u10 = a0.xyz - a1.xyz u12 = a2.xyz - a1.xyz return self.lattice.angle(u10, u12)
[docs] def placeInLattice(self, new_lattice): """place structure into `new_lattice` coordinate system. Sets `lattice` to `new_lattice` and recalculate fractional coordinates of all `Atoms` so their absolute positions remain the same. Parameters ---------- new_lattice : Lattice New `lattice` to place the structure into. Returns ------- Structure Reference to this `Structure` object. The `lattice` attribute is updated to `new_lattice`. """ Tx = numpy.dot(self.lattice.base, new_lattice.recbase) Tu = numpy.dot(self.lattice.normbase, new_lattice.recnormbase) for a in self: a.xyz = numpy.dot(a.xyz, Tx) if a.anisotropy: a.U = numpy.dot(numpy.transpose(Tu), numpy.dot(a.U, Tu)) self.lattice = new_lattice return self
[docs] def read(self, filename, format="auto"): """Load structure from a file, any original data become lost. Parameters ---------- filename : str File to be loaded. format : str, Optional All structure formats are defined in parsers submodule, when ``format == 'auto'`` all parsers are tried one by one. Returns ------- Parser Return instance of data Parser used to process input string. This can be inspected for information related to particular format. """ import diffpy.structure import diffpy.structure.parsers getParser = diffpy.structure.parsers.getParser p = getParser(format) new_structure = p.parseFile(filename) # reinitialize data after successful parsing # avoid calling __init__ from a derived class Structure.__init__(self) if new_structure is not None: self.__dict__.update(new_structure.__dict__) self[:] = new_structure if not self.title: import os.path tailname = os.path.basename(filename) tailbase = os.path.splitext(tailname)[0] self.title = tailbase return p
[docs] def readStr(self, s, format="auto"): """Read structure from a string. Parameters ---------- s : str String with structure definition. format : str, Optional All structure formats are defined in parsers submodule. When ``format == 'auto'``, all parsers are tried one by one. Returns ------- Parser Return instance of data Parser used to process input string. This can be inspected for information related to particular format. """ from diffpy.structure.parsers import getParser p = getParser(format) new_structure = p.parse(s) # reinitialize data after successful parsing # avoid calling __init__ from a derived class Structure.__init__(self) if new_structure is not None: self.__dict__.update(new_structure.__dict__) self[:] = new_structure return p
[docs] def write(self, filename, format): """Save structure to file in the specified format. Parameters ---------- filename : str File to save the structure to. format : str `Structure` format to use for saving. Note ---- Available structure formats can be obtained by: ``from parsers import formats`` """ from diffpy.structure.parsers import getParser p = getParser(format) p.filename = filename s = p.tostring(self) with codecs.open(filename, "w", encoding="UTF-8") as fp: fp.write(s) return
[docs] def writeStr(self, format): """return string representation of the structure in specified format. Note ---- Available structure formats can be obtained by: ``from parsers import formats`` """ from diffpy.structure.parsers import getParser p = getParser(format) s = p.tostring(self) return s
[docs] def tolist(self): """Return `Atoms` in this `Structure` as a standard Python list.""" rv = [a for a in self] return rv
# Overloaded list Methods and Operators ----------------------------------
[docs] def append(self, a, copy=True): """Append `Atom` to a structure and update its `lattice` attribute. Parameters ---------- a : Atom Instance of `Atom` to be appended. copy : bool, Optional Flag for appending a copy of `a`. When ``False``, append `a` and update `a.lattice`. """ adup = copy and Atom(a) or a adup.lattice = self.lattice super(Structure, self).append(adup) return
[docs] def insert(self, idx, a, copy=True): """Insert `Atom` a before position idx in this `Structure`. Parameters ---------- idx : int Position in `Atom` list. a : Atom Instance of `Atom` to be inserted. copy : bool, Optional Flag for inserting a copy of `a`. When ``False``, append `a` and update `a.lattice`. """ adup = copy and copymod.copy(a) or a adup.lattice = self.lattice super(Structure, self).insert(idx, adup) return
[docs] def extend(self, atoms, copy=None): """Extend `Structure` with an iterable of `atoms`. Update the `lattice` attribute of all added `atoms`. Parameters ---------- atoms : Iterable The `Atom` objects to be appended to this `Structure`. copy : bool, Optional Flag for adding copies of `Atom` objects. Make copies when ``True``, append `atoms` unchanged when ``False``. The default behavior is to make copies when `atoms` are of `Structure` type or if new atoms introduce repeated objects. """ adups = (copymod.copy(a) for a in atoms) if copy is None: if isinstance(atoms, Structure): newatoms = adups else: memo = set(id(a) for a in self) def nextatom(a): return a if id(a) not in memo else copymod.copy(a) def mark(a): return (memo.add(id(a)), a)[-1] newatoms = (mark(nextatom(a)) for a in atoms) elif copy: newatoms = adups else: newatoms = atoms def setlat(a): return (setattr(a, "lattice", self.lattice), a)[-1] super(Structure, self).extend(setlat(a) for a in newatoms) return
def __getitem__(self, idx): """Get one or more `Atoms` in this structure. Parameters ---------- idx : int ot str ot Iterable `Atom` identifier. When integer use standard list lookup. For iterables use numpy lookup, this supports integer or boolean flag arrays. For string or string-containing iterables lookup the `Atoms` by string label. Returns ------- Atom or Structure An `Atom` instance for integer or string index or a substructure in all other cases. Raises ------ IndexError If the index is invalid or the `Atom` label is not unique. Examples -------- First `Atom` in the `Structure`: >>> stru[0] Substructure of all ``'Na'`` `Atoms`: >>> stru[stru.element == 'Na'] `Atom` with a unique label ``'Na3'``: >>> stru['Na3'] Substructure of three `Atoms`, lookup by label is more efficient when done for several `Atoms` at once. >>> stru['Na3', 2, 'Cl2'] """ if isinstance(idx, slice): rv = self.__emptySharedStructure() lst = super(Structure, self).__getitem__(idx) rv.extend(lst, copy=False) return rv try: rv = super(Structure, self).__getitem__(idx) return rv except TypeError: pass # check if there is any string label that should be resolved scalarstringlabel = isinstance(idx, str) hasstringlabel = scalarstringlabel or (isiterable(idx) and any(isinstance(ii, str) for ii in idx)) # if not, use numpy indexing to resolve idx if not hasstringlabel: idx1 = idx if type(idx) is tuple: idx1 = numpy.r_[idx] indices = numpy.arange(len(self))[idx1] rhs = [list.__getitem__(self, i) for i in indices] rv = self.__emptySharedStructure() rv.extend(rhs, copy=False) return rv # here we need to resolve at least one string label # build a map of labels to indices and mark duplicate labels duplicate = object() labeltoindex = {} for i, a in enumerate(self): labeltoindex[a.label] = duplicate if a.label in labeltoindex else i def _resolveindex(aid): aid1 = aid if type(aid) is str: aid1 = labeltoindex.get(aid, None) if aid1 is None: raise IndexError("Invalid atom label %r." % aid) if aid1 is duplicate: raise IndexError("Atom label %r is not unique." % aid) return aid1 # generate new index object that has no strings if scalarstringlabel: idx2 = _resolveindex(idx) # for iterables preserve the tuple object type else: idx2 = [_resolveindex(i) for i in idx] if type(idx) is tuple: idx2 = tuple(idx2) # call this function again and hope there is no recursion loop rv = self[idx2] return rv def __setitem__(self, idx, value, copy=True): """Assign `self[idx]` `Atom` to value. Parameters ---------- idx : int or slice Index of `Atom` in this `Structure` or a slice. value : Atom or Iterable Instance of `Atom` or an iterable. copy : bool, Optional Flag for making a copy of the value. When ``False``, update the `lattice` attribute of `Atom` objects present in value. Default is ``True``. """ # handle slice assignment if isinstance(idx, slice): def _fixlat(a): a.lattice = self.lattice return a v1 = value if copy: keep = set(super(Structure, self).__getitem__(idx)) v1 = (a if a in keep else Atom(a) for a in value) vfinal = filter(_fixlat, v1) # handle scalar assingment else: vfinal = Atom(value) if copy else value vfinal.lattice = self.lattice super(Structure, self).__setitem__(idx, vfinal) return def __add__(self, other): """Return new `Structure` object with appended `Atoms` from other. Parameters ---------- other : sequence of Atom Sequence of `Atom` instances. Returns ------- Structure New `Structure` with a copy of `Atom` instances. """ rv = copymod.copy(self) rv += other return rv def __iadd__(self, other): """Extend this `Structure` with `Atoms` from other. Parameters ---------- other : sequence of Atom Sequence of `Atom` instances. Returns ------- Structure Reference to this `Structure` object. """ self.extend(other, copy=True) return self def __sub__(self, other): """Return new `Structure` that has `Atoms` from the other removed. Parameters ---------- other : sequence of Atom Sequence of `Atom` instances. Returns ------- Structure New `Structure` with a copy of `Atom` instances. """ otherset = set(other) keepindices = [i for i, a in enumerate(self) if a not in otherset] rv = copymod.copy(self[keepindices]) return rv def __isub__(self, other): """Remove other `Atoms` if present in this structure. Parameters ---------- other : sequence of Atom Sequence of `Atom` instances. Returns ------- Structure Reference to this `Structure` object. """ otherset = set(other) self[:] = [a for a in self if a not in otherset] return self def __mul__(self, n): """Return new `Structure` with n-times concatenated `Atoms` from self. `Atoms` and `lattice` in the new structure are all copies. Parameters ---------- n : int Integer multiple. Returns ------- Structure New `Structure` with n-times concatenated `Atoms`. """ rv = copymod.copy(self[:0]) rv += n * self.tolist() return rv # right-side multiplication is the same as left-side __rmul__ = __mul__ def __imul__(self, n): """Concatenate this `Structure` to n-times more `Atoms`. For positive multiple the current `Atom` objects remain at the beginning of this `Structure`. Parameters ---------- n : int Integer multiple. Returns ------- Structure Reference to this `Structure` object. """ if n <= 0: self[:] = [] else: self.extend((n - 1) * self.tolist(), copy=True) return self # Properties ------------------------------------------------------------- # lattice def _get_lattice(self): return self._lattice def _set_lattice(self, value): for a in self: a.lattice = value self._lattice = value return lattice = property(_get_lattice, _set_lattice, doc="Coordinate system for this `Structure`.") # composition def _get_composition(self): rv = {} for a in self: rv[a.element] = rv.get(a.element, 0.0) + a.occupancy return rv composition = property(_get_composition, doc="Dictionary of chemical symbols and their total occupancies.") # linked atom attributes element = _linkAtomAttribute( "element", """Character array of `Atom` types. Assignment updates the element attribute of the respective `Atoms`. Set the maximum length of the element string to 5 characters.""", toarray=lambda items: numpy.char.array(items, itemsize=5), ) xyz = _linkAtomAttribute( "xyz", """Array of fractional coordinates of all `Atoms`. Assignment updates `xyz` attribute of all `Atoms`.""", ) x = _linkAtomAttribute( "x", """Array of all fractional coordinates `x`. Assignment updates `xyz` attribute of all `Atoms`.""", ) y = _linkAtomAttribute( "y", """Array of all fractional coordinates `y`. Assignment updates `xyz` attribute of all `Atoms`.""", ) z = _linkAtomAttribute( "z", """Array of all fractional coordinates `z`. Assignment updates `xyz` attribute of all `Atoms`.""", ) label = _linkAtomAttribute( "label", """Character array of `Atom` names. Assignment updates the label attribute of all `Atoms`. Set the maximum length of the label string to 5 characters.""", toarray=lambda items: numpy.char.array(items, itemsize=5), ) occupancy = _linkAtomAttribute( "occupancy", """Array of `Atom` occupancies. Assignment updates the occupancy attribute of all `Atoms`.""", ) xyz_cartn = _linkAtomAttribute( "xyz_cartn", """Array of absolute Cartesian coordinates of all `Atoms`. Assignment updates the `xyz` attribute of all `Atoms`.""", ) anisotropy = _linkAtomAttribute( "anisotropy", """Boolean array for anisotropic thermal displacement flags. Assignment updates the anisotropy attribute of all `Atoms`.""", ) U = _linkAtomAttribute( "U", """Array of anisotropic thermal displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) Uisoequiv = _linkAtomAttribute( "Uisoequiv", """Array of isotropic thermal displacement or equivalent values. Assignment updates the U attribute of all `Atoms`.""", ) U11 = _linkAtomAttribute( "U11", """Array of `U11` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) U22 = _linkAtomAttribute( "U22", """Array of `U22` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) U33 = _linkAtomAttribute( "U33", """Array of `U33` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) U12 = _linkAtomAttribute( "U12", """Array of `U12` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) U13 = _linkAtomAttribute( "U13", """Array of `U13` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) U23 = _linkAtomAttribute( "U23", """Array of `U23` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) Bisoequiv = _linkAtomAttribute( "Bisoequiv", """Array of Debye-Waller isotropic thermal displacement or equivalent values. Assignment updates the U attribute of all `Atoms`.""", ) B11 = _linkAtomAttribute( "B11", """Array of `B11` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) B22 = _linkAtomAttribute( "B22", """Array of `B22` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) B33 = _linkAtomAttribute( "B33", """Array of `B33` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) B12 = _linkAtomAttribute( "B12", """Array of `B12` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) B13 = _linkAtomAttribute( "B13", """Array of `B13` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) B23 = _linkAtomAttribute( "B23", """Array of `B23` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) # Private Methods -------------------------------------------------------- def __emptySharedStructure(self): """Return empty `Structure` with standard attributes same as in self.""" rv = Structure() rv.__dict__.update([(k, getattr(self, k)) for k in rv.__dict__]) return rv
# End of class Structure