From ccb6ebcadd0903f5c56caf407721b816e2e3cb0a Mon Sep 17 00:00:00 2001 From: mzwiessele Date: Wed, 7 Oct 2015 19:03:03 +0100 Subject: [PATCH] [testing] updates again and plotly is going forward --- GPy/installation.cfg | 3 + GPy/plotting/abstract_plotting_library.py | 39 +-- GPy/plotting/gpy_plot/data_plots.py | 48 ++-- GPy/plotting/gpy_plot/gp_plots.py | 230 ++++++++---------- GPy/plotting/gpy_plot/inference_plots.py | 6 +- GPy/plotting/gpy_plot/kernel_plots.py | 92 ++++++- GPy/plotting/gpy_plot/latent_plots.py | 30 ++- GPy/plotting/gpy_plot/plot_util.py | 11 +- GPy/plotting/matplot_dep/plot_definitions.py | 9 +- GPy/plotting/plotly_dep/defaults.py | 61 +++-- GPy/plotting/plotly_dep/plot_definitions.py | 177 +++++++------- GPy/testing/plotting_tests.py | 10 +- .../baseline/sparse_gp_class_raw.png | Bin 15809 -> 22853 bytes .../baseline/sparse_gp_class_raw_link.png | Bin 17230 -> 24425 bytes travis_tests.py | 2 +- 15 files changed, 409 insertions(+), 309 deletions(-) diff --git a/GPy/installation.cfg b/GPy/installation.cfg index 901a7ef5..2056c0b9 100644 --- a/GPy/installation.cfg +++ b/GPy/installation.cfg @@ -1,2 +1,5 @@ # This is the local installation configuration file for GPy +[plotting] +#library = plotly +library = matplotlib \ No newline at end of file diff --git a/GPy/plotting/abstract_plotting_library.py b/GPy/plotting/abstract_plotting_library.py index 4743f1bc..f46843cc 100644 --- a/GPy/plotting/abstract_plotting_library.py +++ b/GPy/plotting/abstract_plotting_library.py @@ -81,16 +81,6 @@ class AbstractPlottingLibrary(object): E.g. in matplotlib this means it deletes references to ax, as plotting is done on the axis itself and is not a kwarg. - """ - raise NotImplementedError("Implement all plot functions in AbstractPlottingLibrary in order to use your own plotting library") - - def show_canvas(self, canvas, plots, legend=True, title=None, **kwargs): - """ - Show the canvas given. - plots is a dictionary with the plots - as the items. - - the kwargs are plotting library specific kwargs! :param xlabel: the label to put on the xaxis :param ylabel: the label to put on the yaxis @@ -100,11 +90,32 @@ class AbstractPlottingLibrary(object): :param (float, float) xlim: the limits for the xaxis :param (float, float) ylim: the limits for the yaxis :param (float, float) zlim: the limits for the zaxis (if plotting in 3d) - - E.g. in matplotlib this does not have to do anything, we make the tight plot, though. """ raise NotImplementedError("Implement all plot functions in AbstractPlottingLibrary in order to use your own plotting library") + def add_to_canvas(self, canvas, plots, legend=True, title=None, **kwargs): + """ + Add plots is a dictionary with the plots as the + items or a list of plots as items to canvas. + + The kwargs are plotting library specific kwargs! + + E.g. in matplotlib this does not have to do anything to add stuff, but + we set the legend and title. + + !This function returns the updated canvas! + + :param title: the title of the plot + :param legend: whether to plot a legend or not + """ + raise NotImplementedError("Implement all plot functions in AbstractPlottingLibrary in order to use your own plotting library") + + def show_canvas(self, canvas, **kwargs): + """ + Draw/Plot the canvas given. + """ + raise NotImplementedError + def plot(self, cavas, X, Y, Z=None, color=None, label=None, **kwargs): """ Make a line plot from for Y on X (Y = f(X)) on the canvas. @@ -177,8 +188,8 @@ class AbstractPlottingLibrary(object): def yerrorbar(self, canvas, X, Y, error, color=None, label=None, **kwargs): """ Make errorbars along the yaxis on the canvas given. - if error is two dimensional, the lower error is error[:,0] and - the upper error is error[:,1] + if error is two dimensional, the lower error is error[0, :] and + the upper error is error[1, :] the kwargs are plotting library specific kwargs! """ diff --git a/GPy/plotting/gpy_plot/data_plots.py b/GPy/plotting/gpy_plot/data_plots.py index f5743aaa..e2fb1fe7 100644 --- a/GPy/plotting/gpy_plot/data_plots.py +++ b/GPy/plotting/gpy_plot/data_plots.py @@ -56,7 +56,7 @@ def plot_data(self, which_data_rows='all', """ canvas, plot_kwargs = pl.new_canvas(projection=projection, **plot_kwargs) plots = _plot_data(self, canvas, which_data_rows, which_data_ycols, visible_dims, projection, label, **plot_kwargs) - return pl.show_canvas(canvas, plots) + return pl.add_to_canvas(canvas, plots) def _plot_data(self, canvas, which_data_rows='all', which_data_ycols='all', visible_dims=None, @@ -81,12 +81,12 @@ def _plot_data(self, canvas, which_data_rows='all', for d in ycols: update_not_existing_kwargs(plot_kwargs, pl.defaults.data_2d) # @UndefinedVariable plots['dataplot'].append(pl.scatter(canvas, X[rows, free_dims[0]], X[rows, free_dims[1]], - color=Y[rows, d], vmin=Y.min(), vmax=Y.max(), label=label, **plot_kwargs)) + color=Y[rows, d], label=label, **plot_kwargs)) else: for d in ycols: update_not_existing_kwargs(plot_kwargs, pl.defaults.data_2d) # @UndefinedVariable plots['dataplot'].append(pl.scatter(canvas, X[rows, free_dims[0]], X[rows, free_dims[1]], - Z=Y[rows, d], vmin=Y.min(), color=Y[rows, d], vmax=Y.max(), label=label, **plot_kwargs)) + Z=Y[rows, d], color=Y[rows, d], label=label, **plot_kwargs)) elif len(free_dims) == 0: pass #Nothing to plot! else: @@ -117,9 +117,9 @@ def plot_data_error(self, which_data_rows='all', :returns list: of plots created. """ - canvas, error_kwargs = pl.new_canvas(projection=='3d', **error_kwargs) + canvas, error_kwargs = pl.new_canvas(projection=projection, **error_kwargs) plots = _plot_data_error(self, canvas, which_data_rows, which_data_ycols, visible_dims, projection, label, **error_kwargs) - return pl.show_canvas(canvas, plots) + return pl.add_to_canvas(canvas, plots) def _plot_data_error(self, canvas, which_data_rows='all', which_data_ycols='all', visible_dims=None, @@ -167,7 +167,7 @@ def plot_inducing(self, visible_dims=None, projection='2d', label=None, **plot_k """ canvas, kwargs = pl.new_canvas(projection=projection, **plot_kwargs) plots = _plot_inducing(self, canvas, visible_dims, projection, label, **kwargs) - return pl.show_canvas(canvas, plots) + return pl.add_to_canvas(canvas, plots) def _plot_inducing(self, canvas, visible_dims, projection, label, **plot_kwargs): if visible_dims is None: @@ -220,7 +220,7 @@ def plot_errorbars_trainset(self, which_data_rows='all', canvas, kwargs = pl.new_canvas(projection=projection, **plot_kwargs) plots = _plot_errorbars_trainset(self, canvas, which_data_rows, which_data_ycols, fixed_inputs, plot_raw, apply_link, label, projection, predict_kw, **kwargs) - return pl.show_canvas(canvas, plots) + return pl.add_to_canvas(canvas, plots) def _plot_errorbars_trainset(self, canvas, which_data_rows='all', which_data_ycols='all', @@ -245,25 +245,31 @@ def _plot_errorbars_trainset(self, canvas, if len(free_dims)<=2: update_not_existing_kwargs(plot_kwargs, pl.defaults.yerrorbar) - if len(free_dims)==1: - if predict_kw is None: + if predict_kw is None: predict_kw = {} - if 'Y_metadata' not in predict_kw: - predict_kw['Y_metadata'] = self.Y_metadata or {} - _, percs, _ = helper_predict_with_model(self, Xgrid, plot_raw, - apply_link, (2.5, 97.5), - ycols, predict_kw) + if 'Y_metadata' not in predict_kw: + predict_kw['Y_metadata'] = self.Y_metadata or {} + mu, percs, _ = helper_predict_with_model(self, Xgrid, plot_raw, + apply_link, (2.5, 97.5), + ycols, predict_kw) + if len(free_dims)==1: for d in ycols: - plots.append(pl.yerrorbar(canvas, X[rows,free_dims[0]], Y[rows,d], - np.vstack([Y[rows,d]-percs[0][rows,d], percs[1][rows,d]-Y[rows,d]]), + plots.append(pl.yerrorbar(canvas, X[rows,free_dims[0]], mu[rows,d], + np.vstack([mu[rows, d] - percs[0][rows, d], percs[1][rows, d] - mu[rows,d]]), label=label, **plot_kwargs)) elif len(free_dims) == 2: - plots.append(pl.yerrorbar(canvas, X[rows,free_dims[0]], X[rows,free_dims[1]], - np.vstack([Y[rows,d]-percs[0][rows,d], percs[1][rows,d]-Y[rows,d]]), - Y[rows,d], - label=label, - **plot_kwargs)) + for d in ycols: + plots.append(pl.yerrorbar(canvas, X[rows,free_dims[0]], X[rows,free_dims[1]], + np.vstack([mu[rows, d] - percs[0][rows, d], percs[1][rows, d] - mu[rows,d]]), + color=Y[rows,d], + label=label, + **plot_kwargs)) + plots.append(pl.xerrorbar(canvas, X[rows,free_dims[0]], X[rows,free_dims[1]], + np.vstack([mu[rows, d] - percs[0][rows, d], percs[1][rows, d] - mu[rows,d]]), + color=Y[rows,d], + label=label, + **plot_kwargs)) pass #Nothing to plot! else: raise NotImplementedError("Cannot plot in more then one dimension.") diff --git a/GPy/plotting/gpy_plot/gp_plots.py b/GPy/plotting/gpy_plot/gp_plots.py index 222a4318..d10e75ff 100644 --- a/GPy/plotting/gpy_plot/gp_plots.py +++ b/GPy/plotting/gpy_plot/gp_plots.py @@ -63,26 +63,23 @@ def plot_mean(self, plot_limits=None, fixed_inputs=None, :param dict predict_kw: the keyword arguments for the prediction. If you want to plot a specific kernel give dict(kern=) in here """ canvas, kwargs = pl.new_canvas(projection=projection, **kwargs) - plots = _plot_mean(self, canvas, plot_limits, fixed_inputs, - resolution, plot_raw, - apply_link, visible_dims, which_data_ycols, - levels, projection, label, predict_kw, **kwargs) - return pl.show_canvas(canvas, plots) - -def _plot_mean(self, canvas, plot_limits=None, fixed_inputs=None, - resolution=None, plot_raw=False, - apply_link=False, visible_dims=None, - which_data_ycols='all', - levels=20, projection='2d', label=None, - predict_kw=None, - **kwargs): - _, _, _, _, free_dims, Xgrid, x, y, _, _, resolution = helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resolution) - - if len(free_dims)<=2: - mu, _, _ = helper_predict_with_model(self, Xgrid, plot_raw, + helper_data = helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resolution) + helper_prediction = helper_predict_with_model(self, helper_data[5], plot_raw, apply_link, None, get_which_data_ycols(self, which_data_ycols), predict_kw) + plots = _plot_mean(self, canvas, helper_data, helper_prediction, + levels, projection, label, **kwargs) + pl.add_to_canvas(canvas, plots) + return pl.show_canvas(canvas) + +def _plot_mean(self, canvas, helper_data, helper_prediction, + levels=20, projection='2d', label=None, + **kwargs): + + _, _, _, _, free_dims, Xgrid, x, y, _, _, resolution = helper_data + if len(free_dims)<=2: + mu, _, _ = helper_prediction if len(free_dims)==1: # 1D plotting: update_not_existing_kwargs(kwargs, pl.defaults.meanplot_1d) # @UndefinedVariable @@ -108,7 +105,7 @@ def _plot_mean(self, canvas, plot_limits=None, fixed_inputs=None, def plot_confidence(self, lower=2.5, upper=97.5, plot_limits=None, fixed_inputs=None, resolution=None, plot_raw=False, apply_link=False, visible_dims=None, - which_data_ycols='all', + which_data_ycols='all', label=None, predict_kw=None, **kwargs): """ @@ -132,33 +129,23 @@ def plot_confidence(self, lower=2.5, upper=97.5, plot_limits=None, fixed_inputs= :param dict predict_kw: the keyword arguments for the prediction. If you want to plot a specific kernel give dict(kern=) in here """ canvas, kwargs = pl.new_canvas(**kwargs) - plots = _plot_confidence(self, canvas, lower, upper, plot_limits, - fixed_inputs, resolution, plot_raw, - apply_link, visible_dims, which_data_ycols, - predict_kw, **kwargs) - return pl.show_canvas(canvas, plots) - -def _plot_confidence(self, canvas, lower, upper, plot_limits=None, fixed_inputs=None, - resolution=None, plot_raw=False, - apply_link=False, visible_dims=None, - which_data_ycols=None, - predict_kw=None, - **kwargs): - _, _, _, _, free_dims, Xgrid, _, _, _, _, _ = helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resolution) - ycols = get_which_data_ycols(self, which_data_ycols) - - update_not_existing_kwargs(kwargs, pl.defaults.confidence_interval) # @UndefinedVariable - - if len(free_dims)<=1: - if len(free_dims)==1: - _, percs, _ = helper_predict_with_model(self, Xgrid, plot_raw, apply_link, + helper_data = helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resolution) + helper_prediction = helper_predict_with_model(self, helper_data[5], plot_raw, apply_link, (lower, upper), ycols, predict_kw) - + plots = _plot_confidence(self, canvas, helper_data, helper_prediction, label, **kwargs) + return pl.add_to_canvas(canvas, plots) + +def _plot_confidence(self, canvas, helper_data, helper_prediction, label, **kwargs): + _, _, _, _, free_dims, Xgrid, _, _, _, _, _ = helper_data + update_not_existing_kwargs(kwargs, pl.defaults.confidence_interval) # @UndefinedVariable + if len(free_dims)<=1: + if len(free_dims)==1: + percs = helper_prediction[1] fills = [] - for d in ycols: - fills.append(pl.fill_between(canvas, Xgrid[:,free_dims[0]], percs[0][:,d], percs[1][:,d], **kwargs)) + for d in range(helper_prediction[0].shape[1]): + fills.append(pl.fill_between(canvas, Xgrid[:,free_dims[0]], percs[0][:,d], percs[1][:,d], label=label, **kwargs)) return dict(gpconfidence=fills) else: pass #Nothing to plot! @@ -170,7 +157,8 @@ def plot_samples(self, plot_limits=None, fixed_inputs=None, resolution=None, plot_raw=True, apply_link=False, visible_dims=None, which_data_ycols='all', - samples=3, projection='2d', predict_kw=None, + samples=3, projection='2d', label=None, + predict_kw=None, **kwargs): """ Plot the mean of the GP. @@ -191,36 +179,31 @@ def plot_samples(self, plot_limits=None, fixed_inputs=None, :param int levels: for 2D plotting, the number of contour levels to use is """ canvas, kwargs = pl.new_canvas(projection=projection, **kwargs) - plots = _plot_samples(self, canvas, plot_limits, fixed_inputs, - resolution, plot_raw, - apply_link, visible_dims, which_data_ycols, samples, projection, - predict_kw, **kwargs) - return pl.show_canvas(canvas, plots) + ycols = get_which_data_ycols(self, which_data_ycols) + helper_data = helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resolution) + helper_prediction = helper_predict_with_model(self, helper_data[5], plot_raw, apply_link, + None, + ycols, predict_kw, samples) + plots = _plot_samples(self, canvas, helper_data, helper_prediction, + projection, label, **kwargs) + return pl.add_to_canvas(canvas, plots) -def _plot_samples(self, canvas, plot_limits=None, fixed_inputs=None, - resolution=None, plot_raw=True, - apply_link=False, visible_dims=None, - which_data_ycols=None, - samples=3, projection='2d', - label=None, - predict_kw=None, **kwargs): - _, _, _, _, free_dims, Xgrid, x, y, _, _, resolution = helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resolution) +def _plot_samples(self, canvas, helper_data, helper_prediction, projection, + label, **kwargs): + _, _, _, _, free_dims, Xgrid, x, y, _, _, resolution = helper_data + samples = helper_prediction[2] if len(free_dims)<=2: if len(free_dims)==1: # 1D plotting: - _, _, samples = helper_predict_with_model(self, Xgrid, plot_raw, apply_link, - None, get_which_data_ycols(self, which_data_ycols), predict_kw, samples) update_not_existing_kwargs(kwargs, pl.defaults.samples_1d) # @UndefinedVariable - return dict(gpmean=[pl.plot(canvas, Xgrid[:, free_dims], samples, **kwargs)]) + return dict(gpmean=[pl.plot(canvas, Xgrid[:, free_dims], samples, label=label, **kwargs)]) elif len(free_dims)==2 and projection=='3d': - _, _, samples = helper_predict_with_model(self, Xgrid, plot_raw, apply_link, - None, get_which_data_ycols(self, which_data_ycols), predict_kw, samples) update_not_existing_kwargs(kwargs, pl.defaults.samples_3d) # @UndefinedVariable for s in range(samples.shape[-1]): return dict(gpmean=[pl.surface(canvas, x, - y, samples[:, s].reshape(resolution, resolution), - **kwargs)]) + y, samples[:, s].reshape(resolution, resolution), + **kwargs)]) else: pass # Nothing to plot! else: @@ -231,7 +214,7 @@ def plot_density(self, plot_limits=None, fixed_inputs=None, resolution=None, plot_raw=False, apply_link=False, visible_dims=None, which_data_ycols='all', - levels=35, + levels=35, label=None, predict_kw=None, **kwargs): """ @@ -254,35 +237,26 @@ def plot_density(self, plot_limits=None, fixed_inputs=None, :param dict predict_kw: the keyword arguments for the prediction. If you want to plot a specific kernel give dict(kern=) in here """ canvas, kwargs = pl.new_canvas(**kwargs) - plots = _plot_density(self, canvas, plot_limits, - fixed_inputs, resolution, plot_raw, - apply_link, visible_dims, which_data_ycols, - levels, - predict_kw, **kwargs) - return pl.show_canvas(canvas, plots) + helper_data = helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resolution) + helper_prediction = helper_predict_with_model(self, helper_data[5], plot_raw, + apply_link, np.linspace(2.5, 97.5, levels*2), + get_which_data_ycols(self, which_data_ycols), + predict_kw) + plots = _plot_density(self, canvas, helper_data, helper_prediction, label, **kwargs) + return pl.add_to_canvas(canvas, plots) -def _plot_density(self, canvas, plot_limits=None, fixed_inputs=None, - resolution=None, plot_raw=False, - apply_link=False, visible_dims=None, - which_data_ycols=None, - levels=35, - predict_kw=None, **kwargs): - _, _, _, _, free_dims, Xgrid, x, y, _, _, resolution = helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resolution) - - ycols = get_which_data_ycols(self, which_data_ycols) +def _plot_density(self, canvas, helper_data, helper_prediction, label, **kwargs): + _, _, _, _, free_dims, Xgrid, _, _, _, _, _ = helper_data + mu, percs, _ = helper_prediction update_not_existing_kwargs(kwargs, pl.defaults.density) # @UndefinedVariable if len(free_dims)<=1: if len(free_dims)==1: - _, percs, _ = helper_predict_with_model(self, Xgrid, plot_raw, - apply_link, np.linspace(2.5, 97.5, levels*2), - get_which_data_ycols(self, which_data_ycols), - predict_kw) # 1D plotting: fills = [] - for d in ycols: - fills.append(pl.fill_gradient(canvas, Xgrid[:, free_dims[0]], [p[:,d] for p in percs], **kwargs)) + for d in range(mu.shape[1]): + fills.append(pl.fill_gradient(canvas, Xgrid[:, free_dims[0]], [p[:,d] for p in percs], label=label, **kwargs)) return dict(gpdensity=fills) else: pass # Nothing to plot! @@ -327,11 +301,28 @@ def plot(self, plot_limits=None, fixed_inputs=None, :param dict predict_kw: the keyword arguments for the prediction. If you want to plot a specific kernel give dict(kern=) in here """ canvas, _ = pl.new_canvas(projection=projection, **kwargs) - plots = _plot(self, canvas, plot_limits, fixed_inputs, resolution, plot_raw, - apply_link, which_data_ycols, which_data_rows, visible_dims, - levels, samples, samples_likelihood, lower, upper, plot_data, - plot_inducing, plot_density, projection, predict_kw) - return pl.show_canvas(canvas, plots) + helper_data = helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resolution) + helper_prediction = helper_predict_with_model(self, helper_data[5], plot_raw, + apply_link, np.linspace(2.5, 97.5, levels*2) if plot_density else (lower,upper), + get_which_data_ycols(self, which_data_ycols), + predict_kw, samples) + if plot_raw and not apply_link: + # It does not make sense to plot the data (which lives not in the latent function space) into latent function space. + plot_data = False + plots = {} + if plot_data: + plots.update(_plot_data(self, canvas, which_data_rows, which_data_ycols, visible_dims, projection)) + plots.update(_plot_data_error(self, canvas, which_data_rows, which_data_ycols, visible_dims, projection)) + plots.update(_plot(self, canvas, plots, helper_data, helper_prediction, levels, plot_inducing, plot_density, projection)) + if plot_raw and (samples_likelihood > 0): + helper_prediction = helper_predict_with_model(self, helper_data[5], False, + apply_link, None, + get_which_data_ycols(self, which_data_ycols), + predict_kw, samples_likelihood) + plots.update(_plot_samples(canvas, helper_data, helper_prediction, projection)) + if hasattr(self, 'Z') and plot_inducing: + plots.update(_plot_inducing(self, canvas, visible_dims, projection, None)) + return pl.add_to_canvas(canvas, plots) def plot_f(self, plot_limits=None, fixed_inputs=None, @@ -375,50 +366,35 @@ def plot_f(self, plot_limits=None, fixed_inputs=None, :param dict error_kwargs: kwargs for the error plot for the plotting library you are using :param kwargs plot_kwargs: kwargs for the data plot for the plotting library you are using """ - canvas, _ = pl.new_canvas(projection=='3d', **kwargs) - plots = _plot(self, canvas, plot_limits, fixed_inputs, resolution, - True, apply_link, which_data_ycols, which_data_rows, - visible_dims, levels, samples, 0, lower, upper, - plot_data, plot_inducing, plot_density, projection, - predict_kw) - return pl.show_canvas(canvas, plots) + canvas, _ = pl.new_canvas(projection=projection, **kwargs) + helper_data = helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resolution) + helper_prediction = helper_predict_with_model(self, helper_data[5], True, + apply_link, np.linspace(2.5, 97.5, levels*2) if plot_density else (lower,upper), + get_which_data_ycols(self, which_data_ycols), + predict_kw, samples) + if not apply_link: + # It does not make sense to plot the data (which lives not in the latent function space) into latent function space. + plot_data = False + plots = {} + if plot_data: + plots.update(_plot_data(self, canvas, which_data_rows, which_data_ycols, visible_dims, projection)) + plots.update(_plot_data_error(self, canvas, which_data_rows, which_data_ycols, visible_dims, projection)) + plots.update(_plot(self, canvas, plots, helper_data, helper_prediction, levels, plot_inducing, plot_density, projection)) + if hasattr(self, 'Z') and plot_inducing: + plots.update(_plot_inducing(self, canvas, visible_dims, projection, None)) + return pl.add_to_canvas(canvas, plots) -def _plot(self, canvas, plot_limits=None, fixed_inputs=None, - resolution=None, - plot_raw=False, apply_link=False, - which_data_ycols='all', which_data_rows='all', - visible_dims=None, - levels=20, samples=0, samples_likelihood=0, lower=2.5, upper=97.5, - plot_data=True, plot_inducing=True, plot_density=False, projection='2d', - predict_kw=None): - plots = {} - if plot_raw and not apply_link: - # It does not make sense to plot the data (which lives not in the latent function space) into latent function space. - plot_data = False - - if plot_data: - plots.update(_plot_data(self, canvas, which_data_rows, which_data_ycols, visible_dims, - projection, label=None)) - plots.update(_plot_data_error(self, canvas, which_data_rows, which_data_ycols, visible_dims, - projection, label=None)) - - plots.update(_plot_mean(self, canvas, plot_limits, fixed_inputs, resolution, plot_raw, apply_link, visible_dims, which_data_ycols, levels, projection, label=None, - predict_kw=None)) +def _plot(self, canvas, plots, helper_data, helper_prediction, levels, plot_inducing=True, plot_density=False, projection='2d'): + plots.update(_plot_mean(self, canvas, helper_data, helper_prediction, levels, projection, None)) if projection=='2d': if not plot_density: - plots.update(_plot_confidence(self, canvas, lower, upper, plot_limits, fixed_inputs, resolution, plot_raw, apply_link, visible_dims, which_data_ycols, predict_kw)) + plots.update(_plot_confidence(self, canvas, helper_data, helper_prediction, None)) else: - plots.update(_plot_density(self, canvas, plot_limits, fixed_inputs, resolution, plot_raw, apply_link, visible_dims, which_data_ycols, levels, predict_kw)) - - if samples > 0: - plots.update(_plot_samples(self, canvas, plot_limits, fixed_inputs, resolution, True, apply_link, visible_dims, which_data_ycols, samples, predict_kw)) - if samples_likelihood > 0: - plots.update(_plot_samples(self, canvas, plot_limits, fixed_inputs, resolution, False, apply_link, visible_dims, which_data_ycols, samples, predict_kw)) - - if hasattr(self, 'Z') and plot_inducing: - plots.update(_plot_inducing(self, canvas, visible_dims, projection, None)) + plots.update(_plot_density(self, canvas, helper_data, helper_prediction, None)) + if helper_prediction[2] is not None: + plots.update(_plot_samples(self, canvas, helper_data, helper_prediction, projection, None)) return plots \ No newline at end of file diff --git a/GPy/plotting/gpy_plot/inference_plots.py b/GPy/plotting/gpy_plot/inference_plots.py index bbb75c3b..8a0d5b04 100644 --- a/GPy/plotting/gpy_plot/inference_plots.py +++ b/GPy/plotting/gpy_plot/inference_plots.py @@ -13,7 +13,7 @@ def plot_optimizer(optimizer, **kwargs): else: canvas, kwargs = pl.new_canvas(**kwargs) plots = dict(trace=pl.plot(range(len(optimizer.trace)), optimizer.trace)) - return pl.show_canvas(canvas, plots, xlabel='Iteration', ylabel='f(x)') + return pl.add_to_canvas(canvas, plots, xlabel='Iteration', ylabel='f(x)') def plot_sgd_traces(optimizer): figure = pl.figure(2,1) @@ -21,8 +21,8 @@ def plot_sgd_traces(optimizer): plots = dict(lines=[]) for k in optimizer.param_traces.keys(): plots['lines'].append(pl.plot(canvas, range(len(optimizer.param_traces[k])), optimizer.param_traces[k], label=k)) - pl.show_canvas(canvas, legend=True) + pl.add_to_canvas(canvas, legend=True) canvas, _ = pl.new_canvas(figure, 1, 2, title="Objective function") pl.plot(canvas, range(len(optimizer.fopt_trace)), optimizer.fopt_trace) - return pl.show_canvas(canvas, plots, legend=True) + return pl.add_to_canvas(canvas, plots, legend=True) \ No newline at end of file diff --git a/GPy/plotting/gpy_plot/kernel_plots.py b/GPy/plotting/gpy_plot/kernel_plots.py index e9b20196..532faf14 100644 --- a/GPy/plotting/gpy_plot/kernel_plots.py +++ b/GPy/plotting/gpy_plot/kernel_plots.py @@ -30,6 +30,10 @@ import numpy as np from . import pl from .. import Tango +from .plot_util import get_x_y_var,\ + update_not_existing_kwargs, \ + helper_for_plot_data, scatter_label_generator, subsample_X,\ + find_best_layout_for_subplots def plot_ARD(kernel, filtering=None, **kwargs): """ @@ -64,11 +68,95 @@ def plot_ARD(kernel, filtering=None, **kwargs): else: print("filtering out {}".format(kernel.parameters[i].name)) - plt.show_canvas() + plt.add_to_canvas() ax.set_xlim(-.5, kernel.input_dim - .5) add_bar_labels(fig, ax, [bars[-1]], bottom=bottom-last_bottom) return dict(barplots=bars) -def plot_covariance(): +def plot_covariance(kernel, x=None, label=None, plot_limits=None, visible_dims=None, resolution=None, projection=None, levels=20, **mpl_kwargs): + """ + plot a kernel. + :param x: the value to use for the other kernel argument (kernels are a function of two variables!) + :param fignum: figure number of the plot + :param ax: matplotlib axis to plot on + :param title: the matplotlib title + :param plot_limits: the range over which to plot the kernel + :resolution: the resolution of the lines used in plotting + :mpl_kwargs avalid keyword arguments to pass through to matplotlib (e.g. lw=7) + """ + canvas, error_kwargs = pl.new_canvas(projection=projection, **error_kwargs) + _, _, _, _, free_dims, Xgrid, x, y, _, _, resolution = helper_for_plot_data(kernel, plot_limits, visible_dims, None, resolution) + + if len(free_dims)<=2: + if len(free_dims)==1: + if x is None: x = np.zeros((1, 1)) + else: + x = np.asarray(x) + assert x.size == 1, "The size of the fixed variable x is not 1" + x = x.reshape((1, 1)) + # 1D plotting: + update_not_existing_kwargs(kwargs, pl.defaults.meanplot_1d) # @UndefinedVariable + plots = dict(covariance=[pl.plot(canvas, Xgrid[:, free_dims], mu, label=label, **kwargs)]) + else: + if projection == '2d': + update_not_existing_kwargs(kwargs, pl.defaults.meanplot_2d) # @UndefinedVariable + plots = dict(covariance=[pl.contour(canvas, x, y, + mu.reshape(resolution, resolution).T, + levels=levels, label=label, **kwargs)]) + elif projection == '3d': + update_not_existing_kwargs(kwargs, pl.defaults.meanplot_3d) # @UndefinedVariable + plots = dict(covariance=[pl.surface(canvas, x, y, + mu.reshape(resolution, resolution).T, + label=label, + **kwargs)]) + + return pl.add_to_canvas(canvas, plots) + + if kernel.input_dim == 1: + + if plot_limits == None: + xmin, xmax = (x - 5).flatten(), (x + 5).flatten() + elif len(plot_limits) == 2: + xmin, xmax = plot_limits + else: + raise ValueError("Bad limits for plotting") + + Xnew = np.linspace(xmin, xmax, resolution or 201)[:, None] + Kx = kernel.K(Xnew, x) + ax.plot(Xnew, Kx, **mpl_kwargs) + ax.set_xlim(xmin, xmax) + ax.set_xlabel("x") + ax.set_ylabel("k(x,%0.1f)" % x) + + elif kernel.input_dim == 2: + if x is None: + x = np.zeros((1, 2)) + else: + x = np.asarray(x) + assert x.size == 2, "The size of the fixed variable x is not 2" + x = x.reshape((1, 2)) + + if plot_limits is None: + xmin, xmax = (x - 5).flatten(), (x + 5).flatten() + elif len(plot_limits) == 2: + xmin, xmax = plot_limits + else: + raise ValueError("Bad limits for plotting") + + + resolution = resolution or 51 + xx, yy = np.mgrid[xmin[0]:xmax[0]:1j * resolution, xmin[1]:xmax[1]:1j * resolution] + Xnew = np.vstack((xx.flatten(), yy.flatten())).T + Kx = kernel.K(Xnew, x) + Kx = Kx.reshape(resolution, resolution).T + ax.contour(xx, yy, Kx, vmin=Kx.min(), vmax=Kx.max(), cmap=pb.cm.jet, **mpl_kwargs) # @UndefinedVariable + ax.set_xlim(xmin[0], xmax[0]) + ax.set_ylim(xmin[1], xmax[1]) + ax.set_xlabel("x1") + ax.set_ylabel("x2") + ax.set_title("k(x1,x2 ; %0.1f,%0.1f)" % (x[0, 0], x[0, 1])) + else: + raise NotImplementedError("Cannot plot a kernel with more than two input dimensions") + pass \ No newline at end of file diff --git a/GPy/plotting/gpy_plot/latent_plots.py b/GPy/plotting/gpy_plot/latent_plots.py index 43e3f650..0f5021ea 100644 --- a/GPy/plotting/gpy_plot/latent_plots.py +++ b/GPy/plotting/gpy_plot/latent_plots.py @@ -94,7 +94,11 @@ def plot_latent_scatter(self, labels=None, else: legend = find_best_layout_for_subplots(len(np.unique(labels)))[1] scatters = _plot_latent_scatter(canvas, X, sig_dims, labels, marker, num_samples, projection=projection, **kwargs) - return pl.show_canvas(canvas, dict(scatter=scatters), legend=legend) + return pl.add_to_canvas(canvas, dict(scatter=scatters), legend=legend) + + + + def plot_latent_inducing(self, which_indices=None, legend=False, @@ -125,7 +129,12 @@ def plot_latent_inducing(self, Z = self.Z.values labels = np.array(['inducing'] * Z.shape[0]) scatters = _plot_latent_scatter(canvas, Z, sig_dims, labels, marker, num_samples, projection=projection, **kwargs) - return pl.show_canvas(canvas, dict(scatter=scatters), legend=legend) + return pl.add_to_canvas(canvas, dict(scatter=scatters), legend=legend) + + + + + def _plot_magnification(self, canvas, which_indices, Xgrid, xmin, xmax, resolution, updates, @@ -182,12 +191,12 @@ def plot_magnification(self, labels=None, which_indices=None, legend = False scatters = _plot_latent_scatter(canvas, X, which_indices, labels, marker, num_samples, projection='2d', **scatter_kwargs or {}) view = _plot_magnification(self, canvas, which_indices[:2], Xgrid, xmin, xmax, resolution, updates, mean, covariance, kern, **imshow_kwargs) - plots = pl.show_canvas(canvas, dict(scatter=scatters, imshow=view), + retval = pl.add_to_canvas(canvas, dict(scatter=scatters, imshow=view), legend=legend, ) _wait_for_updates(view, updates) - return plots - + return retval + @@ -245,10 +254,10 @@ def plot_latent(self, labels=None, which_indices=None, legend = False scatters = _plot_latent_scatter(canvas, X, which_indices, labels, marker, num_samples, projection='2d', **scatter_kwargs or {}) view = _plot_latent(self, canvas, which_indices, Xgrid, xmin, xmax, resolution, updates, kern, **imshow_kwargs) - plots = pl.show_canvas(canvas, dict(scatter=scatters, imshow=view), legend=legend) + retval = pl.add_to_canvas(canvas, dict(scatter=scatters, imshow=view), legend=legend) _wait_for_updates(view, updates) - return plots - + return retval + def _plot_steepest_gradient_map(self, canvas, which_indices, Xgrid, xmin, xmax, resolution, output_labels, updates, kern=None, annotation_kwargs=None, @@ -310,10 +319,9 @@ def plot_steepest_gradient_map(self, output_labels=None, data_labels=None, which legend = False plots = dict(scatter=_plot_latent_scatter(canvas, X, which_indices, data_labels, marker, num_samples, **scatter_kwargs or {})) plots.update(_plot_steepest_gradient_map(self, canvas, which_indices, Xgrid, xmin, xmax, resolution, output_labels, updates, kern, annotation_kwargs=annotation_kwargs, **imshow_kwargs)) - show = pl.show_canvas(canvas, plots, legend=legend) + retval = pl.add_to_canvas(canvas, plots, legend=legend) _wait_for_updates(plots['annotation'], updates) - return show - + return retval diff --git a/GPy/plotting/gpy_plot/plot_util.py b/GPy/plotting/gpy_plot/plot_util.py index 954593d6..749b4a15 100644 --- a/GPy/plotting/gpy_plot/plot_util.py +++ b/GPy/plotting/gpy_plot/plot_util.py @@ -32,6 +32,15 @@ import numpy as np from scipy import sparse import itertools +def in_ipynb(): + try: + cfg = get_ipython().config + if cfg['IPKernelApp']['parent_appname'] == 'ipython-notebook': + return True + else: + return False + except NameError: + return False def find_best_layout_for_subplots(num_subplots): r, c = 1, 1 @@ -109,7 +118,7 @@ def helper_for_plot_data(self, plot_limits, visible_dims, fixed_inputs, resoluti if fixed_inputs is None: fixed_inputs = [] fixed_dims = get_fixed_dims(self, fixed_inputs) - free_dims = get_free_dims(self, visible_dims, fixed_dims)[:2] + free_dims = get_free_dims(self, visible_dims, fixed_dims) if len(free_dims) == 1: #define the frame on which to plot diff --git a/GPy/plotting/matplot_dep/plot_definitions.py b/GPy/plotting/matplot_dep/plot_definitions.py index 4678f1a8..15578446 100644 --- a/GPy/plotting/matplot_dep/plot_definitions.py +++ b/GPy/plotting/matplot_dep/plot_definitions.py @@ -80,7 +80,7 @@ class MatplotlibPlots(AbstractPlottingLibrary): if zlabel is not None: ax.set_zlabel(zlabel) return ax, kwargs - def show_canvas(self, ax, plots, legend=False, title=None, **kwargs): + def add_to_canvas(self, ax, plots, legend=False, title=None, **kwargs): ax.autoscale_view() fontdict=dict(family='sans-serif', weight='light', size=9) if legend is True: @@ -89,8 +89,13 @@ class MatplotlibPlots(AbstractPlottingLibrary): #ax.legend(prop=fontdict) legend_ontop(ax, ncol=legend, fontdict=fontdict) if title is not None: ax.figure.suptitle(title) + return ax + + def show_canvas(self, ax, tight_layout=False, **kwargs): + if tight_layout: + ax.figure.tight_layout() ax.figure.canvas.draw() - return plots + return ax.figure def scatter(self, ax, X, Y, Z=None, color=Tango.colorsHex['mediumBlue'], label=None, marker='o', **kwargs): if Z is not None: diff --git a/GPy/plotting/plotly_dep/defaults.py b/GPy/plotting/plotly_dep/defaults.py index f67b6e14..d10201d6 100644 --- a/GPy/plotting/plotly_dep/defaults.py +++ b/GPy/plotting/plotly_dep/defaults.py @@ -28,7 +28,6 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #=============================================================================== -from matplotlib import cm from .. import Tango ''' @@ -43,33 +42,33 @@ it gives back an empty default, when defaults are not defined. ''' # Data plots: -data_1d = dict(lw=1.5, marker='x', edgecolor='k') -data_2d = dict(s=35, edgecolors='none', linewidth=0., cmap=cm.get_cmap('hot'), alpha=.5) -inducing_1d = dict(lw=0, s=500, facecolors=Tango.colorsHex['darkRed']) -inducing_2d = dict(s=14, edgecolors='k', linewidth=.4, facecolors='white', alpha=.5) -inducing_3d = dict(lw=.3, s=500, facecolors='white', edgecolors='k') -xerrorbar = dict(color='k', fmt='none', elinewidth=.5, alpha=.5) -yerrorbar = dict(color=Tango.colorsHex['darkRed'], fmt='none', elinewidth=.5, alpha=.5) - -# GP plots: -meanplot_1d = dict(color=Tango.colorsHex['mediumBlue'], linewidth=2) -meanplot_2d = dict(cmap='hot', linewidth=.5) -meanplot_3d = dict(linewidth=0, antialiased=True, cstride=1, rstride=1, cmap='hot', alpha=.3) -samples_1d = dict(color=Tango.colorsHex['mediumBlue'], linewidth=.3) -samples_3d = dict(cmap='hot', alpha=.1, antialiased=True, cstride=1, rstride=1, linewidth=0) -confidence_interval = dict(edgecolor=Tango.colorsHex['darkBlue'], linewidth=.5, color=Tango.colorsHex['lightBlue'],alpha=.2) -density = dict(alpha=.5, color=Tango.colorsHex['lightBlue']) - -# GPLVM plots: -data_y_1d = dict(linewidth=0, cmap='RdBu', s=40) -data_y_1d_plot = dict(color='k', linewidth=1.5) - -# Kernel plots: -ard = dict(edgecolor='k', linewidth=1.2) - -# Input plots: -latent = dict(aspect='auto', cmap='Greys', interpolation='bicubic') -gradient = dict(aspect='auto', cmap='RdBu', interpolation='nearest', alpha=.7) -magnification = dict(aspect='auto', cmap='Greys', interpolation='bicubic') -latent_scatter = dict(s=40, linewidth=.2, edgecolor='k', alpha=.9) -annotation = dict(fontdict=dict(family='sans-serif', weight='light', fontsize=9), zorder=.3, alpha=.7) \ No newline at end of file +data_1d = dict(marker_kwargs=dict(linewidth=.7, ), marker='x', color='black') +data_2d = dict(marker='o', cmap='Hot', marker_kwargs=dict(opacity=.5)) +# inducing_1d = dict(lw=0, s=500, facecolors=Tango.colorsHex['darkRed']) +# inducing_2d = dict(s=14, edgecolors='k', linewidth=.4, facecolors='white', alpha=.5) +# inducing_3d = dict(lw=.3, s=500, facecolors='white', edgecolors='k') +# xerrorbar = dict(color='k', fmt='none', elinewidth=.5, alpha=.5) +yerrorbar = dict(color=Tango.colorsHex['darkRed'], error_kwargs=dict(thickness=.5), opacity=.5) +# +# # GP plots: +# meanplot_1d = dict(color=Tango.colorsHex['mediumBlue'], linewidth=2) +# meanplot_2d = dict(cmap='hot', linewidth=.5) +# meanplot_3d = dict(linewidth=0, antialiased=True, cstride=1, rstride=1, cmap='hot', alpha=.3) +# samples_1d = dict(color=Tango.colorsHex['mediumBlue'], linewidth=.3) +# samples_3d = dict(cmap='hot', alpha=.1, antialiased=True, cstride=1, rstride=1, linewidth=0) +# confidence_interval = dict(edgecolor=Tango.colorsHex['darkBlue'], linewidth=.5, color=Tango.colorsHex['lightBlue'],alpha=.2) +# density = dict(alpha=.5, color=Tango.colorsHex['lightBlue']) +# +# # GPLVM plots: +# data_y_1d = dict(linewidth=0, cmap='RdBu', s=40) +# data_y_1d_plot = dict(color='k', linewidth=1.5) +# +# # Kernel plots: +# ard = dict(edgecolor='k', linewidth=1.2) +# +# # Input plots: +# latent = dict(aspect='auto', cmap='Greys', interpolation='bicubic') +# gradient = dict(aspect='auto', cmap='RdBu', interpolation='nearest', alpha=.7) +# magnification = dict(aspect='auto', cmap='Greys', interpolation='bicubic') +# latent_scatter = dict(s=40, linewidth=.2, edgecolor='k', alpha=.9) +# annotation = dict(fontdict=dict(family='sans-serif', weight='light', fontsize=9), zorder=.3, alpha=.7) \ No newline at end of file diff --git a/GPy/plotting/plotly_dep/plot_definitions.py b/GPy/plotting/plotly_dep/plot_definitions.py index 0de1c7c4..41a56d3b 100644 --- a/GPy/plotting/plotly_dep/plot_definitions.py +++ b/GPy/plotting/plotly_dep/plot_definitions.py @@ -34,7 +34,22 @@ from . import defaults import itertools from plotly import tools from plotly import plotly as py -from plotly.graph_objs import Scatter, Line, Data +from plotly import matplotlylib +from plotly.graph_objs import Scatter, Scatter3d, Line, Marker, ErrorX, ErrorY, Bar + +SYMBOL_MAP = { + 'o': 'dot', + 'v': 'triangle-down', + '^': 'triangle-up', + '<': 'triangle-left', + '>': 'triangle-right', + 's': 'square', + '+': 'cross', + 'x': 'x', + '*': 'x', # no star yet in plotly!! + 'D': 'diamond', + 'd': 'diamond', +} class PlotlyPlots(AbstractPlottingLibrary): def __init__(self): @@ -42,87 +57,61 @@ class PlotlyPlots(AbstractPlottingLibrary): self._defaults = defaults.__dict__ self.current_states = dict() - def figure(self, rows=1, cols=1, **kwargs): - if 'filename' not in kwargs: - print('PlotlyWarning: filename was not given, this may clutter your plotly workspace') - filename = None - else: filename = kwargs.pop('filename') - figure = tools.make_subplots(rows, cols, **kwargs) - self.current_states[hex(id(figure))] = dict(filename=filename) - index = 1 - for i in rows: - for j in cols: - self.current_states[hex(id(figure))][(i,j)] = ('xaxis{}'.format(index), 'yaxis{}'.format(index)) - index += 1 + def figure(self, rows=1, cols=1, specs=None, is_3d=False): + if specs is None: + specs = [[{'is_3d': is_3d}]*cols]*rows + figure = tools.make_subplots(rows, cols, specs=specs) return figure def new_canvas(self, figure=None, row=1, col=1, projection='2d', xlabel=None, ylabel=None, zlabel=None, title=None, xlim=None, ylim=None, zlim=None, **kwargs): + if 'filename' not in kwargs: + print('PlotlyWarning: filename was not given, this may clutter your plotly workspace') + filename = None + else: + filename = kwargs.pop('filename') if figure is None: - figure = self.figure(**kwargs) - return (figure, self.current_states[hex(id(figure))][(row,col)]), kwargs + figure = self.figure(is_3d=projection=='3d') + self.current_states[hex(id(figure))] = dict(filename=filename) + return (figure, row, col), kwargs - def show_canvas(self, canvas, traces, legend=False, **kwargs): - fig = canvas[0] - axis = fig['layout'][canvas[1]] - axis.update(title=title, label) - figure.add_traces(traces, row, col, **kwargs) - - # If shared_axes is False (default) use list_of_domains - # This is used for insets and irregular layouts - if not shared_xaxes and not shared_yaxes: - x_dom = list_of_domains[::2] - y_dom = list_of_domains[1::2] - subtitle_pos_x = [] - subtitle_pos_y = [] - for x_domains in x_dom: - subtitle_pos_x.append(sum(x_domains) / 2) - for y_domains in y_dom: - subtitle_pos_y.append(y_domains[1]) - # If shared_axes is True the domin of each subplot is not returned so the - # title position must be calculated for each subplot + def add_to_canvas(self, canvas, traces, legend=False, **kwargs): + figure, row, col = canvas + def recursive_append(traces): + for _, trace in traces.items(): + if isinstance(trace, (tuple, list)): + for t in trace: + figure.append_trace(t, row, col) + elif isinstance(trace, dict): + recursive_append(trace) + else: + figure.append_trace(trace, row, col) + recursive_append(traces) + figure.layout['showlegend'] = legend + return canvas + + def show_canvas(self, canvas, **kwargs): + figure, _, _ = canvas + from ..gpy_plot.plot_util import in_ipynb + if in_ipynb(): + py.iplot(figure, filename=self.current_states[hex(id(figure))]['filename']) else: - subtitle_pos_x = [None] * cols - subtitle_pos_y = [None] * rows - delt_x = (x_e - x_s) - for index in range(cols): - subtitle_pos_x[index] = ((delt_x / 2) + - ((delt_x + horizontal_spacing) * index)) - subtitle_pos_x *= rows - for index in range(rows): - subtitle_pos_y[index] = (1 - ((y_e + vertical_spacing) * index)) - subtitle_pos_y *= cols - subtitle_pos_y = sorted(subtitle_pos_y, reverse=True) + py.plot(figure, filename=self.current_states[hex(id(figure))]['filename']) + return figure - plot_titles = [] - for index in range(len(subplot_titles)): - if not subplot_titles[index]: - pass - else: - plot_titles.append({'y': subtitle_pos_y[index], - 'xref': 'paper', - 'x': subtitle_pos_x[index], - 'yref': 'paper', - 'text': subplot_titles[index], - 'showarrow': False, - 'font': graph_objs.Font(size=16), - 'xanchor': 'center', - 'yanchor': 'bottom' - }) - - layout['annotations'] = plot_titles + def scatter(self, ax, X, Y, Z=None, color=Tango.colorsHex['mediumBlue'], cmap=None, label=None, marker='o', marker_kwargs=None, **kwargs): try: - url = py.iplot(figure, self.current_filename) + marker = SYMBOL_MAP[marker] except: - url = py.plot(figure, self.current_filename) - return url - - def scatter(self, ax, X, Y, Z=None, color=Tango.colorsHex['mediumBlue'], label=None, marker='o', **kwargs): - - - def plot(self, ax, X, Y, Z=None, color=None, label=None, **kwargs): + #not matplotlib marker + pass if Z is not None: - return ax.plot(X, Y, color=color, zs=Z, label=label, **kwargs) - return ax.plot(X, Y, color=color, label=label, **kwargs) + return Scatter3d(x=X, y=Y, z=Z, mode='markers', marker=Marker(color=color, symbol=marker, colorscale=cmap, **marker_kwargs or {}), name=label, **kwargs) + return Scatter(x=X, y=Y, mode='markers', marker=Marker(color=color, symbol=marker, colorscale=cmap, **marker_kwargs or {}), name=label, **kwargs) + + def plot(self, ax, X, Y, Z=None, color=None, label=None, line_kwargs=None, **kwargs): + if Z is not None: + return Scatter3d(x=X, y=Y, z=Z, mode='lines', line=Line(color=color, **line_kwargs or {}), name=label, **kwargs) + return Scatter(x=X, y=Y, mode='lines', line=Line(color=color, **line_kwargs or {}), name=label, **kwargs) def plot_axis_lines(self, ax, X, color=Tango.colorsHex['mediumBlue'], label=None, **kwargs): from matplotlib import transforms @@ -137,26 +126,31 @@ class PlotlyPlots(AbstractPlottingLibrary): return ax.scatter(X[:,0], X[:,1], ax.get_zlim()[0], c=color, label=label, **kwargs) return ax.scatter(X, np.zeros_like(X), c=color, label=label, **kwargs) - def barplot(self, ax, x, height, width=0.8, bottom=0, color=Tango.colorsHex['mediumBlue'], label=None, **kwargs): - if 'align' not in kwargs: - kwargs['align'] = 'center' - return ax.bar(left=x, height=height, width=width, - bottom=bottom, label=label, color=color, - **kwargs) + def barplot(self, canvas, x, height, width=0.8, bottom=0, color=Tango.colorsHex['mediumBlue'], label=None, **kwargs): + figure, _, _ = canvas + if 'barmode' in kwargs: + figure.layout['barmode'] = kwargs.pop('barmode') + return Bar(x=x, y=height, marker=Marker(color=color), name=label) - def xerrorbar(self, ax, X, Y, error, Z=None, color=Tango.colorsHex['mediumBlue'], label=None, **kwargs): - if not('linestyle' in kwargs or 'ls' in kwargs): - kwargs['ls'] = 'none' + def xerrorbar(self, ax, X, Y, error, Z=None, color=Tango.colorsHex['mediumBlue'], label=None, error_kwargs=None, **kwargs): + error_kwargs = error_kwargs or {} + if (error.shape[0] == 2) and (error.ndim == 2): + error_kwargs.update(dict(array=error[1], arrayminus=error[0], symmetric=False)) + else: + error_kwargs.update(dict(array=error, symmetric=True)) if Z is not None: - return ax.errorbar(X, Y, Z, xerr=error, ecolor=color, label=label, **kwargs) - return ax.errorbar(X, Y, xerr=error, ecolor=color, label=label, **kwargs) - - def yerrorbar(self, ax, X, Y, error, Z=None, color=Tango.colorsHex['mediumBlue'], label=None, **kwargs): - if not('linestyle' in kwargs or 'ls' in kwargs): - kwargs['ls'] = 'none' + return Scatter3d(x=X, y=Y, z=Z, mode='markers', error_x=ErrorX(color=color, **error_kwargs or {}), marker=Marker(size='0'), name=label, **kwargs) + return Scatter(x=X, y=Y, mode='markers', error_x=ErrorX(color=color, **error_kwargs or {}), marker=Marker(size='0'), name=label, **kwargs) + + def yerrorbar(self, ax, X, Y, error, Z=None, color=Tango.colorsHex['mediumBlue'], label=None, error_kwargs=None, **kwargs): + error_kwargs = error_kwargs or {} + if (error.shape[0] == 2) and (error.ndim == 2): + error_kwargs.update(dict(array=error[1], arrayminus=error[0], symmetric=False)) + else: + error_kwargs.update(dict(array=error, symmetric=True)) if Z is not None: - return ax.errorbar(X, Y, Z, yerr=error, ecolor=color, label=label, **kwargs) - return ax.errorbar(X, Y, yerr=error, ecolor=color, label=label, **kwargs) + return Scatter3d(x=X, y=Y, z=Z, mode='markers', error_y=ErrorY(color=color, **error_kwargs or {}), marker=Marker(size='0'), name=label, **kwargs) + return Scatter(x=X, y=Y, mode='markers', error_y=ErrorY(color=color, **error_kwargs or {}), marker=Marker(size='0'), name=label, **kwargs) def imshow(self, ax, X, extent=None, label=None, vmin=None, vmax=None, **imshow_kwargs): if 'origin' not in imshow_kwargs: @@ -167,9 +161,8 @@ class PlotlyPlots(AbstractPlottingLibrary): return ax.imshow(X, label=label, extent=extent, vmin=vmin, vmax=vmax, **imshow_kwargs) def imshow_interact(self, ax, plot_function, extent=None, label=None, resolution=None, vmin=None, vmax=None, **imshow_kwargs): - if 'origin' not in imshow_kwargs: - imshow_kwargs['origin'] = 'lower' - return ImshowController(ax, plot_function, extent, resolution=resolution, vmin=vmin, vmax=vmax, **imshow_kwargs) + # TODO stream interaction? + super(PlotlyPlots, self).imshow_interact(ax, plot_function) def annotation_heatmap(self, ax, X, annotation, extent=None, label=None, imshow_kwargs=None, **annotation_kwargs): imshow_kwargs = imshow_kwargs or {} diff --git a/GPy/testing/plotting_tests.py b/GPy/testing/plotting_tests.py index 88aa32c6..4df227ec 100644 --- a/GPy/testing/plotting_tests.py +++ b/GPy/testing/plotting_tests.py @@ -127,7 +127,9 @@ def test_threed(): m.optimize() m.plot_data(projection='3d') m.plot_mean(projection='3d') - for do_test in _image_comparison(baseline_images=['gp_3d_{}'.format(sub) for sub in ["data", "mean"]], extensions=extensions): + m.plot_samples(projection='3d', samples=1) + m.plot_samples(projection='3d', plot_raw=False, samples=1) + for do_test in _image_comparison(baseline_images=['gp_3d_{}'.format(sub) for sub in ["data", "mean", "samples", "samples_lik"]], extensions=extensions): yield (do_test, ) def test_sparse(): @@ -164,9 +166,9 @@ def test_sparse_classification(): Y = f+np.random.normal(0, .1, f.shape) m = GPy.models.SparseGPClassification(X, Y>Y.mean()) m.optimize() - m.plot(plot_raw=False, apply_link=False) - m.plot(plot_raw=True, apply_link=False) - m.plot(plot_raw=True, apply_link=True) + m.plot(plot_raw=False, apply_link=False, samples_likelihood=3) + m.plot(plot_raw=True, apply_link=False, samples=3) + m.plot(plot_raw=True, apply_link=True, samples=3) for do_test in _image_comparison(baseline_images=['sparse_gp_class_{}'.format(sub) for sub in ["likelihood", "raw", 'raw_link']], extensions=extensions): yield (do_test, ) diff --git a/GPy/testing/plotting_tests/baseline/sparse_gp_class_raw.png b/GPy/testing/plotting_tests/baseline/sparse_gp_class_raw.png index c188bb4e263ba43d4952df280f2c863179303a75..2b4eb5521565f6c8cd52ca875554b16089b04031 100644 GIT binary patch literal 22853 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 literal 15809 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 diff --git a/GPy/testing/plotting_tests/baseline/sparse_gp_class_raw_link.png b/GPy/testing/plotting_tests/baseline/sparse_gp_class_raw_link.png index 453941fc7d946d2b45ecf7badf93f6c9103ac453..7a969dc78cad3f6f9acee9c0ac31100b7ee6ba57 100644 GIT binary patch literal 24425 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

e@qT|;92?V>z?&({owNDmPoLDFVChB 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 E1NS&nKL7v# diff --git a/travis_tests.py b/travis_tests.py index 06d54de6..2d71de97 100644 --- a/travis_tests.py +++ b/travis_tests.py @@ -38,5 +38,5 @@ matplotlib.rcParams[u'figure.figsize'] = (4,3) matplotlib.rcParams[u'text.usetex'] = False import nose -nose.main('GPy', defaultTest='GPy/testing') +nose.main('GPy', defaultTest='GPy/testing/plotting_tests.py')