pymatgen.core.sites — pymatgen 2020.7.3 documentation

# Source code for pymatgen.core.sites

    # coding: utf-8
    # Copyright (c) Pymatgen Development Team.
    # Distributed under the terms of the MIT License.

    """
    This module defines classes representing non-periodic and periodic sites.
    """

    import collections
    import numpy as np
    from typing import Union, Dict, Tuple, List

    import json
    from monty.json import MSONable
    from monty.json import MontyDecoder, MontyEncoder
    from monty.dev import deprecated

    from pymatgen.core.lattice import Lattice
    from pymatgen.core.periodic_table import Element, Specie, DummySpecie, \
        get_el_sp
    from pymatgen.util.coord import pbc_diff
    from pymatgen.core.composition import Composition

    class Site(collections.abc.Hashable, MSONable):
        """
        A generalized *non-periodic* site. This is essentially a composition
        at a point in space, with some optional properties associated with it. A
        Composition is used to represent the atoms and occupancy, which allows for
        disordered site representation. Coords are given in standard cartesian
        coordinates.
        """

        position_atol = 1e-5

        def __init__(self,
                     species: Union[str, Element, Specie, DummySpecie, Dict, Composition],
                     coords: Union[Tuple, List, np.ndarray],
                     properties: dict = None,
                     skip_checks: bool = False):
            """
            Creates a non-periodic Site.

            :param species: Species on the site. Can be:
                i.  A Composition-type object (preferred)
                ii. An  element / specie specified either as a string
                    symbols, e.g. "Li", "Fe2+", "P" or atomic numbers,
                    e.g., 3, 56, or actual Element or Specie objects.
                iii.Dict of elements/species and occupancies, e.g.,
                    {"Fe" : 0.5, "Mn":0.5}. This allows the setup of
                    disordered structures.
            :param coords: Cartesian coordinates of site.
            :param properties: Properties associated with the site as a dict, e.g.
                {"magmom": 5}. Defaults to None.
            :param skip_checks: Whether to ignore all the usual checks and just
                create the site. Use this if the Site is created in a controlled
                manner and speed is desired.
            """
            if not skip_checks:
                if not isinstance(species, Composition):
                    try:
                        species = Composition({get_el_sp(species): 1})
                    except TypeError:
                        species = Composition(species)
                totaloccu = species.num_atoms
                if totaloccu > 1 + Composition.amount_tolerance:
                    raise ValueError("Species occupancies sum to more than 1!")
                coords = np.array(coords)
            self._species = species
            self.coords = coords
            self.properties = properties or {}

        def __getattr__(self, a):
            # overriding getattr doens't play nice with pickle, so we
            # can't use self._properties
            p = object.__getattribute__(self, 'properties')
            if a in p:
                return p[a]
            raise AttributeError(a)

        @property
        def species(self) -> Composition:
            """
            :return: The species on the site as a composition, e.g., Fe0.5Mn0.5.
            """
            return self._species  # type: ignore

        @species.setter
        def species(self, species):
            if not isinstance(species, Composition):
                try:
                    species = Composition({get_el_sp(species): 1})
                except TypeError:
                    species = Composition(species)
            totaloccu = species.num_atoms
            if totaloccu > 1 + Composition.amount_tolerance:
                raise ValueError("Species occupancies sum to more than 1!")
            self._species = species

        @property
        def x(self):
            """
            Cartesian x coordinate
            """
            return self.coords[0]

        @x.setter
        def x(self, x: float):
            self.coords[0] = x  # type: ignore

        @property
        def y(self):
            """
            Cartesian y coordinate
            """
            return self.coords[1]

        @y.setter
        def y(self, y: float):
            self.coords[1] = y  # type: ignore

        @property
        def z(self):
            """
            Cartesian z coordinate
            """
            return self.coords[2]

        @z.setter
        def z(self, z: float):
            self.coords[2] = z  # type: ignore

        def distance(self, other):
            """
            Get distance between two sites.

            Args:
                other: Other site.

            Returns:
                Distance (float)
            """
            return np.linalg.norm(other.coords - self.coords)

        def distance_from_point(self, pt):
            """
            Returns distance between the site and a point in space.

            Args:
                pt: Cartesian coordinates of point.

            Returns:
                Distance (float)
            """
            return np.linalg.norm(np.array(pt) - self.coords)

        @property
        def species_string(self):
            """
            String representation of species on the site.
            """
            if self.is_ordered:
                return list(self.species.keys())[0].__str__()
            sorted_species = sorted(self.species.keys())
            return ", ".join(["{}:{:.3f}".format(sp, self.species[sp])
                              for sp in sorted_species])

        @property  # type: ignore
        @deprecated(message="Use site.species instead. This will be deprecated with effect from pymatgen 2020.")
        def species_and_occu(self):
            """
            The species at the site, i.e., a Composition mapping type of
            element/species to occupancy.
            """
            return self.species

        @property
        def specie(self):
            """
            The Specie/Element at the site. Only works for ordered sites. Otherwise
            an AttributeError is raised. Use this property sparingly.  Robust
            design should make use of the property species_and_occu instead.

            Raises:
                AttributeError if Site is not ordered.
            """
            if not self.is_ordered:
                raise AttributeError("specie property only works for ordered "
                                     "sites!")
            return list(self.species.keys())[0]

        @property
        def is_ordered(self):
            """
            True if site is an ordered site, i.e., with a single species with
            occupancy 1.
            """
            totaloccu = self.species.num_atoms
            return totaloccu == 1 and len(self.species) == 1

        def __getitem__(self, el):
            """
            Get the occupancy for element
            """
            return self.species[el]

        def __eq__(self, other):
            """
            Site is equal to another site if the species and occupancies are the
            same, and the coordinates are the same to some tolerance.  numpy
            function `allclose` is used to determine if coordinates are close.
            """
            if other is None:
                return False
            return (self.species == other.species and
                    np.allclose(self.coords, other.coords,
                                atol=Site.position_atol) and
                    self.properties == other.properties)

        def __ne__(self, other):
            return not self.__eq__(other)

        def __hash__(self):
            """
            Minimally effective hash function that just distinguishes between Sites
            with different elements.
            """
            return sum([el.Z for el in self.species.keys()])

        def __contains__(self, el):
            return el in self.species

        def __repr__(self):
            return "Site: {} ({:.4f}, {:.4f}, {:.4f})".format(
                self.species_string, *self.coords)

        def __lt__(self, other):
            """
            Sets a default sort order for atomic species by electronegativity. Very
            useful for getting correct formulas.  For example, FeO4PLi is
            automatically sorted in LiFePO4.
            """
            if self.species.average_electroneg < other.species.average_electroneg:
                return True
            if self.species.average_electroneg > other.species.average_electroneg:
                return False
            if self.species_string < other.species_string:
                return True
            if self.species_string > other.species_string:
                return False
            return False

        def __str__(self):
            return "{} {}".format(self.coords, self.species_string)

        def as_dict(self):
            """
            Json-serializable dict representation for Site.
            """
            species_list = []
            for spec, occu in self.species.items():
                d = spec.as_dict()
                del d["@module"]
                del d["@class"]
                d["occu"] = occu
                species_list.append(d)
            d = {"name": self.species_string, "species": species_list,
                 "xyz": [float(c) for c in self.coords],
                 "properties": self.properties,
                 "@module": self.__class__.__module__,
                 "@class": self.__class__.__name__}
            if self.properties:
                d["properties"] = self.properties
            return d

        @classmethod
        def from_dict(cls, d: dict):
            """
            Create Site from dict representation
            """
            atoms_n_occu = {}
            for sp_occu in d["species"]:
                if "oxidation_state" in sp_occu and Element.is_valid_symbol(
                        sp_occu["element"]):
                    sp = Specie.from_dict(sp_occu)
                elif "oxidation_state" in sp_occu:
                    sp = DummySpecie.from_dict(sp_occu)
                else:
                    sp = Element(sp_occu["element"])
                atoms_n_occu[sp] = sp_occu["occu"]
            props = d.get("properties", None)
            if props is not None:
                for key in props.keys():
                    props[key] = json.loads(json.dumps(props[key], cls=MontyEncoder), cls=MontyDecoder)
            return cls(atoms_n_occu, d["xyz"], properties=props)

    class PeriodicSite(Site, MSONable):
        """
        Extension of generic Site object to periodic systems.
        PeriodicSite includes a lattice system.
        """

        def __init__(self,
                     species: Union[str, Element, Specie, DummySpecie, Dict, Composition],
                     coords: Union[Tuple, List, np.ndarray],
                     lattice: Lattice,
                     to_unit_cell: bool = False,
                     coords_are_cartesian: bool = False,
                     properties: dict = None,
                     skip_checks: bool = False):
            """
            Create a periodic site.

            :param species: Species on the site. Can be:
                i.  A Composition-type object (preferred)
                ii. An  element / specie specified either as a string
                    symbols, e.g. "Li", "Fe2+", "P" or atomic numbers,
                    e.g., 3, 56, or actual Element or Specie objects.
                iii.Dict of elements/species and occupancies, e.g.,
                    {"Fe" : 0.5, "Mn":0.5}. This allows the setup of
                    disordered structures.
            :param coords: Cartesian coordinates of site.
            :param lattice: Lattice associated with the site.
            :param to_unit_cell: Translates fractional coordinate to the
                basic unit cell, i.e. all fractional coordinates satisfy 0
                <= a < 1. Defaults to False.
            :param coords_are_cartesian: Set to True if you are providing
                cartesian coordinates. Defaults to False.
            :param properties: Properties associated with the site as a dict, e.g.
                {"magmom": 5}. Defaults to None.
            :param skip_checks: Whether to ignore all the usual checks and just
                create the site. Use this if the PeriodicSite is created in a
                controlled manner and speed is desired.
            """

            if coords_are_cartesian:
                frac_coords = lattice.get_fractional_coords(coords)
            else:
                frac_coords = coords

            if to_unit_cell:
                frac_coords = np.mod(frac_coords, 1)

            if not skip_checks:
                frac_coords = np.array(frac_coords)
                if not isinstance(species, Composition):
                    try:
                        species = Composition({get_el_sp(species): 1})
                    except TypeError:
                        species = Composition(species)

                totaloccu = species.num_atoms
                if totaloccu > 1 + Composition.amount_tolerance:
                    raise ValueError("Species occupancies sum to more than 1!")

            self._lattice = lattice
            self._frac_coords = frac_coords
            self._species = species
            self._coords = None
            self.properties = properties or {}

        def __hash__(self):
            """
            Minimally effective hash function that just distinguishes between Sites
            with different elements.
            """
            return sum([el.Z for el in self.species.keys()])

        @property
        def lattice(self):
            """
            Lattice associated with PeriodicSite
            """
            return self._lattice

        @lattice.setter
        def lattice(self, lattice: Lattice):
            """
            Sets Lattice associated with PeriodicSite
            """
            self._lattice = lattice
            self._coords = self._lattice.get_cartesian_coords(self._frac_coords)

        @property  # type: ignore
        def coords(self) -> np.ndarray:  # type: ignore
            """
            Cartesian coordinates
            """
            if self._coords is None:
                self._coords = self._lattice.get_cartesian_coords(self._frac_coords)
            return self._coords

        @coords.setter
        def coords(self, coords):
            """
            Set Cartesian coordinates
            """
            self._coords = np.array(coords)
            self._frac_coords = self._lattice.get_fractional_coords(self._coords)

        @property  # type: ignore
        def frac_coords(self) -> np.ndarray:
            """
            Fractional coordinates
            """
            return self._frac_coords

        @frac_coords.setter
        def frac_coords(self, frac_coords):
            """
            Set fractional coordinates
            """
            self._frac_coords = np.array(frac_coords)
            self._coords = self._lattice.get_cartesian_coords(self._frac_coords)

        @property
        def a(self):
            """
            Fractional a coordinate
            """
            return self._frac_coords[0]

        @a.setter
        def a(self, a):
            self._frac_coords[0] = a
            self._coords = self._lattice.get_cartesian_coords(self._frac_coords)

        @property
        def b(self):
            """
            Fractional b coordinate
            """
            return self._frac_coords[1]

        @b.setter
        def b(self, b):
            self._frac_coords[1] = b
            self._coords = self._lattice.get_cartesian_coords(self._frac_coords)

        @property
        def c(self):
            """
            Fractional c coordinate
            """
            return self._frac_coords[2]

        @c.setter
        def c(self, c):
            self._frac_coords[2] = c
            self._coords = self._lattice.get_cartesian_coords(self._frac_coords)

        @property
        def x(self):
            """
            Cartesian x coordinate
            """
            return self.coords[0]

        @x.setter
        def x(self, x):
            self.coords[0] = x
            self._frac_coords = self._lattice.get_fractional_coords(self.coords)

        @property
        def y(self):
            """
            Cartesian y coordinate
            """
            return self.coords[1]

        @y.setter
        def y(self, y):
            self.coords[1] = y
            self._frac_coords = self._lattice.get_fractional_coords(self.coords)

        @property
        def z(self):
            """
            Cartesian z coordinate
            """
            return self.coords[2]

        @z.setter
        def z(self, z):
            self.coords[2] = z
            self._frac_coords = self._lattice.get_fractional_coords(self.coords)

        def to_unit_cell(self, in_place=False):
            """
            Move frac coords to within the unit cell cell.
            """
            frac_coords = np.mod(self.frac_coords, 1)
            if in_place:
                self.frac_coords = frac_coords
                return None
            return PeriodicSite(self.species, frac_coords, self.lattice,
                                properties=self.properties)

        def is_periodic_image(self, other, tolerance=1e-8, check_lattice=True):
            """
            Returns True if sites are periodic images of each other.

            Args:
                other (PeriodicSite): Other site
                tolerance (float): Tolerance to compare fractional coordinates
                check_lattice (bool): Whether to check if the two sites have the
                    same lattice.

            Returns:
                bool: True if sites are periodic images of each other.
            """
            if check_lattice and self.lattice != other.lattice:
                return False
            if self.species != other.species:
                return False

            frac_diff = pbc_diff(self.frac_coords, other.frac_coords)
            return np.allclose(frac_diff, [0, 0, 0], atol=tolerance)

        def __eq__(self, other):
            return self.species == other.species and \
                   self.lattice == other.lattice and \
                   np.allclose(self.coords, other.coords,
                               atol=Site.position_atol) and \
                   self.properties == other.properties

        def __ne__(self, other):
            return not self.__eq__(other)

        def distance_and_image_from_frac_coords(self, fcoords, jimage=None):
            """
            Gets distance between site and a fractional coordinate assuming
            periodic boundary conditions. If the index jimage of two sites atom j
            is not specified it selects the j image nearest to the i atom and
            returns the distance and jimage indices in terms of lattice vector
            translations. If the index jimage of atom j is specified it returns the
            distance between the i atom and the specified jimage atom, the given
            jimage is also returned.

            Args:
                fcoords (3x1 array): fcoords to get distance from.
                jimage (3x1 array): Specific periodic image in terms of
                    lattice translations, e.g., [1,0,0] implies to take periodic
                    image that is one a-lattice vector away. If jimage is None,
                    the image that is nearest to the site is found.

            Returns:
                (distance, jimage): distance and periodic lattice translations
                of the other site for which the distance applies.
            """
            return self.lattice.get_distance_and_image(self.frac_coords, fcoords,
                                                       jimage=jimage)

        def distance_and_image(self, other, jimage=None):
            """
            Gets distance and instance between two sites assuming periodic boundary
            conditions. If the index jimage of two sites atom j is not specified it
            selects the j image nearest to the i atom and returns the distance and
            jimage indices in terms of lattice vector translations. If the index
            jimage of atom j is specified it returns the distance between the ith
            atom and the specified jimage atom, the given jimage is also returned.

            Args:
                other (PeriodicSite): Other site to get distance from.
                jimage (3x1 array): Specific periodic image in terms of lattice
                    translations, e.g., [1,0,0] implies to take periodic image
                    that is one a-lattice vector away. If jimage is None,
                    the image that is nearest to the site is found.

            Returns:
                (distance, jimage): distance and periodic lattice translations
                of the other site for which the distance applies.
            """
            return self.distance_and_image_from_frac_coords(other.frac_coords, jimage)

        def distance(self, other, jimage=None):
            """
            Get distance between two sites assuming periodic boundary conditions.

            Args:
                other (PeriodicSite): Other site to get distance from.
                jimage (3x1 array): Specific periodic image in terms of lattice
                    translations, e.g., [1,0,0] implies to take periodic image
                    that is one a-lattice vector away. If jimage is None,
                    the image that is nearest to the site is found.

            Returns:
                distance (float): Distance between the two sites
            """
            return self.distance_and_image(other, jimage)[0]

        def __repr__(self):
            return "PeriodicSite: {} ({:.4f}, {:.4f}, {:.4f}) [{:.4f}, {:.4f}, " \
                   "{:.4f}]".format(self.species_string, self.coords[0],
                                    self.coords[1], self.coords[2],
                                    *self._frac_coords)

        def as_dict(self, verbosity=0):
            """
            Json-serializable dict representation of PeriodicSite.

            Args:
                verbosity (int): Verbosity level. Default of 0 only includes the
                    matrix representation. Set to 1 for more details such as
                    cartesian coordinates, etc.
            """
            species_list = []
            for spec, occu in self._species.items():
                d = spec.as_dict()
                del d["@module"]
                del d["@class"]
                d["occu"] = occu
                species_list.append(d)

            d = {"species": species_list,
                 "abc": [float(c) for c in self._frac_coords],
                 "lattice": self._lattice.as_dict(verbosity=verbosity),
                 "@module": self.__class__.__module__,
                 "@class": self.__class__.__name__}

            if verbosity > 0:
                d["xyz"] = [float(c) for c in self.coords]
                d["label"] = self.species_string

            d["properties"] = self.properties

            return d

        @classmethod
        def from_dict(cls, d, lattice=None):
            """
            Create PeriodicSite from dict representation.

            Args:
                d (dict): dict representation of PeriodicSite
                lattice: Optional lattice to override lattice specified in d.
                    Useful for ensuring all sites in a structure share the same
                    lattice.

            Returns:
                PeriodicSite
            """
            species = {}
            for sp_occu in d["species"]:
                if "oxidation_state" in sp_occu and Element.is_valid_symbol(
                        sp_occu["element"]):
                    sp = Specie.from_dict(sp_occu)
                elif "oxidation_state" in sp_occu:
                    sp = DummySpecie.from_dict(sp_occu)
                else:
                    sp = Element(sp_occu["element"])
                species[sp] = sp_occu["occu"]
            props = d.get("properties", None)
            if props is not None:
                for key in props.keys():
                    props[key] = json.loads(json.dumps(props[key], cls=MontyEncoder), cls=MontyDecoder)
            lattice = lattice if lattice else Lattice.from_dict(d["lattice"])
            return cls(species, d["abc"], lattice, properties=props)