Skip to content
Permalink
 
 
Cannot retrieve contributors at this time
# 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 json
from typing import Optional, Tuple, Union
import numpy as np
from monty.dev import deprecated
from monty.json import MontyDecoder, MontyEncoder, MSONable
from pymatgen.core.composition import Composition
from pymatgen.core.lattice import Lattice
from pymatgen.core.periodic_table import DummySpecies, Element, Species, get_el_sp
from pymatgen.util.coord import pbc_diff
from pymatgen.util.typing import ArrayLike, SpeciesLike, CompositionLike
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[SpeciesLike, CompositionLike],
coords: ArrayLike,
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 / species specified either as a string
symbols, e.g. "Li", "Fe2+", "P" or atomic numbers,
e.g., 3, 56, or actual Element or Species 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: Composition = species # type: ignore
self.coords: np.ndarray = coords # type: ignore
self.properties: dict = 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: Union[SpeciesLike, CompositionLike]):
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) -> float:
"""
Cartesian x coordinate
"""
return self.coords[0] # type: ignore
@x.setter
def x(self, x: float):
self.coords[0] = x # type: ignore
@property
def y(self) -> float:
"""
Cartesian y coordinate
"""
return self.coords[1] # type: ignore
@y.setter
def y(self, y: float):
self.coords[1] = y # type: ignore
@property
def z(self) -> float:
"""
Cartesian z coordinate
"""
return self.coords[2] # type: ignore
@z.setter
def z(self, z: float):
self.coords[2] = z # type: ignore
def distance(self, other) -> float:
"""
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) -> float:
"""
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) -> str:
"""
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([f"{sp}:{self.species[sp]:.3f}" 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) -> Union[Element, Species, DummySpecies]:
"""
The Species/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 instead. Note that the
singular of species is also species. So the choice of this variable
name is governed by programmatic concerns as opposed to grammar.
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) -> bool:
"""
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 f"{self.coords} {self.species_string}"
def as_dict(self) -> dict:
"""
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], # type: ignore
"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) -> "Site":
"""
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 = Species.from_dict(sp_occu)
elif "oxidation_state" in sp_occu:
sp = DummySpecies.from_dict(sp_occu)
else:
sp = Element(sp_occu["element"]) # type: ignore
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[SpeciesLike, CompositionLike],
coords: ArrayLike,
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 / species specified either as a string
symbols, e.g. "Li", "Fe2+", "P" or atomic numbers,
e.g., 3, 56, or actual Element or Species 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 # type: ignore
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 = lattice
self._frac_coords: ArrayLike = frac_coords
self._species: Composition = species # type: ignore
self._coords: Optional[np.ndarray] = None
self.properties: dict = 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:
"""
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
def frac_coords(self) -> np.ndarray:
"""
Fractional coordinates
"""
return self._frac_coords # type: ignore
@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) -> float:
"""
Fractional a coordinate
"""
return self._frac_coords[0] # type: ignore
@a.setter
def a(self, a: float):
self._frac_coords[0] = a # type: ignore
self._coords = self._lattice.get_cartesian_coords(self._frac_coords)
@property
def b(self) -> float:
"""
Fractional b coordinate
"""
return self._frac_coords[1] # type: ignore
@b.setter
def b(self, b: float):
self._frac_coords[1] = b # type: ignore
self._coords = self._lattice.get_cartesian_coords(self._frac_coords)
@property
def c(self) -> float:
"""
Fractional c coordinate
"""
return self._frac_coords[2] # type: ignore
@c.setter
def c(self, c: float):
self._frac_coords[2] = c # type: ignore
self._coords = self._lattice.get_cartesian_coords(self._frac_coords)
@property
def x(self) -> float:
"""
Cartesian x coordinate
"""
return self.coords[0]
@x.setter
def x(self, x: float):
self.coords[0] = x
self._frac_coords = self._lattice.get_fractional_coords(self.coords)
@property
def y(self) -> float:
"""
Cartesian y coordinate
"""
return self.coords[1]
@y.setter
def y(self, y: float):
self.coords[1] = y
self._frac_coords = self._lattice.get_fractional_coords(self.coords)
@property
def z(self) -> float:
"""
Cartesian z coordinate
"""
return self.coords[2]
@z.setter
def z(self, z: float):
self.coords[2] = z
self._frac_coords = self._lattice.get_fractional_coords(self.coords)
def to_unit_cell(self, in_place=False) -> Optional["PeriodicSite"]:
"""
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: "PeriodicSite", tolerance: float = 1e-8, check_lattice: bool = True) -> bool:
"""
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: ArrayLike, jimage: Optional[ArrayLike] = None
) -> Tuple[float, np.ndarray]:
"""
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: "PeriodicSite", jimage: Optional[ArrayLike] = None) -> Tuple[float, np.ndarray]:
"""
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: "PeriodicSite", jimage: Optional[ArrayLike] = 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: int = 0) -> dict:
"""
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], # type: ignore
"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) -> "PeriodicSite":
"""
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 = Species.from_dict(sp_occu)
elif "oxidation_state" in sp_occu:
sp = DummySpecies.from_dict(sp_occu)
else:
sp = Element(sp_occu["element"]) # type: ignore
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)