def fast_ordering(self, structure, num_remove_dict, num_to_return=1): """ This method uses the matrix form of ewaldsum to calculate the ewald sums of the potential structures. This is on the order of 4 orders of magnitude faster when there are large numbers of permutations to consider. There are further optimizations possible (doing a smarter search of permutations for example), but this wont make a difference until the number of permutations is on the order of 30,000. """ self.logger.debug("Performing fast ordering") starttime = time.time() self.logger.debug("Performing initial ewald sum...") ewaldmatrix = EwaldSummation(structure).total_energy_matrix self.logger.debug("Ewald sum took {} seconds." .format(time.time() - starttime)) starttime = time.time() m_list = [] for indices, num in num_remove_dict.items(): m_list.append([0, num, list(indices), None]) self.logger.debug("Calling EwaldMinimizer...") minimizer = EwaldMinimizer(ewaldmatrix, m_list, num_to_return, PartialRemoveSitesTransformation.ALGO_FAST) self.logger.debug("Minimizing Ewald took {} seconds." .format(time.time() - starttime)) all_structures = [] lowest_energy = minimizer.output_lists[0][0] num_atoms = sum(structure.composition.values()) for output in minimizer.output_lists: se = StructureEditor(structure) del_indices = [] for manipulation in output[1]: if manipulation[1] is None: del_indices.append(manipulation[0]) else: se.replace_site(manipulation[0], manipulation[1]) se.delete_sites(del_indices) struct = se.modified_structure.get_sorted_structure() all_structures.append({"energy": output[0], "energy_above_minimum": (output[0] - lowest_energy) / num_atoms, "structure": struct}) return all_structures
def enumerate_ordering(self, structure): # Generate the disordered structure first. editor = StructureEditor(structure) for indices, fraction in zip(self._indices, self._fractions): for ind in indices: new_sp = {sp: occu * fraction for sp, occu in structure[ind].species_and_occu.items()} editor.replace_site(ind, new_sp) mod_s = editor.modified_structure # Perform enumeration from pymatgen.transformations.advanced_transformations import \ EnumerateStructureTransformation trans = EnumerateStructureTransformation() return trans.apply_transformation(mod_s, 10000)
class StructureEditorTest(unittest.TestCase): def setUp(self): self.si = Element("Si") self.fe = Element("Fe") self.ge = Element("Ge") coords = list() coords.append(np.array([0, 0, 0])) coords.append(np.array([0.75, 0.5, 0.75])) lattice = Lattice.cubic(10) s = Structure(lattice, [self.si, self.fe], coords) self.modifier = StructureEditor(s) def test_translate_sites(self): self.modifier.translate_sites([0, 1], [0.5, 0.5, 0.5], frac_coords=True) self.assertTrue(np.array_equal(self.modifier.modified_structure.frac_coords[0], np.array([ 0.5, 0.5, 0.5]))) self.modifier.translate_sites([0], [0.5, 0.5, 0.5], frac_coords=False) self.assertTrue(np.array_equal(self.modifier.modified_structure.cart_coords[0], np.array([ 5.5, 5.5, 5.5]))) def test_append_site(self): self.modifier.append_site(self.si, [0, 0.5, 0]) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si2", "Wrong formula!") self.assertRaises(ValueError, self.modifier.append_site, self.si, np.array([0, 0.5, 0])) def test_modified_structure(self): self.modifier.insert_site(1, self.si, [0, 0.25, 0]) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si2", "Wrong formula!") self.modifier.delete_site(0) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si1", "Wrong formula!") self.modifier.replace_site(0, self.ge) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Ge1", "Wrong formula!") self.modifier.append_site(self.si, [0, 0.75, 0]) self.modifier.replace_species({self.si: self.ge}) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Ge2", "Wrong formula!") self.modifier.replace_species({self.ge: {self.ge:0.5, self.si:0.5}}) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si1 Ge1", "Wrong formula!") #this should change the .5Si .5Ge sites to .75Si .25Ge self.modifier.replace_species({self.ge: {self.ge:0.5, self.si:0.5}}) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si1.5 Ge0.5", "Wrong formula!") d = 0.1 pre_perturbation_sites = self.modifier.modified_structure.sites self.modifier.perturb_structure(distance=d) post_perturbation_sites = self.modifier.modified_structure.sites for i, x in enumerate(pre_perturbation_sites): self.assertAlmostEqual(x.distance(post_perturbation_sites[i]), d, 3, "Bad perturbation distance") def test_add_site_property(self): self.modifier.add_site_property("charge", [4.1, 5]) s = self.modifier.modified_structure self.assertEqual(s[0].charge, 4.1) self.assertEqual(s[1].charge, 5) #test adding multiple properties. mod2 = StructureEditor(s) mod2.add_site_property("magmom", [3, 2]) s = mod2.modified_structure self.assertEqual(s[0].charge, 4.1) self.assertEqual(s[0].magmom, 3)
class StructureEditorTest(unittest.TestCase): def setUp(self): self.si = Element("Si") self.fe = Element("Fe") self.ge = Element("Ge") coords = list() coords.append(np.array([0, 0, 0])) coords.append(np.array([0.75, 0.5, 0.75])) lattice = Lattice.cubic(10) s = Structure(lattice, ["Si", "Fe"], coords) self.modifier = StructureEditor(s) def test_to_unit_cell(self): self.modifier.append_site(self.fe, [1.75, 0.5, 0.75], validate_proximity=False) self.modifier.to_unit_cell() self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si1", "Wrong formula!") def test_to_unit_cell(self): self.modifier.apply_strain(0.01) self.assertEqual(self.modifier.modified_structure.lattice.abc, (10.1, 10.1, 10.1)) def test_translate_sites(self): self.modifier.translate_sites([0, 1], [0.5, 0.5, 0.5], frac_coords=True) self.assertTrue(np.array_equal(self.modifier.modified_structure .frac_coords[0], np.array([0.5, 0.5, 0.5]))) self.modifier.translate_sites([0], [0.5, 0.5, 0.5], frac_coords=False) self.assertTrue(np.array_equal(self.modifier.modified_structure .cart_coords[0], np.array([5.5, 5.5, 5.5]))) def test_append_site(self): self.modifier.append_site(self.si, [0, 0.5, 0]) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si2", "Wrong formula!") self.assertRaises(ValueError, self.modifier.append_site, self.si, np.array([0, 0.5, 0])) def test_modified_structure(self): self.modifier.insert_site(1, self.si, [0, 0.25, 0]) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si2", "Wrong formula!") self.modifier.delete_site(0) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si1", "Wrong formula!") self.modifier.replace_site(0, self.ge) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Ge1", "Wrong formula!") self.modifier.append_site(self.si, [0, 0.75, 0]) self.modifier.replace_species({self.si: self.ge}) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Ge2", "Wrong formula!") self.modifier.replace_species({self.ge: {self.ge: 0.5, self.si: 0.5}}) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si1 Ge1", "Wrong formula!") #this should change the .5Si .5Ge sites to .75Si .25Ge self.modifier.replace_species({self.ge: {self.ge: 0.5, self.si: 0.5}}) self.assertEqual(self.modifier.modified_structure.formula, "Fe1 Si1.5 Ge0.5", "Wrong formula!") d = 0.1 pre_perturbation_sites = self.modifier.modified_structure.sites self.modifier.perturb_structure(distance=d) post_perturbation_sites = self.modifier.modified_structure.sites for i, x in enumerate(pre_perturbation_sites): self.assertAlmostEqual(x.distance(post_perturbation_sites[i]), d, 3, "Bad perturbation distance") def test_add_site_property(self): self.modifier.add_site_property("charge", [4.1, 5]) s = self.modifier.modified_structure self.assertEqual(s[0].charge, 4.1) self.assertEqual(s[1].charge, 5) #test adding multiple properties. mod2 = StructureEditor(s) mod2.add_site_property("magmom", [3, 2]) s = mod2.modified_structure self.assertEqual(s[0].charge, 4.1) self.assertEqual(s[0].magmom, 3) def test_add_oxidation_states(self): si = Element("Si") fe = Element("Fe") coords = list() coords.append([0, 0, 0]) coords.append([0.75, 0.5, 0.75]) lattice = Lattice.cubic(10) s = Structure(lattice, [si, fe], coords) oxidation_states = {"Fe": 2, "Si": -4} mod = StructureEditor(s) mod.add_oxidation_state_by_element(oxidation_states) mod_s = mod.modified_structure for site in mod_s: for k in site.species_and_occu.keys(): self.assertEqual(k.oxi_state, oxidation_states[k.symbol], "Wrong oxidation state assigned!") oxidation_states = {"Fe": 2} self.assertRaises(ValueError, mod.add_oxidation_state_by_element, oxidation_states) mod.add_oxidation_state_by_site([2, -4]) mod_s = mod.modified_structure self.assertEqual(mod_s[0].specie.oxi_state, 2) self.assertRaises(ValueError, mod.add_oxidation_state_by_site, [1]) def test_remove_oxidation_states(self): co_elem = Element("Co") o_elem = Element("O") co_specie = Specie("Co", 2) o_specie = Specie("O", -2) coords = list() coords.append([0, 0, 0]) coords.append([0.75, 0.5, 0.75]) lattice = Lattice.cubic(10) s_elem = Structure(lattice, [co_elem, o_elem], coords) s_specie = Structure(lattice, [co_specie, o_specie], coords) mod = StructureEditor(s_specie) mod.remove_oxidation_states() mod_s = mod.modified_structure self.assertEqual(s_elem, mod_s, "Oxidation state remover failed")
def apply_transformation(self, structure, return_ranked_list=False): """ For this transformation, the apply_transformation method will return only the ordered structure with the lowest Ewald energy, to be consistent with the method signature of the other transformations. However, all structures are stored in the all_structures attribute in the transformation object for easy access. Args: structure: Oxidation state decorated disordered structure to order return_ranked_list: Boolean stating whether or not multiple structures are returned. If return_ranked_list is a number, that number of structures is returned. Returns: Depending on returned_ranked list, either a transformed structure or a list of dictionaries, where each dictionary is of the form {"structure" = .... , "other_arguments"} the key "transformation" is reserved for the transformation that was actually applied to the structure. This transformation is parsed by the alchemy classes for generating a more specific transformation history. Any other information will be stored in the transformation_parameters dictionary in the transmuted structure class. """ try: num_to_return = int(return_ranked_list) except ValueError: num_to_return = 1 num_to_return = max(1, num_to_return) equivalent_sites = [] exemplars = [] #generate list of equivalent sites to order #equivalency is determined by sp_and_occu and symmetry #if symmetrized structure is true for i, site in enumerate(structure): if site.is_ordered: continue found = False for j, ex in enumerate(exemplars): sp = ex.species_and_occu if not site.species_and_occu.almost_equals(sp): continue if self._symmetrized: sym_equiv = structure.find_equivalent_sites(ex) sym_test = site in sym_equiv else: sym_test = True if sym_test: equivalent_sites[j].append(i) found = True if not found: equivalent_sites.append([i]) exemplars.append(site) #generate the list of manipulations and input structure se = StructureEditor(structure) m_list = [] for g in equivalent_sites: total_occupancy = sum([structure[i].species_and_occu for i in g], Composition()) total_occupancy = dict(total_occupancy.items()) #round total occupancy to possible values for k, v in total_occupancy.items(): if abs(v - round(v)) > 0.25: raise ValueError("Occupancy fractions not consistent " "with size of unit cell") total_occupancy[k] = int(round(v)) #start with an ordered structure initial_sp = max(total_occupancy.keys(), key=lambda x: abs(x.oxi_state)) for i in g: se.replace_site(i, initial_sp) #determine the manipulations for k, v in total_occupancy.items(): if k == initial_sp: continue m = [k.oxi_state / initial_sp.oxi_state, v, list(g), k] m_list.append(m) #determine the number of empty sites empty = len(g) - sum(total_occupancy.values()) if empty > 0.5: m_list.append([0, empty, list(g), None]) structure = se.modified_structure matrix = EwaldSummation(structure).total_energy_matrix ewald_m = EwaldMinimizer(matrix, m_list, num_to_return, self._algo) self._all_structures = [] lowest_energy = ewald_m.output_lists[0][0] num_atoms = sum(structure.composition.values()) for output in ewald_m.output_lists: se = StructureEditor(structure) # do deletions afterwards because they screw up the indices of the # structure del_indices = [] for manipulation in output[1]: if manipulation[1] is None: del_indices.append(manipulation[0]) else: se.replace_site(manipulation[0], manipulation[1]) se.delete_sites(del_indices) self._all_structures.append( {"energy": output[0], "energy_above_minimum": (output[0] - lowest_energy) / num_atoms, "structure": se.modified_structure.get_sorted_structure()}) if return_ranked_list: return self._all_structures else: return self._all_structures[0]["structure"]
def apply_transformation(self, structure): editor = StructureEditor(structure) for i, sp in self._indices_species_map.items(): editor.replace_site(int(i), sp) return editor.modified_structure
def apply_transformation(self, structure, return_ranked_list=False): """ For this transformation, the apply_transformation method will return only the ordered structure with the lowest Ewald energy, to be consistent with the method signature of the other transformations. However, all structures are stored in the all_structures attribute in the transformation object for easy access. Args: structure: Oxidation state decorated disordered structure to order return_ranked_list: Boolean stating whether or not multiple structures are returned. If return_ranked_list is a number, that number of structures is returned. Returns: Depending on returned_ranked list, either a transformed structure or a list of dictionaries, where each dictionary is of the form {'structure' = .... , 'other_arguments'} the key 'transformation' is reserved for the transformation that was actually applied to the structure. This transformation is parsed by the alchemy classes for generating a more specific transformation history. Any other information will be stored in the transformation_parameters dictionary in the transmuted structure class. """ ordered_sites = [] sites_to_order = {} try: num_to_return = int(return_ranked_list) except: num_to_return = 1 num_to_return = max(1, num_to_return) sites = list(structure.sites) for i in range(len(structure)): site = sites[i] if sum(site.species_and_occu.values()) == 1 and len(site.species_and_occu) == 1: ordered_sites.append(site) else: species = tuple([sp for sp, occu in site.species_and_occu.items()]) #group the sites by the list of species on that site for sp, occu in site.species_and_occu.items(): if species not in sites_to_order: sites_to_order[species] = {} if sp not in sites_to_order[species]: sites_to_order[species][sp] = [[occu, i]] else: sites_to_order[species][sp].append([occu, i]) total_occu = sum(site.species_and_occu.values()) #if the total occupancy on a site is less than one, add #a list with None as the species (for removal) if total_occu < 1: if None not in sites_to_order[species]: sites_to_order[species][None] = [[1 - total_occu, i]] else: sites_to_order[species][None].append([1 - total_occu, i]) """ Create a list of [multiplication fraction, number of replacements, [indices], replacement species] """ m_list = [] se = StructureEditor(structure) for species in sites_to_order.values(): initial_sp = None sorted_keys = sorted(species.keys(), key=lambda x: x is not None and -abs(x.oxi_state) or 1000) for sp in sorted_keys: if initial_sp is None: initial_sp = sp for site in species[sp]: se.replace_site(site[1], initial_sp) else: if sp is None: oxi = 0 else: oxi = float(sp.oxi_state) manipulation = [oxi / initial_sp.oxi_state, 0, [], sp] site_list = species[sp] site_list.sort(key=itemgetter(0)) prev_fraction = site_list[0][0] for site in site_list: if site[0] - prev_fraction > .1: """ tolerance for creating a new group of sites. if site occupancies are similar, they will be put in a group where the fraction has to be consistent over the whole. """ manipulation[1] = int(round(manipulation[1])) m_list.append(manipulation) manipulation = [oxi / initial_sp.oxi_state, 0, [], sp] prev_fraction = site[0] manipulation[1] += site[0] manipulation[2].append(site[1]) if abs(manipulation[1] - round(manipulation[1])) > .25: #if the # of atoms to remove isn't within .25 of an integer raise ValueError('Occupancy fractions not consistent with size of unit cell') manipulation[1] = int(round(manipulation[1])) m_list.append(manipulation) structure = se.modified_structure matrix = EwaldSummation(structure).total_energy_matrix ewald_m = EwaldMinimizer(matrix, m_list, num_to_return, self._algo) self._all_structures = [] lowest_energy = ewald_m.output_lists[0][0] num_atoms = sum(structure.composition.values()) for output in ewald_m.output_lists: se = StructureEditor(structure) del_indices = [] #do deletions afterwards because they screw up the indices of the structure for manipulation in output[1]: if manipulation[1] is None: del_indices.append(manipulation[0]) else: se.replace_site(manipulation[0], manipulation[1]) se.delete_sites(del_indices) self._all_structures.append({'energy':output[0], 'energy_above_minimum':(output[0] - lowest_energy) / num_atoms, 'structure': se.modified_structure.get_sorted_structure()}) if return_ranked_list: return self._all_structures else: return self._all_structures[0]['structure']