def test_lll_basis(self): a = np.array([1., 0.1, 0.]) b = np.array([0., 2., 0.]) c = np.array([0., 0., 3.]) l1 = Lattice([a, b, c]) l2 = Lattice([a + b, b + c, c]) ccoords = np.array([[1, 1, 2], [2, 2, 1.5]]) l1_fcoords = l1.get_fractional_coords(ccoords) l2_fcoords = l2.get_fractional_coords(ccoords) self.assertArrayAlmostEqual(l1.matrix, l2.lll_matrix) self.assertArrayAlmostEqual(np.dot(l2.lll_mapping, l2.matrix), l1.matrix) self.assertArrayAlmostEqual(np.dot(l2_fcoords, l2.matrix), np.dot(l1_fcoords, l1.matrix)) lll_fcoords = l2.get_lll_frac_coords(l2_fcoords) self.assertArrayAlmostEqual(lll_fcoords, l1_fcoords) self.assertArrayAlmostEqual(l1.get_cartesian_coords(lll_fcoords), np.dot(lll_fcoords, l2.lll_matrix)) self.assertArrayAlmostEqual(l2.get_frac_coords_from_lll(lll_fcoords), l2_fcoords)
def __init__( self, species: Union[str, Element, Species, DummySpecies, 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 / 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 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 __init__(self, structure, scaling_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1))): """ Create a supercell. Arguments: structure: pymatgen.core.structure Structure object. scaling_matrix: a matrix of transforming the lattice vectors. Defaults to the identity matrix. Has to be all integers. e.g., [[2,1,0],[0,3,0],[0,0,1]] generates a new structure with lattice vectors a' = 2a + b, b' = 3b, c' = c where a, b, and c are the lattice vectors of the original structure. """ self._original_structure = structure old_lattice = structure.lattice scale_matrix = np.array(scaling_matrix) new_lattice = Lattice(np.dot(scale_matrix, old_lattice.matrix)) new_species = [] new_fcoords = [] def range_vec(i): return range(max(scale_matrix[:][:, i]) - min(scale_matrix[:][:, i])) for site in structure.sites: for (i, j, k) in itertools.product(range_vec(0), range_vec(1), range_vec(2)): new_species.append(site.species_and_occu) fcoords = site.frac_coords coords = old_lattice.get_cartesian_coords(fcoords + np.array([i, j, k])) new_fcoords.append(new_lattice.get_fractional_coords(coords)) self._modified_structure = Structure(new_lattice, new_species, new_fcoords, False)
def test_get_vector_along_lattice_directions(self): lattice_mat = np.array([[0.5, 0.0, 0.0], [0.5, np.sqrt(3) / 2.0, 0.0], [0.0, 0.0, 1.0]]) lattice = Lattice(lattice_mat) cart_coord = np.array([0.5, np.sqrt(3) / 4.0, 0.5]) latt_coord = np.array([0.25, 0.5, 0.5]) from_direct = lattice.get_fractional_coords( cart_coord) * lattice.lengths self.assertArrayAlmostEqual( lattice.get_vector_along_lattice_directions(cart_coord), from_direct) self.assertArrayAlmostEqual( lattice.get_vector_along_lattice_directions(cart_coord), latt_coord) self.assertArrayEqual( lattice.get_vector_along_lattice_directions(cart_coord).shape, [ 3, ], ) self.assertArrayEqual( lattice.get_vector_along_lattice_directions( cart_coord.reshape([1, 3])).shape, [1, 3], )
def test_get_points_in_sphere(self): # This is a non-niggli representation of a cubic lattice latt = Lattice([[1, 5, 0], [0, 1, 0], [5, 0, 1]]) # evenly spaced points array between 0 and 1 pts = np.array(list(itertools.product(range(5), repeat=3))) / 5 pts = latt.get_fractional_coords(pts) self.assertEqual( len(latt.get_points_in_sphere(pts, [0, 0, 0], 0.20001)), 7) self.assertEqual( len(latt.get_points_in_sphere(pts, [0.5, 0.5, 0.5], 1.0001)), 552)
def test_get_points_in_sphere(self): # This is a non-niggli representation of a cubic lattice latt = Lattice([[1,5,0],[0,1,0],[5,0,1]]) # evenly spaced points array between 0 and 1 pts = np.array(list(itertools.product(range(5), repeat=3))) / 5 pts = latt.get_fractional_coords(pts) self.assertEqual(len(latt.get_points_in_sphere( pts, [0, 0, 0], 0.20001)), 7) self.assertEqual(len(latt.get_points_in_sphere( pts, [0.5, 0.5, 0.5], 1.0001)), 552)
def test_get_vector_along_lattice_directions(self): lattice_mat = np.array([[0.5, 0., 0.], [0.5, np.sqrt(3) / 2., 0.], [0., 0., 1.0]]) lattice = Lattice(lattice_mat) cart_coord = np.array([0.5, np.sqrt(3)/4., 0.5]) latt_coord = np.array([0.25, 0.5, 0.5]) from_direct = lattice.get_fractional_coords(cart_coord) * lattice.lengths_and_angles[0] self.assertArrayAlmostEqual(lattice.get_vector_along_lattice_directions(cart_coord), from_direct) self.assertArrayAlmostEqual(lattice.get_vector_along_lattice_directions(cart_coord), latt_coord) self.assertArrayEqual(lattice.get_vector_along_lattice_directions(cart_coord).shape, [3,]) self.assertArrayEqual(lattice.get_vector_along_lattice_directions(cart_coord.reshape([1,3])).shape, [1,3])
def write_vib_file(self, xyz_file, qpoint, displ, do_real=True, frac_coords=True, scale_matrix=None, max_supercell=None): """ write into the file descriptor xyz_file the positions and displacements of the atoms Args: xyz_file: file_descriptor qpoint: qpoint to be analyzed displ: eigendisplacements to be analyzed do_real: True if you want to get only real part, False means imaginary part frac_coords: True if the eigendisplacements are given in fractional coordinates scale_matrix: Scale matrix for supercell max_supercell: Maximum size of supercell vectors with respect to primitive cell """ if scale_matrix is None: if max_supercell is None: raise ValueError("If scale_matrix is not provided, please provide max_supercell !") scale_matrix = self.get_smallest_supercell(qpoint, max_supercell=max_supercell) old_lattice = self._lattice new_lattice = Lattice(np.dot(scale_matrix, old_lattice.matrix)) tvects = self.get_trans_vect(scale_matrix) new_displ = np.zeros(3, dtype=np.float) fmtstr = "{{}} {{:.{0}f}} {{:.{0}f}} {{:.{0}f}} {{:.{0}f}} {{:.{0}f}} {{:.{0}f}}\n".format(6) for at, site in enumerate(self): for t in tvects: if do_real: new_displ[:] = np.real(np.exp(2*1j*np.pi*(np.dot(qpoint,t)))*displ[at,:]) else: new_displ[:] = np.imag(np.exp(2*1j*np.pi*(np.dot(qpoint,t)))*displ[at,:]) if frac_coords: # Convert to fractional coordinates. new_displ = self.lattice.get_cartesian_coords(new_displ) # We don't normalize here !!! fcoords = site.frac_coords + t coords = old_lattice.get_cartesian_coords(fcoords) new_fcoords = new_lattice.get_fractional_coords(coords) # New_fcoords -> map into 0 - 1 new_fcoords = np.mod(new_fcoords, 1) coords = new_lattice.get_cartesian_coords(new_fcoords) xyz_file.write(fmtstr.format(site.specie, coords[0], coords[1], coords[2], new_displ[0], new_displ[1], new_displ[2]))
def __init__(self, structure, scaling_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1))): """ Create a supercell. Args: structure: pymatgen.core.structure Structure object. scaling_matrix: a matrix of transforming the lattice vectors. Defaults to the identity matrix. Has to be all integers. e.g., [[2,1,0],[0,3,0],[0,0,1]] generates a new structure with lattice vectors a' = 2a + b, b' = 3b, c' = c where a, b, and c are the lattice vectors of the original structure. """ self._original_structure = structure old_lattice = structure.lattice scale_matrix = np.array(scaling_matrix) new_lattice = Lattice(np.dot(scale_matrix, old_lattice.matrix)) new_sites = [] def range_vec(i): return range( max(scale_matrix[:][:, i]) - min(scale_matrix[:][:, i]) + 1) for site in structure.sites: for (i, j, k) in itertools.product(range_vec(0), range_vec(1), range_vec(2)): fcoords = site.frac_coords + np.array([i, j, k]) coords = old_lattice.get_cartesian_coords(fcoords) new_coords = new_lattice.get_fractional_coords(coords) new_site = PeriodicSite(site.species_and_occu, new_coords, new_lattice, properties=site.properties) contains_site = False for s in new_sites: if s.is_periodic_image(new_site): contains_site = True break if not contains_site: new_sites.append(new_site) self._modified_structure = Structure.from_sites(new_sites)
def get_displacements(self): """ Return the initial structure and displacements for each time step. Used to interface with the DiffusionAnalyzer. Returns: Structure object, numpy array of displacements """ lattice = Lattice([[self.box_lengths[0], 0, 0], [0, self.box_lengths[1], 0], [0, 0, self.box_lengths[2]]]) mass_to_symbol = dict( (round(y["Atomic mass"], 1), x) for x, y in _pt_data.items()) unique_atomic_masses = np.array(self.lammps_data.atomic_masses)[:, 1] frac_coords = [] for step in range(self.timesteps.size): begin = step * self.natoms end = (step + 1) * self.natoms mol_vector_structured = \ self.trajectory[begin:end][:][["x", "y", "z"]] new_shape = mol_vector_structured.shape + (-1, ) mol_vector = mol_vector_structured.view( np.float64).reshape(new_shape) coords = mol_vector.copy() if step == 0: species = [ mass_to_symbol[round(unique_atomic_masses[atype - 1], 1)] for atype in self.trajectory[begin:end][:]["atom_type"] ] structure = Structure(lattice, species, coords, coords_are_cartesian=True) step_frac_coords = [ lattice.get_fractional_coords(crd) for crd in coords ] frac_coords.append(np.array(step_frac_coords)[:, None]) frac_coords = np.concatenate(frac_coords, axis=1) dp = frac_coords[:, 1:] - frac_coords[:, :-1] dp = dp - np.round(dp) f_disp = np.cumsum(dp, axis=1) disp = lattice.get_cartesian_coords(f_disp) return structure, disp
def test_get_points_in_sphere(self): # This is a non-niggli representation of a cubic lattice latt = Lattice([[1, 5, 0], [0, 1, 0], [5, 0, 1]]) # evenly spaced points array between 0 and 1 pts = np.array(list(itertools.product(range(5), repeat=3))) / 5 pts = latt.get_fractional_coords(pts) # Test getting neighbors within 1 neighbor distance of the origin fcoords, dists, inds, images = latt.get_points_in_sphere(pts, [0, 0, 0], 0.20001, zip_results=False) self.assertEqual(len(fcoords), 7) # There are 7 neighbors self.assertEqual(np.isclose(dists, 0.2).sum(), 6) # 6 are at 0.2 self.assertEqual(np.isclose(dists, 0).sum(), 1) # 1 is at 0 self.assertEqual(len(set(inds)), 7) # They have unique indices self.assertArrayEqual(images[np.isclose(dists, 0)], [[0, 0, 0]]) # More complicated case, using the zip output result = latt.get_points_in_sphere(pts, [0.5, 0.5, 0.5], 1.0001) self.assertEqual(len(result), 552) self.assertEqual(len(result[0]), 4) # coords, dists, ind, supercell
def get_displacements(self): """ Return the initial structure and displacements for each time step. Used to interface with the DiffusionAnalyzer. Returns: Structure object, numpy array of displacements """ lattice = Lattice([[self.box_lengths[0], 0, 0], [0, self.box_lengths[1], 0], [0, 0, self.box_lengths[2]]]) mass_to_symbol = dict( (round(y["Atomic mass"], 1), x) for x, y in _pt_data.items()) unique_atomic_masses = np.array([d["mass"] for d in self.lammps_data.masses]) frac_coords = [] for step in range(self.timesteps.size): begin = step * self.natoms end = (step + 1) * self.natoms mol_vector_structured = \ self.trajectory[begin:end][:][["x", "y", "z"]] new_shape = mol_vector_structured.shape + (-1,) mol_vector = mol_vector_structured.view(np.float64).reshape( new_shape) coords = mol_vector.copy() if step == 0: species = [ mass_to_symbol[round(unique_atomic_masses[atype - 1], 1)] for atype in self.trajectory[begin:end][:]["atom_type"]] structure = Structure(lattice, species, coords, coords_are_cartesian=True) step_frac_coords = [lattice.get_fractional_coords(crd) for crd in coords] frac_coords.append(np.array(step_frac_coords)[:, None]) frac_coords = np.concatenate(frac_coords, axis=1) dp = frac_coords[:, 1:] - frac_coords[:, :-1] dp = dp - np.round(dp) f_disp = np.cumsum(dp, axis=1) disp = lattice.get_cartesian_coords(f_disp) return structure, disp
class StructureEditor(StructureModifier): """ Editor for adding, removing and changing sites from a structure """ DISTANCE_TOLERANCE = 0.01 def __init__(self, structure): """ Args: structure: pymatgen.core.structure Structure object. """ self._original_structure = structure self._lattice = structure.lattice self._sites = list(structure.sites) def add_site_property(self, property_name, values): """ Adds a property to a site. Args: property_name: The name of the property to add. values: A sequence of values. Must be same length as number of sites. """ if len(values) != len(self._sites): raise ValueError("Values must be same length as sites.") for i in xrange(len(self._sites)): site = self._sites[i] props = site.properties if not props: props = {} props[property_name] = values[i] self._sites[i] = PeriodicSite(site.species_and_occu, site.frac_coords, self._lattice, properties=props) def replace_species(self, species_mapping): """ Swap species in a structure. Args: species_mapping: dict of species to swap. Species can be elements too. e.g., {Element("Li"): Element("Na")} performs a Li for Na substitution. The second species can be a sp_and_occu dict. For example, a site with 0.5 Si that is passed the mapping {Element('Si): {Element('Ge'):0.75, Element('C'):0.25} } will have .375 Ge and .125 C. """ def mod_site(site): new_atom_occu = dict() for sp, amt in site.species_and_occu.items(): if sp in species_mapping: if species_mapping[sp].__class__.__name__ in ('Element', 'Specie'): if species_mapping[sp] in new_atom_occu: new_atom_occu[species_mapping[sp]] += amt else: new_atom_occu[species_mapping[sp]] = amt elif species_mapping[sp].__class__.__name__ == 'dict': for new_sp, new_amt in species_mapping[sp].items(): if new_sp in new_atom_occu: new_atom_occu[new_sp] += amt * new_amt else: new_atom_occu[new_sp] = amt * new_amt else: if sp in new_atom_occu: new_atom_occu[sp] += amt else: new_atom_occu[sp] = amt return PeriodicSite(new_atom_occu, self._lattice.get_fractional_coords(site.coords), self._lattice) self._sites = map(mod_site, self._sites) def replace_site(self, index, species_n_occu): """ Replace a single site. Takes either a species or a dict of occus Args: index: The index of the site in the _sites list species: A species object """ self._sites[index] = PeriodicSite(species_n_occu, self._lattice.get_fractional_coords(self._sites[index].coords), self._lattice) def remove_species(self, species): """ Remove all occurrences of a species from a structure. Args: species: species to remove """ new_sites = [] for site in self._sites: new_sp_occu = {sp:amt for sp, amt in site.species_and_occu.items() if sp not in species} if len(new_sp_occu) > 0: new_sites.append(PeriodicSite(new_sp_occu, site.frac_coords, self._lattice)) self._sites = new_sites def append_site(self, species, coords, coords_are_cartesian=False, validate_proximity=True): """ Append a site to the structure at the end. Args: species: species of inserted site coords: coordinates of inserted site fractional_coord: Whether coordinates are cartesian. Defaults to False. validate_proximity: Whether to check if inserted site is too close to an existing site. Defaults to True. """ self.insert_site(len(self._sites), species, coords, coords_are_cartesian, validate_proximity) def insert_site(self, i, species, coords, coords_are_cartesian=False, validate_proximity=True, properties=None): """ Insert a site to the structure. Args: i: index to insert site species: species of inserted site coords: coordinates of inserted site coords_are_cartesian: Whether coordinates are cartesian. Defaults to False. validate_proximity: Whether to check if inserted site is too close to an existing site. Defaults to True. """ if not coords_are_cartesian: new_site = PeriodicSite(species, coords, self._lattice, properties=properties) else: new_site = PeriodicSite(species, self._lattice.get_fractional_coords(coords), self._lattice, properties=properties) if validate_proximity: for site in self._sites: if site.distance(new_site) < self.DISTANCE_TOLERANCE: raise ValueError("New site is too close to an existing site!") self._sites.insert(i, new_site) def delete_site(self, i): """ Delete site at index i. Args: i: index of site to delete. """ del(self._sites[i]) def delete_sites(self, indices): """ Delete sites with at indices. Args: indices: sequence of indices of sites to delete. """ self._sites = [self._sites[i] for i in range(len(self._sites)) if i not in indices] def apply_operation(self, symmop): """ Apply a symmetry operation to the structure and return the new structure. The lattice is operated by the rotation matrix only. Coords are operated in full and then transformed to the new lattice. Args: symmop: Symmetry operation to apply. """ self._lattice = Lattice([symmop.apply_rotation_only(row) for row in self._lattice.matrix]) def operate_site(site): new_cart = symmop.operate(site.coords) return PeriodicSite(site.species_and_occu, self._lattice.get_fractional_coords(new_cart), self._lattice) self._sites = map(operate_site, self._sites) def modify_lattice(self, new_lattice): """ Modify the lattice of the structure. Mainly used for changing the basis. Args: new_lattice: New lattice """ self._lattice = new_lattice new_sites = [] for site in self._sites: new_sites.append(PeriodicSite(site.species_and_occu, self._lattice.get_fractional_coords(site.coords), self._lattice)) self._sites = new_sites def translate_sites(self, indices, vector, frac_coords=True): """ Translate specific sites by some vector, keeping the sites within the unit cell. Args: sites: List of site indices on which to perform the translation. vector: Translation vector for sites. frac_coords: Boolean stating whether the vector corresponds to fractional or cartesian coordinates. """ for i in indices: site = self._sites[i] if frac_coords: fcoords = site.frac_coords + vector else: fcoords = self._lattice.get_fractional_coords(site.coords + vector) new_site = PeriodicSite(site.species_and_occu, fcoords, self._lattice, to_unit_cell=True, coords_are_cartesian=False) self._sites[i] = new_site def perturb_structure(self, distance=0.1): ''' performs a random perturbation of the sites in a structure to break symmetries Args: distance: distance by which to perturb each site ''' for i in range(len(self._sites)): vector = np.random.rand(3) vector /= np.linalg.norm(vector) / distance self.translate_sites([i], vector, frac_coords=False) @property def original_structure(self): return self._original_structure @property def modified_structure(self): coords = [site.frac_coords for site in self._sites] species = [site.species_and_occu for site in self._sites] props = {} if self._sites[0].properties: for k in self._sites[0].properties.keys(): props[k] = [site.properties[k] for site in self._sites] return Structure(self._lattice, species, coords, False, site_properties=props)
def write_vib_file(self, xyz_file, qpoint, displ, do_real=True, frac_coords=True, scale_matrix=None, max_supercell=None): """ write into the file descriptor xyz_file the positions and displacements of the atoms Args: xyz_file: file_descriptor qpoint: qpoint to be analyzed displ: eigendisplacements to be analyzed do_real: True if you want to get only real part, False means imaginary part frac_coords: True if the eigendisplacements are given in fractional coordinates scale_matrix: Scale matrix for supercell max_supercell: Maximum size of supercell vectors with respect to primitive cell """ if scale_matrix is None: if max_supercell is None: raise ValueError( "If scale_matrix is not provided, please provide max_supercell !" ) scale_matrix = self.get_smallest_supercell( qpoint, max_supercell=max_supercell) old_lattice = self._lattice new_lattice = Lattice(np.dot(scale_matrix, old_lattice.matrix)) tvects = self.get_trans_vect(scale_matrix) new_displ = np.zeros(3, dtype=np.float) fmtstr = "{{}} {{:.{0}f}} {{:.{0}f}} {{:.{0}f}} {{:.{0}f}} {{:.{0}f}} {{:.{0}f}}\n".format( 6) for at, site in enumerate(self): for t in tvects: if do_real: new_displ[:] = np.real( np.exp(2 * 1j * np.pi * (np.dot(qpoint, t))) * displ[at, :]) else: new_displ[:] = np.imag( np.exp(2 * 1j * np.pi * (np.dot(qpoint, t))) * displ[at, :]) if frac_coords: # Convert to fractional coordinates. new_displ = self.lattice.get_cartesian_coords(new_displ) # We don't normalize here !!! fcoords = site.frac_coords + t coords = old_lattice.get_cartesian_coords(fcoords) new_fcoords = new_lattice.get_fractional_coords(coords) # New_fcoords -> map into 0 - 1 new_fcoords = np.mod(new_fcoords, 1) coords = new_lattice.get_cartesian_coords(new_fcoords) xyz_file.write( fmtstr.format(site.specie, coords[0], coords[1], coords[2], new_displ[0], new_displ[1], new_displ[2]))
class StructureEditor(StructureModifier): """ Editor for adding, removing and changing sites from a structure """ DISTANCE_TOLERANCE = 0.01 def __init__(self, structure): """ Args: structure: pymatgen.core.structure Structure object. """ self._original_structure = structure self._lattice = structure.lattice self._sites = list(structure.sites) def add_site_property(self, property_name, values): """ Adds a property to a site. Args: property_name: The name of the property to add. values: A sequence of values. Must be same length as number of sites. """ if len(values) != len(self._sites): raise ValueError("Values must be same length as sites.") for i in xrange(len(self._sites)): site = self._sites[i] props = site.properties if not props: props = {} props[property_name] = values[i] self._sites[i] = PeriodicSite(site.species_and_occu, site.frac_coords, self._lattice, properties=props) def replace_species(self, species_mapping): """ Swap species in a structure. Args: species_mapping: dict of species to swap. Species can be elements too. e.g., {Element("Li"): Element("Na")} performs a Li for Na substitution. The second species can be a sp_and_occu dict. For example, a site with 0.5 Si that is passed the mapping {Element('Si): {Element('Ge'):0.75, Element('C'):0.25} } will have .375 Ge and .125 C. """ def mod_site(site): new_atom_occu = collections.defaultdict(int) for sp, amt in site.species_and_occu.items(): if sp in species_mapping: if isinstance(species_mapping[sp], (Element, Specie)): new_atom_occu[species_mapping[sp]] += amt elif isinstance(species_mapping[sp], dict): for new_sp, new_amt in species_mapping[sp].items(): new_atom_occu[new_sp] += amt * new_amt else: new_atom_occu[sp] += amt return PeriodicSite(new_atom_occu, site.frac_coords, self._lattice, properties=site.properties) self._sites = map(mod_site, self._sites) def replace_site(self, index, species_n_occu): """ Replace a single site. Takes either a species or a dict of species and occupations. Args: index: The index of the site in the _sites list. species: A species object. """ self._sites[index] = PeriodicSite( species_n_occu, self._sites[index].frac_coords, self._lattice, properties=self._sites[index].properties) def remove_species(self, species): """ Remove all occurrences of a species from a structure. Args: species: species to remove. """ new_sites = [] for site in self._sites: new_sp_occu = { sp: amt for sp, amt in site.species_and_occu.items() if sp not in species } if len(new_sp_occu) > 0: new_sites.append( PeriodicSite(new_sp_occu, site.frac_coords, self._lattice, properties=site.properties)) self._sites = new_sites def append_site(self, species, coords, coords_are_cartesian=False, validate_proximity=True): """ Append a site to the structure at the end. Args: species: species of inserted site coords: coordinates of inserted site fractional_coord: Whether coordinates are cartesian. Defaults to False. validate_proximity: Whether to check if inserted site is too close to an existing site. Defaults to True. """ self.insert_site(len(self._sites), species, coords, coords_are_cartesian, validate_proximity) def insert_site(self, i, species, coords, coords_are_cartesian=False, validate_proximity=True, properties=None): """ Insert a site to the structure. Args: i: index to insert site species: species of inserted site coords: coordinates of inserted site coords_are_cartesian: Whether coordinates are cartesian. Defaults to False. validate_proximity: Whether to check if inserted site is too close to an existing site. Defaults to True. """ if not coords_are_cartesian: new_site = PeriodicSite(species, coords, self._lattice, properties=properties) else: frac_coords = self._lattice.get_fractional_coords(coords) new_site = PeriodicSite(species, frac_coords, self._lattice, properties=properties) if validate_proximity: for site in self._sites: if site.distance(new_site) < self.DISTANCE_TOLERANCE: raise ValueError("New site is too close to an existing " "site!") self._sites.insert(i, new_site) def delete_site(self, i): """ Delete site at index i. Args: i: index of site to delete. """ del (self._sites[i]) def delete_sites(self, indices): """ Delete sites with at indices. Args: indices: sequence of indices of sites to delete. """ self._sites = [ self._sites[i] for i in range(len(self._sites)) if i not in indices ] def apply_operation(self, symmop): """ Apply a symmetry operation to the structure and return the new structure. The lattice is operated by the rotation matrix only. Coords are operated in full and then transformed to the new lattice. Args: symmop: Symmetry operation to apply. """ self._lattice = Lattice( [symmop.apply_rotation_only(row) for row in self._lattice.matrix]) def operate_site(site): new_cart = symmop.operate(site.coords) new_frac = self._lattice.get_fractional_coords(new_cart) return PeriodicSite(site.species_and_occu, new_frac, self._lattice, properties=site.properties) self._sites = map(operate_site, self._sites) def modify_lattice(self, new_lattice): """ Modify the lattice of the structure. Mainly used for changing the basis. Args: new_lattice: New lattice """ self._lattice = new_lattice new_sites = [] for site in self._sites: new_sites.append( PeriodicSite(site.species_and_occu, site.frac_coords, self._lattice, properties=site.properties)) self._sites = new_sites def apply_strain(self, strain): """ Apply an isotropic strain to the lattice. Args: strain: Amount of strain to apply. E.g., 0.01 means all lattice vectors are increased by 1%. This is equivalent to calling modify_lattice with a lattice with lattice parameters that are 1% larger. """ self.modify_lattice(Lattice(self._lattice.matrix * (1 + strain))) def translate_sites(self, indices, vector, frac_coords=True): """ Translate specific sites by some vector, keeping the sites within the unit cell. Args: sites: List of site indices on which to perform the translation. vector: Translation vector for sites. frac_coords: Boolean stating whether the vector corresponds to fractional or cartesian coordinates. """ for i in indices: site = self._sites[i] if frac_coords: fcoords = site.frac_coords + vector else: fcoords = self._lattice.get_fractional_coords(site.coords + vector) new_site = PeriodicSite(site.species_and_occu, fcoords, self._lattice, to_unit_cell=True, coords_are_cartesian=False, properties=site.properties) self._sites[i] = new_site def perturb_structure(self, distance=0.1): """ Performs a random perturbation of the sites in a structure to break symmetries. Args: distance: distance in angstroms by which to perturb each site. """ def get_rand_vec(): #deals with zero vectors. vector = np.random.randn(3) vnorm = np.linalg.norm(vector) return vector / vnorm * distance if vnorm != 0 else get_rand_vec() for i in range(len(self._sites)): self.translate_sites([i], get_rand_vec(), frac_coords=False) def add_oxidation_state_by_element(self, oxidation_states): """ Add oxidation states to a structure. Args: structure: pymatgen.core.structure Structure object. oxidation_states: dict of oxidation states. E.g., {"Li":1, "Fe":2, "P":5, "O":-2} """ try: for i, site in enumerate(self._sites): new_sp = {} for el, occu in site.species_and_occu.items(): sym = el.symbol new_sp[Specie(sym, oxidation_states[sym])] = occu new_site = PeriodicSite(new_sp, site.frac_coords, self._lattice, coords_are_cartesian=False, properties=site.properties) self._sites[i] = new_site except KeyError: raise ValueError("Oxidation state of all elements must be " "specified in the dictionary.") def add_oxidation_state_by_site(self, oxidation_states): """ Add oxidation states to a structure by site. Args: oxidation_states: List of oxidation states. E.g., [1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2] """ try: for i, site in enumerate(self._sites): new_sp = {} for el, occu in site.species_and_occu.items(): sym = el.symbol new_sp[Specie(sym, oxidation_states[i])] = occu new_site = PeriodicSite(new_sp, site.frac_coords, self._lattice, coords_are_cartesian=False, properties=site.properties) self._sites[i] = new_site except IndexError: raise ValueError("Oxidation state of all sites must be " "specified in the dictionary.") def remove_oxidation_states(self): """ Removes oxidation states from a structure. """ for i, site in enumerate(self._sites): new_sp = collections.defaultdict(float) for el, occu in site.species_and_occu.items(): sym = el.symbol new_sp[Element(sym)] += occu new_site = PeriodicSite(new_sp, site.frac_coords, self._lattice, coords_are_cartesian=False, properties=site.properties) self._sites[i] = new_site def to_unit_cell(self, tolerance=0.1): """ Returns all the sites to their position inside the unit cell. If there is a site within the tolerance already there, the site is deleted instead of moved. """ new_sites = [] for site in self._sites: if not new_sites: new_sites.append(site) frac_coords = np.array([site.frac_coords]) continue if len( get_points_in_sphere_pbc(self._lattice, frac_coords, site.coords, tolerance)): continue frac_coords = np.append(frac_coords, [site.frac_coords % 1], axis=0) new_sites.append(site.to_unit_cell) self._sites = new_sites @property def original_structure(self): """ The original structure. """ return self._original_structure @property def modified_structure(self): coords = [site.frac_coords for site in self._sites] species = [site.species_and_occu for site in self._sites] props = {} if self._sites[0].properties: for k in self._sites[0].properties.keys(): props[k] = [site.properties[k] for site in self._sites] return Structure(self._lattice, species, coords, False, site_properties=props)
class Topology(object): """ """ def __init__(self, name: str, slots: List[Fragment], cell: Union[numpy.ndarray, Lattice]) -> None: """ Parameters ---------- name : str the name given to the topology (RCSR symbol in defaults) slots : List[Fragment] the list of Fragment objects describing the orientation and connectivity of slots in the topology. cell : numpy.ndarray The information on periodicity in matrix form (3x3) """ self.name = name if isinstance(cell, Lattice): self.cell = cell else: self.cell = Lattice(cell) self.slots = numpy.array(slots, dtype=object) sizes = [len(fragment.atoms) for fragment in self.slots] self.sizes = numpy.array(sizes, dtype=numpy.int8) mappings = {} for slot_type in set(slots): mappings[slot_type] = [ i for i, s in enumerate(slots) if s == slot_type ] self.mappings = mappings return None def __len__(self): return len(self.slots) def __repr__(self): return self.name def copy(self) -> Topology: """ Provides a deep copy of the starting object Returns ------- Topology the copy of the starting object """ return copy.deepcopy(self) def get_compatible_slots(self, candidate: Fragment) -> Dict[Fragment, List[int]]: """ Returns a dictionary of the slot indices available for a candidate Fragment object, taking into account the symmetry elements common to it and the slots. Parameters ---------- candidate : Fragment the query Fragment with which to test compatibility Returns ------- Dict[Fragment, List[int]] A ictionary of available slot indices """ available_slots = {} for slot in self.mappings: available_slots[slot] = [] if slot.has_compatible_symmetry(candidate): available_slots[slot] += self.mappings[slot] return available_slots def scale_slots( self, scales: Tuple[float, float, float] = (1.0, 1.0, 1.0)) -> None: """ Applies in-place a scaling along cell vectors of the slots contines in the topology. TODO: rename scales to three a, b, c parameters for clarity Parameters ---------- scales : Tuple[float, float, float], optional the cell vector lengths to apply, by default (1.0, 1.0, 1.0) """ alpha, beta, gamma = self.cell.angles a, b, c = scales scaled_cell = Lattice.from_parameters(a, b, c, alpha, beta, gamma) scaled_slots = [] for slot in self.slots: scaled_slot = copy.deepcopy(slot) fract_coords = self.cell.get_fractional_coords( slot.atoms.cart_coords) scaled_coords = scaled_cell.get_cartesian_coords(fract_coords) scaled_slot.atoms = Molecule( slot.atoms.species, scaled_coords, site_properties=slot.atoms.site_properties) scaled_slots.append(scaled_slot) self.slots = scaled_slots self.cell = scaled_cell return None
class StructureEditor(StructureModifier): """ Editor for adding, removing and changing sites from a structure """ DISTANCE_TOLERANCE = 0.01 def __init__(self, structure): """ Args: structure: pymatgen.core.structure Structure object. """ self._original_structure = structure self._lattice = structure.lattice self._sites = list(structure.sites) def add_site_property(self, property_name, values): """ Adds a property to a site. Args: property_name: The name of the property to add. values: A sequence of values. Must be same length as number of sites. """ if len(values) != len(self._sites): raise ValueError("Values must be same length as sites.") for i in xrange(len(self._sites)): site = self._sites[i] props = site.properties if not props: props = {} props[property_name] = values[i] self._sites[i] = PeriodicSite(site.species_and_occu, site.frac_coords, self._lattice, properties=props) def replace_species(self, species_mapping): """ Swap species in a structure. Args: species_mapping: dict of species to swap. Species can be elements too. e.g., {Element("Li"): Element("Na")} performs a Li for Na substitution. The second species can be a sp_and_occu dict. For example, a site with 0.5 Si that is passed the mapping {Element('Si): {Element('Ge'):0.75, Element('C'):0.25} } will have .375 Ge and .125 C. """ def mod_site(site): new_atom_occu = dict() for sp, amt in site.species_and_occu.items(): if sp in species_mapping: if isinstance(species_mapping[sp], (Element, Specie)): if species_mapping[sp] in new_atom_occu: new_atom_occu[species_mapping[sp]] += amt else: new_atom_occu[species_mapping[sp]] = amt elif isinstance(species_mapping[sp], dict): for new_sp, new_amt in species_mapping[sp].items(): if new_sp in new_atom_occu: new_atom_occu[new_sp] += amt * new_amt else: new_atom_occu[new_sp] = amt * new_amt else: if sp in new_atom_occu: new_atom_occu[sp] += amt else: new_atom_occu[sp] = amt return PeriodicSite(new_atom_occu, site.frac_coords, self._lattice, properties=site.properties) self._sites = map(mod_site, self._sites) def replace_site(self, index, species_n_occu): """ Replace a single site. Takes either a species or a dict of species and occupations. Args: index: The index of the site in the _sites list. species: A species object. """ self._sites[index] = PeriodicSite(species_n_occu, self._sites[index].frac_coords, self._lattice, properties=self._sites[index]. properties) def remove_species(self, species): """ Remove all occurrences of a species from a structure. Args: species: species to remove. """ new_sites = [] for site in self._sites: new_sp_occu = {sp: amt for sp, amt in site.species_and_occu.items() if sp not in species} if len(new_sp_occu) > 0: new_sites.append(PeriodicSite(new_sp_occu, site.frac_coords, self._lattice, properties=site.properties)) self._sites = new_sites def append_site(self, species, coords, coords_are_cartesian=False, validate_proximity=True): """ Append a site to the structure at the end. Args: species: species of inserted site coords: coordinates of inserted site fractional_coord: Whether coordinates are cartesian. Defaults to False. validate_proximity: Whether to check if inserted site is too close to an existing site. Defaults to True. """ self.insert_site(len(self._sites), species, coords, coords_are_cartesian, validate_proximity) def insert_site(self, i, species, coords, coords_are_cartesian=False, validate_proximity=True, properties=None): """ Insert a site to the structure. Args: i: index to insert site species: species of inserted site coords: coordinates of inserted site coords_are_cartesian: Whether coordinates are cartesian. Defaults to False. validate_proximity: Whether to check if inserted site is too close to an existing site. Defaults to True. """ if not coords_are_cartesian: new_site = PeriodicSite(species, coords, self._lattice, properties=properties) else: frac_coords = self._lattice.get_fractional_coords(coords) new_site = PeriodicSite(species, frac_coords, self._lattice, properties=properties) if validate_proximity: for site in self._sites: if site.distance(new_site) < self.DISTANCE_TOLERANCE: raise ValueError("New site is too close to an existing " "site!") self._sites.insert(i, new_site) def delete_site(self, i): """ Delete site at index i. Args: i: index of site to delete. """ del(self._sites[i]) def delete_sites(self, indices): """ Delete sites with at indices. Args: indices: sequence of indices of sites to delete. """ self._sites = [self._sites[i] for i in range(len(self._sites)) if i not in indices] def apply_operation(self, symmop): """ Apply a symmetry operation to the structure and return the new structure. The lattice is operated by the rotation matrix only. Coords are operated in full and then transformed to the new lattice. Args: symmop: Symmetry operation to apply. """ self._lattice = Lattice([symmop.apply_rotation_only(row) for row in self._lattice.matrix]) def operate_site(site): new_cart = symmop.operate(site.coords) new_frac = self._lattice.get_fractional_coords(new_cart) return PeriodicSite(site.species_and_occu, new_frac, self._lattice, properties=site.properties) self._sites = map(operate_site, self._sites) def modify_lattice(self, new_lattice): """ Modify the lattice of the structure. Mainly used for changing the basis. Args: new_lattice: New lattice """ self._lattice = new_lattice new_sites = [] for site in self._sites: new_sites.append(PeriodicSite(site.species_and_occu, site.frac_coords, self._lattice, properties=site.properties)) self._sites = new_sites def apply_strain(self, strain): """ Apply an isotropic strain to the lattice. Args: strain: Amount of strain to apply. E.g., 0.01 means all lattice vectors are increased by 1%. This is equivalent to calling modify_lattice with a lattice with lattice parameters that are 1% larger. """ self.modify_lattice(Lattice(self._lattice.matrix * (1 + strain))) def translate_sites(self, indices, vector, frac_coords=True): """ Translate specific sites by some vector, keeping the sites within the unit cell. Args: sites: List of site indices on which to perform the translation. vector: Translation vector for sites. frac_coords: Boolean stating whether the vector corresponds to fractional or cartesian coordinates. """ for i in indices: site = self._sites[i] if frac_coords: fcoords = site.frac_coords + vector else: fcoords = self._lattice.get_fractional_coords(site.coords + vector) new_site = PeriodicSite(site.species_and_occu, fcoords, self._lattice, to_unit_cell=True, coords_are_cartesian=False, properties=site.properties) self._sites[i] = new_site def perturb_structure(self, distance=0.1): """ Performs a random perturbation of the sites in a structure to break symmetries. Args: distance: distance in angstroms by which to perturb each site. """ def get_rand_vec(): #deals with zero vectors. vector = np.random.randn(3) vnorm = np.linalg.norm(vector) return vector / vnorm * distance if vnorm != 0 else get_rand_vec() for i in range(len(self._sites)): self.translate_sites([i], get_rand_vec(), frac_coords=False) def add_oxidation_state_by_element(self, oxidation_states): """ Add oxidation states to a structure. Args: structure: pymatgen.core.structure Structure object. oxidation_states: dict of oxidation states. E.g., {"Li":1, "Fe":2, "P":5, "O":-2} """ try: for i, site in enumerate(self._sites): new_sp = {} for el, occu in site.species_and_occu.items(): sym = el.symbol new_sp[Specie(sym, oxidation_states[sym])] = occu new_site = PeriodicSite(new_sp, site.frac_coords, self._lattice, coords_are_cartesian=False, properties=site.properties) self._sites[i] = new_site except KeyError: raise ValueError("Oxidation state of all elements must be " "specified in the dictionary.") def add_oxidation_state_by_site(self, oxidation_states): """ Add oxidation states to a structure by site. Args: oxidation_states: List of oxidation states. E.g., [1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2] """ try: for i, site in enumerate(self._sites): new_sp = {} for el, occu in site.species_and_occu.items(): sym = el.symbol new_sp[Specie(sym, oxidation_states[i])] = occu new_site = PeriodicSite(new_sp, site.frac_coords, self._lattice, coords_are_cartesian=False, properties=site.properties) self._sites[i] = new_site except IndexError: raise ValueError("Oxidation state of all sites must be " "specified in the dictionary.") def remove_oxidation_states(self): """ Removes oxidation states from a structure. """ for i, site in enumerate(self._sites): new_sp = collections.defaultdict(float) for el, occu in site.species_and_occu.items(): sym = el.symbol new_sp[Element(sym)] += occu new_site = PeriodicSite(new_sp, site.frac_coords, self._lattice, coords_are_cartesian=False, properties=site.properties) self._sites[i] = new_site def to_unit_cell(self, tolerance=0.1): """ Returns all the sites to their position inside the unit cell. If there is a site within the tolerance already there, the site is deleted instead of moved. """ new_sites = [] for site in self._sites: if not new_sites: new_sites.append(site) frac_coords = np.array([site.frac_coords]) continue if len(get_points_in_sphere_pbc(self._lattice, frac_coords, site.coords, tolerance)): continue frac_coords = np.append(frac_coords, [site.frac_coords % 1], axis=0) new_sites.append(site.to_unit_cell) self._sites = new_sites @property def original_structure(self): """ The original structure. """ return self._original_structure @property def modified_structure(self): coords = [site.frac_coords for site in self._sites] species = [site.species_and_occu for site in self._sites] props = {} if self._sites[0].properties: for k in self._sites[0].properties.keys(): props[k] = [site.properties[k] for site in self._sites] return Structure(self._lattice, species, coords, False, site_properties=props)
def get_primitive_structure(self, tolerance=0.25): """ This finds a smaller unit cell than the input. Sometimes it doesn"t find the smallest possible one, so this method is recursively called until it is unable to find a smaller cell. The method works by finding possible smaller translations and then using that translational symmetry instead of one of the lattice basis vectors if more than one vector is found (usually the case for large cells) the one with the smallest norm is used. Things are done in fractional coordinates because its easier to translate back to the unit cell. NOTE: if the tolerance is greater than 1/2 the minimum inter-site distance, the algorithm may find 2 non-equivalent sites that are within tolerance of each other. The algorithm will reject this lattice. Args: tolerance: Tolerance for each coordinate of a particular site. For example, [0.5, 0, 0.5] in cartesian coordinates will be considered to be on the same coordinates as [0, 0, 0] for a tolerance of 0.5. Defaults to 0.5. Returns: The most primitive structure found. The returned structure is guaranteed to have len(new structure) <= len(structure). """ original_volume = self.volume (reduced_formula, num_fu) =\ self.composition.get_reduced_composition_and_factor() min_vol = original_volume * 0.5 / num_fu #get the possible symmetry vectors sites = sorted(self._sites, key=lambda site: site.species_string) grouped_sites = [list(a[1]) for a in itertools.groupby(sites, key=lambda s: s.species_string)] min_site_list = min(grouped_sites, key=lambda group: len(group)) min_site_list = [site.to_unit_cell for site in min_site_list] org = min_site_list[0].coords possible_vectors = [min_site_list[i].coords - org for i in xrange(1, len(min_site_list))] #Let's try to use the shortest vector possible first. Allows for faster #convergence to primitive cell. possible_vectors = sorted(possible_vectors, key=lambda x: np.linalg.norm(x)) # Pre-create a few varibles for faster lookup. all_coords = [site.coords for site in sites] all_sp = [site.species_and_occu for site in sites] new_structure = None #all lattice points need to be projected to 0 under new basis l_points = np.array([[0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]) l_points = self._lattice.get_cartesian_coords(l_points) for v, repl_pos in itertools.product(possible_vectors, xrange(3)): #Try combinations of new lattice vectors with existing lattice #vectors. latt = self._lattice.matrix latt[repl_pos] = v #Exclude coplanar lattices from consideration. if abs(np.dot(np.cross(latt[0], latt[1]), latt[2])) < min_vol: continue latt = Lattice(latt) #Convert to fractional tol tol = tolerance / np.array(latt.abc) #check validity of new basis new_l_points = latt.get_fractional_coords(l_points) f_l_dist = np.abs(new_l_points - np.round(new_l_points)) if np.any(f_l_dist > tol[None, None, :]): continue all_frac = latt.get_fractional_coords(np.array(all_coords)) #calculate grouping of equivalent sites, represented by #adjacency matrix fdist = all_frac[None, :, :] - all_frac[:, None, :] fdist = np.abs(fdist - np.round(fdist)) groups = np.all(fdist < tol[None, None, :], axis=2) #check that all group sizes are the same sizes = np.unique(np.sum(groups, axis=0)) if len(sizes) > 1: continue #check that reduction in number of sites was by the same #amount as the volume reduction if round(self._lattice.volume / latt.volume) != sizes[0]: continue new_sp = [] new_frac = [] #this flag is set to ensure that all sites in a group are #the same species, it is set to false if a group is found #where this is not the case correct = True added = np.zeros(len(groups), dtype='bool') for i, g in enumerate(groups): if added[i]: continue indices = np.where(g)[0] i0 = indices[0] sp = all_sp[i0] added[indices] = 1 if not all([all_sp[i] == sp for i in indices]): correct = False break new_sp.append(all_sp[i0]) new_frac.append(all_frac[i0]) if correct: new_structure = Structure(latt, new_sp, new_frac, to_unit_cell=True) break if new_structure and len(new_structure) != len(self): # If a more primitive structure has been found, try to find an # even more primitive structure again. return new_structure.get_primitive_structure(tolerance=tolerance) else: return self