def get_charges(self, defect_type, site_specie, sub_specie=None): """ Based on the type of defect, site and substitution (if any) species the defect charge states are generated. Args: defect_type (str): Options are vacancy, antisite, substitution, and interstitial site_specie (str): Specie on the host lattice site Use this for interstitials as well sub_specie (str): Specie that is replacing the site specie. At present used for substitution and antisite defects """ if site_specie not in self.min_max_oxi.keys(): max_oxi = max(Element(site_specie).common_oxidation_states) min_oxi = min(Element(site_specie).common_oxidation_states) self.min_max_oxi[site_specie] = [min_oxi, max_oxi] if sub_specie: if sub_specie not in self.min_max_oxi.keys(): max_oxi = max(Element(sub_specie).common_oxidation_states) min_oxi = min(Element(sub_specie).common_oxidation_states) self.min_max_oxi[sub_specie] = [min_oxi, max_oxi] if defect_type == 'vacancy': site_oxi = self.oxi_states[site_specie] if site_oxi: return list(range(-abs(site_oxi), abs(site_oxi) + 1)) else: min_oxi = self.min_max_oxi[site_specie][0] max_oxi = self.min_max_oxi[site_specie][1] if abs(min_oxi) < abs(max_oxi): return list(range(-abs(max_oxi), abs(max_oxi) + 1)) else: return list(range(-abs(min_oxi), abs(min_oxi) + 1)) elif defect_type == 'antisite': return list( range(self.min_max_oxi_bulk[0], self.min_max_oxi_bulk[1] - 1)) elif defect_type == 'substitution': oxi_sub_set = Element(sub_specie).common_oxidation_states oxi_site = self.oxi_states[site_specie] min_max_oxi_bulk_sub = [ min(min(oxi_sub_set) - oxi_site, -1), max(max(oxi_sub_set) - oxi_site, 1) ] if (min_max_oxi_bulk_sub[1] - min_max_oxi_bulk_sub[0]) > 2: if min_max_oxi_bulk_sub[1] > 2: return list( range(min_max_oxi_bulk_sub[0], min_max_oxi_bulk_sub[1] - 2) ) # if range exists, less likely to be a higher charge else: return list( range(min_max_oxi_bulk_sub[0], 2) ) # extend to 2 if upper range does not already include this else: return list( range(min_max_oxi_bulk_sub[0] - 1, min_max_oxi_bulk_sub[1] + 2)) #likely upper bound is 0, so extend to 2 elif defect_type == 'interstitial': if self.min_max_oxi[site_specie][0] > 0: min_oxi = 0 else: min_oxi = self.min_max_oxi[site_specie][0] if self.min_max_oxi[site_specie][1] < 0: max_oxi = 0 else: max_oxi = self.min_max_oxi[site_specie][1] return list(range(min_oxi, max_oxi + 1)) else: raise ValueError("Defect type not understood")
def setUp(self): self.entries = EntrySet.from_csv(str(module_dir / "pdentries_test.csv")) self.pd = GrandPotentialPhaseDiagram(self.entries, {Element("O"): -5}) self.pd6 = GrandPotentialPhaseDiagram(self.entries, {Element("O"): -6})
def __init__(self, entry1, entry2, working_ion_entry): # initialize some internal variables working_element = working_ion_entry.composition.elements[0] entry_charge = entry1 entry_discharge = entry2 if entry_charge.composition.get_atomic_fraction(working_element) \ > entry2.composition.get_atomic_fraction(working_element): (entry_charge, entry_discharge) = (entry_discharge, entry_charge) comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition ion_sym = working_element.symbol frame_charge_comp = Composition({ el: comp_charge[el] for el in comp_charge if el.symbol != ion_sym }) frame_discharge_comp = Composition({ el: comp_discharge[el] for el in comp_discharge if el.symbol != ion_sym }) # Data validation # check that the ion is just a single element if not working_ion_entry.composition.is_element: raise ValueError("VoltagePair: The working ion specified must be " "an element") # check that at least one of the entries contains the working element if not comp_charge.get_atomic_fraction(working_element) > 0 and \ not comp_discharge.get_atomic_fraction(working_element) > 0: raise ValueError("VoltagePair: The working ion must be present in " "one of the entries") # check that the entries do not contain the same amount of the workin # element if comp_charge.get_atomic_fraction(working_element) == \ comp_discharge.get_atomic_fraction(working_element): raise ValueError("VoltagePair: The working ion atomic percentage " "cannot be the same in both the entries") # check that the frameworks of the entries are equivalent if not frame_charge_comp.reduced_formula == \ frame_discharge_comp.reduced_formula: raise ValueError("VoltagePair: the specified entries must have the" " same compositional framework") # Initialize normalization factors, charged and discharged entries valence_list = Element(ion_sym).oxidation_states working_ion_valence = abs(max(valence_list)) (self.framework, norm_charge) = frame_charge_comp.get_reduced_composition_and_factor() norm_discharge = \ frame_discharge_comp.get_reduced_composition_and_factor()[1] self._working_ion_entry = working_ion_entry # Initialize normalized properties self._vol_charge = entry_charge.structure.volume / norm_charge self._vol_discharge = entry_discharge.structure.volume / norm_discharge comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition self._mass_charge = comp_charge.weight / norm_charge self._mass_discharge = comp_discharge.weight / norm_discharge self._num_ions_transferred = \ (comp_discharge[working_element] / norm_discharge) \ - (comp_charge[working_element] / norm_charge) self._voltage = \ (((entry_charge.energy / norm_charge) - (entry_discharge.energy / norm_discharge)) / \ self._num_ions_transferred + working_ion_entry.energy_per_atom) / working_ion_valence self._mAh = self._num_ions_transferred * Charge(1, "e").to("C") * \ Time(1, "s").to("h") * N_A * 1000 * working_ion_valence # Step 4: add (optional) hull and muO2 data self.decomp_e_charge = \ entry_charge.data.get("decomposition_energy", None) self.decomp_e_discharge = \ entry_discharge.data.get("decomposition_energy", None) self.muO2_charge = entry_charge.data.get("muO2", None) self.muO2_discharge = entry_discharge.data.get("muO2", None) self.entry_charge = entry_charge self.entry_discharge = entry_discharge self.normalization_charge = norm_charge self.normalization_discharge = norm_discharge self._frac_charge = comp_charge.get_atomic_fraction(working_element) self._frac_discharge = \ comp_discharge.get_atomic_fraction(working_element)
def test_get_defectsite_coordinated_elements(self): struct_el = self._mgo_uc.composition.elements for i in range(self._mgo_interstitial.defectsite_count()): for el in self._mgo_interstitial.get_coordinated_elements(i): self.assertTrue( Element(el) in struct_el, "Coordinated elements are wrong")
def test_getmu_range_stability_phase(self): results = self.pd.get_chempot_range_stability_phase(Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(results[Element("O")][1], -4.4501812249999997) self.assertAlmostEqual(results[Element("Fe")][0], -6.5961470999999996) self.assertAlmostEqual(results[Element("Li")][0], -3.6250022625000007)
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 sites to speed up oxidation state guesses. If the composition cannot be reduced to this many sites a ValueError will be raised. Set to -1 to just reduce fully. If set to a number less than -1, the formula will be fully reduced but a ValueError will be thrown if the number of atoms in the reduced formula is greater than abs(max_sites). 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 and max_sites < 0: comp = self.reduced_composition if max_sites < -1 and comp.num_atoms > abs(max_sites): raise ValueError("Composition {} cannot accommodate max_sites " "setting!".format(comp)) 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 __str__(self): out = [] site_descriptions = {} if self.pseudo is not None: site_descriptions = self.pseudo else: c = 1 for site in self.structure: name = None for k, v in site_descriptions.items(): if site.properties == v: name = k if name is None: name = site.specie.symbol + str(c) site_descriptions[name] = site.properties c += 1 def to_str(v): if isinstance(v, str): return "'%s'" % v if isinstance(v, float): return "%s" % str(v).replace("e", "d") if isinstance(v, bool): if v: return ".TRUE." return ".FALSE." return v for k1 in ["control", "system", "electrons", "ions", "cell"]: v1 = self.sections[k1] out.append("&%s" % k1.upper()) sub = [] for k2 in sorted(v1.keys()): if isinstance(v1[k2], list): n = 1 for l in v1[k2][: len(site_descriptions)]: sub.append(" %s(%d) = %s" % (k2, n, to_str(v1[k2][n - 1]))) n += 1 else: sub.append(" %s = %s" % (k2, to_str(v1[k2]))) if k1 == "system": if "ibrav" not in self.sections[k1]: sub.append(" ibrav = 0") if "nat" not in self.sections[k1]: sub.append(" nat = %d" % len(self.structure)) if "ntyp" not in self.sections[k1]: sub.append(" ntyp = %d" % len(site_descriptions)) sub.append("/") out.append(",\n".join(sub)) out.append("ATOMIC_SPECIES") for k, v in sorted(site_descriptions.items(), key=lambda i: i[0]): e = re.match(r"[A-Z][a-z]?", k).group(0) if self.pseudo is not None: p = v else: p = v["pseudo"] out.append(" %s %.4f %s" % (k, Element(e).atomic_mass, p)) out.append("ATOMIC_POSITIONS crystal") if self.pseudo is not None: for site in self.structure: out.append( " %s %.6f %.6f %.6f" % (site.specie.symbol, site.a, site.b, site.c) ) else: for site in self.structure: name = None for k, v in sorted(site_descriptions.items(), key=lambda i: i[0]): if v == site.properties: name = k out.append(" %s %.6f %.6f %.6f" % (name, site.a, site.b, site.c)) out.append("K_POINTS %s" % self.kpoints_mode) kpt_str = ["%s" % i for i in self.kpoints_grid] kpt_str.extend(["%s" % i for i in self.kpoints_shift]) out.append(" %s" % " ".join(kpt_str)) out.append("CELL_PARAMETERS angstrom") for vec in self.structure.lattice.matrix: out.append(" %f %f %f" % (vec[0], vec[1], vec[2])) return "\n".join(out)
def generate_basis(self, symbol): """ Author: "Kittithat (Mick) Krongchon" <*****@*****.**> and Lucas K. Wagner Returns a string containing the basis section. It is modified according to a simple recipe: 1) The occupied atomic orbitals are kept, with exponents less than 'cutoff' removed. 2) These atomic orbitals are augmented with uncontracted orbitals according to the formula e_i = params[0]*params[2]**i, where i goes from 0 to params[1] These uncontracted orbitals are added for every occupied atomic orbital (s,p for most elements and s,p,d for transition metals) Args: symbol (str): The symbol of the element to be specified in the D12 file. Returns: str: The pseudopotential and basis section. Uses the following member variables: xml_name (str): The name of the XML pseudopotential and basis set database. cutoff: smallest allowed exponent params: parameters for generating the augmenting uncontracted orbitals initial_charges """ maxorb = 3 basis_name = "vtz" nangular = {"s": 1, "p": 1, "d": 1, "f": 1, "g": 0} maxcharge = {"s": 2, "p": 6, "d": 10, "f": 15} basis_index = {"s": 0, "p": 2, "d": 3, "f": 4} transition_metals = [ "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn" ] if symbol in transition_metals: maxorb = 4 nangular['s'] = 2 tree = ElementTree() tree.parse(self.xml_name) element = tree.find('./Pseudopotential[@symbol="{}"]'.format(symbol)) atom_charge = int(element.find('./Effective_core_charge').text) if symbol in self.initial_charges.keys(): atom_charge -= self.initial_charges[symbol] basis_path = './Basis-set[@name="{}"]/Contraction'.format(basis_name) found_orbitals = [] totcharge = 0 ret = [] ncontract = 0 for contraction in element.findall(basis_path): angular = contraction.get('Angular_momentum') if found_orbitals.count(angular) >= nangular[angular]: continue #Figure out which coefficients to print out based on the minimal exponent nterms = 0 basis_part = [] for basis_term in contraction.findall('./Basis-term'): exp = basis_term.get('Exp') coeff = basis_term.get('Coeff') if float(exp) > self.cutoff: basis_part += [' {} {}'.format(exp, coeff)] nterms += 1 #now write the header if nterms > 0: found_orbitals.append(angular) charge = min(atom_charge - totcharge, maxcharge[angular]) #put in a special case for transition metals: #depopulate the 4s if the atom is charged if symbol in transition_metals and symbol in self.initial_charges.keys() \ and self.initial_charges[symbol] > 0 and found_orbitals.count(angular) > 1 \ and angular=="s": charge = 0 totcharge += charge ret += [ "0 %i %i %g 1" % (basis_index[angular], nterms, charge) ] + basis_part ncontract += 1 #Add in the uncontracted basis elements angular_uncontracted = ['s', 'p'] if symbol in transition_metals: angular_uncontracted.append('d') for angular in angular_uncontracted: for i in range(0, self.basis_params[1]): exp = self.basis_params[0] * self.basis_params[2]**i line = '{} {}'.format(exp, 1.0) ret += ["0 %i %i %g 1" % (basis_index[angular], 1, 0.0), line] ncontract += 1 return ["%i %i"%(Element(symbol).number+200,ncontract)] +\ self.pseudopotential_section(symbol) +\ ret
def from_steps(step1, step2, normalization_els): """ Creates a ConversionVoltagePair from two steps in the element profile from a PD analysis. Args: step1: Starting step step2: Ending step normalization_els: Elements to normalize the reaction by. To ensure correct capacities. """ working_ion_entry = step1["element_reference"] working_ion = working_ion_entry.composition.elements[0].symbol voltage = -step1["chempot"] + working_ion_entry.energy_per_atom mAh = (step2["evolution"] - step1["evolution"]) \ * ELECTRON_TO_AMPERE_HOURS * 1000 licomp = Composition.from_formula(working_ion) prev_rxn = step1["reaction"] reactants = { comp: abs(prev_rxn.get_coeff(comp)) for comp in prev_rxn.products if comp != licomp } curr_rxn = step2["reaction"] products = { comp: abs(curr_rxn.get_coeff(comp)) for comp in curr_rxn.products if comp != licomp } reactants[licomp] = (step2["evolution"] - step1["evolution"]) rxn = BalancedReaction(reactants, products) for el, amt in normalization_els.items(): if rxn.get_el_amount(el) != 0: rxn.normalize_to_element(el, amt) break prev_mass_dischg = sum([ prev_rxn.all_comp[i].weight * abs(prev_rxn.coeffs[i]) for i in xrange(len(prev_rxn.all_comp)) ]) / 2 vol_charge = sum([ abs(prev_rxn.get_coeff(e.composition)) * e.structure.volume for e in step1["entries"] if e.composition.reduced_formula != working_ion ]) mass_discharge = sum([ curr_rxn.all_comp[i].weight * abs(curr_rxn.coeffs[i]) for i in xrange(len(curr_rxn.all_comp)) ]) / 2 mass_charge = prev_mass_dischg mass_discharge = mass_discharge vol_discharge = sum([ abs(curr_rxn.get_coeff(e.composition)) * e.structure.volume for e in step2["entries"] if e.composition.reduced_formula != working_ion ]) totalcomp = Composition({}) for comp in prev_rxn.products: if comp.reduced_formula != working_ion: totalcomp += comp * abs(prev_rxn.get_coeff(comp)) frac_charge = totalcomp.get_atomic_fraction(Element(working_ion)) totalcomp = Composition({}) for comp in curr_rxn.products: if comp.reduced_formula != working_ion: totalcomp += comp * abs(curr_rxn.get_coeff(comp)) frac_discharge = totalcomp.get_atomic_fraction(Element(working_ion)) rxn = rxn entries_charge = step2["entries"] entries_discharge = step1["entries"] return ConversionVoltagePair(rxn, voltage, mAh, vol_charge, vol_discharge, mass_charge, mass_discharge, frac_charge, frac_discharge, entries_charge, entries_discharge, working_ion_entry)
def plot_element_spd(self, elements, ax, order=['s', 'p', 'd'], scale_factor=5, color_dict=None, legend=True, linewidth=0.75, band_color='black'): """ This function plots the projected band structure on the s, p, and d orbitals for each specified element in the calculated structure. Parameters: ax (matplotlib.pyplot.axis): Axis to plot the data on elements (list): List of element symbols to project onto order (list): This determines the order in which the points are plotted on the graph. This is an option because sometimes certain orbitals can be hidden under other orbitals because they have a larger weight. For example, if the signitures of the d orbitals are greater than that of the s orbitals, it might be smart to choose ['d', 'p', 's'] as the order so the s orbitals are plotted over the d orbitals. scale_factor (float): Factor to scale weights. This changes the size of the points in the scatter plot color_dict (dict[str][str]): This option allow the colors of the s, p, and d orbitals to be specified. Should be in the form of: {'s': <s color>, 'p': <p color>, 'd': <d color>} legend (bool): Determines if the legend should be included or not. linewidth (float): Line width of the plain band structure plotted in the background band_color (string): Color of the plain band structure """ scale_factor = scale_factor ** 1.5 self.plot_plain(ax=ax, linewidth=linewidth, color=band_color) element_dict = self._sum_elements( elements=elements, orbitals=True, spd=True) if color_dict is None: color_dict = { 's': self.color_dict[0], 'p': self.color_dict[1], 'd': self.color_dict[2], 'f': self.color_dict[4], } plot_element = {element: pd.DataFrame() for element in elements} if self.forbitals and 'f' not in order: order.append('f') plot_band = [] plot_wave_vec = [] for band in element_dict: plot_band.extend(self.bands_dict[band]) plot_wave_vec.extend(range(len(self.bands_dict[band]))) for element in elements: plot_element[element] = plot_element[element].append( element_dict[band][element]) for (i, element) in enumerate(elements): if self.forbitals: electronic_structure = Element( element).full_electronic_structure if not np.isin('f', electronic_structure): order = order.remove('f') for orbital in order: ax.scatter( plot_wave_vec, plot_band, c=color_dict[orbital], s=scale_factor * plot_element[element][orbital], zorder=1, ) if legend: legend_lines = [] legend_labels = [] for element in elements: for orbital in order: legend_lines.append(plt.Line2D( [0], [0], marker='o', markersize=2, linestyle='', color=color_dict[orbital]) ) legend_labels.append( f'{element}(${orbital}$)' ) leg = ax.get_legend() if leg is None: handles = legend_lines labels = legend_labels else: handles = [l._legmarker for l in leg.legendHandles] labels = [text._text for text in leg.texts] handles.extend(legend_lines) labels.extend(legend_labels) ax.legend( handles, labels, ncol=1, loc='upper left', fontsize=5, bbox_to_anchor=(1, 1), borderaxespad=0, frameon=False, handletextpad=0.1, )
def get_ionic_radius(element, oxidationNo): if element == 'Mn' and oxidationNo == 4: radius = 0.67 else: radius = Element(element).ionic_radii[oxidationNo] return radius
def compute_corrections(self, exp_entries: list, calc_entries: dict) -> dict: """ Computes the corrections and fills in correction, corrections_std_error, and corrections_dict. Args: exp_entries: list of dictionary objects with the following keys/values: {"formula": chemical formula, "exp energy": formation energy in eV/formula unit, "uncertainty": uncertainty in formation energy} calc_entries: dictionary of computed entries, of the form {chemical formula: ComputedEntry} Raises: ValueError: calc_compounds is missing an entry """ self.exp_compounds = exp_entries self.calc_compounds = calc_entries self.names: List[str] = [] self.diffs: List[float] = [] self.coeff_mat: List[List[float]] = [] self.exp_uncer: List[float] = [] # remove any corrections in calc_compounds for entry in self.calc_compounds.values(): entry.correction = 0 for cmpd_info in self.exp_compounds: # to get consistent element ordering in formula name = Composition(cmpd_info["formula"]).reduced_formula allow = True compound = self.calc_compounds.get(name, None) if not compound: warnings.warn( "Compound {} is not found in provided computed entries and is excluded from the fit".format(name) ) continue # filter out compounds with large uncertainties relative_uncertainty = abs(cmpd_info["uncertainty"] / cmpd_info["exp energy"]) if relative_uncertainty > self.max_error: allow = False warnings.warn( "Compound {} is excluded from the fit due to high experimental uncertainty ({}%)".format( name, relative_uncertainty ) ) # filter out compounds containing certain polyanions for anion in self.exclude_polyanions: if anion in name or anion in cmpd_info["formula"]: allow = False warnings.warn( "Compound {} contains the polyanion {} and is excluded from the fit".format(name, anion) ) break # filter out compounds that are unstable if isinstance(self.allow_unstable, float): try: eah = compound.data["e_above_hull"] except KeyError: raise ValueError("Missing e above hull data") if eah > self.allow_unstable: allow = False warnings.warn( "Compound {} is unstable and excluded from the fit (e_above_hull = {})".format(name, eah) ) if allow: comp = Composition(name) elems = list(comp.as_dict()) reactants = [] for elem in elems: try: elem_name = Composition(elem).reduced_formula reactants.append(self.calc_compounds[elem_name]) except KeyError: raise ValueError("Computed entries missing " + elem) rxn = ComputedReaction(reactants, [compound]) rxn.normalize_to(comp) energy = rxn.calculated_reaction_energy coeff = [] for specie in self.species: if specie == "oxide": if compound.data["oxide_type"] == "oxide": coeff.append(comp["O"]) self.oxides.append(name) else: coeff.append(0) elif specie == "peroxide": if compound.data["oxide_type"] == "peroxide": coeff.append(comp["O"]) self.peroxides.append(name) else: coeff.append(0) elif specie == "superoxide": if compound.data["oxide_type"] == "superoxide": coeff.append(comp["O"]) self.superoxides.append(name) else: coeff.append(0) elif specie == "S": if Element("S") in comp: sf_type = "sulfide" if compound.data.get("sulfide_type"): sf_type = compound.data["sulfide_type"] elif hasattr(compound, "structure"): sf_type = sulfide_type(compound.structure) if sf_type == "sulfide": coeff.append(comp["S"]) self.sulfides.append(name) else: coeff.append(0) else: coeff.append(0) else: try: coeff.append(comp[specie]) except ValueError: raise ValueError("We can't detect this specie: {}".format(specie)) self.names.append(name) self.diffs.append((cmpd_info["exp energy"] - energy) / comp.num_atoms) self.coeff_mat.append([i / comp.num_atoms for i in coeff]) self.exp_uncer.append((cmpd_info["uncertainty"]) / comp.num_atoms) # for any exp entries with no uncertainty value, assign average uncertainty value sigma = np.array(self.exp_uncer) sigma[sigma == 0] = np.nan with warnings.catch_warnings(): warnings.simplefilter( "ignore", category=RuntimeWarning ) # numpy raises warning if the entire array is nan values mean_uncer = np.nanmean(sigma) sigma = np.where(np.isnan(sigma), mean_uncer, sigma) if np.isnan(mean_uncer): # no uncertainty values for any compounds, don't try to weight popt, self.pcov = curve_fit(_func, self.coeff_mat, self.diffs, p0=np.ones(len(self.species))) else: popt, self.pcov = curve_fit( _func, self.coeff_mat, self.diffs, p0=np.ones(len(self.species)), sigma=sigma, absolute_sigma=True, ) self.corrections = popt.tolist() self.corrections_std_error = np.sqrt(np.diag(self.pcov)).tolist() for i in range(len(self.species)): self.corrections_dict[self.species[i]] = ( round(self.corrections[i], 3), round(self.corrections_std_error[i], 4), ) return self.corrections_dict
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_get_data(self): props = { "energy", "energy_per_atom", "formation_energy_per_atom", "nsites", "unit_cell_formula", "pretty_formula", "is_hubbard", "elements", "nelements", "e_above_hull", "hubbards", "is_compatible", "task_ids", "density", "icsd_ids", "total_magnetization", } mpid = "mp-1143" vals = requests.get( f"http://www.materialsproject.org/materials/{mpid}/json/") expected_vals = vals.json() for prop in props: if prop not in [ "hubbards", "unit_cell_formula", "elements", "icsd_ids", "task_ids", ]: val = self.rester.get_data(mpid, prop=prop)[0][prop] if prop in ["energy", "energy_per_atom"]: prop = "final_" + prop self.assertAlmostEqual(expected_vals[prop], val, 2, f"Failed with property {prop}") elif prop in ["elements", "icsd_ids", "task_ids"]: upstream_vals = set( self.rester.get_data(mpid, prop=prop)[0][prop]) self.assertLessEqual(set(expected_vals[prop]), upstream_vals) else: self.assertEqual( expected_vals[prop], self.rester.get_data(mpid, prop=prop)[0][prop], ) props = ["structure", "initial_structure", "final_structure", "entry"] for prop in props: obj = self.rester.get_data(mpid, prop=prop)[0][prop] if prop.endswith("structure"): self.assertIsInstance(obj, Structure) elif prop == "entry": obj = self.rester.get_data(mpid, prop=prop)[0][prop] self.assertIsInstance(obj, ComputedEntry) # Test chemsys search data = self.rester.get_data("Fe-Li-O", prop="unit_cell_formula") self.assertTrue(len(data) > 1) elements = {Element("Li"), Element("Fe"), Element("O")} for d in data: self.assertTrue( set(Composition( d["unit_cell_formula"]).elements).issubset(elements)) self.assertRaises(MPRestError, self.rester.get_data, "Fe2O3", "badmethod")
def test_is_metal(self): for metal in ["Fe", "Eu", "Li", "Ca", "In"]: self.assertTrue(Element(metal).is_metal) for non_metal in ["Ge", "Si", "O", "He"]: self.assertFalse(Element(non_metal).is_metal)
def get_structure_type(structure, write_poscar_from_cluster=False): """ This is a topology-scaling algorithm used to describe the periodicity of bonded clusters in a bulk structure. Args: structure (structure): Pymatgen structure object to classify. write_poscar_from_cluster (bool): Set to True to write a POSCAR from the sites in the cluster. Returns: string. 'molecular' (0D), 'chain', 'layered', 'heterogeneous' (intercalated 3D), or 'conventional' (3D) """ # The conventional standard structure is much easier to work # with. structure = SpacegroupAnalyzer( structure).get_conventional_standard_structure() # Noble gases don't have well-defined bonding radii. if not len([ e for e in structure.composition if e.symbol in ['He', 'Ne', 'Ar', 'Kr', 'Xe'] ]) == 0: type = 'noble gas' else: if len(structure.sites) < 45: structure.make_supercell(2) # Create a dict of sites as keys and lists of their # bonded neighbors as values. sites = structure.sites bonds = {} for site in sites: bonds[site] = [] for i in range(len(sites)): site_1 = sites[i] for site_2 in sites[i + 1:]: if (site_1.distance(site_2) < float( Element(site_1.specie).atomic_radius + Element(site_2.specie).atomic_radius) * 1.1): bonds[site_1].append(site_2) bonds[site_2].append(site_1) # Assimilate all bonded atoms in a cluster; terminate # when it stops growing. cluster_terminated = False while not cluster_terminated: original_cluster_size = len(bonds[sites[0]]) for site in bonds[sites[0]]: bonds[sites[0]] += [ s for s in bonds[site] if s not in bonds[sites[0]] ] if len(bonds[sites[0]]) == original_cluster_size: cluster_terminated = True original_cluster = bonds[sites[0]] if len(bonds[sites[0]]) == 0: # i.e. the cluster is a single atom. type = 'molecular' elif len(bonds[sites[0]]) == len(sites): # i.e. all atoms are bonded. type = 'conventional' else: # If the cluster's composition is not equal to the # structure's overall composition, it is a heterogeneous # compound. cluster_composition_dict = {} for site in bonds[sites[0]]: if Element(site.specie) in cluster_composition_dict: cluster_composition_dict[Element(site.specie)] += 1 else: cluster_composition_dict[Element(site.specie)] = 1 uniform = True if len(cluster_composition_dict): cmp = Composition.from_dict(cluster_composition_dict) if cmp.reduced_formula != structure.composition.reduced_formula: uniform = False if not uniform: type = 'heterogeneous' else: # Make a 2x2x2 supercell and recalculate the # cluster's new size. If the new cluster size is # the same as the old size, it is a non-periodic # molecule. If it is 2x as big, it's a 1D chain. # If it's 4x as big, it is a layered material. old_cluster_size = len(bonds[sites[0]]) structure.make_supercell(2) sites = structure.sites bonds = {} for site in sites: bonds[site] = [] for i in range(len(sites)): site_1 = sites[i] for site_2 in sites[i + 1:]: if (site_1.distance(site_2) < float( Element(site_1.specie).atomic_radius + Element(site_2.specie).atomic_radius) * 1.1): bonds[site_1].append(site_2) bonds[site_2].append(site_1) cluster_terminated = False while not cluster_terminated: original_cluster_size = len(bonds[sites[0]]) for site in bonds[sites[0]]: bonds[sites[0]] += [ s for s in bonds[site] if s not in bonds[sites[0]] ] if len(bonds[sites[0]]) == original_cluster_size: cluster_terminated = True if len(bonds[sites[0]]) != 4 * old_cluster_size: type = 'molecular' else: type = 'layered' if write_poscar_from_cluster: Structure.from_sites(original_cluster).to('POSCAR', 'POSCAR') return type
def predict_k_g_list(material_id_list, api_key=API_KEY, query_engine=None): """ Predict bulk (K) and shear (G) moduli for a list of materials. :param material_id_list: list of material-ID strings :param api_key: The API key used by pymatgen.matproj.rest.MPRester to connect to Materials Project :param query_engine: (Optional) QueryEngine object used to query materials instead of MPRester :return: (matid_list, predicted_k_list, predicted_g_list, caveats_list) Note that len(matid_list) may be less than len(material_id_list), if any requested material-IDs are not found. """ if len(material_id_list) == 0 or not isinstance(material_id_list, list): return (None, None, None, None ) # material_id_list not properly specified lvpa_list = [] cepa_list = [] rowH1A_list = [] rowHn3A_list = [] xH4A_list = [] xHn4A_list = [] matid_list = [] k_list = [] g_list = [] caveats_list = [] aiab_problem_list = [] # TODO: figure out if closing the query engine (using 'with' ctx mgr) is an issue # If it is a problem then try manually doing a session.close() for MPRester, but ignore for qe mpr = _get_mp_query(api_key, query_engine) for entry in mpr.query(criteria={"task_id": { "$in": material_id_list }}, properties=[ "material_id", "pretty_formula", "nsites", "volume", "energy_per_atom", "is_hubbard" ]): caveats_str = '' aiab_flag = False f_block_flag = False weight_list = [] energy_list = [] row_list = [] x_list = [] # Construct per-element lists for this material composition = Composition(str(entry["pretty_formula"])) for element_key, amount in composition.get_el_amt_dict().iteritems(): element = Element(element_key) weight_list.append(composition.get_atomic_fraction(element)) aiab_energy = get_element_aiab_energy( element_key) # aiab = atom-in-a-box if aiab_energy is None: aiab_flag = True break energy_list.append(aiab_energy) if element.block == 'f': f_block_flag = True row_list.append(element.row) x_list.append(element.X) # On error, add material to aiab_problem_list and continue with next material if aiab_flag: aiab_problem_list.append(str(entry["material_id"])) continue # Check caveats if bool(entry["is_hubbard"]): if len(caveats_str) > 0: caveats_str += " " caveats_str += CAVEAT_HUBBARD if f_block_flag: if len(caveats_str) > 0: caveats_str += " " caveats_str += CAVEAT_F_BLOCK # Calculate intermediate weighted averages (WA) for this material ewa = np.average(energy_list, weights=weight_list) # atom-in-a-box energy WA print str(entry["material_id"]) # Append descriptors for this material to descriptor lists lvpa_list.append( math.log10(float(entry["volume"]) / float(entry["nsites"]))) cepa_list.append(float(entry["energy_per_atom"]) - ewa) rowH1A_list.append(holder_mean(row_list, 1.0, weights=weight_list)) rowHn3A_list.append(holder_mean(row_list, -3.0, weights=weight_list)) xH4A_list.append(holder_mean(x_list, 4.0, weights=weight_list)) xHn4A_list.append(holder_mean(x_list, -4.0, weights=weight_list)) matid_list.append(str(entry["material_id"])) caveats_list.append(caveats_str) if isinstance(mpr, MPRester): mpr.session.close() # Check that at least one valid material was provided num_predictions = len(matid_list) if num_predictions > 0: # Construct descriptor arrays if (len(lvpa_list) != num_predictions or len(cepa_list) != num_predictions or len(rowH1A_list) != num_predictions or len(rowHn3A_list) != num_predictions or len(xH4A_list) != num_predictions or len(xHn4A_list) != num_predictions): return (None, None, None, None) k_descriptors = np.ascontiguousarray( [lvpa_list, rowH1A_list, cepa_list, xHn4A_list], dtype=float) g_descriptors = np.ascontiguousarray( [cepa_list, lvpa_list, rowHn3A_list, xH4A_list], dtype=float) # Allocate prediction arrays k_predictions = np.empty(num_predictions) g_predictions = np.empty(num_predictions) # Make predictions k_filename = os.path.join(os.path.dirname(__file__), DATAFILE_K) g_filename = os.path.join(os.path.dirname(__file__), DATAFILE_G) gbml.core.predict(k_filename, num_predictions, k_descriptors, k_predictions) gbml.core.predict(g_filename, num_predictions, g_descriptors, g_predictions) k_list = np.power(10.0, k_predictions).tolist() g_list = np.power(10.0, g_predictions).tolist() # Append aiab problem cases for entry in aiab_problem_list: matid_list.append(entry) k_list.append(None) g_list.append(None) caveats_list.append(CAVEAT_AIAB) if len(matid_list) == 0: return (None, None, None, None) else: return (matid_list, k_list, g_list, caveats_list)
def get_correction(self, entry) -> float: """ :param entry: A ComputedEntry/ComputedStructureEntry :return: Correction. """ comp = entry.composition if len(comp) == 1: # Skip element entry return 0 correction = 0 # Check for sulfide corrections if Element("S") in comp: sf_type = "sulfide" if entry.data.get("sulfide_type"): sf_type = entry.data["sulfide_type"] elif hasattr(entry, "structure"): sf_type = sulfide_type(entry.structure) if sf_type in self.sulfide_correction: correction += self.sulfide_correction[sf_type] * comp["S"] # Check for oxide, peroxide, superoxide, and ozonide corrections. if Element("O") in comp: if self.correct_peroxide: if entry.data.get("oxide_type"): if entry.data["oxide_type"] in self.oxide_correction: ox_corr = self.oxide_correction[ entry.data["oxide_type"]] correction += ox_corr * comp["O"] if entry.data["oxide_type"] == "hydroxide": ox_corr = self.oxide_correction["oxide"] correction += ox_corr * comp["O"] elif hasattr(entry, "structure"): ox_type, nbonds = oxide_type(entry.structure, 1.05, return_nbonds=True) if ox_type in self.oxide_correction: correction += self.oxide_correction[ox_type] * \ nbonds elif ox_type == "hydroxide": correction += self.oxide_correction["oxide"] * \ comp["O"] else: warnings.warn( "No structure or oxide_type parameter present. Note " "that peroxide/superoxide corrections are not as " "reliable and relies only on detection of special" "formulas, e.g., Li2O2.") rform = entry.composition.reduced_formula if rform in UCorrection.common_peroxides: correction += self.oxide_correction["peroxide"] * \ comp["O"] elif rform in UCorrection.common_superoxides: correction += self.oxide_correction["superoxide"] * \ comp["O"] elif rform in UCorrection.ozonides: correction += self.oxide_correction["ozonide"] * \ comp["O"] elif Element("O") in comp.elements and len(comp.elements) \ > 1: correction += self.oxide_correction['oxide'] * \ comp["O"] else: correction += self.oxide_correction['oxide'] * comp["O"] return correction
def test_init(self): d = {'Fe': 1, Element('Fe'): 1} self.assertRaises(ValueError, ChemicalPotential, d) for k in ChemicalPotential(Fe=1).keys(): self.assertIsInstance(k, Element)
def write_data_file(self, organism, job_dir_path, composition_space): """ Writes the file (called in.data) containing the structure that LAMMPS reads. Args: organism: the Organism whose structure to write job_dir_path: the path the job directory (as a string) where the file will be written composition_space: the CompositionSpace of the search """ # get xhi, yhi and zhi from the lattice vectors lattice_coords = organism.cell.lattice.matrix xhi = lattice_coords[0][0] yhi = lattice_coords[1][1] zhi = lattice_coords[2][2] box_size = [[0.0, xhi], [0.0, yhi], [0.0, zhi]] # get xy, xz and yz from the lattice vectors xy = lattice_coords[1][0] xz = lattice_coords[2][0] yz = lattice_coords[2][1] # get a list of the elements from the lammps input script to # preserve their order of appearance # TODO: not all formats give the element symbols at the end of the line # containing the pair_coeff keyword. Find a better way. num_elements = len(composition_space.get_all_elements()) if num_elements == 1: all_elements = composition_space.get_all_elements() else: with open(self.input_script, 'r') as f: lines = f.readlines() for line in lines: if 'pair_coeff' in line: element_symbols = line.split()[-1 * num_elements:] all_elements = [] for symbol in element_symbols: all_elements.append(Element(symbol)) # get the dictionary of atomic masses - set the atom types to the order # of their appearance in the lammps input script atomic_masses_dict = {} for i in range(len(all_elements)): atomic_masses_dict[all_elements[i].symbol] = [ i + 1, float(all_elements[i].atomic_mass) ] # get the atoms data atoms_data = LammpsData.get_atoms_data(organism.cell, atomic_masses_dict, set_charge=True) # make a LammpsData object lammps_data = LammpsData(box_size, atomic_masses_dict.values(), atoms_data) # write the data to a file # This method doesn't write the tilts, so we have to add those. lammps_data.write_file(job_dir_path + '/in.data') # read the in.data file as a list of strings with open(job_dir_path + '/in.data', 'r') as f: lines = f.readlines() # find the location to insert the tilts insertion_index = 0 for line in lines: if 'zhi' in line: insertion_index = lines.index(line) + 1 # build the string containing the tilts and add it tilts_string = str(xy) + ' ' + str(xz) + ' ' + str(yz) + ' xy xz yz\n' lines.insert(insertion_index, tilts_string) # overwrite the in.data file with the new contents, including the tilts with open(job_dir_path + '/in.data', 'w') as f: for line in lines: f.write('%s' % line)
def from_entries(cls, entry1, entry2, working_ion_entry): """ Args: entry1: Entry corresponding to one of the entries in the voltage step. entry2: Entry corresponding to the other entry in the voltage step. working_ion_entry: A single ComputedEntry or PDEntry representing the element that carries charge across the battery, e.g. Li. """ # initialize some internal variables working_element = working_ion_entry.composition.elements[0] entry_charge = entry1 entry_discharge = entry2 if entry_charge.composition.get_atomic_fraction(working_element) > entry2.composition.get_atomic_fraction( working_element ): (entry_charge, entry_discharge) = (entry_discharge, entry_charge) comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition ion_sym = working_element.symbol frame_charge_comp = Composition({el: comp_charge[el] for el in comp_charge if el.symbol != ion_sym}) frame_discharge_comp = Composition({el: comp_discharge[el] for el in comp_discharge if el.symbol != ion_sym}) # Data validation # check that the ion is just a single element if not working_ion_entry.composition.is_element: raise ValueError("VoltagePair: The working ion specified must be " "an element") # check that at least one of the entries contains the working element if ( not comp_charge.get_atomic_fraction(working_element) > 0 and not comp_discharge.get_atomic_fraction(working_element) > 0 ): raise ValueError("VoltagePair: The working ion must be present in " "one of the entries") # check that the entries do not contain the same amount of the workin # element if comp_charge.get_atomic_fraction(working_element) == comp_discharge.get_atomic_fraction(working_element): raise ValueError("VoltagePair: The working ion atomic percentage " "cannot be the same in both the entries") # check that the frameworks of the entries are equivalent if not frame_charge_comp.reduced_formula == frame_discharge_comp.reduced_formula: raise ValueError("VoltagePair: the specified entries must have the" " same compositional framework") # Initialize normalization factors, charged and discharged entries valence_list = Element(ion_sym).oxidation_states working_ion_valence = abs(max(valence_list)) ( framework, norm_charge, ) = frame_charge_comp.get_reduced_composition_and_factor() norm_discharge = frame_discharge_comp.get_reduced_composition_and_factor()[1] # Initialize normalized properties if hasattr(entry_charge, "structure"): _vol_charge = entry_charge.structure.volume / norm_charge else: _vol_charge = entry_charge.data.get("volume") if hasattr(entry_discharge, "structure"): _vol_discharge = entry_discharge.structure.volume / norm_discharge else: _vol_discharge = entry_discharge.data.get("volume") comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition _mass_charge = comp_charge.weight / norm_charge _mass_discharge = comp_discharge.weight / norm_discharge _num_ions_transferred = (comp_discharge[working_element] / norm_discharge) - ( comp_charge[working_element] / norm_charge ) _voltage = ( ((entry_charge.energy / norm_charge) - (entry_discharge.energy / norm_discharge)) / _num_ions_transferred + working_ion_entry.energy_per_atom ) / working_ion_valence _mAh = _num_ions_transferred * Charge(1, "e").to("C") * Time(1, "s").to("h") * N_A * 1000 * working_ion_valence _frac_charge = comp_charge.get_atomic_fraction(working_element) _frac_discharge = comp_discharge.get_atomic_fraction(working_element) vpair = cls( voltage=_voltage, mAh=_mAh, mass_charge=_mass_charge, mass_discharge=_mass_discharge, vol_charge=_vol_charge, vol_discharge=_vol_discharge, frac_charge=_frac_charge, frac_discharge=_frac_discharge, working_ion_entry=working_ion_entry, entry_charge=entry_charge, entry_discharge=entry_discharge, _framework_formula=framework.reduced_formula, ) # Step 4: add (optional) hull and muO2 data vpair.decomp_e_charge = entry_charge.data.get("decomposition_energy", None) vpair.decomp_e_discharge = entry_discharge.data.get("decomposition_energy", None) vpair.muO2_charge = entry_charge.data.get("muO2", None) vpair.muO2_discharge = entry_discharge.data.get("muO2", None) return vpair
def get_relaxed_cell(self, atom_dump_path, data_in_path, element_symbols): """ Parses the relaxed cell from the dump.atom file. Returns the relaxed cell as a Cell object. Args: atom_dump_path: the path (as a string) to the dump.atom file in_data_path: the path (as a string) to the in.data file element_symbols: a tuple containing the set of chemical symbols of all the elements in the compositions space """ # read the dump.atom file as a list of strings with open(atom_dump_path, 'r') as atom_dump: lines = atom_dump.readlines() # get the lattice vectors a_data = lines[5].split() b_data = lines[6].split() c_data = lines[7].split() # parse the tilt factors xy = float(a_data[2]) xz = float(b_data[2]) yz = float(c_data[2]) # parse the bounds xlo_bound = float(a_data[0]) xhi_bound = float(a_data[1]) ylo_bound = float(b_data[0]) yhi_bound = float(b_data[1]) zlo_bound = float(c_data[0]) zhi_bound = float(c_data[1]) # compute xlo, xhi, ylo, yhi, zlo and zhi according to the conversion # given by LAMMPS # http://lammps.sandia.gov/doc/Section_howto.html#howto-12 xlo = xlo_bound - min([0.0, xy, xz, xy + xz]) xhi = xhi_bound - max([0.0, xy, xz, xy + xz]) ylo = ylo_bound - min(0.0, yz) yhi = yhi_bound - max([0.0, yz]) zlo = zlo_bound zhi = zhi_bound # construct a Lattice object from the lo's and hi's and tilts a = [xhi - xlo, 0.0, 0.0] b = [xy, yhi - ylo, 0.0] c = [xz, yz, zhi - zlo] relaxed_lattice = Lattice([a, b, c]) # get the number of atoms num_atoms = int(lines[3]) # get the atom types and their Cartesian coordinates types = [] relaxed_cart_coords = [] for i in range(num_atoms): atom_info = lines[9 + i].split() types.append(int(atom_info[1])) relaxed_cart_coords.append([ float(atom_info[2]) - xlo, float(atom_info[3]) - ylo, float(atom_info[4]) - zlo ]) # read the atom types and corresponding atomic masses from in.data with open(data_in_path, 'r') as data_in: lines = data_in.readlines() types_masses = {} for i in range(len(lines)): if 'Masses' in lines[i]: for j in range(len(element_symbols)): types_masses[int(lines[i + j + 2].split()[0])] = float( lines[i + j + 2].split()[1]) # map the atom types to chemical symbols types_symbols = {} for symbol in element_symbols: for atom_type in types_masses: # round the atomic masses to one decimal point for comparison if format(float(Element(symbol).atomic_mass), '.1f') == format(types_masses[atom_type], '.1f'): types_symbols[atom_type] = symbol # make a list of chemical symbols (one for each site) relaxed_symbols = [] for atom_type in types: relaxed_symbols.append(types_symbols[atom_type]) return Cell(relaxed_lattice, relaxed_symbols, relaxed_cart_coords, coords_are_cartesian=True)
def test_get_data(self): props = [ "energy", "energy_per_atom", "formation_energy_per_atom", "nsites", "unit_cell_formula", "pretty_formula", "is_hubbard", "elements", "nelements", "e_above_hull", "hubbards", "is_compatible", "task_ids", "density", "icsd_ids", "total_magnetization" ] # unicode literals have been reintroduced in py>3.2 expected_vals = [ -191.33812137, -6.833504334642858, -2.551358929370749, 28, {k: v for k, v in { 'P': 4, 'Fe': 4, 'O': 16, 'Li': 4 }.items()}, "LiFePO4", True, ['Li', 'O', 'P', 'Fe'], 4, 0.0, { k: v for k, v in { 'Fe': 5.3, 'Li': 0.0, 'O': 0.0, 'P': 0.0 }.items() }, True, [ u'mp-601412', u'mp-19017', u'mp-796535', u'mp-797820', u'mp-540081', u'mp-797269' ], 3.4662026991351147, [ 159107, 154117, 160776, 99860, 181272, 166815, 260571, 92198, 165000, 155580, 38209, 161479, 153699, 260569, 260570, 200155, 260572, 181341, 181342, 72545, 56291, 97764, 162282, 155635 ], 16.0002716 ] for (i, prop) in enumerate(props): if prop not in [ 'hubbards', 'unit_cell_formula', 'elements', 'icsd_ids', 'task_ids' ]: val = self.rester.get_data("mp-19017", prop=prop)[0][prop] self.assertAlmostEqual(expected_vals[i], val) elif prop in ["elements", "icsd_ids", "task_ids"]: self.assertEqual( set(expected_vals[i]), set(self.rester.get_data("mp-19017", prop=prop)[0][prop])) else: self.assertEqual( expected_vals[i], self.rester.get_data("mp-19017", prop=prop)[0][prop]) props = ['structure', 'initial_structure', 'final_structure', 'entry'] for prop in props: obj = self.rester.get_data("mp-19017", prop=prop)[0][prop] if prop.endswith("structure"): self.assertIsInstance(obj, Structure) elif prop == "entry": obj = self.rester.get_data("mp-19017", prop=prop)[0][prop] self.assertIsInstance(obj, ComputedEntry) #Test chemsys search data = self.rester.get_data('Fe-Li-O', prop='unit_cell_formula') self.assertTrue(len(data) > 1) elements = {Element("Li"), Element("Fe"), Element("O")} for d in data: self.assertTrue( set(Composition( d['unit_cell_formula']).elements).issubset(elements)) self.assertRaises(MPRestError, self.rester.get_data, "Fe2O3", "badmethod")
def parse_oxide(self): """ Determines if an oxide is a peroxide/superoxide/ozonide/normal oxide. Returns: oxide_type (str): Type of oxide ozonide/peroxide/superoxide/hydroxide/None. nbonds (int): Number of peroxide/superoxide/hydroxide bonds in structure. """ structure = self.structure relative_cutoff = self.relative_cutoff o_sites_frac_coords = [] h_sites_frac_coords = [] lattice = structure.lattice if isinstance(structure.composition.elements[0], Element): comp = structure.composition elif isinstance(structure.composition.elements[0], Species): elmap = collections.defaultdict(float) for site in structure: for species, occu in site.species.items(): elmap[species.element] += occu comp = Composition(elmap) if Element("O") not in comp or comp.is_element: return "None", 0 for site in structure: syms = [sp.symbol for sp in site.species.keys()] if "O" in syms: o_sites_frac_coords.append(site.frac_coords) if "H" in syms: h_sites_frac_coords.append(site.frac_coords) if h_sites_frac_coords: dist_matrix = lattice.get_all_distances(o_sites_frac_coords, h_sites_frac_coords) if np.any(dist_matrix < relative_cutoff * 0.93): return ( "hydroxide", len(np.where(dist_matrix < relative_cutoff * 0.93)[0]) / 2.0, ) dist_matrix = lattice.get_all_distances(o_sites_frac_coords, o_sites_frac_coords) np.fill_diagonal(dist_matrix, 1000) is_superoxide = False is_peroxide = False is_ozonide = False if np.any(dist_matrix < relative_cutoff * 1.35): bond_atoms = np.where(dist_matrix < relative_cutoff * 1.35)[0] is_superoxide = True elif np.any(dist_matrix < relative_cutoff * 1.49): is_peroxide = True bond_atoms = np.where(dist_matrix < relative_cutoff * 1.49)[0] if is_superoxide: if len(bond_atoms) > len(set(bond_atoms)): is_superoxide = False is_ozonide = True try: nbonds = len(set(bond_atoms)) except UnboundLocalError: nbonds = 0.0 if is_ozonide: str_oxide = "ozonide" elif is_superoxide: str_oxide = "superoxide" elif is_peroxide: str_oxide = "peroxide" else: str_oxide = "oxide" if str_oxide == "oxide": nbonds = comp["O"] return str_oxide, nbonds
def test_get_critical_compositions(self): c1 = Composition("Fe2O3") c2 = Composition("Li3FeO4") c3 = Composition("Li2O") comps = self.pd.get_critical_compositions(c1, c2) expected = [ Composition("Fe2O3"), Composition("Li0.3243244Fe0.1621621O0.51351349") * 7.4, Composition("Li3FeO4"), ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) comps = self.pd.get_critical_compositions(c1, c3) expected = [ Composition("Fe2O3"), Composition("LiFeO2"), Composition("Li5FeO4") / 3, Composition("Li2O"), ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # Don't fail silently if input compositions aren't in phase diagram # Can be very confusing if you're working with a GrandPotentialPD self.assertRaises( ValueError, self.pd.get_critical_compositions, Composition("Xe"), Composition("Mn"), ) # For the moment, should also fail even if compositions are in the gppd # because it isn't handled properly gppd = GrandPotentialPhaseDiagram(self.pd.all_entries, {"Xe": 1}, self.pd.elements + [Element("Xe")]) self.assertRaises( ValueError, gppd.get_critical_compositions, Composition("Fe2O3"), Composition("Li3FeO4Xe"), ) # check that the function still works though comps = gppd.get_critical_compositions(c1, c2) expected = [ Composition("Fe2O3"), Composition("Li0.3243244Fe0.1621621O0.51351349") * 7.4, Composition("Li3FeO4"), ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # case where the endpoints are identical self.assertEqual(self.pd.get_critical_compositions(c1, c1 * 2), [c1, c1 * 2])
def test_attributes(self): is_true = { ("Xe", "Kr"): "is_noble_gas", ("Fe", "Ni"): "is_transition_metal", ("Li", "Cs"): "is_alkali", ("Ca", "Mg"): "is_alkaline", ("F", "Br", "I"): "is_halogen", ("La",): "is_lanthanoid", ("U", "Pu"): "is_actinoid", ("Si", "Ge"): "is_metalloid", ("O", "Te"): "is_chalcogen", } for k, v in is_true.items(): for sym in k: self.assertTrue(getattr(Element(sym), v), sym + " is false") keys = [ "mendeleev_no", "atomic_mass", "electronic_structure", "atomic_radius", "min_oxidation_state", "max_oxidation_state", "electrical_resistivity", "velocity_of_sound", "reflectivity", "refractive_index", "poissons_ratio", "molar_volume", "thermal_conductivity", "melting_point", "boiling_point", "liquid_range", "critical_temperature", "superconduction_temperature", "bulk_modulus", "youngs_modulus", "brinell_hardness", "rigidity_modulus", "mineral_hardness", "vickers_hardness", "density_of_solid", "atomic_orbitals", "coefficient_of_linear_thermal_expansion", "oxidation_states", "common_oxidation_states", "average_ionic_radius", "average_cationic_radius", "average_anionic_radius", "ionic_radii", "long_name", "metallic_radius", "iupac_ordering", ] # Test all elements up to Uranium for i in range(1, 104): el = Element.from_Z(i) d = el.data for k in keys: k_str = k.capitalize().replace("_", " ") if k_str in d and (not str(d[k_str]).startswith("no data")): self.assertIsNotNone(getattr(el, k)) elif k == "long_name": self.assertEqual(getattr(el, "long_name"), d["Name"]) elif k == "iupac_ordering": self.assertTrue("IUPAC ordering" in d) self.assertIsNotNone(getattr(el, k)) el = Element.from_Z(i) if len(el.oxidation_states) > 0: self.assertEqual(max(el.oxidation_states), el.max_oxidation_state) self.assertEqual(min(el.oxidation_states), el.min_oxidation_state) if el.symbol not in ["He", "Ne", "Ar"]: self.assertTrue(el.X > 0, "No electroneg for %s" % el) self.assertRaises(ValueError, Element.from_Z, 1000)
def eval_vasp_xml(file="vasprun.xml", recip=False, norm_fermi=True, print_out=False): dft = pymatgen.io.vasp.outputs.Vasprun(file, parse_projected_eigen=False) orbital_energy = pd.read_csv("element_orbital_energy.csv").set_index("element") lattice = jnp.asarray(dft.get_trajectory().as_dict()['lattice']).squeeze() lattice_normed = lattice / jnp.linalg.norm(lattice, axis=1, keepdims=True) lattice_recip = jnp.asarray(Lattice(lattice).reciprocal_lattice.matrix) # wrong! positions_base = dft.get_trajectory().as_dict()['base_positions'] positions = jnp.dot(positions_base, lattice) k_points = jnp.asarray(dft.actual_kpoints) weights = jnp.asarray(dft.actual_kpoints_weights) # how to use ? species_dict = {} species_arr = np.asarray(dft.atomic_symbols) count = 0 print(species_arr) for key in dict.fromkeys(set(dft.atomic_symbols), {}): species_dict["species_" + Element(key).long_name] = {"symbol": key, "number": count, "Es": orbital_energy.loc["C", "E_s"], "Ep": orbital_energy.loc["C", "E_p"], "Ed": orbital_energy.loc["C", "E_d"], } species_arr[species_arr == key] = count # cycles through elements but returns correct one anyway count += 1 species_arr = jnp.asarray(species_arr.astype(int)) for key in dft.eigenvalues.keys(): key_last = key true_inp = np.zeros( (dft.eigenvalues[key_last][:, :, 0].shape[0], dft.eigenvalues[key_last][:, :, 0].shape[1], len(dft.eigenvalues.keys()))) count = 0 if len(dft.eigenvalues.keys()) != 1: print("only one spin direction supported but", len(dft.eigenvalues.keys()), "where given") for key in dft.eigenvalues.keys(): # OrderedDictionary might be nice true_inp[:, :, count] = dft.eigenvalues[key][:, :, 0] # what is [:, :, 0] ??????????????????? occupied = np.max(jnp.nonzero(dft.eigenvalues[key][:, :, 1])[1]) + 1 fermi = find_fermi(true_inp, occupied) count += 1 if norm_fermi: true_inp -= fermi print("E fermi calculated normed", find_fermi(true_inp, occupied, plot=False)) if print_out: print("Lattice", type(lattice), lattice.shape, "\n", lattice) print("Lattice Normed", type(lattice_normed), lattice_normed.shape, lattice_normed) print("Lattice recip", type(lattice_recip), lattice_recip.shape, "\n", lattice_recip) print("Positions", type(positions_base), positions_base.shape, positions_base) print("Positions dot", type(positions), positions.shape, "\n", positions) print("kpts", k_points.shape, k_points) print("weights", weights.shape, weights) print("True shape", true_inp.shape, true_inp) print("species", species_arr.shape, species_arr, "\n", species_dict) # print("true", dft.eigenvalues[:].shape, "\n", dft.eigenvalues[dft.eigenvalues.keys()[0]][0, :, 0], "\n", # dft.eigenvalues[dft.eigenvalues.keys()[0]][0, :, 1]) print("E fermi vasp", dft.efermi) print("Highest occupied", occupied) print("E fermi calculated", fermi) if recip: return k_points, weights, lattice_recip, positions, species_arr, species_dict, true_inp, occupied else: return k_points, weights, lattice, positions, species_arr, species_dict, true_inp, occupied
def test_is(self): self.assertTrue(Element("Bi").is_post_transition_metal, True)
def setUp(self): comp = Composition("LiFeO2") self.entry = PDEntry(comp, 53) self.gpentry = GrandPotPDEntry(self.entry, {Element('O'): 1.5})
def van_arkel_triangle(list_of_materials, annotate=True): """ A static method that generates a binary van Arkel-Ketelaar triangle to quantify the ionic, metallic and covalent character of a compound by plotting the electronegativity difference (y) vs average (x). See: A.E. van Arkel, Molecules and Crystals in Inorganic Chemistry, Interscience, New York (1956) and J.A.A Ketelaar, Chemical Constitution (2nd edition), An Introduction to the Theory of the Chemical Bond, Elsevier, New York (1958) Args: list_of_materials (list): A list of computed entries of binary materials or a list of lists containing two elements (str). annotate (bool): Whether or not to label the points on the triangle with reduced formula (if list of entries) or pair of elements (if list of list of str). """ # F-Fr has the largest X difference. We set this # as our top corner of the triangle (most ionic) pt1 = np.array([(Element("F").X + Element("Fr").X) / 2, abs(Element("F").X - Element("Fr").X)]) # Cs-Fr has the lowest average X. We set this as our # bottom left corner of the triangle (most metallic) pt2 = np.array([ (Element("Cs").X + Element("Fr").X) / 2, abs(Element("Cs").X - Element("Fr").X), ]) # O-F has the highest average X. We set this as our # bottom right corner of the triangle (most covalent) pt3 = np.array([(Element("O").X + Element("F").X) / 2, abs(Element("O").X - Element("F").X)]) # get the parameters for the lines of the triangle d = np.array(pt1) - np.array(pt2) slope1 = d[1] / d[0] b1 = pt1[1] - slope1 * pt1[0] d = pt3 - pt1 slope2 = d[1] / d[0] b2 = pt3[1] - slope2 * pt3[0] # Initialize the plt object import matplotlib.pyplot as plt # set labels and appropriate limits for plot plt.xlim(pt2[0] - 0.45, -b2 / slope2 + 0.45) plt.ylim(-0.45, pt1[1] + 0.45) plt.annotate("Ionic", xy=[pt1[0] - 0.3, pt1[1] + 0.05], fontsize=20) plt.annotate("Covalent", xy=[-b2 / slope2 - 0.65, -0.4], fontsize=20) plt.annotate("Metallic", xy=[pt2[0] - 0.4, -0.4], fontsize=20) plt.xlabel(r"$\frac{\chi_{A}+\chi_{B}}{2}$", fontsize=25) plt.ylabel(r"$|\chi_{A}-\chi_{B}|$", fontsize=25) # Set the lines of the triangle chi_list = [el.X for el in Element] plt.plot( [min(chi_list), pt1[0]], [slope1 * min(chi_list) + b1, pt1[1]], "k-", linewidth=3, ) plt.plot([pt1[0], -b2 / slope2], [pt1[1], 0], "k-", linewidth=3) plt.plot([min(chi_list), -b2 / slope2], [0, 0], "k-", linewidth=3) plt.xticks(fontsize=15) plt.yticks(fontsize=15) # Shade with appropriate colors corresponding to ionic, metallci and covalent ax = plt.gca() # ionic filling ax.fill_between( [min(chi_list), pt1[0]], [slope1 * min(chi_list) + b1, pt1[1]], facecolor=[1, 1, 0], zorder=-5, edgecolor=[1, 1, 0], ) ax.fill_between( [pt1[0], -b2 / slope2], [pt1[1], slope2 * min(chi_list) - b1], facecolor=[1, 1, 0], zorder=-5, edgecolor=[1, 1, 0], ) # metal filling XPt = Element("Pt").X ax.fill_between( [min(chi_list), (XPt + min(chi_list)) / 2], [0, slope1 * (XPt + min(chi_list)) / 2 + b1], facecolor=[1, 0, 0], zorder=-3, alpha=0.8, ) ax.fill_between( [(XPt + min(chi_list)) / 2, XPt], [slope1 * ((XPt + min(chi_list)) / 2) + b1, 0], facecolor=[1, 0, 0], zorder=-3, alpha=0.8, ) # covalent filling ax.fill_between( [(XPt + min(chi_list)) / 2, ((XPt + min(chi_list)) / 2 + -b2 / slope2) / 2], [0, slope2 * (((XPt + min(chi_list)) / 2 + -b2 / slope2) / 2) + b2], facecolor=[0, 1, 0], zorder=-4, alpha=0.8, ) ax.fill_between( [((XPt + min(chi_list)) / 2 + -b2 / slope2) / 2, -b2 / slope2], [slope2 * (((XPt + min(chi_list)) / 2 + -b2 / slope2) / 2) + b2, 0], facecolor=[0, 1, 0], zorder=-4, alpha=0.8, ) # Label the triangle with datapoints for entry in list_of_materials: if type(entry).__name__ not in [ "ComputedEntry", "ComputedStructureEntry" ]: X_pair = [Element(el).X for el in entry] el_1, el_2 = entry formatted_formula = f"{el_1}-{el_2}" else: X_pair = [ Element(el).X for el in entry.composition.as_dict().keys() ] formatted_formula = format_formula( entry.composition.reduced_formula) plt.scatter(np.mean(X_pair), abs(X_pair[0] - X_pair[1]), c="b", s=100) if annotate: plt.annotate( formatted_formula, fontsize=15, xy=[np.mean(X_pair) + 0.005, abs(X_pair[0] - X_pair[1])], ) plt.tight_layout() return plt