Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions optimizerapi/openapi/specification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ components:
type: string
experimentSuggestionCount:
type: number
maxQuality:
type: number
graphs:
type: array
default: ["objective", "convergence", "pareto"]
items:
type: string
additionalProperties:
type: string
data:
Expand Down
38 changes: 26 additions & 12 deletions optimizerapi/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import matplotlib.pyplot as plt
import numpy
import connexion

from optimizerapi.plot_patch import plot_brownie_bee
from .securepickle import pickleToString, get_crypto

numpy.random.seed(42)
Expand Down Expand Up @@ -224,6 +226,8 @@ def process_result(result, optimizer, dimensions, cfg, extras, data, space):
# behavior is "png", so the API returns png images. Any other input is interpreted
# as "None" at the moment.
graph_format = extras.get("graphFormat", "png")
max_quality = int(extras.get("maxQuality", "5"))
graphs_to_return = extras.get("graphs", ['objective', 'convergence', 'pareto'])

objective_pars = extras.get("objectivePars", "result")

Expand All @@ -247,25 +251,35 @@ def process_result(result, optimizer, dimensions, cfg, extras, data, space):
result_details["models"] = [process_model(model, optimizer) for model in result]
if graph_format == "png":
for idx, model in enumerate(result):
plot_convergence(model)
add_plot(plots, f"convergence_{idx}")

plot_objective(
model,
dimensions=dimensions,
usepartialdependence=False,
show_confidence=True,
pars=objective_pars,
)
add_plot(plots, f"objective_{idx}")
if 'single' in graphs_to_return:
bb_plots = plot_brownie_bee(model, max_quality=max_quality)
for i, plot in enumerate(bb_plots):
pic_io_bytes = io.BytesIO()
plot.savefig(pic_io_bytes, format="png")
pic_io_bytes.seek(0)
pic_hash = base64.b64encode(pic_io_bytes.read())
plots.append({"id": f"single_{idx}_{i}", "plot": str(pic_hash, "utf-8")})
if 'convergence' in graphs_to_return:
plot_convergence(model)
add_plot(plots, f"convergence_{idx}")

if 'objective' in graphs_to_return:
plot_objective(
model,
dimensions=dimensions,
usepartialdependence=False,
show_confidence=True,
pars=objective_pars,
)
add_plot(plots, f"objective_{idx}")

if optimizer.n_objectives == 1:
minimum = expected_minimum(result[0], return_std=True)
result_details["expected_minimum"] = [
round_to_length_scales(minimum[0], optimizer.space),
minimum[1],
]
else:
elif 'pareto' in graphs_to_return:
plot_Pareto(optimizer)
add_plot(plots, "pareto")

Expand Down
204 changes: 204 additions & 0 deletions optimizerapi/plot_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
"""Plotting functions."""
from scipy.stats import norm
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator, FuncFormatter
from ProcessOptimizer.space import (
Categorical
)
from ProcessOptimizer import expected_minimum
from ProcessOptimizer.plots import (
partial,
dependence,
_cat_format,
_map_categories
)

def plot_brownie_bee(
result,
n_points=40,
n_samples=250,
size=2,
max_quality=5,
):
"""Single factor dependence plot of the model intended for use with the
Brownie Bee user interface.
Each plot shows how quality depends on the dimension `i` when all other
factor values are locked to those of the expected minimum. A vertical line
indicates the location of the expected minimum for each factor.
Parameters
----------
* `result` [`OptimizeResult`]
The result for which to create the plots.
* `n_points` [int, default=40]
Number of points at which to evaluate the partial dependence
along each dimension.
* `n_samples` [int, default=250]
Number of random samples to use for averaging the model function
at each of the `n_points`.
* `size` [float, default=2]
Height (in inches) of each returned figure.
* `max_quality` [int, default=5]
The maximal quality obtainable in the setup of Brownie Bee. Quality is
assumed to be measured on a scale from 0 to this number, and the y-axis
of each plot is scaled to reflect this.
Returns
-------
* `plot_list`: [`Figures`]:
A list of individual matplotlib figure handles, one for each dimension
present in 'result' and a last one representing a histogram of samples
drawn at the expected minimum.
"""
# Here we define the value to highlight in each dimension. These
# same values will be used for evaluating the plots when calculating
# dependence. (Unless partial dependence is to be used instead).

space = result.space
# Check if we have any categorical dimensions, as this influences the plots
is_cat = [isinstance(dim, Categorical) for dim in space.dimensions]
# Identify the location of the expected minimum, and its mean and std
x_eval, [res_mean, res_std] = expected_minimum(
result,
n_random_starts=20,
random_state=None,
return_std=True,
)

rvs_transformed = space.transform(space.rvs(n_samples=n_samples))
_, minimum, _ = _map_categories(space, result.x_iters, x_eval)

# Gather all data relevant for plotting
plots_data = []
for i in range(space.n_dims):
row = []
xi, yi, stddevs = dependence(
space,
result.models[-1],
i,
j=None,
sample_points=rvs_transformed,
n_points=n_points,
x_eval=x_eval,
)
row.append({"xi": xi, "yi": yi, "std": stddevs})

plots_data.append(row)

# Create the list to store figure handles
figure_list = []

# Build all the plots in the figure
for n in range(space.n_dims):
# Prepare a figure
fig, ax_ = plt.subplots(
figsize=(size, size),
dpi=200,
)
# Set the padding
fig.subplots_adjust(
left=0.18, right=0.95, bottom=0.2, top=0.95, hspace=0.0, wspace=0.0
)

# Get data to plot in this subplot
xi = plots_data[n][0]["xi"]
yi = plots_data[n][0]["yi"]
stddevs = plots_data[n][0]["std"]

# Set y-axis limits
ax_.set_ylim(0, max_quality)

# Enter here when we plot a categoric factor
if is_cat[n]:
# Expand the x-axis for this factor so we can see the first
# and the last category
ax_.set_xlim(np.min(xi)-0.2, np.max(xi)+0.2)

# Highlight the expected minimum
ax_.axvline(minimum[n], linestyle="--", color="k", lw=1)
# Create one uniformly colored bar for each category.
# Edgecolor ensures we can see the bar when plotting
# at best obeservation, as stddev is often tiny there
ax_.bar(
xi,
2*1.96*stddevs,
width=0.2,
bottom=(-yi-1.96*stddevs),
alpha=0.5,
color="green",
edgecolor="green",
)

# For non-categoric factors
else:
ax_.set_xlim(np.min(xi), np.max(xi))
# Highlight the expected minimum
ax_.axvline(minimum[n], linestyle="--", color="k", lw=1)
# Show the uncertainty
ax_.fill_between(
xi,
y1=-(yi - 1.96*stddevs),
y2=-(yi + 1.96*stddevs),
alpha=0.5,
color="green",
edgecolor="green",
linewidth=0.0,
)

# Fix formatting of the y-axis with ticks from 0 to our max quality
ax_.yaxis.set_major_locator(MaxNLocator(5, integer=True))
ax_.tick_params(axis="y", direction="inout")
# Fix formatting of the x-axis
[labl.set_rotation(45) for labl in ax_.get_xticklabels()]

if space.dimensions[n].prior == "log-uniform":
ax_.set_xscale("log")
else:
ax_.xaxis.set_major_locator(
MaxNLocator(6, prune="both", integer=is_cat[n])
)
if is_cat[n]:
# Axes for categorical dimensions are really integers;
# we have to label them with the category names
ax_.xaxis.set_major_formatter(
FuncFormatter(
partial(_cat_format, space.dimensions[n])
)
)

# Add the figure to the output list
figure_list.append(fig)

# Prepare a figure for a histogram of expected quality
fig, ax_ = plt.subplots(
figsize=(size, size),
dpi=200,
)
# Set the padding
fig.subplots_adjust(
left=0.05, right=0.95, bottom=0.2, top=0.95, hspace=0.0, wspace=0.0
)
# Plot out to 3 standard deviations from the mean but not outside the scale
xi = np.linspace(
max(-res_mean-3*res_std, 0),
min(-res_mean+3*res_std, max_quality),
100,
)
# Create histogram y-values
yi = norm.pdf(xi, -res_mean, res_std)
# Build the plot
ax_.fill_between(
xi,
y1=np.zeros((len(xi),)),
y2=yi,
alpha=0.5,
color="blue",
edgecolor="blue",
linewidth=0.0,
)
# Cosmetics
ax_.get_yaxis().set_visible(False)

# Add the figure to the output list
figure_list.append(fig)

return figure_list