def add_snl(self, snl, force_new=False, snlgroup_guess=None): try: self.lock_db() snl_id = self._get_next_snl_id() spstruc = snl.structure.copy() spstruc.remove_oxidation_states() sf = SpacegroupAnalyzer(spstruc, SPACEGROUP_TOLERANCE) sf.get_space_group_operations() sgnum = sf.get_space_group_number() if sf.get_space_group_number() \ else -1 sgsym = sf.get_space_group_symbol() if sf.get_space_group_symbol() \ else 'unknown' sghall = sf.get_hall() if sf.get_hall() else 'unknown' sgxtal = sf.get_crystal_system() if sf.get_crystal_system() \ else 'unknown' sglatt = sf.get_lattice_type() if sf.get_lattice_type( ) else 'unknown' sgpoint = sf.get_point_group_symbol() mpsnl = MPStructureNL.from_snl(snl, snl_id, sgnum, sgsym, sghall, sgxtal, sglatt, sgpoint) snlgroup, add_new, spec_group = self.add_mpsnl( mpsnl, force_new, snlgroup_guess) self.release_lock() return mpsnl, snlgroup.snlgroup_id, spec_group except: self.release_lock() traceback.print_exc() raise ValueError("Error while adding SNL!")
def add_snl(self, snl, force_new=False, snlgroup_guess=None): try: self.lock_db() snl_id = self._get_next_snl_id() spstruc = snl.structure.copy() spstruc.remove_oxidation_states() sf = SpacegroupAnalyzer(spstruc, SPACEGROUP_TOLERANCE) sf.get_space_group_operations() sgnum = sf.get_space_group_number() if sf.get_space_group_number() \ else -1 sgsym = sf.get_space_group_symbol() if sf.get_space_group_symbol() \ else 'unknown' sghall = sf.get_hall() if sf.get_hall() else 'unknown' sgxtal = sf.get_crystal_system() if sf.get_crystal_system() \ else 'unknown' sglatt = sf.get_lattice_type() if sf.get_lattice_type() else 'unknown' sgpoint = sf.get_point_group_symbol() mpsnl = MPStructureNL.from_snl(snl, snl_id, sgnum, sgsym, sghall, sgxtal, sglatt, sgpoint) snlgroup, add_new, spec_group = self.add_mpsnl(mpsnl, force_new, snlgroup_guess) self.release_lock() return mpsnl, snlgroup.snlgroup_id, spec_group except: self.release_lock() traceback.print_exc() raise ValueError("Error while adding SNL!")
def _complete_ordering(self, structure, num_remove_dict): self.logger.debug("Performing complete ordering...") all_structures = [] symprec = 0.2 s = SpacegroupAnalyzer(structure, symprec=symprec) self.logger.debug( "Symmetry of structure is determined to be {}.".format( s.get_space_group_symbol())) sg = s.get_space_group_operations() tested_sites = [] starttime = time.time() self.logger.debug("Performing initial ewald sum...") ewaldsum = EwaldSummation(structure) self.logger.debug("Ewald sum took {} seconds.".format(time.time() - starttime)) starttime = time.time() allcombis = [] for ind, num in num_remove_dict.items(): allcombis.append(itertools.combinations(ind, num)) count = 0 for allindices in itertools.product(*allcombis): sites_to_remove = [] indices_list = [] for indices in allindices: sites_to_remove.extend([structure[i] for i in indices]) indices_list.extend(indices) s_new = structure.copy() s_new.remove_sites(indices_list) energy = ewaldsum.compute_partial_energy(indices_list) already_tested = False for i, tsites in enumerate(tested_sites): tenergy = all_structures[i]["energy"] if abs((energy - tenergy) / len(s_new)) < 1e-5 and \ sg.are_symmetrically_equivalent(sites_to_remove, tsites, symm_prec=symprec): already_tested = True if not already_tested: tested_sites.append(sites_to_remove) all_structures.append({"structure": s_new, "energy": energy}) count += 1 if count % 10 == 0: timenow = time.time() self.logger.debug("{} structures, {:.2f} seconds.".format( count, timenow - starttime)) self.logger.debug("Average time per combi = {} seconds".format( (timenow - starttime) / count)) self.logger.debug( "{} symmetrically distinct structures found.".format( len(all_structures))) self.logger.debug( "Total symmetrically distinct structures found = {}".format( len(all_structures))) all_structures = sorted(all_structures, key=lambda s: s["energy"]) return all_structures
def test_tensor(self): """Initialize Tensor""" lattice = Lattice.hexagonal(4, 6) #rprimd = np.array([[0,0.5,0.5],[0.5,0,0.5],[0.5,0.5,0]]) #rprimd = rprimd*10 #lattice = Lattice(rprimd) structure = Structure(lattice, ["Ga", "As"], [[0, 0, 0], [0.5, 0.5, 0.5]]) #finder = SymmetryFinder(structure) finder = SpacegroupAnalyzer(structure) spacegroup = finder.get_space_group_operations() pointgroup = finder.get_point_group_symbol() cartesian_tensor = [[2, 3, 1.2], [3, 4, 1.0], [1.2, 1.0, 6]] tensor = Tensor.from_cartesian_tensor(cartesian_tensor, lattice.reciprocal_lattice, space="g") red_tensor = tensor.reduced_tensor tensor2 = Tensor(red_tensor, lattice.reciprocal_lattice, space="g") assert (((np.abs(tensor2.cartesian_tensor) - np.abs(cartesian_tensor)) < 1E-8).all()) self.assertTrue(tensor == tensor2) print(tensor) #print("non-symmetrized cartesian_tensor = ",tensor2.cartesian_tensor) tensor2.symmetrize(structure) #print("symmetrized_cartesian_tensor = ",tensor2.cartesian_tensor) self.serialize_with_pickle(tensor)
def get_all_sym_sites(ent, base_struct_entry, migrating_specie, symprec=2.0, angle_tol=10): """ Return all of the symmetry equivalent sites by applying the symmetry operation of the empty structure Args: ent(ComputedStructureEntry): that contains cation migrating_species(string or Elment): Returns: Structure: containing all of the symmetry equivalent sites """ migrating_specie_el = get_el_sp(migrating_specie) sa = SpacegroupAnalyzer(base_struct_entry.structure, symprec=symprec, angle_tolerance=angle_tol) # start with the base structure but empty host_allsites = base_struct_entry.structure.copy() host_allsites.remove_species(host_allsites.species) pos_Li = list( filter( lambda isite: isite.species_string == migrating_specie_el.name, ent.structure.sites, )) # energy difference per site inserted_energy = (ent.energy - base_struct_entry.energy) / len(pos_Li) for isite in pos_Li: host_allsites.insert( 0, migrating_specie_el.name, np.mod(isite.frac_coords, 1), properties=dict(inserted_energy=inserted_energy, magmom=0), ) # base_ops = sa.get_space_group_operations() # all_ops = generate_full_symmops(base_ops, tol=1.0) for op in sa.get_space_group_operations(): logger.debug(f"{op}") struct_tmp = host_allsites.copy() struct_tmp.apply_operation(symmop=op, fractional=True) for isite in struct_tmp.sites: if isite.species_string == migrating_specie_el.name: logger.debug(f"{op}") host_allsites.insert( 0, migrating_specie_el.name, np.mod(isite.frac_coords, 1), properties=dict(inserted_energy=inserted_energy, magmom=0), ) host_allsites.merge_sites( tol=SITE_MERGE_R, mode="delete") # keeps only remove duplicates return host_allsites
def complete_ordering(self, structure, num_remove_dict): self.logger.debug("Performing complete ordering...") all_structures = [] symprec = 0.2 s = SpacegroupAnalyzer(structure, symprec=symprec) self.logger.debug("Symmetry of structure is determined to be {}." .format(s.get_space_group_symbol())) sg = s.get_space_group_operations() tested_sites = [] starttime = time.time() self.logger.debug("Performing initial ewald sum...") ewaldsum = EwaldSummation(structure) self.logger.debug("Ewald sum took {} seconds." .format(time.time() - starttime)) starttime = time.time() allcombis = [] for ind, num in num_remove_dict.items(): allcombis.append(itertools.combinations(ind, num)) count = 0 for allindices in itertools.product(*allcombis): sites_to_remove = [] indices_list = [] for indices in allindices: sites_to_remove.extend([structure[i] for i in indices]) indices_list.extend(indices) s_new = structure.copy() s_new.remove_sites(indices_list) energy = ewaldsum.compute_partial_energy(indices_list) already_tested = False for i, tsites in enumerate(tested_sites): tenergy = all_structures[i]["energy"] if abs((energy - tenergy) / len(s_new)) < 1e-5 and \ sg.are_symmetrically_equivalent(sites_to_remove, tsites, symm_prec=symprec): already_tested = True if not already_tested: tested_sites.append(sites_to_remove) all_structures.append({"structure": s_new, "energy": energy}) count += 1 if count % 10 == 0: timenow = time.time() self.logger.debug("{} structures, {:.2f} seconds." .format(count, timenow - starttime)) self.logger.debug("Average time per combi = {} seconds" .format((timenow - starttime) / count)) self.logger.debug("{} symmetrically distinct structures found." .format(len(all_structures))) self.logger.debug("Total symmetrically distinct structures found = {}" .format(len(all_structures))) all_structures = sorted(all_structures, key=lambda s: s["energy"]) return all_structures
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 get_all_sym_sites(ent, base_struct_entry, migrating_specie, stol=1.0, atol=10): """ Return all of the symmetry equivalent sites by applying the symmetry operation of the empty structure Args: ent(ComputedStructureEntry): that contains cation migrating_species(string or Elment): Returns: Structure: containing all of the symmetry equivalent sites """ migrating_specie_el = get_el_sp(migrating_specie) sa = SpacegroupAnalyzer(base_struct_entry.structure, symprec=stol, angle_tolerance=atol) # start with the base structure but empty host_allsites = base_struct_entry.structure.copy() host_allsites.remove_species(host_allsites.species) pos_Li = list( filter(lambda isite: isite.species_string == migrating_specie_el.name, ent.structure.sites)) for isite in pos_Li: host_allsites.insert(0, migrating_specie_el.name, np.mod(isite.frac_coords, 1), properties={'inserted_energy': ent.energy}) # base_ops = sa.get_space_group_operations() # all_ops = generate_full_symmops(base_ops, tol=1.0) for op in sa.get_space_group_operations(): logger.debug(f'{op}') struct_tmp = host_allsites.copy() struct_tmp.apply_operation(symmop=op, fractional=True) for isite in struct_tmp.sites: if isite.species_string == migrating_specie_el.name: logger.debug(f'{op}') host_allsites.insert( 0, migrating_specie_el.name, np.mod(isite.frac_coords, 1), properties={'inserted_energy': ent.energy}) host_allsites.merge_sites( mode='average' ) # keeps only one position but average the properties return host_allsites
def analyze_symmetry(self, tol): s = Structure.from_sites(self.framework) site_to_vindex = {} for i, v in enumerate(self.vnodes): s.append("Li", v.frac_coords) site_to_vindex[s[-1]] = i print(len(s)) finder = SpacegroupAnalyzer(s, tol) print(finder.get_space_group_operations()) symm_structure = finder.get_symmetrized_structure() print(len(symm_structure.equivalent_sites)) return [[site_to_vindex[site] for site in sites] for sites in symm_structure.equivalent_sites if sites[0].specie.symbol == "Li"]
def get_all_sym_sites(self, ent): """ Return all of the symmetry equivalent sites by applying the symmetry operation of the empty structure Args: ent(ComputedStructureEntry): that contains cation Returns: Structure: containing all of the symmetry equivalent sites """ sa = SpacegroupAnalyzer( self.base_struct_entry.structure, symprec=0.3, angle_tolerance=10) host_allsites = self.base_struct_entry.structure.copy() host_allsites.remove_species(host_allsites.species) pos_Li = list( filter(lambda isite: isite.species_string == 'Li', ent.structure.sites)) for isite in pos_Li: host_allsites.insert( 0, 'Li', isite.frac_coords, properties={'inserted_energy': ent.energy}) for op in sa.get_space_group_operations(): struct_tmp = host_allsites.copy() struct_tmp.apply_operation(symmop=op, fractional=True) for isite in struct_tmp.sites: if isite.species_string == "Li": host_allsites.insert( 0, 'Li', isite.frac_coords, properties={'inserted_energy': ent.energy}) host_allsites.merge_sites( mode='average' ) # keeps only one position but average the properties return host_allsites
def symmetry_order_2d(structure, point_group_list): ''' Find symmetry order in a two dimensional structure Args: structure: pymatgen structure point_group_list Return: Number of symmetry operations, and site symmetry order of the input structure ''' s = SpacegroupAnalyzer(structure, symprec=0.1) point_group_symbols = s.get_symmetry_dataset()['site_symmetry_symbols'] point_group_order = 0 for point_group_symbol in point_group_symbols : point_group_order += point_group_list[point_group_symbol.replace('.', '')] # return s.get_space_group_symbol(), s.get_space_group_number(), return len(s.get_space_group_operations()), point_group_order/len(point_group_symbols)
def __init__(self, struc, ref_mol=None, tol=0.2, relax_h=False): """ extract the mol_site information from the give cif file and reference molecule Args: struc: cif/poscar file or a Pymatgen Structure object ref_mol: xyz file or a reference Pymatgen molecule object tol: scale factor for covalent bond distance relax_h: whether or not relax the position for hydrogen atoms in structure """ if isinstance(ref_mol, str): ref_mol = Molecule.from_file(ref_mol) elif isinstance(ref_mol, Molecule): ref_mol = ref_mol else: print(type(ref_mol)) raise NameError("reference molecule cannot be defined") if isinstance(struc, str): pmg_struc = Structure.from_file(struc) elif isinstance(struc, Structure): pmg_struc = struc else: print(type(struc)) raise NameError("input structure cannot be intepretted") self.props = ref_mol.site_properties self.ref_mol = ref_mol.get_centered_molecule() self.tol = tol self.diag = False self.relax_h = relax_h sga = SpacegroupAnalyzer(pmg_struc) ops = sga.get_space_group_operations() self.wyc, perm = Wyckoff_position.from_symops( ops, sga.get_space_group_number()) if self.wyc is not None: self.group = Group(self.wyc.number) if isinstance(perm, list): if perm != [0, 1, 2]: lattice = Lattice.from_matrix(pmg_struc.lattice.matrix, self.group.lattice_type) latt = lattice.swap_axis(ids=perm, random=False).get_matrix() coor = pmg_struc.frac_coords[:, perm] pmg_struc = Structure(latt, pmg_struc.atomic_numbers, coor) else: self.diag = True self.perm = perm coords, numbers = search_molecule_in_crystal(pmg_struc, self.tol) #coords -= np.mean(coords, axis=0) if self.relax_h: self.molecule = self.addh(Molecule(numbers, coords)) else: self.molecule = Molecule(numbers, coords) self.pmg_struc = pmg_struc self.lattice = Lattice.from_matrix(pmg_struc.lattice.matrix, self.group.lattice_type) else: raise ValueError( "Cannot find the space group matching the symmetry operation")
def enumerate_ads_config(self, adsorbate, loading, site='all', symmetry_tol=0.01, custom_sites_only=False, csite_name='bcc_octa', save_voronoi=False): vor, mesh = self.Voronoi_tessalate() vcoords = vor.vertices base_coords = [a.position for a in self.bulk_ase] repeat_unit_cell = self.bulk_ase.get_cell().T inv_ruc = np.linalg.inv(repeat_unit_cell) custom_sites = np.dot(repeat_unit_cell, self.custom_sites.positions.T).T corrected_vcoords = [] used = [] for coord in vcoords: if np.linalg.norm(coord) < 1e3: fcoord = np.dot(inv_ruc, coord) zero_threshold_indices = abs(fcoord) < 1e-6 fcoord[zero_threshold_indices] = 0.0 one_threshold_indices = abs(fcoord - 1.0) < 1e-6 fcoord[one_threshold_indices] = 0.0 advance = True for u in used: if np.allclose(fcoord, u): advance = False break for dim in fcoord: if dim > 1.0 or dim < 0.0: advance = False if advance: corrected_vcoords.append(np.dot(repeat_unit_cell, fcoord)) used.append(fcoord) if save_voronoi: with open('voronoi_sites.xyz', 'w') as out: out.write(str(len(base_coords) + len(corrected_vcoords))) out.write('\n') out.write('check') out.write('\n') for vec in base_coords: out.write('{:4} {:>10} {:>10} {:>10}'.format(*['C'] + list(vec))) out.write('\n') for vec in corrected_vcoords: out.write('{:4} {:>10} {:>10} {:>10}'.format(*['H'] + list(vec))) out.write('\n') vtypes = [] if not custom_sites_only: for vert in corrected_vcoords: used = [] spectrum = [] for atom_coord in base_coords: fvert = np.dot(inv_ruc, vert) fatom_coord = np.dot(inv_ruc, atom_coord) fdist_vec, sym = PBC3DF_sym(fvert, fatom_coord) dist = np.linalg.norm(np.dot(repeat_unit_cell, fdist_vec)) sym_atom_coord = np.dot(repeat_unit_cell, fatom_coord - sym) spectrum.append((dist, sym_atom_coord)) spectrum.sort(key=lambda x: x[0]) min_dist = spectrum[0][0] spectrum = [ s[1] for s in spectrum if s[0] - min_dist < symmetry_tol ] + [vert] spectrum_atoms = Atoms() for s in spectrum: spectrum_atoms.append(Atom('H', s)) spectrum_atoms.set_cell(repeat_unit_cell.T) spectrum_struct = AseAtomsAdaptor.get_structure(spectrum_atoms) sga = SpacegroupAnalyzer(spectrum_struct, symprec=symmetry_tol) symmetry_order = len(sga.get_space_group_operations()) symmetry_type = 'PG' + str(symmetry_order) vtypes.append((symmetry_type, vert)) if len(custom_sites) > 0: for custom_site in custom_sites: vtypes.append((csite_name, custom_site)) if site in ('all', 'single_site'): max_loading = float(len(vtypes)) else: max_loading = float(len([ty for ty in vtypes if ty[0] == site])) all_combinations = [s for s in itertools.combinations(vtypes, loading)] max_sgn = 0 if site == 'single_site': site_dict = dict((k, []) for k in set(ty[0] for ty in vtypes)) for entry in vtypes: ty, coord = entry site_dict[ty].append(coord) elif site == 'all': site_dict = {'all': []} for entry in vtypes: site_dict['all'].append(entry) else: site_dict = {site: []} for entry in vtypes: ty, coord = entry if ty == site: site_dict[ty].append(coord) ads_dict = {} fingerprints = dict((len(s), dict((k, []) for k in range(1, 231))) for s in all_combinations) sg_counts = dict((k, 0) for k in range(1, 231)) for subset in all_combinations: types = [s[0] for s in subset] type_string = '' for ty in set(types): type_string += ty + '_' if len(set(types)) > 1 and site == 'single_site': continue elif site not in ('all', 'single_site'): if len(set(types)) > 1: continue else: if list(set(types))[0] != site: continue ld = len(subset) fp_dict = fingerprints[ld] fractional_loading = ld / max_loading adsorbate_combinations = itertools.product(adsorbate, repeat=ld) for ads_comb in adsorbate_combinations: adsonly_positions = [] bulk_coords = [(sl.symbol, sl.position) for sl in self.bulk_ase] for s, a in zip(subset, ads_comb): ads, shift_ind = a ty, site_coord = s elems = [] positions = [] for atom in ads: elems.append(atom.symbol) positions.append(atom.position) elems = np.asarray(elems) positions = np.asarray(positions) positions -= positions[shift_ind] positions += site_coord for e, c in zip(elems, positions): bulk_coords.append((e, c)) adsonly_positions.append(c) advance, index, sgs, sgn, dists, atoms, formula = redundancy_check( bulk_coords, adsonly_positions, fp_dict, repeat_unit_cell, symmetry_tol) if sgn > max_sgn: max_sgn = sgn elif sgn < max_sgn: continue if advance: sg_counts[sgn] += 1 fp_dict[sgn].append(dists) ads_dict[formula + '_' + str(int(fractional_loading * 1000)) + '_' + type_string + str(sgn) + '_' + index] = atoms self.adsorbate_configuration_dict = ads_dict
def find_unique_structures(base_crystal_path, directory): """ DESCRIPTION: Given a base crystal structure along with a directory containing CIF structures, determine how many unique configurations are in the directory, and assign a configuration ID number to each CIF structure in the directory. Results can be seen in the outputted unique.csv file. The function uses the base crystal structure provided to find the space group and associated symmetry operations, which are then used to see whether the CIF structures in the directory are equivalent. PARAMETERS: base_crystal_path: string The path to the base crystal CIF structure. Note that this structure should contain the space group that you want to check all other structures to. If the symmetry of this structure is incorrect, you will get unexpected results. directory: string The path to the directory containing the CIF structures to compare. Note that this function assumes that the CIF names are all prefixed by a dash and followed by an interger, ie "LiCoO2-1", "LiCoO2-2", "LiCoO2-3"..., ect. RETURNS: None """ # convert all files (they should all be cif files) in the directory into Structures and place them into a list cif_file_names = [ os.path.splitext(os.path.basename(path))[0] for path in os.listdir(directory) if path.endswith('.cif') ] cif_files = [ directory + path for path in os.listdir(directory) if path.endswith('.cif') ] cif_files = sorted(cif_files, key=lambda x: int(x.split("-")[-1].replace(".cif", ""))) cif_file_names = sorted(cif_file_names, key=lambda x: int(x.split("-")[-1])) structures = [IStructure.from_file(path) for path in cif_files] print(cif_file_names) # Get the base crystal structure base_crystal = IStructure.from_file(base_crystal_path) # Get the space group of the base crystal analyzer = SpacegroupAnalyzer(base_crystal) spacegroup = analyzer.get_space_group_operations() print(spacegroup) id = 1 num_structures = len(structures) config_dict = {} unique_structs = [] for i in range(num_structures): config1 = structures[i] unique_cif_name = cif_file_names[i] if not unique_cif_name in config_dict: config_dict[unique_cif_name] = id print("%s Unique Structures Found..." % (id)) unique_structs.append(config1) id += 1 for j in range(num_structures): cif_name = cif_file_names[j] config2 = structures[j] if not cif_name in config_dict: isEquivalent = spacegroup.are_symmetrically_equivalent( config1, config2) if isEquivalent: config_dict[cif_name] = config_dict[unique_cif_name] print("----------------------------------------------") print("%s Total Unique Structures Found" % (id - 1)) print("----------------------------------------------") with open('unique.csv', 'w') as f: for key in config_dict.keys(): f.write("%s,%s\n" % (key, config_dict[key])) crystal_name = os.path.splitext(os.path.basename(base_crystal_path))[0] directory_path = "{}-unique".format(crystal_name) if not os.path.isdir(directory_path): os.mkdir(directory_path) num_unique_structures = len(unique_structs) for i in range(num_unique_structures): config = unique_structs[i] #Save to CIF filename = crystal_name + '_ewald' + '_{}'.format(i + 1) w = CifWriter(config) w.write_file(directory_path + '/' + filename + '.cif') # print("Cif file saved to {}.cif".format(filename)) #Save to POSCAR poscar = Poscar(config) poscar.write_file(directory_path + '/' + filename)
def get_interstitial_sites(structure, octahedra=False, unique=False): """ Use a Delaunay triangulation of all atomic sites in the crystal structure to define tetrahedra of open volumes (interstitial sites). Each interstitial site is ranked according to the maximum radius of an atom that could fit in that site without overlapping one of the existing neighboring atoms' radii. The default behavior is to stop there, but by setting `octahedra` to True, the tetrahedra which share faces are combined to form bipyramids (hexahedra) and then points are added to these bipyramids to formoctahedra, in order to identify the largest 5- and 6-fold coordinated sites as well. This takes a little longer since it requires combining tetrahedra. Args: structure (Structure): Pymatgen Structure object octahedra (Boolean): Whether or not to search also for octahedral interstitial sites. unique (Boolean): Whether or not to enforce that only symmetrically inequivalent sites are returned. Determining the symmetry-equivalence is usually by far the slowest task in the algorithm. Returns: interstitials (dict): dictionary of the form {"tetrahedral": [(coordinates, max_radius), ...], "hexahedral": [(coordinates, max_radius), ...], "octahedral": [(coordinates, max_radius), ...]} storing lists of each interstitial site for both coordination types, sorted by largest radius first. Coordinates are given as cartesian. """ # Preserve the original structure st = structure.copy() # Small unit cells make the triangulation unreliable n_sites = structure.num_sites if n_sites < 4: st.make_supercell(3) m_0 = st.lattice._matrix # Make a 3x3x3 supercell so that the center unit cell # is surrounded by its images- i.e. it has no "boundaries", # which can erroneously create tetrahedra of infinite volumes. st.make_supercell(3) m = st.lattice._matrix # These are the vertices of only the center cell cell_vertices = np.array([ np.add(np.add(m[0] / 3., m[1] / 3.), m[2] / 3.), np.add(np.add(m[0] / 1.5, m[1] / 3.), m[2] / 3.), np.add(np.add(m[0] / 3., m[1] / 1.5), m[2] / 3.), np.add(np.add(m[0] / 1.5, m[1] / 1.5), m[2] / 3.), np.add(np.add(m[0] / 3., m[1] / 3.), m[2] / 1.5), np.add(np.add(m[0] / 1.5, m[1] / 3.), m[2] / 1.5), np.add(np.add(m[0] / 3., m[1] / 1.5), m[2] / 1.5), np.add(np.add(m[0] / 1.5, m[1] / 1.5), m[2] / 1.5) ]) cell_center = np.mean(cell_vertices, axis=0) other_cell_centers = [] for i in range(-1, 2): for j in range(-1, 2): for k in range(-1, 2): c = np.add(cell_center, np.multiply(i, m_0[0])) c = np.add(c, np.multiply(j, m_0[1])) c = np.add(c, np.multiply(k, m_0[2])) other_cell_centers.append(c) max_distance_in_cell = sq_dist(cell_vertices[0], cell_center) points = [s.coords for s in st.sites] radii = [float(s.specie.atomic_radius) for s in st.sites] # Create the initial Delaunay triangulation of all sites in the # supercell. delaunay = Delaunay(points) all_simplices = delaunay.simplices.copy() # Now filter those Delaunay simplices to only those with # at least one vertex lying within the center unit cell. simplices = [] center_cell = ConvexHull(cell_vertices) if not octahedra: for simplex in all_simplices: for vertex in simplex: if sq_dist(cell_center, points[vertex]) <= max_distance_in_cell\ and sq_dist(cell_center, points[vertex]) ==\ min([sq_dist(points[vertex], pt) for pt in other_cell_centers]): simplices.append(simplex) break else: for simplex in all_simplices: n = 0 for vertex in simplex: if sq_dist(cell_center, points[vertex]) <= max_distance_in_cell\ and sq_dist(cell_center, points[vertex]) ==\ min([sq_dist(points[vertex], pt) for pt in other_cell_centers]): n += 1 if n == 4: simplices.append(simplex) # Calculate the maximum interstitial # radius for all the relevant tetrahedra. tetrahedra = [] for simplex in simplices: a = points[simplex[0]] r_a = radii[simplex[0]] b = points[simplex[1]] r_b = radii[simplex[1]] c = points[simplex[2]] r_c = radii[simplex[2]] d = points[simplex[3]] r_d = radii[simplex[3]] centroid = np.mean([a, b, c, d], axis=0) # Add the atomic radii to the nuclei loactions to find # their "true" extrema, then use these to find the # "true" centroid. move = 1 while move > 0.01: true_a = pt_btwn(a, centroid, r_a) true_b = pt_btwn(b, centroid, r_b) true_c = pt_btwn(c, centroid, r_c) true_d = pt_btwn(d, centroid, r_d) true_centroid = np.mean([true_a, true_b, true_c, true_d], axis=0) move = sq_dist(true_centroid, centroid) centroid = true_centroid max_radius = sqrt( min([ sq_dist(true_centroid, pt) for pt in [true_a, true_b, true_c, true_d] ])) tetrahedra.append((true_centroid, [tuple(x) for x in [a, b, c, d]], [r_a, r_b, r_c, r_d], 4, max_radius)) interstitials = {"tetrahedral": []} if octahedra: tet_pts = [i[1] for i in tetrahedra] tet_pts = list(set([coords for pt in tet_pts for coords in pt])) interstitials.update({"hexahedral": [], "octahedral": []}) for i in range(len(tetrahedra)): for j in range(i, len(tetrahedra)): # If 3 vertices are shared then the tetrahedra # share a face and form a bipyramid. shared = list(set(tetrahedra[i][1]) & set(tetrahedra[j][1])) if len(shared) == 3: # Vertices of the bipyramid a = tetrahedra[i][1][0] r_a = tetrahedra[i][2][0] b = tetrahedra[i][1][1] r_b = tetrahedra[i][2][1] c = tetrahedra[i][1][2] r_c = tetrahedra[i][2][2] d = tetrahedra[i][1][3] r_d = tetrahedra[i][2][3] # Fifth point to define trigonal bipyramid e, r_e = [(s, tetrahedra[j][2][k]) for k, s in enumerate(tetrahedra[j][1]) if s not in tetrahedra[i][1]][0] h_centroid = np.mean([a, b, c, d, e], axis=0) move = 1 while move > 0.01: true_a = pt_btwn(a, h_centroid, r_a) true_b = pt_btwn(b, h_centroid, r_b) true_c = pt_btwn(c, h_centroid, r_c) true_d = pt_btwn(d, h_centroid, r_d) true_e = pt_btwn(e, h_centroid, r_e) true_h_centroid = np.mean( [true_a, true_b, true_c, true_d, true_e], axis=0) move = sq_dist(true_h_centroid, h_centroid) h_centroid = true_h_centroid r_h = sqrt( min([ sq_dist(true_h_centroid, pt) for pt in [true_a, true_b, true_c, true_d, true_e] ])) # Add the bipyramid to the final list # of interstitials. interstitials["hexahedral"].append( (tuple(h_centroid), r_h)) # Enlarge the bipyramid by one point to create # octahedra. v1 = np.subtract(shared[0], shared[1]) v2 = np.subtract(shared[0], shared[2]) tol = max([ sq_dist(shared[0], shared[1]), sq_dist(shared[0], shared[2]), sq_dist(shared[1], shared[2]) ]) * 1.1 for index, f in enumerate(tet_pts): v3 = np.subtract(shared[0], f) distances = [sq_dist(f, p) for p in shared] distances.sort() if 0 < distances[0] < tol and 0 < distances[1] < tol\ and np.dot(v3, (np.cross(v1, v2))) == 0: r_f = radii[index] o_centroid = np.mean([a, b, c, d, e, f], axis=0) move = 1 while move > 0.01: true_a = pt_btwn(a, o_centroid, r_a) true_b = pt_btwn(b, o_centroid, r_b) true_c = pt_btwn(c, o_centroid, r_c) true_d = pt_btwn(d, o_centroid, r_d) true_e = pt_btwn(e, o_centroid, r_e) true_f = pt_btwn(f, o_centroid, r_f) true_o_centroid = np.mean([ true_a, true_b, true_c, true_d, true_e, true_f ], axis=0) move = sq_dist(true_o_centroid, o_centroid) o_centroid = true_o_centroid r_o = sqrt( min([ sq_dist(true_o_centroid, pt) for pt in [ true_a, true_b, true_c, true_d, true_e, true_f ] ])) # Add the octahedron to the final # list of interstitials. interstitials["octahedral"].append( (tuple(o_centroid), r_o)) interstitials["hexahedral"] = list(set(interstitials["hexahedral"])) interstitials["octahedral"] = list(set(interstitials["octahedral"])) interstitials["tetrahedral"] = [(i[0], i[4]) for i in tetrahedra] # Since the centroid coordinates were given in the center # cell of the supercell, bring them back into the original # unit cell. if n_sites < 4: f = 1. / 3. else: f = 1. for c in interstitials: for i in range(len(interstitials[c])): for r in m_0: interstitials[c][i] = (np.multiply( np.subtract(np.array(interstitials[c][i][0]), r), f), interstitials[c][i][1]) # Sort by the maximum radii for c in interstitials: interstitials[c].sort(key=operator.itemgetter(1)) interstitials[c].reverse() if unique: sga = SpacegroupAnalyzer(structure) sop = sga.get_space_group_operations() l = structure.lattice for c in interstitials: remove = [] for i in range(len(interstitials[c])): if i not in remove: site_i = PeriodicSite("C", interstitials[c][i][0], l) for j in range(i + 1, len(interstitials[c])): if interstitials[c][i][1] == interstitials[c][j][1] and\ sop.are_symmetrically_equivalent( [site_i], [PeriodicSite("C",interstitials[c][j][0],l)] ): remove.append(j) interstitials[c] = [ interstitials[c][x] for x in range(len(interstitials[c])) if x not in remove ] return interstitials
def __init__( self, uuid, structure, # pristine unit cell sc_size="1 1 1", signicant_figures=4, muon_threshold=1e-2, if_pristine=False, if_with_distortions=True # reduced_sites = False, # reduced_distance = 10.0 ): """ Parameters ----------- uuid : int The PK for relax structure structure : Structure object Pymatgen Structure Object sc_size : str Supercell size in a, b and c direction eg "3 3 3" signicant_figures : int Round positios to significant figures. Default=4 muon_threshold : float Absolute threshold for checking distance. if_pristine : bool If True, to generate symmetry pristine structure for muon equivalent sites. Default=False if_with_distortions : bool If True, to generate structure with distortions for each muon equivalent sites. Default=True Returns ------- """ # """ # reduced_sites: Bool # If True, (planning for something). # Default=False # reduced_distance : float # planning for something """ # self.relax_uuid = uuid self.structure = structure self.sc_size = sc_size self.signicant_figures = signicant_figures self.muon_threshold = muon_threshold self.if_pristine = if_pristine self.if_with_distortions = if_with_distortions # This parameters for future # self.reduced_sites = reduced_sites # self.reduced_distance = reduced_distance if sc_size is None or sc_size == "": self.sc_size = "1 1 1" else: self.sc_size = sc_size self.pristine_structure_supercell = self.structure.copy() self.pristine_structure_supercell.make_supercell( [int(x) for x in self.sc_size.split()]) # Check if pritine structure is compatible to supercell size if len(self.pristine_structure_supercell) != self.n_atoms - 1: raise SiteDistortions('Invalid inputs Structure or supercell size') SA = SpacegroupAnalyzer(self.pristine_structure_supercell, symprec=self.muon_threshold) self._SA = SA self._SG = SA.get_space_group_operations() self._PG = SA.get_point_group_operations() self._SGO = SpacegroupOperations( SA.get_space_group_number(), SA.get_space_group_symbol(), SA.get_symmetry_operations(cartesian=False))
def get_num_sym_ops(ent): sga = SpacegroupAnalyzer(ent.structure) return len(sga.get_space_group_operations())