# Constraints

A guide for constraints in `InfiniteOpt`

. See the respective technical manual for more details.

## Overview

Constraints are a key part of infinite dimensional problems and serve as a fundamental utility of `InfiniteOpt`

. In particular, `InfiniteOpt`

supports finite constraints that entail finite variables and/or measures that fully remove any infinite parameter dependencies (e.g., first stage constraints), infinite constraints that are enforced over the entire domain of its infinite parameter dependencies (e.g., path constraints), and restricted constraints which are enforced over some specified sub-domain of its infinite parameter dependencies (e.g., boundary conditions). This page will highlight how to implement these types of constraints in `InfiniteOpt`

.

## Basic Usage

Principally, the `@constraint`

macro is used to define constraints. First, let's set up an infinite model with variables that we can add constraints to:

```
julia> model = InfiniteModel();
julia> @infinite_parameter(model, t in [0, 10]);
julia> @infinite_parameter(model, x[1:2] in [-2, 2]);
julia> @variable(model, ya, Infinite(t, x));
julia> @variable(model, yb, Infinite(t));
julia> @variable(model, z[1:2]);
```

Unlike previous versions, `InfiniteOpt`

now supports all the constraints offered by `JuMP`

, including vector and semi-definite constraints! Please see JuMP's constraint documentation for a thorough explanation of the supported types and syntax.

Nonlinear constraints are defined simply by using `@constraint`

and not using `JuMP.@NLconstraint`

. See Nonlinear Expressions for more information.

### Scalar Constraints

Scalar constraints use scalar functions of variables. For example, let's define the constraint $||z||^2 + 2y_a(t, x) \leq 0, \ \forall t \in [0, 10], x \in [-2, 2]^2$ using `@constraint`

:

```
julia> @constraint(model, c1, sum(z[i]^2 for i = 1:2) + 2ya <= 0)
c1 : z[1]² + z[2]² + 2 ya(t, x) ≤ 0, ∀ t ∈ [0, 10], x[1] ∈ [-2, 2], x[2] ∈ [-2, 2]
```

Thus, we added an infinite constraint (which infinite with respect to `t`

and `x`

) to `model`

and stored the corresponding constraint reference to `c1`

. Note that this is enforced over the full infinite domains of the infinite parameters `t`

and `x`

which are implicitly used by `c1`

. For scalar constraints like this one, the allowed constraint operators are `==`

, `<=`

, `≤`

, `>=`

, and `≥`

.

Linear algebra constraints can also be used when defining constraints when `.`

is added in front of the constraint operators (e.g., `.<=`

). This behavior is further explained in `JuMP`

's constraint documentation.

Similarly, we can define an array of constraints with varied indexes by including an additional argument before the constraint expression. For example, let's define $3z_i - 14 = 0, \ \forall i \in \{1,2\}$:

```
julia> @constraint(model, c2[i = 1:2], 3z[i] - 14 == 0)
2-element Vector{InfOptConstraintRef}:
c2[1] : 3 z[1] = 14
c2[2] : 3 z[2] = 14
```

Thus, we added two constraints to `model`

and stored a vector of the corresponding constraint references to the `Julia`

variable `c2`

. To learn more about building containers of constraints please see `JuMP`

's constraint container documentation.

### Multi-Dimensional Constraints

Building upon `JuMP`

we support a variety of multi-dimensional constraint types. For example, we can define the vector constraint:

```
julia> A = [1 2; 3 4]
2×2 Matrix{Int64}:
1 2
3 4
julia> b = [5, 6]
2-element Vector{Int64}:
5
6
julia> @constraint(model, A * z - b in MOI.Nonnegatives(2))
[z[1] + 2 z[2] - 5, 3 z[1] + 4 z[2] - 6] ∈ MathOptInterface.Nonnegatives(2)
```

See `JuMP`

's constraint documentation for a thorough tutorial on the accepted syntax and constraint types.

### Restricted Constraints

Restricted constraints entail an infinite domain (determined by the infinite parameters they explicitly/implicitly depend on) that is restricted to a certain sub-domain. Such constraints are common for enforcing initial/boundary conditions and for enforcing path constraints over a certain sub-domain.

These types of constraints are defined adding `DomainRestrictions`

. For example, let's add the initial condition $y_b(0) = 0$:

```
julia> @constraint(model, initial, yb == 0, DomainRestrictions(t => 0))
initial : yb(t) = 0, ∀ t = 0
```

Thus, we have added a constraint to `model`

defined over the sub-domain $t = 0$ in accordance with the initial condition.

Boundary conditions can often be more efficiently defined using Restricted Variables. For example, the above initial condition can be expressed:

```
julia> @constraint(model, yb(0) == 0)
yb(0) = 0
```

More complex sub-domains can be specified by simply adding more restrictions. To illustrate this, let's define the constraint $2y_b^2(t, x) + z_1 \geq 3, \ \forall t = 0, \ x \in [-1, 1]^2$:

```
julia> @constraint(model, 2ya^2 + z[1] >= 3, DomainRestrictions(t => 0, x => [-1, 1]))
2 ya(t, x)² + z[1] ≥ 3, ∀ t = 0, x[1] ∈ [-1, 1], x[2] ∈ [-1, 1]
```

Now we have added constraints to our model, and it is ready to be solved!

## Data Structure

Here we detail the data structures used to store constraints in `InfiniteOpt`

. In general, constraints in `JuMP`

are of the form: `function in set`

where `function`

corresponds to a `JuMP`

expression and `set`

corresponds to a `MOI`

set. This leads to the following data structures:

Constraint Type | Function Type | Set Type |
---|---|---|

Scalar | `JuMP.AbstractJuMPScalar` | `MOI.AbstractScalarSet` |

Vector | `Vector{<:JuMP.AbstractJuMPScalar}` | `MOI.AbstractVectorSet` |

Matrix | `Matrix{<:JuMP.AbstractJuMPScalar}` | `MOI.AbstractVectorSet` via vectorization |

The above combos are then stored in `JuMP.ScalarConstraint`

s and `JuMP.VectorConstraints.

Restricted constraints are built upon this data structure where the underlying constraint is created in the same manner. Then the specified `DomainRestrictions`

are added by creating a `DomainRestrictedConstraint`

which stores the `JuMP.AbstractConstraint`

and the restrictions.

These constraint objects are what store constraints in `InfiniteModel`

s. And these are pointed to by `InfOptConstraintRef`

s.

## Definition

In this section, we describe the ins and outs of defining constraints. Note that this process is analogous to the manner in which variables are defined and added to the model.

### Manual Definition

Defining a constraint principally involves the following steps:

- Define the constraint information (i.e., function, set, and domain restrictions)
- Construct a concrete subtype of
`JuMP.AbstractConstraint`

to store the constraint information - Add the
`AbstractConstraint`

object to an`InfiniteModel`

and assign a name - Create an
`InfOptConstraintRef`

that points to the constraint object stored in the model.

The constraint objects are specified via `JuMP.build_constraint`

which requires that the user provides a function, set, and optionally include domain restrictions. For example, let's build a scalar constraint $3y_a(t, x) - y_b^2(t) \leq 0, \ \forall t \in [0, 10], x \in [-2, 2]^2$ over its full infinite domain (i.e., have no `DomainRestrictions`

):

`julia> constr = build_constraint(error, 3ya - yb^2, MOI.LessThan(0.0));`

Now the built constraint object can be added to the infinite model via `add_constraint`

. Let's do so with our example and assign it the name of `c3`

(note that adding a name is optional):

```
julia> cref = add_constraint(model, constr, "c3")
c3 : -yb(t)² + 3 ya(t, x) ≤ 0, ∀ t ∈ [0, 10], x[1] ∈ [-2, 2], x[2] ∈ [-2, 2]
```

Thus, we have made our constraint and added it `model`

and now have a constraint reference `cref`

that we can use to access it.

The `@constraint`

macro automate the above steps.

### Macro Definition

As mentioned above in the Basic Usage section, the `@constraint`

macro should be used to define constraints with the syntax: `@constraint(model::InfiniteModel, [container/name_expr], constr_expr, [rs::DomainRestrictions])`

.

The second argument is optional and is used to assign a name and/or define indexing variables to be used in the constraint expression. When a name is provided it is registered and cannot be used again for another constraint or variable name. The indexing expression can be used to produce an array of constraints as shown below (notice this is equivalent to looping over individual `@constraint`

calls):

```
julia> crefs = @constraint(model, [i = 1:2], 2z[i] - yb == 0)
2-element Vector{InfOptConstraintRef}:
2 z[1] - yb(t) = 0, ∀ t ∈ [0, 10]
2 z[2] - yb(t) = 0, ∀ t ∈ [0, 10]
julia> crefs = Vector{InfOptConstraintRef}(undef, 2);
julia> for i = 1:2
crefs[i] = @constraint(model, 2z[i] - yb == 0)
end
julia> crefs
2-element Vector{InfOptConstraintRef}:
2 z[1] - yb(t) = 0, ∀ t ∈ [0, 10]
2 z[2] - yb(t) = 0, ∀ t ∈ [0, 10]
```

Please refer to `JuMP`

's constraint container documentation for a thorough tutorial on creating containers of constraints.

Any constraint type supported by `JuMP`

can be specified in the `constr_expr`

argument. This includes a wealth of constraint types including:

- Variable constraints
- Scalar constraints
- Vector constraints
- Conic constraints
- Indicator constraints
- Semi-definite constraints

For example, we could define the following semi-definite constraint:

```
julia> @constraint(model, [yb 2yb; 3yb 4yb] >= ones(2, 2), PSDCone())
[yb(t) - 1 2 yb(t) - 1;
3 yb(t) - 1 4 yb(t) - 1] ∈ PSDCone(), ∀ t ∈ [0, 10]
```

See `JuMP`

's constraint documentation for a thorough tutorial on the accepted syntax and constraint types.

Finally, restrictions on the inherent infinite domain of a constraint can be specified via `DomainRestrictions`

with the `rs`

argument. The accepted syntax is `DomainRestrictions(restricts...)`

where each argument of `restricts`

can be any of the following forms:

`pref => value`

`pref => [lb, ub]`

`pref => IntervalDomain(lb, ub)`

`prefs => value`

`prefs => [lb, ub]`

`prefs => IntervalDomain(lb, ub)`

.

Note that `pref`

and `prefs`

must correspond to infinite parameters.

For example, we can define the constraint $y_a^2(t, x) + z_i \leq 1$ and restrict the infinite domain of $x_i$ to be $[0, 1]$:

```
julia> @constraint(model, [i = 1:2], ya^2 + z[i] <= 1, DomainRestrictions(x[i] => [0, 1]))
2-element Vector{InfOptConstraintRef}:
ya(t, x)² + z[1] ≤ 1, ∀ t ∈ [0, 10], x[1] ∈ [0, 1], x[2] ∈ [-2, 2]
ya(t, x)² + z[2] ≤ 1, ∀ t ∈ [0, 10], x[1] ∈ [-2, 2], x[2] ∈ [0, 1]
```

Where possible, using Restricted Variables will tend to be more performant than using `DomainRestrictions`

instead.

## Queries

In this section, we describe a variety of methods to extract constraint information.

### Basic

A number of constraint properties can be extracted via constraint references. Principally, the validity, name, model, index, and constraint object can be queried via `is_valid`

, `name`

, `owner_model`

, `index`

, and `constraint_object`

, respectively. These methods all constitute extensions of `JuMP`

methods and follow exactly the same behavior. Let's try them out with the following example:

```
julia> is_valid(model, c1) # check if contained in model
true
julia> name(c1) # get the name
"c1"
julia> m = owner_model(c1); # get the model it is added to
julia> index(c1) # get the constraint's index
InfOptConstraintIndex(1)
julia> constr = constraint_object(c1); # get the raw constraint datatype
```

Also, `constraint_by_name`

can be used to retrieve a constraint reference if only the name is known and its name is unique. For example, let's extract the reference for `"c1"`

:

```
julia> cref = constraint_by_name(model, "c1")
c1 : z[1]² + z[2]² + 2 ya(t, x) ≤ 0, ∀ t ∈ [0, 10], x[1] ∈ [-2, 2], x[2] ∈ [-2, 2]
```

### Domain Restrictions

As explained above, restricted constraints serve as a key capability of `InfiniteOpt`

. Information about domain restrictions can be obtained via `has_domain_restrictions`

and `domain_restrictions`

which indicate if a constraint is restricted and what its `DomainRestrictions`

are, respectively. These are exemplified below:

```
julia> has_domain_restrictions(c1) # check if constraint is bounded
false
julia> has_domain_restrictions(initial)
true
julia> domain_restrictions(initial)
Subdomain restrictions (1): t = 0
```

### General

Constraints can be defined in a number of ways symbolically that differ from how it is actually stored in the model. This principally occurs since like terms and constants are combined where possible with the variable terms on the left-hand side and the constant on the right-hand side. For instance, the constraint $2y_b(t) + 3y_b(t) - 2 \leq 1 + z_1$ would be normalized $5y_b(t) - z_1 \leq 3$. In accordance with this behavior, `normalized_rhs`

and `normalized_coefficient`

can be used to query the normalized right-hand side and the coefficient of a particular variable reference, respectively. Let's employ the above example to illustrate this:

```
julia> @constraint(model, constr, 2yb + 3yb - 2 <= 1 + z[1])
constr : 5 yb(t) - z[1] ≤ 3, ∀ t ∈ [0, 10]
julia> normalized_rhs(constr)
3.0
julia> normalized_coefficient(constr, yb)
5.0
```

There also exist a number of methods for querying an infinite model about what constraints it contains. `list_of_constraint_types`

can be used query what types of constraints have been added to a model. This is provided as a list of tuples where the first element is the expression type and the second element is the set type (recall that constraints are stored in the form `func-in-set`

). Thus, for our current model we obtain:

```
julia> list_of_constraint_types(model)
4-element Vector{Tuple{DataType,DataType}}:
(GenericQuadExpr{Float64, GeneralVariableRef}, MathOptInterface.LessThan{Float64})
(GenericQuadExpr{Float64, GeneralVariableRef}, MathOptInterface.GreaterThan{Float64})
(GenericAffExpr{Float64, GeneralVariableRef}, MathOptInterface.LessThan{Float64})
(GenericAffExpr{Float64, GeneralVariableRef}, MathOptInterface.EqualTo{Float64})
```

This information is useful when used in combination with the `num_constraints`

and `all_constraints`

methods which can take the expression type and/or the set type as inputs. Here `num_constraints`

provides the number of constraints that match a certain type, and `all_constraints`

returns a list of constraint references matching the criteria provided. These have been extended beyond `JuMP`

functionality such additional methods have been provided for the cases in which one wants to query solely off of set or off expression type. Let's illustrate this with `num_constraints`

:

```
julia> num_constraints(model) # total number of constraints
16
julia> num_constraints(model, GenericQuadExpr{Float64, GeneralVariableRef})
5
julia> num_constraints(model, MOI.LessThan{Float64})
5
julia> num_constraints(model, GenericQuadExpr{Float64, GeneralVariableRef},
MOI.LessThan{Float64})
4
```

## Modification

In this section, we highlight a number of methods that can be used to modify existing constraints.

### Deletion

All constraints in `InfiniteOpt`

can be removed in like manner to typical `JuMP`

constraints with the appropriate extension of `delete`

. This will remove the corresponding constraint object from the model. However, please note any registered names will remain registered in the infinite model. This means that a constraint with a registered name cannot be repeatedly added and removed using the same name. To exemplify this, let's delete the constraint `c1`

:

`julia> delete(model, c1)`

### General

There also are a number of ways to modify information and characteristics of constraints. First, `set_name`

can be used to specify a new name for a particular constraint. For instance, let's update the name of `initial`

to `"init_cond"`

:

```
julia> set_name(initial, "init_cond")
julia> initial
init_cond : yb(t) = 0, ∀ t = 0
```

We can also update the normalized right hand side constant value or normalized left hand side variable coefficient value using `set_normalized_rhs`

and `set_normalized_coefficient`

, respectively. Let's again consider the constraint $5y_b(t) - z_1 \leq 3$ as an example. Let's change the constant term to -1 and the `y_b(t)`

coefficient to 2.5:

```
julia> set_normalized_rhs(constr, -1)
julia> set_normalized_coefficient(constr, yb, 2.5)
julia> constr
constr : 2.5 yb(t) - z[1] ≤ -1, ∀ t ∈ [0, 10]
```

In some cases, it may be more convenient to dynamically modify coefficients and other values via the use of finite parameters. This provides an avenue to update parameters without having to be concerned about the normalized form. For more information, see the Finite Parameters page.

### Domain Restrictions

Domain Restrictions can be added to, modified, or removed from any constraint in `InfiniteOpt`

. Principally, this is accomplished via `add_domain_restrictions`

, `set_domain_restrictions`

, and `delete_domain_restrictions`

.

Previous versions of `InfiniteOpt`

used `@[set/add]_parameter_bounds`

which have been deprecated in favor of using `DomainRestrictions`

with the methods described used in this section.

First, domain restrictions can be added to a constraint via `add_domain_restrictions`

. For example, let's add the bound $t \in [0, 1]$ to `constr`

:

```
julia> add_domain_restrictions(constr, DomainRestrictions(t => [0, 1]))
julia> constr
constr : 2.5 yb(t) - z[1] ≤ -1, ∀ t ∈ [0, 1]
```

In similar manner, `set_domain_restrictions`

can be employed to specify what restrictions a constraint has (overwriting any existing ones if forced). It follows the same syntax, so let's use it to change the bounds on `t`

to $t = 0$:

```
julia> set_domain_restrictions(constr, DomainRestrictions(t => 0), force = true)
julia> constr
constr : 2.5 yb(t) - z[1] ≤ -1, ∀ t = 0
```

Finally, constraint restrictions can be deleted via `delete_domain_restrictions`

. Now let's delete the domain restrictions associated with our example:

```
julia> delete_domain_restrictions(constr)
julia> constr
constr : 2.5 yb(t) - z[1] ≤ -1, ∀ t ∈ [0, 10]
```