Objective functions and constraints¶
Functions¶
disropt comes with many already implemented mathematical functions.
Functions are defined in terms of optimization variables (Variable
) or other functions.
Let us start by defining a Variable
object as:
from disropt.functions import Variable
n = 2 # dimension of the variable
x = Variable(n)
print(x.input_shape) # -> (2, 1)
print(x.output_shape) # -> (2, 1)
Now, suppose you want to define an affine function \(f(x)=A^\top x - b\) with \(A\in\mathbb{R}^{2\times 2}\) and \(b\in\mathbb{R}^2\):
import numpy as np
a = 1
A = np.array([[1,2], [2,4]])
b = np.array([[1], [1]])
f = A @ x - b
# or, alternatively
from disropt.functions import AffineForm
f = AffineForm(x, A, b)
The composition of functions is fully supported. Suppose you want to define a function \(g(x)=f(x)^\top Q f(x)\), then:
from disropt.functions import QuadraticForm
Q = np.random.rand(2,2)
g = QuadraticForm(f, Q) # or: g = f @ (Q.tranpose() @ f)
print(g.input_shape) # -> (2, 1)
print(g.output_shape) # -> (1, 1)
Currently supported operations with functions are sum (+), difference (-), product(*) and matrix product (@). Combination with numpy arrays is supported as well.
Function properties and methods¶
Each function has three properties that can be checked: differentiablity, being affine and quadratic:
g.is_differentiable # -> True
g.is_affine # -> False
g.is_quadratic # -> True
f.is_affine # -> True
and their input and output shapes can be obtained as
g.output_shape # -> (1,1)
g.input_shape # -> (2,1)
Moreover, it is possible to evaluate functions at desired points and to obtain the corresponding (sub)gradient/jacobian/hessian as:
pt = np.random.rand(2,1)
# the value of g computed at pt is obtained as
g.eval(pt)
# the value of the jacobian of g computed at pt is
g.jacobian(pt)
# the value of a (sub)gradient of g is available only if the output shape of g is (1,1)
g.subgradient(pt)
# otherwise it will result in an error
f.subgradient(pt) # -> Error
# the value of the hessian of g computed at pt is
g.hessian(pt)
For affine and quadratic functions, a method called get_parameters
is implemented, which returns the matrices and vectors that define those functions.
The generic form for an affine function is \(A^\top x + b\) while the one for a quadratic form is \(x^\top P x + q^\top x + r\):
f = A @ x + b
f.get_parameters() # -> A, b
Defining constraints from functions¶
Constraints are represented in the canonical forms \(f(x)=0\) and \(f(x)\leq 0\).
They are directly obtained from functions:
constraint = g == 0 # g(x) = 0
constraint = g >= 0 # g(x) >= 0
constraint = g <= 0 # g(x) <= 0
On the right side of (in)equalities, numpy arrays and functions (with appropriate shapes) are also allowed:
c = np.random.rand(2,1)
constr = f <= c
which is automatically translated in the corresponding canonical form.
Constraints can be evaluated at any point by using the eval
method which returns a boolean value if the constraint
is satisfied. Moreover, the function defining a constraints can be retrieved with the function
method:
pt = np.random.rand(2,1)
constr.eval(pt) # -> True if f(pt) <= c
constr.function.eval(pt) # -> value of f - c at pt
Affine and quadratic constraints¶
Parameters defining affine and quadratic constraints can be easily obtained. They can be accessed by calling the get_parameters
method:
f = A @ x + b
constraint = f == 0 # affine equality constraint
# f has the form A^T x + b
constraint.get_parameters() # returns A and b
g = f @ f
constraint = g == 0 # quadratic equality constraint
# g has the form x^T P x + q^T x + r
constraint.get_parameters() # returns P, q and r
Projection onto a constraint set¶
The projection of a point onto the set defined by a constraint can be computed via the projection
method:
projected_point = f.projection(pt)
Constraint sets¶
Some particular constraint sets (for which projection of points is easy to compute)
are also available through specialized classes, which are extensions of the class Constraint
.
For instance, suppose you want all the components of \(x\) to be in \([-1,1]\).
Then you can define a Box
constraint as:
from disropt.constraints import Box
bound = np.ones((2,1))
constr = Box(-bound, bound)
Two methods are available: projection
and intersection
.
The first one returns the projection of a given point on the set,
while the second one intersects the set with another one.
This feature is particularly useful in set-membership estimation algorithms.
Constraint sets can be converted into a list of constraints through the method to_constraints
.