Exemplo n.º 1
0
 def __str__(self):
     gen_string = "\nCompleted generation {}:\n".format(self.generation_idx)
     gen_string += "Number of members: {}\n".format(len(self.populace))
     gen_string += "Number of survivors: {}\n".format(len(self.bourgeoisie))
     gen_string += "Populace:\n"
     gen_string += 84 * "─" + "\n"
     gen_string += "{:^10} {:^10} {:^25} {:^35}\n".format(
         "Formula", "Fitness", "Hull distance (eV/atom)", "ID")
     gen_string += 84 * "─" + "\n"
     for populum in self.populace:
         gen_string += "{:^10} {: ^10.5f} {:^25.5f} {:^35}\n".format(
             get_formula_from_stoich(populum["stoichiometry"]),
             populum["fitness"],
             populum["raw_fitness"],
             populum["source"][0].split("/")[-1].replace(".res",
                                                         "").replace(
                                                             ".castep", ""),
         )
     gen_string += 84 * "─" + "\n"
     gen_string += "Bourgeoisie:\n"
     gen_string += 84 * "─" + "\n"
     gen_string += "{:^10} {:^10} {:^25} {:^35}\n".format(
         "Formula", "Fitness", "Hull distance (eV/atom)", "ID")
     gen_string += 84 * "─" + "\n"
     for bourge in self.bourgeoisie:
         gen_string += "{:^10} {: ^10.5f} {:^25.5f} {:^35}\n".format(
             get_formula_from_stoich(bourge["stoichiometry"]),
             bourge["fitness"],
             bourge["raw_fitness"],
             bourge["source"][0].split("/")[-1].replace(".res", "").replace(
                 ".castep", ""),
         )
     gen_string += "\n"
     return gen_string
Exemplo n.º 2
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))
Exemplo n.º 3
0
    def __init__(
        self,
        starting_stoichiometry: Tuple[Tuple[str, float], ...],
        capacities: List[float],
        voltages: List[float],
        average_voltage: float,
        active_ion: str,
        reactions: List[Tuple[Tuple[float, str], ...]],
    ):
        """ Initialise the voltage profile with the given data. """

        n_steps = len(voltages)
        if any(_len != n_steps for _len in [len(capacities), len(reactions)+1]):
            raise RuntimeError(
                "Invalid size of initial arrays, capacities and voltages must have same length."
                "reactions array must have length 1 smaller than voltages"
            )

        self.starting_stoichiometry = starting_stoichiometry
        self.starting_formula = get_formula_from_stoich(starting_stoichiometry)
        self.capacities = capacities
        self.average_voltage = average_voltage
        self.voltages = voltages
        self.reactions = reactions
        self.active_ion = active_ion
Exemplo n.º 4
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)
Exemplo n.º 5
0
    def print_volume_summary(self):
        """ Prints a volume data summary.

        If self.args['csv'] is True, save the summary to a file.

        """
        for reaction_idx, _ in enumerate(self.volume_data['Q']):
            data_str = '# Reaction {} \n'.format(reaction_idx + 1)
            data_str += '# ' + ''.join(
                get_formula_from_stoich(
                    self.volume_data['endstoichs'][reaction_idx])) + '\n'
            data_str += '# {:>10}\t{:>14} \t{:>14}\n'.format(
                'Q (mAh/g)', 'Volume (A^3)', 'Volume ratio with bulk')
            for idx, _ in enumerate(
                    self.volume_data['electrode_volume'][reaction_idx]):
                data_str += ('{:>10.2f} \t{:14.2f} \t{:14.2f}'.format(
                    self.volume_data['Q'][reaction_idx][idx],
                    self.volume_data['electrode_volume'][reaction_idx][idx],
                    self.volume_data['volume_ratio_with_bulk'][reaction_idx]
                    [idx]))
                if idx != len(self.volume_data['Q'][reaction_idx]) - 1:
                    data_str += '\n'
            if self.args.get('csv'):
                with open(''.join(self.species) + '_volume.csv', 'w') as f:
                    f.write(data_str)
            print('\nVolume data:')
            print('\n' + data_str)
Exemplo n.º 6
0
    def construct_phase_diagram(self):
        """ Create a phase diagram with arbitrary chemical potentials.

        Expects self.cursor to be populated with structures and chemical potential
        labels to be set under self.species.

        """
        self.set_chempots()
        self.cursor = filter_cursor_by_chempots(self.species, self.cursor)

        formation_key = 'formation_{}'.format(self.energy_key)
        extensive_formation_key = 'formation_{}'.format(
            self._extensive_energy_key)
        for ind, doc in enumerate(self.cursor):
            self.cursor[ind][formation_key] = get_formation_energy(
                self.chempot_cursor, doc, energy_key=self.energy_key)
            self.cursor[ind][extensive_formation_key] = doc[
                formation_key] * doc['num_atoms']

        if self._non_elemental and self.args.get('subcmd') in [
                'voltage', 'volume'
        ]:
            raise NotImplementedError(
                'Pseudo-binary/pseudo-ternary voltages not yet implemented.')

        self.phase_diagram = PhaseDiagram(self.cursor, formation_key,
                                          self._dimension)
        # aliases for data stored in phase diagram
        self.structures = self.phase_diagram.structures
        self.hull_dist = self.phase_diagram.hull_dist
        self.convex_hull = self.phase_diagram.convex_hull

        # ensure hull cursor is sorted by enthalpy_per_atom,
        # then by concentration, as it will be by default if from database
        hull_cursor = [
            self.cursor[idx]
            for idx in np.where(self.hull_dist <= self.hull_cutoff + EPS)[0]
        ]
        # TODO: check why this fails when the opposite way around
        hull_cursor = sorted(
            hull_cursor,
            key=lambda doc:
            (doc['concentration'], recursive_get(doc, self.energy_key)))

        # by default hull cursor includes all structures within hull_cutoff
        # if summary requested and we're in hulldiff mode, filter hull_cursor for lowest per stoich
        if self.args.get('summary') and self.args['subcmd'] == 'hulldiff':
            tmp_hull_cursor = []
            compositions = set()
            for ind, member in enumerate(hull_cursor):
                formula = get_formula_from_stoich(member['stoichiometry'],
                                                  sort=True)
                if formula not in compositions:
                    compositions.add(formula)
                    tmp_hull_cursor.append(member)

            hull_cursor = tmp_hull_cursor

        self.hull_cursor = hull_cursor
Exemplo n.º 7
0
def main():
    """ Run GA. """
    from glob import glob
    from sys import argv
    from matador.hull import QueryConvexHull
    from matador.fingerprints.similarity import get_uniq_cursor
    from matador.utils.chem_utils import get_formula_from_stoich
    from matador.scrapers.castep_scrapers import res2dict
    from ilustrado.ilustrado import ArtificialSelector

    cursor = [res2dict(res)[0] for res in glob('seed/*.res')]
    hull = QueryConvexHull(cursor=cursor,
                           no_plot=True,
                           kpoint_tolerance=0.03,
                           summary=True,
                           hull_cutoff=1e-1)
    print('Filtering down to only ternary phases... {}'.format(
        len(hull.hull_cursor)))
    hull.hull_cursor = [
        doc for doc in hull.hull_cursor if len(doc['stoichiometry']) == 3
    ]
    print('Filtering unique structures... {}'.format(len(hull.hull_cursor)))
    # uniq_list, _, _, _ = list(get_uniq_cursor(hull.hull_cursor[1:-1], debug=False))
    # cursor = [hull.hull_cursor[1:-1][ind] for ind in uniq_list]
    cursor = hull.hull_cursor
    print('Final cursor length... {}'.format(len(cursor)))
    print('over {} stoichiometries...'.format(
        len(
            set([
                get_formula_from_stoich(doc['stoichiometry']) for doc in cursor
            ]))))
    print([doc['stoichiometry'] for doc in cursor])

    def filter_fn(doc):
        """ Filter out any non-ternary phases. """
        return True if len(doc['stoichiometry']) == 3 else False

    ArtificialSelector(gene_pool=cursor,
                       seed='KPSn',
                       compute_mode='slurm',
                       entrypoint=__file__,
                       walltime_hrs=12,
                       slurm_template='template.slurm',
                       max_num_nodes=100,
                       hull=hull,
                       check_dupes=0,
                       structure_filter=filter_fn,
                       best_from_stoich=True,
                       max_num_mutations=3,
                       max_num_atoms=50,
                       mutation_rate=0.4,
                       crossover_rate=0.6,
                       num_generations=20,
                       population=30,
                       num_survivors=20,
                       elitism=0.5,
                       loglevel='debug')
Exemplo n.º 8
0
 def test_stoich_to_form(self):
     stoich = [["Li", 1], ["P", 9]]
     form = "LiP9"
     self.assertEqual(form, get_formula_from_stoich(stoich))
     stoich = [["P", 9], ["Li", 1]]
     form = "LiP9"
     self.assertEqual(form, get_formula_from_stoich(stoich))
     stoich = [["Li", 1], ["P", 9]]
     form = "LiP$_\\mathrm{9}$"
     self.assertEqual(
         form,
         get_formula_from_stoich(stoich,
                                 tex=True,
                                 latex_sub_style="\\mathrm"))
     stoich = [["Li", 1], ["P", 9]]
     form = "P9Li"
     self.assertEqual(form,
                      get_formula_from_stoich(stoich, elements=["P", "Li"]))
Exemplo n.º 9
0
    def testVoronoi(self):
        chdir(REAL_PATH)
        with open('data/jdkay7-gen1.json') as f:
            generation = load(f)

        _iter = 0
        loglevel = 'debug'
        numeric_loglevel = getattr(logging, loglevel.upper(), None)
        logging.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s',
                            filename='test.log',
                            level=numeric_loglevel)
        while _iter < 100:
            mutant = deepcopy(np.random.choice(generation[1:-1]))
            initial_stoich = deepcopy(mutant['stoichiometry'])
            voronoi_shuffle(mutant, debug=False)
            print(get_formula_from_stoich(initial_stoich), '---->',
                  get_formula_from_stoich(mutant['stoichiometry']))
            # feasible = check_feasible(mutant, [generation[3]], max_num_atoms=40, debug=True)
            # self.assertTrue(feasible)
            dumps(mutant)
            _iter += 1
Exemplo n.º 10
0
    def fake_chempots(self, custom_elem=None):
        """ Spoof documents for command-line chemical potentials.

        Keyword arguments:
            custom_elem (list(str)): list of element symbols to generate chempots for.

        """
        self.chempot_cursor = []
        print('Generating fake chempots...')

        if custom_elem is None:
            custom_elem = self.species

        if len(custom_elem) != len(self.species):
            raise RuntimeError(
                'Wrong number of compounds/chemical potentials specified: {} vs {}'
                .format(custom_elem, self.args.get('chempots')))
        for i, _ in enumerate(self.args.get('chempots')):
            self.chempot_cursor.append(dict())
            self.chempot_cursor[i]['stoichiometry'] = get_stoich_from_formula(
                custom_elem[i])
            self.chempot_cursor[i][self.energy_key] = -1 * abs(
                self.args.get('chempots')[i])
            self.chempot_cursor[i][self._extensive_energy_key] = self.chempot_cursor[i][self.energy_key] * \
                sum(elem[1] for elem in self.chempot_cursor[i]['stoichiometry'])
            self.chempot_cursor[i]['num_fu'] = 1
            self.chempot_cursor[i]['num_atoms'] = 1
            self.chempot_cursor[i]['text_id'] = ['command', 'line']
            self.chempot_cursor[i]['_id'] = None
            self.chempot_cursor[i]['source'] = ['command_line']
            self.chempot_cursor[i]['space_group'] = 'xxx'
            self.chempot_cursor[i][self._extensive_energy_key +
                                   '_per_b'] = self.chempot_cursor[i][
                                       self.energy_key]
            self.chempot_cursor[i]['num_a'] = 0
            self.chempot_cursor[i]['cell_volume'] = 1
            self.chempot_cursor[i]['concentration'] = [
                1 if i == ind else 0 for ind in range(self._dimension - 1)
            ]
        self.chempot_cursor[0]['num_a'] = float('inf')
        notify = 'Custom chempots:'
        for chempot in self.chempot_cursor:
            notify += '{:3} = {} eV/fu, '.format(
                get_formula_from_stoich(chempot['stoichiometry'], sort=False),
                chempot[self._extensive_energy_key])

        if self.args.get('debug'):
            for match in self.chempot_cursor:
                print(match)
        print(len(notify) * '─')
        print(notify)
        print(len(notify) * '─')
Exemplo n.º 11
0
    def set_bourgeoisie(self, elites=None, best_from_stoich=True):
        """ Set the structures that will continue to the next generation,
        i.e. the bourgeoisie.

        Keyword Arguments:
            elites list(dict)       : list of elite structures to
                include from the previous generation,
            best_from_stoich (bool) : whether to include one structure from
                each stoichiometry.

        """

        # first populate with best precomputed "num_accepted" structures,
        # where "num_accepted" takes into account the number of elites
        self.bourgeoisie = sorted(self.populace,
                                  key=lambda member: member["fitness"],
                                  reverse=True)[:self._num_accepted]

        # find the fittest structure from each stoichiometry sampled
        if best_from_stoich:
            best_from_stoichs = dict()
            for struc in self.populace:
                stoich = get_formula_from_stoich(sorted(
                    struc["stoichiometry"]))
                best_from_stoichs[stoich] = {"fitness": -1}
            for struc in self.populace:
                stoich = get_formula_from_stoich(sorted(
                    struc["stoichiometry"]))
                if best_from_stoichs[stoich]["fitness"] < struc["fitness"]:
                    best_from_stoichs[stoich] = struc

            # if its not already included, add the best structure from this
            # stoichiometry in exchange for the least fit structure already included
            for stoich in best_from_stoichs:
                if best_from_stoichs[stoich] not in self.bourgeoisie:
                    self.bourgeoisie.insert(0, best_from_stoichs[stoich])

        if elites is not None:
            self.bourgeoisie.extend(elites)
Exemplo n.º 12
0
    def generate_stability_statistics(self, group_by='structure'):
        """ Creates a histogram that counts how many times each structure
        is found to be stable in the ensemble.

        Keyword arguments:
            group_by (str): either 'structure' or 'formula' for bar groupings.

        """
        from collections import defaultdict
        histogram = defaultdict(int)
        for pd in self.phase_diagrams:
            for doc in pd.stable_structures:
                if group_by == 'formula':
                    histogram[get_formula_from_stoich(
                        doc['stoichiometry'])] += 1
                else:
                    histogram[get_root_source(doc)] += 1
        return histogram
Exemplo n.º 13
0
def _get_hull_labels(hull,
                     label_cutoff=None,
                     num_species=None,
                     exclude_edges=True):
    """ Return list of structures to labels on phase diagram.

    Parameters:
        hull (matador.hull.QueryConvexHull): phase diagram to plot.

    Keyword arguments:
        num_species (int): structures containing this number of species
            will be labelled.
        exclude_edges (bool): ignore any structure that has zero of any
            chemical potential (i.e the edge of the hull).

    Returns:
        label_cursor (list(dict)): list of matador documents to label.

    """
    eps = 1e-9
    if label_cutoff is None:
        label_cutoff = 0.0
    if isinstance(label_cutoff, list) and len(label_cutoff) == 2:
        label_cutoff = sorted(label_cutoff)
        # first, only apply upper limit as we need to filter by stoich aftewards
        label_cursor = [
            doc for doc in hull.cursor
            if doc['hull_distance'] <= label_cutoff[1]
        ]
    else:
        if isinstance(label_cutoff, list):
            assert len(
                label_cutoff
            ) == 1, 'Incorrect number of label_cutoff values passed, should be 1 or 2.'
            label_cutoff = label_cutoff[0]
        label_cursor = [
            doc for doc in hull.cursor
            if doc['hull_distance'] <= label_cutoff + eps
        ]

    num_labels = len({
        get_formula_from_stoich(doc['stoichiometry'])
        for doc in label_cursor
    })
    if num_labels < len(label_cursor):
        tmp_cursor = []
        for doc in label_cursor:
            if doc['stoichiometry'] not in [
                    _doc['stoichiometry'] for _doc in tmp_cursor
            ]:
                tmp_cursor.append(doc)
            else:
                label_cursor = tmp_cursor
    if isinstance(label_cutoff, list) and len(label_cutoff) == 2:
        # now apply lower limit
        label_cursor = [
            doc for doc in label_cursor
            if label_cutoff[0] <= doc['hull_distance'] <= label_cutoff[1]
        ]
    # remove chemical potentials and unwanted e.g. binaries
    if num_species is not None:
        label_cursor = [
            doc for doc in label_cursor
            if len(doc['stoichiometry']) == num_species
        ]
    if exclude_edges:
        label_cursor = [
            doc for doc in label_cursor
            if (all(doc['concentration']) > 0
                and sum(doc['concentration']) <= 1 - 1e-6)
        ]

    label_cursor = sorted(label_cursor, key=lambda doc: doc['concentration'])

    return label_cursor
Exemplo n.º 14
0
def plot_field(
    data,
    field="formation_energy_per_atom",
    parameter="cut_off_energy",
    ax=None,
    reference="last",
    max_y=None,
    log=False,
    colour_by="formula",
    label_x=True,
    label_y=True,
):
    """ Plot the convergence fields for each structure in `data` at
    each value of `parameter`.

    Parameters:
        data (dict): nested dictionary with keys for each structure
            that stores the convergence data to be plotted. e.g.
            {'structure_A':
                {'formation_energy_per_atom': [1, 2, 3],
                 'cutoff': [500, 400, 300]
                }
            }.

    Keyword arguments:
        field (str): the key under which convergence data is stored. This
            string is used for the y-axis label.
        parameter (str): the key under which the convergence parameter
            values are stored. This string is used for the x-axis label.
        ax (matplotlib.Axis): optional axis object to use for plot.
        reference (str): if 'last' then use the last value of the
            convergence data as the zero.

    """
    import matplotlib.pyplot as plt

    if ax is None:
        fig, ax = plt.subplots()

    lines = []
    labels = []
    labelled = []
    ax.xaxis.grid()

    colourmap = {}
    if colour_by == "formula":
        formulae = sorted(
            list(
                {
                    get_formula_from_stoich(data[key]["stoichiometry"], tex=True)
                    for key in data
                }
            )
        )
        for formula in formulae:
            colourmap[formula] = next(ax._get_lines.prop_cycler)["color"]

    for ind, key in enumerate(data):

        formula = get_formula_from_stoich(data[key]["stoichiometry"], tex=True)
        try:
            values, parameters = get_convergence_values(
                data[key], parameter, field, reference=reference, log=log
            )
        except Exception:
            print("Missing data for {}->{}->{}".format(key, parameter, field))
            continue

        label = None
        if colour_by == "formula":
            c = colourmap[formula]
            if formula not in labelled:
                label = formula
                labelled.append(formula)
        else:
            c = next(ax._get_lines.prop_cycler)["color"]
            label = key

        # only plot points if there's less than 25 lines
        if len(data) < 30:
            ax.plot(
                parameters,
                values,
                "o",
                markersize=5,
                alpha=1,
                label=label,
                lw=0,
                zorder=1000,
                c=c,
            )
            ax.plot(parameters, values, "-", alpha=0.2, lw=1, zorder=1000, c=c)
        else:
            ax.plot(
                parameters, values, "-", alpha=0.5, lw=1, zorder=1000, label=label, c=c
            )

        if label is not None:
            lines, labels = ax.get_legend_handles_labels()

    if label_x:
        ax.set_xlabel(parameter.replace("_", " "))
    if label_y:
        if "force" in field.lower():
            ylabel = "$\\Delta F$ (eV/$\\AA$)"
        else:
            ylabel = "$\\Delta E$ (meV/atom)"
        ax.set_ylabel(ylabel.format(field.replace("_", " ")))

    if max_y is not None:
        ax.set_ylim(0, max_y)

    return ax, lines, labels
Exemplo n.º 15
0
def display_results(cursor,
                    energy_key='enthalpy_per_atom', summary=False,
                    args=None, argstr=None, additions=None, deletions=None,
                    sort=True, hull=False, markdown=False, latex=False, colour=True,
                    return_str=False, use_source=True, details=False,
                    per_atom=False, eform=False, source=False, **kwargs):
    """ Print query results in a table, with many options for customisability.

    TODO: this function has gotten out of control and should be rewritten.

    Parameters:
        cursor (list of dict or pm.cursor.Cursor): list of matador documents

    Keyword arguments:
        summary (bool): print a summary per stoichiometry, that uses the lowest
            energy phase (requires `sort=True`).
        argstr (str): string to store matador initialisation command
        eform (bool): prepend energy key with "formation_".
        sort (bool): sort input cursor by the value of energy key.
        return_str (bool): return string instead of printing.
        details (bool): print extra details as an extra line per structure.
        per_atom (bool): print quantities per atom, rather than per fu.
        source (bool): print all source files associated with the structure.
        use_source (bool): use the source instead of the text id when displaying a structure.
        hull (bool): whether or not to print hull-style (True) or query-style
        energy_key (str or list): key (or recursive key) to print as energy (per atom)
        markdown (bool): whether or not to write a markdown file containing results
        latex (bool): whether or not to create a LaTeX table
        colour (bool): colour on-hull structures
        additions (list): list of string text_ids to be coloured green with a (+)
            or, list of indices referring to those structures in the cursor.
        deletions (list): list of string text_ids to be coloured red with a (-)
            or, list of indices referring to those structures in the cursor.
        kwargs (dict): any extra args are ignored.

    Returns:
        str or None: markdown or latex string, if markdown or latex is True, else None.

    """

    add_index_mode = False
    del_index_mode = False
    if additions:
        if isinstance(additions[0], int):
            add_index_mode = True
    if deletions:
        if isinstance(deletions[0], int):
            del_index_mode = True

    if add_index_mode:
        assert max(additions) <= len(cursor) and min(additions) >= 0
    if del_index_mode:
        assert max(deletions) <= len(cursor) and min(deletions) >= 0

    if markdown and latex:
        latex = False

    # lists in which to accumulate the table
    struct_strings = []
    detail_strings = []
    detail_substrings = []
    source_strings = []
    formulae = []

    # tracking the last formula
    last_formula = ''

    if not cursor:
        raise RuntimeError('No structures found in cursor.')

    if markdown:
        markdown_string = 'Date: {}  \n'.format(strftime("%H:%M %d/%m/%Y"))
        if argstr is not None:
            markdown_string += 'Command: matador {}  \n'.format(' '.join(argstr))
        markdown_string += 'Version: {}  \n\n'.format(__version__)

    if latex:
        latex_string = (
            "\\begin{tabular}{l r r c l l}\n"
            "\\rowcolor{gray!20}\n"
            "formula & "
            "\\thead{$\\Delta E$\\\\(meV/atom)} & "
            "spacegroup & "
            "provenance & "
            "description \\\\ \n\n"
        )
        latex_struct_strings = []
        latex_sub_style = r'\mathrm'
    else:
        latex_sub_style = ''

    header_string, units_string = _construct_header_string(
        markdown, use_source, per_atom, eform, hull, summary, energy_key
    )

    if summary and isinstance(cursor, pm.cursor.Cursor):
        raise RuntimeError("Unable to provide summary when displaying cursor object.")

    # ensure cursor is sorted by enthalpy
    if sort and isinstance(cursor, pm.cursor.Cursor):
        print("Unable to check sorting of cursor, assuming it is already sorted.")
    elif sort:
        sorted_inds = sorted(enumerate(cursor),
                             key=lambda element: recursive_get(element[1], energy_key))
        cursor = [ind[1] for ind in sorted_inds]
        sorted_inds = [ind[0] for ind in sorted_inds]
        if additions is not None and add_index_mode:
            additions = [sorted_inds.index(ind) for ind in additions]
        if deletions is not None and del_index_mode:
            deletions = [sorted_inds.index(ind) for ind in deletions]

    # loop over structures and create pretty output
    for ind, doc in enumerate(cursor):

        # use the formula to see if we need to update gs_enthalpy for this formula
        formula_substring = get_formula_from_stoich(doc['stoichiometry'],
                                                    tex=latex,
                                                    latex_sub_style=latex_sub_style)

        if 'encapsulated' in doc:
            formula_substring += '+CNT'
        if last_formula != formula_substring:
            gs_enthalpy = 0.0
        formulae.append(formula_substring)

        struct_strings.append(
            _construct_structure_string(
                doc, ind, formula_substring, gs_enthalpy, use_source, colour, hull, additions,
                deletions, add_index_mode, del_index_mode, energy_key, per_atom, eform, markdown, latex
            )
        )

        if latex:
            latex_struct_strings.append("{:^30} {:^10} & ".format(formula_substring, '$\\star$'
                                                                  if doc.get('hull_distance') == 0 else ''))
            latex_struct_strings[-1] += ("{:^20.0f} & ".format(doc.get('hull_distance') * 1000)
                                         if doc.get('hull_distance', 0) > 0 else '{:^20} &'.format('-'))
            latex_struct_strings[-1] += "{:^20} & ".format(doc.get('space_group', 'xxx'))
            prov = get_guess_doc_provenance(doc['source'], doc.get('icsd'))
            if doc.get('icsd'):
                prov += ' {}'.format(doc['icsd'])
            latex_struct_strings[-1] += "{:^30} & ".format(prov)
            latex_struct_strings[-1] += "{:^30} \\\\".format('')

        if last_formula != formula_substring:
            if per_atom:
                gs_enthalpy = recursive_get(doc, energy_key)
            else:
                gs_enthalpy = recursive_get(doc, energy_key) * doc['num_atoms'] / doc['num_fu']

        last_formula = formula_substring

        if details:
            detail_string, detail_substring = _construct_detail_strings(doc, padding_length=len(header_string), source=source)
            detail_strings.append(detail_string)
            detail_substrings.append(detail_substring)

        if source:
            source_strings.append(_construct_source_string(doc['source']))

    total_string = ''
    total_string += len(header_string) * '─' + '\n'
    total_string += header_string + '\n'
    total_string += units_string + '\n'
    total_string += len(header_string) * '─' + '\n'

    if markdown:
        markdown_string += len(header_string) * '-' + '\n'
        markdown_string += header_string + '\n'
        markdown_string += units_string + '\n'
        markdown_string += len(header_string) * '-' + '\n'

    summary_inds = []
    # filter for lowest energy phase per stoichiometry
    if summary:
        current_formula = ''
        formula_list = {}
        for ind, substring in enumerate(formulae):
            if substring != current_formula and substring not in formula_list:
                current_formula = substring
                formula_list[substring] = 0
                summary_inds.append(ind)
            formula_list[substring] += 1
    else:
        summary_inds = range(len(struct_strings))

    # construct final string containing table
    if markdown:
        markdown_string += '\n'.join(struct_strings[ind] for ind in summary_inds)
    elif latex:
        latex_string += '\n'.join(latex_struct_strings[ind] for ind in summary_inds)
    else:
        for ind in summary_inds:
            total_string += struct_strings[ind] + '\n'
            if details:
                total_string += detail_strings[ind] + '\n'
                total_string += detail_substrings[ind] + '\n'
            if source:
                total_string += source_strings[ind] + '\n'
            if details or source:
                total_string += len(header_string) * '─' + '\n'

    if markdown:
        markdown_string += '```'
        return markdown_string

    if latex:
        latex_string += '\\end{tabular}'
        return latex_string

    if return_str:
        return total_string

    print(total_string)
Exemplo n.º 16
0
    def _find_hull_pathway_intersections(self, endpoint, endstoich, hull):
        """ This function traverses a ternary phase diagram on a path from `endpoint`
        towards [1, 0, 0], i.e. a pure phase of the active ion. The aim is to find
        all of the faces and intersections along this pathway, making sure to only
        add unique intersections, and making appropriate (symmetry-breaking) choices
        of which face to use. We proceed as follows:

            1. Filter out any vertices that contain only binary phases, or that contain
                only the chemical potentials.
            2. Loop over all faces of the convex hull and test if the pathway touches/intersects
                that face. If it does, ascertain where the intersection occurs, and whether
                it is at one, two or none of the vertices of the face. Create an array of unique
                intersections for this face and save them for later.
            3. Loop over the zeros found between for each face. If the pathway only grazes a single
                point on this face, ignore it. If one of the edges is parallel to the pathway,
                make sure to include this face and the faces either side of it.

        Parameters:
            endpoint (np.ndarray): array containing composition (2D) and energy of
                start point.

        """
        import scipy.spatial.distance
        import itertools

        endpoint = endpoint[:-1]
        compositions = np.zeros_like(hull.points)
        compositions[:, 0] = hull.points[:, 0]
        compositions[:, 1] = hull.points[:, 1]
        compositions[:, 2] = 1 - hull.points[:, 0] - hull.points[:, 1]

        # define the composition pathway through ternary space
        gradient = endpoint[1] / (endpoint[0] - 1)
        # has to intersect [1, 0] so c = y0 - m*x0 = -m
        y0 = -gradient

        # filter the vertices we're going to consider
        skip_inds = set()
        for simp_ind, simplex in enumerate(hull.simplices):
            vertices = np.asarray([compositions[i] for i in simplex])
            # skip all simplices that contain only binary phases
            for i in range(3):
                if np.max(vertices[:, i]) < BOUNDARY_EPS:
                    skip_inds.add(simp_ind)
                    continue
            # skip the outer triangle formed by the chemical potentials
            if all(np.max(vertices, axis=-1) > 1 - BOUNDARY_EPS):
                skip_inds.add(simp_ind)
                continue

        two_phase_crossover = []
        three_phase_crossover = []

        for simp_ind, simplex in enumerate(hull.simplices):
            if simp_ind in skip_inds:
                continue
            vertices = np.asarray([compositions[i] for i in simplex])

            # put each vertex of triangle into line equation and test their signs
            test = vertices[:, 0] * gradient + y0 - vertices[:, 1]
            test[np.abs(test) < BOUNDARY_EPS] = 0.0
            test = np.sign(test)

            # if there are two different signs in the test array, then the line intersects/grazes this face triangle
            if len(np.unique(test)) > 1:
                # now find intersection points and split them into one, two and three-phase crossover regions
                zeros = [val for zero in np.where(test == 0) for val in zero.tolist()]
                num_zeros = len(zeros)
                zero_points = []
                # skip_vertex = None
                for zero in zeros:
                    zero_pos = compositions[simplex[zero]]
                    if num_zeros == 2:
                        zero_points.append(zero_pos)
                        two_phase_crossover.append((zero_pos, simp_ind))

                # if we have already found both crossovers, skip to next face
                if num_zeros == 2:
                    continue

                # now find the non-trivial intersections
                for i, j in itertools.combinations(simplex, r=2):
                    # if skip_vertex in [i, j]:
                    #     continue

                    A = compositions[i]
                    B = compositions[j]
                    C = endpoint
                    D = [1, 0]

                    def coeffs(X, Y):
                        # find line equation for edge AB
                        # a x + b y = c
                        return (Y[1] - X[1], X[0] - Y[0])

                    a1, b1 = coeffs(A, B)
                    c1 = a1 * A[0] + b1 * A[1]

                    a2, b2 = coeffs(C, D)
                    c2 = a2 * C[0] + b2 * C[1]
                    det = a1 * b2 - a2 * b1
                    if det == 0:
                        continue

                    x = (b2 * c1 - b1 * c2) / det
                    y = (a1 * c2 - a2 * c1) / det
                    intersection_point = np.asarray([x, y, 1-x-y])

                    # now have to test whether that intersection point is inside the triangle
                    # which we can do by testing that is inside the rectangle spanned by AB
                    x_bound = sorted([A[0], B[0]])
                    y_bound = sorted([A[1], B[1]])
                    if (
                        x_bound[0] - BOUNDARY_EPS/2 <= intersection_point[0] <= x_bound[1] + BOUNDARY_EPS/2 and
                        y_bound[0] - BOUNDARY_EPS/2 <= intersection_point[1] <= y_bound[1] + BOUNDARY_EPS/2
                    ):
                        # three_phase_crossover.append((intersection_point, simp_ind))
                        zero_points.append(intersection_point)

                unique_zeros = []
                for zero in zero_points:
                    if (len(unique_zeros) >= 1 and
                            np.any(scipy.spatial.distance.cdist(unique_zeros, zero.reshape(1, 3)) < BOUNDARY_EPS)):
                        pass
                    else:
                        unique_zeros.append(zero)

                num_zeros = len(unique_zeros)
                if num_zeros == 1:
                    # we never need to use faces which just touch the triangle
                    continue
                elif num_zeros == 2:
                    three_phase_crossover.append((unique_zeros[0], simp_ind))
                    three_phase_crossover.append((unique_zeros[1], simp_ind))

        # loop over points and only add the unique ones to the crossover array
        # if a point exists as a two-phase region, then always add that simplex
        crossover = [[1, 0, 0]]
        simplices = {}
        zeros = sorted(two_phase_crossover + three_phase_crossover, key=lambda x: x[0][0])
        # for multiplicity, zeros in enumerate([one_phase_crossover, two_phase_crossover, three_phase_crossover]):
        for zero, simp_ind in zeros:
            if len(crossover) >= 1 and np.any(scipy.spatial.distance.cdist(crossover, zero.reshape(1, 3)) < BOUNDARY_EPS):
                pass
            else:
                if simp_ind not in simplices:
                    crossover.append(zero)
                    # if multiplicity > 0:
                    simplices[simp_ind] = zero[0]

        intersections = sorted(simplices.items(), key=lambda x: x[1])

        if len(intersections) == 0:
            raise RuntimeError(
                'No intermediate structures found for starting point {}.'
                .format(get_formula_from_stoich(endstoich))
            )

        return intersections, sorted(crossover, key=lambda x: x[0])
Exemplo n.º 17
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
Exemplo n.º 18
0
hull = QueryConvexHull(cursor=cursor,
                       no_plot=True,
                       kpoint_tolerance=0.03,
                       summary=True,
                       hull_cutoff=7.5e-2)
print('Filtering down to only ternary phases... {}'.format(
    len(hull.hull_cursor)))
hull.hull_cursor = [
    doc for doc in hull.hull_cursor if len(doc['stoichiometry']) == 3
]
print('Filtering unique structures... {}'.format(len(hull.hull_cursor)))
uniq_list, _, _, _ = list(get_uniq_cursor(hull.hull_cursor[1:-1], debug=False))
cursor = [hull.hull_cursor[1:-1][ind] for ind in uniq_list]
print('Final cursor length... {}'.format(len(cursor)))
print('over {} stoichiometries...'.format(
    len(set([get_formula_from_stoich(doc['stoichiometry'])
             for doc in cursor]))))
print([doc['stoichiometry'] for doc in cursor])

ArtificialSelector(gene_pool=cursor,
                   seed='KPSn',
                   hull=hull,
                   debug=False,
                   fitness_metric='hull',
                   nodes=['node1', 'node2', 'node15'],
                   ncores=[16, 16, 20],
                   check_dupes=1,
                   verbosity=1,
                   nprocs=nprocs,
                   executable='castep.mpi',
                   relaxer_params=None,
Exemplo n.º 19
0
def plot_field(data,
               field='formation_energy_per_atom', parameter='cut_off_energy',
               ax=None, reference='last', max_y=None, log=False, colour_by='formula'):
    """ Plot the convergence fields for each structure in `data` at
    each value of `parameter`.

    Parameters:
        data (dict): nested dictionary with keys for each structure
            that stores the convergence data to be plotted. e.g.
            {'structure_A':
                {'formation_energy_per_atom': [1, 2, 3],
                 'cutoff': [500, 400, 300]
                }
            }.

    Keyword arguments:
        field (str): the key under which convergence data is stored. This
            string is used for the y-axis label.
        parameter (str): the key under which the convergence parameter
            values are stored. This string is used for the x-axis label.
        ax (matplotlib.Axis): optional axis object to use for plot.
        reference (str): if 'last' then use the last value of the
            convergence data as the zero.

    """
    import matplotlib.pyplot as plt
    if ax is None:
        fig, ax = plt.subplots()

    lines = []
    labels = []
    labelled = []

    colourmap = {}
    if colour_by == 'formula':
        formulae = sorted(list({get_formula_from_stoich(data[key]['stoichiometry']) for key in data}))
        for formula in formulae:
            colourmap[formula] = next(ax._get_lines.prop_cycler)['color']

    for ind, key in enumerate(data):

        formula = get_formula_from_stoich(data[key]['stoichiometry'])
        try:
            values, parameters = get_convergence_values(data[key], parameter, field, reference=reference, log=log)
        except Exception:
            print('Missing data for {}->{}->{}'.format(key, parameter, field))
            continue

        label = None
        if colour_by == 'formula':
            c = colourmap[formula]
            if formula not in labelled:
                label = formula
                labelled.append(formula)
        else:
            c = next(ax._get_lines.prop_cycler)['color']
            label = key

        # only plot points if there's less than 25 lines
        if len(data) < 30:
            line, = ax.plot(parameters, values, 'o', markersize=5, alpha=1, label=label, lw=0, zorder=1000, c=c)
            points, = ax.plot(parameters, values, '-', alpha=0.2, lw=1, zorder=1000, c=c)
        else:
            points, = ax.plot(parameters, values, '-', alpha=0.5, lw=1, zorder=1000, label=label, c=c)
            line = points

        if label is not None:
            lines.append(line)
            labels.append(label)

    ax.set_xlabel(parameter.replace('_', ' '))
    ax.set_ylabel('Absolute relative {} (meV)'.format(field.replace('_', ' ')))
    if max_y is not None:
        ax.set_ylim(0, max_y)

    return ax, lines, labels
Exemplo n.º 20
0
 def formula(self):
     """ Returns chemical formula of structure. """
     from matador.utils.chem_utils import get_formula_from_stoich
     return get_formula_from_stoich(self.stoichiometry, tex=False)
Exemplo n.º 21
0
def plot_volume_curve(hull, ax=None, show=True, legend=False, as_percentages=False, **kwargs):
    """ Plot volume curve calculated for phase diagram.

    Parameters:
        hull (matador.hull.QueryConvexHull): matador hull object.

    Keyword arguments:
        ax (matplotlib.axes.Axes): an existing axis on which to plot.
        show (bool): whether or not to display plot in X-window.
        legend (bool): whether to add the legend.
        as_percentages (bool): whether to show the expansion as a percentage.

    """
    import matplotlib.pyplot as plt

    if ax is None:
        if hull.savefig or any([kwargs.get(ext) for ext in SAVE_EXTS]):
            fig = plt.figure()
        else:
            fig = plt.figure()
        ax = fig.add_subplot(111)
    else:
        ax = ax

    if as_percentages:
        volume_key = 'volume_expansion_percentage'
    else:
        volume_key = 'volume_ratio_with_bulk'

    for j in range(len(hull.volume_data['electrode_volume'])):
        c = list(plt.rcParams['axes.prop_cycle'].by_key()['color'])[j+2]
        stable_hull_dist = hull.volume_data['hull_distances'][j]
        if len(stable_hull_dist) != len(hull.volume_data['Q'][j]):
            raise RuntimeError("This plot does not support --hull_cutoff.")

        ax.plot(
            [q for ind, q in enumerate(hull.volume_data['Q'][j][:-1]) if stable_hull_dist[ind] == 0],
            [v for ind, v in enumerate(hull.volume_data[volume_key][j]) if stable_hull_dist[ind] == 0],
            marker='o', markeredgewidth=1.5, markeredgecolor='k', c=c, zorder=1000, lw=0,
        )

        ax.plot(
            [q for ind, q in enumerate(hull.volume_data['Q'][j][:-1]) if stable_hull_dist[ind] == 0],
            [v for ind, v in enumerate(hull.volume_data[volume_key][j]) if stable_hull_dist[ind] == 0],
            lw=2, c=c,
            label=("{}"
                   .format(get_formula_from_stoich(hull.volume_data['endstoichs'][j], tex=True)))
        )

    ax.set_xlabel("Gravimetric capacity (mAh/g)")
    if as_percentages:
        ax.set_ylabel('Volume expansion (%)')
    else:
        ax.set_ylabel('Volume ratio with starting electrode')

    if legend or len(hull.volume_data['Q']) > 1:
        ax.legend()
    fname = '{}_volume'.format(''.join(hull.elements))

    if hull.savefig or any([kwargs.get(ext) for ext in SAVE_EXTS]):
        for ext in SAVE_EXTS:
            if hull.args.get(ext) or kwargs.get(ext):
                plt.savefig('{}.{}'.format(fname, ext), transparent=True)
                print('Wrote {}.{}'.format(fname, ext))

    if show:
        plt.show()
Exemplo n.º 22
0
 def formula_tex(self):
     """ Returns chemical formula of structure in LaTeX format. """
     from matador.utils.chem_utils import get_formula_from_stoich
     return get_formula_from_stoich(self.stoichiometry, tex=True)
Exemplo n.º 23
0
    def __init__(self,
                 doc,
                 wavelength: float = 1.5406,
                 lorentzian_width: float = 0.03,
                 two_theta_resolution: float = 0.01,
                 two_theta_bounds: Tuple[float, float] = (0, 90),
                 theta_m: float = 0.0,
                 scattering_factors: str = "RASPA",
                 lazy=False,
                 plot=False,
                 progress=False,
                 *args,
                 **kwargs):
        """ Set up the PXRD, and compute it, if lazy is False.

        Parameters:
            doc (dict/Crystal): matador document to compute PXRD for.

        Keyword arguments:
            lorentzian_width (float): width of Lorentzians for broadening (DEFAULT: 0.03)
            wavelength (float): incident X-ray wavelength
                (DEFAULT: CuKa, 1.5406).
            theta_m (float): the monochromator angle in degrees (DEFAULT: 0)
            two_theta_resolution (float): resolution of grid 2θ
                used for plotting.
            two_theta_bounds (tuple of float): values between which
                to compute the PXRD pattern.
            scattering_factors (str): either "GSAS" or "RASPA" (default),
                which set of atomic scattering factors to use.
            lazy (bool): whether to compute PXRD or just set it up.
            plot (bool): whether to display PXRD as a plot.

        """
        self.wavelength = wavelength
        self.lorentzian_width = lorentzian_width
        self.two_theta_resolution = two_theta_resolution
        if two_theta_bounds is not None:
            self.two_theta_bounds = list(two_theta_bounds)
        else:
            self.two_theta_bounds = [0, 90]
        self.theta_m = theta_m
        self.scattering_factors = scattering_factors
        self.progress = progress

        if self.two_theta_bounds[0] < THETA_TOL:
            self.two_theta_bounds[0] = THETA_TOL

        if np.min(doc.get('site_occupancy', [1.0])) < 1.0:
            print("System has partial occupancy, not refining with spglib.")
            self.doc = Crystal(doc)
        else:
            self.doc = Crystal(standardize_doc_cell(doc, primitive=True))

        self.formula = get_formula_from_stoich(self.doc['stoichiometry'],
                                               tex=True)
        self.spg = self.doc['space_group']

        species = list(set(self.doc['atom_types']))

        # this could be cached across PXRD objects but is much faster than the XRD calculation itself
        if self.scattering_factors == "GSAS":
            from matador.data.atomic_scattering import GSAS_ATOMIC_SCATTERING_COEFFS
            self.atomic_scattering_coeffs = {
                spec: GSAS_ATOMIC_SCATTERING_COEFFS[spec]
                for spec in species
            }
        elif self.scattering_factors == "RASPA":
            from matador.data.atomic_scattering import RASPA_ATOMIC_SCATTERING_COEFFS
            self.atomic_scattering_coeffs = {
                spec: RASPA_ATOMIC_SCATTERING_COEFFS[spec]
                for spec in species
            }
        else:
            raise RuntimeError(
                "No set of scattering factors matched: {}. Please use 'GSAS' or 'RASPA'."
                .format(self.scattering_factors))

        if not lazy:
            self.calculate()
            if plot:
                self.plot()
Exemplo n.º 24
0
def plot_pdf(pdfs,
             labels=None, r_min=None, r_max=None,
             offset=1.2, text_offset=(0.0, 0.0),
             legend=False, annotate=True, figsize=None,
             filename=None,
             **kwargs):
    """ Plot PDFs.

    Parameters:
        pdfs (list of matador.fingerprints.pdf.PDF or matador.crystal.Crystal or dict):
            the PDFs to plot, as a list of PDF or Crystal objects, or a matador document.

    Keyword arguments:
        labels (list of str): labels to add to the PDF plot.
        offset (float): amount by which to separate the PDFs in the plot. A value of 1
            will separate by the maximum intensity across the PDFs. Default is 1.5.
        text_offset (tuple of float): two float values to move annotations around relative
            to the base of their corresponding PDF, in units of (Angstrom, max_gr).
        r_max (float): the radius to plot out to. Default is the minmax(radius across
            all PDFs).
        annotate (bool): whether or not to apply the PDF labels as an annotation.
        legend (bool): whether or not to apply the PDF labels as a legend.
        figsize (tuple of float): matplotlib figure size. Default scales with number of PDFs.

    Returns:
        matplotlib.pyplot.Axes: axis object which can be modified further.

    """

    import matplotlib.pyplot as plt

    if not isinstance(pdfs, list):
        pdfs = [pdfs]
    if labels is not None and not isinstance(labels, list):
        labels = [labels]

    if figsize is None:
        _user_default_figsize = plt.rcParams.get('figure.figsize', (8, 6))
        height = len(pdfs) * max(0.5, _user_default_figsize[1] / 1.5 / len(pdfs))
        figsize = (_user_default_figsize[0], height)

    fig = plt.figure(figsize=figsize)
    ax1 = fig.add_subplot(111)

    if labels is not None and len(labels) != len(pdfs):
        raise RuntimeError("Wrong number of labels {} for PDFs.".format(labels))

    if isinstance(pdfs[0], Crystal):
        gr_max = max(np.max(pdf.pdf.gr) for pdf in pdfs)
        _r_max = min(np.max(pdf.pdf.r_space) for pdf in pdfs)
    elif isinstance(pdfs[0], dict):
        gr_max = max(np.max(pdf['pdf'].gr) for pdf in pdfs)
        _r_max = min(np.max(pdf['pdf'].r_space) for pdf in pdfs)
    else:
        gr_max = max(np.max(pdf.gr) for pdf in pdfs)
        _r_max = min(np.max(pdf.r_space) for pdf in pdfs)
    abs_offset = offset * gr_max

    if r_max is None:
        r_max = _r_max
    if r_min is None:
        r_min = 0.0

    ax1.set_ylabel('Pair distribution function, $g(r)$')
    ax1.get_yaxis().set_ticks([])
    ax1.set_xlim(r_min, r_max+0.5)

    for ind, pdf in enumerate(pdfs):

        if isinstance(pdf, Crystal):
            pdf = pdf.pdf
        elif isinstance(pdf, dict) and 'pdf' in pdf:
            pdf = pdf['pdf']

        if labels:
            label = labels[ind]
        else:
            label = get_space_group_label_latex(pdf.spg) + '-' + get_formula_from_stoich(pdf.stoichiometry, tex=True)

        ax1.plot(pdf.r_space, pdf.gr + abs_offset * ind, label=label)
        if text_offset is not None:
            text_x = text_offset[0]
        if text_offset is not None:
            text_y = abs_offset*ind + text_offset[1]*gr_max
        if label is not None and annotate:
            ax1.text(text_x, text_y, label)

    ax1.set_ylim(-gr_max * 0.2, offset * gr_max * len(pdfs))

    ax1.set_xlabel('$r$ ($\\AA$)')

    if legend:
        legend = ax1.legend()

    if any([kwargs.get('pdf'), kwargs.get('svg'), kwargs.get('png')]):
        bbox_extra_artists = None
        if filename is None:
            filename = '-'.join([get_formula_from_stoich(pdf.stoichiometry) for pdf in pdfs]) + '_pdf'

        if kwargs.get('pdf'):
            plt.savefig('{}.pdf'.format(filename),
                        bbox_inches='tight', transparent=True, bbox_extra_artists=bbox_extra_artists)
        if kwargs.get('svg'):
            plt.savefig('{}.svg'.format(filename),
                        bbox_inches='tight', transparent=True, bbox_extra_artists=bbox_extra_artists)
        if kwargs.get('png'):
            plt.savefig('{}.png'.format(filename),
                        bbox_inches='tight', transparent=True, bbox_extra_artists=bbox_extra_artists)

    return ax1
Exemplo n.º 25
0
    def construct_structure_attributes(self, doc: Crystal):

        structure_attributes = {}
        # from optimade StructureResourceAttributes
        structure_attributes["elements"] = doc.elems
        structure_attributes["nelements"] = len(doc.elems)

        concentration = get_concentration(doc._data,
                                          elements=doc.elems,
                                          include_end=True)
        structure_attributes["elements_ratios"] = concentration
        structure_attributes["chemical_formula_descriptive"] = doc.formula
        structure_attributes["chemical_formula_reduced"] = doc.formula
        structure_attributes["chemical_formula_hill"] = None

        sorted_stoich = sorted(doc.stoichiometry,
                               key=lambda x: x[1],
                               reverse=True)
        gen = anonymous_element_generator()
        for ind, elem in enumerate(sorted_stoich):
            elem[0] = next(gen)

        structure_attributes[
            "chemical_formula_anonymous"] = get_formula_from_stoich(
                doc.stoichiometry,
                elements=[elem[0] for elem in sorted_stoich])
        structure_attributes["dimension_types"] = [1, 1, 1]
        structure_attributes["nperiodic_dimensions"] = 3
        structure_attributes["lattice_vectors"] = doc.lattice_cart
        structure_attributes["lattice_abc"] = doc.lattice_abc
        structure_attributes["cell_volume"] = cart2volume(doc.lattice_cart)
        structure_attributes["fractional_site_positions"] = doc.positions_frac
        structure_attributes["cartesian_site_positions"] = doc.positions_abs
        structure_attributes["nsites"] = doc.num_atoms
        structure_attributes["species_at_sites"] = doc.atom_types

        species = []
        for ind, atom in enumerate(doc.elems):
            species.append(
                Species(name=atom,
                        chemical_symbols=[atom],
                        concentration=[1.0]))

            structure_attributes["species"] = species
        structure_attributes["assemblies"] = None
        structure_attributes["structure_features"] = []

        # from optimade EntryResourceAttributes
        if "text_id" not in doc._data:
            structure_attributes["local_id"] = " ".join([
                self.wlines[random.randint(0, self.num_words - 1)].strip(),
                self.nlines[random.randint(0, self.num_nouns - 1)].strip(),
            ])
        else:
            structure_attributes["local_id"] = " ".join(doc._data["text_id"])
        structure_attributes["last_modified"] = datetime.datetime.now()
        if "_id" in doc._data:
            structure_attributes["immutable_id"] = str(doc._data["_id"])
        else:
            structure_attributes["immutable_id"] = str(
                bson.objectid.ObjectId())
        # if "date" in doc._data:
        # date = [int(val) for val in doc._data["date"].split("-")]
        # structure_attributes["date"] = datetime.date(
        # year=date[-1], month=date[1], day=date[0]
        # )

        # from matador extensions
        structure_attributes[
            "dft_parameters"] = self.construct_dft_hamiltonian(doc)
        structure_attributes["submitter"] = self.construct_submitter(doc)
        structure_attributes["thermodynamics"] = self.construct_thermodynamics(
            doc)
        structure_attributes["space_group"] = self.construct_spacegroup(doc)
        structure_attributes["calculator"] = self.construct_calculator(doc)

        structure_attributes["stress_tensor"] = doc._data.get("stress")
        structure_attributes["stress"] = doc._data["pressure"]
        structure_attributes["forces"] = doc._data.get("forces")
        structure_attributes["max_force_on_atom"] = doc._data.get(
            "max_force_on_atom")

        return MatadorStructureResourceAttributes(**structure_attributes)
Exemplo n.º 26
0
def plot_ternary_hull(hull,
                      axis=None,
                      show=True,
                      plot_points=True,
                      hull_cutoff=None,
                      fig_height=None,
                      label_cutoff=None,
                      label_corners=True,
                      expecting_cbar=True,
                      labels=None,
                      plot_fname=None,
                      hull_dist_unit="meV",
                      efmap=None,
                      sampmap=None,
                      capmap=None,
                      pathways=False,
                      **kwargs):
    """ Plot calculated ternary hull as a 2D projection.

    Parameters:
        hull (matador.hull.QueryConvexHull): matador hull object.

    Keyword arguments:
        axis (matplotlib.axes.Axes): matplotlib axis object on which to plot.
        show (bool): whether or not to show plot in X window.
        plot_points (bool): whether or not to plot each structure as a point.
        label_cutoff (float/:obj:`tuple` of :obj:`float`): draw labels less than or
            between these distances form the hull, also read from hull.args.
        expecting_cbar (bool): whether or not to space out the plot to preserve
            aspect ratio if a colourbar is present.
        labels (bool): whether or not to label on-hull structures
        label_corners (bool): whether or not to put axis labels on corners or edges.
        hull_dist_unit (str): either "eV" or "meV",
        png/pdf/svg (bool): whether or not to write the plot to a file.
        plot_fname (str): filename to write plot to.
        efmap (bool): plot heatmap of formation energy,
        sampmap (bool): plot heatmap showing sampling density,
        capmap (bool): plot heatmap showing gravimetric capacity.
        pathways (bool): plot the pathway from the starting electrode to active ion.

    Returns:
        matplotlib.axes.Axes: matplotlib axis with plot.

    """
    import ternary
    import matplotlib.pyplot as plt
    import matplotlib.colors as colours
    from matador.utils.chem_utils import get_generic_grav_capacity

    plt.rcParams['axes.linewidth'] = 0
    plt.rcParams['xtick.major.size'] = 0
    plt.rcParams['ytick.major.size'] = 0
    plt.rcParams['xtick.minor.size'] = 0
    plt.rcParams['ytick.minor.size'] = 0

    if efmap is None:
        efmap = hull.args.get('efmap')
    if sampmap is None:
        sampmap = hull.args.get('sampmap')
    if capmap is None:
        capmap = hull.args.get('capmap')
    if pathways is None:
        pathways = hull.args.get('pathways')

    if labels is None:
        labels = hull.args.get('labels')
    if label_cutoff is None:
        label_cutoff = hull.args.get('label_cutoff')
        if label_cutoff is None:
            label_cutoff = 0
    else:
        labels = True

    if hull_cutoff is None and hull.hull_cutoff is None:
        hull_cutoff = 0
    else:
        hull_cutoff = hull.hull_cutoff

    print('Plotting ternary hull...')
    if capmap or efmap:
        scale = 100
    elif sampmap:
        scale = 20
    else:
        scale = 1

    if axis is not None:
        fig, ax = ternary.figure(scale=scale, ax=axis)
    else:
        fig, ax = ternary.figure(scale=scale)

    # maintain aspect ratio of triangle
    if fig_height is None:
        _user_height = plt.rcParams.get("figure.figsize", (8, 6))[0]
    else:
        _user_height = fig_height
    if capmap or efmap or sampmap:
        fig.set_size_inches(_user_height, 5 / 8 * _user_height)
    elif not expecting_cbar:
        fig.set_size_inches(_user_height, _user_height)
    else:
        fig.set_size_inches(_user_height, 5 / 6.67 * _user_height)

    ax.boundary(linewidth=2.0, zorder=99)
    ax.clear_matplotlib_ticks()

    chempot_labels = [
        get_formula_from_stoich(get_stoich_from_formula(species, sort=False),
                                sort=False,
                                tex=True) for species in hull.species
    ]

    ax.gridlines(color='black', multiple=scale * 0.1, linewidth=0.5)
    ticks = [float(val) for val in np.linspace(0, 1, 6)]
    if label_corners:
        # remove 0 and 1 ticks when labelling corners
        ticks = ticks[1:-1]
        ax.left_corner_label(chempot_labels[2], fontsize='large')
        ax.right_corner_label(chempot_labels[0], fontsize='large')
        ax.top_corner_label(chempot_labels[1], fontsize='large', offset=0.16)
    else:
        ax.left_axis_label(chempot_labels[2], fontsize='large', offset=0.12)
        ax.right_axis_label(chempot_labels[1], fontsize='large', offset=0.12)
        ax.bottom_axis_label(chempot_labels[0], fontsize='large', offset=0.08)
        ax.set_title('-'.join(['{}'.format(label)
                               for label in chempot_labels]),
                     fontsize='large',
                     y=1.02)

    ax.ticks(axis='lbr',
             linewidth=1,
             offset=0.025,
             fontsize='small',
             locations=(scale * np.asarray(ticks)).tolist(),
             ticks=ticks,
             tick_formats='%.1f')

    concs = np.zeros((len(hull.structures), 3))
    concs[:, :-1] = hull.structures[:, :-1]
    for i in range(len(concs)):
        # set third triangular coordinate
        concs[i, -1] = 1 - concs[i, 0] - concs[i, 1]

    stable = concs[np.where(hull.hull_dist <= 0 + EPS)]

    # sort by hull distances so things are plotting the right order
    concs = concs[np.argsort(hull.hull_dist)].tolist()
    hull_dist = np.sort(hull.hull_dist)

    filtered_concs = []
    filtered_hull_dists = []
    for ind, conc in enumerate(concs):
        if conc not in filtered_concs:
            if hull_dist[ind] <= hull.hull_cutoff or (hull.hull_cutoff == 0 and
                                                      hull_dist[ind] < 0.1):
                filtered_concs.append(conc)
                filtered_hull_dists.append(hull_dist[ind])
    if hull.args.get('debug'):
        print('Trying to plot {} points...'.format(len(filtered_concs)))

    concs = np.asarray(filtered_concs)
    hull_dist = np.asarray(filtered_hull_dists)

    min_cut = 0.0
    max_cut = 0.2

    if hull_dist_unit.lower() == "mev":
        hull_dist *= 1000
        min_cut *= 1000
        max_cut *= 1000

    hull.colours = list(plt.rcParams['axes.prop_cycle'].by_key()['color'])
    hull.default_cmap_list = get_linear_cmap(hull.colours[1:4], list_only=True)
    hull.default_cmap = get_linear_cmap(hull.colours[1:4], list_only=False)
    n_colours = len(hull.default_cmap_list)
    colours_hull = hull.default_cmap_list

    cmap = hull.default_cmap
    cmap_full = plt.cm.get_cmap('Pastel2')
    pastel_cmap = colours.LinearSegmentedColormap.from_list(
        'Pastel2', cmap_full.colors)

    for plane in hull.convex_hull.planes:
        plane.append(plane[0])
        plane = np.asarray(plane)
        ax.plot(scale * plane, c=hull.colours[0], lw=1.5, alpha=1, zorder=98)

    if pathways:
        for phase in stable:
            if phase[0] == 0 and phase[1] != 0 and phase[2] != 0:
                ax.plot([scale * phase, [scale, 0, 0]],
                        c='r',
                        alpha=0.2,
                        lw=6,
                        zorder=99)

    # add points
    if plot_points:
        colours_list = []
        colour_metric = hull_dist
        for i, _ in enumerate(colour_metric):
            if hull_dist[i] >= max_cut:
                colours_list.append(n_colours - 1)
            elif hull_dist[i] <= min_cut:
                colours_list.append(0)
            else:
                colours_list.append(
                    int((n_colours - 1) * (hull_dist[i] / max_cut)))
        colours_list = np.asarray(colours_list)
        ax.scatter(scale * stable,
                   marker='o',
                   color=hull.colours[1],
                   edgecolors='black',
                   zorder=9999999,
                   s=150,
                   lw=1.5)
        ax.scatter(scale * concs,
                   colormap=cmap,
                   colorbar=True,
                   cbarlabel='Distance from hull ({}eV/atom)'.format(
                       "m" if hull_dist_unit.lower() == "mev" else ""),
                   c=colour_metric,
                   vmax=max_cut,
                   vmin=min_cut,
                   zorder=1000,
                   s=40,
                   alpha=0)
        for i, _ in enumerate(concs):
            ax.scatter(scale * concs[i].reshape(1, 3),
                       color=colours_hull[colours_list[i]],
                       marker='o',
                       zorder=10000 - colours_list[i],
                       s=70 * (1 - float(colours_list[i]) / n_colours) + 15,
                       lw=1,
                       edgecolors='black')

    # add colourmaps
    if capmap:
        capacities = dict()
        from ternary.helpers import simplex_iterator
        for (i, j, k) in simplex_iterator(scale):
            capacities[(i, j, k)] = get_generic_grav_capacity([
                float(i) / scale,
                float(j) / scale,
                float(scale - i - j) / scale
            ], hull.species)
        ax.heatmap(capacities,
                   style="hexagonal",
                   cbarlabel='Gravimetric capacity (mAh/g)',
                   vmin=0,
                   vmax=3000,
                   cmap=pastel_cmap)
    elif efmap:
        energies = dict()
        fake_structures = []
        from ternary.helpers import simplex_iterator
        for (i, j, k) in simplex_iterator(scale):
            fake_structures.append([float(i) / scale, float(j) / scale, 0.0])
        fake_structures = np.asarray(fake_structures)
        plane_energies = hull.get_hull_distances(fake_structures,
                                                 precompute=False)
        ind = 0
        for (i, j, k) in simplex_iterator(scale):
            energies[(i, j, k)] = -1 * plane_energies[ind]
            ind += 1
        if isinstance(efmap, str):
            efmap = efmap
        else:
            efmap = 'BuPu_r'
        ax.heatmap(energies,
                   style="hexagonal",
                   cbarlabel='Formation energy (eV/atom)',
                   vmax=0,
                   cmap=efmap)
    elif sampmap:
        sampling = dict()
        from ternary.helpers import simplex_iterator
        eps = 1.0 / float(scale)
        for (i, j, k) in simplex_iterator(scale):
            sampling[(i, j, k)] = np.size(
                np.where((concs[:, 0] <= float(i) / scale + eps) *
                         (concs[:, 0] >= float(i) / scale - eps) *
                         (concs[:, 1] <= float(j) / scale + eps) *
                         (concs[:, 1] >= float(j) / scale - eps) *
                         (concs[:, 2] <= float(k) / scale + eps) *
                         (concs[:, 2] >= float(k) / scale - eps)))
        ax.heatmap(sampling,
                   style="hexagonal",
                   cbarlabel='Number of structures',
                   cmap='afmhot')

    # add labels
    if labels:
        label_cursor = _get_hull_labels(hull, label_cutoff=label_cutoff)
        if len(label_cursor) == 1:
            label_coords = [[0.25, 0.5]]
        else:
            label_coords = [[
                0.1 + (val - 0.5) * 0.3, val
            ] for val in np.linspace(0.5, 0.8,
                                     int(round(len(label_cursor) / 2.) + 1))]
            label_coords += [[0.9 - (val - 0.5) * 0.3, val + 0.2]
                             for val in np.linspace(
                                 0.5, 0.8, int(round(len(label_cursor) / 2.)))]
        from matador.utils.hull_utils import barycentric2cart
        for ind, doc in enumerate(label_cursor):
            conc = np.asarray(doc['concentration'] +
                              [1 - sum(doc['concentration'])])
            formula = get_formula_from_stoich(doc['stoichiometry'],
                                              sort=False,
                                              tex=True,
                                              latex_sub_style=r'\mathregular',
                                              elements=hull.species)
            arrowprops = dict(arrowstyle="-|>",
                              color='k',
                              lw=2,
                              alpha=0.5,
                              zorder=1,
                              shrinkA=2,
                              shrinkB=4)
            cart = barycentric2cart([doc['concentration'] + [0]])[0][:2]
            min_dist = 1e20
            closest_label = 0
            for coord_ind, coord in enumerate(label_coords):
                dist = np.sqrt((cart[0] - coord[0])**2 +
                               (cart[1] - coord[1])**2)
                if dist < min_dist:
                    min_dist = dist
                    closest_label = coord_ind
            ax.annotate(
                formula,
                scale * conc,
                textcoords='data',
                xytext=[scale * val for val in label_coords[closest_label]],
                ha='right',
                va='bottom',
                arrowprops=arrowprops)
            del label_coords[closest_label]

    plt.tight_layout(w_pad=0.2)
    # important for retaining labels if exporting to PDF
    # see https://github.com/marcharper/python-ternary/issues/36
    ax._redraw_labels()  # noqa

    if hull.savefig:
        fname = plot_fname or ''.join(hull.species) + '_hull'
        for ext in SAVE_EXTS:
            if hull.args.get(ext) or kwargs.get(ext):
                plt.savefig('{}.{}'.format(fname, ext),
                            bbox_inches='tight',
                            transparent=True)
                print('Wrote {}.{}'.format(fname, ext))
    elif show:
        print('Showing plot...')
        plt.show()

    return ax
Exemplo n.º 27
0
def query2files(cursor,
                dirname=None,
                max_files=10000,
                top=None,
                prefix=None,
                cell=None,
                param=None,
                res=None,
                pdb=None,
                json=None,
                xsf=None,
                markdown=True,
                latex=False,
                subcmd=None,
                argstr=None,
                **kwargs):
    """ Many-to-many convenience function for many structures being written to
    many file types.

    Parameters:
        cursor (:obj:`list` of :obj:`dict`/:class:`AtomicSwapper`): list of matador dictionaries to write out.

    Keyword arguments:
        dirname (str): the folder to save the results into. Will be created if non-existent.
            Will have integer appended to it if already existing.
        max_files (int): if the number of files to be written exceeds this number, then raise RuntimeError.
        **kwargs (dict): dictionary of {filetype: bool(whether to write)}. Accepted file types
            are cell, param, res, pdb, json, xsf, markdown and latex.

    """
    multiple_files = any((cell, param, res, pdb, xsf))
    prefix = prefix + '-' if prefix is not None else ''

    if isinstance(cursor, AtomicSwapper):
        cursor = cursor.cursor
        subcmd = "swaps"

    if subcmd in ['polish', 'swaps']:
        info = False
        hash_dupe = False
    else:
        info = True
        hash_dupe = False

    if isinstance(cursor, list):
        num = len(cursor)
    else:
        num = cursor.count()

    if top is not None:
        if top < num:
            num = top

    num_files = num * sum(1 for ext in [cell, param, res, pdb, xsf] if ext)

    if multiple_files:
        print('Intending to write', num, 'structures to file...')
        if num_files > max_files:
            raise RuntimeError(
                "Not writing {} files as it exceeds argument `max_files` limit of {}"
                .format(num_files, max_files))

    if dirname is None:
        dirname = generate_relevant_path(subcmd=subcmd, **kwargs)

    _dir = False
    dir_counter = 0
    # postfix integer on end of directory name if it exists
    while not _dir:
        if dir_counter != 0:
            directory = dirname + str(dir_counter)
        else:
            directory = dirname
        if not os.path.isdir(directory):
            os.makedirs(directory)
            _dir = True
        else:
            dir_counter += 1

    for _, doc in enumerate(cursor[:num]):
        # generate an appropriate filename for the structure
        root_source = get_root_source(doc)

        if '_swapped_stoichiometry' in doc:
            formula = get_formula_from_stoich(doc['_swapped_stoichiometry'])
        else:
            formula = get_formula_from_stoich(doc['stoichiometry'])

        if subcmd == 'swaps':
            root_source = root_source.replace('-swap-', '-')

        name = root_source

        if 'OQMD ' in root_source:
            name = '{formula}-OQMD_{src}'.format(
                formula=formula, src=root_source.split(' ')[-1])
        elif 'mp-' in root_source:
            name = '{formula}-MP_{src}'.format(formula=formula,
                                               src=root_source.split('-')[-1])
        if 'icsd' in doc and 'CollCode' not in name:
            name += '-CollCode{}'.format(doc['icsd'])
        else:
            pf_id = None
            for source in doc['source']:
                if 'pf-' in source:
                    pf_id = source.split('-')[-1]
                    break
            else:
                if 'pf_ids' in doc:
                    pf_id = doc['pf_ids'][0]
            if pf_id is not None:
                name += '-PF-{}'.format(pf_id)

        # if swaps, prepend new composition
        if subcmd == 'swaps':
            new_formula = get_formula_from_stoich(get_stoich(
                doc['atom_types']))
            name = '{}-swap-{}'.format(new_formula, name)

        path = "{directory}/{prefix}{name}".format(directory=directory,
                                                   prefix=prefix,
                                                   name=name)

        if param:
            doc2param(doc, path, hash_dupe=hash_dupe)
        if cell:
            doc2cell(doc, path, hash_dupe=hash_dupe)
        if res:
            doc2res(doc, path, info=info, hash_dupe=hash_dupe)
        if json:
            doc2json(doc, path, hash_dupe=hash_dupe)
        if pdb:
            doc2pdb(doc, path, hash_dupe=hash_dupe)
        if xsf:
            doc2xsf(doc, path)

    hull = subcmd in ['hull', 'voltage']
    if isinstance(cursor, pm.cursor.Cursor):
        cursor.rewind()
    md_path = "{directory}/{directory}.md".format(directory=directory)
    md_kwargs = {}
    md_kwargs.update(kwargs)
    md_kwargs.update({
        'markdown': True,
        'latex': False,
        'argstr': argstr,
        'hull': hull
    })
    md_string = display_results(cursor, **md_kwargs)
    with open(md_path, 'w') as f:
        f.write(md_string)

    if latex:
        if isinstance(cursor, pm.cursor.Cursor):
            cursor.rewind()
        tex_path = "{directory}/{directory}.tex".format(directory=directory)
        print('Writing LaTeX file', tex_path + '...')
        tex_kwargs = {}
        tex_kwargs.update(kwargs)
        tex_kwargs.update({
            'latex': True,
            'markdown': False,
            'argstr': argstr,
            'hull': hull
        })
        tex_string = display_results(cursor, **tex_kwargs)
        with open(tex_path, 'w') as f:
            f.write(tex_string)

    print('Done!')
Exemplo n.º 28
0
def plot_2d_hull(hull,
                 ax=None,
                 show=True,
                 plot_points=True,
                 plot_tie_line=True,
                 plot_hull_points=True,
                 labels=None,
                 label_cutoff=None,
                 colour_by_source=False,
                 sources=None,
                 hull_label=None,
                 source_labels=None,
                 title=True,
                 plot_fname=None,
                 show_cbar=True,
                 label_offset=(1.15, 0.05),
                 eform_limits=None,
                 legend_kwargs=None,
                 **kwargs):
    """ Plot calculated hull, returning ax and fig objects for further editing.

    Parameters:
        hull (matador.hull.QueryConvexHull): matador hull object.

    Keyword arguments:
        ax (matplotlib.axes.Axes): an existing axis on which to plot,
        show (bool): whether or not to display the plot in an X window,
        plot_points (bool): whether or not to display off-hull structures,
        plot_hull_points (bool): whether or not to display on-hull structures,
        labels (bool): whether to label formulae of hull structures, also read from
            hull.args.
        label_cutoff (float/:obj:`tuple` of :obj:`float`): draw labels less than or
            between these distances form the hull, also read from hull.args.
        colour_by_source (bool): plot and label points by their sources
        alpha (float): alpha value of points when colour_by_source is True
        sources (list): list of possible provenances to colour when colour_by_source
            is True (others will be grey)
        title (str/bool): whether to include a plot title.
        png/pdf/svg (bool): whether or not to write the plot to a file.
        plot_fname (str): filename to write plot to, without file extension.

    Returns:
        matplotlib.axes.Axes: matplotlib axis with plot.

    """
    import matplotlib.pyplot as plt
    import matplotlib.colors as colours

    if ax is None:
        fig = plt.figure()
        ax = fig.add_subplot(111)

    if not hasattr(hull, 'colours'):
        hull.colours = list(plt.rcParams['axes.prop_cycle'].by_key()['color'])
    hull.default_cmap_list = get_linear_cmap(hull.colours[1:4], list_only=True)
    hull.default_cmap = get_linear_cmap(hull.colours[1:4], list_only=False)

    if labels is None:
        labels = hull.args.get('labels', False)
    if label_cutoff is None:
        label_cutoff = hull.args.get('label_cutoff')

    scale = 1
    scatter = []
    chempot_labels = [
        get_formula_from_stoich(get_stoich_from_formula(species, sort=False),
                                tex=True) for species in hull.species
    ]
    tie_line = hull.convex_hull.points[hull.convex_hull.vertices]

    # plot hull structures
    if plot_hull_points:
        ax.scatter(tie_line[:, 0],
                   tie_line[:, 1],
                   c=hull.colours[1],
                   marker='o',
                   zorder=99999,
                   edgecolor='k',
                   s=scale * 40,
                   lw=1.5)
        if plot_tie_line:
            ax.plot(np.sort(tie_line[:, 0]),
                    tie_line[np.argsort(tie_line[:, 0]), 1],
                    c=hull.colours[0],
                    zorder=1,
                    label=hull_label,
                    marker='o',
                    markerfacecolor=hull.colours[0],
                    markeredgecolor='k',
                    markeredgewidth=1.5,
                    markersize=np.sqrt(scale * 40))
    if plot_tie_line:
        ax.plot(np.sort(tie_line[:, 0]),
                tie_line[np.argsort(tie_line[:, 0]), 1],
                c=hull.colours[0],
                zorder=1,
                label=hull_label,
                markersize=0)

    if hull.hull_cutoff > 0:
        ax.plot(np.sort(tie_line[:, 0]),
                tie_line[np.argsort(tie_line[:, 0]), 1] + hull.hull_cutoff,
                '--',
                c=hull.colours[1],
                alpha=0.5,
                zorder=1,
                label='')

    # annotate hull structures
    if labels or label_cutoff is not None:
        label_cursor = _get_hull_labels(hull,
                                        num_species=2,
                                        label_cutoff=label_cutoff)
        already_labelled = []
        for ind, doc in enumerate(label_cursor):
            formula = get_formula_from_stoich(doc['stoichiometry'], sort=True)
            if formula not in already_labelled:
                arrowprops = dict(arrowstyle="-|>",
                                  lw=2,
                                  alpha=1,
                                  zorder=1,
                                  shrinkA=2,
                                  shrinkB=4)
                min_comp = tie_line[np.argmin(tie_line[:, 1]), 0]
                e_f = label_cursor[ind]['formation_' + str(hull.energy_key)]
                conc = label_cursor[ind]['concentration'][0]
                if conc < min_comp:
                    position = (0.8 * conc,
                                label_offset[0] * (e_f - label_offset[1]))
                elif label_cursor[ind]['concentration'][0] == min_comp:
                    position = (conc,
                                label_offset[0] * (e_f - label_offset[1]))
                else:
                    position = (min(1.1 * conc + 0.15, 0.95),
                                label_offset[0] * (e_f - label_offset[1]))
                ax.annotate(get_formula_from_stoich(
                    doc['stoichiometry'],
                    latex_sub_style=r'\mathregular',
                    tex=True,
                    elements=hull.species,
                    sort=False),
                            xy=(conc, e_f),
                            xytext=position,
                            textcoords='data',
                            ha='right',
                            va='bottom',
                            arrowprops=arrowprops,
                            zorder=1)
                already_labelled.append(formula)

    # points for off hull structures; we either colour by source or by energy
    if plot_points and not colour_by_source:

        if hull.hull_cutoff == 0:
            # if no specified hull cutoff, ignore labels and colour by hull distance
            cmap = hull.default_cmap
            if plot_points:
                scatter = ax.scatter(
                    hull.structures[np.argsort(hull.hull_dist), 0][::-1],
                    hull.structures[np.argsort(hull.hull_dist), -1][::-1],
                    s=scale * 40,
                    c=np.sort(hull.hull_dist)[::-1],
                    zorder=10000,
                    cmap=cmap,
                    norm=colours.LogNorm(0.02, 2))

                if show_cbar:
                    cbar = plt.colorbar(
                        scatter,
                        aspect=30,
                        pad=0.02,
                        ticks=[0, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28])
                    cbar.ax.tick_params(length=0)
                    cbar.ax.set_yticklabels(
                        [0, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28])
                    cbar.ax.yaxis.set_ticks_position('right')
                    cbar.ax.set_frame_on(False)
                    cbar.outline.set_visible(False)
                    cbar.set_label('Distance from hull (eV/atom)')

        elif hull.hull_cutoff != 0:
            # if specified hull cutoff colour those below
            c = hull.colours[1]
            for ind in range(len(hull.structures)):
                if hull.hull_dist[
                        ind] <= hull.hull_cutoff or hull.hull_cutoff == 0:
                    if plot_points:
                        scatter.append(
                            ax.scatter(hull.structures[ind, 0],
                                       hull.structures[ind, 1],
                                       s=scale * 40,
                                       alpha=0.9,
                                       c=c,
                                       zorder=300))
            if plot_points:
                ax.scatter(hull.structures[1:-1, 0],
                           hull.structures[1:-1, 1],
                           s=scale * 30,
                           lw=0,
                           alpha=0.3,
                           c=hull.colours[-2],
                           edgecolor='k',
                           zorder=10)

    elif colour_by_source:
        _scatter_plot_by_source(hull,
                                ax,
                                scale,
                                kwargs,
                                sources=sources,
                                source_labels=source_labels,
                                plot_hull_points=plot_hull_points,
                                legend_kwargs=legend_kwargs)

    if eform_limits is None:
        eform_limits = (np.min(hull.structures[:, 1]),
                        np.max(hull.structures[:, 1]))
        lims = (-0.1 if eform_limits[0] >= 0 else 1.4 * eform_limits[0],
                eform_limits[1] if eform_limits[0] >= 0 else 0.1)
    else:
        lims = sorted(eform_limits)
    ax.set_ylim(lims)

    if isinstance(title, bool) and title:
        if hull._non_elemental:
            ax.set_title(
                r'({d[0]})$_\mathrm{{x}}$({d[1]})$_\mathrm{{1-x}}$'.format(
                    d=chempot_labels))
        else:
            ax.set_title(
                r'{d[0]}$_\mathrm{{x}}${d[1]}$_\mathrm{{1-x}}$'.format(
                    d=chempot_labels))
    elif isinstance(title, str) and title != '':
        ax.set_title(title)

    plt.locator_params(nbins=3)
    if hull._non_elemental:
        ax.set_xlabel(
            r'x in ({d[0]})$_\mathrm{{x}}$({d[1]})$_\mathrm{{1-x}}$'.format(
                d=chempot_labels))
    else:
        ax.set_xlabel(
            r'x in {d[0]}$_\mathrm{{x}}${d[1]}$_\mathrm{{1-x}}$'.format(
                d=chempot_labels))

    ax.grid(False)
    ax.set_xlim(-0.05, 1.05)
    ax.set_xticks([0, 0.25, 0.5, 0.75, 1])
    ax.set_xticklabels(ax.get_xticks())
    ax.set_ylabel('Formation energy (eV/atom)')

    if hull.savefig or any([kwargs.get(ext) for ext in SAVE_EXTS]):
        import os
        fname = plot_fname or ''.join(hull.species) + '_hull'
        for ext in SAVE_EXTS:
            if hull.args.get(ext) or kwargs.get(ext):
                fname_tmp = fname
                ind = 0
                while os.path.isfile('{}.{}'.format(fname_tmp, ext)):
                    ind += 1
                    fname_tmp = fname + str(ind)

                fname = fname_tmp
                plt.savefig('{}.{}'.format(fname, ext),
                            bbox_inches='tight',
                            transparent=True)
                print('Wrote {}.{}'.format(fname, ext))

    if show:
        plt.show()

    return ax
Exemplo n.º 29
0
def plot_voltage_curve(
    profiles: Union[List[VoltageProfile], VoltageProfile],
    ax=None,
    show: bool = False,
    labels: bool = False,
    savefig: Optional[str] = None,
    curve_labels: Optional[Union[str, List[str]]] = None,
    line_kwargs: Optional[Union[Dict, List[Dict]]] = None,
    expt: Optional[str] = None,
    expt_label: Optional[str] = None
):
    """ Plot voltage curve calculated for phase diagram.

    Parameters:
        profiles (list/VoltageProfile): list of/single voltage profile(s).

    Keyword arguments:
        ax (matplotlib.axes.Axes): an existing axis on which to plot.
        show (bool): whether to show plot in an X window.
        savefig (str): filename to use to save the plot.
        curve_labels (list): optional list of labels for the curves in
            the profiles list.
        line_kwargs (list or dict): parameters to pass to the curve plotter,
            if a list then the line kwargs will be passed to each line individually.
        expt (str): string to a filename of a csv Q, V to add to the plot.
        expt_label (str): label for any experimental profile passed to the plot.

    """
    import matplotlib.pyplot as plt

    if ax is None:
        fig = plt.figure()
        ax = fig.add_subplot(111)

    if not isinstance(profiles, list):
        profiles = [profiles]
    if curve_labels is not None and not isinstance(curve_labels, list):
        curve_labels = [curve_labels]
    if line_kwargs is not None and not isinstance(line_kwargs, list):
        line_kwargs = len(profiles) * [line_kwargs]

    if curve_labels is not None and len(curve_labels) != len(profiles):
        raise RuntimeError(
            "Wrong number of labels passed for number of profiles: {} vs {}"
            .format(len(curve_labels), len(profiles))
        )

    if line_kwargs is not None and len(line_kwargs) != len(profiles):
        raise RuntimeError(
            "Wrong number of line kwargs passed for number of profiles: {} vs {}"
            .format(len(line_kwargs), len(profiles))
        )

    dft_label = None
    if expt is not None:
        expt_data = np.loadtxt(expt, delimiter=',')
        if expt_label:
            ax.plot(expt_data[:, 0], expt_data[:, 1], c='k', lw=2, ls='-', label=expt_label)
        else:
            ax.plot(expt_data[:, 0], expt_data[:, 1], c='k', lw=2, ls='-', label='Experiment')

        if len(profiles) == 1:
            dft_label = 'DFT (this work)'

    for ind, profile in enumerate(profiles):
        if dft_label is None and curve_labels is None:
            stoich_label = get_formula_from_stoich(profile.starting_stoichiometry, tex=True)
        else:
            stoich_label = None

        _label = stoich_label if dft_label is None else dft_label
        if curve_labels is not None and len(curve_labels) > ind:
            _label = curve_labels[ind]

        _line_kwargs = {'c': list(plt.rcParams['axes.prop_cycle'].by_key()['color'])[ind+2]}
        if line_kwargs is not None:
            _line_kwargs.update(line_kwargs[ind])

        _add_voltage_curve(profile.capacities, profile.voltages, ax, label=_label, **_line_kwargs)

    if labels:
        if len(profiles) > 1:
            print("Only labelling first voltage profile.")
        for ind, reaction in enumerate(profiles[0].reactions):
            _labels = []
            for phase in reaction:
                if phase[0] is None or phase[0] == 1.0:
                    _label = ""
                else:
                    _label = "{:.1f} ".format(phase[0])
                _label += "{}".format(phase[1])
                _labels.append(_label)
            _label = '+'.join(_labels)
            _position = (profiles[0].capacities[ind], profiles[0].voltages[ind] + max(profiles[0].voltages)*0.01)
            ax.annotate(_label, xy=_position, textcoords="data", ha="center", zorder=9999)

    if expt or len(profiles) > 1:
        ax.legend()

    ax.set_ylabel('Voltage (V) vs {ion}$^+/${ion}'.format(ion=profile.active_ion))
    ax.set_xlabel('Gravimetric cap. (mAh/g)')

    _, end = ax.get_ylim()
    from matplotlib.ticker import MultipleLocator
    ax.yaxis.set_major_locator(MultipleLocator(0.2))
    ax.set_ylim(0, 1.1 * end)
    _, end = ax.get_xlim()
    ax.set_xlim(0, 1.1 * end)
    ax.grid(False)
    plt.tight_layout(pad=0.0, h_pad=1.0, w_pad=0.2)

    if savefig:
        plt.savefig(savefig)
        print('Wrote {}'.format(savefig))

    elif show:
        plt.show()

    return ax
Exemplo n.º 30
0
def get_convergence_data(structure_files, conv_parameter='cut_off_energy', species=None):
    """ Parse cutoff energy/kpt spacing convergence calculations from list of files.

    Parameters:
        structure_files (list): list of filenames.

    Keyword arguments:
        conv_parameter (str): field for convergence parameter.
        species (list): only include structures with these species included.

    """
    scraped_from_filename = None

    form_set = {get_formula_from_stoich(structure_files[structure][0]['stoichiometry']) for structure in structure_files}
    if len(form_set) == 1:
        chempot_mode = False
        single = True
        print('Working in single stoichiometry mode..')

    else:
        print('Searching for chemical potentials')
        chempot_mode = True
        single = False
        chempots_dict = defaultdict(dict)

    if conv_parameter == 'kpoints_mp_spacing':
        rounding = None
    else:
        rounding = None

    data = {}

    if chempot_mode:
        for key in structure_files:
            skip = False
            if species is not None:
                for elem in structure_files[key][0]['stoichiometry']:
                    if elem[0] not in species:
                        skip = True
            if skip:
                continue
            for doc in structure_files[key]:
                scraped_from_filename = None
                if not single and len(doc['stoichiometry']) == 1:
                    if conv_parameter == 'kpoints_mp_spacing':
                        scraped_from_filename = float(doc['source'][0].split('/')[-1].split('_')[-1].split('A')[0])

                    if scraped_from_filename is not None:
                        rounded_field = round(scraped_from_filename, rounding)
                    else:
                        rounded_field = round(doc[conv_parameter], rounding)
                    energy_key = 'total_energy_per_atom'
                    chempots_dict[str(rounded_field)][doc['atom_types'][0]] = doc[energy_key]

    elems = set()

    if chempot_mode:
        for value in chempots_dict:
            for elem in chempots_dict[value]:
                elems.add(elem)

        for value in chempots_dict:
            for elem in elems:
                if elem not in chempots_dict[value]:
                    print('WARNING: {} chemical potential missing at {} = {} eV, skipping this value.'
                          .format(elem, conv_parameter, value))

    for key in structure_files:
        skip = False
        if species is not None:
            for elem in structure_files[key][0]['stoichiometry']:
                if elem[0] not in species:
                    skip = True
        if skip:
            continue

        data[key] = {}
        data[key][conv_parameter] = defaultdict(list)

        for doc in structure_files[key]:
            if conv_parameter == 'kpoints_mp_spacing':
                scraped_from_filename = float(doc['source'][0].split('/')[-1].split('_')[-1].split('A')[0])
            try:
                doc['formation_energy_per_atom'] = doc['total_energy_per_atom']
                if scraped_from_filename is not None:
                    rounded_field = round(scraped_from_filename, rounding)
                else:
                    rounded_field = round(doc[conv_parameter], rounding)
                if chempot_mode and len(doc['stoichiometry']) > 1:
                    for atom in doc['atom_types']:
                        doc['formation_energy_per_atom'] -= chempots_dict[str(rounded_field)][atom] / len(doc['atom_types'])

                data[key][conv_parameter][conv_parameter].append(rounded_field)
                data[key]['stoichiometry'] = doc['stoichiometry']
                data[key][conv_parameter]['formation_energy_per_atom'].append(doc['formation_energy_per_atom'])

                if 'forces' in doc:
                    data[key][conv_parameter]['forces'].append(np.sqrt(np.sum(np.asarray(doc['forces'])**2, axis=-1)))

            except Exception:
                print_exc()
                print('Error with {}'.format(key))

    if conv_parameter == 'kpoints_mp_spacing':
        reverse = True
    else:
        reverse = False

    for key in data:
        inds = [ind for ind, _ in sorted(enumerate(data[key][conv_parameter][conv_parameter]),
                                         key=lambda x: x[1], reverse=reverse)]
        for field in data[key][conv_parameter]:
            if field != 'stoichiometry':
                data[key][conv_parameter][field] = [data[key][conv_parameter][field][ind] for ind in inds]

    return data