# 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`

.