class Site(collections.Mapping, collections.Hashable, PMGSONable): """ 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, atoms_n_occu, coords, properties=None): """ Create a *non-periodic* site. Args: atoms_n_occu: Species on the site. Can be: i. A sequence of element / specie specified either as string symbols, e.g. ["Li", "Fe2+", "P", ...] or atomic numbers, e.g., (3, 56, ...) or actual Element or Specie objects. ii. List of dict of elements/species and occupancies, e.g., [{"Fe" : 0.5, "Mn":0.5}, ...]. This allows the setup of disordered structures. coords: Cartesian coordinates of site. properties: Properties associated with the site as a dict, e.g. {"magmom": 5}. Defaults to None. """ if isinstance(atoms_n_occu, collections.Mapping): self._species = Composition(atoms_n_occu) totaloccu = self._species.num_atoms if totaloccu > 1 + Composition.amount_tolerance: raise ValueError("Species occupancies sum to more than 1!") self._is_ordered = totaloccu == 1 and len(self._species) == 1 else: self._species = Composition({get_el_sp(atoms_n_occu): 1}) self._is_ordered = True self._coords = coords self._properties = properties if properties else {} @property def properties(self): """ Returns a view of properties as a dict. """ return {k: v for k, v in self._properties.items()} 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) 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__() else: sorted_species = sorted(self._species.keys()) return ", ".join(["{}:{:.3f}".format(sp, self._species[sp]) for sp in sorted_species]) @property 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 coords(self): """ A copy of the cartesian coordinates of the site as a numpy array. """ return np.copy(self._coords) @property def is_ordered(self): """ True if site is an ordered site, i.e., with a single species with occupancy 1. """ return self._is_ordered @property def x(self): """ Cartesian x coordinate """ return self._coords[0] @property def y(self): """ Cartesian y coordinate """ return self._coords[1] @property def z(self): """ Cartesian z coordinate """ return self._coords[2] 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 __len__(self): return len(self._species) def __iter__(self): return self._species.__iter__() 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) return {"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__} @classmethod def from_dict(cls, d): """ 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) return cls(atoms_n_occu, d["xyz"], properties=props)
class Site(collections.Mapping, collections.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, atoms_n_occu, coords, properties=None): """ Create a *non-periodic* site. Args: atoms_n_occu: Species on the site. Can be: i. A sequence of element / specie specified either as string symbols, e.g. ["Li", "Fe2+", "P", ...] or atomic numbers, e.g., (3, 56, ...) or actual Element or Specie objects. ii. List of dict of elements/species and occupancies, e.g., [{"Fe" : 0.5, "Mn":0.5}, ...]. This allows the setup of disordered structures. coords: Cartesian coordinates of site. properties: Properties associated with the site as a dict, e.g. {"magmom": 5}. Defaults to None. """ if isinstance(atoms_n_occu, collections.Mapping): self._species = Composition(atoms_n_occu) totaloccu = self._species.num_atoms if totaloccu > 1 + Composition.amount_tolerance: raise ValueError("Species occupancies sum to more than 1!") self._is_ordered = totaloccu == 1 and len(self._species) == 1 else: self._species = Composition({get_el_sp(atoms_n_occu): 1}) self._is_ordered = True self._coords = coords self._properties = properties if properties else {} @property def properties(self): """ Returns a view of properties as a dict. """ return {k: v for k, v in self._properties.items()} 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) 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__() else: sorted_species = sorted(self._species.keys()) return ", ".join([ "{}:{:.3f}".format(sp, self._species[sp]) for sp in sorted_species ]) @property 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 coords(self): """ A copy of the cartesian coordinates of the site as a numpy array. """ return np.copy(self._coords) @property def is_ordered(self): """ True if site is an ordered site, i.e., with a single species with occupancy 1. """ return self._is_ordered @property def x(self): """ Cartesian x coordinate """ return self._coords[0] @property def y(self): """ Cartesian y coordinate """ return self._coords[1] @property def z(self): """ Cartesian z coordinate """ return self._coords[2] 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 __len__(self): return len(self._species) def __iter__(self): return self._species.__iter__() 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) return { "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__ } @classmethod def from_dict(cls, d): """ 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) return cls(atoms_n_occu, d["xyz"], properties=props)