def __init__(self, lambda_table=None, alpha=-5): if lambda_table is not None: self._lambda_table = lambda_table else: module_dir = os.path.dirname(__file__) json_file = os.path.join(module_dir, 'data', 'lambda.json') with open(json_file) as f: self._lambda_table = json.load(f) # build map of specie pairs to lambdas self.alpha = alpha self._l = {} self.species = set() for row in self._lambda_table: if 'D1+' not in row: s1 = Specie.from_string(row[0]) s2 = Specie.from_string(row[1]) self.species.add(s1) self.species.add(s2) self._l[frozenset([s1, s2])] = float(row[2]) # create Z and px self.Z = 0 self._px = defaultdict(float) for s1, s2 in itertools.product(self.species, repeat=2): value = math.exp(self.get_lambda(s1, s2)) self._px[s1] += value / 2 self._px[s2] += value / 2 self.Z += value
def test_to_from_string(self): fe3 = Specie("Fe", 3, {"spin": 5}) self.assertEqual(str(fe3), "Fe3+spin=5") fe = Specie.from_string("Fe3+spin=5") self.assertEqual(fe.spin, 5) mo0 = Specie("Mo", 0, {"spin": 5}) self.assertEqual(str(mo0), "Mo0+spin=5") mo = Specie.from_string("Mo0+spin=4") self.assertEqual(mo.spin, 4)
def as_dict(self): """ JSON serialization of a SubStructureSpecie """ sp = Specie(self.specie.symbol, self.specie.oxi_state, self.specie._properties) return {"@module": self.__class__.__module__, "@class": self.__class__.__name__, "specie": sp.as_dict(), "weight": self.weight}
def from_dict(cls, d, lattice=None): """ Create PeriodicSite from dict representation. Args: d (dict): dict representation of PeriodicSite lattice: Optional lattice to override lattice specified in d. Useful for ensuring all sites in a structure share the same lattice. Returns: PeriodicSite """ atoms_n_occu = {} for sp_occu in d["species"]: if "oxidation_state" in sp_occu and Element.is_valid_symbol( sp_occu["element"]): sp = Specie.from_dict(sp_occu) elif "oxidation_state" in sp_occu: sp = DummySpecie.from_dict(sp_occu) else: sp = Element(sp_occu["element"]) atoms_n_occu[sp] = sp_occu["occu"] props = d.get("properties", None) lattice = lattice if lattice else Lattice.from_dict(d["lattice"]) return cls(atoms_n_occu, d["abc"], lattice, properties=props)
def from_dict(d): """ Create Site from dict representation """ atoms_n_occu = {} for sp_occu in d["species"]: sp = Specie.from_dict(sp_occu) if "oxidation_state" in sp_occu \ else Element(sp_occu["element"]) atoms_n_occu[sp] = sp_occu["occu"] props = d.get("properties", None) return Site(atoms_n_occu, d["xyz"], properties=props)
def __init__(self, lambda_table=None, alpha=-5): #store the input table for the to_dict method self._lambda_table = lambda_table if not lambda_table: module_dir = os.path.dirname(__file__) json_file = os.path.join(module_dir, 'data', 'lambda.json') with open(json_file) as f: lambda_table = json.load(f) #build map of specie pairs to lambdas l = {} for row in lambda_table: if not row[0] == 'D1+' and not row[1] == 'D1+': s1 = Specie.from_string(row[0]) s2 = Specie.from_string(row[1]) l[frozenset([s1, s2])] = float(row[2]) self._lambda = l self._alpha = alpha #create the partition functions Z and px sp_set = set() for key in self._lambda.keys(): sp_set.update(key) px = dict.fromkeys(sp_set, 0.) Z = 0 for s1, s2 in itertools.product(sp_set, repeat=2): value = math.exp(self._lambda.get(frozenset([s1, s2]), self._alpha)) #not sure why the factor of 2 is here but it matches up #with BURP. BURP may actually be missing a factor of 2, #but it doesn't have a huge effect px[s1] += value / 2 px[s2] += value / 2 Z += value self._Z = Z self._px = px self.species_list = list(sp_set)
def find_connected_atoms(struct, tolerance=0.45, ldict=JmolNN().el_radius): """ Finds the list of bonded atoms. Author: "Gowoon Cheon" Email: "*****@*****.**" Args: struct (Structure): Input structure tolerance: length in angstroms used in finding bonded atoms. Two atoms are considered bonded if (radius of atom 1) + (radius of atom 2) + (tolerance) < (distance between atoms 1 and 2). Default value = 0.45, the value used by JMol and Cheon et al. ldict: dictionary of bond lengths used in finding bonded atoms. Values from JMol are used as default Returns: (np.ndarray): A numpy array of shape (number of bonded pairs, 2); each row of is of the form [atomi, atomj]. atomi and atomj are the indices of the atoms in the input structure. If any image of atomj is bonded to atomi with periodic boundary conditions, [atomi, atomj] is included in the list. If atomi is bonded to multiple images of atomj, it is only counted once. """ n_atoms = len(struct.species) fc = np.array(struct.frac_coords) species = list(map(str, struct.species)) # in case of charged species for i, item in enumerate(species): if item not in ldict.keys(): species[i] = str(Specie.from_string(item).element) latmat = struct.lattice.matrix connected_list = [] for i in range(n_atoms): for j in range(i + 1, n_atoms): max_bond_length = ldict[species[i]] + ldict[species[j]] + tolerance add_ij = False for move_cell in itertools.product( [0, 1, -1], [0, 1, -1], [0, 1, -1]): if not add_ij: frac_diff = fc[j] + move_cell - fc[i] distance_ij = np.dot(latmat.T, frac_diff) if np.linalg.norm(distance_ij) < max_bond_length: add_ij = True if add_ij: connected_list.append([i, j]) return np.array(connected_list)
def from_dict(cls, d): """ Create Site from dict representation """ atoms_n_occu = {} for sp_occu in d["species"]: if "oxidation_state" in sp_occu and Element.is_valid_symbol( sp_occu["element"]): sp = Specie.from_dict(sp_occu) elif "oxidation_state" in sp_occu: sp = DummySpecie.from_dict(sp_occu) else: sp = Element(sp_occu["element"]) atoms_n_occu[sp] = sp_occu["occu"] props = d.get("properties", None) return cls(atoms_n_occu, d["xyz"], properties=props)
def test_get_shannon_radius(self): self.assertEqual(Specie("Li", 1).get_shannon_radius("IV"), 0.59) mn2 = Specie("Mn", 2) self.assertEqual(mn2.get_shannon_radius("IV", "High Spin"), 0.66) self.assertEqual(mn2.get_shannon_radius("V", "High Spin"), 0.75) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # Trigger a warning. r = mn2.get_shannon_radius("V") # Verify some things self.assertEqual(len(w), 1) self.assertIs(w[-1].category, UserWarning) self.assertEqual(r, 0.75) self.assertEqual(mn2.get_shannon_radius("VI", "Low Spin"), 0.67) self.assertEqual(mn2.get_shannon_radius("VI", "High Spin"), 0.83) self.assertEqual(mn2.get_shannon_radius("VII", "High Spin"), 0.9) self.assertEqual(mn2.get_shannon_radius("VIII"), 0.96)
def find_connected_atoms(struct, tolerance=0.45, ldict=JmolNN().el_radius): """ Finds bonded atoms and returns a adjacency matrix of bonded atoms. Author: "Gowoon Cheon" Email: "*****@*****.**" Args: struct (Structure): Input structure tolerance: length in angstroms used in finding bonded atoms. Two atoms are considered bonded if (radius of atom 1) + (radius of atom 2) + (tolerance) < (distance between atoms 1 and 2). Default value = 0.45, the value used by JMol and Cheon et al. ldict: dictionary of bond lengths used in finding bonded atoms. Values from JMol are used as default Returns: (np.ndarray): A numpy array of shape (number of atoms, number of atoms); If any image of atom j is bonded to atom i with periodic boundary conditions, the matrix element [atom i, atom j] is 1. """ n_atoms = len(struct.species) fc = np.array(struct.frac_coords) fc_copy = np.repeat(fc[:, :, np.newaxis], 27, axis=2) neighbors = np.array(list(itertools.product([0, 1, -1], [0, 1, -1], [0, 1, -1]))).T neighbors = np.repeat(neighbors[np.newaxis, :, :], 1, axis=0) fc_diff = fc_copy - neighbors species = list(map(str, struct.species)) # in case of charged species for i, item in enumerate(species): if not item in ldict.keys(): species[i] = str(Specie.from_string(item).element) latmat = struct.lattice.matrix connected_matrix = np.zeros((n_atoms,n_atoms)) for i in range(n_atoms): for j in range(i + 1, n_atoms): max_bond_length = ldict[species[i]] + ldict[species[j]] + tolerance frac_diff = fc_diff[j] - fc_copy[i] distance_ij = np.dot(latmat.T, frac_diff) # print(np.linalg.norm(distance_ij,axis=0)) if sum(np.linalg.norm(distance_ij, axis=0) < max_bond_length) > 0: connected_matrix[i, j] = 1 connected_matrix[j, i] = 1 return connected_matrix
def from_dict(d, lattice=None): """ Create PeriodicSite from dict representation. Args: d: dict representation of PeriodicSite lattice: Optional lattice to override lattice specified in d. Useful for ensuring all sites in a structure share the same lattice. """ atoms_n_occu = {} for sp_occu in d["species"]: sp = Specie.from_dict(sp_occu) if "oxidation_state" in sp_occu \ else Element(sp_occu["element"]) atoms_n_occu[sp] = sp_occu["occu"] props = d.get("properties", None) lattice = lattice if lattice else Lattice.from_dict(d["lattice"]) return PeriodicSite(atoms_n_occu, d["abc"], lattice, properties=props)
def coupling_constant(self, specie): """ Computes the couplling constant C_q as defined in: Wasylishen R E, Ashbrook S E, Wimperis S. NMR of quadrupolar nuclei in solid materials[M]. John Wiley & Sons, 2012. (Chapter 3.2) C_q for a specific atom type for this electric field tensor: C_q=e*Q*V_zz/h h: planck's constant Q: nuclear electric quadrupole moment in mb (millibarn e: elementary proton charge Args: specie: flexible input to specify the species at this site. Can take a isotope or element string, Specie object, or Site object Return: the coupling constant as a FloatWithUnit in MHz """ planks_constant=FloatWithUnit(6.62607004E-34, "m^2 kg s^-1") Vzz=FloatWithUnit(self.V_zz, "V ang^-2") e=FloatWithUnit(-1.60217662E-19, "C") # Convert from string to Specie object if isinstance(specie, str): # isotope was provided in string format if len(specie.split("-")) > 1: isotope=str(specie) specie=Specie(specie.split("-")[0]) Q=specie.get_nmr_quadrupole_moment(isotope) else: specie=Specie(specie) Q=specie.get_nmr_quadrupole_moment() elif isinstance(specie, Site): specie=specie.specie Q=specie.get_nmr_quadrupole_moment() elif isinstance(specie, Specie): Q=specie.get_nmr_quadrupole_moment() else: raise ValueError("Invalid speciie provided for quadrupolar coupling constant calcuations") return (e * Q * Vzz / planks_constant).to("MHz")
def apply_transformation(self, structure, return_ranked_list=False): # Make a mutable structure first mods = Structure.from_sites(structure) for sp, spin in self.mag_species_spin.items(): sp = get_el_sp(sp) oxi_state = getattr(sp, "oxi_state", 0) if spin: up = Specie(sp.symbol, oxi_state, {"spin": abs(spin)}) down = Specie(sp.symbol, oxi_state, {"spin": -abs(spin)}) mods.replace_species({ sp: Composition({ up: self.order_parameter, down: 1 - self.order_parameter }) }) else: mods.replace_species( {sp: Specie(sp.symbol, oxi_state, {"spin": spin})}) if mods.is_ordered: return [mods] if return_ranked_list > 1 else mods enum_args = self.kwargs enum_args["min_cell_size"] = max( int( MagOrderingTransformation.determine_min_cell( structure, self.mag_species_spin, self.order_parameter)), enum_args.get("min_cell_size", 1)) max_cell = enum_args.get('max_cell_size') if max_cell: if enum_args["min_cell_size"] > max_cell: raise ValueError('Specified max cell size is smaller' ' than the minimum enumerable cell size') else: enum_args["max_cell_size"] = enum_args["min_cell_size"] t = EnumerateStructureTransformation(**enum_args) alls = t.apply_transformation(mods, return_ranked_list=return_ranked_list) try: num_to_return = int(return_ranked_list) except ValueError: num_to_return = 1 if num_to_return == 1 or not return_ranked_list: return alls[0]["structure"] if num_to_return else alls m = StructureMatcher(comparator=SpinComparator()) key = lambda x: SpacegroupAnalyzer(x, 0.1).get_space_group_number() out = [] for _, g in groupby(sorted([d["structure"] for d in alls], key=key), key): g = list(g) grouped = m.group_structures(g) out.extend([{ "structure": g[0], "energy": self.energy_model.get_energy(g[0]) } for g in grouped]) self._all_structures = sorted(out, key=lambda d: d["energy"]) return self._all_structures[0:num_to_return]
def oxi_state_guesses(self, oxi_states_override=None, target_charge=0, all_oxi_states=False, max_sites=None): """ Checks if the composition is charge-balanced and returns back all charge-balanced oxidation state combinations. Composition must have integer values. Note that more num_atoms in the composition gives more degrees of freedom. e.g., if possible oxidation states of element X are [2,4] and Y are [-3], then XY is not charge balanced but X2Y2 is. Results are returned from most to least probable based on ICSD statistics. Use max_sites to improve performance if needed. Args: oxi_states_override (dict): dict of str->list to override an element's common oxidation states, e.g. {"V": [2,3,4,5]} target_charge (int): the desired total charge on the structure. Default is 0 signifying charge balance. all_oxi_states (bool): if True, an element defaults to all oxidation states in pymatgen Element.icsd_oxidation_states. Otherwise, default is Element.common_oxidation_states. Note that the full oxidation state list is *very* inclusive and can produce nonsensical results. max_sites (int): if possible, will reduce Compositions to at most this many many sites to speed up oxidation state guesses. Set to -1 to just reduce fully. Returns: A list of dicts - each dict reports an element symbol and average oxidation state across all sites in that composition. If the composition is not charge balanced, an empty list is returned. """ comp = self.copy() # reduce Composition if necessary if max_sites == -1: comp = self.reduced_composition elif max_sites and comp.num_atoms > max_sites: reduced_comp, reduced_factor = self.\ get_reduced_composition_and_factor() if reduced_factor > 1: reduced_comp *= max(1, int(max_sites / reduced_comp.num_atoms)) comp = reduced_comp # as close to max_sites as possible if comp.num_atoms > max_sites: raise ValueError("Composition {} cannot accommodate max_sites " "setting!".format(comp)) # Load prior probabilities of oxidation states, used to rank solutions if not Composition.oxi_prob: module_dir = os.path.join(os.path. dirname(os.path.abspath(__file__))) all_data = loadfn(os.path.join(module_dir, "..", "analysis", "icsd_bv.yaml")) Composition.oxi_prob = {Specie.from_string(sp): data for sp, data in all_data["occurrence"].items()} oxi_states_override = oxi_states_override or {} # assert: Composition only has integer amounts if not all(amt == int(amt) for amt in comp.values()): raise ValueError("Charge balance analysis requires integer " "values in Composition!") # for each element, determine all possible sum of oxidations # (taking into account nsites for that particular element) el_amt = comp.get_el_amt_dict() els = el_amt.keys() el_sums = [] # matrix: dim1= el_idx, dim2=possible sums el_sum_scores = defaultdict(set) # dict of el_idx, sum -> score for idx, el in enumerate(els): el_sum_scores[idx] = {} el_sums.append([]) if oxi_states_override.get(el): oxids = oxi_states_override[el] elif all_oxi_states: oxids = Element(el).oxidation_states else: oxids = Element(el).icsd_oxidation_states or \ Element(el).oxidation_states # get all possible combinations of oxidation states # and sum each combination for oxid_combo in combinations_with_replacement(oxids, int(el_amt[el])): if sum(oxid_combo) not in el_sums[idx]: el_sums[idx].append(sum(oxid_combo)) score = sum([Composition.oxi_prob.get(Specie(el, o), 0) for o in oxid_combo]) # how probable is this combo? el_sum_scores[idx][sum(oxid_combo)] = max( el_sum_scores[idx].get(sum(oxid_combo), 0), score) all_sols = [] # will contain all solutions all_scores = [] # will contain a score for each solution for x in product(*el_sums): # each x is a trial of one possible oxidation sum for each element if sum(x) == target_charge: # charge balance condition el_sum_sol = dict(zip(els, x)) # element->oxid_sum # normalize oxid_sum by amount to get avg oxid state sol = {el: v / el_amt[el] for el, v in el_sum_sol.items()} all_sols.append(sol) # add the solution to the list of solutions # determine the score for this solution score = 0 for idx, v in enumerate(x): score += el_sum_scores[idx][v] all_scores.append(score) # sort the solutions by highest to lowest score all_sols = [x for (y, x) in sorted(zip(all_scores, all_sols), key=lambda pair: pair[0], reverse=True)] return all_sols
def test_get_oxi_state_structure(self): s = Structure.from_file(os.path.join(test_dir, "LiMn2O4.json")) news = self.analyzer.get_oxi_state_decorated_structure(s) self.assertIn(Specie("Mn", 3), news.composition.elements) self.assertIn(Specie("Mn", 4), news.composition.elements)
[1 - x_4f, 1 - x_4f, 0], # O(4f) [0.5 - x_4f, 0.5 + x_4f, 0.5], # O(4f) [0.5 + x_4f, 0.5 - x_4f, 0.5], # O(4f) ]) # fmt: on structure = Structure(lattice, species, frac_coords) return structure if __name__ == "__main__": # enumerate rutile-like SnO1-x derivative structures rutile = get_rutile_structure() max_index = 3 mapping_color_species = [DummySpecie("X"), Specie("O"), Specie("Sn")] num_types = len(mapping_color_species) composition_constraints = None # fmt: off base_site_constraints = [ [2], # 2a [2], # 2a [0, 1], # 4f [0, 1], # 4f [0, 1], # 4f [0, 1], # 4f ] # fmt: on dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "SnO1-x")
def _get_structure(self, data, primitive, substitution_dictionary=None): """ Generate structure from part of the cif. """ # Symbols often representing #common representations for elements/water in cif files special_symbols = { "D": "D", "Hw": "H", "Ow": "O", "Wat": "O", "wat": "O" } elements = [el.symbol for el in Element] lattice = self.get_lattice(data) self.symmetry_operations = self.get_symops(data) oxi_states = self.parse_oxi_states(data) coord_to_species = OrderedDict() def parse_symbol(sym): if substitution_dictionary: return substitution_dictionary.get(sym) else: m = re.findall(r"w?[A-Z][a-z]*", sym) if m and m != "?": return m[0] return "" for i in range(len(data["_atom_site_label"])): symbol = parse_symbol(data["_atom_site_label"][i]) if symbol: if symbol not in elements and symbol not in special_symbols: symbol = symbol[:2] else: continue # make sure symbol was properly parsed from _atom_site_label # otherwise get it from _atom_site_type_symbol try: if symbol in special_symbols: get_el_sp(special_symbols.get(symbol)) else: Element(symbol) except (KeyError, ValueError): # sometimes the site doesn't have the type_symbol. # we then hope the type_symbol can be parsed from the label if "_atom_site_type_symbol" in data.data.keys(): symbol = data["_atom_site_type_symbol"][i] if oxi_states is not None: if symbol in special_symbols: el = get_el_sp( special_symbols.get(symbol) + str(oxi_states[symbol])) else: el = Specie(symbol, oxi_states.get(symbol, 0)) else: el = get_el_sp(special_symbols.get(symbol) if \ symbol in special_symbols else symbol) x = str2float(data["_atom_site_fract_x"][i]) y = str2float(data["_atom_site_fract_y"][i]) z = str2float(data["_atom_site_fract_z"][i]) try: occu = str2float(data["_atom_site_occupancy"][i]) except (KeyError, ValueError): occu = 1 if occu > 0: coord = (x, y, z) if coord not in coord_to_species: coord_to_species[coord] = {el: occu} else: coord_to_species[coord][el] = occu coord_to_species = { k: Composition(v) for k, v in coord_to_species.items() } allspecies = [] allcoords = [] if coord_to_species.items(): for species, group in groupby(sorted(list( coord_to_species.items()), key=lambda x: x[1]), key=lambda x: x[1]): tmp_coords = [site[0] for site in group] coords = self._unique_coords(tmp_coords) allcoords.extend(coords) allspecies.extend(len(coords) * [species]) #rescale occupancies if necessary for species in allspecies: totaloccu = sum(species.values()) if 1 < totaloccu <= self._occupancy_tolerance: for key, value in six.iteritems(species): species[key] = value / totaloccu if allspecies and len(allspecies) == len(allcoords): struct = Structure(lattice, allspecies, allcoords) struct = struct.get_sorted_structure() if primitive: struct = struct.get_primitive_structure() struct = struct.get_reduced_structure() return struct
def _get_structure(self, data, primitive): """ Generate structure from part of the cif. """ def get_num_implicit_hydrogens(sym): num_h = {"Wat": 2, "wat": 2, "O-H": 1} return num_h.get(sym[:3], 0) lattice = self.get_lattice(data) # if magCIF, get magnetic symmetry moments and magmoms # else standard CIF, and use empty magmom dict if self.feature_flags["magcif_incommensurate"]: raise NotImplementedError( "Incommensurate structures not currently supported.") elif self.feature_flags["magcif"]: self.symmetry_operations = self.get_magsymops(data) magmoms = self.parse_magmoms(data, lattice=lattice) else: self.symmetry_operations = self.get_symops(data) magmoms = {} oxi_states = self.parse_oxi_states(data) coord_to_species = OrderedDict() coord_to_magmoms = OrderedDict() def get_matching_coord(coord): keys = list(coord_to_species.keys()) coords = np.array(keys) for op in self.symmetry_operations: c = op.operate(coord) inds = find_in_coord_list_pbc(coords, c, atol=self._site_tolerance) # cant use if inds, because python is dumb and np.array([0]) evaluates # to False if len(inds): return keys[inds[0]] return False label_el_dict = {} for i in range(len(data["_atom_site_label"])): try: # If site type symbol exists, use it. Otherwise, we use the # label. symbol = self._parse_symbol(data["_atom_site_type_symbol"][i]) label = data["_atom_site_label"][i] num_h = get_num_implicit_hydrogens( data["_atom_site_type_symbol"][i]) except KeyError: symbol = self._parse_symbol(data["_atom_site_label"][i]) label = data["_atom_site_label"][i] num_h = get_num_implicit_hydrogens(data["_atom_site_label"][i]) if not symbol: continue if oxi_states is not None: o_s = oxi_states.get(symbol, 0) # use _atom_site_type_symbol if possible for oxidation state if "_atom_site_type_symbol" in data.data.keys(): oxi_symbol = data["_atom_site_type_symbol"][i] o_s = oxi_states.get(oxi_symbol, o_s) try: el = Specie(symbol, o_s) except: el = DummySpecie(symbol, o_s) else: el = get_el_sp(symbol) x = str2float(data["_atom_site_fract_x"][i]) y = str2float(data["_atom_site_fract_y"][i]) z = str2float(data["_atom_site_fract_z"][i]) magmom = magmoms.get(data["_atom_site_label"][i], np.array([0, 0, 0])) try: occu = str2float(data["_atom_site_occupancy"][i]) except (KeyError, ValueError): occu = 1 if occu > 0: coord = (x, y, z) match = get_matching_coord(coord) comp_d = {el: occu} if num_h > 0: comp_d["H"] = num_h comp = Composition(comp_d) if not match: coord_to_species[coord] = comp coord_to_magmoms[coord] = magmom else: coord_to_species[match] += comp # disordered magnetic not currently supported coord_to_magmoms[match] = None label_el_dict[coord] = label sum_occu = [ sum(c.values()) for c in coord_to_species.values() if not set(c.elements) == {Element("O"), Element("H")} ] if any([o > 1 for o in sum_occu]): msg = "Some occupancies (%s) sum to > 1! If they are within " \ "the tolerance, they will be rescaled." % str(sum_occu) warnings.warn(msg) self.errors.append(msg) allspecies = [] allcoords = [] allmagmoms = [] allhydrogens = [] alllabels = [] # check to see if magCIF file is disordered if self.feature_flags["magcif"]: for k, v in coord_to_magmoms.items(): if v is None: # Proposed solution to this is to instead store magnetic # moments as Specie 'spin' property, instead of site # property, but this introduces ambiguities for end user # (such as unintended use of `spin` and Specie will have # fictious oxidation state). raise NotImplementedError( 'Disordered magnetic structures not currently supported.' ) if coord_to_species.items(): for comp, group in groupby(sorted(list(coord_to_species.items()), key=lambda x: x[1]), key=lambda x: x[1]): tmp_coords = [site[0] for site in group] #print(tmp_coords) labels = [] for i in tmp_coords: labels.append(label_el_dict[i]) #print(labels) tmp_magmom = [ coord_to_magmoms[tmp_coord] for tmp_coord in tmp_coords ] if self.feature_flags["magcif"]: coords, magmoms, coords_num = self._unique_coords( tmp_coords, magmoms_in=tmp_magmom, lattice=lattice) else: coords, magmoms, coords_num = self._unique_coords( tmp_coords) if set(comp.elements) == {Element("O"), Element("H")}: # O with implicit hydrogens im_h = comp["H"] species = Composition({"O": comp["O"]}) else: im_h = 0 species = comp allhydrogens.extend(len(coords) * [im_h]) allcoords.extend(coords) allspecies.extend(len(coords) * [species]) allmagmoms.extend(magmoms) for i in range(len(coords_num)): alllabels.extend(coords_num[i] * [labels[i]]) # rescale occupancies if necessary for i, species in enumerate(allspecies): totaloccu = sum(species.values()) if 1 < totaloccu <= self._occupancy_tolerance: allspecies[i] = species / totaloccu if allspecies and len(allspecies) == len(allcoords) \ and len(allspecies) == len(allmagmoms): site_properties = dict() if any(allhydrogens): assert len(allhydrogens) == len(allcoords) site_properties["implicit_hydrogens"] = allhydrogens if self.feature_flags["magcif"]: site_properties["magmom"] = allmagmoms if len(site_properties) == 0: site_properties = None struct = Structure(lattice, allspecies, allcoords, site_properties=site_properties) #struct = struct.get_sorted_structure() if primitive and self.feature_flags['magcif']: struct = struct.get_primitive_structure(use_site_props=True) elif primitive: struct = struct.get_primitive_structure() struct = struct.get_reduced_structure() struct.add_site_property("_atom_site_label", alllabels) return struct
def _get_oxid_state_guesses(self, all_oxi_states, max_sites, oxi_states_override, target_charge): """ Utility operation for guessing oxidation states. See `oxi_state_guesses` for full details. This operation does the calculation of the most likely oxidation states Args: oxi_states_override (dict): dict of str->list to override an element's common oxidation states, e.g. {"V": [2,3,4,5]} target_charge (int): the desired total charge on the structure. Default is 0 signifying charge balance. all_oxi_states (bool): if True, an element defaults to all oxidation states in pymatgen Element.icsd_oxidation_states. Otherwise, default is Element.common_oxidation_states. Note that the full oxidation state list is *very* inclusive and can produce nonsensical results. max_sites (int): if possible, will reduce Compositions to at most this many many sites to speed up oxidation state guesses. Set to -1 to just reduce fully. Returns: A list of dicts - each dict reports an element symbol and average oxidation state across all sites in that composition. If the composition is not charge balanced, an empty list is returned. A list of dicts - each dict maps the element symbol to a list of oxidation states for each site of that element. For example, Fe3O4 could return a list of [2,2,2,3,3,3] for the oxidation states of If the composition is """ comp = self.copy() # reduce Composition if necessary if max_sites == -1: comp = self.reduced_composition elif max_sites and comp.num_atoms > max_sites: reduced_comp, reduced_factor = self. \ get_reduced_composition_and_factor() if reduced_factor > 1: reduced_comp *= max(1, int(max_sites / reduced_comp.num_atoms)) comp = reduced_comp # as close to max_sites as possible if comp.num_atoms > max_sites: raise ValueError("Composition {} cannot accommodate max_sites " "setting!".format(comp)) # Load prior probabilities of oxidation states, used to rank solutions if not Composition.oxi_prob: module_dir = os.path.join(os.path. dirname(os.path.abspath(__file__))) all_data = loadfn(os.path.join(module_dir, "..", "analysis", "icsd_bv.yaml")) Composition.oxi_prob = {Specie.from_string(sp): data for sp, data in all_data["occurrence"].items()} oxi_states_override = oxi_states_override or {} # assert: Composition only has integer amounts if not all(amt == int(amt) for amt in comp.values()): raise ValueError("Charge balance analysis requires integer " "values in Composition!") # for each element, determine all possible sum of oxidations # (taking into account nsites for that particular element) el_amt = comp.get_el_amt_dict() els = el_amt.keys() el_sums = [] # matrix: dim1= el_idx, dim2=possible sums el_sum_scores = defaultdict(set) # dict of el_idx, sum -> score el_best_oxid_combo = {} # dict of el_idx, sum -> oxid combo with best score for idx, el in enumerate(els): el_sum_scores[idx] = {} el_best_oxid_combo[idx] = {} el_sums.append([]) if oxi_states_override.get(el): oxids = oxi_states_override[el] elif all_oxi_states: oxids = Element(el).oxidation_states else: oxids = Element(el).icsd_oxidation_states or \ Element(el).oxidation_states # get all possible combinations of oxidation states # and sum each combination for oxid_combo in combinations_with_replacement(oxids, int(el_amt[el])): # List this sum as a possible option oxid_sum = sum(oxid_combo) if oxid_sum not in el_sums[idx]: el_sums[idx].append(oxid_sum) # Determine how probable is this combo? score = sum([Composition.oxi_prob.get(Specie(el, o), 0) for o in oxid_combo]) # If it is the most probable combo for a certain sum, # store the combination if oxid_sum not in el_sum_scores[idx] or score > el_sum_scores[idx].get(oxid_sum, 0): el_sum_scores[idx][oxid_sum] = score el_best_oxid_combo[idx][oxid_sum] = oxid_combo # Determine which combination of oxidation states for each element # is the most probable all_sols = [] # will contain all solutions all_oxid_combo = [] # will contain the best combination of oxidation states for each site all_scores = [] # will contain a score for each solution for x in product(*el_sums): # each x is a trial of one possible oxidation sum for each element if sum(x) == target_charge: # charge balance condition el_sum_sol = dict(zip(els, x)) # element->oxid_sum # normalize oxid_sum by amount to get avg oxid state sol = {el: v / el_amt[el] for el, v in el_sum_sol.items()} all_sols.append(sol) # add the solution to the list of solutions # determine the score for this solution score = 0 for idx, v in enumerate(x): score += el_sum_scores[idx][v] all_scores.append(score) # collect the combination of oxidation states for each site all_oxid_combo.append(dict((e,el_best_oxid_combo[idx][v]) for idx, (e,v) in enumerate(zip(els,x)))) # sort the solutions by highest to lowest score if len(all_scores) > 0: all_sols, all_oxid_combo = zip(*[(y, x) for (z, y, x) in sorted(zip(all_scores, all_sols, all_oxid_combo), key=lambda pair: pair[0], reverse=True)]) return all_sols, all_oxid_combo
def test_no_oxidation_state(self): mo0 = Specie("Mo", None, {"spin": 5}) self.assertEqual(str(mo0), "Mo,spin=5")
def test_sort(self): els = map(get_el_sp, ["N3-", "Si4+", "Si3+"]) self.assertEqual(sorted(els), [Specie("Si", 3), Specie("Si", 4), Specie("N", -3)])
def _get_structure(self, data, primitive): """ Generate structure from part of the cif. """ lengths = [ str2float(data["_cell_length_" + i]) for i in ["a", "b", "c"] ] angles = [ str2float(data["_cell_angle_" + i]) for i in ["alpha", "beta", "gamma"] ] lattice = Lattice.from_lengths_and_angles(lengths, angles) try: sympos = data["_symmetry_equiv_pos_as_xyz"] except KeyError: try: sympos = data["_symmetry_equiv_pos_as_xyz_"] except KeyError: warnings.warn("No _symmetry_equiv_pos_as_xyz type key found. " "Defaulting to P1.") sympos = ['x, y, z'] self.symmetry_operations = parse_symmetry_operations(sympos) def parse_symbol(sym): m = re.search("([A-Z][a-z]*)", sym) if m: return m.group(1) return "" try: oxi_states = { data["_atom_type_symbol"][i]: str2float(data["_atom_type_oxidation_number"][i]) for i in xrange(len(data["_atom_type_symbol"])) } except (ValueError, KeyError): oxi_states = None coord_to_species = OrderedDict() for i in xrange(len(data["_atom_site_type_symbol"])): symbol = parse_symbol(data["_atom_site_type_symbol"][i]) if oxi_states is not None: el = Specie(symbol, oxi_states[data["_atom_site_type_symbol"][i]]) else: el = Element(symbol) x = str2float(data["_atom_site_fract_x"][i]) y = str2float(data["_atom_site_fract_y"][i]) z = str2float(data["_atom_site_fract_z"][i]) try: occu = str2float(data["_atom_site_occupancy"][i]) except (KeyError, ValueError): occu = 1 if occu > 0: coord = (x, y, z) if coord not in coord_to_species: coord_to_species[coord] = {el: occu} else: coord_to_species[coord][el] = occu allspecies = [] allcoords = [] for coord, species in coord_to_species.items(): coords = self._unique_coords(coord) allcoords.extend(coords) allspecies.extend(len(coords) * [species]) #rescale occupancies if necessary for species in allspecies: totaloccu = sum(species.values()) if 1 < totaloccu <= self._occupancy_tolerance: for key, value in species.iteritems(): species[key] = value / totaloccu struct = Structure(lattice, allspecies, allcoords) if primitive: struct = struct.get_primitive_structure().get_reduced_structure() return struct.get_sorted_structure()
def test_cached(self): specie5 = Specie("Fe", 2)
def _get_structure(self, data, primitive, substitution_dictionary=None): """ Generate structure from part of the cif. """ # Symbols often representing # common representations for elements/water in cif files special_symbols = { "D": "D", "Hw": "H", "Ow": "O", "Wat": "O", "wat": "O" } elements = [el.symbol for el in Element] lattice = self.get_lattice(data) self.symmetry_operations = self.get_symops(data) oxi_states = self.parse_oxi_states(data) coord_to_species = OrderedDict() def parse_symbol(sym): if substitution_dictionary: return substitution_dictionary.get(sym) else: m = re.findall(r"w?[A-Z][a-z]*", sym) if m and m != "?": return m[0] return "" def get_matching_coord(coord): for op in self.symmetry_operations: c = op.operate(coord) for k in coord_to_species.keys(): if np.allclose(pbc_diff(c, k), (0, 0, 0), atol=self._site_tolerance): return tuple(k) return False for i in range(len(data["_atom_site_label"])): symbol = parse_symbol(data["_atom_site_label"][i]) if symbol: if symbol not in elements and symbol not in special_symbols: symbol = symbol[:2] else: continue # make sure symbol was properly parsed from _atom_site_label # otherwise get it from _atom_site_type_symbol try: if symbol in special_symbols: get_el_sp(special_symbols.get(symbol)) else: Element(symbol) except (KeyError, ValueError): # sometimes the site doesn't have the type_symbol. # we then hope the type_symbol can be parsed from the label if "_atom_site_type_symbol" in data.data.keys(): symbol = data["_atom_site_type_symbol"][i] if oxi_states is not None: if symbol in special_symbols: el = get_el_sp( special_symbols.get(symbol) + str(oxi_states[symbol])) else: el = Specie(symbol, oxi_states.get(symbol, 0)) else: el = get_el_sp(special_symbols.get(symbol, symbol)) x = str2float(data["_atom_site_fract_x"][i]) y = str2float(data["_atom_site_fract_y"][i]) z = str2float(data["_atom_site_fract_z"][i]) try: occu = str2float(data["_atom_site_occupancy"][i]) except (KeyError, ValueError): occu = 1 if occu > 0: coord = (x, y, z) match = get_matching_coord(coord) if not match: coord_to_species[coord] = Composition({el: occu}) else: coord_to_species[match] += {el: occu} if any([sum(c.values()) > 1 for c in coord_to_species.values()]): warnings.warn("Some occupancies sum to > 1! If they are within " "the tolerance, they will be rescaled.") allspecies = [] allcoords = [] if coord_to_species.items(): for species, group in groupby(sorted(list( coord_to_species.items()), key=lambda x: x[1]), key=lambda x: x[1]): tmp_coords = [site[0] for site in group] coords = self._unique_coords(tmp_coords) allcoords.extend(coords) allspecies.extend(len(coords) * [species]) # rescale occupancies if necessary for i, species in enumerate(allspecies): totaloccu = sum(species.values()) if 1 < totaloccu <= self._occupancy_tolerance: allspecies[i] = species / totaloccu if allspecies and len(allspecies) == len(allcoords): struct = Structure(lattice, allspecies, allcoords) struct = struct.get_sorted_structure() if primitive: struct = struct.get_primitive_structure() struct = struct.get_reduced_structure() return struct
def test_light_structure_environments(self): with ScratchDir("."): f = open("{}/{}".format(se_files_dir, 'se_mp-7000.json'), 'r') dd = json.load(f) f.close() se = StructureEnvironments.from_dict(dd) strategy = SimplestChemenvStrategy() lse = LightStructureEnvironments.from_structure_environments( structure_environments=se, strategy=strategy, valences='undefined') isite = 6 nb_set = lse.neighbors_sets[isite][0] neighb_coords = [ np.array([0.2443798, 1.80409653, -1.13218359]), np.array([1.44020353, 1.11368738, 1.13218359]), np.array([2.75513098, 2.54465207, -0.70467298]), np.array([0.82616785, 3.65833945, 0.70467298]) ] neighb_indices = [0, 3, 5, 1] neighb_images = [[0, 0, -1], [0, 0, 0], [0, 0, -1], [0, 0, 0]] np.testing.assert_array_almost_equal(neighb_coords, nb_set.neighb_coords) np.testing.assert_array_almost_equal( neighb_coords, [s.coords for s in nb_set.neighb_sites]) nb_sai = nb_set.neighb_sites_and_indices np.testing.assert_array_almost_equal( neighb_coords, [sai['site'].coords for sai in nb_sai]) np.testing.assert_array_almost_equal( neighb_indices, [sai['index'] for sai in nb_sai]) nb_iai = nb_set.neighb_indices_and_images np.testing.assert_array_almost_equal( neighb_indices, [iai['index'] for iai in nb_iai]) np.testing.assert_array_equal( neighb_images, [iai['image_cell'] for iai in nb_iai]) self.assertEqual(nb_set.__len__(), 4) self.assertEqual(nb_set.__hash__(), 4) self.assertFalse(nb_set.__ne__(nb_set)) self.assertEqual( nb_set.__str__(), 'Neighbors Set for site #6 :\n' ' - Coordination number : 4\n' ' - Neighbors sites indices : 0, 1, 2, 3\n') stats = lse.get_statistics() neighbors = lse.strategy.get_site_neighbors( site=lse.structure[isite]) self.assertArrayAlmostEqual( neighbors[0].coords, np.array([0.2443798, 1.80409653, -1.13218359])) self.assertArrayAlmostEqual( neighbors[1].coords, np.array([1.44020353, 1.11368738, 1.13218359])) self.assertArrayAlmostEqual( neighbors[2].coords, np.array([2.75513098, 2.54465207, -0.70467298])) self.assertArrayAlmostEqual( neighbors[3].coords, np.array([0.82616785, 3.65833945, 0.70467298])) equiv_site_index_and_transform = lse.strategy.equivalent_site_index_and_transform( neighbors[0]) self.assertEqual(equiv_site_index_and_transform[0], 0) self.assertArrayAlmostEqual(equiv_site_index_and_transform[1], [0.0, 0.0, 0.0]) self.assertArrayAlmostEqual(equiv_site_index_and_transform[2], [0.0, 0.0, -1.0]) equiv_site_index_and_transform = lse.strategy.equivalent_site_index_and_transform( neighbors[1]) self.assertEqual(equiv_site_index_and_transform[0], 3) self.assertArrayAlmostEqual(equiv_site_index_and_transform[1], [0.0, 0.0, 0.0]) self.assertArrayAlmostEqual(equiv_site_index_and_transform[2], [0.0, 0.0, 0.0]) self.assertEqual(stats['atom_coordination_environments_present'], {'Si': { 'T:4': 3.0 }}) self.assertEqual(stats['coordination_environments_atom_present'], {'T:4': { 'Si': 3.0 }}) self.assertEqual( stats['fraction_atom_coordination_environments_present'], {'Si': { 'T:4': 1.0 }}) site_info_ce = lse.get_site_info_for_specie_ce(specie=Specie( 'Si', 4), ce_symbol='T:4') np.testing.assert_array_almost_equal(site_info_ce['fractions'], [1.0, 1.0, 1.0]) np.testing.assert_array_almost_equal(site_info_ce['csms'], [ 0.009887784240541068, 0.009887786546730826, 0.009887787384385317 ]) self.assertEqual(site_info_ce['isites'], [6, 7, 8]) site_info_allces = lse.get_site_info_for_specie_allces( specie=Specie('Si', 4)) self.assertEqual(site_info_allces['T:4'], site_info_ce) self.assertFalse(lse.contains_only_one_anion('I-')) self.assertFalse(lse.contains_only_one_anion_atom('I')) self.assertTrue( lse.site_contains_environment(isite=isite, ce_symbol='T:4')) self.assertFalse( lse.site_contains_environment(isite=isite, ce_symbol='S:4')) self.assertFalse( lse.structure_contains_atom_environment(atom_symbol='Si', ce_symbol='S:4')) self.assertTrue( lse.structure_contains_atom_environment(atom_symbol='Si', ce_symbol='T:4')) self.assertFalse( lse.structure_contains_atom_environment(atom_symbol='O', ce_symbol='T:4')) self.assertTrue(lse.uniquely_determines_coordination_environments) self.assertFalse(lse.__ne__(lse)) envs = lse.strategy.get_site_coordination_environments( lse.structure[6]) self.assertEqual(len(envs), 1) self.assertEqual(envs[0][0], 'T:4') multi_strategy = MultiWeightsChemenvStrategy.stats_article_weights_parameters( ) lse_multi = LightStructureEnvironments.from_structure_environments( strategy=multi_strategy, structure_environments=se, valences='undefined') self.assertAlmostEqual( lse_multi.coordination_environments[isite][0]['csm'], 0.009887784240541068) self.assertAlmostEqual( lse_multi.coordination_environments[isite][0]['ce_fraction'], 1.0) self.assertEqual( lse_multi.coordination_environments[isite][0]['ce_symbol'], 'T:4')
def test_get_el_sp(self): self.assertEqual(get_el_sp("Fe2+"), Specie("Fe", 2)) self.assertEqual(get_el_sp("3"), Element("Li")) self.assertEqual(get_el_sp("U"), Element("U")) self.assertEqual(get_el_sp("X2+"), DummySpecie("X", 2)) self.assertEqual(get_el_sp("Mn3+"), Specie("Mn", 3))
def test_pickle(self): el1 = Specie("Fe", 3) o = pickle.dumps(el1) self.assertEqual(el1, pickle.loads(o))
def _get_structure(self, data, primitive, substitution_dictionary=None): """ Generate structure from part of the cif. """ # Symbols often representing # common representations for elements/water in cif files special_symbols = { "D": "D", "Hw": "H", "Ow": "O", "Wat": "O", "wat": "O" } elements = [el.symbol for el in Element] lattice = self.get_lattice(data) self.symmetry_operations = self.get_symops(data) oxi_states = self.parse_oxi_states(data) coord_to_species = OrderedDict() def parse_symbol(sym): if substitution_dictionary: return substitution_dictionary.get(sym) elif sym in ['OH', 'OH2']: warnings.warn("Symbol '{}' not recognized".format(sym)) return "" else: m = re.findall(r"w?[A-Z][a-z]*", sym) if m and m != "?": return m[0] return "" def get_matching_coord(coord): for op in self.symmetry_operations: c = op.operate(coord) for k in coord_to_species.keys(): if np.allclose(pbc_diff(c, k), (0, 0, 0), atol=self._site_tolerance): return tuple(k) return False ############################################################ """ This part of the code deals with handling formats of data as found in CIF files extracted from the Springer Materials/Pauling File databases, and that are different from standard ICSD formats. """ # Check to see if "_atom_site_type_symbol" exists, as some test CIFs do not contain this key. if "_atom_site_type_symbol" in data.data.keys(): # Keep a track of which data row needs to be removed. # Example of a row: Nb,Zr '0.8Nb + 0.2Zr' .2a .m-3m 0 0 0 1 14 'rhombic dodecahedron, Nb<sub>14</sub>' # Without this code, the above row in a structure would be parsed as an ordered site with only Nb (since # CifParser would try to parse the first two characters of the label "Nb,Zr") and occupancy=1. # However, this site is meant to be a disordered site with 0.8 of Nb and 0.2 of Zr. idxs_to_remove = [] for idx, el_row in enumerate(data["_atom_site_label"]): # CIF files from the Springer Materials/Pauling File have switched the label and symbol. Thus, in the # above shown example row, '0.8Nb + 0.2Zr' is the symbol. Below, we split the strings on ' + ' to # check if the length (or number of elements) in the label and symbol are equal. if len(data["_atom_site_type_symbol"][idx].split(' + ')) > \ len(data["_atom_site_label"][idx].split(' + ')): # Dictionary to hold extracted elements and occupancies els_occu = {} # parse symbol to get element names and occupancy and store in "els_occu" symbol_str = data["_atom_site_type_symbol"][idx] symbol_str_lst = symbol_str.split(' + ') for elocc_idx in range(len(symbol_str_lst)): # Remove any bracketed items in the string symbol_str_lst[elocc_idx] = re.sub( '\([0-9]*\)', '', symbol_str_lst[elocc_idx].strip()) # Extract element name and its occupancy from the string, and store it as a # key-value pair in "els_occ". els_occu[str(re.findall('\D+', symbol_str_lst[elocc_idx].strip())[1]).replace('<sup>', '')] = \ float('0' + re.findall('\.?\d+', symbol_str_lst[elocc_idx].strip())[1]) x = str2float(data["_atom_site_fract_x"][idx]) y = str2float(data["_atom_site_fract_y"][idx]) z = str2float(data["_atom_site_fract_z"][idx]) coord = (x, y, z) # Add each partially occupied element on the site coordinate for et in els_occu: match = get_matching_coord(coord) if not match: coord_to_species[coord] = Composition( {parse_symbol(et): els_occu[parse_symbol(et)]}) else: coord_to_species[match] += { parse_symbol(et): els_occu[parse_symbol(et)] } idxs_to_remove.append(idx) # Remove the original row by iterating over all keys in the CIF data looking for lists, which indicates # multiple data items, one for each row, and remove items from the list that corresponds to the removed row, # so that it's not processed by the rest of this function (which would result in an error). for cif_key in data.data: if type(data.data[cif_key]) == list: for id in sorted(idxs_to_remove, reverse=True): del data.data[cif_key][id] ############################################################ for i in range(len(data["_atom_site_label"])): symbol = parse_symbol(data["_atom_site_label"][i]) if symbol: if symbol not in elements and symbol not in special_symbols: symbol = symbol[:2] else: continue # make sure symbol was properly parsed from _atom_site_label # otherwise get it from _atom_site_type_symbol try: if symbol in special_symbols: get_el_sp(special_symbols.get(symbol)) else: Element(symbol) except (KeyError, ValueError): # sometimes the site doesn't have the type_symbol. # we then hope the type_symbol can be parsed from the label if "_atom_site_type_symbol" in data.data.keys(): symbol = data["_atom_site_type_symbol"][i] if oxi_states is not None: if symbol in special_symbols: el = get_el_sp( special_symbols.get(symbol) + str(oxi_states[symbol])) else: el = Specie(symbol, oxi_states.get(symbol, 0)) else: el = get_el_sp(special_symbols.get(symbol, symbol)) x = str2float(data["_atom_site_fract_x"][i]) y = str2float(data["_atom_site_fract_y"][i]) z = str2float(data["_atom_site_fract_z"][i]) try: occu = str2float(data["_atom_site_occupancy"][i]) except (KeyError, ValueError): occu = 1 if occu > 0: coord = (x, y, z) match = get_matching_coord(coord) if not match: coord_to_species[coord] = Composition({el: occu}) else: coord_to_species[match] += {el: occu} if any([sum(c.values()) > 1 for c in coord_to_species.values()]): warnings.warn("Some occupancies sum to > 1! If they are within " "the tolerance, they will be rescaled.") allspecies = [] allcoords = [] if coord_to_species.items(): for species, group in groupby(sorted(list( coord_to_species.items()), key=lambda x: x[1]), key=lambda x: x[1]): tmp_coords = [site[0] for site in group] coords = self._unique_coords(tmp_coords) allcoords.extend(coords) allspecies.extend(len(coords) * [species]) # rescale occupancies if necessary for i, species in enumerate(allspecies): totaloccu = sum(species.values()) if 1 < totaloccu <= self._occupancy_tolerance: allspecies[i] = species / totaloccu if allspecies and len(allspecies) == len(allcoords): struct = Structure(lattice, allspecies, allcoords) struct = struct.get_sorted_structure() if primitive: struct = struct.get_primitive_structure() struct = struct.get_reduced_structure() return struct
def test_ionic_radius(self): self.assertEqual(self.specie2.ionic_radius, 78.5 / 100) self.assertEqual(self.specie3.ionic_radius, 92 / 100) self.assertAlmostEqual(Specie("Mn", 4).ionic_radius, 0.67)
def setUp(self): self.df = pd.DataFrame({"composition": [Composition("Fe2O3"), Composition({Specie("Fe", 2): 1, Specie("O", -2): 1})]})
def test_get_crystal_field_spin(self): self.assertEqual(Specie("Fe", 2).get_crystal_field_spin(), 4) self.assertEqual(Specie("Fe", 3).get_crystal_field_spin(), 5) self.assertEqual(Specie("Fe", 4).get_crystal_field_spin(), 4) self.assertEqual(Specie("Co", 3).get_crystal_field_spin( spin_config="low"), 0) self.assertEqual(Specie("Co", 4).get_crystal_field_spin( spin_config="low"), 1) self.assertEqual(Specie("Ni", 3).get_crystal_field_spin( spin_config="low"), 1) self.assertEqual(Specie("Ni", 4).get_crystal_field_spin( spin_config="low"), 0) self.assertRaises(AttributeError, Specie("Li", 1).get_crystal_field_spin) self.assertRaises(AttributeError, Specie("Ge", 4).get_crystal_field_spin) self.assertRaises(AttributeError, Specie("H", 1).get_crystal_field_spin) self.assertRaises(AttributeError, Specie("Fe", 10).get_crystal_field_spin) self.assertRaises(ValueError, Specie("Fe", 2).get_crystal_field_spin, "hex")
def oxi_state_guesses(self, oxi_states_override=None, target_charge=0, all_oxi_states=False, max_sites=None): """ Checks if the composition is charge-balanced and returns back all charge-balanced oxidation state combinations. Composition must have integer values. Note that more num_atoms in the composition gives more degrees of freedom. e.g., if possible oxidation states of element X are [2,4] and Y are [-3], then XY is not charge balanced but X2Y2 is. Results are returned from most to least probable based on ICSD statistics. Use max_sites to improve performance if needed. Args: oxi_states_override (dict): dict of str->list to override an element's common oxidation states, e.g. {"V": [2,3,4,5]} target_charge (int): the desired total charge on the structure. Default is 0 signifying charge balance. all_oxi_states (bool): if True, an element defaults to all oxidation states in pymatgen Element.icsd_oxidation_states. Otherwise, default is Element.common_oxidation_states. Note that the full oxidation state list is *very* inclusive and can produce nonsensical results. max_sites (int): if possible, will reduce Compositions to at most this many many sites to speed up oxidation state guesses. Set to -1 to just reduce fully. Returns: A list of dicts - each dict reports an element symbol and average oxidation state across all sites in that composition. If the composition is not charge balanced, an empty list is returned. """ comp = self.copy() # reduce Composition if necessary if max_sites == -1: comp = self.reduced_composition elif max_sites and comp.num_atoms > max_sites: reduced_comp, reduced_factor = self.\ get_reduced_composition_and_factor() if reduced_factor > 1: reduced_comp *= max(1, int(max_sites / reduced_comp.num_atoms)) comp = reduced_comp # as close to max_sites as possible if comp.num_atoms > max_sites: raise ValueError("Composition {} cannot accommodate max_sites " "setting!".format(comp)) # Load prior probabilities of oxidation states, used to rank solutions if not Composition.oxi_prob: module_dir = os.path.join( os.path.dirname(os.path.abspath(__file__))) all_data = loadfn( os.path.join(module_dir, "..", "analysis", "icsd_bv.yaml")) Composition.oxi_prob = { Specie.from_string(sp): data for sp, data in all_data["occurrence"].items() } oxi_states_override = oxi_states_override or {} # assert: Composition only has integer amounts if not all(amt == int(amt) for amt in comp.values()): raise ValueError("Charge balance analysis requires integer " "values in Composition!") # for each element, determine all possible sum of oxidations # (taking into account nsites for that particular element) el_amt = comp.get_el_amt_dict() els = el_amt.keys() el_sums = [] # matrix: dim1= el_idx, dim2=possible sums el_sum_scores = defaultdict(set) # dict of el_idx, sum -> score for idx, el in enumerate(els): el_sum_scores[idx] = {} el_sums.append([]) if oxi_states_override.get(el): oxids = oxi_states_override[el] elif all_oxi_states: oxids = Element(el).oxidation_states else: oxids = Element(el).icsd_oxidation_states or \ Element(el).oxidation_states # get all possible combinations of oxidation states # and sum each combination for oxid_combo in combinations_with_replacement( oxids, int(el_amt[el])): if sum(oxid_combo) not in el_sums[idx]: el_sums[idx].append(sum(oxid_combo)) score = sum([ Composition.oxi_prob.get(Specie(el, o), 0) for o in oxid_combo ]) # how probable is this combo? el_sum_scores[idx][sum(oxid_combo)] = max( el_sum_scores[idx].get(sum(oxid_combo), 0), score) all_sols = [] # will contain all solutions all_scores = [] # will contain a score for each solution for x in product(*el_sums): # each x is a trial of one possible oxidation sum for each element if sum(x) == target_charge: # charge balance condition el_sum_sol = dict(zip(els, x)) # element->oxid_sum # normalize oxid_sum by amount to get avg oxid state sol = {el: v / el_amt[el] for el, v in el_sum_sol.items()} all_sols.append( sol) # add the solution to the list of solutions # determine the score for this solution score = 0 for idx, v in enumerate(x): score += el_sum_scores[idx][v] all_scores.append(score) # sort the solutions by highest to lowest score all_sols = [ x for (y, x) in sorted(zip(all_scores, all_sols), key=lambda pair: pair[0], reverse=True) ] return all_sols
"H", "B", "C", "Si", "N", "P", "As", "Sb", "O", "S", "Se", "Te", "F", "Cl", "Br", "I" ] ] module_dir = os.path.dirname(os.path.abspath(__file__)) # Read in BV parameters. BV_PARAMS = {} for k, v in loadfn(os.path.join(module_dir, "bvparam_1991.yaml")).items(): BV_PARAMS[Element(k)] = v # Read in yaml containing data-mined ICSD BV data. all_data = loadfn(os.path.join(module_dir, "icsd_bv.yaml")) ICSD_BV_DATA = { Specie.from_string(sp): data for sp, data in all_data["bvsum"].items() } PRIOR_PROB = { Specie.from_string(sp): data for sp, data in all_data["occurrence"].items() } def calculate_bv_sum(site, nn_list, scale_factor=1.0): """ Calculates the BV sum of a site. Args: site (PeriodicSite): The central site to calculate the bond valence nn_list ([Neighbor]): A list of namedtuple Neighbors having "distance"
def setUp(self): self.specie1 = Specie.from_string("Fe2+") self.specie2 = Specie("Fe", 3) self.specie3 = Specie("Fe", 2) self.specie4 = Specie("Fe", 2, {"spin": 5})
def test_utils(self): """Test utilities for the generation of Abinit inputs.""" # Test as_structure and from/to abivars si = Structure.as_structure(abidata.cif_file("si.cif")) assert si.formula == "Si2" assert si.latex_formula == "Si$_{2}$" assert si.abi_spacegroup is None and not si.has_abi_spacegroup assert "ntypat" in si.to(fmt="abivars") spgroup = si.spgset_abi_spacegroup(has_timerev=True) assert spgroup is not None assert si.has_abi_spacegroup assert si.abi_spacegroup.spgid == 227 kfrac_coords = si.get_kcoords_from_names(["G", "X", "L", "Gamma"]) self.assert_equal(kfrac_coords, ([[0. , 0. , 0. ], [0.5, 0. , 0.5], [0.5, 0.5, 0.5], [0. , 0. , 0. ]])) si_wfk = Structure.as_structure(abidata.ref_file("si_scf_WFK.nc")) assert si_wfk.formula == "Si2" si_wfk.print_neighbors(radius=2.5) assert si_wfk.has_abi_spacegroup # Cannot change spacegroup with self.assertRaises(ValueError): si_wfk.spgset_abi_spacegroup(has_timerev=True) # K and U are equivalent. [5/8, 1/4, 5/8] should return U assert si_wfk.findname_in_hsym_stars([3/8, 3/8, 3/4]) == "K" assert si_wfk.findname_in_hsym_stars([5/8, 1/4, 5/8]) == "U" # TODO: Fix order of atoms in supercells. # Test __mul__, __rmul__ (should return Abipy structures) assert si_wfk == 1 * si_wfk supcell = si_wfk * [2, 2, 2] assert len(supcell) == 8 * len(si_wfk) and hasattr(supcell, "abi_string") si_abi = Structure.from_file(abidata.ref_file("refs/si_ebands/run.abi")) assert si_abi.formula == "Si2" self.assert_equal(si_abi.frac_coords, [[0, 0, 0], [0.25, 0.25, 0.25]]) si_abo = Structure.from_file(abidata.ref_file("refs/si_ebands/run.abo")) assert si_abo == si_abi assert "ntypat" in si_abi.to(fmt="abivars") znse = Structure.from_file(abidata.ref_file("refs/znse_phonons/ZnSe_hex_qpt_DDB")) assert len(znse) == 4 assert znse.formula == "Zn2 Se2" self.assert_almost_equal(znse.frac_coords.flat, [ 0.33333333333333, 0.66666666666667, 0.99962203020000, 0.66666666666667, 0.33333333333333, 0.49962203020000, 0.33333333333333, 0.66666666666667, 0.62537796980000, 0.66666666666667, 0.33333333333333, 0.12537796980000]) from abipy.core.structure import diff_structures diff_structures([si_abi, znse], headers=["si_abi", "znse"], fmt="abivars", mode="table") diff_structures([si_abi, znse], headers=["si_abi", "znse"], fmt="abivars", mode="diff") # From pickle file. import pickle tmp_path = self.get_tmpname(suffix=".pickle") with open(tmp_path, "wb") as fh: pickle.dump(znse, fh) same_znse = Structure.from_file(tmp_path) assert same_znse == znse same_znse = Structure.as_structure(tmp_path) assert same_znse == znse for fmt in ["abivars", "cif", "POSCAR", "json", "xsf", "qe", "siesta", "wannier90"]: assert len(znse.convert(fmt=fmt)) > 0 for fmt in ["abinit", "w90", "siesta"]: assert len(znse.get_kpath_input_string(fmt=fmt)) > 0 oxi_znse = znse.get_oxi_state_decorated() assert len(oxi_znse.abi_string) from pymatgen.core.periodic_table import Specie assert Specie("Zn", 2) in oxi_znse.composition.elements assert Specie("Se", -2) in oxi_znse.composition.elements system = si.spget_lattice_type() assert system == "cubic" e = si.spget_equivalent_atoms(printout=True) assert len(e.irred_pos) == 1 self.assert_equal(e.eqmap[0], [0, 1]) for irr_pos in e.irred_pos: assert len(e.eqmap[irr_pos]) > 0 assert "equivalent_atoms" in e.spgdata if self.has_matplotlib(): assert si.plot_bz(show=False) assert si.plot_bz(pmg_path=False, show=False) assert si.plot(show=False) if sys.version[0:3] > '2.7': # pmg broke py compatibility assert si.plot_xrd(show=False) if self.has_mayavi(): #assert si.plot_vtk(show=False) # Disabled due to (core dumped) on travis assert si.plot_mayaview(show=False) if self.has_panel(): assert hasattr(si.get_panel(), "show") assert si is Structure.as_structure(si) assert si == Structure.as_structure(si.to_abivars()) assert si == Structure.from_abivars(si.to_abivars()) assert len(si.abi_string) assert si.reciprocal_lattice == si.lattice.reciprocal_lattice kptbounds = si.calc_kptbounds() ksamp = si.calc_ksampling(nksmall=10) shiftk = [[ 0.5, 0.5, 0.5], [ 0.5, 0. , 0. ], [ 0. , 0.5, 0. ], [ 0. , 0. , 0.5]] self.assert_equal(si.calc_ngkpt(nksmall=2), [2, 2, 2]) self.assert_equal(si.calc_shiftk(), shiftk) self.assert_equal(ksamp.ngkpt, [10, 10, 10]) self.assert_equal(ksamp.shiftk, shiftk) lif = Structure.from_abistring(""" acell 7.7030079150 7.7030079150 7.7030079150 Angstrom rprim 0.0000000000 0.5000000000 0.5000000000 0.5000000000 0.0000000000 0.5000000000 0.5000000000 0.5000000000 0.0000000000 natom 2 ntypat 2 typat 1 2 znucl 3 9 xred 0.0000000000 0.0000000000 0.0000000000 0.5000000000 0.5000000000 0.5000000000 """) assert lif.formula == "Li1 F1" same = Structure.rocksalt(7.7030079150, ["Li", "F"], units="ang") self.assert_almost_equal(lif.lattice.a, same.lattice.a) si = Structure.from_mpid("mp-149") assert si.formula == "Si2" # Test abiget_spginfo d = si.abiget_spginfo(tolsym=None, pre="abi_") assert d["abi_spg_symbol"] == "Fd-3m" assert d["abi_spg_number"] == 227 assert d["abi_bravais"] == "Bravais cF (face-center cubic)" llzo = Structure.from_file(abidata.cif_file("LLZO_oxi.cif")) assert llzo.is_ordered d = llzo.abiget_spginfo(tolsym=0.001) assert d["spg_number"] == 142 mgb2_cod = Structure.from_cod_id(1526507, primitive=True) assert mgb2_cod.formula == "Mg1 B2" assert mgb2_cod.spget_lattice_type() == "hexagonal" mgb2 = abidata.structure_from_ucell("MgB2") if self.has_ase(): mgb2.abi_primitive() assert [site.species_string for site in mgb2.get_sorted_structure_z()] == ["B", "B", "Mg"] s2inds = mgb2.get_symbol2indices() self.assert_equal(s2inds["Mg"], [0]) self.assert_equal(s2inds["B"], [1, 2]) s2coords = mgb2.get_symbol2coords() self.assert_equal(s2coords["Mg"], [[0, 0, 0]]) self.assert_equal(s2coords["B"], [[1/3, 2/3, 0.5], [2/3, 1/3, 0.5]]) new_mgb2 = mgb2.scale_lattice(mgb2.volume * 1.1) self.assert_almost_equal(new_mgb2.volume, mgb2.volume * 1.1) assert new_mgb2.lattice.is_hexagonal # TODO: This part should be tested more carefully mgb2.abi_sanitize() mgb2.abi_sanitize(primitive_standard=True) mgb2.get_conventional_standard_structure() assert len(mgb2.abi_string) assert len(mgb2.spget_summary(site_symmetry=True, verbose=10)) self.serialize_with_pickle(mgb2) pseudos = abidata.pseudos("12mg.pspnc", "5b.pspnc") nv = mgb2.num_valence_electrons(pseudos) assert nv == 8 and isinstance(nv , int) assert mgb2.valence_electrons_per_atom(pseudos) == [2, 3, 3] self.assert_equal(mgb2.calc_shiftk() , [[0.0, 0.0, 0.5]]) bmol = Structure.boxed_molecule(pseudos, cart_coords=[[0, 0, 0], [5, 5, 5]], acell=[10, 10, 10]) self.assert_almost_equal(bmol.volume, (10 * bohr_to_ang) ** 3) # FIXME This is buggy #acell = np.array([10, 20, 30]) #batom = Structure.boxed_atom(abidata.pseudo("12mg.pspnc"), cart_coords=[1, 2, 3], acell=acell) #assert isinstance(batom, Structure) #assert len(batom.cart_coords) == 1 #self.assert_equal(batom.cart_coords[0], [1, 2, 3]) # Function to compute cubic a0 from primitive v0 (depends on struct_type) vol2a = {"fcc": lambda vol: (4 * vol) ** (1/3.), "bcc": lambda vol: (2 * vol) ** (1/3.), "zincblende": lambda vol: (4 * vol) ** (1/3.), "rocksalt": lambda vol: (4 * vol) ** (1/3.), "ABO3": lambda vol: vol ** (1/3.), "hH": lambda vol: (4 * vol) ** (1/3.), } a = 10 bcc_prim = Structure.bcc(a, ["Si"], primitive=True) assert len(bcc_prim) == 1 self.assert_almost_equal(a, vol2a["bcc"](bcc_prim.volume)) bcc_conv = Structure.bcc(a, ["Si"], primitive=False) assert len(bcc_conv) == 2 self.assert_almost_equal(a**3, bcc_conv.volume) fcc_prim = Structure.fcc(a, ["Si"], primitive=True) assert len(fcc_prim) == 1 self.assert_almost_equal(a, vol2a["fcc"](fcc_prim.volume)) fcc_conv = Structure.fcc(a, ["Si"], primitive=False) assert len(fcc_conv) == 4 self.assert_almost_equal(a**3, fcc_conv.volume) zns = Structure.zincblende(a / bohr_to_ang, ["Zn", "S"], units="bohr") self.assert_almost_equal(a, vol2a["zincblende"](zns.volume)) rock = Structure.rocksalt(a, ["Na", "Cl"]) assert len(rock) == 2 self.assert_almost_equal(a, vol2a["rocksalt"](rock.volume)) perov = Structure.ABO3(a, ["Ca", "Ti", "O", "O", "O"]) assert len(perov) == 5 self.assert_almost_equal(a**3, perov.volume) # Test notebook generation. if self.has_nbformat(): assert mgb2.write_notebook(nbpath=self.get_tmpname(text=True))
def test_cmp(self): self.assertLess(self.specie1, self.specie2, "Fe2+ should be < Fe3+") self.assertLess(Specie("C", 1), Specie("Se", 1))
def get_dopants_from_shannon_radii(bonded_structure, num_dopants=5, match_oxi_sign=False): """ Get dopant suggestions based on Shannon radii differences. Args: bonded_structure (StructureGraph): A pymatgen structure graph decorated with oxidation states. For example, generated using the CrystalNN.get_bonded_structure() method. num_dopants (int): The nummber of suggestions to return for n- and p-type dopants. match_oxi_sign (bool): Whether to force the dopant and original species to have the same sign of oxidation state. E.g. If the original site is in a negative charge state, then only negative dopants will be returned. Returns: (dict): Dopant suggestions, given as a dictionary with keys "n_type" and "p_type". The suggestions for each doping type are given as a list of dictionaries, each with they keys: - "radii_diff": The difference between the Shannon radii of the species. - "dopant_spcies": The dopant species. - "original_species": The substituted species. """ # get a list of all Specie for all elements in all their common oxid states all_species = [ Specie(el, oxi) for el in Element for oxi in el.common_oxidation_states ] # get a series of tuples with (coordination number, specie) cn_and_species = set((bonded_structure.get_coordination_of_site(i), bonded_structure.structure[i].specie) for i in range(bonded_structure.structure.num_sites)) cn_to_radii_map = {} possible_dopants = [] for cn, species in cn_and_species: cn_roman = _int_to_roman(cn) try: species_radius = species.get_shannon_radius(cn_roman) except KeyError: warnings.warn("Shannon radius not found for {} with coordination " "number {}.\nSkipping...".format(species, cn)) continue if cn not in cn_to_radii_map: cn_to_radii_map[cn] = _shannon_radii_from_cn( all_species, cn_roman, radius_to_compare=species_radius) shannon_radii = cn_to_radii_map[cn] possible_dopants += [{ 'radii_diff': p['radii_diff'], 'dopant_species': p['species'], 'original_species': species } for p in shannon_radii] possible_dopants.sort(key=lambda x: abs(x['radii_diff'])) return _get_dopants(possible_dopants, num_dopants, match_oxi_sign)
def from_dict(cls, d): return SubStructureSpecie.from_specie(Specie.from_dict(d['specie']), d.get('weight', 0.0))
spgr = struc.get_spacegroup_info() print(struc.get_spacegroup_info()) # print(struc) cations = ['Co', 'Rh', 'Ir'] #cations = ['Li', 'Na', 'K', 'Rb', 'Cs'] # cations2 = ['C','Si','Ge','Sn', 'Pb'] anions = ['F', 'Cl', 'Br', 'I'] counter = 0 for cation in cations: # for cation2 in cations2: for anion in anions: try: # list = Subs.pred_from_structures(target_species=[Specie(cation, +1), Specie(cation2, +2), Specie(anion, -1)], structures_list=[{'structure': struc, 'id': 'TEST'}]) list = Subs.pred_from_structures( target_species=[Specie(cation, +3), Specie(anion, -1)], structures_list=[{ 'structure': struc, 'id': 'TEST' }]) except: pass for c in list: counter += 1 # c.final_structure.to(filename=cation + cation2 + anion +'3' + '_' + spgr[0] + ".cif") c.final_structure.to(filename=cation + anion + '3' + '_' + spgr[0] + ".cif") # list_of_all_combos = Subs.pred_from_list([Specie('Be', +2), Specie('O', -2)]) # print(list_of_all_combos)
def _get_structure(self, data, primitive): """ Generate structure from part of the cif. """ def parse_symbol(sym): # Common representations for elements/water in cif files # TODO: fix inconsistent handling of water special = { "D": "D", "Hw": "H", "Ow": "O", "Wat": "O", "wat": "O", "OH": "", "OH2": "" } m = re.findall(r"w?[A-Z][a-z]*", sym) if m and m != "?": if sym in special: v = special[sym] else: v = special.get(m[0], m[0]) if len(m) > 1 or (m[0] in special): warnings.warn("{} parsed as {}".format(sym, v)) return v lattice = self.get_lattice(data) # if magCIF, get magnetic symmetry moments and magmoms # else standard CIF, and use empty magmom dict if self.feature_flags["magcif_incommensurate"]: raise NotImplementedError( "Incommensurate structures not currently supported.") elif self.feature_flags["magcif"]: self.symmetry_operations = self.get_magsymops(data) magmoms = self.parse_magmoms(data, lattice=lattice) else: self.symmetry_operations = self.get_symops(data) magmoms = {} oxi_states = self.parse_oxi_states(data) coord_to_species = OrderedDict() coord_to_magmoms = OrderedDict() def get_matching_coord(coord): keys = list(coord_to_species.keys()) coords = np.array(keys) for op in self.symmetry_operations: c = op.operate(coord) inds = find_in_coord_list_pbc(coords, c, atol=self._site_tolerance) # cant use if inds, because python is dumb and np.array([0]) evaluates # to False if len(inds): return keys[inds[0]] return False for i in range(len(data["_atom_site_label"])): try: # If site type symbol exists, use it. Otherwise, we use the # label. symbol = parse_symbol(data["_atom_site_type_symbol"][i]) except KeyError: symbol = parse_symbol(data["_atom_site_label"][i]) if not symbol: continue if oxi_states is not None: o_s = oxi_states.get(symbol, 0) # use _atom_site_type_symbol if possible for oxidation state if "_atom_site_type_symbol" in data.data.keys(): oxi_symbol = data["_atom_site_type_symbol"][i] o_s = oxi_states.get(oxi_symbol, o_s) try: el = Specie(symbol, o_s) except: el = DummySpecie(symbol, o_s) else: el = get_el_sp(symbol) x = str2float(data["_atom_site_fract_x"][i]) y = str2float(data["_atom_site_fract_y"][i]) z = str2float(data["_atom_site_fract_z"][i]) magmom = magmoms.get(data["_atom_site_label"][i], Magmom(0)) try: occu = str2float(data["_atom_site_occupancy"][i]) except (KeyError, ValueError): occu = 1 if occu > 0: coord = (x, y, z) match = get_matching_coord(coord) if not match: coord_to_species[coord] = Composition({el: occu}) coord_to_magmoms[coord] = magmom else: coord_to_species[match] += {el: occu} coord_to_magmoms[ match] = None # disordered magnetic not currently supported sum_occu = [sum(c.values()) for c in coord_to_species.values()] if any([o > 1 for o in sum_occu]): warnings.warn( "Some occupancies (%s) sum to > 1! If they are within " "the tolerance, they will be rescaled." % str(sum_occu)) allspecies = [] allcoords = [] allmagmoms = [] # check to see if magCIF file is disordered if self.feature_flags["magcif"]: for k, v in coord_to_magmoms.items(): if v is None: # Proposed solution to this is to instead store magnetic moments # as Specie 'spin' property, instead of site property, but this # introduces ambiguities for end user (such as unintended use of # `spin` and Specie will have fictious oxidation state). raise NotImplementedError( 'Disordered magnetic structures not currently supported.' ) if coord_to_species.items(): for species, group in groupby(sorted(list( coord_to_species.items()), key=lambda x: x[1]), key=lambda x: x[1]): tmp_coords = [site[0] for site in group] tmp_magmom = [ coord_to_magmoms[tmp_coord] for tmp_coord in tmp_coords ] if self.feature_flags["magcif"]: coords, magmoms = self._unique_coords( tmp_coords, tmp_magmom) else: coords, magmoms = self._unique_coords(tmp_coords) allcoords.extend(coords) allspecies.extend(len(coords) * [species]) allmagmoms.extend(magmoms) # rescale occupancies if necessary for i, species in enumerate(allspecies): totaloccu = sum(species.values()) if 1 < totaloccu <= self._occupancy_tolerance: allspecies[i] = species / totaloccu if allspecies and len(allspecies) == len(allcoords) and len( allspecies) == len(allmagmoms): if self.feature_flags["magcif"]: struct = Structure(lattice, allspecies, allcoords, site_properties={"magmom": allmagmoms}) else: struct = Structure(lattice, allspecies, allcoords) struct = struct.get_sorted_structure() if primitive: struct = struct.get_primitive_structure() struct = struct.get_reduced_structure() return struct
def _get_oxid_state_guesses(self, all_oxi_states, max_sites, oxi_states_override, target_charge): """ Utility operation for guessing oxidation states. See `oxi_state_guesses` for full details. This operation does the calculation of the most likely oxidation states Args: oxi_states_override (dict): dict of str->list to override an element's common oxidation states, e.g. {"V": [2,3,4,5]} target_charge (int): the desired total charge on the structure. Default is 0 signifying charge balance. all_oxi_states (bool): if True, an element defaults to all oxidation states in pymatgen Element.icsd_oxidation_states. Otherwise, default is Element.common_oxidation_states. Note that the full oxidation state list is *very* inclusive and can produce nonsensical results. max_sites (int): if possible, will reduce Compositions to at most this many many sites to speed up oxidation state guesses. Set to -1 to just reduce fully. Returns: A list of dicts - each dict reports an element symbol and average oxidation state across all sites in that composition. If the composition is not charge balanced, an empty list is returned. A list of dicts - each dict maps the element symbol to a list of oxidation states for each site of that element. For example, Fe3O4 could return a list of [2,2,2,3,3,3] for the oxidation states of If the composition is """ comp = self.copy() # reduce Composition if necessary if max_sites == -1: comp = self.reduced_composition elif max_sites and comp.num_atoms > max_sites: reduced_comp, reduced_factor = self. \ get_reduced_composition_and_factor() if reduced_factor > 1: reduced_comp *= max(1, int(max_sites / reduced_comp.num_atoms)) comp = reduced_comp # as close to max_sites as possible if comp.num_atoms > max_sites: raise ValueError("Composition {} cannot accommodate max_sites " "setting!".format(comp)) # Load prior probabilities of oxidation states, used to rank solutions if not Composition.oxi_prob: module_dir = os.path.join( os.path.dirname(os.path.abspath(__file__))) all_data = loadfn( os.path.join(module_dir, "..", "analysis", "icsd_bv.yaml")) Composition.oxi_prob = { Specie.from_string(sp): data for sp, data in all_data["occurrence"].items() } oxi_states_override = oxi_states_override or {} # assert: Composition only has integer amounts if not all(amt == int(amt) for amt in comp.values()): raise ValueError("Charge balance analysis requires integer " "values in Composition!") # for each element, determine all possible sum of oxidations # (taking into account nsites for that particular element) el_amt = comp.get_el_amt_dict() els = el_amt.keys() el_sums = [] # matrix: dim1= el_idx, dim2=possible sums el_sum_scores = defaultdict(set) # dict of el_idx, sum -> score el_best_oxid_combo = { } # dict of el_idx, sum -> oxid combo with best score for idx, el in enumerate(els): el_sum_scores[idx] = {} el_best_oxid_combo[idx] = {} el_sums.append([]) if oxi_states_override.get(el): oxids = oxi_states_override[el] elif all_oxi_states: oxids = Element(el).oxidation_states else: oxids = Element(el).icsd_oxidation_states or \ Element(el).oxidation_states # get all possible combinations of oxidation states # and sum each combination for oxid_combo in combinations_with_replacement( oxids, int(el_amt[el])): # List this sum as a possible option oxid_sum = sum(oxid_combo) if oxid_sum not in el_sums[idx]: el_sums[idx].append(oxid_sum) # Determine how probable is this combo? score = sum([ Composition.oxi_prob.get(Specie(el, o), 0) for o in oxid_combo ]) # If it is the most probable combo for a certain sum, # store the combination if oxid_sum not in el_sum_scores[ idx] or score > el_sum_scores[idx].get(oxid_sum, 0): el_sum_scores[idx][oxid_sum] = score el_best_oxid_combo[idx][oxid_sum] = oxid_combo # Determine which combination of oxidation states for each element # is the most probable all_sols = [] # will contain all solutions all_oxid_combo = [ ] # will contain the best combination of oxidation states for each site all_scores = [] # will contain a score for each solution for x in product(*el_sums): # each x is a trial of one possible oxidation sum for each element if sum(x) == target_charge: # charge balance condition el_sum_sol = dict(zip(els, x)) # element->oxid_sum # normalize oxid_sum by amount to get avg oxid state sol = {el: v / el_amt[el] for el, v in el_sum_sol.items()} all_sols.append( sol) # add the solution to the list of solutions # determine the score for this solution score = 0 for idx, v in enumerate(x): score += el_sum_scores[idx][v] all_scores.append(score) # collect the combination of oxidation states for each site all_oxid_combo.append( dict((e, el_best_oxid_combo[idx][v]) for idx, (e, v) in enumerate(zip(els, x)))) # sort the solutions by highest to lowest score if len(all_scores) > 0: all_sols, all_oxid_combo = zip( *[(y, x) for (z, y, x) in sorted(zip(all_scores, all_sols, all_oxid_combo), key=lambda pair: pair[0], reverse=True)]) return all_sols, all_oxid_combo
def _get_structure(self, data, primitive): """ Generate structure from part of the cif. """ def parse_symbol(sym): # Common representations for elements/water in cif files # TODO: fix inconsistent handling of water special = {"D": "D", "Hw": "H", "Ow": "O", "Wat": "O", "wat": "O", "OH": "", "OH2": ""} m = re.findall(r"w?[A-Z][a-z]*", sym) if m and m != "?": if sym in special: v = special[sym] else: v = special.get(m[0], m[0]) if len(m) > 1 or (m[0] in special): warnings.warn("{} parsed as {}".format(sym, v)) return v lattice = self.get_lattice(data) self.symmetry_operations = self.get_symops(data) oxi_states = self.parse_oxi_states(data) coord_to_species = OrderedDict() def get_matching_coord(coord): keys = list(coord_to_species.keys()) coords = np.array(keys) for op in self.symmetry_operations: c = op.operate(coord) inds = find_in_coord_list_pbc(coords, c, atol=self._site_tolerance) # cant use if inds, because python is dumb and np.array([0]) evaluates # to False if len(inds): return keys[inds[0]] return False ############################################################ """ This part of the code deals with handling formats of data as found in CIF files extracted from the Springer Materials/Pauling File databases, and that are different from standard ICSD formats. """ # Check to see if "_atom_site_type_symbol" exists, as some test CIFs do # not contain this key. if "_atom_site_type_symbol" in data.data.keys(): # Keep a track of which data row needs to be removed. # Example of a row: Nb,Zr '0.8Nb + 0.2Zr' .2a .m-3m 0 0 0 1 14 # 'rhombic dodecahedron, Nb<sub>14</sub>' # Without this code, the above row in a structure would be parsed # as an ordered site with only Nb (since # CifParser would try to parse the first two characters of the # label "Nb,Zr") and occupancy=1. # However, this site is meant to be a disordered site with 0.8 of # Nb and 0.2 of Zr. idxs_to_remove = [] for idx, el_row in enumerate(data["_atom_site_label"]): # CIF files from the Springer Materials/Pauling File have # switched the label and symbol. Thus, in the # above shown example row, '0.8Nb + 0.2Zr' is the symbol. # Below, we split the strings on ' + ' to # check if the length (or number of elements) in the label and # symbol are equal. if len(data["_atom_site_type_symbol"][idx].split(' + ')) > \ len(data["_atom_site_label"][idx].split(' + ')): # Dictionary to hold extracted elements and occupancies els_occu = {} # parse symbol to get element names and occupancy and store # in "els_occu" symbol_str = data["_atom_site_type_symbol"][idx] symbol_str_lst = symbol_str.split(' + ') for elocc_idx in range(len(symbol_str_lst)): # Remove any bracketed items in the string symbol_str_lst[elocc_idx] = re.sub(r'\([0-9]*\)', '', symbol_str_lst[elocc_idx].strip()) # Extract element name and its occupancy from the # string, and store it as a # key-value pair in "els_occ". els_occu[str(re.findall(r'\D+', symbol_str_lst[ elocc_idx].strip())[1]).replace('<sup>', '')] = \ float('0' + re.findall(r'\.?\d+', symbol_str_lst[ elocc_idx].strip())[1]) x = str2float(data["_atom_site_fract_x"][idx]) y = str2float(data["_atom_site_fract_y"][idx]) z = str2float(data["_atom_site_fract_z"][idx]) coord = (x, y, z) # Add each partially occupied element on the site coordinate for et in els_occu: match = get_matching_coord(coord) if not match: coord_to_species[coord] = Composition( {parse_symbol(et): els_occu[parse_symbol(et)]}) else: coord_to_species[match] += { parse_symbol(et): els_occu[parse_symbol(et)]} idxs_to_remove.append(idx) # Remove the original row by iterating over all keys in the CIF # data looking for lists, which indicates # multiple data items, one for each row, and remove items from the # list that corresponds to the removed row, # so that it's not processed by the rest of this function (which # would result in an error). for cif_key in data.data: if type(data.data[cif_key]) == list: for id in sorted(idxs_to_remove, reverse=True): del data.data[cif_key][id] ############################################################ for i in range(len(data["_atom_site_label"])): try: # If site type symbol exists, use it. Otherwise, we use the # label. symbol = parse_symbol(data["_atom_site_type_symbol"][i]) except KeyError: symbol = parse_symbol(data["_atom_site_label"][i]) if not symbol: continue if oxi_states is not None: o_s = oxi_states.get(symbol, 0) # use _atom_site_type_symbol if possible for oxidation state if "_atom_site_type_symbol" in data.data.keys(): oxi_symbol = data["_atom_site_type_symbol"][i] o_s = oxi_states.get(oxi_symbol, o_s) try: el = Specie(symbol, o_s) except: el = DummySpecie(symbol, o_s) else: el = get_el_sp(symbol) x = str2float(data["_atom_site_fract_x"][i]) y = str2float(data["_atom_site_fract_y"][i]) z = str2float(data["_atom_site_fract_z"][i]) try: occu = str2float(data["_atom_site_occupancy"][i]) except (KeyError, ValueError): occu = 1 if occu > 0: coord = (x, y, z) match = get_matching_coord(coord) if not match: coord_to_species[coord] = Composition({el: occu}) else: coord_to_species[match] += {el: occu} sum_occu = [sum(c.values()) for c in coord_to_species.values()] if any([o > 1 for o in sum_occu]): warnings.warn("Some occupancies (%s) sum to > 1! If they are within " "the tolerance, they will be rescaled." % str(sum_occu)) allspecies = [] allcoords = [] if coord_to_species.items(): for species, group in groupby( sorted(list(coord_to_species.items()), key=lambda x: x[1]), key=lambda x: x[1]): tmp_coords = [site[0] for site in group] coords = self._unique_coords(tmp_coords) allcoords.extend(coords) allspecies.extend(len(coords) * [species]) # rescale occupancies if necessary for i, species in enumerate(allspecies): totaloccu = sum(species.values()) if 1 < totaloccu <= self._occupancy_tolerance: allspecies[i] = species / totaloccu if allspecies and len(allspecies) == len(allcoords): struct = Structure(lattice, allspecies, allcoords) struct = struct.get_sorted_structure() if primitive: struct = struct.get_primitive_structure() struct = struct.get_reduced_structure() return struct
def Analyze_VASP_MD(args): """ Analyze diffusivity from a series vasprun.xml (or vasprun.xml.gz) files at one temperature :param args: please check main function for details of args :return: """ vasprun_dirs = [] for i in range(args.runs_start, args.runs_end + 1): if os.path.exists( os.path.join(args.folder_feature + str(i), 'vasprun.xml.gz')): vasprun_dirs.append( os.path.join(args.folder_feature + str(i), 'vasprun.xml.gz')) elif os.path.exists( os.path.join(args.folder_feature + str(i), 'vasprun.xml')): vasprun_dirs.append( os.path.join(args.folder_feature + str(i), 'vasprun.xml')) else: raise Exception( "No vasprun.xml or vasprun.xml.gz in folder {}".format( args.folder_feature + str(i))) # In analyzing Arrhenius relationship, it is required to provide charged specie. To keep consistent, I also # require charged specie, even it is not necessary specie = Specie.from_string(args.specie) da = DiffusivityAnalyzer.from_files(vasprun_dirs, str(specie.element), step_skip=args.step_skip, ncores=args.ncores, time_intervals_number=args.time_intervals_number, spec_dict={'lower_bound': args.lower_bound_in_a_square \ * args.site_distance \ * args.site_distance, 'upper_bound': args.upper_bound, 'minimum_msd_diff': args.minimum_msd_diff_in_a_square \ * args.site_distance \ * args.site_distance, } ) ea = ErrorAnalysisFromDiffusivityAnalyzer(da, site_distance=args.site_distance) if da.diffusivity > 0: # The linear fitting succeed summary_info = ea.get_summary_dict(oxidized_specie=args.specie) # if the msd profile of the MD doesn't fulfill the fitting requirements, # da.diffusivity is set to be negative else: summary_info = { "diffusion result": "MSD calculated from MD doesn't fulfill the fitting requirement", "max msd": max(da.msd), 'msd': da.msd, 'dt': da.dt, 'msd_component': da.msd_component } print("Output msd-dt into {}K_msd-dt.csv".format(int(da.temperature))) args.msd_file = "{}K_msd-dt.csv".format(int(da.temperature)) # output print("=" * 40) print("Used vasprun.xml files") print("Start run: {}, end run: {}".format(vasprun_dirs[0], vasprun_dirs[-1])) print("=" * 40) # results table header_result = ("Parameter", "Value") result_table = PrettyTable(header_result) result_table.align["Parameter"] = "l" for k, v in summary_info.items(): if k not in ['msd', 'dt', 'msd_component']: result_table.add_row([k, str(v)]) result_table.add_row(['composition', str(da.structure.composition)]) print("Results table: ") print("Diffusivity unit: cm^2/s, Conductivity unit: mS/cm") print(result_table.get_string(sortby='Parameter')) # print(citing_info) # whether output msd if args.msd_file: print("Output msd-dt into file: {}".format(args.msd_file)) with open(args.msd_file, 'w') as fp: w_csv = csv.writer(fp, delimiter=',') data = [ summary_info['dt'], summary_info['msd'], summary_info['msd_component'][0], summary_info['msd_component'][1], summary_info['msd_component'][2] ] w_csv.writerows([[ "dt (fs)", "msd (A^2)", "msd_component_0", "msd_component_1", "msd_component_2" ]]) w_csv.writerows(zip(*data))
def test_cached(self): specie5 = Specie("Fe", 2) self.assertEqual(id(specie5), id(self.specie3))
"N", "P", "As", "Sb", "O", "S", "Se", "Te", "F", "Cl", "Br", "I"]) module_dir = os.path.dirname(os.path.abspath(__file__)) #Read in BV parameters. BV_PARAMS = {} with open(os.path.join(module_dir, "bvparam_1991.json"), "r") as f: for k, v in json.load(f).items(): BV_PARAMS[Element(k)] = v #Read in json containing data-mined ICSD BV data. with open(os.path.join(module_dir, "icsd_bv.json"), "r") as f: all_data = json.load(f) ICSD_BV_DATA = {Specie.from_string(sp): data for sp, data in all_data["bvsum"].items()} PRIOR_PROB = {Specie.from_string(sp): data for sp, data in all_data["occurrence"].items()} def calculate_bv_sum(site, nn_list, scale_factor=1.0): """ Calculates the BV sum of a site. Args: site: The site nn_list: List of nearest neighbors in the format [(nn_site, dist), ...]. anion_el:
def test_deepcopy(self): el1 = Specie("Fe", 4) el2 = Specie("Na", 1) ellist = [el1, el2] self.assertEqual(ellist, deepcopy(ellist), "Deepcopy operation doesn't produce exact copy.")
def from_dict(cls, d): return SubStructureSite.from_coords_and_specie(d['coords'], Specie.from_dict(d['specie']), d.get('weight', 0.0))