def dataplot(comps, phases, conds, datasets, ax=None, plot_kwargs=None, tieline_plot_kwargs=None): """ Plot datapoints corresponding to the components, phases, and conditions. Parameters ---------- comps : list Names of components to consider in the calculation. phases : [] Names of phases to consider in the calculation. conds : dict Maps StateVariables to values and/or iterables of values. datasets : PickleableTinyDB ax : matplotlib.Axes Default axes used if not specified. plot_kwargs : dict Additional keyword arguments to pass to the matplotlib plot function for points tieline_plot_kwargs : dict Additional keyword arguments to pass to the matplotlib plot function for tielines Returns ------- matplotlib.Axes A plot of phase equilibria points as a figure Examples -------- >>> from espei.datasets import load_datasets, recursive_glob >>> from espei.plot import dataplot >>> datasets = load_datasets(recursive_glob('.', '*.json')) >>> my_phases = ['BCC_A2', 'CUMG2', 'FCC_A1', 'LAVES_C15', 'LIQUID'] >>> my_components = ['CU', 'MG' 'VA'] >>> conditions = {v.P: 101325, v.T: (500, 1000, 10), v.X('MG'): (0, 1, 0.01)} >>> dataplot(my_components, my_phases, conditions, datasets) """ indep_comps = [ key for key, value in conds.items() if isinstance(key, v.Composition) and len(np.atleast_1d(value)) > 1 ] indep_pots = [ key for key, value in conds.items() if ((key == v.T) or (key == v.P)) and len(np.atleast_1d(value)) > 1 ] plot_kwargs = plot_kwargs or {} phases = sorted(phases) # determine what the type of plot will be if len(indep_comps) == 1 and len(indep_pots) == 1: projection = None elif len(indep_comps) == 2 and len(indep_pots) == 0: projection = 'triangular' else: raise ValueError( 'The eqplot projection is not defined and cannot be autodetected. There are {} independent compositions and {} indepedent potentials.' .format(len(indep_comps), len(indep_pots))) if projection is None: x = indep_comps[0].species y = indep_pots[0] elif projection == 'triangular': x = indep_comps[0].species y = indep_comps[1].species # set up plot if not done already if ax is None: ax = plt.gca(projection=projection) box = ax.get_position() ax.set_position([box.x0, box.y0, box.width * 0.8, box.height]) ax.tick_params(axis='both', which='major', labelsize=14) ax.grid(True) plot_title = '-'.join([ component.title() for component in sorted(comps) if component != 'VA' ]) ax.set_title(plot_title, fontsize=20) ax.set_xlabel('X({})'.format(x), labelpad=15, fontsize=20) ax.set_xlim((0, 1)) if projection is None: ax.set_ylabel(plot_mapping.get(str(y), y), fontsize=20) elif projection == 'triangular': ax.set_ylabel('X({})'.format(y), labelpad=15, fontsize=20) ax.set_ylim((0, 1)) ax.yaxis.label.set_rotation(60) # Here we adjust the x coordinate of the ylabel. # We make it reasonably comparable to the position of the xlabel from the xaxis # As the figure size gets very large, the label approaches ~0.55 on the yaxis # 0.55*cos(60 deg)=0.275, so that is the xcoord we are approaching. ax.yaxis.label.set_va('baseline') fig_x_size = ax.figure.get_size_inches()[0] y_label_offset = 1 / fig_x_size ax.yaxis.set_label_coords(x=(0.275 - y_label_offset), y=0.5) output = 'ZPF' # TODO: used to include VA. Should this be added by default. Can't determine presence of VA in eq. # Techincally, VA should not be present in any phase equilibria. desired_data = datasets.search( (tinydb.where('output') == output) & (tinydb.where('components').test( lambda x: set(x).issubset(comps + ['VA']))) & (tinydb.where('phases').test( lambda x: len(set(phases).intersection(x)) > 0))) # get all the possible references from the data and create the bibliography map bib_reference_keys = sorted( list({entry['reference'] for entry in desired_data})) symbol_map = bib_marker_map(bib_reference_keys) # The above handled the phases as in the equilibrium, but there may be # phases that are in the datasets but not in the equilibrium diagram that # we would like to plot point for (they need color maps). # To keep consistent colors with the equilibrium diagram, we will append # the new phases from the datasets to the existing phases in the equilibrium # calculation. data_phases = set() for entry in desired_data: data_phases.update(set(entry['phases'])) new_phases = sorted(list(data_phases.difference(set(phases)))) phases.extend(new_phases) legend_handles, phase_color_map = phase_legend(phases) if projection is None: # TODO: There are lot of ways this could break in multi-component situations # plot x vs. T y = 'T' # handle plotting kwargs scatter_kwargs = {'markersize': 6, 'markeredgewidth': 1} # raise warnings if any of the aliased versions of the default values are used possible_aliases = [('markersize', 'ms'), ('markeredgewidth', 'mew')] for actual_arg, aliased_arg in possible_aliases: if aliased_arg in plot_kwargs: warnings.warn( "'{0}' passed as plotting keyword argument to dataplot, but the alias '{1}' is already set to '{2}'. Use the full version of the keyword argument '{1}' to override the default." .format(aliased_arg, actual_arg, scatter_kwargs.get(actual_arg))) scatter_kwargs.update(plot_kwargs) eq_dict = ravel_zpf_values(desired_data, [x]) # two phase updated_tieline_plot_kwargs = {'linewidth': 1, 'color': 'k'} if tieline_plot_kwargs is not None: updated_tieline_plot_kwargs.update(tieline_plot_kwargs) for eq in eq_dict.get(2, []): # list of things in equilibrium # plot the scatter points for the right phases x_points, y_points = [], [] for phase_name, comp_dict, ref_key in eq: sym_ref = symbol_map[ref_key] x_val, y_val = comp_dict[x], comp_dict[y] if x_val is not None and y_val is not None: ax.plot(x_val, y_val, label=sym_ref['formatted'], fillstyle=sym_ref['markers']['fillstyle'], marker=sym_ref['markers']['marker'], linestyle='', color=phase_color_map[phase_name], **scatter_kwargs) x_points.append(x_val) y_points.append(y_val) # plot the tielines if all([ xx is not None and yy is not None for xx, yy in zip(x_points, y_points) ]): ax.plot(x_points, y_points, **updated_tieline_plot_kwargs) elif projection == 'triangular': scatter_kwargs = {'markersize': 4, 'markeredgewidth': 0.4} # raise warnings if any of the aliased versions of the default values are used possible_aliases = [('markersize', 'ms'), ('markeredgewidth', 'mew')] for actual_arg, aliased_arg in possible_aliases: if aliased_arg in plot_kwargs: warnings.warn( "'{0}' passed as plotting keyword argument to dataplot, but the alias '{1}' is already set to '{2}'. Use the full version of the keyword argument '{1}' to override the default." .format(aliased_arg, actual_arg, scatter_kwargs.get(actual_arg))) scatter_kwargs.update(plot_kwargs) eq_dict = ravel_zpf_values(desired_data, [x, y], {'T': conds[v.T]}) # two phase updated_tieline_plot_kwargs = {'linewidth': 1, 'color': 'k'} if tieline_plot_kwargs is not None: updated_tieline_plot_kwargs.update(tieline_plot_kwargs) for eq in eq_dict.get(2, []): # list of things in equilibrium # plot the scatter points for the right phases x_points, y_points = [], [] for phase_name, comp_dict, ref_key in eq: sym_ref = symbol_map[ref_key] x_val, y_val = comp_dict[x], comp_dict[y] if x_val is not None and y_val is not None: ax.plot(x_val, y_val, label=sym_ref['formatted'], fillstyle=sym_ref['markers']['fillstyle'], marker=sym_ref['markers']['marker'], linestyle='', color=phase_color_map[phase_name], **scatter_kwargs) x_points.append(x_val) y_points.append(y_val) # plot the tielines if all([ xx is not None and yy is not None for xx, yy in zip(x_points, y_points) ]): ax.plot(x_points, y_points, **updated_tieline_plot_kwargs) # three phase updated_tieline_plot_kwargs = {'linewidth': 1, 'color': 'r'} if tieline_plot_kwargs is not None: updated_tieline_plot_kwargs.update(tieline_plot_kwargs) for eq in eq_dict.get(3, []): # list of things in equilibrium # plot the scatter points for the right phases x_points, y_points = [], [] for phase_name, comp_dict, ref_key in eq: x_val, y_val = comp_dict[x], comp_dict[y] x_points.append(x_val) y_points.append(y_val) # Make sure the triangle completes x_points.append(x_points[0]) y_points.append(y_points[0]) # plot # check for None values if all([ xx is not None and yy is not None for xx, yy in zip(x_points, y_points) ]): ax.plot(x_points, y_points, **updated_tieline_plot_kwargs) # now we will add the symbols for the references to the legend handles for ref_key in bib_reference_keys: mark = symbol_map[ref_key]['markers'] # The legend marker edge width appears smaller than in the plot. # We will add this small hack to increase the width in the legend only. legend_kwargs = scatter_kwargs.copy() legend_kwargs['markeredgewidth'] = 1 legend_kwargs['markersize'] = 6 legend_handles.append( mlines.Line2D([], [], linestyle='', color='black', markeredgecolor='black', label=symbol_map[ref_key]['formatted'], fillstyle=mark['fillstyle'], marker=mark['marker'], **legend_kwargs)) # finally, add the completed legend ax.legend(handles=legend_handles, loc='center left', bbox_to_anchor=(1, 0.5)) return ax
# load the experimental and DFT datasets datasets = load_datasets(recursive_glob(DATASETS_DIR, '*.json')) # phases = ['LIQUID', 'BCC_A2', 'FCC_A1'] desired_data = datasets.search((tinydb.where('output') == 'ZPF') & (tinydb.where('components').test(lambda x: set(x).issubset(comps + ['VA']))) #& # (tinydb.where('phases').test(lambda x: len(set(phases).intersection(x)) > 0))) ) raveled_dict = ravel_zpf_values(desired_data, [independent_component]) bib_reference_keys = sorted(list({entry['reference'] for entry in desired_data})) symbol_map = bib_marker_map(bib_reference_keys) # map matplotlib string markers to strings of markers for Thermo-Calc's POST dataplot_symbols = ['S'+str(i) for i in range(1, 18)] dataplot_marker_map = dict(zip([v['markers']['marker'] for v in symbol_map.values()], dataplot_symbols)) equilibria_to_plot = raveled_dict.get(2, []) equilibria_lines = [] for eq in equilibria_to_plot: x_points, y_points = [], [] for phase_name, comp_dict, ref_key in eq: