From 281561f083c30f1981c66777981c5c95b9164542 Mon Sep 17 00:00:00 2001 From: Brandon Bocklund Date: Fri, 9 Aug 2024 10:25:00 -0700 Subject: [PATCH] MAINT: Remove ESPEI 0.9 deprecations (plotting and scheduler="None") (#257) Remove deprecated functionality scheduled for removal in ESPEI 0.9: - `espei.plot`: - `plot_parameters`, replaced with `plot_interaction` and `plot_endmember` - `eqdataplot` and `multiplot`, replace with calling appropriate PyCalphad phase diagram plotting function and `espei.plot.dataplot` - `_compare_data_to_parameters` only used by the above functions - YAML input: `scheduler = "None"`, replaced by `scheduler = null` --- espei/espei_script.py | 6 - espei/input-schema.yaml | 4 +- espei/plot.py | 408 ---------------------------------------- 3 files changed, 2 insertions(+), 416 deletions(-) diff --git a/espei/espei_script.py b/espei/espei_script.py index b09dab57..285bc606 100644 --- a/espei/espei_script.py +++ b/espei/espei_script.py @@ -115,12 +115,6 @@ def get_run_settings(input_dict): if run_settings['mcmc'].get('restart_trace') is None: run_settings['mcmc']['chains_per_parameter'] = run_settings['mcmc'].get('chains_per_parameter', 2) run_settings['mcmc']['chain_std_deviation'] = run_settings['mcmc'].get('chain_std_deviation', 0.1) - if run_settings['mcmc']['scheduler'] == 'None': - warnings.warn( - "Setting scheduler to the string 'None' will be deprecated in ESPEI " - "0.9. Use `null` in YAML or `None` in Python.", FutureWarning - ) - run_settings['mcmc']['scheduler'] = None if not schema.validate(run_settings): raise ValueError(schema.errors) if run_settings.get("generate_parameters") is not None: diff --git a/espei/input-schema.yaml b/espei/input-schema.yaml index d2fa337d..83ee4f2d 100644 --- a/espei/input-schema.yaml +++ b/espei/input-schema.yaml @@ -95,8 +95,8 @@ mcmc: required: True scheduler: # scheduler to use for parallelization type: string - default: dask # dask | None | A JSON file - regex: 'dask|None|.*\.json$' + default: dask # dask | A JSON file + regex: 'dask|.*\.json$' required: True nullable: True cores: diff --git a/espei/plot.py b/espei/plot.py index 1fa99b0c..3aea28e5 100644 --- a/espei/plot.py +++ b/espei/plot.py @@ -35,106 +35,6 @@ } -def plot_parameters(dbf, comps, phase_name, configuration, symmetry, datasets=None, fig=None, require_data=True): - """ - Plot parameters of interest compared with data in subplots of a single figure - - 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 - configuration : tuple - Sublattice configuration to plot, such as ('CU', 'CU') or (('CU', 'MG'), 'CU') - symmetry : list - List of lists containing indices of symmetric sublattices e.g. [[0, 1], [2, 3]] - datasets : PickleableTinyDB - ESPEI datasets to compare against. If None, nothing is plotted. - fig : matplotlib.Figure - Figure to create with axes as subplots. - require_data : bool - If True, plot parameters that have data corresponding data. Defaults to - True. Will raise an error for non-interaction configurations. - - Returns - ------- - None - - Examples - -------- - >>> # plot the LAVES_C15 (Cu)(Mg) endmember - >>> plot_parameters(dbf, ['CU', 'MG'], 'LAVES_C15', ('CU', 'MG'), symmetry=None, datasets=datasets) # doctest: +SKIP - >>> # plot the mixing interaction in the first sublattice - >>> plot_parameters(dbf, ['CU', 'MG'], 'LAVES_C15', (('CU', 'MG'), 'MG'), symmetry=None, datasets=datasets) # doctest: +SKIP - - """ - deprecation_msg = ( - "`espei.plot.plot_parameters` is deprecated and will be removed in ESPEI 0.9. " - "Please use `plot_endmember` or `plot_interaction` instead." - ) - warnings.warn(deprecation_msg, category=FutureWarning) - - em_plots = [('T', 'CPM'), ('T', 'CPM_FORM'), ('T', 'SM'), ('T', 'SM_FORM'), - ('T', 'HM'), ('T', 'HM_FORM')] - mix_plots = [ ('Z', 'HM_MIX'), ('Z', 'SM_MIX')] - comps = sorted(comps) - mod = Model(dbf, comps, phase_name) - mod.models['idmix'] = 0 - # This is for computing properties of formation - mod_norefstate = Model(dbf, comps, phase_name, parameters={'GHSER'+(c.upper()*2)[:2]: 0 for c in comps}) - # Is this an interaction parameter or endmember? - if any([isinstance(conf, list) or isinstance(conf, tuple) for conf in configuration]): - plots = mix_plots - else: - plots = em_plots - - # filter which parameters to plot by the data that exists - if require_data and datasets is not None: - filtered_plots = [] - for x_val, y_val in plots: - desired_props = [y_val.split('_')[0]+'_FORM', y_val] if y_val.endswith('_MIX') else [y_val] - solver_qry = (tinydb.where('solver').test(symmetry_filter, configuration, recursive_tuplify(symmetry) if symmetry else symmetry)) - data = get_prop_data(comps, phase_name, desired_props, datasets, additional_query=solver_qry) - data = filter_configurations(data, configuration, symmetry) - data = filter_temperatures(data) - if len(data) > 0: - filtered_plots.append((x_val, y_val, data)) - elif require_data: - raise ValueError('Plots require datasets, but no datasets were passed.') - elif plots == em_plots and not require_data: - # How we treat temperature dependence is ambiguous when there is no data, so we raise an error - raise ValueError('The "require_data=False" option is not supported for non-mixing configurations.') - elif datasets is not None: - filtered_plots = [] - for x_val, y_val in plots: - desired_props = [y_val.split('_')[0]+'_FORM', y_val] if y_val.endswith('_MIX') else [y_val] - solver_qry = (tinydb.where('solver').test(symmetry_filter, configuration, recursive_tuplify(symmetry) if symmetry else symmetry)) - data = get_prop_data(comps, phase_name, desired_props, datasets, additional_query=solver_qry) - data = filter_configurations(data, configuration, symmetry) - data = filter_temperatures(data) - filtered_plots.append((x_val, y_val, data)) - else: - filtered_plots = [(x_val, y_val, []) for x_val, y_val in plots] - - num_plots = len(filtered_plots) - if num_plots == 0: - return - if not fig: - fig = plt.figure(figsize=plt.figaspect(num_plots)) - - # plot them - for i, (x_val, y_val, data) in enumerate(filtered_plots): - if y_val.endswith('_FORM'): - ax = fig.add_subplot(num_plots, 1, i+1) - ax = _compare_data_to_parameters(dbf, comps, phase_name, data, mod_norefstate, configuration, x_val, y_val, ax=ax) - else: - ax = fig.add_subplot(num_plots, 1, i+1) - ax = _compare_data_to_parameters(dbf, comps, phase_name, data, mod, configuration, x_val, y_val, ax=ax) - - def dataplot(comps, phases, conds, datasets, tielines=True, ax=None, plot_kwargs=None, tieline_plot_kwargs=None) -> plt.Axes: """ Plot datapoints corresponding to the components, phases, and conditions. @@ -381,120 +281,6 @@ def dataplot(comps, phases, conds, datasets, tielines=True, ax=None, plot_kwargs return ax -def eqdataplot(eq, datasets, ax=None, plot_kwargs=None): - """ - Plot datapoints corresponding to the components and phases in the eq Dataset. - A convenience function for dataplot. - - Parameters - ---------- - eq : xarray.Dataset - Result of equilibrium calculation. - datasets : PickleableTinyDB - Database of phase equilibria datasets - ax : matplotlib.Axes - Default axes used if not specified. - plot_kwargs : dict - Keyword arguments to pass to dataplot - - Returns - ------- - A plot of phase equilibria points as a figure - - Examples - -------- - - >>> from pycalphad import equilibrium, Database, variables as v # doctest: +SKIP - >>> from pycalphad.plot.eqplot import eqplot # doctest: +SKIP - >>> from espei.datasets import load_datasets, recursive_glob # doctest: +SKIP - >>> datasets = load_datasets(recursive_glob('.', '*.json')) # doctest: +SKIP - >>> dbf = Database('my_databases.tdb') # doctest: +SKIP - >>> my_phases = list(dbf.phases.keys()) # doctest: +SKIP - >>> eq = equilibrium(dbf, ['CU', 'MG', 'VA'], my_phases, {v.P: 101325, v.T: (500, 1000, 10), v.X('MG'): (0, 1, 0.01)}) # doctest: +SKIP - >>> ax = eqplot(eq) # doctest: +SKIP - >>> ax = eqdataplot(eq, datasets, ax=ax) # doctest: +SKIP - - """ - deprecation_msg = ( - "`espei.plot.eqdataplot` is deprecated and will be removed in ESPEI 0.9. " - "Users depending on plotting from an `pycalphad.equilibrium` result should use " - "`pycalphad.plot.eqplot.eqplot` along with `espei.plot.dataplot` directly. " - "Note that pycalphad's mapping can offer signficant reductions in calculation " - "time compared to using `equilibrium` followed by `eqplot`." - ) - warnings.warn(deprecation_msg, category=FutureWarning) - # TODO: support reference legend - conds = OrderedDict([(_map_coord_to_variable(key), unpack_condition(np.asarray(value))) - for key, value in sorted(eq.coords.items(), key=str) - if (key == 'T') or (key == 'P') or (key.startswith('X_'))]) - - phases = list(map(str, sorted(set(np.array(eq.Phase.values.ravel(), dtype='U')) - {''}, key=str))) - comps = list(map(str, sorted(np.array(eq.coords['component'].values, dtype='U'), key=str))) - - ax = dataplot(comps, phases, conds, datasets, ax=ax, plot_kwargs=plot_kwargs) - - return ax - - -def multiplot(dbf, comps, phases, conds, datasets, eq_kwargs=None, plot_kwargs=None, data_kwargs=None): - """ - Plot a phase diagram with datapoints described by datasets. - This is a wrapper around pycalphad.equilibrium, pycalphad's eqplot, and dataplot. - - Parameters - ---------- - dbf : Database - pycalphad thermodynamic database containing the relevant parameters. - comps : list - Names of components to consider in the calculation. - phases : list - Names of phases to consider in the calculation. - conds : dict - Maps StateVariables to values and/or iterables of values. - datasets : PickleableTinyDB - Database of phase equilibria datasets - eq_kwargs : dict - Keyword arguments passed to pycalphad equilibrium() - plot_kwargs : dict - Keyword arguments passed to pycalphad eqplot() - data_kwargs : dict - Keyword arguments passed to dataplot() - - Returns - ------- - A phase diagram with phase equilibria data as a figure - - Examples - -------- - - >>> from pycalphad import Database, variables as v # doctest: +SKIP - >>> from pycalphad.plot.eqplot import eqplot # doctest: +SKIP - >>> from espei.datasets import load_datasets, recursive_glob # doctest: +SKIP - >>> datasets = load_datasets(recursive_glob('.', '*.json')) # doctest: +SKIP - >>> dbf = Database('my_databases.tdb') # doctest: +SKIP - >>> my_phases = list(dbf.phases.keys()) # doctest: +SKIP - >>> multiplot(dbf, ['CU', 'MG', 'VA'], my_phases, {v.P: 101325, v.T: 1000, v.X('MG'): (0, 1, 0.01)}, datasets) # doctest: +SKIP - - """ - deprecation_msg = ( - "`espei.plot.multiplot` is deprecated and will be removed in ESPEI 0.9. " - "Users depending on `multiplot` should use pycalphad's `binplot` or `ternplot` " - "followed by `espei.plot.dataplot`. Note that pycalphad's mapping can offer " - "signficant reductions in calculation time compared to using `multiplot`. See " - "ESPEI's recipes for an example: " - "https://espei.org/en/latest/recipes.html#plot-phase-diagram-with-data" - ) - warnings.warn(deprecation_msg, category=FutureWarning) - eq_kwargs = eq_kwargs or dict() - plot_kwargs = plot_kwargs or dict() - data_kwargs = data_kwargs or dict() - - eq_result = equilibrium(dbf, comps, phases, conds, **eq_kwargs) - ax = eqplot(eq_result, **plot_kwargs) - ax = eqdataplot(eq_result, datasets, ax=ax, plot_kwargs=data_kwargs) - return ax - - def _get_interaction_predicted_values(dbf, comps, phase_name, configuration, output): mod = Model(dbf, comps, phase_name) mod.models['idmix'] = 0 # TODO: better reference state handling @@ -754,200 +540,6 @@ def plot_endmember(dbf, comps, phase_name, configuration, output, datasets=None, return ax -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 - - """ - species = unpack_species(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) - # we don't take in symmetry here, so we we can't consider equivalent sublattices when canonicalizing - sample_condition_dicts = get_sample_condition_dicts(calculate_dict, canonicalize(constituents, None), phase_name) - endpoints = endmembers_from_interaction(configuration) - interacting_subls = [c for c in recursive_tuplify(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: - ax = plt.subplot() - 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 - Ts = calculate_dict['T'] - temperatures = np.asarray(Ts if len(Ts) > 0 else 298.15) - 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({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.float64) - if x == 'T' or x == 'P': - indep_var_data = np.array(data['conditions'][x], dtype=np.float64).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([cond_dict[Symbol('YS')] for cond_dict in sample_condition_dicts]) - 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.float64) - # 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.squeeze() - - response_data += np.array(data['values'], dtype=np.float64) - response_data = response_data.flatten() - if not bar_chart: - extra_kwargs = {} - 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='center left', bbox_to_anchor=(1, 0.5)) # legend outside - leg.get_frame().set_edgecolor('black') - return ax - - def _translate_endmember_to_array(endmember, variables): site_fractions = sorted(variables, key=str) frac_array = np.zeros(len(site_fractions))