def test_get_supercell_matrix(self): sm = StructureMatcher(ltol=0.1, stol=0.3, angle_tol=2, primitive_cell=False, scale=True, attempt_supercell=True) l = Lattice.orthorhombic(1, 2, 3) s1 = Structure(l, ['Si', 'Si', 'Ag'], [[0, 0, 0.1], [0, 0, 0.2], [.7, .4, .5]]) s1.make_supercell([2, 1, 1]) s2 = Structure(l, ['Si', 'Si', 'Ag'], [[0, 0.1, 0], [0, 0.1, -0.95], [-.7, .5, .375]]) result = sm.get_supercell_matrix(s1, s2) self.assertTrue((result == [[-2, 0, 0], [0, 1, 0], [0, 0, 1]]).all()) s1 = Structure(l, ['Si', 'Si', 'Ag'], [[0, 0, 0.1], [0, 0, 0.2], [.7, .4, .5]]) s1.make_supercell([[1, -1, 0], [0, 0, -1], [0, 1, 0]]) s2 = Structure(l, ['Si', 'Si', 'Ag'], [[0, 0.1, 0], [0, 0.1, -0.95], [-.7, .5, .375]]) result = sm.get_supercell_matrix(s1, s2) self.assertTrue((result == [[-1, -1, 0], [0, 0, -1], [0, 1, 0]]).all()) # test when the supercell is a subset sm = StructureMatcher(ltol=0.1, stol=0.3, angle_tol=2, primitive_cell=False, scale=True, attempt_supercell=True, allow_subset=True) del s1[0] result = sm.get_supercell_matrix(s1, s2) self.assertTrue((result == [[-1, -1, 0], [0, 0, -1], [0, 1, 0]]).all())
def test_get_supercell_matrix(self): sm = StructureMatcher(ltol=0.1, stol=0.3, angle_tol=2, primitive_cell=False, scale=True, attempt_supercell=True) l = Lattice.orthorhombic(1, 2, 3) s1 = Structure(l, ['Si', 'Si', 'Ag'], [[0,0,0.1],[0,0,0.2],[.7,.4,.5]]) s1.make_supercell([2,1,1]) s2 = Structure(l, ['Si', 'Si', 'Ag'], [[0,0.1,0],[0,0.1,-0.95],[-.7,.5,.375]]) result = sm.get_supercell_matrix(s1, s2) self.assertTrue((result == [[-2,0,0],[0,1,0],[0,0,1]]).all()) s1 = Structure(l, ['Si', 'Si', 'Ag'], [[0,0,0.1],[0,0,0.2],[.7,.4,.5]]) s1.make_supercell([[1, -1, 0],[0, 0, -1],[0, 1, 0]]) s2 = Structure(l, ['Si', 'Si', 'Ag'], [[0,0.1,0],[0,0.1,-0.95],[-.7,.5,.375]]) result = sm.get_supercell_matrix(s1, s2) self.assertTrue((result == [[-1,-1,0],[0,0,-1],[0,1,0]]).all()) #test when the supercell is a subset sm = StructureMatcher(ltol=0.1, stol=0.3, angle_tol=2, primitive_cell=False, scale=True, attempt_supercell=True, allow_subset=True) del s1[0] result = sm.get_supercell_matrix(s1, s2) self.assertTrue((result == [[-1,-1,0],[0,0,-1],[0,1,0]]).all())
class ClusterExpansion(object): """ Holds lists of SymmetrizedClusters and ClusterSupercells. This is probably the class you're looking for and should be instantiating. You probably want to generate from ClusterExpansion.from_radii, which will auto-generate the symmetrized clusters, unless you want more control over them. """ def __init__(self, structure, expansion_structure, symops, clusters, \ sm_type='pmg_sm', ltol=0.2, stol=0.1, angle_tol=5,\ supercell_size='num_sites', use_ewald=False, use_inv_r=False, eta=None, basis = '01'): """ Args: structure: disordered structure to build a cluster expansion for. Typically the primitive cell radii: dict of {cluster_size: max_radius}. Radii should be strictly decreasing. Typically something like {2:5, 3:4} sm_type: The structure matcher type that you wish to use in structure matching. Can choose from pymatgen default (pmg_sm), anion framework (an_frame) ltol, stol, angle_tol, supercell_size: parameters to pass through to the StructureMatcher, when sm_type == 'pmg_sm' or 'an_frame' Structures that don't match to the primitive cell under these tolerances won't be included in the expansion. Easiest option for supercell_size is usually to use a species that has a constant amount per formula unit. use_ewald: whether to calculate the ewald energy of each structure and use it as a feature. Typically a good idea for ionic materials. use_inv_r: experimental feature that allows fitting to arbitrary 1/r interactions between specie-site combinations. eta: parameter to override the EwaldSummation default eta. Usually only necessary if use_inv_r=True basis: Basis to use in cluster expansion. Currently can be 'ortho' or '01', plan to add 'chebyshev'. """ if use_inv_r and eta is None: warn("Be careful, you might need to change eta to get properly " "converged electrostatic energies. This isn't well tested") self.structure = structure self.expansion_structure = expansion_structure self.symops = symops # test that all the found symmetry operations map back to the input structure # otherwise you can get weird subset/superset bugs fc = self.structure.frac_coords for op in self.symops: if not is_coord_subset_pbc(op.operate_multi(fc), fc, SITE_TOL): raise SYMMETRY_ERROR self.supercell_size = supercell_size self.use_ewald = use_ewald self.eta = eta self.use_inv_r = use_inv_r self.sm_type=sm_type self.stol = stol self.ltol = ltol #self.vor_tol = vor_tol self.basis = basis if self.sm_type == 'pmg_sm' or self.sm_type == 'an_frame': self.angle_tol = angle_tol self.sm = StructureMatcher(primitive_cell=False, attempt_supercell=True, allow_subset=True, scale=True, supercell_size=self.supercell_size, comparator=OrderDisorderElementComparator(), stol=self.stol, ltol=self.ltol, angle_tol=self.angle_tol) # elif self.sm_type == 'an_dmap': # print("Warning: Delaunay matcher only applicable for close packed anion framework!") # try: # from delaunay_matcher import DelaunayMatcher # self.sm = DelauneyMatcher() # # At leaset three methods are required in Delauney Matcher: match, mapping and supercell matrix finding. # except: # pass # I abandoned delaunay because it is not stable with respect to distortion. else: raise ValueError('Structure matcher not implemented!') self.clusters = clusters # assign the cluster ids n_clusters = 1 n_bit_orderings = 1 n_sclusters = 1 for k in sorted(self.clusters.keys()): for y in self.clusters[k]: n_sclusters, n_bit_orderings, n_clusters = y.assign_ids(n_sclusters, n_bit_orderings, n_clusters) self.n_sclusters = n_sclusters self.n_clusters = n_clusters self.n_bit_orderings = n_bit_orderings self._supercells = {} @classmethod def from_radii(cls, structure, radii,\ sm_type = 'pmg_sm', ltol=0.2, stol=0.1, angle_tol=5,\ supercell_size='volume',use_ewald=False, use_inv_r=False, eta=None, basis = '01'): """ Args: structure: disordered structure to build a cluster expansion for. Typically the primitive cell radii: dict of {cluster_size: max_radius}. Radii should be strictly decreasing. Typically something like {2:5, 3:4} ltol, stol, angle_tol, supercell_size: parameters to pass through to the StructureMatcher. Structures that don't match to the primitive cell under these tolerances won't be included in the expansion. Easiest option for supercell_size is usually to use a species that has a constant amount per formula unit. use_ewald: whether to calculate the ewald energy of each structure and use it as a feature. Typically a good idea for ionic materials. use_inv_r: experimental feature that allows fitting to arbitrary 1/r interactions between specie-site combinations. eta: parameter to override the EwaldSummation default eta. Usually only necessary if use_inv_r=True """ symops = SpacegroupAnalyzer(structure).get_symmetry_operations() #get the sites to expand over sites_to_expand = [site for site in structure if site.species.num_atoms < 0.99 \ or len(site.species) > 1] expansion_structure = Structure.from_sites(sites_to_expand) clusters = cls._clusters_from_radii(expansion_structure, radii, symops) return cls(structure=structure, expansion_structure=expansion_structure, symops=symops, \ sm_type = sm_type, ltol=ltol, stol=stol, angle_tol=angle_tol,\ clusters=clusters, supercell_size=supercell_size, use_ewald=use_ewald, \ use_inv_r=use_inv_r,eta=eta, basis=basis) @classmethod def _clusters_from_radii(cls, expansion_structure, radii, symops): """ Generates dictionary of size: [SymmetrizedCluster] given a dictionary of maximal cluster radii and symmetry operations to apply (not necessarily all the symmetries of the expansion_structure) """ bits = get_bits(expansion_structure) nbits = np.array([len(b) - 1 for b in bits]) # nbits = np.array([len(b) for b in bits]) new_clusters = [] clusters = {} for i, site in enumerate(expansion_structure): new_c = Cluster([site.frac_coords], expansion_structure.lattice) new_sc = SymmetrizedCluster(new_c, [np.arange(nbits[i])], symops) if new_sc not in new_clusters: new_clusters.append(new_sc) clusters[1] = sorted(new_clusters, key = lambda x: (np.round(x.max_radius,6), -x.multiplicity)) all_neighbors = expansion_structure.lattice.get_points_in_sphere(expansion_structure.frac_coords, [0.5, 0.5, 0.5], max(radii.values()) + sum(expansion_structure.lattice.abc)/2) for size, radius in sorted(radii.items()): new_clusters = [] for c in clusters[size-1]: if c.max_radius > radius: continue for n in all_neighbors: p = n[0] if is_coord_subset([p], c.sites, atol=SITE_TOL): continue new_c = Cluster(np.concatenate([c.sites, [p]]), expansion_structure.lattice) if new_c.max_radius > radius + 1e-8: continue new_sc = SymmetrizedCluster(new_c, c.bits + [np.arange(nbits[n[2]])], symops) if new_sc not in new_clusters: new_clusters.append(new_sc) clusters[size] = sorted(new_clusters, key = lambda x: (np.round(x.max_radius,6), -x.multiplicity)) return clusters def supercell_matrix_from_structure(self, structure): if self.sm_type == 'pmg_sm': sc_matrix = self.sm.get_supercell_matrix(structure, self.structure) elif self.sm_type == 'an_frame': prim_an_sites = [site for site in self.structure if Is_Anion_Site(site)] prim_an = Structure.from_sites(prim_an_sites) s_an_fracs = [] s_an_sps = [] latt = structure.lattice for site in structure: if Is_Anion_Site(site): s_an_fracs.append(site.frac_coords) s_an_sps.append(site.specie) scaling = ((len(s_an_sps)/len(prim_an))/(structure.volume/self.structure.volume))**(1/3.0) s_an_latt = Lattice.from_parameters(latt.a * scaling, latt.b * scaling, latt.c * scaling, \ latt.alpha, latt.beta, latt.gamma) structure_an = Structure(s_an_latt,s_an_sps,s_an_fracs,to_unit_cell =False, coords_are_cartesian=False) #print('Structure:',structure) #print('Structure_an:',structure_an) #print('Prim_an:',prim_an) sc_matrix = self.sm.get_supercell_matrix(structure_an, prim_an) else: raise ValueError("Structure Matcher type not implemented!") if sc_matrix is None: raise ValueError("Supercell couldn't be found") if np.linalg.det(sc_matrix) < 0: sc_matrix *= -1 return sc_matrix def supercell_from_structure(self, structure): sc_matrix = self.supercell_matrix_from_structure(structure) return self.supercell_from_matrix(sc_matrix) def supercell_from_matrix(self, sc_matrix): sc_matrix = tuple(sorted(tuple(s) for s in sc_matrix)) # print(sc_matrix) if sc_matrix in self._supercells: cs = self._supercells[sc_matrix] else: cs = ClusterSupercell(sc_matrix, self) self._supercells[sc_matrix] = cs return cs # def corr_from_external(self, structure, sc_matrix): # cs = self.supercell_from_matrix(self, sc_matrix) # return cs.corr_from_structure(structure) # This function will be integrated into supercell_from_structure. def corr_from_structure(self, structure): """ Given a structure, determines which supercell to use, and gets the correlation vector """ cs = self.supercell_from_structure(structure) return cs.corr_from_structure(structure) def base_energy(self, structure): sc = self.supercell_from_structure(structure) occu = sc.occu_from_structure(structure) be = sc._get_ewald_eci(occu)[0] #* sc.size return be def refine_structure(self, structure): sc_matrix = self.supercell_matrix_from_structure(structure) sc = self.supercell_from_matrix(sc_matrix) occu = sc.occu_from_structure(structure) return sc.structure_from_occu(occu) # def refine_structure_external(self, structure, sc_matrix): # cs = self.supercell_from_matrix(sc_matrix) # occu = cs.occu_from_structure(structure) # return cs.structure_from_occu(occu) def structure_energy(self, structure, ecis): cs = self.supercell_from_structure(structure) return cs.structure_energy(structure, ecis) @property def symmetrized_clusters(self): """ Yields all symmetrized clusters """ for k in sorted(self.clusters.keys()): for c in self.clusters[k]: # print(c) yield c def __str__(self): s = "ClusterBasis: {}\n".format(self.structure.composition) for k, v in self.clusters.iteritems(): s += " size: {}\n".format(k) for z in v: s += " {}\n".format(z) return s @classmethod def from_dict(cls, d): symops = [SymmOp.from_dict(so) for so in d['symops']] clusters = {} for k, v in d['clusters_and_bits'].items(): clusters[int(k)] = [SymmetrizedCluster(Cluster.from_dict(c[0]), c[1], symops) for c in v] return cls(structure=Structure.from_dict(d['structure']), expansion_structure=Structure.from_dict(d['expansion_structure']), clusters=clusters, symops=symops, sm_type = d['sm_type'] if 'sm_type' in d else 'pmg_sm', ltol=d['ltol'], stol=d['stol'], angle_tol=d['angle_tol'], #vor_tol = d['vor_tol'] if 'vor_tol' in d else 1e-3, supercell_size=d['supercell_size'], use_ewald=d['use_ewald'], use_inv_r=d['use_inv_r'], eta=d['eta'], basis=d['basis'] if 'basis' in d else '01') # Compatible with old datas def as_dict(self): c = {} for k, v in self.clusters.items(): c[int(k)] = [(sc.base_cluster.as_dict(), [list(b) for b in sc.bits]) for sc in v] return {'structure': self.structure.as_dict(), 'expansion_structure': self.expansion_structure.as_dict(), 'symops': [so.as_dict() for so in self.symops], 'clusters_and_bits': c, 'sm_type': self.sm_type, 'ltol': self.ltol, 'stol': self.stol, 'angle_tol': self.angle_tol, #'vor_tol': self.vor_tol, 'supercell_size': self.supercell_size, 'use_ewald': self.use_ewald, 'use_inv_r': self.use_inv_r, 'eta': self.eta, 'basis':self.basis, '@module': self.__class__.__module__, '@class': self.__class__.__name__}