def test_lattice_points_in_supercell(self): supercell = np.array([[1, 3, 5], [-3, 2, 3], [-5, 3, 1]]) points = coord.lattice_points_in_supercell(supercell) self.assertAlmostEqual(len(points), abs(np.linalg.det(supercell))) self.assertGreaterEqual(np.min(points), -1e-10) self.assertLessEqual(np.max(points), 1 - 1e-10) supercell = np.array([[-5, -5, -3], [0, -4, -2], [0, -5, -2]]) points = coord.lattice_points_in_supercell(supercell) self.assertAlmostEqual(len(points), abs(np.linalg.det(supercell))) self.assertGreaterEqual(np.min(points), -1e-10) self.assertLessEqual(np.max(points), 1 - 1e-10)
def _generate_mappings(self): """ Find all the supercell indices associated with each cluster """ ts = lattice_points_in_supercell(self.supercell_matrix) self.cluster_indices = [] self.clusters_by_sites = defaultdict(list) for sc in self.cluster_expansion.symmetrized_clusters: prim_fcoords = np.array([c.sites for c in sc.equivalent_clusters]) fcoords = np.dot(prim_fcoords, self.prim_to_supercell) #tcoords contains all the coordinates of the symmetrically equivalent clusters #the indices are: [equivalent cluster (primitive cell), translational image, index of site in cluster, coordinate index] tcoords = fcoords[:, None, :, :] + ts[None, :, None, :] tcs = tcoords.shape inds = coord_list_mapping_pbc(tcoords.reshape((-1, 3)), self.fcoords, atol=SITE_TOL).reshape((tcs[0] * tcs[1], tcs[2])) self.cluster_indices.append((sc, inds)) #symmetrized cluster, 2d array of index groups that correspond to the cluster #the 2d array may have some duplicates. This is due to symetrically equivalent #groups being matched to the same sites (eg in simply cubic all 6 nn interactions #will all be [0, 0] indices. This multiplicity disappears as supercell size #increases, so I haven't implemented a more efficient method # now we store the symmetrized clusters grouped by site index in the supercell, # to be used by delta_corr. We also store a reduced index array, where only the # rows with the site index are stored. The ratio is needed because the correlations # are averages over the full inds array. for site_index in np.unique(inds): in_inds = np.any(inds == site_index, axis=-1) ratio = len(inds) / np.sum(in_inds) self.clusters_by_sites[site_index].append((sc.bit_combos, sc.sc_b_id, inds[in_inds], ratio))
def sc_generator(s1, s2): s2_fc = np.array(s2.frac_coords) if fu == 1: cc = np.array(s1.cart_coords) for l, sc_m in self._get_lattices(s2.lattice, s1, fu): fc = l.get_fractional_coords(cc) fc -= np.floor(fc) yield fc, s2_fc, av_lat(l, s2.lattice), sc_m else: fc_init = np.array(s1.frac_coords) for l, sc_m in self._get_lattices(s2.lattice, s1, fu): fc = np.dot(fc_init, np.linalg.inv(sc_m)) lp = lattice_points_in_supercell(sc_m) fc = (fc[:, None, :] + lp[None, :, :]).reshape((-1, 3)) fc -= np.floor(fc) yield fc, s2_fc, av_lat(l, s2.lattice), sc_m
def sc_generator(s1, s2): s2_fc = np.array(s2.frac_coords) if fu == 1: cc = np.array(s1.cart_coords) for l, sc_m in self._get_lattices(s2.lattice, s1, fu): fc = l.get_fractional_coords(cc) fc -= np.floor(fc) yield fc, s2_fc, av_lat(l, s2.lattice), sc_m else: fc_init = np.array(s1.frac_coords) for l, sc_m in self._get_lattices(s2.lattice, s1, fu): fc = np.dot(fc_init, np.linalg.inv(sc_m)) lp = lattice_points_in_supercell(sc_m) fc = (fc[:, None, :] + lp[None, :, :]).reshape((-1, 3)) fc -= np.floor(fc) yield fc, s2_fc, av_lat(l, s2.lattice), sc_m
def get_supercell_points(supercell_dim, points, replicate_backwards=True): scale_matrix = np.array(supercell_dim, np.int16) if scale_matrix.shape != (3, 3): scale_matrix = np.array(scale_matrix * np.eye(3), np.int16) f_lat = lattice_points_in_supercell(scale_matrix) # get a list of supercell images, e.g. [[1, 0, 0], [2, 0, 0]] images = np.dot(f_lat, scale_matrix) if replicate_backwards: images = np.unique(np.concatenate((images, -images)), axis=0) repeated_points = np.tile(points, (len(images), 1)) repeated_images = np.repeat(images, len(points), axis=0) return repeated_images + repeated_points
def __mul__(self, scaling_matrix): """ Replicates the graph, creating a supercell, intelligently joining together edges that lie on periodic boundaries. In principle, any operations on the expanded graph could also be done on the original graph, but a larger graph can be easier to visualize and reason about. :param scaling_matrix: same as Structure.__mul__ :return: """ # Developer note: a different approach was also trialed, using # a simple Graph (instead of MultiDiGraph), with node indices # representing both site index and periodic image. Here, the # number of nodes != number of sites in the Structure. This # approach has many benefits, but made it more difficult to # keep the graph in sync with its corresponding Structure. # Broadly, it would be easier to multiply the Structure # *before* generating the StructureGraph, but this isn't # possible when generating the graph using critic2 from # charge density. # Multiplication works by looking for the expected position # of an image node, and seeing if that node exists in the # supercell. If it does, the edge is updated. This is more # computationally expensive than just keeping track of the # which new lattice images present, but should hopefully be # easier to extend to a general 3x3 scaling matrix. # code adapted from Structure.__mul__ scale_matrix = np.array(scaling_matrix, np.int16) if scale_matrix.shape != (3, 3): scale_matrix = np.array(scale_matrix * np.eye(3), np.int16) else: # TODO: test __mul__ with full 3x3 scaling matrices raise NotImplementedError('Not tested with 3x3 scaling matrices yet.') new_lattice = Lattice(np.dot(scale_matrix, self.structure.lattice.matrix)) f_lat = lattice_points_in_supercell(scale_matrix) c_lat = new_lattice.get_cartesian_coords(f_lat) new_sites = [] new_graphs = [] for v in c_lat: # create a map of nodes from original graph to its image mapping = {n: n + len(new_sites) for n in range(len(self.structure))} for idx, site in enumerate(self.structure): s = PeriodicSite(site.species_and_occu, site.coords + v, new_lattice, properties=site.properties, coords_are_cartesian=True, to_unit_cell=False) new_sites.append(s) new_graphs.append(nx.relabel_nodes(self.graph, mapping, copy=True)) new_structure = Structure.from_sites(new_sites) # merge all graphs into one big graph new_g = nx.MultiDiGraph() for new_graph in new_graphs: new_g = nx.union(new_g, new_graph) edges_to_remove = [] # tuple of (u, v, k) edges_to_add = [] # tuple of (u, v, attr_dict) # list of new edges inside supercell # for duplicate checking edges_inside_supercell = [{u, v} for u, v, d in new_g.edges(data=True) if d['to_jimage'] == (0, 0, 0)] new_periodic_images = [] orig_lattice = self.structure.lattice # use k-d tree to match given position to an # existing Site in Structure kd_tree = KDTree(new_structure.cart_coords) # tolerance in Å for sites to be considered equal # this could probably be a lot smaller tol = 0.05 for u, v, k, d in new_g.edges(keys=True, data=True): to_jimage = d['to_jimage'] # for node v # reduce unnecessary checking if to_jimage != (0, 0, 0): # get index in original site n_u = u % len(self.structure) n_v = v % len(self.structure) # get fractional co-ordinates of where atoms defined # by edge are expected to be, relative to original # lattice (keeping original lattice has # significant benefits) v_image_frac = np.add(self.structure[n_v].frac_coords, to_jimage) u_frac = self.structure[n_u].frac_coords # using the position of node u as a reference, # get relative Cartesian co-ordinates of where # atoms defined by edge are expected to be v_image_cart = orig_lattice.get_cartesian_coords(v_image_frac) u_cart = orig_lattice.get_cartesian_coords(u_frac) v_rel = np.subtract(v_image_cart, u_cart) # now retrieve position of node v in # new supercell, and get absolute Cartesian # co-ordinates of where atoms defined by edge # are expected to be v_expec = new_structure[u].coords + v_rel # now search in new structure for these atoms # query returns (distance, index) v_present = kd_tree.query(v_expec) v_present = v_present[1] if v_present[0] <= tol else None # check if image sites now present in supercell # and if so, delete old edge that went through # periodic boundary if v_present is not None: new_u = u new_v = v_present new_d = d.copy() # node now inside supercell new_d['to_jimage'] = (0, 0, 0) edges_to_remove.append((u, v, k)) # make sure we don't try to add duplicate edges # will remove two edges for everyone one we add if {new_u, new_v} not in edges_inside_supercell: # normalize direction if new_v < new_u: new_u, new_v = new_v, new_u edges_inside_supercell.append({new_u, new_v}) edges_to_add.append((new_u, new_v, new_d)) else: # want to find new_v such that we have # full periodic boundary conditions # so that nodes on one side of supercell # are connected to nodes on opposite side v_expec_frac = new_structure.lattice.get_fractional_coords(v_expec) # find new to_jimage # use np.around to fix issues with finite precision leading to incorrect image v_expec_image = np.around(v_expec_frac, decimals=3) v_expec_image = v_expec_image - v_expec_image%1 v_expec_frac = np.subtract(v_expec_frac, v_expec_image) v_expec = new_structure.lattice.get_cartesian_coords(v_expec_frac) v_present = kd_tree.query(v_expec) v_present = v_present[1] if v_present[0] <= tol else None if v_present is not None: new_u = u new_v = v_present new_d = d.copy() new_to_jimage = tuple(map(int, v_expec_image)) # normalize direction if new_v < new_u: new_u, new_v = new_v, new_u new_to_jimage = tuple(np.multiply(-1, d['to_jimage']).astype(int)) new_d['to_jimage'] = new_to_jimage edges_to_remove.append((u, v, k)) if (new_u, new_v, new_to_jimage) not in new_periodic_images: edges_to_add.append((new_u, new_v, new_d)) new_periodic_images.append((new_u, new_v, new_to_jimage)) logger.debug("Removing {} edges, adding {} new edges.".format(len(edges_to_remove), len(edges_to_add))) # add/delete marked edges for edges_to_remove in edges_to_remove: new_g.remove_edge(*edges_to_remove) for (u, v, d) in edges_to_add: new_g.add_edge(u, v, **d) # return new instance of StructureGraph with supercell d = {"@module": self.__class__.__module__, "@class": self.__class__.__name__, "structure": new_structure.as_dict(), "graphs": json_graph.adjacency_data(new_g)} sg = StructureGraph.from_dict(d) return sg
def __mul__(self, scaling_matrix): """ Replicates the graph, creating a supercell, intelligently joining together edges that lie on periodic boundaries. In principle, any operations on the expanded graph could also be done on the original graph, but a larger graph can be easier to visualize and reason about. :param scaling_matrix: same as Structure.__mul__ :return: """ # Developer note: a different approach was also trialed, using # a simple Graph (instead of MultiDiGraph), with node indices # representing both site index and periodic image. Here, the # number of nodes != number of sites in the Structure. This # approach has many benefits, but made it more difficult to # keep the graph in sync with its corresponding Structure. # Broadly, it would be easier to multiply the Structure # *before* generating the StructureGraph, but this isn't # possible when generating the graph using critic2 from # charge density. # Multiplication works by looking for the expected position # of an image node, and seeing if that node exists in the # supercell. If it does, the edge is updated. This is more # computationally expensive than just keeping track of the # which new lattice images present, but should hopefully be # easier to extend to a general 3x3 scaling matrix. # code adapted from Structure.__mul__ scale_matrix = np.array(scaling_matrix, np.int16) if scale_matrix.shape != (3, 3): scale_matrix = np.array(scale_matrix * np.eye(3), np.int16) else: # TODO: test __mul__ with full 3x3 scaling matrices raise NotImplementedError('Not tested with 3x3 scaling matrices yet.') new_lattice = Lattice(np.dot(scale_matrix, self.structure.lattice.matrix)) f_lat = lattice_points_in_supercell(scale_matrix) c_lat = new_lattice.get_cartesian_coords(f_lat) new_sites = [] new_graphs = [] for v in c_lat: # create a map of nodes from original graph to its image mapping = {n: n + len(new_sites) for n in range(len(self.structure))} for idx, site in enumerate(self.structure): s = PeriodicSite(site.species_and_occu, site.coords + v, new_lattice, properties=site.properties, coords_are_cartesian=True, to_unit_cell=False) new_sites.append(s) new_graphs.append(nx.relabel_nodes(self.graph, mapping, copy=True)) new_structure = Structure.from_sites(new_sites) # merge all graphs into one big graph new_g = nx.MultiDiGraph() for new_graph in new_graphs: new_g = nx.union(new_g, new_graph) edges_to_remove = [] # tuple of (u, v, k) edges_to_add = [] # tuple of (u, v, attr_dict) # list of new edges inside supercell # for duplicate checking edges_inside_supercell = [{u, v} for u, v, d in new_g.edges(data=True) if d['to_jimage'] == (0, 0, 0)] new_periodic_images = [] orig_lattice = self.structure.lattice # use k-d tree to match given position to an # existing Site in Structure kd_tree = KDTree(new_structure.cart_coords) # tolerance in Å for sites to be considered equal # this could probably be a lot smaller tol = 0.05 for u, v, k, d in new_g.edges(keys=True, data=True): to_jimage = d['to_jimage'] # for node v # reduce unnecessary checking if to_jimage != (0, 0, 0): # get index in original site n_u = u % len(self.structure) n_v = v % len(self.structure) # get fractional co-ordinates of where atoms defined # by edge are expected to be, relative to original # lattice (keeping original lattice has # significant benefits) v_image_frac = np.add(self.structure[n_v].frac_coords, to_jimage) u_frac = self.structure[n_u].frac_coords # using the position of node u as a reference, # get relative Cartesian co-ordinates of where # atoms defined by edge are expected to be v_image_cart = orig_lattice.get_cartesian_coords(v_image_frac) u_cart = orig_lattice.get_cartesian_coords(u_frac) v_rel = np.subtract(v_image_cart, u_cart) # now retrieve position of node v in # new supercell, and get absolute Cartesian # co-ordinates of where atoms defined by edge # are expected to be v_expec = new_structure[u].coords + v_rel # now search in new structure for these atoms # query returns (distance, index) v_present = kd_tree.query(v_expec) v_present = v_present[1] if v_present[0] <= tol else None # check if image sites now present in supercell # and if so, delete old edge that went through # periodic boundary if v_present is not None: new_u = u new_v = v_present new_d = d.copy() # node now inside supercell new_d['to_jimage'] = (0, 0, 0) edges_to_remove.append((u, v, k)) # make sure we don't try to add duplicate edges # will remove two edges for everyone one we add if {new_u, new_v} not in edges_inside_supercell: # normalize direction if new_v < new_u: new_u, new_v = new_v, new_u edges_inside_supercell.append({new_u, new_v}) edges_to_add.append((new_u, new_v, new_d)) else: # want to find new_v such that we have # full periodic boundary conditions # so that nodes on one side of supercell # are connected to nodes on opposite side v_expec_frac = new_structure.lattice.get_fractional_coords(v_expec) # find new to_jimage # use np.around to fix issues with finite precision leading to incorrect image v_expec_image = np.around(v_expec_frac, decimals=3) v_expec_image = v_expec_image - v_expec_image%1 v_expec_frac = np.subtract(v_expec_frac, v_expec_image) v_expec = new_structure.lattice.get_cartesian_coords(v_expec_frac) v_present = kd_tree.query(v_expec) v_present = v_present[1] if v_present[0] <= tol else None if v_present is not None: new_u = u new_v = v_present new_d = d.copy() new_to_jimage = tuple(map(int, v_expec_image)) # normalize direction if new_v < new_u: new_u, new_v = new_v, new_u new_to_jimage = tuple(np.multiply(-1, d['to_jimage']).astype(int)) new_d['to_jimage'] = new_to_jimage edges_to_remove.append((u, v, k)) if (new_u, new_v, new_to_jimage) not in new_periodic_images: edges_to_add.append((new_u, new_v, new_d)) new_periodic_images.append((new_u, new_v, new_to_jimage)) logger.debug("Removing {} edges, adding {} new edges.".format(len(edges_to_remove), len(edges_to_add))) # add/delete marked edges for edges_to_remove in edges_to_remove: new_g.remove_edge(*edges_to_remove) for (u, v, d) in edges_to_add: new_g.add_edge(u, v, **d) # return new instance of StructureGraph with supercell d = {"@module": self.__class__.__module__, "@class": self.__class__.__name__, "structure": new_structure.as_dict(), "graphs": json_graph.adjacency_data(new_g)} sg = StructureGraph.from_dict(d) return sg
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