ConstrainedZonotope¶
This python notebook goes over various functionalities of pycvxset
for manipulating and plotting constrained zonotopes.
License information: pycvxset code is released under AGPL-3.0-or-later
license, as found in the LICENSE.md file. The documentation for pycvxset is released under CC-BY-4.0
license, as found in the LICENSES/CC-BY-4.0.md.
Constrained zonotope characterizes a convex and compact set in the working dimension $\mathbb{R}^n$, and is an affine transformation of a constrained zonotope defined in latent space $B_\infty(A_e, b_e)\subset \mathbb{R}^{N_C}$. Here, $B_\infty(A_e, b_e)$ is defined as the intersection of a unit $\ell_\infty$-norm ball and a collection of $M_C$ linear constraints. Formally, $$\mathcal{C} = \{G \xi + c | \xi \in B_\infty(A_e, b_e)\} \subset \mathbb{R}^n,$$ where $$B_\infty(A_e, b_e) = \{\xi | \| \xi \|_\infty \leq 1, A_e \xi = b_e\} \subset \mathbb{R}^{N_C}.$$
Note that every constrained zonotope is a polytope, and vice-versa. Consequently, pycvxset
uses Python's duck typing to treat Polytope
and ConstrainedZonotope
objects (almost) equally.
We organize this notebook on pycvxset
operations on ConstrainedZonotopes as follows:
The links below are for the HTML page in the documentation website, and they will not work in the Jupyter notebook.
- Representations and plotting
- Operations involving a single constrained zonotope
- Affine transformation
- Inverse affine transformation with an invertible linear map
- Projection of a constrained zonotope
- Project a point on to a constrained zonotope
- Containment: Check if a point is in a constrained zonotope
- Support function computation
- Chebyshev centering
- Maximum volume inscribing ellipsoid
- Minimum volume circumscribing rectangle
- Interior point computation
- Volume computation
- Cartesian product with itself
- Operations involving two constrained zonotopes or a constrained zonotope and another set
Representations and plotting¶
In preparation to run the notebook, we import the necessary packages.
from pycvxset import (
Ellipsoid,
Polytope,
ConstrainedZonotope,
approximate_volume_from_grid,
spread_points_on_a_unit_sphere,
)
import numpy as np
import matplotlib.pyplot as plt
We also set up matplotlib to have a standard figure size, and call matplotlib widget to enable 3D plotting in the notebook.
plt.rcParams["figure.figsize"] = [5, 3]
plt.rcParams["figure.dpi"] = 150
%matplotlib widget
Constrained zonotopes and zonotopes¶
We can define constrained zonotopes (and zonotopes) directly in pycvxset
.
C1 = ConstrainedZonotope(G=np.eye(2), c=[0, 0], Ae=[1, 0], be=[1])
print("C1 is a", repr(C1))
C2 = ConstrainedZonotope(G=np.eye(2), c=[0, 0])
print("C2 is a", repr(C2))
C1 is a Constrained Zonotope in R^2 with latent dimension 2 and 1 equality constraint C2 is a Constrained Zonotope in R^2 that is a zonotope with latent dimension 2
C1a = ConstrainedZonotope(
G=np.hstack((np.eye(2), 2 * np.eye(2))),
c=[0, 0],
Ae=np.eye(4),
be=[1, 1, 1, 1],
)
print("C1a is a", repr(C1a))
print("C1a.G.shape", C1a.G.shape)
print("C1a.Ae.shape", C1a.Ae.shape)
C1a.remove_redundancies()
print("C1a is a", repr(C1a))
print("C1a.G.shape", C1a.G.shape)
print("C1a.Ae.shape", C1a.Ae.shape)
C1a is a Constrained Zonotope in R^2 with latent dimension 4 and 4 equality constraints C1a.G.shape (2, 4) C1a.Ae.shape (4, 4) C1a is a Constrained Zonotope in R^4 that is a zonotope representing a single point C1a.G.shape (4, 0) C1a.Ae.shape (0, 0)
Using polytopes¶
We create a $\mathbb{R}^3$-dimensional constrained zonotope C_3D
from the corresponding polytope P_hrep
. We confirm that the constrained zonotope is three-dimensional.
You could also define a polytope in V-rep and construct a constrained zonotope.
A = np.array(
[[1, 0, 0], [0, 1, 0], [0, 0, 1], [-1, 0, 0], [0, -1, 0], [0, 0, -1]]
)
b = [2, 3, 1, 2, 3, 1]
P_3D = Polytope(A=A, b=b)
C_3D = ConstrainedZonotope(polytope=P_3D)
print(repr(C_3D))
print(f"The dimension of C_3D is {C_3D.dim:d}.")
print("G", np.array2string(C_3D.G, precision=2, suppress_small=True))
print("c", np.array2string(C_3D.c, precision=2, suppress_small=True))
print("Ae", np.array2string(C_3D.Ae, precision=2, suppress_small=True))
print("be", np.array2string(C_3D.be, precision=2, suppress_small=True))
print("He", np.array2string(C_3D.He, precision=2, suppress_small=True))
print("is_bounded?", C_3D.is_bounded)
print("is_full_dimensional?", C_3D.is_full_dimensional)
Constrained Zonotope in R^3 with latent dimension 9 and 6 equality constraints The dimension of C_3D is 3. G [[2. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 3. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 1. 0. 0. 0. 0. 0. 0.]] c [-0. -0. 0.] Ae [[ 2. 0. 0. -2. 0. 0. 0. 0. 0.] [ 0. 3. 0. 0. -3. 0. 0. 0. 0.] [ 0. 0. 1. 0. 0. -1. 0. 0. 0.] [-2. 0. 0. 0. 0. 0. -2. 0. 0.] [ 0. -3. 0. 0. 0. 0. 0. -3. 0.] [ 0. 0. -1. 0. 0. 0. 0. 0. -1.]] be [0. 0. 0. 0. 0. 0.] He [[ 2. 0. 0. -2. 0. 0. 0. 0. 0. 0.] [ 0. 3. 0. 0. -3. 0. 0. 0. 0. 0.] [ 0. 0. 1. 0. 0. -1. 0. 0. 0. 0.] [-2. 0. 0. 0. 0. 0. -2. 0. 0. 0.] [ 0. -3. 0. 0. 0. 0. 0. -3. 0. 0.] [ 0. 0. -1. 0. 0. 0. 0. 0. -1. 0.]] is_bounded? True is_full_dimensional? True
Plotting a constrained zonotope¶
Next, we plot the constrained zonotope. Note that pycvxset
internally performs a polytopic approximation (by default inner, but an outer-approximation can be requested as well) in order to plot the constrained zonotope C_3D
.
ax, _, _ = C_3D.plot(patch_args={"label": "C_3D"})
ax.legend()
ax.set_title("Plotting C_3D")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z");
Generating direction vectors for 3D plotting¶
Internally, pycvxset
spreads points on a 3-dimensional unit sphere, and then performs ray-shooting. This is accomplished by using spread_points_on_a_unit_sphere
of pycvxset
and extreme
method of ConstrainedZonotope
.
The plot below shows a set of vectors spread on the unit sphere.
dir_vectors = spread_points_on_a_unit_sphere(3)[0]
plt.figure()
ax = plt.subplot(111, projection="3d")
ax.scatter(dir_vectors[:, 0], dir_vectors[:, 1], dir_vectors[:, 2])
ax.set_aspect("equal")
Since the computation of these points takes few seconds, we will make use of dir_vectors
in all future 3D plots.
Axis-aligned cuboids¶
Next, we define an axis-aligned cuboid using the bounds. Internally, pycvxset
stores the constrained zonotope in H-representation.
One can also define an axis-aligned cuboid based on their center and half-sides.
axis_aligned_cuboid_from_bounds = ConstrainedZonotope(
lb=[-1, 2], ub=[5, 4]
)
print(repr(axis_aligned_cuboid_from_bounds))
# Plot axis_aligned_cuboid_from_bounds
ax, _, _ = axis_aligned_cuboid_from_bounds.plot(
patch_args={"label": "Rectangle"}, vertex_args={"visible": True}
)
axis_aligned_cuboid_from_center_and_sides = ConstrainedZonotope(
c=[0, 0], h=1
)
print(repr(axis_aligned_cuboid_from_center_and_sides))
# Plot axis_aligned_cuboid_from_center_and_sides
axis_aligned_cuboid_from_center_and_sides.plot(
ax=ax,
patch_args={"label": "Cube", "facecolor": "m"},
vertex_args={"visible": True},
)
ax.set_aspect("equal")
ax.legend(loc="best")
ax.set_title("Axis-aligned cuboids");
Constrained Zonotope in R^2 that is a zonotope with latent dimension 2
Constrained Zonotope in R^2 that is a zonotope with latent dimension 2
Empty constrained zonotope (and checking for emptiness)¶
We can also define an empty constrained zonotope of desired dimension as well. pycvxset
can also check if a constrained zonotope is empty or
nonempty.
empty_constrained_zonotope = ConstrainedZonotope(dim=3)
print(empty_constrained_zonotope)
print(
"Is empty_constrained_zonotope empty?",
empty_constrained_zonotope.is_empty,
)
Constrained Zonotope (empty) in R^3 Is empty_constrained_zonotope empty? True
We also confirm if previously defined constrained zonotopes were not empty.
print("Was C_3D empty?", C_3D.is_empty)
print(
"Was axis_aligned_cuboid_from_bounds empty?",
axis_aligned_cuboid_from_bounds.is_empty,
)
print(
"Was axis_aligned_cuboid_from_center_and_sides empty?",
axis_aligned_cuboid_from_center_and_sides.is_empty,
)
Was C_3D empty? False Was axis_aligned_cuboid_from_bounds empty? False Was axis_aligned_cuboid_from_center_and_sides empty? False
Embedded constrained zonotopes¶
Similarly to Polytope
class, we can embed a lower-dimensional constrained zonotope in a higher-dimensional space.
unit_box_in_3D = ConstrainedZonotope(c=[0, 0, 0], h=1)
print(repr(unit_box_in_3D))
low_dim_P_in_high_dim_space = unit_box_in_3D.intersection_with_affine_set(
Ae=[0.03, 0, 0.2], be=[0]
)
print(repr(low_dim_P_in_high_dim_space))
Constrained Zonotope in R^3 that is a zonotope with latent dimension 3 Constrained Zonotope in R^3 with latent dimension 3 and 1 equality constraint
However, plotting low-dimensional constrained zonotopes embedded in high-dimensional volumes is difficult since ray shooting approach may completely miss the affine set in which the constrained zonotope lives.
Operations involving a single constrained zonotope¶
We provide examples for the following operations supported by pycvxset
on the ConstrainedZonotope
objects.
- Affine transformation
- Inverse affine transformation with an invertible linear map
- Projection of a constrained zonotope
- Project a point on to a constrained zonotope
- Support function computation
- Chebyshev centering
- Maximum volume inscribing ellipsoid
- Minimum volume circumscribing ellipsoid
- Minimum volume circumscribing rectangle
- Containment: Check if a point is in a constrained zonotope
- Volume computation
- Interior point computation
For constrained zonotope transformations, we will use OLD_POLYTOPE_COLOR_1
to denote the old constrained zonotope and NEW_POLYTOPE_COLOR
to denote the new constrained zonotope.
OLD_CONSTRAINED_ZONOTOPE_COLOR_1 = "skyblue"
NEW_CONSTRAINED_ZONOTOPE_COLOR = "lightgray"
Affine transformation¶
Affine transformations of constrained zonotopes can rotate, shear, and translate constrained zonotopes.
Recall that rotation matrix $R(\theta)=\left[\begin{array}{cc}\cos(\theta)&-\sin(\theta)\\\sin(\theta)&\cos(\theta)\end{array}\right]$ rotates a 2D vector by $\theta\in(-\pi,\pi]$. Thus, given a 2D constrained zonotope $\mathcal{P}$, we are interested in computing the constrained zonotope $$\mathcal{Q}=R(\theta)\mathcal{P} + \left[\begin{array}{c} 2\\-2\end{array}\right].$$
Similarly to Polytope
class, the ConstrainedZonotope
class overloads the operator @
and +
to accomplish these transformations. Alternatively, you can use affine_map
and plus
methods.
P_from_V = Polytope(V=[[0, 0], [0, 1], [2, 0]])
CZ = ConstrainedZonotope(polytope=P_from_V)
rotation_degree = 45
rotation_radians = np.deg2rad(rotation_degree)
rotation_matrix = np.array(
[
[np.cos(rotation_radians), -np.sin(rotation_radians)],
[np.sin(rotation_radians), np.cos(rotation_radians)],
]
)
translation = np.array([[2, -2]]).T
# Computation via operator overloading
new_constrained_zonotope_from_affine_transformation = (
rotation_matrix @ CZ + translation
)
patch_args_old = {
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_1,
"label": "Original constrained zonotope",
}
patch_args_new = {
"facecolor": NEW_CONSTRAINED_ZONOTOPE_COLOR,
"label": "Affine transformed constrained zonotope",
}
ax, _, _ = CZ.plot(patch_args=patch_args_old)
new_constrained_zonotope_from_affine_transformation.plot(
ax=ax, patch_args=patch_args_new
)
ax.legend()
ax.set_title("Affine transformation")
# Explicit computation via methods
new_constrained_zonotope_from_affine_transformation_explicit = (
CZ.affine_map(rotation_matrix).plus(translation)
)
print(
"Are constrained zonotopes obtained from these methods the same?",
new_constrained_zonotope_from_affine_transformation
== new_constrained_zonotope_from_affine_transformation_explicit,
)
Set parameter ServerPassword
Set parameter TokenServer to value "atlas3.merl.com"
Are constrained zonotopes obtained from these methods the same? True
patch_args_old = {
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_1,
"label": "Original constrained zonotope",
}
patch_args_new = {
"facecolor": NEW_CONSTRAINED_ZONOTOPE_COLOR,
"label": "Affine transformed constrained zonotope",
}
C = Polytope(V=spread_points_on_a_unit_sphere(2, 7)[0])
C = ConstrainedZonotope(polytope=C)
new_C = [[2, 0], [0, 0.5]] @ C
ax, _, _ = C.plot(patch_args=patch_args_old)
new_C.plot(ax=ax, patch_args=dict(patch_args_new, **{"alpha": 0.5}))
ax.legend(loc="lower right", bbox_to_anchor=(1, 0))
ax.grid()
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title("Grow in x, but shrink in y");
Negation and scaling by a scalar¶
negated_C = -C
scaled_C = 0.2 * C
ax = C.plot(
patch_args={
"alpha": 0.4,
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_1,
"label": "Original",
}
)[0]
negated_C.plot(
ax=ax,
patch_args={
"alpha": 0.4,
"facecolor": NEW_CONSTRAINED_ZONOTOPE_COLOR,
"label": "-C",
},
)
scaled_C.plot(
ax=ax,
patch_args={"alpha": 0.4, "facecolor": "lightgreen", "label": "0.2C"},
)
ax.legend()
<matplotlib.legend.Legend at 0x7bb73239cd90>
Inverse affine transformation¶
We can also undo an affine transformation when it is invertible.
Note that the ConstrainedZonotope
class overloads the operator @
and -
to accomplish these transformations. Alternatively, you can use inverse_affine_map
and minus
methods.
We use ==
to check if constrained zonotopes are equal. For more details, see containment.
old_constrained_zonotope_from_new_constrained_zonotope = (
new_constrained_zonotope_from_affine_transformation - translation
) @ rotation_matrix
old_constrained_zonotope_from_new_constrained_zonotope_explicit = (
new_constrained_zonotope_from_affine_transformation.minus(
translation
).inverse_affine_map_under_invertible_matrix(rotation_matrix)
)
print(
"Are constrained zonotopes obtained from these methods the same?",
old_constrained_zonotope_from_new_constrained_zonotope_explicit
== old_constrained_zonotope_from_new_constrained_zonotope,
)
print(
"Did we get the old constrained zonotope back?",
old_constrained_zonotope_from_new_constrained_zonotope_explicit == CZ,
)
patch_args_new = {
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_1,
"label": "Affine transformed constrained zonotope",
}
patch_args_new_undo = {
"facecolor": NEW_CONSTRAINED_ZONOTOPE_COLOR,
"label": "Undo the affine transformation",
}
ax, _, _ = new_constrained_zonotope_from_affine_transformation.plot(
patch_args=patch_args_new
)
old_constrained_zonotope_from_new_constrained_zonotope_explicit.plot(
ax=ax, patch_args=patch_args_new_undo
)
ax.legend()
ax.set_title("Inverse affine transformation");
Are constrained zonotopes obtained from these methods the same? True
Did we get the old constrained zonotope back? True
Projection of a constrained zonotope¶
Given a constrained zonotope $\mathcal{P}\subset\mathbb{R}^n$, the (orthogonal) projection of a constrained zonotopes is defined as $$\mathcal{R} = \{r \in \mathbb{R}^{m}: \exists v \in \mathbb{R}^{n - m},\ \mathrm{Lift}(r,v)\in \mathcal{P}\}$$ Here, $m = \mathcal{P}.dim - \text{length}(\text{project\_away\_dimensions})$, and $\mathrm{Lift}(r,v)$ lifts (undoes the projection) by combining $r$ and $v$ to produce a $n$-dimensional vector.
You can rotate the 3D plots using your mouse.
WARNING: You will see issues with matplotlib's rendering when you rotate the figure (see the second plot). See https://matplotlib.org/stable/api/toolkits/mplot3d/faq.html#my-3d-plot-doesn-t-look-right-at-certain-viewing-angles for more details. When a visual comparison of the plots is desired, it may be better to just plot the frame by setting patch_args['facecolor]=None
. This issue does not appear for 2D plotting.
L1_norm_ball_polytope = Polytope(V=np.vstack((np.eye(3), -np.eye(3))))
L1_norm_ball = ConstrainedZonotope(polytope=L1_norm_ball_polytope)
simplex_in_R_pos_polytope = Polytope(V=np.vstack((np.eye(3), [0, 0, 0])))
simplex_in_R_pos = ConstrainedZonotope(polytope=simplex_in_R_pos_polytope)
constrained_zonotope_list = [
L1_norm_ball,
simplex_in_R_pos,
simplex_in_R_pos,
]
view_dict = [
{"elev": 21, "azim": -120},
{"elev": 21, "azim": -35},
{"elev": 21, "azim": -35},
]
facecolor_3d_list = ["lightblue", "lightblue", None]
for C, view_dict, facecolor_3d in zip(
constrained_zonotope_list, view_dict, facecolor_3d_list
):
fig = plt.figure()
ax = fig.add_subplot(1, 2, 1, projection="3d")
ax.view_init(**view_dict)
C.plot(
ax=ax,
direction_vectors=dir_vectors,
patch_args={"facecolor": facecolor_3d},
)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z")
ax.set_aspect("equal")
ax.set_title("3D constrained zonotope")
P_projection = C.projection(project_away_dims=2)
ax2d = fig.add_subplot(1, 2, 2)
P_projection.plot(ax=ax2d)
ax2d.set_xlabel("x")
ax2d.set_ylabel("y")
ax2d.set_aspect("equal")
ax2d.set_title("Projection on x-y")
plt.subplots_adjust(wspace=1)
Project a point on to a constrained zonotope¶
Given a constrained zonotope $\mathcal{P}\subset\mathbb{R}^n$ and a point $y\in\mathbb{R}^n$, the projection of the point on to $\mathcal{P}$ is defined as the optimal solution to the optimization problem $x^\ast = \arg\inf_{x\in\mathcal{P}} \|x - y\|_2$.
The ConstrainedZonotope
class also provides closest_point
and distance
methods that are wrapper to project
method. Projection can also be computed using 1
and inf
norms.
L1_norm_ball_polytope = Polytope(V=np.vstack((np.eye(2), -np.eye(2))))
L1_norm_ball = ConstrainedZonotope(polytope=L1_norm_ball_polytope)
point_to_project = np.array([1, 0.5])
projected_point, distance_to_constrained_zonotope = L1_norm_ball.project(
point_to_project
)
print(
f"Distance of point to project {np.array2string(point_to_project)} "
f"to the constrained zonotope: {distance_to_constrained_zonotope[0]:1.2f}"
)
print(
"Did closest_point return the same point?",
np.allclose(
projected_point, L1_norm_ball.closest_point(point_to_project)
),
)
print(
"Did distance return the same distance?",
np.allclose(
distance_to_constrained_zonotope,
L1_norm_ball.distance(point_to_project),
),
)
patch_args = {
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_1,
"label": "L1 norm ball",
}
ax, _, _ = L1_norm_ball.plot(patch_args=patch_args)
ax.scatter(
point_to_project[0],
point_to_project[1],
color="red",
label="Point to project",
)
ax.scatter(
projected_point[0, 0],
projected_point[0, 1],
color="k",
label="Projected point",
)
ax.plot(
[point_to_project[0], projected_point[0, 0]],
[point_to_project[1], projected_point[0, 1]],
"k--",
label="Distance",
)
ax.legend(bbox_to_anchor=(1, 1))
ax.set_aspect("equal")
ax.grid()
plt.subplots_adjust(right=0.7)
ax.set_title("Projection of a point on a constrained zonotope");
Distance of point to project [1. 0.5] to the constrained zonotope: 0.35 Did closest_point return the same point? True Did distance return the same distance? True
Containment: Check if a point is in a constrained zonotope¶
Given a constrained zonotope $\mathcal{P}\subset\mathbb{R}^n$ and a point $y\in\mathbb{R}^n$, we can check if $y\in\mathcal{P}$.
The ConstrainedZonotope
class also overloads in
, <
, <=
operator for this purpose.
L1_norm_ball_polytope = Polytope(V=np.vstack((np.eye(2), -np.eye(2))))
L1_norm_ball = ConstrainedZonotope(polytope=L1_norm_ball_polytope)
point_to_project = np.array([1, 0.5])
projected_point = L1_norm_ball.closest_point(point_to_project)
print(
"Does the unit l1-norm ball contain point_to_project? "
f"{L1_norm_ball.contains(point_to_project)}"
)
print(
"Is point_to_project in the unit l1-norm ball? "
f"{point_to_project in L1_norm_ball}"
)
print(
"Is point_to_project <= the unit l1-norm ball? "
f"{point_to_project <= L1_norm_ball}"
)
print(
"Is point_to_project < the unit l1-norm ball? "
f"{point_to_project < L1_norm_ball}"
)
print(
"\nDoes the unit l1-norm ball contain projected_point? "
f"{L1_norm_ball.contains(projected_point)}"
)
print(
"Is projected_point in the unit l1-norm ball? "
f"{projected_point in L1_norm_ball}"
)
print(
"Is projected_point <= the unit l1-norm ball? "
f"{projected_point <= L1_norm_ball}"
)
print(
"Is projected_point < the unit l1-norm ball? "
f"{projected_point < L1_norm_ball}"
)
Does the unit l1-norm ball contain point_to_project? False Is point_to_project in the unit l1-norm ball? False Is point_to_project <= the unit l1-norm ball? False Is point_to_project < the unit l1-norm ball? False Does the unit l1-norm ball contain projected_point? True Is projected_point in the unit l1-norm ball? True Is projected_point <= the unit l1-norm ball? True Is projected_point < the unit l1-norm ball? True
Support function¶
Given a constrained zonotope $\mathcal{P}\subset\mathbb{R}^n$ and a vector $\eta\in\mathbb{R}^n$, the support function of the constrained zonotope is defined as $\rho_{\mathcal{P}}=\sup_{x\in\mathcal{P}} \eta^\top x$, and its optimal solution as the support vector. Recall that $$\mathcal{P}\subset\{x:\eta^\top x \leq \rho_{\mathcal{P}}(\eta)\}$$ for any direction $\eta\in\mathbb{R}^n$.
The ConstrainedZonotope
class also provides extreme
as a wrapper method to the support
method to directly compute the support vectors.
P = Polytope(V=spread_points_on_a_unit_sphere(2, 7)[0])
C = np.diag([0.5, 0.75]) @ ConstrainedZonotope(polytope=P)
direction_vectors = np.array([[0, -1], [0, 1], [1, 0.5], [-2.5, -1]])
support_evaluations, support_points = C.support(direction_vectors)
outer_approximating_polytope = Polytope(
A=direction_vectors, b=support_evaluations
)
ax, _, _ = C.plot(
patch_args={"facecolor": "lightgreen", "label": "ConstrainedZonotope"}
)
outer_approx_patch_args = {
"facecolor": "lightblue",
"alpha": 0.4,
"label": "Outer-approx. polytope",
}
outer_approximating_polytope.plot(
ax=ax, patch_args=outer_approx_patch_args
)
ax.scatter(
support_points[:, 0],
support_points[:, 1],
color="red",
label="Support points",
)
ax.set_aspect("equal")
ax.grid()
ax.legend(bbox_to_anchor=(0.8, 1))
plt.subplots_adjust(right=0.6)
print(
"Did the extreme function return the same support points?",
np.allclose(C.extreme(direction_vectors), support_points),
)
ax.set_title("Support function/vector evaluation");
Did the extreme function return the same support points? True
CZ1 = ConstrainedZonotope(
polytope=np.diag([2, 0.75])
@ Polytope(V=spread_points_on_a_unit_sphere(2, 7)[0])
)
CZ2 = ConstrainedZonotope(
G=np.hstack((np.eye(2), 3 * np.eye(2))),
c=[10, 10],
Ae=[1, 1, 1, 1],
be=2.85,
)
for P in [CZ1, CZ2]:
c, r = P.chebyshev_centering()
ax, _, _ = P.plot(
patch_args={"facecolor": "lightgreen", "label": "Polytope"}
)
cheby_ball = Ellipsoid(c=c, r=r)
cheby_ball.plot(
ax=ax,
patch_args={"facecolor": None, "label": "Chebyshev ball"},
center_args={"color": "k", "label": "Chebyshev center"},
)
ax.set_aspect("equal")
ax.grid()
ax.legend(loc="lower right", bbox_to_anchor=(1.2, 0))
plt.subplots_adjust(right=0.8)
ax.set_title("Chebyshev centering");
/home/vinod/workspace/git/PaperCodes/general/pycvxset/pycvxset/ConstrainedZonotope/operations_unary.py:78: UserWarning: This function returns a sub-optimal but feasible solution for Chebyshev centering. warnings.warn("This function returns a sub-optimal but feasible solution for Chebyshev centering.", UserWarning)
P = Polytope(V=spread_points_on_a_unit_sphere(2, 7)[0])
C = np.diag([0.5, 0.75]) @ ConstrainedZonotope(polytope=P)
direction_vectors = np.array([[0, -1], [0, 1], [1, 0.5], [-2.5, -1]])
support_evaluations, support_points = C.support(direction_vectors)
outer_approximating_polytope = Polytope(
A=direction_vectors, b=support_evaluations
)
ax, _, _ = C.plot(
patch_args={"facecolor": "lightgreen", "label": "ConstrainedZonotope"}
)
outer_approx_patch_args = {
"facecolor": "lightblue",
"alpha": 0.4,
"label": "Outer-approx. polytope",
}
outer_approximating_polytope.plot(
ax=ax, patch_args=outer_approx_patch_args
)
ax.scatter(
support_points[:, 0],
support_points[:, 1],
color="red",
label="Support points",
)
ax.set_aspect("equal")
ax.grid()
ax.legend(bbox_to_anchor=(0.8, 1))
plt.subplots_adjust(right=0.6)
print(
"Did the extreme function return the same support points?",
np.allclose(C.extreme(direction_vectors), support_points),
)
ax.set_title("Support function/vector evaluation");
Did the extreme function return the same support points? True
Maximum volume inscribing ellipsoid¶
We can compute the maximum volume inscribing ellipsoid center (approximately) of a constrained zonotope.
CZ1 = ConstrainedZonotope(
polytope=np.diag([2, 0.75])
@ Polytope(V=spread_points_on_a_unit_sphere(2, 7)[0])
)
CZ2 = ConstrainedZonotope(
G=np.hstack((np.eye(2), 3 * np.eye(2))),
c=[10, 10],
Ae=[1, 1, 1, 1],
be=2.85,
)
for P in [CZ1, CZ2]:
c, Q, _ = P.maximum_volume_inscribing_ellipsoid()
ax, _, _ = P.plot(
patch_args={"facecolor": "lightgreen", "label": "Polytope"}
)
mvi_ellipsoid = Ellipsoid(c=c, Q=Q)
mvi_ellipsoid.plot(
ax=ax,
patch_args={"facecolor": None, "label": "MVIE"},
center_args={"color": "k", "label": "MVIE center"},
)
ax.set_aspect("equal")
ax.grid()
ax.legend(loc="lower right", bbox_to_anchor=(1.2, 0))
plt.subplots_adjust(right=0.8)
ax.set_title("Maximum volume inscribing ellipsoid (MVIE)");
/home/vinod/workspace/git/PaperCodes/general/pycvxset/pycvxset/ConstrainedZonotope/operations_unary.py:146: UserWarning: This function returns a sub-optimal but feasible solution for maximum volume inscribed ellipsoid-based centering. warnings.warn(
Minimum volume circumscribing rectangle¶
Given a constrained zonotope $\mathcal{P}\subset\mathbb{R}^n$, we now compute the minimum volume rectangle that completely covers the constrained zonotope. Recall that rectangles are parameterized by their lower and upper bounds $(l, u)$, i.e., $\mathcal{R}(l, u)=\{x| l \leq x \leq u \}$.
We compute the ellipsoid by solving the following optimization problem $\inf_{\mathcal{P}\subseteq\mathcal{R}(l, u)} \mathrm{Vol}(\mathcal{R}(l,u))$. This optimization problem can be solved via linear programming.
P = Polytope(V=[[0, 0], [1, 0], [0, 2]])
C = (
rotation_matrix
@ np.diag([1.5, 1.25])
@ ConstrainedZonotope(polytope=P)
)
l, u = C.minimum_volume_circumscribing_rectangle()
min_vol_circ_rect = Polytope(lb=l, ub=u)
print(f"Rectangle lower bound is: {np.array2string(l)}")
print(f"Rectangle upper bound is: {np.array2string(u)}")
print(f"Rectangle volume is: {min_vol_circ_rect.volume():1.2f}")
P_patch_args = {"facecolor": "lightgreen"}
min_vol_circ_rect_patch_args = {"facecolor": "lightblue"}
ax, hE, _ = min_vol_circ_rect.plot(patch_args=min_vol_circ_rect_patch_args)
_, hP, _ = C.plot(ax=ax, patch_args=P_patch_args)
ax.set_aspect("equal")
ax.grid()
ax.legend(
[hP, hE],
["ConstrainedZonotope", "Min. vol. rectangle"],
bbox_to_anchor=(1, 0.75),
)
plt.subplots_adjust(right=0.6)
ax.set_title("Minimum volume circumscribing rectangle");
Rectangle lower bound is: [-1.76776695e+00 5.97257454e-10] Rectangle upper bound is: [1.06066017 1.76776695] Rectangle volume is: 5.00
CZ1 = ConstrainedZonotope(
polytope=np.diag([2, 0.75])
@ Polytope(V=spread_points_on_a_unit_sphere(2, 7)[0])
)
CZ2 = ConstrainedZonotope(
G=np.hstack((np.eye(2), 3 * np.eye(2))),
c=[10, 10],
Ae=[1, 1, 1, 1],
be=2.85,
)
for P in [CZ1, CZ2]:
chebyshev_center = P.interior_point()
ax, _, _ = P.plot(
patch_args={"facecolor": "lightgreen", "label": "Polytope"}
)
ax.scatter(
chebyshev_center[0],
chebyshev_center[1],
color="red",
label="Chebyshev center",
)
ax.scatter(
P.c[0],
P.c[1],
color="k",
label="CZ.c",
)
ax.set_aspect("equal")
ax.grid()
ax.legend(loc="lower right", bbox_to_anchor=(1.2, 0))
plt.subplots_adjust(right=0.8)
ax.set_title("Interior point");
Volume computation¶
We can scipy.spatial.ConvexHull
to compute the volume of a constrained zonotope.
We perform a simple sanity check where we compute the volume a unit l1-norm ball. From the figures above, it is evident that the "rhombus" consists of four isosceles right angled triangles with base as $1$.
L1_norm_ball_polytope = Polytope(V=np.vstack((np.eye(2), -np.eye(2))))
L1_norm_ball = ConstrainedZonotope(polytope=L1_norm_ball_polytope)
volume_of_an_isosceles_triangle_with_unit_base = 0.5
volume_of_unit_l1_norm_ball = approximate_volume_from_grid(
L1_norm_ball, 0.1
)
print(volume_of_unit_l1_norm_ball)
print(
"Is the volume of the unit l1-norm ball (approximately) 2?",
abs(
volume_of_unit_l1_norm_ball
- 4 * volume_of_an_isosceles_triangle_with_unit_base
)
<= 0.25,
)
2.1999999987300662 Is the volume of the unit l1-norm ball (approximately) 2? True
Cartesian product with itself¶
cartesian_Z = ConstrainedZonotope(lb=[-1, -1], ub=[1, 1])
cartesian_Z_4D = cartesian_Z**2
print(
"Is cartesian_Z_4D a cuboid?",
cartesian_Z_4D == ConstrainedZonotope(c=[0, 0, 0, 0], h=1),
)
Is cartesian_Z_4D a cuboid? True
Operations involving two constrained zonotopes¶
OLD_CONSTRAINED_ZONOTOPE_2 = ConstrainedZonotope(c=[0, 0], h=2) + [-2, 4]
OLD_POLYTOPE_2 = Polytope(c=[0, 0], h=2) + [-2, 4]
OLD_CONSTRAINED_ZONOTOPE_COLOR_2 = "lightsalmon"
Minkowski sum¶
We perform a Minkowski sum operation on two constrained zonotopes. Specifically, given two constrained zonotopes $\mathcal{P}_1$ and $\mathcal{P}_2$, we compute $$\mathcal{Q}=\{p_1+p_2\ |\ p_1\in\mathcal{P}_1,p_2\in\mathcal{P}_2\}.$$
Sanity check: The convex hull of the constrained zonotopes obtained by translating $\mathcal{P}_1$ to $\mathcal{P}_2$'s vertices coincides with $\mathcal{Q}$.
Note: This code snippet also shows the advantage of obtaining handles. In line 15, if label is passed to patch_args, we would have as many legend entries as the number of vertices in OLD_POLYTOPE_2.V
.
new_constrained_zonotope = CZ + OLD_CONSTRAINED_ZONOTOPE_2
fig, ax = plt.subplots(1, 2)
CZ.plot(
ax=ax[0],
patch_args={
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_1,
"label": "P1",
},
)
OLD_CONSTRAINED_ZONOTOPE_2.plot(
ax=ax[0],
patch_args={
"zorder": 10,
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_2,
"label": "P2",
},
)
new_constrained_zonotope.plot(
ax=ax[0],
patch_args={"facecolor": NEW_CONSTRAINED_ZONOTOPE_COLOR, "label": "Q"},
)
ax[0].legend(loc="best")
ax[0].set_title("Minkowski sum Q = P1 $\oplus$ P2")
CZ.plot(
ax=ax[1], patch_args={"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_1}
)
new_constrained_zonotope.plot(
ax=ax[1],
patch_args={
"facecolor": NEW_CONSTRAINED_ZONOTOPE_COLOR,
"alpha": 0.9,
"label": r"Q=P1 $\oplus$ P2.v",
},
)
OLD_CONSTRAINED_ZONOTOPE_2.plot(
ax=ax[1],
patch_args={
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_2,
"alpha": 0.2,
},
vertex_args={"s": 50, "color": "r"},
)
for v in OLD_POLYTOPE_2.V:
temp_constrained_zonotope = v + CZ
_, h, _ = temp_constrained_zonotope.plot(
ax=ax[1], patch_args={"facecolor": "k", "alpha": 0.8}
)
handles, labels = ax[1].get_legend_handles_labels()
ax[1].legend([*handles, h], [*labels, "P2.v"], loc="best")
ax[1].set_title("Sanity check");
/tmp/ipykernel_236822/1984422472.py:3: RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`). Consider using `matplotlib.pyplot.close()`. fig, ax = plt.subplots(1, 2)
Pontryagin difference¶
We perform a Pontryagin difference operation on two constrained zonotopes. Specifically, given two constrained zonotopes $\mathcal{P}_1$ and $\mathcal{P}_2$, we compute $$\mathcal{Q}=\mathcal{P}_2 \ominus \mathcal{P}_1 = \{p_2\ |\ \{p_2\} \oplus \mathcal{P}_1 \subseteq\mathcal{P}_2\} = \{p_2\ |\ \forall p_1 \in\mathcal{P}_1,\ p_1 + p_2 \in\mathcal{P}_2\}.$$
Sanity check: 1) $\mathcal{Q}\subseteq \mathcal{P}_2$, and 2) Translating $\mathcal{P}_1$ by $\mathcal{Q}$'s vertices still keeps it inside $\mathcal{P}_2$.
P_from_V = Polytope(V=[[0, 0], [0, 1], [2, 0]])
CZ = ConstrainedZonotope(polytope=P_from_V)
Z = 0.1 * ConstrainedZonotope(c=[0, 0], G=np.eye(2))
CZ_minus_Z = CZ - Z
CZ_minus_Z_polytope = CZ_minus_Z.polytopic_inner_approximation()
CZ_minus_Z_polytope.minimize_V_rep()
fig, ax = plt.subplots(1, 2)
CZ.plot(
ax=ax[0],
patch_args={
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_1,
"label": "C",
},
)
Z.plot(
ax=ax[0],
patch_args={
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_2,
"label": "Z",
},
)
CZ_minus_Z.plot(
ax=ax[0],
patch_args={
"facecolor": NEW_CONSTRAINED_ZONOTOPE_COLOR,
"label": r"Q=C $\ominus$ Z",
},
)
ax[0].legend(loc="best")
ax[0].set_title("Pontryagin difference")
CZ.plot(
ax=ax[1],
patch_args={
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_2,
"label": "C",
},
)
CZ_minus_Z.plot(
ax=ax[1],
patch_args={
"facecolor": NEW_CONSTRAINED_ZONOTOPE_COLOR,
"label": "Q = C $\ominus$ Z",
},
)
for v in CZ_minus_Z_polytope.V:
temp_polytope = v + Z
_, h, _ = temp_polytope.plot(
ax=ax[1], patch_args={"facecolor": "k", "alpha": 0.8}
)
handles, labels = ax[1].get_legend_handles_labels()
ax[1].legend([*handles, h], [*labels, r"Z $\oplus$ Q.v"], loc="best")
ax[1].set_title("Sanity check");
/home/vinod/workspace/git/PaperCodes/general/pycvxset/pycvxset/ConstrainedZonotope/operations_binary.py:644: UserWarning: This function returns an inner-approximation of the Pontryagin difference with a zonotope. warnings.warn(
Intersection¶
We perform an intersection operation on two constrained zonotopes. Specifically, given two constrained zonotopes $\mathcal{P}_1$ and $\mathcal{P}_2$, we compute $$\mathcal{Q}=\{p\ |\ p\in\mathcal{P}_1,p\in\mathcal{P}_2\}.$$
To obtain a non-empty intersection, we first translate $\mathcal{P}_1$ to obtain $\mathcal{P}_1^\dagger$.
Sanity check: Defining $\mathcal{Q}=\mathcal{P}_1^\dagger\cap\mathcal{P}_2$, we have 1) $\mathcal{Q}\subseteq\mathcal{P}_1^\dagger$ and 2) $\mathcal{Q}\subseteq\mathcal{P}_2$.
You can also compute intersection with affine sets, halfspaces, as well as intersections with other constrained zonotopes under an inverse-affine maps.
translate_by_vector = (-1, 3)
P4 = CZ + translate_by_vector
new_constrained_zonotope = OLD_CONSTRAINED_ZONOTOPE_2.intersection(P4)
fig = plt.figure()
ax = plt.gca()
CZ.plot(
ax=ax,
patch_args={
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_1,
"label": "P1",
},
)
OLD_CONSTRAINED_ZONOTOPE_2.plot(
ax=ax,
patch_args={
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_2,
"label": "P2",
},
)
P4.plot(
ax=ax,
patch_args={
"facecolor": "yellow",
"linewidth": 3,
"label": r"P1$^\dagger$",
},
)
new_constrained_zonotope.plot(
ax=ax,
patch_args={
"edgecolor": NEW_CONSTRAINED_ZONOTOPE_COLOR,
"fill": False,
"linewidth": 1,
"label": "Q",
},
)
ax.legend(loc="best")
ax.set_title(r"Intersection Q=P1$^\dagger\cap$ P2");
Intersection under inverse affine map¶
Given constrained zonotopes $\mathcal{P}=\{x|-1\leq x \leq 1\}\subset\mathbb{R}^3$ and $\mathcal{Q}=\{x|-1\leq x - 1\leq 1\}\subset\mathbb{R}^2$ and matrix $R=[I_2, 0_{2\times 1}]$, we compute $\mathcal{P}\cap_R\mathcal{Q}=\{x\in\mathcal{P}|Rx\in\mathcal{Q}\}=\{x|-1\leq x \leq 1, [I_2, 0_{2\times 1}]x\in\mathcal{Q}\}=\{x|-1\leq x \leq 1, 0\leq x_i \leq 2\text{ for }i\in\{1,2\}\}$. Unlike $\mathcal{P}\cap(\mathcal{Q}@R)$, this function does not require $R$ to be square.
C_3D = ConstrainedZonotope(c=[0, 0, 0], h=1)
ax, _, _ = C_3D.plot(
direction_vectors=dir_vectors, patch_args={"label": "C_3D"}
)
C_3D.intersection_under_inverse_affine_map(
ConstrainedZonotope(c=[1, 1], h=1), [[1, 0, 0], [0, 1, 0]]
).plot(
ax=ax,
direction_vectors=dir_vectors,
patch_args={"facecolor": "yellow", "label": "C_3D after intersection"},
)
ax.legend()
ax.view_init(elev=15, azim=-24)
ax.set_title("Intersection under inverse affine map")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z");
Intersection with a halfspace¶
ax, _, _ = C_3D.plot(
direction_vectors=dir_vectors, patch_args={"label": "C_3D"}
)
C_3D.intersection_with_halfspaces(A=[1, 1, 1], b=0).plot(
ax=ax,
direction_vectors=dir_vectors,
patch_args={"facecolor": "yellow", "label": "C_3D after intersection"},
)
ax.legend()
ax.view_init(elev=15, azim=-24)
ax.set_title("Intersection with a halfspace")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z");
Intersection with an affine set¶
ax, _, _ = C_3D.plot(
direction_vectors=dir_vectors, patch_args={"label": "C_3D"}
)
C_3D.intersection_with_affine_set(Ae=[1, 1, 1], be=0).plot(
ax=ax,
direction_vectors=dir_vectors,
patch_args={"facecolor": "yellow", "label": "C_3D after intersection"},
)
ax.legend()
ax.view_init(elev=15, azim=-24)
ax.set_title("Intersection with affine set")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z");
Slice¶
ax, _, _ = C_3D.plot(
direction_vectors=dir_vectors, patch_args={"label": "C_3D"}
)
C_3D.slice(dims=2, constants=0.5).plot(
ax=ax,
direction_vectors=dir_vectors,
patch_args={"facecolor": "yellow", "label": "C_3D after slicing"},
)
ax.legend()
ax.view_init(elev=15, azim=-24)
ax.set_title("Slicing")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z");
Containment and equality check¶
We can see if a constrained zonotope covers another --- given two constrained zonotopes
$\mathcal{P}_1$ and $\mathcal{P}_2$, we can check if $\mathcal{P}_1\overset{?}{\subseteq}\mathcal{P}_2$. pycvxset
provides a method contains
to do so, but also overloads binary operators <=
, <
, >=
, >
, in
for the same. Finally, we can also check if constrained zonotopes are equal using ==
.
You can also check the constrained zonotope contains another ellipsoid or a constrained zonotope as well.
P5 = 0.5 * (CZ - CZ.c) + CZ.c
fig, ax = plt.subplots(1, 2)
CZ.plot(
ax=ax[0],
patch_args={
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_1,
"label": "P1",
},
)
P5.plot(ax=ax[0], patch_args={"facecolor": "black", "label": "P5"})
ax[0].legend(loc="best")
print(f"Does P1 cover P5? {CZ.contains(P5)}")
print(f"Is P1 >= P5? {CZ >= P5}")
print(f"Is P1 > P5? {CZ > P5}")
print(f"Is P5 in P1? {P5 in CZ}")
print(f"Is P1 <= P5? {CZ <= P5}")
print(f"Is P1 < P5? {CZ < P5}")
print(f"Is P1 in P5? {CZ in P5}")
OLD_CONSTRAINED_ZONOTOPE_2.plot(
ax=ax[1],
patch_args={
"facecolor": OLD_CONSTRAINED_ZONOTOPE_COLOR_2,
"label": "P2",
},
)
P4.plot(ax=ax[1], patch_args={"facecolor": "grey", "label": "P4"})
ax[1].legend(loc="best")
print(f"\n\nDoes P2 cover P4? {OLD_CONSTRAINED_ZONOTOPE_2.contains(P4)}")
print(f"Is P2 >= P4? {OLD_CONSTRAINED_ZONOTOPE_2 >= P4}")
print(f"Is P2 > P4? {OLD_CONSTRAINED_ZONOTOPE_2 > P4}")
print(f"Is P4 in P2? {P4 in OLD_CONSTRAINED_ZONOTOPE_2}")
print(f"Is P2 <= P4? {OLD_CONSTRAINED_ZONOTOPE_2 <= P4}")
print(f"Is P2 < P4? {OLD_CONSTRAINED_ZONOTOPE_2 < P4}")
print(f"Is P2 in P4? {OLD_CONSTRAINED_ZONOTOPE_2 in P4}")
plt.suptitle("Containment check for two sets of constrained zonotopes");
Does P1 cover P5? True Is P1 >= P5? True
Is P1 > P5? True Is P5 in P1? True Is P1 <= P5? False
Is P1 < P5? False Is P1 in P5? False
Does P2 cover P4? False Is P2 >= P4? False Is P2 > P4? False Is P4 in P2? False Is P2 <= P4? False Is P2 < P4? False Is P2 in P4? False