def _filter_and_merge(inserted_structure: Structure): """ For each site in a structure, split it into a migration sublattice where all sites contain the "insertion_energy" property and a host lattice. For each site in the migration sublattice if there is collision with the host sites, remove the migration site. Finally merge all the migration sites. """ migration_sites = [] base_sites = [] for i_site in inserted_structure: if "insertion_energy" in i_site.properties and isinstance( i_site.properties["insertion_energy"], float): migration_sites.append(i_site) else: base_sites.append(i_site) migration = Structure.from_sites(migration_sites) base = Structure.from_sites(base_sites) non_colliding_sites = [] for i_site in migration.sites: col_sites = base.get_sites_in_sphere(i_site.coords, BASE_COLLISION_R) if len(col_sites) == 0: non_colliding_sites.append(i_site) res = Structure.from_sites(non_colliding_sites + base.sites) res.merge_sites(tol=SITE_MERGE_R, mode="average") return res
def make_site_diff(self): removed_sites = [ self._perfect_structure[x] for x in self.removed_indices ] inserted_sites = [ self._defect_structure[x] for x in self.inserted_indices ] try: removed_str = Structure.from_sites(removed_sites) except ValueError: removed_str = None try: inserted_str = Structure.from_sites(inserted_sites) except ValueError: inserted_str = None if inserted_str and removed_str: r_to_i = self._atom_projection(removed_str, inserted_str, specie=False) i_to_r = self._atom_projection(inserted_str, removed_str, specie=False) else: r_to_i = [None] * len(removed_sites) i_to_r = [None] * len(inserted_sites) mapping, removed_mapping, inserted_mapping = [], [], [] for x, y in enumerate(r_to_i): if y and x == i_to_r[y]: mapping.append( (self.removed_indices[x], self.inserted_indices[y])) removed_mapping.append(self.removed_indices[x]) inserted_mapping.append(self.inserted_indices[y]) removed, removed_by_sub = [], [] for idx in self.removed_indices: site = self._perfect_structure[idx] val = idx, site.species_string, tuple(site.frac_coords) if idx in removed_mapping: removed_by_sub.append(val) else: removed.append(val) inserted, inserted_by_sub = [], [] for idx in self.inserted_indices: site = self._defect_structure[idx] val = idx, site.species_string, tuple(site.frac_coords) if idx in inserted_mapping: inserted_by_sub.append(val) else: inserted.append(val) return SiteDiff(removed=removed, inserted=inserted, removed_by_sub=removed_by_sub, inserted_by_sub=inserted_by_sub)
def get_structures(self, nimages=5, vac_mode=True, idpp=False, **idpp_kwargs): r""" Generate structures for NEB calculation. Args: nimages (int): Defaults to 5. Number of NEB images. Total number of structures returned in nimages+2. vac_mode (bool): Defaults to True. In vac_mode, a vacancy diffusion mechanism is assumed. The initial and end sites of the path are assumed to be the initial and ending positions of the vacancies. If vac_mode is False, an interstitial mechanism is assumed. The initial and ending positions are assumed to be the initial and ending positions of the interstitial, and all other sites of the same specie are removed. E.g., if NEBPaths were obtained using a Li4Fe4P4O16 structure, vac_mode=True would generate structures with formula Li3Fe4P4O16, while vac_mode=False would generate structures with formula LiFe4P4O16. idpp (bool): Defaults to False. If True, the generated structures will be run through the IDPPSolver to generate a better guess for the minimum energy path. \*\*idpp_kwargs: Passthrough kwargs for the IDPPSolver.run. Returns: [Structure] Note that the first site of each structure is always the migrating ion. This makes it easier to perform subsequent analysis. """ migrating_specie_sites = [] other_sites = [] isite = self.isite esite = self.esite for site in self.symm_structure.sites: if site.specie != isite.specie: other_sites.append(site) else: if vac_mode and ( isite.distance(site) > 1e-8 and esite.distance(site) > 1e-8 ): migrating_specie_sites.append(site) start_structure = Structure.from_sites( [self.isite] + migrating_specie_sites + other_sites ) end_structure = Structure.from_sites( [self.esite] + migrating_specie_sites + other_sites ) structures = start_structure.interpolate( end_structure, nimages=nimages + 1, pbc=False ) if idpp: solver = IDPPSolver(structures) return solver.run(**idpp_kwargs) return structures
def remove_site_at_pos(structure: Structure, site: PeriodicSite): new_struct_sites = [] for isite in structure: if not vac_mode or (isite.distance(site) <= 1e-8): continue new_struct_sites.append(isite) return Structure.from_sites(new_struct_sites)
def test_get_mapping(self): sm = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5, primitive_cell=False, scale=True, attempt_supercell=False, allow_subset=True) l = Lattice.orthorhombic(1, 2, 3) s1 = Structure(l, ['Ag', 'Si', 'Si'], [[.7, .4, .5], [0, 0, 0.1], [0, 0, 0.2]]) s1.make_supercell([2, 1, 1]) s2 = Structure(l, ['Si', 'Si', 'Ag'], [[0, 0.1, -0.95], [0, 0.1, 0], [-.7, .5, .375]]) shuffle = [2, 0, 1, 3, 5, 4] s1 = Structure.from_sites([s1[i] for i in shuffle]) #test the mapping s2.make_supercell([2, 1, 1]) #equal sizes for i, x in enumerate(sm.get_mapping(s1, s2)): self.assertEqual(s1[x].species_and_occu, s2[i].species_and_occu) del s1[0] #s1 is subset of s2 for i, x in enumerate(sm.get_mapping(s2, s1)): self.assertEqual(s1[i].species_and_occu, s2[x].species_and_occu) #s2 is smaller than s1 del s2[0] del s2[1] self.assertRaises(ValueError, sm.get_mapping, s2, s1)
def get_full_sites(self): """ Get each group of symmetry inequivalent sites and combine them Args: Returns: a Structure with all possible Li sites, the enregy of the structure is stored as a site property """ res = [] for itr in self.translated_single_cat_entries: sub_site_list = get_all_sym_sites( itr, self.base_struct_entry, self.migrating_specie, symprec=self.symprec, angle_tol=self.angle_tol, ) # ic(sub_site_list._sites) res.extend(sub_site_list._sites) # check to see if the sites collide with the base struture filtered_res = [] for itr in res: col_sites = self.base_struct_entry.structure.get_sites_in_sphere( itr.coords, BASE_COLLISION_R) if len(col_sites) == 0: filtered_res.append(itr) res = Structure.from_sites(filtered_res) if len(res) > 1: res.merge_sites(tol=SITE_MERGE_R, mode="average") return res
def test_get_mapping(self): sm = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5, primitive_cell=False, scale=True, attempt_supercell=False, allow_subset = True) l = Lattice.orthorhombic(1, 2, 3) s1 = Structure(l, ['Ag', 'Si', 'Si'], [[.7,.4,.5],[0,0,0.1],[0,0,0.2]]) s1.make_supercell([2,1,1]) s2 = Structure(l, ['Si', 'Si', 'Ag'], [[0,0.1,-0.95],[0,0.1,0],[-.7,.5,.375]]) shuffle = [2,0,1,3,5,4] s1 = Structure.from_sites([s1[i] for i in shuffle]) #test the mapping s2.make_supercell([2,1,1]) #equal sizes for i, x in enumerate(sm.get_mapping(s1, s2)): self.assertEqual(s1[x].species_and_occu, s2[i].species_and_occu) del s1[0] #s1 is subset of s2 for i, x in enumerate(sm.get_mapping(s2, s1)): self.assertEqual(s1[i].species_and_occu, s2[x].species_and_occu) #s2 is smaller than s1 del s2[0] del s2[1] self.assertRaises(ValueError, sm.get_mapping, s2, s1)
def slab_from_file(structure, hkl): """ Reads in structure from the file and returns slab object. Args: structure (str): Structure file in any format supported by pymatgen. Will accept a pymatgen.Structure object directly. hkl (tuple): Miller index of the slab in the input file. Returns: Slab object """ if type(structure) == str: slab_input = Structure.from_file(structure) else: slab_input = structure return Slab( slab_input.lattice, slab_input.species_and_occu, slab_input.frac_coords, hkl, Structure.from_sites(slab_input, to_unit_cell=True), # this OUC is not correct, need to get it from slabgenerator shift=0, scale_factor=np.eye(3, dtype=np.int), site_properties=slab_input.site_properties)
def test_site_index_mapping_one(self): a = 6.19399 lattice = Lattice.from_parameters(a, a, a, 90, 90, 90) coords1 = np.array([[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]]) coords2 = np.array([[0.1, 0.1, 0.1], [0.4, 0.6, 0.4]]) sites1 = [ PeriodicSite(species='Na', coords=c, lattice=lattice) for c in coords1 ] sites2 = [ PeriodicSite(species='Na', coords=c, lattice=lattice) for c in coords2 ] structure1 = Structure.from_sites(sites1) structure2 = Structure.from_sites(sites2) mapping = site_index_mapping(structure1, structure2) np.testing.assert_array_equal(mapping, np.array([0, 1]))
def test_filter_and_merge(self): combined_struct = Structure.from_sites( self.struct_inserted_1Li1.sites + self.struct_inserted_1Li2.sites + self.struct_inserted_2Li.sites) filtered_struct = _filter_and_merge(combined_struct) for i_insert_site in filtered_struct: if i_insert_site.species_string == "Li": self.assertIn(i_insert_site.properties["insertion_energy"], {4.5, 5.5})
def test_site_index_mapping_with_species_1_as_string(self): a = 6.19399 lattice = Lattice.from_parameters(a, a, a, 90, 90, 90) coords1 = np.array([[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]]) coords2 = np.array([[0.4, 0.6, 0.4], [0.1, 0.1, 0.1]]) species1 = ['Na', 'Cl'] sites1 = [ PeriodicSite(species=s, coords=c, lattice=lattice) for s, c in zip(species1, coords1) ] sites2 = [ PeriodicSite(species='Na', coords=c, lattice=lattice) for c in coords2 ] structure1 = Structure.from_sites(sites1) structure2 = Structure.from_sites(sites2) mapping = site_index_mapping(structure1, structure2, species1='Na') np.testing.assert_array_equal(mapping, np.array([1]))
def get_sc_structures( self, vac_mode: bool, min_atoms: int = 80, max_atoms: int = 240, min_length: float = 10.0, ) -> Tuple[Structure, Structure, Structure]: """ Construct supercells that represents the start and end positions for migration analysis. Args: vac_mode: If true simulate vacancy diffusion. max_atoms: Maximum number of atoms allowed in the supercell. min_atoms: Minimum number of atoms allowed in the supercell. min_length: Minimum length of the smallest supercell lattice vector. Returns: Start, End, Base Structures. If not vacancy mode, the base structure is just the host lattice. If in vacancy mode, the base structure is the fully intercalated structure """ migrating_specie_sites, other_sites = self._split_migrating_and_other_sites( vac_mode) if vac_mode: base_struct = Structure.from_sites(other_sites + migrating_specie_sites) else: base_struct = Structure.from_sites(other_sites) sc_mat = get_sc_fromstruct( base_struct=base_struct, min_atoms=min_atoms, max_atoms=max_atoms, min_length=min_length, ) start_struct, end_struct, base_sc = get_start_end_structures( self.isite, self.esite, base_struct, sc_mat, vac_mode=vac_mode # type: ignore ) return start_struct, end_struct, base_sc
def setUp(self): c1 = [[0.5] * 3, [0.9] * 3] c2 = [[0.5] * 3, [0.9, 0.1, 0.1]] s1 = Structure(Lattice.cubic(5), ['Si', 'Si'], c1) s2 = Structure(Lattice.cubic(5), ['Si', 'Si'], c2) structs = [] for s in s1.interpolate(s2, 3, pbc=True): structs.append(Structure.from_sites(s.sites, to_unit_cell=True)) self.structures = structs self.vis = MITNEBSet(self.structures)
def test_site_index_mapping_with_one_to_one_mapping_raises_ValueError_one( self): a = 6.19399 lattice = Lattice.from_parameters(a, a, a, 90, 90, 90) coords1 = np.array([[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]]) coords2 = np.array([[0.4, 0.6, 0.4], [0.1, 0.1, 0.1]]) species2 = ['Na', 'Cl'] sites1 = [ PeriodicSite(species='Na', coords=c, lattice=lattice) for c in coords1 ] sites2 = [ PeriodicSite(species=s, coords=c, lattice=lattice) for s, c in zip(species2, coords2) ] structure1 = Structure.from_sites(sites1) structure2 = Structure.from_sites(sites2) with self.assertRaises(ValueError): site_index_mapping(structure1, structure2, species2='Na')
def write_input(self, output_dir, make_dir_if_not_present=True, write_cif=False, write_path_cif=False, write_endpoint_inputs=False): """ NEB inputs has a special directory structure where inputs are in 00, 01, 02, .... Args: output_dir (str): Directory to output the VASP input files make_dir_if_not_present (bool): Set to True if you want the directory (and the whole path) to be created if it is not present. write_cif (bool): If true, writes a cif along with each POSCAR. write_path_cif (bool): If true, writes a cif for each image. write_endpoint_inputs (bool): If true, writes input files for running endpoint calculations. """ if make_dir_if_not_present and not os.path.exists(output_dir): os.makedirs(output_dir) self.incar.write_file(os.path.join(output_dir, 'INCAR')) self.kpoints.write_file(os.path.join(output_dir, 'KPOINTS')) self.potcar.write_file(os.path.join(output_dir, 'POTCAR')) for i, p in enumerate(self.poscars): d = os.path.join(output_dir, str(i).zfill(2)) if not os.path.exists(d): os.makedirs(d) p.write_file(os.path.join(d, 'POSCAR')) if write_cif: p.structure.to(filename=os.path.join(d, '{}.cif'.format(i))) if write_endpoint_inputs: end_point_param = BulkRelaxSet( self.structures[0], user_incar_settings=self.user_incar_settings) for image in ['00', str(len(self.structures) - 1).zfill(2)]: end_point_param.incar.write_file( os.path.join(output_dir, image, 'INCAR') ) end_point_param.kpoints.write_file( os.path.join(output_dir, image, 'KPOINTS') ) end_point_param.potcar.write_file( os.path.join(output_dir, image, 'POTCAR') ) if write_path_cif: sites = set() lattice = self.structures[0].lattice for site in chain(*(s.sites for s in self.structures)): sites.add(PeriodicSite(site.species_and_occu, site.frac_coords, lattice)) nebpath = Structure.from_sites(sorted(sites)) nebpath.to(filename=os.path.join(output_dir, 'path.cif'))
def convert_to_structure(self, coloring) -> Structure: list_psites = [ self.precomputed_psites[i][coloring[i]] for i in range(self.dshash.num_sites) ] if self.additional_psites is not None: list_psites.extend(self.additional_psites) dstruct = Structure.from_sites(list_psites) return dstruct
def get_endpoints_from_index(structure, site_indices): """ This class reads in one perfect structure and the two endpoint structures are generated using site_indices. Args: structure (Structure): A perfect structure. site_indices (list of int): a two-element list indicating site indices. Returns: endpoints (list of Structure): a two-element list of two endpoints Structure object. """ if len(site_indices) != 2 or len(set(site_indices)) != 2: raise ValueError("Invalid indices!") if structure[site_indices[0]].specie != structure[site_indices[1]].specie: raise ValueError("The site indices must be " "associated with identical species!") s = structure.copy() sites = s.sites # Move hopping atoms to the beginning of species index. init_site = sites[site_indices[0]] final_site = sites[site_indices[1]] sites.remove(init_site) sites.remove(final_site) init_sites = copy.deepcopy(sites) final_sites = copy.deepcopy(sites) init_sites.insert(0, final_site) final_sites.insert(0, init_site) s_0 = Structure.from_sites(init_sites) s_1 = Structure.from_sites(final_sites) endpoints = [s_0, s_1] return endpoints
def get_sym_migration_ion_sites( base_struct: Structure, inserted_struct: Structure, migrating_ion: str, symprec: float = 0.01, angle_tol: float = 5.0, ) -> Structure: """ Take one inserted entry then map out all symmetry equivalent copies of the cation sites in base entry. Each site is decorated with the insertion energy calculated from the base and inserted entries. Args: inserted_entry: entry that contains cation base_struct_entry: the entry containing the base structure migrating_ion_entry: the name of the migrating species symprec: the symprec tolerance for the space group analysis angle_tol: the angle tolerance for the space group analysis Returns: Structure with only the migrating ion sites decorated with insertion energies. """ wi_ = migrating_ion sa = SpacegroupAnalyzer(base_struct, symprec=symprec, angle_tolerance=angle_tol) # start with the base structure but empty sym_migration_ion_sites = list( filter( lambda isite: isite.species_string == wi_, inserted_struct.sites, )) sym_migration_struct = Structure.from_sites(sym_migration_ion_sites) for op in sa.get_space_group_operations(): struct_tmp = sym_migration_struct.copy() struct_tmp.apply_operation(symmop=op, fractional=True) for isite in struct_tmp.sites: if isite.species_string == wi_: sym_migration_struct.insert( 0, wi_, coords=np.mod(isite.frac_coords, 1.0), properties=isite.properties, ) # must clean up as you go or the number of sites explodes if len(sym_migration_struct) > 1: sym_migration_struct.merge_sites( tol=SITE_MERGE_R, mode="average") # keeps removing duplicates return sym_migration_struct
def with_base_structure(cls, base_structure: Structure, m_graph: StructureGraph, **kwargs) -> "MigrationGraph": """ Args: base_structure: base framework structure that does not contain any migrating sites. Returns: A constructed MigrationGraph object """ sites = m_graph.structure.sites + base_structure.sites structure = Structure.from_sites(sites) return cls(structure=structure, m_graph=m_graph, **kwargs)
def test_site_index_mapping_with_return_mapping_distances(self): a = 6.19399 lattice = Lattice.from_parameters(a, a, a, 90, 90, 90) coords1 = np.array([[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]]) coords2 = np.array([[0.1, 0.1, 0.1], [0.4, 0.6, 0.4]]) sites1 = [ PeriodicSite(species='Na', coords=c, lattice=lattice) for c in coords1 ] sites2 = [ PeriodicSite(species='Na', coords=c, lattice=lattice) for c in coords2 ] structure1 = Structure.from_sites(sites1) structure2 = Structure.from_sites(sites2) mapping, distances = site_index_mapping(structure1, structure2, return_mapping_distances=True) np.testing.assert_array_equal(mapping, np.array([0, 1])) expected_distance = np.sqrt(3 * ((0.1 * a)**2)) np.testing.assert_array_almost_equal( distances, np.array([expected_distance, expected_distance]))
def write_path(self, fname, **kwargs): r""" Write the path to a file for easy viewing. Args: fname (str): File name. \*\*kwargs: Kwargs supported by NEBPath.get_structures. """ sites = [] for st in self.get_structures(**kwargs): sites.extend(st) st = Structure.from_sites(sites) st.to(filename=fname)
def write_all_paths(self, fname, nimages=5, **kwargs): r""" Write a file containing all paths, using hydrogen as a placeholder for the images. H is chosen as it is the smallest atom. This is extremely useful for path visualization in a standard software like VESTA. Args: fname (str): Filename nimages (int): Number of images per path. \*\*kwargs: Passthrough kwargs to path.get_structures. """ sites = [] for p in self.get_paths(): structures = p.get_structures( nimages=nimages, species=[self.migrating_specie], **kwargs ) sites.append(structures[0][0]) sites.append(structures[-1][0]) for s in structures[1:-1]: sites.append(PeriodicSite("H", s[0].frac_coords, s.lattice)) sites.extend(structures[0].sites[1:]) Structure.from_sites(sites).to(filename=fname)
def from_dict(cls, d): lattice = Lattice.from_dict(d["lattice"]) sites = [PeriodicSite.from_dict(sd, lattice) for sd in d["sites"]] s = Structure.from_sites(sites) return cls( lattice=lattice, species=s.species_and_occu, coords=s.frac_coords, miller_index=d["miller_index"], oriented_unit_cell=Structure.from_dict(d["oriented_unit_cell"]), shift=d["shift"], scale_factor=MontyDecoder().process_decoded(d["scale_factor"]), site_properties=s.site_properties, energy=d["energy"] )
def clusters_from_structure(structure, rcut, elements): """ Take a pymatgen structure, and converts it to a graph object Args: - structure (Structure): pymatgen structure object to set up graph from - rcut (float): cut-off radii for node-node connections in forming clusters - elements ({str,str,.....}): set of element strings to include in setting up graph Returns: - clusters ({clusters}): set of clusters """ symbols = set([species for species in structure.symbol_set]) if elements.issubset(structure.symbol_set): all_elements = set([species for species in structure.symbol_set]) remove_elements = [x for x in all_elements if x not in elements] structure.remove_species(remove_elements) folded_structure = Structure.from_sites(structure.sites, to_unit_cell=True) nodes = nodes_from_structure(folded_structure, rcut, get_halo=True) set_fort_nodes(nodes) clusters = set() uc_nodes = set( [node for node in nodes if node.labels["Halo"] == False]) while uc_nodes: node = uc_nodes.pop() if node.labels["Halo"] == False: cluster = Cluster({node}) cluster.grow_cluster() uc_nodes.difference_update(cluster.nodes) clusters.add(cluster) set_cluster_periodic(cluster) return clusters else: raise ValueError( "The element set fed to 'clusters_from_file' is not a subset of the elements in the file" )
def get_full_sites(self): """ Get each group of symmetry inequivalent sites and combine them Args: Returns: a Structure with all possible Li sites, the enregy of the structure is stored as a site property """ res = [] for itr in self.translated_single_cat_entries: res.extend(self.get_all_sym_sites(itr).sites) res = Structure.from_sites(res) res.merge_sites(tol=1.0, mode='average') return res
def get_only_sites(self): """ Get a copy of the structure with only the sites Args: Returns: Structure: Structure with all possible migrating ion sites """ migrating_ion_sites = list( filter( lambda site: site.species == Composition( {self.migrating_specie: 1}), self.structure.sites)) return Structure.from_sites(migrating_ion_sites)
def as_ordered_structure(self): """ Return the structure as a pymatgen.core.Structure, removing the unoccupied sites. This is because many of the IO methods of pymatgen run into issues when empty occupancies are present. Returns: pymatgen.core.Structure """ return Structure.from_sites( [site for site in self.sites if site.species != Composition()] )
def get_only_sites_from_structure(structure: Structure, migrating_specie: str) -> Structure: """ Get a copy of the structure with only the migrating sites. Args: structure: The full_structure that contains all the sites migrating_specie: The name of migrating species Returns: Structure: Structure with all possible migrating ion sites """ migrating_ion_sites = list( filter( lambda site: site.species == Composition({migrating_specie: 1}), structure.sites, )) return Structure.from_sites(migrating_ion_sites)
def get_sorted_structure(self, key=None, reverse=False) -> Structure: """ Get a sorted structure for the interface. The parameters have the same meaning as in list.sort. By default, sites are sorted by the electronegativity of the species. Args: key: Specifies a function of one argument that is used to extract a comparison key from each list element: key=str.lower. The default value is None (compare the elements directly). reverse (bool): If set to True, then the list elements are sorted as if each comparison were reversed. """ struct_copy = Structure.from_sites(self) struct_copy.sort(key=key, reverse=reverse) return struct_copy
def __init__( self, structure: Structure, migration_graph: StructureGraph, structure_is_base=False, symprec=0.1, vac_mode=False, ): """ Construct the MigrationGraph object using a potential_field will all mobile sites occupied. A potential_field graph is generated by connecting all sites withing max_path_length distance of each other The sites are decorated with Migration graph objects and then grouped together based on their equivalence. Args: structure: Structure with base framework and mobile sites. When used with structure_is_base = True, only the base framework structure, does not contain any migrating sites. migration_graph: The StructureGraph object that defines the migration network structure_is_base: Flag to indicate if the structure provided is only the base framework structure and does not contain any migrating sites. symprec (float): Symmetry precision to determine equivalence of migration events """ if structure_is_base is True: sites = migration_graph.structure.sites + structure.sites self.structure = Structure.from_sites(sites) else: self.structure = structure self.migration_graph = migration_graph self.symprec = symprec self.vac_mode = vac_mode if self.vac_mode: raise NotImplementedError("Vacancy mode is not yet implemented") # Generate the graph edges between these all the sites self.migration_graph.set_node_attributes( ) # popagate the sites properties to the graph nodes # For poperies like unique_hops we might be interested in modifying them after creation # So let's not convert them into properties for now. (Awaiting rewrite once the usage becomes more clear.) self._populate_edges_with_migration_paths() self._group_and_label_hops() self.unique_hops = None self._populate_unique_hops_dict()
def label_termination(slab: Structure) -> str: """Labels the slab surface termination""" frac_coords = slab.frac_coords n = len(frac_coords) if n == 1: # Clustering does not work when there is only one data point. form = slab.composition.reduced_formula sp_symbol = SpacegroupAnalyzer(slab, symprec=0.1).get_space_group_symbol() return f"{form}_{sp_symbol}_{len(slab)}" dist_matrix = np.zeros((n, n)) h = slab.lattice.c # Projection of c lattice vector in # direction of surface normal. for i, j in combinations(list(range(n)), 2): if i != j: cdist = frac_coords[i][2] - frac_coords[j][2] cdist = abs(cdist - round(cdist)) * h dist_matrix[i, j] = cdist dist_matrix[j, i] = cdist condensed_m = squareform(dist_matrix) z = linkage(condensed_m) clusters = fcluster(z, 0.25, criterion="distance") clustered_sites: Dict[int, List[Site]] = {c: [] for c in clusters} for i, c in enumerate(clusters): clustered_sites[c].append(slab[i]) plane_heights = { np.average(np.mod([s.frac_coords[2] for s in sites], 1)): c for c, sites in clustered_sites.items() } top_plane_cluster = sorted(plane_heights.items(), key=lambda x: x[0])[-1][1] top_plane_sites = clustered_sites[top_plane_cluster] top_plane = Structure.from_sites(top_plane_sites) sp_symbol = SpacegroupAnalyzer(top_plane, symprec=0.1).get_space_group_symbol() form = top_plane.composition.reduced_formula return f"{form}_{sp_symbol}_{len(top_plane)}"
def predict(self, structure, icsd_vol=False): """ Given a structure, returns the predicted volume. Args: structure (Structure) : a crystal structure with an unknown volume. icsd_vol (bool) : True if the input structure's volume comes from ICSD. Returns: a float value of the predicted volume. """ # Get standard deviation of electronnegativity in the structure. std_x = np.std([site.specie.X for site in structure]) # Sites that have atomic radii sub_sites = [] # Record the "DLS estimated radius" from bond_params. bp_dict = {} for sp in list(structure.composition.keys()): if sp.atomic_radius: sub_sites.extend([site for site in structure if site.specie == sp]) else: warnings.warn("VolumePredictor: no atomic radius data for " "{}".format(sp)) if sp.symbol not in bond_params: warnings.warn("VolumePredictor: bond parameters not found, " "used atomic radii for {}".format(sp)) else: r, k = bond_params[sp.symbol]["r"], bond_params[sp.symbol]["k"] bp_dict[sp] = float(r) + float(k) * std_x # Structure object that include only sites with known atomic radii. reduced_structure = Structure.from_sites(sub_sites) smallest_ratio = None for site1 in reduced_structure: sp1 = site1.specie neighbors = reduced_structure.get_neighbors(site1, sp1.atomic_radius + self.cutoff) for site2, dist in neighbors: sp2 = site2.specie if sp1 in bp_dict and sp2 in bp_dict: expected_dist = bp_dict[sp1] + bp_dict[sp2] else: expected_dist = sp1.atomic_radius + sp2.atomic_radius if not smallest_ratio or dist / expected_dist < smallest_ratio: smallest_ratio = dist / expected_dist if not smallest_ratio: raise ValueError("Could not find any bonds within the given cutoff " "in this structure.") volume_factor = (1 / smallest_ratio) ** 3 # icsd volume fudge factor if icsd_vol: volume_factor *= 1.05 return structure.volume * volume_factor
def test_supercell_subsets(self): sm = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5, primitive_cell=False, scale=True, attempt_supercell=True, allow_subset=True, supercell_size='volume') sm_no_s = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5, primitive_cell=False, scale=True, attempt_supercell=True, allow_subset=False, supercell_size='volume') l = Lattice.orthorhombic(1, 2, 3) s1 = Structure(l, ['Ag', 'Si', 'Si'], [[.7,.4,.5],[0,0,0.1],[0,0,0.2]]) s1.make_supercell([2,1,1]) s2 = Structure(l, ['Si', 'Si', 'Ag'], [[0,0.1,-0.95],[0,0.1,0],[-.7,.5,.375]]) shuffle = [0,2,1,3,4,5] s1 = Structure.from_sites([s1[i] for i in shuffle]) #test when s1 is exact supercell of s2 result = sm.get_s2_like_s1(s1, s2) for a, b in zip(s1, result): self.assertTrue(a.distance(b) < 0.08) self.assertEqual(a.species_and_occu, b.species_and_occu) self.assertTrue(sm.fit(s1, s2)) self.assertTrue(sm.fit(s2, s1)) self.assertTrue(sm_no_s.fit(s1, s2)) self.assertTrue(sm_no_s.fit(s2, s1)) rms = (0.048604032430991401, 0.059527539448807391) self.assertTrue(np.allclose(sm.get_rms_dist(s1, s2), rms)) self.assertTrue(np.allclose(sm.get_rms_dist(s2, s1), rms)) #test when the supercell is a subset of s2 subset_supercell = s1.copy() del subset_supercell[0] result = sm.get_s2_like_s1(subset_supercell, s2) self.assertEqual(len(result), 6) for a, b in zip(subset_supercell, result): self.assertTrue(a.distance(b) < 0.08) self.assertEqual(a.species_and_occu, b.species_and_occu) self.assertTrue(sm.fit(subset_supercell, s2)) self.assertTrue(sm.fit(s2, subset_supercell)) self.assertFalse(sm_no_s.fit(subset_supercell, s2)) self.assertFalse(sm_no_s.fit(s2, subset_supercell)) rms = (0.053243049896333279, 0.059527539448807336) self.assertTrue(np.allclose(sm.get_rms_dist(subset_supercell, s2), rms)) self.assertTrue(np.allclose(sm.get_rms_dist(s2, subset_supercell), rms)) #test when s2 (once made a supercell) is a subset of s1 s2_missing_site = s2.copy() del s2_missing_site[1] result = sm.get_s2_like_s1(s1, s2_missing_site) for a, b in zip((s1[i] for i in (0, 2, 4, 5)), result): self.assertTrue(a.distance(b) < 0.08) self.assertEqual(a.species_and_occu, b.species_and_occu) self.assertTrue(sm.fit(s1, s2_missing_site)) self.assertTrue(sm.fit(s2_missing_site, s1)) self.assertFalse(sm_no_s.fit(s1, s2_missing_site)) self.assertFalse(sm_no_s.fit(s2_missing_site, s1)) rms = (0.029763769724403633, 0.029763769724403987) self.assertTrue(np.allclose(sm.get_rms_dist(s1, s2_missing_site), rms)) self.assertTrue(np.allclose(sm.get_rms_dist(s2_missing_site, s1), rms))
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