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 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 calc_lattice_energy_and_pot(atomic_coords_without_defect: List[np.ndarray], charge: int, defect_center_coords: List[float], ewald: "Ewald", lattice: Lattice) -> Tuple[float, List[float]]: """ Args: atomic_coords_without_defect (List): List of fractional coordinates for all the atomic sites except for the defect. e.g., [[0.0, 0.0, 0.0], [0.1, 0.1, 0.1], ...] charge: Defect charge state. defect_center_coords: Defect center position in fractional coordinates. ewald: Ewald object. lattice: Lattice object for the supercell. Returns: Tuple of the lattice energy and list of the model potential. """ volume = lattice.volume coeff, diff_pot, mod_ewald_param, root_det_epsilon = \ constants_for_anisotropic_ewald_sum(charge, ewald, volume) # model potential and lattice energy model_pot = [] for r in atomic_coords_without_defect: # Ewald real part # \sum erfc(ewald*\sqrt(R*\epsilon_inv*R)) # / \sqrt(det(\epsilon)) / \sqrt(R*\epsilon_inv*R) [1/A] shift = lattice.get_cartesian_coords(r - defect_center_coords) real_part, reciprocal_part = \ calc_ewald_sum(ewald.dielectric_tensor, ewald.real_lattice_set(True, shift), ewald.reciprocal_lattice_set(), mod_ewald_param=mod_ewald_param, root_det_epsilon=root_det_epsilon, volume=volume) model_pot.append((real_part + reciprocal_part + diff_pot) * coeff) lattice_energy = point_charge_energy(charge, ewald, volume) return lattice_energy, model_pot
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_distance_and_image_strict(self): for count in range(10): lengths = [np.random.randint(1, 100) for i in range(3)] lattice = [np.random.rand(3) * lengths[i] for i in range(3)] lattice = Lattice(np.array(lattice)) f1 = np.random.rand(3) f2 = np.random.rand(3) scope = list(range(-3, 4)) min_image_dist = (float("inf"), None) for image in itertools.product(scope, scope, scope): cart = lattice.get_cartesian_coords(f1 - (f2 + image)) dist = np.dot(cart, cart) ** 0.5 if dist < min_image_dist[0]: min_image_dist = (dist, image) pmg_result = lattice.get_distance_and_image(f1, f2) self.assertGreaterEqual(min_image_dist[0] + 1e-7, pmg_result[0])
def test_get_distance_and_image_strict(self): for count in range(10): lengths = [np.random.randint(1, 100) for i in range(3)] lattice = [np.random.rand(3) * lengths[i] for i in range(3)] lattice = Lattice(np.array(lattice)) f1 = np.random.rand(3) f2 = np.random.rand(3) scope = list(range(-3, 4)) min_image_dist = (float("inf"), None) for image in itertools.product(scope, scope, scope): cart = lattice.get_cartesian_coords(f1 - (f2 + image)) dist = np.dot(cart, cart)**0.5 if dist < min_image_dist[0]: min_image_dist = (dist, image) pmg_result = lattice.get_distance_and_image(f1, f2) self.assertGreaterEqual(min_image_dist[0] + 1e-7, pmg_result[0]) if abs(min_image_dist[0] - pmg_result[0]) < 1e-12: self.assertArrayAlmostEqual(min_image_dist[1], pmg_result[1])
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
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 Structure(SiteCollection, MSONable): """ Basic Structure object with periodicity. Essentially a sequence of PeriodicSites having a common lattice. Structure is made to be immutable so that they can function as keys in a dict. Modifications should be done by making a new Structure using the structure_modifier module or your own methods. Structure extends Sequence and Hashable, which means that in many cases, it can be used like any Python sequence. Iterating through a structure is equivalent to going through the sites in sequence. """ def __init__(self, lattice, species, coords, validate_proximity=False, to_unit_cell=False, coords_are_cartesian=False, site_properties=None): """ Create a periodic structure. Args: lattice: The lattice, either as a pymatgen.core.lattice.Lattice or simply as any 2D array. Each row should correspond to a lattice vector. E.g., [[10,0,0], [20,10,0], [0,0,30]] specifies a lattice with lattice vectors [10,0,0], [20,10,0] and [0,0,30]. species: List of species on each site. Can take in flexible input, including: 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. fractional_coords: list of fractional coordinates of each species. validate_proximity: Whether to check if there are sites that are less than 0.01 Ang apart. Defaults to False. coords_are_cartesian: Set to True if you are providing coordinates in cartesian coordinates. Defaults to False. site_properties: Properties associated with the sites as a dict of sequences, e.g., {"magmom":[5,5,5,5]}. The sequences have to be the same length as the atomic species and fractional_coords. Defaults to None for no properties. """ if len(species) != len(coords): raise StructureError("The list of atomic species must be of the" "same length as the list of fractional" " coordinates.") if isinstance(lattice, Lattice): self._lattice = lattice else: self._lattice = Lattice(lattice) sites = [] for i in xrange(len(species)): prop = None if site_properties: prop = {k: v[i] for k, v in site_properties.items()} sites.append(PeriodicSite(species[i], coords[i], self._lattice, to_unit_cell, coords_are_cartesian, properties=prop)) self._sites = tuple(sites) if validate_proximity and not self.is_valid(): raise StructureError(("Structure contains sites that are ", "less than 0.01 Angstrom apart!")) @staticmethod def from_sites(sites): """ Convenience constructor to make a Structure from a list of sites. Args: sites: Sequence of PeriodicSites. Sites must have the same lattice. """ props = collections.defaultdict(list) lattice = None for site in sites: if not lattice: lattice = site.lattice elif site.lattice != lattice: raise ValueError("Sites must belong to the same lattice") for k, v in site.properties.items(): props[k].append(v) return Structure(lattice, [site.species_and_occu for site in sites], [site.frac_coords for site in sites], site_properties=props) @property def sites(self): """ Returns an iterator for the sites in the Structure. """ return self._sites @property def lattice(self): """ Lattice of the structure. """ return self._lattice def lattice_vectors(self, space="r"): """ Returns the vectors of the unit cell in Angstrom. Args: space: "r" for real space vectors, "g" for reciprocal space basis vectors. """ if space.lower() == "r": return self.lattice.matrix if space.lower() == "g": return self.lattice.reciprocal_lattice.matrix raise ValueError("Wrong value for space: %s " % space) @property def density(self): """ Returns the density in units of g/cc """ constant = AMU_TO_KG * 1000 / 1e-24 return self.composition.weight / self.volume * constant def __eq__(self, other): if other is None: return False if len(self) != len(other): return False if self._lattice != other._lattice: return False for site in self: if site not in other: return False return True def __ne__(self, other): return not self.__eq__(other) def __hash__(self): # For now, just use the composition hash code. return self.composition.__hash__() @property def frac_coords(self): """ Returns the fractional coordinates. """ return [site.frac_coords for site in self._sites] @property def volume(self): """ Returns the volume of the structure. """ return self._lattice.volume def get_distance(self, i, j, jimage=None): """ Get distance between site i and j assuming periodic boundary conditions. If the index jimage of two sites atom j is not specified it selects the jimage 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. Args: i: Index of first site j: Index of second site jimage: Number of lattice translations in each lattice direction. Default is None for nearest image. Returns: distance """ return self[i].distance(self[j], jimage) def get_sites_in_sphere(self, pt, r, include_index=False): """ Find all sites within a sphere from the point. This includes sites in other periodic images. Algorithm: 1. place sphere of radius r in crystal and determine minimum supercell (parallelpiped) which would contain a sphere of radius r. for this we need the projection of a_1 on a unit vector perpendicular to a_2 & a_3 (i.e. the unit vector in the direction b_1) to determine how many a_1"s it will take to contain the sphere. Nxmax = r * length_of_b_1 / (2 Pi) 2. keep points falling within r. Args: pt: cartesian coordinates of center of sphere. r: radius of sphere. include_index: boolean that determines whether the non-supercell site index is included in the returned data Returns: [(site, dist) ...] since most of the time, subsequent processing requires the distance. """ site_fcoords = np.mod(self.frac_coords, 1) neighbors = [] for fcoord, dist, i in get_points_in_sphere_pbc(self._lattice, site_fcoords, pt, r): nnsite = PeriodicSite(self[i].species_and_occu, fcoord, self._lattice, properties=self[i].properties) neighbors.append((nnsite, dist) if not include_index else (nnsite, dist, i)) return neighbors def get_neighbors(self, site, r, include_index=False): """ Get all neighbors to a site within a sphere of radius r. Excludes the site itself. Args: site: site, which is the center of the sphere. r: radius of sphere. include_index: boolean that determines whether the non-supercell site index is included in the returned data Returns: [(site, dist) ...] since most of the time, subsequent processing requires the distance. """ nn = self.get_sites_in_sphere(site.coords, r, include_index=include_index) return [d for d in nn if site != d[0]] def get_all_neighbors(self, r, include_index=False): """ Get neighbors for each atom in the unit cell, out to a distance r Returns a list of list of neighbors for each site in structure. Use this method if you are planning on looping over all sites in the crystal. If you only want neighbors for a particular site, use the method get_neighbors as it may not have to build such a large supercell However if you are looping over all sites in the crystal, this method is more efficient since it only performs one pass over a large enough supercell to contain all possible atoms out to a distance r. The return type is a [(site, dist) ...] since most of the time, subsequent processing requires the distance. Args: r: radius of sphere. include_index: boolean that determines whether the non-supercell site index is included in the returned data Returns: A list of a list of nearest neighbors for each site, i.e., [[(site, dist, index) ...], ..] Index only supplied if include_index = True. The index is the index of the site in the original (non-supercell) structure. This is needed for ewaldmatrix by keeping track of which sites contribute to the ewald sum. """ # Use same algorithm as get_sites_in_sphere to determine supercell but # loop over all atoms in crystal recp_len = self.lattice.reciprocal_lattice.abc sr = r + 0.15 nmax = [sr * l / (2 * math.pi) for l in recp_len] site_nminmax = [] floor = math.floor for site in self: pcoords = site.frac_coords inmax = [int(floor(pcoords[i] + nmax[i])) for i in xrange(3)] inmin = [int(floor(pcoords[i] - nmax[i])) for i in xrange(3)] site_nminmax.append(zip(inmin, inmax)) nmin = [min([i[j][0] for i in site_nminmax]) for j in xrange(3)] nmax = [max([i[j][1] for i in site_nminmax]) for j in xrange(3)] all_ranges = [range(nmin[i], nmax[i] + 1) for i in xrange(3)] neighbors = [list() for i in xrange(len(self._sites))] all_fcoords = np.mod(self.frac_coords, 1) site_coords = np.array(self.cart_coords) latt = self._lattice frac_2_cart = latt.get_cartesian_coords n = len(self) indices = np.array(range(n)) for image in itertools.product(*all_ranges): for (j, fcoord) in enumerate(all_fcoords): fcoords = fcoord + image coords = frac_2_cart(fcoords) submat = np.tile(coords, (n, 1)) dists = (site_coords - submat) ** 2 dists = np.sqrt(dists.sum(axis=1)) withindists = (dists <= r) * (dists > 1e-8) sp = self[j].species_and_occu props = self[j].properties for i in indices[withindists]: nnsite = PeriodicSite(sp, fcoords, latt, properties=props) item = (nnsite, dists[i], j) if include_index else ( nnsite, dists[i]) neighbors[i].append(item) return neighbors def get_neighbors_in_shell(self, origin, r, dr): """ Returns all sites in a shell centered on origin (coords) between radii r-dr and r+dr. Args: origin: cartesian coordinates of center of sphere. r: inner radius of shell. dr: width of shell. Returns: [(site, dist) ...] since most of the time, subsequent processing requires the distance. """ outer = self.get_sites_in_sphere(origin, r + dr) inner = r - dr return [(site, dist) for (site, dist) in outer if dist > inner] def get_sorted_structure(self): """ Get a sorted copy of the structure. Sites are sorted by the electronegativity of the species. """ sites = sorted(self) return Structure.from_sites(sites) def get_reduced_structure(self, reduction_algo="niggli"): """ Get a reduced structure. Args: reduction_algo: The lattice reduction algorithm to use. Currently supported options are "niggli" or "LLL". """ if reduction_algo == "niggli": reduced_latt = self._lattice.get_niggli_reduced_lattice() elif reduction_algo == "LLL": reduced_latt = self._lattice.get_lll_reduced_lattice() else: raise ValueError("Invalid reduction algo : {}" .format(reduction_algo)) return Structure(reduced_latt, self.species_and_occu, self.cart_coords, coords_are_cartesian=True, to_unit_cell=True) def copy(self, site_properties=None, sanitize=False): """ Convenience method to get a copy of the structure, with options to add site properties. Args: site_properties: Properties to add or override. The properties are specified in the same way as the constructor, i.e., as a dict of the form {property: [values]}. The properties should be in the order of the *original* structure if you are performing sanitization. sanitize: If True, this method will return a sanitized structure. Sanitization performs a few things: (i) The sites are sorted by electronegativity, (ii) a LLL lattice reduction is carried out to obtain a relatively orthogonalized cell, (iii) all fractional coords for sites are mapped into the unit cell. Returns: A copy of the Structure, with optionally new site_properties and optionally sanitized. """ props = self.site_properties if site_properties: props.update(site_properties) if not sanitize: return Structure(self._lattice, [site.species_and_occu for site in self], [site.frac_coords for site in self], site_properties=props) else: reduced_latt = self._lattice.get_lll_reduced_lattice() new_sites = [] for i, site in enumerate(self): frac_coords = reduced_latt.get_fractional_coords(site.coords) site_props = {} for p in props: site_props[p] = props[p][i] new_sites.append(PeriodicSite(site.species_and_occu, frac_coords, reduced_latt, to_unit_cell=True, properties=site_props)) new_sites = sorted(new_sites) return Structure.from_sites(new_sites) def interpolate(self, end_structure, nimages=10): """ Interpolate between this structure and end_structure. Useful for construction of NEB inputs. Args: end_structure: structure to interpolate between this structure and end. nimages: number of interpolation images. Defaults to 10 images. Returns: List of interpolated structures. The starting and ending structures included as the first and last structures respectively. A total of (nimages + 1) structures are returned. """ #Check length of structures if len(self) != len(end_structure): raise ValueError("Structures have different lengths!") #Check that both structures have the same lattice if not np.allclose(self.lattice.matrix, end_structure.lattice.matrix): raise ValueError("Structures with different lattices!") #Check that both structures have the same species for i in range(0, len(self)): if self[i].species_and_occu != end_structure[i].species_and_occu: raise ValueError("Different species!\nStructure 1:\n" + str(self) + "\nStructure 2\n" + str(end_structure)) start_coords = np.array(self.frac_coords) end_coords = np.array(end_structure.frac_coords) vec = end_coords - start_coords structs = [Structure(self.lattice, [site.species_and_occu for site in self._sites], start_coords + float(x) / float(nimages) * vec, site_properties=self.site_properties) for x in range(0, nimages + 1)] return structs 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 def __repr__(self): outs = ["Structure Summary", repr(self.lattice)] for s in self: outs.append(repr(s)) return "\n".join(outs) def __str__(self): outs = ["Structure Summary ({s})".format(s=str(self.composition)), "Reduced Formula: {}" .format(self.composition.reduced_formula)] to_s = lambda x: "%0.6f" % x outs.append("abc : " + " ".join([to_s(i).rjust(10) for i in self.lattice.abc])) outs.append("angles: " + " ".join([to_s(i).rjust(10) for i in self.lattice.angles])) outs.append("Sites ({i})".format(i=len(self))) for i, site in enumerate(self): outs.append(" ".join([str(i + 1), site.species_string, " ".join([to_s(j).rjust(12) for j in site.frac_coords])])) return "\n".join(outs) @property def to_dict(self): """ Json-serializable dict representation of Structure """ d = {"@module": self.__class__.__module__, "@class": self.__class__.__name__, "lattice": self._lattice.to_dict, "sites": []} for site in self: site_dict = site.to_dict del site_dict["lattice"] del site_dict["@module"] del site_dict["@class"] d["sites"].append(site_dict) return d @staticmethod def from_dict(d): """ Reconstitute a Structure object from a dict representation of Structure created using to_dict. Args: d: dict representation of structure. Returns: Structure object """ lattice = Lattice.from_dict(d["lattice"]) sites = [PeriodicSite.from_dict(sd, lattice) for sd in d["sites"]] return Structure.from_sites(sites)
def __init__( self, reciprocal_lattice: Lattice, original_points: np.ndarray, original_dim: np.ndarray, extra_points: np.ndarray, ir_to_full_idx: Optional[np.ndarray] = None, extra_ir_points_idx: Optional[np.ndarray] = None, nworkers: int = pdefaults["nworkers"], ): """ Add a warning about only using the symmetry options if you are sure your extra k-points have been symmetrized Args: original_points: nworkers: """ self._nworkers = nworkers if nworkers != -1 else cpu_count() self._final_points = np.concatenate([original_points, extra_points]) self._reciprocal_lattice = reciprocal_lattice if ir_to_full_idx is None: ir_to_full_idx = np.arange( len(original_points) + len(extra_points)) if extra_ir_points_idx is None: extra_ir_points_idx = np.arange(len(extra_points)) logger.debug("Initializing periodic Voronoi calculator") all_points = np.concatenate((original_points, extra_points)) logger.debug(" ├── getting supercell k-points") supercell_points = get_supercell_points(all_points) supercell_idxs = np.arange(supercell_points.shape[0]) # filter points far from the zone boundary, this will lead to errors for # very small meshes < 5x5x5 but we are not interested in those mask = ((supercell_points > -0.75) & (supercell_points < 0.75)).all(axis=1) supercell_points = supercell_points[mask] supercell_idxs = supercell_idxs[mask] # want points in cartesian space so we can define a regular spherical # cutoff even if reciprocal lattice is not cubic. If we used a # fractional cutoff, the cutoff regions would not be spherical logger.debug(" ├── getting cartesian points") cart_points = reciprocal_lattice.get_cartesian_coords(supercell_points) cart_extra_points = reciprocal_lattice.get_cartesian_coords( extra_points[extra_ir_points_idx]) # small cutoff is slightly larger than the max regular grid spacing # means at least 1 neighbour point will always be included in each # direction, need to find cartesian length which covers the longest direction # of the mesh spacing = 1 / original_dim body_diagonal = reciprocal_lattice.get_cartesian_coords(spacing) xy = reciprocal_lattice.get_cartesian_coords( [spacing[0], spacing[1], 0]) xz = reciprocal_lattice.get_cartesian_coords( [spacing[0], 0, spacing[2]]) yz = reciprocal_lattice.get_cartesian_coords( [0, spacing[1], spacing[2]]) len_diagonal = np.linalg.norm(body_diagonal) len_xy = np.linalg.norm(xy) len_xz = np.linalg.norm(xz) len_yz = np.linalg.norm(yz) small_cutoff = (np.max([len_diagonal, len_xy, len_xz, len_yz]) * 1.6) big_cutoff = (small_cutoff * 1.77) logger.debug(" ├── initializing ball tree") # use BallTree for quickly evaluating which points are within cutoffs tree = BallTree(cart_points) n_supercell_points = len(supercell_points) # big points are those which surround the extra points within the big cutoff # (including the extra points themselves) logger.debug(" ├── calculating points in big radius") big_points_idx = _query_radius_iteratively(tree, n_supercell_points, cart_extra_points, big_cutoff) # Voronoi points are those we actually include in the Voronoi diagram self._voronoi_points = cart_points[big_points_idx] # small points are the points in all_points (i.e., original + extra points) for # which we want to calculate the Voronoi volumes. Outside the small cutoff, the # weights will just be the regular grid weight. logger.debug(" └── calculating points in small radius") small_points_idx = _query_radius_iteratively(tree, n_supercell_points, cart_extra_points, small_cutoff) # get the irreducible small points small_points_in_all_points = supercell_idxs[small_points_idx] % len( all_points) mapping = ir_to_full_idx[small_points_in_all_points] unique_mappings, ir_idx = np.unique(mapping, return_index=True) small_points_idx = small_points_idx[ir_idx] # get a mapping to go from the ir small points to the full BZ. groups = groupby(np.arange(len(all_points)), ir_to_full_idx) grouped_ir = groups[unique_mappings] counts = [len(g) for g in grouped_ir] self._expand_ir = np.repeat(np.arange(len(ir_idx)), counts) # get the indices of the expanded ir_small_points in all_points self._volume_in_final_idx = np.concatenate(grouped_ir) # get the indices of ir_small_points_idx (i.e., the points for which we will # calculate the volume) in voronoi_points self._volume_points_idx = _get_loc(big_points_idx, small_points_idx) # Prepopulate the final volumes array. By default, each point has the # volume of the original mesh. Note: at this point, the extra points # will have zero volume. This will array will be updated by # compute_volumes self._volume = reciprocal_lattice.volume self._final_volumes = np.full(len(all_points), 1 / len(original_points)) self._final_volumes[len(original_points):] = 0 self._final_volumes[self._volume_in_final_idx] = 0
def make_supercell(structure, distance, method='bec', wrap=True, standardize=True, do_niggli_first=True, diagonal=False, implementation='fort', verbosity=1): """ Creates from a given structure a supercell based on the required minimal dimension :param structure: The pymatgen structure to create the supercell for :param float distance: The minimum image distance as a float, The cell created will not have any periodic image below this distance :param str method: The method to get the optimal supercell. For now, the only implemented option is *best enclosing cell* :param bool wrap: Wrap the atoms into the created cell after replication :param bool standardize: Standardize the created cell. This is done based on the rules in Hinuma etal, http://arxiv.org/abs/1506.01455 However, only rules for the triclinic case are applied, so further standardization using spglib is recommended, if a truly standardized cell is required. :param bool do_niggli_first: Start with a niggli reduction of the cell, to enable a faster search. Disable if there are problems with the reduction of if the cell is already Niggli or LLL reduced. :param bool diagonal: Whether to return the diagonal solution, instead of the optimal cell. :param str implementation: Either fortran ('fort') or python-implementation ('pyth'), defaults to 'fort' :param int verbosity: Sets the verbosity level. :returns: A new pymatgen core structure instance and the used scaling matrix :returns: The scaling matrix used. """ if not isinstance(structure, Structure): raise TypeError("Structure passed has to be a pymatgen structure") try: distance = float(distance) assert distance > 1e-12, "Non-positive number" except Exception as e: print("You have to pass positive float or integer as distance") raise e if not isinstance(wrap, bool): raise TypeError("wrap has to be a boolean") if not isinstance(standardize, bool): raise TypeError("standardize has to be a boolean") # I'm getting the niggli reduced structure as first: if verbosity > 1: print("given cell:\n", structure._lattice) if do_niggli_first: starting_structure = structure.get_reduced_structure( reduction_algo=u'niggli') else: starting_structure = structure if verbosity > 1: print("starting cell:\n", starting_structure._lattice) for i, v in enumerate(starting_structure._lattice.matrix): print(i, np.linalg.norm(v)) # the lattice of the niggle reduced structure: lattice_cellvecs = np.array(starting_structure._lattice.matrix, dtype=np.float64) # trial_vecs are all possible vectors sorted by the norm if method == 'bec': if diagonal: lattice_cellvecs = np.array(lattice_cellvecs) # I get the diagonal solutions scale_matrix, supercell_cellvecs = get_diagonal_solution_bec( lattice_cellvecs, distance) else: # I get all possible midpoint vectors, based on the distance, # which for BEC method is the diameter of the sphere (norms_of_sorted_Gr_r2, sorted_Gc_r2, sorted_Gr_r2, r_outer, v_diag) = get_possible_solutions(lattice_cellvecs, distance, verbosity=verbosity) if verbosity: print("I received {} possible solutions".format( len(norms_of_sorted_Gr_r2))) # I pass these trial vectors into the function to find the minimum volume: if implementation == 'pyth': scale_matrix, supercell_cellvecs = get_optimal_solution_bec( norms_of_sorted_Gr_r2, sorted_Gc_r2, sorted_Gr_r2, r_outer, v_diag, r_inner=distance, verbosity=verbosity) elif implementation == 'fort': scale_matrix, supercell_cellvecs = fort_optimal_supercell_bec( norms_of_sorted_Gr_r2, sorted_Gc_r2, sorted_Gr_r2, r_outer, v_diag, distance, verbosity, len(norms_of_sorted_Gr_r2)) else: raise RuntimeError("Implementation {}".formt(implementation)) elif method == 'hnf': if diagonal: lattice_cellvecs = np.array(lattice_cellvecs) scale_matrix, supercell_cellvecs = get_diagonal_solution_hnf( lattice_cellvecs, distance) else: if implementation == 'pyth': scale_matrix, supercell_cellvecs = get_optimal_solution_hnf( lattice_cellvecs, distance, verbosity) elif implementation == 'fort': scale_matrix, supercell_cellvecs = fort_optimal_supercell_hnf( lattice_cellvecs, distance, verbosity) else: raise RuntimeError("Implementation {}".formt(implementation)) #raise NotImplementedError("HNF has not been fully implemented") else: raise ValueError("Unknown method {}".format(method)) # Constructing the new lattice: new_lattice = Lattice(supercell_cellvecs) # I create f_lat, which are the fractional lattice points of the niggle_reduced: f_lat = lattice_points_in_supercell(scale_matrix) # and transforrm to cartesian coords here: c_lat = new_lattice.get_cartesian_coords(f_lat) #~ cellT = supercell_cellvecs.T if verbosity > 1: print("Given Scaling:\n") print(scale_matrix) print("Given lattice:\n") print(new_lattice) for i, v in enumerate(new_lattice.matrix): print(i, np.linalg.norm(v)) new_sites = [] if verbosity: print("Done, constructing structure") for site in starting_structure: for v in c_lat: new_sites.append( PeriodicSite(site.species_and_occu, site.coords + v, new_lattice, properties=site.properties, coords_are_cartesian=True, to_unit_cell=wrap)) supercell = Structure.from_sites(new_sites) if standardize: supercell = standardize_cell(supercell, wrap) if verbosity > 1: print("Cell after standardization:\n", new_lattice) for i, v in enumerate(new_lattice.matrix): print(i, np.linalg.norm(v)) return supercell, scale_matrix
def __init__(self, reciprocal_lattice: Lattice, original_points: np.ndarray, original_dim: np.ndarray, extra_points: np.ndarray, nworkers: int = pdefaults["nworkers"]): """ Args: original_points: nworkers: """ self._nworkers = nworkers if nworkers != -1 else cpu_count() supercell_points = get_supercell_points([2, 2, 2], original_points) # want points in cartesian space so we can define a regular spherical # cutoff even if reciprocal lattice is not cubic. If we used a # fractional cutoff, the cutoff regions would not be spherical cart_points = reciprocal_lattice.get_cartesian_coords(supercell_points) cart_extra_points = reciprocal_lattice.get_cartesian_coords( extra_points) # small cutoff is slighly larger than the max regular grid spacing # means at least 1 neighbour point will always be included in each # direction dim_lengths = np.dot(1 / original_dim, reciprocal_lattice.matrix) small_cutoff = np.max(dim_lengths) * 1.01 big_cutoff = small_cutoff * 2 # use BallTree for quickly evaluating which points are within cutoffs tree = BallTree(cart_points) # big cutoff points are those which surround the extra points within # the big cutoff (it does not include the extra points themselves) big_cutoff_points_idx = np.concatenate(tree.query_radius( cart_extra_points, big_cutoff), axis=0) # Voronoi points are those we actually calculate in the Voronoi diagram # e.g. the big points + extra points voronoi_points = supercell_points[big_cutoff_points_idx] self._voronoi_points = np.concatenate((voronoi_points, extra_points)) # small points are the points in original_points for which we want to # calculate the Voronoi volumes. Note this does not include the # indices of the extra points. Outside the small cutoff, the weights # will just be the regular grid weight. small_cutoff_points_idx = np.concatenate(tree.query_radius( cart_extra_points, small_cutoff), axis=0) # get the indices of small_cutoff_points in voronoi_points small_in_voronoi_idx = _get_loc(big_cutoff_points_idx, small_cutoff_points_idx) # get the indices of the small cutoff points + extra points # in voronoi points that we want the volumes for. The extra points # were just added at the end of big_cutoff_points, so getting their # indices is simple self._volume_points_idx = np.concatenate( (small_in_voronoi_idx, np.arange(len(extra_points)) + len(big_cutoff_points_idx))) # get the indices of the small_cutoff_points (not including the extra # points) in the original mesh. this works because the supercell # points are in the same order as the original mesh, just repeated for # each cell in the supercell small_in_original_idx = (small_cutoff_points_idx % len(original_points)) # get the indices of the small cutoff points + extra points in the # final volume array. Note that the final volume array has the same # order as original_mesh + extra_points self._volume_in_final_idx = np.concatenate( (small_in_original_idx, np.arange(len(extra_points)) + len(original_points))) # prepopulate the final volumes array. By default, each point has the # volume of the original mesh. Note: at this point, the extra points # will have zero volume. This will array will be updated by # compute_volumes self._final_volumes = np.full( len(original_points) + len(extra_points), 1 / len(original_points)) self._final_volumes[len(original_points):] = 0