def test_integration(self): """ Sanity check: for a long enough diagonaly hop, if we turn the radius of the tube way up, it should cover the entire unit cell """ total_chg_per_vol = (self.cbg.potential_field.data["total"].sum() / self.cbg.potential_field.ngridpts / self.cbg.potential_field.structure.volume) self.assertAlmostEqual( self.cbg._get_chg_between_sites_tube( self.cbg.unique_hops[2]["hop"]), total_chg_per_vol, ) self.cbg._tube_radius = 2 # self.cbg.populate_edges_with_chg_density_info() # find this particular hop ipos = [0.33079153, 0.18064031, 0.67945924] epos = [0.33587514, -0.3461259, 1.15269302] isite = PeriodicSite("Li", ipos, self.cbg.structure.lattice) esite = PeriodicSite("Li", epos, self.cbg.structure.lattice) ref_hop = MigrationHop(isite=isite, esite=esite, symm_structure=self.cbg.symm_structure) hop_idx = -1 for k, d in self.cbg.unique_hops.items(): if d["hop"] == ref_hop: hop_idx = k self.assertAlmostEqual( self.cbg._get_chg_between_sites_tube( self.cbg.unique_hops[hop_idx]["hop"]), 0.188952739835188, 3, )
def are_equal(self, d1, d2): """ Args: d1: First defect. A pymatgen Defect object. d2: Second defect. A pymatgen Defect object. Returns: True if defects are identical in type and sublattice. """ possible_defect_types = (Defect, Vacancy, Substitution, Interstitial) if not isinstance(d1, possible_defect_types) or not isinstance(d2, possible_defect_types): raise ValueError("Cannot use PointDefectComparator to" " compare non-defect objects...") if not isinstance(d1, d2.__class__): return False if d1.site.specie != d2.site.specie: return False if self.check_charge and (d1.charge != d2.charge): return False sm = StructureMatcher( ltol=0.01, primitive_cell=self.check_primitive_cell, scale=self.check_lattice_scale, ) if not sm.fit(d1.bulk_structure, d2.bulk_structure): return False d1 = d1.copy() d2 = d2.copy() if self.check_primitive_cell or self.check_lattice_scale: # if allowing for base structure volume or supercell modifications, # then need to preprocess defect objects to allow for matching d1_mod_bulk_structure, d2_mod_bulk_structure, _, _ = sm._preprocess(d1.bulk_structure, d2.bulk_structure) d1_defect_site = PeriodicSite( d1.site.specie, d1.site.coords, d1_mod_bulk_structure.lattice, to_unit_cell=True, coords_are_cartesian=True, ) d2_defect_site = PeriodicSite( d2.site.specie, d2.site.coords, d2_mod_bulk_structure.lattice, to_unit_cell=True, coords_are_cartesian=True, ) d1._structure = d1_mod_bulk_structure d2._structure = d2_mod_bulk_structure d1._defect_site = d1_defect_site d2._defect_site = d2_defect_site return sm.fit(d1.generate_defect_structure(), d2.generate_defect_structure())
def convert_cd_to_de( cd, b_cse): """ As of pymatgen v2.0, ComputedDefect objects were deprecated in favor of DefectEntry objects in pymatgen.analysis.defects.core This function takes a ComputedDefect (either as a dict or object) and converts it into a DefectEntry object in order to handle legacy PyCDT creation within the current paradigm of PyCDT. :param cd (dict or ComputedDefect object): ComputedDefect as an object or as a dictionary :params b_cse (dict or ComputedStructureEntry object): ComputedStructureEntry of bulk entry associated with the ComputedDefect. :return: de (DefectEntry): Resulting DefectEntry object """ if type(cd) != dict: cd = cd.as_dict() if type(b_cse) != dict: b_cse = b_cse.as_dict() bulk_sc_structure = Structure.from_dict( b_cse["structure"]) #modify defect_site as required for Defect object, confirming site exists in bulk structure site_cls = cd["site"] defect_site = PeriodicSite.from_dict( site_cls) def_nom = cd["name"].lower() if "sub_" in def_nom or "as_" in def_nom: #modify site object for substitution site of Defect object site_cls["species"][0]["element"] = cd["name"].split("_")[2] defect_site = PeriodicSite.from_dict( site_cls) poss_deflist = sorted( bulk_sc_structure.get_sites_in_sphere(defect_site.coords, 0.1, include_index=True), key=lambda x: x[1]) if len(poss_deflist) != 1: raise ValueError("ComputedDefect to DefectEntry conversion failed. " "Could not determine periodic site position in bulk supercell.") # create defect object if "vac_" in def_nom: defect_obj = Vacancy(bulk_sc_structure, defect_site, charge=cd["charge"]) elif "as_" in def_nom or "sub_" in def_nom: defect_obj = Substitution(bulk_sc_structure, defect_site, charge=cd["charge"]) elif "int_" in def_nom: defect_obj = Interstitial(bulk_sc_structure, defect_site, charge=cd["charge"]) else: raise ValueError("Could not recognize defect type for {}".format( cd["name"])) # assign proper energy and parameter metadata uncorrected_energy = cd["entry"]["energy"] - b_cse["energy"] def_path = os.path.split( cd["entry"]["data"]["locpot_path"])[0] bulk_path = os.path.split( b_cse["data"]["locpot_path"])[0] p = {"defect_path": def_path, "bulk_path": bulk_path, "encut": cd["entry"]["data"]["encut"]} de = DefectEntry( defect_obj, uncorrected_energy, parameters = p) return de
def _get_pos_and_migration_path(self, u, v, w): """ insert a single MigrationPath object on a graph edge Args: u (int): index of initial node v (int): index of final node w (int): index for multiple edges that share the same two nodes """ edge = self.migration_graph.graph[u][v][w] i_site = self.only_sites.sites[u] e_site = PeriodicSite( self.only_sites.sites[v].species, self.only_sites.sites[v].frac_coords + np.array(edge["to_jimage"]), lattice=self.only_sites.lattice, ) # Positions might be useful for plotting edge["ipos"] = i_site.frac_coords edge["epos"] = e_site.frac_coords edge["ipos_cart"] = np.dot(i_site.frac_coords, self.only_sites.lattice.matrix) edge["epos_cart"] = np.dot(e_site.frac_coords, self.only_sites.lattice.matrix) edge["hop"] = MigrationPath(i_site, e_site, self.symm_structure) edge["properties"] = {}
def set_up_brillouin_facets(lattice): """ Set up the facets of the 1st Brillouin zone as a list of cage.core.Facets. Args: lattice (pymatgen.core.lattice.Lattice): Lattice of the structure. Returns: (list) List of cage.core.Facet objects. """ rec_lattice = lattice.reciprocal_lattice bz_facet_coords = lattice.get_brillouin_zone() bz_facet_sites = [] for facet in bz_facet_coords: # The coordinates are transferred into sites to be able to use the # Facet object more easily. bz_facet_sites.append( [PeriodicSite("H", coords, rec_lattice, coords_are_cartesian=True) for coords in facet] ) bz_facets = [Facet(facet_sites) for facet_sites in bz_facet_sites] return bz_facets
def __init__(self, structure, element): """ Initializes a Substitution Generator note: an Antisite is considered a type of substitution Args: structure(Structure): pymatgen structure object element (str or Element or Specie): element for the substitution """ self.structure = structure self.element = element # Find equivalent site list sga = SpacegroupAnalyzer(self.structure) self.symm_structure = sga.get_symmetrized_structure() self.equiv_sub = [] for equiv_site_set in list(self.symm_structure.equivalent_sites): vac_site = equiv_site_set[0] if isinstance( element, str ): # make sure you compare with specie symbol or Element type vac_specie = vac_site.specie.symbol else: vac_specie = vac_site.specie if element != vac_specie: defect_site = PeriodicSite(element, vac_site.coords, structure.lattice, coords_are_cartesian=True) sub = Substitution(structure, defect_site) self.equiv_sub.append(sub)
def setUp(self): self.lifepo = self.get_structure("LiFePO4") migration_graph = MigrationGraph.with_distance(self.lifepo, max_distance=4.0, migrating_specie="Li") gen = iter(migration_graph.migration_graph.graph.edges(data=True)) u, v, d = next(gen) i_site = PeriodicSite("Li", coords=d["ipos"], lattice=self.lifepo.lattice) e_site = PeriodicSite("Li", coords=d["epos"], lattice=self.lifepo.lattice) a = SpacegroupAnalyzer(self.lifepo) symm_structure = a.get_symmetrized_structure() self.m_hop = MigrationHop(i_site, e_site, symm_structure)
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_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 __init__( self, isite: Site, esite: Site, symm_structure: SymmetrizedStructure, symprec: float = 0.001, ): """ Args: isite: Initial site esite: End site symm_structure: SymmetrizedStructure symprec: used to determine equivalence """ self.isite = isite self.esite = esite self.iindex = None self.eindex = None self.symm_structure = symm_structure self.symprec = symprec self.msite = PeriodicSite(esite.specie, (isite.frac_coords + esite.frac_coords) / 2, esite.lattice) sg = self.symm_structure.spacegroup for i, sites in enumerate(self.symm_structure.equivalent_sites): if sg.are_symmetrically_equivalent([isite], [sites[0]]): self.iindex = i if sg.are_symmetrically_equivalent([esite], [sites[0]]): self.eindex = i # if no index was identified then loop over each site until something is found if self.iindex is None: for i, sites in enumerate(self.symm_structure.equivalent_sites): for itr_site in sites: if sg.are_symmetrically_equivalent([isite], [itr_site]): self.iindex = i break else: continue break if self.eindex is None: for i, sites in enumerate(self.symm_structure.equivalent_sites): for itr_site in sites: if sg.are_symmetrically_equivalent([esite], [itr_site]): self.eindex = i break else: continue break if self.iindex is None: raise RuntimeError( f"No symmetrically equivalent site was found for {isite}") if self.eindex is None: raise RuntimeError( f"No symmetrically equivalent site was found for {esite}")
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 test_subs_and_interstits(self): # test manual subtitution specification CDS = ChargedDefectsStructures(self.gaas_struct, antisites_flag=False, substitutions={'Ga':['Si','In'], 'As':['Sb']}) check_subs = {sub['name']: sub['unique_site'] for sub in CDS.defects['substitutions']} self.assertEqual(3, len(check_subs)) self.assertEqual(self.ga_site, check_subs['sub_1_Si_on_Ga']) self.assertEqual(self.ga_site, check_subs['sub_1_In_on_Ga']) self.assertEqual(self.as_site, check_subs['sub_2_Sb_on_As']) # Test automatic interstitial finding. CDS = ChargedDefectsStructures(self.gaas_struct, include_interstitials=True, interstitial_elements=['Mn']) self.assertEqual(CDS.get_n_defects_of_type('interstitials'), 2) fnames = [i['name'][i['name'].index('M'):] for i in CDS.defects['interstitials']] self.assertEqual(sorted(fnames), sorted(['Mn_InFiT1_mult6', 'Mn_InFiT2_mult6'])) nsites = len(CDS.defects['interstitials'][0]['supercell']['structure'].sites) self.assertEqual(len(CDS.get_ith_supercell_of_defect_type( 0, 'interstitials').sites), nsites) self.assertEqual(CDS.defects['interstitials'][0]['charges'], \ [0, 1, 2, 3, 4, 5, 6, 7]) self.assertEqual(CDS.defects['interstitials'][1]['charges'], \ [0, 1, 2, 3, 4, 5, 6, 7]) # Test manual interstitial specification. isite = PeriodicSite('Mn', CDS.defects['interstitials'][0]['supercell']['structure'][nsites-1].coords, self.gaas_struct.lattice) cds2 = ChargedDefectsStructures(self.gaas_struct, include_interstitials=True, standardized=False, # standardized = False is required when manually specifying interstitial, # because dont want base structure lattice to change intersites=(isite,)) self.assertEqual(cds2.get_n_defects_of_type('interstitials'), 2) fnames = [i['name'][i['name'].index('_')+3:] for i in cds2.defects['interstitials']] self.assertEqual(sorted(fnames), sorted(['As', 'Ga'])) nsites = len(cds2.defects['interstitials'][0]['supercell']['structure'].sites) self.assertEqual(len(cds2.get_ith_supercell_of_defect_type( 0, 'interstitials').sites), nsites) cds3 = ChargedDefectsStructures(self.gaas_struct, include_interstitials=True, standardized=False, # standardized = False is required when manually specifying interstitial, # because dont want base structure lattice to change interstitial_elements=['Mn'], intersites=(isite,)) self.assertEqual(cds3.get_n_defects_of_type('interstitials'), 1) fnames = [i['name'][i['name'].index('_')+3:] for i in cds3.defects['interstitials']] self.assertEqual(sorted(fnames), sorted(['Mn'])) nsites = len(cds3.defects['interstitials'][0]['supercell']['structure'].sites) self.assertEqual(len(cds3.get_ith_supercell_of_defect_type( 0, 'interstitials').sites), nsites)
def get_connected_sites(self, n, jimage=(0, 0, 0)): """ Returns a named tuple of neighbors of site n: periodic_site, jimage, index, weight. Index is the index of the corresponding site in the original structure, weight can be None if not defined. :param n: index of Site in Structure :param jimage: lattice vector of site :return: list of ConnectedSite tuples, sorted by closest first """ connected_sites = set() out_edges = [(u, v, d, 'out') for u, v, d in self.graph.out_edges(n, data=True)] in_edges = [(u, v, d, 'in') for u, v, d in self.graph.in_edges(n, data=True)] for u, v, d, dir in out_edges + in_edges: to_jimage = d['to_jimage'] if dir == 'in': u, v = v, u to_jimage = np.multiply(-1, to_jimage) site_d = self.structure[v].as_dict() site_d['abc'] = np.add(site_d['abc'], to_jimage).tolist() to_jimage = tuple(map(int, np.add(to_jimage, jimage))) periodic_site = PeriodicSite.from_dict(site_d) weight = d.get('weight', None) # from_site if jimage arg != (0, 0, 0) relative_jimage = np.subtract(to_jimage, jimage) dist = self.structure[u].distance(self.structure[v], jimage=relative_jimage) connected_site = ConnectedSite(periodic_site=periodic_site, jimage=to_jimage, index=v, weight=weight, dist=dist) connected_sites.add(connected_site) # return list sorted by closest sites first connected_sites = list(connected_sites) connected_sites.sort(key=lambda x: x.dist) return connected_sites
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 test_convert_cd_to_de(self): #create a ComputedDefect object similar to legacy format # Vacancy type first struc = PymatgenTest.get_structure("VO2") struc.make_supercell(3) vac = Vacancy(struc, struc.sites[0], charge=-3) ids = vac.generate_defect_structure(1) defect_data = { "locpot_path": "defect/path/to/files/LOCPOT", "encut": 520 } bulk_data = {"locpot_path": "bulk/path/to/files/LOCPOT"} cse_defect = ComputedStructureEntry(ids, 100., data=defect_data) cd = ComputedDefect(cse_defect, struc.sites[0], charge=-3, name="Vac_1_O") b_cse = ComputedStructureEntry(struc, 10., data=bulk_data) de = convert_cd_to_de(cd, b_cse) self.assertIsInstance(de.defect, Vacancy) self.assertIsInstance(de, DefectEntry) self.assertEqual(de.parameters["defect_path"], "defect/path/to/files") self.assertEqual(de.parameters["bulk_path"], "bulk/path/to/files") self.assertEqual(de.parameters["encut"], 520) self.assertEqual(de.site.specie.symbol, "O") # try again for substitution type # (site object had bulk specie for ComputedDefects, # while it should have substituional site specie for DefectEntrys...) de_site_type = PeriodicSite("Sb", vac.site.frac_coords, struc.lattice) sub = Substitution(struc, de_site_type, charge=1) ids = sub.generate_defect_structure(1) cse_defect = ComputedStructureEntry(ids, 100., data=defect_data) cd = ComputedDefect(cse_defect, struc.sites[0], charge=1, name="Sub_1_Sb_on_O") de = convert_cd_to_de(cd, b_cse) self.assertIsInstance(de.defect, Substitution) self.assertIsInstance(de, DefectEntry) self.assertEqual(de.site.specie.symbol, "Sb")
def __init__(self, isite, esite, symm_structure): """ Args: isite: Initial site esite: End site symm_structure: SymmetrizedStructure """ self.isite = isite self.esite = esite self.symm_structure = symm_structure self.msite = PeriodicSite(esite.specie, (isite.frac_coords + esite.frac_coords) / 2, esite.lattice) sg = self.symm_structure.spacegroup for i, sites in enumerate(self.symm_structure.equivalent_sites): if sg.are_symmetrically_equivalent([isite], [sites[0]]): self.iindex = i if sg.are_symmetrically_equivalent([esite], [sites[0]]): self.eindex = i
def test_symmetrized_structure(self): t = OrderDisorderedStructureTransformation(symmetrized_structures=True) c = [] sp = [] c.append([0.5, 0.5, 0.5]) sp.append('Si4+') c.append([0.45, 0.45, 0.45]) sp.append({"Si4+": 0.5}) c.append([0.56, 0.56, 0.56]) sp.append({"Si4+": 0.5}) c.append([0.25, 0.75, 0.75]) sp.append({"Si4+": 0.5}) c.append([0.75, 0.25, 0.25]) sp.append({"Si4+": 0.5}) l = Lattice.cubic(5) s = Structure(l, sp, c) test_site = PeriodicSite("Si4+", c[2], l) s = SymmetrizedStructure(s, 'not_real', [0,1,1,2,2]) output = t.apply_transformation(s) self.assertTrue(test_site in output.sites)
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 test_defect_matching(self): # SETUP DEFECTS FOR TESTING # symmorphic defect test set s_struc = Structure.from_file(os.path.join( test_dir, "CsSnI3.cif")) # tetragonal CsSnI3 identical_Cs_vacs = [ Vacancy(s_struc, s_struc[0]), Vacancy(s_struc, s_struc[1]) ] identical_I_vacs_sublattice1 = [ Vacancy(s_struc, s_struc[4]), Vacancy(s_struc, s_struc[5]), Vacancy(s_struc, s_struc[8]), Vacancy(s_struc, s_struc[9]) ] # in plane halides identical_I_vacs_sublattice2 = [ Vacancy(s_struc, s_struc[6]), Vacancy(s_struc, s_struc[7]) ] # out of plane halides pdc = PointDefectComparator() # NOW TEST DEFECTS # test vacancy matching self.assertTrue( pdc.are_equal(identical_Cs_vacs[0], identical_Cs_vacs[0])) # trivial vacancy test self.assertTrue( pdc.are_equal( identical_Cs_vacs[0], identical_Cs_vacs[1])) # vacancies on same sublattice for i, j in itertools.combinations(range(4), 2): self.assertTrue( pdc.are_equal(identical_I_vacs_sublattice1[i], identical_I_vacs_sublattice1[j])) self.assertTrue( pdc.are_equal(identical_I_vacs_sublattice2[0], identical_I_vacs_sublattice2[1])) self.assertFalse( pdc.are_equal( identical_Cs_vacs[ 0], # both vacancies, but different specie types identical_I_vacs_sublattice1[0])) self.assertFalse( pdc.are_equal( identical_I_vacs_sublattice1[ 0], # same specie type, different sublattice identical_I_vacs_sublattice2[0])) # test substitutional matching sub_Cs_on_I_sublattice1_set1 = PeriodicSite( 'Cs', identical_I_vacs_sublattice1[0].site.frac_coords, s_struc.lattice) sub_Cs_on_I_sublattice1_set2 = PeriodicSite( 'Cs', identical_I_vacs_sublattice1[1].site.frac_coords, s_struc.lattice) sub_Cs_on_I_sublattice2 = PeriodicSite( 'Cs', identical_I_vacs_sublattice2[0].site.frac_coords, s_struc.lattice) sub_Rb_on_I_sublattice2 = PeriodicSite( 'Rb', identical_I_vacs_sublattice2[0].site.frac_coords, s_struc.lattice) self.assertTrue( pdc.are_equal( # trivial substitution test Substitution(s_struc, sub_Cs_on_I_sublattice1_set1), Substitution(s_struc, sub_Cs_on_I_sublattice1_set1))) self.assertTrue( pdc.are_equal( # same sublattice, different coords Substitution(s_struc, sub_Cs_on_I_sublattice1_set1), Substitution(s_struc, sub_Cs_on_I_sublattice1_set2))) self.assertFalse( pdc.are_equal( # different subs (wrong specie) Substitution(s_struc, sub_Cs_on_I_sublattice2), Substitution(s_struc, sub_Rb_on_I_sublattice2))) self.assertFalse( pdc.are_equal( # different subs (wrong sublattice) Substitution(s_struc, sub_Cs_on_I_sublattice1_set1), Substitution(s_struc, sub_Cs_on_I_sublattice2))) # test symmorphic interstitial matching # (using set generated from Voronoi generator, with same sublattice given by saturatated_interstitial_structure function) inter_H_sublattice1_set1 = PeriodicSite('H', [0., 0.75, 0.25], s_struc.lattice) inter_H_sublattice1_set2 = PeriodicSite('H', [0., 0.75, 0.75], s_struc.lattice) inter_H_sublattice2 = PeriodicSite( 'H', [0.57796112, 0.06923687, 0.56923687], s_struc.lattice) inter_H_sublattice3 = PeriodicSite('H', [0.25, 0.25, 0.54018268], s_struc.lattice) inter_He_sublattice3 = PeriodicSite('He', [0.25, 0.25, 0.54018268], s_struc.lattice) self.assertTrue( pdc.are_equal( # trivial interstitial test Interstitial(s_struc, inter_H_sublattice1_set1), Interstitial(s_struc, inter_H_sublattice1_set1))) self.assertTrue( pdc.are_equal( # same sublattice, different coords Interstitial(s_struc, inter_H_sublattice1_set1), Interstitial(s_struc, inter_H_sublattice1_set2))) self.assertFalse( pdc.are_equal( # different interstitials (wrong sublattice) Interstitial(s_struc, inter_H_sublattice1_set1), Interstitial(s_struc, inter_H_sublattice2))) self.assertFalse( pdc.are_equal( # different interstitials (wrong sublattice) Interstitial(s_struc, inter_H_sublattice1_set1), Interstitial(s_struc, inter_H_sublattice3))) self.assertFalse( pdc.are_equal( # different interstitials (wrong specie) Interstitial(s_struc, inter_H_sublattice3), Interstitial(s_struc, inter_He_sublattice3))) # test non-symmorphic interstitial matching # (using set generated from Voronoi generator, with same sublattice given by saturatated_interstitial_structure function) ns_struc = Structure.from_file(os.path.join(test_dir, "CuCl.cif")) ns_inter_H_sublattice1_set1 = PeriodicSite( 'H', [0.06924513, 0.06308959, 0.86766528], ns_struc.lattice) ns_inter_H_sublattice1_set2 = PeriodicSite( 'H', [0.43691041, 0.36766528, 0.06924513], ns_struc.lattice) ns_inter_H_sublattice2 = PeriodicSite( 'H', [0.06022109, 0.60196031, 0.1621814], ns_struc.lattice) ns_inter_He_sublattice2 = PeriodicSite( 'He', [0.06022109, 0.60196031, 0.1621814], ns_struc.lattice) self.assertTrue( pdc.are_equal( # trivial interstitial test Interstitial(ns_struc, ns_inter_H_sublattice1_set1), Interstitial(ns_struc, ns_inter_H_sublattice1_set1))) self.assertTrue( pdc.are_equal( # same sublattice, different coords Interstitial(ns_struc, ns_inter_H_sublattice1_set1), Interstitial(ns_struc, ns_inter_H_sublattice1_set2))) self.assertFalse( pdc.are_equal( Interstitial(ns_struc, ns_inter_H_sublattice1_set1 ), # different interstitials (wrong sublattice) Interstitial(ns_struc, ns_inter_H_sublattice2))) self.assertFalse( pdc.are_equal( # different interstitials (wrong specie) Interstitial(ns_struc, ns_inter_H_sublattice2), Interstitial(ns_struc, ns_inter_He_sublattice2))) # test influence of charge on defect matching (default is to be charge agnostic) vac_diff_chg = identical_Cs_vacs[0].copy() vac_diff_chg.set_charge(3.) self.assertTrue(pdc.are_equal(identical_Cs_vacs[0], vac_diff_chg)) chargecheck_pdc = PointDefectComparator( check_charge=True) # switch to PDC which cares about charge state self.assertFalse( chargecheck_pdc.are_equal(identical_Cs_vacs[0], vac_diff_chg)) # test different supercell size # (comparing same defect but different supercells - default is to not check for this) sc_agnostic_pdc = PointDefectComparator(check_primitive_cell=True) sc_scaled_s_struc = s_struc.copy() sc_scaled_s_struc.make_supercell([2, 2, 3]) sc_scaled_I_vac_sublatt1_ps1 = PeriodicSite( 'I', identical_I_vacs_sublattice1[0].site.coords, sc_scaled_s_struc.lattice, coords_are_cartesian=True) sc_scaled_I_vac_sublatt1_ps2 = PeriodicSite( 'I', identical_I_vacs_sublattice1[1].site.coords, sc_scaled_s_struc.lattice, coords_are_cartesian=True) sc_scaled_I_vac_sublatt2_ps = PeriodicSite( 'I', identical_I_vacs_sublattice2[1].site.coords, sc_scaled_s_struc.lattice, coords_are_cartesian=True) sc_scaled_I_vac_sublatt1_defect1 = Vacancy( sc_scaled_s_struc, sc_scaled_I_vac_sublatt1_ps1) sc_scaled_I_vac_sublatt1_defect2 = Vacancy( sc_scaled_s_struc, sc_scaled_I_vac_sublatt1_ps2) sc_scaled_I_vac_sublatt2_defect = Vacancy(sc_scaled_s_struc, sc_scaled_I_vac_sublatt2_ps) self.assertFalse( pdc.are_equal( identical_I_vacs_sublattice1[ 0], # trivially same defect site but between different supercells sc_scaled_I_vac_sublatt1_defect1)) self.assertTrue( sc_agnostic_pdc.are_equal(identical_I_vacs_sublattice1[0], sc_scaled_I_vac_sublatt1_defect1)) self.assertFalse( pdc.are_equal( identical_I_vacs_sublattice1[ 1], # same coords, different lattice structure sc_scaled_I_vac_sublatt1_defect1)) self.assertTrue( sc_agnostic_pdc.are_equal(identical_I_vacs_sublattice1[1], sc_scaled_I_vac_sublatt1_defect1)) self.assertFalse( pdc.are_equal( identical_I_vacs_sublattice1[ 0], # same sublattice, different coords sc_scaled_I_vac_sublatt1_defect2)) self.assertTrue( sc_agnostic_pdc.are_equal(identical_I_vacs_sublattice1[0], sc_scaled_I_vac_sublatt1_defect2)) self.assertFalse( sc_agnostic_pdc.are_equal( identical_I_vacs_sublattice1[ 0], # different defects (wrong sublattice) sc_scaled_I_vac_sublatt2_defect)) # test same structure size, but scaled lattice volume # (default is to not allow these to be equal, but check_lattice_scale=True allows for this) vol_agnostic_pdc = PointDefectComparator(check_lattice_scale=True) vol_scaled_s_struc = s_struc.copy() vol_scaled_s_struc.scale_lattice(s_struc.volume * 0.95) vol_scaled_I_vac_sublatt1_defect1 = Vacancy(vol_scaled_s_struc, vol_scaled_s_struc[4]) vol_scaled_I_vac_sublatt1_defect2 = Vacancy(vol_scaled_s_struc, vol_scaled_s_struc[5]) vol_scaled_I_vac_sublatt2_defect = Vacancy(vol_scaled_s_struc, vol_scaled_s_struc[6]) self.assertFalse( pdc.are_equal( identical_I_vacs_sublattice1[ 0], # trivially same defect (but vol change) vol_scaled_I_vac_sublatt1_defect1)) self.assertTrue( vol_agnostic_pdc.are_equal(identical_I_vacs_sublattice1[0], vol_scaled_I_vac_sublatt1_defect1)) self.assertFalse( pdc.are_equal( identical_I_vacs_sublattice1[ 0], # same defect, different sublattice point (and vol change) vol_scaled_I_vac_sublatt1_defect2)) self.assertTrue( vol_agnostic_pdc.are_equal(identical_I_vacs_sublattice1[0], vol_scaled_I_vac_sublatt1_defect2)) self.assertFalse( vol_agnostic_pdc.are_equal( identical_I_vacs_sublattice1[ 0], # different defect (wrong sublattice) vol_scaled_I_vac_sublatt2_defect)) # test identical defect which has had entire lattice shifted shift_s_struc = s_struc.copy() shift_s_struc.translate_sites(range(len(s_struc)), [0.2, 0.3, 0.4], frac_coords=True, to_unit_cell=True) shifted_identical_Cs_vacs = [ Vacancy(shift_s_struc, shift_s_struc[0]), Vacancy(shift_s_struc, shift_s_struc[1]) ] self.assertTrue( pdc.are_equal( identical_Cs_vacs[0], # trivially same defect (but shifted) shifted_identical_Cs_vacs[0])) self.assertTrue( pdc.are_equal( identical_Cs_vacs[ 0], # same defect on different sublattice point (and shifted) shifted_identical_Cs_vacs[1])) # test uniform lattice shift within non-symmorphic structure shift_ns_struc = ns_struc.copy() shift_ns_struc.translate_sites(range(len(ns_struc)), [0., 0.6, 0.3], frac_coords=True, to_unit_cell=True) shift_ns_inter_H_sublattice1_set1 = PeriodicSite( 'H', ns_inter_H_sublattice1_set1.frac_coords + [0., 0.6, 0.3], shift_ns_struc.lattice) shift_ns_inter_H_sublattice1_set2 = PeriodicSite( 'H', ns_inter_H_sublattice1_set2.frac_coords + [0., 0.6, 0.3], shift_ns_struc.lattice) self.assertTrue( pdc.are_equal( Interstitial(ns_struc, ns_inter_H_sublattice1_set1 ), # trivially same defect (but shifted) Interstitial(shift_ns_struc, shift_ns_inter_H_sublattice1_set1))) self.assertTrue( pdc.are_equal( Interstitial(ns_struc, ns_inter_H_sublattice1_set1), # same defect on different sublattice point (and shifted) Interstitial(shift_ns_struc, shift_ns_inter_H_sublattice1_set2))) # test a rotational + supercell type structure transformation (requires check_primitive_cell=True) rotated_s_struc = s_struc.copy() rotated_s_struc.make_supercell([[2, 1, 0], [-1, 3, 0], [0, 0, 2]]) rotated_identical_Cs_vacs = [ Vacancy(rotated_s_struc, rotated_s_struc[0]), Vacancy(rotated_s_struc, rotated_s_struc[1]) ] self.assertFalse( pdc.are_equal( identical_Cs_vacs[0], # trivially same defect (but rotated) rotated_identical_Cs_vacs[0])) self.assertTrue( sc_agnostic_pdc.are_equal(identical_Cs_vacs[0], rotated_identical_Cs_vacs[0])) self.assertFalse( pdc.are_equal( identical_Cs_vacs[ 0], # same defect on different sublattice (and rotated) rotated_identical_Cs_vacs[1])) self.assertTrue( sc_agnostic_pdc.are_equal( identical_Cs_vacs[ 0], # same defect on different sublattice point (and rotated) rotated_identical_Cs_vacs[1])) # test a rotational + supercell + shift type structure transformation for non-symmorphic structure rotANDshift_ns_struc = ns_struc.copy() rotANDshift_ns_struc.translate_sites(range(len(ns_struc)), [0., 0.6, 0.3], frac_coords=True, to_unit_cell=True) rotANDshift_ns_struc.make_supercell([[2, 1, 0], [-1, 3, 0], [0, 0, 2]]) ns_vac_Cs_set1 = Vacancy(ns_struc, ns_struc[0]) rotANDshift_ns_vac_Cs_set1 = Vacancy(rotANDshift_ns_struc, rotANDshift_ns_struc[0]) rotANDshift_ns_vac_Cs_set2 = Vacancy(rotANDshift_ns_struc, rotANDshift_ns_struc[1]) self.assertTrue( sc_agnostic_pdc.are_equal( ns_vac_Cs_set1, # trivially same defect (but rotated and sublattice shifted) rotANDshift_ns_vac_Cs_set1)) self.assertTrue( sc_agnostic_pdc.are_equal( ns_vac_Cs_set1, # same defect on different sublattice point (shifted and rotated) rotANDshift_ns_vac_Cs_set2))
def test_check_final_relaxed_structure_delocalized(self): # test structure delocalization analysis # first test no movement in atoms initial_defect_structure = self.vac.generate_defect_structure() final_defect_structure = initial_defect_structure.copy() sampling_radius = 4.55 defect_frac_sc_coords = self.vac.site.frac_coords[:] params = { "initial_defect_structure": initial_defect_structure, "final_defect_structure": final_defect_structure, "sampling_radius": sampling_radius, "defect_frac_sc_coords": defect_frac_sc_coords, "is_compatible": True, } dentry = DefectEntry(self.vac, 0.0, corrections={}, parameters=params, entry_id=None) dc = DefectCompatibility(tot_relax_tol=0.1, perc_relax_tol=0.1, defect_tot_relax_tol=0.1) dentry = dc.check_final_relaxed_structure_delocalized(dentry) struc_delocal = dentry.parameters["delocalization_meta"]["structure_relax"] self.assertTrue(dentry.parameters["is_compatible"]) self.assertTrue(struc_delocal["is_compatible"]) self.assertTrue(struc_delocal["metadata"]["structure_tot_relax_compatible"]) self.assertEqual(struc_delocal["metadata"]["tot_relax_outside_rad"], 0.0) self.assertTrue(struc_delocal["metadata"]["structure_perc_relax_compatible"]) self.assertEqual(struc_delocal["metadata"]["perc_relax_outside_rad"], 0.0) self.assertEqual( len(struc_delocal["metadata"]["full_structure_relax_data"]), len(initial_defect_structure), ) self.assertIsNone(struc_delocal["metadata"]["defect_index"]) defect_delocal = dentry.parameters["delocalization_meta"]["defectsite_relax"] self.assertTrue(defect_delocal["is_compatible"]) self.assertIsNone(defect_delocal["metadata"]["relax_amount"]) # next test for when structure has delocalized outside of radius from defect pert_struct_fin_struct = initial_defect_structure.copy() pert_struct_fin_struct.perturb(0.1) dentry.parameters.update({"final_defect_structure": pert_struct_fin_struct}) dentry = dc.check_final_relaxed_structure_delocalized(dentry) struc_delocal = dentry.parameters["delocalization_meta"]["structure_relax"] self.assertFalse(dentry.parameters["is_compatible"]) self.assertFalse(struc_delocal["is_compatible"]) self.assertFalse(struc_delocal["metadata"]["structure_tot_relax_compatible"]) self.assertAlmostEqual(struc_delocal["metadata"]["tot_relax_outside_rad"], 12.5) self.assertFalse(struc_delocal["metadata"]["structure_perc_relax_compatible"]) self.assertAlmostEqual(struc_delocal["metadata"]["perc_relax_outside_rad"], 77.63975155) # now test for when an interstitial defect has migrated too much inter_def_site = PeriodicSite( "H", [7.58857304, 11.70848069, 12.97817518], self.vac.bulk_structure.lattice, to_unit_cell=True, coords_are_cartesian=True, ) inter = Interstitial(self.vac.bulk_structure, inter_def_site, charge=0) initial_defect_structure = inter.generate_defect_structure() final_defect_structure = initial_defect_structure.copy() poss_deflist = sorted( final_defect_structure.get_sites_in_sphere(inter.site.coords, 2, include_index=True), key=lambda x: x[1], ) def_index = poss_deflist[0][2] final_defect_structure.translate_sites( indices=[def_index], vector=[0.0, 0.0, 0.008] ) # fractional coords translation defect_frac_sc_coords = inter_def_site.frac_coords[:] params = { "initial_defect_structure": initial_defect_structure, "final_defect_structure": final_defect_structure, "sampling_radius": sampling_radius, "defect_frac_sc_coords": defect_frac_sc_coords, "is_compatible": True, } dentry = DefectEntry(inter, 0.0, corrections={}, parameters=params, entry_id=None) dentry = dc.check_final_relaxed_structure_delocalized(dentry) defect_delocal = dentry.parameters["delocalization_meta"]["defectsite_relax"] self.assertFalse(defect_delocal["is_compatible"]) self.assertAlmostEqual(defect_delocal["metadata"]["relax_amount"], 0.10836054)
def folded_coords(site: PeriodicSite, center: GenCoords) -> GenCoords: _, image = site.distance_and_image_from_frac_coords(center) return tuple(site.frac_coords - image)
def evaluate_from_coordinates(self, structure_name, supercell, min_distance=2.0, energy_precision=6, energy_threshold=0.00, numpy_array=True): """Calculates the average energies/ minimum energy/ boltzmann average energies/ list of energies Args: structure_name (str): name of the structure as referenced in the cif file in the Raspa directory supercell (list): list of ints representing the size of each unicell vector for the supercell system of Raspa2 and Zeo++ (different from the one used in pymatgen) Returns: """ if not os.path.exists("Coordinates"): raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), "Coordinates") coord_path = os.path.join("Coordinates", structure_name + '.csv') if not os.path.exists(coord_path): return np.nan, np.nan, np.nan df_voro = pd.read_csv(coord_path, sep=" ") df_voro = df_voro[df_voro["dist_to_nearest"] > min_distance] if len(df_voro) == 0: return np.nan, np.nan, np.nan cif_file = self._load_cif_file(structure_name, self.RASPA_DIR) structure = CifParser.from_string(cif_file).get_structures( primitive=False)[0] structure.make_supercell(supercell) lattice_matrix = self._frac_to_cart_matrix(structure) inv_lattice_matrix = np.linalg.inv(lattice_matrix) df_voro["cartesian_coordinates"] = df_voro.apply( lambda row: np.array([float( row.x), float(row.y), float(row.z)]), axis=1) df_voro["fractional_coordinates"] = df_voro[ "cartesian_coordinates"].to_numpy().dot(inv_lattice_matrix) accessible_mean_energy, min_energy, boltz_energy = [], [], [] for atom_type in self.atoms: df_voro["pymat_site_%s" % atom_type] = df_voro["fractional_coordinates"].apply( lambda frac_coord: PeriodicSite( {atom_type: 1}, frac_coord, structure.lattice)) if numpy_array: df_voro["Energy_%s" % atom_type] = df_voro[ "pymat_site_%s" % atom_type].apply( lambda site: self.lennard_jones_from_pymatgen_np( site, structure)) else: df_voro["Energy_%s" % atom_type] = df_voro[ "pymat_site_%s" % atom_type].apply( lambda site: self.lennard_jones_from_pymatgen( site, structure)) accessible_mean_energy.append( round( df_voro[df_voro["Energy_%s" % atom_type] < energy_threshold]["Energy_%s" % atom_type].mean(), energy_precision)) min_energy.append( round(df_voro["Energy_%s" % atom_type].min(), energy_precision)) df_voro["exp_energy_%s" % atom_type] = np.exp(-df_voro["Energy_%s" % atom_type] / (self.R * self.temperature)) Sum_exp = df_voro["exp_energy_%s" % atom_type].sum() df_voro["pond_energy_%s" % atom_type] = df_voro[ "exp_energy_%s" % atom_type] * df_voro["Energy_%s" % atom_type] / Sum_exp boltz_energy.append( round(df_voro["pond_energy_%s" % atom_type].sum(), energy_precision)) df_voro.to_csv('Energies/%s.csv' % structure_name, index=False) return accessible_mean_energy, min_energy, boltz_energy
def is_final_relaxed_structure_delocalized(self, defect_entry): structure_relax_analyze_meta = {} initial_defect_structure = defect_entry.parameters[ 'initial_defect_structure'] final_defect_structure = defect_entry.parameters[ 'final_defect_structure'] radius_to_sample = defect_entry.parameters['sampling_radius'] #determine the defect index within the structure and append fractional_coordinates if not isinstance(defect_entry.defect, Vacancy): poss_deflist = sorted(initial_defect_structure.get_sites_in_sphere( defect_entry.defect.site.coords, 2, include_index=True), key=lambda x: x[1]) defindex = poss_deflist[0][2] def_frac_coords = poss_deflist[0][0].frac_coords else: #if vacancy than create periodic site for finding distance from other atoms to defect defindex = None vac_site = PeriodicSite('H', defect_entry.defect.site.coords, initial_defect_structure.lattice, to_unit_cell=True, coords_are_cartesian=True) def_frac_coords = vac_site.frac_coords initsites = [site.frac_coords for site in initial_defect_structure] finalsites = [site.frac_coords for site in final_defect_structure] distmatrix = initial_defect_structure.lattice.get_all_distances( finalsites, initsites) #calculate distance moved as a function of the distance from the defect distdata = [] totpert = 0. for ind in range(len(initsites)): if ind == defindex: continue else: totpert += distmatrix[ind, ind] # append [distance to defect, distance traveled, index in structure] distance_to_defect = initial_defect_structure.lattice.get_distance_and_image( def_frac_coords, initsites[ind])[0] distdata.append( [distance_to_defect, distmatrix[ind, ind], ind]) distdata.sort() tot_relax_outside_rad = 0. perc_relax_outside_rad = 0. for newind in range(len(distdata)): perc_relax = 100 * distdata[newind][1] / totpert if totpert else 0. distdata[newind].append( perc_relax) # percentage contribution to total relaxation if distdata[newind][0] > radius_to_sample: tot_relax_outside_rad += distdata[newind][1] perc_relax_outside_rad += distdata[newind][3] structure_tot_relax_compatible = True if tot_relax_outside_rad <= self.tot_relax_tol else False structure_perc_relax_compatible = True if perc_relax_outside_rad <= self.perc_relax_tol else False structure_relax_analyze_meta.update({ 'structure_tot_relax_compatible': structure_tot_relax_compatible, 'tot_relax_outside_rad': tot_relax_outside_rad, 'tot_relax_tol': self.tot_relax_tol, 'structure_perc_relax_compatible': structure_perc_relax_compatible, 'perc_relax_outside_rad': perc_relax_outside_rad, 'perc_relax_tol': self.perc_relax_tol, 'full_structure_relax_data': distdata, 'defect_index': defindex }) structure_relax_allows_compatible = True if ( structure_tot_relax_compatible and structure_perc_relax_compatible) else False #NEXT: do single defect delocalization analysis (requires similar data, so might as well run in tandem # with structural delocalization defectsite_relax_analyze_meta = {} if isinstance(defect_entry.defect, Vacancy): defectsite_relax_allows_compatible = True defectsite_relax_analyze_meta.update({ 'relax_amount': None, 'defect_tot_relax_tol': self.defect_tot_relax_tol }) else: defect_relax_amount = distmatrix[defindex, defindex] defectsite_relax_allows_compatible = True if defect_relax_amount <= self.defect_tot_relax_tol else False defectsite_relax_analyze_meta.update({ 'relax_amount': defect_relax_amount, 'defect_tot_relax_tol': self.defect_tot_relax_tol }) if 'delocalization_meta' not in defect_entry.parameters.keys(): defect_entry.parameters['delocalization_meta'] = {} defect_entry.parameters['delocalization_meta'].update({ 'defectsite_relax': { 'is_compatible': defectsite_relax_allows_compatible, 'metadata': defectsite_relax_analyze_meta } }) defect_entry.parameters['delocalization_meta'].update({ 'structure_relax': { 'is_compatible': structure_relax_allows_compatible, 'metadata': structure_relax_analyze_meta } }) if (not structure_relax_allows_compatible) or ( not defectsite_relax_allows_compatible): defect_entry.parameters.update({'is_compatible': False}) return defect_entry
def parse_defect_calculations(self): """ Parses the defect calculations as DefectEntry objects, from a PyCDT root_fldr file structure. Charge correction is missing in the first run. """ logger = logging.getLogger(__name__) parsed_defects = [] subfolders = glob.glob(os.path.join(self._root_fldr, "vac_*")) subfolders += glob.glob(os.path.join(self._root_fldr, "as_*")) subfolders += glob.glob(os.path.join(self._root_fldr, "sub_*")) subfolders += glob.glob(os.path.join(self._root_fldr, "inter_*")) def get_vr_and_check_locpot(fldr): vr_file = os.path.join(fldr,"vasprun.xml") if not os.path.exists(vr_file): logger.warning("{} doesn't exit".format(vr_file)) error_msg = ": Failure, vasprun.xml doesn't exist." return (None, error_msg) #Further processing is not useful try: vr = Vasprun(vr_file, parse_potcar_file=False) except: logger.warning("Couldn't parse {}".format(vr_file)) error_msg = ": Failure, couldn't parse vaprun.xml file." return (None, error_msg) if not vr.converged: logger.warning( "Vasp calculation at {} not converged".format(fldr)) error_msg = ": Failure, Vasp calculation not converged." return (None, error_msg) # Further processing is not useful # Check if locpot exists locpot_file = os.path.join(fldr, "LOCPOT") if not os.path.exists(locpot_file): logger.warning("{} doesn't exit".format(locpot_file)) error_msg = ": Failure, LOCPOT doesn't exist" return (None, error_msg) #Further processing is not useful return (vr, None) def get_encut_from_potcar(fldr): potcar_file = os.path.join(fldr,"POTCAR") if not os.path.exists(potcar_file): logger.warning("Not POTCAR in {} to parse ENCUT".format(fldr)) error_msg = ": Failure, No POTCAR file." return (None, error_msg) #Further processing is not useful try: potcar = Potcar.from_file(potcar_file) except: logger.warning("Couldn't parse {}".format(potcar_file)) error_msg = ": Failure, couldn't read POTCAR file." return (None, error_msg) encut = max(ptcr_sngl.enmax for ptcr_sngl in potcar) return (encut, None) # get bulk entry information first fldr = os.path.join(self._root_fldr, "bulk") vr, error_msg = get_vr_and_check_locpot(fldr) if error_msg: logger.error("Abandoning parsing of the calculations") return {} bulk_energy = vr.final_energy bulk_sc_struct = vr.final_structure try: encut = vr.incar["ENCUT"] except: # ENCUT not specified in INCAR. Read from POTCAR encut, error_msg = get_encut_from_potcar(fldr) if error_msg: logger.error("Abandoning parsing of the calculations") return {} trans_dict = loadfn( os.path.join(fldr, "transformation.json"), cls=MontyDecoder) supercell_size = trans_dict["supercell"] bulk_file_path = fldr bulk_entry = ComputedStructureEntry( bulk_sc_struct, bulk_energy, data={"bulk_path": bulk_file_path, "encut": encut, "supercell_size": supercell_size}) # get defect entry information for fldr in subfolders: fldr_name = os.path.split(fldr)[1] chrg_fldrs = glob.glob(os.path.join(fldr,"charge*")) for chrg_fldr in chrg_fldrs: trans_dict = loadfn( os.path.join(chrg_fldr, "transformation.json"), cls=MontyDecoder) chrg = trans_dict["charge"] vr, error_msg = get_vr_and_check_locpot(chrg_fldr) if error_msg: logger.warning("Parsing the rest of the calculations") continue if "substitution_specie" in trans_dict and \ trans_dict["substitution_specie"] not in bulk_sc_struct.symbol_set: self._substitution_species.add( trans_dict["substitution_specie"]) elif "inter" in trans_dict["defect_type"] and \ trans_dict["defect_site"].specie.symbol not in bulk_sc_struct.symbol_set: # added because extrinsic interstitials don't have # "substitution_specie" character... trans_dict["substitution_specie"] = trans_dict["defect_site"].specie.symbol self._substitution_species.add( trans_dict["defect_site"].specie.symbol) defect_type = trans_dict.get("defect_type", None) energy = vr.final_energy try: encut = vr.incar["ENCUT"] except: # ENCUT not specified in INCAR. Read from POTCAR encut, error_msg = get_encut_from_potcar(chrg_fldr) if error_msg: logger.warning("Not able to determine ENCUT " "in {}".format(fldr_name)) logger.warning("Parsing the rest of the " "calculations") continue comp_data = {"bulk_path": bulk_file_path, "defect_path": chrg_fldr, "encut": encut, "fldr_name": fldr_name, "supercell_size": supercell_size} if "substitution_specie" in trans_dict: comp_data["substitution_specie"] = \ trans_dict["substitution_specie"] # create Defect Object as dict, then load to DefectEntry object defect_dict = {"structure": bulk_sc_struct, "charge": chrg, "@module": "pymatgen.analysis.defects.core" } defect_site = trans_dict["defect_supercell_site"] if "vac_" in defect_type: defect_dict["@class"] = "Vacancy" elif "as_" in defect_type or "sub_" in defect_type: defect_dict["@class"] = "Substitution" substitution_specie = trans_dict["substitution_specie"] defect_site = PeriodicSite( substitution_specie, defect_site.frac_coords, defect_site.lattice, coords_are_cartesian=False) elif "int_" in defect_type: defect_dict["@class"] = "Interstitial" else: raise ValueError("defect type {} not recognized...".format(defect_type)) defect_dict.update( {"defect_site": defect_site}) defect = MontyDecoder().process_decoded( defect_dict) parsed_defects.append( DefectEntry( defect, energy - bulk_energy, parameters=comp_data)) try: parsed_defects_data = {} parsed_defects_data["bulk_entry"] = bulk_entry parsed_defects_data["defects"] = parsed_defects return parsed_defects_data except: return {} # Return Null dict due to failure
def get_primitive_standard_structure(self, international_monoclinic=True): """ Gives a structure with a primitive cell according to certain standards the standards are defined in Setyawan, W., & Curtarolo, S. (2010). High-throughput electronic band structure calculations: Challenges and tools. Computational Materials Science, 49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010 Returns: The structure in a primitive standardized cell """ if hasattr(self, "_conv"): conv = self._conv else: conv = self.get_conventional_standard_structure( international_monoclinic=international_monoclinic) lattice = self.lattice_type if "P" in self.spg_symbol or lattice == "hexagonal": return conv transf = self.get_conventional_to_primitive_transformation_matrix( international_monoclinic=international_monoclinic) new_sites = [] latt = Lattice(np.dot(transf, conv.lattice.matrix)) for s in conv: new_s = PeriodicSite(s.specie, s.coords, latt, to_unit_cell=True, coords_are_cartesian=True, properties=s.properties) if not any(map(new_s.is_periodic_image, new_sites)): new_sites.append(new_s) if lattice == "rhombohedral": prim = Structure.from_sites(new_sites) lengths = prim.lattice.lengths angles = prim.lattice.angles a = lengths[0] alpha = math.pi * angles[0] / 180 new_matrix = [[a * cos(alpha / 2), -a * sin(alpha / 2), 0], [a * cos(alpha / 2), a * sin(alpha / 2), 0], [ a * cos(alpha) / cos(alpha / 2), 0, a * math.sqrt(1 - (cos(alpha)**2 / (cos(alpha / 2)**2))) ]] new_sites = [] latt = Lattice(new_matrix) for s in prim: new_s = PeriodicSite(s.specie, s.frac_coords, latt, to_unit_cell=True, properties=s.properties) if not any(map(new_s.is_periodic_image, new_sites)): new_sites.append(new_s) return Structure.from_sites(new_sites) return Structure.from_sites(new_sites)
def run( self, maxiter=1000, tol=1e-5, gtol=1e-3, step_size=0.05, max_disp=0.05, spring_const=5.0, species=None, ): """ Perform iterative minimization of the set of objective functions in an NEB-like manner. In each iteration, the total force matrix for each image is constructed, which comprises both the spring forces and true forces. For more details about the NEB approach, please see the references, e.g. Henkelman et al., J. Chem. Phys. 113, 9901 (2000). Args: maxiter (int): Maximum number of iterations in the minimization process. tol (float): Tolerance of the change of objective functions between consecutive steps. gtol (float): Tolerance of maximum force component (absolute value). step_size (float): Step size associated with the displacement of the atoms during the minimization process. max_disp (float): Maximum allowed atomic displacement in each iteration. spring_const (float): A virtual spring constant used in the NEB-like relaxation process that yields so-called IDPP path. species (list of string): If provided, only those given species are allowed to move. The atomic positions of other species are obtained via regular linear interpolation approach. Returns: [Structure] Complete IDPP path (including end-point structures) """ coords = self.init_coords.copy() old_funcs = np.zeros((self.nimages,), dtype=np.float64) idpp_structures = [self.structures[0]] if species is None: indices = list(range(len(self.structures[0]))) else: species = [get_el_sp(sp) for sp in species] indices = [ i for i, site in enumerate(self.structures[0]) if site.specie in species ] if len(indices) == 0: raise ValueError("The given species are not in the system!") # Iterative minimization for n in range(maxiter): # Get the sets of objective functions, true and total force # matrices. funcs, true_forces = self._get_funcs_and_forces(coords) tot_forces = self._get_total_forces( coords, true_forces, spring_const=spring_const ) # Each atom is allowed to move up to max_disp disp_mat = step_size * tot_forces[:, indices, :] disp_mat = np.where( np.abs(disp_mat) > max_disp, np.sign(disp_mat) * max_disp, disp_mat ) coords[1 : (self.nimages + 1), indices] += disp_mat max_force = np.abs(tot_forces[:, indices, :]).max() tot_res = np.sum(np.abs(old_funcs - funcs)) if tot_res < tol and max_force < gtol: break old_funcs = funcs else: warnings.warn( "Maximum iteration number is reached without convergence!", UserWarning ) for ni in range(self.nimages): # generate the improved image structure new_sites = [] for site, cart_coords in zip(self.structures[ni + 1], coords[ni + 1]): new_site = PeriodicSite( site.species, coords=cart_coords, lattice=site.lattice, coords_are_cartesian=True, properties=site.properties, ) new_sites.append(new_site) idpp_structures.append(Structure.from_sites(new_sites)) # Also include end-point structure. idpp_structures.append(self.structures[-1]) return idpp_structures
def get_all_neighbors_and_image(structure, r, include_index=False): """ Modified from `pymatgen <http://pymatgen.org/_modules/pymatgen/core/structure.html#IStructure.get_all_neighbors>`_ to return image (used for mapping to supercell), and to use the f2py wrapped OpenMP dist subroutine to get the distances (smaller memory footprint and faster than numpy). Get neighbours for each atom in the unit cell, out to a distance r Returns a list of list of neighbors for each site in structure. Use this method if you are planning on looping over all sites in the crystal. If you only want neighbors for a particular site, use the method get_neighbors as it may not have to build such a large supercell However if you are looping over all sites in the crystal, this method is more efficient since it only performs one pass over a large enough supercell to contain all possible atoms out to a distance r. The return type is a [(site, dist) ...] since most of the time, subsequent processing requires the distance. Args: - r (float): Radius of sphere. - include_index (bool): Whether to include the non-supercell site - in the returned data Returns: - A list of a list of nearest neighbors for each site, i.e., [[(site, dist, index, image) ...], ..]. Index only supplied if include_index = True. The index is the index of the site in the original (non-supercell) structure. This is needed for ewaldmatrix by keeping track of which sites contribute to the ewald sum. """ recp_len = np.array(structure.lattice.reciprocal_lattice.abc) maxr = np.ceil((r + 0.15) * recp_len / (2 * math.pi)) nmin = np.floor(np.min(structure.frac_coords, axis=0)) - maxr nmax = np.ceil(np.max(structure.frac_coords, axis=0)) + maxr all_ranges = [np.arange(x, y) for x, y in zip(nmin, nmax)] latt = structure._lattice neighbors = [list() for i in range(len(structure._sites))] all_fcoords = np.mod(structure.frac_coords, 1) coords_in_cell = latt.get_cartesian_coords(all_fcoords) site_coords = structure.cart_coords indices = np.arange(len(structure)) for image in itertools.product(*all_ranges): coords = latt.get_cartesian_coords(image) + coords_in_cell all_dists = dist.dist(coords, site_coords, len(coords)) all_within_r = np.bitwise_and(all_dists <= r, all_dists > 1e-8) for (j, d, within_r) in zip(indices, all_dists, all_within_r): nnsite = PeriodicSite( structure[j].specie, coords[j], latt, properties=structure[j].properties, coords_are_cartesian=True, ) for i in indices[within_r]: item = (nnsite, d[i], j, image) if include_index else (nnsite, d[i]) neighbors[i].append(item) return neighbors
def __mul__(self, scaling_matrix): """ Replicates the graph, creating a supercell, intelligently joining together edges that lie on periodic boundaries. In principle, any operations on the expanded graph could also be done on the original graph, but a larger graph can be easier to visualize and reason about. :param scaling_matrix: same as Structure.__mul__ :return: """ # Developer note: a different approach was also trialed, using # a simple Graph (instead of MultiDiGraph), with node indices # representing both site index and periodic image. Here, the # number of nodes != number of sites in the Structure. This # approach has many benefits, but made it more difficult to # keep the graph in sync with its corresponding Structure. # Broadly, it would be easier to multiply the Structure # *before* generating the StructureGraph, but this isn't # possible when generating the graph using critic2 from # charge density. # Multiplication works by looking for the expected position # of an image node, and seeing if that node exists in the # supercell. If it does, the edge is updated. This is more # computationally expensive than just keeping track of the # which new lattice images present, but should hopefully be # easier to extend to a general 3x3 scaling matrix. # code adapted from Structure.__mul__ scale_matrix = np.array(scaling_matrix, np.int16) if scale_matrix.shape != (3, 3): scale_matrix = np.array(scale_matrix * np.eye(3), np.int16) else: # TODO: test __mul__ with full 3x3 scaling matrices raise NotImplementedError('Not tested with 3x3 scaling matrices yet.') new_lattice = Lattice(np.dot(scale_matrix, self.structure.lattice.matrix)) f_lat = lattice_points_in_supercell(scale_matrix) c_lat = new_lattice.get_cartesian_coords(f_lat) new_sites = [] new_graphs = [] for v in c_lat: # create a map of nodes from original graph to its image mapping = {n: n + len(new_sites) for n in range(len(self.structure))} for idx, site in enumerate(self.structure): s = PeriodicSite(site.species_and_occu, site.coords + v, new_lattice, properties=site.properties, coords_are_cartesian=True, to_unit_cell=False) new_sites.append(s) new_graphs.append(nx.relabel_nodes(self.graph, mapping, copy=True)) new_structure = Structure.from_sites(new_sites) # merge all graphs into one big graph new_g = nx.MultiDiGraph() for new_graph in new_graphs: new_g = nx.union(new_g, new_graph) edges_to_remove = [] # tuple of (u, v, k) edges_to_add = [] # tuple of (u, v, attr_dict) # list of new edges inside supercell # for duplicate checking edges_inside_supercell = [{u, v} for u, v, d in new_g.edges(data=True) if d['to_jimage'] == (0, 0, 0)] new_periodic_images = [] orig_lattice = self.structure.lattice # use k-d tree to match given position to an # existing Site in Structure kd_tree = KDTree(new_structure.cart_coords) # tolerance in Å for sites to be considered equal # this could probably be a lot smaller tol = 0.05 for u, v, k, d in new_g.edges(keys=True, data=True): to_jimage = d['to_jimage'] # for node v # reduce unnecessary checking if to_jimage != (0, 0, 0): # get index in original site n_u = u % len(self.structure) n_v = v % len(self.structure) # get fractional co-ordinates of where atoms defined # by edge are expected to be, relative to original # lattice (keeping original lattice has # significant benefits) v_image_frac = np.add(self.structure[n_v].frac_coords, to_jimage) u_frac = self.structure[n_u].frac_coords # using the position of node u as a reference, # get relative Cartesian co-ordinates of where # atoms defined by edge are expected to be v_image_cart = orig_lattice.get_cartesian_coords(v_image_frac) u_cart = orig_lattice.get_cartesian_coords(u_frac) v_rel = np.subtract(v_image_cart, u_cart) # now retrieve position of node v in # new supercell, and get absolute Cartesian # co-ordinates of where atoms defined by edge # are expected to be v_expec = new_structure[u].coords + v_rel # now search in new structure for these atoms # query returns (distance, index) v_present = kd_tree.query(v_expec) v_present = v_present[1] if v_present[0] <= tol else None # check if image sites now present in supercell # and if so, delete old edge that went through # periodic boundary if v_present is not None: new_u = u new_v = v_present new_d = d.copy() # node now inside supercell new_d['to_jimage'] = (0, 0, 0) edges_to_remove.append((u, v, k)) # make sure we don't try to add duplicate edges # will remove two edges for everyone one we add if {new_u, new_v} not in edges_inside_supercell: # normalize direction if new_v < new_u: new_u, new_v = new_v, new_u edges_inside_supercell.append({new_u, new_v}) edges_to_add.append((new_u, new_v, new_d)) else: # want to find new_v such that we have # full periodic boundary conditions # so that nodes on one side of supercell # are connected to nodes on opposite side v_expec_frac = new_structure.lattice.get_fractional_coords(v_expec) # find new to_jimage # use np.around to fix issues with finite precision leading to incorrect image v_expec_image = np.around(v_expec_frac, decimals=3) v_expec_image = v_expec_image - v_expec_image%1 v_expec_frac = np.subtract(v_expec_frac, v_expec_image) v_expec = new_structure.lattice.get_cartesian_coords(v_expec_frac) v_present = kd_tree.query(v_expec) v_present = v_present[1] if v_present[0] <= tol else None if v_present is not None: new_u = u new_v = v_present new_d = d.copy() new_to_jimage = tuple(map(int, v_expec_image)) # normalize direction if new_v < new_u: new_u, new_v = new_v, new_u new_to_jimage = tuple(np.multiply(-1, d['to_jimage']).astype(int)) new_d['to_jimage'] = new_to_jimage edges_to_remove.append((u, v, k)) if (new_u, new_v, new_to_jimage) not in new_periodic_images: edges_to_add.append((new_u, new_v, new_d)) new_periodic_images.append((new_u, new_v, new_to_jimage)) logger.debug("Removing {} edges, adding {} new edges.".format(len(edges_to_remove), len(edges_to_add))) # add/delete marked edges for edges_to_remove in edges_to_remove: new_g.remove_edge(*edges_to_remove) for (u, v, d) in edges_to_add: new_g.add_edge(u, v, **d) # return new instance of StructureGraph with supercell d = {"@module": self.__class__.__module__, "@class": self.__class__.__name__, "structure": new_structure.as_dict(), "graphs": json_graph.adjacency_data(new_g)} sg = StructureGraph.from_dict(d) return sg
def get_start_end_structures( isite: PeriodicSite, esite: PeriodicSite, base_struct: Structure, sc_mat: List[List[Union[int, float]]], vac_mode: bool, debug: bool = False, ) -> Tuple[Structure, Structure, Structure]: """ Obtain the starting and terminating structures in a supercell for NEB calculations. Args: hop: object presenting the migration event base_struct: unit cell representation of the structure sc_mat: supercell transformation to create the simulation cell for the NEB calc Returns: initial structure, final structure, empty structure all in the supercell """ 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) base_sc = base_struct.copy() * sc_mat start_struct = base_struct.copy() * sc_mat end_struct = base_struct.copy() * sc_mat sc_mat_inv = np.linalg.inv(sc_mat) if not vac_mode: # insertion the endpoints start_struct.insert( 0, esite.species_string, np.dot(isite.frac_coords, sc_mat_inv), properties={"magmom": 0}, ) end_struct.insert( 0, esite.species_string, np.dot(esite.frac_coords, sc_mat_inv), properties={"magmom": 0}, ) else: # remove the other endpoint ipos_sc = np.dot(isite.frac_coords, sc_mat_inv) epos_sc = np.dot(esite.frac_coords, sc_mat_inv) if debug: icart = base_sc.lattice.get_cartesian_coords(ipos_sc) ecart = base_sc.lattice.get_cartesian_coords(epos_sc) assert abs( np.linalg.norm(icart - ecart) - np.linalg.norm(isite.coords - esite.coords)) < 1e-5 i_ref_ = PeriodicSite(species=esite.species_string, coords=ipos_sc, lattice=base_sc.lattice) e_ref_ = PeriodicSite(species=esite.species_string, coords=epos_sc, lattice=base_sc.lattice) start_struct = remove_site_at_pos(start_struct, e_ref_) end_struct = remove_site_at_pos(end_struct, i_ref_) return start_struct, end_struct, base_sc