Пример #1
0
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
Пример #2
0
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
Пример #3
0
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
Пример #4
0
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
Пример #5
0
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
Пример #6
0



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']])
Пример #7
0
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