def test_chemicalSRO(self): df_sc = pd.DataFrame({'struct': [self.sc], 'site': [0]}) df_cscl = pd.DataFrame({'struct': [self.cscl], 'site': [0]}) vnn = ChemicalSRO.from_preset("VoronoiNN", cutoff=6.0) vnn.fit(df_sc[['struct', 'site']]) vnn_csros = vnn.featurize_dataframe(df_sc, ['struct', 'site']) self.assertAlmostEqual(vnn_csros['CSRO_Al_VoronoiNN'][0], 0.0) vnn = ChemicalSRO(VoronoiNN(), includes="Cs") vnn.fit(df_cscl[['struct', 'site']]) vnn_csros = vnn.featurize_dataframe(df_cscl, ['struct', 'site']) self.assertAlmostEqual(vnn_csros['CSRO_Cs_VoronoiNN'][0], 0.0714285714) vnn = ChemicalSRO(VoronoiNN(), excludes="Cs") vnn.fit(df_cscl[['struct', 'site']]) vnn_csros = vnn.featurize_dataframe(df_cscl, ['struct', 'site']) self.assertAlmostEqual(vnn_csros['CSRO_Cl_VoronoiNN'][0], -0.0714285714) jmnn = ChemicalSRO.from_preset("JMolNN", el_radius_updates={"Al": 1.55}) jmnn.fit(df_sc[['struct', 'site']]) jmnn_csros = jmnn.featurize_dataframe(df_sc, ['struct', 'site']) self.assertAlmostEqual(jmnn_csros['CSRO_Al_JMolNN'][0], 0.0) jmnn = ChemicalSRO.from_preset("JMolNN") jmnn.fit(df_cscl[['struct', 'site']]) jmnn_csros = jmnn.featurize_dataframe(df_cscl, ['struct', 'site']) self.assertAlmostEqual(jmnn_csros['CSRO_Cs_JMolNN'][0], -0.5) self.assertAlmostEqual(jmnn_csros['CSRO_Cl_JMolNN'][0], -0.5) mdnn = ChemicalSRO.from_preset("MinimumDistanceNN") mdnn.fit(df_cscl[['struct', 'site']]) mdnn_csros = mdnn.featurize_dataframe(df_cscl, ['struct', 'site']) self.assertAlmostEqual(mdnn_csros['CSRO_Cs_MinimumDistanceNN'][0], 0.5) self.assertAlmostEqual(mdnn_csros['CSRO_Cl_MinimumDistanceNN'][0], -0.5) monn = ChemicalSRO.from_preset("MinimumOKeeffeNN") monn.fit(df_cscl[['struct', 'site']]) monn_csros = monn.featurize_dataframe(df_cscl, ['struct', 'site']) self.assertAlmostEqual(monn_csros['CSRO_Cs_MinimumOKeeffeNN'][0], 0.5) self.assertAlmostEqual(monn_csros['CSRO_Cl_MinimumOKeeffeNN'][0], -0.5) mvnn = ChemicalSRO.from_preset("MinimumVIRENN") mvnn.fit(df_cscl[['struct', 'site']]) mvnn_csros = mvnn.featurize_dataframe(df_cscl, ['struct', 'site']) self.assertAlmostEqual(mvnn_csros['CSRO_Cs_MinimumVIRENN'][0], 0.5) self.assertAlmostEqual(mvnn_csros['CSRO_Cl_MinimumVIRENN'][0], -0.5) # test fit + transform vnn = ChemicalSRO.from_preset("VoronoiNN") vnn.fit(df_cscl[['struct', 'site']]) # dataframe vnn_csros = vnn.transform(df_cscl[['struct', 'site']].values) self.assertAlmostEqual(vnn_csros[0][0], 0.071428571428571286) self.assertAlmostEqual(vnn_csros[0][1], -0.071428571428571286) vnn = ChemicalSRO.from_preset("VoronoiNN") vnn.fit(df_cscl[['struct', 'site']].values) # np.array vnn_csros = vnn.transform(df_cscl[['struct', 'site']].values) self.assertAlmostEqual(vnn_csros[0][0], 0.071428571428571286) self.assertAlmostEqual(vnn_csros[0][1], -0.071428571428571286) vnn = ChemicalSRO.from_preset("VoronoiNN") vnn.fit([[self.cscl, 0]]) # list vnn_csros = vnn.transform([[self.cscl, 0]]) self.assertAlmostEqual(vnn_csros[0][0], 0.071428571428571286) self.assertAlmostEqual(vnn_csros[0][1], -0.071428571428571286) # test fit_transform vnn = ChemicalSRO.from_preset("VoronoiNN") vnn_csros = vnn.fit_transform(df_cscl[['struct', 'site']].values) self.assertAlmostEqual(vnn_csros[0][0], 0.071428571428571286) self.assertAlmostEqual(vnn_csros[0][1], -0.071428571428571286)
def get_adj_matrix(structure, tol=0, cutoff=10): n_atoms = len(structure) adj_matrix_no_weight = np.zeros((n_atoms, n_atoms)) adj_matrix_multi_edge = np.zeros((n_atoms, n_atoms)) adj_matrix_sol_angle = np.zeros((n_atoms, n_atoms)) # compute adjacency matrix voro = VoronoiNN(tol=tol, cutoff=cutoff, allow_pathological=True, extra_nn_info=False, compute_adj_neighbors=False) all_nn_info = voro.get_all_nn_info(structure) # we force a graph not to have self-loops for center_index in range(n_atoms): for nn_index in range(len(all_nn_info[center_index])): nn_site_index = all_nn_info[center_index][nn_index]['site_index'] if nn_site_index > center_index: # weight_type: None adj_matrix_no_weight[center_index, nn_site_index] = 1 # weight_type: 'multi_edge' adj_matrix_multi_edge[center_index, nn_site_index] += 1 # weight_type: 'sol_angle': sol_angle = all_nn_info[center_index][nn_index]['weight'] adj_matrix_sol_angle[center_index, nn_site_index] += sol_angle else: adj_matrix_no_weight[center_index, nn_site_index] = \ adj_matrix_no_weight[nn_site_index, center_index] adj_matrix_multi_edge[center_index, nn_site_index] = \ adj_matrix_multi_edge[nn_site_index, center_index] adj_matrix_sol_angle[center_index, nn_site_index] = \ adj_matrix_sol_angle[nn_site_index, center_index] return adj_matrix_no_weight, adj_matrix_multi_edge, adj_matrix_sol_angle
def average_coordination_number(structures, freq=10): """ Calculates the ensemble averaged Voronoi coordination numbers of a list of Structures using VoronoiNN. Typically used for analyzing the output of a Molecular Dynamics run. Args: structures (list): list of Structures. freq (int): sampling frequency of coordination number [every freq steps]. Returns: Dictionary of elements as keys and average coordination numbers as values. """ coordination_numbers = {} for spec in structures[0].composition.as_dict().keys(): coordination_numbers[spec] = 0.0 count = 0 for t in range(len(structures)): if t % freq != 0: continue count += 1 vnn = VoronoiNN() for atom in range(len(structures[0])): cn = vnn.get_cn(structures[t], atom, use_weights=True) coordination_numbers[structures[t][atom].species_string] += cn elements = structures[0].composition.as_dict() for el in coordination_numbers: coordination_numbers[el] = coordination_numbers[el] / elements[ el] / count return coordination_numbers
def calculate_coordination_of_bulk_atoms(self, bulk_atoms): ''' Finds all unique atoms in a bulk structure and then determines their coordination number. Then parses these coordination numbers into a dictionary whose keys are the elements of the atoms and whose values are their possible coordination numbers. For example: `bulk_cns = {'Pt': {3., 12.}, 'Pd': {12.}}` Arg: bulk_atoms An `ase.Atoms` object of the bulk structure. Returns: bulk_cn_dict A defaultdict whose keys are the elements within `bulk_atoms` and whose values are a set of integers of the coordination numbers of that element. ''' voronoi_nn = VoronoiNN(tol=0.1) # 0.1 chosen for better detection # Object type conversion so we can use Voronoi bulk_struct = AseAtomsAdaptor.get_structure(bulk_atoms) sga = SpacegroupAnalyzer(bulk_struct) sym_struct = sga.get_symmetrized_structure() # We'll only loop over the symmetrically distinct sites for speed's sake bulk_cn_dict = defaultdict(set) for idx in sym_struct.equivalent_indices: site = sym_struct[idx[0]] cn = voronoi_nn.get_cn(sym_struct, idx[0], use_weights=True) cn = round(cn, 5) bulk_cn_dict[site.species_string].add(cn) return bulk_cn_dict
def test_all_nn_classes(self): self.assertEqual(MinimumDistanceNN(cutoff=5, get_all_sites=True).get_cn( self.cscl, 0), 14) self.assertEqual(MinimumDistanceNN().get_cn(self.diamond, 0), 4) self.assertEqual(MinimumDistanceNN().get_cn(self.nacl, 0), 6) self.assertEqual(MinimumDistanceNN().get_cn(self.lifepo4, 0), 6) self.assertEqual(MinimumDistanceNN(tol=0.01).get_cn(self.cscl, 0), 8) self.assertEqual(MinimumDistanceNN(tol=0.1).get_cn(self.mos2, 0), 6) for image in MinimumDistanceNN(tol=0.1).get_nn_images(self.mos2, 0): self.assertTrue(image in [(0, 0, 0), (0, 1, 0), (-1, 0, 0), (0, 0, 0), (0, 1, 0), (-1, 0, 0)]) okeeffe = MinimumOKeeffeNN(tol=0.01) self.assertEqual(okeeffe.get_cn(self.diamond, 0), 4) self.assertEqual(okeeffe.get_cn(self.nacl, 0), 6) self.assertEqual(okeeffe.get_cn(self.cscl, 0), 8) self.assertEqual(okeeffe.get_cn(self.lifepo4, 0), 2) virenn = MinimumVIRENN(tol=0.01) self.assertEqual(virenn.get_cn(self.diamond, 0), 4) self.assertEqual(virenn.get_cn(self.nacl, 0), 6) self.assertEqual(virenn.get_cn(self.cscl, 0), 8) self.assertEqual(virenn.get_cn(self.lifepo4, 0), 2) brunner_recip = BrunnerNN_reciprocal(tol=0.01) self.assertEqual(brunner_recip.get_cn(self.diamond, 0), 4) self.assertEqual(brunner_recip.get_cn(self.nacl, 0), 6) self.assertEqual(brunner_recip.get_cn(self.cscl, 0), 14) self.assertEqual(brunner_recip.get_cn(self.lifepo4, 0), 6) brunner_rel = BrunnerNN_relative(tol=0.01) self.assertEqual(brunner_rel.get_cn(self.diamond, 0), 4) self.assertEqual(brunner_rel.get_cn(self.nacl, 0), 6) self.assertEqual(brunner_rel.get_cn(self.cscl, 0), 14) self.assertEqual(brunner_rel.get_cn(self.lifepo4, 0), 6) brunner_real = BrunnerNN_real(tol=0.01) self.assertEqual(brunner_real.get_cn(self.diamond, 0), 4) self.assertEqual(brunner_real.get_cn(self.nacl, 0), 6) self.assertEqual(brunner_real.get_cn(self.cscl, 0), 14) self.assertEqual(brunner_real.get_cn(self.lifepo4, 0), 30) econn = EconNN() self.assertEqual(econn.get_cn(self.diamond, 0), 4) self.assertEqual(econn.get_cn(self.nacl, 0), 6) self.assertEqual(econn.get_cn(self.cscl, 0), 14) self.assertEqual(econn.get_cn(self.lifepo4, 0), 6) voroinn = VoronoiNN(tol=0.5) self.assertEqual(voroinn.get_cn(self.diamond, 0), 4) self.assertEqual(voroinn.get_cn(self.nacl, 0), 6) self.assertEqual(voroinn.get_cn(self.cscl, 0), 8) self.assertEqual(voroinn.get_cn(self.lifepo4, 0), 6) crystalnn = CrystalNN() self.assertEqual(crystalnn.get_cn(self.diamond, 0), 4) self.assertEqual(crystalnn.get_cn(self.nacl, 0), 6) self.assertEqual(crystalnn.get_cn(self.cscl, 0), 8) self.assertEqual(crystalnn.get_cn(self.lifepo4, 0), 6)
def average_coordination_number(structures, freq=10): """ Calculates the ensemble averaged Voronoi coordination numbers of a list of Structures using VoronoiNN. Typically used for analyzing the output of a Molecular Dynamics run. Args: structures (list): list of Structures. freq (int): sampling frequency of coordination number [every freq steps]. Returns: Dictionary of elements as keys and average coordination numbers as values. """ coordination_numbers = {} for spec in structures[0].composition.as_dict().keys(): coordination_numbers[spec] = 0.0 count = 0 for t in range(len(structures)): if t % freq != 0: continue count += 1 vnn = VoronoiNN() for atom in range(len(structures[0])): cn = vnn.get_cn(structures[t], atom, use_weights=True) coordination_numbers[structures[t][atom].species_string] += cn elements = structures[0].composition.as_dict() for el in coordination_numbers: coordination_numbers[ el] = coordination_numbers[el] / elements[el] / count return coordination_numbers
def _count_voronoinn(s1, mess='face_dist'): """ :type s1: pymatgen structre type """ vo = VoronoiNN(tol=0.01, allow_pathological=False, weight='solid_angle', extra_nn_info=True, compute_adj_neighbors=True) vo_data = vo.get_all_nn_info(s1) site_ele = [i.specie.name for i in s1.sites] nei_all_ele = _count_i(vo_data, site_ele, mess=mess) a_dict = {} for i in s1.types_of_specie: a_list = [] for j in nei_all_ele: if j[0] == i.name: a_list.extend(j[1]) a_list = np.array(a_list) a_dict[i] = a_list result_0 = a_dict[s1.types_of_specie[0]] number = [] for ele in s1.types_of_specie: b_index = np.where(result_0[:, 0] == ele.name)[0] int_ = result_0[b_index, 1:].astype(float) number.append(np.sum(int_) / s1.num_sites) return number
def find_bulk_cn_dict(bulk_atoms): ''' Get a dictionary of coordination numbers for each distinct site in the bulk structure. Taken from pymatgen.core.surface Class Slab `get_surface_sites`. https://pymatgen.org/pymatgen.core.surface.html ''' struct = AseAtomsAdaptor.get_structure(bulk_atoms) sga = SpacegroupAnalyzer(struct) sym_struct = sga.get_symmetrized_structure() unique_indices = [equ[0] for equ in sym_struct.equivalent_indices] # Get a dictionary of unique coordination numbers # for atoms in each structure. # for example, Pt[1,1,1] would have cn=3 and cn=12 # depends on the Pt atom. voronoi_nn = VoronoiNN() cn_dict = {} for idx in unique_indices: elem = sym_struct[idx].species_string if elem not in cn_dict.keys(): cn_dict[elem] = [] cn = voronoi_nn.get_cn(sym_struct, idx, use_weights=True) cn = float('%.5f' % (round(cn, 5))) if cn not in cn_dict[elem]: cn_dict[elem].append(cn) return cn_dict
def find_adsorption_vector(bulk_cn_dict, slab_atoms, surface_indices, adsorption_site): """ Returns the vector of an adsorption site representing the furthest distance from the neighboring atoms. The vector is a (1,3) numpy array. The idea comes from CatKit. https://catkit.readthedocs.io/en/latest/?badge=latest Arg: bulk_cn_dict A dictionary of coordination numbers for each distinct site in the respective bulk structure slab_atoms The `ase.Atoms` format of a supercell slab. surface_indices The index of the surface atoms in a list. adsorption_site A `numpy.ndarray` object that contains the x-y-z coordinates of the adsorptions sites. Output: vector numpy.ndarray. Adsorption vector for an adsorption site. """ vnn = VoronoiNN(allow_pathological=True) slab_atoms += Atoms('U', [adsorption_site]) U_index = slab_atoms.get_chemical_symbols().index('U') struct_with_U = AseAtomsAdaptor.get_structure(slab_atoms) nn_info = vnn.get_nn_info(struct_with_U, n=U_index) nn_indices = [neighbor['site_index'] for neighbor in nn_info] surface_nn_indices = [idx for idx in nn_indices if idx in surface_indices] # get the index of the closest 4 atom to the site to form a plane # chose 4 because it will gaurantee a more accurate plane for edge cases nn_dists_from_U = { idx: np.linalg.norm(slab_atoms[idx].position - slab_atoms[U_index].position) for idx in surface_nn_indices } sorted_dists = { idx: distance for idx, distance in sorted(nn_dists_from_U.items(), key=lambda item: item[1]) } closest_4_nn_indices = np.array(list(sorted_dists.keys())[:4], dtype=int) plane_coords = struct_with_U.cart_coords[closest_4_nn_indices] vector = _plane_normal(plane_coords) # Check to see if the vector is reasonable. # set an arbitay threshold where the vector and [0, 0, 1] # should be less than 60 degrees. # If someone has a better way to detect in the future, go for it if _ang_between_vectors(np.array([0., 0., 1.]), vector) > 60.: message = ('Warning: this might be an edge case where the ' 'adsorption vector is not appropriate.' ' We will place adsorbates using default [0, 0, 1] vector.') warnings.warn(message) vector = np.array([0., 0., 1.]) del slab_atoms[[U_index]] return vector
def test_all_nn_classes(self): self.assertAlmostEqual(MinimumDistanceNN().get_cn(self.diamond, 0), 4) self.assertAlmostEqual(MinimumDistanceNN().get_cn(self.nacl, 0), 6) self.assertAlmostEqual( MinimumDistanceNN(tol=0.01).get_cn(self.cscl, 0), 8) self.assertAlmostEqual( MinimumDistanceNN(tol=0.1).get_cn(self.mos2, 0), 6) for image in MinimumDistanceNN(tol=0.1).get_nn_images(self.mos2, 0): self.assertTrue(image in [(0, 0, 0), (0, 1, 0), (-1, 0, 0), (0, 0, 0), (0, 1, 0), (-1, 0, 0)]) self.assertAlmostEqual( MinimumOKeeffeNN(tol=0.01).get_cn(self.diamond, 0), 4) self.assertAlmostEqual( MinimumOKeeffeNN(tol=0.01).get_cn(self.nacl, 0), 6) self.assertAlmostEqual( MinimumOKeeffeNN(tol=0.01).get_cn(self.cscl, 0), 8) self.assertAlmostEqual( MinimumVIRENN(tol=0.01).get_cn(self.diamond, 0), 4) self.assertAlmostEqual(MinimumVIRENN(tol=0.01).get_cn(self.nacl, 0), 6) self.assertAlmostEqual(MinimumVIRENN(tol=0.01).get_cn(self.cscl, 0), 8) self.assertAlmostEqual( BrunnerNN_reciprocal(tol=0.01).get_cn(self.diamond, 0), 4) self.assertAlmostEqual( BrunnerNN_reciprocal(tol=0.01).get_cn(self.nacl, 0), 6) self.assertAlmostEqual( BrunnerNN_reciprocal(tol=0.01).get_cn(self.cscl, 0), 14) self.assertAlmostEqual( BrunnerNN_relative(tol=0.01).get_cn(self.diamond, 0), 16) self.assertAlmostEqual( BrunnerNN_relative(tol=0.01).get_cn(self.nacl, 0), 18) self.assertAlmostEqual( BrunnerNN_relative(tol=0.01).get_cn(self.cscl, 0), 8) self.assertAlmostEqual( BrunnerNN_real(tol=0.01).get_cn(self.diamond, 0), 16) self.assertAlmostEqual( BrunnerNN_real(tol=0.01).get_cn(self.nacl, 0), 18) self.assertAlmostEqual( BrunnerNN_real(tol=0.01).get_cn(self.cscl, 0), 8) self.assertAlmostEqual(EconNN(tol=0.01).get_cn(self.diamond, 0), 4) self.assertAlmostEqual(EconNN(tol=0.01).get_cn(self.nacl, 0), 6) self.assertAlmostEqual(EconNN(tol=0.01).get_cn(self.cscl, 0), 14) self.assertAlmostEqual(VoronoiNN(tol=0.5).get_cn(self.diamond, 0), 4) self.assertAlmostEqual(VoronoiNN(tol=0.5).get_cn(self.nacl, 0), 6) self.assertAlmostEqual(VoronoiNN(tol=0.5).get_cn(self.cscl, 0), 8)
def _find_surface_atoms_with_voronoi(self, bulk_atoms, surface_atoms): ''' Labels atoms as surface or bulk atoms according to their coordination relative to their bulk structure. If an atom's coordination is less than it normally is in a bulk, then we consider it a surface atom. We calculate the coordination using pymatgen's Voronoi algorithms. Note that if a single element has different sites within a bulk and these sites have different coordinations, then we consider slab atoms "under-coordinated" only if they are less coordinated than the most under undercoordinated bulk atom. For example: Say we have a bulk with two Cu sites. One site has a coordination of 12 and another a coordination of 9. If a slab atom has a coordination of 10, we will consider it a bulk atom. Args: bulk_atoms `ase.Atoms` of the bulk structure the surface was cut from. surface_atoms `ase.Atoms` of the surface Returns: tags A list of 0's and 1's whose indices align with the atoms in `surface_atoms`. 0's indicate a bulk atom and 1 indicates a surface atom. ''' # Initializations surface_struct = AseAtomsAdaptor.get_structure(surface_atoms) center_of_mass = self.calculate_center_of_mass(surface_struct) bulk_cn_dict = self.calculate_coordination_of_bulk_atoms(bulk_atoms) voronoi_nn = VoronoiNN(tol=0.1) # 0.1 chosen for better detection tags = [] for idx, site in enumerate(surface_struct): # Tag as surface atom only if it's above the center of mass if site.frac_coords[2] > center_of_mass[2]: try: # Tag as surface if atom is under-coordinated cn = voronoi_nn.get_cn(surface_struct, idx, use_weights=True) cn = round(cn, 5) if cn < min(bulk_cn_dict[site.species_string]): tags.append(1) else: tags.append(0) # Tag as surface if we get a pathological error except RuntimeError: tags.append(1) # Tag as bulk otherwise else: tags.append(0) return tags
def fxn(self, unit, base, start, final): material = unit['material'][1] #type: str elements = unit['elements'].copy() elements.pop(elements.index('O')) base_s = Poscar.from_dict(base['poscar']).structure start_s = Poscar.from_dict(start['poscar']).structure run_s = Poscar.from_dict( self.get_run(unit, base, start, final)['poscar']).structure # print(len(base_s)) # print(len(start_s)) start_i = get_coords(base_s.copy(), run_s.copy()) print(start_i) vnn = VoronoiNN(targets=[Element(x) for x in elements]) start_vnn = vnn.get_nn_info(base_s, start_i) weight = {} total_length = {} for pt in start_vnn: # print('{}: {:3.2f}'.format(pt['site_index'], pt['weight'])) temp_weight = round(pt['weight'] + 0.45) if pt['site'].species_string in weight: weight[pt['site'].species_string] = weight[ pt['site'].species_string] + temp_weight else: weight[pt['site'].species_string] = temp_weight if temp_weight >= 0.99: bond_length = base_s.get_distance(start_i, pt['site_index']) if pt['site'].species_string in total_length: total_length[pt['site'].species_string] = total_length[ pt['site'].species_string] + bond_length else: total_length[pt['site'].species_string] = bond_length if material.find(elements[0].lower()) == 0 and material.find( elements[1].lower()) == 0: raise Exception('Both Could be Element A') elif material.find(elements[0].lower()) == 0: bonds = { 'A_Bonds': weight[elements[0]], 'A_length': total_length[elements[0]] / weight[elements[0]], 'B_Bonds': weight[elements[1]], 'B_length': total_length[elements[1]] / weight[elements[1]], } elif material.find(elements[1].lower()) == 0: bonds = { 'A_Bonds': weight[elements[1]], 'A_length': total_length[elements[1]] / weight[elements[1]], 'B_Bonds': weight[elements[0]], 'B_length': total_length[elements[0]] / weight[elements[0]], } else: raise Exception('Couldn\'t find A') return bonds
def featurize(self, strc): # Get the Voronoi tessellation of each site voro = VoronoiNN() nns = [voro.get_voronoi_polyhedra(strc, i) for i in range(len(strc))] # Compute the radius of largest possible atom for each site # The largest radius is equal to the distance from the center of the # cell to the closest Voronoi face max_r = [min(x['face_dist'] for x in nn.values()) for nn in nns] # Compute the packing efficiency return [4. / 3. * np.pi * np.power(max_r, 3).sum() / strc.volume]
def get_ave_cn(self): coordination_numbers = {} vnn = VoronoiNN() for spec in self.poscar.structure.composition.as_dict().keys(): coordination_numbers[spec] = 0.0 for j, atom in enumerate(self.poscar.structure): cn = vnn.get_cn(self.poscar.structure, j, use_weights=True) coordination_numbers[atom.species_string] += cn elements = self.poscar.structure.composition.as_dict() for el in coordination_numbers: coordination_numbers[el] = coordination_numbers[el] / elements[el] return coordination_numbers
def test_cns(self): cnv = CoordinationNumber.from_preset('VoronoiNN') self.assertEqual(len(cnv.feature_labels()), 1) self.assertEqual(cnv.feature_labels()[0], 'CN_VoronoiNN') self.assertAlmostEqual(cnv.featurize(self.sc, 0)[0], 6) self.assertAlmostEqual(cnv.featurize(self.cscl, 0)[0], 14) self.assertAlmostEqual(cnv.featurize(self.cscl, 1)[0], 14) self.assertEqual(len(cnv.citations()), 2) cnv = CoordinationNumber(VoronoiNN(), use_weights='sum') self.assertEqual(cnv.feature_labels()[0], 'CN_VoronoiNN') self.assertAlmostEqual(cnv.featurize(self.cscl, 0)[0], 9.2584516) self.assertAlmostEqual(cnv.featurize(self.cscl, 1)[0], 9.2584516) self.assertEqual(len(cnv.citations()), 2) cnv = CoordinationNumber(VoronoiNN(), use_weights='effective') self.assertEqual(cnv.feature_labels()[0], 'CN_VoronoiNN') self.assertAlmostEqual(cnv.featurize(self.cscl, 0)[0], 11.648923254) self.assertAlmostEqual(cnv.featurize(self.cscl, 1)[0], 11.648923254) self.assertEqual(len(cnv.citations()), 2) cnj = CoordinationNumber.from_preset('JmolNN') self.assertEqual(cnj.feature_labels()[0], 'CN_JmolNN') self.assertAlmostEqual(cnj.featurize(self.sc, 0)[0], 0) self.assertAlmostEqual(cnj.featurize(self.cscl, 0)[0], 0) self.assertAlmostEqual(cnj.featurize(self.cscl, 1)[0], 0) self.assertEqual(len(cnj.citations()), 1) jmnn = JmolNN(el_radius_updates={"Al": 1.55, "Cl": 1.7, "Cs": 1.7}) cnj = CoordinationNumber(jmnn) self.assertEqual(cnj.feature_labels()[0], 'CN_JmolNN') self.assertAlmostEqual(cnj.featurize(self.sc, 0)[0], 6) self.assertAlmostEqual(cnj.featurize(self.cscl, 0)[0], 8) self.assertAlmostEqual(cnj.featurize(self.cscl, 1)[0], 8) self.assertEqual(len(cnj.citations()), 1) cnmd = CoordinationNumber.from_preset('MinimumDistanceNN') self.assertEqual(cnmd.feature_labels()[0], 'CN_MinimumDistanceNN') self.assertAlmostEqual(cnmd.featurize(self.sc, 0)[0], 6) self.assertAlmostEqual(cnmd.featurize(self.cscl, 0)[0], 8) self.assertAlmostEqual(cnmd.featurize(self.cscl, 1)[0], 8) self.assertEqual(len(cnmd.citations()), 1) cnmok = CoordinationNumber.from_preset('MinimumOKeeffeNN') self.assertEqual(cnmok.feature_labels()[0], 'CN_MinimumOKeeffeNN') self.assertAlmostEqual(cnmok.featurize(self.sc, 0)[0], 6) self.assertAlmostEqual(cnmok.featurize(self.cscl, 0)[0], 8) self.assertAlmostEqual(cnmok.featurize(self.cscl, 1)[0], 6) self.assertEqual(len(cnmok.citations()), 2) cnmvire = CoordinationNumber.from_preset('MinimumVIRENN') self.assertEqual(cnmvire.feature_labels()[0], 'CN_MinimumVIRENN') self.assertAlmostEqual(cnmvire.featurize(self.sc, 0)[0], 6) self.assertAlmostEqual(cnmvire.featurize(self.cscl, 0)[0], 8) self.assertAlmostEqual(cnmvire.featurize(self.cscl, 1)[0], 14) self.assertEqual(len(cnmvire.citations()), 2) self.assertEqual(len(cnmvire.implementors()), 2) self.assertEqual(cnmvire.implementors()[0], 'Nils E. R. Zimmermann')
def get_chemical_ordering_parameters(self): ''' Computes the mean chemical ordering parameter and chemical ordering parameter variance for all elements in the crystal using Voronoi polyhedra returns: [mean coordination number, coordination number variance] ''' if len(self._crystal.composition) == 1: return [0] * len(self._shells) # Get a list of types elems, fracs = zip(*self._crystal.composition.element_composition. fractional_composition.items()) # Precompute the list of NNs in the structure weight = 'area' voro = VoronoiNN(weight=weight) all_nn = self._get_all_nearest_neighbors(voro, self._crystal) # Evaluate each shell output = [] for shell in self._shells: # Initialize an array to store the ordering parameters ordering = np.zeros((len(self._crystal), len(elems))) # Get the ordering of each type of each atom for site_idx in range(len(self._crystal)): nns = voro._get_nn_shell_info(self._crystal, all_nn, site_idx, shell) # Sum up the weights total_weight = sum(x['weight'] for x in nns) # Get weight by type for nn in nns: site_elem = nn['site'].specie.element \ if isinstance(nn['site'].specie, Specie) else \ nn['site'].specie elem_idx = elems.index(site_elem) ordering[site_idx, elem_idx] += nn['weight'] # Compute the ordering parameter ordering[site_idx, :] = 1 - ordering[site_idx, :] / \ total_weight / np.array(fracs) # Compute the average ordering for the entire structure output.append(np.abs(ordering).mean()) return output
def get_local_env_method(method): # pylint:disable=too-many-return-statements """get a local environment method based on its name""" method = method.lower() if method.lower() == "crystalnn": # see eq. 15 and 16 in # https://pubs.acs.org/doi/full/10.1021/acs.inorgchem.0c02996 # for the x_diff_weight parameter. # in the paper it is called δen and it is set to 3 # we found better results by lowering this weight return CrystalNN(porous_adjustment=True, x_diff_weight=1.5, search_cutoff=4.5) if method.lower() == "econnn": return EconNN() if method.lower() == "brunnernn": return BrunnerNN_relative() if method.lower() == "minimumdistance": return MinimumDistanceNN() if method.lower() == "vesta": return VESTA_NN if method.lower() == "voronoinn": return VoronoiNN() if method.lower() == "atr": return ATR_NN if method.lower() == "li": return LI_NN return JmolNN()
def _compute_polyhedra(self): ''' Compute the voronoi polyhedron and specie of each atomic site, format as a tuple (polyhedron, specie), and store those tuples in a list as an attribute ''' # call the voronoi object voronoi = VoronoiNN() self._polyhedra = [] # declare polyhedra attribute # populate the attribute with the polyhedron associated with each # atomic site for i, _ in enumerate(self._crystal): self._polyhedra.append( (voronoi.get_voronoi_polyhedra(self._crystal, i), self._crystal[i].specie))
def featurize(self, strc, idx): # Get the targeted site my_site = strc[idx] # Get the tessellation of a site nn = get_nearest_neighbors( VoronoiNN(weight=self.weight, cutoff=8, compute_adj_neighbors=False), strc, idx, ) # Get the element and weight of each site elems = [n['site'].specie for n in nn] weights = [n['weight'] for n in nn] # Compute the difference for each property output = np.zeros((len(self.properties),)) output_signed = np.zeros((len(self.properties),)) output_max = np.zeros((len(self.properties),)) output_min = np.zeros((len(self.properties),)) total_weight = np.sum(weights) for i, p in enumerate(self.properties): my_prop = self.data_source.get_elemental_property(my_site.specie, p) n_props = self.data_source.get_elemental_properties(elems, p) output[i] = (np.dot(weights, np.abs(np.subtract(n_props, my_prop))) / total_weight) output_signed[i] = (np.dot(weights, np.subtract(n_props, my_prop)) / total_weight) output_max[i] = np.max(np.subtract(n_props, my_prop)) output_min[i] = np.min(np.subtract(n_props, my_prop)) return np.hstack([output, output_signed, output_max, output_min])
def test_filtered(self): nn = VoronoiNN(weight='area') # Make a bcc crystal bcc = Structure([[1, 0, 0], [0, 1, 0], [0, 0, 1]], ['Cu', 'Cu'], [[0, 0, 0], [0.5, 0.5, 0.5]], coords_are_cartesian=False) # Compute the weight of the little face big_face_area = np.sqrt(3) * 3 / 2 * (2 / 4 / 4) small_face_area = 0.125 little_weight = small_face_area / big_face_area # Run one test where you get the small neighbors nn.tol = little_weight * 0.99 nns = nn.get_nn_info(bcc, 0) self.assertEqual(14, len(nns)) # Run a second test where we screen out little faces nn.tol = little_weight * 1.01 nns = nn.get_nn_info(bcc, 0) self.assertEqual(8, len(nns)) # Make sure it works for the `get_all` operation all_nns = nn.get_all_nn_info(bcc * [2, 2, 2]) self.assertEqual([8, ] * 16, [len(x) for x in all_nns])
class VoronoiNNTest(PymatgenTest): def setUp(self): self.s = self.get_structure('LiFePO4') self.nn = VoronoiNN(targets=[Element("O")]) def test_get_voronoi_polyhedra(self): self.assertEqual(len(self.nn.get_voronoi_polyhedra(self.s, 0).items()), 8) def test_get_cn(self): self.assertAlmostEqual(self.nn.get_cn( self.s, 0, use_weights=True), 5.809265748999465, 7) def test_get_coordinated_sites(self): self.assertEqual(len(self.nn.get_nn(self.s, 0)), 8) def tearDown(self): del self.s del self.nn
class VoronoiNNTest(PymatgenTest): def setUp(self): self.s = self.get_structure('LiFePO4') self.nn = VoronoiNN(targets=[Element("O")]) def test_get_voronoi_polyhedra(self): self.assertEqual(len(self.nn.get_voronoi_polyhedra(self.s, 0).items()), 8) def test_get_cn(self): self.assertAlmostEqual(self.nn.get_cn(self.s, 0, use_weights=True), 5.809265748999465, 7) def test_get_coordinated_sites(self): self.assertEqual(len(self.nn.get_nn(self.s, 0)), 8) def tearDown(self): del self.s del self.nn
def get_polyhedras_for_all_sites(structure): """get polyhedras for each of the sites in the structure return a list""" # index of list = index of the structure site # given an index returns a list of dictionaries # index correspods to the site of the central atom and dictionary has all neighbors as key and corresponding angle ratios as values v_polyhedra_sites = [] for i in range(structure.num_sites): v_polyhedra_sites.append(VoronoiNN(allow_pathological=True).get_voronoi_polyhedra(structure, i)) return v_polyhedra_sites
def featurize(self, strc): # Shortcut: Return 0 if there is only 1 type of atom if len(strc.composition) == 1: return [0] * len(self.shells) # Get a list of types elems, fracs = zip(*strc.composition.element_composition. fractional_composition.items()) # Precompute the list of NNs in the structure voro = VoronoiNN(weight=self.weight) all_nn = get_all_nearest_neighbors(voro, strc) # Evaluate each shell output = [] for shell in self.shells: # Initialize an array to store the ordering parameters ordering = np.zeros((len(strc), len(elems))) # Get the ordering of each type of each atom for site_idx in range(len(strc)): nns = voro._get_nn_shell_info(strc, all_nn, site_idx, shell) # Sum up the weights total_weight = sum(x['weight'] for x in nns) # Get weight by type for nn in nns: site_elem = nn['site'].specie.element \ if isinstance(nn['site'].specie, Specie) else \ nn['site'].specie elem_idx = elems.index(site_elem) ordering[site_idx, elem_idx] += nn['weight'] # Compute the ordering parameter ordering[site_idx, :] = 1 - ordering[site_idx, :] / \ total_weight / np.array(fracs) # Compute the average ordering for the entire structure output.append(np.abs(ordering).mean()) return output
def is_config_reasonable(adslab): """ Function that check weather the adsorbate placement is reasonable. For any atom in the adsorbate, if the distance between the atom and slab atoms are closer than 80% of their expected covalent bond, we reject that placement. Args: adslab An `ase.Atoms` object of the adsorbate+slab complex. Returns: A boolean indicating whether or not the adsorbate placement is reasonable. """ vnn = VoronoiNN(allow_pathological=True, tol=0.2, cutoff=10) adsorbate_indices = [atom.index for atom in adslab if atom.tag == 2] structure = AseAtomsAdaptor.get_structure(adslab) slab_lattice = structure.lattice # Check to see if adsorpton site is within the unit cell for idx in adsorbate_indices: coord = slab_lattice.get_fractional_coords(structure[idx].coords) if np.any((coord < 0) | (coord > 1)): return False # Then, check the covalent radius between each adsorbate atoms # and its nearest neighbors that are slab atoms # to make sure adsorbate is not buried into the surface nearneighbors = vnn.get_nn_info(structure, n=idx) slab_nn = [ nn for nn in nearneighbors if nn['site_index'] not in adsorbate_indices ] for nn in slab_nn: ads_elem = structure[idx].species_string nn_elem = structure[nn['site_index']].species_string cov_bond_thres = 0.8 * (covalent_radius[ads_elem] + covalent_radius[nn_elem]) / 100 actual_dist = adslab.get_distance(idx, nn['site_index'], mic=True) if actual_dist < cov_bond_thres: return False return True
def test_Cs2O(self): """A problematic structure in the Materials Project""" strc = Structure([[4.358219, 0.192833, 6.406960], [2.114414, 3.815824, 6.406960], [0.311360, 0.192833, 7.742498]], ['O', 'Cs', 'Cs'], [[0, 0, 0], [0.264318, 0.264318, 0.264318], [0.735682, 0.735682, 0.735682]], coords_are_cartesian=False) # Compute the voronoi tessellation result = VoronoiNN().get_all_voronoi_polyhedra(strc) self.assertEqual(3, len(result))
def test_cache(self): x = Structure(Lattice([[2.189, 0, 1.264], [0.73, 2.064, 1.264], [0, 0, 2.528]]), ["C0+", "C0+"], [[2.554, 1.806, 4.423], [0.365, 0.258, 0.632]], validate_proximity=False, to_unit_cell=False, coords_are_cartesian=True) # Reset the cache _get_all_nearest_neighbors.cache_clear() # Compute the nearest neighbors method = VoronoiNN() nn_1 = get_nearest_neighbors(method, x, 0) # Compute it again and make sure the cache hits nn_2 = get_nearest_neighbors(method, x, 0) self.assertAlmostEqual(nn_1[0]['weight'], nn_2[0]['weight']) self.assertEqual(1, _get_all_nearest_neighbors.cache_info().misses) self.assertEqual(1, _get_all_nearest_neighbors.cache_info().hits) # Reinstantiate the VoronoiNN class, should not cause a miss method = VoronoiNN() nn_2 = get_nearest_neighbors(method, x, 0) self.assertAlmostEqual(nn_1[0]['weight'], nn_2[0]['weight']) self.assertEqual(1, _get_all_nearest_neighbors.cache_info().misses) self.assertEqual(2, _get_all_nearest_neighbors.cache_info().hits) # Change the NN method, should induce a miss method = VoronoiNN(weight='volume') get_nearest_neighbors(method, x, 0) self.assertEqual(2, _get_all_nearest_neighbors.cache_info().misses) self.assertEqual(2, _get_all_nearest_neighbors.cache_info().hits) # Perturb the structure, make sure it induces a miss and # a change in the NN weights x.perturb(0.1) nn_2 = get_nearest_neighbors(method, x, 0) self.assertNotAlmostEqual(nn_1[0]['weight'], nn_2[0]['weight']) self.assertEqual(3, _get_all_nearest_neighbors.cache_info().misses) self.assertEqual(2, _get_all_nearest_neighbors.cache_info().hits)
def fxn(self, unit, base, start, final): elements = unit['elements'].copy() elements.pop(elements.index('O')) base_s = Poscar.from_dict(base['poscar']).structure run_s = Poscar.from_dict( self.get_run(unit, base, start, final)['poscar']).structure start_i = get_coords(base_s.copy(), run_s.copy()) print(start_i) vnn = VoronoiNN(targets=[Element(x) for x in elements]) start_vnn = vnn.get_nn_info(base_s, start_i) pauling_total = 0 weight = 0 for pt in start_vnn: # print('{}: {:3.2f}'.format(pt['site_index'], pt['weight'])) pauling_total += self.pauling[ pt['site'].species_string] * pt['weight'] weight += pt['weight'] pauling_diff = self.pauling['O'] - pauling_total / weight return pauling_diff
def find_surface_atoms_indices(bulk_cn_dict, atoms): ''' A helper function referencing codes from pymatgen to get a list of surface atoms indices of a slab's top surface. Due to how our workflow is setup, the pymatgen method cannot be directly applied. Taken from pymatgen.core.surface Class Slab, `get_surface_sites`. https://pymatgen.org/pymatgen.core.surface.html Arg: bulk_cn_dict A dictionary of coordination numbers for each distinct site in the respective bulk structure atoms The slab where you are trying to find surface sites in `ase.Atoms` format Output: indices_list A list that contains the indices of the surface atoms ''' struct = AseAtomsAdaptor.get_structure(atoms) voronoi_nn = VoronoiNN() # Identify index of the surface atoms indices_list = [] weights = [site.species.weight for site in struct] center_of_mass = np.average(struct.frac_coords, weights=weights, axis=0) for idx, site in enumerate(struct): if site.frac_coords[2] > center_of_mass[2]: try: cn = voronoi_nn.get_cn(struct, idx, use_weights=True) cn = float('%.5f' % (round(cn, 5))) # surface atoms are undercoordinated if cn < min(bulk_cn_dict[site.species_string]): indices_list.append(idx) except RuntimeError: # or if pathological error is returned, # indicating a surface site indices_list.append(idx) return indices_list
def VoronoiInfo(poscar, vasprun, elements): base_s = poscar.structure start_i = get_center_i( base_s, Element('O'), ) print(start_i) vnn = VoronoiNN(targets=[Element(x) for x in elements]) start_vnn = vnn.get_nn_info(base_s, start_i) weight = {} total_length = {} for pt in start_vnn: # print('{}: {:3.2f}'.format(pt['site_index'], pt['weight'])) temp_weight = round(pt['weight'] + 0.45) if pt['site'].species_string in weight: weight[pt['site'].species_string] = weight[ pt['site'].species_string] + temp_weight else: weight[pt['site'].species_string] = temp_weight if temp_weight >= 0.99: bond_length = base_s.get_distance(start_i, pt['site_index']) if pt['site'].species_string in total_length: total_length[pt['site'].species_string] = total_length[ pt['site'].species_string] + bond_length else: total_length[pt['site'].species_string] = bond_length bonds = { 'A_Bonds': weight[elements[0]], 'A_length': total_length[elements[0]] / weight[elements[0]], 'B_Bonds': weight[elements[1]], 'B_length': total_length[elements[1]] / weight[elements[1]], 'avg_length': (total_length[elements[0]] + total_length[elements[1]]) / (weight[elements[0]] + weight[elements[1]]), } return bonds
def test_filtered(self): nn = VoronoiNN(weight='area') # Make a bcc crystal bcc = Structure([[1, 0, 0], [0, 1, 0], [0, 0, 1]], ['Cu', 'Cu'], [[0, 0, 0], [0.5, 0.5, 0.5]], coords_are_cartesian=False) # Compute the weight of the little face big_face_area = np.sqrt(3) * 3 / 2 * (2 / 4 / 4) small_face_area = 0.125 little_weight = small_face_area / big_face_area # Run one test where you get the small neighbors nn.tol = little_weight * 0.99 nns = nn.get_nn_info(bcc, 0) self.assertEqual(14, len(nns)) # Run a second test where we screen out little faces nn.tol = little_weight * 1.01 nns = nn.get_nn_info(bcc, 0) self.assertEqual(8, len(nns)) # Make sure it works for the `get_all` operation all_nns = nn.get_all_nn_info(bcc * [2, 2, 2]) self.assertEqual([8,]*16, [len(x) for x in all_nns])
def __init__(self, max_l=10, compute_w=False, compute_w_hat=False): """ Initialize the featurizer Args: max_l (int) - Maximum spherical harmonic to consider compute_w (bool) - Whether to compute Ws as well compute_w_hat (bool) - Whether to compute What """ self._nn = VoronoiNN(weight='solid_angle') self.max_l = max_l self.compute_W = compute_w self.compute_What = compute_w_hat
def test_multiple_same_methods(self): # test that if running the benchmark on multiple NN methods of the same class, # that each NN method is named differently and appears in the benchmark and # score results bm = Benchmark(self.structures) nn_methods = [VoronoiNN(), VoronoiNN(tol=0.5)] results = bm.benchmark(nn_methods) expected_results = { "VoronoiNN(0)0": {"test_structure": {"Cl": 6}}, "VoronoiNN(0)1": {"test_structure": {"Na": 6}}, "VoronoiNN(1)0": {"test_structure": {"Cl": 6}}, "VoronoiNN(1)1": {"test_structure": {"Na": 6}}, } self.assertEqual(results.to_dict(), expected_results) scores = bm.score(nn_methods) expected_scores = { "VoronoiNN(0)": {"Total": 4.0, "test_structure": 4.0}, "VoronoiNN(1)": {"Total": 4.0, "test_structure": 4.0}, } self.assertEqual(scores.to_dict(), expected_scores)
def get_coordination_numbers(d): """ Helper method to get the coordination number of all sites in the final structure from a run. Args: d: Run dict generated by VaspToDbTaskDrone. Returns: Coordination numbers as a list of dict of [{"site": site_dict, "coordination": number}, ...]. """ structure = Structure.from_dict(d["output"]["crystal"]) f = VoronoiNN() cn = [] for i, s in enumerate(structure.sites): try: n = f.get_cn(structure, i) number = int(round(n)) cn.append({"site": s.as_dict(), "coordination": number}) except Exception: logger.error("Unable to parse coordination errors") return cn
class VoronoiNNTest(PymatgenTest): def setUp(self): self.s = self.get_structure('LiFePO4') self.nn = VoronoiNN(targets=[Element("O")]) self.s_sic = self.get_structure('Si') self.s_sic["Si"] = {'Si': 0.5, 'C': 0.5} self.nn_sic = VoronoiNN() def test_get_voronoi_polyhedra(self): self.assertEqual(len(self.nn.get_voronoi_polyhedra(self.s, 0).items()), 8) def test_get_cn(self): self.assertAlmostEqual(self.nn.get_cn( self.s, 0, use_weights=True), 5.809265748999465, 7) self.assertAlmostEqual(self.nn_sic.get_cn( self.s_sic, 0, use_weights=True), 4.5381161643940668, 7) def test_get_coordinated_sites(self): self.assertEqual(len(self.nn.get_nn(self.s, 0)), 8) def test_volume(self): self.nn.targets = None volume = 0 for n in range(len(self.s)): for nn in self.nn.get_voronoi_polyhedra(self.s, n).values(): volume += nn['volume'] self.assertAlmostEqual(self.s.volume, volume) def test_solid_angle(self): self.nn.targets = None for n in range(len(self.s)): angle = 0 for nn in self.nn.get_voronoi_polyhedra(self.s, n).values(): angle += nn['solid_angle'] self.assertAlmostEqual(4 * np.pi, angle) self.assertEqual(solid_angle([0,0,0], [[1,0,0],[-1,0,0],[0,1,0]]), pi) def test_nn_shell(self): # First, make a SC lattice. Make my math easier s = Structure([[1, 0, 0], [0, 1, 0], [0, 0, 1]], ['Cu'], [[0, 0, 0]]) # Get the 1NN shell self.nn.targets = None nns = self.nn.get_nn_shell_info(s, 0, 1) self.assertEqual(6, len(nns)) # Test the 2nd NN shell nns = self.nn.get_nn_shell_info(s, 0, 2) self.assertEqual(18, len(nns)) self.assertArrayAlmostEqual([1] * 6, [x['weight'] for x in nns if max(np.abs(x['image'])) == 2]) self.assertArrayAlmostEqual([2] * 12, [x['weight'] for x in nns if max(np.abs(x['image'])) == 1]) # Test the 3rd NN shell nns = self.nn.get_nn_shell_info(s, 0, 3) for nn in nns: # Check that the coordinates were set correctly self.assertArrayAlmostEqual(nn['site'].frac_coords, nn['image']) # Test with a structure that has unequal faces cscl = Structure(Lattice([[4.209, 0, 0], [0, 4.209, 0], [0, 0, 4.209]]), ["Cl1-", "Cs1+"], [[2.1045, 2.1045, 2.1045], [0, 0, 0]], validate_proximity=False, to_unit_cell=False, coords_are_cartesian=True, site_properties=None) self.nn.weight = 'area' nns = self.nn.get_nn_shell_info(cscl, 0, 1) self.assertEqual(14, len(nns)) self.assertEqual(6, np.isclose([x['weight'] for x in nns], 0.125/0.32476).sum()) # Square faces self.assertEqual(8, np.isclose([x['weight'] for x in nns], 1).sum()) nns = self.nn.get_nn_shell_info(cscl, 0, 2) # Weight of getting back on to own site # Square-square hop: 6*5 options times (0.125/0.32476)^2 weight each # Hex-hex hop: 8*7 options times 1 weight each self.assertAlmostEqual(60.4444, np.sum([x['weight'] for x in nns if x['site_index'] == 0]), places=3) def test_adj_neighbors(self): # Make a simple cubic structure s = Structure([[1, 0, 0], [0, 1, 0], [0, 0, 1]], ['Cu'], [[0, 0, 0]]) # Compute the NNs with adjacency self.nn.targets = None neighbors = self.nn.get_voronoi_polyhedra(s, 0) # Each neighbor has 4 adjacent neighbors, all orthogonal for nn_key, nn_info in neighbors.items(): self.assertEqual(4, len(nn_info['adj_neighbors'])) for adj_key in nn_info['adj_neighbors']: self.assertEqual(0, np.dot(nn_info['normal'], neighbors[adj_key]['normal'])) def test_all_at_once(self): # Get all of the sites for LiFePO4 all_sites = self.nn.get_all_voronoi_polyhedra(self.s) # Make sure they are the same as the single-atom ones for i, site in enumerate(all_sites): # Compute the tessellation using only one site by_one = self.nn.get_voronoi_polyhedra(self.s, i) # Match the coordinates the of the neighbors, as site matching does not seem to work? all_coords = np.sort([x['site'].coords for x in site.values()], axis=0) by_one_coords = np.sort([x['site'].coords for x in by_one.values()], axis=0) self.assertArrayAlmostEqual(all_coords, by_one_coords) # Test the nn_info operation all_nn_info = self.nn.get_all_nn_info(self.s) for i, info in enumerate(all_nn_info): # Compute using the by-one method by_one = self.nn.get_nn_info(self.s, i) # Get the weights all_weights = sorted([x['weight'] for x in info]) by_one_weights = sorted([x['weight'] for x in by_one]) self.assertArrayAlmostEqual(all_weights, by_one_weights) def test_Cs2O(self): """A problematic structure in the Materials Project""" strc = Structure([[4.358219, 0.192833, 6.406960], [2.114414, 3.815824, 6.406960], [0.311360, 0.192833, 7.742498]], ['O', 'Cs', 'Cs'], [[0, 0, 0], [0.264318, 0.264318, 0.264318], [0.735682, 0.735682, 0.735682]], coords_are_cartesian=False) # Compute the voronoi tessellation result = VoronoiNN().get_all_voronoi_polyhedra(strc) self.assertEqual(3, len(result)) def test_filtered(self): nn = VoronoiNN(weight='area') # Make a bcc crystal bcc = Structure([[1, 0, 0], [0, 1, 0], [0, 0, 1]], ['Cu', 'Cu'], [[0, 0, 0], [0.5, 0.5, 0.5]], coords_are_cartesian=False) # Compute the weight of the little face big_face_area = np.sqrt(3) * 3 / 2 * (2 / 4 / 4) small_face_area = 0.125 little_weight = small_face_area / big_face_area # Run one test where you get the small neighbors nn.tol = little_weight * 0.99 nns = nn.get_nn_info(bcc, 0) self.assertEqual(14, len(nns)) # Run a second test where we screen out little faces nn.tol = little_weight * 1.01 nns = nn.get_nn_info(bcc, 0) self.assertEqual(8, len(nns)) # Make sure it works for the `get_all` operation all_nns = nn.get_all_nn_info(bcc * [2, 2, 2]) self.assertEqual([8,]*16, [len(x) for x in all_nns]) def tearDown(self): del self.s del self.nn
def setUp(self): self.s = self.get_structure('LiFePO4') self.nn = VoronoiNN(targets=[Element("O")])
def setUp(self): self.s = self.get_structure('LiFePO4') self.nn = VoronoiNN(targets=[Element("O")]) self.s_sic = self.get_structure('Si') self.s_sic["Si"] = {'Si': 0.5, 'C': 0.5} self.nn_sic = VoronoiNN()
class VoronoiNNTest(PymatgenTest): def setUp(self): self.s = self.get_structure('LiFePO4') self.nn = VoronoiNN(targets=[Element("O")]) def test_get_voronoi_polyhedra(self): self.assertEqual(len(self.nn.get_voronoi_polyhedra(self.s, 0).items()), 8) def test_get_cn(self): self.assertAlmostEqual(self.nn.get_cn( self.s, 0, use_weights=True), 5.809265748999465, 7) def test_get_coordinated_sites(self): self.assertEqual(len(self.nn.get_nn(self.s, 0)), 8) def test_volume(self): self.nn.targets = None volume = 0 for n in range(len(self.s)): for nn in self.nn.get_voronoi_polyhedra(self.s, n).values(): volume += nn['volume'] self.assertAlmostEqual(self.s.volume, volume) def test_solid_angle(self): self.nn.targets = None for n in range(len(self.s)): angle = 0 for nn in self.nn.get_voronoi_polyhedra(self.s, n).values(): angle += nn['solid_angle'] self.assertAlmostEqual(4 * np.pi, angle) self.assertEqual(solid_angle([0,0,0], [[1,0,0],[-1,0,0],[0,1,0]]), pi) def test_nn_shell(self): # First, make a SC lattice. Make my math easier s = Structure([[1, 0, 0], [0, 1, 0], [0, 0, 1]], ['Cu'], [[0, 0, 0]]) # Get the 1NN shell self.nn.targets = None nns = self.nn.get_nn_shell_info(s, 0, 1) self.assertEqual(6, len(nns)) # Test the 2nd NN shell nns = self.nn.get_nn_shell_info(s, 0, 2) self.assertEqual(18, len(nns)) self.assertArrayAlmostEqual([1] * 6, [x['weight'] for x in nns if max(np.abs(x['image'])) == 2]) self.assertArrayAlmostEqual([2] * 12, [x['weight'] for x in nns if max(np.abs(x['image'])) == 1]) # Test the 3rd NN shell nns = self.nn.get_nn_shell_info(s, 0, 3) for nn in nns: # Check that the coordinates were set correctly self.assertArrayAlmostEqual(nn['site'].frac_coords, nn['image']) # Test with a structure that has unequal faces cscl = Structure(Lattice([[4.209, 0, 0], [0, 4.209, 0], [0, 0, 4.209]]), ["Cl1-", "Cs1+"], [[2.1045, 2.1045, 2.1045], [0, 0, 0]], validate_proximity=False, to_unit_cell=False, coords_are_cartesian=True, site_properties=None) self.nn.weight = 'area' nns = self.nn.get_nn_shell_info(cscl, 0, 1) self.assertEqual(14, len(nns)) self.assertEqual(6, np.isclose([x['weight'] for x in nns], 0.125/0.32476).sum()) # Square faces self.assertEqual(8, np.isclose([x['weight'] for x in nns], 1).sum()) nns = self.nn.get_nn_shell_info(cscl, 0, 2) # Weight of getting back on to own site # Square-square hop: 6*5 options times (0.125/0.32476)^2 weight each # Hex-hex hop: 8*7 options times 1 weight each self.assertAlmostEqual(60.4444, np.sum([x['weight'] for x in nns if x['site_index'] == 0]), places=3) def tearDown(self): del self.s del self.nn
def get_interstitial_diffusion_pathways_from_cell(structure : Structure, interstitial_atom : str, vis=False, get_midpoints=False, dummy='He', min_dist=0.5, weight_cutoff=0.0001, is_interstitial_structure=False): """ Find Vacancy Strucutres for diffusion into and out of the specified atom_i site. :param structure: Structure Structure to calculate diffusion pathways :param atom_i: int Atom to get diffion path from :return: [ Structure ] """ vnn = VoronoiNN(targets=[interstitial_atom]) # To Find Pathway, look for voronoi edges if not is_interstitial_structure: orig_structure = structure.copy() structure = structure.copy() # type: Structure interstitial_structure = structure.copy() interstitial_structure.DISTANCE_TOLERANCE = 0.01 if vis: Poscar(structure).write_file(vis) open_in_VESTA(vis) inter_gen = list(VoronoiInterstitialGenerator(orig_structure, interstitial_atom)) if vis: print(len(inter_gen)) for interstitial in inter_gen: sat_structure = None for dist_tol in [0.2, 0.15, 0.1, 0.05, 0.01, 0.001]: try: sat_structure = create_saturated_interstitial_structure(interstitial, dist_tol=dist_tol) # type: Structure break except ValueError: continue except TypeError: continue if not sat_structure: continue sat_structure.remove_site_property('velocities') if vis: Poscar(sat_structure).write_file(vis) open_in_VESTA(vis) time.sleep(0.5) for site in sat_structure: # type: PeriodicSite if site.specie == interstitial_atom: try: interstitial_structure.append(site.specie, site.coords, coords_are_cartesian=True, validate_proximity=True) except StructureError: pass # combined_structure.merge_sites(mode='delete') interstitial_structure.remove_site_property('velocities') if vis: Poscar(interstitial_structure).write_file(vis) open_in_VESTA(vis) else: interstitial_structure = structure.copy() # edges = vnn.get_nn_info(structure, atom_i) # base_coords = structure[atom_i].coords pathway_structure = interstitial_structure.copy() # type: Structure pathway_structure.DISTANCE_TOLERANCE = 0.01 # Add H for all other diffusion atoms, so symmetry is preserved for i in get_atom_i(interstitial_structure, interstitial_atom): sym_edges = vnn.get_nn_info(interstitial_structure, i) base = pathway_structure[i] # type: PeriodicSite for edge in sym_edges: dest = edge['site'] if base.distance(dest, jimage=edge['image']) > min_dist and edge['weight'] > weight_cutoff: coords = (base.coords + dest.coords) / 2 try: neighbors = [i, edge['site_index']] # neighbors.sort() pathway_structure.append(dummy, coords, True, validate_proximity=True, properties={'neighbors': neighbors, 'image' : edge['image']}) except StructureError: pass except ValueError: pass if vis: Poscar(pathway_structure).write_file(vis) open_in_VESTA(vis) # Remove symmetrically equivalent pathways: # sga = SpacegroupAnalyzer(pathway_structure, 0.1, angle_tolerance=10) # ss = sga.get_symmetrized_structure() return interstitial_structure, pathway_structure
def from_bulk_and_miller(cls, structure, miller_index, min_slab_size=8.0, min_vacuum_size=10.0, max_normal_search=None, center_slab=True, selective_dynamics=False, undercoord_threshold=0.09): """ This method constructs the adsorbate site finder from a bulk structure and a miller index, which allows the surface sites to be determined from the difference in bulk and slab coordination, as opposed to the height threshold. Args: structure (Structure): structure from which slab input to the ASF is constructed miller_index (3-tuple or list): miller index to be used min_slab_size (float): min slab size for slab generation min_vacuum_size (float): min vacuum size for slab generation max_normal_search (int): max normal search for slab generation center_slab (bool): whether to center slab in slab generation selective dynamics (bool): whether to assign surface sites to selective dynamics undercoord_threshold (float): threshold of "undercoordation" to use for the assignment of surface sites. Default is 0.1, for which surface sites will be designated if they are 10% less coordinated than their bulk counterpart """ # TODO: for some reason this works poorly with primitive cells # may want to switch the coordination algorithm eventually vnn_bulk = VoronoiNN(tol=0.05) bulk_coords = [len(vnn_bulk.get_nn(structure, n)) for n in range(len(structure))] struct = structure.copy(site_properties={'bulk_coordinations': bulk_coords}) slabs = generate_all_slabs(struct, max_index=max(miller_index), min_slab_size=min_slab_size, min_vacuum_size=min_vacuum_size, max_normal_search=max_normal_search, center_slab=center_slab) slab_dict = {slab.miller_index: slab for slab in slabs} if miller_index not in slab_dict: raise ValueError("Miller index not in slab dict") this_slab = slab_dict[miller_index] vnn_surface = VoronoiNN(tol=0.05, allow_pathological=True) surf_props, undercoords = [], [] this_mi_vec = get_mi_vec(this_slab) mi_mags = [np.dot(this_mi_vec, site.coords) for site in this_slab] average_mi_mag = np.average(mi_mags) for n, site in enumerate(this_slab): bulk_coord = this_slab.site_properties['bulk_coordinations'][n] slab_coord = len(vnn_surface.get_nn(this_slab, n)) mi_mag = np.dot(this_mi_vec, site.coords) undercoord = (bulk_coord - slab_coord) / bulk_coord undercoords += [undercoord] if undercoord > undercoord_threshold and mi_mag > average_mi_mag: surf_props += ['surface'] else: surf_props += ['subsurface'] new_site_properties = {'surface_properties': surf_props, 'undercoords': undercoords} new_slab = this_slab.copy(site_properties=new_site_properties) return cls(new_slab, selective_dynamics)
def get_vacancy_diffusion_pathways_from_cell(structure : Structure, atom_i : int, vis=False, get_midpoints=False): """ Find Vacancy Strucutres for diffusion into and out of the specified atom_i site. :param structure: Structure Structure to calculate diffusion pathways :param atom_i: int Atom to get diffion path from :return: [ Structure ] """ # To Find Pathway, look for voronoi edges orig_structure = structure.copy() structure = structure.copy() # type: Structure target_atom = structure[atom_i].specie vnn = VoronoiNN(targets=[target_atom]) edges = vnn.get_nn_info(structure, atom_i) base_coords = structure[atom_i].coords # Add H in middle of the discovered pathways. Use symmetry analysis to elminate equivlent H and therfore # equivalent pathways site_dir = {} for edge in edges: coords = np.round((base_coords + edge['site'].coords)/2,3) structure.append('H', coords, True) # site_dir[tuple(np.round(coords))] = structure.index(edge['site']) # Use Tuple for indexing dict, need to round site_dir[tuple(np.round(coords))] = [list(x) for x in np.round(structure.frac_coords % 1,2) ].index(list(np.round(edge['site'].frac_coords % 1, 2))) # Use Tuple for indexing dict, need to round # Add H for all other diffusion atoms, so symmetry is preserved for i in get_atom_i(orig_structure, target_atom): sym_edges = vnn.get_nn_info(orig_structure, i) base_coords = structure[i].coords for edge in sym_edges: coords = (base_coords + edge['site'].coords) / 2 try: structure.append('H', coords, True, True) except: pass # Remove symmetrically equivalent pathways: sga = SpacegroupAnalyzer(structure, 0.5, angle_tolerance=20) ss = sga.get_symmetrized_structure() final_structure = structure.copy() indices = [] for i in range(len(orig_structure), len(orig_structure)+len(edges)): # get all 'original' edge sites sites = ss.find_equivalent_sites(ss[i]) new_indices = [ss.index(site) for site in sites if ss.index(site) < len(orig_structure) + len(edges)] # Check if symmetrically equivalent to other original edge sites new_indices.remove(i) if i not in indices: # Don't duplicate effort indices = indices + new_indices indices.sort() indices = indices + list(range(len(orig_structure)+len(edges), len(final_structure))) final_structure.remove_sites(indices) diffusion_elements = [ site_dir[tuple(np.round(h.coords))] for h in final_structure[len(orig_structure):] ] if vis: view(final_structure, 'VESTA') print(diffusion_elements) if get_midpoints: centers = [h.frac_coords for h in final_structure[len(orig_structure):]] return (diffusion_elements, centers) return diffusion_elements