import numpy as np
[docs]class AbstractSet:
"""Abstract constraint set
"""
[docs] def projection(self, x):
"""Project a point onto the set
"""
pass
[docs] def to_constraints(self):
"""Convert the set in a list of Constraints
"""
pass
[docs]class Box(AbstractSet):
"""Box set
:math:`X = \\{x\\mid l\\leq x \\leq u\\}`
with :math:`l,x,u\\in \\mathbb{R}^{n}`
Args:
lower_bound (numpy.ndarray, optional): array with lower bounds for each axis. Defaults to None.
upper_bound (numpy.ndarray, optional): array with upper bounds for each axis. Defaults to None.
Attributes:
lower_bound (numpy.ndarray): lower bounds
upper_bound (numpy.ndarray): upper bounds
input_shape (tuple): space dimensions
"""
def __init__(self, lower_bound=None, upper_bound=None):
if lower_bound is None and upper_bound is None:
raise ValueError("At least one argument must be provided.")
if (lower_bound is not None):
if not isinstance(lower_bound, np.ndarray):
raise ValueError("lower_bound must be numpy.ndarray")
elif len(lower_bound.shape) > 2 or lower_bound.shape[1] != 1:
raise ValueError(
"inputs must be 2D numpy.ndarray with shape (n,1), n>=1")
self.lower_bound = lower_bound
if (upper_bound is not None):
if not isinstance(upper_bound, np.ndarray):
raise ValueError("upper_bound must be numpy.ndarray")
elif len(upper_bound.shape) > 2 or upper_bound.shape[1] != 1:
raise ValueError(
"inputs must be 2D numpy.ndarray with shape (n,1), n>=1")
self.upper_bound = upper_bound
if (lower_bound is not None) and (upper_bound is not None):
if lower_bound.shape != upper_bound.shape:
raise ValueError(
"Lower and upper bounds must have the same shape")
else:
self.input_shape = lower_bound.shape
else:
if lower_bound is None:
self.lower_bound = -np.inf * np.ones(upper_bound.shape)
self.input_shape = lower_bound.shape
elif upper_bound is None:
self.upper_bound = np.inf * np.ones(lower_bound.shape)
self.input_shape = lower_bound.shape
[docs] def intersection(self, box):
"""Compute the intersection with another box
Args:
box (Box): box to compute the intersection with
Raises:
ValueError: Only intersection with another box is supported
ValueError: The two boxex must have the same input_shape
"""
if not isinstance(box, Box):
raise ValueError(
"Only intersection with another box is supported")
if self.input_shape != box.input_shape:
raise ValueError(
"Boxes must have the same shape {}.".format(self.input_shape))
else:
self.upper_bound = np.min([self.upper_bound, box.upper_bound], axis=0)
self.lower_bound = np.max([self.lower_bound, box.lower_bound], axis=0)
[docs] def projection(self, x):
"""Project a point onto the box
Arguments:
x (numpy.ndarray): Point to be projected
Returns:
numpy.ndarray: projected point
"""
if not isinstance(x, np.ndarray):
raise ValueError("input must be a numpy.ndarray")
if x.shape != self.input_shape:
raise ValueError(
"Dimensions mismatch. Required input shape is {}".format(self.input_shape))
x_projected = np.zeros(self.input_shape)
x_projected += x
for i in range(self.input_shape[0]):
if x[i] < self.lower_bound[i]:
x_projected[i] = self.lower_bound[i]
elif x[i] > self.upper_bound[i]:
x_projected[i] = self.upper_bound[i]
return x_projected
[docs] def to_constraints(self):
from ..functions import Variable
x = Variable(self.input_shape[0])
upper_bound = x <= self.upper_bound
lower_bound = x >= self.lower_bound
return [upper_bound, lower_bound]
[docs]class Strip(AbstractSet):
"""Strip set
:math:`X = \\{x\\mid -w<=|a^\\top x - s|<=w\\}`
with :math:`a,x\\in \\mathbb{R}^{n}` and :math:`w\\in\\mathbb{R}`
Args:
regressor (numpy.ndarray): regressor of the strip
shift (float): shift from the origin
width (float): width of the strip
Attributes:
regressor (numpy.ndarray): regressor of the strip
shift (float): shift from the origin
upper (float): upper border (shift + half width)
lower (float): lower border (shift - half width)
input_shape (tuple): space dimensions
"""
def __init__(self, regressor, shift, width):
if not isinstance(regressor, np.ndarray):
raise ValueError("regressor must be numpy.ndarray")
if len(regressor.shape) > 2 or regressor.shape[1] != 1:
raise ValueError(
"regressor must be 2D numpy.ndarray with shape (n,1), n>=1")
self.regressor = regressor
self.shift = shift
self.upper = shift + width/2.0
self.lower = shift - width/2.0
self.input_shape = regressor.shape
def __str__(self):
description = r'{}<=|{}^T x - {}|<={}'.format(
self.lower, self.regressor, self.shift, self.upper)
return description
[docs] def intersection(self, strip):
"""Compute the intersection with another strip
Args:
strip (Strip): strip to compute the intersection with
Raises:
ValueError: Only intersection with another strip is supported
ValueError: The two strips must have the same regressor
"""
if not isinstance(strip, Strip):
raise ValueError(
"Only intersection with another strip is supported")
if self.regressor.any() != strip.regressor.any():
raise ValueError(
"Strips must have the same regressor {}.".format(self.regressor))
else:
self.upper = np.min([self.upper, strip.upper])
self.lower = np.max([self.lower, strip.lower])
[docs] def projection(self, x):
"""Project a point onto the strip
Arguments:
x (numpy.ndarray): Point to be projected
Returns:
numpy.ndarray: projected point
"""
if self.input_shape != x.shape:
raise ValueError("shape of the point {} is not equal to the shape of the regressor {}".format(
x.shape, self.regressor.shape))
distance = self.regressor.T.dot(x)
if distance > self.upper:
x_projected = x + float(self.upper - distance) * self.regressor
return x_projected
elif distance < self.lower:
x_projected = x + float(self.lower - distance) * self.regressor
return x_projected
else:
return x
[docs] def to_constraints(self):
from ..functions import Variable, Abs
x = Variable(self.input_shape[0])
upper_bound = Abs(self.regressor @ x - self.shift) <= self.upper
lower_bound = Abs(self.regressor @ x - self.shift) >= self.lower
return [upper_bound, lower_bound]
[docs]class Circle(AbstractSet):
"""Circle set
:math:`X = \\{x\\mid \\|x - c\\|<= r\\}`
with :math:`x,c\\in \\mathbb{R}^{n}` and :math:`r\\in\\mathbb{R}`
Args:
center (numpy.ndarray): center of the circle
radius (float): radius of the circle
Attributes:
center (numpy.ndarray): center of the circle
radius (float): radius of the circle
input_shape (tuple): space dimensions
"""
def __init__(self, center, radius):
if not isinstance(center, np.ndarray):
raise ValueError("center must be numpy.ndarray")
if len(center.shape) > 2 or center.shape[1] != 1:
raise ValueError(
"center must be 2D numpy.ndarray with shape (n,1), n>=1")
self.center = center
self.radius = radius
self.input_shape = center.shape
def __str__(self):
description = r'\|x - {}\|<={}'.format(
self.center.flatten(), self.radius)
return description
[docs] def intersection(self, circle):
"""Compute the intersection with another circle
Args:
circle (Circle): circle to compute the intersection with
Raises:
ValueError: Only intersection with another circle is supported
ValueError: The two circles must have the same center
"""
if not isinstance(circle, Circle):
raise ValueError(
"Only intersection with another circle is supported")
if self.center.any() != circle.center.any():
raise ValueError(
"Circles must have the same center {}.".format(self.center))
else:
self.radius = np.min([self.radius, circle.radius])
[docs] def projection(self, x):
"""Project a point onto the circle
Arguments:
x (numpy.ndarray): Point to be projected
Returns:
numpy.ndarray: projected point
"""
if self.input_shape != x.shape:
raise ValueError("shape of the point {} does not comply with the dimension of the circle {}".format(
x.shape, self.input_shape))
distance = np.linalg.norm(self.center - x)
if distance > self.radius:
x_projected = self.center + self.radius * \
(x - self.center) / distance
return x_projected
else:
return x
[docs] def to_constraints(self):
from ..functions import Variable, Norm
x = Variable(self.input_shape[0])
f = Norm(x - self.center) <= self.radius
return [f]
[docs]class CircularSector(AbstractSet):
"""Circular sector set.
Args:
vertex (numpy.ndarray): vertex of the circular sector
angle (float): direction of the circular sector (in rad)
radius (float): radius of the circular sector
width (float): width of the circular sector (in rad)
Attributes:
vertex (numpy.ndarray): vertex of the circular sector
angle (float): direction of the circular sector (in rad)
radius (float): radius of the circular sector
h_angle (float): left border (from the vertex pov) of the circular sector (in rad)
l_angle (float): right border (from the vertex pov) of the circular sector (in rad)
input_shape (tuple): space dimensions
"""
def __init__(self, vertex, angle, radius, width):
if not isinstance(vertex, np.ndarray):
raise ValueError("vertex must be a numpy.ndarray")
if len(vertex.shape) > 2 or vertex.shape[1] != 1:
raise ValueError(
"vertex must be 2D numpy.ndarray with shape (n,1), n>=1")
if vertex.shape[0] != 2:
raise ValueError("only dimension (2,1) is curtrently supported")
self.vertex = vertex
self.radius = radius
self.angle = angle
self.h_angle = self.angle + width/2.0
self.l_angle = self.angle - width/2.0
self.input_shape = vertex.shape
def __str__(self):
return "Circular sector" # TODO
[docs] def intersection(self, circular_sector):
"""Compute the intersection with another circular sector
Args:
circular_sector (CircularSector): circula sector to compute the intersection with
Raises:
ValueError: Only intersection with another circular_sector is supported
ValueError: The two circular_sector must have the same vertex
"""
if not isinstance(circular_sector, CircularSector):
raise ValueError(
"Only intersection with another circular_sector is supported")
if self.vertex.any() != circular_sector.vertex.any():
raise ValueError("Circular sectors must have the same vertex.")
else:
self.radius = np.min([self.radius, circular_sector.radius])
sl = np.unwrap(self.l_angle)
cl = np.unwrap(circular_sector.l_angle)
if self.h_angle >= 0:
self.h_angle = np.min([self.h_angle, circular_sector.h_angle])
elif self.h_angle < 0:
if circular_sector.h_angle < 0:
self.h_angle = np.min(
[self.h_angle, circular_sector.h_angle])
else:
self.h_angle = circular_sector.h_angle
if self.l_angle >= 0:
if circular_sector.l_angle >= 0:
self.l_angle = np.max(
[self.l_angle, circular_sector.l_angle])
else:
if cl-sl < np.pi:
self.l_angle = circular_sector.l_angle
if self.l_angle < 0:
if circular_sector.l_angle < 0:
self.l_angle = np.max(
[self.l_angle, circular_sector.l_angle])
else:
if cl-sl > np.pi:
self.l_angle = circular_sector.l_angle
self.h_angle = np.array([self.h_angle]).flatten()
self.l_angle = np.array([self.l_angle]).flatten()
def __set_bounds(self):
"""set the bounds of the circular sector
"""
self.a = np.array([-np.sin(self.h_angle), np.cos(self.h_angle)])
self.a = self.a/np.linalg.norm(self.a)
self.b = np.array([-np.sin(self.l_angle), np.cos(self.l_angle)])
self.b = self.b/np.linalg.norm(self.b)
self.ca = np.dot(self.a.T, self.vertex)
self.cb = np.dot(self.b.T, self.vertex)
[docs] def projection(self, x):
"""Project a point onto the ciruclar sector
Arguments:
x (numpy.ndarray): Point to be projected
Returns:
numpy.ndarray: projected point
"""
if self.input_shape != x.shape:
raise ValueError(
"shape of the point {} does not comply with the dimension of the circular sector {}".format(
x.shape, self.input_shape))
self.__set_bounds()
v = x-self.vertex
# if in cone remain the same
if np.dot(self.a.T, v) <= 0 and np.dot(self.b.T, v) >= 0:
x_projected = x
# if behind the projection is the vertex
elif np.dot(self.a.T, v) > 0 and np.dot(self.b.T, v) < 0:
x_projected = self.vertex
elif np.dot(self.a.T, v) > 0 and np.dot(self.b.T, v) >= 0:
aa = self.a
cc = np.dot(aa.T, self.vertex)
x_projected = x+((cc-np.dot(aa.T, x)) /
np.linalg.norm(aa, ord=2)**2)*aa
if np.dot(self.b.T, x_projected-self.vertex) < 0:
x_projected = self.vertex
elif np.dot(self.a.T, v) <= 0 and np.dot(self.b.T, v) < 0:
bb = self.b
cc = np.dot(bb.T, self.vertex)
x_projected = x+((cc-np.dot(bb.T, x)) /
np.linalg.norm(bb, ord=2)**2)*bb
if np.dot(self.a.T, x_projected-self.vertex) > 0:
x_projected = self.vertex
else: # TODO check
x_projected = x
distance = np.linalg.norm(x_projected-self.vertex, ord=2)
if distance > self.radius:
x_projected = self.vertex+self.radius * \
(x_projected-self.vertex)/distance
return x_projected
[docs] def to_constraints(self):
from ..functions import Variable, Norm
x = Variable(self.input_shape[0])
circular_bound = Norm(x - self.vertex) <= self.radius
upper_bound = self.a @ (x - self.vertex) <= 0
lower_bound = self.b @ (x - self.vertex) >= 0
return [circular_bound, upper_bound, lower_bound]