def get_comp_amt(comp_str):
     return {Composition(m.group(2)): float(m.group(1) or 1)
             for m in re.finditer(r"([\d\.]*(?:[eE]-?[\d\.]+)?)\s*([A-Z][\w\.\(\)]*)",
                                  comp_str)}
Example #2
0
    def from_entries(cls, entries, working_ion_entry, strip_structures=False):
        """
        Create a new InsertionElectrode.

        Args:
            entries: A list of ComputedStructureEntries (or subclasses)
                representing the different topotactic states of the battery,
                e.g. TiO2 and LiTiO2.
            working_ion_entry: A single ComputedEntry or PDEntry
                representing the element that carries charge across the
                battery, e.g. Li.
            strip_structures: Since the electrode document only uses volume we can make the
                electrode object significantly leaner by dropping the structure data.
                If this parameter is set to True, the ComputedStructureEntry will be replaced
                with ComputedEntry and the volume will be stored in ComputedEntry.data['volume']
        """

        if strip_structures:
            ents = []
            for ient in entries:
                dd = ient.as_dict()
                ent = ComputedEntry.from_dict(dd)
                ent.data["volume"] = ient.structure.volume
                ents.append(ent)
            entries = ents

        _working_ion = working_ion_entry.composition.elements[0]
        _working_ion_entry = working_ion_entry

        # Prepare to make phase diagram: determine elements and set their energy
        # to be very high
        elements = set()
        for entry in entries:
            elements.update(entry.composition.elements)

        # Set an artificial energy for each element for convex hull generation
        element_energy = max([entry.energy_per_atom for entry in entries]) + 10

        pdentries = []
        pdentries.extend(entries)
        pdentries.extend(
            [PDEntry(Composition({el: 1}), element_energy) for el in elements])

        # Make phase diagram to determine which entries are stable vs. unstable
        pd = PhaseDiagram(pdentries)

        def lifrac(e):
            return e.composition.get_atomic_fraction(_working_ion)

        # stable entries ordered by amount of Li asc
        _stable_entries = tuple(
            sorted([e for e in pd.stable_entries if e in entries], key=lifrac))

        # unstable entries ordered by amount of Li asc
        _unstable_entries = tuple(
            sorted([e for e in pd.unstable_entries if e in entries],
                   key=lifrac))

        # create voltage pairs
        _vpairs = tuple([
            InsertionVoltagePair.from_entries(
                _stable_entries[i],
                _stable_entries[i + 1],
                working_ion_entry,
            ) for i in range(len(_stable_entries) - 1)
        ])
        framework = _vpairs[0].framework
        return cls(
            voltage_pairs=_vpairs,
            working_ion_entry=_working_ion_entry,
            _stable_entries=_stable_entries,
            _unstable_entries=_unstable_entries,
            _framework_formula=framework.reduced_formula,
        )
Example #3
0
    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
Example #4
0
 def setUp(self):
     self.entries = EntrySet.from_csv(str(module_dir / "pdentries_test.csv"))
     self.pd = CompoundPhaseDiagram(self.entries, [Composition("Li2O"), Composition("Fe2O3")])
Example #5
0
    def process_vasprun(self, dir_name, taskname, filename):
        """
        Adapted from matgendb.creator

        Process a vasprun.xml file.
        """
        vasprun_file = os.path.join(dir_name, filename)

        vrun = Vasprun(vasprun_file, parse_potcar_file=self.parse_potcar_file)

        d = vrun.as_dict()

        # rename formula keys
        for k, v in {
                "formula_pretty": "pretty_formula",
                "composition_reduced": "reduced_cell_formula",
                "composition_unit_cell": "unit_cell_formula",
        }.items():
            d[k] = d.pop(v)

        for k in [
                "eigenvalues",
                "projected_eigenvalues",
        ]:  # large storage space breaks some docs
            if k in d["output"]:
                del d["output"][k]

        comp = Composition(d["composition_unit_cell"])
        d["formula_anonymous"] = comp.anonymized_formula
        d["formula_reduced_abc"] = comp.reduced_composition.alphabetical_formula
        d["dir_name"] = os.path.abspath(dir_name)
        d["completed_at"] = str(
            datetime.datetime.fromtimestamp(os.path.getmtime(vasprun_file)))
        d["density"] = vrun.final_structure.density

        # replace 'crystal' with 'structure'
        d["input"]["structure"] = d["input"].pop("crystal")
        d["output"]["structure"] = d["output"].pop("crystal")
        for k, v in {
                "energy": "final_energy",
                "energy_per_atom": "final_energy_per_atom",
        }.items():
            d["output"][k] = d["output"].pop(v)

        # Process bandstructure and DOS
        if self.bandstructure_mode != False:  # noqa
            bs = self.process_bandstructure(vrun)
            if bs:
                d["bandstructure"] = bs

        if self.parse_dos != False:  # noqa
            dos = self.process_dos(vrun)
            if dos:
                d["dos"] = dos

        # Parse electronic information if possible.
        # For certain optimizers this is broken and we don't get an efermi resulting in the bandstructure
        try:
            bs = vrun.get_band_structure(efermi="smart")
            bs_gap = bs.get_band_gap()
            d["output"]["vbm"] = bs.get_vbm()["energy"]
            d["output"]["cbm"] = bs.get_cbm()["energy"]
            d["output"]["bandgap"] = bs_gap["energy"]
            d["output"]["is_gap_direct"] = bs_gap["direct"]
            d["output"]["is_metal"] = bs.is_metal()
            if not bs_gap["direct"]:
                d["output"]["direct_gap"] = bs.get_direct_band_gap()
            if isinstance(bs, BandStructureSymmLine):
                d["output"]["transition"] = bs_gap["transition"]

        except Exception:
            logger.warning("Error in parsing bandstructure")
            if vrun.incar["IBRION"] == 1:
                logger.warning(
                    "Vasp doesn't properly output efermi for IBRION == 1")
            if self.bandstructure_mode is True:
                logger.error(traceback.format_exc())
                logger.error("Error in " + os.path.abspath(dir_name) + ".\n" +
                             traceback.format_exc())
                raise

        # Should roughly agree with information from .get_band_structure() above, subject to tolerances
        # If there is disagreement, it may be related to VASP incorrectly assigning the Fermi level
        try:
            band_props = vrun.eigenvalue_band_properties
            d["output"]["eigenvalue_band_properties"] = {
                "bandgap": band_props[0],
                "cbm": band_props[1],
                "vbm": band_props[2],
                "is_gap_direct": band_props[3],
            }
        except Exception:
            logger.warning("Error in parsing eigenvalue band properties")

        # store run name and location ,e.g. relax1, relax2, etc.
        d["task"] = {"type": taskname, "name": taskname}

        # include output file names
        d["output_file_paths"] = self.process_raw_data(dir_name,
                                                       taskname=taskname)

        # parse axially averaged locpot
        if "locpot" in d["output_file_paths"] and self.parse_locpot:
            locpot = Locpot.from_file(
                os.path.join(dir_name, d["output_file_paths"]["locpot"]))
            d["output"]["locpot"] = {
                i: locpot.get_average_along_axis(i)
                for i in range(3)
            }

        if self.store_volumetric_data:
            for file in self.store_volumetric_data:
                if file in d["output_file_paths"]:
                    try:
                        # assume volumetric data is all in CHGCAR format
                        data = Chgcar.from_file(
                            os.path.join(dir_name,
                                         d["output_file_paths"][file]))
                        d[file] = data.as_dict()
                    except Exception:
                        raise ValueError("Failed to parse {} at {}.".format(
                            file, d["output_file_paths"][file]))

        # parse force constants
        if hasattr(vrun, "force_constants"):
            d["output"]["force_constants"] = vrun.force_constants.tolist()
            d["output"][
                "normalmode_eigenvals"] = vrun.normalmode_eigenvals.tolist()
            d["output"][
                "normalmode_eigenvecs"] = vrun.normalmode_eigenvecs.tolist()

        # perform Bader analysis using Henkelman bader
        if self.parse_bader and "chgcar" in d["output_file_paths"]:
            suffix = "" if taskname == "standard" else ".{}".format(taskname)
            bader = bader_analysis_from_path(dir_name, suffix=suffix)
            d["bader"] = bader

        # parse output from loptics
        if vrun.incar.get("LOPTICS", False):
            dielectric = vrun.dielectric
            d["output"]["dielectric"] = dict(energy=dielectric[0],
                                             real=dielectric[1],
                                             imag=dielectric[2])
            d["output"][
                "optical_absorption_coeff"] = vrun.optical_absorption_coeff

        return d
Example #6
0
 def test_normalize(self):
     norm_entry = self.transformed_entry.normalize(mode="atom")
     expected_comp = Composition(
         {DummySpecies("Xf"): 7 / 23, DummySpecies("Xg"): 15 / 23, DummySpecies("Xh"): 1 / 23}
     )
     self.assertEqual(norm_entry.composition, expected_comp, "Wrong composition!")
Example #7
0
 def setUp(self):
     comp = Composition("LiFeO2")
     self.entry = PDEntry(comp, 53)
     self.gpentry = GrandPotPDEntry(self.entry, {Element("O"): 1.5})
Example #8
0
def filter_compounds_formula(data,
                             icsd,
                             verbosity=True,
                             remove_non_comp_ox=-1):

    #check if at least one compound belongs to ICSD
    t_icsd = False
    for idata in range(len(data)):
        if len(data[idata]['icsd_ids']) > 0:
            t_icsd = True
            break

    if (icsd):
        if (not t_icsd):
            return None

    if (verbosity):
        print('filter_compounds_formula')
        print(data[0]['formula'])


# Take only simulations with a certain number of sites "nsites_cutoff"

    nsite_max = data[0]['nsites']
    isite_max = 0
    nsites = []
    for idata in range(len(data)):
        nsites.append(data[idata]['nsites'])
        if (data[idata]['nsites'] >= nsite_max):
            nsite_max = data[idata]['nsites']
            isite_max = idata
    if (verbosity):
        print(nsite_max, isite_max)

    nsites_cutoff = np.mean(np.array(nsites))

    # Select simulations with the minimum energy

    minimo = data[isite_max]['energy_per_atom']
    imin = isite_max
    cont = 0
    for idata in range(len(data)):

        if (verbosity):
            print(idata, data[cont]['final_energy'] / data[cont]['nsites'],
                  data[cont]['energy_per_atom'], data[cont]['nsites'])

        if ((data[cont]['energy_per_atom'] < minimo)
                and (data[cont]['nsites'] > nsites_cutoff)):
            imin = cont
        cont += 1

    if (verbosity):
        print(imin)

    if (verbosity):
        print('End filter compounds formula---- \n')

    if (remove_non_comp_ox > -1):

        # for pure elements we have no issue
        if (len(data[imin]['formula'].keys()) > 1):

            if (t_icsd):
                aos = True
            else:
                aos = False

            comp = Composition(data[imin]['pretty_formula'])

            # if pymatgen does not find good oxidation states we have an issue
            if (len(comp.oxi_state_guesses(all_oxi_states=aos)) == 0):

                if (remove_non_comp_ox == 0):

                    print(data[imin]['pretty_formula'],
                          ' non compatible. Asked to remove')
                    return None

                else:

                    found = False

                    for nhyd in range(100):
                        app = 'H' + str(nhyd)
                        comp = Composition(app + data[imin]['pretty_formula'])

                        #when adding H we try to be more sure of the oxi state and we have therefore all_oxi_states = False
                        if (len(comp.oxi_state_guesses(all_oxi_states=False)) >
                                0):
                            print('aos ', aos)
                            found = True
                            if ('H' in data[imin]['formula'].keys()):
                                data[imin]['formula'][
                                    'H'] = data[imin]['formula']['H'] + nhyd
                            else:
                                data[imin]['formula']['H'] = nhyd
                            print(data[imin]['pretty_formula'],
                                  ' non compatible. Asked to add ', str(nhyd),
                                  'hydrogen atoms')
                            print('OX states: ',
                                  comp.oxi_state_guesses(all_oxi_states=False))
                            break

                    if not found:
                        print(
                            data[imin]['pretty_formula'],
                            ' non compatible but did not find hydrogens to add.'
                        )
                        return None

    return data[imin]
Example #9
0
 def test_total_electrons(self):
     test_cases = {'C': 6, 'SrTiO3': 84}
     for item in test_cases.keys():
         c = Composition(item)
         self.assertAlmostEqual(c.total_electrons, test_cases[item])
    def apply_transformation(self, structure, return_ranked_list=False):
        """
        For this transformation, the apply_transformation method will return
        only the ordered structure with the lowest Ewald energy, to be
        consistent with the method signature of the other transformations.
        However, all structures are stored in the  all_structures attribute in
        the transformation object for easy access.

        Args:
            structure: Oxidation state decorated disordered structure to order
            return_ranked_list (bool): Whether or not multiple structures are
                returned. If return_ranked_list is a number, that number of
                structures is returned.

        Returns:
            Depending on returned_ranked list, either a transformed structure
            or a list of dictionaries, where each dictionary is of the form
            {"structure" = .... , "other_arguments"}
            the key "transformation" is reserved for the transformation that
            was actually applied to the structure.
            This transformation is parsed by the alchemy classes for generating
            a more specific transformation history. Any other information will
            be stored in the transformation_parameters dictionary in the
            transmuted structure class.
        """

        try:
            num_to_return = int(return_ranked_list)
        except ValueError:
            num_to_return = 1

        num_to_return = max(1, num_to_return)

        equivalent_sites = []
        exemplars = []
        #generate list of equivalent sites to order
        #equivalency is determined by sp_and_occu and symmetry
        #if symmetrized structure is true
        for i, site in enumerate(structure):
            if site.is_ordered:
                continue
            found = False
            for j, ex in enumerate(exemplars):
                sp = ex.species_and_occu
                if not site.species_and_occu.almost_equals(sp):
                    continue
                if self._symmetrized:
                    sym_equiv = structure.find_equivalent_sites(ex)
                    sym_test = site in sym_equiv
                else:
                    sym_test = True
                if sym_test:
                    equivalent_sites[j].append(i)
                    found = True
                    break
            if not found:
                equivalent_sites.append([i])
                exemplars.append(site)

        #generate the list of manipulations and input structure
        s = Structure.from_sites(structure)
        m_list = []
        for g in equivalent_sites:
            total_occupancy = sum([structure[i].species_and_occu for i in g],
                                  Composition())
            total_occupancy = dict(total_occupancy.items())
            #round total occupancy to possible values
            for k, v in total_occupancy.items():
                if abs(v - round(v)) > 0.25:
                    raise ValueError("Occupancy fractions not consistent "
                                     "with size of unit cell")
                total_occupancy[k] = int(round(v))
            #start with an ordered structure
            initial_sp = max(total_occupancy.keys(),
                             key=lambda x: abs(x.oxi_state))
            for i in g:
                s[i] = initial_sp
            #determine the manipulations
            for k, v in total_occupancy.items():
                if k == initial_sp:
                    continue
                m = [k.oxi_state / initial_sp.oxi_state if initial_sp.oxi_state
                     else 0, v, list(g), k]
                m_list.append(m)
            #determine the number of empty sites
            empty = len(g) - sum(total_occupancy.values())
            if empty > 0.5:
                m_list.append([0, empty, list(g), None])

        matrix = EwaldSummation(s).total_energy_matrix
        ewald_m = EwaldMinimizer(matrix, m_list, num_to_return, self._algo)

        self._all_structures = []

        lowest_energy = ewald_m.output_lists[0][0]
        num_atoms = sum(structure.composition.values())

        for output in ewald_m.output_lists:
            s_copy = s.copy()
            # do deletions afterwards because they screw up the indices of the
            # structure
            del_indices = []
            for manipulation in output[1]:
                if manipulation[1] is None:
                    del_indices.append(manipulation[0])
                else:
                    s_copy[manipulation[0]] = manipulation[1]
            s_copy.remove_sites(del_indices)
            self._all_structures.append(
                {"energy": output[0],
                 "energy_above_minimum":
                 (output[0] - lowest_energy) / num_atoms,
                 "structure": s_copy.get_sorted_structure()})

        if return_ranked_list:
            return self._all_structures
        else:
            return self._all_structures[0]["structure"]
Example #11
0
def voltage_profile(objs,
                    xs=None,
                    invert=1,
                    xlabel='x in K$_{1-x}$TiPO$_4$F',
                    ylabel='Voltage, V',
                    ax=None,
                    first=1,
                    last=1,
                    fmt='k-',
                    label=None,
                    color=None,
                    filename='voltage_curve',
                    xlim=None,
                    ylim=None,
                    last_point=1,
                    exclude=None,
                    formula=None,
                    fit_power=4):
    """
    objs - dict of objects with concentration of alkali (*invert* = 1) or vacancies (*invert* = 0) as a key
    xs - choose specific concentrations
    invert - 0 or 1 for concentration axis, see above
    ax - matplotlib object, if more profiles on one plot are needed

    exclude - list of objects to skip

    formula - chemical formula used to calculate capacity in mAh/g

    fit_power - power of fit polynomial
    """

    if xs is None:
        xs = sorted(objs.keys())

    es2 = []
    xs2 = []
    x_prev = None
    V_prev = None

    if exclude is None:
        exclude = []
    # if last_point:

    for i in range(len(xs))[:-1]:
        if i in exclude:
            continue

        x = xs[i]
        # process_cathode_material('KTiPO4F', step = 3, target_x = x, params = params , update = 0 ) #
        # es.append(obj.e0)
        # objs[xs[i]].res()
        # objs[xs[i]].run('1uTU32r', add = 0, up = 'up1')

        V = calc_redox(objs[xs[i + 1]], objs[xs[i]])['redox_pot']
        # print(V)
        if V_prev is not None:
            es2.append(V_prev)
            xs2.append(x)
        es2.append(V)
        xs2.append(x)
        V_prev = V

    if last_point:
        xs2.append(1)
        es2.append(V_prev)

    if invert:
        es_inv = list(reversed(es2))
    else:
        es_inv = es2
    # xs_inv = list(reversed(xs2))

    # print( len(es_inv) )
    # print( len(xs2) )

    xf = [float(x) for x in xs2]  #x float
    # formula = 0
    if formula:
        from pymatgen.core.composition import Composition
        # from
        comp = Composition(formula)

        x = [x * header.F / comp.weight / 3.6 for x in xf]  # capacity in mA/g
        g = lambda x: x * header.F / comp.weight / 3.6
        f = lambda x: x * comp.weight * 3.6 / header.F

    else:
        x = xf  # just in concentration
        xlim = (0, 1.2)
        f = None
        g = None

    x.insert(0, 0)
    es_inv.insert(0, 2.75)

    font = {
        'family': 'Arial',
        # 'weight' : 'boolld',
        'size': 14
    }

    header.mpl.rc('font', **font)

    xi = [f(xi) for xi in x]
    print(xi)

    print('Full capacity is ', g(1))

    for i in range(len(x)):
        print('{:6.2f}, {:4.2f}, {:4.2f}'.format(x[i], float(xi[i]),
                                                 es_inv[i]))

    fit_and_plot(
        ax=ax,
        first=first,
        last=last,
        power=fit_power,
        dE1={
            'x': x,
            'x2_func': f,
            'x2_func_inv': g,
            'x2label': 'x in Li$_{x}$TiPO$_4$',
            'y': es_inv,
            'fmt': fmt,
            'label': label,
            'color': color,
        },  #'xticks':np.arange(0, 170, 20)}, 
        ylim=ylim,
        xlim=xlim,
        legend='best',
        ver=0,
        alpha=1,
        filename='figs/' + filename,
        fig_format='pdf',
        ylabel=ylabel,
        xlabel=xlabel,
        linewidth=2,
        fontsize=None)
    return
Example #12
0
    def graph_residual_error_per_species(self, specie: str) -> go.Figure:
        """
        Graphs the residual errors for each compound that contains specie after applying computed corrections.

        Args:
            specie: the specie/group that residual errors are being plotted for

        Raises:
            ValueError: the specie is not a valid specie that this class fits corrections for
        """

        if specie not in self.species:
            raise ValueError("not a valid specie")

        if len(self.corrections) == 0:
            raise RuntimeError(
                "Please call compute_corrections or compute_from_files to calculate corrections first"
            )

        abs_errors = [
            abs(i)
            for i in self.diffs - np.dot(self.coeff_mat, self.corrections)
        ]
        labels_species = self.names.copy()
        diffs_cpy = self.diffs.copy()
        num = len(labels_species)

        if specie in ("oxide", "peroxide", "superoxide", "S"):
            if specie == "oxide":
                compounds = self.oxides
            elif specie == "peroxide":
                compounds = self.peroxides
            elif specie == "superoxides":
                compounds = self.superoxides
            else:
                compounds = self.sulfides
            for i in range(num):
                if labels_species[num - i - 1] not in compounds:
                    del labels_species[num - i - 1]
                    del abs_errors[num - i - 1]
                    del diffs_cpy[num - i - 1]
        else:
            for i in range(num):
                if not Composition(labels_species[num - i - 1])[specie]:
                    del labels_species[num - i - 1]
                    del abs_errors[num - i - 1]
                    del diffs_cpy[num - i - 1]
        abs_errors, labels_species = (
            list(t) for t in zip(*sorted(zip(abs_errors, labels_species)))
        )  # sort by error

        num = len(abs_errors)
        fig = go.Figure(
            data=go.Scatter(
                x=np.linspace(1, num, num),
                y=abs_errors,
                mode="markers",
                text=labels_species,
            ),
            layout=go.Layout(
                title=go.layout.Title(text="Residual Errors for " + specie),
                yaxis=go.layout.YAxis(title=go.layout.yaxis.Title(
                    text="Residual Error (eV/atom)")),
            ),
        )

        print("Residual Error:")
        print("Median = " + str(np.median(np.array(abs_errors))))
        print("Mean = " + str(np.mean(np.array(abs_errors))))
        print("Std Dev = " + str(np.std(np.array(abs_errors))))
        print("Original Error:")
        print("Median = " + str(abs(np.median(np.array(diffs_cpy)))))
        print("Mean = " + str(abs(np.mean(np.array(diffs_cpy)))))
        print("Std Dev = " + str(np.std(np.array(diffs_cpy))))

        return fig
Example #13
0
    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),
            )

        # set ozonide correction to 0 so that this species does not recieve a correction
        # while other oxide types do
        self.corrections_dict["ozonide"] = (0, 0)

        return self.corrections_dict
 def from_dict(cls, d):
     reactants = [Composition(sym_amt) for sym_amt in d["reactants"]]
     products = [Composition(sym_amt) for sym_amt in d["products"]]
     return cls(reactants, products)
Example #15
0
    def __init__(
        self,
        c1: Composition,
        c2: Composition,
        grand_pd: GrandPotentialPhaseDiagram,
        pd_non_grand: PhaseDiagram,
        include_no_mixing_energy: bool = False,
        norm: bool = True,
        use_hull_energy: bool = True,
    ):
        """
        Args:
            c1: Reactant 1 composition
            c2: Reactant 2 composition
            grand_pd: Grand potential phase diagram object built from all elements in
                composition c1 and c2.
            include_no_mixing_energy: No_mixing_energy for a reactant is the
                opposite number of its energy above grand potential convex hull. In
                cases where reactions involve elements reservoir, this param
                determines whether no_mixing_energy of reactants will be included
                in the final reaction energy calculation. By definition, if pd is
                not a GrandPotentialPhaseDiagram object, this param is False.
            pd_non_grand: PhaseDiagram object but not
                GrandPotentialPhaseDiagram object built from elements in c1 and c2.
            norm: Whether or not the total number of atoms in composition
                of reactant will be normalized to 1.
            use_hull_energy: Whether or not use the convex hull energy for
                a given composition for reaction energy calculation. If false,
                the energy of ground state structure will be used instead.
                Note that in case when ground state can not be found for a
                composition, convex hull energy will be used associated with a
                warning message.
        """

        if not isinstance(grand_pd, GrandPotentialPhaseDiagram):
            raise ValueError(
                "Please use the InterfacialReactivity class if using a regular phase diagram!"
            )

        super().__init__(c1=c1,
                         c2=c2,
                         pd=grand_pd,
                         norm=norm,
                         use_hull_energy=use_hull_energy,
                         bypass_grand_warning=True)

        self.pd_non_grand = pd_non_grand
        self.grand = True

        self.comp1 = Composition(
            {k: v
             for k, v in c1.items() if k not in grand_pd.chempots})
        self.comp2 = Composition(
            {k: v
             for k, v in c2.items() if k not in grand_pd.chempots})

        if self.norm:
            self.factor1 = self.comp1.num_atoms / c1.num_atoms
            self.factor2 = self.comp2.num_atoms / c2.num_atoms
            self.comp1 = self.comp1.fractional_composition
            self.comp2 = self.comp2.fractional_composition

        if include_no_mixing_energy:
            self.e1 = self._get_grand_potential(self.c1)
            self.e2 = self._get_grand_potential(self.c2)
        else:
            self.e1 = self.pd.get_hull_energy(self.comp1)
            self.e2 = self.pd.get_hull_energy(self.comp2)
Example #16
0
 def test_mixed_valence(self):
     comp = Composition({"Fe2+": 2, "Fe3+": 4, "Li+": 8})
     self.assertEqual(comp.reduced_formula, "Li4Fe3")
     self.assertEqual(comp.alphabetical_formula, "Fe6 Li8")
     self.assertEqual(comp.formula, "Li8 Fe6")
Example #17
0
 def test_get_composition(self):
     comp = self.transformed_entry.composition
     expected_comp = Composition({DummySpecies("Xf"): 14 / 30, DummySpecies("Xg"): 1.0, DummySpecies("Xh"): 2 / 30})
     self.assertEqual(comp, expected_comp, "Wrong composition!")
Example #18
0
 def test_to_data_dict(self):
     comp = Composition('Fe0.00009Ni0.99991')
     d = comp.to_data_dict
     self.assertAlmostEqual(d["reduced_cell_composition"]["Fe"], 9e-5)
Example #19
0
    def test_get_decomposition(self):
        for entry in self.pd.stable_entries:
            self.assertEqual(
                len(self.pd.get_decomposition(entry.composition)),
                1,
                "Stable composition should have only 1 decomposition!",
            )
        dim = len(self.pd.elements)
        for entry in self.pd.all_entries:
            ndecomp = len(self.pd.get_decomposition(entry.composition))
            self.assertTrue(
                ndecomp > 0 and ndecomp <= dim,
                "The number of decomposition phases can at most be equal to the number of components.",
            )

        # Just to test decomp for a ficitious composition
        ansdict = {
            entry.composition.formula: amt for entry, amt in self.pd.get_decomposition(Composition("Li3Fe7O11")).items()
        }
        expected_ans = {
            "Fe2 O2": 0.0952380952380949,
            "Li1 Fe1 O2": 0.5714285714285714,
            "Fe6 O8": 0.33333333333333393,
        }
        for k, v in expected_ans.items():
            self.assertAlmostEqual(ansdict[k], v, 7)
Example #20
0
 def test_init_numerical_tolerance(self):
     self.assertEqual(Composition({'B': 1, 'C': -1e-12}), Composition('B'))
Example #21
0
    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])
Example #22
0
    def test_negative_compositions(self):
        self.assertEqual(Composition('Li-1(PO-1)4', allow_negative=True).formula,
                         'Li-1 P4 O-4')
        self.assertEqual(Composition('Li-1(PO-1)4', allow_negative=True).reduced_formula,
                         'Li-1(PO-1)4')
        self.assertEqual(Composition('Li-2Mg4', allow_negative=True).reduced_composition,
                         Composition('Li-1Mg2', allow_negative=True))
        self.assertEqual(Composition('Li-2.5Mg4', allow_negative=True).reduced_composition,
                         Composition('Li-2.5Mg4', allow_negative=True))

        # test math
        c1 = Composition('LiCl', allow_negative=True)
        c2 = Composition('Li')
        self.assertEqual(c1 - 2 * c2, Composition({'Li': -1, 'Cl': 1},
                                                  allow_negative=True))
        self.assertEqual((c1 + c2).allow_negative, True)
        self.assertEqual(c1 / -1, Composition('Li-1Cl-1', allow_negative=True))

        # test num_atoms
        c1 = Composition('Mg-1Li', allow_negative=True)
        self.assertEqual(c1.num_atoms, 2)
        self.assertEqual(c1.get_atomic_fraction('Mg'), 0.5)
        self.assertEqual(c1.get_atomic_fraction('Li'), 0.5)
        self.assertEqual(c1.fractional_composition,
                         Composition('Mg-0.5Li0.5', allow_negative=True))

        # test copy
        self.assertEqual(c1.copy(), c1)

        # test species
        c1 = Composition({'Mg': 1, 'Mg2+': -1}, allow_negative=True)
        self.assertEqual(c1.num_atoms, 2)
        self.assertEqual(c1.element_composition, Composition())
        self.assertEqual(c1.average_electroneg, 1.31)
Example #23
0
    def generate_doc(self, dir_name, vasprun_files, outcar_files):
        """
        Adapted from matgendb.creator.generate_doc
        """
        try:
            # basic properties, incl. calcs_reversed and run_stats
            fullpath = os.path.abspath(dir_name)
            d = jsanitize(self.additional_fields, strict=True)
            d["schema"] = {"code": "atomate", "version": VaspDrone.__version__}
            d["dir_name"] = fullpath
            d["calcs_reversed"] = [
                self.process_vasprun(dir_name, taskname, filename)
                for taskname, filename in vasprun_files.items()
            ]
            outcar_data = [
                Outcar(os.path.join(dir_name, filename)).as_dict()
                for taskname, filename in outcar_files.items()
            ]
            run_stats = {}
            for i, d_calc in enumerate(d["calcs_reversed"]):
                run_stats[d_calc["task"]["name"]] = outcar_data[i].pop(
                    "run_stats")
                if d_calc.get("output"):
                    d_calc["output"].update({"outcar": outcar_data[i]})
                else:
                    d_calc["output"] = {"outcar": outcar_data[i]}
            try:
                overall_run_stats = {}
                for key in [
                        "Total CPU time used (sec)",
                        "User time (sec)",
                        "System time (sec)",
                        "Elapsed time (sec)",
                ]:
                    overall_run_stats[key] = sum(
                        [v[key] for v in run_stats.values()])
                run_stats["overall"] = overall_run_stats
            except Exception:
                logger.error("Bad run stats for {}.".format(fullpath))
            d["run_stats"] = run_stats

            # reverse the calculations data order so newest calc is first
            d["calcs_reversed"].reverse()

            # set root formula/composition keys based on initial and final calcs
            d_calc_init = d["calcs_reversed"][-1]
            d_calc_final = d["calcs_reversed"][0]
            d["chemsys"] = "-".join(sorted(d_calc_final["elements"]))
            comp = Composition(d_calc_final["composition_unit_cell"])
            d["formula_anonymous"] = comp.anonymized_formula
            d["formula_reduced_abc"] = comp.reduced_composition.alphabetical_formula
            for root_key in [
                    "completed_at",
                    "nsites",
                    "composition_unit_cell",
                    "composition_reduced",
                    "formula_pretty",
                    "elements",
                    "nelements",
            ]:
                d[root_key] = d_calc_final[root_key]

            # store the input key based on initial calc
            # store any overrides to the exchange correlation functional
            xc = d_calc_init["input"]["incar"].get("GGA")
            if xc:
                xc = xc.upper()
            p = d_calc_init["input"]["potcar_type"][0].split("_")
            pot_type = p[0]
            functional = "lda" if len(pot_type) == 1 else "_".join(p[1:])
            d["input"] = {
                "structure": d_calc_init["input"]["structure"],
                "is_hubbard": d_calc_init.pop("is_hubbard"),
                "hubbards": d_calc_init.pop("hubbards"),
                "is_lasph": d_calc_init["input"]["incar"].get("LASPH", False),
                "potcar_spec": d_calc_init["input"].get("potcar_spec"),
                "xc_override": xc,
                "pseudo_potential": {
                    "functional": functional.lower(),
                    "pot_type": pot_type.lower(),
                    "labels": d_calc_init["input"]["potcar"],
                },
                "parameters": d_calc_init["input"]["parameters"],
                "incar": d_calc_init["input"]["incar"],
            }

            # store the output key based on final calc
            d["output"] = {
                "structure": d_calc_final["output"]["structure"],
                "density": d_calc_final.pop("density"),
                "energy": d_calc_final["output"]["energy"],
                "energy_per_atom": d_calc_final["output"]["energy_per_atom"],
                "forces":
                d_calc_final["output"]["ionic_steps"][-1].get("forces"),
                "stress":
                d_calc_final["output"]["ionic_steps"][-1].get("stress"),
            }

            # patch calculated magnetic moments into final structure
            if len(d_calc_final["output"]["outcar"]["magnetization"]) != 0:
                magmoms = [
                    m["tot"]
                    for m in d_calc_final["output"]["outcar"]["magnetization"]
                ]
                s = Structure.from_dict(d["output"]["structure"])
                s.add_site_property("magmom", magmoms)
                d["output"]["structure"] = s.as_dict()

            calc = d["calcs_reversed"][0]

            # copy band gap and properties into output
            d["output"].update({
                "bandgap":
                calc["output"]["bandgap"],
                "cbm":
                calc["output"]["cbm"],
                "vbm":
                calc["output"]["vbm"],
                "is_gap_direct":
                calc["output"]["is_gap_direct"],
            })
            try:
                d["output"].update({"is_metal": calc["output"]["is_metal"]})
                if not calc["output"]["is_gap_direct"]:
                    d["output"]["direct_gap"] = calc["output"]["direct_gap"]
                if "transition" in calc["output"]:
                    d["output"]["transition"] = calc["output"]["transition"]

            except Exception:
                if self.bandstructure_mode is True:
                    logger.error(traceback.format_exc())
                    logger.error("Error in " + os.path.abspath(dir_name) +
                                 ".\n" + traceback.format_exc())
                    raise

            # Store symmetry information
            sg = SpacegroupAnalyzer(
                Structure.from_dict(d_calc_final["output"]["structure"]), 0.1)
            if not sg.get_symmetry_dataset():
                sg = SpacegroupAnalyzer(
                    Structure.from_dict(d_calc_final["output"]["structure"]),
                    1e-3, 1)
            d["output"]["spacegroup"] = {
                "source": "spglib",
                "symbol": sg.get_space_group_symbol(),
                "number": sg.get_space_group_number(),
                "point_group": sg.get_point_group_symbol(),
                "crystal_system": sg.get_crystal_system(),
                "hall": sg.get_hall(),
            }

            # store dieelctric and piezo information
            if d["input"]["parameters"].get("LEPSILON"):
                for k in [
                        "epsilon_static", "epsilon_static_wolfe",
                        "epsilon_ionic"
                ]:
                    d["output"][k] = d_calc_final["output"][k]
                if SymmOp.inversion() not in sg.get_symmetry_operations():
                    for k in ["piezo_ionic_tensor", "piezo_tensor"]:
                        d["output"][k] = d_calc_final["output"]["outcar"][k]

            # store optical data
            if d["input"]["parameters"].get("LOPTICS"):
                for k in ["optical_absorption_coeff", "dielectric"]:
                    d["output"][k] = d_calc_final["output"][k]

            d["state"] = ("successful"
                          if d_calc["has_vasp_completed"] else "unsuccessful")

            self.set_analysis(d)

            d["last_updated"] = datetime.datetime.utcnow()
            return d

        except Exception:
            logger.error(traceback.format_exc())
            logger.error("Error in " + os.path.abspath(dir_name) + ".\n" +
                         traceback.format_exc())
            raise
Example #24
0
    def test_oxi_state_guesses(self):
        self.assertEqual(Composition("LiFeO2").oxi_state_guesses(),
                         ({"Li": 1, "Fe": 3, "O": -2},))

        self.assertEqual(Composition("Fe4O5").oxi_state_guesses(),
                         ({"Fe": 2.5, "O": -2},))

        self.assertEqual(Composition("V2O3").oxi_state_guesses(),
                         ({"V": 3, "O": -2},))

        # all_oxidation_states produces *many* possible responses
        self.assertEqual(len(Composition("MnO").oxi_state_guesses(
            all_oxi_states=True)), 4)

        # can't balance b/c missing V4+
        self.assertEqual(Composition("VO2").oxi_state_guesses(
            oxi_states_override={"V": [2, 3, 5]}), [])

        # missing V4+, but can balance due to additional sites
        self.assertEqual(Composition("V2O4").oxi_state_guesses(
            oxi_states_override={"V": [2, 3, 5]}), ({"V": 4, "O": -2},))

        # multiple solutions - Mn/Fe = 2+/4+ or 3+/3+ or 4+/2+
        self.assertEqual(len(Composition("MnFeO3").oxi_state_guesses(
            oxi_states_override={"Mn": [2, 3, 4], "Fe": [2, 3, 4]})), 3)

        # multiple solutions prefers 3/3 over 2/4 or 4/2
        self.assertEqual(Composition("MnFeO3").oxi_state_guesses(
            oxi_states_override={"Mn": [2, 3, 4], "Fe": [2, 3, 4]})[0],
                         {"Mn": 3, "Fe": 3, "O": -2})

        # target charge of 1
        self.assertEqual(Composition("V2O6").oxi_state_guesses(
            oxi_states_override={"V": [2, 3, 4, 5]}, target_charge=-2),
            ({"V": 5, "O": -2},))

        # max_sites for very large composition - should timeout if incorrect
        self.assertEqual(Composition("Li10000Fe10000P10000O40000").
                         oxi_state_guesses(max_sites=7)[0],
                         {"Li": 1, "Fe": 2, "P": 5, "O": -2})

        # max_sites for very large composition - should timeout if incorrect
        self.assertEqual(Composition("Li10000Fe10000P10000O40000").
                         oxi_state_guesses(max_sites=-1)[0],
                         {"Li": 1, "Fe": 2, "P": 5, "O": -2})

        # negative max_sites less than -1 - should throw error if cannot reduce
        # to under the abs(max_sites) number of sites. Will also timeout if
        # incorrect.
        self.assertEqual(
            Composition("Sb10000O10000F10000").oxi_state_guesses(
                max_sites=-3)[0],
            {"Sb": 3, "O": -2, "F": -1})
        self.assertRaises(ValueError, Composition("LiOF").oxi_state_guesses,
                          max_sites=-2)

        self.assertRaises(ValueError, Composition("V2O3").
                          oxi_state_guesses, max_sites=1)
Example #25
0
 def test_getmu_range_stability_phase(self):
     results = self.analyzer.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)
Example #26
0
    def test_chemical_system(self):

        formula = "NaCl"
        cmp = Composition(formula)
        self.assertEqual(cmp.chemical_system, "Cl-Na")
Example #27
0
    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
Example #28
0
 def test_hill_formula(self):
     c = Composition("CaCO3")
     self.assertEqual(c.hill_formula, "C Ca O3")
     c = Composition("C2H5OH")
     self.assertEqual(c.hill_formula, "C2 H6 O")
Example #29
0
    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 = 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 from_dict(cls, d):
     reactants = {Composition(comp): coeff
                  for comp, coeff in d["reactants"].items()}
     products = {Composition(comp): coeff
                 for comp, coeff in d["products"].items()}
     return cls(reactants, products)