Manipulating expressions
This guide highlights a syntactically appealing way to build expressions at the MOI level, but also to look at their contents. It may be especially useful when writing models or bridge code.
Creating functions
This section details the ways to create functions with MathOptInterface.
Creating scalar affine functions
The simplest scalar function is simply a variable:
julia> x = MOI.add_variable(model) # Create the variable x
MOI.VariableIndex(1)
This type of function is extremely simple; to express more complex functions, other types must be used. For instance, a ScalarAffineFunction
is a sum of linear terms (a factor times a variable) and a constant. Such an object can be built using the standard constructor:
julia> f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1, x)], 2) # x + 2
(2) + (1) MOI.VariableIndex(1)
However, you can also use operators to build the same scalar function:
julia> f = x + 2
(2) + (1) MOI.VariableIndex(1)
Creating scalar quadratic functions
Scalar quadratic functions are stored in ScalarQuadraticFunction
objects, in a way that is highly similar to scalar affine functions. You can obtain a quadratic function as a product of affine functions:
julia> 1 * x * x
(0) + 1.0 MOI.VariableIndex(1)²
julia> f * f # (x + 2)²
(4) + (2) MOI.VariableIndex(1) + (2) MOI.VariableIndex(1) + 1.0 MOI.VariableIndex(1)²
julia> f^2 # (x + 2)² too
(4) + (2) MOI.VariableIndex(1) + (2) MOI.VariableIndex(1) + 1.0 MOI.VariableIndex(1)²
Creating vector functions
A vector function is a function with several values, irrespective of the number of input variables. Similarly to scalar functions, there are three main types of vector functions: VectorOfVariables
, VectorAffineFunction
, and VectorQuadraticFunction
.
The easiest way to create a vector function is to stack several scalar functions using Utilities.vectorize
. It takes a vector as input, and the generated vector function (of the most appropriate type) has each dimension corresponding to a dimension of the vector.
julia> g = MOI.Utilities.vectorize([f, 2 * f])
┌ ┐
│(2) + (1) MOI.VariableIndex(1)│
│(4) + (2) MOI.VariableIndex(1)│
└ ┘
Utilities.vectorize
only takes a vector of similar scalar functions: you cannot mix VariableIndex
and ScalarAffineFunction
, for instance. In practice, it means that Utilities.vectorize([x, f])
does not work; you should rather use Utilities.vectorize([1 * x, f])
instead to only have ScalarAffineFunction
objects.
Canonicalizing functions
In more advanced use cases, you might need to ensure that a function is "canonical." Functions are stored as an array of terms, but there is no check that these terms are redundant: a ScalarAffineFunction
object might have two terms with the same variable, like x + x + 1
. These terms could be merged without changing the semantics of the function: 2x + 1
.
Working with these objects might be cumbersome. Canonicalization helps maintain redundancy to zero.
Utilities.is_canonical
checks whether a function is already in its canonical form:
julia> MOI.Utilities.is_canonical(f + f) # (x + 2) + (x + 2) is stored as x + x + 4
false
Utilities.canonical
returns the equivalent canonical version of the function:
julia> MOI.Utilities.canonical(f + f) # Returns 2x + 4
(4) + (2) MOI.VariableIndex(1)
Exploring functions
At some point, you might need to dig into a function, for instance to map it into solver constructs.
Vector functions
Utilities.scalarize
returns a vector of scalar functions from a vector function:
julia> MOI.Utilities.scalarize(g) # Returns a vector [f, 2 * f].
2-element Vector{MathOptInterface.ScalarAffineFunction{Int64}}:
(2) + (1) MOI.VariableIndex(1)
(4) + (2) MOI.VariableIndex(1)
Utilities.eachscalar
returns an iterator on the dimensions, which serves the same purpose as Utilities.scalarize
.
output_dimension
returns the number of dimensions of the output of a function:
julia> MOI.output_dimension(g)
2