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
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))
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
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)
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)
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
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')
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"]))
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
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) * '─')
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)
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
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
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
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)
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])
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
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,
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
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)
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()
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)
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()
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
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)
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
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!')
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
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
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