Open NicolasRouquette opened 1 year ago
This would be a great feature! A related feature that I have worked on would implement something like:
def plot_guarantees(
contract: List[PolyhedralContract],
x_var: Var,
y_vars: Var,
var_values: List[Dict[Var, numeric]],
x_lims: List[Dict[Var, Tuple[numeric, numeric]]],
y_lims: List[Tuple[numeric, numeric]],
) -> MplFigure
Here a list of contracts is passed in and the resulting plot is a stitched matplotlib figure of the listed contracts with corresponding var_values, x_lims, y_lims. It might make the plot_guarantees
function too overloaded so I'm open to adding this as a new function.
@iincer @ayush9pandey What do you think of the following plot_input_output_polyhedral_term_list
and plot_input_outputs_polyhedral_term_list
API below?
The current plotting API requires providing bounds for x
and y
; we could instead ask Pacti to calculate them as done below:
# Add a callback function for the mouse click event
def _on_hover(ptl: PolyhedralTermList, x_var: Var, y_var: Var, fig, ax: Axes, event):
if event.inaxes == ax:
x_coord, y_coord = event.xdata, event.ydata
y_min, y_max = calculate_output_bounds_for_input_value(
ptl, {x_var: x_coord}, y_var
)
ax.set_title(
f"@ {x_var.name}={x_coord:.2f}\n{y_min:.2f} <= {y_var.name} <= {y_max:.2f}"
)
fig.canvas.draw_idle()
def plot_input_output_polyhedral_term_list(
ptl: PolyhedralTermList, x_var: Var, y_var: Var
) -> MplFigure:
"""
In a Jupyter notebook, show a 2-D plot of the input/output relationship
among variables of a polyhedral term list.
Clicking in an area of the 2-D plot shows the bounds of the output variable
corresponding to the value of the input variable corresponding to the location
of the mouse click.
Args:
ptl: PolyhedralTermList
A polyhedral term list of two variables only: x_var and y_var.
x_var: Var
The input variable.
y_var: Var
The output variable.
Returns:
A 2-D plot figure for interactive visualization in a Jupyter notebook.
"""
x_lims = get_bounds(ptl, x_var.name)
y_lims = get_bounds(ptl, y_var.name)
res_tuple = PolyhedralTermList.termlist_to_polytope(ptl, PolyhedralTermList([]))
# variables = res_tuple[0]
a_mat = res_tuple[1]
b = res_tuple[2]
x, y = plh_plots._get_bounding_vertices(a_mat, b)
# generate figure
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, aspect="equal")
ax.set_xlim(x_lims)
ax.set_ylim(ymin=y_lims[0], ymax=y_lims[1])
ax.set_xlabel(x_var.name)
ax.set_ylabel(y_var.name)
ax.set_aspect((x_lims[1] - x_lims[0]) / (y_lims[1] - y_lims[0]))
poly = MplPatchPolygon(
xy=np.column_stack([x, y]).tolist(),
# xy=np.column_stack([x, y]).flatten().tolist(),
animated=False,
closed=True,
facecolor="deepskyblue",
edgecolor="deepskyblue",
)
ax.add_patch(poly)
# Connect the event to the callback function
fig.canvas.mpl_connect(
"button_press_event", lambda event: _on_hover(ptl, x_var, y_var, fig, ax, event)
)
return fig
The button_press_event
callback function, _on_hover
, will add a text with the calculated bound for the corresponding x
coordinates of the mouse click.
I have generalized this for a list of y
variables like this:
def _on_hovers(ptls: List[PolyhedralTermList], x_var: Var, y_vars: List[Var], fig, axl: List[Axes], event):
if event.inaxes in axl:
x_coord = event.xdata
for i, ax in enumerate(axl):
y_min, y_max = calculate_output_bounds_for_input_value(
ptls[i], {x_var: x_coord}, y_vars[i]
)
axl[i].set_title(
f"@ {x_var.name}={x_coord:.2f}\n{y_min:.2f} <= {y_vars[i].name} <= {y_max:.2f}"
)
fig.canvas.draw_idle()
def retain_constraints_involving_variables(
ptl: PolyhedralTermList, vs: List[Var]
) -> PolyhedralTermList:
"""
Retain only the constraints (terms) involving all variables in the provided list.
Args:
ptl: PolyhedralTermList
The initial polyhedral term list.
vs: List[Var]
List of variables. Only terms involving all variables from this list are retained.
Returns:
A new PolyhedralTermList containing only the terms involving all variables from the list.
"""
ts = []
for term in ptl.terms:
if all(term.contains_var(var_to_seek=v) for v in vs):
ts.append(term)
elif any(term.contains_var(var_to_seek=v) for v in vs) and all(
v in vs for v in term.variables.keys()
):
ts.append(term)
return PolyhedralTermList(terms=ts)
def plot_input_outputs_polyhedral_term_list(
ptl: PolyhedralTermList, x_var: Var, y_vars: List[Var]
) -> MplFigure:
"""
In a Jupyter notebook, show a 2-D plot of the input/output relationship
among variables of a polyhedral term list.
Clicking in an area of the 2-D plot shows the bounds of the output variable
corresponding to the value of the input variable corresponding to the location
of the mouse click.
Args:
ptl: PolyhedralTermList
A polyhedral term list of two variables only: x_var and y_var.
x_var: Var
The input variable.
y_vars: Var
The output variables.
Returns:
A stacked figure of a 2-D plot figure for each output variable
as a function of the input variable.
This stacked figure supports interactive visualization in a Jupyter notebook.
"""
num_plots = len(y_vars)
x_lims = get_bounds(ptl, x_var.name)
fig, axs = plt.subplots(
nrows=num_plots, ncols=1,
figsize=(6, 4 * num_plots),
constrained_layout=True
)
ptls: List[PolyhedralTermList] = []
axl: List[Axes] = []
for i, y_var in enumerate(y_vars):
y_ptl: PolyhedralTermList = retain_constraints_involving_variables(
ptl, [x_var, y_var]
)
ptls.append(y_ptl)
y_lim: tuple[float, float] = get_bounds(y_ptl, y_var.name)
res_tuple = PolyhedralTermList.termlist_to_polytope(
y_ptl, PolyhedralTermList([])
)
# variables = res_tuple[0]
a_mat: np.ndarray = res_tuple[1]
b: np.ndarray = res_tuple[2]
try:
x, y = plh_plots._get_bounding_vertices(a_mat, b)
ax: Axes = axs[i]
ax.set_xlim(x_lims)
ax.set_ylim(ymin=y_lim[0], ymax=y_lim[1])
ax.set_xlabel(x_var.name)
ax.set_ylabel(y_var.name)
ax.set_aspect((x_lims[1] - x_lims[0]) / (y_lim[1] - y_lim[0]))
poly = MplPatchPolygon(
xy=np.column_stack([x, y]).tolist(),
# xy=np.column_stack([x, y]).flatten().tolist(),
animated=False,
closed=True,
facecolor="deepskyblue",
edgecolor="deepskyblue",
)
ax.add_patch(poly)
axl.append(ax)
except QhullError as e:
print(f"x_var: {x_var.name}, y_var: {y_var.name}")
print(f"term list\n" + show_termlist(y_ptl))
print(f"QhullError: {e}")
pass
except IndexError as e:
print(f"x_var: {x_var.name}, y_var: {y_var.name}")
print(f"term list\n" + show_termlist(y_ptl))
print(f"IndexError: {e}")
pass
# Connect the event to the callback function
fig.canvas.mpl_connect(
"button_press_event", lambda event: _on_hovers(ptls, x_var, y_vars, fig, axl, event)
)
return fig
Here is an example where the PolyhedralTermList
is the following:
[
-d_var <= 0
d_var + 4 t_var <= 40
-d_var - 5 t_var <= -40
-duration <= -0
0.2 duration <= 80
-duration + t_var <= 0
r_var = 80
soc_var + 0.1 t_var <= 80
-soc_var - 0.2 t_var <= -80
-t_var <= 0
0.1 t_var - u_var <= -20
-0.2 t_var + u_var <= 20
u_var <= 100
]
I get this:
The 1st and 3rd plots show the effect of the _on_hovers
callback where I clicked somewhere in the plot area and each plot shows the bound of the corresponding output variable for the x
coordinate of the mouse click.
The 2nd plot does now show anything because pacti.terms.polyhedra.plots._get_bounding_vertices
does not handle the degenerate case where the output variable is a constant:
x_var: t_var, y_var: r_var
term list
[
r_var = 80
-t_var <= 0
]
Hi @NicolasRouquette, I think the functionality to interactively show values and bounds could be very useful. A couple of comments/questions:
x_lims = get_bounds(ptl, x_var.name)
y_lims = get_bounds(ptl, y_var.name)
would fail, so we would have to add cases to handle this.
Regarding your first point, the main advantage stems from the interactive hover that uses the same x-coordinate of the mouse click to compute the ranges for all plots. The screenshot shows an example where the x-coordinate corresponds to t_var=6.10
and each plot shows the corresponding range for the different variables, r_var
and d_var
.
Regarding your second point: clearly, we need to improve the support for corner cases:
Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
The current
pacti.terms.polyhedra.plots
API supports plotting 1 variable as a function of another. I would like support for plotting multiple variables as a function of a single variable.Describe the solution you'd like A clear and concise description of what you want to happen.
Expand the current API:
To something like this:
Expand the current API:
In either form, this would produce a stacked list of plots, one for each
y_vars
as a function ofx_var
.Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
Additional context Add any other context or screenshots about the feature request here.