Esempio n. 1
0
    def _calculate_binary_volume_curve(self):
        """ Take stable compositions and volume and calculate volume
        expansion per "B" in AB binary.

        """
        stable_comp = get_array_from_cursor(self.hull_cursor, 'concentration')
        stable_comp, unique_comp_inds = np.unique(stable_comp,
                                                  return_index=True)
        for doc in self.hull_cursor:
            if 'cell_volume_per_b' not in doc:
                raise RuntimeError(
                    "Document missing key `cell_volume_per_b`: {}".format(doc))
        stable_stoichs = get_array_from_cursor(
            self.hull_cursor, 'stoichiometry')[unique_comp_inds]
        stable_vol = get_array_from_cursor(
            self.hull_cursor, 'cell_volume_per_b')[unique_comp_inds]
        stable_cap = get_array_from_cursor(
            self.hull_cursor, 'gravimetric_capacity')[unique_comp_inds]
        hull_distances = get_array_from_cursor(
            self.hull_cursor, 'hull_distance')[unique_comp_inds]
        with np.errstate(divide='ignore'):
            stable_x = stable_comp / (1 - stable_comp)
        non_nans = np.argwhere(np.isfinite(stable_x))
        self.volume_data['x'].append(stable_x[non_nans].flatten())
        self.volume_data['Q'].append(stable_cap[non_nans].flatten())
        self.volume_data['electrode_volume'].append(
            stable_vol[non_nans].flatten())
        self.volume_data['volume_ratio_with_bulk'].append(
            (stable_vol[non_nans] / stable_vol[0]).flatten())
        self.volume_data['volume_expansion_percentage'].append(
            ((stable_vol[non_nans] / stable_vol[0]) - 1) * 100)
        self.volume_data['hull_distances'].append(
            hull_distances[non_nans].flatten())
        self.volume_data['endstoichs'].append(
            stable_stoichs[non_nans].flatten()[0])
Esempio n. 2
0
    def _calculate_binary_voltage_curve(self, hull_cursor):
        """ Generate binary voltage curve, setting the self.voltage_data
        dictionary.

        Parameters:
            hull_cursor (list(dict)): list of structures to include in the voltage curve.

        """
        mu_enthalpy = get_array_from_cursor(self.chempot_cursor,
                                            self.energy_key)
        # take a copy of the cursor so it can be reordered
        _hull_cursor = deepcopy(hull_cursor)
        x = get_num_intercalated(_hull_cursor)
        _x_order = np.argsort(x)
        capacities = np.asarray([
            get_generic_grav_capacity(
                get_padded_composition(doc['stoichiometry'], self.elements),
                self.elements) for doc in _hull_cursor
        ])
        set_cursor_from_array(_hull_cursor, x, 'conc_of_active_ion')
        set_cursor_from_array(_hull_cursor, capacities, 'gravimetric_capacity')
        _hull_cursor = sorted(_hull_cursor,
                              key=lambda doc: doc['conc_of_active_ion'])
        capacities = capacities[_x_order]
        x = np.sort(x)
        stable_enthalpy_per_b = get_array_from_cursor(
            _hull_cursor, self._extensive_energy_key + '_per_b')

        x, uniq_idxs = np.unique(x, return_index=True)
        stable_enthalpy_per_b = stable_enthalpy_per_b[uniq_idxs]
        capacities = capacities[uniq_idxs]

        V = np.zeros_like(x)
        V[1:] = -np.diff(stable_enthalpy_per_b) / np.diff(x) + mu_enthalpy[0]
        V[0] = V[1]

        reactions = [((None, get_formula_from_stoich(doc['stoichiometry'])), )
                     for doc in _hull_cursor[1:]]

        # make V, Q and x available for plotting, stripping NaNs to re-add later
        # in the edge case of duplicate chemical potentials
        capacities, unique_caps = np.unique(capacities, return_index=True)
        non_nan = np.argwhere(np.isfinite(capacities))
        V = (np.asarray(V)[unique_caps])[non_nan].flatten().tolist()
        x = (np.asarray(x)[unique_caps])[non_nan].flatten().tolist()
        capacities = capacities[non_nan].flatten().tolist()

        x.append(np.nan)
        capacities.append(np.nan)
        V.append(0.0)

        average_voltage = Electrode.calculate_average_voltage(capacities, V)
        profile = VoltageProfile(
            voltages=V,
            capacities=capacities,
            average_voltage=average_voltage,
            starting_stoichiometry=[[self.species[1], 1.0]],
            reactions=reactions,
            active_ion=self.species[0])
        self.voltage_data.append(profile)
Esempio n. 3
0
    def _calculate_ternary_voltage_curve(self, hull_cursor):
        """ Calculate tenary voltage curve, setting self.voltage_data.
        First pass written by James Darby, [email protected].

        Parameters:
            hull_cursor (list(dict)): list of structures to include in the voltage curve.

        """

        # construct working array of concentrations and energies
        stoichs = get_array_from_cursor(hull_cursor, 'stoichiometry')

        # do another convex hull on just the known hull points, to allow access to useful indices
        import scipy.spatial

        # construct working array of concentrations and energies
        points = np.hstack((
            get_array_from_cursor(hull_cursor, 'concentration'),
            get_array_from_cursor(hull_cursor, self.energy_key).reshape(len(hull_cursor), 1)
        ))
        stoichs = get_array_from_cursor(hull_cursor, 'stoichiometry')

        # do another convex hull on just the known hull points, to allow access to useful indices
        convex_hull = scipy.spatial.ConvexHull(points)

        endpoints, endstoichs = Electrode._find_starting_materials(convex_hull.points, stoichs)
        print('{} starting point(s) found.'.format(len(endstoichs)))
        for endstoich in endstoichs:
            print(get_formula_from_stoich(endstoich), end=' ')
        print('\n')

        # iterate over possible delithiated phases
        for reaction_ind, endpoint in enumerate(endpoints):
            print(30 * '-')
            print('Reaction {}, {}:'.format(reaction_ind+1, get_formula_from_stoich(endstoichs[reaction_ind])))
            reactions, capacities, voltages, average_voltage, volumes = self._construct_electrode(
                convex_hull, endpoint, endstoichs[reaction_ind], hull_cursor
            )

            profile = VoltageProfile(
                starting_stoichiometry=endstoichs[reaction_ind],
                reactions=reactions,
                capacities=capacities,
                voltages=voltages,
                average_voltage=average_voltage,
                active_ion=self.species[0]
            )
            self.voltage_data.append(profile)

            if not self._non_elemental:
                self.volume_data['x'].append(capacities)
                self.volume_data['Q'].append(capacities)
                self.volume_data['electrode_volume'].append(volumes)
                self.volume_data['endstoichs'].append(endstoichs[reaction_ind])
                self.volume_data['volume_ratio_with_bulk'].append(np.asarray(volumes) / volumes[0])
                self.volume_data['volume_expansion_percentage'].append(((np.asarray(volumes) / volumes[0]) - 1) * 100)
                self.volume_data['hull_distances'].append(np.zeros_like(capacities))
Esempio n. 4
0
    def _get_hull_distance(self, generation):
        """ Assign distance from the hull from hull for generation,
        assigning it.

        Parameters:
            generation (Generation): list of optimised structures.

        Returns:
            hull_dist (list(float)): list of distances to the hull.

        """
        for ind, populum in enumerate(generation):
            generation[ind]["concentration"] = get_concentration(
                populum, self.hull.elements)
            generation[ind][
                "formation_enthalpy_per_atom"] = get_formation_energy(
                    self.chempots, populum)
            if self.debug:
                print(
                    generation[ind]["concentration"],
                    generation[ind]["formation_enthalpy_per_atom"],
                )
        if self.testing:
            for ind, populum in enumerate(generation):
                generation[ind][
                    "formation_enthalpy_per_atom"] = np.random.rand() - 0.5
        structures = np.hstack((
            get_array_from_cursor(generation, "concentration"),
            get_array_from_cursor(generation,
                                  "formation_enthalpy_per_atom").reshape(
                                      len(generation), 1),
        ))
        if self.debug:
            print(structures)
        hull_dist = self.hull.get_hull_distances(structures, precompute=False)
        for ind, populum in enumerate(generation):
            generation[ind]["hull_distance"] = hull_dist[ind]
        return hull_dist
Esempio n. 5
0
    def _compute_voltages_from_intersections(
            self, intersections, hull, crossover, endstoich, hull_cursor
    ):
        """ Traverse the composition pathway and its intersections with hull facets
        and compute the voltage and volume drops along the way, for a ternary system
        with N 3-phase regions.

        Parameters:
            intersections (np.ndarray): Nx3 array containing the list of face indices
                crossed by the path.
            hull (scipy.spatial.ConvexHull): the temporary hull object that is referred
                to by any indices.
            crossover (np.ndarray): (N+1)x3 array containing the coordinates at which
                the composition pathway crosses the hull faces.
            endstoich (list): a matador stoichiometry that defines the end of the pathway.
            hull_cursor (list): the actual structures used to create the hull.

        """
        if len(crossover) != len(intersections) + 1:
            raise RuntimeError("Incompatible number of crossovers ({}) and intersections ({})."
                               .format(np.shape(crossover), np.shape(intersections)))

        # set up output arrays
        voltages = np.empty(len(intersections) + 1)
        volumes = np.empty(len(intersections))
        reactions = []

        # set up some useful arrays for later
        points = hull.points
        mu_enthalpy = get_array_from_cursor(self.chempot_cursor, self.energy_key)
        if not self._non_elemental:
            input_volumes = get_array_from_cursor(hull_cursor, 'cell_volume') / get_array_from_cursor(hull_cursor, 'num_atoms')
        capacities = [get_generic_grav_capacity(point, self.species) for point in crossover]

        # initial composition of one formula unit of the starting material
        initial_comp = get_padded_composition(endstoich, self.species)

        # loop over intersected faces and compute the voltage and volume at the
        # crossover with that face
        for ind, face in enumerate(intersections):

            simplex_index = int(face[0])

            final_stoichs = [hull_cursor[idx]['stoichiometry'] for idx in hull.simplices[simplex_index]]
            atoms_per_fu = [sum([elem[1] for elem in stoich]) for stoich in final_stoichs]

            energy_vec = points[hull.simplices[simplex_index], 2]
            comp = points[hull.simplices[simplex_index], :]
            comp[:, 2] = 1 - comp[:, 0] - comp[:, 1]

            # normalize the crossover composition to one formula unit of the starting electrode
            norm = np.asarray(crossover[ind][1:]) / np.asarray(initial_comp[1:])
            ratios_of_phases = np.linalg.solve(comp.T, crossover[ind] / norm[0])

            # remove small numerical noise
            ratios_of_phases[np.where(ratios_of_phases < EPS)] = 0

            # create a list containing the sections of the balanced reaction for printing
            balanced_reaction = []
            for i, ratio in enumerate(ratios_of_phases):
                if ratio < EPS:
                    continue
                else:
                    formula = get_formula_from_stoich(final_stoichs[i])
                    balanced_reaction.append((ratio / atoms_per_fu[i], formula))
            reactions.append(balanced_reaction)

            # compute the voltage as the difference between the projection of the gradient down to pure active ion,
            # and the reference chemical potential
            comp_inv = np.linalg.inv(comp.T)
            V = -(comp_inv.dot([1, 0, 0])).dot(energy_vec)
            V = V + mu_enthalpy[0]

            # compute the volume of the final electrode
            if not self._non_elemental:
                if ind <= len(intersections) - 1:
                    volume_vec = input_volumes[hull.simplices[simplex_index]]
                    volumes[ind] = np.dot(ratios_of_phases, volume_vec)

            # double up on first voltage
            if ind == 0:
                voltages[0] = V
            voltages[ind+1] = V

        average_voltage = Electrode.calculate_average_voltage(capacities, voltages)

        # print the reaction over a few lines, remove 1s and rounded ratios
        print(
            ' ---> '.join(
                [' + '.join(
                    ["{}{}".format(
                        str(round(chem[0], 3)) + ' ' if abs(chem[0] - 1) > EPS else '',
                        chem[1])
                     for chem in region])
                 for region in reactions])
        )

        return reactions, capacities, voltages, average_voltage, volumes
Esempio n. 6
0
    def __init__(self, cursor, formation_key, dimension):
        """ Compute the convex hull of data passed, to retrieve hull
        distances and thus stable structures.

        Parameters:
            cursor (list[dict]): list of matador documents to make
                phase diagram from.
            formation_key (str or list):  location of the formation energy
                inside each document, either a single key or iterable of
                keys to use with `recursive_get`.

        """
        self._dimension = dimension
        self.cursor = cursor
        self.formation_key = formation_key

        structures = np.hstack((
            get_array_from_cursor(cursor, 'concentration').reshape(len(cursor), dimension-1),
            get_array_from_cursor(cursor, self.formation_key).reshape(len(cursor), 1)))

        # define self._structure_slice as the filtered array of points actually used to create the convex hull
        # which can include/exclude points from the passed structures. This array is the one indexed by
        # vertices/simplices in ConvexHull

        if self._dimension == 3:
            # add a point "above" the hull
            # for simple removal of extraneous vertices (e.g. top of 2D hull)
            dummy_point = [0.333, 0.333, 1e5]
            # if ternary, use all structures, not just those with negative eform for compatibility reasons
            self._structure_slice = np.vstack((structures, dummy_point))
        else:
            # filter out those with positive formation energy, to reduce expense computing hull
            self._structure_slice = structures[np.where(structures[:, -1] <= 0 + EPS)]

        # filter out "duplicates" in _structure_slice
        # this prevents breakages if no structures are on the hull and chempots are duplicated
        # but it might be faster to hardcode this case individually
        self._structure_slice = np.unique(self._structure_slice, axis=0)

        # if we only have the chempots (or worse) with negative formation energy, don't even make the hull
        if len(self._structure_slice) <= dimension:
            if len(self._structure_slice) < dimension:
                raise RuntimeError('No chemical potentials on hull... either mysterious use of custom chempots, or worry!')
            self.convex_hull = FakeHull()
        else:
            try:
                self.convex_hull = scipy.spatial.ConvexHull(self._structure_slice)
            except scipy.spatial.qhull.QhullError:
                print(self._structure_slice)
                print('Error with QHull, plotting formation energies only...')
                print_exc()
                self.convex_hull = FakeHull()

        # remove vertices that have positive formation energy
        filtered_vertices = [vertex for vertex in self.convex_hull.vertices if self._structure_slice[vertex, -1] <= 0 + EPS]
        bad_simplices = set()
        for ind, simplex in enumerate(self.convex_hull.simplices):
            for vertex in simplex:
                if vertex not in filtered_vertices:
                    bad_simplices.add(ind)

        filtered_simplices = [simplex for ind, simplex in enumerate(self.convex_hull.simplices) if ind not in bad_simplices]

        self.convex_hull = FakeHull()
        self.convex_hull.points = self._structure_slice
        self.convex_hull.vertices = list(filtered_vertices)
        self.convex_hull.simplices = list(filtered_simplices)

        self.hull_dist = self.get_hull_distances(structures, precompute=True)
        set_cursor_from_array(self.cursor, self.hull_dist, 'hull_distance')
        self.structures = structures
        self.stable_structures = [doc for doc in self.cursor if doc['hull_distance'] < EPS]