def test_bib_marker_map(): """bib_marker_map should return a proper dict""" marker_dict = bib_marker_map(['otis2016', 'bocklund2018']) EXEMPLAR_DICT = { 'bocklund2018': { 'formatted': 'bocklund2018', 'markers': {'fillstyle': 'none', 'marker': 'o'} }, 'otis2016': { 'formatted': 'otis2016', 'markers': {'fillstyle': 'none', 'marker': 'v'} } } assert EXEMPLAR_DICT == marker_dict
def _compare_data_to_parameters(dbf, comps, phase_name, desired_data, mod, configuration, x, y, ax=None): """ Return one set of plotted Axes with data compared to calculated parameters Parameters ---------- dbf : Database pycalphad thermodynamic database containing the relevant parameters. comps : list Names of components to consider in the calculation. phase_name : str Name of the considered phase phase desired_data : mod : Model A pycalphad Model. The Model may or may not have the reference state zeroed out for formation properties. configuration : x : str Model property to plot on the x-axis e.g. 'T', 'HM_MIX', 'SM_FORM' y : str Model property to plot on the y-axis e.g. 'T', 'HM_MIX', 'SM_FORM' ax : matplotlib.Axes Default axes used if not specified. Returns ------- matplotlib.Axes """ all_samples = np.array(get_samples(desired_data), dtype=np.object) endpoints = endmembers_from_interaction(configuration) interacting_subls = [ c for c in list_to_tuple(configuration) if isinstance(c, tuple) ] disordered_config = False if (len(set(interacting_subls)) == 1) and (len(interacting_subls[0]) == 2): # This configuration describes all sublattices with the same two elements interacting # In general this is a high-dimensional space; just plot the diagonal to see the disordered mixing endpoints = [endpoints[0], endpoints[-1]] disordered_config = True if not ax: fig = plt.figure(figsize=plt.figaspect(1)) ax = fig.gca() bar_chart = False bar_labels = [] bar_data = [] if y.endswith('_FORM'): # We were passed a Model object with zeroed out reference states yattr = y[:-5] else: yattr = y if len(endpoints) == 1: # This is an endmember so we can just compute T-dependent stuff temperatures = np.array([i[0] for i in all_samples], dtype=np.float) if temperatures.min() != temperatures.max(): temperatures = np.linspace(temperatures.min(), temperatures.max(), num=100) else: # We only have one temperature: let's do a bar chart instead bar_chart = True temperatures = temperatures.min() endmember = _translate_endmember_to_array( endpoints[0], mod.ast.atoms(v.SiteFraction))[None, None] predicted_quantities = calculate(dbf, comps, [phase_name], output=yattr, T=temperatures, P=101325, points=endmember, model=mod, mode='numpy') if y == 'HM' and x == 'T': # Shift enthalpy data so that value at minimum T is zero predicted_quantities[yattr] -= predicted_quantities[yattr].sel( T=temperatures[0]).values.flatten() response_data = predicted_quantities[yattr].values.flatten() if not bar_chart: extra_kwargs = {} if len(response_data) < 10: extra_kwargs['markersize'] = 20 extra_kwargs['marker'] = '.' extra_kwargs['linestyle'] = 'none' extra_kwargs['clip_on'] = False ax.plot(temperatures, response_data, label='This work', color='k', **extra_kwargs) ax.set_xlabel(plot_mapping.get(x, x)) ax.set_ylabel(plot_mapping.get(y, y)) else: bar_labels.append('This work') bar_data.append(response_data[0]) elif len(endpoints) == 2: # Binary interaction parameter first_endpoint = _translate_endmember_to_array( endpoints[0], mod.ast.atoms(v.SiteFraction)) second_endpoint = _translate_endmember_to_array( endpoints[1], mod.ast.atoms(v.SiteFraction)) point_matrix = np.linspace(0, 1, num=100)[None].T * second_endpoint + \ (1 - np.linspace(0, 1, num=100))[None].T * first_endpoint # TODO: Real temperature support point_matrix = point_matrix[None, None] predicted_quantities = calculate(dbf, comps, [phase_name], output=yattr, T=300, P=101325, points=point_matrix, model=mod, mode='numpy') response_data = predicted_quantities[yattr].values.flatten() if not bar_chart: extra_kwargs = {} if len(response_data) < 10: extra_kwargs['markersize'] = 20 extra_kwargs['marker'] = '.' extra_kwargs['linestyle'] = 'none' extra_kwargs['clip_on'] = False ax.plot(np.linspace(0, 1, num=100), response_data, label='This work', color='k', **extra_kwargs) ax.set_xlim((0, 1)) ax.set_xlabel( str(':'.join(endpoints[0])) + ' to ' + str(':'.join(endpoints[1]))) ax.set_ylabel(plot_mapping.get(y, y)) else: bar_labels.append('This work') bar_data.append(response_data[0]) else: raise NotImplementedError( 'No support for plotting configuration {}'.format(configuration)) bib_reference_keys = sorted( list({entry['reference'] for entry in desired_data})) symbol_map = bib_marker_map(bib_reference_keys) for data in desired_data: indep_var_data = None response_data = np.zeros_like(data['values'], dtype=np.float) if x == 'T' or x == 'P': indep_var_data = np.array(data['conditions'][x], dtype=np.float).flatten() elif x == 'Z': if disordered_config: # Take the second element of the first interacting sublattice as the coordinate # Because it's disordered all sublattices should be equivalent # TODO: Fix this to filter because we need to guarantee the plot points are disordered occ = data['solver']['sublattice_occupancies'] subl_idx = np.nonzero( [isinstance(c, (list, tuple)) for c in occ[0]])[0] if len(subl_idx) > 1: subl_idx = int(subl_idx[0]) else: subl_idx = int(subl_idx) indep_var_data = [c[subl_idx][1] for c in occ] else: interactions = np.array([i[1][1] for i in get_samples([data])], dtype=np.float) indep_var_data = 1 - (interactions + 1) / 2 if y.endswith('_MIX') and data['output'].endswith('_FORM'): # All the _FORM data we have still has the lattice stability contribution # Need to zero it out to shift formation data to mixing mod_latticeonly = Model( dbf, comps, phase_name, parameters={'GHSER' + c.upper(): 0 for c in comps}) mod_latticeonly.models = { key: value for key, value in mod_latticeonly.models.items() if key == 'ref' } temps = data['conditions'].get('T', 300) pressures = data['conditions'].get('P', 101325) points = build_sitefractions( phase_name, data['solver']['sublattice_configurations'], data['solver']['sublattice_occupancies']) for point_idx in range(len(points)): missing_variables = mod_latticeonly.ast.atoms( v.SiteFraction) - set(points[point_idx].keys()) # Set unoccupied values to zero points[point_idx].update( {key: 0 for key in missing_variables}) # Change entry to a sorted array of site fractions points[point_idx] = list( OrderedDict(sorted(points[point_idx].items(), key=str)).values()) points = np.array(points, dtype=np.float) # TODO: Real temperature support points = points[None, None] stability = calculate(dbf, comps, [phase_name], output=data['output'][:-5], T=temps, P=pressures, points=points, model=mod_latticeonly, mode='numpy') response_data -= stability[data['output'][:-5]].values response_data += np.array(data['values'], dtype=np.float) response_data = response_data.flatten() if not bar_chart: extra_kwargs = {} if len(response_data) < 10: extra_kwargs['markersize'] = 8 extra_kwargs['linestyle'] = 'none' extra_kwargs['clip_on'] = False ref = data.get('reference', '') mark = symbol_map[ref]['markers'] ax.plot(indep_var_data, response_data, label=symbol_map[ref]['formatted'], marker=mark['marker'], fillstyle=mark['fillstyle'], **extra_kwargs) else: bar_labels.append(data.get('reference', None)) bar_data.append(response_data[0]) if bar_chart: ax.barh(0.02 * np.arange(len(bar_data)), bar_data, color='k', height=0.01) endmember_title = ' to '.join([':'.join(i) for i in endpoints]) ax.get_figure().suptitle('{} (T = {} K)'.format( endmember_title, temperatures), fontsize=20) ax.set_yticks(0.02 * np.arange(len(bar_data))) ax.set_yticklabels(bar_labels, fontsize=20) # This bar chart is rotated 90 degrees, so "y" is now x ax.set_xlabel(plot_mapping.get(y, y)) else: ax.set_frame_on(False) leg = ax.legend(loc='best') leg.get_frame().set_edgecolor('black') return ax
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
def plot_endmember(dbf, comps, phase_name, configuration, output, datasets=None, symmetry=None, x='T', ax=None, plot_kwargs=None, dataplot_kwargs=None) -> plt.Axes: """ Return one set of plotted Axes with data compared to calculated parameters Parameters ---------- dbf : Database pycalphad thermodynamic database containing the relevant parameters. comps : Sequence[str] Names of components to consider in the calculation. phase_name : str Name of the considered phase phase configuration : Tuple[Tuple[str]] ESPEI-style configuration output : str Model property to plot on the y-axis e.g. ``'HM_MIX'``, or ``'SM_MIX'``. Must be a ``'_MIX'`` property. datasets : tinydb.TinyDB symmetry : list List of lists containing indices of symmetric sublattices e.g. [[0, 1], [2, 3]] ax : plt.Axes Default axes used if not specified. plot_kwargs : Optional[Dict[str, Any]] Keyword arguments to ``ax.plot`` for the predicted data. dataplot_kwargs : Optional[Dict[str, Any]] Keyword arguments to ``ax.plot`` the observed data. Returns ------- plt.Axes """ if output.endswith('_MIX'): raise ValueError("`plot_interaction` only supports HM, HM_FORM, SM, SM_FORM or CPM, CPM_FORM outputs.") if x not in ('T',): raise ValueError(f'`x` passed to `plot_endmember` must be "T" got {x}') if not plot_kwargs: plot_kwargs = {} if not dataplot_kwargs: dataplot_kwargs = {} if not ax: ax = plt.subplot() if datasets is not None: solver_qry = (tinydb.where('solver').test(symmetry_filter, configuration, recursive_tuplify(symmetry) if symmetry else symmetry)) desired_data = get_prop_data(comps, phase_name, output, datasets, additional_query=solver_qry) desired_data = filter_configurations(desired_data, configuration, symmetry) desired_data = filter_temperatures(desired_data) else: desired_data = [] # Plot predicted values from the database endpoints = endmembers_from_interaction(configuration) if len(endpoints) != 1: raise ValueError(f"The configuration passed to `plot_endmember` must be an endmebmer configuration. Got {configuration}") if output.endswith('_FORM'): # TODO: better reference state handling mod = Model(dbf, comps, phase_name, parameters={'GHSER'+(c.upper()*2)[:2]: 0 for c in comps}) prop = output[:-5] else: mod = Model(dbf, comps, phase_name) prop = output endmember = _translate_endmember_to_array(endpoints[0], mod.ast.atoms(v.SiteFraction))[None, None] # Set up the domain of the calculation species = unpack_components(dbf, comps) # phase constituents are Species objects, so we need to be doing intersections with those phase_constituents = dbf.phases[phase_name].constituents # phase constituents must be filtered to only active constituents = [[sp.name for sp in sorted(subl_constituents.intersection(species))] for subl_constituents in phase_constituents] calculate_dict = get_prop_samples(desired_data, constituents) potential_values = np.asarray(calculate_dict[x] if len(calculate_dict[x]) > 0 else 298.15) potential_grid = np.linspace(max(potential_values.min()-1, 0), potential_values.max()+1, num=100) predicted_values = calculate(dbf, comps, [phase_name], output=prop, T=potential_grid, P=101325, points=endmember, model=mod)[prop].values.flatten() ax.plot(potential_grid, predicted_values, **plot_kwargs) # Plot observed values # TODO: model exclusions handling bib_reference_keys = sorted({entry.get('reference', '') for entry in desired_data}) symbol_map = bib_marker_map(bib_reference_keys) for data in desired_data: indep_var_data = None response_data = np.zeros_like(data['values'], dtype=np.float_) indep_var_data = np.array(data['conditions'][x], dtype=np.float_).flatten() response_data += np.array(data['values'], dtype=np.float_) response_data = response_data.flatten() ref = data.get('reference', '') dataplot_kwargs.setdefault('markersize', 8) dataplot_kwargs.setdefault('linestyle', 'none') dataplot_kwargs.setdefault('clip_on', False) # Cannot use setdefault because it won't overwrite previous iterations dataplot_kwargs['label'] = symbol_map[ref]['formatted'] dataplot_kwargs['marker'] = symbol_map[ref]['markers']['marker'] dataplot_kwargs['fillstyle'] = symbol_map[ref]['markers']['fillstyle'] ax.plot(indep_var_data, response_data, **dataplot_kwargs) ax.set_xlabel(plot_mapping.get(x, x)) ax.set_ylabel(plot_mapping.get(output, output)) leg = ax.legend(loc='center left', bbox_to_anchor=(1, 0.5)) # legend outside leg.get_frame().set_edgecolor('black') return ax
def plot_interaction(dbf, comps, phase_name, configuration, output, datasets=None, symmetry=None, ax=None, plot_kwargs=None, dataplot_kwargs=None) -> plt.Axes: """ Return one set of plotted Axes with data compared to calculated parameters Parameters ---------- dbf : Database pycalphad thermodynamic database containing the relevant parameters. comps : Sequence[str] Names of components to consider in the calculation. phase_name : str Name of the considered phase phase configuration : Tuple[Tuple[str]] ESPEI-style configuration output : str Model property to plot on the y-axis e.g. ``'HM_MIX'``, or ``'SM_MIX'``. Must be a ``'_MIX'`` property. datasets : tinydb.TinyDB symmetry : list List of lists containing indices of symmetric sublattices e.g. [[0, 1], [2, 3]] ax : plt.Axes Default axes used if not specified. plot_kwargs : Optional[Dict[str, Any]] Keyword arguments to ``ax.plot`` for the predicted data. dataplot_kwargs : Optional[Dict[str, Any]] Keyword arguments to ``ax.plot`` the observed data. Returns ------- plt.Axes """ if not output.endswith('_MIX'): raise ValueError("`plot_interaction` only supports HM_MIX, SM_MIX, or CPM_MIX outputs.") if not plot_kwargs: plot_kwargs = {} if not dataplot_kwargs: dataplot_kwargs = {} if not ax: ax = plt.subplot() # Plot predicted values from the database grid, predicted_values = _get_interaction_predicted_values(dbf, comps, phase_name, configuration, output) plot_kwargs.setdefault('label', 'This work') plot_kwargs.setdefault('color', 'k') ax.plot(grid, predicted_values, **plot_kwargs) # Plot the observed values from the datasets # TODO: model exclusions handling # TODO: better reference state handling mod_srf = Model(dbf, comps, phase_name, parameters={'GHSER'+c.upper(): 0 for c in comps}) mod_srf.models = {'ref': mod_srf.models['ref']} # _MIX assumption prop = output.split('_MIX')[0] desired_props = (f"{prop}_MIX", f"{prop}_FORM") if datasets is not None: solver_qry = (tinydb.where('solver').test(symmetry_filter, configuration, recursive_tuplify(symmetry) if symmetry else symmetry)) desired_data = get_prop_data(comps, phase_name, desired_props, datasets, additional_query=solver_qry) desired_data = filter_configurations(desired_data, configuration, symmetry) desired_data = filter_temperatures(desired_data) else: desired_data = [] species = unpack_components(dbf, comps) # phase constituents are Species objects, so we need to be doing intersections with those phase_constituents = dbf.phases[phase_name].constituents # phase constituents must be filtered to only active constituents = [[sp.name for sp in sorted(subl_constituents.intersection(species))] for subl_constituents in phase_constituents] subl_dof = list(map(len, constituents)) calculate_dict = get_prop_samples(desired_data, constituents) sample_condition_dicts = _get_sample_condition_dicts(calculate_dict, subl_dof) interacting_subls = [c for c in recursive_tuplify(configuration) if isinstance(c, tuple)] if (len(set(interacting_subls)) == 1) and (len(interacting_subls[0]) == 2): # This configuration describes all sublattices with the same two elements interacting # In general this is a high-dimensional space; just plot the diagonal to see the disordered mixing endpoints = endmembers_from_interaction(configuration) endpoints = [endpoints[0], endpoints[-1]] disordered_config = True else: disordered_config = False bib_reference_keys = sorted({entry.get('reference', '') for entry in desired_data}) symbol_map = bib_marker_map(bib_reference_keys) for data in desired_data: indep_var_data = None response_data = np.zeros_like(data['values'], dtype=np.float_) if disordered_config: # Take the second element of the first interacting sublattice as the coordinate # Because it's disordered all sublattices should be equivalent # TODO: Fix this to filter because we need to guarantee the plot points are disordered occ = data['solver']['sublattice_occupancies'] subl_idx = np.nonzero([isinstance(c, (list, tuple)) for c in occ[0]])[0] if len(subl_idx) > 1: subl_idx = int(subl_idx[0]) else: subl_idx = int(subl_idx) indep_var_data = [c[subl_idx][1] for c in occ] else: interactions = np.array([cond_dict[Symbol('YS')] for cond_dict in sample_condition_dicts]) indep_var_data = 1 - (interactions+1)/2 if data['output'].endswith('_FORM'): # All the _FORM data we have still has the lattice stability contribution # Need to zero it out to shift formation data to mixing temps = data['conditions'].get('T', 298.15) pressures = data['conditions'].get('P', 101325) points = build_sitefractions(phase_name, data['solver']['sublattice_configurations'], data['solver']['sublattice_occupancies']) for point_idx in range(len(points)): missing_variables = mod_srf.ast.atoms(v.SiteFraction) - set(points[point_idx].keys()) # Set unoccupied values to zero points[point_idx].update({key: 0 for key in missing_variables}) # Change entry to a sorted array of site fractions points[point_idx] = list(OrderedDict(sorted(points[point_idx].items(), key=str)).values()) points = np.array(points, dtype=np.float_) # TODO: Real temperature support points = points[None, None] stability = calculate(dbf, comps, [phase_name], output=data['output'][:-5], T=temps, P=pressures, points=points, model=mod_srf) response_data -= stability[data['output'][:-5]].values.squeeze() response_data += np.array(data['values'], dtype=np.float_) response_data = response_data.flatten() ref = data.get('reference', '') dataplot_kwargs.setdefault('markersize', 8) dataplot_kwargs.setdefault('linestyle', 'none') dataplot_kwargs.setdefault('clip_on', False) # Cannot use setdefault because it won't overwrite previous iterations dataplot_kwargs['label'] = symbol_map[ref]['formatted'] dataplot_kwargs['marker'] = symbol_map[ref]['markers']['marker'] dataplot_kwargs['fillstyle'] = symbol_map[ref]['markers']['fillstyle'] ax.plot(indep_var_data, response_data, **dataplot_kwargs) ax.set_xlim((0, 1)) ax.set_xlabel(str(':'.join(endpoints[0])) + ' to ' + str(':'.join(endpoints[1]))) ax.set_ylabel(plot_mapping.get(output, output)) leg = ax.legend(loc='center left', bbox_to_anchor=(1, 0.5)) # legend outside leg.get_frame().set_edgecolor('black') return ax
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: sym_ref = symbol_map[ref_key] x_val, y_val = comp_dict[independent_component], comp_dict['T'] if x_val is not None and y_val is not None: line = "{} {} {}".format(x_val, y_val, dataplot_marker_map[sym_ref['markers']['marker']])
def dataplot(comps, phases, conds, datasets, ax=None, 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 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: 1000, 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: # TODO: support isotherm plotting raise NotImplementedError('Triangular plotting is not yet implemented') 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] # 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_ylabel(plot_mapping.get(str(y), y), fontsize=20) ax.set_xlim((0, 1)) # handle plotting kwargs scatter_kwargs = {'markersize': 6, 'markeredgewidth': 0.2} # 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) plots = [('ZPF', 'T')] for output, y in plots: # 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 # TODO: explore building tielines from multiphase equilibria. Should we do this? 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) # 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'] *= 5 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)) # TODO: There are lot of ways this could break in multi-component situations for data in desired_data: payload = data['values'] # TODO: Add broadcast_conditions support # Repeat the temperature (or whatever variable) vector to align with the unraveled data temp_repeats = np.zeros(len(np.atleast_1d(data['conditions'][y])), dtype=np.int) for idx, p in enumerate(payload): temp_repeats[idx] = len(p) temps_ravelled = np.repeat(data['conditions'][y], temp_repeats) payload_ravelled = [] phases_ravelled = [] comps_ravelled = [] symbols_ravelled = [] # TODO: Fix to only include equilibria listed in 'phases' for p in payload: markers = symbol_map[data['reference']]['markers'] fill_sym_tuple = (markers['fillstyle'], markers['marker']) symbols_ravelled.extend([fill_sym_tuple] * len(p)) payload_ravelled.extend(p) for rp in payload_ravelled: phases_ravelled.append(rp[0]) comp_dict = dict(zip([x.upper() for x in rp[1]], rp[2])) dependent_comp = list( set(comps) - set(comp_dict.keys()) - set(['VA'])) if len(dependent_comp) > 1: raise ValueError('Dependent components greater than one') elif len(dependent_comp) == 1: dependent_comp = dependent_comp[0] # TODO: Assuming N=1 comp_dict[dependent_comp] = 1 - sum( np.array(list(comp_dict.values()), dtype=np.float)) chosen_comp_value = comp_dict[x] comps_ravelled.append(chosen_comp_value) symbols_ravelled = np.array(symbols_ravelled) comps_ravelled = np.array(comps_ravelled) temps_ravelled = np.array(temps_ravelled) phases_ravelled = np.array(phases_ravelled) # We can't pass an array of markers to scatter, sadly for fill_sym in symbols_ravelled: # fill_sym is a tuple of ('fillstyle', 'marker') selected = np.all(symbols_ravelled == fill_sym, axis=1) # ax.plot does not seem to be able to take a list of hex values # for colors in the same was as ax.scatter. To get around this, # we'll use the set_prop_cycler for each plot cycle phase_colors = [ phase_color_map[x] for x in phases_ravelled[selected] ] ax.set_prop_cycle(cycler('color', phase_colors)) ax.plot(comps_ravelled[selected], temps_ravelled[selected], fillstyle=fill_sym[0], marker=fill_sym[1], linestyle='', **scatter_kwargs) return ax